@assethub/cli 0.1.31 → 0.1.32
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/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/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
|
@@ -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"}
|
package/dist/hooks/trim.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Keep only the conversation from a Claude Code transcript
|
|
1
|
+
// Keep only the conversation from a Claude Code transcript (or a Codex rollout,
|
|
2
|
+
// converted to the same shape) before it is saved
|
|
2
3
|
// or uploaded: the artist's prompts, the agent's replies, its tool calls and
|
|
3
4
|
// their results, and compaction summaries. Everything else Claude Code writes
|
|
4
5
|
// to the file (skill, tool and agent listings, the system prompt, hook
|
|
@@ -85,9 +86,118 @@ export const trimTranscriptEntry = (entry) => {
|
|
|
85
86
|
},
|
|
86
87
|
};
|
|
87
88
|
};
|
|
89
|
+
/** Top-level record types of a Codex rollout (`~/.codex/sessions/.../rollout-*.jsonl`). */
|
|
90
|
+
const CODEX_LINE_TYPES = new Set(['session_meta', 'response_item', 'event_msg', 'turn_context', 'compacted']);
|
|
91
|
+
// Codex sends its context as user messages: AGENTS.md, and tagged blocks such
|
|
92
|
+
// as <environment_context> or <user_instructions>. They are not the artist's words.
|
|
93
|
+
const CODEX_CONTEXT_BLOCK = /<([A-Za-z][\w-]*(?:context|instructions|plugins))(?:\s[^>]*)?>[\s\S]*?<\/\1>\s*/g;
|
|
94
|
+
const codexUserText = (text) => /^\s*# AGENTS\.md instructions/.test(text) ? '' : cleanText(text.replace(CODEX_CONTEXT_BLOCK, ''));
|
|
95
|
+
const codexText = (content, blockType, clean) => (Array.isArray(content) ? content : [])
|
|
96
|
+
.map(block => (isObject(block) && block.type === blockType && typeof block.text === 'string' ? clean(block.text) : ''))
|
|
97
|
+
.filter(text => text.trim())
|
|
98
|
+
.join('\n');
|
|
99
|
+
// An exec result is JSON text with timing and token bookkeeping around the
|
|
100
|
+
// command's output; only the output is conversation.
|
|
101
|
+
const codexOutputText = (text) => {
|
|
102
|
+
if (text.trimStart().startsWith('{')) {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(text);
|
|
105
|
+
if (isObject(parsed) && typeof parsed.output === 'string')
|
|
106
|
+
return parsed.output;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// plain text that happens to start with a brace
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return text;
|
|
113
|
+
};
|
|
114
|
+
/** Tool output: a string, `{output}`, or a list of `{type, text}` items (images are dropped). */
|
|
115
|
+
const codexToolOutput = (output) => {
|
|
116
|
+
if (typeof output === 'string')
|
|
117
|
+
return cleanText(codexOutputText(output));
|
|
118
|
+
if (Array.isArray(output))
|
|
119
|
+
return cleanText(output
|
|
120
|
+
.map(item => (isObject(item) && typeof item.text === 'string' ? codexOutputText(item.text) : ''))
|
|
121
|
+
.filter(Boolean)
|
|
122
|
+
.join('\n'));
|
|
123
|
+
if (isObject(output)) {
|
|
124
|
+
for (const key of ['output', 'content'])
|
|
125
|
+
if (typeof output[key] === 'string')
|
|
126
|
+
return cleanText(output[key]);
|
|
127
|
+
}
|
|
128
|
+
return output === undefined ? '' : JSON.stringify(output);
|
|
129
|
+
};
|
|
130
|
+
const codexArguments = (value) => {
|
|
131
|
+
if (typeof value !== 'string')
|
|
132
|
+
return value ?? {};
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(value);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { arguments: value };
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const codexMessage = (type, timestamp, content) => ({
|
|
141
|
+
type,
|
|
142
|
+
...(typeof timestamp === 'string' ? { timestamp } : {}),
|
|
143
|
+
message: { role: type, content },
|
|
144
|
+
});
|
|
145
|
+
/**
|
|
146
|
+
* One Codex rollout record as a conversation entry in the same shape as a
|
|
147
|
+
* trimmed Claude Code transcript, or undefined to drop it. Kept: the artist's
|
|
148
|
+
* prompts, the agent's replies, its tool calls and their output, and compaction
|
|
149
|
+
* summaries. Dropped: session metadata (base instructions, git, cwd), developer
|
|
150
|
+
* and context messages, reasoning, turn settings and the event stream, which
|
|
151
|
+
* repeats the messages.
|
|
152
|
+
*/
|
|
153
|
+
export const codexEntry = (entry) => {
|
|
154
|
+
if (!isObject(entry) || !isObject(entry.payload))
|
|
155
|
+
return undefined;
|
|
156
|
+
const { payload, timestamp } = entry;
|
|
157
|
+
if (entry.type === 'compacted')
|
|
158
|
+
return typeof payload.message === 'string' && payload.message.trim() ? { type: 'summary', summary: payload.message } : undefined;
|
|
159
|
+
if (entry.type !== 'response_item')
|
|
160
|
+
return undefined;
|
|
161
|
+
switch (payload.type) {
|
|
162
|
+
case 'message': {
|
|
163
|
+
if (payload.role === 'user') {
|
|
164
|
+
const text = codexText(payload.content, 'input_text', codexUserText).trim();
|
|
165
|
+
return text ? codexMessage('user', timestamp, text) : undefined;
|
|
166
|
+
}
|
|
167
|
+
if (payload.role !== 'assistant')
|
|
168
|
+
return undefined;
|
|
169
|
+
const text = codexText(payload.content, 'output_text', cleanText);
|
|
170
|
+
return text.trim() ? codexMessage('assistant', timestamp, [{ type: 'text', text }]) : undefined;
|
|
171
|
+
}
|
|
172
|
+
case 'function_call':
|
|
173
|
+
case 'custom_tool_call':
|
|
174
|
+
case 'local_shell_call': {
|
|
175
|
+
const id = typeof payload.call_id === 'string' ? payload.call_id : payload.id;
|
|
176
|
+
if (typeof id !== 'string')
|
|
177
|
+
return undefined;
|
|
178
|
+
const name = typeof payload.name === 'string' ? payload.name : payload.type === 'local_shell_call' ? 'shell' : 'unknown';
|
|
179
|
+
const input = payload.type === 'function_call'
|
|
180
|
+
? codexArguments(payload.arguments)
|
|
181
|
+
: payload.type === 'custom_tool_call'
|
|
182
|
+
? { input: payload.input }
|
|
183
|
+
: (payload.action ?? {});
|
|
184
|
+
return codexMessage('assistant', timestamp, [{ type: 'tool_use', id, name, input }]);
|
|
185
|
+
}
|
|
186
|
+
case 'function_call_output':
|
|
187
|
+
case 'custom_tool_call_output': {
|
|
188
|
+
if (typeof payload.call_id !== 'string')
|
|
189
|
+
return undefined;
|
|
190
|
+
return codexMessage('user', timestamp, [{ type: 'tool_result', tool_use_id: payload.call_id, content: codexToolOutput(payload.output) }]);
|
|
191
|
+
}
|
|
192
|
+
default:
|
|
193
|
+
// reasoning, web search and anything else that is not conversation
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
88
197
|
/**
|
|
89
|
-
* Trim a JSONL transcript
|
|
90
|
-
*
|
|
198
|
+
* Trim a JSONL transcript: a Claude Code transcript, or a Codex rollout turned
|
|
199
|
+
* into the same conversation shape. A file in any other format is returned
|
|
200
|
+
* unchanged.
|
|
91
201
|
*/
|
|
92
202
|
export const trimTranscript = (raw) => {
|
|
93
203
|
const entries = raw
|
|
@@ -101,8 +211,10 @@ export const trimTranscript = (raw) => {
|
|
|
101
211
|
return undefined;
|
|
102
212
|
}
|
|
103
213
|
});
|
|
104
|
-
|
|
214
|
+
const isClaude = entries.some(entry => isObject(entry) && CLAUDE_LINE_TYPES.has(String(entry.type)));
|
|
215
|
+
const isCodex = !isClaude && entries.some(entry => isObject(entry) && CODEX_LINE_TYPES.has(String(entry.type)) && isObject(entry.payload));
|
|
216
|
+
if (!isClaude && !isCodex)
|
|
105
217
|
return raw;
|
|
106
|
-
const kept = entries.map(trimTranscriptEntry).filter((entry) => entry !== undefined);
|
|
218
|
+
const kept = entries.map(isCodex ? codexEntry : trimTranscriptEntry).filter((entry) => entry !== undefined);
|
|
107
219
|
return kept.map(entry => `${JSON.stringify(entry)}\n`).join('');
|
|
108
220
|
};
|
package/dist/hooks/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/hooks/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE3C,MAAM,MAAM,SAAS,GAAG;IACtB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,gBAAgB,GAAG,UAAU,CAAA;AAEnE,MAAM,MAAM,YAAY,GAAG;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAA;AAEvF,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/hooks/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE3C,MAAM,MAAM,SAAS,GAAG;IACtB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,gBAAgB,GAAG,UAAU,CAAA;AAEnE,MAAM,MAAM,YAAY,GAAG;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAA;AAEvF,MAAM,MAAM,WAAW,GAAG;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,UAAU,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,aAAa,CAAA;IACrB,MAAM,EAAE,YAAY,EAAE,CAAA;IACtB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,gGAAgG;IAChG,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,mEAAmE;IACnE,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,sFAAsF;IACtF,SAAS,CAAC,EAAE,IAAI,CAAA;CACjB,CAAA"}
|
package/dist/hooks/upload.js
CHANGED
|
@@ -177,7 +177,7 @@ const uploadSessionUnlocked = async (sessionDir, options) => {
|
|
|
177
177
|
graphId: sessionGraphId(basename(sessionDir)),
|
|
178
178
|
rev,
|
|
179
179
|
description: `Agent session ${meta.sessionId}`,
|
|
180
|
-
tags: ['session', '
|
|
180
|
+
tags: ['session', `agent:${meta.client ?? 'claude'}`],
|
|
181
181
|
now: now(),
|
|
182
182
|
});
|
|
183
183
|
}
|
package/dist/setupCommand.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ type DoctorReport = {
|
|
|
53
53
|
export type SetupDeps = {
|
|
54
54
|
interactive: boolean;
|
|
55
55
|
installHooks: InstallHooks;
|
|
56
|
+
/** Session saving is installed for this folder only. */
|
|
57
|
+
cwd: string;
|
|
56
58
|
codexHome: string;
|
|
57
59
|
now: () => Date;
|
|
58
60
|
hasWorkingKey: () => Promise<boolean>;
|
|
@@ -108,6 +110,7 @@ export declare const runProcess: (file: string, args: string[]) => Promise<{
|
|
|
108
110
|
export declare const defaultFileDeps: () => {
|
|
109
111
|
platform: NodeJS.Platform;
|
|
110
112
|
home: string;
|
|
113
|
+
cwd: string;
|
|
111
114
|
codexHome: string;
|
|
112
115
|
now: () => Date;
|
|
113
116
|
findBinary: (name: string) => Promise<string | undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupCommand.d.ts","sourceRoot":"","sources":["../src/setupCommand.ts"],"names":[],"mappings":"AAKA,OAAO,EAAC,WAAW,EAAwE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"setupCommand.d.ts","sourceRoot":"","sources":["../src/setupCommand.ts"],"names":[],"mappings":"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"}
|
package/dist/setupCommand.js
CHANGED
|
@@ -4,7 +4,7 @@ import { access, copyFile, mkdir, readFile, stat, writeFile } from 'node:fs/prom
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { delimiter, dirname, join } from 'node:path';
|
|
6
6
|
import { API_KEY_ENV, launchAgentPath, launchAgentPlist, loadKeyIntoLaunchd, readLaunchdKey } from './appEnv.js';
|
|
7
|
-
import {
|
|
7
|
+
import { hookScopeError, sessionSaveDisclosure } from './hooks/install.js';
|
|
8
8
|
import { mcpConfig } from './setup.js';
|
|
9
9
|
export const redactKey = (key) => `ah_…${key.slice(-4)}`;
|
|
10
10
|
const scrub = (text, key) => key ? text.split(key).join(redactKey(key)) : text;
|
|
@@ -116,6 +116,28 @@ export const runSetup = async (options, deps) => {
|
|
|
116
116
|
const say = (line) => deps.log(line);
|
|
117
117
|
const wantClaude = options.client !== 'codex';
|
|
118
118
|
const wantCodex = options.client !== 'claude';
|
|
119
|
+
const hookClients = [...(wantClaude ? ['claude'] : []), ...(wantCodex ? ['codex'] : [])];
|
|
120
|
+
const hookNames = hookClients.map(client => (client === 'claude' ? 'Claude Code' : 'Codex')).join(' and ');
|
|
121
|
+
let hookRefusal;
|
|
122
|
+
for (const client of hookClients) {
|
|
123
|
+
const refused = await hookScopeError(deps.home, { projectDir: deps.cwd }, client, deps.codexHome);
|
|
124
|
+
if (refused) {
|
|
125
|
+
hookRefusal = `${client}: ${refused}`;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// 0. --save-sessions asks for something the home folder cannot have, so stop
|
|
130
|
+
// before login, MCP or app-env changes rather than half-applying setup.
|
|
131
|
+
if (hookRefusal && options.saveSessions && !options.noHook) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
dryRun: options.dryRun,
|
|
135
|
+
profile: '',
|
|
136
|
+
baseUrl: '',
|
|
137
|
+
steps: [{ name: 'hook', status: 'fail', detail: hookRefusal }],
|
|
138
|
+
nextStep: 'Run `assethub setup --save-sessions` inside the project folder whose sessions should be saved, or run it without --save-sessions.',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
119
141
|
// 1. Login (reuses the existing `auth login` path through deps.login)
|
|
120
142
|
const explicitKey = options.apiKeySource === 'flag' ? options.apiKey : undefined;
|
|
121
143
|
const hadKey = explicitKey ? false : await deps.hasWorkingKey();
|
|
@@ -300,51 +322,63 @@ export const runSetup = async (options, deps) => {
|
|
|
300
322
|
else if (options.noHook) {
|
|
301
323
|
steps.push({ name: 'hook', status: 'skip', detail: 'skipped (--no-hook)' });
|
|
302
324
|
}
|
|
303
|
-
else if (
|
|
304
|
-
|
|
325
|
+
else if (hookRefusal) {
|
|
326
|
+
// Not offered: the dry run and the question would promise an install the guard refuses.
|
|
327
|
+
steps.push({ name: 'hook', status: 'skip', detail: hookRefusal });
|
|
305
328
|
}
|
|
306
329
|
else if (options.dryRun) {
|
|
307
330
|
steps.push({
|
|
308
331
|
name: 'hook',
|
|
309
332
|
status: 'skip',
|
|
310
333
|
detail: options.saveSessions
|
|
311
|
-
?
|
|
334
|
+
? `would install the ${hookNames} session-saving hooks for ${deps.cwd} only (dry run)`
|
|
312
335
|
: 'would leave session saving off (enable with --save-sessions)',
|
|
313
336
|
});
|
|
314
337
|
}
|
|
315
338
|
else {
|
|
339
|
+
const hookScope = { projectDir: deps.cwd };
|
|
316
340
|
let consent = options.saveSessions;
|
|
341
|
+
const disclose = () => {
|
|
342
|
+
for (const client of hookClients)
|
|
343
|
+
say(sessionSaveDisclosure(hookScope, client));
|
|
344
|
+
};
|
|
317
345
|
if (!consent && deps.interactive && !options.yes) {
|
|
318
|
-
|
|
319
|
-
consent = await deps.confirmOptIn(
|
|
346
|
+
disclose();
|
|
347
|
+
consent = await deps.confirmOptIn(`Save and upload your ${hookNames} sessions?`);
|
|
320
348
|
}
|
|
321
349
|
if (!consent) {
|
|
322
350
|
steps.push({
|
|
323
351
|
name: 'hook',
|
|
324
352
|
status: 'skip',
|
|
325
|
-
detail:
|
|
353
|
+
detail: `session saving is off; enable it with \`assethub setup --save-sessions\`, or ${hookClients.map(client => `\`assethub hooks install --client ${client}\``).join(' and ')} inside the project folder`,
|
|
326
354
|
});
|
|
327
355
|
}
|
|
328
356
|
else {
|
|
329
357
|
if (options.saveSessions)
|
|
330
|
-
|
|
358
|
+
disclose();
|
|
331
359
|
const pathNote = (await deps.findBinary('assethub'))
|
|
332
360
|
? ''
|
|
333
361
|
: ' Note: the hooks run `assethub`, which is not on PATH; install the CLI globally.';
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
362
|
+
for (const client of hookClients) {
|
|
363
|
+
try {
|
|
364
|
+
const result = await deps.installHooks({
|
|
365
|
+
client,
|
|
366
|
+
...hookScope,
|
|
367
|
+
...(client === 'codex' ? { codexHome: deps.codexHome } : {}),
|
|
368
|
+
});
|
|
369
|
+
steps.push({
|
|
370
|
+
name: 'hook',
|
|
371
|
+
status: result.installed ? 'ok' : 'fail',
|
|
372
|
+
detail: `${client}: ${result.detail}${result.installed ? pathNote : ''}`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
steps.push({
|
|
377
|
+
name: 'hook',
|
|
378
|
+
status: 'fail',
|
|
379
|
+
detail: `${client}: ${scrub(error instanceof Error ? error.message : String(error), key)}`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
348
382
|
}
|
|
349
383
|
}
|
|
350
384
|
}
|
|
@@ -459,6 +493,7 @@ export const runProcess = (file, args) => new Promise(done => {
|
|
|
459
493
|
export const defaultFileDeps = () => ({
|
|
460
494
|
platform: process.platform,
|
|
461
495
|
home: homedir(),
|
|
496
|
+
cwd: process.cwd(),
|
|
462
497
|
codexHome: process.env.CODEX_HOME?.trim() || join(homedir(), '.codex'),
|
|
463
498
|
now: () => new Date(),
|
|
464
499
|
findBinary: (name) => findOnPath(name),
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export type InstallHooks = (opts: {
|
|
2
2
|
client: 'claude' | 'codex';
|
|
3
|
+
/** The project folder whose .claude/settings.local.json or .codex/hooks.json gets the hooks. */
|
|
4
|
+
projectDir?: string;
|
|
5
|
+
/** Codex's user folder ($CODEX_HOME), where Codex records which projects are trusted. */
|
|
6
|
+
codexHome?: string;
|
|
3
7
|
}) => Promise<{
|
|
4
8
|
installed: boolean;
|
|
5
9
|
detail: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setupHooksBridge.d.ts","sourceRoot":"","sources":["../src/setupHooksBridge.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE;IAChC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"setupHooksBridge.d.ts","sourceRoot":"","sources":["../src/setupHooksBridge.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE;IAChC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAA;IAC1B,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yFAAyF;IACzF,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,KAAK,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAAC,CAAA;AAEnD,eAAO,MAAM,mBAAmB,EAAE,YAGhC,CAAA"}
|