Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/docs/content/docs/cli/files.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ sim files versions read <fileId> <version> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--max-bytes <value>` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
| `--offset <value>` | No | First line to return, 1-based. Absent starts at the first line. |
| `--offset <value>` | No | First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line. |
| `--limit <value>` | No | How many lines to return from `offset`. Absent reads to the end. |

</CommandTable>
Expand Down Expand Up @@ -497,7 +497,7 @@ sim files read <fileId> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--max-bytes <value>` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
| `--offset <value>` | No | First line to return, 1-based. Absent starts at the first line. |
| `--offset <value>` | No | First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line. |
| `--limit <value>` | No | How many lines to return from `offset`. Absent reads to the end. |

</CommandTable>
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/content/docs/cli/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1039,7 +1039,7 @@ sim files versions read <fileId> <version> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--max-bytes <value>` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
| `--offset <value>` | No | First line to return, 1-based. Absent starts at the first line. |
| `--offset <value>` | No | First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line. |
| `--limit <value>` | No | How many lines to return from `offset`. Absent reads to the end. |

</CommandTable>
Expand Down Expand Up @@ -1281,7 +1281,7 @@ sim files read <fileId> [options]
| Option | Required | Description |
| --- | --- | --- |
| `--max-bytes <value>` | No | Optional ceiling on the source bytes fed to the parser, lowering but never raising the server limit. |
| `--offset <value>` | No | First line to return, 1-based. Absent starts at the first line. |
| `--offset <value>` | No | First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line. |
| `--limit <value>` | No | How many lines to return from `offset`. Absent reads to the end. |

</CommandTable>
Expand Down
12 changes: 6 additions & 6 deletions apps/docs/openapi-v2-files-audit.json
Original file line number Diff line number Diff line change
Expand Up @@ -790,11 +790,11 @@
"name": "offset",
"in": "query",
"required": false,
"description": "First line to return, 1-based. Absent starts at the first line.",
"description": "First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.",
"schema": {
"description": "First line to return, 1-based. Absent starts at the first line.",
"description": "First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.",
"type": "integer",
"minimum": 1,
"minimum": 0,
"maximum": 9007199254740991
}
},
Expand Down Expand Up @@ -1241,11 +1241,11 @@
"name": "offset",
"in": "query",
"required": false,
"description": "First line to return, 1-based. Absent starts at the first line.",
"description": "First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.",
"schema": {
"description": "First line to return, 1-based. Absent starts at the first line.",
"description": "First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.",
"type": "integer",
"minimum": 1,
"minimum": 0,
"maximum": 9007199254740991
}
},
Expand Down
10 changes: 10 additions & 0 deletions apps/sim/app/api/v2/files/[fileId]/text/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ describe('GET /api/v2/files/[fileId]/text', () => {
expect(mocks.readText).not.toHaveBeenCalled()
})

/**
* Callers used to zero-based paging send `offset=0` for the first window. It
* names no other line, so it reads from the first one instead of failing.
*/
it('reads offset 0 as the first line', async () => {
const response = await GET(textRequest(`workspaceId=${WORKSPACE_ID}&offset=0&limit=2`), context)

expect(response.status).toBe(200)
})

it('conceals a cross-tenant file as a missing file', async () => {
mocks.readText.mockRejectedValueOnce(new NoWorkspaceAccessError())

Expand Down
9 changes: 5 additions & 4 deletions apps/sim/lib/api/contracts/tools/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,12 @@ export const fileManageContentBodySchema = z
fileId: fileIdSelectionSchema.optional(),
fileInput: z.unknown().optional(),
/**
* First line to return, 1-based. Applied to each selected file separately,
* because a selection can be several files and one running offset across
* them would depend on an ordering the caller cannot see.
* First line to return, 1-based; 0 also starts at the first line, so a
* zero-based caller's first window reads instead of failing. Applied to each
* selected file separately, because a selection can be several files and one
* running offset across them would depend on an ordering the caller cannot see.
*/
offset: z.number().int().min(1, 'offset starts at line 1').optional(),
offset: z.number().int().min(0, 'offset cannot be negative').optional(),
/** How many lines to return from `offset`. Absent reads to the end. */
limit: z.number().int().min(1, 'limit must be at least 1').optional(),
})
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/lib/api/contracts/v2/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -853,9 +853,11 @@ export const v2ReadFileTextQuerySchema = z
offset: z.coerce
.number()
.int()
.min(1, 'offset starts at line 1')
.min(0, 'offset cannot be negative')
.optional()
.describe('First line to return, 1-based. Absent starts at the first line.'),
.describe(
'First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.'
),
limit: z.coerce
.number()
.int()
Expand Down
15 changes: 15 additions & 0 deletions apps/sim/lib/internal/file/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,21 @@ describe('file manage operations', () => {
},
})
})
it('reads offset 0 as the first line of each selected file', async () => {
const response = await POST(
createMockRequest('POST', {
operation: 'content',
workspaceId: 'workspace-1',
fileId: 'file-1',
offset: 0,
limit: 1,
})
)
expect(response.status).toBe(200)
expect(await response.json()).toMatchObject({
data: { contents: ['content:file-1.txt'], lineRanges: [{ offset: 1, lineCount: 1 }] },
})
})
it('does not bypass complete extraction limits with a raw-text fallback', async () => {
vi.mocked(isSupportedFileType).mockReturnValueOnce(true)
vi.mocked(parseBuffer).mockRejectedValueOnce(
Expand Down
19 changes: 18 additions & 1 deletion apps/sim/lib/mothership/agent-cli/engines/file-read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,28 @@ describe('content-aware files read augmentation', () => {
}
)

it('forwards --offset 0 to the text API instead of refusing it', async () => {
mockResolveWorkspaceFileReference.mockResolvedValue({
...file,
name: 'notes.txt',
type: 'text/plain',
})
mocks.request.mockResolvedValue(textResponse('notes.txt'))
const result = await runEngine('files read', ['uploads/notes.txt'], runtime, {
offset: '0',
limit: '2',
})
expect(result.exitCode).toBe(0)
expect(mocks.request).toHaveBeenCalledWith(`/api/v2/files/${fileId}/text`, {
Comment thread
waleedlatif1 marked this conversation as resolved.
query: { workspaceId, offset: '0', limit: '2' },
})
})

it('rejects incompatible flags and invalid byte/line bounds before lookup', async () => {
for (const flags of [
{ render: true, limit: '2' } as const,
{ pages: '1', offset: '1' },
{ offset: '0' },
{ offset: '-1' },
Comment thread
waleedlatif1 marked this conversation as resolved.
{ offset: true } as const,
{ limit: true } as const,
{ 'max-bytes': true } as const,
Expand Down
6 changes: 6 additions & 0 deletions apps/sim/lib/workspace-files/text-lines.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ describe('file text line windows', () => {
}
}
)
it('reads offset 0 as the first line and reports the 1-based offset it used', () => {
expect(sliceFileTextLines('one\ntwo\nthree', 0, 2, false)).toEqual({
text: 'one\ntwo',
lineRange: { offset: 1, lineCount: 2, totalLines: 3, totalLinesExact: true },
})
})
it('reads a late window in a newline-dense document', () => {
const result = sliceFileTextLines(
`${'abc\n'.repeat(1_000_000)}tail needle\n`,
Expand Down
6 changes: 4 additions & 2 deletions packages/sim-cli/src/generated/v2-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20276,7 +20276,8 @@ export const V2_OPERATIONS = {
},
offset: {
kind: 'integer',
describe: 'First line to return, 1-based. Absent starts at the first line.',
describe:
'First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.',
},
limit: {
kind: 'integer',
Expand All @@ -20300,7 +20301,8 @@ export const V2_OPERATIONS = {
},
offset: {
kind: 'integer',
describe: 'First line to return, 1-based. Absent starts at the first line.',
describe:
'First line to return, 1-based; 0 also starts at the first line. Absent starts at the first line.',
},
limit: {
kind: 'integer',
Expand Down
Loading