@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.
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../src/hooks/command.ts"],"names":[],"mappings":"AASA,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;AA8CD,eAAO,MAAM,eAAe,GAC1B,MAAM,MAAM,EAAE,EACd,OAAM,gBAAqB,KAC1B,OAAO,CAAC,MAAM,CAkIhB,CAAA"}
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"}
@@ -1,11 +1,12 @@
1
1
  // `assethub hooks <subcommand>` — the registration in src/index.ts just calls
2
2
  // runHooksCommand(argv).
3
- import { installHooks, SESSION_SAVE_DISCLOSURE, uninstallHooks } from './install.js';
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
- ' save [--transcript <path> --session-id <id>] (reads hook JSON from stdin without flags)\n' +
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
- if (client === 'claude') {
75
- const auth = await resolveUploadAuth(uploadOptions);
76
- if (!auth || !(await canUploadSessions(auth, deps.fetchImpl))) {
77
- write('Session saving is not available for this account.\n');
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
- client: client,
83
- home: deps.home,
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({ client: client, home: deps.home });
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,CAiLhB,CAAA;AAED,oEAAoE;AACpE,eAAO,MAAM,uBAAuB,GAAU,YAAY,MAAM,KAAG,OAAO,CAAC,IAAI,CAE9E,CAAA"}
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"}
@@ -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', 'agent:claude'],
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', 'agent:claude'],
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', 'agent:claude', `tool:${item.toolName}`],
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', 'agent:claude'],
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', 'agent:claude'],
220
+ tags: ['session', agentTag],
219
221
  metadata: {
220
222
  sessionId: meta.sessionId, client: meta.client, lastEvent: meta.lastEvent,
221
223
  messageCount: messageNodes.length, skippedImages,
@@ -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 SESSION_SAVE_DISCLOSURE: string;
20
+ export declare const sessionSaveDisclosure: (scope: HookScope, client?: HookClient) => string;
12
21
  export declare const isOurCommand: (command: unknown) => boolean;
13
- export declare const hookCommand: (cliPath?: string) => string;
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 the agent.
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":"AAUA,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,mFAAmF;AACnF,eAAO,MAAM,uBAAuB,QAQ0B,CAAA;AAa9D,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,OACa,CAAA;AAI7D,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,KAAG,MACO,CAAA;AAiDtD,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;;;;;GAKG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,CAAC,OAAO,WAAW,EAAE,MAAM,CAAC,EAAE,SAAS,MAAM,KAAG,SAGL,CAAA;AAE5E,eAAO,MAAM,YAAY,GAAU,SAAS;IAC1C,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,KAAG,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAuC/C,CAAA;AAED,eAAO,MAAM,cAAc,GAAU,SAAS;IAC5C,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,KAAG,OAAO,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAwC7C,CAAA"}
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"}
@@ -1,10 +1,18 @@
1
- // Register / remove our Claude Code hooks in the user's settings.json.
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 { copyFile, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
7
- import { basename, dirname, join } from 'node:path';
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 SESSION_SAVE_DISCLOSURE = 'Session saving adds SessionStart, Stop, PreCompact and SessionEnd hooks to ~/.claude/settings.json. ' +
19
- 'Every Claude Code session on this machine, in any project, is then copied to ~/.assethub/sessions/ when it ends ' +
20
- '(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), ' +
21
- 'and uploaded to AssetHub as a coding-agent session only you can read (internal accounts only for now): the masked transcript, ' +
22
- 'your prompts, the agent\'s replies, its tool calls and images from the project. Uploads that fail are retried in the background ' +
23
- 'when a later session starts or ends. Secret masking is best effort. ' +
24
- 'Opt a project out with .assethub/no-session-save, or turn it off with ASSETHUB_SESSION_SAVE=off / ASSETHUB_SESSION_UPLOAD=off; ' +
25
- 'remove it with `assethub hooks uninstall --client claude`.';
26
- const CODEX_DETAIL = 'Codex: run `assethub hooks save --transcript <path>` manually for now';
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
- export const hookCommand = (cliPath) => `${quoteIfNeeded(cliPath ?? 'assethub')} hooks save`;
34
- const settingsPath = (home) => join(resolveHome(home), '.claude', 'settings.json');
35
- const load = async (home) => {
36
- const path = settingsPath(home);
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 BACKUP_RE = /^settings\.json\.bak-\d+$/;
162
+ const escapeRe = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
60
163
  const pruneBackups = async (path) => {
61
164
  const dir = dirname(path);
62
- const ours = (await readdir(dir)).filter(name => BACKUP_RE.test(name));
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 the agent.
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
- : { type: 'command', command, timeout: HOOK_TIMEOUT_SECONDS, async: true };
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
- if (options.client === 'codex')
90
- return { installed: false, detail: CODEX_DETAIL };
91
- const loaded = await load(options.home);
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
- const command = hookCommand(options.cliPath);
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
- if (options.client === 'codex') {
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: 'No assethub hooks found.' };
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: 'No assethub hooks found.' };
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);
@@ -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;
@@ -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;AACpE,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,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,CAiF7F,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"}
@@ -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: 'claude',
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: 'claude',
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;AAID,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,KAAG,MAAM,EAGtD,CAAA;AAgBD,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,KAAG,gBAiD7C,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"}
@@ -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
- const IMAGE_PATH_RE = /(?<![\w.~-])(?:\/[^\s"'\\<>|:*?`()[\]{}]+)+\.(?:png|jpe?g|webp)\b/gi;
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)];
@@ -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
- * Trim a JSONL transcript. A file that is not a Claude Code transcript (for
11
- * example a Codex rollout saved by hand) is returned unchanged.
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 {};
@@ -1 +1 @@
1
- {"version":3,"file":"trim.d.ts","sourceRoot":"","sources":["../../src/hooks/trim.ts"],"names":[],"mappings":"AASA;;;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;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,MAc5C,CAAA"}
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"}
@@ -1,4 +1,5 @@
1
- // Keep only the conversation from a Claude Code transcript before it is saved
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. A file that is not a Claude Code transcript (for
90
- * example a Codex rollout saved by hand) is returned unchanged.
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
- if (!entries.some(entry => isObject(entry) && CLAUDE_LINE_TYPES.has(String(entry.type))))
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
  };
@@ -15,7 +15,7 @@ export type SessionImage = {
15
15
  export type SessionMeta = {
16
16
  sessionId: string;
17
17
  cwd: string;
18
- client: 'claude';
18
+ client: HookClient;
19
19
  lastEvent: string;
20
20
  savedAt: string;
21
21
  canvasId?: string;
@@ -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,QAAQ,CAAA;IAChB,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"}
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"}
@@ -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', 'agent:claude'],
180
+ tags: ['session', `agent:${meta.client ?? 'claude'}`],
181
181
  now: now(),
182
182
  });
183
183
  }
@@ -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":"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"}
1
+ {"version":3,"file":"setupCommand.d.ts","sourceRoot":"","sources":["../src/setupCommand.ts"],"names":[],"mappings":"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"}
@@ -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 { SESSION_SAVE_DISCLOSURE } from './hooks/install.js';
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 (!wantClaude) {
304
- steps.push({ name: 'hook', status: 'skip', detail: 'Codex session saving is not supported yet' });
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
- ? 'would install the session-saving hooks (dry run)'
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
- say(SESSION_SAVE_DISCLOSURE);
319
- consent = await deps.confirmOptIn('Save and upload your Claude Code sessions?');
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: 'session saving is off; enable it with `assethub setup --save-sessions` or `assethub hooks install --client claude`',
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
- say(SESSION_SAVE_DISCLOSURE);
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
- try {
335
- const result = await deps.installHooks({ client: 'claude' });
336
- steps.push({
337
- name: 'hook',
338
- status: result.installed ? 'ok' : 'fail',
339
- detail: `claude: ${result.detail}${result.installed ? pathNote : ''}`,
340
- });
341
- }
342
- catch (error) {
343
- steps.push({
344
- name: 'hook',
345
- status: 'fail',
346
- detail: `claude: ${scrub(error instanceof Error ? error.message : String(error), key)}`,
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;CAC3B,KAAK,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,CAAC,CAAA;AAEnD,eAAO,MAAM,mBAAmB,EAAE,YAGhC,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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assethub/cli",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "Command line interface for AssetHub.",
5
5
  "type": "module",
6
6
  "bin": {