appduct 0.13.0 → 0.15.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 +55 -24
- package/dist/artifact-inspect.d.ts +38 -39
- package/dist/bin.js +106 -7
- package/dist/cli/result-types.d.ts +35 -2
- package/dist/cli/routes/daemon/index.js +90 -2
- package/dist/cli/routes/daemon/run.js +1088 -517
- package/dist/cli/routes/daemon/start.js +98 -2
- package/dist/cli/routes/daemon/status.js +99 -2
- package/dist/cli/routes/daemon/stop.js +98 -2
- package/dist/cli/routes/doctor.js +98 -3
- package/dist/cli/routes/events/index.d.ts +0 -3
- package/dist/cli/routes/events/index.js +92 -3
- package/dist/cli/routes/events/ls.d.ts +5 -0
- package/dist/cli/routes/events/ls.js +1837 -0
- package/dist/cli/routes/events/since.js +98 -2
- package/dist/cli/routes/events/tail.js +98 -2
- package/dist/cli/routes/init.js +265 -108
- package/dist/cli/routes/keygen.js +90 -2
- package/dist/cli/routes/mcp.js +243 -25
- package/dist/cli/routes/sessions/index.js +90 -2
- package/dist/cli/routes/sessions/link.js +199 -22
- package/dist/cli/routes/sessions/ls.js +98 -2
- package/dist/cli/routes/sessions/revoke.js +98 -2
- package/dist/cli/routes/tools/call.js +98 -2
- package/dist/cli/routes/tools/describe.js +98 -2
- package/dist/cli/routes/tools/index.js +90 -2
- package/dist/cli/routes/tools/ls.js +98 -2
- package/dist/client/attach-page.d.ts +39 -0
- package/dist/client/bootstrap.d.ts +17 -3
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +286 -112
- package/dist/commands/doctor.d.ts +3 -4
- package/dist/commands/events-ls.d.ts +21 -0
- package/dist/commands/init.d.ts +8 -2
- package/dist/commands/link.d.ts +10 -1
- package/dist/daemon/calls.d.ts +3 -1
- package/dist/daemon/config.d.ts +6 -0
- package/dist/daemon/daemon.d.ts +6 -1
- package/dist/daemon/event-bus.d.ts +10 -0
- package/dist/daemon/links.d.ts +9 -5
- package/dist/daemon/listener.d.ts +28 -1
- package/dist/daemon/registry.d.ts +20 -12
- package/dist/daemon/rpc-errors.d.ts +6 -0
- package/dist/daemon/sessions.d.ts +14 -8
- package/dist/daemon/tls.d.ts +0 -3
- package/dist/devtools-relay/attach-tab.d.ts +33 -0
- package/dist/devtools-relay/index.d.ts +8 -0
- package/dist/devtools-relay/memory-daemon-sockets.d.ts +20 -0
- package/dist/devtools-relay/memory-devtools-browser.d.ts +10 -0
- package/dist/devtools-relay/memory-page-channel.d.ts +16 -0
- package/dist/devtools-relay/node-daemon-socket.d.ts +3 -0
- package/dist/devtools-relay/node-devtools-browser.d.ts +3 -0
- package/dist/devtools-relay/ports.d.ts +48 -0
- package/dist/devtools-relay/relay.d.ts +16 -0
- package/dist/index.js +106 -7
- package/dist/link.d.ts +35 -1
- package/dist/mcp/app-tools.d.ts +19 -0
- package/dist/mcp/connect-tool.d.ts +15 -2
- package/dist/mcp/events-tool.d.ts +39 -0
- package/dist/native-scheme.d.ts +11 -7
- package/dist/scheme.d.ts +5 -1
- package/package.json +15 -5
package/dist/cli/routes/mcp.js
CHANGED
|
@@ -14,6 +14,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
14
14
|
"daemonLogMaxBytes",
|
|
15
15
|
"eventsLogMaxBytes",
|
|
16
16
|
"policy",
|
|
17
|
+
"webOrigins",
|
|
17
18
|
"advertisedIp",
|
|
18
19
|
"scheme",
|
|
19
20
|
"restartDaemonOnVersionMismatch"
|
|
@@ -74,6 +75,7 @@ var defaultConfig = (paths) => {
|
|
|
74
75
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
75
76
|
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
76
77
|
restartDaemonOnVersionMismatch: false,
|
|
78
|
+
webOrigins: [],
|
|
77
79
|
policy: {
|
|
78
80
|
default: "allow",
|
|
79
81
|
destructive: "allow"
|
|
@@ -144,6 +146,12 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
144
146
|
"restartDaemonOnVersionMismatch"
|
|
145
147
|
);
|
|
146
148
|
}
|
|
149
|
+
if (parsed.webOrigins !== void 0) {
|
|
150
|
+
if (!Array.isArray(parsed.webOrigins)) {
|
|
151
|
+
throw configError("webOrigins", "must be an array of origin strings.");
|
|
152
|
+
}
|
|
153
|
+
config.webOrigins = parsed.webOrigins.map((origin) => requireNonEmptyString(origin, "webOrigins"));
|
|
154
|
+
}
|
|
147
155
|
if (parsed.policy !== void 0) {
|
|
148
156
|
if (typeof parsed.policy !== "object" || parsed.policy === null || Array.isArray(parsed.policy)) {
|
|
149
157
|
throw configError("policy", "must be an object.");
|
|
@@ -1069,7 +1077,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
|
|
|
1069
1077
|
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
1070
1078
|
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
1071
1079
|
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
1072
|
-
// assertion failure
|
|
1080
|
+
// assertion failure.
|
|
1073
1081
|
inspection_error: 66,
|
|
1074
1082
|
connection_error: 70,
|
|
1075
1083
|
session_error: 71,
|
|
@@ -1829,23 +1837,34 @@ var probeIosProjectYml = async (root) => {
|
|
|
1829
1837
|
};
|
|
1830
1838
|
};
|
|
1831
1839
|
var PROBES = [
|
|
1832
|
-
probeAndroidGradle,
|
|
1833
|
-
probeAndroidManifest,
|
|
1834
|
-
probeIosInfoPlist,
|
|
1835
|
-
probeIosProjectYml
|
|
1840
|
+
["android", probeAndroidGradle],
|
|
1841
|
+
["android", probeAndroidManifest],
|
|
1842
|
+
["ios", probeIosInfoPlist],
|
|
1843
|
+
["ios", probeIosProjectYml]
|
|
1836
1844
|
];
|
|
1845
|
+
var isFlutterProject = async (root) => {
|
|
1846
|
+
return (await readCappedText(join5(root, "pubspec.yaml"))).ok;
|
|
1847
|
+
};
|
|
1837
1848
|
var discoverNativeScheme = async (root) => {
|
|
1838
1849
|
const tried = [];
|
|
1839
1850
|
const results = [];
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1851
|
+
const flutter = await isFlutterProject(root);
|
|
1852
|
+
const androidRoot = flutter ? join5(root, "android") : root;
|
|
1853
|
+
const iosRoots = flutter ? [join5(root, "ios"), join5(root, "macos")] : [root];
|
|
1854
|
+
for (const [platform, probe] of PROBES) {
|
|
1855
|
+
let roots = [root];
|
|
1856
|
+
if (platform === "android") {
|
|
1857
|
+
roots = [androidRoot];
|
|
1858
|
+
} else if (probe === probeIosInfoPlist) {
|
|
1859
|
+
roots = iosRoots;
|
|
1860
|
+
}
|
|
1861
|
+
for (const probeRoot of roots) {
|
|
1862
|
+
const outcome = await probe(probeRoot);
|
|
1863
|
+
tried.push(...outcome.tried);
|
|
1864
|
+
if (outcome.result !== void 0) {
|
|
1865
|
+
results.push(outcome.result);
|
|
1866
|
+
}
|
|
1845
1867
|
}
|
|
1846
|
-
}
|
|
1847
|
-
if (results.length === 0) {
|
|
1848
|
-
return { tried };
|
|
1849
1868
|
}
|
|
1850
1869
|
const distinctSchemes = new Set(results.map((entry) => entry.scheme));
|
|
1851
1870
|
if (distinctSchemes.size > 1) {
|
|
@@ -1853,7 +1872,7 @@ var discoverNativeScheme = async (root) => {
|
|
|
1853
1872
|
"Conflicting deep-link schemes found in this project: " + results.map((entry) => `${entry.origin} says ${JSON.stringify(entry.scheme)}`).join(", but ") + ". Appduct will not guess which one is right \u2014 pass --scheme <scheme> to pick one."
|
|
1854
1873
|
);
|
|
1855
1874
|
}
|
|
1856
|
-
return {
|
|
1875
|
+
return { results, tried };
|
|
1857
1876
|
};
|
|
1858
1877
|
|
|
1859
1878
|
// src/scheme.ts
|
|
@@ -1916,19 +1935,21 @@ var discoverStaticProjectScheme = async (cwd) => {
|
|
|
1916
1935
|
const tried = [`${appJsonPath} ("expo.scheme")`];
|
|
1917
1936
|
const appJsonScheme = await discoverExpoScheme(cwd);
|
|
1918
1937
|
if (appJsonScheme !== void 0) {
|
|
1919
|
-
return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, tried };
|
|
1938
|
+
return { scheme: appJsonScheme, source: "app-json", origin: appJsonPath, nativeResults: [], tried };
|
|
1920
1939
|
}
|
|
1921
1940
|
const native = await discoverNativeScheme(resolve(cwd));
|
|
1922
1941
|
tried.push(...native.tried);
|
|
1923
|
-
|
|
1942
|
+
const [first] = native.results;
|
|
1943
|
+
if (first !== void 0) {
|
|
1924
1944
|
return {
|
|
1925
|
-
scheme:
|
|
1926
|
-
source:
|
|
1927
|
-
origin:
|
|
1945
|
+
scheme: first.scheme,
|
|
1946
|
+
source: first.source,
|
|
1947
|
+
origin: first.origin,
|
|
1948
|
+
nativeResults: native.results,
|
|
1928
1949
|
tried
|
|
1929
1950
|
};
|
|
1930
1951
|
}
|
|
1931
|
-
return { tried };
|
|
1952
|
+
return { nativeResults: [], tried };
|
|
1932
1953
|
};
|
|
1933
1954
|
var globalConfigDirs = (options = {}) => {
|
|
1934
1955
|
const excluded = /* @__PURE__ */ new Set([join6(resolve(options.homeDir ?? homedir()), PROJECT_CONFIG_DIR)]);
|
|
@@ -2104,6 +2125,25 @@ ${tried.map((location, index) => ` ${index + 1}. ${location}`).join("\n")}
|
|
|
2104
2125
|
|
|
2105
2126
|
// src/link.ts
|
|
2106
2127
|
var composeDeepLink = (scheme, result) => `${scheme}:///?appduct=${result.deepLinkPayload}&pin=${encodeURIComponent(result.pin)}`;
|
|
2128
|
+
var assertWebPageUrl = (pageUrl) => {
|
|
2129
|
+
let protocol;
|
|
2130
|
+
try {
|
|
2131
|
+
protocol = new URL(pageUrl).protocol;
|
|
2132
|
+
} catch {
|
|
2133
|
+
}
|
|
2134
|
+
if (protocol !== "http:" && protocol !== "https:") {
|
|
2135
|
+
throw usageError(`"url" must be an http(s) URL (got "${pageUrl}").`);
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
var composeWebLink = (pageUrl, result) => {
|
|
2139
|
+
const payload = result.deepLinkPayload;
|
|
2140
|
+
return {
|
|
2141
|
+
sessionId: result.sessionId,
|
|
2142
|
+
url: `${pageUrl.split("#")[0]}#appduct=${payload}`,
|
|
2143
|
+
script: `window.__APPDUCT__.connect("${payload}")`,
|
|
2144
|
+
expiresAt: result.expiresAt
|
|
2145
|
+
};
|
|
2146
|
+
};
|
|
2107
2147
|
|
|
2108
2148
|
// src/qr-terminal.ts
|
|
2109
2149
|
import { toQR } from "toqr";
|
|
@@ -2146,14 +2186,18 @@ var WAIT_FOR_SESSION_TOOL_NAME = "appduct_wait_for_session";
|
|
|
2146
2186
|
var OPEN_TARGET_ADDRESS_OVERRIDE = "127.0.0.1";
|
|
2147
2187
|
var DEFAULT_WAIT_TIMEOUT_MS = 12e4;
|
|
2148
2188
|
var NO_TARGET = "none";
|
|
2189
|
+
var WEB_TARGET = "web";
|
|
2149
2190
|
var QR_INSTRUCTIONS = 'This link was NOT delivered to a device \u2014 a human has to open it. Before calling appduct_wait_for_session: show the user the "qr" field verbatim inside a fenced code block (it is pre-rendered terminal art \u2014 do not redraw, summarize, or describe it), show "deepLink" underneath it for a device that cannot scan, and ask the user to scan it with the target device. Only then call appduct_wait_for_session; it blocks until the device connects and gives no output while it waits, so calling it before you have asked will look like a hang.';
|
|
2150
2191
|
var CONNECT_TOOL_DESCRIPTOR = {
|
|
2151
2192
|
name: CONNECT_TOOL_NAME,
|
|
2152
|
-
description:
|
|
2193
|
+
description: `Mint an Appduct session link and get it onto a device. With no "target", auto-detects a booted iOS simulator or attached Android device and delivers the link to it, returning { sessionId, delivered: true, autoDetected: true } \u2014 this is the normal agent path and needs no human. Pass target "android" or "ios-sim" (optionally with "device") to choose explicitly, or target "none" to force the human flow. Delivering to "android" (explicit or auto-detected) needs "appId" (the Android package name) \u2014 without it, more than one installed app declaring the same scheme would show an ambiguous "Open with" chooser on the device instead of failing loudly. Target "ios-device" is an experimental, never-auto-detected path that delivers to a paired physical iPhone/iPad via xcrun devicectl; it needs "appId" (the iOS bundle id), iOS 17+, Xcode 15+, a connected and trusted device with Developer Mode on, a dev-signed build installed, and the phone on the same network as this machine. An "appId" not supplied here falls back to "appId.<platform>" in the nearest .appduct/config.json (see "appduct init"); it is rejected outright with target "ios-sim" or "none", which need no app id. Pass "relaunch": true with ios-device if the app is already running and the delivery does not take. Target "web" is for a web page: pass "url" (the page to open) and get back { url, script } \u2014 open "url" in a browser, or run "script" in a page that is already open, then call appduct_wait_for_session. It needs no scheme and no device. To have the daemon attach a page that is already open in a Chrome launched with --remote-debugging-port, also pass "browserUrl" (its debugging endpoint on this machine: 127.0.0.1, [::1] or localhost, such as http://127.0.0.1:9222): the tab whose address starts with "url" is attached and the call returns { sessionId, url, targetId, attached: true }, with no further step; "url" is the tab's own address. When no tab or several match, the error lists the open tabs with their target ids; pass one as "targetId" to pick it. A popup, or a page in a new tab, is not attached. Only when no device is detected (or target is "none") does this return a QR code for a human to scan, along with an "instructions" field saying what to do with it. Follow up with appduct_wait_for_session to know when the device has connected.`,
|
|
2153
2194
|
inputSchema: {
|
|
2154
2195
|
type: "object",
|
|
2155
2196
|
properties: {
|
|
2156
|
-
target: { type: "string", enum: ["android", "ios-sim", "ios-device", NO_TARGET] },
|
|
2197
|
+
target: { type: "string", enum: ["android", "ios-sim", "ios-device", NO_TARGET, WEB_TARGET] },
|
|
2198
|
+
url: { type: "string" },
|
|
2199
|
+
browserUrl: { type: "string" },
|
|
2200
|
+
targetId: { type: "string" },
|
|
2157
2201
|
device: { type: "string" },
|
|
2158
2202
|
appId: { type: "string" },
|
|
2159
2203
|
relaunch: { type: "boolean" },
|
|
@@ -2192,10 +2236,10 @@ var asOptionalConnectTarget = (value) => {
|
|
|
2192
2236
|
if (value === void 0) {
|
|
2193
2237
|
return void 0;
|
|
2194
2238
|
}
|
|
2195
|
-
if (typeof value !== "string" || !(isOpenTarget(value) || value === NO_TARGET)) {
|
|
2239
|
+
if (typeof value !== "string" || !(isOpenTarget(value) || value === NO_TARGET || value === WEB_TARGET)) {
|
|
2196
2240
|
throw new McpBuiltinToolError(
|
|
2197
2241
|
"invalid_request",
|
|
2198
|
-
'"target" must be "android", "ios-sim", "ios-device", or "none".'
|
|
2242
|
+
'"target" must be "android", "ios-sim", "ios-device", "web", or "none".'
|
|
2199
2243
|
);
|
|
2200
2244
|
}
|
|
2201
2245
|
return value;
|
|
@@ -2273,6 +2317,42 @@ var handleConnectTool = async (rawArgs, deps) => {
|
|
|
2273
2317
|
const appId = asOptionalNonEmptyString(args.appId, "appId");
|
|
2274
2318
|
const relaunch = asOptionalBoolean(args.relaunch, "relaunch");
|
|
2275
2319
|
const ttlSeconds = asOptionalPositiveNumber(args.ttlSeconds, "ttlSeconds");
|
|
2320
|
+
const url = asOptionalNonEmptyString(args.url, "url");
|
|
2321
|
+
const browserUrl = asOptionalNonEmptyString(args.browserUrl, "browserUrl");
|
|
2322
|
+
const targetId = asOptionalNonEmptyString(args.targetId, "targetId");
|
|
2323
|
+
if ((browserUrl !== void 0 || targetId !== void 0) && requestedTarget !== WEB_TARGET) {
|
|
2324
|
+
throw new McpBuiltinToolError("invalid_request", '"browserUrl" and "targetId" only apply with target "web".');
|
|
2325
|
+
}
|
|
2326
|
+
if (targetId !== void 0 && browserUrl === void 0) {
|
|
2327
|
+
throw new McpBuiltinToolError("invalid_request", '"targetId" needs "browserUrl": it names a tab of that browser.');
|
|
2328
|
+
}
|
|
2329
|
+
if (requestedTarget === WEB_TARGET) {
|
|
2330
|
+
if (url === void 0) {
|
|
2331
|
+
throw new McpBuiltinToolError("invalid_request", '"url" (the page to open) is required with target "web".');
|
|
2332
|
+
}
|
|
2333
|
+
if (device !== void 0 || appId !== void 0 || relaunch !== void 0) {
|
|
2334
|
+
throw new McpBuiltinToolError(
|
|
2335
|
+
"invalid_request",
|
|
2336
|
+
'"device", "appId" and "relaunch" do not apply with target "web".'
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
try {
|
|
2340
|
+
assertWebPageUrl(url);
|
|
2341
|
+
} catch (error) {
|
|
2342
|
+
throw new McpBuiltinToolError("invalid_request", error.message);
|
|
2343
|
+
}
|
|
2344
|
+
if (browserUrl !== void 0) {
|
|
2345
|
+
const attached = await deps.call(RPC_METHODS3.webAttach, { url, browserUrl, targetId, ttlSeconds });
|
|
2346
|
+
return { ...attached, attached: true };
|
|
2347
|
+
}
|
|
2348
|
+
return composeWebLink(
|
|
2349
|
+
url,
|
|
2350
|
+
await deps.call(RPC_METHODS3.linkCreate, { ttlSeconds, transport: "web" })
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
if (url !== void 0) {
|
|
2354
|
+
throw new McpBuiltinToolError("invalid_request", '"url" only applies with target "web".');
|
|
2355
|
+
}
|
|
2276
2356
|
if (device !== void 0 && (requestedTarget === void 0 || requestedTarget === NO_TARGET)) {
|
|
2277
2357
|
throw new McpBuiltinToolError(
|
|
2278
2358
|
"invalid_request",
|
|
@@ -2657,6 +2737,7 @@ var resolveAppTool = async (call, selector, name) => {
|
|
|
2657
2737
|
// src/mcp/events-tool.ts
|
|
2658
2738
|
import {
|
|
2659
2739
|
RPC_METHODS as RPC_METHODS6,
|
|
2740
|
+
renderEventSignature,
|
|
2660
2741
|
toAppEvent as toAppEvent2
|
|
2661
2742
|
} from "@appduct/shared";
|
|
2662
2743
|
|
|
@@ -2760,16 +2841,36 @@ var waitForAppEvent = async (stream, options) => {
|
|
|
2760
2841
|
};
|
|
2761
2842
|
|
|
2762
2843
|
// src/mcp/events-tool.ts
|
|
2844
|
+
var LIST_EVENTS_TOOL_NAME = "appduct_list_events";
|
|
2763
2845
|
var EVENTS_TOOL_NAME = "appduct_events";
|
|
2764
2846
|
var WAIT_FOR_EVENT_TOOL_NAME = "appduct_wait_for_event";
|
|
2765
2847
|
var DEFAULT_EVENTS_LIMIT = 50;
|
|
2766
2848
|
var DEFAULT_EVENTS_PAYLOAD_MAX_BYTES = 4096;
|
|
2849
|
+
var DEFAULT_LIST_EVENTS_LIMIT = 50;
|
|
2767
2850
|
var DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS = 12e4;
|
|
2768
2851
|
var MAX_WAIT_FOR_EVENT_TIMEOUT_MS = 25 * 60 * 1e3;
|
|
2769
2852
|
var WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS = 6e4;
|
|
2770
2853
|
var clampWaitForEventTimeoutMs = (requestedMs) => {
|
|
2771
2854
|
return Math.min(requestedMs ?? DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS, MAX_WAIT_FOR_EVENT_TIMEOUT_MS);
|
|
2772
2855
|
};
|
|
2856
|
+
var LIST_EVENTS_TOOL_DESCRIPTOR = {
|
|
2857
|
+
name: LIST_EVENTS_TOOL_NAME,
|
|
2858
|
+
description: `List the events the connected app declared, as one-line signatures (\`name { field: type }\`) with each event's description. Call this before appduct_wait_for_event or appduct_events so you wait on a name the app really posts instead of guessing one. An app that declares no events returns an empty list, which does not mean it posts none. name is a whole-name, case-sensitive glob (* matches any run of characters), e.g. "cart.*"; a name without * that matches a declared event also returns that event's payload_schema. limit (default ${DEFAULT_LIST_EVENTS_LIMIT}) and offset page the name-sorted list, and total counts every match before paging. Returns { session, total, limit, events }, each event { name, signature, description }.`,
|
|
2859
|
+
inputSchema: {
|
|
2860
|
+
type: "object",
|
|
2861
|
+
properties: {
|
|
2862
|
+
selector: {
|
|
2863
|
+
type: "string",
|
|
2864
|
+
minLength: 1,
|
|
2865
|
+
description: "Session alias or id. Omit to target the sole active/suspended session."
|
|
2866
|
+
},
|
|
2867
|
+
name: { type: "string", minLength: 1 },
|
|
2868
|
+
limit: { type: "integer", exclusiveMinimum: 0 },
|
|
2869
|
+
offset: { type: "integer", minimum: 0 }
|
|
2870
|
+
},
|
|
2871
|
+
additionalProperties: false
|
|
2872
|
+
}
|
|
2873
|
+
};
|
|
2773
2874
|
var EVENTS_TOOL_DESCRIPTOR = {
|
|
2774
2875
|
name: EVENTS_TOOL_NAME,
|
|
2775
2876
|
description: `Drain the events the app posted with postEvent(name, payload) that the daemon retained for a session since a cursor, for checking what the app reported after calling a tool or triggering app behavior. With no selector, targets the sole active/suspended session. name filters to events whose name matches a whole-name, case-sensitive glob (* matches any run of characters; a pattern without * is an exact name), e.g. "cart.*". Returns { events, cursor, dropped, remaining } with each event as { name, payload, ts, seq, sessionId, alias } or, once a payload's JSON exceeds payloadMaxBytes (default 4096), as { name, payloadPreview, truncated: true, payloadBytes, ts, seq, sessionId, alias } instead \u2014 check truncated before reading payload. pass cursor back as since on the next call to avoid re-reading events you've already seen; dropped counts app events after since that were evicted before this call could return them (0 once nothing has fallen off), and remaining counts events still matching this query after the returned page (0 on the last page). limit (default 50) keeps the OLDEST events in the window and advances cursor only past what was actually returned, so repeated calls page forward through everything retained rather than skipping ahead.`,
|
|
@@ -2859,6 +2960,31 @@ var rejectMatch = (args) => {
|
|
|
2859
2960
|
);
|
|
2860
2961
|
}
|
|
2861
2962
|
};
|
|
2963
|
+
var handleListEventsTool = async (rawArgs, call) => {
|
|
2964
|
+
const args = asRecord3(rawArgs);
|
|
2965
|
+
rejectUnknownKeys(args, LIST_EVENTS_TOOL_NAME, ["selector", "name", "limit", "offset"]);
|
|
2966
|
+
const session = await resolveSession(call, asOptionalString(args.selector, "selector"));
|
|
2967
|
+
const name = asOptionalString(args.name, "name");
|
|
2968
|
+
const limit = args.limit ?? DEFAULT_LIST_EVENTS_LIMIT;
|
|
2969
|
+
const result = await call(RPC_METHODS6.eventsList, {
|
|
2970
|
+
selector: session.sessionId,
|
|
2971
|
+
...name !== void 0 ? { name } : {},
|
|
2972
|
+
limit,
|
|
2973
|
+
...args.offset !== void 0 && args.offset !== null ? { offset: args.offset } : {}
|
|
2974
|
+
});
|
|
2975
|
+
const lookup = name !== void 0 && !name.includes("*");
|
|
2976
|
+
return {
|
|
2977
|
+
session: session.alias,
|
|
2978
|
+
total: result.total,
|
|
2979
|
+
limit,
|
|
2980
|
+
events: result.events.map((event) => ({
|
|
2981
|
+
name: event.name,
|
|
2982
|
+
signature: renderEventSignature(event),
|
|
2983
|
+
description: event.description,
|
|
2984
|
+
...lookup && event.payload_schema !== void 0 ? { payload_schema: event.payload_schema } : {}
|
|
2985
|
+
}))
|
|
2986
|
+
};
|
|
2987
|
+
};
|
|
2862
2988
|
var handleEventsTool = async (rawArgs, deps) => {
|
|
2863
2989
|
const args = asRecord3(rawArgs);
|
|
2864
2990
|
rejectKinds(args);
|
|
@@ -3153,6 +3279,7 @@ var createMcpServer = async (options) => {
|
|
|
3153
3279
|
LIST_TOOLS_TOOL_DESCRIPTOR,
|
|
3154
3280
|
DESCRIBE_TOOL_TOOL_DESCRIPTOR,
|
|
3155
3281
|
CALL_TOOL_TOOL_DESCRIPTOR,
|
|
3282
|
+
LIST_EVENTS_TOOL_DESCRIPTOR,
|
|
3156
3283
|
EVENTS_TOOL_DESCRIPTOR,
|
|
3157
3284
|
WAIT_FOR_EVENT_TOOL_DESCRIPTOR
|
|
3158
3285
|
]
|
|
@@ -3181,6 +3308,9 @@ var createMcpServer = async (options) => {
|
|
|
3181
3308
|
await handleWaitForSessionTool(args, { stateDir: options.stateDir, spawn: options.spawn })
|
|
3182
3309
|
);
|
|
3183
3310
|
}
|
|
3311
|
+
if (name === LIST_EVENTS_TOOL_NAME) {
|
|
3312
|
+
return toolSuccessContent(await handleListEventsTool(args, stream.call));
|
|
3313
|
+
}
|
|
3184
3314
|
if (name === EVENTS_TOOL_NAME) {
|
|
3185
3315
|
return toolSuccessContent(await handleEventsTool(args, { call: stream.call }));
|
|
3186
3316
|
}
|
|
@@ -3329,6 +3459,7 @@ var formatJson = (value, flags) => {
|
|
|
3329
3459
|
import pc from "picocolors";
|
|
3330
3460
|
import {
|
|
3331
3461
|
formatAgentWebSocketUrl,
|
|
3462
|
+
renderEventSignature as renderEventSignature2,
|
|
3332
3463
|
renderToolSignature as renderToolSignature2,
|
|
3333
3464
|
summarizeToolDescription as summarizeToolDescription2
|
|
3334
3465
|
} from "@appduct/shared";
|
|
@@ -3387,6 +3518,13 @@ var formatDevice = (device) => {
|
|
|
3387
3518
|
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
3388
3519
|
return device.os ? `${label} (${device.os})` : label;
|
|
3389
3520
|
};
|
|
3521
|
+
var SUSPEND_REASON_LABELS = {
|
|
3522
|
+
app_backgrounded: "app in background",
|
|
3523
|
+
connection_lost: "connection lost"
|
|
3524
|
+
};
|
|
3525
|
+
var formatSessionState = (session) => {
|
|
3526
|
+
return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
|
|
3527
|
+
};
|
|
3390
3528
|
var renderLsData = (colors, data, now) => {
|
|
3391
3529
|
if (data.length === 0) {
|
|
3392
3530
|
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
@@ -3395,7 +3533,7 @@ var renderLsData = (colors, data, now) => {
|
|
|
3395
3533
|
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
3396
3534
|
const rows = data.map((session) => [
|
|
3397
3535
|
session.alias,
|
|
3398
|
-
session
|
|
3536
|
+
formatSessionState(session),
|
|
3399
3537
|
formatDevice(session.device),
|
|
3400
3538
|
String(session.toolCount),
|
|
3401
3539
|
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
@@ -3569,10 +3707,87 @@ var renderToolsListData = (colors, data, flags, full) => {
|
|
|
3569
3707
|
}
|
|
3570
3708
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
3571
3709
|
};
|
|
3710
|
+
var renderEventsTruncationLine = (data) => {
|
|
3711
|
+
const offset = data.offset ?? 0;
|
|
3712
|
+
if (data.events.length >= data.total) {
|
|
3713
|
+
return [];
|
|
3714
|
+
}
|
|
3715
|
+
return [
|
|
3716
|
+
"",
|
|
3717
|
+
`Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
|
|
3718
|
+
];
|
|
3719
|
+
};
|
|
3720
|
+
var renderEventsListData = (colors, data, flags) => {
|
|
3721
|
+
const [only] = data.events;
|
|
3722
|
+
if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
|
|
3723
|
+
return renderFields(
|
|
3724
|
+
colors.green(`Event: ${only.name}`),
|
|
3725
|
+
[
|
|
3726
|
+
["Signature", renderEventSignature2(only)],
|
|
3727
|
+
["Description", only.description],
|
|
3728
|
+
["Payload schema", only.payload_schema]
|
|
3729
|
+
],
|
|
3730
|
+
flags
|
|
3731
|
+
);
|
|
3732
|
+
}
|
|
3733
|
+
if (data.events.length === 0 && data.total > 0) {
|
|
3734
|
+
return [
|
|
3735
|
+
colors.green("Events"),
|
|
3736
|
+
` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
|
|
3737
|
+
];
|
|
3738
|
+
}
|
|
3739
|
+
if (data.events.length === 0) {
|
|
3740
|
+
return [
|
|
3741
|
+
colors.green("Events"),
|
|
3742
|
+
data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
|
|
3743
|
+
];
|
|
3744
|
+
}
|
|
3745
|
+
const lines = data.events.flatMap((event) => [
|
|
3746
|
+
` ${renderEventSignature2(event)}`,
|
|
3747
|
+
` ${summarizeToolDescription2(event.description)}`
|
|
3748
|
+
]);
|
|
3749
|
+
return [
|
|
3750
|
+
colors.green("Events"),
|
|
3751
|
+
...lines,
|
|
3752
|
+
...renderEventsTruncationLine(data),
|
|
3753
|
+
"",
|
|
3754
|
+
"Run `appduct events ls --name <name>` for an event's full payload schema."
|
|
3755
|
+
];
|
|
3756
|
+
};
|
|
3572
3757
|
var renderInvokeData = (colors, data, flags) => {
|
|
3573
3758
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
3574
3759
|
};
|
|
3575
3760
|
var renderLinkData = (colors, data, flags, qr) => {
|
|
3761
|
+
if ("attached" in data) {
|
|
3762
|
+
return [
|
|
3763
|
+
colors.green("Page Attached"),
|
|
3764
|
+
...renderFields(
|
|
3765
|
+
"Page",
|
|
3766
|
+
[
|
|
3767
|
+
["Session", data.sessionId],
|
|
3768
|
+
["Tab", data.targetId],
|
|
3769
|
+
["URL", data.url],
|
|
3770
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
3771
|
+
],
|
|
3772
|
+
flags
|
|
3773
|
+
)
|
|
3774
|
+
];
|
|
3775
|
+
}
|
|
3776
|
+
if ("script" in data) {
|
|
3777
|
+
return [
|
|
3778
|
+
colors.green("Link Created"),
|
|
3779
|
+
...renderFields(
|
|
3780
|
+
"Link",
|
|
3781
|
+
[
|
|
3782
|
+
["Session", data.sessionId],
|
|
3783
|
+
["URL", data.url],
|
|
3784
|
+
["Script", data.script],
|
|
3785
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
3786
|
+
],
|
|
3787
|
+
flags
|
|
3788
|
+
)
|
|
3789
|
+
];
|
|
3790
|
+
}
|
|
3576
3791
|
const lines = [
|
|
3577
3792
|
colors.green("Link Created"),
|
|
3578
3793
|
...renderFields(
|
|
@@ -3693,6 +3908,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
|
|
|
3693
3908
|
["PID", data.daemon.pid],
|
|
3694
3909
|
["Started at", data.daemon.started_at],
|
|
3695
3910
|
["WSS port", data.daemon.wss_port],
|
|
3911
|
+
["Web port", data.daemon.web_port],
|
|
3696
3912
|
["Pinned keys", data.daemon.pinned_keys.length],
|
|
3697
3913
|
["Sessions", data.daemon.session_count]
|
|
3698
3914
|
],
|
|
@@ -3760,6 +3976,8 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
3760
3976
|
return renderToolsListData(colors, data, flags, options.full);
|
|
3761
3977
|
case "tools describe":
|
|
3762
3978
|
return renderToolDetail(colors, data, flags);
|
|
3979
|
+
case "events ls":
|
|
3980
|
+
return renderEventsListData(colors, data, flags);
|
|
3763
3981
|
case "tools call":
|
|
3764
3982
|
return renderInvokeData(colors, data, flags);
|
|
3765
3983
|
case "sessions revoke":
|
|
@@ -93,7 +93,7 @@ var EXIT_CODE_BY_ERROR_TYPE = {
|
|
|
93
93
|
// EX_NOINPUT (66): "an input file did not exist or was not readable" — `doctor` throws this for a
|
|
94
94
|
// missing external tool or an unreadable/corrupt artifact, deliberately distinct from every other
|
|
95
95
|
// code here so a release pipeline can't mistake "we couldn't tell" for either a clean pass or an
|
|
96
|
-
// assertion failure
|
|
96
|
+
// assertion failure.
|
|
97
97
|
inspection_error: 66,
|
|
98
98
|
connection_error: 70,
|
|
99
99
|
session_error: 71,
|
|
@@ -213,6 +213,7 @@ var formatJson = (value, flags) => {
|
|
|
213
213
|
import pc from "picocolors";
|
|
214
214
|
import {
|
|
215
215
|
formatAgentWebSocketUrl,
|
|
216
|
+
renderEventSignature,
|
|
216
217
|
renderToolSignature,
|
|
217
218
|
summarizeToolDescription
|
|
218
219
|
} from "@appduct/shared";
|
|
@@ -308,6 +309,13 @@ var formatDevice = (device) => {
|
|
|
308
309
|
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
309
310
|
return device.os ? `${label} (${device.os})` : label;
|
|
310
311
|
};
|
|
312
|
+
var SUSPEND_REASON_LABELS = {
|
|
313
|
+
app_backgrounded: "app in background",
|
|
314
|
+
connection_lost: "connection lost"
|
|
315
|
+
};
|
|
316
|
+
var formatSessionState = (session) => {
|
|
317
|
+
return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
|
|
318
|
+
};
|
|
311
319
|
var renderLsData = (colors, data, now) => {
|
|
312
320
|
if (data.length === 0) {
|
|
313
321
|
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
@@ -316,7 +324,7 @@ var renderLsData = (colors, data, now) => {
|
|
|
316
324
|
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
317
325
|
const rows = data.map((session) => [
|
|
318
326
|
session.alias,
|
|
319
|
-
session
|
|
327
|
+
formatSessionState(session),
|
|
320
328
|
formatDevice(session.device),
|
|
321
329
|
String(session.toolCount),
|
|
322
330
|
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
@@ -490,10 +498,87 @@ var renderToolsListData = (colors, data, flags, full) => {
|
|
|
490
498
|
}
|
|
491
499
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
492
500
|
};
|
|
501
|
+
var renderEventsTruncationLine = (data) => {
|
|
502
|
+
const offset = data.offset ?? 0;
|
|
503
|
+
if (data.events.length >= data.total) {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
return [
|
|
507
|
+
"",
|
|
508
|
+
`Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
|
|
509
|
+
];
|
|
510
|
+
};
|
|
511
|
+
var renderEventsListData = (colors, data, flags) => {
|
|
512
|
+
const [only] = data.events;
|
|
513
|
+
if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
|
|
514
|
+
return renderFields(
|
|
515
|
+
colors.green(`Event: ${only.name}`),
|
|
516
|
+
[
|
|
517
|
+
["Signature", renderEventSignature(only)],
|
|
518
|
+
["Description", only.description],
|
|
519
|
+
["Payload schema", only.payload_schema]
|
|
520
|
+
],
|
|
521
|
+
flags
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
if (data.events.length === 0 && data.total > 0) {
|
|
525
|
+
return [
|
|
526
|
+
colors.green("Events"),
|
|
527
|
+
` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
|
|
528
|
+
];
|
|
529
|
+
}
|
|
530
|
+
if (data.events.length === 0) {
|
|
531
|
+
return [
|
|
532
|
+
colors.green("Events"),
|
|
533
|
+
data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
const lines = data.events.flatMap((event) => [
|
|
537
|
+
` ${renderEventSignature(event)}`,
|
|
538
|
+
` ${summarizeToolDescription(event.description)}`
|
|
539
|
+
]);
|
|
540
|
+
return [
|
|
541
|
+
colors.green("Events"),
|
|
542
|
+
...lines,
|
|
543
|
+
...renderEventsTruncationLine(data),
|
|
544
|
+
"",
|
|
545
|
+
"Run `appduct events ls --name <name>` for an event's full payload schema."
|
|
546
|
+
];
|
|
547
|
+
};
|
|
493
548
|
var renderInvokeData = (colors, data, flags) => {
|
|
494
549
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
495
550
|
};
|
|
496
551
|
var renderLinkData = (colors, data, flags, qr) => {
|
|
552
|
+
if ("attached" in data) {
|
|
553
|
+
return [
|
|
554
|
+
colors.green("Page Attached"),
|
|
555
|
+
...renderFields(
|
|
556
|
+
"Page",
|
|
557
|
+
[
|
|
558
|
+
["Session", data.sessionId],
|
|
559
|
+
["Tab", data.targetId],
|
|
560
|
+
["URL", data.url],
|
|
561
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
562
|
+
],
|
|
563
|
+
flags
|
|
564
|
+
)
|
|
565
|
+
];
|
|
566
|
+
}
|
|
567
|
+
if ("script" in data) {
|
|
568
|
+
return [
|
|
569
|
+
colors.green("Link Created"),
|
|
570
|
+
...renderFields(
|
|
571
|
+
"Link",
|
|
572
|
+
[
|
|
573
|
+
["Session", data.sessionId],
|
|
574
|
+
["URL", data.url],
|
|
575
|
+
["Script", data.script],
|
|
576
|
+
["Expires", new Date(data.expiresAt * 1e3).toISOString()]
|
|
577
|
+
],
|
|
578
|
+
flags
|
|
579
|
+
)
|
|
580
|
+
];
|
|
581
|
+
}
|
|
497
582
|
const lines = [
|
|
498
583
|
colors.green("Link Created"),
|
|
499
584
|
...renderFields(
|
|
@@ -614,6 +699,7 @@ var renderDaemonStatusData = (colors, data, flags) => {
|
|
|
614
699
|
["PID", data.daemon.pid],
|
|
615
700
|
["Started at", data.daemon.started_at],
|
|
616
701
|
["WSS port", data.daemon.wss_port],
|
|
702
|
+
["Web port", data.daemon.web_port],
|
|
617
703
|
["Pinned keys", data.daemon.pinned_keys.length],
|
|
618
704
|
["Sessions", data.daemon.session_count]
|
|
619
705
|
],
|
|
@@ -681,6 +767,8 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
681
767
|
return renderToolsListData(colors, data, flags, options.full);
|
|
682
768
|
case "tools describe":
|
|
683
769
|
return renderToolDetail(colors, data, flags);
|
|
770
|
+
case "events ls":
|
|
771
|
+
return renderEventsListData(colors, data, flags);
|
|
684
772
|
case "tools call":
|
|
685
773
|
return renderInvokeData(colors, data, flags);
|
|
686
774
|
case "sessions revoke":
|