tlc-claude-code 2.4.3 → 2.4.5
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/.claude/commands/tlc/build.md +33 -13
- package/.claude/commands/tlc/recall.md +59 -87
- package/.claude/commands/tlc/remember.md +76 -71
- package/.claude/commands/tlc/review.md +76 -21
- package/.claude/hooks/tlc-capture-exchange.sh +50 -21
- package/.claude/hooks/tlc-session-init.sh +30 -0
- package/bin/init.js +12 -3
- package/package.json +1 -1
- package/server/lib/capture/classifier.js +71 -0
- package/server/lib/capture/classifier.test.js +71 -0
- package/server/lib/capture/claude-capture.js +140 -0
- package/server/lib/capture/claude-capture.test.js +152 -0
- package/server/lib/capture/codex-capture.js +79 -0
- package/server/lib/capture/codex-capture.test.js +161 -0
- package/server/lib/capture/codex-event-parser.js +76 -0
- package/server/lib/capture/codex-event-parser.test.js +83 -0
- package/server/lib/capture/ensure-ready.js +56 -0
- package/server/lib/capture/ensure-ready.test.js +135 -0
- package/server/lib/capture/envelope.js +77 -0
- package/server/lib/capture/envelope.test.js +169 -0
- package/server/lib/capture/extractor.js +51 -0
- package/server/lib/capture/extractor.test.js +92 -0
- package/server/lib/capture/generic-capture.js +96 -0
- package/server/lib/capture/generic-capture.test.js +171 -0
- package/server/lib/capture/index.js +117 -0
- package/server/lib/capture/index.test.js +263 -0
- package/server/lib/capture/redactor.js +68 -0
- package/server/lib/capture/redactor.test.js +93 -0
- package/server/lib/capture/spool-processor.js +155 -0
- package/server/lib/capture/spool-processor.test.js +278 -0
- package/server/lib/health-check.js +255 -0
- package/server/lib/health-check.test.js +243 -0
- package/server/lib/orchestration/cli-dispatch.js +200 -0
- package/server/lib/orchestration/cli-dispatch.test.js +242 -0
- package/server/lib/orchestration/prompt-builder.js +118 -0
- package/server/lib/orchestration/prompt-builder.test.js +200 -0
- package/server/lib/orchestration/standalone-compat.js +39 -0
- package/server/lib/orchestration/standalone-compat.test.js +144 -0
- package/server/lib/orchestration/worktree-manager.js +43 -0
- package/server/lib/orchestration/worktree-manager.test.js +50 -0
- package/server/lib/task-router-config.js +22 -5
- package/server/lib/task-router-config.test.js +46 -13
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const { extractDecisions } = require('./extractor.js');
|
|
4
|
+
|
|
5
|
+
describe('capture/extractor', () => {
|
|
6
|
+
it('extracts a decision statement from explicit decision language', () => {
|
|
7
|
+
const results = extractDecisions('We decided to use Postgres for the event store.');
|
|
8
|
+
|
|
9
|
+
expect(results).toHaveLength(1);
|
|
10
|
+
expect(results[0]).toMatchObject({
|
|
11
|
+
statement: 'We decided to use Postgres for the event store.',
|
|
12
|
+
context: 'We decided to use Postgres for the event store.',
|
|
13
|
+
role: 'unknown',
|
|
14
|
+
});
|
|
15
|
+
expect(results[0].confidence).toBeGreaterThan(0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('boosts confidence by 1.5x for assistant role', () => {
|
|
19
|
+
const text = 'We decided to use Postgres.';
|
|
20
|
+
|
|
21
|
+
const base = extractDecisions(text)[0];
|
|
22
|
+
const boosted = extractDecisions(text, { role: 'assistant' })[0];
|
|
23
|
+
|
|
24
|
+
expect(boosted.confidence).toBeCloseTo(base.confidence * 1.5, 5);
|
|
25
|
+
expect(boosted.role).toBe('assistant');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('extracts gotchas from warning phrases', () => {
|
|
29
|
+
const results = extractDecisions("Don't use synchronous fs calls in request handlers.");
|
|
30
|
+
|
|
31
|
+
expect(results).toHaveLength(1);
|
|
32
|
+
expect(results[0].statement).toBe("Don't use synchronous fs calls in request handlers.");
|
|
33
|
+
expect(results[0].context).toBe("Don't use synchronous fs calls in request handlers.");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('extracts preferences from personal preference phrases', () => {
|
|
37
|
+
const results = extractDecisions('I prefer dark theme for late-night debugging.');
|
|
38
|
+
|
|
39
|
+
expect(results).toHaveLength(1);
|
|
40
|
+
expect(results[0].statement).toBe('I prefer dark theme for late-night debugging.');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns an empty array when no phrases match', () => {
|
|
44
|
+
expect(extractDecisions('The server responded in 120ms and all tests passed.')).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('dedupes repeated statements with the same text', () => {
|
|
48
|
+
const text = 'We decided to use Postgres. We decided to use Postgres.';
|
|
49
|
+
|
|
50
|
+
const results = extractDecisions(text);
|
|
51
|
+
|
|
52
|
+
expect(results).toHaveLength(1);
|
|
53
|
+
expect(results[0].statement).toBe('We decided to use Postgres.');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('extracts the containing sentence as context from multi-sentence input', () => {
|
|
57
|
+
const text = 'We reviewed the tradeoffs. The approach is to split into worker and api modules. Shipping stays on Friday.';
|
|
58
|
+
|
|
59
|
+
const results = extractDecisions(text);
|
|
60
|
+
|
|
61
|
+
expect(results).toHaveLength(1);
|
|
62
|
+
expect(results[0].statement).toBe('The approach is to split into worker and api modules.');
|
|
63
|
+
expect(results[0].context).toBe('The approach is to split into worker and api modules.');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('extracts multiple unique matches across decision, gotcha, and preference sentences', () => {
|
|
67
|
+
const text = [
|
|
68
|
+
'We decided to use Postgres.',
|
|
69
|
+
'Careful with long transactions on SQLite.',
|
|
70
|
+
'I like terminal-first workflows.',
|
|
71
|
+
].join(' ');
|
|
72
|
+
|
|
73
|
+
const results = extractDecisions(text);
|
|
74
|
+
|
|
75
|
+
expect(results).toHaveLength(3);
|
|
76
|
+
expect(results.map((entry) => entry.statement)).toEqual([
|
|
77
|
+
'We decided to use Postgres.',
|
|
78
|
+
'Careful with long transactions on SQLite.',
|
|
79
|
+
'I like terminal-first workflows.',
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('captures code-change style decision phrases such as created and implemented', () => {
|
|
84
|
+
const text = 'Created 3 modules for auth boundaries. Implemented request tracing for retries.';
|
|
85
|
+
|
|
86
|
+
const results = extractDecisions(text);
|
|
87
|
+
|
|
88
|
+
expect(results).toHaveLength(2);
|
|
89
|
+
expect(results[0].statement).toBe('Created 3 modules for auth boundaries.');
|
|
90
|
+
expect(results[1].statement).toBe('Implemented request tracing for retries.');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const { createEnvelope } = require('./envelope');
|
|
4
|
+
const { redact } = require('./redactor');
|
|
5
|
+
const { ensureMemoryReady } = require('./ensure-ready');
|
|
6
|
+
|
|
7
|
+
const SPOOL_PATH = path.join('.tlc', 'memory', '.spool.jsonl');
|
|
8
|
+
|
|
9
|
+
function appendEnvelope(projectDir, envelope, fs) {
|
|
10
|
+
ensureMemoryReady(projectDir, { fs });
|
|
11
|
+
|
|
12
|
+
const spoolPath = path.join(projectDir, SPOOL_PATH);
|
|
13
|
+
fs.appendFileSync(spoolPath, `${JSON.stringify(envelope)}\n`, 'utf8');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createRedactedEnvelope({ text, provider, source, taskName }) {
|
|
17
|
+
const envelope = createEnvelope({
|
|
18
|
+
provider,
|
|
19
|
+
source,
|
|
20
|
+
taskName,
|
|
21
|
+
assistantMessage: text,
|
|
22
|
+
});
|
|
23
|
+
const redactedText = redact(envelope.text);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...envelope,
|
|
27
|
+
text: redactedText,
|
|
28
|
+
metadata: {
|
|
29
|
+
...envelope.metadata,
|
|
30
|
+
assistantMessage: redact(envelope.metadata.assistantMessage || ''),
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function captureFromStdout({
|
|
36
|
+
stdout,
|
|
37
|
+
provider,
|
|
38
|
+
taskName,
|
|
39
|
+
projectDir,
|
|
40
|
+
fs = require('fs'),
|
|
41
|
+
}) {
|
|
42
|
+
try {
|
|
43
|
+
const envelope = createRedactedEnvelope({
|
|
44
|
+
text: stdout,
|
|
45
|
+
provider,
|
|
46
|
+
source: 'stdout',
|
|
47
|
+
taskName,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
appendEnvelope(projectDir, envelope, fs);
|
|
51
|
+
|
|
52
|
+
return { captured: 1, skipped: 0 };
|
|
53
|
+
} catch (_) {
|
|
54
|
+
return { captured: 0, skipped: 1 };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractResponseText(response) {
|
|
59
|
+
if (typeof response?.content === 'string') {
|
|
60
|
+
return response.content;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof response?.choices?.[0]?.message?.content === 'string') {
|
|
64
|
+
return response.choices[0].message.content;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return JSON.stringify(response);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function captureFromApiResponse({
|
|
71
|
+
response,
|
|
72
|
+
provider,
|
|
73
|
+
taskName,
|
|
74
|
+
projectDir,
|
|
75
|
+
fs = require('fs'),
|
|
76
|
+
}) {
|
|
77
|
+
try {
|
|
78
|
+
const envelope = createRedactedEnvelope({
|
|
79
|
+
text: extractResponseText(response),
|
|
80
|
+
provider,
|
|
81
|
+
source: 'api-response',
|
|
82
|
+
taskName,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
appendEnvelope(projectDir, envelope, fs);
|
|
86
|
+
|
|
87
|
+
return { captured: 1, skipped: 0 };
|
|
88
|
+
} catch (_) {
|
|
89
|
+
return { captured: 0, skipped: 1 };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
captureFromStdout,
|
|
95
|
+
captureFromApiResponse,
|
|
96
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const SPOOL_PATH = path.join('.tlc', 'memory', '.spool.jsonl');
|
|
7
|
+
|
|
8
|
+
function readSpoolEntries(projectDir) {
|
|
9
|
+
const spoolPath = path.join(projectDir, SPOOL_PATH);
|
|
10
|
+
const content = fs.readFileSync(spoolPath, 'utf8').trim();
|
|
11
|
+
|
|
12
|
+
return content.split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('capture/generic-capture', () => {
|
|
16
|
+
let projectDir;
|
|
17
|
+
let captureFromStdout;
|
|
18
|
+
let captureFromApiResponse;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
vi.useFakeTimers();
|
|
22
|
+
vi.setSystemTime(new Date('2026-03-28T12:34:56.000Z'));
|
|
23
|
+
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-generic-capture-'));
|
|
24
|
+
({ captureFromStdout, captureFromApiResponse } = await import('./generic-capture.js'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
vi.useRealTimers();
|
|
29
|
+
vi.resetModules();
|
|
30
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('captures stdout into a redacted envelope', () => {
|
|
34
|
+
const result = captureFromStdout({
|
|
35
|
+
stdout: 'token=abc123 password=hunter2 /Users/jurgen/private/app.js',
|
|
36
|
+
provider: 'gemini',
|
|
37
|
+
taskName: 'Implement auth',
|
|
38
|
+
projectDir,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const [entry] = readSpoolEntries(projectDir);
|
|
42
|
+
|
|
43
|
+
expect(result).toEqual({ captured: 1, skipped: 0 });
|
|
44
|
+
expect(entry).toMatchObject({
|
|
45
|
+
provider: 'gemini',
|
|
46
|
+
source: 'stdout',
|
|
47
|
+
taskName: 'Implement auth',
|
|
48
|
+
timestamp: '2026-03-28T12:34:56.000Z',
|
|
49
|
+
metadata: {
|
|
50
|
+
assistantMessage: 'token=[TOKEN_REDACTED] password=[REDACTED] ~/private/app.js',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
expect(entry.text).toBe('token=[TOKEN_REDACTED] password=[REDACTED] ~/private/app.js');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns skipped when stdout is empty', () => {
|
|
57
|
+
const result = captureFromStdout({
|
|
58
|
+
stdout: ' ',
|
|
59
|
+
provider: 'gemini',
|
|
60
|
+
taskName: 'Implement auth',
|
|
61
|
+
projectDir,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result).toEqual({ captured: 0, skipped: 1 });
|
|
65
|
+
expect(fs.existsSync(path.join(projectDir, SPOOL_PATH))).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('appends to an existing spool file', () => {
|
|
69
|
+
const spoolPath = path.join(projectDir, SPOOL_PATH);
|
|
70
|
+
fs.mkdirSync(path.dirname(spoolPath), { recursive: true });
|
|
71
|
+
fs.writeFileSync(spoolPath, `${JSON.stringify({ existing: true })}\n`);
|
|
72
|
+
|
|
73
|
+
captureFromStdout({
|
|
74
|
+
stdout: 'We decided to use SQLite for local state.',
|
|
75
|
+
provider: 'openrouter',
|
|
76
|
+
taskName: 'State storage',
|
|
77
|
+
projectDir,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const lines = fs.readFileSync(spoolPath, 'utf8').trim().split('\n');
|
|
81
|
+
|
|
82
|
+
expect(lines).toHaveLength(2);
|
|
83
|
+
expect(JSON.parse(lines[0])).toEqual({ existing: true });
|
|
84
|
+
expect(JSON.parse(lines[1])).toMatchObject({
|
|
85
|
+
provider: 'openrouter',
|
|
86
|
+
source: 'stdout',
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('captures api response content when present', () => {
|
|
91
|
+
const result = captureFromApiResponse({
|
|
92
|
+
response: {
|
|
93
|
+
content: 'secret=hunter2\nWe decided to use retries.',
|
|
94
|
+
},
|
|
95
|
+
provider: 'gemini',
|
|
96
|
+
taskName: 'Retry policy',
|
|
97
|
+
projectDir,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const [entry] = readSpoolEntries(projectDir);
|
|
101
|
+
|
|
102
|
+
expect(result).toEqual({ captured: 1, skipped: 0 });
|
|
103
|
+
expect(entry.source).toBe('api-response');
|
|
104
|
+
expect(entry.text).toContain('[REDACTED]');
|
|
105
|
+
expect(entry.text).toContain('We decided to use retries.');
|
|
106
|
+
expect(entry.text).not.toContain('hunter2');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('falls back to choices[0].message.content for api responses', () => {
|
|
110
|
+
captureFromApiResponse({
|
|
111
|
+
response: {
|
|
112
|
+
choices: [
|
|
113
|
+
{
|
|
114
|
+
message: {
|
|
115
|
+
content: 'Use Redis for rate limits.',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
provider: 'openai',
|
|
121
|
+
taskName: 'Rate limits',
|
|
122
|
+
projectDir,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const [entry] = readSpoolEntries(projectDir);
|
|
126
|
+
|
|
127
|
+
expect(entry.text).toBe('Use Redis for rate limits.');
|
|
128
|
+
expect(entry.metadata.assistantMessage).toBe('Use Redis for rate limits.');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('falls back to JSON stringification when api response has no direct text field', () => {
|
|
132
|
+
captureFromApiResponse({
|
|
133
|
+
response: {
|
|
134
|
+
status: 'ok',
|
|
135
|
+
data: {
|
|
136
|
+
message: 'We decided to shard by tenant.',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
provider: 'anthropic-proxy',
|
|
140
|
+
taskName: 'Sharding',
|
|
141
|
+
projectDir,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const [entry] = readSpoolEntries(projectDir);
|
|
145
|
+
|
|
146
|
+
expect(entry.text).toContain('"status":"ok"');
|
|
147
|
+
expect(entry.text).toContain('"message":"We decided to shard by tenant."');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('supports injected fs implementations', () => {
|
|
151
|
+
const injectedFs = {
|
|
152
|
+
...fs,
|
|
153
|
+
appendFileSync: vi.fn(fs.appendFileSync),
|
|
154
|
+
existsSync: vi.fn(fs.existsSync),
|
|
155
|
+
mkdirSync: vi.fn(fs.mkdirSync),
|
|
156
|
+
readFileSync: vi.fn(fs.readFileSync),
|
|
157
|
+
writeFileSync: vi.fn(fs.writeFileSync),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const result = captureFromApiResponse({
|
|
161
|
+
response: { content: 'Use a queue for webhooks.' },
|
|
162
|
+
provider: 'gemini',
|
|
163
|
+
taskName: 'Webhook delivery',
|
|
164
|
+
projectDir,
|
|
165
|
+
fs: injectedFs,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(result).toEqual({ captured: 1, skipped: 0 });
|
|
169
|
+
expect(injectedFs.appendFileSync).toHaveBeenCalledTimes(1);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const { captureFromStdout, captureFromApiResponse } = require('./generic-capture');
|
|
4
|
+
|
|
5
|
+
const zeroCaptureCounts = new Map();
|
|
6
|
+
let warningCount = 0;
|
|
7
|
+
const MAX_WARNINGS_PER_SESSION = 3;
|
|
8
|
+
const WARNING_LOG_PATH = path.join('.tlc', 'memory', '.capture-warnings.log');
|
|
9
|
+
|
|
10
|
+
function warningsEnabled() {
|
|
11
|
+
return process.env.TLC_CAPTURE_WARNINGS !== 'false';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function maybeWarn(projectDir, fs, message) {
|
|
15
|
+
if (!warningsEnabled() || warningCount >= MAX_WARNINGS_PER_SESSION) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
warningCount += 1;
|
|
20
|
+
console.warn(message);
|
|
21
|
+
|
|
22
|
+
if (!projectDir) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const logPath = path.join(projectDir, WARNING_LOG_PATH);
|
|
28
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
29
|
+
fs.appendFileSync(logPath, `${new Date().toISOString()} ${message}\n`, 'utf8');
|
|
30
|
+
} catch (_) {
|
|
31
|
+
// Warning emission must not break capture routing.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function maybeEmitWarning({ provider, output, taskName, projectDir, result, fs }) {
|
|
36
|
+
if (result && result.captured > 0) {
|
|
37
|
+
zeroCaptureCounts.set(provider, 0);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const nextCount = (zeroCaptureCounts.get(provider) || 0) + 1;
|
|
42
|
+
zeroCaptureCounts.set(provider, nextCount);
|
|
43
|
+
|
|
44
|
+
if (typeof output !== 'string' || output.length <= 500) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (nextCount >= 3) {
|
|
49
|
+
maybeWarn(
|
|
50
|
+
projectDir,
|
|
51
|
+
fs,
|
|
52
|
+
`[TLC WARNING] ${provider} has produced 0 memory captures for ${nextCount} consecutive tasks. Check capture configuration.`,
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
maybeWarn(
|
|
58
|
+
projectDir,
|
|
59
|
+
fs,
|
|
60
|
+
`[TLC WARNING] No decisions captured from ${provider} for task "${taskName}". Memory capture may not be working for this provider.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function captureFromProvider({
|
|
65
|
+
provider,
|
|
66
|
+
output,
|
|
67
|
+
taskName,
|
|
68
|
+
threadId,
|
|
69
|
+
projectDir,
|
|
70
|
+
hookInput,
|
|
71
|
+
lastMessageFile,
|
|
72
|
+
jsonStream,
|
|
73
|
+
fs = require('fs'),
|
|
74
|
+
}) {
|
|
75
|
+
let result;
|
|
76
|
+
|
|
77
|
+
if (provider === 'claude') {
|
|
78
|
+
const { captureClaudeExchange } = require('./claude-capture');
|
|
79
|
+
result = captureClaudeExchange({ hookInput, projectDir, fs });
|
|
80
|
+
} else if (provider === 'codex') {
|
|
81
|
+
const { captureCodexOutput } = require('./codex-capture');
|
|
82
|
+
result = captureCodexOutput({
|
|
83
|
+
lastMessageFile,
|
|
84
|
+
jsonStream,
|
|
85
|
+
stdout: output,
|
|
86
|
+
taskName,
|
|
87
|
+
threadId,
|
|
88
|
+
projectDir,
|
|
89
|
+
fs,
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
result = captureFromStdout({
|
|
93
|
+
stdout: output,
|
|
94
|
+
provider,
|
|
95
|
+
taskName,
|
|
96
|
+
projectDir,
|
|
97
|
+
fs,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
maybeEmitWarning({
|
|
102
|
+
provider,
|
|
103
|
+
output,
|
|
104
|
+
taskName,
|
|
105
|
+
projectDir,
|
|
106
|
+
result,
|
|
107
|
+
fs,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
captureFromProvider,
|
|
115
|
+
captureFromStdout,
|
|
116
|
+
captureFromApiResponse,
|
|
117
|
+
};
|