@respan/cli 0.5.3 → 0.6.1
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/auth/login.js +2 -2
- package/dist/commands/integrate/claude-code.js +9 -28
- package/dist/commands/integrate/codex-cli.js +7 -27
- package/dist/commands/integrate/gemini-cli.js +99 -53
- package/dist/hooks/claude-code.cjs +951 -0
- package/dist/hooks/claude-code.d.ts +1 -0
- package/dist/hooks/claude-code.js +641 -0
- package/dist/hooks/codex-cli.cjs +793 -0
- package/dist/hooks/codex-cli.d.ts +1 -0
- package/dist/hooks/codex-cli.js +469 -0
- package/dist/hooks/gemini-cli.cjs +826 -0
- package/dist/hooks/gemini-cli.d.ts +1 -0
- package/dist/hooks/gemini-cli.js +563 -0
- package/dist/hooks/shared.d.ts +82 -0
- package/dist/hooks/shared.js +461 -0
- package/dist/lib/integrate.d.ts +3 -3
- package/dist/lib/integrate.js +4 -8
- package/oclif.manifest.json +466 -466
- package/package.json +6 -3
- package/dist/assets/codex_hook.py +0 -897
- package/dist/assets/hook.py +0 -1052
|
@@ -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: '
|
|
117
|
-
{ name: '
|
|
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
|
-
|
|
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,
|
|
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.
|
|
26
|
-
|
|
27
|
-
|
|
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,
|
|
32
|
+
writeTextFile(hookPath, getJsHookScript('claude-code'));
|
|
52
33
|
fs.chmodSync(hookPath, 0o755);
|
|
53
34
|
this.log(`Wrote hook script: ${hookPath}`);
|
|
54
35
|
}
|
|
55
|
-
//
|
|
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: `
|
|
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;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
2
|
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
|
-
import { integrateFlags, readTextFile, writeTextFile, writeJsonFile, readJsonFile, expandHome, parseAttrs,
|
|
3
|
+
import { integrateFlags, readTextFile, writeTextFile, writeJsonFile, readJsonFile, expandHome, parseAttrs, getJsHookScript, resolveScope, findProjectRoot, } from '../../lib/integrate.js';
|
|
5
4
|
class IntegrateCodexCli extends BaseCommand {
|
|
6
5
|
async run() {
|
|
7
6
|
const { flags } = await this.parse(IntegrateCodexCli);
|
|
@@ -21,40 +20,21 @@ class IntegrateCodexCli extends BaseCommand {
|
|
|
21
20
|
const doLocal = scope === 'local' || scope === 'both';
|
|
22
21
|
// ── Global: hook script + notify registration ──────────────────
|
|
23
22
|
if (doGlobal) {
|
|
24
|
-
// 1.
|
|
25
|
-
|
|
26
|
-
|
|
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');
|
|
23
|
+
// 1. Write JS hook script (no Python dependency needed)
|
|
24
|
+
const hookDir = expandHome('~/.respan/hooks');
|
|
25
|
+
const hookPath = `${hookDir}/codex-cli.cjs`;
|
|
46
26
|
if (dryRun) {
|
|
47
27
|
this.log(`[dry-run] Would write hook script to: ${hookPath}`);
|
|
48
28
|
}
|
|
49
29
|
else {
|
|
50
|
-
writeTextFile(hookPath,
|
|
30
|
+
writeTextFile(hookPath, getJsHookScript('codex-cli'));
|
|
51
31
|
fs.chmodSync(hookPath, 0o755);
|
|
52
32
|
this.log(`Wrote hook script: ${hookPath}`);
|
|
53
33
|
}
|
|
54
|
-
//
|
|
34
|
+
// 2. Register notify hook in ~/.codex/config.toml
|
|
55
35
|
const configPath = expandHome('~/.codex/config.toml');
|
|
56
36
|
const existing = readTextFile(configPath);
|
|
57
|
-
const notifyValue = `notify = ["
|
|
37
|
+
const notifyValue = `notify = ["node", "${hookPath}"]`;
|
|
58
38
|
const updated = this.updateTomlNotify(existing, notifyValue);
|
|
59
39
|
if (dryRun) {
|
|
60
40
|
this.log(`[dry-run] Would update: ${configPath}`);
|
|
@@ -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,
|
|
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
|
-
|
|
10
|
-
|
|
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, '
|
|
15
|
-
//
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
'service.name': 'gemini-cli',
|
|
23
|
-
...attrs,
|
|
35
|
+
const hookEntry = {
|
|
36
|
+
hooks: [{ type: 'command', command: `node ${hookPath}` }],
|
|
24
37
|
};
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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('
|
|
75
|
-
this.log('
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
--
|
|
100
|
-
--
|
|
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 --
|
|
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
|
];
|