@respan/cli 0.15.0 → 0.16.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.
- package/dist/commands/tools/call.d.ts +19 -0
- package/dist/commands/tools/call.js +31 -0
- package/dist/commands/tools/list.d.ts +14 -0
- package/dist/commands/tools/list.js +22 -0
- package/dist/lib/mcp-client.d.ts +38 -0
- package/dist/lib/mcp-client.js +52 -0
- package/oclif.manifest.json +167 -785
- package/package.json +2 -1
- package/dist/commands/integrate/claude-code.d.ts +0 -25
- package/dist/commands/integrate/claude-code.js +0 -210
- package/dist/commands/integrate/codex-cli.d.ts +0 -33
- package/dist/commands/integrate/codex-cli.js +0 -191
- package/dist/commands/integrate/gemini-cli.d.ts +0 -25
- package/dist/commands/integrate/gemini-cli.js +0 -189
- package/dist/commands/integrate/opencode.d.ts +0 -25
- package/dist/commands/integrate/opencode.js +0 -123
- package/dist/commands/integrate/pi.d.ts +0 -28
- package/dist/commands/integrate/pi.js +0 -198
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
|
-
import { integrateFlags, deepMerge, readJsonFile, writeJsonFile, writeTextFile, expandHome, parseAttrs, getJsHookScript, resolveScope, findProjectRoot, DEFAULT_BASE_URL, } from '../../lib/integrate.js';
|
|
5
|
-
class IntegrateGeminiCli extends BaseCommand {
|
|
6
|
-
async run() {
|
|
7
|
-
const { flags } = await this.parse(IntegrateGeminiCli);
|
|
8
|
-
this.globalFlags = flags;
|
|
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) ────────────────────────────────────
|
|
44
|
-
// Verify the user is authenticated (key is read by hook from ~/.respan/)
|
|
45
|
-
this.resolveApiKey();
|
|
46
|
-
const projectId = flags['project-id'];
|
|
47
|
-
const customerId = flags['customer-id'];
|
|
48
|
-
const spanName = flags['span-name'];
|
|
49
|
-
const workflowName = flags['workflow-name'];
|
|
50
|
-
const attrs = parseAttrs(flags.attrs);
|
|
51
|
-
// ── 1. Install hook script ──────────────────────────────────
|
|
52
|
-
const hookDir = expandHome('~/.respan/hooks');
|
|
53
|
-
const hookPath = `${hookDir}/gemini-cli.cjs`;
|
|
54
|
-
if (dryRun) {
|
|
55
|
-
this.log(`[dry-run] Would write hook script to: ${hookPath}`);
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
writeTextFile(hookPath, getJsHookScript('gemini-cli'));
|
|
59
|
-
fs.chmodSync(hookPath, 0o755);
|
|
60
|
-
this.log(`Wrote hook script: ${hookPath}`);
|
|
61
|
-
}
|
|
62
|
-
// ── 2. Register hooks in settings.json ────────────────────────
|
|
63
|
-
const settingsPath = scope === 'global'
|
|
64
|
-
? expandHome('~/.gemini/settings.json')
|
|
65
|
-
: path.join(findProjectRoot(), '.gemini', 'settings.json');
|
|
66
|
-
const existing = readJsonFile(settingsPath);
|
|
67
|
-
const hookEntry = {
|
|
68
|
-
hooks: [{ type: 'command', command: `node ${hookPath}` }],
|
|
69
|
-
};
|
|
70
|
-
const hooksSection = (existing.hooks || {});
|
|
71
|
-
// Register the same hook script for AfterModel, BeforeTool, and AfterTool.
|
|
72
|
-
// AfterModel captures streaming text; BeforeTool/AfterTool capture tool
|
|
73
|
-
// names, arguments, and output for rich tool spans.
|
|
74
|
-
const hookEvents = ['AfterModel', 'BeforeTool', 'AfterTool'];
|
|
75
|
-
const updatedHooks = { ...hooksSection };
|
|
76
|
-
for (const eventName of hookEvents) {
|
|
77
|
-
const eventHooks = Array.isArray(hooksSection[eventName])
|
|
78
|
-
? [...hooksSection[eventName]]
|
|
79
|
-
: [];
|
|
80
|
-
// Replace existing respan hook or add new one
|
|
81
|
-
const existingIdx = eventHooks.findIndex((entry) => {
|
|
82
|
-
const inner = Array.isArray(entry.hooks)
|
|
83
|
-
? entry.hooks
|
|
84
|
-
: [];
|
|
85
|
-
return inner.some((h) => typeof h.command === 'string' &&
|
|
86
|
-
(h.command.includes('respan') || h.command.includes('gemini_hook') || h.command.includes('gemini-cli.js')));
|
|
87
|
-
});
|
|
88
|
-
if (existingIdx >= 0) {
|
|
89
|
-
eventHooks[existingIdx] = hookEntry;
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
eventHooks.push(hookEntry);
|
|
93
|
-
}
|
|
94
|
-
updatedHooks[eventName] = eventHooks;
|
|
95
|
-
}
|
|
96
|
-
// Remove legacy telemetry config (from older CLI versions) to avoid
|
|
97
|
-
// Gemini CLI sending broken OTEL requests to the wrong endpoint.
|
|
98
|
-
// The hooks handle all trace export now.
|
|
99
|
-
const mergeSource = { hooks: updatedHooks };
|
|
100
|
-
if (existing.telemetry) {
|
|
101
|
-
mergeSource.telemetry = undefined;
|
|
102
|
-
}
|
|
103
|
-
const merged = deepMerge(existing, mergeSource);
|
|
104
|
-
// Explicitly remove telemetry key if it existed
|
|
105
|
-
delete merged.telemetry;
|
|
106
|
-
// ── 3. Write respan.json with non-secret config ─────────────
|
|
107
|
-
const configPath = expandHome('~/.gemini/respan.json');
|
|
108
|
-
const respanConfig = readJsonFile(configPath);
|
|
109
|
-
const newConfig = { ...respanConfig };
|
|
110
|
-
const baseUrl = flags['base-url'];
|
|
111
|
-
if (baseUrl && baseUrl !== DEFAULT_BASE_URL) {
|
|
112
|
-
newConfig.base_url = baseUrl;
|
|
113
|
-
}
|
|
114
|
-
if (customerId)
|
|
115
|
-
newConfig.customer_id = customerId;
|
|
116
|
-
if (spanName)
|
|
117
|
-
newConfig.span_name = spanName;
|
|
118
|
-
if (workflowName)
|
|
119
|
-
newConfig.workflow_name = workflowName;
|
|
120
|
-
if (projectId)
|
|
121
|
-
newConfig.project_id = projectId;
|
|
122
|
-
for (const [k, v] of Object.entries(attrs)) {
|
|
123
|
-
newConfig[k] = v;
|
|
124
|
-
}
|
|
125
|
-
if (dryRun) {
|
|
126
|
-
this.log(`[dry-run] Would update: ${settingsPath}`);
|
|
127
|
-
this.log(JSON.stringify(merged, null, 2));
|
|
128
|
-
if (Object.keys(newConfig).length > 0) {
|
|
129
|
-
this.log('');
|
|
130
|
-
this.log(`[dry-run] Would write: ${configPath}`);
|
|
131
|
-
this.log(JSON.stringify(newConfig, null, 2));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
writeJsonFile(settingsPath, merged);
|
|
136
|
-
this.log(`Updated settings: ${settingsPath}`);
|
|
137
|
-
if (Object.keys(newConfig).length > 0) {
|
|
138
|
-
writeJsonFile(configPath, newConfig);
|
|
139
|
-
this.log(`Wrote Respan config: ${configPath}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
this.log('');
|
|
143
|
-
this.log(`Gemini CLI integration complete (${scope}).`);
|
|
144
|
-
this.log('');
|
|
145
|
-
this.log('Auth: ~/.respan/credentials.json (from `respan auth login`)');
|
|
146
|
-
this.log('Config: ~/.gemini/respan.json (shareable, non-secret)');
|
|
147
|
-
this.log('');
|
|
148
|
-
this.log('Set properties via integrate flags or edit ~/.gemini/respan.json:');
|
|
149
|
-
this.log(' respan integrate gemini-cli --customer-id "frank" --span-name "my-app"');
|
|
150
|
-
this.log(' respan integrate gemini-cli --attrs \'{"team":"platform","env":"staging"}\'');
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
this.handleError(error);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
resolveApiKey() {
|
|
157
|
-
const auth = this.getAuth();
|
|
158
|
-
if (auth.apiKey)
|
|
159
|
-
return auth.apiKey;
|
|
160
|
-
if (auth.accessToken) {
|
|
161
|
-
this.warn('Using access token (JWT) which may expire. Consider using an API key instead.');
|
|
162
|
-
return auth.accessToken;
|
|
163
|
-
}
|
|
164
|
-
this.error('No API key found. Pass --api-key, set RESPAN_API_KEY, or run: respan auth login');
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
IntegrateGeminiCli.description = `Integrate Respan with Gemini CLI.
|
|
168
|
-
|
|
169
|
-
Installs an AfterModel hook that captures LLM request/response data
|
|
170
|
-
and sends it to Respan as structured spans with model, token counts,
|
|
171
|
-
and input/output.
|
|
172
|
-
|
|
173
|
-
Scope:
|
|
174
|
-
--global Write to ~/.gemini/settings.json (default)
|
|
175
|
-
--local Write to .gemini/settings.json in project root
|
|
176
|
-
|
|
177
|
-
Gemini CLI ignores workspace-level telemetry settings, so --global is the default.`;
|
|
178
|
-
IntegrateGeminiCli.examples = [
|
|
179
|
-
'respan integrate gemini-cli',
|
|
180
|
-
'respan integrate gemini-cli --disable',
|
|
181
|
-
'respan integrate gemini-cli --local',
|
|
182
|
-
'respan integrate gemini-cli --project-id my-project --attrs \'{"env":"prod"}\'',
|
|
183
|
-
'respan integrate gemini-cli --dry-run',
|
|
184
|
-
];
|
|
185
|
-
IntegrateGeminiCli.flags = {
|
|
186
|
-
...BaseCommand.baseFlags,
|
|
187
|
-
...integrateFlags,
|
|
188
|
-
};
|
|
189
|
-
export default IntegrateGeminiCli;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
-
export default class IntegrateOpencode extends BaseCommand {
|
|
3
|
-
static description: string;
|
|
4
|
-
static examples: string[];
|
|
5
|
-
static flags: {
|
|
6
|
-
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
|
-
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
-
enable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
-
disable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
-
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
-
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
-
attrs: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
-
'customer-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
|
-
'span-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
-
'workflow-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
-
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
|
-
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
-
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
-
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
-
csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
21
|
-
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
22
|
-
};
|
|
23
|
-
run(): Promise<void>;
|
|
24
|
-
private resolveApiKey;
|
|
25
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as path from 'node:path';
|
|
3
|
-
import { execSync } from 'node:child_process';
|
|
4
|
-
import { BaseCommand } from '../../lib/base-command.js';
|
|
5
|
-
import { integrateFlags, writeJsonFile, expandHome, parseAttrs, resolveScope, findProjectRoot, } from '../../lib/integrate.js';
|
|
6
|
-
class IntegrateOpencode extends BaseCommand {
|
|
7
|
-
async run() {
|
|
8
|
-
const { flags } = await this.parse(IntegrateOpencode);
|
|
9
|
-
this.globalFlags = flags;
|
|
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) ────────────────────────────────────
|
|
34
|
-
const apiKey = this.resolveApiKey();
|
|
35
|
-
const baseUrl = (flags['base-url']).replace(/\/+$/, '');
|
|
36
|
-
const projectId = flags['project-id'];
|
|
37
|
-
const attrs = parseAttrs(flags.attrs);
|
|
38
|
-
// ── 1. Install opencode-otel (always global) ─────────────────
|
|
39
|
-
if (dryRun) {
|
|
40
|
-
this.log('[dry-run] Would run: npm install -g opencode-otel');
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
this.log('Installing opencode-otel...');
|
|
44
|
-
try {
|
|
45
|
-
execSync('npm install -g opencode-otel', { stdio: 'pipe' });
|
|
46
|
-
this.log('Installed opencode-otel globally.');
|
|
47
|
-
}
|
|
48
|
-
catch (err) {
|
|
49
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
50
|
-
this.warn(`Failed to install opencode-otel: ${msg}`);
|
|
51
|
-
this.warn('You may need to install it manually: npm install -g opencode-otel');
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// ── 2. Write plugin config ────────────────────────────────────
|
|
55
|
-
const pluginPath = scope === 'global'
|
|
56
|
-
? expandHome('~/.config/opencode/plugins/otel.json')
|
|
57
|
-
: path.join(findProjectRoot(), '.opencode', 'plugins', 'otel.json');
|
|
58
|
-
const resourceAttrs = {
|
|
59
|
-
'service.name': 'opencode',
|
|
60
|
-
...attrs,
|
|
61
|
-
};
|
|
62
|
-
if (projectId) {
|
|
63
|
-
resourceAttrs['respan.project_id'] = projectId;
|
|
64
|
-
}
|
|
65
|
-
const pluginConfig = {
|
|
66
|
-
tracesEndpoint: `${baseUrl}/v2/traces`,
|
|
67
|
-
logsEndpoint: `${baseUrl}/v2/logs`,
|
|
68
|
-
headers: {
|
|
69
|
-
Authorization: `Bearer ${apiKey}`,
|
|
70
|
-
},
|
|
71
|
-
resourceAttributes: resourceAttrs,
|
|
72
|
-
};
|
|
73
|
-
if (dryRun) {
|
|
74
|
-
this.log(`[dry-run] Would write: ${pluginPath}`);
|
|
75
|
-
this.log(JSON.stringify(pluginConfig, null, 2));
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
writeJsonFile(pluginPath, pluginConfig);
|
|
79
|
-
this.log(`Wrote plugin config: ${pluginPath}`);
|
|
80
|
-
}
|
|
81
|
-
// ── Done ──────────────────────────────────────────────────────
|
|
82
|
-
this.log('');
|
|
83
|
-
this.log(`OpenCode integration complete (${scope}).`);
|
|
84
|
-
this.log('');
|
|
85
|
-
this.log('Set dynamic attributes before a session:');
|
|
86
|
-
this.log(' export OTEL_RESOURCE_ATTRIBUTES="env=prod,task_id=T-123"');
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
this.handleError(error);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
resolveApiKey() {
|
|
93
|
-
const auth = this.getAuth();
|
|
94
|
-
if (auth.apiKey)
|
|
95
|
-
return auth.apiKey;
|
|
96
|
-
if (auth.accessToken) {
|
|
97
|
-
this.warn('Using access token (JWT) which may expire. Consider using an API key instead.');
|
|
98
|
-
return auth.accessToken;
|
|
99
|
-
}
|
|
100
|
-
this.error('No API key found. Pass --api-key, set RESPAN_API_KEY, or run: respan auth login');
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
IntegrateOpencode.description = `Integrate Respan with OpenCode.
|
|
104
|
-
|
|
105
|
-
OpenCode's built-in OTel does not work reliably, so this uses the
|
|
106
|
-
community opencode-otel plugin instead.
|
|
107
|
-
|
|
108
|
-
Scope:
|
|
109
|
-
--local Write plugin config to project root (default)
|
|
110
|
-
--global Write to ~/.config/opencode/plugins/otel.json
|
|
111
|
-
The opencode-otel package is always installed globally.`;
|
|
112
|
-
IntegrateOpencode.examples = [
|
|
113
|
-
'respan integrate opencode',
|
|
114
|
-
'respan integrate opencode --disable',
|
|
115
|
-
'respan integrate opencode --global',
|
|
116
|
-
'respan integrate opencode --project-id my-project --attrs \'{"env":"prod"}\'',
|
|
117
|
-
'respan integrate opencode --dry-run',
|
|
118
|
-
];
|
|
119
|
-
IntegrateOpencode.flags = {
|
|
120
|
-
...BaseCommand.baseFlags,
|
|
121
|
-
...integrateFlags,
|
|
122
|
-
};
|
|
123
|
-
export default IntegrateOpencode;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { BaseCommand } from '../../lib/base-command.js';
|
|
2
|
-
export default class IntegratePi extends BaseCommand {
|
|
3
|
-
static description: string;
|
|
4
|
-
static examples: string[];
|
|
5
|
-
static flags: {
|
|
6
|
-
'workflow-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
'span-name': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
-
'trace-scope': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
-
'with-collector': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
-
'collector-port': import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
-
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
-
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
-
enable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
-
disable: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
|
-
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
-
'base-url': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
|
-
attrs: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
|
-
'customer-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
|
-
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
-
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
-
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
-
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
23
|
-
csv: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
24
|
-
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
25
|
-
};
|
|
26
|
-
run(): Promise<void>;
|
|
27
|
-
private resolveApiKey;
|
|
28
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process';
|
|
2
|
-
import { Flags } from '@oclif/core';
|
|
3
|
-
import { BaseCommand } from '../../lib/base-command.js';
|
|
4
|
-
import { isBinaryInstalled } from '../../lib/agents.js';
|
|
5
|
-
import { DEFAULT_BASE_URL, integrateFlags, readJsonFile, writeJsonFile, parseAttrs, resolveScope, findProjectRoot, } from '../../lib/integrate.js';
|
|
6
|
-
import { PI_PACKAGE_SPEC, buildPiRespanConfig, piConfigPath, piInstallArgs, } from '../../lib/pi-integrate.js';
|
|
7
|
-
import { DEFAULT_COLLECTOR_PORT, collectorBaseUrl } from '../../lib/collector.js';
|
|
8
|
-
class IntegratePi extends BaseCommand {
|
|
9
|
-
async run() {
|
|
10
|
-
const { flags } = await this.parse(IntegratePi);
|
|
11
|
-
this.globalFlags = flags;
|
|
12
|
-
try {
|
|
13
|
-
const dryRun = flags['dry-run'];
|
|
14
|
-
// pi loads global packages from ~/.pi/agent, so global is the default.
|
|
15
|
-
const scope = resolveScope(flags, 'global');
|
|
16
|
-
const projectRoot = scope === 'local' ? findProjectRoot() : process.cwd();
|
|
17
|
-
const configPath = piConfigPath(scope, projectRoot);
|
|
18
|
-
// ── Disable mode ─────────────────────────────────────────────
|
|
19
|
-
if (flags.disable) {
|
|
20
|
-
const disabled = buildPiRespanConfig(readJsonFile(configPath), { enabled: false });
|
|
21
|
-
if (dryRun) {
|
|
22
|
-
this.log(`[dry-run] Would write: ${configPath}`);
|
|
23
|
-
this.log(JSON.stringify(disabled, null, 2));
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
writeJsonFile(configPath, disabled);
|
|
27
|
-
this.log(`Disabled tracing in: ${configPath}`);
|
|
28
|
-
}
|
|
29
|
-
this.log('');
|
|
30
|
-
this.log('pi tracing disabled (the extension package stays installed). Run "respan integrate pi" to re-enable.');
|
|
31
|
-
this.log(`To uninstall the extension package: pi remove ${PI_PACKAGE_SPEC}`);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
// ── Enable mode (default) ────────────────────────────────────
|
|
35
|
-
// Auth precheck only — the key is read by the extension at runtime.
|
|
36
|
-
this.resolveApiKey();
|
|
37
|
-
const baseUrl = flags['base-url'];
|
|
38
|
-
const projectId = flags['project-id'];
|
|
39
|
-
const customerId = flags['customer-id'];
|
|
40
|
-
// The extension treats span_name as an alias of workflow_name.
|
|
41
|
-
const workflowName = flags['workflow-name'] ?? flags['span-name'];
|
|
42
|
-
const traceScope = flags['trace-scope'];
|
|
43
|
-
const attrs = parseAttrs(flags.attrs);
|
|
44
|
-
const withCollector = flags['with-collector'];
|
|
45
|
-
const collectorPort = flags['collector-port'] ?? DEFAULT_COLLECTOR_PORT;
|
|
46
|
-
if (flags['collector-port'] !== undefined && !withCollector) {
|
|
47
|
-
this.warn('--collector-port has no effect without --with-collector.');
|
|
48
|
-
}
|
|
49
|
-
if (withCollector && baseUrl && baseUrl.replace(/\/+$/, '') !== DEFAULT_BASE_URL) {
|
|
50
|
-
this.warn(`--with-collector overrides --base-url: traces go to ${collectorBaseUrl(collectorPort)}. ` +
|
|
51
|
-
'Point the collector at your endpoint with `respan collector start --export-url ...`.');
|
|
52
|
-
}
|
|
53
|
-
// ── 1. Install the pi package ─────────────────────────────────
|
|
54
|
-
const installCommand = ['pi', ...piInstallArgs(scope)].join(' ');
|
|
55
|
-
if (!isBinaryInstalled('pi')) {
|
|
56
|
-
const message = 'pi is not installed. Install it with `npm install -g @earendil-works/pi-coding-agent` (see https://pi.dev)';
|
|
57
|
-
if (dryRun) {
|
|
58
|
-
this.warn(message);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
this.error(message);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
if (dryRun) {
|
|
65
|
-
this.log(`[dry-run] Would run: ${installCommand}`);
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
this.log(`Installing ${PI_PACKAGE_SPEC}...`);
|
|
69
|
-
try {
|
|
70
|
-
execSync(installCommand, { stdio: 'pipe', cwd: projectRoot });
|
|
71
|
-
this.log(`Installed ${PI_PACKAGE_SPEC} (${scope}).`);
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
75
|
-
this.warn(`Failed to install ${PI_PACKAGE_SPEC}: ${msg}`);
|
|
76
|
-
this.warn(`You may need to install it manually: ${installCommand}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
// ── 2. Write respan.json (non-secret) ─────────────────────────
|
|
80
|
-
const config = buildPiRespanConfig(readJsonFile(configPath), {
|
|
81
|
-
enabled: true,
|
|
82
|
-
customerId,
|
|
83
|
-
workflowName,
|
|
84
|
-
projectId,
|
|
85
|
-
traceScope,
|
|
86
|
-
baseUrl,
|
|
87
|
-
collectorPort: withCollector ? collectorPort : undefined,
|
|
88
|
-
attrs,
|
|
89
|
-
});
|
|
90
|
-
if (dryRun) {
|
|
91
|
-
this.log(`[dry-run] Would write: ${configPath}`);
|
|
92
|
-
this.log(JSON.stringify(config, null, 2));
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
writeJsonFile(configPath, config);
|
|
96
|
-
this.log(`Wrote Respan config: ${configPath}`);
|
|
97
|
-
}
|
|
98
|
-
// ── Done ──────────────────────────────────────────────────────
|
|
99
|
-
this.log('');
|
|
100
|
-
this.log(`pi integration complete (${scope}). Restart pi to load the extension.`);
|
|
101
|
-
this.log('');
|
|
102
|
-
this.log('Auth: RESPAN_API_KEY or ~/.respan/credentials.json (from `respan auth login`)');
|
|
103
|
-
this.log(`Config: ${configPath} (shareable, non-secret)`);
|
|
104
|
-
if (withCollector) {
|
|
105
|
-
this.log('');
|
|
106
|
-
this.log(`Traces go through the local collector at ${collectorBaseUrl(collectorPort)} (base_url in respan.json).`);
|
|
107
|
-
this.log('Start it once per machine; it keeps running across pi sessions:');
|
|
108
|
-
this.log(` respan collector start${collectorPort === DEFAULT_COLLECTOR_PORT ? '' : ` --port ${collectorPort}`}`);
|
|
109
|
-
this.log('Remove base_url from respan.json to send directly to Respan again.');
|
|
110
|
-
}
|
|
111
|
-
this.log('');
|
|
112
|
-
this.log('Set properties via integrate flags or edit respan.json:');
|
|
113
|
-
this.log(' respan integrate pi --customer-id "frank" --workflow-name "my-agent"');
|
|
114
|
-
this.log(' respan integrate pi --attrs \'{"team":"platform","env":"staging"}\'');
|
|
115
|
-
this.log(' respan integrate pi --trace-scope run # one trace per prompt instead of one per session ("trace_scope")');
|
|
116
|
-
this.log('');
|
|
117
|
-
this.log('Override per-session with env vars:');
|
|
118
|
-
this.log(' export RESPAN_CUSTOMER_ID="your-name"');
|
|
119
|
-
this.log(' export RESPAN_PI_TRACE_SCOPE=run # or session (default)');
|
|
120
|
-
this.log(' export RESPAN_PI_TRACING=false # skip tracing for one session');
|
|
121
|
-
this.log('');
|
|
122
|
-
this.log('One-off run without installing (from a project that has the package):');
|
|
123
|
-
this.log(' pi -e ./node_modules/@respan/instrumentation-pi/dist/extension.js');
|
|
124
|
-
this.log('');
|
|
125
|
-
this.log('Debug: RESPAN_PI_DEBUG=true pi (logs to stderr only; nothing is written to disk)');
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
this.handleError(error);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
resolveApiKey() {
|
|
132
|
-
const auth = this.getAuth();
|
|
133
|
-
if (auth.apiKey)
|
|
134
|
-
return auth.apiKey;
|
|
135
|
-
if (auth.accessToken) {
|
|
136
|
-
this.warn('Using access token (JWT) which may expire. Consider using an API key instead.');
|
|
137
|
-
return auth.accessToken;
|
|
138
|
-
}
|
|
139
|
-
this.error('No API key found. Pass --api-key, set RESPAN_API_KEY, or run: respan auth login');
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
IntegratePi.description = `Integrate Respan with the pi coding agent (https://pi.dev).
|
|
143
|
-
|
|
144
|
-
Installs the @respan/instrumentation-pi pi package — a pi extension that
|
|
145
|
-
traces every agent run as one turn span per prompt with chat and tool spans underneath,
|
|
146
|
-
correlating all runs of a pi session by its session id — and writes a
|
|
147
|
-
non-secret respan.json config for it.
|
|
148
|
-
|
|
149
|
-
Scope:
|
|
150
|
-
--global pi install npm:@respan/instrumentation-pi + ~/.pi/agent/respan.json (default)
|
|
151
|
-
--local pi install -l npm:@respan/instrumentation-pi + .pi/respan.json in the project root
|
|
152
|
-
|
|
153
|
-
Trace scope (--trace-scope, "trace_scope" in respan.json):
|
|
154
|
-
session one multi-root trace per pi session, across processes and pauses (default)
|
|
155
|
-
run one trace per agent run; runs of a session share a thread id
|
|
156
|
-
|
|
157
|
-
Durable delivery (--with-collector):
|
|
158
|
-
Sets base_url to a local Respan collector (http://127.0.0.1:4318) so traces
|
|
159
|
-
go through its persistent queue and survive Respan outages. Run
|
|
160
|
-
"respan collector start" once per machine.
|
|
161
|
-
|
|
162
|
-
Credentials are never written to these files: the extension reads
|
|
163
|
-
RESPAN_API_KEY or ~/.respan/credentials.json (from "respan auth login").`;
|
|
164
|
-
IntegratePi.examples = [
|
|
165
|
-
'respan integrate pi',
|
|
166
|
-
'respan integrate pi --disable',
|
|
167
|
-
'respan integrate pi --local',
|
|
168
|
-
'respan integrate pi --customer-id frank --workflow-name email-agent',
|
|
169
|
-
'respan integrate pi --trace-scope run',
|
|
170
|
-
'respan integrate pi --with-collector',
|
|
171
|
-
'respan integrate pi --project-id my-project --attrs \'{"env":"prod"}\'',
|
|
172
|
-
'respan integrate pi --dry-run',
|
|
173
|
-
];
|
|
174
|
-
IntegratePi.flags = {
|
|
175
|
-
...BaseCommand.baseFlags,
|
|
176
|
-
...integrateFlags,
|
|
177
|
-
// The shared descriptions advertise the claude-code default; pi's is "pi".
|
|
178
|
-
'workflow-name': Flags.string({
|
|
179
|
-
description: 'traceloop.workflow.name on pi turn spans (default: pi)',
|
|
180
|
-
}),
|
|
181
|
-
'span-name': Flags.string({
|
|
182
|
-
description: 'Alias of --workflow-name (default: pi)',
|
|
183
|
-
}),
|
|
184
|
-
'trace-scope': Flags.string({
|
|
185
|
-
description: 'Trace scope: "session" = one multi-root trace per pi session (default), "run" = one trace per agent run',
|
|
186
|
-
options: ['run', 'session'],
|
|
187
|
-
}),
|
|
188
|
-
'with-collector': Flags.boolean({
|
|
189
|
-
description: 'Send traces through a local Respan collector (respan collector start) for durable delivery',
|
|
190
|
-
default: false,
|
|
191
|
-
}),
|
|
192
|
-
'collector-port': Flags.integer({
|
|
193
|
-
description: `Port of the local collector, with --with-collector (default: ${DEFAULT_COLLECTOR_PORT})`,
|
|
194
|
-
min: 1,
|
|
195
|
-
max: 65535,
|
|
196
|
-
}),
|
|
197
|
-
};
|
|
198
|
-
export default IntegratePi;
|