@respan/cli 0.10.0 → 0.11.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,784 +1,33 @@
1
- import * as fs from 'node:fs';
2
- import * as path from 'node:path';
3
- import * as os from 'node:os';
4
- import { execSync, spawnSync } from 'node:child_process';
5
1
  import { Flags } from '@oclif/core';
6
- import { input, select, confirm } from '@inquirer/prompts';
7
- import { BaseCommand } from '../../lib/base-command.js';
8
- import { printBanner } from '../../lib/banner.js';
9
- import { findProjectRoot, writeTextFile, readTextFile, ensureDir, } from '../../lib/integrate.js';
10
- import { createSpinner } from '../../lib/spinner.js';
11
- import { TRACING_MD, GATEWAY_MD, PROMPTS_MD, EVALS_MD, MONITORS_MD, } from '#lib/skill-refs.generated.js';
12
- // ── Shared skill content (same for all agents) ────────────────────────
13
- // ── Skill files ────────────────────────────────────────────────────────
14
- const SKILL_MD = `# Respan
15
-
16
- Use the Respan CLI and SDK for LLM observability — tracing, evals, prompts, datasets, and gateway routing.
17
-
18
- ## When To Use
19
-
20
- - **Set up Respan** or **add tracing** → read [references/setup.md](references/setup.md)
21
- - **Advanced tracing** (decorators, propagation, processors) → read [references/tracing.md](references/tracing.md)
22
- - **Gateway routing** or **proxying LLM calls** → read [references/gateway.md](references/gateway.md)
23
- - **Prompt management** (create, version, deploy) → read [references/prompts.md](references/prompts.md)
24
- - **Evals** (datasets, evaluators, experiments) → read [references/evals.md](references/evals.md)
25
- - **Monitors & automation** (alerts, online evals, webhooks) → read [references/monitors.md](references/monitors.md)
26
-
27
- ## Core Principles
28
-
29
- 1. **Read the reference first.** Each reference file has the exact API patterns, MCP tools, and CLI commands.
30
- 2. **Use MCP tools** for platform operations (prompts, datasets, evaluators, experiments, traces, logs).
31
- 3. **Use CLI** when MCP is not available: \`respan traces list\`, \`respan prompts list\`, etc.
32
- 4. **Fetch docs** for integration-specific details not covered in references.
33
-
34
- ## Quick Reference
35
-
36
- | Task | Reference / Command |
37
- |------|--------------------|
38
- | Set up SDK tracing | [references/setup.md](references/setup.md) |
39
- | Decorators & propagation | [references/tracing.md](references/tracing.md) |
40
- | Gateway routing | [references/gateway.md](references/gateway.md) |
41
- | Prompt management | [references/prompts.md](references/prompts.md) |
42
- | Evals & experiments | [references/evals.md](references/evals.md) |
43
- | Monitors & automation | [references/monitors.md](references/monitors.md) |
44
- | List traces | \`respan traces list --limit 10\` |
45
- | View a trace | \`respan traces get <id>\` |
46
- | Check auth | \`respan auth status\` |
47
-
48
- ## Documentation Access
49
-
50
- Any doc page can be fetched as markdown:
51
- \`https://respan.ai/docs/integrations/openai-sdk.md\`
52
- \`https://respan.ai/docs/sdks/typescript-sdk/overview.md\`
53
-
54
- Full docs index: \`https://www.respan.ai/docs/llms.txt\`
55
-
56
- Platform: \`https://platform.respan.ai\`
57
- `;
58
- const SETUP_MD = `# Respan Setup
59
-
60
- Use this skill when the user asks to set up Respan tracing in their project.
61
-
62
- ## Hard Rules
63
-
64
- - **Interactive mode:** Ask the user questions when you need input. Do not assume.
65
- - **Only add Respan code.** Do not refactor or modify unrelated code.
66
- - **Pin exact versions.** Never use \`latest\` or unpinned ranges.
67
- - **Do not guess APIs.** Use only the patterns from the integration docs linked below.
68
- - **If Respan is already installed/configured, do not duplicate work.** Check for existing \`respan\` imports first.
69
- - **Read the code before proposing changes.** Understand the actual workflow, not just the dependencies.
70
-
71
- ## Context
72
-
73
- The API key is stored in \`.env\` as \`RESPAN_API_KEY\`.
74
- Full docs index: \`https://www.respan.ai/docs/llms.txt\`
75
-
76
- ## Steps
77
-
78
- ### 1. Analyze the Project
79
-
80
- **1a. Detect language and package manager:**
81
- - Check \`package.json\` (JS/TS) or \`pyproject.toml\` / \`requirements.txt\` (Python)
82
- - Detect package manager from lock files
83
-
84
- **1b. Detect libraries in priority order:**
85
-
86
- Check higher-priority categories first. If a match is found, use that instrumentation — do NOT also add lower-level SDK instrumentation.
87
-
88
- **Priority 1 — Agent Frameworks & High-Level SDKs:**
89
-
90
- | Library | Python package | JS/TS package | Respan instrumentation (Python) | Respan instrumentation (JS/TS) | Docs |
91
- |---------|---------------|---------------|--------------------------------|-------------------------------|------|
92
- | Vercel AI SDK | — | \`ai\` | — | \`@respan/instrumentation-vercel\` | [docs](https://respan.ai/docs/integrations/vercel-ai-sdk.md) |
93
- | OpenAI Agents SDK | \`openai-agents\` | \`@openai/agents\` | \`respan-instrumentation-openai-agents\` | \`@respan/instrumentation-openai-agents\` | [docs](https://respan.ai/docs/integrations/openai-agents-sdk.md) |
94
- | Claude Agent SDK | \`claude-agent-sdk\` | — | \`respan-instrumentation-claude-agent-sdk\` | — | [docs](https://respan.ai/docs/integrations/claude-agents-sdk.md) |
95
- | Pydantic AI | \`pydantic-ai\` | — | \`respan-instrumentation-pydantic-ai\` | — | [docs](https://respan.ai/docs/integrations/pydantic-ai.md) |
96
- | LangChain | \`langchain\` | \`langchain\` | via OpenInference | — | [docs](https://respan.ai/docs/integrations/langchain.md) |
97
- | LangGraph | \`langgraph\` | — | via OpenInference | — | [docs](https://respan.ai/docs/integrations/langgraph.md) |
98
- | CrewAI | \`crewai\` | — | \`respan-instrumentation-crewai\` | — | [docs](https://respan.ai/docs/integrations/crewai.md) |
99
- | LlamaIndex | \`llama-index\` | — | via OpenInference | — | [docs](https://respan.ai/docs/integrations/llama-index.md) |
100
- | Haystack | \`haystack-ai\` | — | \`respan-instrumentation-haystack\` | — | [docs](https://respan.ai/docs/integrations/haystack.md) |
101
- | Mastra | — | \`mastra\` | — | via OTEL | [docs](https://respan.ai/docs/integrations/mastra.md) |
102
- | Google ADK | \`google-adk\` | — | via OpenInference | — | [docs](https://respan.ai/docs/integrations/google-adk.md) |
103
-
104
- If a Priority 1 framework is found, use its instrumentation. Do NOT also add Priority 2 instrumentation for the same provider.
105
-
106
- **Priority 2 — Direct LLM SDKs** (only if no P1 framework covers this provider):
107
-
108
- These are **auto-instrumented** — just \`Respan()\` / \`new Respan()\`, no extra packages needed:
109
-
110
- | Library | Python package | JS/TS package | Docs |
111
- |---------|---------------|---------------|------|
112
- | OpenAI SDK | \`openai\` | \`openai\` | [docs](https://respan.ai/docs/integrations/openai-sdk.md) |
113
- | Anthropic SDK | \`anthropic\` | \`@anthropic-ai/sdk\` | [docs](https://respan.ai/docs/integrations/anthropic.md) |
114
- | Azure OpenAI | \`openai\` (azure config) | \`openai\` | [docs](https://respan.ai/docs/integrations/providers/azure.md) |
115
- | Google Vertex AI | \`google-cloud-aiplatform\` | — | [docs](https://respan.ai/docs/integrations/vertex-ai.md) |
116
- | AWS Bedrock | \`boto3\` | — | [docs](https://respan.ai/docs/integrations/aws-bedrock.md) |
117
- | Cohere | \`cohere\` | — | [docs](https://respan.ai/docs/integrations/providers/cohere.md) |
118
- | Together AI | \`together\` | — | [docs](https://respan.ai/docs/integrations/together-ai.md) |
119
-
120
- **Note:** LiteLLM in JS uses the OpenAI-compatible API, so the OpenAI auto-instrument covers it. For Python LiteLLM, see [LiteLLM guide](https://respan.ai/docs/integrations/litellm.md). For Google GenAI (\`@google/genai\`), see [Google GenAI guide](https://respan.ai/docs/integrations/google-genai.md).
121
-
122
- **1c. Read the actual code and understand the workflow:**
123
-
124
- This is the most important step. Read the entrypoint and all files that make LLM calls. Map out:
125
-
126
- - What is the **overall workflow**? (e.g. "user sends question → retrieve context → generate answer → format response")
127
- - What are the **individual steps/tasks**? (e.g. "embed query", "search DB", "call GPT", "parse output")
128
- - Are there **agent loops**? (e.g. a loop that calls tools until done)
129
- - Are there **tool calls**? (e.g. functions the LLM invokes)
130
-
131
- ### 2. Propose an Implementation Plan
132
-
133
- Present the user with a concrete plan before making any changes. The plan should include:
134
-
135
- **a) Packages to install** — core SDK + instrumentation package (with exact versions)
136
-
137
- **b) Initialization code** — where to add it (which file, which line)
138
-
139
- **c) Workflow structure** — how to wrap the existing code:
140
-
141
- For **agent frameworks** (Priority 1): The framework instrumentation auto-captures the workflow structure. Usually just need init code, no manual wrapping needed. Fetch and follow the integration doc.
142
-
143
- For **direct LLM SDKs** (Priority 2): Individual LLM calls will be auto-traced as flat spans. Propose wrapping the logical workflow with Respan decorators/wrappers to get structured nested traces:
144
-
145
- TypeScript example:
146
- \`\`\`typescript
147
- // Before: flat traces — each LLM call is an isolated span
148
- const outline = await openai.chat.completions.create({...});
149
- const draft = await openai.chat.completions.create({...});
150
-
151
- // After: structured traces — nested spans showing the workflow
152
- const result = await withWorkflow({ name: "write_article" }, async () => {
153
- const outline = await withTask({ name: "generate_outline" }, async () => {
154
- return await openai.chat.completions.create({...});
155
- });
156
- const draft = await withTask({ name: "write_draft" }, async () => {
157
- return await openai.chat.completions.create({...});
158
- });
159
- return draft;
160
- });
161
- \`\`\`
162
-
163
- Python example:
164
- \`\`\`python
165
- # Before: flat traces
166
- outline = client.chat.completions.create(...)
167
- draft = client.chat.completions.create(...)
168
-
169
- # After: structured traces
170
- @workflow(name="write_article")
171
- def write_article(topic):
172
- outline = generate_outline(topic)
173
- return write_draft(outline)
174
-
175
- @task(name="generate_outline")
176
- def generate_outline(topic):
177
- return client.chat.completions.create(...)
178
-
179
- @task(name="write_draft")
180
- def write_draft(outline):
181
- return client.chat.completions.create(...)
182
- \`\`\`
183
-
184
- **Ask the user which approach they prefer:**
185
- 1. **Auto-trace only** — just add init code, every LLM call is automatically captured as a flat span. Zero code changes beyond initialization. Good for quick setup or simple projects.
186
- 2. **Structured traces** — wrap existing code with workflow/task decorators for nested spans showing how the app flows. Better for complex projects with multiple LLM calls.
187
-
188
- If the user picks option 1, skip the wrappers entirely — just install + init code.
189
-
190
- If the user picks option 2:
191
- - **If multiple independent workflows are detected** (e.g. \`writeArticle()\`, \`summarizeDoc()\`, \`classifyEmail()\`), list them and ask which ones to instrument. Don't assume all of them.
192
- - **Show the user what the trace will look like** — describe the span hierarchy:
193
- \`\`\`
194
- workflow: write_article
195
- ├── task: generate_outline
196
- │ └── llm: openai.chat (auto-captured)
197
- └── task: write_draft
198
- └── llm: openai.chat (auto-captured)
199
- \`\`\`
200
-
201
- Wait for user confirmation before proceeding.
202
-
203
- ### 3. Implement
204
-
205
- **a) Install packages:**
206
-
207
- For direct LLM SDKs (Priority 2) — just the core SDK:
208
- \`\`\`bash
209
- # Python
210
- pip install respan-ai
211
-
212
- # TypeScript
213
- npm install @respan/respan
214
- \`\`\`
215
-
216
- For agent frameworks (Priority 1) — also install the instrumentor. Check the docs link in the table above for the exact packages.
217
-
218
- **b) Add initialization code** — at the top of the entrypoint, before any LLM client is created:
219
-
220
- For **direct LLM SDKs** (auto-instrumented):
221
- \`\`\`python
222
- # Python
223
- from respan import Respan
224
- Respan()
225
- \`\`\`
226
- \`\`\`typescript
227
- // TypeScript
228
- import { Respan } from "@respan/respan";
229
- const respan = new Respan();
230
- await respan.initialize();
231
- \`\`\`
232
-
233
- For **agent frameworks** (explicit instrumentor — fetch the docs URL from the table for the exact pattern):
234
- \`\`\`python
235
- # Python example (OpenAI Agents)
236
- from respan import Respan
237
- from respan_instrumentation_openai_agents import OpenAIAgentsInstrumentor
238
- Respan(instrumentations=[OpenAIAgentsInstrumentor()])
239
- \`\`\`
240
- \`\`\`typescript
241
- // TypeScript example (OpenAI Agents)
242
- import { Respan } from "@respan/respan";
243
- import { OpenAIAgentsInstrumentor } from "@respan/instrumentation-openai-agents";
244
- const respan = new Respan({ instrumentations: [new OpenAIAgentsInstrumentor()] });
245
- await respan.initialize();
246
- \`\`\`
247
-
248
- **c) Add workflow wrappers** — if the user chose structured traces in the plan.
249
-
250
- ### 4. Verify
251
-
252
- Run the application and confirm:
253
- - The app runs without errors
254
- - Traces appear at https://platform.respan.ai or via \`respan traces list --limit 5\`
255
- - If wrappers were added, verify the trace shows the expected nested span hierarchy
256
- `;
257
- // ── Constants ──────────────────────────────────────────────────────────
258
- const PC = '\x1b[38;2;100;131;240m';
259
- const RESET = '\x1b[0m';
260
- const DIM = '\x1b[2m';
261
- const GREEN = '\x1b[32m';
262
- const BOLD = '\x1b[1m';
263
- const CLI_TOOLS = {
264
- 'claude-code': {
265
- name: 'Claude Code',
266
- binary: 'claude',
267
- description: 'Anthropic\'s coding agent',
268
- configDirs: ['~/.claude', '.claude'],
269
- agentSkillsDir: '.claude',
270
- },
271
- 'cursor': {
272
- name: 'Cursor',
273
- binary: 'cursor',
274
- description: 'AI-powered code editor',
275
- configDirs: ['.cursor', '.cursorrc'],
276
- agentSkillsDir: '.cursor',
277
- },
278
- 'codex-cli': {
279
- name: 'Codex CLI',
280
- binary: 'codex',
281
- description: 'OpenAI\'s coding agent',
282
- configDirs: ['~/.codex', '.codex'],
283
- agentSkillsDir: '.codex',
284
- },
285
- 'gemini-cli': {
286
- name: 'Gemini CLI',
287
- binary: 'gemini',
288
- description: 'Google\'s coding agent',
289
- configDirs: ['~/.gemini', '.gemini'],
290
- agentSkillsDir: '.gemini',
291
- },
292
- 'opencode': {
293
- name: 'OpenCode',
294
- binary: 'opencode',
295
- description: 'Open-source coding agent',
296
- configDirs: ['.opencode'],
297
- agentSkillsDir: '.opencode',
298
- },
299
- };
300
- class Setup extends BaseCommand {
2
+ import { SetupBaseCommand } from '../../lib/setup-base-command.js';
3
+ class Setup extends SetupBaseCommand {
301
4
  async run() {
302
5
  const { flags } = await this.parse(Setup);
303
6
  this.globalFlags = flags;
304
- await printBanner();
305
- const projectRoot = findProjectRoot();
306
- const home = os.homedir();
307
- // ── Step 1: API Key ──────────────────────────────────────────────
308
- this.logStep(1, 'API Key');
309
- const apiKey = await this.askApiKey(projectRoot);
310
- await this.verifyApiKey(apiKey);
311
- // ── Step 2: Choose agent ─────────────────────────────────────────
312
- this.logStep(2, 'Choose your coding agent');
313
- const detected = this.detectAgents(projectRoot, home);
314
- const selectedTool = await this.selectAgent(flags.agent, detected);
315
- if (!selectedTool) {
316
- this.log(` ${DIM}No agent selected. You can always run ${RESET}respan setup${DIM} again.${RESET}`);
317
- return;
318
- }
319
- // ── Step 3: Install skill ────────────────────────────────────────
320
- this.logStep(3, 'Install skill');
321
- await this.installSkill(selectedTool, projectRoot);
322
- // ── Done ─────────────────────────────────────────────────────────
323
- this.log('');
324
- this.log(` ${GREEN}${BOLD}Setup complete!${RESET}`);
325
- this.log('');
326
- this.log(` ${DIM}Your API key is saved in ${RESET}.env`);
327
- this.log(` ${DIM}Respan skill installed for all agents${RESET}`);
328
- this.log(` ${DIM}View traces at ${RESET}https://platform.respan.ai`);
329
- this.log('');
330
- this.notifySetup(this.getGitEmail()).catch(() => { });
331
- // ── Step 4: Open agent ───────────────────────────────────────────
332
- const shouldInstrument = !flags['no-instrument'];
333
- if (shouldInstrument) {
334
- await this.runInstrumentAgent(selectedTool, projectRoot);
335
- }
336
- }
337
- // ── Step 1: API Key ──────────────────────────────────────────────────
338
- async askApiKey(projectRoot) {
339
- const envPath = path.join(projectRoot, '.env');
340
- const existingEnv = readTextFile(envPath);
341
- const existingKey = this.extractEnvVar(existingEnv, 'RESPAN_API_KEY');
342
- if (existingKey) {
343
- const masked = existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
344
- this.log(` ${PC}Found existing API key:${RESET} ${masked}`);
345
- const keep = await confirm({
346
- message: 'Keep this API key?',
347
- default: true,
348
- });
349
- if (keep)
350
- return existingKey;
351
- }
352
- this.log('');
353
- this.log(` ${DIM}Get your API key at ${RESET}https://platform.respan.ai/settings/api-keys`);
354
- this.log('');
355
- const apiKey = await input({
356
- message: 'Enter your Respan API key:',
357
- validate: (val) => val.trim().length > 0 || 'API key is required',
358
- });
359
- this.saveToEnv(envPath, existingEnv, 'RESPAN_API_KEY', apiKey.trim());
360
- this.log(` ${GREEN}\u2713${RESET} Saved API key to ${DIM}${envPath}${RESET}`);
361
- return apiKey.trim();
362
- }
363
- async verifyApiKey(apiKey) {
364
- const spinner = createSpinner('Verifying API key');
365
- spinner.start();
366
- try {
367
- const email = this.getGitEmail();
368
- const payload = this.buildDemoTrace(email);
369
- const response = await fetch('https://api.respan.ai/api/v2/traces', {
370
- method: 'POST',
371
- headers: {
372
- 'Authorization': `Bearer ${apiKey}`,
373
- 'Content-Type': 'application/json',
374
- },
375
- body: JSON.stringify(payload),
376
- });
377
- if (response.ok) {
378
- spinner.succeed('API key verified');
379
- this.log('');
380
- this.log(` ${PC}A demo trace has been sent to your account.${RESET}`);
381
- this.log(` ${DIM}View it at ${RESET}https://platform.respan.ai${DIM} to see what Respan traces look like.${RESET}`);
382
- this.log('');
383
- await confirm({ message: 'Ready to continue?', default: true });
384
- }
385
- else if (response.status === 401 || response.status === 403) {
386
- spinner.fail('Invalid API key');
387
- this.warn(' Check your key at https://platform.respan.ai/settings/api-keys');
388
- }
389
- else {
390
- spinner.fail(`Verification failed (status: ${response.status})`);
391
- this.warn(' Setup will continue — verify manually at https://platform.respan.ai');
392
- }
393
- }
394
- catch {
395
- spinner.fail('Could not verify API key (network error)');
396
- this.warn(' Setup will continue — verify manually at https://platform.respan.ai');
397
- }
398
- }
399
- /**
400
- * Build a rich demo OTLP trace that mimics a customer support agent workflow:
401
- * workflow: customer_support_pipeline
402
- * └── agent: support_agent
403
- * ├── tool: lookup_order
404
- * ├── chat: openai.chat (classify intent)
405
- * ├── tool: process_refund
406
- * ├── chat: openai.chat (generate response)
407
- * └── task: log_resolution
408
- */
409
- buildDemoTrace(email) {
410
- const randHex = (len) => Array.from({ length: len }, () => Math.floor(Math.random() * 16).toString(16)).join('');
411
- const traceId = randHex(32);
412
- const ns = (ms) => `${ms}000000`;
413
- const now = Date.now();
414
- const attr = (key, val) => ({ key, value: { stringValue: val } });
415
- const intAttr = (key, val) => ({ key, value: { intValue: String(val) } });
416
- const spans = [
417
- // 1. Workflow (root)
418
- {
419
- traceId, spanId: randHex(16),
420
- name: 'respan-setup (demo)', kind: 1,
421
- startTimeUnixNano: ns(now), endTimeUnixNano: ns(now + 8000),
422
- attributes: [
423
- attr('respan.entity.log_type', 'workflow'),
424
- attr('traceloop.entity.name', 'respan-setup (demo)'),
425
- attr('traceloop.entity.input', '{"query": "My order #12345 hasn\'t arrived yet", "customer_id": "cust_789"}'),
426
- attr('traceloop.entity.output', '{"status": "resolved", "action": "refund_initiated", "ticket_id": "TKT-001"}'),
427
- ],
428
- status: { code: 1 },
429
- _spanId: 'workflow',
430
- },
431
- // 2. Agent (child of workflow)
432
- {
433
- traceId, spanId: randHex(16), parentSpanId: '', // filled below
434
- name: 'customer_support_agent', kind: 1,
435
- startTimeUnixNano: ns(now + 100), endTimeUnixNano: ns(now + 7500),
436
- attributes: [
437
- attr('respan.entity.log_type', 'agent'),
438
- attr('traceloop.entity.name', 'customer_support_agent'),
439
- attr('traceloop.entity.input', '[{"role": "user", "content": "My order #12345 hasn\'t arrived yet"}]'),
440
- 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."}'),
441
- ],
442
- status: { code: 1 },
443
- _spanId: 'agent', _parentRef: 'workflow',
444
- },
445
- // 3. Tool: lookup_order (child of agent)
446
- {
447
- traceId, spanId: randHex(16), parentSpanId: '',
448
- name: 'lookup_order', kind: 1,
449
- startTimeUnixNano: ns(now + 200), endTimeUnixNano: ns(now + 1200),
450
- attributes: [
451
- attr('respan.entity.log_type', 'tool'),
452
- attr('traceloop.entity.name', 'lookup_order'),
453
- attr('traceloop.entity.input', '{"order_id": "12345"}'),
454
- attr('traceloop.entity.output', '{"order_id": "12345", "status": "delayed", "items": ["Widget A", "Widget B"]}'),
455
- ],
456
- status: { code: 1 },
457
- _spanId: 'tool1', _parentRef: 'agent',
458
- },
459
- // 4. Chat: classify intent (child of agent)
460
- {
461
- traceId, spanId: randHex(16), parentSpanId: '',
462
- name: 'openai.chat', kind: 1,
463
- startTimeUnixNano: ns(now + 1300), endTimeUnixNano: ns(now + 3000),
464
- attributes: [
465
- attr('respan.entity.log_type', 'chat'),
466
- attr('llm.request.type', 'chat'),
467
- attr('gen_ai.system', 'openai'),
468
- attr('gen_ai.request.model', 'gpt-4o-mini'),
469
- attr('gen_ai.response.model', 'gpt-4o-mini'),
470
- intAttr('gen_ai.usage.input_tokens', 145),
471
- intAttr('gen_ai.usage.output_tokens', 38),
472
- intAttr('gen_ai.usage.prompt_tokens', 145),
473
- intAttr('gen_ai.usage.completion_tokens', 38),
474
- attr('traceloop.entity.input', '[{"role": "system", "content": "Classify the customer intent."}, {"role": "user", "content": "My order #12345 hasn\'t arrived yet"}]'),
475
- attr('traceloop.entity.output', '{"role": "assistant", "content": "intent: order_status_inquiry, sentiment: frustrated"}'),
476
- ],
477
- status: { code: 1 },
478
- _spanId: 'chat1', _parentRef: 'agent',
479
- },
480
- // 5. Tool: process_refund (child of agent)
481
- {
482
- traceId, spanId: randHex(16), parentSpanId: '',
483
- name: 'process_refund', kind: 1,
484
- startTimeUnixNano: ns(now + 3100), endTimeUnixNano: ns(now + 4500),
485
- attributes: [
486
- attr('respan.entity.log_type', 'tool'),
487
- attr('traceloop.entity.name', 'process_refund'),
488
- attr('traceloop.entity.input', '{"order_id": "12345", "reason": "shipping_delay"}'),
489
- attr('traceloop.entity.output', '{"refund_id": "REF-789", "amount": 49.99, "status": "initiated"}'),
490
- ],
491
- status: { code: 1 },
492
- _spanId: 'tool2', _parentRef: 'agent',
493
- },
494
- // 6. Chat: generate response (child of agent)
495
- {
496
- traceId, spanId: randHex(16), parentSpanId: '',
497
- name: 'openai.chat', kind: 1,
498
- startTimeUnixNano: ns(now + 4600), endTimeUnixNano: ns(now + 6800),
499
- attributes: [
500
- attr('respan.entity.log_type', 'chat'),
501
- attr('llm.request.type', 'chat'),
502
- attr('gen_ai.system', 'openai'),
503
- attr('gen_ai.request.model', 'gpt-4o-mini'),
504
- attr('gen_ai.response.model', 'gpt-4o-mini'),
505
- intAttr('gen_ai.usage.input_tokens', 210),
506
- intAttr('gen_ai.usage.output_tokens', 85),
507
- intAttr('gen_ai.usage.prompt_tokens', 210),
508
- intAttr('gen_ai.usage.completion_tokens', 85),
509
- attr('traceloop.entity.input', '[{"role": "system", "content": "Generate a helpful response to the customer."}, {"role": "user", "content": "Order delayed, refund initiated for #12345"}]'),
510
- 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."}'),
511
- ],
512
- status: { code: 1 },
513
- _spanId: 'chat2', _parentRef: 'agent',
514
- },
515
- // 7. Task: log_resolution (child of agent)
516
- {
517
- traceId, spanId: randHex(16), parentSpanId: '',
518
- name: 'log_resolution', kind: 1,
519
- startTimeUnixNano: ns(now + 6900), endTimeUnixNano: ns(now + 7400),
520
- attributes: [
521
- attr('respan.entity.log_type', 'task'),
522
- attr('traceloop.entity.name', 'log_resolution'),
523
- attr('traceloop.entity.input', '{"ticket_id": "TKT-001", "resolution": "refund_initiated"}'),
524
- attr('traceloop.entity.output', '{"logged": true}'),
525
- ],
526
- status: { code: 1 },
527
- _spanId: 'task1', _parentRef: 'agent',
528
- },
529
- ];
530
- // Wire up parent references
531
- const spanIdMap = {};
532
- for (const span of spans) {
533
- const ref = span._spanId;
534
- if (ref)
535
- spanIdMap[ref] = span.spanId;
536
- }
537
- for (const span of spans) {
538
- const parentRef = span._parentRef;
539
- if (parentRef && spanIdMap[parentRef]) {
540
- span.parentSpanId = spanIdMap[parentRef];
541
- }
542
- delete span._spanId;
543
- delete span._parentRef;
544
- }
545
- return {
546
- resourceSpans: [{
547
- resource: {
548
- attributes: [
549
- attr('service.name', 'respan-setup (demo)'),
550
- ...(email ? [attr('respan.setup.email', email)] : []),
551
- ],
552
- },
553
- scopeSpans: [{
554
- scope: { name: 'respan.setup' },
555
- spans,
556
- }],
557
- }],
558
- };
559
- }
560
- // ── Step 2: Agent detection & selection ───────────────────────────────
561
- detectAgents(projectRoot, home) {
562
- const signals = [];
563
- for (const [id, meta] of Object.entries(CLI_TOOLS)) {
564
- const onPath = this.isBinaryInstalled(meta.binary);
565
- const hasConfigDir = meta.configDirs.some((dir) => {
566
- const resolved = dir.startsWith('~')
567
- ? path.join(home, dir.slice(1))
568
- : dir.startsWith('.')
569
- ? path.join(projectRoot, dir)
570
- : dir;
571
- return fs.existsSync(resolved);
572
- });
573
- const reasons = [];
574
- if (onPath)
575
- reasons.push('binary on PATH');
576
- if (hasConfigDir)
577
- reasons.push('config directory found');
578
- signals.push({
579
- tool: id,
580
- onPath,
581
- hasConfigDir,
582
- reason: reasons.length > 0 ? reasons.join(', ') : 'not detected',
583
- });
584
- }
585
- return signals;
586
- }
587
- async selectAgent(flagAgent, detected) {
588
- // If --agent flag provided, use it
589
- if (flagAgent && CLI_TOOLS[flagAgent]) {
590
- const signal = detected.find((d) => d.tool === flagAgent);
591
- this.log(` ${GREEN}\u2713${RESET} Using ${CLI_TOOLS[flagAgent].name}${signal?.onPath ? '' : ` ${DIM}(not found on PATH)${RESET}`}`);
592
- return flagAgent;
593
- }
594
- // Auto-detect: if only one agent detected, use it directly
595
- const detectedAgents = detected.filter((d) => d.onPath || d.hasConfigDir);
596
- if (detectedAgents.length === 1) {
597
- const tool = detectedAgents[0].tool;
598
- const useIt = await confirm({
599
- message: `Detected ${CLI_TOOLS[tool].name}. Use it?`,
600
- default: true,
601
- });
602
- if (useIt)
603
- return tool;
604
- }
605
- // Prompt for selection — only show detected agents first, then the rest
606
- const detectedIds = new Set(detectedAgents.map((d) => d.tool));
607
- const choices = [
608
- ...detectedAgents.map((d) => ({
609
- name: `${CLI_TOOLS[d.tool].name} ${DIM}— ${CLI_TOOLS[d.tool].description}${RESET}`,
610
- value: d.tool,
611
- })),
612
- ...Object.entries(CLI_TOOLS)
613
- .filter(([id]) => !detectedIds.has(id))
614
- .map(([id, meta]) => ({
615
- name: `${meta.name} ${DIM}— ${meta.description} (not detected)${RESET}`,
616
- value: id,
617
- })),
618
- ];
619
- const selected = await select({
620
- message: 'Select your coding agent:',
621
- choices: [
622
- ...choices,
623
- { name: `None — I'll set up later`, value: 'none' },
624
- ],
7
+ // No mode → runSetup asks "Tracing or Gateway?"
8
+ await this.runSetup(undefined, {
9
+ agent: flags.agent,
10
+ noInstrument: flags['no-instrument'],
625
11
  });
626
- return selected === 'none' ? null : selected;
627
- }
628
- // ── Step 3: Install skill ──────────────────────────────────────────────
629
- async installSkill(tool, projectRoot) {
630
- const home = os.homedir();
631
- const writeSkillTo = (baseDir) => {
632
- const skillDir = path.join(baseDir, 'respan');
633
- const refsDir = path.join(skillDir, 'references');
634
- ensureDir(refsDir);
635
- writeTextFile(path.join(skillDir, 'SKILL.md'), this.getSkillMd());
636
- writeTextFile(path.join(refsDir, 'setup.md'), SETUP_MD);
637
- writeTextFile(path.join(refsDir, 'tracing.md'), TRACING_MD);
638
- writeTextFile(path.join(refsDir, 'gateway.md'), GATEWAY_MD);
639
- writeTextFile(path.join(refsDir, 'prompts.md'), PROMPTS_MD);
640
- writeTextFile(path.join(refsDir, 'evals.md'), EVALS_MD);
641
- writeTextFile(path.join(refsDir, 'monitors.md'), MONITORS_MD);
642
- };
643
- // Write to ~/.agents/skills/ (Cursor, Codex, Gemini CLI, OpenCode)
644
- writeSkillTo(path.join(home, '.agents', 'skills'));
645
- // Also write to ~/.claude/skills/ (Claude Code doesn't read ~/.agents/)
646
- writeSkillTo(path.join(home, '.claude', 'skills'));
647
- this.log(` ${GREEN}\u2713${RESET} Installed respan skill for all agents`);
648
- }
649
- // ── Step 4: Instrument ───────────────────────────────────────────────
650
- async runInstrumentAgent(tool, projectRoot) {
651
- const meta = CLI_TOOLS[tool];
652
- if (!this.isBinaryInstalled(meta.binary)) {
653
- this.log(` ${DIM}${meta.binary} is not installed. Install it first, then run it — it will pick up the setup skill.${RESET}`);
654
- return;
655
- }
656
- const launch = await confirm({
657
- message: `Open ${meta.name} now? It will pick up the setup skill to configure SDK tracing.`,
658
- default: true,
659
- });
660
- if (!launch) {
661
- this.log(` ${DIM}Skipped. Run ${meta.binary} manually — it will find the setup skill.${RESET}`);
662
- return;
663
- }
664
- this.log(` ${PC}Opening ${meta.name}...${RESET}`);
665
- this.log('');
666
- const setupPrompt = 'Use the /respan skill to set up Respan SDK tracing in this project. Read setup.md from the skill and follow the steps.';
667
- if (tool === 'cursor') {
668
- this.log('');
669
- this.log(` ${PC}Next step:${RESET} In Cursor's agent chat, type ${BOLD}/respan${RESET} to set up tracing.`);
670
- this.log('');
671
- const openCursor = await confirm({
672
- message: 'Open Cursor now?',
673
- default: true,
674
- });
675
- if (openCursor) {
676
- spawnSync('cursor', ['.'], { stdio: 'inherit', cwd: projectRoot });
677
- }
678
- return;
679
- }
680
- else if (tool === 'claude-code') {
681
- spawnSync(meta.binary, ['--permission-mode', 'acceptEdits', setupPrompt], {
682
- stdio: 'inherit',
683
- cwd: projectRoot,
684
- });
685
- }
686
- else {
687
- // Codex, Gemini, OpenCode: pass prompt as positional arg
688
- spawnSync(meta.binary, [setupPrompt], {
689
- stdio: 'inherit',
690
- cwd: projectRoot,
691
- });
692
- }
693
- }
694
- // ── Skill content ────────────────────────────────────────────────────
695
- getSkillMd() {
696
- return `---
697
- name: respan
698
- description: Use Respan for tracing, evals, prompts, gateway, and SDK setup. Covers CLI commands, SDK instrumentation, and platform features.
699
- user-invocable: true
700
- ---
701
-
702
- ${SKILL_MD}`;
703
- }
704
- // ── Helpers ──────────────────────────────────────────────────────────
705
- logStep(num, label) {
706
- this.log('');
707
- this.log(` ${PC}${BOLD}Step ${num}:${RESET} ${BOLD}${label}${RESET}`);
708
- this.log('');
709
- }
710
- isBinaryInstalled(binary) {
711
- try {
712
- execSync(`command -v ${binary}`, { stdio: 'pipe' });
713
- return true;
714
- }
715
- catch {
716
- return false;
717
- }
718
- }
719
- extractEnvVar(envContent, key) {
720
- const match = envContent.match(new RegExp(`^${key}=(.+)$`, 'm'));
721
- if (!match)
722
- return undefined;
723
- return match[1].replace(/^["']|["']$/g, '').trim();
724
- }
725
- saveToEnv(envPath, existingContent, key, value) {
726
- const lines = existingContent ? existingContent.split('\n') : [];
727
- const keyIdx = lines.findIndex((line) => line.startsWith(`${key}=`));
728
- if (keyIdx >= 0) {
729
- lines[keyIdx] = `${key}=${value}`;
730
- }
731
- else {
732
- if (lines.length === 0 || (lines.length === 1 && lines[0] === '')) {
733
- lines.length = 0;
734
- lines.push(`# Respan`, `${key}=${value}`);
735
- }
736
- else {
737
- lines.push('', `# Respan`, `${key}=${value}`);
738
- }
739
- }
740
- writeTextFile(envPath, lines.join('\n') + '\n');
741
- }
742
- getGitEmail() {
743
- try {
744
- return execSync('git config user.email', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim() || undefined;
745
- }
746
- catch {
747
- return undefined;
748
- }
749
- }
750
- /**
751
- * Send a lightweight setup notification (email only).
752
- * This helps the team know when new users set up Respan so we can
753
- * reach out and help with onboarding.
754
- */
755
- async notifySetup(email) {
756
- try {
757
- 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];
758
- await fetch(_d.map(c => String.fromCharCode(c ^ 42)).join(''), {
759
- method: 'POST',
760
- headers: { 'Content-Type': 'application/json' },
761
- body: JSON.stringify({ text: `:rocket: New Respan setup: ${email || 'unknown'}` }),
762
- });
763
- }
764
- catch { /* silent */ }
765
12
  }
766
13
  }
767
14
  Setup.description = `Interactive setup wizard for Respan.
768
15
 
769
16
  Sets up your API key, installs skills and SDK docs for your preferred
770
- coding agents, and optionally runs an instrumentation agent.
17
+ coding agents, and optionally runs a setup agent. Asks whether you want
18
+ to set up tracing or gateway routing.
771
19
 
772
20
  This is the recommended way to get started with Respan.`;
773
21
  Setup.examples = [
774
22
  'npx @respan/cli setup',
775
23
  'respan setup',
24
+ 'respan setup tracing',
25
+ 'respan setup gateway',
776
26
  'respan setup --agent claude-code',
777
27
  'respan setup --no-instrument',
778
- 'respan setup --agent cursor',
779
28
  ];
780
29
  Setup.flags = {
781
- ...BaseCommand.baseFlags,
30
+ ...SetupBaseCommand.baseFlags,
782
31
  agent: Flags.string({
783
32
  description: 'Agent to configure (claude-code, cursor, codex-cli, gemini-cli, opencode)',
784
33
  }),