@assethub/cli 0.1.31 → 0.1.33
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 +9 -2
- package/dist/hooks/command.d.ts.map +1 -1
- package/dist/hooks/command.js +21 -18
- package/dist/hooks/graph.d.ts.map +1 -1
- package/dist/hooks/graph.js +7 -5
- package/dist/hooks/install.d.ts +24 -9
- package/dist/hooks/install.d.ts.map +1 -1
- package/dist/hooks/install.js +147 -35
- package/dist/hooks/save.d.ts +3 -1
- package/dist/hooks/save.d.ts.map +1 -1
- package/dist/hooks/save.js +3 -2
- package/dist/hooks/transcript.d.ts.map +1 -1
- package/dist/hooks/transcript.js +5 -1
- package/dist/hooks/trim.d.ts +12 -2
- package/dist/hooks/trim.d.ts.map +1 -1
- package/dist/hooks/trim.js +117 -5
- package/dist/hooks/types.d.ts +1 -1
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/hooks/upload.js +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -16
- package/dist/productionBatch.d.ts +2 -0
- package/dist/productionBatch.d.ts.map +1 -1
- package/dist/productionBatch.js +17 -1
- package/dist/progressReport.d.ts +42 -0
- package/dist/progressReport.d.ts.map +1 -0
- package/dist/progressReport.js +201 -0
- package/dist/setupCommand.d.ts +3 -0
- package/dist/setupCommand.d.ts.map +1 -1
- package/dist/setupCommand.js +57 -22
- package/dist/setupHooksBridge.d.ts +4 -0
- package/dist/setupHooksBridge.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -378,12 +378,19 @@ assembly was made. Follow a run started without `--wait` with
|
|
|
378
378
|
While a V4 run works, `--wait`, `runs watch` and `runs get` show where it is.
|
|
379
379
|
The receipt's `progress` field has the phase, a one-line summary, each part's
|
|
380
380
|
state and the Blender assembly rounds, in the words of the canvas Part Composer
|
|
381
|
-
card. `--wait` and `runs watch` print a stderr line
|
|
381
|
+
card. `--wait` and `runs watch` print a stderr line, with the local time, only
|
|
382
|
+
when the run really moves on (a new step, another mesh ready, a part kept with
|
|
383
|
+
an issue, the end); part states that come and go do not print:
|
|
382
384
|
|
|
383
385
|
```text
|
|
384
|
-
|
|
386
|
+
19:52 run 557d2a58 3/4 Making the parts · 3 of 9 meshes ready
|
|
387
|
+
19:58 run 557d2a58 ⚠ kept with an issue: Cropped top · open hem at the lower chest
|
|
388
|
+
20:05 run 557d2a58 ✗ FAILED · at 3/4 Making the parts · Blender assembly failed
|
|
385
389
|
```
|
|
386
390
|
|
|
391
|
+
`production batch --wait` prints the same lines per image, named by letter and
|
|
392
|
+
file name, and a status table every 5 minutes; see `docs/guides/production-batch.md`.
|
|
393
|
+
|
|
387
394
|
```sh
|
|
388
395
|
assethub runs watch <run-id> --timeout-ms 7200000 --download --out-dir ./character
|
|
389
396
|
assethub runs get <run-id> # JSON receipt, progress included
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/hooks/command.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/hooks/command.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;IACjC,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IAChB,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5D,UAAU,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CACxC,CAAA;AA+CD,eAAO,MAAM,eAAe,GAC1B,MAAM,MAAM,EAAE,EACd,OAAM,gBAAqB,KAC1B,OAAO,CAAC,MAAM,CAiIhB,CAAA"}
|
package/dist/hooks/command.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// `assethub hooks <subcommand>` — the registration in src/index.ts just calls
|
|
2
2
|
// runHooksCommand(argv).
|
|
3
|
-
import { installHooks,
|
|
3
|
+
import { installHooks, sessionSaveDisclosure, uninstallHooks } from './install.js';
|
|
4
4
|
import { listSessionDirs, readMeta } from './paths.js';
|
|
5
5
|
import { saveSession } from './save.js';
|
|
6
6
|
import { canUploadSessions, resolveUploadAuth, uploadPending, uploadSession } from './upload.js';
|
|
7
7
|
const USAGE = 'Usage: assethub hooks <install|uninstall|save|upload|list|status> [--client claude|codex] [--json]\n' +
|
|
8
|
-
'
|
|
8
|
+
' install|uninstall [--global] (the current folder by default; --global for every project)\n' +
|
|
9
|
+
' save [--client codex] [--transcript <path> --session-id <id>] (reads hook JSON from stdin without flags)\n' +
|
|
9
10
|
' upload [--session <dir> [--auto] [--sweep <n>]|--pending [--force] [--limit <n>]]\n' +
|
|
10
11
|
' --auto and --pending wait out earlier failures (1h, 2h, 4h … up to a day); --force retries now';
|
|
11
12
|
const parseFlags = (args) => {
|
|
@@ -68,31 +69,32 @@ export const runHooksCommand = async (argv, deps = {}) => {
|
|
|
68
69
|
write('--client claude|codex is required\n');
|
|
69
70
|
return 2;
|
|
70
71
|
}
|
|
72
|
+
// Sessions are saved only where the hooks are: this folder, unless --global.
|
|
73
|
+
const scope = flags.global === true ? {} : { projectDir: deps.cwd ?? process.cwd() };
|
|
74
|
+
const paths = { home: deps.home, codexHome: (deps.env ?? process.env).CODEX_HOME?.trim() || undefined };
|
|
71
75
|
if (sub === 'install') {
|
|
72
76
|
// Only accounts that may upload sessions (internal for now) can turn
|
|
73
77
|
// session saving on; for anyone else the hooks would only pile up.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return 1;
|
|
79
|
-
}
|
|
78
|
+
const auth = await resolveUploadAuth(uploadOptions);
|
|
79
|
+
if (!auth || !(await canUploadSessions(auth, deps.fetchImpl))) {
|
|
80
|
+
write('Session saving is not available for this account.\n');
|
|
81
|
+
return 1;
|
|
80
82
|
}
|
|
81
|
-
const result = await installHooks({
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
cliPath: deps.cliPath,
|
|
85
|
-
});
|
|
86
|
-
out(result.installed && client === 'claude'
|
|
87
|
-
? `${result.detail}\n${SESSION_SAVE_DISCLOSURE}`
|
|
88
|
-
: result.detail, result);
|
|
89
|
-
return result.installed || client === 'codex' ? 0 : 1;
|
|
83
|
+
const result = await installHooks({ ...scope, ...paths, client, cliPath: deps.cliPath });
|
|
84
|
+
out(result.installed ? `${result.detail}\n${sessionSaveDisclosure(scope, client)}` : result.detail, result);
|
|
85
|
+
return result.installed ? 0 : 1;
|
|
90
86
|
}
|
|
91
|
-
const result = await uninstallHooks({
|
|
87
|
+
const result = await uninstallHooks({ ...scope, ...paths, client });
|
|
92
88
|
out(result.detail, result);
|
|
93
89
|
return 0;
|
|
94
90
|
}
|
|
95
91
|
case 'save': {
|
|
92
|
+
// A misspelt client must not be saved as Claude. Exit 1, not 2: a
|
|
93
|
+
// hook's exit 2 can block the agent.
|
|
94
|
+
if (flags.client !== undefined && flags.client !== 'claude' && flags.client !== 'codex') {
|
|
95
|
+
write('Nothing saved: use --client claude or --client codex.\n');
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
96
98
|
let stdin;
|
|
97
99
|
let input;
|
|
98
100
|
if (typeof flags.transcript === 'string') {
|
|
@@ -109,6 +111,7 @@ export const runHooksCommand = async (argv, deps = {}) => {
|
|
|
109
111
|
const result = await saveSession({
|
|
110
112
|
stdin,
|
|
111
113
|
input,
|
|
114
|
+
client: flags.client === 'codex' ? 'codex' : 'claude',
|
|
112
115
|
home: deps.home,
|
|
113
116
|
env: deps.env,
|
|
114
117
|
now: deps.now,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/hooks/graph.ts"],"names":[],"mappings":"AAkCA,eAAO,MAAM,oBAAoB,iBAAiB,CAAA;AAClD,eAAO,MAAM,iBAAiB,4BAA4B,CAAA;AAC1D,eAAO,MAAM,oBAAoB,yBAAyB,CAAA;AAC1D,sDAAsD;AACtD,eAAO,MAAM,sBAAsB,QAAkB,CAAA;AAIrD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,QAAoB,CAAA;AAIvD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAA;AAGlC,KAAK,MAAM,GAAG;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,CAAA;AAC9F,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAQ7C,CAAA;AA4BD,eAAO,MAAM,cAAc,GAAI,gBAAgB,MAAM,KAAG,MAMvD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,EAAE,YAA4B,KAAG,MAAM,EAanF,CAAA;AAED,eAAO,MAAM,uBAAuB,GAClC,YAAY,MAAM,EAClB,UAAS;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAM,KACpC,OAAO,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/hooks/graph.ts"],"names":[],"mappings":"AAkCA,eAAO,MAAM,oBAAoB,iBAAiB,CAAA;AAClD,eAAO,MAAM,iBAAiB,4BAA4B,CAAA;AAC1D,eAAO,MAAM,oBAAoB,yBAAyB,CAAA;AAC1D,sDAAsD;AACtD,eAAO,MAAM,sBAAsB,QAAkB,CAAA;AAIrD;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,QAAoB,CAAA;AAIvD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAA;AAGlC,KAAK,MAAM,GAAG;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAC,CAAA;AAC9F,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,MAQ7C,CAAA;AA4BD,eAAO,MAAM,cAAc,GAAI,gBAAgB,MAAM,KAAG,MAMvD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,EAAE,YAA4B,KAAG,MAAM,EAanF,CAAA;AAED,eAAO,MAAM,uBAAuB,GAClC,YAAY,MAAM,EAClB,UAAS;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAM,KACpC,OAAO,CAAC,MAAM,CAmLhB,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,uBAAuB,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAE9E,CAAA"}
|
package/dist/hooks/graph.js
CHANGED
|
@@ -103,6 +103,8 @@ export const buildSessionGraphFolder = async (sessionDir, options = {}) => {
|
|
|
103
103
|
if (!meta)
|
|
104
104
|
throw new Error(`No meta.json in ${sessionDir}`);
|
|
105
105
|
const limits = compactLimits(options.compactLevel ?? 0);
|
|
106
|
+
// A session an older CLI saved has no client: it came from Claude Code.
|
|
107
|
+
const agentTag = `agent:${meta.client ?? 'claude'}`;
|
|
106
108
|
const transcriptBytes = await readFile(join(sessionDir, 'transcript.jsonl'));
|
|
107
109
|
const transcript = parseTranscript(transcriptBytes.toString('utf8'));
|
|
108
110
|
await resetSessionGraphFolder(sessionDir);
|
|
@@ -134,7 +136,7 @@ export const buildSessionGraphFolder = async (sessionDir, options = {}) => {
|
|
|
134
136
|
const id = `transcript_${pad(index)}`;
|
|
135
137
|
const blobRef = await addBlob(part, TRANSCRIPT_BLOB_MIME, `transcript-${pad(index)}.jsonl`);
|
|
136
138
|
nodes.push({
|
|
137
|
-
id, artifactKind: 'file', semanticType: 'transcript', tags: ['session',
|
|
139
|
+
id, artifactKind: 'file', semanticType: 'transcript', tags: ['session', agentTag],
|
|
138
140
|
metadata: { part: index, parts: transcriptParts.length },
|
|
139
141
|
payload: { blobRef },
|
|
140
142
|
});
|
|
@@ -155,12 +157,12 @@ export const buildSessionGraphFolder = async (sessionDir, options = {}) => {
|
|
|
155
157
|
}
|
|
156
158
|
: item.kind === 'assistant'
|
|
157
159
|
? {
|
|
158
|
-
id, artifactKind: 'text', tags: ['session',
|
|
160
|
+
id, artifactKind: 'text', tags: ['session', agentTag],
|
|
159
161
|
metadata: { role: 'assistant', timestamp: createdAt },
|
|
160
162
|
payload: clip(redactText(item.text), limits.textChars),
|
|
161
163
|
}
|
|
162
164
|
: {
|
|
163
|
-
id, artifactKind: 'run', tags: ['session',
|
|
165
|
+
id, artifactKind: 'run', tags: ['session', agentTag, `tool:${item.toolName}`],
|
|
164
166
|
metadata: { toolUseId: item.toolUseId, timestamp: createdAt },
|
|
165
167
|
payload: { toolName: item.toolName, input: redactedInput(item.input, limits.toolInputBytes) },
|
|
166
168
|
};
|
|
@@ -207,7 +209,7 @@ export const buildSessionGraphFolder = async (sessionDir, options = {}) => {
|
|
|
207
209
|
const blobRef = await addBlob(bytes, image.mime, image.file.slice(0, 200));
|
|
208
210
|
const id = `image_${pad(index)}`;
|
|
209
211
|
nodes.push({
|
|
210
|
-
id, artifactKind: 'image', tags: ['session',
|
|
212
|
+
id, artifactKind: 'image', tags: ['session', agentTag],
|
|
211
213
|
metadata: { sourcePath: redactText(image.original) },
|
|
212
214
|
payload: { blobRef },
|
|
213
215
|
});
|
|
@@ -215,7 +217,7 @@ export const buildSessionGraphFolder = async (sessionDir, options = {}) => {
|
|
|
215
217
|
}
|
|
216
218
|
nodes.push({
|
|
217
219
|
id: sessionNodeId, artifactKind: 'console', semanticType: 'agent-session',
|
|
218
|
-
tags: ['session',
|
|
220
|
+
tags: ['session', agentTag],
|
|
219
221
|
metadata: {
|
|
220
222
|
sessionId: meta.sessionId, client: meta.client, lastEvent: meta.lastEvent,
|
|
221
223
|
messageCount: messageNodes.length, skippedImages,
|
package/dist/hooks/install.d.ts
CHANGED
|
@@ -7,10 +7,26 @@ export declare const HOOK_TIMEOUT_SECONDS = 30;
|
|
|
7
7
|
*/
|
|
8
8
|
export declare const SESSION_END_TIMEOUT_SECONDS = 10;
|
|
9
9
|
export declare const MAX_SETTINGS_BACKUPS = 3;
|
|
10
|
+
/** Where the hooks go: one project folder, or every project when omitted (--global). */
|
|
11
|
+
export type HookScope = {
|
|
12
|
+
projectDir?: string;
|
|
13
|
+
};
|
|
14
|
+
/** Where each client keeps its config, per project and for the user. */
|
|
15
|
+
type ClientPaths = {
|
|
16
|
+
home?: string;
|
|
17
|
+
codexHome?: string;
|
|
18
|
+
};
|
|
10
19
|
/** What `hooks install` and `setup` must tell the user before recording starts. */
|
|
11
|
-
export declare const
|
|
20
|
+
export declare const sessionSaveDisclosure: (scope: HookScope, client?: HookClient) => string;
|
|
12
21
|
export declare const isOurCommand: (command: unknown) => boolean;
|
|
13
|
-
|
|
22
|
+
/** Codex hooks name their client, so the saved session is recorded as Codex. */
|
|
23
|
+
export declare const hookCommand: (cliPath?: string, client?: HookClient) => string;
|
|
24
|
+
/**
|
|
25
|
+
* A project install in the home folder would land in ~/.claude (or ~/.codex),
|
|
26
|
+
* which the client also reads for every project, so it is refused rather than
|
|
27
|
+
* silently global.
|
|
28
|
+
*/
|
|
29
|
+
export declare const hookScopeError: (home: string | undefined, scope: HookScope, client?: HookClient, codexHome?: string) => Promise<string | undefined>;
|
|
14
30
|
type HookEntry = {
|
|
15
31
|
type: 'command';
|
|
16
32
|
command: string;
|
|
@@ -19,22 +35,21 @@ type HookEntry = {
|
|
|
19
35
|
};
|
|
20
36
|
/**
|
|
21
37
|
* SessionStart (sweep and retry), Stop (every turn) and PreCompact only mark or
|
|
22
|
-
* spawn work, and run in the background so they never delay
|
|
38
|
+
* spawn work, and run in the background so they never delay Claude Code.
|
|
23
39
|
* SessionEnd stays synchronous, because Claude Code is exiting and a
|
|
24
|
-
* background hook could be cut off.
|
|
40
|
+
* background hook could be cut off. Codex hooks all run synchronously; the
|
|
41
|
+
* work is the same few local file writes.
|
|
25
42
|
*/
|
|
26
|
-
export declare const hookEntry: (event: (typeof HOOK_EVENTS)[number], command: string) => HookEntry;
|
|
27
|
-
export declare const installHooks: (options: {
|
|
43
|
+
export declare const hookEntry: (event: (typeof HOOK_EVENTS)[number], command: string, client?: HookClient) => HookEntry;
|
|
44
|
+
export declare const installHooks: (options: HookScope & ClientPaths & {
|
|
28
45
|
client: HookClient;
|
|
29
|
-
home?: string;
|
|
30
46
|
cliPath?: string;
|
|
31
47
|
}) => Promise<{
|
|
32
48
|
installed: boolean;
|
|
33
49
|
detail: string;
|
|
34
50
|
}>;
|
|
35
|
-
export declare const uninstallHooks: (options: {
|
|
51
|
+
export declare const uninstallHooks: (options: HookScope & ClientPaths & {
|
|
36
52
|
client: HookClient;
|
|
37
|
-
home?: string;
|
|
38
53
|
}) => Promise<{
|
|
39
54
|
removed: boolean;
|
|
40
55
|
detail: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/hooks/install.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,YAAY,CAAA;AAE1C,eAAO,MAAM,WAAW,+DAAgE,CAAA;AACxF,eAAO,MAAM,oBAAoB,KAAK,CAAA;AACtC;;;GAGG;AACH,eAAO,MAAM,2BAA2B,KAAK,CAAA;AAC7C,eAAO,MAAM,oBAAoB,IAAI,CAAA;AAErC,wFAAwF;AACxF,MAAM,MAAM,SAAS,GAAG;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAC,CAAA;AAE7C,wEAAwE;AACxE,KAAK,WAAW,GAAG;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAC,CAAA;AAkBtD,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,GAAI,OAAO,SAAS,EAAE,SAAQ,UAAqB,KAAG,MAmBvF,CAAA;AAWD,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,OACa,CAAA;AAI7D,gFAAgF;AAChF,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,EAAE,SAAQ,UAAqB,KAAG,MACsB,CAAA;AAMpG;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,GAAG,SAAS,EACxB,OAAO,SAAS,EAChB,SAAQ,UAAqB,EAC7B,YAAY,MAAM,KACjB,OAAO,CAAC,MAAM,GAAG,SAAS,CAY5B,CAAA;AA6GD,KAAK,SAAS,GAAG;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,IAAI,CAAA;CAAC,CAAA;AAElF;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,GACpB,OAAO,CAAC,OAAO,WAAW,EAAE,MAAM,CAAC,EACnC,SAAS,MAAM,EACf,SAAQ,UAAqB,KAC5B,SAK2E,CAAA;AAE9E,eAAO,MAAM,YAAY,GAAU,SAAS,SAAS,GAAG,WAAW,GAAG;IACpE,MAAM,EAAE,UAAU,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,KAAG,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CA8C/C,CAAA;AAED,eAAO,MAAM,cAAc,GAAU,SAAS,SAAS,GAAG,WAAW,GAAG;IACtE,MAAM,EAAE,UAAU,CAAA;CACnB,KAAG,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAqC7C,CAAA"}
|
package/dist/hooks/install.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
// Register / remove our
|
|
1
|
+
// Register / remove our session hooks.
|
|
2
|
+
//
|
|
3
|
+
// Claude Code: one project's .claude/settings.local.json (the default for
|
|
4
|
+
// `hooks install`), or with --global the user's ~/.claude/settings.json.
|
|
5
|
+
// Codex: one project's .codex/hooks.json, or with --global
|
|
6
|
+
// $CODEX_HOME/hooks.json. Codex has no personal per-project file, so the
|
|
7
|
+
// project file is kept out of git (info/exclude) and a tracked one is refused.
|
|
2
8
|
//
|
|
3
9
|
// Merge rules: keep every existing key and hook, identify our entry by its
|
|
4
10
|
// command string (`<assethub> hooks save`), never duplicate it, and back the
|
|
5
11
|
// original file up before any change.
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
12
|
+
import { execFile } from 'node:child_process';
|
|
13
|
+
import { appendFile, copyFile, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
15
|
+
import { promisify } from 'node:util';
|
|
8
16
|
import { resolveHome } from './paths.js';
|
|
9
17
|
export const HOOK_EVENTS = ['SessionStart', 'Stop', 'SessionEnd', 'PreCompact'];
|
|
10
18
|
export const HOOK_TIMEOUT_SECONDS = 30;
|
|
@@ -14,26 +22,121 @@ export const HOOK_TIMEOUT_SECONDS = 30;
|
|
|
14
22
|
*/
|
|
15
23
|
export const SESSION_END_TIMEOUT_SECONDS = 10;
|
|
16
24
|
export const MAX_SETTINGS_BACKUPS = 3;
|
|
25
|
+
const CLIENT_NAME = { claude: 'Claude Code', codex: 'Codex' };
|
|
26
|
+
const PROJECT_DIR = { claude: '.claude', codex: '.codex' };
|
|
27
|
+
const PROJECT_FILE = { claude: 'settings.local.json', codex: 'hooks.json' };
|
|
28
|
+
const projectSettingsPath = (projectDir, client = 'claude') => join(resolve(projectDir), PROJECT_DIR[client], PROJECT_FILE[client]);
|
|
29
|
+
/** The client's user-level folder: ~/.claude, or $CODEX_HOME (default ~/.codex). */
|
|
30
|
+
const userConfigDir = (client, paths) => client === 'codex' ? (paths.codexHome ?? join(resolveHome(paths.home), '.codex')) : join(resolveHome(paths.home), '.claude');
|
|
31
|
+
const settingsPath = (client, paths, scope) => scope.projectDir
|
|
32
|
+
? projectSettingsPath(scope.projectDir, client)
|
|
33
|
+
: join(userConfigDir(client, paths), client === 'codex' ? 'hooks.json' : 'settings.json');
|
|
17
34
|
/** What `hooks install` and `setup` must tell the user before recording starts. */
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
export const sessionSaveDisclosure = (scope, client = 'claude') => {
|
|
36
|
+
const name = CLIENT_NAME[client];
|
|
37
|
+
const where = scope.projectDir
|
|
38
|
+
? `${projectSettingsPath(scope.projectDir, client)}. ` +
|
|
39
|
+
`Every ${name} session opened in ${resolve(scope.projectDir)} (and only there)`
|
|
40
|
+
: `${client === 'codex' ? '$CODEX_HOME/hooks.json' : '~/.claude/settings.json'} (--global). Every ${name} session on this machine, in any project,`;
|
|
41
|
+
const remove = scope.projectDir
|
|
42
|
+
? `\`assethub hooks uninstall --client ${client}\` from that folder`
|
|
43
|
+
: `\`assethub hooks uninstall --client ${client} --global\``;
|
|
44
|
+
return (`Session saving adds SessionStart, Stop, PreCompact and SessionEnd hooks to ${where} ` +
|
|
45
|
+
'is then copied to ~/.assethub/sessions/ when it ends ' +
|
|
46
|
+
'(the conversation only: your prompts, the agent\'s replies and tool calls, with recognised secrets masked, readable only by you; copies older than 30 days are deleted), ' +
|
|
47
|
+
'and uploaded to AssetHub as a coding-agent session only you can read (internal accounts only for now): the masked transcript, ' +
|
|
48
|
+
'your prompts, the agent\'s replies, its tool calls and images from the project. Uploads that fail are retried in the background ' +
|
|
49
|
+
'when a later session starts or ends. Secret masking is best effort. ' +
|
|
50
|
+
'Opt a project out with .assethub/no-session-save, or turn it off with ASSETHUB_SESSION_SAVE=off / ASSETHUB_SESSION_UPLOAD=off; ' +
|
|
51
|
+
`remove it with ${remove}.`);
|
|
52
|
+
};
|
|
27
53
|
const isObject = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
|
|
28
54
|
// Only a command that runs the CLI itself: `assethub hooks save`, optionally a
|
|
29
55
|
// (quoted) path to it. `echo assethub hooks save` is somebody else's hook.
|
|
30
56
|
const OUR_COMMAND_RE = /^\s*(?:"(?:[^"]*[/\\])?assethub(?:\.js)?"|'(?:[^']*[/\\])?assethub(?:\.js)?'|(?:[^\s"']*[/\\])?assethub(?:\.js)?)\s+hooks\s+save(?:\s|$)/;
|
|
31
57
|
export const isOurCommand = (command) => typeof command === 'string' && OUR_COMMAND_RE.test(command);
|
|
32
58
|
const quoteIfNeeded = (value) => (/\s/.test(value) ? JSON.stringify(value) : value);
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
/** Codex hooks name their client, so the saved session is recorded as Codex. */
|
|
60
|
+
export const hookCommand = (cliPath, client = 'claude') => `${quoteIfNeeded(cliPath ?? 'assethub')} hooks save${client === 'codex' ? ' --client codex' : ''}`;
|
|
61
|
+
// Symlinks are followed: a project path or a client folder that leads to the
|
|
62
|
+
// home folder is the home folder. A path that does not exist yet is taken as is.
|
|
63
|
+
const realOrResolved = async (path) => realpath(path).catch(() => resolve(path));
|
|
64
|
+
/**
|
|
65
|
+
* A project install in the home folder would land in ~/.claude (or ~/.codex),
|
|
66
|
+
* which the client also reads for every project, so it is refused rather than
|
|
67
|
+
* silently global.
|
|
68
|
+
*/
|
|
69
|
+
export const hookScopeError = async (home, scope, client = 'claude', codexHome) => {
|
|
70
|
+
if (!scope.projectDir)
|
|
71
|
+
return undefined;
|
|
72
|
+
const homeDir = resolveHome(home);
|
|
73
|
+
const [project, projectConfig, realHome, userConfig] = await Promise.all([
|
|
74
|
+
realOrResolved(scope.projectDir),
|
|
75
|
+
realOrResolved(join(scope.projectDir, PROJECT_DIR[client])),
|
|
76
|
+
realOrResolved(homeDir),
|
|
77
|
+
realOrResolved(userConfigDir(client, { home, codexHome })),
|
|
78
|
+
]);
|
|
79
|
+
return project === realHome || projectConfig === userConfig
|
|
80
|
+
? 'Run this inside a project folder: in your home folder the hooks would apply to every project. Use --global if that is what you want.'
|
|
81
|
+
: undefined;
|
|
82
|
+
};
|
|
83
|
+
const run = promisify(execFile);
|
|
84
|
+
const git = async (cwd, ...args) => run('git', ['-C', cwd, ...args]).then(({ stdout }) => stdout, () => undefined);
|
|
85
|
+
/**
|
|
86
|
+
* Codex reads hooks only from .codex/hooks.json, which a repository may share.
|
|
87
|
+
* Ours stay personal: a tracked file is refused, an untracked one goes into
|
|
88
|
+
* the repository's info/exclude (never the shared .gitignore).
|
|
89
|
+
*/
|
|
90
|
+
const keepCodexHooksOutOfGit = async (path) => {
|
|
91
|
+
const dir = dirname(dirname(path));
|
|
92
|
+
const found = await git(dir, 'rev-parse', '--show-toplevel', '--git-path', 'info/exclude');
|
|
93
|
+
if (!found)
|
|
94
|
+
return undefined; // not a repository, or no git
|
|
95
|
+
const [top, excludePath] = found.trim().split('\n');
|
|
96
|
+
const file = relative(await realOrResolved(top), join(await realOrResolved(dir), PROJECT_DIR.codex, PROJECT_FILE.codex));
|
|
97
|
+
if (file.startsWith('..'))
|
|
98
|
+
return undefined;
|
|
99
|
+
if ((await git(top, 'ls-files', '--', file))?.trim())
|
|
100
|
+
return `${path} is tracked by git, so the hooks would be shared with everyone using the repository; left untouched.`;
|
|
101
|
+
const exclude = resolve(dir, excludePath);
|
|
102
|
+
// The file and the backups an install leaves next to it.
|
|
103
|
+
const pattern = `/${file.split(sep).join('/')}*`;
|
|
104
|
+
const current = await readFile(exclude, 'utf8').catch(() => '');
|
|
105
|
+
if (!current.split('\n').includes(pattern)) {
|
|
106
|
+
await mkdir(dirname(exclude), { recursive: true });
|
|
107
|
+
await appendFile(exclude, `${current && !current.endsWith('\n') ? '\n' : ''}${pattern}\n`);
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
};
|
|
111
|
+
/** `[projects."<path>"]` sections with `trust_level = "trusted"` in Codex's config.toml. */
|
|
112
|
+
const codexTrustedProjects = async (codexDir) => {
|
|
113
|
+
const toml = await readFile(join(codexDir, 'config.toml'), 'utf8').catch(() => '');
|
|
114
|
+
const trusted = new Set();
|
|
115
|
+
const sections = toml.split(/^(?=\[)/m);
|
|
116
|
+
for (const section of sections) {
|
|
117
|
+
const header = /^\[projects\."((?:[^"\\]|\\.)*)"\]/.exec(section);
|
|
118
|
+
if (header && /^\s*trust_level\s*=\s*"trusted"/m.test(section))
|
|
119
|
+
trusted.add(header[1].replace(/\\(.)/g, '$1'));
|
|
120
|
+
}
|
|
121
|
+
return trusted;
|
|
122
|
+
};
|
|
123
|
+
/** What Codex still needs before the hooks run. */
|
|
124
|
+
const codexNotes = async (codexDir, scope) => {
|
|
125
|
+
const review = ' Codex runs a new or changed hook only after you approve it: open /hooks in Codex.';
|
|
126
|
+
if (!scope.projectDir)
|
|
127
|
+
return review;
|
|
128
|
+
const dir = scope.projectDir;
|
|
129
|
+
const top = (await git(dir, 'rev-parse', '--show-toplevel'))?.trim();
|
|
130
|
+
const candidates = [resolve(dir), await realOrResolved(dir), ...(top ? [top, await realOrResolved(top)] : [])];
|
|
131
|
+
const trusted = await codexTrustedProjects(codexDir);
|
|
132
|
+
if (candidates.some(path => trusted.has(path)))
|
|
133
|
+
return review;
|
|
134
|
+
return (` Codex loads a project's hooks only when the project is trusted: add [projects."${resolve(dir)}"] ` +
|
|
135
|
+
`with trust_level = "trusted" to ${join(codexDir, 'config.toml')}, or trust the folder when Codex asks.` +
|
|
136
|
+
review);
|
|
137
|
+
};
|
|
138
|
+
const load = async (client, paths, scope) => {
|
|
139
|
+
const path = settingsPath(client, paths, scope);
|
|
37
140
|
let raw;
|
|
38
141
|
try {
|
|
39
142
|
raw = await readFile(path, 'utf8');
|
|
@@ -56,10 +159,11 @@ const load = async (home) => {
|
|
|
56
159
|
return { path, error: `${path} is not valid JSON; left untouched.` };
|
|
57
160
|
}
|
|
58
161
|
};
|
|
59
|
-
const
|
|
162
|
+
const escapeRe = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
60
163
|
const pruneBackups = async (path) => {
|
|
61
164
|
const dir = dirname(path);
|
|
62
|
-
const
|
|
165
|
+
const backupRe = new RegExp(`^${escapeRe(basename(path))}\\.bak-\\d+$`);
|
|
166
|
+
const ours = (await readdir(dir)).filter(name => backupRe.test(name));
|
|
63
167
|
const byAge = ours.sort((a, b) => Number(b.split('-').pop()) - Number(a.split('-').pop()));
|
|
64
168
|
for (const name of byAge.slice(MAX_SETTINGS_BACKUPS))
|
|
65
169
|
await rm(join(dir, name), { force: true });
|
|
@@ -78,26 +182,37 @@ const backupAndWrite = async (loaded, next) => {
|
|
|
78
182
|
};
|
|
79
183
|
/**
|
|
80
184
|
* SessionStart (sweep and retry), Stop (every turn) and PreCompact only mark or
|
|
81
|
-
* spawn work, and run in the background so they never delay
|
|
185
|
+
* spawn work, and run in the background so they never delay Claude Code.
|
|
82
186
|
* SessionEnd stays synchronous, because Claude Code is exiting and a
|
|
83
|
-
* background hook could be cut off.
|
|
187
|
+
* background hook could be cut off. Codex hooks all run synchronously; the
|
|
188
|
+
* work is the same few local file writes.
|
|
84
189
|
*/
|
|
85
|
-
export const hookEntry = (event, command) => event === 'SessionEnd'
|
|
190
|
+
export const hookEntry = (event, command, client = 'claude') => event === 'SessionEnd'
|
|
86
191
|
? { type: 'command', command, timeout: SESSION_END_TIMEOUT_SECONDS }
|
|
87
|
-
:
|
|
192
|
+
: client === 'codex'
|
|
193
|
+
? { type: 'command', command, timeout: HOOK_TIMEOUT_SECONDS }
|
|
194
|
+
: { type: 'command', command, timeout: HOOK_TIMEOUT_SECONDS, async: true };
|
|
88
195
|
export const installHooks = async (options) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
196
|
+
const { client } = options;
|
|
197
|
+
const refused = await hookScopeError(options.home, options, client, options.codexHome);
|
|
198
|
+
if (refused)
|
|
199
|
+
return { installed: false, detail: refused };
|
|
200
|
+
const loaded = await load(client, options, options);
|
|
92
201
|
if ('error' in loaded)
|
|
93
202
|
return { installed: false, detail: loaded.error };
|
|
94
|
-
|
|
203
|
+
if (client === 'codex' && options.projectDir) {
|
|
204
|
+
const shared = await keepCodexHooksOutOfGit(loaded.path);
|
|
205
|
+
if (shared)
|
|
206
|
+
return { installed: false, detail: shared };
|
|
207
|
+
}
|
|
208
|
+
const notes = client === 'codex' ? await codexNotes(userConfigDir(client, options), options) : '';
|
|
209
|
+
const command = hookCommand(options.cliPath, client);
|
|
95
210
|
const settings = structuredClone(loaded.settings);
|
|
96
211
|
const hooks = isObject(settings.hooks) ? settings.hooks : {};
|
|
97
212
|
let changed = false;
|
|
98
213
|
for (const event of HOOK_EVENTS) {
|
|
99
214
|
const groups = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
100
|
-
const wanted = hookEntry(event, command);
|
|
215
|
+
const wanted = hookEntry(event, command, client);
|
|
101
216
|
let found = false;
|
|
102
217
|
for (const group of groups) {
|
|
103
218
|
if (!isObject(group) || !Array.isArray(group.hooks))
|
|
@@ -121,20 +236,17 @@ export const installHooks = async (options) => {
|
|
|
121
236
|
}
|
|
122
237
|
settings.hooks = hooks;
|
|
123
238
|
if (!changed) {
|
|
124
|
-
return { installed: true, detail: `Hooks already installed in ${loaded.path}` };
|
|
239
|
+
return { installed: true, detail: `Hooks already installed in ${loaded.path}.${notes}` };
|
|
125
240
|
}
|
|
126
241
|
await backupAndWrite(loaded, settings);
|
|
127
|
-
return { installed: true, detail: `Installed ${HOOK_EVENTS.join(', ')} hooks in ${loaded.path}` };
|
|
242
|
+
return { installed: true, detail: `Installed ${HOOK_EVENTS.join(', ')} hooks in ${loaded.path}.${notes}` };
|
|
128
243
|
};
|
|
129
244
|
export const uninstallHooks = async (options) => {
|
|
130
|
-
|
|
131
|
-
return { removed: false, detail: 'Codex: no hooks were installed by assethub.' };
|
|
132
|
-
}
|
|
133
|
-
const loaded = await load(options.home);
|
|
245
|
+
const loaded = await load(options.client, options, options);
|
|
134
246
|
if ('error' in loaded)
|
|
135
247
|
return { removed: false, detail: loaded.error };
|
|
136
248
|
if (!loaded.exists || !isObject(loaded.settings.hooks)) {
|
|
137
|
-
return { removed: false, detail:
|
|
249
|
+
return { removed: false, detail: `No assethub hooks found in ${loaded.path}.` };
|
|
138
250
|
}
|
|
139
251
|
const settings = structuredClone(loaded.settings);
|
|
140
252
|
const hooks = settings.hooks;
|
|
@@ -166,7 +278,7 @@ export const uninstallHooks = async (options) => {
|
|
|
166
278
|
delete hooks[event];
|
|
167
279
|
}
|
|
168
280
|
if (!changed)
|
|
169
|
-
return { removed: false, detail:
|
|
281
|
+
return { removed: false, detail: `No assethub hooks found in ${loaded.path}.` };
|
|
170
282
|
if (Object.keys(hooks).length === 0)
|
|
171
283
|
delete settings.hooks;
|
|
172
284
|
await backupAndWrite(loaded, settings);
|
package/dist/hooks/save.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HookInput, SessionMeta } from './types.js';
|
|
1
|
+
import type { HookClient, HookInput, SessionMeta } from './types.js';
|
|
2
2
|
import type { UploadOptions } from './upload.js';
|
|
3
3
|
export declare const MAX_IMAGES = 50;
|
|
4
4
|
export declare const MAX_IMAGE_BYTES: number;
|
|
@@ -8,6 +8,8 @@ export type SaveSessionOptions = {
|
|
|
8
8
|
stdin?: string;
|
|
9
9
|
/** Or the already-parsed fields. */
|
|
10
10
|
input?: HookInput;
|
|
11
|
+
/** The agent the hook runs in; its hook command says so (`--client codex`). */
|
|
12
|
+
client?: HookClient;
|
|
11
13
|
home?: string;
|
|
12
14
|
now?: () => Date;
|
|
13
15
|
env?: NodeJS.ProcessEnv;
|
package/dist/hooks/save.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/hooks/save.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAC,SAAS,EAAgB,WAAW,EAAC,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"save.d.ts","sourceRoot":"","sources":["../../src/hooks/save.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAC,UAAU,EAAE,SAAS,EAAgB,WAAW,EAAC,MAAM,YAAY,CAAA;AAChF,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,aAAa,CAAA;AAE9C,eAAO,MAAM,UAAU,KAAK,CAAA;AAC5B,eAAO,MAAM,eAAe,QAAmB,CAAA;AAC/C,eAAO,MAAM,sBAAsB,KAAK,CAAA;AASxC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oCAAoC;IACpC,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,SAAS,CAAC,CAAA;IACvC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5D,uEAAuE;IACvE,UAAU,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CACxC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,SAAS,GAAG,UAAU,CAAA;CACjC,CAAA;AAYD,wFAAwF;AACxF,eAAO,MAAM,mBAAmB,GAAI,YAAY,MAAM,EAAE,UAAU,MAAM,KAAG,IACgC,CAAA;AAE3G,8DAA8D;AAC9D,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,KAAG,IAC+B,CAAA;AAErF,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,CAAC,UAAU,KAAG,MAAM,GAAG,IAM/D,CAAA;AAED,4FAA4F;AAC5F,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,EACZ,KAAK,IAAI,EACT,MAAM,MAAM,EACZ,MAAM,MAAM,KACX,OAAO,CAAC,MAAM,CAwBhB,CAAA;AAgFD,qGAAqG;AACrG,eAAO,MAAM,kBAAkB,QAAqB,CAAA;AACpD,wEAAwE;AACxE,eAAO,MAAM,WAAW,IAAI,CAAA;AAK5B;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAC7B,YAAY,MAAM,EAClB,UAAS;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAM,KAC/D,OAAO,CAAC,WAAW,GAAG,IAAI,CA6C5B,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,MAAM,EACZ,KAAK,IAAI,EACT,mBAAmB,MAAM,KACxB,OAAO,CAAC,MAAM,CAmBhB,CAAA;AAED,eAAO,MAAM,WAAW,GAAU,UAAS,kBAAuB,KAAG,OAAO,CAAC,iBAAiB,CAkF7F,CAAA"}
|
package/dist/hooks/save.js
CHANGED
|
@@ -281,11 +281,12 @@ export const saveSession = async (options = {}) => {
|
|
|
281
281
|
const canvasId = await readCanvasId(cwd, env);
|
|
282
282
|
const sessionId = input.session_id;
|
|
283
283
|
const transcriptPath = input.transcript_path;
|
|
284
|
+
const client = options.client ?? 'claude';
|
|
284
285
|
const full = FULL_SAVE_EVENTS.has(event);
|
|
285
286
|
const fresh = {
|
|
286
287
|
sessionId,
|
|
287
288
|
cwd,
|
|
288
|
-
client
|
|
289
|
+
client,
|
|
289
290
|
lastEvent: event,
|
|
290
291
|
savedAt: now.toISOString(),
|
|
291
292
|
status: 'saved',
|
|
@@ -297,7 +298,7 @@ export const saveSession = async (options = {}) => {
|
|
|
297
298
|
...latest,
|
|
298
299
|
sessionId,
|
|
299
300
|
cwd,
|
|
300
|
-
client
|
|
301
|
+
client,
|
|
301
302
|
transcriptPath,
|
|
302
303
|
lastEvent: event,
|
|
303
304
|
savedAt: now.toISOString(),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../src/hooks/transcript.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GACtB;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAC,GAClD;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAC,GACrD;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAC,CAAA;AAE3F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,mEAAmE;IACnE,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;CACpC,CAAA;
|
|
1
|
+
{"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../../src/hooks/transcript.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,cAAc,GACtB;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAC,GAClD;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAC,GACrD;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAC,CAAA;AAE3F,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,mEAAmE;IACnE,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;CACpC,CAAA;AAQD,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,KAAG,MAAM,EAGtD,CAAA;AAgBD,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,KAAG,gBAiD7C,CAAA"}
|
package/dist/hooks/transcript.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// Parse a Claude Code transcript (JSONL) into the flat sequence the session
|
|
2
2
|
// graph is built from.
|
|
3
|
-
|
|
3
|
+
// One run of path characters (they include `/`), capped at Linux's PATH_MAX
|
|
4
|
+
// (4096) so a long line with no spaces, such as base64, is scanned in linear
|
|
5
|
+
// time. A nested `(?:/…+)+` backtracks exponentially there and hung the save
|
|
6
|
+
// on a Codex rollout.
|
|
7
|
+
const IMAGE_PATH_RE = /(?<![\w.~-])\/[^\s"'\\<>|:*?`()[\]{}]{0,4096}?\.(?:png|jpe?g|webp)\b/gi;
|
|
4
8
|
export const extractImagePaths = (text) => {
|
|
5
9
|
const found = text.match(IMAGE_PATH_RE) ?? [];
|
|
6
10
|
return [...new Set(found)];
|
package/dist/hooks/trim.d.ts
CHANGED
|
@@ -7,8 +7,18 @@ type Json = Record<string, unknown>;
|
|
|
7
7
|
/** One transcript entry reduced to what the conversation needs, or undefined to drop it. */
|
|
8
8
|
export declare const trimTranscriptEntry: (entry: unknown) => Json | undefined;
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* One Codex rollout record as a conversation entry in the same shape as a
|
|
11
|
+
* trimmed Claude Code transcript, or undefined to drop it. Kept: the artist's
|
|
12
|
+
* prompts, the agent's replies, its tool calls and their output, and compaction
|
|
13
|
+
* summaries. Dropped: session metadata (base instructions, git, cwd), developer
|
|
14
|
+
* and context messages, reasoning, turn settings and the event stream, which
|
|
15
|
+
* repeats the messages.
|
|
16
|
+
*/
|
|
17
|
+
export declare const codexEntry: (entry: unknown) => Json | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Trim a JSONL transcript: a Claude Code transcript, or a Codex rollout turned
|
|
20
|
+
* into the same conversation shape. A file in any other format is returned
|
|
21
|
+
* unchanged.
|
|
12
22
|
*/
|
|
13
23
|
export declare const trimTranscript: (raw: string) => string;
|
|
14
24
|
export {};
|
package/dist/hooks/trim.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trim.d.ts","sourceRoot":"","sources":["../../src/hooks/trim.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"trim.d.ts","sourceRoot":"","sources":["../../src/hooks/trim.ts"],"names":[],"mappings":"AAUA;;;GAGG;AACH,eAAO,MAAM,iBAAiB,oBAAoB,CAAA;AAElD,KAAK,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAuDnC,4FAA4F;AAC5F,eAAO,MAAM,mBAAmB,GAAI,OAAO,OAAO,KAAG,IAAI,GAAG,SAqB3D,CAAA;AA8DD;;;;;;;GAOG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,IAAI,GAAG,SAuClD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAgB5C,CAAA"}
|