@sentrial/sdk 0.4.4 → 0.5.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/cli.js ADDED
@@ -0,0 +1,1108 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { spawn } from "child_process";
6
+ import os from "os";
7
+ import path from "path";
8
+ import process from "process";
9
+ import readline from "readline/promises";
10
+
11
+ // src/constants.ts
12
+ var DEFAULT_API_URL = "https://api.sentrial.com";
13
+
14
+ // package.json
15
+ var version = "0.5.0";
16
+
17
+ // src/cli.ts
18
+ var CONFIG_DIR = path.join(os.homedir(), ".sentrial");
19
+ var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
20
+ var TIME_RANGES = /* @__PURE__ */ new Set(["24h", "3d", "7d", "30d"]);
21
+ var CliError = class extends Error {
22
+ exitCode;
23
+ constructor(message, exitCode = 1) {
24
+ super(message);
25
+ this.name = "CliError";
26
+ this.exitCode = exitCode;
27
+ }
28
+ };
29
+ var SHORT_FLAGS = {
30
+ a: "agent",
31
+ f: "force",
32
+ h: "help",
33
+ j: "json",
34
+ l: "last",
35
+ s: "status",
36
+ t: "type",
37
+ v: "version",
38
+ y: "yes"
39
+ };
40
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
41
+ "force",
42
+ "help",
43
+ "include-api-key",
44
+ "json",
45
+ "dry-run",
46
+ "version",
47
+ "yes"
48
+ ]);
49
+ var HELP = `Sentrial CLI
50
+
51
+ Usage:
52
+ sentrial <command> [options]
53
+
54
+ Commands:
55
+ install Install the SDK package and scaffold integration files
56
+ init Scaffold a safe example integration in this repo
57
+ login Save an API key for future CLI calls
58
+ sessions list List recent sessions
59
+ sessions get <id> Show a session and its events
60
+ events list <session-id> List events for a session
61
+ signals list List detected signals
62
+ diagnose <session-id> Print a terminal diagnosis from session events
63
+ evaluate <experiment-id> Run the /api/v1/evaluate CI gate (API key needs the metrics:read scope)
64
+ fix <session-id> Reserved for the upcoming Fixer API
65
+ replay <session-id> Reserved for the upcoming replay API
66
+
67
+ Global options:
68
+ --api-key <key> API key, otherwise SENTRIAL_API_KEY or ~/.sentrial/config.json
69
+ --api-url <url> API URL, otherwise SENTRIAL_API_URL or ${DEFAULT_API_URL}
70
+ --json Emit machine-readable JSON
71
+ --no-color Disable ANSI color
72
+ -h, --help Show help
73
+ -v, --version Show version
74
+
75
+ Examples:
76
+ sentrial login --api-key sentrial_live_xxx
77
+ sentrial install
78
+ sentrial init
79
+ sentrial sessions list --last 7d --status failed --agent support
80
+ sentrial sessions get 00000000-0000-0000-0000-000000000000
81
+ sentrial events list 00000000-0000-0000-0000-000000000000 --type tool_call
82
+ sentrial signals list --type negative --last 30d
83
+ sentrial diagnose 00000000-0000-0000-0000-000000000000
84
+ sentrial evaluate 00000000-0000-0000-0000-000000000000 --min-score 80
85
+ `;
86
+ function parseArgs(argv) {
87
+ const flags = {};
88
+ const positionals = [];
89
+ for (let i = 0; i < argv.length; i++) {
90
+ const token = argv[i];
91
+ if (token === "--") {
92
+ positionals.push(...argv.slice(i + 1));
93
+ break;
94
+ }
95
+ if (token.startsWith("--no-")) {
96
+ flags[token.slice(5)] = false;
97
+ continue;
98
+ }
99
+ if (token.startsWith("--")) {
100
+ const [rawName, inlineValue] = token.slice(2).split("=", 2);
101
+ if (inlineValue !== void 0) {
102
+ flags[rawName] = inlineValue;
103
+ } else if (BOOLEAN_FLAGS.has(rawName)) {
104
+ flags[rawName] = true;
105
+ } else {
106
+ const next = argv[i + 1];
107
+ if (next && !next.startsWith("-")) {
108
+ flags[rawName] = next;
109
+ i++;
110
+ } else {
111
+ flags[rawName] = true;
112
+ }
113
+ }
114
+ continue;
115
+ }
116
+ if (token.startsWith("-") && token.length === 2) {
117
+ const mapped = SHORT_FLAGS[token.slice(1)];
118
+ if (!mapped) {
119
+ throw new CliError(`Unknown flag: ${token}`);
120
+ }
121
+ if (BOOLEAN_FLAGS.has(mapped)) {
122
+ flags[mapped] = true;
123
+ } else {
124
+ const next = argv[i + 1];
125
+ if (!next || next.startsWith("-")) {
126
+ throw new CliError(`Flag ${token} requires a value`);
127
+ }
128
+ flags[mapped] = next;
129
+ i++;
130
+ }
131
+ continue;
132
+ }
133
+ positionals.push(token);
134
+ }
135
+ return { flags, positionals };
136
+ }
137
+ function getFlag(flags, ...names) {
138
+ for (const name of names) {
139
+ if (flags[name] !== void 0) return flags[name];
140
+ }
141
+ return void 0;
142
+ }
143
+ function getStringFlag(flags, ...names) {
144
+ const value = getFlag(flags, ...names);
145
+ if (typeof value === "string") return value;
146
+ return void 0;
147
+ }
148
+ function getBooleanFlag(flags, name) {
149
+ return flags[name] === true;
150
+ }
151
+ function readConfig() {
152
+ if (!existsSync(CONFIG_FILE)) return {};
153
+ try {
154
+ const parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
155
+ return parsed && typeof parsed === "object" ? parsed : {};
156
+ } catch {
157
+ return {};
158
+ }
159
+ }
160
+ function writeConfig(config) {
161
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
162
+ writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}
163
+ `, { mode: 384 });
164
+ try {
165
+ chmodSync(CONFIG_FILE, 384);
166
+ } catch {
167
+ }
168
+ }
169
+ function createContext(flags) {
170
+ const config = readConfig();
171
+ const apiKey = getStringFlag(flags, "api-key") ?? process.env.SENTRIAL_API_KEY ?? config.apiKey;
172
+ const apiUrl = (getStringFlag(flags, "api-url") ?? process.env.SENTRIAL_API_URL ?? config.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
173
+ return {
174
+ apiKey,
175
+ apiUrl,
176
+ json: getBooleanFlag(flags, "json"),
177
+ color: flags.color !== false && process.stdout.isTTY && !process.env.NO_COLOR
178
+ };
179
+ }
180
+ function requireApiKey(ctx) {
181
+ if (!ctx.apiKey) {
182
+ throw new CliError(
183
+ "Missing API key. Run `sentrial login --api-key sentrial_live_xxx` or set SENTRIAL_API_KEY."
184
+ );
185
+ }
186
+ return ctx.apiKey;
187
+ }
188
+ async function apiRequest(ctx, apiPath, options = {}) {
189
+ const apiKey = requireApiKey(ctx);
190
+ const url = new URL(apiPath, `${ctx.apiUrl}/`);
191
+ for (const [key, value] of Object.entries(options.query ?? {})) {
192
+ if (value !== void 0) {
193
+ url.searchParams.set(key, String(value));
194
+ }
195
+ }
196
+ const headers = {
197
+ Authorization: `Bearer ${apiKey}`,
198
+ Accept: "application/json"
199
+ };
200
+ if (options.body !== void 0) {
201
+ headers["Content-Type"] = "application/json";
202
+ }
203
+ let response;
204
+ try {
205
+ response = await fetch(url, {
206
+ method: options.method ?? "GET",
207
+ headers,
208
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
209
+ });
210
+ } catch (error) {
211
+ throw new CliError(
212
+ `Could not reach ${ctx.apiUrl}. ${error instanceof Error ? error.message : String(error)}`
213
+ );
214
+ }
215
+ const text = await response.text();
216
+ const parsed = parseResponseBody(text);
217
+ if (!response.ok) {
218
+ throw new CliError(
219
+ `${options.method ?? "GET"} ${url.pathname} failed (${response.status}): ${formatApiError(
220
+ parsed
221
+ )}`,
222
+ response.status === 404 ? 4 : 1
223
+ );
224
+ }
225
+ return parsed;
226
+ }
227
+ function parseResponseBody(text) {
228
+ if (!text.trim()) return {};
229
+ try {
230
+ return JSON.parse(text);
231
+ } catch {
232
+ return text;
233
+ }
234
+ }
235
+ function formatApiError(value) {
236
+ if (value && typeof value === "object") {
237
+ const record = value;
238
+ if (typeof record.error === "string") return record.error;
239
+ if (record.error && typeof record.error === "object") {
240
+ const error = record.error;
241
+ if (typeof error.message === "string") return error.message;
242
+ }
243
+ }
244
+ return typeof value === "string" ? value : "Unexpected API response";
245
+ }
246
+ async function commandLogin(ctx, flags) {
247
+ let apiKey = getStringFlag(flags, "api-key") ?? process.env.SENTRIAL_API_KEY;
248
+ const apiUrl = getStringFlag(flags, "api-url") ?? process.env.SENTRIAL_API_URL ?? ctx.apiUrl;
249
+ if (!apiKey) {
250
+ if (!process.stdin.isTTY) {
251
+ throw new CliError("Pass --api-key or set SENTRIAL_API_KEY when stdin is not interactive.");
252
+ }
253
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
254
+ apiKey = (await rl.question("Sentrial API key: ")).trim();
255
+ rl.close();
256
+ }
257
+ if (!apiKey.startsWith("sentrial_")) {
258
+ throw new CliError("API keys should look like sentrial_live_xxx or sentrial_test_xxx.");
259
+ }
260
+ writeConfig({ apiKey, apiUrl });
261
+ if (ctx.json) {
262
+ printJson({ ok: true, configPath: CONFIG_FILE, apiUrl });
263
+ return;
264
+ }
265
+ process.stdout.write(`Saved credentials to ${CONFIG_FILE}
266
+ `);
267
+ }
268
+ async function commandInstall(ctx, flags) {
269
+ const projectRoot = process.cwd();
270
+ const requestedLanguage = getStringFlag(flags, "language");
271
+ const pythonCommand = getStringFlag(flags, "python");
272
+ const packageOverride = getStringFlag(flags, "sdk-package") ?? process.env.SENTRIAL_SDK_PACKAGE;
273
+ const plan = buildInstallPlan(projectRoot, requestedLanguage, pythonCommand, packageOverride);
274
+ const dryRun = getBooleanFlag(flags, "dry-run");
275
+ if (ctx.json) {
276
+ if (!dryRun) {
277
+ throw new CliError("Use --dry-run with --json; real installs stream package-manager output.");
278
+ }
279
+ printJson({ dryRun: true, plan, wouldWrite: plannedScaffoldFiles(projectRoot, plan.language) });
280
+ return;
281
+ }
282
+ process.stdout.write(`Detected: ${plan.framework ?? plan.language}
283
+ `);
284
+ process.stdout.write(`Install: ${formatCommand(plan.command, plan.args)}
285
+ `);
286
+ if (dryRun) {
287
+ process.stdout.write("Dry run only. No files were changed.\n");
288
+ return;
289
+ }
290
+ await runSubprocess(plan.command, plan.args, projectRoot);
291
+ await commandInit(ctx, flags);
292
+ }
293
+ async function commandInit(ctx, flags) {
294
+ const projectRoot = process.cwd();
295
+ const detected = detectProject(projectRoot);
296
+ const requestedLanguage = getStringFlag(flags, "language");
297
+ const language = requestedLanguage ?? detected.language ?? "typescript";
298
+ const force = getBooleanFlag(flags, "force");
299
+ const includeApiKey = getBooleanFlag(flags, "include-api-key");
300
+ if (language !== "typescript" && language !== "python") {
301
+ throw new CliError('Supported init languages are "typescript" and "python".');
302
+ }
303
+ const files = language === "python" ? [
304
+ {
305
+ path: path.join(projectRoot, "sentrial.example.py"),
306
+ content: pythonExample()
307
+ }
308
+ ] : [
309
+ {
310
+ path: path.join(projectRoot, "sentrial.example.ts"),
311
+ content: typescriptExample()
312
+ }
313
+ ];
314
+ files.push({
315
+ path: path.join(projectRoot, ".env.sentrial.example"),
316
+ content: `SENTRIAL_API_KEY=sentrial_live_xxx
317
+ SENTRIAL_API_URL=${ctx.apiUrl}
318
+ `
319
+ });
320
+ if (includeApiKey && ctx.apiKey) {
321
+ files.push({
322
+ path: path.join(projectRoot, ".env.sentrial"),
323
+ content: `SENTRIAL_API_KEY=${ctx.apiKey}
324
+ SENTRIAL_API_URL=${ctx.apiUrl}
325
+ `
326
+ });
327
+ }
328
+ const written = [];
329
+ const skipped = [];
330
+ for (const file of files) {
331
+ if (existsSync(file.path) && !force) {
332
+ skipped.push(file.path);
333
+ continue;
334
+ }
335
+ writeFileSync(file.path, file.content);
336
+ written.push(file.path);
337
+ }
338
+ const installCommand = language === "python" ? "pip install sentrial" : `${detectPackageManager(projectRoot)} add @sentrial/sdk`;
339
+ if (ctx.json) {
340
+ printJson({
341
+ detected,
342
+ language,
343
+ installCommand,
344
+ written,
345
+ skipped
346
+ });
347
+ return;
348
+ }
349
+ process.stdout.write(`Detected: ${detected.framework ?? language}
350
+ `);
351
+ process.stdout.write(`Install: ${installCommand}
352
+ `);
353
+ if (written.length > 0) {
354
+ process.stdout.write(
355
+ `Written:
356
+ ${written.map((file) => ` ${path.relative(projectRoot, file)}`).join("\n")}
357
+ `
358
+ );
359
+ }
360
+ if (skipped.length > 0) {
361
+ process.stdout.write(
362
+ `Skipped existing files:
363
+ ${skipped.map((file) => ` ${path.relative(projectRoot, file)}`).join("\n")}
364
+ `
365
+ );
366
+ }
367
+ }
368
+ async function commandSessions(ctx, flags, rest) {
369
+ const subcommand = rest[0];
370
+ if (subcommand === "list") {
371
+ await commandSessionsList(ctx, flags);
372
+ return;
373
+ }
374
+ if (subcommand === "get") {
375
+ const sessionId = rest[1];
376
+ if (!sessionId) throw new CliError("Usage: sentrial sessions get <id>");
377
+ await commandSessionsGet(ctx, flags, sessionId);
378
+ return;
379
+ }
380
+ throw new CliError("Usage: sentrial sessions <list|get>");
381
+ }
382
+ async function commandSessionsList(ctx, flags) {
383
+ const query = buildTimeQuery(flags);
384
+ const agentFilter = getStringFlag(flags, "agent")?.toLowerCase();
385
+ const hasExplicitLimit = getStringFlag(flags, "limit") !== void 0;
386
+ query.status = getStringFlag(flags, "status") ?? "all";
387
+ query.limit = parseIntegerFlag(flags, "limit", agentFilter && !hasExplicitLimit ? 500 : 50);
388
+ query.offset = parseIntegerFlag(flags, "offset", 0);
389
+ const response = await apiRequest(ctx, "/api/v1/sessions", {
390
+ query
391
+ });
392
+ const sessions = agentFilter ? response.data.filter(
393
+ (session) => `${session.agentName ?? ""} ${session.agentSlug ?? ""}`.toLowerCase().includes(agentFilter)
394
+ ) : response.data;
395
+ if (ctx.json) {
396
+ printJson({ data: sessions, meta: response.meta });
397
+ return;
398
+ }
399
+ if (sessions.length === 0) {
400
+ process.stdout.write("No sessions found.\n");
401
+ return;
402
+ }
403
+ printTable(sessions, [
404
+ { header: "ID", render: (s) => s.id, maxWidth: 36 },
405
+ { header: "Agent", render: (s) => s.agentName ?? s.agentSlug ?? "-", maxWidth: 18 },
406
+ { header: "Status", render: (s) => s.status ?? "-", maxWidth: 10 },
407
+ { header: "Result", render: (s) => formatResult(s.success), maxWidth: 8 },
408
+ { header: "Score", render: (s) => formatNullableNumber(s.score), maxWidth: 6 },
409
+ { header: "Dur", render: (s) => formatDuration(s.durationMs), maxWidth: 8 },
410
+ { header: "Cost", render: (s) => formatCost(s.estimatedCost), maxWidth: 10 },
411
+ { header: "Created", render: (s) => formatDate(s.createdAt), maxWidth: 16 },
412
+ { header: "Name", render: (s) => s.name ?? "-", maxWidth: 28 }
413
+ ]);
414
+ if (agentFilter) {
415
+ process.stdout.write(`
416
+ Filtered locally by agent: ${agentFilter}
417
+ `);
418
+ }
419
+ }
420
+ async function commandSessionsGet(ctx, flags, sessionId) {
421
+ const sessionResponse = await apiRequest(
422
+ ctx,
423
+ `/api/v1/sessions/${sessionId}`
424
+ );
425
+ const includeEvents = flags.events !== false;
426
+ const events = includeEvents ? await fetchSessionEvents(ctx, flags, sessionId) : [];
427
+ if (ctx.json) {
428
+ printJson({ data: sessionResponse.data, events });
429
+ return;
430
+ }
431
+ printSession(sessionResponse.data, events, ctx);
432
+ }
433
+ async function fetchSessionEvents(ctx, flags, sessionId) {
434
+ const eventType = getStringFlag(flags, "event-type") ?? "all";
435
+ const response = await apiRequest(
436
+ ctx,
437
+ `/api/v1/sessions/${sessionId}/events`,
438
+ {
439
+ query: {
440
+ limit: parseIntegerFlag(flags, "events-limit", 100),
441
+ offset: parseIntegerFlag(flags, "events-offset", 0),
442
+ eventType
443
+ }
444
+ }
445
+ );
446
+ return response.data;
447
+ }
448
+ async function commandEvents(ctx, flags, rest) {
449
+ const subcommand = rest[0] ?? "list";
450
+ if (subcommand !== "list") {
451
+ throw new CliError("Usage: sentrial events list <session-id>");
452
+ }
453
+ const sessionId = rest[1] ?? getStringFlag(flags, "session", "session-id");
454
+ if (!sessionId) {
455
+ throw new CliError("Usage: sentrial events list <session-id>");
456
+ }
457
+ const eventType = getStringFlag(flags, "type", "event-type") ?? "all";
458
+ const response = await apiRequest(
459
+ ctx,
460
+ `/api/v1/sessions/${sessionId}/events`,
461
+ {
462
+ query: {
463
+ limit: parseIntegerFlag(flags, "limit", 100),
464
+ offset: parseIntegerFlag(flags, "offset", 0),
465
+ eventType
466
+ }
467
+ }
468
+ );
469
+ if (ctx.json) {
470
+ printJson(response);
471
+ return;
472
+ }
473
+ if (response.data.length === 0) {
474
+ process.stdout.write("No events found.\n");
475
+ return;
476
+ }
477
+ process.stdout.write(`${bold(ctx, "Events")} ${sessionId}
478
+ `);
479
+ for (const event of response.data) {
480
+ printEvent(event, ctx);
481
+ }
482
+ }
483
+ async function commandSignals(ctx, flags, rest) {
484
+ const subcommand = rest[0] ?? "list";
485
+ if (subcommand !== "list") {
486
+ throw new CliError("Usage: sentrial signals list");
487
+ }
488
+ const query = buildTimeQuery(flags);
489
+ query.type = getStringFlag(flags, "type") ?? "all";
490
+ query.source = getStringFlag(flags, "source") ?? "all";
491
+ const response = await apiRequest(ctx, "/api/v1/signals", {
492
+ query
493
+ });
494
+ if (ctx.json) {
495
+ printJson(response);
496
+ return;
497
+ }
498
+ if (response.data.length === 0) {
499
+ process.stdout.write("No signals found.\n");
500
+ return;
501
+ }
502
+ printTable(response.data, [
503
+ { header: "Name", render: (s) => s.name, maxWidth: 28 },
504
+ { header: "Type", render: (s) => s.type, maxWidth: 10 },
505
+ { header: "Source", render: (s) => s.source ?? "-", maxWidth: 12 },
506
+ { header: "Count", render: (s) => String(s.count), maxWidth: 8 },
507
+ { header: "Sessions", render: (s) => String(s.affectedSessions), maxWidth: 10 },
508
+ { header: "Users", render: (s) => String(s.affectedUsers), maxWidth: 8 },
509
+ { header: "Created", render: (s) => formatDate(s.createdAt), maxWidth: 16 },
510
+ { header: "Description", render: (s) => s.description ?? "-", maxWidth: 36 }
511
+ ]);
512
+ }
513
+ async function commandDiagnose(ctx, flags, sessionId) {
514
+ if (!sessionId) throw new CliError("Usage: sentrial diagnose <session-id>");
515
+ const sessionResponse = await apiRequest(
516
+ ctx,
517
+ `/api/v1/sessions/${sessionId}`
518
+ );
519
+ const events = await fetchSessionEvents(ctx, flags, sessionId);
520
+ const findings = diagnoseSession(sessionResponse.data, events);
521
+ if (ctx.json) {
522
+ printJson({ session: sessionResponse.data, findings });
523
+ return;
524
+ }
525
+ process.stdout.write(`${bold(ctx, "Diagnosis")}
526
+ `);
527
+ process.stdout.write(`Session: ${sessionResponse.data.id}
528
+ `);
529
+ process.stdout.write(
530
+ `Agent: ${sessionResponse.data.agentName ?? sessionResponse.data.agentSlug ?? "-"}
531
+ `
532
+ );
533
+ process.stdout.write(
534
+ `Status: ${sessionResponse.data.status ?? "-"} (${formatResult(sessionResponse.data.success)})
535
+ `
536
+ );
537
+ if (sessionResponse.data.failureReason) {
538
+ process.stdout.write(`Failure: ${sessionResponse.data.failureReason}
539
+ `);
540
+ }
541
+ process.stdout.write("\n");
542
+ if (findings.length === 0) {
543
+ process.stdout.write("No obvious terminal-level issues found in the session events.\n");
544
+ return;
545
+ }
546
+ for (const finding of findings) {
547
+ process.stdout.write(`${severityLabel(ctx, finding.severity)} ${finding.title}
548
+ `);
549
+ process.stdout.write(` ${finding.detail}
550
+ `);
551
+ }
552
+ }
553
+ async function commandEvaluate(ctx, flags, experimentId) {
554
+ const resolvedExperimentId = experimentId ?? getStringFlag(flags, "experiment", "experiment-id");
555
+ if (!resolvedExperimentId) {
556
+ throw new CliError("Usage: sentrial evaluate <experiment-id> [--min-score 80]");
557
+ }
558
+ const body = {
559
+ experimentId: resolvedExperimentId,
560
+ baselineExperimentId: getStringFlag(flags, "baseline", "baseline-experiment-id"),
561
+ thresholds: {
562
+ minScore: parseOptionalNumberFlag(flags, "min-score"),
563
+ failOnRegression: flags["fail-on-regression"] === false ? false : void 0,
564
+ maxRegressionPercent: parseOptionalNumberFlag(flags, "max-regression-percent")
565
+ }
566
+ };
567
+ const response = await apiRequest(ctx, "/api/v1/evaluate", {
568
+ method: "POST",
569
+ body
570
+ });
571
+ if (ctx.json) {
572
+ printJson(response);
573
+ } else {
574
+ const passed = response.passed === true;
575
+ process.stdout.write(`${passed ? color(ctx, "green", "PASS") : color(ctx, "red", "FAIL")}
576
+ `);
577
+ if (Array.isArray(response.failures) && response.failures.length > 0) {
578
+ process.stdout.write(
579
+ `Failures:
580
+ ${response.failures.map((f) => ` - ${String(f)}`).join("\n")}
581
+ `
582
+ );
583
+ }
584
+ if (response.summary && typeof response.summary === "object") {
585
+ printKeyValues(response.summary);
586
+ }
587
+ if (response.regression && typeof response.regression === "object") {
588
+ process.stdout.write("\nRegression:\n");
589
+ printKeyValues(response.regression);
590
+ }
591
+ }
592
+ if (response.passed === false) {
593
+ process.exitCode = 1;
594
+ }
595
+ }
596
+ function commandReserved(command) {
597
+ const messages = {
598
+ fix: "The Fixer CLI is reserved for the upcoming scriptable Fixer API.",
599
+ replay: "The replay CLI is reserved for the upcoming replay API."
600
+ };
601
+ throw new CliError(messages[command] ?? `${command} is not available yet.`, 2);
602
+ }
603
+ function buildTimeQuery(flags) {
604
+ const query = {};
605
+ const last = getStringFlag(flags, "last", "time-range") ?? "7d";
606
+ const from = getStringFlag(flags, "from");
607
+ const to = getStringFlag(flags, "to");
608
+ if (from) {
609
+ query.from = from;
610
+ } else if (TIME_RANGES.has(last)) {
611
+ query.timeRange = last;
612
+ } else {
613
+ throw new CliError(`--last must be one of: ${Array.from(TIME_RANGES).join(", ")}`);
614
+ }
615
+ if (to) query.to = to;
616
+ return query;
617
+ }
618
+ function parseIntegerFlag(flags, name, fallback) {
619
+ const raw = getStringFlag(flags, name);
620
+ if (raw === void 0) return fallback;
621
+ const parsed = Number.parseInt(raw, 10);
622
+ if (!Number.isFinite(parsed)) throw new CliError(`--${name} must be an integer`);
623
+ return parsed;
624
+ }
625
+ function parseOptionalNumberFlag(flags, name) {
626
+ const raw = getStringFlag(flags, name);
627
+ if (raw === void 0) return void 0;
628
+ const parsed = Number.parseFloat(raw);
629
+ if (!Number.isFinite(parsed)) throw new CliError(`--${name} must be a number`);
630
+ return parsed;
631
+ }
632
+ function diagnoseSession(session, events) {
633
+ const findings = [];
634
+ const toolErrors = events.filter((event) => hasMeaningfulValue(event.toolError));
635
+ const explicitErrors = events.filter((event) => event.eventType === "error");
636
+ const lowConfidence = events.filter(
637
+ (event) => typeof event.confidence === "number" && event.confidence < 0.5
638
+ );
639
+ if (session.success === false || session.status === "failed") {
640
+ findings.push({
641
+ severity: "high",
642
+ title: "Session ended unsuccessfully",
643
+ detail: session.failureReason ?? "The session is marked failed without a failure reason."
644
+ });
645
+ }
646
+ if (toolErrors.length > 0) {
647
+ const grouped = groupBy(toolErrors, (event) => event.toolName ?? "unknown_tool");
648
+ const top = Array.from(grouped.entries()).map(([tool, group]) => `${tool} (${group.length})`).join(", ");
649
+ findings.push({
650
+ severity: "high",
651
+ title: "Tool errors occurred",
652
+ detail: `${toolErrors.length} tool error event(s): ${top}. First error: ${formatCompactJson(
653
+ toolErrors[0].toolError
654
+ )}`
655
+ });
656
+ }
657
+ if (explicitErrors.length > 0) {
658
+ findings.push({
659
+ severity: "high",
660
+ title: "Error events were recorded",
661
+ detail: `${explicitErrors.length} error event(s). First event: ${summarizeEvent(
662
+ explicitErrors[0]
663
+ )}`
664
+ });
665
+ }
666
+ const repeatedCalls = findRepeatedToolCalls(events);
667
+ if (repeatedCalls.length > 0) {
668
+ findings.push({
669
+ severity: "medium",
670
+ title: "Repeated identical tool calls",
671
+ detail: repeatedCalls.slice(0, 3).map((call) => `${call.toolName} repeated ${call.count} times`).join("; ")
672
+ });
673
+ }
674
+ if (lowConfidence.length > 0) {
675
+ findings.push({
676
+ severity: "medium",
677
+ title: "Low-confidence decisions",
678
+ detail: `${lowConfidence.length} decision event(s) had confidence below 0.5.`
679
+ });
680
+ }
681
+ if (session.totalTokens && session.totalTokens > 1e5) {
682
+ findings.push({
683
+ severity: "low",
684
+ title: "High token usage",
685
+ detail: `This session used ${session.totalTokens.toLocaleString()} tokens.`
686
+ });
687
+ }
688
+ if (session.durationMs && session.durationMs > 12e4) {
689
+ findings.push({
690
+ severity: "low",
691
+ title: "Long-running session",
692
+ detail: `Duration was ${formatDuration(session.durationMs)}.`
693
+ });
694
+ }
695
+ if (session.status === "completed" && !session.assistantOutput) {
696
+ findings.push({
697
+ severity: "info",
698
+ title: "No assistant output stored",
699
+ detail: "The session completed, but assistantOutput is empty. Confirm finish({ output }) is wired."
700
+ });
701
+ }
702
+ return findings;
703
+ }
704
+ function findRepeatedToolCalls(events) {
705
+ const counts = /* @__PURE__ */ new Map();
706
+ for (const event of events) {
707
+ if (event.eventType !== "tool_call" || !event.toolName) continue;
708
+ const key = `${event.toolName}:${formatCompactJson(event.toolInput)}`;
709
+ const current = counts.get(key) ?? { toolName: event.toolName, count: 0 };
710
+ current.count++;
711
+ counts.set(key, current);
712
+ }
713
+ return Array.from(counts.values()).filter((entry) => entry.count >= 3);
714
+ }
715
+ function groupBy(items, keyFor) {
716
+ const result = /* @__PURE__ */ new Map();
717
+ for (const item of items) {
718
+ const key = keyFor(item);
719
+ const existing = result.get(key) ?? [];
720
+ existing.push(item);
721
+ result.set(key, existing);
722
+ }
723
+ return result;
724
+ }
725
+ function hasMeaningfulValue(value) {
726
+ if (value === null || value === void 0) return false;
727
+ if (typeof value === "string") return value.trim().length > 0;
728
+ if (Array.isArray(value)) return value.length > 0;
729
+ if (typeof value === "object") return Object.keys(value).length > 0;
730
+ return true;
731
+ }
732
+ function detectProject(projectRoot) {
733
+ const packageJsonPath = path.join(projectRoot, "package.json");
734
+ if (existsSync(packageJsonPath)) {
735
+ try {
736
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf8"));
737
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
738
+ if (deps.next) return { language: "typescript", framework: "Next.js" };
739
+ if (deps.ai || deps["@ai-sdk/openai"])
740
+ return { language: "typescript", framework: "Vercel AI SDK" };
741
+ if (deps.langchain || deps["@langchain/core"])
742
+ return { language: "typescript", framework: "LangChain" };
743
+ if (deps.openai) return { language: "typescript", framework: "OpenAI Node" };
744
+ return { language: "typescript", framework: "Node.js" };
745
+ } catch {
746
+ return { language: "typescript", framework: "Node.js" };
747
+ }
748
+ }
749
+ const requirementsPath = path.join(projectRoot, "requirements.txt");
750
+ const pyprojectPath = path.join(projectRoot, "pyproject.toml");
751
+ if (existsSync(requirementsPath) || existsSync(pyprojectPath)) {
752
+ const text = [requirementsPath, pyprojectPath].filter((file) => existsSync(file)).map((file) => readFileSync(file, "utf8").toLowerCase()).join("\n");
753
+ if (text.includes("fastapi")) return { language: "python", framework: "FastAPI" };
754
+ if (text.includes("langchain")) return { language: "python", framework: "LangChain" };
755
+ if (text.includes("crewai")) return { language: "python", framework: "CrewAI" };
756
+ if (text.includes("openai")) return { language: "python", framework: "OpenAI Python" };
757
+ return { language: "python", framework: "Python" };
758
+ }
759
+ return {};
760
+ }
761
+ function buildInstallPlan(projectRoot, languageOverride, pythonCommand, packageOverride) {
762
+ const detected = detectProject(projectRoot);
763
+ const language = languageOverride ?? detected.language ?? "typescript";
764
+ if (language !== "typescript" && language !== "python") {
765
+ throw new CliError('Supported install languages are "typescript" and "python".');
766
+ }
767
+ if (language === "python") {
768
+ const command = pythonCommand ?? (process.platform === "win32" ? "python" : "python3");
769
+ const packageName2 = packageOverride ?? "sentrial";
770
+ return {
771
+ language,
772
+ framework: detected.framework,
773
+ command,
774
+ args: ["-m", "pip", "install", packageName2],
775
+ packageName: packageName2
776
+ };
777
+ }
778
+ const packageManager = detectPackageManager(projectRoot);
779
+ const packageName = packageOverride ?? "@sentrial/sdk";
780
+ return {
781
+ language,
782
+ framework: detected.framework,
783
+ command: packageManager,
784
+ args: packageManager === "npm" ? ["install", packageName] : ["add", packageName],
785
+ packageName
786
+ };
787
+ }
788
+ function detectPackageManager(projectRoot) {
789
+ if (existsSync(path.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
790
+ if (existsSync(path.join(projectRoot, "yarn.lock"))) return "yarn";
791
+ return "npm";
792
+ }
793
+ function plannedScaffoldFiles(projectRoot, language) {
794
+ const sampleFile = language === "python" ? "sentrial.example.py" : "sentrial.example.ts";
795
+ return [path.join(projectRoot, sampleFile), path.join(projectRoot, ".env.sentrial.example")];
796
+ }
797
+ function runSubprocess(command, args, cwd) {
798
+ return new Promise((resolve, reject) => {
799
+ const child = spawn(command, args, {
800
+ cwd,
801
+ stdio: "inherit",
802
+ shell: process.platform === "win32"
803
+ });
804
+ child.on("error", (error) => {
805
+ reject(new CliError(`Failed to run ${formatCommand(command, args)}: ${error.message}`));
806
+ });
807
+ child.on("close", (code) => {
808
+ if (code === 0) {
809
+ resolve();
810
+ return;
811
+ }
812
+ reject(
813
+ new CliError(`${formatCommand(command, args)} failed with exit code ${code ?? "unknown"}`)
814
+ );
815
+ });
816
+ });
817
+ }
818
+ function formatCommand(command, args) {
819
+ return [command, ...args].join(" ");
820
+ }
821
+ function typescriptExample() {
822
+ return `import { configure, withSession } from '@sentrial/sdk';
823
+
824
+ configure({
825
+ apiKey: process.env.SENTRIAL_API_KEY,
826
+ apiUrl: process.env.SENTRIAL_API_URL,
827
+ });
828
+
829
+ export const runAgent = withSession('my-agent', async (userId: string, input: string) => {
830
+ // Replace this with your real agent call. LLM/tool wrappers can be added around it.
831
+ const output = \`Echo: \${input}\`;
832
+ return output;
833
+ });
834
+
835
+ await runAgent('user_123', 'Hello from Sentrial');
836
+ `;
837
+ }
838
+ function pythonExample() {
839
+ return `from sentrial import SentrialClient
840
+
841
+ client = SentrialClient()
842
+
843
+ interaction = client.begin(
844
+ user_id="user_123",
845
+ event="my-agent",
846
+ input="Hello from Sentrial",
847
+ )
848
+
849
+ try:
850
+ # Replace this with your real agent call.
851
+ output = "Echo: Hello from Sentrial"
852
+ interaction.finish(output=output, success=True)
853
+ except Exception as exc:
854
+ interaction.finish(success=False, failure_reason=str(exc))
855
+ raise
856
+ `;
857
+ }
858
+ function printSession(session, events, ctx) {
859
+ process.stdout.write(`${bold(ctx, "Session")} ${session.id}
860
+ `);
861
+ printKeyValues({
862
+ name: session.name ?? "-",
863
+ agent: session.agentName ?? session.agentSlug ?? "-",
864
+ userId: session.userId ?? "-",
865
+ status: `${session.status ?? "-"} (${formatResult(session.success)})`,
866
+ score: formatNullableNumber(session.score),
867
+ duration: formatDuration(session.durationMs),
868
+ cost: formatCost(session.estimatedCost),
869
+ tokens: session.totalTokens ?? "-",
870
+ events: session.totalEvents ?? events.length,
871
+ createdAt: formatDate(session.createdAt),
872
+ completedAt: formatDate(session.completedAt)
873
+ });
874
+ if (session.failureReason) {
875
+ process.stdout.write(`
876
+ Failure reason:
877
+ ${indent(wrap(session.failureReason))}
878
+ `);
879
+ }
880
+ if (session.userInput) {
881
+ process.stdout.write(`
882
+ Input:
883
+ ${indent(wrap(session.userInput))}
884
+ `);
885
+ }
886
+ if (session.assistantOutput) {
887
+ process.stdout.write(`
888
+ Output:
889
+ ${indent(wrap(session.assistantOutput))}
890
+ `);
891
+ }
892
+ if (events.length > 0) {
893
+ process.stdout.write(`
894
+ ${bold(ctx, "Events")}
895
+ `);
896
+ for (const event of events) {
897
+ printEvent(event, ctx);
898
+ }
899
+ }
900
+ }
901
+ function printEvent(event, ctx) {
902
+ const prefix = `#${event.sequenceNumber ?? "-"} ${event.eventType}`;
903
+ const label = event.eventType === "error" || hasMeaningfulValue(event.toolError) ? color(ctx, "red", prefix) : color(ctx, "cyan", prefix);
904
+ process.stdout.write(`${label} ${formatDate(event.timestamp)}`);
905
+ if (event.toolName) process.stdout.write(` tool=${event.toolName}`);
906
+ if (event.estimatedCost) process.stdout.write(` cost=${formatCost(event.estimatedCost)}`);
907
+ if (event.tokenCount) process.stdout.write(` tokens=${event.tokenCount}`);
908
+ process.stdout.write("\n");
909
+ const summary = summarizeEvent(event);
910
+ if (summary) process.stdout.write(`${indent(wrap(summary, 88))}
911
+ `);
912
+ }
913
+ function summarizeEvent(event) {
914
+ if (hasMeaningfulValue(event.toolError)) {
915
+ return `Error: ${formatCompactJson(event.toolError)}`;
916
+ }
917
+ if (event.reasoning) {
918
+ return event.reasoning;
919
+ }
920
+ if (hasMeaningfulValue(event.toolOutput)) {
921
+ return `Output: ${formatCompactJson(event.toolOutput)}`;
922
+ }
923
+ if (hasMeaningfulValue(event.toolInput)) {
924
+ return `Input: ${formatCompactJson(event.toolInput)}`;
925
+ }
926
+ return "";
927
+ }
928
+ function printKeyValues(values) {
929
+ const entries = Object.entries(values);
930
+ const width = entries.reduce((max, [key]) => Math.max(max, key.length), 0);
931
+ for (const [key, value] of entries) {
932
+ process.stdout.write(`${key.padEnd(width)} ${String(value)}
933
+ `);
934
+ }
935
+ }
936
+ function printJson(value) {
937
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
938
+ `);
939
+ }
940
+ function printTable(rows, columns) {
941
+ const widths = columns.map((column) => {
942
+ const max = Math.max(column.header.length, ...rows.map((row) => column.render(row).length));
943
+ return Math.min(max, column.maxWidth);
944
+ });
945
+ const header = columns.map((column, index) => pad(truncate(column.header, widths[index]), widths[index])).join(" ");
946
+ const divider = widths.map((width) => "-".repeat(width)).join(" ");
947
+ process.stdout.write(`${header}
948
+ ${divider}
949
+ `);
950
+ for (const row of rows) {
951
+ const line = columns.map((column, index) => pad(truncate(column.render(row), widths[index]), widths[index])).join(" ");
952
+ process.stdout.write(`${line}
953
+ `);
954
+ }
955
+ }
956
+ function truncate(value, width) {
957
+ if (value.length <= width) return value;
958
+ if (width <= 1) return value.slice(0, width);
959
+ return `${value.slice(0, width - 1)}...`.slice(0, width);
960
+ }
961
+ function pad(value, width) {
962
+ return `${value}${" ".repeat(Math.max(0, width - value.length))}`;
963
+ }
964
+ function color(ctx, name, value) {
965
+ if (!ctx.color) return value;
966
+ const codes = {
967
+ cyan: 36,
968
+ green: 32,
969
+ red: 31,
970
+ yellow: 33
971
+ };
972
+ return `\x1B[${codes[name]}m${value}\x1B[0m`;
973
+ }
974
+ function bold(ctx, value) {
975
+ return ctx.color ? `\x1B[1m${value}\x1B[0m` : value;
976
+ }
977
+ function severityLabel(ctx, severity) {
978
+ if (severity === "high") return color(ctx, "red", "[high]");
979
+ if (severity === "medium") return color(ctx, "yellow", "[medium]");
980
+ if (severity === "low") return color(ctx, "cyan", "[low]");
981
+ return "[info]";
982
+ }
983
+ function formatResult(success) {
984
+ if (success === true) return "pass";
985
+ if (success === false) return "fail";
986
+ return "-";
987
+ }
988
+ function formatNullableNumber(value) {
989
+ return typeof value === "number" ? String(Math.round(value * 10) / 10) : "-";
990
+ }
991
+ function formatDuration(ms) {
992
+ if (typeof ms !== "number") return "-";
993
+ if (ms < 1e3) return `${ms}ms`;
994
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
995
+ return `${(ms / 6e4).toFixed(1)}m`;
996
+ }
997
+ function formatCost(cost) {
998
+ return typeof cost === "number" ? `$${cost.toFixed(4)}` : "-";
999
+ }
1000
+ function formatDate(value) {
1001
+ if (!value) return "-";
1002
+ const date = new Date(value);
1003
+ if (Number.isNaN(date.getTime())) return value;
1004
+ return date.toISOString().replace("T", " ").slice(0, 16);
1005
+ }
1006
+ function formatCompactJson(value) {
1007
+ if (value === void 0) return "";
1008
+ if (typeof value === "string") return value;
1009
+ try {
1010
+ return JSON.stringify(value);
1011
+ } catch {
1012
+ return String(value);
1013
+ }
1014
+ }
1015
+ function wrap(text, width = 92) {
1016
+ const words = text.replace(/\s+/g, " ").trim().split(" ");
1017
+ const lines = [];
1018
+ let current = "";
1019
+ for (const word of words) {
1020
+ if (`${current} ${word}`.trim().length > width && current) {
1021
+ lines.push(current);
1022
+ current = word;
1023
+ } else {
1024
+ current = `${current} ${word}`.trim();
1025
+ }
1026
+ }
1027
+ if (current) lines.push(current);
1028
+ return lines.join("\n");
1029
+ }
1030
+ function indent(text, spaces = 2) {
1031
+ const prefix = " ".repeat(spaces);
1032
+ return text.split("\n").map((line) => `${prefix}${line}`).join("\n");
1033
+ }
1034
+ async function run(argv) {
1035
+ const parsed = parseArgs(argv);
1036
+ const ctx = createContext(parsed.flags);
1037
+ const [command, ...rest] = parsed.positionals;
1038
+ if (getBooleanFlag(parsed.flags, "version")) {
1039
+ process.stdout.write(`${version}
1040
+ `);
1041
+ return;
1042
+ }
1043
+ if (!command || getBooleanFlag(parsed.flags, "help")) {
1044
+ process.stdout.write(HELP);
1045
+ return;
1046
+ }
1047
+ switch (command) {
1048
+ case "install":
1049
+ await commandInstall(ctx, parsed.flags);
1050
+ break;
1051
+ case "init":
1052
+ await commandInit(ctx, parsed.flags);
1053
+ break;
1054
+ case "login":
1055
+ await commandLogin(ctx, parsed.flags);
1056
+ break;
1057
+ case "sessions":
1058
+ await commandSessions(ctx, parsed.flags, rest);
1059
+ break;
1060
+ case "events":
1061
+ await commandEvents(ctx, parsed.flags, rest);
1062
+ break;
1063
+ case "signals":
1064
+ await commandSignals(ctx, parsed.flags, rest);
1065
+ break;
1066
+ case "diagnose":
1067
+ await commandDiagnose(ctx, parsed.flags, rest[0]);
1068
+ break;
1069
+ case "evaluate":
1070
+ await commandEvaluate(ctx, parsed.flags, rest[0]);
1071
+ break;
1072
+ case "fix":
1073
+ case "replay":
1074
+ commandReserved(command);
1075
+ break;
1076
+ default:
1077
+ throw new CliError(`Unknown command: ${command}. Run sentrial --help.`);
1078
+ }
1079
+ }
1080
+ if (process.env.SENTRIAL_CLI_SKIP_AUTO_RUN !== "1") {
1081
+ run(process.argv.slice(2)).catch((error) => {
1082
+ if (error instanceof CliError) {
1083
+ if (process.env.SENTRIAL_CLI_DEBUG) {
1084
+ process.stderr.write(`${error.stack ?? error.message}
1085
+ `);
1086
+ } else {
1087
+ process.stderr.write(`Error: ${error.message}
1088
+ `);
1089
+ }
1090
+ process.exitCode = error.exitCode;
1091
+ return;
1092
+ }
1093
+ process.stderr.write(
1094
+ `${error instanceof Error ? error.stack ?? error.message : String(error)}
1095
+ `
1096
+ );
1097
+ process.exitCode = 1;
1098
+ });
1099
+ }
1100
+ export {
1101
+ CliError,
1102
+ buildInstallPlan,
1103
+ detectProject,
1104
+ diagnoseSession,
1105
+ parseArgs,
1106
+ run
1107
+ };
1108
+ //# sourceMappingURL=cli.js.map