@zleap-ai/sag-cli 0.3.0 → 0.4.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/README.en.md +5 -5
- package/README.md +5 -6
- package/dist/cli.js +232 -191
- 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.0",
|
|
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(),
|
|
@@ -1423,6 +1429,14 @@ var SagClient = class {
|
|
|
1423
1429
|
me() {
|
|
1424
1430
|
return this.#request("/api/v1/auth/me", userSchema);
|
|
1425
1431
|
}
|
|
1432
|
+
async login(name) {
|
|
1433
|
+
const response = await this.#request("/api/v1/auth/login", loginResponseSchema, {
|
|
1434
|
+
authenticated: false,
|
|
1435
|
+
method: "POST",
|
|
1436
|
+
body: { name }
|
|
1437
|
+
});
|
|
1438
|
+
return { accessToken: response.access_token, user: response.user };
|
|
1439
|
+
}
|
|
1426
1440
|
mcpDescriptor() {
|
|
1427
1441
|
return this.#request("/api/v1/system/mcp", mcpDescriptorSchema);
|
|
1428
1442
|
}
|
|
@@ -1447,10 +1461,10 @@ var SagClient = class {
|
|
|
1447
1461
|
documentSchema
|
|
1448
1462
|
);
|
|
1449
1463
|
}
|
|
1450
|
-
search(
|
|
1464
|
+
search(input2) {
|
|
1451
1465
|
return this.#request("/api/v1/search", searchResponseSchema, {
|
|
1452
1466
|
method: "POST",
|
|
1453
|
-
body:
|
|
1467
|
+
body: input2
|
|
1454
1468
|
});
|
|
1455
1469
|
}
|
|
1456
1470
|
outline(sourceId, documentId) {
|
|
@@ -1603,81 +1617,98 @@ function hostFailure3(message, cause, hint) {
|
|
|
1603
1617
|
hint: hint ?? "Inspect the Agent MCP configuration and run the command again."
|
|
1604
1618
|
});
|
|
1605
1619
|
}
|
|
1606
|
-
function determinePlan(
|
|
1620
|
+
function determinePlan(input2) {
|
|
1607
1621
|
let action;
|
|
1608
|
-
if (!
|
|
1609
|
-
if (
|
|
1622
|
+
if (!input2.hostFingerprint || !input2.hostSpecFingerprint) {
|
|
1623
|
+
if (input2.managed) {
|
|
1610
1624
|
throw conflict("Managed state exists but the Agent MCP entry is missing");
|
|
1611
1625
|
}
|
|
1612
1626
|
action = "create";
|
|
1613
|
-
} else if (!
|
|
1627
|
+
} else if (!input2.managed) {
|
|
1614
1628
|
throw conflict("The Agent MCP name is already owned by the user");
|
|
1615
|
-
} else if (
|
|
1629
|
+
} else if (input2.managed.fingerprint !== input2.hostFingerprint) {
|
|
1616
1630
|
throw conflict("The managed Agent MCP entry was changed outside SAG CLI");
|
|
1617
|
-
} else if (
|
|
1631
|
+
} else if (input2.hostSpecFingerprint === input2.desiredFingerprint) {
|
|
1618
1632
|
action = "unchanged";
|
|
1619
1633
|
} else {
|
|
1620
1634
|
action = "update";
|
|
1621
1635
|
}
|
|
1622
1636
|
return {
|
|
1623
1637
|
action,
|
|
1624
|
-
agent:
|
|
1625
|
-
serverName:
|
|
1626
|
-
scope:
|
|
1638
|
+
agent: input2.agent,
|
|
1639
|
+
serverName: input2.serverName,
|
|
1640
|
+
scope: input2.scope,
|
|
1627
1641
|
provider: "docker-stdio",
|
|
1628
|
-
target:
|
|
1629
|
-
sourceId:
|
|
1630
|
-
...
|
|
1631
|
-
desiredFingerprint:
|
|
1642
|
+
target: input2.target.name,
|
|
1643
|
+
sourceId: input2.sourceId ?? null,
|
|
1644
|
+
...input2.hostSpecFingerprint ? { currentFingerprint: input2.hostSpecFingerprint } : {},
|
|
1645
|
+
desiredFingerprint: input2.desiredFingerprint
|
|
1632
1646
|
};
|
|
1633
1647
|
}
|
|
1634
|
-
function managedConnection(
|
|
1648
|
+
function managedConnection(input2, resolved, fingerprint, existing) {
|
|
1635
1649
|
return {
|
|
1636
|
-
agent:
|
|
1650
|
+
agent: input2.agent,
|
|
1637
1651
|
provider: "docker-stdio",
|
|
1638
|
-
profile:
|
|
1639
|
-
serverName:
|
|
1652
|
+
profile: input2.profile,
|
|
1653
|
+
serverName: input2.serverName,
|
|
1640
1654
|
docker: {
|
|
1641
1655
|
...resolved.target.composeProject ? { composeProject: resolved.target.composeProject } : {},
|
|
1642
1656
|
...resolved.target.composeService ? { composeService: resolved.target.composeService } : {},
|
|
1643
1657
|
containerName: resolved.target.name
|
|
1644
1658
|
},
|
|
1645
|
-
sourceId:
|
|
1646
|
-
scope:
|
|
1659
|
+
sourceId: input2.sourceId ?? null,
|
|
1660
|
+
scope: input2.scope,
|
|
1647
1661
|
fingerprint,
|
|
1648
|
-
createdAt: existing?.createdAt ??
|
|
1662
|
+
createdAt: existing?.createdAt ?? input2.now().toISOString()
|
|
1649
1663
|
};
|
|
1650
1664
|
}
|
|
1651
|
-
async function rollbackHost(
|
|
1652
|
-
if (
|
|
1665
|
+
async function rollbackHost(input2) {
|
|
1666
|
+
if (input2.previousSpec) {
|
|
1653
1667
|
try {
|
|
1654
|
-
await
|
|
1668
|
+
await input2.adapter.remove(input2.serverName, input2.scope);
|
|
1655
1669
|
} catch {
|
|
1656
1670
|
}
|
|
1657
|
-
await
|
|
1658
|
-
const restored = await
|
|
1659
|
-
if (!restored || restored.fingerprint !== (
|
|
1671
|
+
await input2.adapter.add(input2.serverName, input2.scope, input2.previousSpec);
|
|
1672
|
+
const restored = await input2.adapter.read(input2.serverName, input2.scope);
|
|
1673
|
+
if (!restored || restored.fingerprint !== (input2.previousFingerprint ?? fingerprintConnection(input2.previousSpec))) {
|
|
1660
1674
|
throw new Error("Previous Agent MCP entry was not restored");
|
|
1661
1675
|
}
|
|
1662
1676
|
return;
|
|
1663
1677
|
}
|
|
1664
|
-
await
|
|
1678
|
+
await input2.adapter.remove(input2.serverName, input2.scope);
|
|
1665
1679
|
}
|
|
1666
|
-
async function connectAgent(
|
|
1667
|
-
validateServerName(
|
|
1668
|
-
const resolved = await
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1680
|
+
async function connectAgent(input2) {
|
|
1681
|
+
validateServerName(input2.serverName);
|
|
1682
|
+
const resolved = await input2.resolveConnection();
|
|
1683
|
+
if (!input2.adapter.capabilities.transports.includes(resolved.spec.transport)) {
|
|
1684
|
+
throw new CliError(
|
|
1685
|
+
"INVALID_ARGUMENT",
|
|
1686
|
+
`${input2.agent} adapter does not support ${resolved.spec.transport} MCP connections`,
|
|
1687
|
+
{
|
|
1688
|
+
exitCode: exitCodes.invalidArgument,
|
|
1689
|
+
hint: "Choose a provider supported by the installed Agent adapter."
|
|
1690
|
+
}
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
if (!input2.adapter.capabilities.scopes.includes(input2.scope)) {
|
|
1694
|
+
throw new CliError(
|
|
1695
|
+
"INVALID_ARGUMENT",
|
|
1696
|
+
`${input2.agent} adapter does not support ${input2.scope} scope`,
|
|
1697
|
+
{ exitCode: exitCodes.invalidArgument }
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
await input2.verifier.verify(resolved.spec, { timeoutMs: input2.timeoutMs });
|
|
1701
|
+
await input2.adapter.detect();
|
|
1702
|
+
const host = await input2.adapter.read(input2.serverName, input2.scope);
|
|
1703
|
+
const key = connectionKey(input2.agent, input2.scope, input2.serverName);
|
|
1704
|
+
const managed = await input2.state.get(key);
|
|
1674
1705
|
const desiredFingerprint = fingerprintConnection(resolved.spec);
|
|
1675
1706
|
const plan = determinePlan({
|
|
1676
|
-
agent:
|
|
1677
|
-
serverName:
|
|
1678
|
-
scope:
|
|
1707
|
+
agent: input2.agent,
|
|
1708
|
+
serverName: input2.serverName,
|
|
1709
|
+
scope: input2.scope,
|
|
1679
1710
|
target: resolved.target,
|
|
1680
|
-
...
|
|
1711
|
+
...input2.sourceId ? { sourceId: input2.sourceId } : {},
|
|
1681
1712
|
desiredFingerprint,
|
|
1682
1713
|
...host ? { hostFingerprint: host.fingerprint } : {},
|
|
1683
1714
|
...host ? { hostSpecFingerprint: fingerprintConnection(host.spec) } : {},
|
|
@@ -1685,9 +1716,9 @@ async function connectAgent(input) {
|
|
|
1685
1716
|
});
|
|
1686
1717
|
if (plan.action === "unchanged") {
|
|
1687
1718
|
return {
|
|
1688
|
-
agent:
|
|
1689
|
-
serverName:
|
|
1690
|
-
scope:
|
|
1719
|
+
agent: input2.agent,
|
|
1720
|
+
serverName: input2.serverName,
|
|
1721
|
+
scope: input2.scope,
|
|
1691
1722
|
action: "unchanged",
|
|
1692
1723
|
changed: false,
|
|
1693
1724
|
verified: true,
|
|
@@ -1695,11 +1726,11 @@ async function connectAgent(input) {
|
|
|
1695
1726
|
plan
|
|
1696
1727
|
};
|
|
1697
1728
|
}
|
|
1698
|
-
if (
|
|
1729
|
+
if (input2.dryRun) {
|
|
1699
1730
|
return {
|
|
1700
|
-
agent:
|
|
1701
|
-
serverName:
|
|
1702
|
-
scope:
|
|
1731
|
+
agent: input2.agent,
|
|
1732
|
+
serverName: input2.serverName,
|
|
1733
|
+
scope: input2.scope,
|
|
1703
1734
|
action: plan.action,
|
|
1704
1735
|
changed: false,
|
|
1705
1736
|
verified: true,
|
|
@@ -1707,13 +1738,13 @@ async function connectAgent(input) {
|
|
|
1707
1738
|
plan
|
|
1708
1739
|
};
|
|
1709
1740
|
}
|
|
1710
|
-
if (!
|
|
1711
|
-
`${plan.action === "create" ? "Add" : "Update"} ${
|
|
1741
|
+
if (!input2.yes && !await input2.confirm(
|
|
1742
|
+
`${plan.action === "create" ? "Add" : "Update"} ${input2.serverName} in ${input2.agent}?`
|
|
1712
1743
|
)) {
|
|
1713
1744
|
return {
|
|
1714
|
-
agent:
|
|
1715
|
-
serverName:
|
|
1716
|
-
scope:
|
|
1745
|
+
agent: input2.agent,
|
|
1746
|
+
serverName: input2.serverName,
|
|
1747
|
+
scope: input2.scope,
|
|
1717
1748
|
action: "cancelled",
|
|
1718
1749
|
changed: false,
|
|
1719
1750
|
verified: true,
|
|
@@ -1723,23 +1754,23 @@ async function connectAgent(input) {
|
|
|
1723
1754
|
}
|
|
1724
1755
|
try {
|
|
1725
1756
|
if (plan.action === "update") {
|
|
1726
|
-
await
|
|
1757
|
+
await input2.adapter.remove(input2.serverName, input2.scope);
|
|
1727
1758
|
}
|
|
1728
|
-
await
|
|
1729
|
-
const readBack = await
|
|
1759
|
+
await input2.adapter.add(input2.serverName, input2.scope, resolved.spec);
|
|
1760
|
+
const readBack = await input2.adapter.read(input2.serverName, input2.scope);
|
|
1730
1761
|
if (!readBack || fingerprintConnection(readBack.spec) !== desiredFingerprint) {
|
|
1731
1762
|
throw hostFailure3("Agent MCP read-back did not match the requested connection");
|
|
1732
1763
|
}
|
|
1733
|
-
await
|
|
1734
|
-
await
|
|
1735
|
-
managedConnection(
|
|
1764
|
+
await input2.verifier.verify(readBack.spec, { timeoutMs: input2.timeoutMs });
|
|
1765
|
+
await input2.state.set(
|
|
1766
|
+
managedConnection(input2, resolved, readBack.fingerprint, managed)
|
|
1736
1767
|
);
|
|
1737
1768
|
} catch (cause) {
|
|
1738
1769
|
try {
|
|
1739
1770
|
await rollbackHost({
|
|
1740
|
-
adapter:
|
|
1741
|
-
serverName:
|
|
1742
|
-
scope:
|
|
1771
|
+
adapter: input2.adapter,
|
|
1772
|
+
serverName: input2.serverName,
|
|
1773
|
+
scope: input2.scope,
|
|
1743
1774
|
...host ? { previousSpec: host.spec } : {},
|
|
1744
1775
|
...host ? { previousFingerprint: host.fingerprint } : {}
|
|
1745
1776
|
});
|
|
@@ -1747,7 +1778,7 @@ async function connectAgent(input) {
|
|
|
1747
1778
|
throw hostFailure3(
|
|
1748
1779
|
"Agent MCP configuration failed and rollback also failed",
|
|
1749
1780
|
cause,
|
|
1750
|
-
`Inspect the ${
|
|
1781
|
+
`Inspect the ${input2.agent} MCP entry \`${input2.serverName}\` before retrying.`
|
|
1751
1782
|
);
|
|
1752
1783
|
}
|
|
1753
1784
|
throw hostFailure3(
|
|
@@ -1756,9 +1787,9 @@ async function connectAgent(input) {
|
|
|
1756
1787
|
);
|
|
1757
1788
|
}
|
|
1758
1789
|
return {
|
|
1759
|
-
agent:
|
|
1760
|
-
serverName:
|
|
1761
|
-
scope:
|
|
1790
|
+
agent: input2.agent,
|
|
1791
|
+
serverName: input2.serverName,
|
|
1792
|
+
scope: input2.scope,
|
|
1762
1793
|
action: plan.action,
|
|
1763
1794
|
changed: true,
|
|
1764
1795
|
verified: true,
|
|
@@ -1766,15 +1797,15 @@ async function connectAgent(input) {
|
|
|
1766
1797
|
plan
|
|
1767
1798
|
};
|
|
1768
1799
|
}
|
|
1769
|
-
async function disconnectAgent(
|
|
1770
|
-
validateServerName(
|
|
1771
|
-
await
|
|
1772
|
-
const key = connectionKey(
|
|
1773
|
-
const managed = await
|
|
1800
|
+
async function disconnectAgent(input2) {
|
|
1801
|
+
validateServerName(input2.serverName);
|
|
1802
|
+
await input2.adapter.detect();
|
|
1803
|
+
const key = connectionKey(input2.agent, input2.scope, input2.serverName);
|
|
1804
|
+
const managed = await input2.state.get(key);
|
|
1774
1805
|
if (!managed) {
|
|
1775
1806
|
throw conflict("The Agent MCP entry is not managed by SAG CLI");
|
|
1776
1807
|
}
|
|
1777
|
-
const host = await
|
|
1808
|
+
const host = await input2.adapter.read(input2.serverName, input2.scope);
|
|
1778
1809
|
if (!host) {
|
|
1779
1810
|
throw conflict("Managed state exists but the Agent MCP entry is missing");
|
|
1780
1811
|
}
|
|
@@ -1783,20 +1814,20 @@ async function disconnectAgent(input) {
|
|
|
1783
1814
|
}
|
|
1784
1815
|
const plan = {
|
|
1785
1816
|
action: "unchanged",
|
|
1786
|
-
agent:
|
|
1787
|
-
serverName:
|
|
1788
|
-
scope:
|
|
1817
|
+
agent: input2.agent,
|
|
1818
|
+
serverName: input2.serverName,
|
|
1819
|
+
scope: input2.scope,
|
|
1789
1820
|
provider: "docker-stdio",
|
|
1790
1821
|
target: managed.docker.containerName,
|
|
1791
1822
|
sourceId: managed.sourceId,
|
|
1792
1823
|
currentFingerprint: host.fingerprint,
|
|
1793
1824
|
desiredFingerprint: host.fingerprint
|
|
1794
1825
|
};
|
|
1795
|
-
if (!
|
|
1826
|
+
if (!input2.yes && !await input2.confirm(`Remove ${input2.serverName} from ${input2.agent}?`)) {
|
|
1796
1827
|
return {
|
|
1797
|
-
agent:
|
|
1798
|
-
serverName:
|
|
1799
|
-
scope:
|
|
1828
|
+
agent: input2.agent,
|
|
1829
|
+
serverName: input2.serverName,
|
|
1830
|
+
scope: input2.scope,
|
|
1800
1831
|
action: "cancelled",
|
|
1801
1832
|
changed: false,
|
|
1802
1833
|
verified: true,
|
|
@@ -1804,12 +1835,12 @@ async function disconnectAgent(input) {
|
|
|
1804
1835
|
plan
|
|
1805
1836
|
};
|
|
1806
1837
|
}
|
|
1807
|
-
await
|
|
1838
|
+
await input2.adapter.remove(input2.serverName, input2.scope);
|
|
1808
1839
|
try {
|
|
1809
|
-
await
|
|
1840
|
+
await input2.state.delete(key);
|
|
1810
1841
|
} catch (cause) {
|
|
1811
1842
|
try {
|
|
1812
|
-
await
|
|
1843
|
+
await input2.adapter.add(input2.serverName, input2.scope, host.spec);
|
|
1813
1844
|
} catch {
|
|
1814
1845
|
throw hostFailure3(
|
|
1815
1846
|
"Managed state cleanup failed and the Agent entry could not be restored",
|
|
@@ -1822,9 +1853,9 @@ async function disconnectAgent(input) {
|
|
|
1822
1853
|
);
|
|
1823
1854
|
}
|
|
1824
1855
|
return {
|
|
1825
|
-
agent:
|
|
1826
|
-
serverName:
|
|
1827
|
-
scope:
|
|
1856
|
+
agent: input2.agent,
|
|
1857
|
+
serverName: input2.serverName,
|
|
1858
|
+
scope: input2.scope,
|
|
1828
1859
|
action: "disconnect",
|
|
1829
1860
|
changed: true,
|
|
1830
1861
|
verified: true,
|
|
@@ -1837,21 +1868,21 @@ async function disconnectAgent(input) {
|
|
|
1837
1868
|
var SOURCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/u;
|
|
1838
1869
|
var DockerStdioProvider = class {
|
|
1839
1870
|
id = "docker-stdio";
|
|
1840
|
-
async inspect(
|
|
1871
|
+
async inspect(input2) {
|
|
1841
1872
|
return {
|
|
1842
1873
|
provider: this.id,
|
|
1843
1874
|
ready: true,
|
|
1844
|
-
target:
|
|
1875
|
+
target: input2.target.name
|
|
1845
1876
|
};
|
|
1846
1877
|
}
|
|
1847
|
-
async createSpec(
|
|
1848
|
-
if (
|
|
1878
|
+
async createSpec(input2) {
|
|
1879
|
+
if (input2.sourceId && !SOURCE_ID_PATTERN.test(input2.sourceId)) {
|
|
1849
1880
|
throw new CliError("INVALID_ARGUMENT", "Invalid SAG source id", {
|
|
1850
1881
|
exitCode: exitCodes.invalidArgument,
|
|
1851
1882
|
hint: "Use 1-128 letters, numbers, underscores, or dashes."
|
|
1852
1883
|
});
|
|
1853
1884
|
}
|
|
1854
|
-
const sourceArguments =
|
|
1885
|
+
const sourceArguments = input2.sourceId ? ["-e", `SAG_MCP_SOURCE_ID=${input2.sourceId}`] : [];
|
|
1855
1886
|
return {
|
|
1856
1887
|
transport: "stdio",
|
|
1857
1888
|
command: "docker",
|
|
@@ -1859,7 +1890,7 @@ var DockerStdioProvider = class {
|
|
|
1859
1890
|
"exec",
|
|
1860
1891
|
"-i",
|
|
1861
1892
|
...sourceArguments,
|
|
1862
|
-
|
|
1893
|
+
input2.target.name,
|
|
1863
1894
|
"python",
|
|
1864
1895
|
"-m",
|
|
1865
1896
|
"sag_api.mcp.server"
|
|
@@ -1909,13 +1940,13 @@ async function validateTarget(docker, container) {
|
|
|
1909
1940
|
}
|
|
1910
1941
|
return toTarget(inspection);
|
|
1911
1942
|
}
|
|
1912
|
-
async function discoverSagContainer(
|
|
1913
|
-
await
|
|
1914
|
-
if (
|
|
1915
|
-
validateContainer(
|
|
1916
|
-
return validateTarget(
|
|
1943
|
+
async function discoverSagContainer(input2) {
|
|
1944
|
+
await input2.docker.version();
|
|
1945
|
+
if (input2.container) {
|
|
1946
|
+
validateContainer(input2.container);
|
|
1947
|
+
return validateTarget(input2.docker, input2.container);
|
|
1917
1948
|
}
|
|
1918
|
-
const containers = await
|
|
1949
|
+
const containers = await input2.docker.listRunningContainers();
|
|
1919
1950
|
const apiCandidates = containers.filter(
|
|
1920
1951
|
(container) => container.labels["com.docker.compose.service"] === "api"
|
|
1921
1952
|
);
|
|
@@ -1930,7 +1961,7 @@ async function discoverSagContainer(input) {
|
|
|
1930
1961
|
let preferredFailure;
|
|
1931
1962
|
for (const candidate of apiCandidates) {
|
|
1932
1963
|
try {
|
|
1933
|
-
targets.push(await validateTarget(
|
|
1964
|
+
targets.push(await validateTarget(input2.docker, candidate.id));
|
|
1934
1965
|
} catch (error) {
|
|
1935
1966
|
firstFailure ??= error;
|
|
1936
1967
|
if (candidate.labels["com.docker.compose.project"] === "sag") {
|
|
@@ -1946,13 +1977,13 @@ async function discoverSagContainer(input) {
|
|
|
1946
1977
|
if (rankedTargets.length === 1) {
|
|
1947
1978
|
return rankedTargets[0];
|
|
1948
1979
|
}
|
|
1949
|
-
if (!
|
|
1980
|
+
if (!input2.interactive || !input2.select) {
|
|
1950
1981
|
throw new CliError("CONFIG_CONFLICT", "Multiple SAG API containers were found", {
|
|
1951
1982
|
exitCode: exitCodes.configConflict,
|
|
1952
1983
|
hint: "Pass `--container <name-or-id>` to select one."
|
|
1953
1984
|
});
|
|
1954
1985
|
}
|
|
1955
|
-
const selectedId = await
|
|
1986
|
+
const selectedId = await input2.select(rankedTargets);
|
|
1956
1987
|
const selected = rankedTargets.find(
|
|
1957
1988
|
(target) => target.id === selectedId || target.name === selectedId
|
|
1958
1989
|
);
|
|
@@ -1965,34 +1996,34 @@ async function discoverSagContainer(input) {
|
|
|
1965
1996
|
}
|
|
1966
1997
|
|
|
1967
1998
|
// src/commands/agent.ts
|
|
1968
|
-
async function connectLocalAgent(runtime2,
|
|
1999
|
+
async function connectLocalAgent(runtime2, input2) {
|
|
1969
2000
|
return connectAgent({
|
|
1970
|
-
agent:
|
|
1971
|
-
serverName:
|
|
2001
|
+
agent: input2.agent,
|
|
2002
|
+
serverName: input2.serverName,
|
|
1972
2003
|
scope: "user",
|
|
1973
|
-
profile:
|
|
1974
|
-
...
|
|
1975
|
-
timeoutMs:
|
|
1976
|
-
dryRun:
|
|
1977
|
-
yes:
|
|
2004
|
+
profile: input2.profile,
|
|
2005
|
+
...input2.sourceId ? { sourceId: input2.sourceId } : {},
|
|
2006
|
+
timeoutMs: input2.timeoutMs,
|
|
2007
|
+
dryRun: input2.dryRun,
|
|
2008
|
+
yes: input2.yes,
|
|
1978
2009
|
resolveConnection: async () => {
|
|
1979
2010
|
const target = await discoverSagContainer({
|
|
1980
2011
|
docker: runtime2.docker,
|
|
1981
|
-
...
|
|
2012
|
+
...input2.container ? { container: input2.container } : {},
|
|
1982
2013
|
interactive: runtime2.interactive,
|
|
1983
2014
|
select: runtime2.selectContainer
|
|
1984
2015
|
});
|
|
1985
2016
|
const spec = await new DockerStdioProvider().createSpec({
|
|
1986
2017
|
target,
|
|
1987
|
-
...
|
|
2018
|
+
...input2.sourceId ? { sourceId: input2.sourceId } : {}
|
|
1988
2019
|
});
|
|
1989
2020
|
return { target, spec };
|
|
1990
2021
|
},
|
|
1991
2022
|
verifier: runtime2.verifier,
|
|
1992
|
-
adapter: runtime2.adapters[
|
|
2023
|
+
adapter: runtime2.adapters[input2.agent],
|
|
1993
2024
|
state: runtime2.state,
|
|
1994
|
-
confirm:
|
|
1995
|
-
now:
|
|
2025
|
+
confirm: input2.confirm,
|
|
2026
|
+
now: input2.now
|
|
1996
2027
|
});
|
|
1997
2028
|
}
|
|
1998
2029
|
async function agentStatus(runtime2, agent, serverName) {
|
|
@@ -2071,10 +2102,10 @@ async function agentStatus(runtime2, agent, serverName) {
|
|
|
2071
2102
|
};
|
|
2072
2103
|
}
|
|
2073
2104
|
}
|
|
2074
|
-
async function statusLocalAgents(runtime2,
|
|
2075
|
-
const agents =
|
|
2105
|
+
async function statusLocalAgents(runtime2, input2) {
|
|
2106
|
+
const agents = input2.agent ? [input2.agent] : ["codex", "claude-code"];
|
|
2076
2107
|
return Promise.all(
|
|
2077
|
-
agents.map((agent) => agentStatus(runtime2, agent,
|
|
2108
|
+
agents.map((agent) => agentStatus(runtime2, agent, input2.serverName))
|
|
2078
2109
|
);
|
|
2079
2110
|
}
|
|
2080
2111
|
async function listAgentHosts(runtime2) {
|
|
@@ -2095,58 +2126,69 @@ async function listAgentHosts(runtime2) {
|
|
|
2095
2126
|
})
|
|
2096
2127
|
);
|
|
2097
2128
|
}
|
|
2098
|
-
async function disconnectLocalAgent(runtime2,
|
|
2129
|
+
async function disconnectLocalAgent(runtime2, input2) {
|
|
2099
2130
|
return disconnectAgent({
|
|
2100
|
-
agent:
|
|
2101
|
-
serverName:
|
|
2131
|
+
agent: input2.agent,
|
|
2132
|
+
serverName: input2.serverName,
|
|
2102
2133
|
scope: "user",
|
|
2103
|
-
adapter: runtime2.adapters[
|
|
2134
|
+
adapter: runtime2.adapters[input2.agent],
|
|
2104
2135
|
state: runtime2.state,
|
|
2105
|
-
yes:
|
|
2106
|
-
confirm:
|
|
2136
|
+
yes: input2.yes,
|
|
2137
|
+
confirm: input2.confirm
|
|
2107
2138
|
});
|
|
2108
2139
|
}
|
|
2109
2140
|
|
|
2110
2141
|
// src/commands/auth.ts
|
|
2111
|
-
async function login(
|
|
2112
|
-
const
|
|
2113
|
-
if (
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2142
|
+
async function login(input2) {
|
|
2143
|
+
const environmentToken = input2.environmentToken?.trim();
|
|
2144
|
+
if (environmentToken) {
|
|
2145
|
+
const user2 = await input2.validate(environmentToken);
|
|
2146
|
+
await input2.store.set(input2.credentialRef, environmentToken);
|
|
2147
|
+
return {
|
|
2148
|
+
authenticated: true,
|
|
2149
|
+
credentialRef: input2.credentialRef,
|
|
2150
|
+
credentialStore: input2.store.kind,
|
|
2151
|
+
user: user2
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
const name = (input2.name ?? await input2.promptName()).trim();
|
|
2155
|
+
if (!name) {
|
|
2156
|
+
throw new CliError("INVALID_ARGUMENT", "A SAG user name is required", {
|
|
2157
|
+
exitCode: exitCodes.invalidArgument,
|
|
2158
|
+
hint: "Pass `--name <name>` or enter a name when prompted."
|
|
2117
2159
|
});
|
|
2118
2160
|
}
|
|
2119
|
-
const user = await
|
|
2120
|
-
await
|
|
2161
|
+
const { accessToken, user } = await input2.authenticate(name);
|
|
2162
|
+
await input2.store.set(input2.credentialRef, accessToken);
|
|
2121
2163
|
return {
|
|
2122
2164
|
authenticated: true,
|
|
2123
|
-
credentialRef:
|
|
2124
|
-
credentialStore:
|
|
2165
|
+
credentialRef: input2.credentialRef,
|
|
2166
|
+
credentialStore: input2.store.kind,
|
|
2125
2167
|
user
|
|
2126
2168
|
};
|
|
2127
2169
|
}
|
|
2128
|
-
async function status(
|
|
2129
|
-
const token =
|
|
2170
|
+
async function status(input2) {
|
|
2171
|
+
const token = input2.environmentToken?.trim() || await input2.store.get(input2.credentialRef);
|
|
2130
2172
|
if (!token) {
|
|
2131
2173
|
return {
|
|
2132
2174
|
authenticated: false,
|
|
2133
|
-
credentialRef:
|
|
2134
|
-
credentialStore:
|
|
2175
|
+
credentialRef: input2.credentialRef,
|
|
2176
|
+
credentialStore: input2.store.kind
|
|
2135
2177
|
};
|
|
2136
2178
|
}
|
|
2137
|
-
const user = await
|
|
2179
|
+
const user = await input2.validate(token);
|
|
2138
2180
|
return {
|
|
2139
2181
|
authenticated: true,
|
|
2140
|
-
credentialRef:
|
|
2141
|
-
credentialStore:
|
|
2182
|
+
credentialRef: input2.credentialRef,
|
|
2183
|
+
credentialStore: input2.store.kind,
|
|
2142
2184
|
user
|
|
2143
2185
|
};
|
|
2144
2186
|
}
|
|
2145
|
-
async function logout(
|
|
2146
|
-
await
|
|
2187
|
+
async function logout(input2) {
|
|
2188
|
+
await input2.store.delete(input2.credentialRef);
|
|
2147
2189
|
return {
|
|
2148
2190
|
authenticated: false,
|
|
2149
|
-
credentialRef:
|
|
2191
|
+
credentialRef: input2.credentialRef
|
|
2150
2192
|
};
|
|
2151
2193
|
}
|
|
2152
2194
|
|
|
@@ -2246,24 +2288,24 @@ async function getEntityContext(client, sourceId, name) {
|
|
|
2246
2288
|
}
|
|
2247
2289
|
|
|
2248
2290
|
// src/commands/mcp.ts
|
|
2249
|
-
async function testLocalMcp(runtime2,
|
|
2291
|
+
async function testLocalMcp(runtime2, input2) {
|
|
2250
2292
|
const container = await discoverSagContainer({
|
|
2251
2293
|
docker: runtime2.docker,
|
|
2252
|
-
...
|
|
2294
|
+
...input2.container ? { container: input2.container } : {},
|
|
2253
2295
|
interactive: runtime2.interactive,
|
|
2254
2296
|
select: runtime2.selectContainer
|
|
2255
2297
|
});
|
|
2256
2298
|
const spec = await new DockerStdioProvider().createSpec({
|
|
2257
2299
|
target: container,
|
|
2258
|
-
...
|
|
2300
|
+
...input2.sourceId ? { sourceId: input2.sourceId } : {}
|
|
2259
2301
|
});
|
|
2260
2302
|
const verification = await runtime2.verifier.verify(spec, {
|
|
2261
|
-
timeoutMs:
|
|
2303
|
+
timeoutMs: input2.timeoutMs
|
|
2262
2304
|
});
|
|
2263
2305
|
return {
|
|
2264
2306
|
provider: "docker-stdio",
|
|
2265
2307
|
container,
|
|
2266
|
-
sourceId:
|
|
2308
|
+
sourceId: input2.sourceId ?? null,
|
|
2267
2309
|
...verification
|
|
2268
2310
|
};
|
|
2269
2311
|
}
|
|
@@ -2275,9 +2317,9 @@ function skipped(name, detail) {
|
|
|
2275
2317
|
detail
|
|
2276
2318
|
};
|
|
2277
2319
|
}
|
|
2278
|
-
async function localMcpDoctorChecks(runtime2,
|
|
2320
|
+
async function localMcpDoctorChecks(runtime2, input2) {
|
|
2279
2321
|
try {
|
|
2280
|
-
const report = await testLocalMcp(runtime2,
|
|
2322
|
+
const report = await testLocalMcp(runtime2, input2);
|
|
2281
2323
|
return [
|
|
2282
2324
|
{
|
|
2283
2325
|
name: "docker",
|
|
@@ -2474,16 +2516,16 @@ function directCredentialRef(url) {
|
|
|
2474
2516
|
const fingerprint = createHash2("sha256").update(url).digest("hex").slice(0, 16);
|
|
2475
2517
|
return `sag-cli/direct-${fingerprint}`;
|
|
2476
2518
|
}
|
|
2477
|
-
async function createRuntimeContext(
|
|
2478
|
-
const config = await
|
|
2479
|
-
const connection = resolveConnection(
|
|
2519
|
+
async function createRuntimeContext(input2) {
|
|
2520
|
+
const config = await input2.configStore.load();
|
|
2521
|
+
const connection = resolveConnection(input2.options, input2.environment, config);
|
|
2480
2522
|
const credentialRef = connection.credentialRef ?? directCredentialRef(connection.url);
|
|
2481
|
-
const token = connection.environmentToken ?? await
|
|
2523
|
+
const token = connection.environmentToken ?? await input2.credentialStore.get(credentialRef) ?? void 0;
|
|
2482
2524
|
return {
|
|
2483
2525
|
connection,
|
|
2484
2526
|
credentialRef,
|
|
2485
2527
|
...token ? { token } : {},
|
|
2486
|
-
client:
|
|
2528
|
+
client: input2.createClient({
|
|
2487
2529
|
origin: connection.url,
|
|
2488
2530
|
...token ? { token } : {}
|
|
2489
2531
|
})
|
|
@@ -2587,7 +2629,7 @@ function documentDetail(total, statuses) {
|
|
|
2587
2629
|
const parts = displayOrder.filter((status2) => statuses[status2] > 0).map((status2) => `${statuses[status2]} ${status2}`);
|
|
2588
2630
|
return `${total} ${total === 1 ? "document" : "documents"}${parts.length ? `: ${parts.join(", ")}` : ""}`;
|
|
2589
2631
|
}
|
|
2590
|
-
async function runDoctor(client,
|
|
2632
|
+
async function runDoctor(client, input2, localMcpChecks) {
|
|
2591
2633
|
const checks = [];
|
|
2592
2634
|
let sagVersion;
|
|
2593
2635
|
let authenticated = false;
|
|
@@ -2702,8 +2744,8 @@ async function runDoctor(client, input, localMcpChecks) {
|
|
|
2702
2744
|
}
|
|
2703
2745
|
return {
|
|
2704
2746
|
status: overallStatus(checks),
|
|
2705
|
-
url:
|
|
2706
|
-
...
|
|
2747
|
+
url: input2.url,
|
|
2748
|
+
...input2.profileName ? { profileName: input2.profileName } : {},
|
|
2707
2749
|
...sagVersion ? { sagVersion } : {},
|
|
2708
2750
|
checks,
|
|
2709
2751
|
summary: {
|
|
@@ -3010,12 +3052,14 @@ function addProfileCommands(program, dependencies2) {
|
|
|
3010
3052
|
}
|
|
3011
3053
|
function addAuthCommands(program, dependencies2) {
|
|
3012
3054
|
const auth = program.command("auth").description("Manage SAG credentials");
|
|
3013
|
-
auth.command("login").action(async () => {
|
|
3055
|
+
auth.command("login").option("--name <name>", "SAG user name for the existing local login").action(async (options) => {
|
|
3014
3056
|
const context = await runtime(program, dependencies2);
|
|
3015
3057
|
const result = await login({
|
|
3016
3058
|
credentialRef: context.credentialRef,
|
|
3017
3059
|
...context.connection.environmentToken ? { environmentToken: context.connection.environmentToken } : {},
|
|
3018
|
-
|
|
3060
|
+
...options.name ? { name: options.name } : {},
|
|
3061
|
+
promptName: dependencies2.promptName,
|
|
3062
|
+
authenticate: (name) => dependencies2.createClient({ origin: context.connection.url }).login(name),
|
|
3019
3063
|
validate: async (token) => dependencies2.createClient({ origin: context.connection.url, token }).me(),
|
|
3020
3064
|
store: dependencies2.credentialStore
|
|
3021
3065
|
});
|
|
@@ -3284,10 +3328,7 @@ var dependencies = {
|
|
|
3284
3328
|
configStore: new ConfigStore(defaultConfigPath()),
|
|
3285
3329
|
credentialStore: credentialSelection.store,
|
|
3286
3330
|
createClient: defaultClientFactory,
|
|
3287
|
-
|
|
3288
|
-
message: "SAG Token",
|
|
3289
|
-
mask: "*"
|
|
3290
|
-
}),
|
|
3331
|
+
promptName: () => input({ message: "SAG user name" }),
|
|
3291
3332
|
confirm: (message) => confirm({ message, default: false }),
|
|
3292
3333
|
writeStdout: (value) => process.stdout.write(value),
|
|
3293
3334
|
writeStderr: (value) => process.stderr.write(value),
|