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