@respan/cli 0.5.2 → 0.6.0

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.
@@ -113,8 +113,8 @@ class AuthLogin extends BaseCommand {
113
113
  const method = await select({
114
114
  message: 'How would you like to authenticate?',
115
115
  choices: [
116
- { name: 'Browser login (recommended)', value: 'browser' },
117
- { name: 'API key', value: 'api_key' },
116
+ { name: 'API key (recommended)', value: 'api_key' },
117
+ { name: 'Browser login', value: 'browser' },
118
118
  ],
119
119
  });
120
120
  if (method === 'api_key') {
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
- import { execSync } from 'node:child_process';
2
+ // execSync no longer needed — JS hooks don't require pip install
3
3
  import { BaseCommand } from '../../lib/base-command.js';
4
- import { integrateFlags, deepMerge, readJsonFile, writeJsonFile, writeTextFile, expandHome, parseAttrs, getHookScript, resolveScope, findProjectRoot, } from '../../lib/integrate.js';
4
+ import { integrateFlags, deepMerge, readJsonFile, writeJsonFile, writeTextFile, expandHome, parseAttrs, getJsHookScript, resolveScope, findProjectRoot, } from '../../lib/integrate.js';
5
5
  class IntegrateClaudeCode extends BaseCommand {
6
6
  async run() {
7
7
  const { flags } = await this.parse(IntegrateClaudeCode);
@@ -22,42 +22,23 @@ class IntegrateClaudeCode extends BaseCommand {
22
22
  const doLocal = scope === 'local' || scope === 'both';
23
23
  // ── Global: hook script + registration ────────────────────────
24
24
  if (doGlobal) {
25
- // 1. Ensure Python 'requests' is available
26
- if (!dryRun) {
27
- try {
28
- execSync('python3 -c "import requests"', { stdio: 'pipe' });
29
- }
30
- catch {
31
- this.log('Installing Python requests package...');
32
- try {
33
- execSync('pip3 install requests', { stdio: 'pipe' });
34
- this.log('Installed requests.');
35
- }
36
- catch {
37
- this.warn('Could not install Python requests package automatically.\n' +
38
- 'Install it manually: pip3 install requests');
39
- }
40
- }
41
- }
42
- else {
43
- this.log('[dry-run] Would verify Python requests package');
44
- }
45
- // 2. Write hook script
46
- const hookPath = expandHome('~/.respan/hook.py');
25
+ // 1. Write JS hook script (no Python dependency needed)
26
+ const hookDir = expandHome('~/.respan/hooks');
27
+ const hookPath = `${hookDir}/claude-code.cjs`;
47
28
  if (dryRun) {
48
29
  this.log(`[dry-run] Would write hook script to: ${hookPath}`);
49
30
  }
50
31
  else {
51
- writeTextFile(hookPath, getHookScript());
32
+ writeTextFile(hookPath, getJsHookScript('claude-code'));
52
33
  fs.chmodSync(hookPath, 0o755);
53
34
  this.log(`Wrote hook script: ${hookPath}`);
54
35
  }
55
- // 3. Register Stop hook in global settings (no credentials here)
36
+ // 2. Register Stop hook in global settings (no credentials here)
56
37
  const globalSettingsPath = expandHome('~/.claude/settings.json');
57
38
  const globalSettings = readJsonFile(globalSettingsPath);
58
39
  const hookEntry = {
59
40
  matcher: '',
60
- hooks: [{ type: 'command', command: `python3 ${hookPath}` }],
41
+ hooks: [{ type: 'command', command: `node ${hookPath}` }],
61
42
  };
62
43
  const hooksSection = (globalSettings.hooks || {});
63
44
  const stopHooks = Array.isArray(hooksSection.Stop)
@@ -68,7 +49,7 @@ class IntegrateClaudeCode extends BaseCommand {
68
49
  ? entry.hooks
69
50
  : [];
70
51
  return inner.some((h) => typeof h.command === 'string' &&
71
- (h.command.includes('respan') || h.command.includes('hook.py')));
52
+ (h.command.includes('respan') || h.command.includes('hook.py') || h.command.includes('claude-code.js')));
72
53
  });
73
54
  if (existingIdx >= 0) {
74
55
  stopHooks[existingIdx] = hookEntry;
@@ -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,141 @@
1
- import * as path from 'node:path';
1
+ import * as fs from 'node:fs';
2
2
  import { BaseCommand } from '../../lib/base-command.js';
3
- import { integrateFlags, readTextFile, writeTextFile, expandHome, parseAttrs, toOtelResourceAttrs, resolveScope, findProjectRoot, } from '../../lib/integrate.js';
3
+ import { integrateFlags, readTextFile, writeTextFile, writeJsonFile, readJsonFile, expandHome, parseAttrs, getJsHookScript, resolveScope, findProjectRoot, } from '../../lib/integrate.js';
4
4
  class IntegrateCodexCli extends BaseCommand {
5
5
  async run() {
6
6
  const { flags } = await this.parse(IntegrateCodexCli);
7
7
  this.globalFlags = flags;
8
8
  try {
9
- const apiKey = this.resolveApiKey();
10
- const baseUrl = (flags['base-url']).replace(/\/+$/, '');
9
+ // Verify the user is authenticated (key is read by hook from ~/.respan/)
10
+ this.resolveApiKey();
11
11
  const projectId = flags['project-id'];
12
+ const customerId = flags['customer-id'];
13
+ const spanName = flags['span-name'];
14
+ const workflowName = flags['workflow-name'];
12
15
  const attrs = parseAttrs(flags.attrs);
13
16
  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;
17
+ // Codex CLI default: both global + local
18
+ const scope = resolveScope(flags, 'both');
19
+ const doGlobal = scope === 'global' || scope === 'both';
20
+ const doLocal = scope === 'local' || scope === 'both';
21
+ // ── Global: hook script + notify registration ──────────────────
22
+ if (doGlobal) {
23
+ // 1. Write JS hook script (no Python dependency needed)
24
+ const hookDir = expandHome('~/.respan/hooks');
25
+ const hookPath = `${hookDir}/codex-cli.cjs`;
26
+ if (dryRun) {
27
+ this.log(`[dry-run] Would write hook script to: ${hookPath}`);
28
+ }
29
+ else {
30
+ writeTextFile(hookPath, getJsHookScript('codex-cli'));
31
+ fs.chmodSync(hookPath, 0o755);
32
+ this.log(`Wrote hook script: ${hookPath}`);
33
+ }
34
+ // 2. Register notify hook in ~/.codex/config.toml
35
+ const configPath = expandHome('~/.codex/config.toml');
36
+ const existing = readTextFile(configPath);
37
+ const notifyValue = `notify = ["node", "${hookPath}"]`;
38
+ const updated = this.updateTomlNotify(existing, notifyValue);
39
+ if (dryRun) {
40
+ this.log(`[dry-run] Would update: ${configPath}`);
41
+ this.log(updated);
42
+ }
43
+ else {
44
+ writeTextFile(configPath, updated);
45
+ this.log(`Updated config: ${configPath}`);
46
+ }
25
47
  }
26
- // Build resource attributes
27
- const resourceAttrs = {
28
- 'service.name': 'codex-cli',
29
- ...attrs,
30
- };
31
- if (projectId) {
32
- resourceAttrs['respan.project_id'] = projectId;
48
+ // ── Local: .codex/respan.json ──────────────────────────────────
49
+ if (doLocal) {
50
+ const projectRoot = findProjectRoot();
51
+ const respanConfigPath = `${projectRoot}/.codex/respan.json`;
52
+ const respanConfig = readJsonFile(respanConfigPath);
53
+ const newConfig = { ...respanConfig };
54
+ if (customerId) {
55
+ newConfig.customer_id = customerId;
56
+ }
57
+ if (spanName) {
58
+ newConfig.span_name = spanName;
59
+ }
60
+ if (workflowName) {
61
+ newConfig.workflow_name = workflowName;
62
+ }
63
+ if (projectId) {
64
+ newConfig.project_id = projectId;
65
+ }
66
+ // Custom attrs go as top-level keys (unknown keys = custom properties)
67
+ for (const [k, v] of Object.entries(attrs)) {
68
+ newConfig[k] = v;
69
+ }
70
+ if (Object.keys(newConfig).length > 0) {
71
+ if (dryRun) {
72
+ this.log(`[dry-run] Would write: ${respanConfigPath}`);
73
+ this.log(JSON.stringify(newConfig, null, 2));
74
+ }
75
+ else {
76
+ writeJsonFile(respanConfigPath, newConfig);
77
+ this.log(`Wrote Respan config: ${respanConfigPath}`);
78
+ }
79
+ }
33
80
  }
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}"`);
81
+ // ── Done ────────────────────────────────────────────────────────
82
+ this.log('');
83
+ if (doGlobal && doLocal) {
84
+ this.log('Codex CLI integration complete (global hook + project config).');
45
85
  }
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);
86
+ else if (doGlobal) {
87
+ this.log('Codex CLI global hook installed.');
88
+ this.log('Run without --global in a project to configure tracing there.');
66
89
  }
67
90
  else {
68
- writeTextFile(configPath, updated);
69
- this.log(`Updated config: ${configPath}`);
91
+ this.log('Codex CLI tracing configured for this project.');
70
92
  }
71
93
  this.log('');
72
- this.log(`Codex CLI integration complete (${scope}).`);
94
+ this.log('Auth: ~/.respan/credentials.json (from `respan auth login`)');
95
+ this.log('Config: .codex/respan.json (shareable, non-secret)');
96
+ this.log('');
97
+ this.log('Set properties via integrate flags or edit .codex/respan.json:');
98
+ this.log(' respan integrate codex-cli --customer-id "frank" --span-name "my-app"');
99
+ this.log(' respan integrate codex-cli --attrs \'{"team":"platform","env":"staging"}\'');
73
100
  this.log('');
74
- this.log('Set dynamic attributes before a session:');
75
- this.log(' export OTEL_RESOURCE_ATTRIBUTES="env=prod,task_id=T-123"');
101
+ this.log('Override per-session with env vars:');
102
+ this.log(' export RESPAN_CUSTOMER_ID="your-name"');
103
+ this.log(" export RESPAN_METADATA='{\"task_id\":\"T-123\"}'");
104
+ this.log('');
105
+ this.log('Debug: CODEX_RESPAN_DEBUG=true → check ~/.codex/state/respan_hook.log');
76
106
  }
77
107
  catch (error) {
78
108
  this.handleError(error);
79
109
  }
80
110
  }
111
+ /**
112
+ * Update or insert the `notify` line in a TOML config string.
113
+ *
114
+ * TOML bare keys must appear before the first [table] header.
115
+ * If a `notify` line already exists, replace it in-place.
116
+ * Otherwise, insert the notify line before the first [table] header.
117
+ */
118
+ updateTomlNotify(existing, notifyValue) {
119
+ const lines = existing.split('\n');
120
+ // Check if notify line already exists
121
+ const notifyIdx = lines.findIndex((line) => /^\s*notify\s*=/.test(line));
122
+ if (notifyIdx >= 0) {
123
+ // Replace existing notify line
124
+ lines[notifyIdx] = notifyValue;
125
+ return lines.join('\n');
126
+ }
127
+ // Find the first [table] header to insert before it
128
+ const firstTableIdx = lines.findIndex((line) => /^\s*\[/.test(line));
129
+ if (firstTableIdx >= 0) {
130
+ // Insert notify + comment before the first table header
131
+ lines.splice(firstTableIdx, 0, '# Respan observability (added by respan integrate codex-cli)', notifyValue, '');
132
+ }
133
+ else {
134
+ // No tables at all — append to end
135
+ lines.push('', '# Respan observability (added by respan integrate codex-cli)', notifyValue);
136
+ }
137
+ return lines.join('\n');
138
+ }
81
139
  resolveApiKey() {
82
140
  const auth = this.getAuth();
83
141
  if (auth.apiKey)
@@ -91,17 +149,18 @@ class IntegrateCodexCli extends BaseCommand {
91
149
  }
92
150
  IntegrateCodexCli.description = `Integrate Respan with Codex CLI.
93
151
 
94
- Codex CLI has native OTel traces (prompts, tool approvals, results)
95
- so we append an [otel] block to its TOML config.
152
+ Installs a notify hook that reads session JSONL files and sends
153
+ them to Respan as structured spans (chat, tool, reasoning).
96
154
 
97
155
  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.`;
156
+ --global Install hook script + register notify in ~/.codex/config.toml
157
+ --local Write .codex/respan.json with customer_id, span_name, etc.
158
+ (default) Both: install hook globally + config for current project`;
101
159
  IntegrateCodexCli.examples = [
102
160
  'respan integrate codex-cli',
103
161
  'respan integrate codex-cli --global',
104
- 'respan integrate codex-cli --project-id my-project --attrs \'{"env":"prod"}\'',
162
+ 'respan integrate codex-cli --local --customer-id frank',
163
+ 'respan integrate codex-cli --attrs \'{"env":"prod"}\'',
105
164
  'respan integrate codex-cli --dry-run',
106
165
  ];
107
166
  IntegrateCodexCli.flags = {
@@ -1,78 +1,121 @@
1
+ import * as fs from 'node:fs';
1
2
  import * as path from 'node:path';
2
3
  import { BaseCommand } from '../../lib/base-command.js';
3
- import { integrateFlags, deepMerge, readJsonFile, writeJsonFile, readTextFile, writeTextFile, expandHome, parseAttrs, toOtelResourceAttrs, resolveScope, findProjectRoot, } from '../../lib/integrate.js';
4
+ import { integrateFlags, deepMerge, readJsonFile, writeJsonFile, writeTextFile, expandHome, parseAttrs, getJsHookScript, resolveScope, findProjectRoot, DEFAULT_BASE_URL, } from '../../lib/integrate.js';
4
5
  class IntegrateGeminiCli extends BaseCommand {
5
6
  async run() {
6
7
  const { flags } = await this.parse(IntegrateGeminiCli);
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 settings file
18
+ const scope = resolveScope(flags, 'global');
19
+ // ── 1. Install hook script ──────────────────────────────────
20
+ const hookDir = expandHome('~/.respan/hooks');
21
+ const hookPath = `${hookDir}/gemini-cli.cjs`;
22
+ if (dryRun) {
23
+ this.log(`[dry-run] Would write hook script to: ${hookPath}`);
24
+ }
25
+ else {
26
+ writeTextFile(hookPath, getJsHookScript('gemini-cli'));
27
+ fs.chmodSync(hookPath, 0o755);
28
+ this.log(`Wrote hook script: ${hookPath}`);
29
+ }
30
+ // ── 2. Register hooks in settings.json ────────────────────────
16
31
  const settingsPath = scope === 'global'
17
32
  ? expandHome('~/.gemini/settings.json')
18
33
  : path.join(findProjectRoot(), '.gemini', 'settings.json');
19
34
  const existing = readJsonFile(settingsPath);
20
- // Build resource attributes
21
- const resourceAttrs = {
22
- 'service.name': 'gemini-cli',
23
- ...attrs,
35
+ const hookEntry = {
36
+ hooks: [{ type: 'command', command: `node ${hookPath}` }],
24
37
  };
25
- if (projectId) {
26
- resourceAttrs['respan.project_id'] = projectId;
38
+ const hooksSection = (existing.hooks || {});
39
+ // Register the same hook script for AfterModel, BeforeTool, and AfterTool.
40
+ // AfterModel captures streaming text; BeforeTool/AfterTool capture tool
41
+ // names, arguments, and output for rich tool spans.
42
+ const hookEvents = ['AfterModel', 'BeforeTool', 'AfterTool'];
43
+ const updatedHooks = { ...hooksSection };
44
+ for (const eventName of hookEvents) {
45
+ const eventHooks = Array.isArray(hooksSection[eventName])
46
+ ? [...hooksSection[eventName]]
47
+ : [];
48
+ // Replace existing respan hook or add new one
49
+ const existingIdx = eventHooks.findIndex((entry) => {
50
+ const inner = Array.isArray(entry.hooks)
51
+ ? entry.hooks
52
+ : [];
53
+ return inner.some((h) => typeof h.command === 'string' &&
54
+ (h.command.includes('respan') || h.command.includes('gemini_hook') || h.command.includes('gemini-cli.js')));
55
+ });
56
+ if (existingIdx >= 0) {
57
+ eventHooks[existingIdx] = hookEntry;
58
+ }
59
+ else {
60
+ eventHooks.push(hookEntry);
61
+ }
62
+ updatedHooks[eventName] = eventHooks;
27
63
  }
28
- // settings.json only telemetry fields Gemini CLI supports
29
- const patch = {
30
- telemetry: {
31
- enabled: true,
32
- otlpEndpoint: `${baseUrl}/v2/traces`,
33
- otlpProtocol: 'http',
34
- },
35
- };
36
- const merged = deepMerge(existing, patch);
37
- // .env file — OTel SDK picks up headers & resource attrs from env
38
- const envDir = scope === 'global'
39
- ? expandHome('~/.gemini')
40
- : path.join(findProjectRoot(), '.gemini');
41
- const envPath = path.join(envDir, '.env');
42
- const envLines = [];
43
- envLines.push(`OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${apiKey}`);
44
- const otelResStr = toOtelResourceAttrs(resourceAttrs);
45
- if (otelResStr) {
46
- envLines.push(`OTEL_RESOURCE_ATTRIBUTES=${otelResStr}`);
64
+ // Remove legacy telemetry config (from older CLI versions) to avoid
65
+ // Gemini CLI sending broken OTEL requests to the wrong endpoint.
66
+ // The hooks handle all trace export now.
67
+ const mergeSource = { hooks: updatedHooks };
68
+ if (existing.telemetry) {
69
+ mergeSource.telemetry = undefined;
70
+ }
71
+ const merged = deepMerge(existing, mergeSource);
72
+ // Explicitly remove telemetry key if it existed
73
+ delete merged.telemetry;
74
+ // ── 3. Write respan.json with non-secret config ─────────────
75
+ const configPath = expandHome('~/.gemini/respan.json');
76
+ const respanConfig = readJsonFile(configPath);
77
+ const newConfig = { ...respanConfig };
78
+ const baseUrl = flags['base-url'];
79
+ if (baseUrl && baseUrl !== DEFAULT_BASE_URL) {
80
+ newConfig.base_url = baseUrl;
81
+ }
82
+ if (customerId)
83
+ newConfig.customer_id = customerId;
84
+ if (spanName)
85
+ newConfig.span_name = spanName;
86
+ if (workflowName)
87
+ newConfig.workflow_name = workflowName;
88
+ if (projectId)
89
+ newConfig.project_id = projectId;
90
+ for (const [k, v] of Object.entries(attrs)) {
91
+ newConfig[k] = v;
47
92
  }
48
- // Merge with existing .env (replace our keys, keep the rest)
49
- const existingEnv = readTextFile(envPath);
50
- const envKeysToSet = new Set(envLines.map(l => l.split('=')[0]));
51
- const keptLines = existingEnv
52
- .split('\n')
53
- .filter(line => {
54
- const key = line.split('=')[0];
55
- return !envKeysToSet.has(key);
56
- });
57
- const finalEnv = [...keptLines.filter(l => l.trim() !== ''), ...envLines].join('\n') + '\n';
58
93
  if (dryRun) {
59
94
  this.log(`[dry-run] Would update: ${settingsPath}`);
60
95
  this.log(JSON.stringify(merged, null, 2));
61
- this.log('');
62
- this.log(`[dry-run] Would update: ${envPath}`);
63
- this.log(finalEnv);
96
+ if (Object.keys(newConfig).length > 0) {
97
+ this.log('');
98
+ this.log(`[dry-run] Would write: ${configPath}`);
99
+ this.log(JSON.stringify(newConfig, null, 2));
100
+ }
64
101
  }
65
102
  else {
66
103
  writeJsonFile(settingsPath, merged);
67
104
  this.log(`Updated settings: ${settingsPath}`);
68
- writeTextFile(envPath, finalEnv);
69
- this.log(`Updated env: ${envPath}`);
105
+ if (Object.keys(newConfig).length > 0) {
106
+ writeJsonFile(configPath, newConfig);
107
+ this.log(`Wrote Respan config: ${configPath}`);
108
+ }
70
109
  }
71
110
  this.log('');
72
111
  this.log(`Gemini CLI integration complete (${scope}).`);
73
112
  this.log('');
74
- this.log('Set dynamic attributes before a session:');
75
- this.log(' export OTEL_RESOURCE_ATTRIBUTES="env=prod,task_id=T-123"');
113
+ this.log('Auth: ~/.respan/credentials.json (from `respan auth login`)');
114
+ this.log('Config: ~/.gemini/respan.json (shareable, non-secret)');
115
+ this.log('');
116
+ this.log('Set properties via integrate flags or edit ~/.gemini/respan.json:');
117
+ this.log(' respan integrate gemini-cli --customer-id "frank" --span-name "my-app"');
118
+ this.log(' respan integrate gemini-cli --attrs \'{"team":"platform","env":"staging"}\'');
76
119
  }
77
120
  catch (error) {
78
121
  this.handleError(error);
@@ -91,16 +134,19 @@ class IntegrateGeminiCli extends BaseCommand {
91
134
  }
92
135
  IntegrateGeminiCli.description = `Integrate Respan with Gemini CLI.
93
136
 
94
- Gemini CLI has native OTel traces (tool_call, llm_call, agent_call)
95
- so we configure it to send telemetry directly to the Respan OTLP
96
- endpoint.
137
+ Installs an AfterModel hook that captures LLM request/response data
138
+ and sends it to Respan as structured spans with model, token counts,
139
+ and input/output.
97
140
 
98
141
  Scope:
99
- --local Write to .gemini/settings.json in project root (default)
100
- --global Write to ~/.gemini/settings.json`;
142
+ --global Write to ~/.gemini/settings.json (default)
143
+ --local Write to .gemini/settings.json in project root
144
+
145
+ Note: Gemini CLI ignores workspace-level telemetry settings, so
146
+ --global is the default.`;
101
147
  IntegrateGeminiCli.examples = [
102
148
  'respan integrate gemini-cli',
103
- 'respan integrate gemini-cli --global',
149
+ 'respan integrate gemini-cli --local',
104
150
  'respan integrate gemini-cli --project-id my-project --attrs \'{"env":"prod"}\'',
105
151
  'respan integrate gemini-cli --dry-run',
106
152
  ];