appduct 0.9.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/LICENSE +21 -0
- package/README.md +274 -0
- package/bin.js +2 -0
- package/dist/artifact-inspect.d.ts +110 -0
- package/dist/artifact-inspect.d.ts.map +1 -0
- package/dist/artifact-inspect.js +315 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/cli/command-options.d.ts +44 -0
- package/dist/cli/command-options.d.ts.map +1 -0
- package/dist/cli/command-options.js +95 -0
- package/dist/cli/create-cli.d.ts +2 -0
- package/dist/cli/create-cli.d.ts.map +1 -0
- package/dist/cli/create-cli.js +56 -0
- package/dist/cli/dispatch.d.ts +3 -0
- package/dist/cli/dispatch.d.ts.map +1 -0
- package/dist/cli/dispatch.js +275 -0
- package/dist/cli/open-target.d.ts +112 -0
- package/dist/cli/open-target.d.ts.map +1 -0
- package/dist/cli/open-target.js +438 -0
- package/dist/cli/result-types.d.ts +194 -0
- package/dist/cli/result-types.d.ts.map +1 -0
- package/dist/cli/result-types.js +6 -0
- package/dist/cli/runner.d.ts +35 -0
- package/dist/cli/runner.d.ts.map +1 -0
- package/dist/cli/runner.js +111 -0
- package/dist/cli/types.d.ts +20 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli/types.js +10 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/client/app-client.d.ts +126 -0
- package/dist/client/app-client.d.ts.map +1 -0
- package/dist/client/app-client.js +223 -0
- package/dist/client/bootstrap.d.ts +48 -0
- package/dist/client/bootstrap.d.ts.map +1 -0
- package/dist/client/bootstrap.js +108 -0
- package/dist/client/connect.d.ts +17 -0
- package/dist/client/connect.d.ts.map +1 -0
- package/dist/client/connect.js +38 -0
- package/dist/client/errors.d.ts +26 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +40 -0
- package/dist/client/index.d.ts +19 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +16 -0
- package/dist/commands/daemon.d.ts +22 -0
- package/dist/commands/daemon.d.ts.map +1 -0
- package/dist/commands/daemon.js +128 -0
- package/dist/commands/doctor.d.ts +23 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +42 -0
- package/dist/commands/events.d.ts +37 -0
- package/dist/commands/events.d.ts.map +1 -0
- package/dist/commands/events.js +76 -0
- package/dist/commands/init.d.ts +51 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +231 -0
- package/dist/commands/invoke.d.ts +22 -0
- package/dist/commands/invoke.d.ts.map +1 -0
- package/dist/commands/invoke.js +54 -0
- package/dist/commands/keygen.d.ts +23 -0
- package/dist/commands/keygen.d.ts.map +1 -0
- package/dist/commands/keygen.js +46 -0
- package/dist/commands/link.d.ts +30 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +43 -0
- package/dist/commands/ls.d.ts +9 -0
- package/dist/commands/ls.d.ts.map +1 -0
- package/dist/commands/ls.js +7 -0
- package/dist/commands/mcp.d.ts +49 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +98 -0
- package/dist/commands/revoke.d.ts +12 -0
- package/dist/commands/revoke.d.ts.map +1 -0
- package/dist/commands/revoke.js +7 -0
- package/dist/commands/tools.d.ts +23 -0
- package/dist/commands/tools.d.ts.map +1 -0
- package/dist/commands/tools.js +52 -0
- package/dist/daemon/address.d.ts +30 -0
- package/dist/daemon/address.d.ts.map +1 -0
- package/dist/daemon/address.js +71 -0
- package/dist/daemon/audit.d.ts +107 -0
- package/dist/daemon/audit.d.ts.map +1 -0
- package/dist/daemon/audit.js +237 -0
- package/dist/daemon/calls.d.ts +84 -0
- package/dist/daemon/calls.d.ts.map +1 -0
- package/dist/daemon/calls.js +158 -0
- package/dist/daemon/config.d.ts +79 -0
- package/dist/daemon/config.d.ts.map +1 -0
- package/dist/daemon/config.js +170 -0
- package/dist/daemon/daemon.d.ts +43 -0
- package/dist/daemon/daemon.d.ts.map +1 -0
- package/dist/daemon/daemon.js +560 -0
- package/dist/daemon/event-bus.d.ts +49 -0
- package/dist/daemon/event-bus.d.ts.map +1 -0
- package/dist/daemon/event-bus.js +111 -0
- package/dist/daemon/links.d.ts +54 -0
- package/dist/daemon/links.d.ts.map +1 -0
- package/dist/daemon/links.js +118 -0
- package/dist/daemon/listener.d.ts +34 -0
- package/dist/daemon/listener.d.ts.map +1 -0
- package/dist/daemon/listener.js +132 -0
- package/dist/daemon/log-rotation.d.ts +82 -0
- package/dist/daemon/log-rotation.d.ts.map +1 -0
- package/dist/daemon/log-rotation.js +136 -0
- package/dist/daemon/pidfile.d.ts +38 -0
- package/dist/daemon/pidfile.d.ts.map +1 -0
- package/dist/daemon/pidfile.js +90 -0
- package/dist/daemon/policy.d.ts +22 -0
- package/dist/daemon/policy.d.ts.map +1 -0
- package/dist/daemon/policy.js +24 -0
- package/dist/daemon/registry.d.ts +19 -0
- package/dist/daemon/registry.d.ts.map +1 -0
- package/dist/daemon/registry.js +31 -0
- package/dist/daemon/rpc-server.d.ts +54 -0
- package/dist/daemon/rpc-server.d.ts.map +1 -0
- package/dist/daemon/rpc-server.js +203 -0
- package/dist/daemon/sessions.d.ts +86 -0
- package/dist/daemon/sessions.d.ts.map +1 -0
- package/dist/daemon/sessions.js +429 -0
- package/dist/daemon/socket-probe.d.ts +16 -0
- package/dist/daemon/socket-probe.d.ts.map +1 -0
- package/dist/daemon/socket-probe.js +28 -0
- package/dist/daemon/state-dir.d.ts +31 -0
- package/dist/daemon/state-dir.d.ts.map +1 -0
- package/dist/daemon/state-dir.js +43 -0
- package/dist/daemon/timers.d.ts +16 -0
- package/dist/daemon/timers.d.ts.map +1 -0
- package/dist/daemon/timers.js +12 -0
- package/dist/daemon/tls.d.ts +58 -0
- package/dist/daemon/tls.d.ts.map +1 -0
- package/dist/daemon/tls.js +79 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +162 -0
- package/dist/host-certificate.d.ts +8 -0
- package/dist/host-certificate.d.ts.map +1 -0
- package/dist/host-certificate.js +111 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/key-material.d.ts +14 -0
- package/dist/key-material.d.ts.map +1 -0
- package/dist/key-material.js +42 -0
- package/dist/link.d.ts +76 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +117 -0
- package/dist/mcp/connect-tool.d.ts +115 -0
- package/dist/mcp/connect-tool.d.ts.map +1 -0
- package/dist/mcp/connect-tool.js +396 -0
- package/dist/mcp/daemon-tools.d.ts +13 -0
- package/dist/mcp/daemon-tools.d.ts.map +1 -0
- package/dist/mcp/daemon-tools.js +29 -0
- package/dist/mcp/events-tool.d.ts +111 -0
- package/dist/mcp/events-tool.d.ts.map +1 -0
- package/dist/mcp/events-tool.js +322 -0
- package/dist/mcp/server.d.ts +58 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +538 -0
- package/dist/mcp/tool-mapping.d.ts +58 -0
- package/dist/mcp/tool-mapping.d.ts.map +1 -0
- package/dist/mcp/tool-mapping.js +143 -0
- package/dist/mcp/tool-namespace.d.ts +40 -0
- package/dist/mcp/tool-namespace.d.ts.map +1 -0
- package/dist/mcp/tool-namespace.js +65 -0
- package/dist/native-scheme.d.ts +59 -0
- package/dist/native-scheme.d.ts.map +1 -0
- package/dist/native-scheme.js +465 -0
- package/dist/output.d.ts +28 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +342 -0
- package/dist/package-version.d.ts +30 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +47 -0
- package/dist/qr-terminal.d.ts +13 -0
- package/dist/qr-terminal.d.ts.map +1 -0
- package/dist/qr-terminal.js +45 -0
- package/dist/rpc/client.d.ts +137 -0
- package/dist/rpc/client.d.ts.map +1 -0
- package/dist/rpc/client.js +819 -0
- package/dist/scheme.d.ts +181 -0
- package/dist/scheme.d.ts.map +1 -0
- package/dist/scheme.js +321 -0
- package/dist/spki-pin.d.ts +3 -0
- package/dist/spki-pin.d.ts.map +1 -0
- package/dist/spki-pin.js +14 -0
- package/package.json +65 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches the effective (possibly namespaced) MCP tool list live from the daemon
|
|
3
|
+
* (ARCHITECTURE.md §9): `sessions.list` for the live sessions, then `tools.list` per session.
|
|
4
|
+
* There is no caching here — every `tools/list` request and every list-changed check re-fetches,
|
|
5
|
+
* since the daemon's session/registry state is the single source of truth and can change between
|
|
6
|
+
* any two calls.
|
|
7
|
+
*/
|
|
8
|
+
import { RPC_METHODS, } from "@appduct/shared";
|
|
9
|
+
import { DaemonRpcError } from "../rpc/client.js";
|
|
10
|
+
import { buildNamespacedTools } from "./tool-namespace.js";
|
|
11
|
+
export const fetchEffectiveTools = async (call) => {
|
|
12
|
+
const sessions = await call(RPC_METHODS.sessionsList);
|
|
13
|
+
const toolsByAlias = new Map();
|
|
14
|
+
for (const session of sessions) {
|
|
15
|
+
try {
|
|
16
|
+
toolsByAlias.set(session.alias, await call(RPC_METHODS.toolsList, { selector: session.alias }));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
// The session can transition (revoke/expire) between `sessions.list` and this per-session
|
|
20
|
+
// `tools.list` call; treat it as having no tools rather than failing the whole listing.
|
|
21
|
+
if (error instanceof DaemonRpcError) {
|
|
22
|
+
toolsByAlias.set(session.alias, []);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return buildNamespacedTools(sessions, toolsByAlias);
|
|
29
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in `appduct_events` / `appduct_wait_for_event` MCP tools (issue #6): a pull
|
|
3
|
+
* surface over the daemon's `app_event` plumbing (`postEvent()` on the app side, `events.since`'s
|
|
4
|
+
* per-session retention buffer on the daemon side — see `daemon/event-bus.ts`). MCP is
|
|
5
|
+
* request/response, so an agent otherwise has no way to observe anything the app pushes.
|
|
6
|
+
*
|
|
7
|
+
* `appduct_wait_for_event` drains the retained buffer for an already-arrived match before
|
|
8
|
+
* falling back to a live wait, exactly to avoid the race `appduct_wait_for_session` doesn't
|
|
9
|
+
* have to worry about (a session is either claimed or it isn't, but an `app_event` can easily fire
|
|
10
|
+
* between "the agent decides to wait" and "the wait subscription lands"). The live notification
|
|
11
|
+
* listener is registered *before* the `events.since` drain call is even sent — not merely before
|
|
12
|
+
* it resolves — because the daemon's response to that call and a subsequent `notify()` for a new
|
|
13
|
+
* event can arrive in the same socket chunk; `rpc/client.ts` fans a chunk's notification lines out
|
|
14
|
+
* to whatever listeners exist at the moment it's processed, so a listener added only after `await
|
|
15
|
+
* stream.call(eventsSince, …)` returns can miss an event that was already in that same chunk.
|
|
16
|
+
* Arrivals before the drain has been merged into the backlog are buffered, then merged in
|
|
17
|
+
* (deduped by `seq`) before falling through to the live phase — see `handleWaitForEventTool` below.
|
|
18
|
+
*/
|
|
19
|
+
import { type EventsSinceResult } from "@appduct/shared";
|
|
20
|
+
import { type SpawnFn } from "../rpc/client.js";
|
|
21
|
+
import type { DaemonCall } from "./daemon-tools.js";
|
|
22
|
+
export declare const EVENTS_TOOL_NAME = "appduct_events";
|
|
23
|
+
export declare const WAIT_FOR_EVENT_TOOL_NAME = "appduct_wait_for_event";
|
|
24
|
+
/** Server-side cap on `timeoutMs` (issue #6's "Cap timeoutMs server-side below the idle window
|
|
25
|
+
* rather than trusting the caller") — a pure function so the cap's boundary behavior is
|
|
26
|
+
* unit-testable without spinning up a daemon or actually waiting out either bound. */
|
|
27
|
+
export declare const clampWaitForEventTimeoutMs: (requestedMs: number | undefined) => number;
|
|
28
|
+
export declare const EVENTS_TOOL_DESCRIPTOR: {
|
|
29
|
+
readonly name: "appduct_events";
|
|
30
|
+
readonly description: string;
|
|
31
|
+
readonly inputSchema: {
|
|
32
|
+
readonly type: "object";
|
|
33
|
+
readonly properties: {
|
|
34
|
+
readonly selector: {
|
|
35
|
+
readonly type: "string";
|
|
36
|
+
};
|
|
37
|
+
readonly since: {
|
|
38
|
+
readonly type: "integer";
|
|
39
|
+
readonly minimum: 0;
|
|
40
|
+
};
|
|
41
|
+
readonly kinds: {
|
|
42
|
+
readonly type: "array";
|
|
43
|
+
readonly items: {
|
|
44
|
+
readonly type: "string";
|
|
45
|
+
readonly enum: readonly ["daemon_started", "link_created", "link_expired", "session_claimed", "session_suspended", "session_resumed", "session_revoked", "session_expired", "tools_changed", "app_event", "tool_call_started", "tool_call_progress", "tool_call_finished"];
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
readonly limit: {
|
|
49
|
+
readonly type: "integer";
|
|
50
|
+
readonly exclusiveMinimum: 0;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
readonly additionalProperties: false;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export declare const WAIT_FOR_EVENT_TOOL_DESCRIPTOR: {
|
|
57
|
+
readonly name: "appduct_wait_for_event";
|
|
58
|
+
readonly description: string;
|
|
59
|
+
readonly inputSchema: {
|
|
60
|
+
readonly type: "object";
|
|
61
|
+
readonly properties: {
|
|
62
|
+
readonly selector: {
|
|
63
|
+
readonly type: "string";
|
|
64
|
+
};
|
|
65
|
+
readonly name: {
|
|
66
|
+
readonly type: "string";
|
|
67
|
+
};
|
|
68
|
+
readonly match: {
|
|
69
|
+
readonly type: "object";
|
|
70
|
+
readonly additionalProperties: {
|
|
71
|
+
readonly type: readonly ["string", "number", "boolean", "null"];
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
readonly since: {
|
|
75
|
+
readonly type: "integer";
|
|
76
|
+
readonly minimum: 0;
|
|
77
|
+
};
|
|
78
|
+
readonly timeoutMs: {
|
|
79
|
+
readonly type: "number";
|
|
80
|
+
readonly exclusiveMinimum: 0;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
readonly required: readonly ["name"];
|
|
84
|
+
readonly additionalProperties: false;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
export type EventsToolDeps = {
|
|
88
|
+
call: DaemonCall;
|
|
89
|
+
};
|
|
90
|
+
export declare const handleEventsTool: (rawArgs: unknown, deps: EventsToolDeps) => Promise<EventsSinceResult>;
|
|
91
|
+
export type WaitForEventToolDeps = {
|
|
92
|
+
stateDir: string;
|
|
93
|
+
spawn?: SpawnFn;
|
|
94
|
+
/** Present only when the incoming `tools/call` carried a progress token; used to emit periodic
|
|
95
|
+
* "still waiting" progress notifications during a live wait. */
|
|
96
|
+
progress?: {
|
|
97
|
+
token: string | number;
|
|
98
|
+
sendNotification: (notification: unknown) => Promise<void>;
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
export type WaitForEventToolResult = {
|
|
102
|
+
sessionId: string;
|
|
103
|
+
alias?: string;
|
|
104
|
+
name: string;
|
|
105
|
+
payload: unknown;
|
|
106
|
+
/** Unix ms. */
|
|
107
|
+
ts: number;
|
|
108
|
+
seq: number;
|
|
109
|
+
};
|
|
110
|
+
export declare const handleWaitForEventTool: (rawArgs: unknown, deps: WaitForEventToolDeps) => Promise<WaitForEventToolResult>;
|
|
111
|
+
//# sourceMappingURL=events-tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events-tool.d.ts","sourceRoot":"","sources":["../../src/mcp/events-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAKL,KAAK,iBAAiB,EAEvB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAoB,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,eAAO,MAAM,gBAAgB,mBAAmB,CAAC;AACjD,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAcjE;;sFAEsF;AACtF,eAAO,MAAM,0BAA0B,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG,MAE5E,CAAC;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoBzB,CAAC;AAEX,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BjC,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,UAAU,CAAC;CAClB,CAAC;AA0GF,eAAO,MAAM,gBAAgB,GAAU,SAAS,OAAO,EAAE,MAAM,cAAc,KAAG,OAAO,CAAC,iBAAiB,CASxG,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;oEACgE;IAChE,QAAQ,CAAC,EAAE;QACT,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QACvB,gBAAgB,EAAE,CAAC,YAAY,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC5D,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe;IACf,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAYF,eAAO,MAAM,sBAAsB,GACjC,SAAS,OAAO,EAChB,MAAM,oBAAoB,KACzB,OAAO,CAAC,sBAAsB,CA6LhC,CAAC"}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in `appduct_events` / `appduct_wait_for_event` MCP tools (issue #6): a pull
|
|
3
|
+
* surface over the daemon's `app_event` plumbing (`postEvent()` on the app side, `events.since`'s
|
|
4
|
+
* per-session retention buffer on the daemon side — see `daemon/event-bus.ts`). MCP is
|
|
5
|
+
* request/response, so an agent otherwise has no way to observe anything the app pushes.
|
|
6
|
+
*
|
|
7
|
+
* `appduct_wait_for_event` drains the retained buffer for an already-arrived match before
|
|
8
|
+
* falling back to a live wait, exactly to avoid the race `appduct_wait_for_session` doesn't
|
|
9
|
+
* have to worry about (a session is either claimed or it isn't, but an `app_event` can easily fire
|
|
10
|
+
* between "the agent decides to wait" and "the wait subscription lands"). The live notification
|
|
11
|
+
* listener is registered *before* the `events.since` drain call is even sent — not merely before
|
|
12
|
+
* it resolves — because the daemon's response to that call and a subsequent `notify()` for a new
|
|
13
|
+
* event can arrive in the same socket chunk; `rpc/client.ts` fans a chunk's notification lines out
|
|
14
|
+
* to whatever listeners exist at the moment it's processed, so a listener added only after `await
|
|
15
|
+
* stream.call(eventsSince, …)` returns can miss an event that was already in that same chunk.
|
|
16
|
+
* Arrivals before the drain has been merged into the backlog are buffered, then merged in
|
|
17
|
+
* (deduped by `seq`) before falling through to the live phase — see `handleWaitForEventTool` below.
|
|
18
|
+
*/
|
|
19
|
+
import { RPC_METHODS, EVENT_KINDS, } from "@appduct/shared";
|
|
20
|
+
import { openDaemonStream } from "../rpc/client.js";
|
|
21
|
+
import { McpBuiltinToolError } from "./connect-tool.js";
|
|
22
|
+
export const EVENTS_TOOL_NAME = "appduct_events";
|
|
23
|
+
export const WAIT_FOR_EVENT_TOOL_NAME = "appduct_wait_for_event";
|
|
24
|
+
const DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS = 120_000;
|
|
25
|
+
/** Kept safely under the 30-minute idle window a stdio MCP server gets before Claude Code aborts a
|
|
26
|
+
* tool call that has sent neither a response nor a progress notification (issue #6's "Constraint on
|
|
27
|
+
* the blocking wait tool") — with margin for RPC round-trip and scheduling delay. */
|
|
28
|
+
const MAX_WAIT_FOR_EVENT_TIMEOUT_MS = 25 * 60 * 1000;
|
|
29
|
+
/** How often a progress notification is emitted during a live wait, when the caller supplied a
|
|
30
|
+
* progress token — informational only (the timeout cap above is what actually keeps the call
|
|
31
|
+
* within the idle window), but it gives a caller watching progress something to show. */
|
|
32
|
+
const WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS = 60_000;
|
|
33
|
+
const KNOWN_EVENT_KINDS = new Set(EVENT_KINDS);
|
|
34
|
+
/** Server-side cap on `timeoutMs` (issue #6's "Cap timeoutMs server-side below the idle window
|
|
35
|
+
* rather than trusting the caller") — a pure function so the cap's boundary behavior is
|
|
36
|
+
* unit-testable without spinning up a daemon or actually waiting out either bound. */
|
|
37
|
+
export const clampWaitForEventTimeoutMs = (requestedMs) => {
|
|
38
|
+
return Math.min(requestedMs ?? DEFAULT_WAIT_FOR_EVENT_TIMEOUT_MS, MAX_WAIT_FOR_EVENT_TIMEOUT_MS);
|
|
39
|
+
};
|
|
40
|
+
export const EVENTS_TOOL_DESCRIPTOR = {
|
|
41
|
+
name: EVENTS_TOOL_NAME,
|
|
42
|
+
description: "Drain events retained in the daemon's per-session ring buffer since a cursor — the pull " +
|
|
43
|
+
"counterpart to a live subscription, for checking what happened after calling a tool or " +
|
|
44
|
+
"triggering app behavior. With no selector, targets the sole active/suspended session. " +
|
|
45
|
+
"Returns { events, cursor }; pass cursor back as since on the next call to avoid re-reading " +
|
|
46
|
+
"events you've already seen. limit (if given) keeps the OLDEST events in the window and " +
|
|
47
|
+
"advances cursor only past what was actually returned, so repeated calls page forward through " +
|
|
48
|
+
"everything retained rather than skipping ahead.",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
selector: { type: "string" },
|
|
53
|
+
since: { type: "integer", minimum: 0 },
|
|
54
|
+
kinds: { type: "array", items: { type: "string", enum: EVENT_KINDS } },
|
|
55
|
+
limit: { type: "integer", exclusiveMinimum: 0 },
|
|
56
|
+
},
|
|
57
|
+
additionalProperties: false,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
export const WAIT_FOR_EVENT_TOOL_DESCRIPTOR = {
|
|
61
|
+
name: WAIT_FOR_EVENT_TOOL_NAME,
|
|
62
|
+
description: "Wait for the connected app to push an event via postEvent(name, payload) matching name (and, " +
|
|
63
|
+
"if match is given, every one of its keys strictly-equal — primitives only, not objects/arrays " +
|
|
64
|
+
"— in the event's payload). Checks already-retained events first, so an event that already " +
|
|
65
|
+
"fired before this call still resolves immediately; pass since (a cursor from a previous " +
|
|
66
|
+
"appduct_events/appduct_wait_for_event call) to skip events you've already handled and " +
|
|
67
|
+
"wait only for a new one — omitting it can return an old match instantly on every call. " +
|
|
68
|
+
"Requires a claimed session to already exist (use appduct_wait_for_session first if not). " +
|
|
69
|
+
"Resolves with { sessionId, alias, name, payload, ts, seq }, or rejects with tool_timeout after " +
|
|
70
|
+
"timeoutMs (default 120000ms, capped server-side at 1500000ms). A call still running after " +
|
|
71
|
+
"about two minutes moves to a Claude Code background task, so the result may arrive well after " +
|
|
72
|
+
"this call returns.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
selector: { type: "string" },
|
|
77
|
+
name: { type: "string" },
|
|
78
|
+
match: { type: "object", additionalProperties: { type: ["string", "number", "boolean", "null"] } },
|
|
79
|
+
since: { type: "integer", minimum: 0 },
|
|
80
|
+
timeoutMs: { type: "number", exclusiveMinimum: 0 },
|
|
81
|
+
},
|
|
82
|
+
required: ["name"],
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const asRecord = (value) => {
|
|
87
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
};
|
|
92
|
+
const asOptionalString = (value, field) => {
|
|
93
|
+
if (value === undefined) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
97
|
+
throw new McpBuiltinToolError("invalid_request", `"${field}" must be a non-empty string.`);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
};
|
|
101
|
+
/** `since`/`limit` mirror the daemon's own `asEventsSinceParams` (`daemon/daemon.ts`), which
|
|
102
|
+
* requires integers — accepting e.g. `1.5` here would just round-trip to a `DaemonRpcError`
|
|
103
|
+
* instead of this tool's own clearer `invalid_request`. */
|
|
104
|
+
const asOptionalNonNegativeInteger = (value, field) => {
|
|
105
|
+
if (value === undefined) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
109
|
+
throw new McpBuiltinToolError("invalid_request", `"${field}" must be a non-negative integer.`);
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
};
|
|
113
|
+
const asOptionalPositiveInteger = (value, field) => {
|
|
114
|
+
if (value === undefined) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
118
|
+
throw new McpBuiltinToolError("invalid_request", `"${field}" must be a positive integer.`);
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
};
|
|
122
|
+
/** `timeoutMs` is not required to be an integer (unlike the daemon's `since`/`limit`) — it's never
|
|
123
|
+
* forwarded to the daemon verbatim, only used as this process's own `setTimeout` duration. */
|
|
124
|
+
const asOptionalPositiveNumber = (value, field) => {
|
|
125
|
+
if (value === undefined) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
129
|
+
throw new McpBuiltinToolError("invalid_request", `"${field}" must be a positive number.`);
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
};
|
|
133
|
+
const asOptionalEventKinds = (value) => {
|
|
134
|
+
if (value === undefined) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
if (!Array.isArray(value) || !value.every((kind) => typeof kind === "string" && KNOWN_EVENT_KINDS.has(kind))) {
|
|
138
|
+
throw new McpBuiltinToolError("invalid_request", '"kinds" must be an array of known event kinds.');
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
};
|
|
142
|
+
const isMatchPrimitive = (value) => {
|
|
143
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
144
|
+
};
|
|
145
|
+
/** `payloadShallowMatches` compares with `===`, so an object/array value here could never equal
|
|
146
|
+
* anything and would just make the wait time out with no explanation — reject it up front instead. */
|
|
147
|
+
const asOptionalMatch = (value) => {
|
|
148
|
+
if (value === undefined) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
152
|
+
throw new McpBuiltinToolError("invalid_request", '"match" must be a JSON object.');
|
|
153
|
+
}
|
|
154
|
+
const record = value;
|
|
155
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
156
|
+
if (!isMatchPrimitive(entry)) {
|
|
157
|
+
throw new McpBuiltinToolError("invalid_request", `"match.${key}" must be a string, number, boolean, or null (objects/arrays can never match).`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return record;
|
|
161
|
+
};
|
|
162
|
+
export const handleEventsTool = async (rawArgs, deps) => {
|
|
163
|
+
const args = asRecord(rawArgs);
|
|
164
|
+
return deps.call(RPC_METHODS.eventsSince, {
|
|
165
|
+
selector: asOptionalString(args.selector, "selector"),
|
|
166
|
+
since: asOptionalNonNegativeInteger(args.since, "since"),
|
|
167
|
+
kinds: asOptionalEventKinds(args.kinds),
|
|
168
|
+
limit: asOptionalPositiveInteger(args.limit, "limit"),
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
const payloadShallowMatches = (payload, match) => {
|
|
172
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const record = payload;
|
|
176
|
+
return Object.entries(match).every(([key, value]) => record[key] === value);
|
|
177
|
+
};
|
|
178
|
+
export const handleWaitForEventTool = async (rawArgs, deps) => {
|
|
179
|
+
const args = asRecord(rawArgs);
|
|
180
|
+
const selector = asOptionalString(args.selector, "selector");
|
|
181
|
+
const name = asOptionalString(args.name, "name");
|
|
182
|
+
if (!name) {
|
|
183
|
+
throw new McpBuiltinToolError("invalid_request", '"name" must be a non-empty string.');
|
|
184
|
+
}
|
|
185
|
+
const match = asOptionalMatch(args.match);
|
|
186
|
+
const since = asOptionalNonNegativeInteger(args.since, "since");
|
|
187
|
+
const timeoutMs = clampWaitForEventTimeoutMs(asOptionalPositiveNumber(args.timeoutMs, "timeoutMs"));
|
|
188
|
+
const stream = await openDaemonStream({ stateDir: deps.stateDir, spawn: deps.spawn });
|
|
189
|
+
try {
|
|
190
|
+
// Resolve the selector to one concrete session up front (same shape as
|
|
191
|
+
// `handleWaitForSessionTool`) so the buffer drain below and the live subscription target the
|
|
192
|
+
// exact same session — `events.subscribe`'s own `sessionSelector` has no "sole session" default,
|
|
193
|
+
// so leaving it unresolved here would silently widen the live half of the wait to every session.
|
|
194
|
+
// A `DaemonRpcError` (no_session/ambiguous_session/unknown_session) propagates unchanged —
|
|
195
|
+
// `toolErrorContentFromError` (mcp/server.ts) already maps it to its precise `data.type`.
|
|
196
|
+
const described = await stream.call(RPC_METHODS.sessionsDescribe, { selector });
|
|
197
|
+
const sessionId = described.sessionId;
|
|
198
|
+
const alias = described.alias;
|
|
199
|
+
const toResult = (event) => {
|
|
200
|
+
if (event.kind !== "app_event" || event.sessionId !== sessionId) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const data = event.data;
|
|
204
|
+
if (data.name !== name) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
if (match && !payloadShallowMatches(data.payload, match)) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return { sessionId, alias, name, payload: data.payload, ts: event.ts, seq: event.seq };
|
|
211
|
+
};
|
|
212
|
+
// The notification listener is registered *before* `events.subscribe` is even sent, and starts
|
|
213
|
+
// buffering into `earlyEvents` rather than resolving anything (`liveHandler` is still null) —
|
|
214
|
+
// see the module doc comment for why: the `events.since` response and a `notify()` for a
|
|
215
|
+
// brand-new event can arrive in the same socket chunk, and a listener added only after `await`ing
|
|
216
|
+
// that response has already missed a notification line from that same chunk.
|
|
217
|
+
let liveHandler = null;
|
|
218
|
+
const earlyEvents = [];
|
|
219
|
+
const unsubscribeNotification = stream.onNotification((payload) => {
|
|
220
|
+
const event = payload;
|
|
221
|
+
if (liveHandler) {
|
|
222
|
+
liveHandler(event);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
earlyEvents.push(event);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
let unsubscribeClose = () => { };
|
|
229
|
+
try {
|
|
230
|
+
await stream.call(RPC_METHODS.eventsSubscribe, { sessionSelector: sessionId, kinds: ["app_event"] });
|
|
231
|
+
const sinceResult = await stream.call(RPC_METHODS.eventsSince, {
|
|
232
|
+
selector: sessionId,
|
|
233
|
+
since,
|
|
234
|
+
kinds: ["app_event"],
|
|
235
|
+
});
|
|
236
|
+
// Merge the retained backlog with whatever arrived on the live channel while the two calls
|
|
237
|
+
// above were in flight, deduped by `seq` (the same event can legitimately show up in both:
|
|
238
|
+
// the daemon buffers before it fans out, so a late-drained event and an in-flight live one can
|
|
239
|
+
// be the same notification). Everything from here to `liveHandler = …` below is synchronous —
|
|
240
|
+
// no `await` — so nothing can arrive on the socket and be missed in the gap.
|
|
241
|
+
const seenSeqs = new Set();
|
|
242
|
+
const backlog = [];
|
|
243
|
+
for (const event of [...sinceResult.events, ...earlyEvents]) {
|
|
244
|
+
if (event.sessionId === sessionId && !seenSeqs.has(event.seq)) {
|
|
245
|
+
seenSeqs.add(event.seq);
|
|
246
|
+
backlog.push(event);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
backlog.sort((a, b) => a.seq - b.seq);
|
|
250
|
+
for (const event of backlog) {
|
|
251
|
+
const result = toResult(event);
|
|
252
|
+
if (result) {
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Nothing matched yet: the cursor to resume live from is the last event actually considered
|
|
257
|
+
// (backlog's own scan already covers everything since.events + earlyEvents jointly saw), or —
|
|
258
|
+
// if nothing was retained/arrived at all — `events.since`'s own cursor, which (per
|
|
259
|
+
// `event-bus.ts`'s `since()`) already reflects the session's true high-water mark even when
|
|
260
|
+
// the `kinds` filter matched nothing.
|
|
261
|
+
const highestConsideredSeq = backlog.length > 0 ? backlog[backlog.length - 1].seq : sinceResult.cursor;
|
|
262
|
+
return await new Promise((resolve, reject) => {
|
|
263
|
+
let settled = false;
|
|
264
|
+
const settle = (fn) => {
|
|
265
|
+
if (settled) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
settled = true;
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
if (progressTimer) {
|
|
271
|
+
clearInterval(progressTimer);
|
|
272
|
+
}
|
|
273
|
+
liveHandler = null;
|
|
274
|
+
fn();
|
|
275
|
+
};
|
|
276
|
+
const timer = setTimeout(() => {
|
|
277
|
+
settle(() => reject(new McpBuiltinToolError("tool_timeout", `Timed out after ${timeoutMs}ms waiting for event "${name}".`)));
|
|
278
|
+
}, timeoutMs);
|
|
279
|
+
const startedAt = Date.now();
|
|
280
|
+
const progressTimer = deps.progress
|
|
281
|
+
? setInterval(() => {
|
|
282
|
+
deps.progress.sendNotification({
|
|
283
|
+
method: "notifications/progress",
|
|
284
|
+
params: {
|
|
285
|
+
progressToken: deps.progress.token,
|
|
286
|
+
progress: Date.now() - startedAt,
|
|
287
|
+
total: timeoutMs,
|
|
288
|
+
message: `Still waiting for event "${name}"...`,
|
|
289
|
+
},
|
|
290
|
+
}).catch(() => {
|
|
291
|
+
// A failed progress notification must never abort the wait itself.
|
|
292
|
+
});
|
|
293
|
+
}, WAIT_FOR_EVENT_PROGRESS_INTERVAL_MS)
|
|
294
|
+
: undefined;
|
|
295
|
+
unsubscribeClose = stream.onClose(() => {
|
|
296
|
+
settle(() => reject(new McpBuiltinToolError("tool_execution_error", `The connection to the Appduct daemon closed while waiting for event "${name}".`)));
|
|
297
|
+
});
|
|
298
|
+
// From here on, every notification goes straight to this handler instead of `earlyEvents` —
|
|
299
|
+
// assigned synchronously (no `await` since the backlog scan above), so nothing is missed.
|
|
300
|
+
liveHandler = (event) => {
|
|
301
|
+
if (settled) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (event.sessionId === sessionId && event.seq <= highestConsideredSeq) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const result = toResult(event);
|
|
308
|
+
if (result) {
|
|
309
|
+
settle(() => resolve(result));
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
unsubscribeNotification();
|
|
316
|
+
unsubscribeClose();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
finally {
|
|
320
|
+
stream.close();
|
|
321
|
+
}
|
|
322
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `appduct mcp` stdio MCP server (ARCHITECTURE.md §9): a thin proxy that maps the daemon RPC
|
|
3
|
+
* surface (`rpc/client.ts`, same auto-spawning client the CLI uses) onto MCP's `tools/list`,
|
|
4
|
+
* `tools/call`, `notifications/tools/list_changed`, progress notifications, and one resource
|
|
5
|
+
* (`appduct://sessions`). All logging here goes to stderr only — stdout is reserved for the MCP
|
|
6
|
+
* transport's protocol frames.
|
|
7
|
+
*
|
|
8
|
+
* One persistent daemon connection (`stream`) is used for everything except progress-correlated
|
|
9
|
+
* `tools.call`s, which get their own short-lived connection (see `callProxiedTool` below) so the
|
|
10
|
+
* `tool_call_started` event that reveals the call's `callId` is unambiguous.
|
|
11
|
+
*
|
|
12
|
+
* This is also where both `"prompt"`-policy consent channels live (ARCHITECTURE.md §12):
|
|
13
|
+
* `resolveToolCallConsent` prefers `elicitation/create` (issue #10, any client that declares the
|
|
14
|
+
* `elicitation` capability) over the `_meta["anthropic/requiresUserInteraction"]` flag (issue #14,
|
|
15
|
+
* Claude Code ≥ v2.1.199 only), and the two never arm for the same call — see
|
|
16
|
+
* `clientSupportsElicitation` and the `tools/list` handler below.
|
|
17
|
+
*/
|
|
18
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
19
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
20
|
+
import type { ExecFn } from "../cli/open-target.js";
|
|
21
|
+
import { type SpawnFn, type VersionCheckOptions } from "../rpc/client.js";
|
|
22
|
+
export declare const SESSIONS_RESOURCE_URI = "appduct://sessions";
|
|
23
|
+
export type CreateMcpServerOptions = {
|
|
24
|
+
stateDir: string;
|
|
25
|
+
spawn?: SpawnFn;
|
|
26
|
+
/**
|
|
27
|
+
* Daemon/CLI version check (issue #30), applied to the startup stream only — never to the
|
|
28
|
+
* short-lived progress streams below, which must never restart the daemon out from under a call
|
|
29
|
+
* that is already in flight. A mismatch that cannot be resolved therefore fails server startup,
|
|
30
|
+
* with the same message the CLI prints. Drift introduced *after* this server started is out of
|
|
31
|
+
* scope: nothing re-checks a connection that is already established.
|
|
32
|
+
*/
|
|
33
|
+
checkVersion?: VersionCheckOptions;
|
|
34
|
+
/** The scheme composing `appduct_connect`'s deep link, already resolved by `commands/mcp.ts`
|
|
35
|
+
* against the shared order in `scheme.ts` (`--scheme` > `APPDUCT_SCHEME` > project
|
|
36
|
+
* `.appduct/config.json` > state `config.json` > `app.json`). May legitimately be undefined —
|
|
37
|
+
* the server starts either way and only `appduct_connect` needs one. */
|
|
38
|
+
scheme?: string;
|
|
39
|
+
/** Every location consulted while resolving {@link scheme}, so `appduct_connect` can name
|
|
40
|
+
* them when it has to fail. Only meaningful when `scheme` is undefined. */
|
|
41
|
+
schemeTried?: string[];
|
|
42
|
+
/** `config.json`'s `iosBundleId`; the default bundle id for `appduct_connect`'s experimental
|
|
43
|
+
* `ios-device` target (issue #31). */
|
|
44
|
+
iosBundleId?: string;
|
|
45
|
+
exec?: ExecFn;
|
|
46
|
+
env?: NodeJS.ProcessEnv;
|
|
47
|
+
/** Overrides `ELICITATION_TIMEOUT_MS` (ARCHITECTURE.md §12 / issue #10) — test-only seam so the
|
|
48
|
+
* timeout branch of `requestElicitationConsent` can be exercised without an actual 10-minute
|
|
49
|
+
* wait. Every real caller (`cli/mcp-command.ts`) leaves this unset. */
|
|
50
|
+
elicitationTimeoutMs?: number;
|
|
51
|
+
};
|
|
52
|
+
export type McpServerHandle = {
|
|
53
|
+
server: Server;
|
|
54
|
+
connect: (transport: Transport) => Promise<void>;
|
|
55
|
+
close: () => Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
export declare const createMcpServer: (options: CreateMcpServerOptions) => Promise<McpServerHandle>;
|
|
58
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAa/E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAGpD,OAAO,EAKL,KAAK,OAAO,EACZ,KAAK,mBAAmB,EACzB,MAAM,kBAAkB,CAAC;AAwB1B,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AA8Q1D,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC;;;4EAGwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;+EAC2E;IAC3E,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB;0CACsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;;2EAEuE;IACvE,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,SAAS,sBAAsB,KAAG,OAAO,CAAC,eAAe,CAiY9F,CAAC"}
|