@respan/cli 0.7.0 → 0.7.2
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/commands/integrate/claude-code.d.ts +2 -0
- package/dist/commands/integrate/claude-code.js +35 -5
- package/dist/commands/integrate/codex-cli.d.ts +2 -0
- package/dist/commands/integrate/codex-cli.js +23 -2
- package/dist/commands/integrate/gemini-cli.d.ts +2 -0
- package/dist/commands/integrate/gemini-cli.js +36 -4
- package/dist/commands/integrate/opencode.d.ts +2 -0
- package/dist/commands/integrate/opencode.js +25 -2
- package/dist/hooks/claude-code.cjs +3 -1
- package/dist/hooks/claude-code.js +1 -0
- package/dist/hooks/codex-cli.cjs +73 -49
- package/dist/hooks/codex-cli.js +77 -57
- package/dist/hooks/gemini-cli.cjs +33 -55
- package/dist/hooks/gemini-cli.js +34 -61
- package/dist/hooks/shared.d.ts +1 -0
- package/dist/hooks/shared.js +3 -1
- package/dist/lib/integrate.d.ts +2 -0
- package/dist/lib/integrate.js +10 -0
- package/oclif.manifest.json +588 -512
- package/package.json +10 -2
|
@@ -5,6 +5,8 @@ export default class IntegrateClaudeCode extends BaseCommand {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
enable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
disable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
10
|
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
11
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
attrs: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -7,6 +7,37 @@ class IntegrateClaudeCode extends BaseCommand {
|
|
|
7
7
|
const { flags } = await this.parse(IntegrateClaudeCode);
|
|
8
8
|
this.globalFlags = flags;
|
|
9
9
|
try {
|
|
10
|
+
const dryRun = flags['dry-run'];
|
|
11
|
+
const scope = resolveScope(flags, 'both');
|
|
12
|
+
const doLocal = scope === 'local' || scope === 'both';
|
|
13
|
+
// ── Disable mode ─────────────────────────────────────────────
|
|
14
|
+
if (flags.disable) {
|
|
15
|
+
// Remove the Stop hook entry from global settings
|
|
16
|
+
const globalSettingsPath = expandHome('~/.claude/settings.json');
|
|
17
|
+
const globalSettings = readJsonFile(globalSettingsPath);
|
|
18
|
+
const hooksSection = (globalSettings.hooks || {});
|
|
19
|
+
if (Array.isArray(hooksSection.Stop)) {
|
|
20
|
+
hooksSection.Stop = hooksSection.Stop.filter((entry) => {
|
|
21
|
+
const inner = Array.isArray(entry.hooks)
|
|
22
|
+
? entry.hooks
|
|
23
|
+
: [];
|
|
24
|
+
return !inner.some((h) => typeof h.command === 'string' &&
|
|
25
|
+
(h.command.includes('respan') || h.command.includes('hook.py') || h.command.includes('claude-code')));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const merged = deepMerge(globalSettings, { hooks: hooksSection });
|
|
29
|
+
if (dryRun) {
|
|
30
|
+
this.log(`[dry-run] Would update: ${globalSettingsPath}`);
|
|
31
|
+
this.log(JSON.stringify(merged, null, 2));
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
writeJsonFile(globalSettingsPath, merged);
|
|
35
|
+
this.log(`Removed hook entry: ${globalSettingsPath}`);
|
|
36
|
+
}
|
|
37
|
+
this.log('Claude Code tracing disabled. Run "respan integrate claude-code" to re-enable.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// ── Enable mode (default) ────────────────────────────────────
|
|
10
41
|
// Verify the user is authenticated (key is read by hook from ~/.respan/)
|
|
11
42
|
this.resolveApiKey();
|
|
12
43
|
const baseUrl = flags['base-url'];
|
|
@@ -15,11 +46,8 @@ class IntegrateClaudeCode extends BaseCommand {
|
|
|
15
46
|
const spanName = flags['span-name'];
|
|
16
47
|
const workflowName = flags['workflow-name'];
|
|
17
48
|
const attrs = parseAttrs(flags.attrs);
|
|
18
|
-
|
|
19
|
-
// Claude Code default: both global + local
|
|
20
|
-
const scope = resolveScope(flags, 'both');
|
|
49
|
+
// Claude Code default: both global + local.
|
|
21
50
|
const doGlobal = scope === 'global' || scope === 'both';
|
|
22
|
-
const doLocal = scope === 'local' || scope === 'both';
|
|
23
51
|
// ── Global: hook script + registration ────────────────────────
|
|
24
52
|
if (doGlobal) {
|
|
25
53
|
// 1. Write JS hook script (no Python dependency needed)
|
|
@@ -165,9 +193,11 @@ them to Respan as structured spans (chat, tool, thinking).
|
|
|
165
193
|
Scope:
|
|
166
194
|
--global Install hook script + register in ~/.claude/settings.json
|
|
167
195
|
--local Write credentials + enable flag to .claude/settings.local.json
|
|
168
|
-
|
|
196
|
+
|
|
197
|
+
Default behavior installs the global hook and writes local project config.`;
|
|
169
198
|
IntegrateClaudeCode.examples = [
|
|
170
199
|
'respan integrate claude-code',
|
|
200
|
+
'respan integrate claude-code --disable',
|
|
171
201
|
'respan integrate claude-code --global',
|
|
172
202
|
'respan integrate claude-code --local --project-id my-project',
|
|
173
203
|
'respan integrate claude-code --attrs \'{"env":"prod"}\'',
|
|
@@ -5,6 +5,8 @@ export default class IntegrateCodexCli extends BaseCommand {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
enable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
disable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
10
|
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
11
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
attrs: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -6,6 +6,26 @@ class IntegrateCodexCli extends BaseCommand {
|
|
|
6
6
|
const { flags } = await this.parse(IntegrateCodexCli);
|
|
7
7
|
this.globalFlags = flags;
|
|
8
8
|
try {
|
|
9
|
+
const dryRun = flags['dry-run'];
|
|
10
|
+
// ── Disable mode ─────────────────────────────────────────────
|
|
11
|
+
if (flags.disable) {
|
|
12
|
+
// Remove the notify line from ~/.codex/config.toml
|
|
13
|
+
const configPath = expandHome('~/.codex/config.toml');
|
|
14
|
+
const existing = readTextFile(configPath);
|
|
15
|
+
const lines = existing.split('\n');
|
|
16
|
+
const filtered = lines.filter((line) => !/^\s*notify\s*=/.test(line) && !line.includes('respan integrate codex-cli'));
|
|
17
|
+
if (dryRun) {
|
|
18
|
+
this.log(`[dry-run] Would update: ${configPath}`);
|
|
19
|
+
this.log(filtered.join('\n'));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
writeTextFile(configPath, filtered.join('\n'));
|
|
23
|
+
this.log(`Removed notify hook: ${configPath}`);
|
|
24
|
+
}
|
|
25
|
+
this.log('Codex CLI tracing disabled. Run "respan integrate codex-cli" to re-enable.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// ── Enable mode (default) ────────────────────────────────────
|
|
9
29
|
// Verify the user is authenticated (key is read by hook from ~/.respan/)
|
|
10
30
|
this.resolveApiKey();
|
|
11
31
|
const projectId = flags['project-id'];
|
|
@@ -13,7 +33,6 @@ class IntegrateCodexCli extends BaseCommand {
|
|
|
13
33
|
const spanName = flags['span-name'];
|
|
14
34
|
const workflowName = flags['workflow-name'];
|
|
15
35
|
const attrs = parseAttrs(flags.attrs);
|
|
16
|
-
const dryRun = flags['dry-run'];
|
|
17
36
|
// Codex CLI default: both global + local
|
|
18
37
|
const scope = resolveScope(flags, 'both');
|
|
19
38
|
const doGlobal = scope === 'global' || scope === 'both';
|
|
@@ -155,9 +174,11 @@ them to Respan as structured spans (chat, tool, reasoning).
|
|
|
155
174
|
Scope:
|
|
156
175
|
--global Install hook script + register notify in ~/.codex/config.toml
|
|
157
176
|
--local Write .codex/respan.json with customer_id, span_name, etc.
|
|
158
|
-
|
|
177
|
+
|
|
178
|
+
Default behavior installs the global notify hook and writes local project config.`;
|
|
159
179
|
IntegrateCodexCli.examples = [
|
|
160
180
|
'respan integrate codex-cli',
|
|
181
|
+
'respan integrate codex-cli --disable',
|
|
161
182
|
'respan integrate codex-cli --global',
|
|
162
183
|
'respan integrate codex-cli --local --customer-id frank',
|
|
163
184
|
'respan integrate codex-cli --attrs \'{"env":"prod"}\'',
|
|
@@ -5,6 +5,8 @@ export default class IntegrateGeminiCli extends BaseCommand {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
enable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
disable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
10
|
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
11
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
attrs: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -7,6 +7,40 @@ class IntegrateGeminiCli extends BaseCommand {
|
|
|
7
7
|
const { flags } = await this.parse(IntegrateGeminiCli);
|
|
8
8
|
this.globalFlags = flags;
|
|
9
9
|
try {
|
|
10
|
+
const dryRun = flags['dry-run'];
|
|
11
|
+
const scope = resolveScope(flags, 'global');
|
|
12
|
+
// ── Disable mode ─────────────────────────────────────────────
|
|
13
|
+
if (flags.disable) {
|
|
14
|
+
// Remove respan hook entries from settings.json
|
|
15
|
+
const settingsPath = scope === 'global'
|
|
16
|
+
? expandHome('~/.gemini/settings.json')
|
|
17
|
+
: path.join(findProjectRoot(), '.gemini', 'settings.json');
|
|
18
|
+
const existing = readJsonFile(settingsPath);
|
|
19
|
+
const hooksSection = (existing.hooks || {});
|
|
20
|
+
for (const eventName of ['AfterModel', 'BeforeTool', 'AfterTool']) {
|
|
21
|
+
if (!Array.isArray(hooksSection[eventName]))
|
|
22
|
+
continue;
|
|
23
|
+
hooksSection[eventName] = hooksSection[eventName].filter((entry) => {
|
|
24
|
+
const inner = Array.isArray(entry.hooks)
|
|
25
|
+
? entry.hooks
|
|
26
|
+
: [];
|
|
27
|
+
return !inner.some((h) => typeof h.command === 'string' &&
|
|
28
|
+
(h.command.includes('respan') || h.command.includes('gemini-cli')));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const merged = { ...existing, hooks: hooksSection };
|
|
32
|
+
if (dryRun) {
|
|
33
|
+
this.log(`[dry-run] Would update: ${settingsPath}`);
|
|
34
|
+
this.log(JSON.stringify(merged, null, 2));
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
writeJsonFile(settingsPath, merged);
|
|
38
|
+
this.log(`Removed hook entries: ${settingsPath}`);
|
|
39
|
+
}
|
|
40
|
+
this.log('Gemini CLI tracing disabled. Run "respan integrate gemini-cli" to re-enable.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// ── Enable mode (default) ────────────────────────────────────
|
|
10
44
|
// Verify the user is authenticated (key is read by hook from ~/.respan/)
|
|
11
45
|
this.resolveApiKey();
|
|
12
46
|
const projectId = flags['project-id'];
|
|
@@ -14,8 +48,6 @@ class IntegrateGeminiCli extends BaseCommand {
|
|
|
14
48
|
const spanName = flags['span-name'];
|
|
15
49
|
const workflowName = flags['workflow-name'];
|
|
16
50
|
const attrs = parseAttrs(flags.attrs);
|
|
17
|
-
const dryRun = flags['dry-run'];
|
|
18
|
-
const scope = resolveScope(flags, 'global');
|
|
19
51
|
// ── 1. Install hook script ──────────────────────────────────
|
|
20
52
|
const hookDir = expandHome('~/.respan/hooks');
|
|
21
53
|
const hookPath = `${hookDir}/gemini-cli.cjs`;
|
|
@@ -142,10 +174,10 @@ Scope:
|
|
|
142
174
|
--global Write to ~/.gemini/settings.json (default)
|
|
143
175
|
--local Write to .gemini/settings.json in project root
|
|
144
176
|
|
|
145
|
-
|
|
146
|
-
--global is the default.`;
|
|
177
|
+
Gemini CLI ignores workspace-level telemetry settings, so --global is the default.`;
|
|
147
178
|
IntegrateGeminiCli.examples = [
|
|
148
179
|
'respan integrate gemini-cli',
|
|
180
|
+
'respan integrate gemini-cli --disable',
|
|
149
181
|
'respan integrate gemini-cli --local',
|
|
150
182
|
'respan integrate gemini-cli --project-id my-project --attrs \'{"env":"prod"}\'',
|
|
151
183
|
'respan integrate gemini-cli --dry-run',
|
|
@@ -5,6 +5,8 @@ export default class IntegrateOpencode extends BaseCommand {
|
|
|
5
5
|
static flags: {
|
|
6
6
|
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
7
|
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
enable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
disable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
10
|
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
11
|
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
12
|
attrs: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
1
2
|
import * as path from 'node:path';
|
|
2
3
|
import { execSync } from 'node:child_process';
|
|
3
4
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
@@ -7,12 +8,33 @@ class IntegrateOpencode extends BaseCommand {
|
|
|
7
8
|
const { flags } = await this.parse(IntegrateOpencode);
|
|
8
9
|
this.globalFlags = flags;
|
|
9
10
|
try {
|
|
11
|
+
const dryRun = flags['dry-run'];
|
|
12
|
+
const scope = resolveScope(flags, 'local');
|
|
13
|
+
// ── Disable mode ─────────────────────────────────────────────
|
|
14
|
+
if (flags.disable) {
|
|
15
|
+
const pluginPath = scope === 'global'
|
|
16
|
+
? expandHome('~/.config/opencode/plugins/otel.json')
|
|
17
|
+
: path.join(findProjectRoot(), '.opencode', 'plugins', 'otel.json');
|
|
18
|
+
if (dryRun) {
|
|
19
|
+
this.log(`[dry-run] Would remove: ${pluginPath}`);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
try {
|
|
23
|
+
fs.unlinkSync(pluginPath);
|
|
24
|
+
this.log(`Removed plugin config: ${pluginPath}`);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
this.log(`No plugin config found at: ${pluginPath}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
this.log('OpenCode tracing disabled. Run with --enable to re-enable.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
// ── Enable mode (default) ────────────────────────────────────
|
|
10
34
|
const apiKey = this.resolveApiKey();
|
|
11
35
|
const baseUrl = (flags['base-url']).replace(/\/+$/, '');
|
|
12
36
|
const projectId = flags['project-id'];
|
|
13
37
|
const attrs = parseAttrs(flags.attrs);
|
|
14
|
-
const dryRun = flags['dry-run'];
|
|
15
|
-
const scope = resolveScope(flags, 'local');
|
|
16
38
|
// ── 1. Install opencode-otel (always global) ─────────────────
|
|
17
39
|
if (dryRun) {
|
|
18
40
|
this.log('[dry-run] Would run: npm install -g opencode-otel');
|
|
@@ -89,6 +111,7 @@ Scope:
|
|
|
89
111
|
The opencode-otel package is always installed globally.`;
|
|
90
112
|
IntegrateOpencode.examples = [
|
|
91
113
|
'respan integrate opencode',
|
|
114
|
+
'respan integrate opencode --disable',
|
|
92
115
|
'respan integrate opencode --global',
|
|
93
116
|
'respan integrate opencode --project-id my-project --attrs \'{"env":"prod"}\'',
|
|
94
117
|
'respan integrate opencode --dry-run',
|
|
@@ -280,6 +280,7 @@ function stringToSpanId(s) {
|
|
|
280
280
|
function toOtlpPayload(spans) {
|
|
281
281
|
const otlpSpans = spans.map((span) => {
|
|
282
282
|
const attrs = {};
|
|
283
|
+
if (span.session_identifier) attrs["respan.sessions.session_identifier"] = span.session_identifier;
|
|
283
284
|
if (span.thread_identifier) attrs["respan.threads.thread_identifier"] = span.thread_identifier;
|
|
284
285
|
if (span.customer_identifier) attrs["respan.customer_params.customer_identifier"] = span.customer_identifier;
|
|
285
286
|
if (span.span_workflow_name) attrs["traceloop.workflow.name"] = span.span_workflow_name;
|
|
@@ -340,7 +341,7 @@ function toOtlpPayload(spans) {
|
|
|
340
341
|
})
|
|
341
342
|
},
|
|
342
343
|
scopeSpans: [{
|
|
343
|
-
scope: { name: "respan-cli-hooks", version: "0.7.
|
|
344
|
+
scope: { name: "respan-cli-hooks", version: "0.7.1" },
|
|
344
345
|
spans: otlpSpans
|
|
345
346
|
}]
|
|
346
347
|
}]
|
|
@@ -599,6 +600,7 @@ function createSpans(sessionId, turnNum, userMsg, assistantMsgs, toolResults, co
|
|
|
599
600
|
const rootSpanId = `claudecode_${traceUniqueId}_root`;
|
|
600
601
|
spans.push({
|
|
601
602
|
trace_unique_id: traceUniqueId,
|
|
603
|
+
session_identifier: threadId,
|
|
602
604
|
thread_identifier: threadId,
|
|
603
605
|
customer_identifier: customerId,
|
|
604
606
|
span_unique_id: rootSpanId,
|
|
@@ -248,6 +248,7 @@ function createSpans(sessionId, turnNum, userMsg, assistantMsgs, toolResults, co
|
|
|
248
248
|
const rootSpanId = `claudecode_${traceUniqueId}_root`;
|
|
249
249
|
spans.push({
|
|
250
250
|
trace_unique_id: traceUniqueId,
|
|
251
|
+
session_identifier: threadId,
|
|
251
252
|
thread_identifier: threadId,
|
|
252
253
|
customer_identifier: customerId,
|
|
253
254
|
span_unique_id: rootSpanId,
|
package/dist/hooks/codex-cli.cjs
CHANGED
|
@@ -26,6 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
var fs2 = __toESM(require("node:fs"), 1);
|
|
27
27
|
var os2 = __toESM(require("node:os"), 1);
|
|
28
28
|
var path2 = __toESM(require("node:path"), 1);
|
|
29
|
+
var import_node_child_process = require("node:child_process");
|
|
29
30
|
|
|
30
31
|
// src/hooks/shared.ts
|
|
31
32
|
var fs = __toESM(require("node:fs"), 1);
|
|
@@ -279,6 +280,7 @@ function stringToSpanId(s) {
|
|
|
279
280
|
function toOtlpPayload(spans) {
|
|
280
281
|
const otlpSpans = spans.map((span) => {
|
|
281
282
|
const attrs = {};
|
|
283
|
+
if (span.session_identifier) attrs["respan.sessions.session_identifier"] = span.session_identifier;
|
|
282
284
|
if (span.thread_identifier) attrs["respan.threads.thread_identifier"] = span.thread_identifier;
|
|
283
285
|
if (span.customer_identifier) attrs["respan.customer_params.customer_identifier"] = span.customer_identifier;
|
|
284
286
|
if (span.span_workflow_name) attrs["traceloop.workflow.name"] = span.span_workflow_name;
|
|
@@ -339,7 +341,7 @@ function toOtlpPayload(spans) {
|
|
|
339
341
|
})
|
|
340
342
|
},
|
|
341
343
|
scopeSpans: [{
|
|
342
|
-
scope: { name: "respan-cli-hooks", version: "0.7.
|
|
344
|
+
scope: { name: "respan-cli-hooks", version: "0.7.1" },
|
|
343
345
|
spans: otlpSpans
|
|
344
346
|
}]
|
|
345
347
|
}]
|
|
@@ -572,6 +574,7 @@ function createSpans(sessionId, turnNum, turn, config) {
|
|
|
572
574
|
const rootSpanId = `codexcli_${traceUniqueId}_root`;
|
|
573
575
|
spans.push({
|
|
574
576
|
trace_unique_id: traceUniqueId,
|
|
577
|
+
session_identifier: threadId,
|
|
575
578
|
thread_identifier: threadId,
|
|
576
579
|
customer_identifier: customerId,
|
|
577
580
|
span_unique_id: rootSpanId,
|
|
@@ -699,50 +702,18 @@ function findLatestSessionFile() {
|
|
|
699
702
|
return null;
|
|
700
703
|
}
|
|
701
704
|
}
|
|
702
|
-
async function
|
|
705
|
+
async function mainWorker() {
|
|
703
706
|
const scriptStart = Date.now();
|
|
704
|
-
debug("
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
let payload;
|
|
710
|
-
try {
|
|
711
|
-
payload = JSON.parse(process.argv[2]);
|
|
712
|
-
} catch (e) {
|
|
713
|
-
debug(`Invalid JSON in argv[2]: ${e}`);
|
|
714
|
-
process.exit(0);
|
|
715
|
-
}
|
|
716
|
-
const eventType = String(payload.type ?? "");
|
|
717
|
-
if (eventType !== "agent-turn-complete") {
|
|
718
|
-
debug(`Ignoring event type: ${eventType}`);
|
|
719
|
-
process.exit(0);
|
|
720
|
-
}
|
|
721
|
-
let sessionId = String(payload["thread-id"] ?? "");
|
|
722
|
-
if (!sessionId) {
|
|
723
|
-
debug("No thread-id in notify payload");
|
|
724
|
-
process.exit(0);
|
|
725
|
-
}
|
|
726
|
-
debug(`Processing notify: type=${eventType}, session=${sessionId}`);
|
|
707
|
+
debug("Worker started");
|
|
708
|
+
const sessionId = process.env._RESPAN_CODEX_SESSION;
|
|
709
|
+
const sessionFile = process.env._RESPAN_CODEX_FILE;
|
|
710
|
+
const cwd = process.env._RESPAN_CODEX_CWD ?? "";
|
|
727
711
|
const creds = resolveCredentials();
|
|
728
712
|
if (!creds) {
|
|
729
|
-
log("ERROR", "No API key
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
let sessionFile = findSessionFile(sessionId);
|
|
733
|
-
if (!sessionFile) {
|
|
734
|
-
const latest = findLatestSessionFile();
|
|
735
|
-
if (latest) {
|
|
736
|
-
sessionId = latest.sessionId;
|
|
737
|
-
sessionFile = latest.sessionFile;
|
|
738
|
-
} else {
|
|
739
|
-
debug("No session file found");
|
|
740
|
-
process.exit(0);
|
|
741
|
-
}
|
|
713
|
+
log("ERROR", "No API key");
|
|
714
|
+
return;
|
|
742
715
|
}
|
|
743
|
-
const cwd = String(payload.cwd ?? "");
|
|
744
716
|
const config = cwd ? loadRespanConfig(path2.join(cwd, ".codex", "respan.json")) : null;
|
|
745
|
-
if (config) debug(`Loaded respan.json config from ${cwd}`);
|
|
746
717
|
const maxAttempts = 3;
|
|
747
718
|
let turns = 0;
|
|
748
719
|
try {
|
|
@@ -774,20 +745,73 @@ async function main() {
|
|
|
774
745
|
}
|
|
775
746
|
if (turns > 0) break;
|
|
776
747
|
if (attempt < maxAttempts - 1) {
|
|
777
|
-
|
|
778
|
-
debug(`No turns processed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay}ms...`);
|
|
779
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
748
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
780
749
|
}
|
|
781
750
|
}
|
|
782
751
|
const duration = (Date.now() - scriptStart) / 1e3;
|
|
783
752
|
log("INFO", `Processed ${turns} turns in ${duration.toFixed(1)}s`);
|
|
784
|
-
if (duration > 180) log("WARN", `Hook took ${duration.toFixed(1)}s (>3min)`);
|
|
785
753
|
} catch (e) {
|
|
786
754
|
log("ERROR", `Failed to process session: ${e}`);
|
|
787
|
-
if (DEBUG_MODE) debug(String(e.stack ?? e));
|
|
788
755
|
}
|
|
789
756
|
}
|
|
790
|
-
main()
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
757
|
+
function main() {
|
|
758
|
+
if (process.env._RESPAN_CODEX_WORKER === "1") {
|
|
759
|
+
mainWorker().catch((e) => log("ERROR", `Worker crashed: ${e}`));
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
debug("Codex hook started");
|
|
763
|
+
if (process.argv.length < 3) {
|
|
764
|
+
debug("No argument provided");
|
|
765
|
+
process.exit(0);
|
|
766
|
+
}
|
|
767
|
+
let payload;
|
|
768
|
+
try {
|
|
769
|
+
payload = JSON.parse(process.argv[2]);
|
|
770
|
+
} catch (e) {
|
|
771
|
+
debug(`Invalid JSON in argv[2]: ${e}`);
|
|
772
|
+
process.exit(0);
|
|
773
|
+
}
|
|
774
|
+
const eventType = String(payload.type ?? "");
|
|
775
|
+
if (eventType !== "agent-turn-complete") {
|
|
776
|
+
debug(`Ignoring event type: ${eventType}`);
|
|
777
|
+
process.exit(0);
|
|
778
|
+
}
|
|
779
|
+
let sessionId = String(payload["thread-id"] ?? "");
|
|
780
|
+
if (!sessionId) {
|
|
781
|
+
debug("No thread-id in notify payload");
|
|
782
|
+
process.exit(0);
|
|
783
|
+
}
|
|
784
|
+
let sessionFile = findSessionFile(sessionId);
|
|
785
|
+
if (!sessionFile) {
|
|
786
|
+
const latest = findLatestSessionFile();
|
|
787
|
+
if (latest) {
|
|
788
|
+
sessionId = latest.sessionId;
|
|
789
|
+
sessionFile = latest.sessionFile;
|
|
790
|
+
} else {
|
|
791
|
+
debug("No session file found");
|
|
792
|
+
process.exit(0);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const cwd = String(payload.cwd ?? "");
|
|
796
|
+
debug(`Forking worker for session: ${sessionId}`);
|
|
797
|
+
try {
|
|
798
|
+
const scriptPath = __filename || process.argv[1];
|
|
799
|
+
const child = (0, import_node_child_process.execFile)("node", [scriptPath], {
|
|
800
|
+
env: {
|
|
801
|
+
...process.env,
|
|
802
|
+
_RESPAN_CODEX_WORKER: "1",
|
|
803
|
+
_RESPAN_CODEX_SESSION: sessionId,
|
|
804
|
+
_RESPAN_CODEX_FILE: sessionFile,
|
|
805
|
+
_RESPAN_CODEX_CWD: cwd
|
|
806
|
+
},
|
|
807
|
+
stdio: "ignore",
|
|
808
|
+
detached: true
|
|
809
|
+
});
|
|
810
|
+
child.unref();
|
|
811
|
+
debug("Worker launched");
|
|
812
|
+
} catch (e) {
|
|
813
|
+
log("ERROR", `Failed to fork worker: ${e}`);
|
|
814
|
+
}
|
|
815
|
+
process.exit(0);
|
|
816
|
+
}
|
|
817
|
+
main();
|
package/dist/hooks/codex-cli.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import * as fs from 'node:fs';
|
|
17
17
|
import * as os from 'node:os';
|
|
18
18
|
import * as path from 'node:path';
|
|
19
|
+
import { execFile } from 'node:child_process';
|
|
19
20
|
import { initLogging, log, debug, resolveCredentials, loadRespanConfig, loadState, saveState, acquireLock, sendSpans, addDefaultsToAll, resolveSpanFields, buildMetadata, nowISO, latencySeconds, truncate, } from './shared.js';
|
|
20
21
|
// ── Config ────────────────────────────────────────────────────────
|
|
21
22
|
const STATE_DIR = path.join(os.homedir(), '.codex', 'state');
|
|
@@ -221,6 +222,7 @@ function createSpans(sessionId, turnNum, turn, config) {
|
|
|
221
222
|
const rootSpanId = `codexcli_${traceUniqueId}_root`;
|
|
222
223
|
spans.push({
|
|
223
224
|
trace_unique_id: traceUniqueId,
|
|
225
|
+
session_identifier: threadId,
|
|
224
226
|
thread_identifier: threadId,
|
|
225
227
|
customer_identifier: customerId,
|
|
226
228
|
span_unique_id: rootSpanId,
|
|
@@ -363,57 +365,18 @@ function findLatestSessionFile() {
|
|
|
363
365
|
}
|
|
364
366
|
}
|
|
365
367
|
// ── Main ──────────────────────────────────────────────────────────
|
|
366
|
-
async function
|
|
368
|
+
async function mainWorker() {
|
|
367
369
|
const scriptStart = Date.now();
|
|
368
|
-
debug('
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
process.exit(0);
|
|
373
|
-
}
|
|
374
|
-
let payload;
|
|
375
|
-
try {
|
|
376
|
-
payload = JSON.parse(process.argv[2]);
|
|
377
|
-
}
|
|
378
|
-
catch (e) {
|
|
379
|
-
debug(`Invalid JSON in argv[2]: ${e}`);
|
|
380
|
-
process.exit(0);
|
|
381
|
-
}
|
|
382
|
-
const eventType = String(payload.type ?? '');
|
|
383
|
-
if (eventType !== 'agent-turn-complete') {
|
|
384
|
-
debug(`Ignoring event type: ${eventType}`);
|
|
385
|
-
process.exit(0);
|
|
386
|
-
}
|
|
387
|
-
let sessionId = String(payload['thread-id'] ?? '');
|
|
388
|
-
if (!sessionId) {
|
|
389
|
-
debug('No thread-id in notify payload');
|
|
390
|
-
process.exit(0);
|
|
391
|
-
}
|
|
392
|
-
debug(`Processing notify: type=${eventType}, session=${sessionId}`);
|
|
370
|
+
debug('Worker started');
|
|
371
|
+
const sessionId = process.env._RESPAN_CODEX_SESSION;
|
|
372
|
+
const sessionFile = process.env._RESPAN_CODEX_FILE;
|
|
373
|
+
const cwd = process.env._RESPAN_CODEX_CWD ?? '';
|
|
393
374
|
const creds = resolveCredentials();
|
|
394
375
|
if (!creds) {
|
|
395
|
-
log('ERROR', 'No API key
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
// Find session file
|
|
399
|
-
let sessionFile = findSessionFile(sessionId);
|
|
400
|
-
if (!sessionFile) {
|
|
401
|
-
const latest = findLatestSessionFile();
|
|
402
|
-
if (latest) {
|
|
403
|
-
sessionId = latest.sessionId;
|
|
404
|
-
sessionFile = latest.sessionFile;
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
debug('No session file found');
|
|
408
|
-
process.exit(0);
|
|
409
|
-
}
|
|
376
|
+
log('ERROR', 'No API key');
|
|
377
|
+
return;
|
|
410
378
|
}
|
|
411
|
-
// Load config
|
|
412
|
-
const cwd = String(payload.cwd ?? '');
|
|
413
379
|
const config = cwd ? loadRespanConfig(path.join(cwd, '.codex', 'respan.json')) : null;
|
|
414
|
-
if (config)
|
|
415
|
-
debug(`Loaded respan.json config from ${cwd}`);
|
|
416
|
-
// Process with retry
|
|
417
380
|
const maxAttempts = 3;
|
|
418
381
|
let turns = 0;
|
|
419
382
|
try {
|
|
@@ -447,23 +410,80 @@ async function main() {
|
|
|
447
410
|
if (turns > 0)
|
|
448
411
|
break;
|
|
449
412
|
if (attempt < maxAttempts - 1) {
|
|
450
|
-
|
|
451
|
-
debug(`No turns processed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay}ms...`);
|
|
452
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
413
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
453
414
|
}
|
|
454
415
|
}
|
|
455
416
|
const duration = (Date.now() - scriptStart) / 1000;
|
|
456
417
|
log('INFO', `Processed ${turns} turns in ${duration.toFixed(1)}s`);
|
|
457
|
-
if (duration > 180)
|
|
458
|
-
log('WARN', `Hook took ${duration.toFixed(1)}s (>3min)`);
|
|
459
418
|
}
|
|
460
419
|
catch (e) {
|
|
461
420
|
log('ERROR', `Failed to process session: ${e}`);
|
|
462
|
-
if (DEBUG_MODE)
|
|
463
|
-
debug(String(e.stack ?? e));
|
|
464
421
|
}
|
|
465
422
|
}
|
|
466
|
-
main()
|
|
467
|
-
|
|
468
|
-
process.
|
|
469
|
-
});
|
|
423
|
+
function main() {
|
|
424
|
+
// Worker mode: re-invoked as detached subprocess
|
|
425
|
+
if (process.env._RESPAN_CODEX_WORKER === '1') {
|
|
426
|
+
mainWorker().catch((e) => log('ERROR', `Worker crashed: ${e}`));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
debug('Codex hook started');
|
|
430
|
+
if (process.argv.length < 3) {
|
|
431
|
+
debug('No argument provided');
|
|
432
|
+
process.exit(0);
|
|
433
|
+
}
|
|
434
|
+
let payload;
|
|
435
|
+
try {
|
|
436
|
+
payload = JSON.parse(process.argv[2]);
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
debug(`Invalid JSON in argv[2]: ${e}`);
|
|
440
|
+
process.exit(0);
|
|
441
|
+
}
|
|
442
|
+
const eventType = String(payload.type ?? '');
|
|
443
|
+
if (eventType !== 'agent-turn-complete') {
|
|
444
|
+
debug(`Ignoring event type: ${eventType}`);
|
|
445
|
+
process.exit(0);
|
|
446
|
+
}
|
|
447
|
+
let sessionId = String(payload['thread-id'] ?? '');
|
|
448
|
+
if (!sessionId) {
|
|
449
|
+
debug('No thread-id in notify payload');
|
|
450
|
+
process.exit(0);
|
|
451
|
+
}
|
|
452
|
+
// Find session file
|
|
453
|
+
let sessionFile = findSessionFile(sessionId);
|
|
454
|
+
if (!sessionFile) {
|
|
455
|
+
const latest = findLatestSessionFile();
|
|
456
|
+
if (latest) {
|
|
457
|
+
sessionId = latest.sessionId;
|
|
458
|
+
sessionFile = latest.sessionFile;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
debug('No session file found');
|
|
462
|
+
process.exit(0);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Fork self as detached worker so Codex CLI doesn't block
|
|
466
|
+
const cwd = String(payload.cwd ?? '');
|
|
467
|
+
debug(`Forking worker for session: ${sessionId}`);
|
|
468
|
+
try {
|
|
469
|
+
const scriptPath = __filename || process.argv[1];
|
|
470
|
+
const child = execFile('node', [scriptPath], {
|
|
471
|
+
env: {
|
|
472
|
+
...process.env,
|
|
473
|
+
_RESPAN_CODEX_WORKER: '1',
|
|
474
|
+
_RESPAN_CODEX_SESSION: sessionId,
|
|
475
|
+
_RESPAN_CODEX_FILE: sessionFile,
|
|
476
|
+
_RESPAN_CODEX_CWD: cwd,
|
|
477
|
+
},
|
|
478
|
+
stdio: 'ignore',
|
|
479
|
+
detached: true,
|
|
480
|
+
});
|
|
481
|
+
child.unref();
|
|
482
|
+
debug('Worker launched');
|
|
483
|
+
}
|
|
484
|
+
catch (e) {
|
|
485
|
+
log('ERROR', `Failed to fork worker: ${e}`);
|
|
486
|
+
}
|
|
487
|
+
process.exit(0);
|
|
488
|
+
}
|
|
489
|
+
main();
|