appduct 0.12.0 → 0.14.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 -37
- package/dist/bin.js +102 -37
- package/dist/cli/command-options.d.ts +7 -17
- package/dist/cli/result-types.d.ts +19 -9
- package/dist/cli/routes/daemon/index.js +70 -17
- package/dist/cli/routes/daemon/run.js +288 -50
- package/dist/cli/routes/daemon/start.js +70 -17
- package/dist/cli/routes/daemon/status.js +70 -17
- package/dist/cli/routes/daemon/stop.js +70 -17
- package/dist/cli/routes/doctor.js +70 -17
- package/dist/cli/routes/events/index.d.ts +6 -0
- package/dist/cli/routes/events/index.js +900 -0
- package/dist/cli/routes/events/ls.d.ts +5 -0
- package/dist/cli/routes/events/ls.js +1798 -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 +2034 -0
- package/dist/cli/routes/events/tail.d.ts +6 -0
- package/dist/cli/routes/{events.js → events/tail.js} +235 -163
- package/dist/cli/routes/init.js +72 -19
- package/dist/cli/routes/keygen.js +70 -17
- package/dist/cli/routes/mcp.js +289 -166
- package/dist/cli/routes/sessions/index.d.ts +7 -0
- package/dist/cli/routes/sessions/index.js +900 -0
- package/dist/cli/routes/sessions/link.d.ts +4 -0
- package/dist/cli/routes/{link.js → sessions/link.js} +71 -18
- package/dist/cli/routes/sessions/ls.d.ts +4 -0
- package/dist/cli/routes/{ls.js → sessions/ls.js} +71 -18
- package/dist/cli/routes/sessions/revoke.d.ts +4 -0
- package/dist/cli/routes/{revoke.js → sessions/revoke.js} +72 -19
- package/dist/cli/routes/tools/call.d.ts +4 -0
- package/dist/cli/routes/{invoke.js → tools/call.js} +73 -20
- package/dist/cli/routes/tools/describe.d.ts +6 -0
- package/dist/cli/routes/tools/describe.js +1894 -0
- package/dist/cli/routes/tools/index.d.ts +6 -0
- package/dist/cli/routes/tools/index.js +900 -0
- package/dist/cli/routes/tools/ls.d.ts +4 -0
- package/dist/cli/routes/{tools.js → tools/ls.js} +98 -74
- 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 +178 -138
- package/dist/commands/events-ls.d.ts +21 -0
- package/dist/commands/events.d.ts +31 -14
- 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/calls.d.ts +3 -1
- package/dist/daemon/config.d.ts +1 -1
- package/dist/daemon/event-bus.d.ts +42 -0
- package/dist/daemon/registry.d.ts +20 -12
- package/dist/daemon/rpc-errors.d.ts +4 -1
- package/dist/daemon/sessions.d.ts +4 -2
- package/dist/errors.d.ts +1 -1
- package/dist/events/index.d.ts +60 -0
- package/dist/index.js +114 -40
- package/dist/link.d.ts +1 -1
- package/dist/mcp/app-tools.d.ts +22 -2
- package/dist/mcp/connect-tool.d.ts +1 -1
- package/dist/mcp/events-tool.d.ts +67 -27
- 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
package/dist/cli/routes/mcp.js
CHANGED
|
@@ -208,7 +208,7 @@ import {
|
|
|
208
208
|
McpError,
|
|
209
209
|
ReadResourceRequestSchema
|
|
210
210
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
211
|
-
import { RPC_METHODS as
|
|
211
|
+
import { RPC_METHODS as RPC_METHODS7 } from "@appduct/shared";
|
|
212
212
|
|
|
213
213
|
// src/daemon/calls.ts
|
|
214
214
|
import {
|
|
@@ -2656,40 +2656,167 @@ var resolveAppTool = async (call, selector, name) => {
|
|
|
2656
2656
|
|
|
2657
2657
|
// src/mcp/events-tool.ts
|
|
2658
2658
|
import {
|
|
2659
|
-
RPC_METHODS as
|
|
2659
|
+
RPC_METHODS as RPC_METHODS6,
|
|
2660
|
+
renderEventSignature,
|
|
2661
|
+
toAppEvent as toAppEvent2
|
|
2660
2662
|
} from "@appduct/shared";
|
|
2663
|
+
|
|
2664
|
+
// src/events/index.ts
|
|
2665
|
+
import {
|
|
2666
|
+
RPC_METHODS as RPC_METHODS5,
|
|
2667
|
+
toAppEvent
|
|
2668
|
+
} from "@appduct/shared";
|
|
2669
|
+
var WaitForAppEventTimeoutError = class extends Error {
|
|
2670
|
+
constructor(eventName, timeoutMs) {
|
|
2671
|
+
super(`Timed out after ${timeoutMs}ms waiting for event "${eventName}".`);
|
|
2672
|
+
this.eventName = eventName;
|
|
2673
|
+
this.timeoutMs = timeoutMs;
|
|
2674
|
+
this.name = "WaitForAppEventTimeoutError";
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
var WaitForAppEventConnectionClosedError = class extends Error {
|
|
2678
|
+
constructor(eventName) {
|
|
2679
|
+
super(`The connection to the Appduct daemon closed while waiting for event "${eventName}".`);
|
|
2680
|
+
this.eventName = eventName;
|
|
2681
|
+
this.name = "WaitForAppEventConnectionClosedError";
|
|
2682
|
+
}
|
|
2683
|
+
};
|
|
2684
|
+
var waitForAppEvent = async (stream, options) => {
|
|
2685
|
+
const { sessionId, name, since, timeoutMs, payloadMaxBytes } = options;
|
|
2686
|
+
let liveHandler = null;
|
|
2687
|
+
const earlyEvents = [];
|
|
2688
|
+
const unsubscribeNotification = stream.onNotification((payload) => {
|
|
2689
|
+
const event = payload;
|
|
2690
|
+
if (liveHandler) {
|
|
2691
|
+
liveHandler(event);
|
|
2692
|
+
} else {
|
|
2693
|
+
earlyEvents.push(event);
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
let unsubscribeClose = () => {
|
|
2697
|
+
};
|
|
2698
|
+
try {
|
|
2699
|
+
await stream.call(RPC_METHODS5.eventsSubscribe, {
|
|
2700
|
+
sessionSelector: sessionId,
|
|
2701
|
+
kinds: ["app_event"],
|
|
2702
|
+
name,
|
|
2703
|
+
payloadMaxBytes
|
|
2704
|
+
});
|
|
2705
|
+
const sinceResult = await stream.call(RPC_METHODS5.eventsSince, {
|
|
2706
|
+
selector: sessionId,
|
|
2707
|
+
since,
|
|
2708
|
+
name,
|
|
2709
|
+
payloadMaxBytes
|
|
2710
|
+
});
|
|
2711
|
+
const seenSeqs = /* @__PURE__ */ new Set();
|
|
2712
|
+
const backlog = [];
|
|
2713
|
+
for (const event of [...sinceResult.events, ...earlyEvents]) {
|
|
2714
|
+
if (event.sessionId === sessionId && !seenSeqs.has(event.seq)) {
|
|
2715
|
+
seenSeqs.add(event.seq);
|
|
2716
|
+
backlog.push(event);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
backlog.sort((a, b) => a.seq - b.seq);
|
|
2720
|
+
for (const event of backlog) {
|
|
2721
|
+
const appEvent = toAppEvent(event, sessionId);
|
|
2722
|
+
if (appEvent) {
|
|
2723
|
+
return { event: appEvent, dropped: sinceResult.dropped };
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
const highestConsideredSeq = backlog.length > 0 ? backlog[backlog.length - 1].seq : sinceResult.cursor;
|
|
2727
|
+
return await new Promise((resolve2, reject) => {
|
|
2728
|
+
let settled = false;
|
|
2729
|
+
const settle = (fn) => {
|
|
2730
|
+
if (settled) {
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
settled = true;
|
|
2734
|
+
clearTimeout(timer);
|
|
2735
|
+
liveHandler = null;
|
|
2736
|
+
fn();
|
|
2737
|
+
};
|
|
2738
|
+
const timer = setTimeout(() => {
|
|
2739
|
+
settle(() => reject(new WaitForAppEventTimeoutError(name, timeoutMs)));
|
|
2740
|
+
}, timeoutMs);
|
|
2741
|
+
unsubscribeClose = stream.onClose(() => {
|
|
2742
|
+
settle(() => reject(new WaitForAppEventConnectionClosedError(name)));
|
|
2743
|
+
});
|
|
2744
|
+
liveHandler = (event) => {
|
|
2745
|
+
if (settled) {
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
if (event.sessionId === sessionId && event.seq <= highestConsideredSeq) {
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
const appEvent = toAppEvent(event, sessionId);
|
|
2752
|
+
if (appEvent) {
|
|
2753
|
+
settle(() => resolve2({ event: appEvent, dropped: sinceResult.dropped }));
|
|
2754
|
+
}
|
|
2755
|
+
};
|
|
2756
|
+
});
|
|
2757
|
+
} finally {
|
|
2758
|
+
unsubscribeNotification();
|
|
2759
|
+
unsubscribeClose();
|
|
2760
|
+
}
|
|
2761
|
+
};
|
|
2762
|
+
|
|
2763
|
+
// src/mcp/events-tool.ts
|
|
2764
|
+
var LIST_EVENTS_TOOL_NAME = "appduct_list_events";
|
|
2661
2765
|
var EVENTS_TOOL_NAME = "appduct_events";
|
|
2662
2766
|
var WAIT_FOR_EVENT_TOOL_NAME = "appduct_wait_for_event";
|
|
2767
|
+
var DEFAULT_EVENTS_LIMIT = 50;
|
|
2768
|
+
var DEFAULT_EVENTS_PAYLOAD_MAX_BYTES = 4096;
|
|
2769
|
+
var DEFAULT_LIST_EVENTS_LIMIT = 50;
|
|
2663
2770
|
var DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS = 12e4;
|
|
2664
2771
|
var MAX_WAIT_FOR_EVENT_TIMEOUT_MS = 25 * 60 * 1e3;
|
|
2665
2772
|
var WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS = 6e4;
|
|
2666
2773
|
var clampWaitForEventTimeoutMs = (requestedMs) => {
|
|
2667
2774
|
return Math.min(requestedMs ?? DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS, MAX_WAIT_FOR_EVENT_TIMEOUT_MS);
|
|
2668
2775
|
};
|
|
2776
|
+
var LIST_EVENTS_TOOL_DESCRIPTOR = {
|
|
2777
|
+
name: LIST_EVENTS_TOOL_NAME,
|
|
2778
|
+
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 }.`,
|
|
2779
|
+
inputSchema: {
|
|
2780
|
+
type: "object",
|
|
2781
|
+
properties: {
|
|
2782
|
+
selector: {
|
|
2783
|
+
type: "string",
|
|
2784
|
+
minLength: 1,
|
|
2785
|
+
description: "Session alias or id. Omit to target the sole active/suspended session."
|
|
2786
|
+
},
|
|
2787
|
+
name: { type: "string", minLength: 1 },
|
|
2788
|
+
limit: { type: "integer", exclusiveMinimum: 0 },
|
|
2789
|
+
offset: { type: "integer", minimum: 0 }
|
|
2790
|
+
},
|
|
2791
|
+
additionalProperties: false
|
|
2792
|
+
}
|
|
2793
|
+
};
|
|
2669
2794
|
var EVENTS_TOOL_DESCRIPTOR = {
|
|
2670
2795
|
name: EVENTS_TOOL_NAME,
|
|
2671
|
-
description:
|
|
2796
|
+
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.`,
|
|
2672
2797
|
inputSchema: {
|
|
2673
2798
|
type: "object",
|
|
2674
2799
|
properties: {
|
|
2675
2800
|
selector: { type: "string" },
|
|
2676
2801
|
since: { type: "integer", minimum: 0 },
|
|
2677
|
-
limit: { type: "integer", exclusiveMinimum: 0 }
|
|
2802
|
+
limit: { type: "integer", exclusiveMinimum: 0 },
|
|
2803
|
+
name: { type: "string" },
|
|
2804
|
+
payloadMaxBytes: { type: "integer", exclusiveMinimum: 0 }
|
|
2678
2805
|
},
|
|
2679
2806
|
additionalProperties: false
|
|
2680
2807
|
}
|
|
2681
2808
|
};
|
|
2682
2809
|
var WAIT_FOR_EVENT_TOOL_DESCRIPTOR = {
|
|
2683
2810
|
name: WAIT_FOR_EVENT_TOOL_NAME,
|
|
2684
|
-
description:
|
|
2811
|
+
description: `Wait for the connected app to push an event via postEvent(name, payload) whose name matches name, a whole-name, case-sensitive glob (* matches any run of characters; a pattern without * is an exact name, e.g. "cart.*", or "*" for the next event of any name). Checks already-retained events first, so an event that already fired before this call still resolves immediately; pass since (a cursor from a previous appduct_events/appduct_wait_for_event call) to skip events you've already handled and wait only for a new one \u2014 omitting it can return an old match instantly on every call. Requires a claimed session to already exist (use appduct_wait_for_session first if not). Resolves with { sessionId, alias, name, payload, ts, seq, dropped } or, once a payload's JSON exceeds payloadMaxBytes (default 4096), with { payloadPreview, truncated: true, payloadBytes } in place of payload \u2014 check truncated before reading payload; dropped counts app events after since that were evicted before this call could return them (0 once nothing has fallen off). Rejects with tool_timeout after timeoutMs (default 120000ms, capped server-side at 1500000ms). A call still running after about two minutes moves to a Claude Code background task, so the result may arrive well after this call returns.`,
|
|
2685
2812
|
inputSchema: {
|
|
2686
2813
|
type: "object",
|
|
2687
2814
|
properties: {
|
|
2688
2815
|
selector: { type: "string" },
|
|
2689
2816
|
name: { type: "string" },
|
|
2690
|
-
match: { type: "object", additionalProperties: { type: ["string", "number", "boolean", "null"] } },
|
|
2691
2817
|
since: { type: "integer", minimum: 0 },
|
|
2692
|
-
timeoutMs: { type: "number", exclusiveMinimum: 0 }
|
|
2818
|
+
timeoutMs: { type: "number", exclusiveMinimum: 0 },
|
|
2819
|
+
payloadMaxBytes: { type: "integer", exclusiveMinimum: 0 }
|
|
2693
2820
|
},
|
|
2694
2821
|
required: ["name"],
|
|
2695
2822
|
additionalProperties: false
|
|
@@ -2745,169 +2872,108 @@ var rejectKinds = (args) => {
|
|
|
2745
2872
|
);
|
|
2746
2873
|
}
|
|
2747
2874
|
};
|
|
2748
|
-
var
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
}
|
|
2755
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2756
|
-
throw new McpBuiltinToolError("invalid_request", '"match" must be a JSON object.');
|
|
2757
|
-
}
|
|
2758
|
-
const record = value;
|
|
2759
|
-
for (const [key, entry] of Object.entries(record)) {
|
|
2760
|
-
if (!isMatchPrimitive(entry)) {
|
|
2761
|
-
throw new McpBuiltinToolError(
|
|
2762
|
-
"invalid_request",
|
|
2763
|
-
`"match.${key}" must be a string, number, boolean, or null (objects/arrays can never match).`
|
|
2764
|
-
);
|
|
2765
|
-
}
|
|
2875
|
+
var rejectMatch = (args) => {
|
|
2876
|
+
if (args.match !== void 0) {
|
|
2877
|
+
throw new McpBuiltinToolError(
|
|
2878
|
+
"invalid_request",
|
|
2879
|
+
'"match" is not supported: "name" is a glob, and a payload predicate is not offered on any surface \u2014 loop on appduct_wait_for_event with "since" instead.'
|
|
2880
|
+
);
|
|
2766
2881
|
}
|
|
2767
|
-
|
|
2882
|
+
};
|
|
2883
|
+
var handleListEventsTool = async (rawArgs, call) => {
|
|
2884
|
+
const args = asRecord3(rawArgs);
|
|
2885
|
+
rejectUnknownKeys(args, LIST_EVENTS_TOOL_NAME, ["selector", "name", "limit", "offset"]);
|
|
2886
|
+
const session = await resolveSession(call, asOptionalString(args.selector, "selector"));
|
|
2887
|
+
const name = asOptionalString(args.name, "name");
|
|
2888
|
+
const limit = args.limit ?? DEFAULT_LIST_EVENTS_LIMIT;
|
|
2889
|
+
const result = await call(RPC_METHODS6.eventsList, {
|
|
2890
|
+
selector: session.sessionId,
|
|
2891
|
+
...name !== void 0 ? { name } : {},
|
|
2892
|
+
limit,
|
|
2893
|
+
...args.offset !== void 0 && args.offset !== null ? { offset: args.offset } : {}
|
|
2894
|
+
});
|
|
2895
|
+
const lookup = name !== void 0 && !name.includes("*");
|
|
2896
|
+
return {
|
|
2897
|
+
session: session.alias,
|
|
2898
|
+
total: result.total,
|
|
2899
|
+
limit,
|
|
2900
|
+
events: result.events.map((event) => ({
|
|
2901
|
+
name: event.name,
|
|
2902
|
+
signature: renderEventSignature(event),
|
|
2903
|
+
description: event.description,
|
|
2904
|
+
...lookup && event.payload_schema !== void 0 ? { payload_schema: event.payload_schema } : {}
|
|
2905
|
+
}))
|
|
2906
|
+
};
|
|
2768
2907
|
};
|
|
2769
2908
|
var handleEventsTool = async (rawArgs, deps) => {
|
|
2770
2909
|
const args = asRecord3(rawArgs);
|
|
2771
2910
|
rejectKinds(args);
|
|
2772
|
-
|
|
2911
|
+
const result = await deps.call(RPC_METHODS6.eventsSince, {
|
|
2773
2912
|
selector: asOptionalString2(args.selector, "selector"),
|
|
2774
2913
|
since: asOptionalNonNegativeInteger(args.since, "since"),
|
|
2775
|
-
limit: asOptionalPositiveInteger(args.limit, "limit")
|
|
2914
|
+
limit: asOptionalPositiveInteger(args.limit, "limit") ?? DEFAULT_EVENTS_LIMIT,
|
|
2915
|
+
name: asOptionalString2(args.name, "name"),
|
|
2916
|
+
// Issue #113: this tool always caps a payload, so a caller reading the result straight into
|
|
2917
|
+
// its context can't be blown out by one gigantic app-pushed payload; `payloadMaxBytes` lets a
|
|
2918
|
+
// caller raise or lower that default.
|
|
2919
|
+
payloadMaxBytes: asOptionalPositiveInteger(args.payloadMaxBytes, "payloadMaxBytes") ?? DEFAULT_EVENTS_PAYLOAD_MAX_BYTES
|
|
2776
2920
|
});
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
return Object.entries(match).every(([key, value]) => record[key] === value);
|
|
2921
|
+
return {
|
|
2922
|
+
events: result.events.map((event) => event.sessionId === void 0 ? void 0 : toAppEvent2(event, event.sessionId)).filter((event) => event !== void 0),
|
|
2923
|
+
cursor: result.cursor,
|
|
2924
|
+
dropped: result.dropped,
|
|
2925
|
+
remaining: result.remaining
|
|
2926
|
+
};
|
|
2784
2927
|
};
|
|
2785
2928
|
var handleWaitForEventTool = async (rawArgs, deps) => {
|
|
2786
2929
|
const args = asRecord3(rawArgs);
|
|
2787
2930
|
rejectKinds(args);
|
|
2931
|
+
rejectMatch(args);
|
|
2788
2932
|
const selector = asOptionalString2(args.selector, "selector");
|
|
2789
2933
|
const name = asOptionalString2(args.name, "name");
|
|
2790
2934
|
if (!name) {
|
|
2791
2935
|
throw new McpBuiltinToolError("invalid_request", '"name" must be a non-empty string.');
|
|
2792
2936
|
}
|
|
2793
|
-
const match = asOptionalMatch(args.match);
|
|
2794
2937
|
const since = asOptionalNonNegativeInteger(args.since, "since");
|
|
2795
2938
|
const timeoutMs = clampWaitForEventTimeoutMs(asOptionalPositiveNumber2(args.timeoutMs, "timeoutMs"));
|
|
2939
|
+
const payloadMaxBytes = asOptionalPositiveInteger(args.payloadMaxBytes, "payloadMaxBytes") ?? DEFAULT_EVENTS_PAYLOAD_MAX_BYTES;
|
|
2796
2940
|
const stream = await openDaemonStream({ stateDir: deps.stateDir, spawn: deps.spawn });
|
|
2797
2941
|
try {
|
|
2798
|
-
const described = await stream.call(
|
|
2799
|
-
const
|
|
2800
|
-
const
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
return { sessionId, alias, name, payload: data.payload, ts: event.ts, seq: event.seq };
|
|
2813
|
-
};
|
|
2814
|
-
let liveHandler = null;
|
|
2815
|
-
const earlyEvents = [];
|
|
2816
|
-
const unsubscribeNotification = stream.onNotification((payload) => {
|
|
2817
|
-
const event = payload;
|
|
2818
|
-
if (liveHandler) {
|
|
2819
|
-
liveHandler(event);
|
|
2820
|
-
} else {
|
|
2821
|
-
earlyEvents.push(event);
|
|
2822
|
-
}
|
|
2823
|
-
});
|
|
2824
|
-
let unsubscribeClose = () => {
|
|
2825
|
-
};
|
|
2942
|
+
const described = await stream.call(RPC_METHODS6.sessionsDescribe, { selector });
|
|
2943
|
+
const startedAt = Date.now();
|
|
2944
|
+
const progressTimer = deps.progress ? setInterval(() => {
|
|
2945
|
+
deps.progress.sendNotification({
|
|
2946
|
+
method: "notifications/progress",
|
|
2947
|
+
params: {
|
|
2948
|
+
progressToken: deps.progress.token,
|
|
2949
|
+
progress: Date.now() - startedAt,
|
|
2950
|
+
total: timeoutMs,
|
|
2951
|
+
message: `Still waiting for event "${name}"...`
|
|
2952
|
+
}
|
|
2953
|
+
}).catch(() => {
|
|
2954
|
+
});
|
|
2955
|
+
}, WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS) : void 0;
|
|
2826
2956
|
try {
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
since
|
|
2957
|
+
const { event, dropped } = await waitForAppEvent(stream, {
|
|
2958
|
+
sessionId: described.sessionId,
|
|
2959
|
+
name,
|
|
2960
|
+
since,
|
|
2961
|
+
timeoutMs,
|
|
2962
|
+
payloadMaxBytes
|
|
2831
2963
|
});
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
seenSeqs.add(event.seq);
|
|
2837
|
-
backlog.push(event);
|
|
2838
|
-
}
|
|
2964
|
+
return { ...event, dropped };
|
|
2965
|
+
} catch (error) {
|
|
2966
|
+
if (error instanceof WaitForAppEventTimeoutError) {
|
|
2967
|
+
throw new McpBuiltinToolError("tool_timeout", error.message);
|
|
2839
2968
|
}
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
const result = toResult(event);
|
|
2843
|
-
if (result) {
|
|
2844
|
-
return result;
|
|
2845
|
-
}
|
|
2969
|
+
if (error instanceof WaitForAppEventConnectionClosedError) {
|
|
2970
|
+
throw new McpBuiltinToolError("tool_execution_error", error.message);
|
|
2846
2971
|
}
|
|
2847
|
-
|
|
2848
|
-
return await new Promise((resolve2, reject) => {
|
|
2849
|
-
let settled = false;
|
|
2850
|
-
const settle = (fn) => {
|
|
2851
|
-
if (settled) {
|
|
2852
|
-
return;
|
|
2853
|
-
}
|
|
2854
|
-
settled = true;
|
|
2855
|
-
clearTimeout(timer);
|
|
2856
|
-
if (progressTimer) {
|
|
2857
|
-
clearInterval(progressTimer);
|
|
2858
|
-
}
|
|
2859
|
-
liveHandler = null;
|
|
2860
|
-
fn();
|
|
2861
|
-
};
|
|
2862
|
-
const timer = setTimeout(() => {
|
|
2863
|
-
settle(
|
|
2864
|
-
() => reject(
|
|
2865
|
-
new McpBuiltinToolError(
|
|
2866
|
-
"tool_timeout",
|
|
2867
|
-
`Timed out after ${timeoutMs}ms waiting for event "${name}".`
|
|
2868
|
-
)
|
|
2869
|
-
)
|
|
2870
|
-
);
|
|
2871
|
-
}, timeoutMs);
|
|
2872
|
-
const startedAt = Date.now();
|
|
2873
|
-
const progressTimer = deps.progress ? setInterval(() => {
|
|
2874
|
-
deps.progress.sendNotification({
|
|
2875
|
-
method: "notifications/progress",
|
|
2876
|
-
params: {
|
|
2877
|
-
progressToken: deps.progress.token,
|
|
2878
|
-
progress: Date.now() - startedAt,
|
|
2879
|
-
total: timeoutMs,
|
|
2880
|
-
message: `Still waiting for event "${name}"...`
|
|
2881
|
-
}
|
|
2882
|
-
}).catch(() => {
|
|
2883
|
-
});
|
|
2884
|
-
}, WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS) : void 0;
|
|
2885
|
-
unsubscribeClose = stream.onClose(() => {
|
|
2886
|
-
settle(
|
|
2887
|
-
() => reject(
|
|
2888
|
-
new McpBuiltinToolError(
|
|
2889
|
-
"tool_execution_error",
|
|
2890
|
-
`The connection to the Appduct daemon closed while waiting for event "${name}".`
|
|
2891
|
-
)
|
|
2892
|
-
)
|
|
2893
|
-
);
|
|
2894
|
-
});
|
|
2895
|
-
liveHandler = (event) => {
|
|
2896
|
-
if (settled) {
|
|
2897
|
-
return;
|
|
2898
|
-
}
|
|
2899
|
-
if (event.sessionId === sessionId && event.seq <= highestConsideredSeq) {
|
|
2900
|
-
return;
|
|
2901
|
-
}
|
|
2902
|
-
const result = toResult(event);
|
|
2903
|
-
if (result) {
|
|
2904
|
-
settle(() => resolve2(result));
|
|
2905
|
-
}
|
|
2906
|
-
};
|
|
2907
|
-
});
|
|
2972
|
+
throw error;
|
|
2908
2973
|
} finally {
|
|
2909
|
-
|
|
2910
|
-
|
|
2974
|
+
if (progressTimer) {
|
|
2975
|
+
clearInterval(progressTimer);
|
|
2976
|
+
}
|
|
2911
2977
|
}
|
|
2912
2978
|
} finally {
|
|
2913
2979
|
stream.close();
|
|
@@ -3047,7 +3113,7 @@ var createMcpServer = async (options) => {
|
|
|
3047
3113
|
}
|
|
3048
3114
|
if (progressToken === void 0) {
|
|
3049
3115
|
const result = await stream.call(
|
|
3050
|
-
|
|
3116
|
+
RPC_METHODS7.toolsCall,
|
|
3051
3117
|
{
|
|
3052
3118
|
selector: tool.sessionId,
|
|
3053
3119
|
name: tool.descriptor.name,
|
|
@@ -3062,7 +3128,7 @@ var createMcpServer = async (options) => {
|
|
|
3062
3128
|
}
|
|
3063
3129
|
const progressStream = await openStream({ stateDir: options.stateDir, spawn: options.spawn });
|
|
3064
3130
|
try {
|
|
3065
|
-
await progressStream.call(
|
|
3131
|
+
await progressStream.call(RPC_METHODS7.eventsSubscribe, {
|
|
3066
3132
|
sessionSelector: tool.sessionId,
|
|
3067
3133
|
kinds: ["tool_call_started", "tool_call_progress"]
|
|
3068
3134
|
});
|
|
@@ -3072,7 +3138,7 @@ var createMcpServer = async (options) => {
|
|
|
3072
3138
|
if (callId === void 0 || !cancelRequested) {
|
|
3073
3139
|
return;
|
|
3074
3140
|
}
|
|
3075
|
-
progressStream.call(
|
|
3141
|
+
progressStream.call(RPC_METHODS7.toolsCancel, { selector: tool.sessionId, callId, reason: "mcp_client_cancelled" }).catch((error) => {
|
|
3076
3142
|
console.error("appduct mcp: failed to cancel an app tool call:", error);
|
|
3077
3143
|
});
|
|
3078
3144
|
};
|
|
@@ -3105,7 +3171,7 @@ var createMcpServer = async (options) => {
|
|
|
3105
3171
|
});
|
|
3106
3172
|
try {
|
|
3107
3173
|
const result = await progressStream.call(
|
|
3108
|
-
|
|
3174
|
+
RPC_METHODS7.toolsCall,
|
|
3109
3175
|
{
|
|
3110
3176
|
selector: tool.sessionId,
|
|
3111
3177
|
name: tool.descriptor.name,
|
|
@@ -3133,6 +3199,7 @@ var createMcpServer = async (options) => {
|
|
|
3133
3199
|
LIST_TOOLS_TOOL_DESCRIPTOR,
|
|
3134
3200
|
DESCRIBE_TOOL_TOOL_DESCRIPTOR,
|
|
3135
3201
|
CALL_TOOL_TOOL_DESCRIPTOR,
|
|
3202
|
+
LIST_EVENTS_TOOL_DESCRIPTOR,
|
|
3136
3203
|
EVENTS_TOOL_DESCRIPTOR,
|
|
3137
3204
|
WAIT_FOR_EVENT_TOOL_DESCRIPTOR
|
|
3138
3205
|
]
|
|
@@ -3161,6 +3228,9 @@ var createMcpServer = async (options) => {
|
|
|
3161
3228
|
await handleWaitForSessionTool(args, { stateDir: options.stateDir, spawn: options.spawn })
|
|
3162
3229
|
);
|
|
3163
3230
|
}
|
|
3231
|
+
if (name === LIST_EVENTS_TOOL_NAME) {
|
|
3232
|
+
return toolSuccessContent(await handleListEventsTool(args, stream.call));
|
|
3233
|
+
}
|
|
3164
3234
|
if (name === EVENTS_TOOL_NAME) {
|
|
3165
3235
|
return toolSuccessContent(await handleEventsTool(args, { call: stream.call }));
|
|
3166
3236
|
}
|
|
@@ -3217,7 +3287,7 @@ var createMcpServer = async (options) => {
|
|
|
3217
3287
|
if (request.params.uri !== SESSIONS_RESOURCE_URI) {
|
|
3218
3288
|
throw new Error(`Unknown resource "${request.params.uri}".`);
|
|
3219
3289
|
}
|
|
3220
|
-
const sessions = await stream.call(
|
|
3290
|
+
const sessions = await stream.call(RPC_METHODS7.sessionsList);
|
|
3221
3291
|
return {
|
|
3222
3292
|
contents: [{ uri: SESSIONS_RESOURCE_URI, mimeType: "application/json", text: JSON.stringify(sessions) }]
|
|
3223
3293
|
};
|
|
@@ -3309,6 +3379,7 @@ var formatJson = (value, flags) => {
|
|
|
3309
3379
|
import pc from "picocolors";
|
|
3310
3380
|
import {
|
|
3311
3381
|
formatAgentWebSocketUrl,
|
|
3382
|
+
renderEventSignature as renderEventSignature2,
|
|
3312
3383
|
renderToolSignature as renderToolSignature2,
|
|
3313
3384
|
summarizeToolDescription as summarizeToolDescription2
|
|
3314
3385
|
} from "@appduct/shared";
|
|
@@ -3367,6 +3438,13 @@ var formatDevice = (device) => {
|
|
|
3367
3438
|
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
3368
3439
|
return device.os ? `${label} (${device.os})` : label;
|
|
3369
3440
|
};
|
|
3441
|
+
var SUSPEND_REASON_LABELS = {
|
|
3442
|
+
app_backgrounded: "app in background",
|
|
3443
|
+
connection_lost: "connection lost"
|
|
3444
|
+
};
|
|
3445
|
+
var formatSessionState = (session) => {
|
|
3446
|
+
return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
|
|
3447
|
+
};
|
|
3370
3448
|
var renderLsData = (colors, data, now) => {
|
|
3371
3449
|
if (data.length === 0) {
|
|
3372
3450
|
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
@@ -3375,7 +3453,7 @@ var renderLsData = (colors, data, now) => {
|
|
|
3375
3453
|
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
3376
3454
|
const rows = data.map((session) => [
|
|
3377
3455
|
session.alias,
|
|
3378
|
-
session
|
|
3456
|
+
formatSessionState(session),
|
|
3379
3457
|
formatDevice(session.device),
|
|
3380
3458
|
String(session.toolCount),
|
|
3381
3459
|
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
@@ -3389,9 +3467,6 @@ var renderLsData = (colors, data, now) => {
|
|
|
3389
3467
|
...rows.map((row) => ` ${row.map((cell, index) => cell.padEnd(widths[index])).join(" ")}`)
|
|
3390
3468
|
];
|
|
3391
3469
|
};
|
|
3392
|
-
var isToolsListing = (data) => {
|
|
3393
|
-
return typeof data === "object" && data !== null && Array.isArray(data.tools);
|
|
3394
|
-
};
|
|
3395
3470
|
var isToolGroupsListing = (data) => {
|
|
3396
3471
|
return typeof data === "object" && data !== null && Array.isArray(data.groups) && !Array.isArray(data.tools);
|
|
3397
3472
|
};
|
|
@@ -3409,7 +3484,7 @@ var renderEmptyToolsLine = (data) => {
|
|
|
3409
3484
|
}
|
|
3410
3485
|
if (data.group !== void 0) {
|
|
3411
3486
|
const match = data.filter === void 0 ? "" : ` match ${JSON.stringify(data.filter)}`;
|
|
3412
|
-
return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools --groups\` to see the session's groups.`;
|
|
3487
|
+
return ` No tools in group ${JSON.stringify(data.group)}${match}. Run \`appduct tools ls --groups\` to see the session's groups.`;
|
|
3413
3488
|
}
|
|
3414
3489
|
return data.filter === void 0 ? " No tools registered." : ` No tools match ${JSON.stringify(data.filter)}.`;
|
|
3415
3490
|
};
|
|
@@ -3491,7 +3566,7 @@ var renderToolSummaryTable = (colors, data) => {
|
|
|
3491
3566
|
...grouped ? renderGroupedToolLines(colors, data.tools) : data.tools.flatMap((tool) => renderToolSummaryLines(tool, " ")),
|
|
3492
3567
|
...renderTruncationLine(data),
|
|
3493
3568
|
"",
|
|
3494
|
-
"Run `appduct tools <name>` for a tool's full schema."
|
|
3569
|
+
"Run `appduct tools describe <name>` for a tool's full schema."
|
|
3495
3570
|
];
|
|
3496
3571
|
};
|
|
3497
3572
|
var renderToolDetail = (colors, tool, flags) => {
|
|
@@ -3543,18 +3618,62 @@ var renderToolGroups = (colors, data) => {
|
|
|
3543
3618
|
colors.green("Groups"),
|
|
3544
3619
|
...data.groups.map((entry) => ` ${label(entry).padEnd(width)} ${String(entry.total).padStart(countWidth)}`),
|
|
3545
3620
|
"",
|
|
3546
|
-
`${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.")
|
|
3621
|
+
`${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.")
|
|
3547
3622
|
];
|
|
3548
3623
|
};
|
|
3549
|
-
var
|
|
3624
|
+
var renderToolsListData = (colors, data, flags, full) => {
|
|
3550
3625
|
if (isToolGroupsListing(data)) {
|
|
3551
3626
|
return renderToolGroups(colors, data);
|
|
3552
3627
|
}
|
|
3553
|
-
if (!isToolsListing(data)) {
|
|
3554
|
-
return renderToolDetail(colors, data, flags);
|
|
3555
|
-
}
|
|
3556
3628
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
3557
3629
|
};
|
|
3630
|
+
var renderEventsTruncationLine = (data) => {
|
|
3631
|
+
const offset = data.offset ?? 0;
|
|
3632
|
+
if (data.events.length >= data.total) {
|
|
3633
|
+
return [];
|
|
3634
|
+
}
|
|
3635
|
+
return [
|
|
3636
|
+
"",
|
|
3637
|
+
`Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
|
|
3638
|
+
];
|
|
3639
|
+
};
|
|
3640
|
+
var renderEventsListData = (colors, data, flags) => {
|
|
3641
|
+
const [only] = data.events;
|
|
3642
|
+
if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
|
|
3643
|
+
return renderFields(
|
|
3644
|
+
colors.green(`Event: ${only.name}`),
|
|
3645
|
+
[
|
|
3646
|
+
["Signature", renderEventSignature2(only)],
|
|
3647
|
+
["Description", only.description],
|
|
3648
|
+
["Payload schema", only.payload_schema]
|
|
3649
|
+
],
|
|
3650
|
+
flags
|
|
3651
|
+
);
|
|
3652
|
+
}
|
|
3653
|
+
if (data.events.length === 0 && data.total > 0) {
|
|
3654
|
+
return [
|
|
3655
|
+
colors.green("Events"),
|
|
3656
|
+
` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
|
|
3657
|
+
];
|
|
3658
|
+
}
|
|
3659
|
+
if (data.events.length === 0) {
|
|
3660
|
+
return [
|
|
3661
|
+
colors.green("Events"),
|
|
3662
|
+
data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
|
|
3663
|
+
];
|
|
3664
|
+
}
|
|
3665
|
+
const lines = data.events.flatMap((event) => [
|
|
3666
|
+
` ${renderEventSignature2(event)}`,
|
|
3667
|
+
` ${summarizeToolDescription2(event.description)}`
|
|
3668
|
+
]);
|
|
3669
|
+
return [
|
|
3670
|
+
colors.green("Events"),
|
|
3671
|
+
...lines,
|
|
3672
|
+
...renderEventsTruncationLine(data),
|
|
3673
|
+
"",
|
|
3674
|
+
"Run `appduct events ls --name <name>` for an event's full payload schema."
|
|
3675
|
+
];
|
|
3676
|
+
};
|
|
3558
3677
|
var renderInvokeData = (colors, data, flags) => {
|
|
3559
3678
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
3560
3679
|
};
|
|
@@ -3738,15 +3857,19 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
3738
3857
|
return renderInitData(colors, data, flags);
|
|
3739
3858
|
case "keygen":
|
|
3740
3859
|
return renderKeygenData(colors, data, flags);
|
|
3741
|
-
case "link":
|
|
3860
|
+
case "sessions link":
|
|
3742
3861
|
return renderLinkData(colors, data, flags, options.qr);
|
|
3743
|
-
case "ls":
|
|
3862
|
+
case "sessions ls":
|
|
3744
3863
|
return renderLsData(colors, data, options.now ?? /* @__PURE__ */ new Date());
|
|
3745
|
-
case "tools":
|
|
3746
|
-
return
|
|
3747
|
-
case "
|
|
3864
|
+
case "tools ls":
|
|
3865
|
+
return renderToolsListData(colors, data, flags, options.full);
|
|
3866
|
+
case "tools describe":
|
|
3867
|
+
return renderToolDetail(colors, data, flags);
|
|
3868
|
+
case "events ls":
|
|
3869
|
+
return renderEventsListData(colors, data, flags);
|
|
3870
|
+
case "tools call":
|
|
3748
3871
|
return renderInvokeData(colors, data, flags);
|
|
3749
|
-
case "revoke":
|
|
3872
|
+
case "sessions revoke":
|
|
3750
3873
|
return renderRevokeData(colors, data);
|
|
3751
3874
|
case "daemon run":
|
|
3752
3875
|
return renderDaemonRunData(colors, data, flags);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route for `appduct sessions` — itself a router over the session verbs, so `sessions ls` loads
|
|
3
|
+
* only `ls.ts` (issue #96). cac matches only the noun (`cli/create-cli.ts`), so the verb arrives
|
|
4
|
+
* here as the first positional rather than as a cac command of its own — the same shape
|
|
5
|
+
* `routes/daemon/index.ts` already uses for `daemon status`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const route: import("../../router.js").Route;
|