@linzumi/cli 1.0.18 → 1.0.20
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/README.md +1 -1
- package/dist/index.js +352 -325
- package/dist/mcp-server.mjs +8 -8
- package/package.json +12 -3
- package/scripts/normalize-agent-replay-fixture.ts +281 -0
- package/scripts/promote-gstack-claude-smoke-fixture.ts +891 -0
- package/scripts/record-agent-replay-fixtures.ts +580 -0
- package/scripts/replay-agent-backend-browser.ts +198 -0
- package/scripts/replay-agent-backend.ts +1782 -0
- package/scripts/replay-agent-fixture.ts +325 -0
- package/scripts/vendor-codex-app-server-protocol.ts +383 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linzumi/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.20",
|
|
4
4
|
"description": "Linzumi CLI \u2014 point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,15 @@
|
|
|
18
18
|
"codex:history-table": "node scripts/codex_history_table.mjs",
|
|
19
19
|
"test": "vitest run --config vitest.config.mjs",
|
|
20
20
|
"prepack": "node scripts/build.mjs",
|
|
21
|
-
"pack:dry-run": "npm pack --dry-run"
|
|
21
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
22
|
+
"record:agent-fixtures": "tsx scripts/record-agent-replay-fixtures.ts",
|
|
23
|
+
"normalize:agent-fixture": "tsx scripts/normalize-agent-replay-fixture.ts",
|
|
24
|
+
"vendor:codex-app-server-protocol": "tsx scripts/vendor-codex-app-server-protocol.ts",
|
|
25
|
+
"replay:agent-fixture": "tsx scripts/replay-agent-fixture.ts",
|
|
26
|
+
"replay:agent-backend": "tsx scripts/replay-agent-backend.ts",
|
|
27
|
+
"vendor:codex-app-server-protocol:check": "tsx scripts/vendor-codex-app-server-protocol.ts --check",
|
|
28
|
+
"replay:agent-backend-browser": "tsx scripts/replay-agent-backend-browser.ts",
|
|
29
|
+
"promote:gstack-claude-smoke-fixture": "tsx scripts/promote-gstack-claude-smoke-fixture.ts"
|
|
22
30
|
},
|
|
23
31
|
"keywords": [
|
|
24
32
|
"linzumi",
|
|
@@ -50,6 +58,7 @@
|
|
|
50
58
|
"esbuild": "^0.27.2",
|
|
51
59
|
"fast-check": "^3.23.2",
|
|
52
60
|
"tsx": "^4.21.0",
|
|
53
|
-
"vitest": "^1.6.1"
|
|
61
|
+
"vitest": "^1.6.1",
|
|
62
|
+
"typescript": "^5.9.3"
|
|
54
63
|
}
|
|
55
64
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- Date: 2026-06-29
|
|
3
|
+
Spec: plans/2026-06-29-claude-code-raw-replay-harness-plan.md
|
|
4
|
+
Relationship: Promotion-time normalizer for real raw agent replay captures.
|
|
5
|
+
Capture files stay exact in /tmp; this script produces deterministic fixtures
|
|
6
|
+
by normalizing machine-local paths and volatile run/session identifiers.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
9
|
+
import { basename, dirname, extname, join, resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export type AgentReplayNormalizeOptions = {
|
|
12
|
+
readonly normalizeProviderIds: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type NormalizerState = {
|
|
16
|
+
readonly options: AgentReplayNormalizeOptions;
|
|
17
|
+
readonly mappings: Map<string, string>;
|
|
18
|
+
nextRunId: number;
|
|
19
|
+
nextSessionId: number;
|
|
20
|
+
nextMessageId: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type CliOptions =
|
|
24
|
+
| {
|
|
25
|
+
readonly type: 'single';
|
|
26
|
+
readonly input: string;
|
|
27
|
+
readonly output: string;
|
|
28
|
+
readonly normalizeProviderIds: boolean;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
readonly type: 'directory';
|
|
32
|
+
readonly inputDir: string;
|
|
33
|
+
readonly outputDir: string;
|
|
34
|
+
readonly normalizeProviderIds: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const defaultOptions: AgentReplayNormalizeOptions = {
|
|
38
|
+
normalizeProviderIds: true,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const homePathPattern = /\/home\/([A-Za-z0-9._-]+)/gu;
|
|
42
|
+
const tmpRunPathPattern =
|
|
43
|
+
/\/tmp\/linzumi-(codex|claude)-fixture-[A-Za-z0-9]+/gu;
|
|
44
|
+
const tmpReplayPathPattern = /\/tmp\/linzumi-agent-replay-[A-Za-z0-9._-]+/gu;
|
|
45
|
+
const codexSessionFilePattern =
|
|
46
|
+
/rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-[0-9a-f-]{36}\.jsonl/giu;
|
|
47
|
+
const uuidPattern =
|
|
48
|
+
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/giu;
|
|
49
|
+
const codexMessageIdPattern = /\bmsg_[A-Za-z0-9_-]{16,}\b/gu;
|
|
50
|
+
const claudeSignaturePattern = /("signature"\s*:\s*")[^"]+(")/gu;
|
|
51
|
+
|
|
52
|
+
export function normalizeAgentReplayText(
|
|
53
|
+
text: string,
|
|
54
|
+
options: AgentReplayNormalizeOptions = defaultOptions
|
|
55
|
+
): string {
|
|
56
|
+
const state: NormalizerState = {
|
|
57
|
+
options,
|
|
58
|
+
mappings: new Map(),
|
|
59
|
+
nextRunId: 1,
|
|
60
|
+
nextSessionId: 1,
|
|
61
|
+
nextMessageId: 1,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return text
|
|
65
|
+
.split(/\r?\n/u)
|
|
66
|
+
.map((line) => normalizeNdjsonLine(line, state))
|
|
67
|
+
.join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeNdjsonLine(line: string, state: NormalizerState): string {
|
|
71
|
+
if (line.trim() === '') {
|
|
72
|
+
return line;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(line) as unknown;
|
|
77
|
+
return JSON.stringify(normalizeJsonValue(parsed, state));
|
|
78
|
+
} catch (_error) {
|
|
79
|
+
return normalizeString(line, state);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeJsonValue(value: unknown, state: NormalizerState): unknown {
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value.map((item) => normalizeJsonValue(item, state));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (value !== null && typeof value === 'object') {
|
|
89
|
+
const normalizedEntries = Object.entries(value).map(([key, child]) => {
|
|
90
|
+
if (key === 'rawText' && typeof child === 'string') {
|
|
91
|
+
return [key, normalizeRawText(child, state)] as const;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (key === 'signature' && typeof child === 'string') {
|
|
95
|
+
return [key, '[fixture-signature]'] as const;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return [key, normalizeJsonValue(child, state)] as const;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return Object.fromEntries(normalizedEntries);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return typeof value === 'string' ? normalizeString(value, state) : value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeRawText(value: string, state: NormalizerState): string {
|
|
108
|
+
try {
|
|
109
|
+
const parsed = JSON.parse(value) as unknown;
|
|
110
|
+
return JSON.stringify(normalizeJsonValue(parsed, state));
|
|
111
|
+
} catch (_error) {
|
|
112
|
+
return normalizeString(value, state);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeString(value: string, state: NormalizerState): string {
|
|
117
|
+
let normalized = value
|
|
118
|
+
.replace(tmpRunPathPattern, (_match, provider: string) =>
|
|
119
|
+
mappedValue(
|
|
120
|
+
state,
|
|
121
|
+
_match,
|
|
122
|
+
() => `/tmp/linzumi-agent-fixture/${provider}-run-${state.nextRunId++}`
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
.replace(tmpReplayPathPattern, '/tmp/linzumi-agent-replay')
|
|
126
|
+
.replace(homePathPattern, '/home/fixture-user')
|
|
127
|
+
.replace(codexSessionFilePattern, (match) =>
|
|
128
|
+
mappedValue(
|
|
129
|
+
state,
|
|
130
|
+
match,
|
|
131
|
+
() =>
|
|
132
|
+
`rollout-2026-06-29T00-00-00-fixture-session-${state.nextSessionId++}.jsonl`
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
.replace(claudeSignaturePattern, '$1[fixture-signature]$2');
|
|
136
|
+
|
|
137
|
+
switch (state.options.normalizeProviderIds) {
|
|
138
|
+
case true:
|
|
139
|
+
normalized = normalized
|
|
140
|
+
.replace(uuidPattern, (match) =>
|
|
141
|
+
mappedValue(
|
|
142
|
+
state,
|
|
143
|
+
match,
|
|
144
|
+
() =>
|
|
145
|
+
`00000000-0000-4000-8000-${String(state.nextSessionId++).padStart(12, '0')}`
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
.replace(codexMessageIdPattern, (match) =>
|
|
149
|
+
mappedValue(
|
|
150
|
+
state,
|
|
151
|
+
match,
|
|
152
|
+
() =>
|
|
153
|
+
`msg_fixture_${String(state.nextMessageId++).padStart(4, '0')}`
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
break;
|
|
157
|
+
case false:
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return normalized;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function mappedValue(
|
|
165
|
+
state: NormalizerState,
|
|
166
|
+
input: string,
|
|
167
|
+
create: () => string
|
|
168
|
+
): string {
|
|
169
|
+
const existing = state.mappings.get(input);
|
|
170
|
+
|
|
171
|
+
if (existing !== undefined) {
|
|
172
|
+
return existing;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const next = create();
|
|
176
|
+
state.mappings.set(input, next);
|
|
177
|
+
return next;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function main(): Promise<void> {
|
|
181
|
+
const args = process.argv.slice(2);
|
|
182
|
+
|
|
183
|
+
if (args.includes('--help')) {
|
|
184
|
+
printHelp();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const options = parseCliOptions(args);
|
|
189
|
+
|
|
190
|
+
switch (options.type) {
|
|
191
|
+
case 'single':
|
|
192
|
+
await normalizeFile(options.input, options.output, {
|
|
193
|
+
normalizeProviderIds: options.normalizeProviderIds,
|
|
194
|
+
});
|
|
195
|
+
process.stdout.write(`wrote ${options.output}\n`);
|
|
196
|
+
break;
|
|
197
|
+
case 'directory': {
|
|
198
|
+
await mkdir(options.outputDir, { recursive: true });
|
|
199
|
+
const entries = await readdir(options.inputDir, { withFileTypes: true });
|
|
200
|
+
const ndjsonFiles = entries.filter(
|
|
201
|
+
(entry) => entry.isFile() && extname(entry.name) === '.ndjson'
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
for (const entry of ndjsonFiles) {
|
|
205
|
+
const input = join(options.inputDir, entry.name);
|
|
206
|
+
const output = join(options.outputDir, entry.name);
|
|
207
|
+
await normalizeFile(input, output, {
|
|
208
|
+
normalizeProviderIds: options.normalizeProviderIds,
|
|
209
|
+
});
|
|
210
|
+
process.stdout.write(`wrote ${output}\n`);
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function normalizeFile(
|
|
218
|
+
input: string,
|
|
219
|
+
output: string,
|
|
220
|
+
options: AgentReplayNormalizeOptions
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
const source = await readFile(input, 'utf8');
|
|
223
|
+
const normalized = normalizeAgentReplayText(source, options);
|
|
224
|
+
await mkdir(dirname(output), { recursive: true });
|
|
225
|
+
await writeFile(output, normalized, 'utf8');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function parseCliOptions(args: readonly string[]): CliOptions {
|
|
229
|
+
const normalizeProviderIds = !args.includes('--keep-provider-ids');
|
|
230
|
+
const input = argValue(args, '--input');
|
|
231
|
+
const output = argValue(args, '--output');
|
|
232
|
+
const inputDir = argValue(args, '--input-dir');
|
|
233
|
+
const outputDir = argValue(args, '--output-dir');
|
|
234
|
+
|
|
235
|
+
if (input !== undefined && output !== undefined) {
|
|
236
|
+
return {
|
|
237
|
+
type: 'single',
|
|
238
|
+
input: resolve(input),
|
|
239
|
+
output: resolve(output),
|
|
240
|
+
normalizeProviderIds,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (inputDir !== undefined && outputDir !== undefined) {
|
|
245
|
+
return {
|
|
246
|
+
type: 'directory',
|
|
247
|
+
inputDir: resolve(inputDir),
|
|
248
|
+
outputDir: resolve(outputDir),
|
|
249
|
+
normalizeProviderIds,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
throw new Error(
|
|
254
|
+
'provide either --input/--output or --input-dir/--output-dir'
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function argValue(args: readonly string[], name: string): string | undefined {
|
|
259
|
+
const index = args.indexOf(name);
|
|
260
|
+
|
|
261
|
+
if (index === -1) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return args[index + 1];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function printHelp(): void {
|
|
269
|
+
process.stdout.write(
|
|
270
|
+
`Usage: pnpm run normalize:agent-fixture -- [options]\n\nOptions:\n --input <file> --output <file> Normalize one NDJSON fixture\n --input-dir <dir> --output-dir <dir> Normalize every .ndjson file in a directory\n --keep-provider-ids Preserve provider session/message ids\n\nExamples:\n pnpm run normalize:agent-fixture -- --input-dir /tmp/linzumi-agent-replay-probe --output-dir test/fixtures/agent-raw-replay/promoted\n`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (basename(process.argv[1] ?? '') === 'normalize-agent-replay-fixture.ts') {
|
|
275
|
+
main().catch((error) => {
|
|
276
|
+
process.stderr.write(
|
|
277
|
+
`${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`
|
|
278
|
+
);
|
|
279
|
+
process.exitCode = 1;
|
|
280
|
+
});
|
|
281
|
+
}
|