@zleap-ai/sag-cli 0.2.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,3109 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { confirm, password, select } from "@inquirer/prompts";
5
+
6
+ // package.json
7
+ var package_default = {
8
+ name: "@zleap-ai/sag-cli",
9
+ version: "0.2.0",
10
+ description: "Command-line client and diagnostics for SAG knowledge bases",
11
+ type: "module",
12
+ bin: {
13
+ sag: "dist/cli.js"
14
+ },
15
+ files: [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ scripts: {
21
+ build: "tsup",
22
+ dev: "tsx src/cli.ts",
23
+ test: "vitest run",
24
+ "test:watch": "vitest",
25
+ "test:coverage": "vitest run --coverage",
26
+ typecheck: "tsc --noEmit",
27
+ lint: "eslint .",
28
+ format: "prettier --write .",
29
+ "format:check": "prettier --check .",
30
+ check: "npm run format:check && npm run lint && npm run typecheck && npm test && npm run build",
31
+ prepare: "husky",
32
+ commitlint: "commitlint",
33
+ changeset: "changeset",
34
+ "version-packages": "changeset version",
35
+ "release:check": "bash scripts/release.sh",
36
+ release: "bash scripts/release.sh --publish"
37
+ },
38
+ engines: {
39
+ node: ">=20.19.0"
40
+ },
41
+ publishConfig: {
42
+ access: "public"
43
+ },
44
+ license: "MIT",
45
+ dependencies: {
46
+ "@inquirer/prompts": "^8.5.2",
47
+ "@modelcontextprotocol/client": "^2.0.0",
48
+ commander: "^14.0.3",
49
+ "proper-lockfile": "^4.1.2",
50
+ yaml: "^2.9.0",
51
+ zod: "^4.4.3"
52
+ },
53
+ optionalDependencies: {
54
+ "@napi-rs/keyring": "^1.3.0"
55
+ },
56
+ devDependencies: {
57
+ "@changesets/cli": "^2.31.1",
58
+ "@commitlint/cli": "^20.0.0",
59
+ "@commitlint/config-conventional": "^20.0.0",
60
+ "@eslint/js": "^10.0.1",
61
+ "@types/node": "^20.19.43",
62
+ "@types/proper-lockfile": "^4.1.4",
63
+ "@typescript-eslint/eslint-plugin": "^8.65.0",
64
+ "@typescript-eslint/parser": "^8.65.0",
65
+ "@vitest/coverage-v8": "^4.1.10",
66
+ eslint: "^10.8.0",
67
+ husky: "^9.1.7",
68
+ "lint-staged": "^16.2.7",
69
+ prettier: "^3.9.6",
70
+ tsup: "^8.5.1",
71
+ tsx: "^4.23.1",
72
+ typescript: "^6.0.3",
73
+ vitest: "^4.1.10"
74
+ },
75
+ "lint-staged": {
76
+ "*.{ts,js,mjs,cjs}": [
77
+ "eslint --fix",
78
+ "prettier --write"
79
+ ],
80
+ "*.{json,md,yml,yaml}": [
81
+ "prettier --write"
82
+ ]
83
+ }
84
+ };
85
+
86
+ // src/agents/claude-code.ts
87
+ import { readFile } from "fs/promises";
88
+ import os from "os";
89
+ import path from "path";
90
+ import { z } from "zod";
91
+
92
+ // src/core/errors.ts
93
+ var CliError = class extends Error {
94
+ code;
95
+ exitCode;
96
+ hint;
97
+ cause;
98
+ constructor(code, message, options) {
99
+ super(message);
100
+ this.name = "CliError";
101
+ this.code = code;
102
+ this.exitCode = options.exitCode;
103
+ this.hint = options.hint;
104
+ this.cause = options.cause;
105
+ }
106
+ toShape() {
107
+ return {
108
+ code: this.code,
109
+ message: this.message,
110
+ ...this.hint ? { hint: this.hint } : {}
111
+ };
112
+ }
113
+ };
114
+ var exitCodes = {
115
+ success: 0,
116
+ invalidArgument: 2,
117
+ networkUnreachable: 10,
118
+ serviceNotReady: 11,
119
+ authRequired: 20,
120
+ permissionDenied: 21,
121
+ resourceNotFound: 30,
122
+ documentNotReady: 31,
123
+ mcpFailed: 40,
124
+ hostNotFound: 41,
125
+ hostConfigFailed: 42,
126
+ configConflict: 50,
127
+ dependencyMissing: 60,
128
+ internalError: 70
129
+ };
130
+ function toCliError(error) {
131
+ if (error instanceof CliError) {
132
+ return error;
133
+ }
134
+ return new CliError("INTERNAL_ERROR", "Unexpected internal error", {
135
+ exitCode: exitCodes.internalError,
136
+ cause: error,
137
+ hint: "Run again with SAG_DEBUG=1 for technical details."
138
+ });
139
+ }
140
+
141
+ // src/agents/adapter.ts
142
+ import { createHash } from "crypto";
143
+ function canonicalize(value) {
144
+ if (Array.isArray(value)) {
145
+ return value.map(canonicalize);
146
+ }
147
+ if (value && typeof value === "object") {
148
+ return Object.fromEntries(
149
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, canonicalize(nested)])
150
+ );
151
+ }
152
+ return value;
153
+ }
154
+ function fingerprintValue(value) {
155
+ const canonical = JSON.stringify(canonicalize(value));
156
+ return `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
157
+ }
158
+ function fingerprintConnection(spec) {
159
+ return fingerprintValue(spec);
160
+ }
161
+ function connectionKey(agent, scope, serverName) {
162
+ return `${agent}:${scope}:${serverName}`;
163
+ }
164
+
165
+ // src/agents/claude-code.ts
166
+ var claudeConfigSchema = z.object({
167
+ mcpServers: z.record(z.string(), z.unknown()).optional()
168
+ });
169
+ var claudeEntrySchema = z.object({
170
+ type: z.literal("stdio").optional(),
171
+ command: z.string().min(1),
172
+ args: z.array(z.string()).optional(),
173
+ env: z.record(z.string(), z.string()).optional()
174
+ });
175
+ function invalidSpec() {
176
+ return new CliError(
177
+ "INVALID_ARGUMENT",
178
+ "Claude Code v0.2 supports Docker stdio MCP connections only",
179
+ {
180
+ exitCode: exitCodes.invalidArgument
181
+ }
182
+ );
183
+ }
184
+ function hostFailure(message, cause) {
185
+ return new CliError("HOST_CONFIG_FAILED", message, {
186
+ exitCode: exitCodes.hostConfigFailed,
187
+ ...cause ? { cause } : {},
188
+ hint: "Run `claude mcp list` to inspect the current Claude Code MCP configuration."
189
+ });
190
+ }
191
+ function ensureSuccess(result, action) {
192
+ if (result.exitCode !== 0) {
193
+ throw hostFailure(`Claude Code failed to ${action}`);
194
+ }
195
+ }
196
+ var ClaudeCodeAdapter = class {
197
+ constructor(runner, options = {}) {
198
+ this.runner = runner;
199
+ const configPath = options.configPath ?? path.join(os.homedir(), ".claude.json");
200
+ this.readConfig = options.readConfig ?? (() => readFile(configPath, "utf8").catch((error) => {
201
+ if (error.code === "ENOENT") return "{}";
202
+ throw error;
203
+ }));
204
+ }
205
+ runner;
206
+ id = "claude-code";
207
+ readConfig;
208
+ async detect() {
209
+ try {
210
+ const result = await this.runner.run({
211
+ command: "claude",
212
+ args: ["--version"],
213
+ timeoutMs: 5e3
214
+ });
215
+ if (result.exitCode !== 0) {
216
+ throw new Error("Claude Code returned an error");
217
+ }
218
+ return { id: this.id, version: result.stdout.trim() };
219
+ } catch (cause) {
220
+ throw new CliError("HOST_NOT_FOUND", "Claude Code CLI was not found", {
221
+ exitCode: exitCodes.hostNotFound,
222
+ cause,
223
+ hint: "Install Claude Code and ensure `claude` is available on PATH."
224
+ });
225
+ }
226
+ }
227
+ async read(name, scope) {
228
+ try {
229
+ const config = claudeConfigSchema.parse(JSON.parse(await this.readConfig()));
230
+ const rawEntry = config.mcpServers?.[name];
231
+ if (!rawEntry) return null;
232
+ const entry = claudeEntrySchema.parse(rawEntry);
233
+ const spec = {
234
+ transport: "stdio",
235
+ command: entry.command,
236
+ args: entry.args ?? [],
237
+ env: entry.env ?? {}
238
+ };
239
+ return {
240
+ agent: this.id,
241
+ name,
242
+ scope,
243
+ spec,
244
+ fingerprint: fingerprintConnection(spec)
245
+ };
246
+ } catch (cause) {
247
+ throw hostFailure("Claude Code user MCP configuration is invalid", cause);
248
+ }
249
+ }
250
+ async add(name, scope, spec) {
251
+ if (spec.transport !== "stdio" || Object.keys(spec.env).length) {
252
+ throw invalidSpec();
253
+ }
254
+ ensureSuccess(
255
+ await this.runner.run({
256
+ command: "claude",
257
+ args: ["mcp", "add", "--scope", scope, name, "--", spec.command, ...spec.args],
258
+ timeoutMs: 15e3
259
+ }),
260
+ "add the MCP server"
261
+ );
262
+ }
263
+ async remove(name, scope) {
264
+ ensureSuccess(
265
+ await this.runner.run({
266
+ command: "claude",
267
+ args: ["mcp", "remove", "--scope", scope, name],
268
+ timeoutMs: 15e3
269
+ }),
270
+ "remove the MCP server"
271
+ );
272
+ }
273
+ };
274
+
275
+ // src/agents/codex.ts
276
+ import { z as z2 } from "zod";
277
+ var codexEntrySchema = z2.object({
278
+ name: z2.string().optional(),
279
+ enabled: z2.boolean(),
280
+ disabled_reason: z2.unknown().optional(),
281
+ transport: z2.object({
282
+ type: z2.literal("stdio"),
283
+ command: z2.string().min(1),
284
+ args: z2.array(z2.string()).optional(),
285
+ env: z2.record(z2.string(), z2.string()).nullish()
286
+ }).passthrough(),
287
+ startup_timeout_sec: z2.number().nullish(),
288
+ tool_timeout_sec: z2.number().nullish(),
289
+ auth_status: z2.unknown().optional()
290
+ }).passthrough();
291
+ var CODEX_STATUS_FIELDS = /* @__PURE__ */ new Set(["name", "disabled_reason", "auth_status"]);
292
+ function invalidSpec2() {
293
+ return new CliError(
294
+ "INVALID_ARGUMENT",
295
+ "Codex v0.2 supports Docker stdio MCP connections only",
296
+ {
297
+ exitCode: exitCodes.invalidArgument
298
+ }
299
+ );
300
+ }
301
+ function hostFailure2(message, cause) {
302
+ return new CliError("HOST_CONFIG_FAILED", message, {
303
+ exitCode: exitCodes.hostConfigFailed,
304
+ ...cause ? { cause } : {},
305
+ hint: "Run `codex mcp list` to inspect the current Codex MCP configuration."
306
+ });
307
+ }
308
+ function ensureSuccess2(result, action) {
309
+ if (result.exitCode !== 0) {
310
+ throw hostFailure2(`Codex failed to ${action}`);
311
+ }
312
+ }
313
+ var CodexAdapter = class {
314
+ constructor(runner) {
315
+ this.runner = runner;
316
+ }
317
+ runner;
318
+ id = "codex";
319
+ async detect() {
320
+ try {
321
+ const result = await this.runner.run({
322
+ command: "codex",
323
+ args: ["--version"],
324
+ timeoutMs: 5e3
325
+ });
326
+ if (result.exitCode !== 0) throw new Error("Codex returned an error");
327
+ return { id: this.id, version: result.stdout.trim() };
328
+ } catch (cause) {
329
+ throw new CliError("HOST_NOT_FOUND", "Codex CLI was not found", {
330
+ exitCode: exitCodes.hostNotFound,
331
+ cause,
332
+ hint: "Install Codex CLI and ensure `codex` is available on PATH."
333
+ });
334
+ }
335
+ }
336
+ async read(name, scope) {
337
+ const result = await this.runner.run({
338
+ command: "codex",
339
+ args: ["mcp", "get", name, "--json"],
340
+ timeoutMs: 15e3
341
+ });
342
+ if (result.exitCode !== 0) {
343
+ if (/not found|does not exist|no mcp server/iu.test(result.stderr)) {
344
+ return null;
345
+ }
346
+ throw hostFailure2("Codex failed to read the MCP configuration");
347
+ }
348
+ try {
349
+ const entry = codexEntrySchema.parse(JSON.parse(result.stdout));
350
+ const spec = {
351
+ transport: "stdio",
352
+ command: entry.transport.command,
353
+ args: entry.transport.args ?? [],
354
+ env: entry.transport.env ?? {}
355
+ };
356
+ const configuration = Object.fromEntries(
357
+ Object.entries(entry).filter(([key]) => !CODEX_STATUS_FIELDS.has(key))
358
+ );
359
+ return {
360
+ agent: this.id,
361
+ name,
362
+ scope,
363
+ spec,
364
+ fingerprint: fingerprintValue(configuration)
365
+ };
366
+ } catch (cause) {
367
+ throw hostFailure2("Codex returned an invalid MCP configuration", cause);
368
+ }
369
+ }
370
+ async add(name, scope, spec) {
371
+ if (scope !== "user" || spec.transport !== "stdio" || Object.keys(spec.env).length) {
372
+ throw invalidSpec2();
373
+ }
374
+ ensureSuccess2(
375
+ await this.runner.run({
376
+ command: "codex",
377
+ args: ["mcp", "add", name, "--", spec.command, ...spec.args],
378
+ timeoutMs: 15e3
379
+ }),
380
+ "add the MCP server"
381
+ );
382
+ }
383
+ async remove(name, scope) {
384
+ if (scope !== "user") {
385
+ throw invalidSpec2();
386
+ }
387
+ ensureSuccess2(
388
+ await this.runner.run({
389
+ command: "codex",
390
+ args: ["mcp", "remove", name],
391
+ timeoutMs: 15e3
392
+ }),
393
+ "remove the MCP server"
394
+ );
395
+ }
396
+ };
397
+
398
+ // src/agents/state.ts
399
+ import { chmod, mkdir, readFile as readFile2, rename, unlink, writeFile } from "fs/promises";
400
+ import path2 from "path";
401
+ import { lock } from "proper-lockfile";
402
+ import { parse, stringify } from "yaml";
403
+ import { z as z3 } from "zod";
404
+ var managedConnectionSchema = z3.strictObject({
405
+ agent: z3.enum(["codex", "claude-code"]),
406
+ provider: z3.literal("docker-stdio"),
407
+ profile: z3.string().min(1),
408
+ serverName: z3.string().min(1),
409
+ docker: z3.strictObject({
410
+ composeProject: z3.string().min(1).optional(),
411
+ composeService: z3.string().min(1).optional(),
412
+ containerName: z3.string().min(1)
413
+ }),
414
+ sourceId: z3.string().min(1).nullable(),
415
+ scope: z3.literal("user"),
416
+ fingerprint: z3.string().regex(/^sha256:[a-f0-9]{64}$/u),
417
+ createdAt: z3.string().min(1)
418
+ });
419
+ var managedConnectionsSchema = z3.strictObject({
420
+ version: z3.literal(1),
421
+ connections: z3.record(z3.string(), managedConnectionSchema)
422
+ });
423
+ function emptyState() {
424
+ return { version: 1, connections: {} };
425
+ }
426
+ function invalidState(cause, filePath) {
427
+ return new CliError("CONFIG_CONFLICT", "Managed connection state is invalid", {
428
+ exitCode: exitCodes.configConflict,
429
+ cause,
430
+ hint: `Review or restore ${filePath}.`
431
+ });
432
+ }
433
+ var ManagedConnectionStore = class {
434
+ constructor(filePath) {
435
+ this.filePath = filePath;
436
+ }
437
+ filePath;
438
+ async #withLock(operation) {
439
+ await mkdir(path2.dirname(this.filePath), { recursive: true });
440
+ let release;
441
+ try {
442
+ release = await lock(this.filePath, {
443
+ realpath: false,
444
+ stale: 1e4,
445
+ update: 2e3,
446
+ retries: {
447
+ retries: 40,
448
+ factor: 1,
449
+ minTimeout: 25,
450
+ maxTimeout: 100
451
+ }
452
+ });
453
+ } catch (cause) {
454
+ throw new CliError("CONFIG_CONFLICT", "Managed connection state is busy", {
455
+ exitCode: exitCodes.configConflict,
456
+ cause,
457
+ hint: "Wait for the other SAG CLI Agent command to finish and retry."
458
+ });
459
+ }
460
+ try {
461
+ return await operation();
462
+ } finally {
463
+ await release();
464
+ }
465
+ }
466
+ async load() {
467
+ let raw;
468
+ try {
469
+ raw = await readFile2(this.filePath, "utf8");
470
+ } catch (error) {
471
+ if (error.code === "ENOENT") {
472
+ return emptyState();
473
+ }
474
+ throw error;
475
+ }
476
+ try {
477
+ return managedConnectionsSchema.parse(parse(raw));
478
+ } catch (cause) {
479
+ throw invalidState(cause, this.filePath);
480
+ }
481
+ }
482
+ async #saveUnlocked(state) {
483
+ let validated;
484
+ try {
485
+ validated = managedConnectionsSchema.parse(state);
486
+ } catch (cause) {
487
+ throw invalidState(cause, this.filePath);
488
+ }
489
+ const directory = path2.dirname(this.filePath);
490
+ await mkdir(directory, { recursive: true });
491
+ const temporaryPath = path2.join(
492
+ directory,
493
+ `.${path2.basename(this.filePath)}.${process.pid}.${Date.now()}.tmp`
494
+ );
495
+ try {
496
+ await writeFile(temporaryPath, stringify(validated), {
497
+ encoding: "utf8",
498
+ mode: 384
499
+ });
500
+ await rename(temporaryPath, this.filePath);
501
+ await chmod(this.filePath, 384);
502
+ } catch (error) {
503
+ await unlink(temporaryPath).catch(() => void 0);
504
+ throw error;
505
+ }
506
+ }
507
+ async save(state) {
508
+ await this.#withLock(() => this.#saveUnlocked(state));
509
+ }
510
+ async get(key) {
511
+ return (await this.load()).connections[key] ?? null;
512
+ }
513
+ async set(connection) {
514
+ const validated = (() => {
515
+ try {
516
+ return managedConnectionSchema.parse(connection);
517
+ } catch (cause) {
518
+ throw invalidState(cause, this.filePath);
519
+ }
520
+ })();
521
+ await this.#withLock(async () => {
522
+ const state = await this.load();
523
+ const key = connectionKey(validated.agent, validated.scope, validated.serverName);
524
+ state.connections[key] = validated;
525
+ await this.#saveUnlocked(state);
526
+ });
527
+ }
528
+ async delete(key) {
529
+ await this.#withLock(async () => {
530
+ const state = await this.load();
531
+ delete state.connections[key];
532
+ await this.#saveUnlocked(state);
533
+ });
534
+ }
535
+ async list(input) {
536
+ return Object.values((await this.load()).connections).filter(
537
+ (connection) => (!input?.agent || connection.agent === input.agent) && (!input?.scope || connection.scope === input.scope)
538
+ );
539
+ }
540
+ };
541
+
542
+ // src/config/store.ts
543
+ import { mkdir as mkdir2, readFile as readFile3, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
544
+ import path3 from "path";
545
+ import { parse as parse2, stringify as stringify2 } from "yaml";
546
+
547
+ // src/config/resolve.ts
548
+ var PROFILE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/u;
549
+ var STRIPPED_PATHS = /* @__PURE__ */ new Set(["/", "/api/v1", "/api/v1/", "/mcp", "/mcp/"]);
550
+ function validateProfileName(name) {
551
+ if (!PROFILE_NAME.test(name)) {
552
+ throw new CliError("INVALID_ARGUMENT", `Invalid profile name: ${name}`, {
553
+ exitCode: exitCodes.invalidArgument,
554
+ hint: "Use 1-64 letters, numbers, hyphens, or underscores."
555
+ });
556
+ }
557
+ return name;
558
+ }
559
+ function normalizeOrigin(input) {
560
+ let url;
561
+ try {
562
+ url = new URL(input);
563
+ } catch (cause) {
564
+ throw new CliError("INVALID_ARGUMENT", `Invalid SAG URL: ${input}`, {
565
+ exitCode: exitCodes.invalidArgument,
566
+ cause,
567
+ hint: "Use an absolute HTTP or HTTPS URL such as http://localhost:8000."
568
+ });
569
+ }
570
+ if (!["http:", "https:"].includes(url.protocol)) {
571
+ throw new CliError("INVALID_ARGUMENT", "SAG URL must use HTTP or HTTPS", {
572
+ exitCode: exitCodes.invalidArgument
573
+ });
574
+ }
575
+ if (url.username || url.password) {
576
+ throw new CliError("INVALID_ARGUMENT", "SAG URL must not contain credentials", {
577
+ exitCode: exitCodes.invalidArgument
578
+ });
579
+ }
580
+ if (url.search || url.hash) {
581
+ throw new CliError(
582
+ "INVALID_ARGUMENT",
583
+ "SAG URL must not contain a query string or fragment",
584
+ { exitCode: exitCodes.invalidArgument }
585
+ );
586
+ }
587
+ if (!STRIPPED_PATHS.has(url.pathname)) {
588
+ throw new CliError(
589
+ "INVALID_ARGUMENT",
590
+ `Unsupported path in SAG URL: ${url.pathname}`,
591
+ {
592
+ exitCode: exitCodes.invalidArgument,
593
+ hint: "Store only the SAG origin; /api/v1 and /mcp/ are added automatically."
594
+ }
595
+ );
596
+ }
597
+ return url.origin;
598
+ }
599
+ function profileOrThrow(config, name) {
600
+ validateProfileName(name);
601
+ const profile = config.profiles[name];
602
+ if (!profile) {
603
+ throw new CliError("RESOURCE_NOT_FOUND", `Profile not found: ${name}`, {
604
+ exitCode: exitCodes.resourceNotFound,
605
+ hint: "Run `sag profile list` to view configured profiles."
606
+ });
607
+ }
608
+ return profile;
609
+ }
610
+ function withOptionalFields(base, profileName, profile, environmentToken) {
611
+ return {
612
+ ...base,
613
+ ...profileName ? { profileName } : {},
614
+ ...profile ? { credentialRef: profile.credentialRef } : {},
615
+ ...environmentToken ? { environmentToken } : {}
616
+ };
617
+ }
618
+ function resolveConnection(options, environment, config) {
619
+ const environmentToken = environment.SAG_TOKEN?.trim() || void 0;
620
+ if (options.url) {
621
+ const profileName = options.profile ? validateProfileName(options.profile) : void 0;
622
+ const profile = profileName ? profileOrThrow(config, profileName) : void 0;
623
+ return withOptionalFields(
624
+ { url: normalizeOrigin(options.url), source: "explicit-url" },
625
+ profileName,
626
+ profile,
627
+ environmentToken
628
+ );
629
+ }
630
+ if (options.profile) {
631
+ const profileName = validateProfileName(options.profile);
632
+ const profile = profileOrThrow(config, profileName);
633
+ return withOptionalFields(
634
+ { url: normalizeOrigin(profile.url), source: "explicit-profile" },
635
+ profileName,
636
+ profile,
637
+ environmentToken
638
+ );
639
+ }
640
+ if (environment.SAG_URL) {
641
+ return withOptionalFields(
642
+ {
643
+ url: normalizeOrigin(environment.SAG_URL),
644
+ source: "environment-url"
645
+ },
646
+ void 0,
647
+ void 0,
648
+ environmentToken
649
+ );
650
+ }
651
+ if (environment.SAG_PROFILE) {
652
+ const profileName = validateProfileName(environment.SAG_PROFILE);
653
+ const profile = profileOrThrow(config, profileName);
654
+ return withOptionalFields(
655
+ { url: normalizeOrigin(profile.url), source: "environment-profile" },
656
+ profileName,
657
+ profile,
658
+ environmentToken
659
+ );
660
+ }
661
+ if (config.currentProfile) {
662
+ const profileName = config.currentProfile;
663
+ const profile = profileOrThrow(config, profileName);
664
+ return withOptionalFields(
665
+ { url: normalizeOrigin(profile.url), source: "current-profile" },
666
+ profileName,
667
+ profile,
668
+ environmentToken
669
+ );
670
+ }
671
+ return withOptionalFields(
672
+ { url: "http://127.0.0.1:8000", source: "local-probe" },
673
+ void 0,
674
+ void 0,
675
+ environmentToken
676
+ );
677
+ }
678
+
679
+ // src/config/schema.ts
680
+ import { z as z4 } from "zod";
681
+ var profileSchema = z4.object({
682
+ url: z4.string(),
683
+ credentialRef: z4.string().min(1),
684
+ defaults: z4.object({
685
+ output: z4.enum(["human", "json"]).optional()
686
+ }).optional()
687
+ });
688
+ var cliConfigSchema = z4.object({
689
+ version: z4.literal(1),
690
+ currentProfile: z4.string().optional(),
691
+ profiles: z4.record(z4.string(), profileSchema)
692
+ });
693
+ var emptyConfig = () => ({
694
+ version: 1,
695
+ profiles: {}
696
+ });
697
+
698
+ // src/config/store.ts
699
+ var ConfigStore = class {
700
+ constructor(filePath) {
701
+ this.filePath = filePath;
702
+ }
703
+ filePath;
704
+ async load() {
705
+ let raw;
706
+ try {
707
+ raw = await readFile3(this.filePath, "utf8");
708
+ } catch (error) {
709
+ if (error.code === "ENOENT") {
710
+ return emptyConfig();
711
+ }
712
+ throw error;
713
+ }
714
+ try {
715
+ return cliConfigSchema.parse(parse2(raw));
716
+ } catch (cause) {
717
+ throw new CliError("CONFIG_CONFLICT", "CLI config is invalid", {
718
+ exitCode: exitCodes.configConflict,
719
+ cause,
720
+ hint: `Review or restore ${this.filePath}.`
721
+ });
722
+ }
723
+ }
724
+ async save(config) {
725
+ const validated = cliConfigSchema.parse(config);
726
+ const directory = path3.dirname(this.filePath);
727
+ await mkdir2(directory, { recursive: true });
728
+ const temporaryPath = path3.join(
729
+ directory,
730
+ `.${path3.basename(this.filePath)}.${process.pid}.${Date.now()}.tmp`
731
+ );
732
+ try {
733
+ await writeFile2(temporaryPath, stringify2(validated), {
734
+ encoding: "utf8",
735
+ mode: 384
736
+ });
737
+ await rename2(temporaryPath, this.filePath);
738
+ } catch (error) {
739
+ await unlink2(temporaryPath).catch(() => void 0);
740
+ throw error;
741
+ }
742
+ }
743
+ async addProfile(name, url) {
744
+ validateProfileName(name);
745
+ const normalizedUrl = normalizeOrigin(url);
746
+ const config = await this.load();
747
+ const existing = config.profiles[name];
748
+ if (existing && existing.url !== normalizedUrl) {
749
+ throw new CliError(
750
+ "CONFIG_CONFLICT",
751
+ `Profile already points to a different SAG origin: ${name}`,
752
+ {
753
+ exitCode: exitCodes.configConflict,
754
+ hint: "Remove the profile first or choose another name."
755
+ }
756
+ );
757
+ }
758
+ const profile = existing ?? {
759
+ url: normalizedUrl,
760
+ credentialRef: `sag-cli/${name}`
761
+ };
762
+ config.profiles[name] = profile;
763
+ config.currentProfile ??= name;
764
+ await this.save(config);
765
+ return {
766
+ name,
767
+ current: config.currentProfile === name,
768
+ ...profile
769
+ };
770
+ }
771
+ async listProfiles() {
772
+ const config = await this.load();
773
+ return Object.entries(config.profiles).sort(([left], [right]) => left.localeCompare(right)).map(([name, profile]) => ({
774
+ name,
775
+ current: config.currentProfile === name,
776
+ ...profile
777
+ }));
778
+ }
779
+ async getProfile(name) {
780
+ const config = await this.load();
781
+ const selectedName = name ?? config.currentProfile;
782
+ if (!selectedName || !config.profiles[selectedName]) {
783
+ throw new CliError("RESOURCE_NOT_FOUND", "Profile not found", {
784
+ exitCode: exitCodes.resourceNotFound,
785
+ hint: "Run `sag profile add <name> <url>` first."
786
+ });
787
+ }
788
+ return {
789
+ name: selectedName,
790
+ current: config.currentProfile === selectedName,
791
+ ...config.profiles[selectedName]
792
+ };
793
+ }
794
+ async useProfile(name) {
795
+ const config = await this.load();
796
+ validateProfileName(name);
797
+ const profile = config.profiles[name];
798
+ if (!profile) {
799
+ throw new CliError("RESOURCE_NOT_FOUND", `Profile not found: ${name}`, {
800
+ exitCode: exitCodes.resourceNotFound
801
+ });
802
+ }
803
+ config.currentProfile = name;
804
+ await this.save(config);
805
+ return { name, current: true, ...profile };
806
+ }
807
+ async removeProfile(name) {
808
+ const config = await this.load();
809
+ validateProfileName(name);
810
+ if (!config.profiles[name]) {
811
+ throw new CliError("RESOURCE_NOT_FOUND", `Profile not found: ${name}`, {
812
+ exitCode: exitCodes.resourceNotFound
813
+ });
814
+ }
815
+ delete config.profiles[name];
816
+ if (config.currentProfile === name) {
817
+ config.currentProfile = Object.keys(config.profiles).sort()[0];
818
+ if (!config.currentProfile) {
819
+ delete config.currentProfile;
820
+ }
821
+ }
822
+ await this.save(config);
823
+ }
824
+ };
825
+
826
+ // src/config/paths.ts
827
+ import os2 from "os";
828
+ import path4 from "path";
829
+ function defaultConfigPath(options = {}) {
830
+ const platform = options.platform ?? process.platform;
831
+ const homeDirectory = options.homeDirectory ?? os2.homedir();
832
+ const environment = options.environment ?? process.env;
833
+ if (platform === "darwin") {
834
+ return path4.join(
835
+ homeDirectory,
836
+ "Library",
837
+ "Application Support",
838
+ "sag-cli",
839
+ "config.yaml"
840
+ );
841
+ }
842
+ if (platform === "win32") {
843
+ const appData = environment.APPDATA;
844
+ return path4.join(
845
+ appData || path4.join(homeDirectory, "AppData", "Roaming"),
846
+ "sag-cli",
847
+ "config.yaml"
848
+ );
849
+ }
850
+ const xdgConfig = environment.XDG_CONFIG_HOME;
851
+ return path4.join(
852
+ xdgConfig || path4.join(homeDirectory, ".config"),
853
+ "sag-cli",
854
+ "config.yaml"
855
+ );
856
+ }
857
+ function defaultManagedConnectionsPath(options = {}) {
858
+ return path4.join(
859
+ path4.dirname(defaultConfigPath(options)),
860
+ "managed-connections.yaml"
861
+ );
862
+ }
863
+
864
+ // src/credentials/memory.ts
865
+ var MemoryCredentialStore = class {
866
+ kind = "memory";
867
+ #values = /* @__PURE__ */ new Map();
868
+ async get(reference) {
869
+ return this.#values.get(reference) ?? null;
870
+ }
871
+ async set(reference, value) {
872
+ this.#values.set(reference, value);
873
+ }
874
+ async delete(reference) {
875
+ this.#values.delete(reference);
876
+ }
877
+ };
878
+
879
+ // src/credentials/keychain.ts
880
+ var SERVICE_NAME = "@zleap-ai/sag-cli";
881
+ var KeychainCredentialStore = class {
882
+ constructor(entryFactory) {
883
+ this.entryFactory = entryFactory;
884
+ }
885
+ entryFactory;
886
+ kind = "keychain";
887
+ async get(reference) {
888
+ return this.entryFactory(SERVICE_NAME, reference).getPassword();
889
+ }
890
+ async set(reference, value) {
891
+ this.entryFactory(SERVICE_NAME, reference).setPassword(value);
892
+ }
893
+ async delete(reference) {
894
+ this.entryFactory(SERVICE_NAME, reference).deletePassword();
895
+ }
896
+ };
897
+ async function createKeychainCredentialStore() {
898
+ const { Entry } = await import("@napi-rs/keyring");
899
+ return new KeychainCredentialStore((service, account) => new Entry(service, account));
900
+ }
901
+ async function createCredentialStore(loadKeychain = createKeychainCredentialStore) {
902
+ try {
903
+ return { store: await loadKeychain() };
904
+ } catch {
905
+ return {
906
+ store: new MemoryCredentialStore(),
907
+ warning: "System Keychain is unavailable; credentials will remain in memory only and must be provided again next time."
908
+ };
909
+ }
910
+ }
911
+
912
+ // src/docker/schemas.ts
913
+ import { z as z5 } from "zod";
914
+ var dockerVersionSchema = z5.object({
915
+ Platform: z5.object({
916
+ Name: z5.string().min(1)
917
+ })
918
+ });
919
+ var dockerPsLineSchema = z5.object({
920
+ ID: z5.string().min(1),
921
+ Names: z5.string().min(1),
922
+ Image: z5.string(),
923
+ Labels: z5.string()
924
+ });
925
+ var dockerInspectSchema = z5.array(
926
+ z5.object({
927
+ Id: z5.string().min(1),
928
+ Name: z5.string().min(1),
929
+ Config: z5.object({
930
+ Labels: z5.record(z5.string(), z5.string()).nullish()
931
+ }),
932
+ State: z5.object({
933
+ Running: z5.boolean(),
934
+ Health: z5.object({
935
+ Status: z5.enum(["healthy", "unhealthy", "starting"])
936
+ }).optional()
937
+ })
938
+ })
939
+ ).length(1);
940
+
941
+ // src/docker/client.ts
942
+ var DOCKER_TIMEOUT_MS = 5e3;
943
+ function dependencyFailure(message, cause) {
944
+ return new CliError("DEPENDENCY_MISSING", message, {
945
+ exitCode: exitCodes.dependencyMissing,
946
+ ...cause ? { cause } : {},
947
+ hint: "Check that Docker Desktop or Docker Engine is running and accessible."
948
+ });
949
+ }
950
+ function invalidDockerResponse(cause) {
951
+ return new CliError("INVALID_RESPONSE", "Docker returned an invalid response", {
952
+ exitCode: exitCodes.internalError,
953
+ cause,
954
+ hint: "Check the installed Docker CLI version and try again."
955
+ });
956
+ }
957
+ function parseLabels(value) {
958
+ const labels = {};
959
+ for (const pair of value.split(",")) {
960
+ if (!pair) continue;
961
+ const separator = pair.indexOf("=");
962
+ if (separator === -1) continue;
963
+ labels[pair.slice(0, separator)] = pair.slice(separator + 1);
964
+ }
965
+ return labels;
966
+ }
967
+ var DockerClient = class {
968
+ constructor(runner) {
969
+ this.runner = runner;
970
+ }
971
+ runner;
972
+ async version() {
973
+ const result = await this.runner.run({
974
+ command: "docker",
975
+ args: ["version", "--format", "{{json .Server}}"],
976
+ timeoutMs: DOCKER_TIMEOUT_MS
977
+ });
978
+ if (result.exitCode !== 0) {
979
+ throw dependencyFailure("Docker daemon is unavailable");
980
+ }
981
+ try {
982
+ const parsed = dockerVersionSchema.parse(JSON.parse(result.stdout));
983
+ return { platformName: parsed.Platform.Name };
984
+ } catch (cause) {
985
+ throw invalidDockerResponse(cause);
986
+ }
987
+ }
988
+ async listRunningContainers() {
989
+ const result = await this.runner.run({
990
+ command: "docker",
991
+ args: ["ps", "--format", "{{json .}}"],
992
+ timeoutMs: DOCKER_TIMEOUT_MS
993
+ });
994
+ if (result.exitCode !== 0) {
995
+ throw dependencyFailure("Unable to list Docker containers");
996
+ }
997
+ if (!result.stdout.trim()) return [];
998
+ try {
999
+ return result.stdout.trim().split(/\r?\n/u).map((line) => dockerPsLineSchema.parse(JSON.parse(line))).map((container) => ({
1000
+ id: container.ID,
1001
+ name: container.Names,
1002
+ image: container.Image,
1003
+ labels: parseLabels(container.Labels)
1004
+ }));
1005
+ } catch (cause) {
1006
+ throw invalidDockerResponse(cause);
1007
+ }
1008
+ }
1009
+ async inspect(container) {
1010
+ const result = await this.runner.run({
1011
+ command: "docker",
1012
+ args: ["inspect", container],
1013
+ timeoutMs: DOCKER_TIMEOUT_MS
1014
+ });
1015
+ if (result.exitCode !== 0) {
1016
+ if (/no such (?:object|container)/iu.test(result.stderr)) {
1017
+ throw new CliError("RESOURCE_NOT_FOUND", "Docker container was not found", {
1018
+ exitCode: exitCodes.resourceNotFound,
1019
+ hint: "Check the container name or omit `--container` to auto-discover SAG."
1020
+ });
1021
+ }
1022
+ throw dependencyFailure("Unable to inspect the Docker container");
1023
+ }
1024
+ try {
1025
+ const inspection = dockerInspectSchema.parse(JSON.parse(result.stdout))[0];
1026
+ return {
1027
+ id: inspection.Id,
1028
+ name: inspection.Name.replace(/^\/+/u, ""),
1029
+ running: inspection.State.Running,
1030
+ health: inspection.State.Health?.Status ?? "none",
1031
+ labels: inspection.Config.Labels ?? {}
1032
+ };
1033
+ } catch (cause) {
1034
+ throw invalidDockerResponse(cause);
1035
+ }
1036
+ }
1037
+ async canImportMcp(container) {
1038
+ const result = await this.runner.run({
1039
+ command: "docker",
1040
+ args: ["exec", container, "python", "-c", "import sag_api.mcp.server"],
1041
+ timeoutMs: DOCKER_TIMEOUT_MS
1042
+ });
1043
+ return result.exitCode === 0;
1044
+ }
1045
+ };
1046
+
1047
+ // src/mcp/verifier.ts
1048
+ import { Client } from "@modelcontextprotocol/client";
1049
+ import {
1050
+ getDefaultEnvironment,
1051
+ StdioClientTransport
1052
+ } from "@modelcontextprotocol/client/stdio";
1053
+
1054
+ // src/mcp/failure.ts
1055
+ function localMcpFailure(stage, message, options = {}) {
1056
+ const failureCause = {
1057
+ stage,
1058
+ ...options.cause ? { cause: options.cause } : {}
1059
+ };
1060
+ return new CliError("MCP_FAILED", message, {
1061
+ exitCode: exitCodes.mcpFailed,
1062
+ cause: failureCause,
1063
+ hint: options.hint ?? "Check the SAG API container logs and retry `sag mcp test`."
1064
+ });
1065
+ }
1066
+ function localMcpFailureStage(error) {
1067
+ if (error.code !== "MCP_FAILED" || typeof error.cause !== "object") {
1068
+ return void 0;
1069
+ }
1070
+ const stage = error.cause?.stage;
1071
+ return stage === "entrypoint" || stage === "protocol" || stage === "tools" || stage === "knowledge" ? stage : void 0;
1072
+ }
1073
+
1074
+ // src/mcp/verifier.ts
1075
+ var REQUIRED_SAG_TOOLS = [
1076
+ "list_sources",
1077
+ "list_documents",
1078
+ "outline",
1079
+ "search",
1080
+ "grep",
1081
+ "get_chunk",
1082
+ "read",
1083
+ "get_entity"
1084
+ ];
1085
+ var McpVerifier = class {
1086
+ constructor(cliVersion) {
1087
+ this.cliVersion = cliVersion;
1088
+ }
1089
+ cliVersion;
1090
+ async verify(spec, options) {
1091
+ if (spec.transport !== "stdio") {
1092
+ throw new CliError(
1093
+ "INVALID_ARGUMENT",
1094
+ "HTTP MCP is not supported in SAG CLI v0.2",
1095
+ {
1096
+ exitCode: exitCodes.invalidArgument,
1097
+ hint: "Use a local Docker stdio connection."
1098
+ }
1099
+ );
1100
+ }
1101
+ const client = new Client(
1102
+ {
1103
+ name: "@zleap-ai/sag-cli",
1104
+ version: this.cliVersion
1105
+ },
1106
+ {
1107
+ versionNegotiation: {
1108
+ mode: "auto",
1109
+ probe: {
1110
+ timeoutMs: Math.min(options.timeoutMs, 250),
1111
+ maxRetries: 0
1112
+ }
1113
+ }
1114
+ }
1115
+ );
1116
+ const transport = new StdioClientTransport({
1117
+ command: spec.command,
1118
+ args: spec.args,
1119
+ env: {
1120
+ ...getDefaultEnvironment(),
1121
+ ...spec.env
1122
+ },
1123
+ stderr: "pipe"
1124
+ });
1125
+ transport.stderr?.on("data", () => void 0);
1126
+ try {
1127
+ await client.connect(transport, { timeout: options.timeoutMs });
1128
+ const era = client.getProtocolEra();
1129
+ const protocolVersion = client.getNegotiatedProtocolVersion();
1130
+ if (!era || !protocolVersion) {
1131
+ throw localMcpFailure(
1132
+ "protocol",
1133
+ "MCP protocol negotiation returned no version"
1134
+ );
1135
+ }
1136
+ const listed = await client.listTools(void 0, {
1137
+ timeout: options.timeoutMs,
1138
+ cacheMode: "refresh"
1139
+ }).catch((cause) => {
1140
+ throw localMcpFailure("tools", "Unable to list SAG MCP tools", {
1141
+ cause
1142
+ });
1143
+ });
1144
+ const tools = listed.tools.map((tool) => tool.name);
1145
+ const missingTools = REQUIRED_SAG_TOOLS.filter(
1146
+ (required) => !tools.includes(required)
1147
+ );
1148
+ if (missingTools.length) {
1149
+ throw localMcpFailure(
1150
+ "tools",
1151
+ `SAG MCP is missing required tools: ${missingTools.join(", ")}`
1152
+ );
1153
+ }
1154
+ const listSources2 = await client.callTool(
1155
+ {
1156
+ name: "list_sources",
1157
+ arguments: {}
1158
+ },
1159
+ { timeout: options.timeoutMs }
1160
+ ).catch((cause) => {
1161
+ throw localMcpFailure("knowledge", "Unable to call SAG MCP list_sources", {
1162
+ cause
1163
+ });
1164
+ });
1165
+ if (listSources2.isError) {
1166
+ throw localMcpFailure("knowledge", "SAG MCP list_sources returned an error");
1167
+ }
1168
+ const server = client.getServerVersion();
1169
+ return {
1170
+ transport: "stdio",
1171
+ era,
1172
+ protocolVersion,
1173
+ ...server ? { server } : {},
1174
+ tools,
1175
+ missingTools,
1176
+ listSourcesCallable: true
1177
+ };
1178
+ } catch (cause) {
1179
+ if (cause instanceof CliError) {
1180
+ throw cause;
1181
+ }
1182
+ throw localMcpFailure("protocol", "Unable to verify SAG MCP", { cause });
1183
+ } finally {
1184
+ await client.close().catch(() => void 0);
1185
+ }
1186
+ }
1187
+ };
1188
+
1189
+ // src/process/runner.ts
1190
+ import { spawn } from "child_process";
1191
+ var NodeProcessRunner = class {
1192
+ async run(input) {
1193
+ return new Promise((resolve, reject) => {
1194
+ const child = spawn(input.command, input.args, {
1195
+ shell: false,
1196
+ env: input.env ?? process.env,
1197
+ stdio: [input.stdin === "pipe" ? "pipe" : "ignore", "pipe", "pipe"]
1198
+ });
1199
+ const childStdout = child.stdout;
1200
+ const childStderr = child.stderr;
1201
+ let stdout = "";
1202
+ let stderr = "";
1203
+ let timedOut = false;
1204
+ let forceKill;
1205
+ const timeout = setTimeout(() => {
1206
+ timedOut = true;
1207
+ child.kill("SIGTERM");
1208
+ forceKill = setTimeout(() => {
1209
+ child.kill("SIGKILL");
1210
+ }, 250);
1211
+ }, input.timeoutMs);
1212
+ childStdout?.setEncoding("utf8");
1213
+ childStderr?.setEncoding("utf8");
1214
+ childStdout?.on("data", (chunk) => {
1215
+ stdout += chunk;
1216
+ });
1217
+ childStderr?.on("data", (chunk) => {
1218
+ stderr += chunk;
1219
+ });
1220
+ child.on("error", (cause) => {
1221
+ clearTimeout(timeout);
1222
+ if (forceKill) clearTimeout(forceKill);
1223
+ reject(
1224
+ new CliError("DEPENDENCY_MISSING", `Unable to start ${input.command}`, {
1225
+ exitCode: exitCodes.dependencyMissing,
1226
+ cause,
1227
+ hint: `Install ${input.command} and ensure it is available on PATH.`
1228
+ })
1229
+ );
1230
+ });
1231
+ child.on("close", (exitCode) => {
1232
+ clearTimeout(timeout);
1233
+ if (forceKill) clearTimeout(forceKill);
1234
+ if (timedOut) {
1235
+ reject(
1236
+ new CliError("DEPENDENCY_MISSING", `${input.command} timed out`, {
1237
+ exitCode: exitCodes.dependencyMissing,
1238
+ hint: `Check that ${input.command} is responsive and try again.`
1239
+ })
1240
+ );
1241
+ return;
1242
+ }
1243
+ resolve({
1244
+ exitCode: exitCode ?? 1,
1245
+ stdout,
1246
+ stderr
1247
+ });
1248
+ });
1249
+ });
1250
+ }
1251
+ };
1252
+
1253
+ // src/program.ts
1254
+ import { Command, CommanderError, Option } from "commander";
1255
+
1256
+ // src/api/schemas.ts
1257
+ import { z as z6 } from "zod";
1258
+ var dateTime = z6.string().min(1);
1259
+ var rootSchema = z6.object({
1260
+ name: z6.string().min(1),
1261
+ version: z6.string().min(1),
1262
+ docs: z6.string()
1263
+ }).passthrough();
1264
+ var readySchema = z6.object({
1265
+ status: z6.string(),
1266
+ db: z6.boolean()
1267
+ }).passthrough();
1268
+ var capabilitiesSchema = z6.record(z6.string(), z6.unknown());
1269
+ var userSchema = z6.object({
1270
+ id: z6.string().min(1),
1271
+ email: z6.string(),
1272
+ name: z6.string(),
1273
+ created_at: dateTime.optional()
1274
+ }).passthrough();
1275
+ var sourceSchema = z6.object({
1276
+ id: z6.string().min(1),
1277
+ name: z6.string(),
1278
+ description: z6.string(),
1279
+ source_type: z6.enum(["document", "web", "message", "audio"]),
1280
+ connector_kind: z6.string(),
1281
+ status: z6.enum(["active", "paused", "error"]),
1282
+ document_count: z6.number().int().nonnegative(),
1283
+ chunk_count: z6.number().int().nonnegative(),
1284
+ event_count: z6.number().int().nonnegative(),
1285
+ created_at: dateTime,
1286
+ updated_at: dateTime
1287
+ }).passthrough();
1288
+ var documentStatusSchema = z6.enum([
1289
+ "pending",
1290
+ "loading",
1291
+ "extracting",
1292
+ "paused",
1293
+ "ready",
1294
+ "failed"
1295
+ ]);
1296
+ var documentSchema = z6.object({
1297
+ id: z6.string().min(1),
1298
+ source_id: z6.string().min(1),
1299
+ filename: z6.string(),
1300
+ content_type: z6.string(),
1301
+ size_bytes: z6.number().int().nonnegative(),
1302
+ status: documentStatusSchema,
1303
+ chunk_count: z6.number().int().nonnegative(),
1304
+ event_count: z6.number().int().nonnegative(),
1305
+ progress: z6.number().int().min(0).max(100),
1306
+ token_usage: z6.number().int().nonnegative(),
1307
+ error: z6.string().nullable(),
1308
+ created_at: dateTime,
1309
+ updated_at: dateTime
1310
+ }).passthrough();
1311
+ var searchSectionSchema = z6.object({
1312
+ chunk_id: z6.string().nullable(),
1313
+ heading: z6.string(),
1314
+ content: z6.string(),
1315
+ score: z6.number(),
1316
+ rank: z6.number().int(),
1317
+ source_id: z6.string().nullable(),
1318
+ source_name: z6.string().nullable().optional()
1319
+ }).passthrough();
1320
+ var searchEventSchema = z6.object({
1321
+ id: z6.string(),
1322
+ title: z6.string(),
1323
+ summary: z6.string(),
1324
+ rank: z6.number().int(),
1325
+ score: z6.number()
1326
+ }).passthrough();
1327
+ var sourceHitSchema = z6.object({
1328
+ source_id: z6.string(),
1329
+ source_name: z6.string().nullable().optional(),
1330
+ event_hits: z6.number().int(),
1331
+ max_score: z6.number(),
1332
+ latest_event_time: z6.string().nullable().optional()
1333
+ }).passthrough();
1334
+ var searchResponseSchema = z6.object({
1335
+ query: z6.string(),
1336
+ sections: z6.array(searchSectionSchema),
1337
+ events: z6.array(searchEventSchema),
1338
+ entities: z6.array(z6.record(z6.string(), z6.unknown())),
1339
+ relations: z6.array(z6.record(z6.string(), z6.unknown())),
1340
+ source_hits: z6.array(sourceHitSchema),
1341
+ summary: z6.string(),
1342
+ exploration_id: z6.string().nullable(),
1343
+ stats: z6.record(z6.string(), z6.unknown())
1344
+ }).passthrough();
1345
+ var mcpDescriptorSchema = z6.object({
1346
+ name: z6.string(),
1347
+ scope: z6.string(),
1348
+ source_count: z6.number().int().nonnegative(),
1349
+ tools: z6.array(z6.string()),
1350
+ http: z6.object({
1351
+ transport: z6.string(),
1352
+ url: z6.string().url(),
1353
+ headers: z6.record(z6.string(), z6.string())
1354
+ }).passthrough()
1355
+ }).passthrough();
1356
+
1357
+ // src/api/client.ts
1358
+ var SagClient = class {
1359
+ #origin;
1360
+ #token;
1361
+ #timeoutMs;
1362
+ #locale;
1363
+ #fetch;
1364
+ constructor(options) {
1365
+ this.#origin = normalizeOrigin(options.origin);
1366
+ this.#token = options.token;
1367
+ this.#timeoutMs = options.timeoutMs ?? 1e4;
1368
+ this.#locale = options.locale ?? "zh-CN";
1369
+ this.#fetch = options.fetchImplementation ?? fetch;
1370
+ }
1371
+ root() {
1372
+ return this.#request("/", rootSchema, { authenticated: false });
1373
+ }
1374
+ ready() {
1375
+ return this.#request("/api/v1/system/ready", readySchema, {
1376
+ authenticated: false
1377
+ });
1378
+ }
1379
+ capabilities() {
1380
+ return this.#request("/api/v1/system/capabilities", capabilitiesSchema, {
1381
+ authenticated: false
1382
+ });
1383
+ }
1384
+ me() {
1385
+ return this.#request("/api/v1/auth/me", userSchema);
1386
+ }
1387
+ mcpDescriptor() {
1388
+ return this.#request("/api/v1/system/mcp", mcpDescriptorSchema);
1389
+ }
1390
+ listSources() {
1391
+ return this.#request("/api/v1/sources", sourceSchema.array());
1392
+ }
1393
+ getSource(sourceId) {
1394
+ return this.#request(
1395
+ `/api/v1/sources/${encodeURIComponent(sourceId)}`,
1396
+ sourceSchema
1397
+ );
1398
+ }
1399
+ listDocuments(sourceId) {
1400
+ return this.#request(
1401
+ `/api/v1/sources/${encodeURIComponent(sourceId)}/documents`,
1402
+ documentSchema.array()
1403
+ );
1404
+ }
1405
+ getDocument(sourceId, documentId) {
1406
+ return this.#request(
1407
+ `/api/v1/sources/${encodeURIComponent(sourceId)}/documents/${encodeURIComponent(documentId)}`,
1408
+ documentSchema
1409
+ );
1410
+ }
1411
+ search(input) {
1412
+ return this.#request("/api/v1/search", searchResponseSchema, {
1413
+ method: "POST",
1414
+ body: input
1415
+ });
1416
+ }
1417
+ async #request(path5, schema, options = {}) {
1418
+ const authenticated = options.authenticated ?? true;
1419
+ if (authenticated && !this.#token) {
1420
+ throw new CliError("AUTH_REQUIRED", "No SAG token is configured", {
1421
+ exitCode: exitCodes.authRequired,
1422
+ hint: "Run `sag auth login` or set SAG_TOKEN."
1423
+ });
1424
+ }
1425
+ const headers = new Headers({
1426
+ Accept: "application/json",
1427
+ "Accept-Language": this.#locale
1428
+ });
1429
+ if (authenticated && this.#token) {
1430
+ headers.set("Authorization", `Bearer ${this.#token}`);
1431
+ }
1432
+ if (options.body !== void 0) {
1433
+ headers.set("Content-Type", "application/json");
1434
+ }
1435
+ let response;
1436
+ try {
1437
+ response = await this.#fetch(new URL(path5, `${this.#origin}/`), {
1438
+ method: options.method ?? "GET",
1439
+ headers,
1440
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
1441
+ signal: AbortSignal.timeout(this.#timeoutMs)
1442
+ });
1443
+ } catch (cause) {
1444
+ throw new CliError("NETWORK_UNREACHABLE", `Cannot reach SAG at ${this.#origin}`, {
1445
+ exitCode: exitCodes.networkUnreachable,
1446
+ cause,
1447
+ hint: "Check that SAG is running and the URL is reachable."
1448
+ });
1449
+ }
1450
+ const payload = await this.#readPayload(response);
1451
+ if (!response.ok) {
1452
+ throw this.#httpError(response.status, payload);
1453
+ }
1454
+ const parsed = schema.safeParse(payload);
1455
+ if (!parsed.success) {
1456
+ throw new CliError("INVALID_RESPONSE", "SAG returned an incompatible response", {
1457
+ exitCode: exitCodes.internalError,
1458
+ cause: parsed.error,
1459
+ hint: "Check the SAG/CLI compatibility matrix."
1460
+ });
1461
+ }
1462
+ return parsed.data;
1463
+ }
1464
+ async #readPayload(response) {
1465
+ try {
1466
+ return await response.json();
1467
+ } catch (cause) {
1468
+ throw new CliError("INVALID_RESPONSE", "SAG returned invalid JSON", {
1469
+ exitCode: exitCodes.internalError,
1470
+ cause
1471
+ });
1472
+ }
1473
+ }
1474
+ #httpError(status2, payload) {
1475
+ const serverMessage = payload && typeof payload === "object" && "error" in payload && payload.error && typeof payload.error === "object" && "message" in payload.error && typeof payload.error.message === "string" ? payload.error.message : `SAG request failed with HTTP ${status2}`;
1476
+ if (status2 === 401) {
1477
+ return new CliError("AUTH_REQUIRED", serverMessage, {
1478
+ exitCode: exitCodes.authRequired,
1479
+ hint: "Run `sag auth login` to refresh the token."
1480
+ });
1481
+ }
1482
+ if (status2 === 403) {
1483
+ return new CliError("PERMISSION_DENIED", serverMessage, {
1484
+ exitCode: exitCodes.permissionDenied
1485
+ });
1486
+ }
1487
+ if (status2 === 404) {
1488
+ return new CliError("RESOURCE_NOT_FOUND", serverMessage, {
1489
+ exitCode: exitCodes.resourceNotFound
1490
+ });
1491
+ }
1492
+ if (status2 === 409) {
1493
+ return new CliError("DOCUMENT_NOT_READY", serverMessage, {
1494
+ exitCode: exitCodes.documentNotReady
1495
+ });
1496
+ }
1497
+ if (status2 >= 500) {
1498
+ return new CliError("SERVICE_NOT_READY", serverMessage, {
1499
+ exitCode: exitCodes.serviceNotReady
1500
+ });
1501
+ }
1502
+ return new CliError("INVALID_RESPONSE", serverMessage, {
1503
+ exitCode: exitCodes.internalError
1504
+ });
1505
+ }
1506
+ };
1507
+
1508
+ // src/agents/orchestrator.ts
1509
+ var SERVER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/u;
1510
+ function validateServerName(serverName) {
1511
+ if (!SERVER_NAME_PATTERN.test(serverName)) {
1512
+ throw new CliError("INVALID_ARGUMENT", "Invalid MCP server name", {
1513
+ exitCode: exitCodes.invalidArgument,
1514
+ hint: "Use 1-64 letters, numbers, dots, underscores, or dashes."
1515
+ });
1516
+ }
1517
+ }
1518
+ function conflict(message) {
1519
+ return new CliError("CONFIG_CONFLICT", message, {
1520
+ exitCode: exitCodes.configConflict,
1521
+ hint: "Choose another `--name` or restore the SAG CLI managed entry."
1522
+ });
1523
+ }
1524
+ function hostFailure3(message, cause, hint) {
1525
+ return new CliError("HOST_CONFIG_FAILED", message, {
1526
+ exitCode: exitCodes.hostConfigFailed,
1527
+ ...cause ? { cause } : {},
1528
+ hint: hint ?? "Inspect the Agent MCP configuration and run the command again."
1529
+ });
1530
+ }
1531
+ function determinePlan(input) {
1532
+ let action;
1533
+ if (!input.hostFingerprint || !input.hostSpecFingerprint) {
1534
+ if (input.managed) {
1535
+ throw conflict("Managed state exists but the Agent MCP entry is missing");
1536
+ }
1537
+ action = "create";
1538
+ } else if (!input.managed) {
1539
+ throw conflict("The Agent MCP name is already owned by the user");
1540
+ } else if (input.managed.fingerprint !== input.hostFingerprint) {
1541
+ throw conflict("The managed Agent MCP entry was changed outside SAG CLI");
1542
+ } else if (input.hostSpecFingerprint === input.desiredFingerprint) {
1543
+ action = "unchanged";
1544
+ } else {
1545
+ action = "update";
1546
+ }
1547
+ return {
1548
+ action,
1549
+ agent: input.agent,
1550
+ serverName: input.serverName,
1551
+ scope: input.scope,
1552
+ provider: "docker-stdio",
1553
+ target: input.target.name,
1554
+ sourceId: input.sourceId ?? null,
1555
+ ...input.hostSpecFingerprint ? { currentFingerprint: input.hostSpecFingerprint } : {},
1556
+ desiredFingerprint: input.desiredFingerprint
1557
+ };
1558
+ }
1559
+ function managedConnection(input, resolved, fingerprint, existing) {
1560
+ return {
1561
+ agent: input.agent,
1562
+ provider: "docker-stdio",
1563
+ profile: input.profile,
1564
+ serverName: input.serverName,
1565
+ docker: {
1566
+ ...resolved.target.composeProject ? { composeProject: resolved.target.composeProject } : {},
1567
+ ...resolved.target.composeService ? { composeService: resolved.target.composeService } : {},
1568
+ containerName: resolved.target.name
1569
+ },
1570
+ sourceId: input.sourceId ?? null,
1571
+ scope: input.scope,
1572
+ fingerprint,
1573
+ createdAt: existing?.createdAt ?? input.now().toISOString()
1574
+ };
1575
+ }
1576
+ async function rollbackHost(input) {
1577
+ if (input.previousSpec) {
1578
+ try {
1579
+ await input.adapter.remove(input.serverName, input.scope);
1580
+ } catch {
1581
+ }
1582
+ await input.adapter.add(input.serverName, input.scope, input.previousSpec);
1583
+ const restored = await input.adapter.read(input.serverName, input.scope);
1584
+ if (!restored || restored.fingerprint !== (input.previousFingerprint ?? fingerprintConnection(input.previousSpec))) {
1585
+ throw new Error("Previous Agent MCP entry was not restored");
1586
+ }
1587
+ return;
1588
+ }
1589
+ await input.adapter.remove(input.serverName, input.scope);
1590
+ }
1591
+ async function connectAgent(input) {
1592
+ validateServerName(input.serverName);
1593
+ const resolved = await input.resolveConnection();
1594
+ await input.verifier.verify(resolved.spec, { timeoutMs: input.timeoutMs });
1595
+ await input.adapter.detect();
1596
+ const host = await input.adapter.read(input.serverName, input.scope);
1597
+ const key = connectionKey(input.agent, input.scope, input.serverName);
1598
+ const managed = await input.state.get(key);
1599
+ const desiredFingerprint = fingerprintConnection(resolved.spec);
1600
+ const plan = determinePlan({
1601
+ agent: input.agent,
1602
+ serverName: input.serverName,
1603
+ scope: input.scope,
1604
+ target: resolved.target,
1605
+ ...input.sourceId ? { sourceId: input.sourceId } : {},
1606
+ desiredFingerprint,
1607
+ ...host ? { hostFingerprint: host.fingerprint } : {},
1608
+ ...host ? { hostSpecFingerprint: fingerprintConnection(host.spec) } : {},
1609
+ managed
1610
+ });
1611
+ if (plan.action === "unchanged") {
1612
+ return {
1613
+ agent: input.agent,
1614
+ serverName: input.serverName,
1615
+ scope: input.scope,
1616
+ action: "unchanged",
1617
+ changed: false,
1618
+ verified: true,
1619
+ dryRun: false,
1620
+ plan
1621
+ };
1622
+ }
1623
+ if (input.dryRun) {
1624
+ return {
1625
+ agent: input.agent,
1626
+ serverName: input.serverName,
1627
+ scope: input.scope,
1628
+ action: plan.action,
1629
+ changed: false,
1630
+ verified: true,
1631
+ dryRun: true,
1632
+ plan
1633
+ };
1634
+ }
1635
+ if (!input.yes && !await input.confirm(
1636
+ `${plan.action === "create" ? "Add" : "Update"} ${input.serverName} in ${input.agent}?`
1637
+ )) {
1638
+ return {
1639
+ agent: input.agent,
1640
+ serverName: input.serverName,
1641
+ scope: input.scope,
1642
+ action: "cancelled",
1643
+ changed: false,
1644
+ verified: true,
1645
+ dryRun: false,
1646
+ plan
1647
+ };
1648
+ }
1649
+ try {
1650
+ if (plan.action === "update") {
1651
+ await input.adapter.remove(input.serverName, input.scope);
1652
+ }
1653
+ await input.adapter.add(input.serverName, input.scope, resolved.spec);
1654
+ const readBack = await input.adapter.read(input.serverName, input.scope);
1655
+ if (!readBack || fingerprintConnection(readBack.spec) !== desiredFingerprint) {
1656
+ throw hostFailure3("Agent MCP read-back did not match the requested connection");
1657
+ }
1658
+ await input.verifier.verify(readBack.spec, { timeoutMs: input.timeoutMs });
1659
+ await input.state.set(
1660
+ managedConnection(input, resolved, readBack.fingerprint, managed)
1661
+ );
1662
+ } catch (cause) {
1663
+ try {
1664
+ await rollbackHost({
1665
+ adapter: input.adapter,
1666
+ serverName: input.serverName,
1667
+ scope: input.scope,
1668
+ ...host ? { previousSpec: host.spec } : {},
1669
+ ...host ? { previousFingerprint: host.fingerprint } : {}
1670
+ });
1671
+ } catch {
1672
+ throw hostFailure3(
1673
+ "Agent MCP configuration failed and rollback also failed",
1674
+ cause,
1675
+ `Inspect the ${input.agent} MCP entry \`${input.serverName}\` before retrying.`
1676
+ );
1677
+ }
1678
+ throw hostFailure3(
1679
+ "Agent MCP configuration failed; changes were rolled back",
1680
+ cause
1681
+ );
1682
+ }
1683
+ return {
1684
+ agent: input.agent,
1685
+ serverName: input.serverName,
1686
+ scope: input.scope,
1687
+ action: plan.action,
1688
+ changed: true,
1689
+ verified: true,
1690
+ dryRun: false,
1691
+ plan
1692
+ };
1693
+ }
1694
+ async function disconnectAgent(input) {
1695
+ validateServerName(input.serverName);
1696
+ await input.adapter.detect();
1697
+ const key = connectionKey(input.agent, input.scope, input.serverName);
1698
+ const managed = await input.state.get(key);
1699
+ if (!managed) {
1700
+ throw conflict("The Agent MCP entry is not managed by SAG CLI");
1701
+ }
1702
+ const host = await input.adapter.read(input.serverName, input.scope);
1703
+ if (!host) {
1704
+ throw conflict("Managed state exists but the Agent MCP entry is missing");
1705
+ }
1706
+ if (host.fingerprint !== managed.fingerprint) {
1707
+ throw conflict("The managed Agent MCP entry was changed outside SAG CLI");
1708
+ }
1709
+ const plan = {
1710
+ action: "unchanged",
1711
+ agent: input.agent,
1712
+ serverName: input.serverName,
1713
+ scope: input.scope,
1714
+ provider: "docker-stdio",
1715
+ target: managed.docker.containerName,
1716
+ sourceId: managed.sourceId,
1717
+ currentFingerprint: host.fingerprint,
1718
+ desiredFingerprint: host.fingerprint
1719
+ };
1720
+ if (!input.yes && !await input.confirm(`Remove ${input.serverName} from ${input.agent}?`)) {
1721
+ return {
1722
+ agent: input.agent,
1723
+ serverName: input.serverName,
1724
+ scope: input.scope,
1725
+ action: "cancelled",
1726
+ changed: false,
1727
+ verified: true,
1728
+ dryRun: false,
1729
+ plan
1730
+ };
1731
+ }
1732
+ await input.adapter.remove(input.serverName, input.scope);
1733
+ try {
1734
+ await input.state.delete(key);
1735
+ } catch (cause) {
1736
+ try {
1737
+ await input.adapter.add(input.serverName, input.scope, host.spec);
1738
+ } catch {
1739
+ throw hostFailure3(
1740
+ "Managed state cleanup failed and the Agent entry could not be restored",
1741
+ cause
1742
+ );
1743
+ }
1744
+ throw hostFailure3(
1745
+ "Managed state cleanup failed; the Agent entry was restored",
1746
+ cause
1747
+ );
1748
+ }
1749
+ return {
1750
+ agent: input.agent,
1751
+ serverName: input.serverName,
1752
+ scope: input.scope,
1753
+ action: "disconnect",
1754
+ changed: true,
1755
+ verified: true,
1756
+ dryRun: false,
1757
+ plan
1758
+ };
1759
+ }
1760
+
1761
+ // src/mcp/docker-stdio-provider.ts
1762
+ var SOURCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
1763
+ var DockerStdioProvider = class {
1764
+ id = "docker-stdio";
1765
+ async inspect(input) {
1766
+ return {
1767
+ provider: this.id,
1768
+ ready: true,
1769
+ target: input.target.name
1770
+ };
1771
+ }
1772
+ async createSpec(input) {
1773
+ if (input.sourceId && !SOURCE_ID_PATTERN.test(input.sourceId)) {
1774
+ throw new CliError("INVALID_ARGUMENT", "Invalid SAG source id", {
1775
+ exitCode: exitCodes.invalidArgument,
1776
+ hint: "Use 1-128 letters, numbers, underscores, or dashes."
1777
+ });
1778
+ }
1779
+ const sourceArguments = input.sourceId ? ["-e", `SAG_MCP_SOURCE_ID=${input.sourceId}`] : [];
1780
+ return {
1781
+ transport: "stdio",
1782
+ command: "docker",
1783
+ args: [
1784
+ "exec",
1785
+ "-i",
1786
+ ...sourceArguments,
1787
+ input.target.name,
1788
+ "python",
1789
+ "-m",
1790
+ "sag_api.mcp.server"
1791
+ ],
1792
+ env: {}
1793
+ };
1794
+ }
1795
+ };
1796
+
1797
+ // src/docker/discovery.ts
1798
+ var CONTAINER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u;
1799
+ function validateContainer(value) {
1800
+ if (!CONTAINER_PATTERN.test(value)) {
1801
+ throw new CliError("INVALID_ARGUMENT", "Invalid Docker container name or id", {
1802
+ exitCode: exitCodes.invalidArgument,
1803
+ hint: "Use 1-128 letters, numbers, dots, underscores, or dashes."
1804
+ });
1805
+ }
1806
+ }
1807
+ function toTarget(inspection) {
1808
+ const composeProject = inspection.labels["com.docker.compose.project"] || void 0;
1809
+ const composeService = inspection.labels["com.docker.compose.service"] || void 0;
1810
+ return {
1811
+ id: inspection.id,
1812
+ name: inspection.name,
1813
+ ...composeProject ? { composeProject } : {},
1814
+ ...composeService ? { composeService } : {},
1815
+ health: inspection.health
1816
+ };
1817
+ }
1818
+ async function validateTarget(docker, container) {
1819
+ const inspection = await docker.inspect(container);
1820
+ if (!inspection.running || inspection.health === "unhealthy" || inspection.health === "starting") {
1821
+ throw new CliError("SERVICE_NOT_READY", "SAG API container is not ready", {
1822
+ exitCode: exitCodes.serviceNotReady,
1823
+ hint: "Check the SAG API container health and logs."
1824
+ });
1825
+ }
1826
+ if (!await docker.canImportMcp(inspection.name)) {
1827
+ throw localMcpFailure(
1828
+ "entrypoint",
1829
+ "SAG API container does not provide the stdio MCP entrypoint",
1830
+ {
1831
+ hint: "Use a compatible SAG image that includes sag_api.mcp.server."
1832
+ }
1833
+ );
1834
+ }
1835
+ return toTarget(inspection);
1836
+ }
1837
+ async function discoverSagContainer(input) {
1838
+ await input.docker.version();
1839
+ if (input.container) {
1840
+ validateContainer(input.container);
1841
+ return validateTarget(input.docker, input.container);
1842
+ }
1843
+ const containers = await input.docker.listRunningContainers();
1844
+ const apiCandidates = containers.filter(
1845
+ (container) => container.labels["com.docker.compose.service"] === "api"
1846
+ );
1847
+ if (!apiCandidates.length) {
1848
+ throw new CliError("RESOURCE_NOT_FOUND", "No SAG API container was found", {
1849
+ exitCode: exitCodes.resourceNotFound,
1850
+ hint: "Start SAG Docker or pass `--container <name-or-id>`."
1851
+ });
1852
+ }
1853
+ const targets = [];
1854
+ let firstFailure;
1855
+ let preferredFailure;
1856
+ for (const candidate of apiCandidates) {
1857
+ try {
1858
+ targets.push(await validateTarget(input.docker, candidate.id));
1859
+ } catch (error) {
1860
+ firstFailure ??= error;
1861
+ if (candidate.labels["com.docker.compose.project"] === "sag") {
1862
+ preferredFailure ??= error;
1863
+ }
1864
+ }
1865
+ }
1866
+ if (!targets.length) {
1867
+ throw preferredFailure ?? firstFailure;
1868
+ }
1869
+ const preferredTargets = targets.filter((target) => target.composeProject === "sag");
1870
+ const rankedTargets = preferredTargets.length ? preferredTargets : targets;
1871
+ if (rankedTargets.length === 1) {
1872
+ return rankedTargets[0];
1873
+ }
1874
+ if (!input.interactive || !input.select) {
1875
+ throw new CliError("CONFIG_CONFLICT", "Multiple SAG API containers were found", {
1876
+ exitCode: exitCodes.configConflict,
1877
+ hint: "Pass `--container <name-or-id>` to select one."
1878
+ });
1879
+ }
1880
+ const selectedId = await input.select(rankedTargets);
1881
+ const selected = rankedTargets.find(
1882
+ (target) => target.id === selectedId || target.name === selectedId
1883
+ );
1884
+ if (!selected) {
1885
+ throw new CliError("INVALID_ARGUMENT", "Invalid Docker container selection", {
1886
+ exitCode: exitCodes.invalidArgument
1887
+ });
1888
+ }
1889
+ return selected;
1890
+ }
1891
+
1892
+ // src/commands/agent.ts
1893
+ async function connectLocalAgent(runtime2, input) {
1894
+ return connectAgent({
1895
+ agent: input.agent,
1896
+ serverName: input.serverName,
1897
+ scope: "user",
1898
+ profile: input.profile,
1899
+ ...input.sourceId ? { sourceId: input.sourceId } : {},
1900
+ timeoutMs: input.timeoutMs,
1901
+ dryRun: input.dryRun,
1902
+ yes: input.yes,
1903
+ resolveConnection: async () => {
1904
+ const target = await discoverSagContainer({
1905
+ docker: runtime2.docker,
1906
+ ...input.container ? { container: input.container } : {},
1907
+ interactive: runtime2.interactive,
1908
+ select: runtime2.selectContainer
1909
+ });
1910
+ const spec = await new DockerStdioProvider().createSpec({
1911
+ target,
1912
+ ...input.sourceId ? { sourceId: input.sourceId } : {}
1913
+ });
1914
+ return { target, spec };
1915
+ },
1916
+ verifier: runtime2.verifier,
1917
+ adapter: runtime2.adapters[input.agent],
1918
+ state: runtime2.state,
1919
+ confirm: input.confirm,
1920
+ now: input.now
1921
+ });
1922
+ }
1923
+ async function agentStatus(runtime2, agent, serverName) {
1924
+ const adapter = runtime2.adapters[agent];
1925
+ let version;
1926
+ try {
1927
+ version = (await adapter.detect()).version;
1928
+ } catch {
1929
+ return {
1930
+ agent,
1931
+ installed: false,
1932
+ serverName,
1933
+ status: "unavailable",
1934
+ managed: false,
1935
+ verified: false
1936
+ };
1937
+ }
1938
+ const key = connectionKey(agent, "user", serverName);
1939
+ const [managed, host] = await Promise.all([
1940
+ runtime2.state.get(key),
1941
+ adapter.read(serverName, "user")
1942
+ ]);
1943
+ if (!host) {
1944
+ return {
1945
+ agent,
1946
+ installed: true,
1947
+ version,
1948
+ serverName,
1949
+ status: managed ? "drifted" : "disconnected",
1950
+ managed: Boolean(managed),
1951
+ verified: false
1952
+ };
1953
+ }
1954
+ if (!managed) {
1955
+ return {
1956
+ agent,
1957
+ installed: true,
1958
+ version,
1959
+ serverName,
1960
+ status: "unmanaged",
1961
+ managed: false,
1962
+ verified: false
1963
+ };
1964
+ }
1965
+ if (host.fingerprint !== managed.fingerprint) {
1966
+ return {
1967
+ agent,
1968
+ installed: true,
1969
+ version,
1970
+ serverName,
1971
+ status: "drifted",
1972
+ managed: true,
1973
+ verified: false
1974
+ };
1975
+ }
1976
+ try {
1977
+ await runtime2.verifier.verify(host.spec, { timeoutMs: 15e3 });
1978
+ return {
1979
+ agent,
1980
+ installed: true,
1981
+ version,
1982
+ serverName,
1983
+ status: "connected",
1984
+ managed: true,
1985
+ verified: true
1986
+ };
1987
+ } catch {
1988
+ return {
1989
+ agent,
1990
+ installed: true,
1991
+ version,
1992
+ serverName,
1993
+ status: "unavailable",
1994
+ managed: true,
1995
+ verified: false
1996
+ };
1997
+ }
1998
+ }
1999
+ async function statusLocalAgents(runtime2, input) {
2000
+ const agents = input.agent ? [input.agent] : ["codex", "claude-code"];
2001
+ return Promise.all(
2002
+ agents.map((agent) => agentStatus(runtime2, agent, input.serverName))
2003
+ );
2004
+ }
2005
+ async function listAgentHosts(runtime2) {
2006
+ return Promise.all(
2007
+ ["codex", "claude-code"].map(async (agent) => {
2008
+ const managedConnections = (await runtime2.state.list({ agent })).length;
2009
+ try {
2010
+ const installation = await runtime2.adapters[agent].detect();
2011
+ return {
2012
+ agent,
2013
+ installed: true,
2014
+ version: installation.version,
2015
+ managedConnections
2016
+ };
2017
+ } catch {
2018
+ return { agent, installed: false, managedConnections };
2019
+ }
2020
+ })
2021
+ );
2022
+ }
2023
+ async function disconnectLocalAgent(runtime2, input) {
2024
+ return disconnectAgent({
2025
+ agent: input.agent,
2026
+ serverName: input.serverName,
2027
+ scope: "user",
2028
+ adapter: runtime2.adapters[input.agent],
2029
+ state: runtime2.state,
2030
+ yes: input.yes,
2031
+ confirm: input.confirm
2032
+ });
2033
+ }
2034
+
2035
+ // src/commands/auth.ts
2036
+ async function login(input) {
2037
+ const token = input.environmentToken?.trim() || await input.promptToken();
2038
+ if (!token.trim()) {
2039
+ throw new CliError("AUTH_REQUIRED", "A SAG token is required", {
2040
+ exitCode: exitCodes.authRequired,
2041
+ hint: "Paste a token when prompted or set SAG_TOKEN."
2042
+ });
2043
+ }
2044
+ const user = await input.validate(token);
2045
+ await input.store.set(input.credentialRef, token);
2046
+ return {
2047
+ authenticated: true,
2048
+ credentialRef: input.credentialRef,
2049
+ credentialStore: input.store.kind,
2050
+ user
2051
+ };
2052
+ }
2053
+ async function status(input) {
2054
+ const token = input.environmentToken?.trim() || await input.store.get(input.credentialRef);
2055
+ if (!token) {
2056
+ return {
2057
+ authenticated: false,
2058
+ credentialRef: input.credentialRef,
2059
+ credentialStore: input.store.kind
2060
+ };
2061
+ }
2062
+ const user = await input.validate(token);
2063
+ return {
2064
+ authenticated: true,
2065
+ credentialRef: input.credentialRef,
2066
+ credentialStore: input.store.kind,
2067
+ user
2068
+ };
2069
+ }
2070
+ async function logout(input) {
2071
+ await input.store.delete(input.credentialRef);
2072
+ return {
2073
+ authenticated: false,
2074
+ credentialRef: input.credentialRef
2075
+ };
2076
+ }
2077
+
2078
+ // src/commands/document.ts
2079
+ function requiredIdentifier(value, label) {
2080
+ const normalized = value.trim();
2081
+ if (!normalized) {
2082
+ throw new CliError("INVALID_ARGUMENT", `${label} is required`, {
2083
+ exitCode: exitCodes.invalidArgument
2084
+ });
2085
+ }
2086
+ return normalized;
2087
+ }
2088
+ function summarizeDocuments(documents) {
2089
+ const statuses = {
2090
+ pending: 0,
2091
+ loading: 0,
2092
+ extracting: 0,
2093
+ paused: 0,
2094
+ ready: 0,
2095
+ failed: 0
2096
+ };
2097
+ for (const document of documents) {
2098
+ statuses[document.status] += 1;
2099
+ }
2100
+ return {
2101
+ total: documents.length,
2102
+ statuses,
2103
+ searchable: documents.length > 0 && statuses.ready === documents.length
2104
+ };
2105
+ }
2106
+ async function listDocuments(client, sourceId) {
2107
+ return client.listDocuments(requiredIdentifier(sourceId, "Source ID"));
2108
+ }
2109
+ async function getDocument(client, sourceId, documentId) {
2110
+ return client.getDocument(
2111
+ requiredIdentifier(sourceId, "Source ID"),
2112
+ requiredIdentifier(documentId, "Document ID")
2113
+ );
2114
+ }
2115
+ async function documentStatus(client, sourceId, documentId) {
2116
+ const normalizedSourceId = requiredIdentifier(sourceId, "Source ID");
2117
+ if (documentId !== void 0) {
2118
+ const document = await getDocument(client, normalizedSourceId, documentId);
2119
+ return {
2120
+ mode: "document",
2121
+ document,
2122
+ searchable: document.status === "ready"
2123
+ };
2124
+ }
2125
+ const summary = summarizeDocuments(await client.listDocuments(normalizedSourceId));
2126
+ return {
2127
+ mode: "source",
2128
+ sourceId: normalizedSourceId,
2129
+ total: summary.total,
2130
+ statuses: summary.statuses,
2131
+ searchable: summary.searchable
2132
+ };
2133
+ }
2134
+
2135
+ // src/commands/mcp.ts
2136
+ async function testLocalMcp(runtime2, input) {
2137
+ const container = await discoverSagContainer({
2138
+ docker: runtime2.docker,
2139
+ ...input.container ? { container: input.container } : {},
2140
+ interactive: runtime2.interactive,
2141
+ select: runtime2.selectContainer
2142
+ });
2143
+ const spec = await new DockerStdioProvider().createSpec({
2144
+ target: container,
2145
+ ...input.sourceId ? { sourceId: input.sourceId } : {}
2146
+ });
2147
+ const verification = await runtime2.verifier.verify(spec, {
2148
+ timeoutMs: input.timeoutMs
2149
+ });
2150
+ return {
2151
+ provider: "docker-stdio",
2152
+ container,
2153
+ sourceId: input.sourceId ?? null,
2154
+ ...verification
2155
+ };
2156
+ }
2157
+ function skipped(name, detail) {
2158
+ return {
2159
+ name,
2160
+ status: "warn",
2161
+ code: "CHECK_SKIPPED",
2162
+ detail
2163
+ };
2164
+ }
2165
+ async function localMcpDoctorChecks(runtime2, input) {
2166
+ try {
2167
+ const report = await testLocalMcp(runtime2, input);
2168
+ return [
2169
+ {
2170
+ name: "docker",
2171
+ status: "pass",
2172
+ code: "DOCKER_READY",
2173
+ detail: "Docker daemon is available"
2174
+ },
2175
+ {
2176
+ name: "sagContainer",
2177
+ status: "pass",
2178
+ code: "SAG_CONTAINER_READY",
2179
+ detail: `${report.container.name} is ${report.container.health}`
2180
+ },
2181
+ {
2182
+ name: "mcpEntrypoint",
2183
+ status: "pass",
2184
+ code: "MCP_ENTRYPOINT_READY",
2185
+ detail: "sag_api.mcp.server is importable"
2186
+ },
2187
+ {
2188
+ name: "mcpProtocol",
2189
+ status: "pass",
2190
+ code: "MCP_PROTOCOL_READY",
2191
+ detail: `${report.protocolVersion} ${report.era}`
2192
+ },
2193
+ {
2194
+ name: "mcpTools",
2195
+ status: "pass",
2196
+ code: "MCP_TOOLS_READY",
2197
+ detail: `${report.tools.length} tools`
2198
+ },
2199
+ {
2200
+ name: "mcpKnowledge",
2201
+ status: "pass",
2202
+ code: "MCP_KNOWLEDGE_READY",
2203
+ detail: "list_sources callable"
2204
+ }
2205
+ ];
2206
+ } catch (error) {
2207
+ const cliError = toCliError(error);
2208
+ const mcpStage = localMcpFailureStage(cliError);
2209
+ const failedName = cliError.code === "DEPENDENCY_MISSING" ? "docker" : cliError.code === "RESOURCE_NOT_FOUND" || cliError.code === "SERVICE_NOT_READY" ? "sagContainer" : mcpStage === "entrypoint" ? "mcpEntrypoint" : mcpStage === "tools" ? "mcpTools" : mcpStage === "knowledge" ? "mcpKnowledge" : "mcpProtocol";
2210
+ const names = [
2211
+ "docker",
2212
+ "sagContainer",
2213
+ "mcpEntrypoint",
2214
+ "mcpProtocol",
2215
+ "mcpTools",
2216
+ "mcpKnowledge"
2217
+ ];
2218
+ const failedIndex = names.indexOf(failedName);
2219
+ return names.map((name, index) => {
2220
+ if (index < failedIndex) {
2221
+ return {
2222
+ name,
2223
+ status: "pass",
2224
+ code: "CHECK_COMPLETED",
2225
+ detail: "Prerequisite check passed"
2226
+ };
2227
+ }
2228
+ if (index === failedIndex) {
2229
+ return {
2230
+ name,
2231
+ status: "fail",
2232
+ code: cliError.code,
2233
+ detail: cliError.message,
2234
+ ...cliError.hint ? { hint: cliError.hint } : {}
2235
+ };
2236
+ }
2237
+ return skipped(name, `Skipped because ${failedName} failed`);
2238
+ });
2239
+ }
2240
+ }
2241
+
2242
+ // src/commands/search.ts
2243
+ function invalid(message) {
2244
+ throw new CliError("INVALID_ARGUMENT", message, {
2245
+ exitCode: exitCodes.invalidArgument
2246
+ });
2247
+ }
2248
+ async function searchKnowledge(client, query, options = {}) {
2249
+ const normalizedQuery = query.trim();
2250
+ if (!normalizedQuery) {
2251
+ invalid("Query is required");
2252
+ }
2253
+ if (normalizedQuery.length > 4e3) {
2254
+ invalid("Query must not exceed 4000 characters");
2255
+ }
2256
+ if (options.topK !== void 0 && (!Number.isInteger(options.topK) || options.topK < 1 || options.topK > 50)) {
2257
+ invalid("top-k must be between 1 and 50");
2258
+ }
2259
+ if (options.strategy !== void 0 && !["vector", "multi"].includes(options.strategy)) {
2260
+ invalid("strategy must be vector or multi");
2261
+ }
2262
+ const sourceIds = [
2263
+ ...new Set(
2264
+ (options.sourceIds ?? []).map((sourceId) => sourceId.trim()).filter(Boolean)
2265
+ )
2266
+ ];
2267
+ return client.search({
2268
+ query: normalizedQuery,
2269
+ ...sourceIds.length ? { source_ids: sourceIds } : {},
2270
+ ...options.topK !== void 0 ? { top_k: options.topK } : {},
2271
+ ...options.strategy !== void 0 ? { strategy: options.strategy } : {},
2272
+ save_exploration: false
2273
+ });
2274
+ }
2275
+
2276
+ // src/commands/source.ts
2277
+ function requiredSourceId(sourceId) {
2278
+ const normalized = sourceId.trim();
2279
+ if (!normalized) {
2280
+ throw new CliError("INVALID_ARGUMENT", "Source ID is required", {
2281
+ exitCode: exitCodes.invalidArgument
2282
+ });
2283
+ }
2284
+ return normalized;
2285
+ }
2286
+ function listSources(client) {
2287
+ return client.listSources();
2288
+ }
2289
+ async function getSource(client, sourceId) {
2290
+ return client.getSource(requiredSourceId(sourceId));
2291
+ }
2292
+ async function sourceStatus(client, sourceId) {
2293
+ if (sourceId === void 0) {
2294
+ const sources = await client.listSources();
2295
+ const documentSummaries = await Promise.all(
2296
+ sources.map(
2297
+ async (source2) => summarizeDocuments(await client.listDocuments(source2.id))
2298
+ )
2299
+ );
2300
+ return {
2301
+ total: sources.length,
2302
+ statuses: {
2303
+ active: sources.filter(({ status: status2 }) => status2 === "active").length,
2304
+ paused: sources.filter(({ status: status2 }) => status2 === "paused").length,
2305
+ error: sources.filter(({ status: status2 }) => status2 === "error").length
2306
+ },
2307
+ documents: {
2308
+ total: documentSummaries.reduce((total, summary2) => total + summary2.total, 0),
2309
+ ready: documentSummaries.reduce(
2310
+ (total, summary2) => total + summary2.statuses.ready,
2311
+ 0
2312
+ ),
2313
+ pending: documentSummaries.reduce(
2314
+ (total, summary2) => total + summary2.statuses.pending,
2315
+ 0
2316
+ ),
2317
+ loading: documentSummaries.reduce(
2318
+ (total, summary2) => total + summary2.statuses.loading,
2319
+ 0
2320
+ ),
2321
+ extracting: documentSummaries.reduce(
2322
+ (total, summary2) => total + summary2.statuses.extracting,
2323
+ 0
2324
+ ),
2325
+ paused: documentSummaries.reduce(
2326
+ (total, summary2) => total + summary2.statuses.paused,
2327
+ 0
2328
+ ),
2329
+ failed: documentSummaries.reduce(
2330
+ (total, summary2) => total + summary2.statuses.failed,
2331
+ 0
2332
+ )
2333
+ },
2334
+ searchableSources: documentSummaries.filter(({ searchable }) => searchable).length
2335
+ };
2336
+ }
2337
+ const normalizedSourceId = requiredSourceId(sourceId);
2338
+ const [source, documents] = await Promise.all([
2339
+ client.getSource(normalizedSourceId),
2340
+ client.listDocuments(normalizedSourceId)
2341
+ ]);
2342
+ const summary = summarizeDocuments(documents);
2343
+ return {
2344
+ source,
2345
+ documents: {
2346
+ total: summary.total,
2347
+ ready: summary.statuses.ready,
2348
+ pending: summary.statuses.pending,
2349
+ loading: summary.statuses.loading,
2350
+ extracting: summary.statuses.extracting,
2351
+ paused: summary.statuses.paused,
2352
+ failed: summary.statuses.failed,
2353
+ searchable: summary.searchable
2354
+ }
2355
+ };
2356
+ }
2357
+
2358
+ // src/core/context.ts
2359
+ import { createHash as createHash2 } from "crypto";
2360
+ function directCredentialRef(url) {
2361
+ const fingerprint = createHash2("sha256").update(url).digest("hex").slice(0, 16);
2362
+ return `sag-cli/direct-${fingerprint}`;
2363
+ }
2364
+ async function createRuntimeContext(input) {
2365
+ const config = await input.configStore.load();
2366
+ const connection = resolveConnection(input.options, input.environment, config);
2367
+ const credentialRef = connection.credentialRef ?? directCredentialRef(connection.url);
2368
+ const token = connection.environmentToken ?? await input.credentialStore.get(credentialRef) ?? void 0;
2369
+ return {
2370
+ connection,
2371
+ credentialRef,
2372
+ ...token ? { token } : {},
2373
+ client: input.createClient({
2374
+ origin: connection.url,
2375
+ ...token ? { token } : {}
2376
+ })
2377
+ };
2378
+ }
2379
+
2380
+ // src/core/result.ts
2381
+ var OUTPUT_SCHEMA = "sag.cli.v1";
2382
+ function success(data) {
2383
+ return {
2384
+ schema: OUTPUT_SCHEMA,
2385
+ ok: true,
2386
+ data
2387
+ };
2388
+ }
2389
+ function failure(error) {
2390
+ return {
2391
+ schema: OUTPUT_SCHEMA,
2392
+ ok: false,
2393
+ error: error.toShape()
2394
+ };
2395
+ }
2396
+
2397
+ // src/diagnostics/doctor.ts
2398
+ function emptyDocumentStatuses() {
2399
+ return {
2400
+ pending: 0,
2401
+ loading: 0,
2402
+ extracting: 0,
2403
+ paused: 0,
2404
+ ready: 0,
2405
+ failed: 0
2406
+ };
2407
+ }
2408
+ function errorCheck(name, error, fallbackHint) {
2409
+ const cliError = toCliError(error);
2410
+ const hint = cliError.hint ?? fallbackHint;
2411
+ return {
2412
+ name,
2413
+ status: "fail",
2414
+ code: cliError.code,
2415
+ detail: cliError.message,
2416
+ ...hint ? { hint } : {}
2417
+ };
2418
+ }
2419
+ function skippedCheck(name, reason) {
2420
+ return {
2421
+ name,
2422
+ status: "warn",
2423
+ code: "CHECK_SKIPPED",
2424
+ detail: reason
2425
+ };
2426
+ }
2427
+ function overallStatus(checks) {
2428
+ if (checks.some((check) => check.status === "fail")) {
2429
+ return "fail";
2430
+ }
2431
+ if (checks.some((check) => check.status === "warn")) {
2432
+ return "warn";
2433
+ }
2434
+ return "pass";
2435
+ }
2436
+ function sagVersionCheck(name, version) {
2437
+ const match = /^(\d+)\.(\d+)(?:\.|$)/u.exec(version);
2438
+ if (!match) {
2439
+ return {
2440
+ name: "service",
2441
+ status: "warn",
2442
+ code: "SAG_VERSION_UNVERIFIED",
2443
+ detail: `${name} ${version}`,
2444
+ hint: "This SAG version could not be checked against >=1.4.0 <2.0.0."
2445
+ };
2446
+ }
2447
+ const major = Number(match[1]);
2448
+ const minor = Number(match[2]);
2449
+ if (major !== 1 || minor < 4) {
2450
+ return {
2451
+ name: "service",
2452
+ status: "fail",
2453
+ code: "SAG_VERSION_INCOMPATIBLE",
2454
+ detail: `${name} ${version} is outside the supported range >=1.4.0 <2.0.0`,
2455
+ hint: "Use a compatible SAG version or upgrade SAG CLI."
2456
+ };
2457
+ }
2458
+ return {
2459
+ name: "service",
2460
+ status: "pass",
2461
+ code: "SAG_REACHABLE",
2462
+ detail: `${name} ${version}`
2463
+ };
2464
+ }
2465
+ function documentDetail(total, statuses) {
2466
+ const displayOrder = [
2467
+ "ready",
2468
+ "pending",
2469
+ "loading",
2470
+ "extracting",
2471
+ "paused",
2472
+ "failed"
2473
+ ];
2474
+ const parts = displayOrder.filter((status2) => statuses[status2] > 0).map((status2) => `${statuses[status2]} ${status2}`);
2475
+ return `${total} ${total === 1 ? "document" : "documents"}${parts.length ? `: ${parts.join(", ")}` : ""}`;
2476
+ }
2477
+ async function runDoctor(client, input, localMcpChecks) {
2478
+ const checks = [];
2479
+ let sagVersion;
2480
+ let authenticated = false;
2481
+ let sources;
2482
+ const documentStatuses = emptyDocumentStatuses();
2483
+ let documentCount = 0;
2484
+ let mcpTools = 0;
2485
+ try {
2486
+ const root = await client.root();
2487
+ sagVersion = root.version;
2488
+ checks.push(sagVersionCheck(root.name, root.version));
2489
+ } catch (error) {
2490
+ checks.push(errorCheck("service", error));
2491
+ }
2492
+ try {
2493
+ const ready = await client.ready();
2494
+ if (!ready.db || ready.status !== "ready") {
2495
+ checks.push({
2496
+ name: "readiness",
2497
+ status: "fail",
2498
+ code: "SERVICE_NOT_READY",
2499
+ detail: "SAG database is not ready",
2500
+ hint: "Check SAG API and database logs."
2501
+ });
2502
+ } else {
2503
+ checks.push({
2504
+ name: "readiness",
2505
+ status: "pass",
2506
+ code: "SAG_READY",
2507
+ detail: "Database is ready"
2508
+ });
2509
+ }
2510
+ } catch (error) {
2511
+ checks.push(errorCheck("readiness", error));
2512
+ }
2513
+ try {
2514
+ const user = await client.me();
2515
+ authenticated = true;
2516
+ checks.push({
2517
+ name: "authentication",
2518
+ status: "pass",
2519
+ code: "AUTH_VALID",
2520
+ detail: `${user.name} <${user.email}>`
2521
+ });
2522
+ } catch (error) {
2523
+ checks.push(
2524
+ errorCheck("authentication", error, "Run `sag auth login` or set SAG_TOKEN.")
2525
+ );
2526
+ }
2527
+ if (!authenticated) {
2528
+ const reason = "Skipped because authentication failed";
2529
+ checks.push(skippedCheck("sources", reason));
2530
+ checks.push(skippedCheck("documents", reason));
2531
+ checks.push(skippedCheck("mcp", reason));
2532
+ } else {
2533
+ try {
2534
+ sources = await client.listSources();
2535
+ checks.push({
2536
+ name: "sources",
2537
+ status: sources.length ? "pass" : "warn",
2538
+ code: sources.length ? "SOURCES_AVAILABLE" : "NO_SOURCES",
2539
+ detail: `${sources.length} ${sources.length === 1 ? "source" : "sources"}`,
2540
+ ...!sources.length ? { hint: "Create and populate a knowledge source in SAG." } : {}
2541
+ });
2542
+ } catch (error) {
2543
+ checks.push(errorCheck("sources", error));
2544
+ }
2545
+ if (!sources) {
2546
+ checks.push(
2547
+ skippedCheck("documents", "Skipped because sources could not be listed")
2548
+ );
2549
+ } else {
2550
+ try {
2551
+ const documents = (await Promise.all(sources.map((source) => client.listDocuments(source.id)))).flat();
2552
+ documentCount = documents.length;
2553
+ for (const document of documents) {
2554
+ documentStatuses[document.status] += 1;
2555
+ }
2556
+ const hasUnready = documentStatuses.pending + documentStatuses.loading + documentStatuses.extracting + documentStatuses.paused + documentStatuses.failed > 0;
2557
+ checks.push({
2558
+ name: "documents",
2559
+ status: hasUnready || !documents.length ? "warn" : "pass",
2560
+ code: !documents.length ? "NO_DOCUMENTS" : hasUnready ? "DOCUMENTS_PROCESSING" : "DOCUMENTS_READY",
2561
+ detail: documentDetail(documentCount, documentStatuses),
2562
+ ...hasUnready ? {
2563
+ hint: `Run \`sag document status --source ${sources[0]?.id ?? "<source-id>"}\` for details.`
2564
+ } : {}
2565
+ });
2566
+ } catch (error) {
2567
+ checks.push(errorCheck("documents", error));
2568
+ }
2569
+ }
2570
+ try {
2571
+ const descriptor = await client.mcpDescriptor();
2572
+ mcpTools = descriptor.tools.length;
2573
+ checks.push({
2574
+ name: "mcp",
2575
+ status: "pass",
2576
+ code: "MCP_DESCRIPTOR_READY",
2577
+ detail: `${mcpTools} tools via ${descriptor.http.transport}`
2578
+ });
2579
+ } catch (error) {
2580
+ checks.push(errorCheck("mcp", error));
2581
+ }
2582
+ }
2583
+ if (localMcpChecks) {
2584
+ try {
2585
+ checks.push(...await localMcpChecks());
2586
+ } catch (error) {
2587
+ checks.push(errorCheck("docker", error));
2588
+ }
2589
+ }
2590
+ return {
2591
+ status: overallStatus(checks),
2592
+ url: input.url,
2593
+ ...input.profileName ? { profileName: input.profileName } : {},
2594
+ ...sagVersion ? { sagVersion } : {},
2595
+ checks,
2596
+ summary: {
2597
+ sources: sources?.length ?? 0,
2598
+ documents: documentCount,
2599
+ documentStatuses,
2600
+ mcpTools
2601
+ }
2602
+ };
2603
+ }
2604
+
2605
+ // src/output/human.ts
2606
+ function isRecord(value) {
2607
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2608
+ }
2609
+ function renderRows(rows) {
2610
+ if (!rows.length) return "No results.\n";
2611
+ const preferredColumns = [
2612
+ "current",
2613
+ "agent",
2614
+ "installed",
2615
+ "version",
2616
+ "serverName",
2617
+ "managed",
2618
+ "verified",
2619
+ "managedConnections",
2620
+ "id",
2621
+ "name",
2622
+ "filename",
2623
+ "status",
2624
+ "url",
2625
+ "document_count",
2626
+ "chunk_count",
2627
+ "progress"
2628
+ ];
2629
+ const available = new Set(rows.flatMap((row) => Object.keys(row)));
2630
+ const columns = preferredColumns.filter((column) => available.has(column));
2631
+ const selected = columns.length ? columns : Object.keys(rows[0] ?? {});
2632
+ const widths = selected.map(
2633
+ (column) => Math.max(column.length, ...rows.map((row) => String(row[column] ?? "").length))
2634
+ );
2635
+ const line = (values) => values.map((value, index) => String(value ?? "").padEnd(widths[index] ?? 0)).join(" ").trimEnd();
2636
+ return `${[
2637
+ line(selected),
2638
+ line(widths.map((width) => "-".repeat(width))),
2639
+ ...rows.map((row) => line(selected.map((column) => row[column])))
2640
+ ].join("\n")}
2641
+ `;
2642
+ }
2643
+ function renderDoctor(report) {
2644
+ const lines = [
2645
+ `SAG doctor: ${report.status.toUpperCase()}`,
2646
+ `URL: ${report.url}`,
2647
+ ...report.checks.map(
2648
+ (check) => `${check.status === "pass" ? "\u2713" : check.status === "warn" ? "!" : "\u2717"} ${check.name}: ${check.detail}${check.hint ? ` \u2014 ${check.hint}` : ""}`
2649
+ )
2650
+ ];
2651
+ return `${lines.join("\n")}
2652
+ `;
2653
+ }
2654
+ function renderSearch(value) {
2655
+ const summary = typeof value.summary === "string" ? value.summary : "";
2656
+ const sections = Array.isArray(value.sections) ? value.sections.filter(isRecord) : [];
2657
+ const lines = [
2658
+ summary,
2659
+ ...sections.map((section) => {
2660
+ const source = section.source_name || section.source_id || "Unknown source";
2661
+ const heading = section.heading || "Result";
2662
+ const content = section.content || "";
2663
+ return `[${source}] ${heading}
2664
+ ${content}`;
2665
+ })
2666
+ ].filter(Boolean);
2667
+ return `${lines.join("\n\n")}
2668
+ `;
2669
+ }
2670
+ function renderLocalMcp(value) {
2671
+ const container = isRecord(value.container) ? value.container : {};
2672
+ const tools = Array.isArray(value.tools) ? value.tools : [];
2673
+ return `${[
2674
+ "SAG MCP: PASS",
2675
+ `Container: ${String(container.name ?? "unknown")}`,
2676
+ `Health: ${String(container.health ?? "unknown")}`,
2677
+ `Protocol: ${String(value.protocolVersion ?? "unknown")} (${String(value.era ?? "unknown")})`,
2678
+ `Tools: ${tools.length}`,
2679
+ `Knowledge: ${value.listSourcesCallable ? "list_sources callable" : "unavailable"}`
2680
+ ].join("\n")}
2681
+ `;
2682
+ }
2683
+ function renderHuman(value, quiet = false) {
2684
+ if (quiet) {
2685
+ if (Array.isArray(value)) {
2686
+ return `${value.map(
2687
+ (item) => isRecord(item) ? item.id ?? item.name ?? JSON.stringify(item) : item
2688
+ ).join("\n")}
2689
+ `;
2690
+ }
2691
+ if (isRecord(value) && typeof value.summary === "string") {
2692
+ return `${value.summary}
2693
+ `;
2694
+ }
2695
+ }
2696
+ if (Array.isArray(value) && value.every(isRecord)) {
2697
+ return renderRows(value);
2698
+ }
2699
+ if (isRecord(value) && Array.isArray(value.checks) && "summary" in value) {
2700
+ return renderDoctor(value);
2701
+ }
2702
+ if (isRecord(value) && Array.isArray(value.sections) && "query" in value) {
2703
+ return renderSearch(value);
2704
+ }
2705
+ if (isRecord(value) && value.provider === "docker-stdio" && "protocolVersion" in value && "container" in value) {
2706
+ return renderLocalMcp(value);
2707
+ }
2708
+ if (typeof value === "string") {
2709
+ return `${value}
2710
+ `;
2711
+ }
2712
+ return `${JSON.stringify(value, null, 2)}
2713
+ `;
2714
+ }
2715
+
2716
+ // src/output/redaction.ts
2717
+ var SENSITIVE_KEYS = /* @__PURE__ */ new Set([
2718
+ "authorization",
2719
+ "token",
2720
+ "access_token",
2721
+ "password",
2722
+ "secret",
2723
+ "secretkey",
2724
+ "secret_key"
2725
+ ]);
2726
+ var BEARER_TOKEN = /\bBearer\s+[^\s"',}\]]+/giu;
2727
+ function redactString(value) {
2728
+ return value.replace(BEARER_TOKEN, "Bearer [REDACTED]");
2729
+ }
2730
+ function redact(value) {
2731
+ if (typeof value === "string") {
2732
+ return redactString(value);
2733
+ }
2734
+ if (Array.isArray(value)) {
2735
+ return value.map((item) => redact(item));
2736
+ }
2737
+ if (value && typeof value === "object") {
2738
+ return Object.fromEntries(
2739
+ Object.entries(value).map(([key, item]) => {
2740
+ const normalizedKey = key.toLowerCase();
2741
+ if (normalizedKey === "authorization" && typeof item === "string") {
2742
+ return [key, redactString(item)];
2743
+ }
2744
+ return [key, SENSITIVE_KEYS.has(normalizedKey) ? "[REDACTED]" : redact(item)];
2745
+ })
2746
+ );
2747
+ }
2748
+ return value;
2749
+ }
2750
+
2751
+ // src/output/json.ts
2752
+ function renderJson(result) {
2753
+ return `${JSON.stringify(redact(result))}
2754
+ `;
2755
+ }
2756
+
2757
+ // src/program.ts
2758
+ function parseInteger(value) {
2759
+ return Number.parseInt(value, 10);
2760
+ }
2761
+ function parsePositiveInteger(value) {
2762
+ const parsed = Number.parseInt(value, 10);
2763
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
2764
+ throw new CliError("INVALID_ARGUMENT", "Expected a positive integer", {
2765
+ exitCode: exitCodes.invalidArgument
2766
+ });
2767
+ }
2768
+ return parsed;
2769
+ }
2770
+ function parseAgent(value) {
2771
+ if (value === "codex" || value === "claude-code") {
2772
+ return value;
2773
+ }
2774
+ throw new CliError("INVALID_ARGUMENT", `Unsupported Agent: ${value}`, {
2775
+ exitCode: exitCodes.invalidArgument,
2776
+ hint: "Choose codex or claude-code."
2777
+ });
2778
+ }
2779
+ function commandOptions(program) {
2780
+ return program.opts();
2781
+ }
2782
+ function emit(program, dependencies2, data) {
2783
+ const options = commandOptions(program);
2784
+ dependencies2.writeStdout(
2785
+ options.json ? renderJson(success(data)) : renderHuman(data, options.quiet ?? false)
2786
+ );
2787
+ }
2788
+ async function runtime(program, dependencies2) {
2789
+ const options = commandOptions(program);
2790
+ return createRuntimeContext({
2791
+ options: {
2792
+ ...options.url ? { url: options.url } : {},
2793
+ ...options.profile ? { profile: options.profile } : {}
2794
+ },
2795
+ environment: dependencies2.environment,
2796
+ configStore: dependencies2.configStore,
2797
+ credentialStore: dependencies2.credentialStore,
2798
+ createClient: dependencies2.createClient
2799
+ });
2800
+ }
2801
+ function localMcpRuntime(dependencies2) {
2802
+ if (!dependencies2.localMcp) {
2803
+ throw new CliError("DEPENDENCY_MISSING", "Local MCP runtime is unavailable", {
2804
+ exitCode: exitCodes.dependencyMissing
2805
+ });
2806
+ }
2807
+ return dependencies2.localMcp;
2808
+ }
2809
+ async function selectedProfileName(program, dependencies2) {
2810
+ const options = commandOptions(program);
2811
+ if (options.profile) {
2812
+ await dependencies2.configStore.getProfile(options.profile);
2813
+ return options.profile;
2814
+ }
2815
+ return (await dependencies2.configStore.load()).currentProfile ?? "local";
2816
+ }
2817
+ async function selectedServerName(program, dependencies2, explicitName) {
2818
+ const profile = await selectedProfileName(program, dependencies2);
2819
+ return {
2820
+ profile,
2821
+ serverName: explicitName ?? `sag-knowledge-${profile}`
2822
+ };
2823
+ }
2824
+ function addProfileCommands(program, dependencies2) {
2825
+ const profile = program.command("profile").description("Manage SAG profiles");
2826
+ profile.command("add").argument("<name>").argument("<url>").action(async (name, url) => {
2827
+ emit(program, dependencies2, await dependencies2.configStore.addProfile(name, url));
2828
+ });
2829
+ profile.command("list").action(async () => {
2830
+ emit(program, dependencies2, await dependencies2.configStore.listProfiles());
2831
+ });
2832
+ profile.command("use").argument("<name>").action(async (name) => {
2833
+ emit(program, dependencies2, await dependencies2.configStore.useProfile(name));
2834
+ });
2835
+ profile.command("show").argument("[name]").action(async (name) => {
2836
+ emit(program, dependencies2, await dependencies2.configStore.getProfile(name));
2837
+ });
2838
+ profile.command("remove").argument("<name>").action(async (name) => {
2839
+ if (!commandOptions(program).yes && !await dependencies2.confirm(`Remove profile ${name}?`)) {
2840
+ emit(program, dependencies2, { removed: null, cancelled: true });
2841
+ return;
2842
+ }
2843
+ await dependencies2.configStore.removeProfile(name);
2844
+ emit(program, dependencies2, { removed: name });
2845
+ });
2846
+ }
2847
+ function addAuthCommands(program, dependencies2) {
2848
+ const auth = program.command("auth").description("Manage SAG credentials");
2849
+ auth.command("login").action(async () => {
2850
+ const context = await runtime(program, dependencies2);
2851
+ const result = await login({
2852
+ credentialRef: context.credentialRef,
2853
+ ...context.connection.environmentToken ? { environmentToken: context.connection.environmentToken } : {},
2854
+ promptToken: dependencies2.promptToken,
2855
+ validate: async (token) => dependencies2.createClient({ origin: context.connection.url, token }).me(),
2856
+ store: dependencies2.credentialStore
2857
+ });
2858
+ emit(program, dependencies2, result);
2859
+ });
2860
+ auth.command("status").action(async () => {
2861
+ const context = await runtime(program, dependencies2);
2862
+ const result = await status({
2863
+ credentialRef: context.credentialRef,
2864
+ ...context.connection.environmentToken ? { environmentToken: context.connection.environmentToken } : {},
2865
+ validate: async (token) => dependencies2.createClient({ origin: context.connection.url, token }).me(),
2866
+ store: dependencies2.credentialStore
2867
+ });
2868
+ emit(program, dependencies2, result);
2869
+ });
2870
+ auth.command("logout").action(async () => {
2871
+ const context = await runtime(program, dependencies2);
2872
+ emit(
2873
+ program,
2874
+ dependencies2,
2875
+ await logout({
2876
+ credentialRef: context.credentialRef,
2877
+ store: dependencies2.credentialStore
2878
+ })
2879
+ );
2880
+ });
2881
+ }
2882
+ function addKnowledgeCommands(program, dependencies2) {
2883
+ program.command("doctor").action(async () => {
2884
+ const context = await runtime(program, dependencies2);
2885
+ emit(
2886
+ program,
2887
+ dependencies2,
2888
+ await runDoctor(
2889
+ context.client,
2890
+ {
2891
+ url: context.connection.url,
2892
+ ...context.connection.profileName ? { profileName: context.connection.profileName } : {}
2893
+ },
2894
+ dependencies2.localMcp ? () => localMcpDoctorChecks(dependencies2.localMcp, {
2895
+ timeoutMs: 15e3
2896
+ }) : void 0
2897
+ )
2898
+ );
2899
+ });
2900
+ const source = program.command("source").description("Inspect SAG sources");
2901
+ source.command("list").action(async () => {
2902
+ const context = await runtime(program, dependencies2);
2903
+ emit(program, dependencies2, await listSources(context.client));
2904
+ });
2905
+ source.command("get").argument("<source-id>").action(async (sourceId) => {
2906
+ const context = await runtime(program, dependencies2);
2907
+ emit(program, dependencies2, await getSource(context.client, sourceId));
2908
+ });
2909
+ source.command("status").argument("[source-id]").action(async (sourceId) => {
2910
+ const context = await runtime(program, dependencies2);
2911
+ emit(program, dependencies2, await sourceStatus(context.client, sourceId));
2912
+ });
2913
+ const document = program.command("document").description("Inspect SAG documents");
2914
+ document.command("list").requiredOption("--source <source-id>").action(async (options) => {
2915
+ const context = await runtime(program, dependencies2);
2916
+ emit(program, dependencies2, await listDocuments(context.client, options.source));
2917
+ });
2918
+ document.command("get").argument("<document-id>").requiredOption("--source <source-id>").action(async (documentId, options) => {
2919
+ const context = await runtime(program, dependencies2);
2920
+ emit(
2921
+ program,
2922
+ dependencies2,
2923
+ await getDocument(context.client, options.source, documentId)
2924
+ );
2925
+ });
2926
+ document.command("status").argument("[document-id]").requiredOption("--source <source-id>").action(async (documentId, options) => {
2927
+ const context = await runtime(program, dependencies2);
2928
+ emit(
2929
+ program,
2930
+ dependencies2,
2931
+ await documentStatus(context.client, options.source, documentId)
2932
+ );
2933
+ });
2934
+ program.command("search").argument("<query>").option("--source <source-id...>").option("--top-k <number>", "Maximum result count", parseInteger).addOption(new Option("--strategy <strategy>").choices(["vector", "multi"])).action(
2935
+ async (query, options) => {
2936
+ const context = await runtime(program, dependencies2);
2937
+ emit(
2938
+ program,
2939
+ dependencies2,
2940
+ await searchKnowledge(context.client, query, {
2941
+ ...options.source ? { sourceIds: options.source } : {},
2942
+ ...options.topK !== void 0 ? { topK: options.topK } : {},
2943
+ ...options.strategy ? { strategy: options.strategy } : {}
2944
+ })
2945
+ );
2946
+ }
2947
+ );
2948
+ }
2949
+ function addLocalMcpCommands(program, dependencies2) {
2950
+ const mcp = program.command("mcp").description("Verify local SAG MCP");
2951
+ mcp.command("test").option("--source-id <source-id>").option("--container <name-or-id>").option("--timeout <milliseconds>", "MCP timeout", parsePositiveInteger, 15e3).action(
2952
+ async (options) => {
2953
+ emit(
2954
+ program,
2955
+ dependencies2,
2956
+ await testLocalMcp(localMcpRuntime(dependencies2), {
2957
+ ...options.sourceId ? { sourceId: options.sourceId } : {},
2958
+ ...options.container ? { container: options.container } : {},
2959
+ timeoutMs: options.timeout
2960
+ })
2961
+ );
2962
+ }
2963
+ );
2964
+ }
2965
+ function addAgentCommands(program, dependencies2) {
2966
+ const agent = program.command("agent").description("Manage Agent MCP connections");
2967
+ agent.command("list").action(async () => {
2968
+ emit(program, dependencies2, await listAgentHosts(localMcpRuntime(dependencies2)));
2969
+ });
2970
+ agent.command("connect").argument("<agent>", "codex or claude-code", parseAgent).option("--source-id <source-id>").option("--container <name-or-id>").option("--name <server-name>").option("--timeout <milliseconds>", "MCP timeout", parsePositiveInteger, 15e3).option("--dry-run", "Show the plan without changing configuration").action(
2971
+ async (agentId, options) => {
2972
+ const selection = await selectedServerName(program, dependencies2, options.name);
2973
+ emit(
2974
+ program,
2975
+ dependencies2,
2976
+ await connectLocalAgent(localMcpRuntime(dependencies2), {
2977
+ agent: agentId,
2978
+ serverName: selection.serverName,
2979
+ profile: selection.profile,
2980
+ ...options.sourceId ? { sourceId: options.sourceId } : {},
2981
+ ...options.container ? { container: options.container } : {},
2982
+ timeoutMs: options.timeout,
2983
+ dryRun: options.dryRun ?? false,
2984
+ yes: commandOptions(program).yes ?? false,
2985
+ confirm: dependencies2.confirm,
2986
+ now: () => /* @__PURE__ */ new Date()
2987
+ })
2988
+ );
2989
+ }
2990
+ );
2991
+ agent.command("status").argument("[agent]", "codex or claude-code", parseAgent).option("--name <server-name>").action(async (agentId, options) => {
2992
+ const selection = await selectedServerName(program, dependencies2, options.name);
2993
+ emit(
2994
+ program,
2995
+ dependencies2,
2996
+ await statusLocalAgents(localMcpRuntime(dependencies2), {
2997
+ ...agentId ? { agent: agentId } : {},
2998
+ serverName: selection.serverName
2999
+ })
3000
+ );
3001
+ });
3002
+ agent.command("disconnect").argument("<agent>", "codex or claude-code", parseAgent).option("--name <server-name>").action(async (agentId, options) => {
3003
+ const selection = await selectedServerName(program, dependencies2, options.name);
3004
+ emit(
3005
+ program,
3006
+ dependencies2,
3007
+ await disconnectLocalAgent(localMcpRuntime(dependencies2), {
3008
+ agent: agentId,
3009
+ serverName: selection.serverName,
3010
+ yes: commandOptions(program).yes ?? false,
3011
+ confirm: dependencies2.confirm
3012
+ })
3013
+ );
3014
+ });
3015
+ }
3016
+ function createProgram(dependencies2) {
3017
+ const program = new Command();
3018
+ program.name("sag").description("Command-line client and diagnostics for SAG").showHelpAfterError().exitOverride().option("--json", "Output a stable JSON envelope").option("--quiet", "Output only essential values").option("--profile <name>", "Use a configured SAG profile").option("--url <origin>", "Use a SAG origin without saving it").option("--yes", "Confirm safe local configuration changes").configureOutput({
3019
+ writeOut: dependencies2.writeStdout,
3020
+ writeErr: dependencies2.writeStderr
3021
+ });
3022
+ program.command("version").action(() => {
3023
+ const options = commandOptions(program);
3024
+ if (options.json) {
3025
+ dependencies2.writeStdout(
3026
+ renderJson(success({ name: "sag", version: dependencies2.version }))
3027
+ );
3028
+ } else {
3029
+ dependencies2.writeStdout(`sag ${dependencies2.version}
3030
+ `);
3031
+ }
3032
+ });
3033
+ addProfileCommands(program, dependencies2);
3034
+ addAuthCommands(program, dependencies2);
3035
+ addKnowledgeCommands(program, dependencies2);
3036
+ addLocalMcpCommands(program, dependencies2);
3037
+ addAgentCommands(program, dependencies2);
3038
+ return program;
3039
+ }
3040
+ async function runCli(arguments_, dependencies2) {
3041
+ const json = arguments_.includes("--json");
3042
+ try {
3043
+ await createProgram(dependencies2).parseAsync(arguments_);
3044
+ return exitCodes.success;
3045
+ } catch (error) {
3046
+ if (error instanceof CommanderError && error.exitCode === 0) {
3047
+ return 0;
3048
+ }
3049
+ const cliError = error instanceof CommanderError ? new CliError("INVALID_ARGUMENT", error.message, {
3050
+ exitCode: exitCodes.invalidArgument
3051
+ }) : toCliError(error);
3052
+ if (json) {
3053
+ dependencies2.writeStdout(renderJson(failure(cliError)));
3054
+ } else {
3055
+ dependencies2.writeStderr(
3056
+ `Error [${cliError.code}]: ${cliError.message}${cliError.hint ? `
3057
+ Hint: ${cliError.hint}` : ""}
3058
+ `
3059
+ );
3060
+ if (dependencies2.environment.SAG_DEBUG === "1" && cliError.cause instanceof Error) {
3061
+ dependencies2.writeStderr(`${cliError.cause.stack ?? cliError.cause.message}
3062
+ `);
3063
+ }
3064
+ }
3065
+ return cliError.exitCode;
3066
+ }
3067
+ }
3068
+ var defaultClientFactory = (options) => new SagClient(options);
3069
+
3070
+ // src/cli.ts
3071
+ var credentialSelection = await createCredentialStore();
3072
+ if (credentialSelection.warning) {
3073
+ process.stderr.write(`Warning: ${credentialSelection.warning}
3074
+ `);
3075
+ }
3076
+ var processRunner = new NodeProcessRunner();
3077
+ var dependencies = {
3078
+ version: package_default.version,
3079
+ environment: process.env,
3080
+ configStore: new ConfigStore(defaultConfigPath()),
3081
+ credentialStore: credentialSelection.store,
3082
+ createClient: defaultClientFactory,
3083
+ promptToken: () => password({
3084
+ message: "SAG Token",
3085
+ mask: "*"
3086
+ }),
3087
+ confirm: (message) => confirm({ message, default: false }),
3088
+ writeStdout: (value) => process.stdout.write(value),
3089
+ writeStderr: (value) => process.stderr.write(value),
3090
+ localMcp: {
3091
+ docker: new DockerClient(processRunner),
3092
+ verifier: new McpVerifier(package_default.version),
3093
+ state: new ManagedConnectionStore(defaultManagedConnectionsPath()),
3094
+ adapters: {
3095
+ codex: new CodexAdapter(processRunner),
3096
+ "claude-code": new ClaudeCodeAdapter(processRunner)
3097
+ },
3098
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
3099
+ selectContainer: (candidates) => select({
3100
+ message: "Select the SAG API container",
3101
+ choices: candidates.map((candidate) => ({
3102
+ name: `${candidate.name} (${candidate.composeProject ?? "no Compose project"})`,
3103
+ value: candidate.id
3104
+ }))
3105
+ })
3106
+ }
3107
+ };
3108
+ process.exitCode = await runCli(process.argv, dependencies);
3109
+ //# sourceMappingURL=cli.js.map