appduct 0.11.1 → 0.13.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.md +43 -38
- package/dist/bin.js +50 -36
- package/dist/cli/command-options.d.ts +7 -17
- package/dist/cli/result-types.d.ts +11 -8
- package/dist/cli/routes/daemon/index.js +12 -16
- package/dist/cli/routes/daemon/run.js +216 -46
- package/dist/cli/routes/daemon/start.js +18 -16
- package/dist/cli/routes/daemon/status.js +18 -16
- package/dist/cli/routes/daemon/stop.js +18 -16
- package/dist/cli/routes/doctor.js +12 -16
- package/dist/cli/routes/events/index.d.ts +9 -0
- package/dist/cli/routes/events/index.js +842 -0
- package/dist/cli/routes/events/shared-options.d.ts +12 -0
- package/dist/cli/routes/events/shared-options.js +102 -0
- package/dist/cli/routes/events/since.d.ts +6 -0
- package/dist/cli/routes/events/since.js +1977 -0
- package/dist/cli/routes/events/tail.d.ts +6 -0
- package/dist/cli/routes/{events.js → events/tail.js} +184 -162
- package/dist/cli/routes/init.js +14 -18
- package/dist/cli/routes/keygen.js +13 -16
- package/dist/cli/routes/mcp.js +193 -175
- package/dist/cli/routes/sessions/index.d.ts +7 -0
- package/dist/cli/routes/sessions/index.js +843 -0
- package/dist/cli/routes/sessions/link.d.ts +4 -0
- package/dist/cli/routes/{link.js → sessions/link.js} +19 -17
- package/dist/cli/routes/sessions/ls.d.ts +4 -0
- package/dist/cli/routes/{ls.js → sessions/ls.js} +19 -17
- package/dist/cli/routes/sessions/revoke.d.ts +4 -0
- package/dist/cli/routes/{revoke.js → sessions/revoke.js} +20 -18
- package/dist/cli/routes/tools/call.d.ts +4 -0
- package/dist/cli/routes/{invoke.js → tools/call.js} +21 -19
- package/dist/cli/routes/tools/describe.d.ts +6 -0
- package/dist/cli/routes/tools/describe.js +1837 -0
- package/dist/cli/routes/tools/index.d.ts +6 -0
- package/dist/cli/routes/tools/index.js +843 -0
- package/dist/cli/routes/tools/ls.d.ts +4 -0
- package/dist/cli/routes/{tools.js → tools/ls.js} +46 -73
- package/dist/cli/routes/tools/shared-options.d.ts +16 -0
- package/dist/cli/routes/tools/shared-options.js +122 -0
- package/dist/client/app-client.d.ts +96 -37
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.js +183 -139
- package/dist/commands/events.d.ts +31 -13
- package/dist/commands/invoke.d.ts +1 -1
- package/dist/commands/link.d.ts +1 -1
- package/dist/commands/ls.d.ts +1 -1
- package/dist/commands/revoke.d.ts +1 -1
- package/dist/commands/tools.d.ts +12 -13
- package/dist/daemon/config.d.ts +5 -2
- package/dist/daemon/daemon.d.ts +3 -0
- package/dist/daemon/event-bus.d.ts +36 -4
- package/dist/daemon/events-log.d.ts +31 -0
- package/dist/daemon/memory-append-only-file.d.ts +17 -0
- package/dist/daemon/node-append-only-file.d.ts +9 -0
- package/dist/daemon/rpc-errors.d.ts +1 -1
- package/dist/daemon/state-dir.d.ts +2 -0
- package/dist/errors.d.ts +1 -1
- package/dist/events/index.d.ts +60 -0
- package/dist/index.js +62 -39
- package/dist/link.d.ts +1 -1
- package/dist/mcp/app-tools.d.ts +3 -2
- package/dist/mcp/connect-tool.d.ts +1 -1
- package/dist/mcp/events-tool.d.ts +28 -34
- package/dist/native-scheme.d.ts +1 -1
- package/dist/output.d.ts +29 -6
- package/dist/rpc/client.d.ts +1 -1
- package/dist/scheme.d.ts +4 -4
- package/package.json +2 -2
- package/dist/cli/routes/events.d.ts +0 -3
- package/dist/cli/routes/invoke.d.ts +0 -3
- package/dist/cli/routes/link.d.ts +0 -3
- package/dist/cli/routes/ls.d.ts +0 -3
- package/dist/cli/routes/revoke.d.ts +0 -3
- package/dist/cli/routes/tools.d.ts +0 -3
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// src/cli/routes/tools.ts
|
|
2
|
-
import { isValidToolGroup } from "@appduct/shared";
|
|
3
|
-
|
|
4
1
|
// src/commands/tools.ts
|
|
5
2
|
import { RPC_METHODS as RPC_METHODS2 } from "@appduct/shared";
|
|
6
3
|
|
|
@@ -25,6 +22,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
25
22
|
"eventBufferSize",
|
|
26
23
|
"auditRetentionDays",
|
|
27
24
|
"daemonLogMaxBytes",
|
|
25
|
+
"eventsLogMaxBytes",
|
|
28
26
|
"policy",
|
|
29
27
|
"advertisedIp",
|
|
30
28
|
"scheme",
|
|
@@ -84,6 +82,7 @@ var defaultConfig = (paths) => {
|
|
|
84
82
|
eventBufferSize: 256,
|
|
85
83
|
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
86
84
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
85
|
+
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
87
86
|
restartDaemonOnVersionMismatch: false,
|
|
88
87
|
policy: {
|
|
89
88
|
default: "allow",
|
|
@@ -146,6 +145,9 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
146
145
|
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
147
146
|
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
148
147
|
}
|
|
148
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
149
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
150
|
+
}
|
|
149
151
|
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
150
152
|
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
151
153
|
parsed.restartDaemonOnVersionMismatch,
|
|
@@ -246,6 +248,7 @@ var getStateDirPaths = (stateDir) => {
|
|
|
246
248
|
socketPath: join(stateDir, "daemon.sock"),
|
|
247
249
|
pidFilePath: join(stateDir, "daemon.pid"),
|
|
248
250
|
logFilePath: join(stateDir, "daemon.log"),
|
|
251
|
+
eventsLogPath: join(stateDir, "events.log"),
|
|
249
252
|
keyPath: join(stateDir, "key.pem"),
|
|
250
253
|
configPath: join(stateDir, "config.json"),
|
|
251
254
|
auditDir: join(stateDir, "audit"),
|
|
@@ -1099,38 +1102,18 @@ var handleToolsCommand = async (options, context) => {
|
|
|
1099
1102
|
if (options.name !== void 0 && hasListingOnlyOptions(options)) {
|
|
1100
1103
|
throw listingOnlyError();
|
|
1101
1104
|
}
|
|
1102
|
-
if (options.
|
|
1105
|
+
if (options.name !== void 0) {
|
|
1103
1106
|
const result = await listTools(options.selector, context);
|
|
1104
1107
|
const tool = findTool(result.tools, options.name);
|
|
1105
1108
|
if (!tool) {
|
|
1106
1109
|
throw usageError(
|
|
1107
|
-
`Tool "${options.name}" is not registered on session "${options.selector}".`,
|
|
1110
|
+
options.selector !== void 0 ? `Tool "${options.name}" is not registered on session "${options.selector}".` : `Tool "${options.name}" is not registered.`,
|
|
1108
1111
|
{ available: result.tools.map((entry) => entry.name) }
|
|
1109
1112
|
);
|
|
1110
1113
|
}
|
|
1111
1114
|
return { ok: true, data: tool };
|
|
1112
1115
|
}
|
|
1113
|
-
|
|
1114
|
-
let implicitTools;
|
|
1115
|
-
try {
|
|
1116
|
-
implicitTools = await listTools(void 0, context);
|
|
1117
|
-
} catch (error) {
|
|
1118
|
-
if (!(error instanceof DaemonRpcError)) {
|
|
1119
|
-
throw error;
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
if (implicitTools) {
|
|
1123
|
-
const tool = findTool(implicitTools.tools, options.selector);
|
|
1124
|
-
if (tool) {
|
|
1125
|
-
if (hasListingOnlyOptions(options)) {
|
|
1126
|
-
throw listingOnlyError();
|
|
1127
|
-
}
|
|
1128
|
-
return { ok: true, data: tool };
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
return { ok: true, data: await listOrGroups(options.selector, options, context) };
|
|
1132
|
-
}
|
|
1133
|
-
return { ok: true, data: await listOrGroups(void 0, options, context) };
|
|
1116
|
+
return { ok: true, data: await listOrGroups(options.selector, options, context) };
|
|
1134
1117
|
};
|
|
1135
1118
|
|
|
1136
1119
|
// src/cli/command-options.ts
|
|
@@ -1176,17 +1159,11 @@ var readTextOption = (argv, value, flagName) => {
|
|
|
1176
1159
|
}
|
|
1177
1160
|
return raw ?? String(Array.isArray(value) ? value.at(-1) : value);
|
|
1178
1161
|
};
|
|
1179
|
-
var
|
|
1180
|
-
if (args.length >
|
|
1162
|
+
var splitOptionalSelector = (args, commandUsage) => {
|
|
1163
|
+
if (args.length > 1) {
|
|
1181
1164
|
throw usageError(`Usage: ${commandUsage} (too many arguments).`);
|
|
1182
1165
|
}
|
|
1183
|
-
|
|
1184
|
-
return { selector: args[0], target: args[1] };
|
|
1185
|
-
}
|
|
1186
|
-
if (args.length === 1) {
|
|
1187
|
-
return { selectorOrTarget: args[0] };
|
|
1188
|
-
}
|
|
1189
|
-
return {};
|
|
1166
|
+
return { selector: args[0] };
|
|
1190
1167
|
};
|
|
1191
1168
|
|
|
1192
1169
|
// src/cli/global-flags.ts
|
|
@@ -1315,9 +1292,6 @@ var renderLsData = (colors, data, now) => {
|
|
|
1315
1292
|
...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
|
|
1316
1293
|
];
|
|
1317
1294
|
};
|
|
1318
|
-
var isToolsListing = (data) => {
|
|
1319
|
-
return typeof data === "object" && data !== null && Array.isArray(data.tools);
|
|
1320
|
-
};
|
|
1321
1295
|
var isToolGroupsListing = (data) => {
|
|
1322
1296
|
return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
|
|
1323
1297
|
};
|
|
@@ -1335,7 +1309,7 @@ var renderEmptyToolsLine = (data) => {
|
|
|
1335
1309
|
}
|
|
1336
1310
|
if (data.group !== void 0) {
|
|
1337
1311
|
const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
|
|
1338
|
-
return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
|
|
1312
|
+
return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
|
|
1339
1313
|
}
|
|
1340
1314
|
return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
|
|
1341
1315
|
};
|
|
@@ -1417,7 +1391,7 @@ var renderToolSummaryTable = (colors, data) => {
|
|
|
1417
1391
|
...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
|
|
1418
1392
|
...renderTruncationLine(data),
|
|
1419
1393
|
"",
|
|
1420
|
-
"Run `appduct tools <name>` for a tool's full schema."
|
|
1394
|
+
"Run `appduct tools describe <name>` for a tool's full schema."
|
|
1421
1395
|
];
|
|
1422
1396
|
};
|
|
1423
1397
|
var renderToolDetail = (colors, tool, flags) => {
|
|
@@ -1469,16 +1443,13 @@ var renderToolGroups = (colors, data) => {
|
|
|
1469
1443
|
colors.green("Groups"),
|
|
1470
1444
|
...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
|
|
1471
1445
|
"",
|
|
1472
|
-
`${data.total} tool${data.total === 1 ? "" : "s"} in total. ` + (hasAnyGroup(data.groups) ? "Run `appduct tools --group <name>` to list one group's tools." : "No tool declares a group.")
|
|
1446
|
+
`${data.total} tool${data.total === 1 ? "" : "s"} in total. ` + (hasAnyGroup(data.groups) ? "Run `appduct tools ls --group <name>` to list one group's tools." : "No tool declares a group.")
|
|
1473
1447
|
];
|
|
1474
1448
|
};
|
|
1475
|
-
var
|
|
1449
|
+
var renderToolsListData = (colors, data, flags, full) => {
|
|
1476
1450
|
if (isToolGroupsListing(data)) {
|
|
1477
1451
|
return renderToolGroups(colors, data);
|
|
1478
1452
|
}
|
|
1479
|
-
if (!isToolsListing(data)) {
|
|
1480
|
-
return renderToolDetail(colors, data, flags);
|
|
1481
|
-
}
|
|
1482
1453
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
1483
1454
|
};
|
|
1484
1455
|
var renderInvokeData = (colors, data, flags) => {
|
|
@@ -1664,15 +1635,17 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
1664
1635
|
return renderInitData(colors, data, flags);
|
|
1665
1636
|
case "keygen":
|
|
1666
1637
|
return renderKeygenData(colors, data, flags);
|
|
1667
|
-
case "link":
|
|
1638
|
+
case "sessions link":
|
|
1668
1639
|
return renderLinkData(colors, data, flags, options.qr);
|
|
1669
|
-
case "ls":
|
|
1640
|
+
case "sessions ls":
|
|
1670
1641
|
return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
|
|
1671
|
-
case "tools":
|
|
1672
|
-
return
|
|
1673
|
-
case "
|
|
1642
|
+
case "tools ls":
|
|
1643
|
+
return renderToolsListData(colors, data, flags, options.full);
|
|
1644
|
+
case "tools describe":
|
|
1645
|
+
return renderToolDetail(colors, data, flags);
|
|
1646
|
+
case "tools call":
|
|
1674
1647
|
return renderInvokeData(colors, data, flags);
|
|
1675
|
-
case "revoke":
|
|
1648
|
+
case "sessions revoke":
|
|
1676
1649
|
return renderRevokeData(colors, data);
|
|
1677
1650
|
case "daemon run":
|
|
1678
1651
|
return renderDaemonRunData(colors, data, flags);
|
|
@@ -1818,7 +1791,24 @@ var guarded = (context) => {
|
|
|
1818
1791
|
};
|
|
1819
1792
|
};
|
|
1820
1793
|
|
|
1821
|
-
// src/cli/routes/tools.ts
|
|
1794
|
+
// src/cli/routes/tools/shared-options.ts
|
|
1795
|
+
import { isValidToolGroup } from "@appduct/shared";
|
|
1796
|
+
var parseToolsListingFlags = (context) => {
|
|
1797
|
+
const { options } = context;
|
|
1798
|
+
const limit = parsePositiveIntegerOption(options.limit, "--limit");
|
|
1799
|
+
const offset = parseNonNegativeIntegerOption(options.offset, "--offset");
|
|
1800
|
+
const filter = readTextOption(context.argv, options.filter, "--filter");
|
|
1801
|
+
const group = readTextOption(context.argv, options.group, "--group");
|
|
1802
|
+
if (group !== void 0 && !isValidToolGroup(group)) {
|
|
1803
|
+
throw usageError(
|
|
1804
|
+
`"--group" must be a group name like "checkout" or "checkout/payment" (one or two "/"-separated segments of [a-zA-Z0-9_-], at most 64 characters each); got ${JSON.stringify(group)}.`
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
const groups = options.groups === void 0 ? void 0 : Boolean(options.groups);
|
|
1808
|
+
return { group, groups, filter, limit, offset };
|
|
1809
|
+
};
|
|
1810
|
+
|
|
1811
|
+
// src/cli/routes/tools/ls.ts
|
|
1822
1812
|
var route = async (context) => {
|
|
1823
1813
|
const { options, stateDir } = context;
|
|
1824
1814
|
return executeCommand(
|
|
@@ -1827,29 +1817,12 @@ var route = async (context) => {
|
|
|
1827
1817
|
// 64, `--json` envelope) instead of escaping the route as an uncaught rejection, and before
|
|
1828
1818
|
// the version check so a typo never waits on (or restarts) the daemon.
|
|
1829
1819
|
() => {
|
|
1830
|
-
const { selector
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
);
|
|
1834
|
-
const limit = parsePositiveIntegerOption(options.limit, "--limit");
|
|
1835
|
-
const offset = parseNonNegativeIntegerOption(options.offset, "--offset");
|
|
1836
|
-
const filter = readTextOption(context.argv, options.filter, "--filter");
|
|
1837
|
-
const group = readTextOption(context.argv, options.group, "--group");
|
|
1838
|
-
if (group !== void 0 && !isValidToolGroup(group)) {
|
|
1839
|
-
throw usageError(
|
|
1840
|
-
`"--group" must be a group name like "checkout" or "checkout/payment" (one or two "/"-separated segments of [a-zA-Z0-9_-], at most 64 characters each); got ${JSON.stringify(group)}.`
|
|
1841
|
-
);
|
|
1842
|
-
}
|
|
1843
|
-
const groups = options.groups === void 0 ? void 0 : Boolean(options.groups);
|
|
1844
|
-
if (groups && options.full) {
|
|
1820
|
+
const { selector } = splitOptionalSelector(context.args, "tools ls [selector]");
|
|
1821
|
+
const listing = parseToolsListingFlags(context);
|
|
1822
|
+
if (listing.groups && options.full) {
|
|
1845
1823
|
throw usageError('"--groups" lists groups only and cannot be combined with "--full".');
|
|
1846
1824
|
}
|
|
1847
|
-
return guarded(context)(
|
|
1848
|
-
() => handleToolsCommand(
|
|
1849
|
-
{ selector: selector ?? selectorOrTarget, name: target, group, groups, filter, limit, offset },
|
|
1850
|
-
{ stateDir }
|
|
1851
|
-
)
|
|
1852
|
-
)();
|
|
1825
|
+
return guarded(context)(() => handleToolsCommand({ selector, ...listing }, { stateDir }))();
|
|
1853
1826
|
},
|
|
1854
1827
|
context.env,
|
|
1855
1828
|
{ full: Boolean(options.full) }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The listing-only flags (`--group`/`--groups`/`--filter`/`--limit`/`--offset`) shared by
|
|
3
|
+
* `tools ls`, which acts on them, and `tools describe`, which only needs them parsed so it can
|
|
4
|
+
* reject one next to a `<name>` with the same usage error it has always gotten
|
|
5
|
+
* (`commands/tools.ts`'s `hasListingOnlyOptions`). Kept out of `index.ts` — nothing outside this
|
|
6
|
+
* directory imports it — since a route module, not the noun's router, is the caller.
|
|
7
|
+
*/
|
|
8
|
+
import type { RouteContext } from "../../router.js";
|
|
9
|
+
export type ToolsListingFlags = {
|
|
10
|
+
group?: string;
|
|
11
|
+
groups?: boolean;
|
|
12
|
+
filter?: string;
|
|
13
|
+
limit?: number;
|
|
14
|
+
offset?: number;
|
|
15
|
+
};
|
|
16
|
+
export declare const parseToolsListingFlags: (context: RouteContext) => ToolsListingFlags;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// src/cli/routes/tools/shared-options.ts
|
|
2
|
+
import { isValidToolGroup } from "@appduct/shared";
|
|
3
|
+
|
|
4
|
+
// src/rpc/client.ts
|
|
5
|
+
import { join as join2 } from "node:path";
|
|
6
|
+
import { RPC_METHODS } from "@appduct/shared";
|
|
7
|
+
|
|
8
|
+
// src/daemon/config.ts
|
|
9
|
+
var DEFAULT_DAEMON_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
// src/package-root.ts
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
var PACKAGE_NAME = "appduct";
|
|
16
|
+
var cached;
|
|
17
|
+
var getPackageRoot = () => {
|
|
18
|
+
if (cached !== void 0) {
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
21
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
for (; ; ) {
|
|
23
|
+
const candidate = join(dir, "package.json");
|
|
24
|
+
if (existsSync(candidate)) {
|
|
25
|
+
let name;
|
|
26
|
+
try {
|
|
27
|
+
name = JSON.parse(readFileSync(candidate, "utf8")).name;
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
if (name === PACKAGE_NAME) {
|
|
31
|
+
cached = dir;
|
|
32
|
+
return dir;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const parent = dirname(dir);
|
|
36
|
+
if (parent === dir) {
|
|
37
|
+
throw new Error(`Could not locate the "${PACKAGE_NAME}" package root above ${import.meta.url}.`);
|
|
38
|
+
}
|
|
39
|
+
dir = parent;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/rpc/client.ts
|
|
44
|
+
var defaultBinPath = join2(getPackageRoot(), "bin.js");
|
|
45
|
+
|
|
46
|
+
// src/errors.ts
|
|
47
|
+
var AppductCliError = class extends Error {
|
|
48
|
+
type;
|
|
49
|
+
details;
|
|
50
|
+
constructor(type, message, details) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "AppductCliError";
|
|
53
|
+
this.type = type;
|
|
54
|
+
this.details = details;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var usageError = (message, details) => {
|
|
58
|
+
return new AppductCliError("usage_error", message, details);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/cli/command-options.ts
|
|
62
|
+
var parsePositiveIntegerOption = (value, flagName) => {
|
|
63
|
+
if (value === void 0) {
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
67
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) {
|
|
68
|
+
throw usageError(`"${flagName}" must be a positive integer.`);
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
};
|
|
72
|
+
var parseNonNegativeIntegerOption = (value, flagName) => {
|
|
73
|
+
if (value === void 0) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
|
|
77
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) {
|
|
78
|
+
throw usageError(`"${flagName}" must be a non-negative integer.`);
|
|
79
|
+
}
|
|
80
|
+
return parsed;
|
|
81
|
+
};
|
|
82
|
+
var readTextOption = (argv, value, flagName) => {
|
|
83
|
+
if (value === void 0) {
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === "boolean") {
|
|
87
|
+
throw usageError(`"${flagName}" requires a value.`);
|
|
88
|
+
}
|
|
89
|
+
let raw;
|
|
90
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
91
|
+
const token = argv[index];
|
|
92
|
+
if (token === "--") {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
if (token === flagName && index + 1 < argv.length) {
|
|
96
|
+
raw = argv[index + 1];
|
|
97
|
+
index += 1;
|
|
98
|
+
} else if (token.startsWith(`${flagName}=`)) {
|
|
99
|
+
raw = token.slice(flagName.length + 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return raw ?? String(Array.isArray(value) ? value.at(-1) : value);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/cli/routes/tools/shared-options.ts
|
|
106
|
+
var parseToolsListingFlags = (context) => {
|
|
107
|
+
const { options } = context;
|
|
108
|
+
const limit = parsePositiveIntegerOption(options.limit, "--limit");
|
|
109
|
+
const offset = parseNonNegativeIntegerOption(options.offset, "--offset");
|
|
110
|
+
const filter = readTextOption(context.argv, options.filter, "--filter");
|
|
111
|
+
const group = readTextOption(context.argv, options.group, "--group");
|
|
112
|
+
if (group !== void 0 && !isValidToolGroup(group)) {
|
|
113
|
+
throw usageError(
|
|
114
|
+
`"--group" must be a group name like "checkout" or "checkout/payment" (one or two "/"-separated segments of [a-zA-Z0-9_-], at most 64 characters each); got ${JSON.stringify(group)}.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const groups = options.groups === void 0 ? void 0 : Boolean(options.groups);
|
|
118
|
+
return { group, groups, filter, limit, offset };
|
|
119
|
+
};
|
|
120
|
+
export {
|
|
121
|
+
parseToolsListingFlags
|
|
122
|
+
};
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* wrapper over the same `tools.list`/`tools.call`/`events.subscribe`/`events.since` RPC the CLI and
|
|
4
4
|
* MCP server use (`rpc/client.ts`'s `DaemonStream`), bound to one resolved session. No new privilege
|
|
5
5
|
* or transport — every call is attributed `caller: "client"` for the audit log (ARCHITECTURE.md
|
|
6
|
-
* §12). {@link AppClient.waitForEvent}'s
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* §12). {@link AppClient.waitForEvent} is `events/index.ts`'s `waitForAppEvent` (issue #114) — the
|
|
7
|
+
* same drain-then-live implementation the built-in `appduct_wait_for_event` MCP tool
|
|
8
|
+
* (`mcp/events-tool.ts`) uses — called on a fresh stream opened for that one wait, so concurrent
|
|
9
|
+
* waits on the same `AppClient` never share (and can't clobber) one connection's subscription.
|
|
9
10
|
*/
|
|
10
|
-
import { type ListedToolDescriptor } from "@appduct/shared";
|
|
11
|
+
import { type AppEvent, type FullAppEvent, type ListedToolDescriptor } from "@appduct/shared";
|
|
11
12
|
import type { DaemonStream } from "../rpc/client.js";
|
|
12
13
|
/** A project's own tool map — declared once, e.g. `type Tools = { sum: { args: { a: number; b:
|
|
13
14
|
* number }; result: { total: number } } }` — to get typed {@link AppClient.call} throughout a test
|
|
@@ -39,9 +40,6 @@ export type CallOptions = {
|
|
|
39
40
|
export type WaitForEventOptions = {
|
|
40
41
|
/** Defaults to 30s. */
|
|
41
42
|
timeoutMs?: number;
|
|
42
|
-
/** Extra filter over the event's payload; the event still must match `name` first. A predicate
|
|
43
|
-
* that throws rejects the wait with that error rather than crashing the connection. */
|
|
44
|
-
match?: (payload: unknown) => boolean;
|
|
45
43
|
/**
|
|
46
44
|
* Exclusive lower bound on `AppEvent.seq` (a cursor from a previous {@link AppClient.events} or
|
|
47
45
|
* {@link AppClient.waitForEvent} call) — skips already-retained events at/before it instead of
|
|
@@ -49,6 +47,12 @@ export type WaitForEventOptions = {
|
|
|
49
47
|
* so a matching event that already fired before this call still resolves immediately.
|
|
50
48
|
*/
|
|
51
49
|
since?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Caps the resolved event's payload to this many UTF-8 bytes of its JSON, daemon-side (issue
|
|
52
|
+
* #114, same cap {@link AppClient.events} takes). Omitted, the payload is delivered whole; passed,
|
|
53
|
+
* the resolved event may come back truncated — see {@link AppClient.waitForEvent}'s overloads.
|
|
54
|
+
*/
|
|
55
|
+
payloadMaxBytes?: number;
|
|
52
56
|
};
|
|
53
57
|
export type EventsOptions = {
|
|
54
58
|
/** Exclusive lower bound on `AppEvent.seq`; omitted returns the whole retained buffer (oldest
|
|
@@ -58,26 +62,35 @@ export type EventsOptions = {
|
|
|
58
62
|
* matching `since` up to the buffer's own retention limit. */
|
|
59
63
|
limit?: number;
|
|
60
64
|
};
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
/** `events.since`'s `dropped`/`remaining`, shared by both of {@link AppClient.events}'s overloaded
|
|
66
|
+
* results (issue #113). */
|
|
67
|
+
export type EventsCounts = {
|
|
68
|
+
/** App events after `since` that were evicted before this call could return them (`0` once
|
|
69
|
+
* nothing has fallen off — including across a gap of only Appduct's own kinds, which never
|
|
70
|
+
* advance `seq`). */
|
|
71
|
+
dropped: number;
|
|
72
|
+
/** Events still matching this query after the returned (possibly `limit`-truncated) page; `0`
|
|
73
|
+
* on the last page. */
|
|
74
|
+
remaining: number;
|
|
75
|
+
};
|
|
76
|
+
/** {@link AppClient.events}'s result with no `payloadMaxBytes` — every event's `payload` is
|
|
77
|
+
* readable unnarrowed. */
|
|
78
|
+
export type FullEventsResult = EventsCounts & {
|
|
79
|
+
events: FullAppEvent[];
|
|
63
80
|
/** The highest `seq` currently retained for this session (not just among the returned events) —
|
|
64
|
-
* pass it back as `since
|
|
65
|
-
*
|
|
81
|
+
* pass it back as `since` on the next {@link AppClient.events}/{@link AppClient.waitForEvent}
|
|
82
|
+
* call to resume after it. */
|
|
66
83
|
cursor: number;
|
|
67
84
|
};
|
|
68
|
-
/**
|
|
69
|
-
* `
|
|
70
|
-
export type
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
/** Unix ms. */
|
|
74
|
-
ts: number;
|
|
75
|
-
sessionId: string;
|
|
76
|
-
alias?: string;
|
|
77
|
-
/** Monotonically increasing per-session cursor (ARCHITECTURE.md §5) assigned by the daemon's
|
|
78
|
-
* retention buffer at emit time — pass back into `since` to resume after this event. */
|
|
79
|
-
seq: number;
|
|
85
|
+
/** {@link AppClient.events}'s result once a `payloadMaxBytes` cap is given — narrow each event on
|
|
86
|
+
* `truncated` before reading `payload`. */
|
|
87
|
+
export type EventsResult = EventsCounts & {
|
|
88
|
+
events: AppEvent[];
|
|
89
|
+
cursor: number;
|
|
80
90
|
};
|
|
91
|
+
/** Re-exported from `@appduct/shared` (issue #112) so `appduct/client` keeps its own public
|
|
92
|
+
* `AppEvent` name and doc comment for a `waitForEvent`/`events` caller. */
|
|
93
|
+
export type { AppEvent, FullAppEvent } from "@appduct/shared";
|
|
81
94
|
export type AppClient<TTools = ToolMap> = {
|
|
82
95
|
readonly sessionId: string;
|
|
83
96
|
/** `tools.list` for this session. An entry spells an ungrouped tool's `group` as `null`,
|
|
@@ -86,26 +99,70 @@ export type AppClient<TTools = ToolMap> = {
|
|
|
86
99
|
/** `tools.call`; rejects with a {@link AppductError} whose `type` preserves the wire error
|
|
87
100
|
* type verbatim (e.g. `"tool_timeout"`, `"policy_denied"`, `"session_suspended"`). */
|
|
88
101
|
call<K extends keyof TTools & string>(name: K, args: ToolArgs<TTools, K>, options?: CallOptions): Promise<ToolResult<TTools, K>>;
|
|
89
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* Drains `app_event`s retained in the daemon's per-session ring buffer since a cursor — the pull
|
|
90
104
|
* counterpart to {@link AppClient.waitForEvent}, for checking what already happened instead of
|
|
91
105
|
* waiting for the next one. Pass `cursor` from the result back as `since` on the next call to
|
|
92
|
-
* avoid re-reading events already seen.
|
|
93
|
-
|
|
106
|
+
* avoid re-reading events already seen. `dropped`/`remaining` (issue #113) report, respectively,
|
|
107
|
+
* how many app events after `since` were evicted before this call could return them, and how
|
|
108
|
+
* much more still matches after this page.
|
|
109
|
+
*
|
|
110
|
+
* Overloaded on `payloadMaxBytes` (issue #113): omit it and every event's `payload` is readable
|
|
111
|
+
* unnarrowed ({@link FullEventsResult}); pass it to cap each payload's JSON to that many UTF-8
|
|
112
|
+
* bytes, and narrow each event on `truncated` before reading `payload` ({@link EventsResult}) —
|
|
113
|
+
* an event whose payload was over the cap has `payloadPreview`/`payloadBytes` instead.
|
|
114
|
+
*
|
|
115
|
+
* The uncapped overload is listed first, but excludes `payloadMaxBytes` from its type
|
|
116
|
+
* (`payloadMaxBytes?: undefined`) rather than simply omitting the key from `EventsOptions`:
|
|
117
|
+
* TypeScript's excess-property check only rejects an extra `payloadMaxBytes` on an object
|
|
118
|
+
* *literal* passed directly as the argument, so an options bag held in a variable would
|
|
119
|
+
* otherwise still structurally match plain `EventsOptions` and resolve to `FullEventsResult`
|
|
120
|
+
* — letting `event.payload` compile unnarrowed even though the daemon truncated it (issue
|
|
121
|
+
* #113's review). Requiring the key to be `undefined` when present means a variable whose
|
|
122
|
+
* `payloadMaxBytes` is typed `number` (required or optional) fails to match this overload —
|
|
123
|
+
* by excess-property check for a literal, by property-type mismatch for a variable — and
|
|
124
|
+
* falls through to the capped overload below, which accepts any `payloadMaxBytes` (including
|
|
125
|
+
* `number | undefined`) and returns the narrowed `EventsResult` whenever the type cannot
|
|
126
|
+
* prove the payload is uncapped.
|
|
127
|
+
*/
|
|
128
|
+
events(options?: EventsOptions & {
|
|
129
|
+
payloadMaxBytes?: undefined;
|
|
130
|
+
}): Promise<FullEventsResult>;
|
|
131
|
+
events(options: EventsOptions & {
|
|
132
|
+
payloadMaxBytes?: number;
|
|
133
|
+
}): Promise<EventsResult>;
|
|
94
134
|
/**
|
|
95
135
|
* Waits for the next `app_event` (as pushed by the connected app's `postEvent(name, payload)`)
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
136
|
+
* whose name matches `name`, a whole-name, case-sensitive glob (issue #114: `*` matches any run
|
|
137
|
+
* of characters, a pattern with no `*` is an exact name — e.g. `"cart.*"`, or `"*"` for the next
|
|
138
|
+
* event of any name). Checks the daemon's retained buffer for an already-arrived match first,
|
|
139
|
+
* then falls back to a live wait — so an event emitted between "the caller decides to wait" and
|
|
140
|
+
* "the subscription lands" is never missed; pass `since` (a cursor from a previous {@link
|
|
99
141
|
* AppClient.events}/`waitForEvent` call) to skip events already handled and wait only for a new
|
|
100
142
|
* one, since omitting it can return an old match instantly on every call.
|
|
101
143
|
*
|
|
102
|
-
*
|
|
103
|
-
* connection,
|
|
104
|
-
*
|
|
105
|
-
*
|
|
144
|
+
* Opens its own daemon stream for this one wait (issue #114) rather than sharing this
|
|
145
|
+
* `AppClient`'s connection, so concurrent `waitForEvent` calls with different `name`s each get
|
|
146
|
+
* their own `events.subscribe` filter and resolve on their own event.
|
|
147
|
+
*
|
|
148
|
+
* Overloaded on `payloadMaxBytes` the same way {@link AppClient.events} is (issue #114): omit it
|
|
149
|
+
* and the resolved event's `payload` is readable unnarrowed ({@link FullAppEvent}); pass it to
|
|
150
|
+
* cap the payload's JSON to that many UTF-8 bytes daemon-side, and narrow on `truncated` before
|
|
151
|
+
* reading `payload` ({@link AppEvent}) — an event whose payload was over the cap has
|
|
152
|
+
* `payloadPreview`/`payloadBytes` instead. See {@link AppClient.events}'s doc comment for why the
|
|
153
|
+
* uncapped overload spells its `payloadMaxBytes` as `undefined` rather than omitting the key.
|
|
106
154
|
*/
|
|
107
|
-
waitForEvent<TPayload = unknown>(name: string, options?: WaitForEventOptions
|
|
108
|
-
|
|
155
|
+
waitForEvent<TPayload = unknown>(name: string, options?: WaitForEventOptions & {
|
|
156
|
+
payloadMaxBytes?: undefined;
|
|
157
|
+
}): Promise<FullAppEvent<TPayload>>;
|
|
158
|
+
waitForEvent<TPayload = unknown>(name: string, options: WaitForEventOptions & {
|
|
159
|
+
payloadMaxBytes?: number;
|
|
160
|
+
}): Promise<AppEvent<TPayload>>;
|
|
161
|
+
/** Closes the underlying connection, and every stream a still-pending {@link
|
|
162
|
+
* AppClient.waitForEvent} call opened for itself — each such call rejects with a `"connection_error"`
|
|
163
|
+
* {@link AppductError}, the same way it did before `waitForEvent` moved onto its own stream (issue
|
|
164
|
+
* #114). A `waitForEvent` call made after `close()` rejects immediately with the same error and
|
|
165
|
+
* never opens a stream (so it never auto-spawns a daemon). Safe to call more than once. */
|
|
109
166
|
close(): void;
|
|
110
167
|
};
|
|
111
168
|
/**
|
|
@@ -122,5 +179,7 @@ export type AppClient<TTools = ToolMap> = {
|
|
|
122
179
|
* generic transport error in place of the daemon's real one.
|
|
123
180
|
*/
|
|
124
181
|
export declare const transportTimeoutForToolCall: (timeoutMs: number | undefined) => number;
|
|
125
|
-
export declare const makeAppClient: <TTools = ToolMap>(stream: DaemonStream, sessionId: string
|
|
126
|
-
|
|
182
|
+
export declare const makeAppClient: <TTools = ToolMap>(stream: DaemonStream, sessionId: string,
|
|
183
|
+
/** Opens a fresh `DaemonStream` for one {@link AppClient.waitForEvent} call (issue #114) — every
|
|
184
|
+
* other method keeps using `stream` above. */
|
|
185
|
+
openStream: () => Promise<DaemonStream>) => AppClient<TTools>;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `appduct/client` (issue #8): a first-class programmatic client for test runners — a thin typed
|
|
3
3
|
* wrapper over the same daemon RPC the CLI and MCP server use, so a Jest/Vitest/Detox spec can
|
|
4
|
-
* `connect()`/`call()` instead of spawning `appduct
|
|
4
|
+
* `connect()`/`call()` instead of spawning `appduct tools call ... --json` and parsing stdout.
|
|
5
5
|
*
|
|
6
6
|
* ```ts
|
|
7
7
|
* import { connect } from "appduct/client";
|
|
@@ -14,5 +14,6 @@
|
|
|
14
14
|
export { connect, type ConnectOptions } from "./connect.js";
|
|
15
15
|
export { link, waitForSession, type LinkOptions, type LinkResult, type WaitForSessionOptions } from "./bootstrap.js";
|
|
16
16
|
export { AppductError, type AppductErrorType } from "./errors.js";
|
|
17
|
-
export type { AppClient, AppEvent, CallOptions, EventsOptions, EventsResult, ToolMap, WaitForEventOptions, } from "./app-client.js";
|
|
17
|
+
export type { AppClient, AppEvent, CallOptions, EventsOptions, EventsResult, FullAppEvent, FullEventsResult, ToolMap, WaitForEventOptions, } from "./app-client.js";
|
|
18
|
+
export type { TruncatedAppEvent } from "@appduct/shared";
|
|
18
19
|
export type { AgentEndpoint, ErrorType, ListedToolDescriptor, ToolDescriptor } from "@appduct/shared";
|