@zleap-ai/sag-cli 0.3.0 → 0.4.1
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/README.en.md +5 -5
- package/README.md +5 -6
- package/dist/cli.js +235 -192
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +10 -0
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { confirm,
|
|
4
|
+
import { confirm, input, select } from "@inquirer/prompts";
|
|
5
5
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@zleap-ai/sag-cli",
|
|
9
|
-
version: "0.
|
|
9
|
+
version: "0.4.1",
|
|
10
10
|
description: "Command-line client and diagnostics for SAG knowledge bases",
|
|
11
11
|
type: "module",
|
|
12
12
|
bin: {
|
|
@@ -177,7 +177,7 @@ var claudeEntrySchema = z.object({
|
|
|
177
177
|
function invalidSpec() {
|
|
178
178
|
return new CliError(
|
|
179
179
|
"INVALID_ARGUMENT",
|
|
180
|
-
"Claude Code
|
|
180
|
+
"The Claude Code adapter supports Docker stdio MCP connections only",
|
|
181
181
|
{
|
|
182
182
|
exitCode: exitCodes.invalidArgument
|
|
183
183
|
}
|
|
@@ -206,6 +206,12 @@ var ClaudeCodeAdapter = class {
|
|
|
206
206
|
}
|
|
207
207
|
runner;
|
|
208
208
|
id = "claude-code";
|
|
209
|
+
capabilities = {
|
|
210
|
+
transports: ["stdio"],
|
|
211
|
+
scopes: ["user"],
|
|
212
|
+
structuredRead: true,
|
|
213
|
+
projectApproval: false
|
|
214
|
+
};
|
|
209
215
|
readConfig;
|
|
210
216
|
async detect() {
|
|
211
217
|
try {
|
|
@@ -294,7 +300,7 @@ var CODEX_STATUS_FIELDS = /* @__PURE__ */ new Set(["name", "disabled_reason", "a
|
|
|
294
300
|
function invalidSpec2() {
|
|
295
301
|
return new CliError(
|
|
296
302
|
"INVALID_ARGUMENT",
|
|
297
|
-
"Codex
|
|
303
|
+
"The Codex adapter supports Docker stdio MCP connections only",
|
|
298
304
|
{
|
|
299
305
|
exitCode: exitCodes.invalidArgument
|
|
300
306
|
}
|
|
@@ -318,6 +324,12 @@ var CodexAdapter = class {
|
|
|
318
324
|
}
|
|
319
325
|
runner;
|
|
320
326
|
id = "codex";
|
|
327
|
+
capabilities = {
|
|
328
|
+
transports: ["stdio"],
|
|
329
|
+
scopes: ["user"],
|
|
330
|
+
structuredRead: true,
|
|
331
|
+
projectApproval: false
|
|
332
|
+
};
|
|
321
333
|
async detect() {
|
|
322
334
|
try {
|
|
323
335
|
const result = await this.runner.run({
|
|
@@ -534,9 +546,9 @@ var ManagedConnectionStore = class {
|
|
|
534
546
|
await this.#saveUnlocked(state);
|
|
535
547
|
});
|
|
536
548
|
}
|
|
537
|
-
async list(
|
|
549
|
+
async list(input2) {
|
|
538
550
|
return Object.values((await this.load()).connections).filter(
|
|
539
|
-
(connection) => (!
|
|
551
|
+
(connection) => (!input2?.agent || connection.agent === input2.agent) && (!input2?.scope || connection.scope === input2.scope)
|
|
540
552
|
);
|
|
541
553
|
}
|
|
542
554
|
};
|
|
@@ -558,12 +570,12 @@ function validateProfileName(name) {
|
|
|
558
570
|
}
|
|
559
571
|
return name;
|
|
560
572
|
}
|
|
561
|
-
function normalizeOrigin(
|
|
573
|
+
function normalizeOrigin(input2) {
|
|
562
574
|
let url;
|
|
563
575
|
try {
|
|
564
|
-
url = new URL(
|
|
576
|
+
url = new URL(input2);
|
|
565
577
|
} catch (cause) {
|
|
566
|
-
throw new CliError("INVALID_ARGUMENT", `Invalid SAG URL: ${
|
|
578
|
+
throw new CliError("INVALID_ARGUMENT", `Invalid SAG URL: ${input2}`, {
|
|
567
579
|
exitCode: exitCodes.invalidArgument,
|
|
568
580
|
cause,
|
|
569
581
|
hint: "Use an absolute HTTP or HTTPS URL such as http://localhost:8000."
|
|
@@ -1090,16 +1102,6 @@ var McpVerifier = class {
|
|
|
1090
1102
|
}
|
|
1091
1103
|
cliVersion;
|
|
1092
1104
|
async verify(spec, options) {
|
|
1093
|
-
if (spec.transport !== "stdio") {
|
|
1094
|
-
throw new CliError(
|
|
1095
|
-
"INVALID_ARGUMENT",
|
|
1096
|
-
"HTTP MCP is not supported in SAG CLI v0.2",
|
|
1097
|
-
{
|
|
1098
|
-
exitCode: exitCodes.invalidArgument,
|
|
1099
|
-
hint: "Use a local Docker stdio connection."
|
|
1100
|
-
}
|
|
1101
|
-
);
|
|
1102
|
-
}
|
|
1103
1105
|
const client = new Client(
|
|
1104
1106
|
{
|
|
1105
1107
|
name: "@zleap-ai/sag-cli",
|
|
@@ -1191,12 +1193,12 @@ var McpVerifier = class {
|
|
|
1191
1193
|
// src/process/runner.ts
|
|
1192
1194
|
import { spawn } from "child_process";
|
|
1193
1195
|
var NodeProcessRunner = class {
|
|
1194
|
-
async run(
|
|
1196
|
+
async run(input2) {
|
|
1195
1197
|
return new Promise((resolve, reject) => {
|
|
1196
|
-
const child = spawn(
|
|
1198
|
+
const child = spawn(input2.command, input2.args, {
|
|
1197
1199
|
shell: false,
|
|
1198
|
-
env:
|
|
1199
|
-
stdio: [
|
|
1200
|
+
env: input2.env ?? process.env,
|
|
1201
|
+
stdio: [input2.stdin === "pipe" ? "pipe" : "ignore", "pipe", "pipe"]
|
|
1200
1202
|
});
|
|
1201
1203
|
const childStdout = child.stdout;
|
|
1202
1204
|
const childStderr = child.stderr;
|
|
@@ -1210,7 +1212,7 @@ var NodeProcessRunner = class {
|
|
|
1210
1212
|
forceKill = setTimeout(() => {
|
|
1211
1213
|
child.kill("SIGKILL");
|
|
1212
1214
|
}, 250);
|
|
1213
|
-
},
|
|
1215
|
+
}, input2.timeoutMs);
|
|
1214
1216
|
childStdout?.setEncoding("utf8");
|
|
1215
1217
|
childStderr?.setEncoding("utf8");
|
|
1216
1218
|
childStdout?.on("data", (chunk) => {
|
|
@@ -1223,10 +1225,10 @@ var NodeProcessRunner = class {
|
|
|
1223
1225
|
clearTimeout(timeout);
|
|
1224
1226
|
if (forceKill) clearTimeout(forceKill);
|
|
1225
1227
|
reject(
|
|
1226
|
-
new CliError("DEPENDENCY_MISSING", `Unable to start ${
|
|
1228
|
+
new CliError("DEPENDENCY_MISSING", `Unable to start ${input2.command}`, {
|
|
1227
1229
|
exitCode: exitCodes.dependencyMissing,
|
|
1228
1230
|
cause,
|
|
1229
|
-
hint: `Install ${
|
|
1231
|
+
hint: `Install ${input2.command} and ensure it is available on PATH.`
|
|
1230
1232
|
})
|
|
1231
1233
|
);
|
|
1232
1234
|
});
|
|
@@ -1235,9 +1237,9 @@ var NodeProcessRunner = class {
|
|
|
1235
1237
|
if (forceKill) clearTimeout(forceKill);
|
|
1236
1238
|
if (timedOut) {
|
|
1237
1239
|
reject(
|
|
1238
|
-
new CliError("DEPENDENCY_MISSING", `${
|
|
1240
|
+
new CliError("DEPENDENCY_MISSING", `${input2.command} timed out`, {
|
|
1239
1241
|
exitCode: exitCodes.dependencyMissing,
|
|
1240
|
-
hint: `Check that ${
|
|
1242
|
+
hint: `Check that ${input2.command} is responsive and try again.`
|
|
1241
1243
|
})
|
|
1242
1244
|
);
|
|
1243
1245
|
return;
|
|
@@ -1274,6 +1276,10 @@ var userSchema = z6.object({
|
|
|
1274
1276
|
name: z6.string(),
|
|
1275
1277
|
created_at: dateTime.optional()
|
|
1276
1278
|
}).passthrough();
|
|
1279
|
+
var loginResponseSchema = z6.object({
|
|
1280
|
+
access_token: z6.string().min(1),
|
|
1281
|
+
user: userSchema
|
|
1282
|
+
}).passthrough();
|
|
1277
1283
|
var sourceSchema = z6.object({
|
|
1278
1284
|
id: z6.string().min(1),
|
|
1279
1285
|
name: z6.string(),
|
|
@@ -1394,6 +1400,7 @@ var entityContextSchema = z6.object({
|
|
|
1394
1400
|
}).passthrough();
|
|
1395
1401
|
|
|
1396
1402
|
// src/api/client.ts
|
|
1403
|
+
var searchTimeoutMs = 15e3;
|
|
1397
1404
|
var SagClient = class {
|
|
1398
1405
|
#origin;
|
|
1399
1406
|
#token;
|
|
@@ -1423,6 +1430,14 @@ var SagClient = class {
|
|
|
1423
1430
|
me() {
|
|
1424
1431
|
return this.#request("/api/v1/auth/me", userSchema);
|
|
1425
1432
|
}
|
|
1433
|
+
async login(name) {
|
|
1434
|
+
const response = await this.#request("/api/v1/auth/login", loginResponseSchema, {
|
|
1435
|
+
authenticated: false,
|
|
1436
|
+
method: "POST",
|
|
1437
|
+
body: { name }
|
|
1438
|
+
});
|
|
1439
|
+
return { accessToken: response.access_token, user: response.user };
|
|
1440
|
+
}
|
|
1426
1441
|
mcpDescriptor() {
|
|
1427
1442
|
return this.#request("/api/v1/system/mcp", mcpDescriptorSchema);
|
|
1428
1443
|
}
|
|
@@ -1447,10 +1462,11 @@ var SagClient = class {
|
|
|
1447
1462
|
documentSchema
|
|
1448
1463
|
);
|
|
1449
1464
|
}
|
|
1450
|
-
search(
|
|
1465
|
+
search(input2) {
|
|
1451
1466
|
return this.#request("/api/v1/search", searchResponseSchema, {
|
|
1452
1467
|
method: "POST",
|
|
1453
|
-
body:
|
|
1468
|
+
body: input2,
|
|
1469
|
+
timeoutMs: searchTimeoutMs
|
|
1454
1470
|
});
|
|
1455
1471
|
}
|
|
1456
1472
|
outline(sourceId, documentId) {
|
|
@@ -1513,7 +1529,7 @@ var SagClient = class {
|
|
|
1513
1529
|
method: options.method ?? "GET",
|
|
1514
1530
|
headers,
|
|
1515
1531
|
...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
|
|
1516
|
-
signal: AbortSignal.timeout(this.#timeoutMs)
|
|
1532
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? this.#timeoutMs)
|
|
1517
1533
|
});
|
|
1518
1534
|
} catch (cause) {
|
|
1519
1535
|
throw new CliError("NETWORK_UNREACHABLE", `Cannot reach SAG at ${this.#origin}`, {
|
|
@@ -1603,81 +1619,98 @@ function hostFailure3(message, cause, hint) {
|
|
|
1603
1619
|
hint: hint ?? "Inspect the Agent MCP configuration and run the command again."
|
|
1604
1620
|
});
|
|
1605
1621
|
}
|
|
1606
|
-
function determinePlan(
|
|
1622
|
+
function determinePlan(input2) {
|
|
1607
1623
|
let action;
|
|
1608
|
-
if (!
|
|
1609
|
-
if (
|
|
1624
|
+
if (!input2.hostFingerprint || !input2.hostSpecFingerprint) {
|
|
1625
|
+
if (input2.managed) {
|
|
1610
1626
|
throw conflict("Managed state exists but the Agent MCP entry is missing");
|
|
1611
1627
|
}
|
|
1612
1628
|
action = "create";
|
|
1613
|
-
} else if (!
|
|
1629
|
+
} else if (!input2.managed) {
|
|
1614
1630
|
throw conflict("The Agent MCP name is already owned by the user");
|
|
1615
|
-
} else if (
|
|
1631
|
+
} else if (input2.managed.fingerprint !== input2.hostFingerprint) {
|
|
1616
1632
|
throw conflict("The managed Agent MCP entry was changed outside SAG CLI");
|
|
1617
|
-
} else if (
|
|
1633
|
+
} else if (input2.hostSpecFingerprint === input2.desiredFingerprint) {
|
|
1618
1634
|
action = "unchanged";
|
|
1619
1635
|
} else {
|
|
1620
1636
|
action = "update";
|
|
1621
1637
|
}
|
|
1622
1638
|
return {
|
|
1623
1639
|
action,
|
|
1624
|
-
agent:
|
|
1625
|
-
serverName:
|
|
1626
|
-
scope:
|
|
1640
|
+
agent: input2.agent,
|
|
1641
|
+
serverName: input2.serverName,
|
|
1642
|
+
scope: input2.scope,
|
|
1627
1643
|
provider: "docker-stdio",
|
|
1628
|
-
target:
|
|
1629
|
-
sourceId:
|
|
1630
|
-
...
|
|
1631
|
-
desiredFingerprint:
|
|
1644
|
+
target: input2.target.name,
|
|
1645
|
+
sourceId: input2.sourceId ?? null,
|
|
1646
|
+
...input2.hostSpecFingerprint ? { currentFingerprint: input2.hostSpecFingerprint } : {},
|
|
1647
|
+
desiredFingerprint: input2.desiredFingerprint
|
|
1632
1648
|
};
|
|
1633
1649
|
}
|
|
1634
|
-
function managedConnection(
|
|
1650
|
+
function managedConnection(input2, resolved, fingerprint, existing) {
|
|
1635
1651
|
return {
|
|
1636
|
-
agent:
|
|
1652
|
+
agent: input2.agent,
|
|
1637
1653
|
provider: "docker-stdio",
|
|
1638
|
-
profile:
|
|
1639
|
-
serverName:
|
|
1654
|
+
profile: input2.profile,
|
|
1655
|
+
serverName: input2.serverName,
|
|
1640
1656
|
docker: {
|
|
1641
1657
|
...resolved.target.composeProject ? { composeProject: resolved.target.composeProject } : {},
|
|
1642
1658
|
...resolved.target.composeService ? { composeService: resolved.target.composeService } : {},
|
|
1643
1659
|
containerName: resolved.target.name
|
|
1644
1660
|
},
|
|
1645
|
-
sourceId:
|
|
1646
|
-
scope:
|
|
1661
|
+
sourceId: input2.sourceId ?? null,
|
|
1662
|
+
scope: input2.scope,
|
|
1647
1663
|
fingerprint,
|
|
1648
|
-
createdAt: existing?.createdAt ??
|
|
1664
|
+
createdAt: existing?.createdAt ?? input2.now().toISOString()
|
|
1649
1665
|
};
|
|
1650
1666
|
}
|
|
1651
|
-
async function rollbackHost(
|
|
1652
|
-
if (
|
|
1667
|
+
async function rollbackHost(input2) {
|
|
1668
|
+
if (input2.previousSpec) {
|
|
1653
1669
|
try {
|
|
1654
|
-
await
|
|
1670
|
+
await input2.adapter.remove(input2.serverName, input2.scope);
|
|
1655
1671
|
} catch {
|
|
1656
1672
|
}
|
|
1657
|
-
await
|
|
1658
|
-
const restored = await
|
|
1659
|
-
if (!restored || restored.fingerprint !== (
|
|
1673
|
+
await input2.adapter.add(input2.serverName, input2.scope, input2.previousSpec);
|
|
1674
|
+
const restored = await input2.adapter.read(input2.serverName, input2.scope);
|
|
1675
|
+
if (!restored || restored.fingerprint !== (input2.previousFingerprint ?? fingerprintConnection(input2.previousSpec))) {
|
|
1660
1676
|
throw new Error("Previous Agent MCP entry was not restored");
|
|
1661
1677
|
}
|
|
1662
1678
|
return;
|
|
1663
1679
|
}
|
|
1664
|
-
await
|
|
1680
|
+
await input2.adapter.remove(input2.serverName, input2.scope);
|
|
1665
1681
|
}
|
|
1666
|
-
async function connectAgent(
|
|
1667
|
-
validateServerName(
|
|
1668
|
-
const resolved = await
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1682
|
+
async function connectAgent(input2) {
|
|
1683
|
+
validateServerName(input2.serverName);
|
|
1684
|
+
const resolved = await input2.resolveConnection();
|
|
1685
|
+
if (!input2.adapter.capabilities.transports.includes(resolved.spec.transport)) {
|
|
1686
|
+
throw new CliError(
|
|
1687
|
+
"INVALID_ARGUMENT",
|
|
1688
|
+
`${input2.agent} adapter does not support ${resolved.spec.transport} MCP connections`,
|
|
1689
|
+
{
|
|
1690
|
+
exitCode: exitCodes.invalidArgument,
|
|
1691
|
+
hint: "Choose a provider supported by the installed Agent adapter."
|
|
1692
|
+
}
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
if (!input2.adapter.capabilities.scopes.includes(input2.scope)) {
|
|
1696
|
+
throw new CliError(
|
|
1697
|
+
"INVALID_ARGUMENT",
|
|
1698
|
+
`${input2.agent} adapter does not support ${input2.scope} scope`,
|
|
1699
|
+
{ exitCode: exitCodes.invalidArgument }
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
await input2.verifier.verify(resolved.spec, { timeoutMs: input2.timeoutMs });
|
|
1703
|
+
await input2.adapter.detect();
|
|
1704
|
+
const host = await input2.adapter.read(input2.serverName, input2.scope);
|
|
1705
|
+
const key = connectionKey(input2.agent, input2.scope, input2.serverName);
|
|
1706
|
+
const managed = await input2.state.get(key);
|
|
1674
1707
|
const desiredFingerprint = fingerprintConnection(resolved.spec);
|
|
1675
1708
|
const plan = determinePlan({
|
|
1676
|
-
agent:
|
|
1677
|
-
serverName:
|
|
1678
|
-
scope:
|
|
1709
|
+
agent: input2.agent,
|
|
1710
|
+
serverName: input2.serverName,
|
|
1711
|
+
scope: input2.scope,
|
|
1679
1712
|
target: resolved.target,
|
|
1680
|
-
...
|
|
1713
|
+
...input2.sourceId ? { sourceId: input2.sourceId } : {},
|
|
1681
1714
|
desiredFingerprint,
|
|
1682
1715
|
...host ? { hostFingerprint: host.fingerprint } : {},
|
|
1683
1716
|
...host ? { hostSpecFingerprint: fingerprintConnection(host.spec) } : {},
|
|
@@ -1685,9 +1718,9 @@ async function connectAgent(input) {
|
|
|
1685
1718
|
});
|
|
1686
1719
|
if (plan.action === "unchanged") {
|
|
1687
1720
|
return {
|
|
1688
|
-
agent:
|
|
1689
|
-
serverName:
|
|
1690
|
-
scope:
|
|
1721
|
+
agent: input2.agent,
|
|
1722
|
+
serverName: input2.serverName,
|
|
1723
|
+
scope: input2.scope,
|
|
1691
1724
|
action: "unchanged",
|
|
1692
1725
|
changed: false,
|
|
1693
1726
|
verified: true,
|
|
@@ -1695,11 +1728,11 @@ async function connectAgent(input) {
|
|
|
1695
1728
|
plan
|
|
1696
1729
|
};
|
|
1697
1730
|
}
|
|
1698
|
-
if (
|
|
1731
|
+
if (input2.dryRun) {
|
|
1699
1732
|
return {
|
|
1700
|
-
agent:
|
|
1701
|
-
serverName:
|
|
1702
|
-
scope:
|
|
1733
|
+
agent: input2.agent,
|
|
1734
|
+
serverName: input2.serverName,
|
|
1735
|
+
scope: input2.scope,
|
|
1703
1736
|
action: plan.action,
|
|
1704
1737
|
changed: false,
|
|
1705
1738
|
verified: true,
|
|
@@ -1707,13 +1740,13 @@ async function connectAgent(input) {
|
|
|
1707
1740
|
plan
|
|
1708
1741
|
};
|
|
1709
1742
|
}
|
|
1710
|
-
if (!
|
|
1711
|
-
`${plan.action === "create" ? "Add" : "Update"} ${
|
|
1743
|
+
if (!input2.yes && !await input2.confirm(
|
|
1744
|
+
`${plan.action === "create" ? "Add" : "Update"} ${input2.serverName} in ${input2.agent}?`
|
|
1712
1745
|
)) {
|
|
1713
1746
|
return {
|
|
1714
|
-
agent:
|
|
1715
|
-
serverName:
|
|
1716
|
-
scope:
|
|
1747
|
+
agent: input2.agent,
|
|
1748
|
+
serverName: input2.serverName,
|
|
1749
|
+
scope: input2.scope,
|
|
1717
1750
|
action: "cancelled",
|
|
1718
1751
|
changed: false,
|
|
1719
1752
|
verified: true,
|
|
@@ -1723,23 +1756,23 @@ async function connectAgent(input) {
|
|
|
1723
1756
|
}
|
|
1724
1757
|
try {
|
|
1725
1758
|
if (plan.action === "update") {
|
|
1726
|
-
await
|
|
1759
|
+
await input2.adapter.remove(input2.serverName, input2.scope);
|
|
1727
1760
|
}
|
|
1728
|
-
await
|
|
1729
|
-
const readBack = await
|
|
1761
|
+
await input2.adapter.add(input2.serverName, input2.scope, resolved.spec);
|
|
1762
|
+
const readBack = await input2.adapter.read(input2.serverName, input2.scope);
|
|
1730
1763
|
if (!readBack || fingerprintConnection(readBack.spec) !== desiredFingerprint) {
|
|
1731
1764
|
throw hostFailure3("Agent MCP read-back did not match the requested connection");
|
|
1732
1765
|
}
|
|
1733
|
-
await
|
|
1734
|
-
await
|
|
1735
|
-
managedConnection(
|
|
1766
|
+
await input2.verifier.verify(readBack.spec, { timeoutMs: input2.timeoutMs });
|
|
1767
|
+
await input2.state.set(
|
|
1768
|
+
managedConnection(input2, resolved, readBack.fingerprint, managed)
|
|
1736
1769
|
);
|
|
1737
1770
|
} catch (cause) {
|
|
1738
1771
|
try {
|
|
1739
1772
|
await rollbackHost({
|
|
1740
|
-
adapter:
|
|
1741
|
-
serverName:
|
|
1742
|
-
scope:
|
|
1773
|
+
adapter: input2.adapter,
|
|
1774
|
+
serverName: input2.serverName,
|
|
1775
|
+
scope: input2.scope,
|
|
1743
1776
|
...host ? { previousSpec: host.spec } : {},
|
|
1744
1777
|
...host ? { previousFingerprint: host.fingerprint } : {}
|
|
1745
1778
|
});
|
|
@@ -1747,7 +1780,7 @@ async function connectAgent(input) {
|
|
|
1747
1780
|
throw hostFailure3(
|
|
1748
1781
|
"Agent MCP configuration failed and rollback also failed",
|
|
1749
1782
|
cause,
|
|
1750
|
-
`Inspect the ${
|
|
1783
|
+
`Inspect the ${input2.agent} MCP entry \`${input2.serverName}\` before retrying.`
|
|
1751
1784
|
);
|
|
1752
1785
|
}
|
|
1753
1786
|
throw hostFailure3(
|
|
@@ -1756,9 +1789,9 @@ async function connectAgent(input) {
|
|
|
1756
1789
|
);
|
|
1757
1790
|
}
|
|
1758
1791
|
return {
|
|
1759
|
-
agent:
|
|
1760
|
-
serverName:
|
|
1761
|
-
scope:
|
|
1792
|
+
agent: input2.agent,
|
|
1793
|
+
serverName: input2.serverName,
|
|
1794
|
+
scope: input2.scope,
|
|
1762
1795
|
action: plan.action,
|
|
1763
1796
|
changed: true,
|
|
1764
1797
|
verified: true,
|
|
@@ -1766,15 +1799,15 @@ async function connectAgent(input) {
|
|
|
1766
1799
|
plan
|
|
1767
1800
|
};
|
|
1768
1801
|
}
|
|
1769
|
-
async function disconnectAgent(
|
|
1770
|
-
validateServerName(
|
|
1771
|
-
await
|
|
1772
|
-
const key = connectionKey(
|
|
1773
|
-
const managed = await
|
|
1802
|
+
async function disconnectAgent(input2) {
|
|
1803
|
+
validateServerName(input2.serverName);
|
|
1804
|
+
await input2.adapter.detect();
|
|
1805
|
+
const key = connectionKey(input2.agent, input2.scope, input2.serverName);
|
|
1806
|
+
const managed = await input2.state.get(key);
|
|
1774
1807
|
if (!managed) {
|
|
1775
1808
|
throw conflict("The Agent MCP entry is not managed by SAG CLI");
|
|
1776
1809
|
}
|
|
1777
|
-
const host = await
|
|
1810
|
+
const host = await input2.adapter.read(input2.serverName, input2.scope);
|
|
1778
1811
|
if (!host) {
|
|
1779
1812
|
throw conflict("Managed state exists but the Agent MCP entry is missing");
|
|
1780
1813
|
}
|
|
@@ -1783,20 +1816,20 @@ async function disconnectAgent(input) {
|
|
|
1783
1816
|
}
|
|
1784
1817
|
const plan = {
|
|
1785
1818
|
action: "unchanged",
|
|
1786
|
-
agent:
|
|
1787
|
-
serverName:
|
|
1788
|
-
scope:
|
|
1819
|
+
agent: input2.agent,
|
|
1820
|
+
serverName: input2.serverName,
|
|
1821
|
+
scope: input2.scope,
|
|
1789
1822
|
provider: "docker-stdio",
|
|
1790
1823
|
target: managed.docker.containerName,
|
|
1791
1824
|
sourceId: managed.sourceId,
|
|
1792
1825
|
currentFingerprint: host.fingerprint,
|
|
1793
1826
|
desiredFingerprint: host.fingerprint
|
|
1794
1827
|
};
|
|
1795
|
-
if (!
|
|
1828
|
+
if (!input2.yes && !await input2.confirm(`Remove ${input2.serverName} from ${input2.agent}?`)) {
|
|
1796
1829
|
return {
|
|
1797
|
-
agent:
|
|
1798
|
-
serverName:
|
|
1799
|
-
scope:
|
|
1830
|
+
agent: input2.agent,
|
|
1831
|
+
serverName: input2.serverName,
|
|
1832
|
+
scope: input2.scope,
|
|
1800
1833
|
action: "cancelled",
|
|
1801
1834
|
changed: false,
|
|
1802
1835
|
verified: true,
|
|
@@ -1804,12 +1837,12 @@ async function disconnectAgent(input) {
|
|
|
1804
1837
|
plan
|
|
1805
1838
|
};
|
|
1806
1839
|
}
|
|
1807
|
-
await
|
|
1840
|
+
await input2.adapter.remove(input2.serverName, input2.scope);
|
|
1808
1841
|
try {
|
|
1809
|
-
await
|
|
1842
|
+
await input2.state.delete(key);
|
|
1810
1843
|
} catch (cause) {
|
|
1811
1844
|
try {
|
|
1812
|
-
await
|
|
1845
|
+
await input2.adapter.add(input2.serverName, input2.scope, host.spec);
|
|
1813
1846
|
} catch {
|
|
1814
1847
|
throw hostFailure3(
|
|
1815
1848
|
"Managed state cleanup failed and the Agent entry could not be restored",
|
|
@@ -1822,9 +1855,9 @@ async function disconnectAgent(input) {
|
|
|
1822
1855
|
);
|
|
1823
1856
|
}
|
|
1824
1857
|
return {
|
|
1825
|
-
agent:
|
|
1826
|
-
serverName:
|
|
1827
|
-
scope:
|
|
1858
|
+
agent: input2.agent,
|
|
1859
|
+
serverName: input2.serverName,
|
|
1860
|
+
scope: input2.scope,
|
|
1828
1861
|
action: "disconnect",
|
|
1829
1862
|
changed: true,
|
|
1830
1863
|
verified: true,
|
|
@@ -1837,21 +1870,21 @@ async function disconnectAgent(input) {
|
|
|
1837
1870
|
var SOURCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
|
|
1838
1871
|
var DockerStdioProvider = class {
|
|
1839
1872
|
id = "docker-stdio";
|
|
1840
|
-
async inspect(
|
|
1873
|
+
async inspect(input2) {
|
|
1841
1874
|
return {
|
|
1842
1875
|
provider: this.id,
|
|
1843
1876
|
ready: true,
|
|
1844
|
-
target:
|
|
1877
|
+
target: input2.target.name
|
|
1845
1878
|
};
|
|
1846
1879
|
}
|
|
1847
|
-
async createSpec(
|
|
1848
|
-
if (
|
|
1880
|
+
async createSpec(input2) {
|
|
1881
|
+
if (input2.sourceId && !SOURCE_ID_PATTERN.test(input2.sourceId)) {
|
|
1849
1882
|
throw new CliError("INVALID_ARGUMENT", "Invalid SAG source id", {
|
|
1850
1883
|
exitCode: exitCodes.invalidArgument,
|
|
1851
1884
|
hint: "Use 1-128 letters, numbers, underscores, or dashes."
|
|
1852
1885
|
});
|
|
1853
1886
|
}
|
|
1854
|
-
const sourceArguments =
|
|
1887
|
+
const sourceArguments = input2.sourceId ? ["-e", `SAG_MCP_SOURCE_ID=${input2.sourceId}`] : [];
|
|
1855
1888
|
return {
|
|
1856
1889
|
transport: "stdio",
|
|
1857
1890
|
command: "docker",
|
|
@@ -1859,7 +1892,7 @@ var DockerStdioProvider = class {
|
|
|
1859
1892
|
"exec",
|
|
1860
1893
|
"-i",
|
|
1861
1894
|
...sourceArguments,
|
|
1862
|
-
|
|
1895
|
+
input2.target.name,
|
|
1863
1896
|
"python",
|
|
1864
1897
|
"-m",
|
|
1865
1898
|
"sag_api.mcp.server"
|
|
@@ -1909,13 +1942,13 @@ async function validateTarget(docker, container) {
|
|
|
1909
1942
|
}
|
|
1910
1943
|
return toTarget(inspection);
|
|
1911
1944
|
}
|
|
1912
|
-
async function discoverSagContainer(
|
|
1913
|
-
await
|
|
1914
|
-
if (
|
|
1915
|
-
validateContainer(
|
|
1916
|
-
return validateTarget(
|
|
1945
|
+
async function discoverSagContainer(input2) {
|
|
1946
|
+
await input2.docker.version();
|
|
1947
|
+
if (input2.container) {
|
|
1948
|
+
validateContainer(input2.container);
|
|
1949
|
+
return validateTarget(input2.docker, input2.container);
|
|
1917
1950
|
}
|
|
1918
|
-
const containers = await
|
|
1951
|
+
const containers = await input2.docker.listRunningContainers();
|
|
1919
1952
|
const apiCandidates = containers.filter(
|
|
1920
1953
|
(container) => container.labels["com.docker.compose.service"] === "api"
|
|
1921
1954
|
);
|
|
@@ -1930,7 +1963,7 @@ async function discoverSagContainer(input) {
|
|
|
1930
1963
|
let preferredFailure;
|
|
1931
1964
|
for (const candidate of apiCandidates) {
|
|
1932
1965
|
try {
|
|
1933
|
-
targets.push(await validateTarget(
|
|
1966
|
+
targets.push(await validateTarget(input2.docker, candidate.id));
|
|
1934
1967
|
} catch (error) {
|
|
1935
1968
|
firstFailure ??= error;
|
|
1936
1969
|
if (candidate.labels["com.docker.compose.project"] === "sag") {
|
|
@@ -1946,13 +1979,13 @@ async function discoverSagContainer(input) {
|
|
|
1946
1979
|
if (rankedTargets.length === 1) {
|
|
1947
1980
|
return rankedTargets[0];
|
|
1948
1981
|
}
|
|
1949
|
-
if (!
|
|
1982
|
+
if (!input2.interactive || !input2.select) {
|
|
1950
1983
|
throw new CliError("CONFIG_CONFLICT", "Multiple SAG API containers were found", {
|
|
1951
1984
|
exitCode: exitCodes.configConflict,
|
|
1952
1985
|
hint: "Pass `--container <name-or-id>` to select one."
|
|
1953
1986
|
});
|
|
1954
1987
|
}
|
|
1955
|
-
const selectedId = await
|
|
1988
|
+
const selectedId = await input2.select(rankedTargets);
|
|
1956
1989
|
const selected = rankedTargets.find(
|
|
1957
1990
|
(target) => target.id === selectedId || target.name === selectedId
|
|
1958
1991
|
);
|
|
@@ -1965,34 +1998,34 @@ async function discoverSagContainer(input) {
|
|
|
1965
1998
|
}
|
|
1966
1999
|
|
|
1967
2000
|
// src/commands/agent.ts
|
|
1968
|
-
async function connectLocalAgent(runtime2,
|
|
2001
|
+
async function connectLocalAgent(runtime2, input2) {
|
|
1969
2002
|
return connectAgent({
|
|
1970
|
-
agent:
|
|
1971
|
-
serverName:
|
|
2003
|
+
agent: input2.agent,
|
|
2004
|
+
serverName: input2.serverName,
|
|
1972
2005
|
scope: "user",
|
|
1973
|
-
profile:
|
|
1974
|
-
...
|
|
1975
|
-
timeoutMs:
|
|
1976
|
-
dryRun:
|
|
1977
|
-
yes:
|
|
2006
|
+
profile: input2.profile,
|
|
2007
|
+
...input2.sourceId ? { sourceId: input2.sourceId } : {},
|
|
2008
|
+
timeoutMs: input2.timeoutMs,
|
|
2009
|
+
dryRun: input2.dryRun,
|
|
2010
|
+
yes: input2.yes,
|
|
1978
2011
|
resolveConnection: async () => {
|
|
1979
2012
|
const target = await discoverSagContainer({
|
|
1980
2013
|
docker: runtime2.docker,
|
|
1981
|
-
...
|
|
2014
|
+
...input2.container ? { container: input2.container } : {},
|
|
1982
2015
|
interactive: runtime2.interactive,
|
|
1983
2016
|
select: runtime2.selectContainer
|
|
1984
2017
|
});
|
|
1985
2018
|
const spec = await new DockerStdioProvider().createSpec({
|
|
1986
2019
|
target,
|
|
1987
|
-
...
|
|
2020
|
+
...input2.sourceId ? { sourceId: input2.sourceId } : {}
|
|
1988
2021
|
});
|
|
1989
2022
|
return { target, spec };
|
|
1990
2023
|
},
|
|
1991
2024
|
verifier: runtime2.verifier,
|
|
1992
|
-
adapter: runtime2.adapters[
|
|
2025
|
+
adapter: runtime2.adapters[input2.agent],
|
|
1993
2026
|
state: runtime2.state,
|
|
1994
|
-
confirm:
|
|
1995
|
-
now:
|
|
2027
|
+
confirm: input2.confirm,
|
|
2028
|
+
now: input2.now
|
|
1996
2029
|
});
|
|
1997
2030
|
}
|
|
1998
2031
|
async function agentStatus(runtime2, agent, serverName) {
|
|
@@ -2071,10 +2104,10 @@ async function agentStatus(runtime2, agent, serverName) {
|
|
|
2071
2104
|
};
|
|
2072
2105
|
}
|
|
2073
2106
|
}
|
|
2074
|
-
async function statusLocalAgents(runtime2,
|
|
2075
|
-
const agents =
|
|
2107
|
+
async function statusLocalAgents(runtime2, input2) {
|
|
2108
|
+
const agents = input2.agent ? [input2.agent] : ["codex", "claude-code"];
|
|
2076
2109
|
return Promise.all(
|
|
2077
|
-
agents.map((agent) => agentStatus(runtime2, agent,
|
|
2110
|
+
agents.map((agent) => agentStatus(runtime2, agent, input2.serverName))
|
|
2078
2111
|
);
|
|
2079
2112
|
}
|
|
2080
2113
|
async function listAgentHosts(runtime2) {
|
|
@@ -2095,58 +2128,69 @@ async function listAgentHosts(runtime2) {
|
|
|
2095
2128
|
})
|
|
2096
2129
|
);
|
|
2097
2130
|
}
|
|
2098
|
-
async function disconnectLocalAgent(runtime2,
|
|
2131
|
+
async function disconnectLocalAgent(runtime2, input2) {
|
|
2099
2132
|
return disconnectAgent({
|
|
2100
|
-
agent:
|
|
2101
|
-
serverName:
|
|
2133
|
+
agent: input2.agent,
|
|
2134
|
+
serverName: input2.serverName,
|
|
2102
2135
|
scope: "user",
|
|
2103
|
-
adapter: runtime2.adapters[
|
|
2136
|
+
adapter: runtime2.adapters[input2.agent],
|
|
2104
2137
|
state: runtime2.state,
|
|
2105
|
-
yes:
|
|
2106
|
-
confirm:
|
|
2138
|
+
yes: input2.yes,
|
|
2139
|
+
confirm: input2.confirm
|
|
2107
2140
|
});
|
|
2108
2141
|
}
|
|
2109
2142
|
|
|
2110
2143
|
// src/commands/auth.ts
|
|
2111
|
-
async function login(
|
|
2112
|
-
const
|
|
2113
|
-
if (
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2144
|
+
async function login(input2) {
|
|
2145
|
+
const environmentToken = input2.environmentToken?.trim();
|
|
2146
|
+
if (environmentToken) {
|
|
2147
|
+
const user2 = await input2.validate(environmentToken);
|
|
2148
|
+
await input2.store.set(input2.credentialRef, environmentToken);
|
|
2149
|
+
return {
|
|
2150
|
+
authenticated: true,
|
|
2151
|
+
credentialRef: input2.credentialRef,
|
|
2152
|
+
credentialStore: input2.store.kind,
|
|
2153
|
+
user: user2
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
const name = (input2.name ?? await input2.promptName()).trim();
|
|
2157
|
+
if (!name) {
|
|
2158
|
+
throw new CliError("INVALID_ARGUMENT", "A SAG user name is required", {
|
|
2159
|
+
exitCode: exitCodes.invalidArgument,
|
|
2160
|
+
hint: "Pass `--name <name>` or enter a name when prompted."
|
|
2117
2161
|
});
|
|
2118
2162
|
}
|
|
2119
|
-
const user = await
|
|
2120
|
-
await
|
|
2163
|
+
const { accessToken, user } = await input2.authenticate(name);
|
|
2164
|
+
await input2.store.set(input2.credentialRef, accessToken);
|
|
2121
2165
|
return {
|
|
2122
2166
|
authenticated: true,
|
|
2123
|
-
credentialRef:
|
|
2124
|
-
credentialStore:
|
|
2167
|
+
credentialRef: input2.credentialRef,
|
|
2168
|
+
credentialStore: input2.store.kind,
|
|
2125
2169
|
user
|
|
2126
2170
|
};
|
|
2127
2171
|
}
|
|
2128
|
-
async function status(
|
|
2129
|
-
const token =
|
|
2172
|
+
async function status(input2) {
|
|
2173
|
+
const token = input2.environmentToken?.trim() || await input2.store.get(input2.credentialRef);
|
|
2130
2174
|
if (!token) {
|
|
2131
2175
|
return {
|
|
2132
2176
|
authenticated: false,
|
|
2133
|
-
credentialRef:
|
|
2134
|
-
credentialStore:
|
|
2177
|
+
credentialRef: input2.credentialRef,
|
|
2178
|
+
credentialStore: input2.store.kind
|
|
2135
2179
|
};
|
|
2136
2180
|
}
|
|
2137
|
-
const user = await
|
|
2181
|
+
const user = await input2.validate(token);
|
|
2138
2182
|
return {
|
|
2139
2183
|
authenticated: true,
|
|
2140
|
-
credentialRef:
|
|
2141
|
-
credentialStore:
|
|
2184
|
+
credentialRef: input2.credentialRef,
|
|
2185
|
+
credentialStore: input2.store.kind,
|
|
2142
2186
|
user
|
|
2143
2187
|
};
|
|
2144
2188
|
}
|
|
2145
|
-
async function logout(
|
|
2146
|
-
await
|
|
2189
|
+
async function logout(input2) {
|
|
2190
|
+
await input2.store.delete(input2.credentialRef);
|
|
2147
2191
|
return {
|
|
2148
2192
|
authenticated: false,
|
|
2149
|
-
credentialRef:
|
|
2193
|
+
credentialRef: input2.credentialRef
|
|
2150
2194
|
};
|
|
2151
2195
|
}
|
|
2152
2196
|
|
|
@@ -2246,24 +2290,24 @@ async function getEntityContext(client, sourceId, name) {
|
|
|
2246
2290
|
}
|
|
2247
2291
|
|
|
2248
2292
|
// src/commands/mcp.ts
|
|
2249
|
-
async function testLocalMcp(runtime2,
|
|
2293
|
+
async function testLocalMcp(runtime2, input2) {
|
|
2250
2294
|
const container = await discoverSagContainer({
|
|
2251
2295
|
docker: runtime2.docker,
|
|
2252
|
-
...
|
|
2296
|
+
...input2.container ? { container: input2.container } : {},
|
|
2253
2297
|
interactive: runtime2.interactive,
|
|
2254
2298
|
select: runtime2.selectContainer
|
|
2255
2299
|
});
|
|
2256
2300
|
const spec = await new DockerStdioProvider().createSpec({
|
|
2257
2301
|
target: container,
|
|
2258
|
-
...
|
|
2302
|
+
...input2.sourceId ? { sourceId: input2.sourceId } : {}
|
|
2259
2303
|
});
|
|
2260
2304
|
const verification = await runtime2.verifier.verify(spec, {
|
|
2261
|
-
timeoutMs:
|
|
2305
|
+
timeoutMs: input2.timeoutMs
|
|
2262
2306
|
});
|
|
2263
2307
|
return {
|
|
2264
2308
|
provider: "docker-stdio",
|
|
2265
2309
|
container,
|
|
2266
|
-
sourceId:
|
|
2310
|
+
sourceId: input2.sourceId ?? null,
|
|
2267
2311
|
...verification
|
|
2268
2312
|
};
|
|
2269
2313
|
}
|
|
@@ -2275,9 +2319,9 @@ function skipped(name, detail) {
|
|
|
2275
2319
|
detail
|
|
2276
2320
|
};
|
|
2277
2321
|
}
|
|
2278
|
-
async function localMcpDoctorChecks(runtime2,
|
|
2322
|
+
async function localMcpDoctorChecks(runtime2, input2) {
|
|
2279
2323
|
try {
|
|
2280
|
-
const report = await testLocalMcp(runtime2,
|
|
2324
|
+
const report = await testLocalMcp(runtime2, input2);
|
|
2281
2325
|
return [
|
|
2282
2326
|
{
|
|
2283
2327
|
name: "docker",
|
|
@@ -2474,16 +2518,16 @@ function directCredentialRef(url) {
|
|
|
2474
2518
|
const fingerprint = createHash2("sha256").update(url).digest("hex").slice(0, 16);
|
|
2475
2519
|
return `sag-cli/direct-${fingerprint}`;
|
|
2476
2520
|
}
|
|
2477
|
-
async function createRuntimeContext(
|
|
2478
|
-
const config = await
|
|
2479
|
-
const connection = resolveConnection(
|
|
2521
|
+
async function createRuntimeContext(input2) {
|
|
2522
|
+
const config = await input2.configStore.load();
|
|
2523
|
+
const connection = resolveConnection(input2.options, input2.environment, config);
|
|
2480
2524
|
const credentialRef = connection.credentialRef ?? directCredentialRef(connection.url);
|
|
2481
|
-
const token = connection.environmentToken ?? await
|
|
2525
|
+
const token = connection.environmentToken ?? await input2.credentialStore.get(credentialRef) ?? void 0;
|
|
2482
2526
|
return {
|
|
2483
2527
|
connection,
|
|
2484
2528
|
credentialRef,
|
|
2485
2529
|
...token ? { token } : {},
|
|
2486
|
-
client:
|
|
2530
|
+
client: input2.createClient({
|
|
2487
2531
|
origin: connection.url,
|
|
2488
2532
|
...token ? { token } : {}
|
|
2489
2533
|
})
|
|
@@ -2587,7 +2631,7 @@ function documentDetail(total, statuses) {
|
|
|
2587
2631
|
const parts = displayOrder.filter((status2) => statuses[status2] > 0).map((status2) => `${statuses[status2]} ${status2}`);
|
|
2588
2632
|
return `${total} ${total === 1 ? "document" : "documents"}${parts.length ? `: ${parts.join(", ")}` : ""}`;
|
|
2589
2633
|
}
|
|
2590
|
-
async function runDoctor(client,
|
|
2634
|
+
async function runDoctor(client, input2, localMcpChecks) {
|
|
2591
2635
|
const checks = [];
|
|
2592
2636
|
let sagVersion;
|
|
2593
2637
|
let authenticated = false;
|
|
@@ -2702,8 +2746,8 @@ async function runDoctor(client, input, localMcpChecks) {
|
|
|
2702
2746
|
}
|
|
2703
2747
|
return {
|
|
2704
2748
|
status: overallStatus(checks),
|
|
2705
|
-
url:
|
|
2706
|
-
...
|
|
2749
|
+
url: input2.url,
|
|
2750
|
+
...input2.profileName ? { profileName: input2.profileName } : {},
|
|
2707
2751
|
...sagVersion ? { sagVersion } : {},
|
|
2708
2752
|
checks,
|
|
2709
2753
|
summary: {
|
|
@@ -3010,12 +3054,14 @@ function addProfileCommands(program, dependencies2) {
|
|
|
3010
3054
|
}
|
|
3011
3055
|
function addAuthCommands(program, dependencies2) {
|
|
3012
3056
|
const auth = program.command("auth").description("Manage SAG credentials");
|
|
3013
|
-
auth.command("login").action(async () => {
|
|
3057
|
+
auth.command("login").option("--name <name>", "SAG user name for the existing local login").action(async (options) => {
|
|
3014
3058
|
const context = await runtime(program, dependencies2);
|
|
3015
3059
|
const result = await login({
|
|
3016
3060
|
credentialRef: context.credentialRef,
|
|
3017
3061
|
...context.connection.environmentToken ? { environmentToken: context.connection.environmentToken } : {},
|
|
3018
|
-
|
|
3062
|
+
...options.name ? { name: options.name } : {},
|
|
3063
|
+
promptName: dependencies2.promptName,
|
|
3064
|
+
authenticate: (name) => dependencies2.createClient({ origin: context.connection.url }).login(name),
|
|
3019
3065
|
validate: async (token) => dependencies2.createClient({ origin: context.connection.url, token }).me(),
|
|
3020
3066
|
store: dependencies2.credentialStore
|
|
3021
3067
|
});
|
|
@@ -3284,10 +3330,7 @@ var dependencies = {
|
|
|
3284
3330
|
configStore: new ConfigStore(defaultConfigPath()),
|
|
3285
3331
|
credentialStore: credentialSelection.store,
|
|
3286
3332
|
createClient: defaultClientFactory,
|
|
3287
|
-
|
|
3288
|
-
message: "SAG Token",
|
|
3289
|
-
mask: "*"
|
|
3290
|
-
}),
|
|
3333
|
+
promptName: () => input({ message: "SAG user name" }),
|
|
3291
3334
|
confirm: (message) => confirm({ message, default: false }),
|
|
3292
3335
|
writeStdout: (value) => process.stdout.write(value),
|
|
3293
3336
|
writeStderr: (value) => process.stderr.write(value),
|