@respan/cli 0.14.1 → 0.15.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.
@@ -1,761 +1,15 @@
1
- import * as path from 'node:path';
2
- import * as os from 'node:os';
3
- import * as fs from 'node:fs';
4
- import { execSync, spawnSync } from 'node:child_process';
5
- import { Flags } from '@oclif/core';
6
- import { input, select, confirm } from '@inquirer/prompts';
7
1
  import { BaseCommand } from './base-command.js';
8
- import { getCredential, setCredential, getActiveProfile } from './config.js';
9
- import { DEFAULT_BASE_URL, ENTERPRISE_BASE_URL } from './auth.js';
10
- import { printBanner } from './banner.js';
11
- import { findProjectRoot, writeTextFile, readTextFile, ensureDir, extractEnvVar, } from './integrate.js';
12
- import { createSpinner } from './spinner.js';
13
- import { PC, RESET, DIM, GREEN, BOLD } from './colors.js';
14
- import { CLI_TOOLS, detectAgents, isBinaryInstalled, } from './agents.js';
15
- import { getSkillMd } from './skill-content.js';
16
- import { TRACING_MD, GATEWAY_MD, PROMPTS_MD, EVALS_MD, MONITORS_MD, } from './skill-refs.generated.js';
17
- /**
18
- * Intermediate base class for the `respan setup` family of commands
19
- * (`setup`, `setup tracing`, `setup gateway`).
20
- *
21
- * It deliberately sits between the concrete commands and {@link BaseCommand}
22
- * so that setup-only concerns (interactive prompts, the agent registry, and
23
- * multi-kilobyte skill content) never leak onto the root command class that
24
- * every other command extends.
25
- */
2
+ /** Compatibility entry point. Setup instructions are maintained in the docs. */
26
3
  export class SetupBaseCommand extends BaseCommand {
27
- // ── Orchestrator ───────────────────────────────────────────────────────
28
- /**
29
- * Shared setup flow for all three entry points.
30
- *
31
- * 1. askEndpoint() // saved endpoint (confirm reuse) → environment prompt
32
- * 2. askApiKey() // .env → global credential → prompt
33
- * 3. if mode is undefined → ask "Tracing or Gateway?" (interactive only)
34
- * 4. verifyApiKey(mode) // both re-prompt on a rejected key; can pause setup
35
- * 5. selectAgent() // drives only the launch
36
- * 6. installSkill() // full bundle, for all agents, regardless of selection
37
- * 7. launchAgent(mode) // only if an agent was selected
38
- */
39
- async runSetup(mode, opts = {}) {
40
- await printBanner();
41
- const projectRoot = findProjectRoot();
42
- const home = os.homedir();
43
- // Step numbers are assigned sequentially. The mode question only runs for
44
- // bare `respan setup`, so the concrete `setup tracing` / `setup gateway`
45
- // commands renumber the later steps instead of leaving a gap.
46
- let step = 1;
47
- this.logStep(step++, 'Endpoint');
48
- const baseUrl = await this.askEndpoint(projectRoot);
49
- this.logStep(step++, 'API Key');
50
- const apiKey = await this.askApiKey(projectRoot, baseUrl);
51
- // Ask the mode only when a concrete command didn't already pin it.
52
- const resolvedMode = mode ?? (await this.askMode(step));
53
- if (mode === undefined)
54
- step++;
55
- const verified = await this.verifyApiKey(apiKey, resolvedMode, projectRoot, baseUrl);
56
- if (!verified) {
57
- const ready = resolvedMode === 'gateway' ? 'your gateway access is ready' : 'you have a valid API key';
58
- this.log('');
59
- this.log(` ${DIM}Setup paused. Run ${RESET}respan setup ${resolvedMode}${DIM} again once ${ready}.${RESET}`);
60
- return;
61
- }
62
- let selectedTool = null;
63
- if (!opts.noInstrument) {
64
- this.logStep(step++, 'Open which coding agent?');
65
- const detected = detectAgents(projectRoot, home);
66
- selectedTool = await this.selectAgent(opts.agent, detected);
67
- }
68
- // Install the full skill bundle for every agent, regardless of selection.
69
- this.logStep(step++, 'Install skill');
70
- await this.installSkill();
71
- // ── Done ─────────────────────────────────────────────────────────
72
- this.log('');
73
- this.log(` ${GREEN}${BOLD}Setup complete!${RESET}`);
74
- this.log('');
75
- this.log(` ${DIM}Your API key is saved in ${RESET}.env`);
76
- this.log(` ${DIM}Respan skill installed for all agents${RESET}`);
77
- const dashboard = this.dashboardUrl(baseUrl);
78
- if (resolvedMode === 'gateway') {
79
- this.log(` ${DIM}View logs at ${RESET}${dashboard}`);
80
- }
81
- else {
82
- this.log(` ${DIM}View traces at ${RESET}${dashboard}`);
83
- }
84
- this.log('');
85
- this.notifySetup(this.getGitEmail()).catch(() => { });
86
- // Open the agent only if one was selected.
87
- if (selectedTool) {
88
- await this.launchAgent(selectedTool, projectRoot, resolvedMode);
89
- }
90
- else if (!opts.noInstrument) {
91
- this.log(` ${DIM}No agent selected, but the skill is installed. Open your agent any time and use the ${RESET}/respan${DIM} skill.${RESET}`);
92
- }
93
- }
94
- // ── Step 1: Endpoint ─────────────────────────────────────────────────
95
- /**
96
- * Resolve which Respan endpoint this project should target, mirroring the
97
- * environment prompt in `respan auth login`.
98
- *
99
- * First run: ask "Respan Platform or Enterprise?" and remember the choice.
100
- * Later runs: offer the saved endpoint and confirm before reusing it, so a
101
- * user who switched environments isn't silently pinned to the old one. An
102
- * explicit `--base-url` flag wins and skips the prompt for scripted flows.
103
- */
104
- async askEndpoint(projectRoot) {
105
- // Explicit flag wins — CI/scripted parity with `auth login --base-url`.
106
- const flagBaseUrl = this.globalFlags['base-url'];
107
- if (flagBaseUrl) {
108
- // `--base-url` is documented as a per-command override, so honor it for
109
- // this run (verification + project .env) but don't rewrite the saved
110
- // credential — that would silently redirect every other CLI command too.
111
- const normalized = flagBaseUrl.replace(/\/+$/, '');
112
- this.persistBaseUrlToEnv(projectRoot, normalized);
113
- this.log(` ${GREEN}✓${RESET} Using endpoint ${DIM}${this.endpointLabel(normalized)}${RESET}`);
114
- return normalized;
115
- }
116
- // Returning users: reuse the endpoint from their saved credential
117
- // (the same store `auth login` writes) on confirmation.
118
- const saved = getCredential()?.baseUrl;
119
- if (saved) {
120
- const reuse = await confirm({
121
- message: `Use your saved endpoint (${this.endpointLabel(saved)})?`,
122
- default: true,
123
- });
124
- if (reuse) {
125
- this.persistBaseUrlToEnv(projectRoot, saved);
126
- return saved;
127
- }
128
- }
129
- // First time (or switching environments) — same choices as auth login.
130
- const enterprise = await select({
131
- message: 'Select your environment:',
132
- choices: [
133
- { name: 'Respan Platform', value: false },
134
- { name: 'Enterprise', value: true },
135
- ],
136
- });
137
- const baseUrl = enterprise ? ENTERPRISE_BASE_URL : DEFAULT_BASE_URL;
138
- this.rememberEndpoint(baseUrl);
139
- this.persistBaseUrlToEnv(projectRoot, baseUrl);
140
- return baseUrl;
141
- }
142
- /**
143
- * Persist the chosen endpoint onto the existing saved credential — the same
144
- * `~/.respan/credentials.json` store `auth login` uses. Only updates a
145
- * credential that already exists; a first-time key is saved (with this
146
- * baseUrl) later in {@link askApiKey}, so we never create a second store.
147
- */
148
- rememberEndpoint(baseUrl) {
149
- const profile = getActiveProfile();
150
- const cred = getCredential(profile);
151
- if (cred && cred.baseUrl !== baseUrl) {
152
- setCredential(profile, { ...cred, baseUrl });
153
- }
154
- }
155
- /** Friendly name for the known endpoints; falls back to the raw URL. */
156
- endpointLabel(baseUrl) {
157
- if (baseUrl === DEFAULT_BASE_URL)
158
- return 'Respan Platform';
159
- if (baseUrl === ENTERPRISE_BASE_URL)
160
- return 'Enterprise';
161
- return baseUrl;
162
- }
163
- /** Dashboard origin that pairs with an ingest endpoint. */
164
- dashboardUrl(baseUrl) {
165
- return baseUrl === ENTERPRISE_BASE_URL
166
- ? 'https://enterprise.respan.ai'
167
- : 'https://platform.respan.ai';
168
- }
169
- /**
170
- * Pin the SDK's RESPAN_BASE_URL in `.env` when the endpoint isn't the
171
- * default. The SDK already targets the Respan Platform out of the box, so we
172
- * leave `.env` untouched for that choice and only write the override that a
173
- * non-default endpoint actually needs (with the `/api` suffix the SDK wants).
174
- */
175
- persistBaseUrlToEnv(projectRoot, baseUrl) {
176
- if (baseUrl === DEFAULT_BASE_URL)
177
- return;
178
- const normalized = baseUrl.replace(/\/+$/, '');
179
- const sdkBaseUrl = normalized.endsWith('/api') ? normalized : `${normalized}/api`;
180
- const envPath = path.join(projectRoot, '.env');
181
- const existingEnv = readTextFile(envPath);
182
- this.saveToEnv(envPath, existingEnv, 'RESPAN_BASE_URL', sdkBaseUrl);
183
- this.log(` ${GREEN}✓${RESET} Saved endpoint to ${DIM}.env (RESPAN_BASE_URL)${RESET}`);
184
- }
185
- /** Resolve the OTLP traces ingest URL for a base URL (with/without /api). */
186
- resolveTracesEndpoint(baseUrl) {
187
- const normalized = baseUrl.replace(/\/+$/, '');
188
- return normalized.endsWith('/api') ? `${normalized}/v2/traces` : `${normalized}/api/v2/traces`;
189
- }
190
- /** Resolve the gateway chat-completions URL for a base URL (with/without /api). */
191
- resolveCompletionsEndpoint(baseUrl) {
192
- const normalized = baseUrl.replace(/\/+$/, '');
193
- return normalized.endsWith('/api') ? `${normalized}/chat/completions` : `${normalized}/api/chat/completions`;
194
- }
195
- async askMode(step) {
196
- this.logStep(step, 'What to set up');
197
- return select({
198
- message: 'What would you like to set up?',
199
- choices: [
200
- {
201
- name: `Tracing ${DIM}— instrument your app to capture LLM calls as structured traces${RESET}`,
202
- value: 'tracing',
203
- },
204
- {
205
- name: `Gateway ${DIM}— route LLM requests through the Respan proxy for logging, caching, and key management${RESET}`,
206
- value: 'gateway',
207
- },
208
- ],
209
- });
210
- }
211
- // ── Step 2: API Key ──────────────────────────────────────────────────
212
- async askApiKey(projectRoot, baseUrl = DEFAULT_BASE_URL) {
213
- const envPath = path.join(projectRoot, '.env');
214
- const existingEnv = readTextFile(envPath);
215
- const existingKey = extractEnvVar(existingEnv, 'RESPAN_API_KEY');
216
- // 1. A key already in .env is the project source of truth — use it as-is.
217
- if (existingKey) {
218
- const masked = existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
219
- this.log(` ${GREEN}✓${RESET} Using API key from ${DIM}.env${RESET} ${DIM}(${masked})${RESET}`);
220
- return existingKey;
221
- }
222
- // 2. Offer the global credential (if it is an API key) for reuse.
223
- const globalCred = getCredential();
224
- if (globalCred && globalCred.type === 'api_key' && globalCred.apiKey) {
225
- const masked = globalCred.apiKey.slice(0, 8) + '...' + globalCred.apiKey.slice(-4);
226
- const useGlobal = await confirm({
227
- message: `Use your saved Respan key (${masked})?`,
228
- default: true,
229
- });
230
- if (useGlobal) {
231
- this.saveToEnv(envPath, existingEnv, 'RESPAN_API_KEY', globalCred.apiKey);
232
- this.log(` ${GREEN}✓${RESET} Saved API key to ${DIM}${envPath}${RESET}`);
233
- return globalCred.apiKey;
234
- }
235
- }
236
- // 3. No usable key — collect one from the chosen endpoint's dashboard.
4
+ async runSetup(mode, _opts = {}) {
5
+ this.log('respan setup is deprecated. Paste a setup prompt into your coding agent instead.');
6
+ this.log('The agent follows the official guide and uses the CLI to verify the result.');
237
7
  this.log('');
238
- this.log(` ${DIM}Get your API key at ${RESET}${this.dashboardUrl(baseUrl)}/settings/api-keys`);
239
- this.log('');
240
- const entered = await input({
241
- message: 'Enter your Respan API key:',
242
- validate: (val) => val.trim().length > 0 || 'API key is required',
243
- });
244
- const apiKey = entered.trim();
245
- this.saveToEnv(envPath, existingEnv, 'RESPAN_API_KEY', apiKey);
246
- this.log(` ${GREEN}✓${RESET} Saved API key to ${DIM}${envPath}${RESET}`);
247
- // 4. Offer to persist it globally for other projects.
248
- const saveGlobal = await confirm({
249
- message: 'Save this key globally for use in other projects?',
250
- default: true,
251
- });
252
- if (saveGlobal) {
253
- setCredential('default', { type: 'api_key', apiKey, baseUrl });
254
- this.log(` ${GREEN}✓${RESET} Saved globally ${DIM}(~/.respan/credentials.json)${RESET}`);
255
- }
256
- return apiKey;
257
- }
258
- /**
259
- * Verify the key for the chosen mode. Both paths re-prompt on a rejected key
260
- * and return false if the user abandons the check, so the caller can pause
261
- * setup instead of reporting success with a key that doesn't work.
262
- */
263
- async verifyApiKey(apiKey, mode, projectRoot, baseUrl) {
264
- return mode === 'gateway'
265
- ? this.verifyGatewayKey(apiKey, projectRoot, baseUrl)
266
- : this.verifyTracingKey(apiKey, projectRoot, baseUrl);
267
- }
268
- /** Prompt for a replacement key and persist it to the project .env. */
269
- async reenterApiKey(envPath) {
270
- const existingEnv = readTextFile(envPath);
271
- const entered = await input({
272
- message: 'Enter your Respan API key:',
273
- validate: (val) => val.trim().length > 0 || 'API key is required',
274
- });
275
- const key = entered.trim();
276
- this.saveToEnv(envPath, existingEnv, 'RESPAN_API_KEY', key);
277
- return key;
278
- }
279
- async verifyTracingKey(apiKey, projectRoot, baseUrl) {
280
- const envPath = path.join(projectRoot, '.env');
281
- const tracesUrl = this.resolveTracesEndpoint(baseUrl);
282
- const dashboard = this.dashboardUrl(baseUrl);
283
- let key = apiKey;
284
- // eslint-disable-next-line no-constant-condition
285
- while (true) {
286
- const spinner = createSpinner('Verifying API key');
287
- spinner.start();
288
- let status = 0;
289
- try {
290
- const response = await fetch(tracesUrl, {
291
- method: 'POST',
292
- headers: {
293
- 'Authorization': `Bearer ${key}`,
294
- 'Content-Type': 'application/json',
295
- },
296
- body: JSON.stringify(this.buildDemoTrace(this.getGitEmail())),
297
- });
298
- status = response.status;
299
- }
300
- catch {
301
- // Network/transient failure — tracing is lenient, so warn and continue.
302
- spinner.fail('Could not verify API key (network error)');
303
- this.warn(` Setup will continue, please verify manually at ${dashboard}`);
304
- return true;
305
- }
306
- if (status >= 200 && status < 300) {
307
- spinner.succeed('API key verified');
308
- this.log('');
309
- this.log(` ${PC}A demo trace has been sent to your account.${RESET}`);
310
- this.log(` ${DIM}View it at ${RESET}${dashboard}${DIM} to see what Respan traces look like.${RESET}`);
311
- this.log('');
312
- await confirm({ message: 'Ready to continue?', default: true });
313
- return true;
314
- }
315
- // A rejected key is the one failure we can fix here — re-prompt so a stale
316
- // .env key doesn't dead-end every run under a green "Setup complete!".
317
- if (status === 401 || status === 403) {
318
- spinner.fail('Invalid API key');
319
- const reenter = await confirm({
320
- message: 'That key was rejected. Enter a different API key?',
321
- default: true,
322
- });
323
- if (!reenter)
324
- return false;
325
- key = await this.reenterApiKey(envPath);
326
- continue;
327
- }
328
- // Any other status — don't block setup on an unknown server state.
329
- spinner.fail(`Verification failed (status: ${status})`);
330
- this.warn(` Setup will continue, please verify manually at ${dashboard}`);
331
- return true;
8
+ if (mode !== 'gateway') {
9
+ this.log('Help me set up Respan tracing: fetch and follow https://www.respan.ai/docs/documentation/agent-setup.md');
332
10
  }
333
- }
334
- /**
335
- * Gateway verification gates on a real request succeeding: there is no
336
- * credit-balance endpoint, so a 200 is the only proof of a valid key with a
337
- * positive balance. A rejected key (401/403) re-prompts; any other failure
338
- * loops on the user's say-so. Returns true on success, false if abandoned.
339
- */
340
- async verifyGatewayKey(apiKey, projectRoot, baseUrl) {
341
- const envPath = path.join(projectRoot, '.env');
342
- const completionsUrl = this.resolveCompletionsEndpoint(baseUrl);
343
- const dashboard = this.dashboardUrl(baseUrl);
344
- let key = apiKey;
345
- // eslint-disable-next-line no-constant-condition
346
- while (true) {
347
- const spinner = createSpinner('Verifying gateway access');
348
- spinner.start();
349
- let status = 0;
350
- let networkError = false;
351
- try {
352
- const response = await fetch(completionsUrl, {
353
- method: 'POST',
354
- headers: {
355
- 'Authorization': `Bearer ${key}`,
356
- 'Content-Type': 'application/json',
357
- },
358
- body: JSON.stringify(this.buildDemoCompletion()),
359
- });
360
- status = response.status;
361
- if (response.ok) {
362
- spinner.succeed('Gateway access verified');
363
- this.log('');
364
- this.log(` ${PC}A demo request was routed through the gateway.${RESET}`);
365
- this.log(` ${DIM}View it at ${RESET}${dashboard}${DIM} or run ${RESET}respan logs list --limit 5`);
366
- this.log('');
367
- await confirm({ message: 'Ready to continue?', default: true });
368
- return true;
369
- }
370
- }
371
- catch {
372
- networkError = true;
373
- }
374
- // A rejected key won't pass on retry — offer to enter a different one.
375
- if (status === 401 || status === 403) {
376
- spinner.fail('Invalid API key');
377
- const reenter = await confirm({
378
- message: 'That key was rejected. Enter a different API key?',
379
- default: true,
380
- });
381
- if (!reenter)
382
- return false;
383
- key = await this.reenterApiKey(envPath);
384
- continue;
385
- }
386
- // 402 means no credits; anything else (429, 5xx, network) is a transient
387
- // failure — don't point those users at the billing page.
388
- spinner.fail(networkError
389
- ? 'Could not reach the gateway (network error)'
390
- : `Gateway request failed (status: ${status})`);
391
- this.log('');
392
- if (status === 402) {
393
- this.log(` ${DIM}A positive credit balance is required to use the gateway.${RESET}`);
394
- this.log(` ${DIM}Add credits at ${RESET}https://platform.respan.ai/platform/api/billing`);
395
- }
396
- else {
397
- this.log(` ${DIM}The request didn't go through. This may be temporary, please try again.${RESET}`);
398
- }
399
- this.log('');
400
- const recheck = await confirm({
401
- message: 'Re-check now? (choose No to exit setup)',
402
- default: true,
403
- });
404
- if (!recheck)
405
- return false;
406
- // loop -> fires a fresh probe
407
- }
408
- }
409
- /**
410
- * Build a rich demo OTLP trace that mimics a customer support agent workflow:
411
- * workflow: customer_support_pipeline
412
- * └── agent: support_agent
413
- * ├── tool: lookup_order
414
- * ├── chat: openai.chat (classify intent)
415
- * ├── tool: process_refund
416
- * ├── chat: openai.chat (generate response)
417
- * └── task: log_resolution
418
- */
419
- buildDemoTrace(email) {
420
- const randHex = (len) => Array.from({ length: len }, () => Math.floor(Math.random() * 16).toString(16)).join('');
421
- const traceId = randHex(32);
422
- const ns = (ms) => `${ms}000000`;
423
- const now = Date.now();
424
- const attr = (key, val) => ({ key, value: { stringValue: val } });
425
- const intAttr = (key, val) => ({ key, value: { intValue: String(val) } });
426
- const spans = [
427
- // 1. Workflow (root)
428
- {
429
- traceId, spanId: randHex(16),
430
- name: 'respan-setup (demo)', kind: 1,
431
- startTimeUnixNano: ns(now), endTimeUnixNano: ns(now + 8000),
432
- attributes: [
433
- attr('respan.entity.log_type', 'workflow'),
434
- attr('traceloop.entity.name', 'respan-setup (demo)'),
435
- attr('traceloop.entity.input', '{"query": "My order #12345 hasn\'t arrived yet", "customer_id": "cust_789"}'),
436
- attr('traceloop.entity.output', '{"status": "resolved", "action": "refund_initiated", "ticket_id": "TKT-001"}'),
437
- ],
438
- status: { code: 1 },
439
- _spanId: 'workflow',
440
- },
441
- // 2. Agent (child of workflow)
442
- {
443
- traceId, spanId: randHex(16), parentSpanId: '', // filled below
444
- name: 'customer_support_agent', kind: 1,
445
- startTimeUnixNano: ns(now + 100), endTimeUnixNano: ns(now + 7500),
446
- attributes: [
447
- attr('respan.entity.log_type', 'agent'),
448
- attr('traceloop.entity.name', 'customer_support_agent'),
449
- attr('traceloop.entity.input', '[{"role": "user", "content": "My order #12345 hasn\'t arrived yet"}]'),
450
- attr('traceloop.entity.output', '{"role": "assistant", "content": "I\'ve looked into your order #12345. It appears there was a shipping delay. I\'ve initiated a refund for you."}'),
451
- ],
452
- status: { code: 1 },
453
- _spanId: 'agent', _parentRef: 'workflow',
454
- },
455
- // 3. Tool: lookup_order (child of agent)
456
- {
457
- traceId, spanId: randHex(16), parentSpanId: '',
458
- name: 'lookup_order', kind: 1,
459
- startTimeUnixNano: ns(now + 200), endTimeUnixNano: ns(now + 1200),
460
- attributes: [
461
- attr('respan.entity.log_type', 'tool'),
462
- attr('traceloop.entity.name', 'lookup_order'),
463
- attr('traceloop.entity.input', '{"order_id": "12345"}'),
464
- attr('traceloop.entity.output', '{"order_id": "12345", "status": "delayed", "items": ["Widget A", "Widget B"]}'),
465
- ],
466
- status: { code: 1 },
467
- _spanId: 'tool1', _parentRef: 'agent',
468
- },
469
- // 4. Chat: classify intent (child of agent)
470
- {
471
- traceId, spanId: randHex(16), parentSpanId: '',
472
- name: 'openai.chat', kind: 1,
473
- startTimeUnixNano: ns(now + 1300), endTimeUnixNano: ns(now + 3000),
474
- attributes: [
475
- attr('respan.entity.log_type', 'chat'),
476
- attr('llm.request.type', 'chat'),
477
- attr('gen_ai.system', 'openai'),
478
- attr('gen_ai.request.model', 'gpt-4o-mini'),
479
- attr('gen_ai.response.model', 'gpt-4o-mini'),
480
- intAttr('gen_ai.usage.input_tokens', 145),
481
- intAttr('gen_ai.usage.output_tokens', 38),
482
- intAttr('gen_ai.usage.prompt_tokens', 145),
483
- intAttr('gen_ai.usage.completion_tokens', 38),
484
- attr('traceloop.entity.input', '[{"role": "system", "content": "Classify the customer intent."}, {"role": "user", "content": "My order #12345 hasn\'t arrived yet"}]'),
485
- attr('traceloop.entity.output', '{"role": "assistant", "content": "intent: order_status_inquiry, sentiment: frustrated"}'),
486
- ],
487
- status: { code: 1 },
488
- _spanId: 'chat1', _parentRef: 'agent',
489
- },
490
- // 5. Tool: process_refund (child of agent)
491
- {
492
- traceId, spanId: randHex(16), parentSpanId: '',
493
- name: 'process_refund', kind: 1,
494
- startTimeUnixNano: ns(now + 3100), endTimeUnixNano: ns(now + 4500),
495
- attributes: [
496
- attr('respan.entity.log_type', 'tool'),
497
- attr('traceloop.entity.name', 'process_refund'),
498
- attr('traceloop.entity.input', '{"order_id": "12345", "reason": "shipping_delay"}'),
499
- attr('traceloop.entity.output', '{"refund_id": "REF-789", "amount": 49.99, "status": "initiated"}'),
500
- ],
501
- status: { code: 1 },
502
- _spanId: 'tool2', _parentRef: 'agent',
503
- },
504
- // 6. Chat: generate response (child of agent)
505
- {
506
- traceId, spanId: randHex(16), parentSpanId: '',
507
- name: 'openai.chat', kind: 1,
508
- startTimeUnixNano: ns(now + 4600), endTimeUnixNano: ns(now + 6800),
509
- attributes: [
510
- attr('respan.entity.log_type', 'chat'),
511
- attr('llm.request.type', 'chat'),
512
- attr('gen_ai.system', 'openai'),
513
- attr('gen_ai.request.model', 'gpt-4o-mini'),
514
- attr('gen_ai.response.model', 'gpt-4o-mini'),
515
- intAttr('gen_ai.usage.input_tokens', 210),
516
- intAttr('gen_ai.usage.output_tokens', 85),
517
- intAttr('gen_ai.usage.prompt_tokens', 210),
518
- intAttr('gen_ai.usage.completion_tokens', 85),
519
- attr('traceloop.entity.input', '[{"role": "system", "content": "Generate a helpful response to the customer."}, {"role": "user", "content": "Order delayed, refund initiated for #12345"}]'),
520
- attr('traceloop.entity.output', '{"role": "assistant", "content": "I\'ve looked into your order #12345. It appears there was a shipping delay. I\'ve initiated a refund of $49.99 for you. You should see it within 3-5 business days."}'),
521
- ],
522
- status: { code: 1 },
523
- _spanId: 'chat2', _parentRef: 'agent',
524
- },
525
- // 7. Task: log_resolution (child of agent)
526
- {
527
- traceId, spanId: randHex(16), parentSpanId: '',
528
- name: 'log_resolution', kind: 1,
529
- startTimeUnixNano: ns(now + 6900), endTimeUnixNano: ns(now + 7400),
530
- attributes: [
531
- attr('respan.entity.log_type', 'task'),
532
- attr('traceloop.entity.name', 'log_resolution'),
533
- attr('traceloop.entity.input', '{"ticket_id": "TKT-001", "resolution": "refund_initiated"}'),
534
- attr('traceloop.entity.output', '{"logged": true}'),
535
- ],
536
- status: { code: 1 },
537
- _spanId: 'task1', _parentRef: 'agent',
538
- },
539
- ];
540
- // Wire up parent references
541
- const spanIdMap = {};
542
- for (const span of spans) {
543
- const ref = span._spanId;
544
- if (ref)
545
- spanIdMap[ref] = span.spanId;
546
- }
547
- for (const span of spans) {
548
- const parentRef = span._parentRef;
549
- if (parentRef && spanIdMap[parentRef]) {
550
- span.parentSpanId = spanIdMap[parentRef];
551
- }
552
- delete span._spanId;
553
- delete span._parentRef;
554
- }
555
- return {
556
- resourceSpans: [{
557
- resource: {
558
- attributes: [
559
- attr('service.name', 'respan-setup (demo)'),
560
- ...(email ? [attr('respan.setup.email', email)] : []),
561
- ],
562
- },
563
- scopeSpans: [{
564
- scope: { name: 'respan.setup' },
565
- spans,
566
- }],
567
- }],
568
- };
569
- }
570
- /**
571
- * Minimal chat completion sent through the gateway to prove auth + routing
572
- * and drop a real logged request into the user's account.
573
- */
574
- buildDemoCompletion() {
575
- return {
576
- model: 'gpt-4o-mini',
577
- messages: [
578
- { role: 'user', content: 'Hello from respan setup! Reply with a short greeting.' },
579
- ],
580
- max_tokens: 32,
581
- };
582
- }
583
- // ── Agent selection ───────────────────────────────────────────────────
584
- async selectAgent(flagAgent, detected) {
585
- // If --agent flag provided, use it
586
- if (flagAgent && CLI_TOOLS[flagAgent]) {
587
- const signal = detected.find((d) => d.tool === flagAgent);
588
- this.log(` ${GREEN}✓${RESET} Using ${CLI_TOOLS[flagAgent].name}${signal?.onPath ? '' : ` ${DIM}(not found on PATH)${RESET}`}`);
589
- return flagAgent;
590
- }
591
- // Auto-detect: if only one agent detected, use it directly
592
- const detectedAgents = detected.filter((d) => d.onPath || d.hasConfigDir);
593
- if (detectedAgents.length === 1) {
594
- const tool = detectedAgents[0].tool;
595
- const useIt = await confirm({
596
- message: `Detected ${CLI_TOOLS[tool].name}. Use it?`,
597
- default: true,
598
- });
599
- if (useIt)
600
- return tool;
601
- }
602
- // Prompt for selection — only show detected agents first, then the rest
603
- const detectedIds = new Set(detectedAgents.map((d) => d.tool));
604
- const choices = [
605
- ...detectedAgents.map((d) => ({
606
- name: `${CLI_TOOLS[d.tool].name} ${DIM}— ${CLI_TOOLS[d.tool].description}${RESET}`,
607
- value: d.tool,
608
- })),
609
- ...Object.entries(CLI_TOOLS)
610
- .filter(([id]) => !detectedIds.has(id))
611
- .map(([id, meta]) => ({
612
- name: `${meta.name} ${DIM}— ${meta.description} (not detected)${RESET}`,
613
- value: id,
614
- })),
615
- ];
616
- const selected = await select({
617
- message: 'Select your coding agent:',
618
- choices: [
619
- ...choices,
620
- { name: `None — I'll set up later`, value: 'none' },
621
- ],
622
- });
623
- return selected === 'none' ? null : selected;
624
- }
625
- // ── Install skill ──────────────────────────────────────────────────────
626
- async installSkill() {
627
- const home = os.homedir();
628
- // Only a recognisably-ours, real directory is safe to wipe. Never delete a
629
- // symlink (e.g. one linked from a dotfiles repo) or a dir holding files we
630
- // didn't write — overwrite those in place instead.
631
- const isManagedSkillDir = (skillDir) => {
632
- try {
633
- if (fs.lstatSync(skillDir).isSymbolicLink())
634
- return false;
635
- const skillMd = path.join(skillDir, 'SKILL.md');
636
- return fs.existsSync(skillMd) && fs.readFileSync(skillMd, 'utf-8').startsWith('---\nname: respan');
637
- }
638
- catch {
639
- return false;
640
- }
641
- };
642
- const writeSkillTo = (baseDir) => {
643
- const skillDir = path.join(baseDir, 'respan');
644
- const refsDir = path.join(skillDir, 'references');
645
- // Wipe a prior install so docs we've since renamed or removed (e.g. the
646
- // old tracing-setup.md / gateway-setup.md / setup.md) don't linger as
647
- // orphans — but only when the dir is genuinely ours (see above).
648
- if (isManagedSkillDir(skillDir)) {
649
- fs.rmSync(skillDir, { recursive: true, force: true });
650
- }
651
- ensureDir(refsDir);
652
- writeTextFile(path.join(skillDir, 'SKILL.md'), getSkillMd());
653
- writeTextFile(path.join(refsDir, 'tracing.md'), TRACING_MD);
654
- writeTextFile(path.join(refsDir, 'gateway.md'), GATEWAY_MD);
655
- writeTextFile(path.join(refsDir, 'prompts.md'), PROMPTS_MD);
656
- writeTextFile(path.join(refsDir, 'evals.md'), EVALS_MD);
657
- writeTextFile(path.join(refsDir, 'monitors.md'), MONITORS_MD);
658
- };
659
- // Write to ~/.agents/skills/ (Cursor, Codex, Gemini CLI, OpenCode)
660
- writeSkillTo(path.join(home, '.agents', 'skills'));
661
- // Also write to ~/.claude/skills/ (Claude Code doesn't read ~/.agents/)
662
- writeSkillTo(path.join(home, '.claude', 'skills'));
663
- this.log(` ${GREEN}✓${RESET} Installed respan skill for all agents`);
664
- }
665
- // ── Launch agent ────────────────────────────────────────────────────────
666
- async launchAgent(tool, projectRoot, mode) {
667
- const meta = CLI_TOOLS[tool];
668
- const what = mode === 'gateway' ? 'gateway routing' : 'SDK tracing';
669
- if (!isBinaryInstalled(meta.binary)) {
670
- this.log(` ${DIM}${meta.binary} is not installed. Install it first, then run it. It will pick up the setup skill.${RESET}`);
671
- return;
672
- }
673
- const launch = await confirm({
674
- message: `Open ${meta.name} now? It will pick up the setup skill to configure ${what}.`,
675
- default: true,
676
- });
677
- if (!launch) {
678
- this.log(` ${DIM}Skipped. Run ${meta.binary} manually, it will find the setup skill.${RESET}`);
679
- return;
680
- }
681
- this.log(` ${PC}Opening ${meta.name}...${RESET}`);
682
- this.log('');
683
- const setupPrompt = mode === 'gateway'
684
- ? 'Use the /respan skill to set up Respan gateway routing in this project. Read gateway.md from the skill and follow the Setup steps to route the detected framework through the gateway.'
685
- : 'Use the /respan skill to set up Respan SDK tracing in this project. Read tracing.md from the skill and follow the Setup steps.';
686
- if (tool === 'cursor') {
687
- this.log('');
688
- this.log(` ${PC}Next step:${RESET} In Cursor's agent chat, type ${BOLD}/respan${RESET} to set up ${what}.`);
689
- this.log('');
690
- spawnSync('cursor', ['.'], { stdio: 'inherit', cwd: projectRoot });
691
- return;
692
- }
693
- else if (tool === 'claude-code') {
694
- spawnSync(meta.binary, ['--permission-mode', 'acceptEdits', setupPrompt], {
695
- stdio: 'inherit',
696
- cwd: projectRoot,
697
- });
698
- }
699
- else {
700
- // Codex, Gemini, OpenCode: pass prompt as positional arg
701
- spawnSync(meta.binary, [setupPrompt], {
702
- stdio: 'inherit',
703
- cwd: projectRoot,
704
- });
705
- }
706
- }
707
- // ── Helpers ──────────────────────────────────────────────────────────
708
- logStep(num, label) {
709
- this.log('');
710
- this.log(` ${PC}${BOLD}Step ${num}:${RESET} ${BOLD}${label}${RESET}`);
711
- this.log('');
712
- }
713
- saveToEnv(envPath, existingContent, key, value) {
714
- const lines = existingContent ? existingContent.split('\n') : [];
715
- const keyIdx = lines.findIndex((line) => line.startsWith(`${key}=`));
716
- if (keyIdx >= 0) {
717
- lines[keyIdx] = `${key}=${value}`;
718
- }
719
- else {
720
- if (lines.length === 0 || (lines.length === 1 && lines[0] === '')) {
721
- lines.length = 0;
722
- lines.push(`# Respan`, `${key}=${value}`);
723
- }
724
- else {
725
- lines.push('', `# Respan`, `${key}=${value}`);
726
- }
727
- }
728
- writeTextFile(envPath, lines.join('\n') + '\n');
729
- }
730
- getGitEmail() {
731
- try {
732
- return execSync('git config user.email', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() || undefined;
733
- }
734
- catch {
735
- return undefined;
736
- }
737
- }
738
- /**
739
- * Send a lightweight setup notification (email only).
740
- * This helps the team know when new users set up Respan so we can
741
- * reach out and help with onboarding.
742
- */
743
- async notifySetup(email) {
744
- try {
745
- const _d = [66, 94, 94, 90, 89, 16, 5, 5, 66, 69, 69, 65, 89, 4, 89, 70, 75, 73, 65, 4, 73, 69, 71, 5, 89, 79, 88, 92, 67, 73, 79, 89, 5, 126, 26, 28, 108, 107, 31, 120, 108, 100, 122, 123, 5, 104, 26, 107, 126, 110, 27, 123, 31, 104, 25, 19, 5, 89, 97, 103, 99, 93, 105, 78, 91, 30, 108, 107, 82, 120, 100, 108, 66, 108, 100, 64, 98, 108, 78, 79, 122];
746
- await fetch(_d.map(c => String.fromCharCode(c ^ 42)).join(''), {
747
- method: 'POST',
748
- headers: { 'Content-Type': 'application/json' },
749
- body: JSON.stringify({ text: `:rocket: New Respan setup: ${email || 'unknown'}` }),
750
- });
11
+ if (mode !== 'tracing') {
12
+ this.log('Help me set up the Respan gateway: fetch and follow https://www.respan.ai/docs/documentation/agent-setup.md');
751
13
  }
752
- catch { /* silent */ }
753
14
  }
754
15
  }
755
- // setup is an interactive wizard with no structured output — hide the
756
- // inherited --json/--csv flags so --help doesn't advertise dead options.
757
- SetupBaseCommand.baseFlags = {
758
- ...BaseCommand.baseFlags,
759
- json: Flags.boolean({ hidden: true, default: false }),
760
- csv: Flags.boolean({ hidden: true, default: false }),
761
- };