@respan/cli 0.5.2 → 0.5.3

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.
@@ -19,5 +19,13 @@ export default class IntegrateCodexCli extends BaseCommand {
19
19
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
20
20
  };
21
21
  run(): Promise<void>;
22
+ /**
23
+ * Update or insert the `notify` line in a TOML config string.
24
+ *
25
+ * TOML bare keys must appear before the first [table] header.
26
+ * If a `notify` line already exists, replace it in-place.
27
+ * Otherwise, insert the notify line before the first [table] header.
28
+ */
29
+ private updateTomlNotify;
22
30
  private resolveApiKey;
23
31
  }
@@ -1,83 +1,161 @@
1
- import * as path from 'node:path';
1
+ import * as fs from 'node:fs';
2
+ import { execSync } from 'node:child_process';
2
3
  import { BaseCommand } from '../../lib/base-command.js';
3
- import { integrateFlags, readTextFile, writeTextFile, expandHome, parseAttrs, toOtelResourceAttrs, resolveScope, findProjectRoot, } from '../../lib/integrate.js';
4
+ import { integrateFlags, readTextFile, writeTextFile, writeJsonFile, readJsonFile, expandHome, parseAttrs, getCodexHookScript, resolveScope, findProjectRoot, } from '../../lib/integrate.js';
4
5
  class IntegrateCodexCli extends BaseCommand {
5
6
  async run() {
6
7
  const { flags } = await this.parse(IntegrateCodexCli);
7
8
  this.globalFlags = flags;
8
9
  try {
9
- const apiKey = this.resolveApiKey();
10
- const baseUrl = (flags['base-url']).replace(/\/+$/, '');
10
+ // Verify the user is authenticated (key is read by hook from ~/.respan/)
11
+ this.resolveApiKey();
11
12
  const projectId = flags['project-id'];
13
+ const customerId = flags['customer-id'];
14
+ const spanName = flags['span-name'];
15
+ const workflowName = flags['workflow-name'];
12
16
  const attrs = parseAttrs(flags.attrs);
13
17
  const dryRun = flags['dry-run'];
14
- const scope = resolveScope(flags, 'local');
15
- // Resolve target config file
16
- const configPath = scope === 'global'
17
- ? expandHome('~/.codex/config.toml')
18
- : path.join(findProjectRoot(), '.codex', 'config.toml');
19
- const existing = readTextFile(configPath);
20
- // Check for existing [otel] block
21
- if (/^\[otel\]/m.test(existing)) {
22
- this.warn(`[otel] section already exists in ${configPath} — skipping to avoid overwrite.`);
23
- this.warn('Remove or rename the existing [otel] section and re-run to update.');
24
- return;
18
+ // Codex CLI default: both global + local
19
+ const scope = resolveScope(flags, 'both');
20
+ const doGlobal = scope === 'global' || scope === 'both';
21
+ const doLocal = scope === 'local' || scope === 'both';
22
+ // ── Global: hook script + notify registration ──────────────────
23
+ if (doGlobal) {
24
+ // 1. Ensure Python 'requests' is available
25
+ if (!dryRun) {
26
+ try {
27
+ execSync('python3 -c "import requests"', { stdio: 'pipe' });
28
+ }
29
+ catch {
30
+ this.log('Installing Python requests package...');
31
+ try {
32
+ execSync('pip3 install requests', { stdio: 'pipe' });
33
+ this.log('Installed requests.');
34
+ }
35
+ catch {
36
+ this.warn('Could not install Python requests package automatically.\n' +
37
+ 'Install it manually: pip3 install requests');
38
+ }
39
+ }
40
+ }
41
+ else {
42
+ this.log('[dry-run] Would verify Python requests package');
43
+ }
44
+ // 2. Write hook script
45
+ const hookPath = expandHome('~/.respan/codex_hook.py');
46
+ if (dryRun) {
47
+ this.log(`[dry-run] Would write hook script to: ${hookPath}`);
48
+ }
49
+ else {
50
+ writeTextFile(hookPath, getCodexHookScript());
51
+ fs.chmodSync(hookPath, 0o755);
52
+ this.log(`Wrote hook script: ${hookPath}`);
53
+ }
54
+ // 3. Register notify hook in ~/.codex/config.toml
55
+ const configPath = expandHome('~/.codex/config.toml');
56
+ const existing = readTextFile(configPath);
57
+ const notifyValue = `notify = ["python3", "${hookPath}"]`;
58
+ const updated = this.updateTomlNotify(existing, notifyValue);
59
+ if (dryRun) {
60
+ this.log(`[dry-run] Would update: ${configPath}`);
61
+ this.log(updated);
62
+ }
63
+ else {
64
+ writeTextFile(configPath, updated);
65
+ this.log(`Updated config: ${configPath}`);
66
+ }
25
67
  }
26
- // Build resource attributes
27
- const resourceAttrs = {
28
- 'service.name': 'codex-cli',
29
- ...attrs,
30
- };
31
- if (projectId) {
32
- resourceAttrs['respan.project_id'] = projectId;
68
+ // ── Local: .codex/respan.json ──────────────────────────────────
69
+ if (doLocal) {
70
+ const projectRoot = findProjectRoot();
71
+ const respanConfigPath = `${projectRoot}/.codex/respan.json`;
72
+ const respanConfig = readJsonFile(respanConfigPath);
73
+ const newConfig = { ...respanConfig };
74
+ if (customerId) {
75
+ newConfig.customer_id = customerId;
76
+ }
77
+ if (spanName) {
78
+ newConfig.span_name = spanName;
79
+ }
80
+ if (workflowName) {
81
+ newConfig.workflow_name = workflowName;
82
+ }
83
+ if (projectId) {
84
+ newConfig.project_id = projectId;
85
+ }
86
+ // Custom attrs go as top-level keys (unknown keys = custom properties)
87
+ for (const [k, v] of Object.entries(attrs)) {
88
+ newConfig[k] = v;
89
+ }
90
+ if (Object.keys(newConfig).length > 0) {
91
+ if (dryRun) {
92
+ this.log(`[dry-run] Would write: ${respanConfigPath}`);
93
+ this.log(JSON.stringify(newConfig, null, 2));
94
+ }
95
+ else {
96
+ writeJsonFile(respanConfigPath, newConfig);
97
+ this.log(`Wrote Respan config: ${respanConfigPath}`);
98
+ }
99
+ }
33
100
  }
34
- const otelResStr = toOtelResourceAttrs(resourceAttrs);
35
- // Build TOML block
36
- const endpoint = `${baseUrl}/v2/traces`;
37
- const lines = [
38
- '',
39
- '# Respan observability (added by respan integrate codex-cli)',
40
- '[otel]',
41
- 'log_user_prompt = true',
42
- ];
43
- if (otelResStr) {
44
- lines.push(`resource_attributes = "${otelResStr}"`);
101
+ // ── Done ────────────────────────────────────────────────────────
102
+ this.log('');
103
+ if (doGlobal && doLocal) {
104
+ this.log('Codex CLI integration complete (global hook + project config).');
45
105
  }
46
- lines.push('');
47
- lines.push('[otel.exporter."otlp-http"]');
48
- lines.push(`endpoint = "${endpoint}"`);
49
- lines.push('protocol = "binary"');
50
- lines.push('');
51
- lines.push('[otel.exporter."otlp-http".headers]');
52
- lines.push(`"Authorization" = "Bearer ${apiKey}"`);
53
- lines.push('');
54
- lines.push('[otel.trace_exporter."otlp-http"]');
55
- lines.push(`endpoint = "${endpoint}"`);
56
- lines.push('protocol = "binary"');
57
- lines.push('');
58
- lines.push('[otel.trace_exporter."otlp-http".headers]');
59
- lines.push(`"Authorization" = "Bearer ${apiKey}"`);
60
- lines.push('');
61
- const block = lines.join('\n');
62
- const updated = existing.trimEnd() + '\n' + block;
63
- if (dryRun) {
64
- this.log(`[dry-run] Would append to: ${configPath}`);
65
- this.log(block);
106
+ else if (doGlobal) {
107
+ this.log('Codex CLI global hook installed.');
108
+ this.log('Run without --global in a project to configure tracing there.');
66
109
  }
67
110
  else {
68
- writeTextFile(configPath, updated);
69
- this.log(`Updated config: ${configPath}`);
111
+ this.log('Codex CLI tracing configured for this project.');
70
112
  }
71
113
  this.log('');
72
- this.log(`Codex CLI integration complete (${scope}).`);
114
+ this.log('Auth: ~/.respan/credentials.json (from `respan auth login`)');
115
+ this.log('Config: .codex/respan.json (shareable, non-secret)');
116
+ this.log('');
117
+ this.log('Set properties via integrate flags or edit .codex/respan.json:');
118
+ this.log(' respan integrate codex-cli --customer-id "frank" --span-name "my-app"');
119
+ this.log(' respan integrate codex-cli --attrs \'{"team":"platform","env":"staging"}\'');
73
120
  this.log('');
74
- this.log('Set dynamic attributes before a session:');
75
- this.log(' export OTEL_RESOURCE_ATTRIBUTES="env=prod,task_id=T-123"');
121
+ this.log('Override per-session with env vars:');
122
+ this.log(' export RESPAN_CUSTOMER_ID="your-name"');
123
+ this.log(" export RESPAN_METADATA='{\"task_id\":\"T-123\"}'");
124
+ this.log('');
125
+ this.log('Debug: CODEX_RESPAN_DEBUG=true → check ~/.codex/state/respan_hook.log');
76
126
  }
77
127
  catch (error) {
78
128
  this.handleError(error);
79
129
  }
80
130
  }
131
+ /**
132
+ * Update or insert the `notify` line in a TOML config string.
133
+ *
134
+ * TOML bare keys must appear before the first [table] header.
135
+ * If a `notify` line already exists, replace it in-place.
136
+ * Otherwise, insert the notify line before the first [table] header.
137
+ */
138
+ updateTomlNotify(existing, notifyValue) {
139
+ const lines = existing.split('\n');
140
+ // Check if notify line already exists
141
+ const notifyIdx = lines.findIndex((line) => /^\s*notify\s*=/.test(line));
142
+ if (notifyIdx >= 0) {
143
+ // Replace existing notify line
144
+ lines[notifyIdx] = notifyValue;
145
+ return lines.join('\n');
146
+ }
147
+ // Find the first [table] header to insert before it
148
+ const firstTableIdx = lines.findIndex((line) => /^\s*\[/.test(line));
149
+ if (firstTableIdx >= 0) {
150
+ // Insert notify + comment before the first table header
151
+ lines.splice(firstTableIdx, 0, '# Respan observability (added by respan integrate codex-cli)', notifyValue, '');
152
+ }
153
+ else {
154
+ // No tables at all — append to end
155
+ lines.push('', '# Respan observability (added by respan integrate codex-cli)', notifyValue);
156
+ }
157
+ return lines.join('\n');
158
+ }
81
159
  resolveApiKey() {
82
160
  const auth = this.getAuth();
83
161
  if (auth.apiKey)
@@ -91,17 +169,18 @@ class IntegrateCodexCli extends BaseCommand {
91
169
  }
92
170
  IntegrateCodexCli.description = `Integrate Respan with Codex CLI.
93
171
 
94
- Codex CLI has native OTel traces (prompts, tool approvals, results)
95
- so we append an [otel] block to its TOML config.
172
+ Installs a notify hook that reads session JSONL files and sends
173
+ them to Respan as structured spans (chat, tool, reasoning).
96
174
 
97
175
  Scope:
98
- --local Write to .codex/config.toml in project root (default)
99
- --global Write to ~/.codex/config.toml
100
- If [otel] already exists in the target file, warns and does not overwrite.`;
176
+ --global Install hook script + register notify in ~/.codex/config.toml
177
+ --local Write .codex/respan.json with customer_id, span_name, etc.
178
+ (default) Both: install hook globally + config for current project`;
101
179
  IntegrateCodexCli.examples = [
102
180
  'respan integrate codex-cli',
103
181
  'respan integrate codex-cli --global',
104
- 'respan integrate codex-cli --project-id my-project --attrs \'{"env":"prod"}\'',
182
+ 'respan integrate codex-cli --local --customer-id frank',
183
+ 'respan integrate codex-cli --attrs \'{"env":"prod"}\'',
105
184
  'respan integrate codex-cli --dry-run',
106
185
  ];
107
186
  IntegrateCodexCli.flags = {
@@ -35,6 +35,10 @@ export declare function findProjectRoot(): string;
35
35
  * version — upgrading the CLI and re-running integrate updates the hook.
36
36
  */
37
37
  export declare function getHookScript(): string;
38
+ /**
39
+ * Return the bundled Codex CLI hook script contents.
40
+ */
41
+ export declare function getCodexHookScript(): string;
38
42
  /**
39
43
  * Deep merge source into target.
40
44
  * Objects are recursively merged; arrays and primitives from source overwrite.
@@ -87,6 +87,14 @@ export function getHookScript() {
87
87
  const hookPath = path.join(dir, '..', 'assets', 'hook.py');
88
88
  return fs.readFileSync(hookPath, 'utf-8');
89
89
  }
90
+ /**
91
+ * Return the bundled Codex CLI hook script contents.
92
+ */
93
+ export function getCodexHookScript() {
94
+ const dir = path.dirname(fileURLToPath(import.meta.url));
95
+ const hookPath = path.join(dir, '..', 'assets', 'codex_hook.py');
96
+ return fs.readFileSync(hookPath, 'utf-8');
97
+ }
90
98
  // ── Utilities ─────────────────────────────────────────────────────────────
91
99
  /**
92
100
  * Deep merge source into target.