@fixy/claude-adapter 0.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/dist/__tests__/adapter.test.d.ts +2 -0
- package/dist/__tests__/adapter.test.d.ts.map +1 -0
- package/dist/__tests__/adapter.test.js +156 -0
- package/dist/__tests__/adapter.test.js.map +1 -0
- package/dist/__tests__/parse.test.d.ts +2 -0
- package/dist/__tests__/parse.test.d.ts.map +1 -0
- package/dist/__tests__/parse.test.js +80 -0
- package/dist/__tests__/parse.test.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +98 -0
- package/dist/index.js.map +1 -0
- package/dist/parse.d.ts +6 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +85 -0
- package/dist/parse.js.map +1 -0
- package/package.json +25 -0
- package/src/__tests__/adapter.test.ts +184 -0
- package/src/__tests__/parse.test.ts +93 -0
- package/src/index.ts +123 -0
- package/src/parse.ts +92 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adapter.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { createClaudeAdapter } from '../index.js';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
let tmpDir;
|
|
7
|
+
let originalPath;
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fixy-claude-test-'));
|
|
10
|
+
originalPath = process.env.PATH ?? '';
|
|
11
|
+
const mockClaude = path.join(tmpDir, 'claude');
|
|
12
|
+
fs.writeFileSync(mockClaude, `#!/bin/bash
|
|
13
|
+
if [[ "$1" == "--version" ]]; then
|
|
14
|
+
echo "claude-code 1.0.42"
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if [[ "$1" == "--print" ]]; then
|
|
19
|
+
RESUME_ID=""
|
|
20
|
+
prev=""
|
|
21
|
+
for arg in "$@"; do
|
|
22
|
+
if [[ "$prev" == "--resume" ]]; then
|
|
23
|
+
RESUME_ID="$arg"
|
|
24
|
+
fi
|
|
25
|
+
prev="$arg"
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
STDIN_CONTENT=$(cat)
|
|
29
|
+
SESSION_ID=\${RESUME_ID:-"new-session-id-001"}
|
|
30
|
+
|
|
31
|
+
echo '{"type":"system","subtype":"init","session_id":"'"$SESSION_ID"'","model":"claude-sonnet-4-6"}'
|
|
32
|
+
echo '{"type":"assistant","session_id":"'"$SESSION_ID"'","message":{"content":[{"type":"text","text":"I received: '"$STDIN_CONTENT"'"}]}}'
|
|
33
|
+
echo '{"type":"result","session_id":"'"$SESSION_ID"'","result":"Mock response to prompt","usage":{"input_tokens":10,"output_tokens":20},"total_cost_usd":0.001}'
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
echo "Unknown command" >&2
|
|
38
|
+
exit 1
|
|
39
|
+
`, { mode: 0o755 });
|
|
40
|
+
process.env.PATH = tmpDir + ':' + originalPath;
|
|
41
|
+
});
|
|
42
|
+
afterAll(() => {
|
|
43
|
+
process.env.PATH = originalPath;
|
|
44
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
function makeCtx(overrides = {}) {
|
|
47
|
+
return {
|
|
48
|
+
runId: 'test-run-001',
|
|
49
|
+
agent: { id: 'claude', name: 'Claude Code' },
|
|
50
|
+
threadContext: {
|
|
51
|
+
threadId: 'thread-001',
|
|
52
|
+
projectRoot: tmpDir,
|
|
53
|
+
worktreePath: tmpDir,
|
|
54
|
+
repoRef: null,
|
|
55
|
+
},
|
|
56
|
+
messages: [],
|
|
57
|
+
prompt: 'Hello Claude',
|
|
58
|
+
session: null,
|
|
59
|
+
onLog: () => { },
|
|
60
|
+
onMeta: () => { },
|
|
61
|
+
onSpawn: () => { },
|
|
62
|
+
signal: AbortSignal.timeout(30_000),
|
|
63
|
+
...overrides,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
describe('ClaudeAdapter.probe()', () => {
|
|
67
|
+
it('finds the mock claude binary and reports available=true', async () => {
|
|
68
|
+
const adapter = createClaudeAdapter();
|
|
69
|
+
const result = await adapter.probe();
|
|
70
|
+
expect(result.available).toBe(true);
|
|
71
|
+
expect(result.version).toContain('1.0.42');
|
|
72
|
+
});
|
|
73
|
+
it('reports available=false when claude is not on PATH', async () => {
|
|
74
|
+
const adapter = createClaudeAdapter();
|
|
75
|
+
const savedPath = process.env.PATH;
|
|
76
|
+
process.env.PATH = '/nonexistent-dir-fixy-test';
|
|
77
|
+
try {
|
|
78
|
+
const result = await adapter.probe();
|
|
79
|
+
expect(result.available).toBe(false);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
process.env.PATH = savedPath;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('ClaudeAdapter.execute()', () => {
|
|
87
|
+
it('executes with mock claude and returns expected result', async () => {
|
|
88
|
+
const adapter = createClaudeAdapter();
|
|
89
|
+
const logs = [];
|
|
90
|
+
let metaCalled = false;
|
|
91
|
+
let spawnPid = null;
|
|
92
|
+
const ctx = makeCtx({
|
|
93
|
+
onLog: (stream, chunk) => {
|
|
94
|
+
logs.push({ stream, chunk });
|
|
95
|
+
},
|
|
96
|
+
onMeta: (_meta) => {
|
|
97
|
+
metaCalled = true;
|
|
98
|
+
},
|
|
99
|
+
onSpawn: (pid) => {
|
|
100
|
+
spawnPid = pid;
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const result = await adapter.execute(ctx);
|
|
104
|
+
expect(result.summary).toContain('Mock response to prompt');
|
|
105
|
+
expect(result.session).not.toBeNull();
|
|
106
|
+
expect(result.session?.sessionId).toBe('new-session-id-001');
|
|
107
|
+
expect(result.exitCode).toBe(0);
|
|
108
|
+
expect(result.timedOut).toBe(false);
|
|
109
|
+
expect(result.patches).toEqual([]);
|
|
110
|
+
expect(metaCalled).toBe(true);
|
|
111
|
+
expect(typeof spawnPid).toBe('number');
|
|
112
|
+
expect(spawnPid).toBeGreaterThan(0);
|
|
113
|
+
});
|
|
114
|
+
it('resumes a session by passing --resume with the session id', async () => {
|
|
115
|
+
const adapter = createClaudeAdapter();
|
|
116
|
+
const ctx = makeCtx({
|
|
117
|
+
session: { sessionId: 'resume-sess-42', params: {} },
|
|
118
|
+
});
|
|
119
|
+
const result = await adapter.execute(ctx);
|
|
120
|
+
expect(result.session).not.toBeNull();
|
|
121
|
+
expect(result.session?.sessionId).toBe('resume-sess-42');
|
|
122
|
+
});
|
|
123
|
+
it('inherits HOME without redaction (HOME does not match sensitive key regex)', async () => {
|
|
124
|
+
const adapter = createClaudeAdapter();
|
|
125
|
+
let metaEnv = {};
|
|
126
|
+
const ctx = makeCtx({
|
|
127
|
+
onMeta: (meta) => {
|
|
128
|
+
metaEnv = meta.env;
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
await adapter.execute(ctx);
|
|
132
|
+
// HOME does not match /(key|token|secret|password|passwd|authorization|cookie)/i
|
|
133
|
+
// so it must NOT be redacted
|
|
134
|
+
expect(metaEnv['HOME']).not.toBe('***REDACTED***');
|
|
135
|
+
expect(typeof metaEnv['HOME']).toBe('string');
|
|
136
|
+
});
|
|
137
|
+
it('redacts env keys matching the sensitive key pattern', async () => {
|
|
138
|
+
const adapter = createClaudeAdapter();
|
|
139
|
+
let metaEnv = {};
|
|
140
|
+
process.env['MY_SECRET_TOKEN'] = 'super-secret';
|
|
141
|
+
const ctx = makeCtx({
|
|
142
|
+
onMeta: (meta) => {
|
|
143
|
+
metaEnv = meta.env;
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
await adapter.execute(ctx);
|
|
148
|
+
// "TOKEN" matches /(key|token|secret|password|passwd|authorization|cookie)/i
|
|
149
|
+
expect(metaEnv['MY_SECRET_TOKEN']).toBe('***REDACTED***');
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
delete process.env['MY_SECRET_TOKEN'];
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
//# sourceMappingURL=adapter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.test.js","sourceRoot":"","sources":["../../src/__tests__/adapter.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/parse.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseClaudeStreamJson } from '../parse.js';
|
|
3
|
+
describe('parseClaudeStreamJson', () => {
|
|
4
|
+
it('extracts sessionId and summary from a result line', () => {
|
|
5
|
+
const input = [
|
|
6
|
+
JSON.stringify({
|
|
7
|
+
type: 'system',
|
|
8
|
+
subtype: 'init',
|
|
9
|
+
session_id: 'sess-abc-123',
|
|
10
|
+
model: 'claude-sonnet-4-6',
|
|
11
|
+
}),
|
|
12
|
+
JSON.stringify({
|
|
13
|
+
type: 'assistant',
|
|
14
|
+
session_id: 'sess-abc-123',
|
|
15
|
+
message: { content: [{ type: 'text', text: 'Hello there!' }] },
|
|
16
|
+
}),
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
type: 'result',
|
|
19
|
+
session_id: 'sess-abc-123',
|
|
20
|
+
result: 'Final answer here',
|
|
21
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
22
|
+
total_cost_usd: 0.01,
|
|
23
|
+
}),
|
|
24
|
+
].join('\n');
|
|
25
|
+
const result = parseClaudeStreamJson(input);
|
|
26
|
+
expect(result.sessionId).toBe('sess-abc-123');
|
|
27
|
+
expect(result.summary).toBe('Final answer here');
|
|
28
|
+
});
|
|
29
|
+
it('falls back to concatenated assistant content when there is no result line', () => {
|
|
30
|
+
const input = [
|
|
31
|
+
JSON.stringify({ type: 'system', subtype: 'init', session_id: 'sess-no-result' }),
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
type: 'assistant',
|
|
34
|
+
session_id: 'sess-no-result',
|
|
35
|
+
message: { content: [{ type: 'text', text: 'Part 1' }] },
|
|
36
|
+
}),
|
|
37
|
+
JSON.stringify({
|
|
38
|
+
type: 'assistant',
|
|
39
|
+
session_id: 'sess-no-result',
|
|
40
|
+
message: { content: [{ type: 'text', text: 'Part 2' }] },
|
|
41
|
+
}),
|
|
42
|
+
].join('\n');
|
|
43
|
+
const result = parseClaudeStreamJson(input);
|
|
44
|
+
expect(result.sessionId).toBe('sess-no-result');
|
|
45
|
+
expect(result.summary).toContain('Part 1');
|
|
46
|
+
expect(result.summary).toContain('Part 2');
|
|
47
|
+
});
|
|
48
|
+
it('falls back to raw stdout for completely invalid (non-JSON) output', () => {
|
|
49
|
+
const input = 'This is not JSON at all\nJust random text\n';
|
|
50
|
+
const result = parseClaudeStreamJson(input);
|
|
51
|
+
expect(result.sessionId).toBeNull();
|
|
52
|
+
// summary should be the raw input (the fallback path in parseClaudeStreamJson)
|
|
53
|
+
expect(result.summary).toBe(input);
|
|
54
|
+
});
|
|
55
|
+
it('returns null sessionId and empty summary for an empty string', () => {
|
|
56
|
+
const result = parseClaudeStreamJson('');
|
|
57
|
+
expect(result.sessionId).toBeNull();
|
|
58
|
+
expect(result.summary).toBe('');
|
|
59
|
+
});
|
|
60
|
+
it('uses assistant content as summary when result line has no result field', () => {
|
|
61
|
+
const input = [
|
|
62
|
+
JSON.stringify({ type: 'system', subtype: 'init', session_id: 'sess-empty-result' }),
|
|
63
|
+
JSON.stringify({
|
|
64
|
+
type: 'assistant',
|
|
65
|
+
session_id: 'sess-empty-result',
|
|
66
|
+
message: { content: [{ type: 'text', text: 'Assistant text fallback' }] },
|
|
67
|
+
}),
|
|
68
|
+
// result line exists but has no "result" field
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
type: 'result',
|
|
71
|
+
session_id: 'sess-empty-result',
|
|
72
|
+
usage: { input_tokens: 5, output_tokens: 5 },
|
|
73
|
+
}),
|
|
74
|
+
].join('\n');
|
|
75
|
+
const result = parseClaudeStreamJson(input);
|
|
76
|
+
expect(result.sessionId).toBe('sess-empty-result');
|
|
77
|
+
expect(result.summary).toBe('Assistant text fallback');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
//# sourceMappingURL=parse.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.test.js","sourceRoot":"","sources":["../../src/__tests__/parse.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EAIZ,MAAM,YAAY,CAAC;AA+GpB,wBAAgB,mBAAmB,IAAI,WAAW,CAEjD;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// packages/claude-adapter/src/index.ts
|
|
2
|
+
import { buildInheritedEnv, redactEnvForLogs, resolveCommand, runChildProcess, } from '@fixy/adapter-utils';
|
|
3
|
+
import { parseClaudeStreamJson } from './parse.js';
|
|
4
|
+
class ClaudeAdapter {
|
|
5
|
+
id = 'claude';
|
|
6
|
+
name = 'Claude Code';
|
|
7
|
+
async probe() {
|
|
8
|
+
let resolvedCommand;
|
|
9
|
+
try {
|
|
10
|
+
resolvedCommand = await resolveCommand('claude');
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {
|
|
14
|
+
available: false,
|
|
15
|
+
version: null,
|
|
16
|
+
authStatus: 'unknown',
|
|
17
|
+
detail: 'claude CLI not found in PATH',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const result = await runChildProcess({
|
|
22
|
+
command: resolvedCommand,
|
|
23
|
+
args: ['--version'],
|
|
24
|
+
cwd: process.cwd(),
|
|
25
|
+
env: buildInheritedEnv(),
|
|
26
|
+
timeoutMs: 10_000,
|
|
27
|
+
});
|
|
28
|
+
const version = result.stdout.trim() || null;
|
|
29
|
+
return {
|
|
30
|
+
available: true,
|
|
31
|
+
version,
|
|
32
|
+
authStatus: 'unknown',
|
|
33
|
+
detail: null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
38
|
+
return {
|
|
39
|
+
available: false,
|
|
40
|
+
version: null,
|
|
41
|
+
authStatus: 'unknown',
|
|
42
|
+
detail,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async execute(ctx) {
|
|
47
|
+
const args = ['--print', '--output-format', 'stream-json', '--verbose'];
|
|
48
|
+
if (ctx.session) {
|
|
49
|
+
args.push('--resume', ctx.session.sessionId);
|
|
50
|
+
}
|
|
51
|
+
const env = buildInheritedEnv({});
|
|
52
|
+
const resolvedCommand = await resolveCommand('claude');
|
|
53
|
+
ctx.onMeta({
|
|
54
|
+
resolvedCommand,
|
|
55
|
+
args,
|
|
56
|
+
cwd: ctx.threadContext.worktreePath,
|
|
57
|
+
env: redactEnvForLogs(env),
|
|
58
|
+
});
|
|
59
|
+
const result = await runChildProcess({
|
|
60
|
+
command: resolvedCommand,
|
|
61
|
+
args,
|
|
62
|
+
cwd: ctx.threadContext.worktreePath,
|
|
63
|
+
env,
|
|
64
|
+
stdin: ctx.prompt,
|
|
65
|
+
signal: ctx.signal,
|
|
66
|
+
onLog: ctx.onLog,
|
|
67
|
+
onSpawn: ctx.onSpawn,
|
|
68
|
+
});
|
|
69
|
+
const parsed = parseClaudeStreamJson(result.stdout);
|
|
70
|
+
const warnings = [];
|
|
71
|
+
if (result.stderr && result.stderr.trim().length > 0) {
|
|
72
|
+
warnings.push(result.stderr.trim());
|
|
73
|
+
}
|
|
74
|
+
let errorMessage = null;
|
|
75
|
+
if (result.exitCode !== 0 && !result.timedOut) {
|
|
76
|
+
const stderrMsg = result.stderr.trim();
|
|
77
|
+
errorMessage =
|
|
78
|
+
stderrMsg.length > 0
|
|
79
|
+
? `claude exited with code ${result.exitCode}: ${stderrMsg}`
|
|
80
|
+
: `claude exited with code ${result.exitCode}`;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
exitCode: result.exitCode,
|
|
84
|
+
signal: result.signal,
|
|
85
|
+
timedOut: result.timedOut,
|
|
86
|
+
summary: parsed.summary,
|
|
87
|
+
session: parsed.sessionId ? { sessionId: parsed.sessionId, params: {} } : null,
|
|
88
|
+
patches: [],
|
|
89
|
+
warnings,
|
|
90
|
+
errorMessage,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function createClaudeAdapter() {
|
|
95
|
+
return new ClaudeAdapter();
|
|
96
|
+
}
|
|
97
|
+
export { parseClaudeStreamJson } from './parse.js';
|
|
98
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;CACjB;AAkBD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,CAsExE"}
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
function tryParseJson(line) {
|
|
2
|
+
try {
|
|
3
|
+
const parsed = JSON.parse(line);
|
|
4
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
5
|
+
return parsed;
|
|
6
|
+
}
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function asString(value, fallback) {
|
|
14
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
15
|
+
}
|
|
16
|
+
export function parseClaudeStreamJson(stdout) {
|
|
17
|
+
if (stdout.length === 0) {
|
|
18
|
+
return { sessionId: null, summary: '' };
|
|
19
|
+
}
|
|
20
|
+
let sessionId = null;
|
|
21
|
+
const assistantTexts = [];
|
|
22
|
+
let finalResult = null;
|
|
23
|
+
const lines = stdout.split('\n');
|
|
24
|
+
for (const rawLine of lines) {
|
|
25
|
+
const line = rawLine.trim();
|
|
26
|
+
if (line.length === 0)
|
|
27
|
+
continue;
|
|
28
|
+
const obj = tryParseJson(line);
|
|
29
|
+
if (obj === null)
|
|
30
|
+
continue;
|
|
31
|
+
const type = obj['type'];
|
|
32
|
+
if (type === 'system') {
|
|
33
|
+
if (obj['subtype'] === 'init') {
|
|
34
|
+
sessionId = asString(obj['session_id'], sessionId ?? '');
|
|
35
|
+
if (sessionId === '')
|
|
36
|
+
sessionId = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (type === 'assistant') {
|
|
40
|
+
const sid = asString(obj['session_id'], '');
|
|
41
|
+
if (sid !== '')
|
|
42
|
+
sessionId = sid;
|
|
43
|
+
const message = obj['message'];
|
|
44
|
+
if (typeof message === 'object' && message !== null && !Array.isArray(message)) {
|
|
45
|
+
const msg = message;
|
|
46
|
+
const content = msg['content'];
|
|
47
|
+
if (Array.isArray(content)) {
|
|
48
|
+
for (const block of content) {
|
|
49
|
+
if (typeof block === 'object' && block !== null && !Array.isArray(block)) {
|
|
50
|
+
const b = block;
|
|
51
|
+
if (b['type'] === 'text') {
|
|
52
|
+
const text = asString(b['text'], '');
|
|
53
|
+
if (text !== '') {
|
|
54
|
+
assistantTexts.push(text);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (type === 'result') {
|
|
63
|
+
finalResult = obj;
|
|
64
|
+
const sid = asString(obj['session_id'], '');
|
|
65
|
+
if (sid !== '')
|
|
66
|
+
sessionId = sid;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let summary;
|
|
70
|
+
if (finalResult !== null) {
|
|
71
|
+
const resultText = asString(finalResult['result'], '');
|
|
72
|
+
summary = resultText !== '' ? resultText : assistantTexts.join('\n\n').trim();
|
|
73
|
+
const sid = asString(finalResult['session_id'], '');
|
|
74
|
+
if (sid !== '')
|
|
75
|
+
sessionId = sid;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
summary = assistantTexts.join('\n\n').trim();
|
|
79
|
+
}
|
|
80
|
+
if (summary === '' && sessionId === null) {
|
|
81
|
+
return { sessionId: null, summary: stdout };
|
|
82
|
+
}
|
|
83
|
+
return { sessionId, summary };
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fixy/claude-adapter",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@fixy/adapter-utils": "workspace:*",
|
|
19
|
+
"@fixy/core": "workspace:*"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.0.0"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT"
|
|
25
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { createClaudeAdapter } from '../index.js';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import type { FixyExecutionContext, FixyInvocationMeta } from '@fixy/core';
|
|
7
|
+
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
let originalPath: string;
|
|
10
|
+
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fixy-claude-test-'));
|
|
13
|
+
originalPath = process.env.PATH ?? '';
|
|
14
|
+
|
|
15
|
+
const mockClaude = path.join(tmpDir, 'claude');
|
|
16
|
+
fs.writeFileSync(
|
|
17
|
+
mockClaude,
|
|
18
|
+
`#!/bin/bash
|
|
19
|
+
if [[ "$1" == "--version" ]]; then
|
|
20
|
+
echo "claude-code 1.0.42"
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [[ "$1" == "--print" ]]; then
|
|
25
|
+
RESUME_ID=""
|
|
26
|
+
prev=""
|
|
27
|
+
for arg in "$@"; do
|
|
28
|
+
if [[ "$prev" == "--resume" ]]; then
|
|
29
|
+
RESUME_ID="$arg"
|
|
30
|
+
fi
|
|
31
|
+
prev="$arg"
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
STDIN_CONTENT=$(cat)
|
|
35
|
+
SESSION_ID=\${RESUME_ID:-"new-session-id-001"}
|
|
36
|
+
|
|
37
|
+
echo '{"type":"system","subtype":"init","session_id":"'"$SESSION_ID"'","model":"claude-sonnet-4-6"}'
|
|
38
|
+
echo '{"type":"assistant","session_id":"'"$SESSION_ID"'","message":{"content":[{"type":"text","text":"I received: '"$STDIN_CONTENT"'"}]}}'
|
|
39
|
+
echo '{"type":"result","session_id":"'"$SESSION_ID"'","result":"Mock response to prompt","usage":{"input_tokens":10,"output_tokens":20},"total_cost_usd":0.001}'
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
echo "Unknown command" >&2
|
|
44
|
+
exit 1
|
|
45
|
+
`,
|
|
46
|
+
{ mode: 0o755 },
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
process.env.PATH = tmpDir + ':' + originalPath;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterAll(() => {
|
|
53
|
+
process.env.PATH = originalPath;
|
|
54
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function makeCtx(overrides: Partial<FixyExecutionContext> = {}): FixyExecutionContext {
|
|
58
|
+
return {
|
|
59
|
+
runId: 'test-run-001',
|
|
60
|
+
agent: { id: 'claude', name: 'Claude Code' },
|
|
61
|
+
threadContext: {
|
|
62
|
+
threadId: 'thread-001',
|
|
63
|
+
projectRoot: tmpDir,
|
|
64
|
+
worktreePath: tmpDir,
|
|
65
|
+
repoRef: null,
|
|
66
|
+
},
|
|
67
|
+
messages: [],
|
|
68
|
+
prompt: 'Hello Claude',
|
|
69
|
+
session: null,
|
|
70
|
+
onLog: () => {},
|
|
71
|
+
onMeta: () => {},
|
|
72
|
+
onSpawn: () => {},
|
|
73
|
+
signal: AbortSignal.timeout(30_000),
|
|
74
|
+
...overrides,
|
|
75
|
+
} as unknown as FixyExecutionContext;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe('ClaudeAdapter.probe()', () => {
|
|
79
|
+
it('finds the mock claude binary and reports available=true', async () => {
|
|
80
|
+
const adapter = createClaudeAdapter();
|
|
81
|
+
const result = await adapter.probe();
|
|
82
|
+
|
|
83
|
+
expect(result.available).toBe(true);
|
|
84
|
+
expect(result.version).toContain('1.0.42');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('reports available=false when claude is not on PATH', async () => {
|
|
88
|
+
const adapter = createClaudeAdapter();
|
|
89
|
+
const savedPath = process.env.PATH;
|
|
90
|
+
process.env.PATH = '/nonexistent-dir-fixy-test';
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const result = await adapter.probe();
|
|
94
|
+
expect(result.available).toBe(false);
|
|
95
|
+
} finally {
|
|
96
|
+
process.env.PATH = savedPath;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('ClaudeAdapter.execute()', () => {
|
|
102
|
+
it('executes with mock claude and returns expected result', async () => {
|
|
103
|
+
const adapter = createClaudeAdapter();
|
|
104
|
+
const logs: Array<{ stream: string; chunk: string }> = [];
|
|
105
|
+
let metaCalled = false;
|
|
106
|
+
let spawnPid: number | null = null;
|
|
107
|
+
|
|
108
|
+
const ctx = makeCtx({
|
|
109
|
+
onLog: (stream, chunk) => {
|
|
110
|
+
logs.push({ stream, chunk });
|
|
111
|
+
},
|
|
112
|
+
onMeta: (_meta: FixyInvocationMeta) => {
|
|
113
|
+
metaCalled = true;
|
|
114
|
+
},
|
|
115
|
+
onSpawn: (pid: number) => {
|
|
116
|
+
spawnPid = pid;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = await adapter.execute(ctx);
|
|
121
|
+
|
|
122
|
+
expect(result.summary).toContain('Mock response to prompt');
|
|
123
|
+
expect(result.session).not.toBeNull();
|
|
124
|
+
expect(result.session?.sessionId).toBe('new-session-id-001');
|
|
125
|
+
expect(result.exitCode).toBe(0);
|
|
126
|
+
expect(result.timedOut).toBe(false);
|
|
127
|
+
expect(result.patches).toEqual([]);
|
|
128
|
+
expect(metaCalled).toBe(true);
|
|
129
|
+
expect(typeof spawnPid).toBe('number');
|
|
130
|
+
expect(spawnPid).toBeGreaterThan(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('resumes a session by passing --resume with the session id', async () => {
|
|
134
|
+
const adapter = createClaudeAdapter();
|
|
135
|
+
|
|
136
|
+
const ctx = makeCtx({
|
|
137
|
+
session: { sessionId: 'resume-sess-42', params: {} },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const result = await adapter.execute(ctx);
|
|
141
|
+
|
|
142
|
+
expect(result.session).not.toBeNull();
|
|
143
|
+
expect(result.session?.sessionId).toBe('resume-sess-42');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('inherits HOME without redaction (HOME does not match sensitive key regex)', async () => {
|
|
147
|
+
const adapter = createClaudeAdapter();
|
|
148
|
+
let metaEnv: Record<string, string> = {};
|
|
149
|
+
|
|
150
|
+
const ctx = makeCtx({
|
|
151
|
+
onMeta: (meta: FixyInvocationMeta) => {
|
|
152
|
+
metaEnv = meta.env;
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await adapter.execute(ctx);
|
|
157
|
+
|
|
158
|
+
// HOME does not match /(key|token|secret|password|passwd|authorization|cookie)/i
|
|
159
|
+
// so it must NOT be redacted
|
|
160
|
+
expect(metaEnv['HOME']).not.toBe('***REDACTED***');
|
|
161
|
+
expect(typeof metaEnv['HOME']).toBe('string');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('redacts env keys matching the sensitive key pattern', async () => {
|
|
165
|
+
const adapter = createClaudeAdapter();
|
|
166
|
+
let metaEnv: Record<string, string> = {};
|
|
167
|
+
|
|
168
|
+
process.env['MY_SECRET_TOKEN'] = 'super-secret';
|
|
169
|
+
|
|
170
|
+
const ctx = makeCtx({
|
|
171
|
+
onMeta: (meta: FixyInvocationMeta) => {
|
|
172
|
+
metaEnv = meta.env;
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await adapter.execute(ctx);
|
|
178
|
+
// "TOKEN" matches /(key|token|secret|password|passwd|authorization|cookie)/i
|
|
179
|
+
expect(metaEnv['MY_SECRET_TOKEN']).toBe('***REDACTED***');
|
|
180
|
+
} finally {
|
|
181
|
+
delete process.env['MY_SECRET_TOKEN'];
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseClaudeStreamJson } from '../parse.js';
|
|
3
|
+
|
|
4
|
+
describe('parseClaudeStreamJson', () => {
|
|
5
|
+
it('extracts sessionId and summary from a result line', () => {
|
|
6
|
+
const input = [
|
|
7
|
+
JSON.stringify({
|
|
8
|
+
type: 'system',
|
|
9
|
+
subtype: 'init',
|
|
10
|
+
session_id: 'sess-abc-123',
|
|
11
|
+
model: 'claude-sonnet-4-6',
|
|
12
|
+
}),
|
|
13
|
+
JSON.stringify({
|
|
14
|
+
type: 'assistant',
|
|
15
|
+
session_id: 'sess-abc-123',
|
|
16
|
+
message: { content: [{ type: 'text', text: 'Hello there!' }] },
|
|
17
|
+
}),
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
type: 'result',
|
|
20
|
+
session_id: 'sess-abc-123',
|
|
21
|
+
result: 'Final answer here',
|
|
22
|
+
usage: { input_tokens: 100, output_tokens: 50 },
|
|
23
|
+
total_cost_usd: 0.01,
|
|
24
|
+
}),
|
|
25
|
+
].join('\n');
|
|
26
|
+
|
|
27
|
+
const result = parseClaudeStreamJson(input);
|
|
28
|
+
|
|
29
|
+
expect(result.sessionId).toBe('sess-abc-123');
|
|
30
|
+
expect(result.summary).toBe('Final answer here');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('falls back to concatenated assistant content when there is no result line', () => {
|
|
34
|
+
const input = [
|
|
35
|
+
JSON.stringify({ type: 'system', subtype: 'init', session_id: 'sess-no-result' }),
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
type: 'assistant',
|
|
38
|
+
session_id: 'sess-no-result',
|
|
39
|
+
message: { content: [{ type: 'text', text: 'Part 1' }] },
|
|
40
|
+
}),
|
|
41
|
+
JSON.stringify({
|
|
42
|
+
type: 'assistant',
|
|
43
|
+
session_id: 'sess-no-result',
|
|
44
|
+
message: { content: [{ type: 'text', text: 'Part 2' }] },
|
|
45
|
+
}),
|
|
46
|
+
].join('\n');
|
|
47
|
+
|
|
48
|
+
const result = parseClaudeStreamJson(input);
|
|
49
|
+
|
|
50
|
+
expect(result.sessionId).toBe('sess-no-result');
|
|
51
|
+
expect(result.summary).toContain('Part 1');
|
|
52
|
+
expect(result.summary).toContain('Part 2');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('falls back to raw stdout for completely invalid (non-JSON) output', () => {
|
|
56
|
+
const input = 'This is not JSON at all\nJust random text\n';
|
|
57
|
+
|
|
58
|
+
const result = parseClaudeStreamJson(input);
|
|
59
|
+
|
|
60
|
+
expect(result.sessionId).toBeNull();
|
|
61
|
+
// summary should be the raw input (the fallback path in parseClaudeStreamJson)
|
|
62
|
+
expect(result.summary).toBe(input);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns null sessionId and empty summary for an empty string', () => {
|
|
66
|
+
const result = parseClaudeStreamJson('');
|
|
67
|
+
|
|
68
|
+
expect(result.sessionId).toBeNull();
|
|
69
|
+
expect(result.summary).toBe('');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('uses assistant content as summary when result line has no result field', () => {
|
|
73
|
+
const input = [
|
|
74
|
+
JSON.stringify({ type: 'system', subtype: 'init', session_id: 'sess-empty-result' }),
|
|
75
|
+
JSON.stringify({
|
|
76
|
+
type: 'assistant',
|
|
77
|
+
session_id: 'sess-empty-result',
|
|
78
|
+
message: { content: [{ type: 'text', text: 'Assistant text fallback' }] },
|
|
79
|
+
}),
|
|
80
|
+
// result line exists but has no "result" field
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
type: 'result',
|
|
83
|
+
session_id: 'sess-empty-result',
|
|
84
|
+
usage: { input_tokens: 5, output_tokens: 5 },
|
|
85
|
+
}),
|
|
86
|
+
].join('\n');
|
|
87
|
+
|
|
88
|
+
const result = parseClaudeStreamJson(input);
|
|
89
|
+
|
|
90
|
+
expect(result.sessionId).toBe('sess-empty-result');
|
|
91
|
+
expect(result.summary).toBe('Assistant text fallback');
|
|
92
|
+
});
|
|
93
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// packages/claude-adapter/src/index.ts
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
FixyAdapter,
|
|
5
|
+
FixyProbeResult,
|
|
6
|
+
FixyExecutionContext,
|
|
7
|
+
FixyExecutionResult,
|
|
8
|
+
} from '@fixy/core';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
buildInheritedEnv,
|
|
12
|
+
redactEnvForLogs,
|
|
13
|
+
resolveCommand,
|
|
14
|
+
runChildProcess,
|
|
15
|
+
} from '@fixy/adapter-utils';
|
|
16
|
+
|
|
17
|
+
import { parseClaudeStreamJson } from './parse.js';
|
|
18
|
+
|
|
19
|
+
class ClaudeAdapter implements FixyAdapter {
|
|
20
|
+
readonly id = 'claude';
|
|
21
|
+
readonly name = 'Claude Code';
|
|
22
|
+
|
|
23
|
+
async probe(): Promise<FixyProbeResult> {
|
|
24
|
+
let resolvedCommand: string;
|
|
25
|
+
try {
|
|
26
|
+
resolvedCommand = await resolveCommand('claude');
|
|
27
|
+
} catch {
|
|
28
|
+
return {
|
|
29
|
+
available: false,
|
|
30
|
+
version: null,
|
|
31
|
+
authStatus: 'unknown',
|
|
32
|
+
detail: 'claude CLI not found in PATH',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const result = await runChildProcess({
|
|
38
|
+
command: resolvedCommand,
|
|
39
|
+
args: ['--version'],
|
|
40
|
+
cwd: process.cwd(),
|
|
41
|
+
env: buildInheritedEnv(),
|
|
42
|
+
timeoutMs: 10_000,
|
|
43
|
+
});
|
|
44
|
+
const version = result.stdout.trim() || null;
|
|
45
|
+
return {
|
|
46
|
+
available: true,
|
|
47
|
+
version,
|
|
48
|
+
authStatus: 'unknown',
|
|
49
|
+
detail: null,
|
|
50
|
+
};
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
53
|
+
return {
|
|
54
|
+
available: false,
|
|
55
|
+
version: null,
|
|
56
|
+
authStatus: 'unknown',
|
|
57
|
+
detail,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async execute(ctx: FixyExecutionContext): Promise<FixyExecutionResult> {
|
|
63
|
+
const args: string[] = ['--print', '--output-format', 'stream-json', '--verbose'];
|
|
64
|
+
|
|
65
|
+
if (ctx.session) {
|
|
66
|
+
args.push('--resume', ctx.session.sessionId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const env = buildInheritedEnv({});
|
|
70
|
+
const resolvedCommand = await resolveCommand('claude');
|
|
71
|
+
|
|
72
|
+
ctx.onMeta({
|
|
73
|
+
resolvedCommand,
|
|
74
|
+
args,
|
|
75
|
+
cwd: ctx.threadContext.worktreePath,
|
|
76
|
+
env: redactEnvForLogs(env),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await runChildProcess({
|
|
80
|
+
command: resolvedCommand,
|
|
81
|
+
args,
|
|
82
|
+
cwd: ctx.threadContext.worktreePath,
|
|
83
|
+
env,
|
|
84
|
+
stdin: ctx.prompt,
|
|
85
|
+
signal: ctx.signal,
|
|
86
|
+
onLog: ctx.onLog,
|
|
87
|
+
onSpawn: ctx.onSpawn,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const parsed = parseClaudeStreamJson(result.stdout);
|
|
91
|
+
|
|
92
|
+
const warnings: string[] = [];
|
|
93
|
+
if (result.stderr && result.stderr.trim().length > 0) {
|
|
94
|
+
warnings.push(result.stderr.trim());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let errorMessage: string | null = null;
|
|
98
|
+
if (result.exitCode !== 0 && !result.timedOut) {
|
|
99
|
+
const stderrMsg = result.stderr.trim();
|
|
100
|
+
errorMessage =
|
|
101
|
+
stderrMsg.length > 0
|
|
102
|
+
? `claude exited with code ${result.exitCode}: ${stderrMsg}`
|
|
103
|
+
: `claude exited with code ${result.exitCode}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
exitCode: result.exitCode,
|
|
108
|
+
signal: result.signal,
|
|
109
|
+
timedOut: result.timedOut,
|
|
110
|
+
summary: parsed.summary,
|
|
111
|
+
session: parsed.sessionId ? { sessionId: parsed.sessionId, params: {} } : null,
|
|
112
|
+
patches: [],
|
|
113
|
+
warnings,
|
|
114
|
+
errorMessage,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createClaudeAdapter(): FixyAdapter {
|
|
120
|
+
return new ClaudeAdapter();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { parseClaudeStreamJson } from './parse.js';
|
package/src/parse.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface ClaudeStreamResult {
|
|
2
|
+
sessionId: string | null;
|
|
3
|
+
summary: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function tryParseJson(line: string): Record<string, unknown> | null {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = JSON.parse(line);
|
|
9
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
10
|
+
return parsed as Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function asString(value: unknown, fallback: string): string {
|
|
19
|
+
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseClaudeStreamJson(stdout: string): ClaudeStreamResult {
|
|
23
|
+
if (stdout.length === 0) {
|
|
24
|
+
return { sessionId: null, summary: '' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let sessionId: string | null = null;
|
|
28
|
+
const assistantTexts: string[] = [];
|
|
29
|
+
let finalResult: Record<string, unknown> | null = null;
|
|
30
|
+
|
|
31
|
+
const lines = stdout.split('\n');
|
|
32
|
+
|
|
33
|
+
for (const rawLine of lines) {
|
|
34
|
+
const line = rawLine.trim();
|
|
35
|
+
if (line.length === 0) continue;
|
|
36
|
+
|
|
37
|
+
const obj = tryParseJson(line);
|
|
38
|
+
if (obj === null) continue;
|
|
39
|
+
|
|
40
|
+
const type = obj['type'];
|
|
41
|
+
|
|
42
|
+
if (type === 'system') {
|
|
43
|
+
if (obj['subtype'] === 'init') {
|
|
44
|
+
sessionId = asString(obj['session_id'], sessionId ?? '');
|
|
45
|
+
if (sessionId === '') sessionId = null;
|
|
46
|
+
}
|
|
47
|
+
} else if (type === 'assistant') {
|
|
48
|
+
const sid = asString(obj['session_id'], '');
|
|
49
|
+
if (sid !== '') sessionId = sid;
|
|
50
|
+
|
|
51
|
+
const message = obj['message'];
|
|
52
|
+
if (typeof message === 'object' && message !== null && !Array.isArray(message)) {
|
|
53
|
+
const msg = message as Record<string, unknown>;
|
|
54
|
+
const content = msg['content'];
|
|
55
|
+
if (Array.isArray(content)) {
|
|
56
|
+
for (const block of content) {
|
|
57
|
+
if (typeof block === 'object' && block !== null && !Array.isArray(block)) {
|
|
58
|
+
const b = block as Record<string, unknown>;
|
|
59
|
+
if (b['type'] === 'text') {
|
|
60
|
+
const text = asString(b['text'], '');
|
|
61
|
+
if (text !== '') {
|
|
62
|
+
assistantTexts.push(text);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} else if (type === 'result') {
|
|
70
|
+
finalResult = obj;
|
|
71
|
+
const sid = asString(obj['session_id'], '');
|
|
72
|
+
if (sid !== '') sessionId = sid;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let summary: string;
|
|
77
|
+
|
|
78
|
+
if (finalResult !== null) {
|
|
79
|
+
const resultText = asString(finalResult['result'], '');
|
|
80
|
+
summary = resultText !== '' ? resultText : assistantTexts.join('\n\n').trim();
|
|
81
|
+
const sid = asString(finalResult['session_id'], '');
|
|
82
|
+
if (sid !== '') sessionId = sid;
|
|
83
|
+
} else {
|
|
84
|
+
summary = assistantTexts.join('\n\n').trim();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (summary === '' && sessionId === null) {
|
|
88
|
+
return { sessionId: null, summary: stdout };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { sessionId, summary };
|
|
92
|
+
}
|