@shipfox/api-logs 10.1.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +26 -0
- package/dist/core/append-logs.d.ts.map +1 -1
- package/dist/core/append-logs.js +13 -5
- package/dist/core/append-logs.js.map +1 -1
- package/dist/core/close-stream.d.ts.map +1 -1
- package/dist/core/close-stream.js +16 -1
- package/dist/core/close-stream.js.map +1 -1
- package/dist/core/entities/attempt-stream.d.ts +2 -1
- package/dist/core/entities/attempt-stream.d.ts.map +1 -1
- package/dist/core/entities/attempt-stream.js.map +1 -1
- package/dist/core/session/claude/rows.d.ts +15 -3
- package/dist/core/session/claude/rows.d.ts.map +1 -1
- package/dist/core/session/claude/rows.js +127 -13
- package/dist/core/session/claude/rows.js.map +1 -1
- package/dist/core/session/claude-parser.d.ts +4 -2
- package/dist/core/session/claude-parser.d.ts.map +1 -1
- package/dist/core/session/claude-parser.js +56 -10
- package/dist/core/session/claude-parser.js.map +1 -1
- package/dist/db/db.d.ts +113 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/schema/attempt-streams.d.ts +113 -0
- package/dist/db/schema/attempt-streams.d.ts.map +1 -1
- package/dist/db/schema/attempt-streams.js +2 -0
- package/dist/db/schema/attempt-streams.js.map +1 -1
- package/dist/db/streams.d.ts +3 -1
- package/dist/db/streams.d.ts.map +1 -1
- package/dist/db/streams.js +6 -1
- package/dist/db/streams.js.map +1 -1
- package/dist/presentation/routes/read-logs.d.ts.map +1 -1
- package/dist/presentation/routes/read-logs.js +11 -5
- package/dist/presentation/routes/read-logs.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0002_wise_dorian_gray.sql +1 -0
- package/drizzle/meta/0002_snapshot.json +561 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +3 -3
- package/src/core/append-logs.test.ts +57 -0
- package/src/core/append-logs.ts +15 -3
- package/src/core/close-stream.ts +17 -1
- package/src/core/entities/attempt-stream.ts +2 -1
- package/src/core/session/claude/rows.ts +172 -9
- package/src/core/session/claude-parser.test.ts +453 -4
- package/src/core/session/claude-parser.ts +59 -10
- package/src/db/schema/attempt-streams.ts +3 -1
- package/src/db/streams.ts +10 -2
- package/src/presentation/routes/read-logs.test.ts +18 -4
- package/src/presentation/routes/read-logs.ts +9 -5
- package/test/fixtures/claude-session/agent-step.jsonl +12 -0
- package/test/fixtures/claude-session/sdk-system-subtypes.json +32 -0
- package/test/fixtures/claude-session.ts +45 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -50,13 +50,15 @@ const fakeUserAuth: AuthMethod = {
|
|
|
50
50
|
throw new ClientError('Invalid user token', 'unauthorized', {status: 401});
|
|
51
51
|
}
|
|
52
52
|
const header = request.headers['x-test-workspace'];
|
|
53
|
-
const
|
|
53
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
54
|
+
const [workspaceId, status] = value?.split('|') ?? [];
|
|
55
|
+
const workspaceStatus = status === 'suspended' || status === 'deleted' ? status : 'active';
|
|
54
56
|
setUserContext(
|
|
55
57
|
request,
|
|
56
58
|
buildUserContext({
|
|
57
59
|
userId: 'user-1',
|
|
58
60
|
email: 'user@example.com',
|
|
59
|
-
memberships: workspaceId ? [{workspaceId, role: 'admin'}] : [],
|
|
61
|
+
memberships: workspaceId ? [{workspaceId, role: 'admin', workspaceStatus}] : [],
|
|
60
62
|
}),
|
|
61
63
|
);
|
|
62
64
|
return Promise.resolve();
|
|
@@ -174,11 +176,11 @@ describe('GET /steps/:stepId/attempts/:attempt/logs', () => {
|
|
|
174
176
|
return `/steps/${stepId}/attempts/${attempt}/logs?cursor=${cursor}`;
|
|
175
177
|
}
|
|
176
178
|
|
|
177
|
-
function authedGet(stream: AttemptStream, cursor: number,
|
|
179
|
+
function authedGet(stream: AttemptStream, cursor: number, workspaceClaim = stream.workspaceId) {
|
|
178
180
|
return app.inject({
|
|
179
181
|
method: 'GET',
|
|
180
182
|
url: readUrl(stream.stepId, stream.attempt, cursor),
|
|
181
|
-
headers: {authorization: 'Bearer user', 'x-test-workspace':
|
|
183
|
+
headers: {authorization: 'Bearer user', 'x-test-workspace': workspaceClaim},
|
|
182
184
|
});
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -288,6 +290,18 @@ describe('GET /steps/:stepId/attempts/:attempt/logs', () => {
|
|
|
288
290
|
expect(res.json().code).toBe('not-found');
|
|
289
291
|
});
|
|
290
292
|
|
|
293
|
+
it('returns workspace-suspended for a suspended membership claim', async () => {
|
|
294
|
+
const stream = await arrangeStream({
|
|
295
|
+
workspaceId: crypto.randomUUID(),
|
|
296
|
+
chunks: [ndjsonBody(outputLine('secret\n'))],
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const res = await authedGet(stream, 0, `${stream.workspaceId}|suspended`);
|
|
300
|
+
|
|
301
|
+
expect(res.statusCode).toBe(409);
|
|
302
|
+
expect(res.json().code).toBe('workspace-suspended');
|
|
303
|
+
});
|
|
304
|
+
|
|
291
305
|
it('serves an open stream inline with the next cursor and stream state', async () => {
|
|
292
306
|
const workspaceId = crypto.randomUUID();
|
|
293
307
|
const lineA = outputLine('alpha\n');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {requireWorkspaceResourceAccess} from '@shipfox/api-auth-context';
|
|
2
2
|
import {readLogsQuerySchema, readLogsResponseSchema} from '@shipfox/api-logs-dto';
|
|
3
3
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
4
4
|
import {z} from 'zod';
|
|
@@ -22,16 +22,20 @@ export const readLogsRoute = defineRoute({
|
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
handler: async (request) => {
|
|
25
|
-
const user = requireUserContext(request);
|
|
26
25
|
const {stepId, attempt} = request.params;
|
|
27
26
|
const {cursor} = request.query;
|
|
28
27
|
|
|
29
28
|
const stream = await getStreamByStepAttempt({stepId, attempt});
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
if (!stream
|
|
29
|
+
// Missing streams and memberships remain 404 so the endpoint never leaks another
|
|
30
|
+
// workspace's step; lifecycle claims propagate their stable access errors.
|
|
31
|
+
if (!stream) {
|
|
33
32
|
throw new ClientError('Logs not found', 'not-found', {status: 404});
|
|
34
33
|
}
|
|
34
|
+
requireWorkspaceResourceAccess({
|
|
35
|
+
request,
|
|
36
|
+
workspaceId: stream.workspaceId,
|
|
37
|
+
notFoundError: new ClientError('Logs not found', 'not-found', {status: 404}),
|
|
38
|
+
});
|
|
35
39
|
|
|
36
40
|
return toReadLogsDto(await buildLogReadResult(stream, cursor));
|
|
37
41
|
},
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{"type":"system","subtype":"init","apiKeySource":"user","claude_code_version":"1.0.0","cwd":"/workspace","tools":["Read","Bash"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"default","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"uuid":"init-1","session_id":"session-1"}
|
|
2
|
+
{"type":"assistant","message":{"id":"msg-1","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"I will inspect the repository."},{"type":"tool_use","id":"toolu-1","name":"Read","input":{"file_path":"README.md"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":100,"output_tokens":20}},"parent_tool_use_id":null,"uuid":"assistant-1","session_id":"session-1"}
|
|
3
|
+
{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu-1","content":[{"type":"text","text":"# Shipfox\n"}],"is_error":false}]},"parent_tool_use_id":null,"uuid":"user-1","session_id":"session-1"}
|
|
4
|
+
{"type":"system","subtype":"permission_denied","tool_name":"mcp__shipfox_outputs__set_output","tool_use_id":"toolu-2","decision_reason_type":"mode","decision_reason":"Permission mode","message":"Permission denied","uuid":"permission-1","session_id":"session-1"}
|
|
5
|
+
{"type":"system","subtype":"api_retry","attempt":2,"max_retries":3,"retry_delay_ms":1500,"error_status":429,"error":"rate_limit","uuid":"retry-1","session_id":"session-1"}
|
|
6
|
+
{"type":"system","subtype":"hook_response","hook_id":"hook-1","hook_name":"PreToolUse","hook_event":"PreToolUse","output":"Hook completed","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"hook-1","session_id":"session-1"}
|
|
7
|
+
{"type":"system","subtype":"compact_boundary","compact_metadata":{"trigger":"auto","pre_tokens":12000,"post_tokens":4000,"duration_ms":250},"uuid":"compact-1","session_id":"session-1"}
|
|
8
|
+
{"type":"result","subtype":"success","is_error":false,"result":"First turn response","duration_ms":1200,"duration_api_ms":900,"num_turns":1,"total_cost_usd":0.0042,"uuid":"result-1","session_id":"session-1"}
|
|
9
|
+
{"type":"system","subtype":"init","apiKeySource":"user","claude_code_version":"1.0.0","cwd":"/workspace","tools":["Read","Bash"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"default","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"uuid":"init-2","session_id":"session-1"}
|
|
10
|
+
{"type":"user","message":{"role":"user","content":"The previous turn ended without setting required workflow outputs: answer. Call set_output for each missing key, then provide your final response."},"parent_tool_use_id":null,"isSynthetic":true,"uuid":"user-2","session_id":"session-1"}
|
|
11
|
+
{"type":"assistant","message":{"id":"msg-2","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"The answer is ready."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":120,"output_tokens":8}},"parent_tool_use_id":null,"uuid":"assistant-2","session_id":"session-1"}
|
|
12
|
+
{"type":"result","subtype":"success","is_error":false,"result":"Final response","duration_ms":800,"duration_api_ms":600,"num_turns":1,"total_cost_usd":0.0021,"uuid":"result-2","session_id":"session-1"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"package": "@anthropic-ai/claude-agent-sdk",
|
|
3
|
+
"version": "0.3.200",
|
|
4
|
+
"subtypes": [
|
|
5
|
+
"api_retry",
|
|
6
|
+
"commands_changed",
|
|
7
|
+
"compact_boundary",
|
|
8
|
+
"elicitation_complete",
|
|
9
|
+
"files_persisted",
|
|
10
|
+
"hook_progress",
|
|
11
|
+
"hook_response",
|
|
12
|
+
"hook_started",
|
|
13
|
+
"informational",
|
|
14
|
+
"init",
|
|
15
|
+
"local_command_output",
|
|
16
|
+
"memory_recall",
|
|
17
|
+
"mirror_error",
|
|
18
|
+
"model_refusal_fallback",
|
|
19
|
+
"model_refusal_no_fallback",
|
|
20
|
+
"notification",
|
|
21
|
+
"permission_denied",
|
|
22
|
+
"plugin_install",
|
|
23
|
+
"session_state_changed",
|
|
24
|
+
"status",
|
|
25
|
+
"task_notification",
|
|
26
|
+
"task_progress",
|
|
27
|
+
"task_started",
|
|
28
|
+
"task_updated",
|
|
29
|
+
"thinking_tokens",
|
|
30
|
+
"worker_shutting_down"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {readFileSync} from 'node:fs';
|
|
2
|
+
import type {AgentSessionRecord} from '#core/session/session-record.js';
|
|
3
|
+
|
|
4
|
+
export interface ClaudeSystemSubtypeFixture {
|
|
5
|
+
package: string;
|
|
6
|
+
version: string;
|
|
7
|
+
subtypes: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SDK_ENTRY_URL = import.meta.resolve('@anthropic-ai/claude-agent-sdk');
|
|
11
|
+
const SDK_TYPES_URL = new URL('./sdk.d.ts', SDK_ENTRY_URL);
|
|
12
|
+
|
|
13
|
+
export function readClaudeSessionFixture(name: string): AgentSessionRecord[] {
|
|
14
|
+
const contents = readFileSync(
|
|
15
|
+
new URL(`./claude-session/${name}`, import.meta.url),
|
|
16
|
+
'utf8',
|
|
17
|
+
).trim();
|
|
18
|
+
|
|
19
|
+
return contents.split('\n').map((data, index) => ({data, ts: index + 1}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readClaudeSystemSubtypeFixture(): ClaudeSystemSubtypeFixture {
|
|
23
|
+
const contents = readFileSync(
|
|
24
|
+
new URL('./claude-session/sdk-system-subtypes.json', import.meta.url),
|
|
25
|
+
'utf8',
|
|
26
|
+
);
|
|
27
|
+
return JSON.parse(contents) as ClaudeSystemSubtypeFixture;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readInstalledClaudeSdkVersion(): string {
|
|
31
|
+
const contents = readFileSync(new URL('./package.json', SDK_TYPES_URL), 'utf8');
|
|
32
|
+
const packageJson = JSON.parse(contents) as {version?: unknown};
|
|
33
|
+
if (typeof packageJson.version !== 'string') {
|
|
34
|
+
throw new Error('The installed Claude SDK package has no version');
|
|
35
|
+
}
|
|
36
|
+
return packageJson.version;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function readInstalledClaudeSystemSubtypes(): string[] {
|
|
40
|
+
const declaration = readFileSync(SDK_TYPES_URL, 'utf8');
|
|
41
|
+
return [...declaration.matchAll(/type: 'system';\s+subtype: '([^']+)'/g)]
|
|
42
|
+
.map((match) => match[1])
|
|
43
|
+
.filter((subtype): subtype is string => subtype !== undefined)
|
|
44
|
+
.sort();
|
|
45
|
+
}
|