@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.
- package/dist/commands/setup/gateway.d.ts +2 -2
- package/dist/commands/setup/gateway.js +3 -7
- package/dist/commands/setup/index.d.ts +2 -2
- package/dist/commands/setup/index.js +4 -10
- package/dist/commands/setup/tracing.d.ts +2 -2
- package/dist/commands/setup/tracing.js +3 -7
- package/dist/lib/demo/seed-prompts.js +7 -7
- package/dist/lib/demo/trace-fixtures.d.ts +14 -5
- package/dist/lib/demo/trace-fixtures.js +26 -17
- package/dist/lib/setup-base-command.d.ts +2 -108
- package/dist/lib/setup-base-command.js +8 -754
- package/oclif.manifest.json +531 -534
- package/package.json +2 -3
- package/dist/lib/skill-content.d.ts +0 -1
- package/dist/lib/skill-content.js +0 -16
- package/dist/lib/skill-refs.generated.d.ts +0 -6
- package/dist/lib/skill-refs.generated.js +0 -1585
|
@@ -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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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
|
-
};
|