appduct 0.13.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 +3 -2
- package/dist/bin.js +61 -4
- package/dist/cli/result-types.d.ts +8 -1
- package/dist/cli/routes/daemon/index.js +58 -1
- package/dist/cli/routes/daemon/run.js +152 -22
- package/dist/cli/routes/daemon/start.js +58 -1
- package/dist/cli/routes/daemon/status.js +58 -1
- package/dist/cli/routes/daemon/stop.js +58 -1
- package/dist/cli/routes/doctor.js +58 -1
- package/dist/cli/routes/events/index.d.ts +0 -3
- package/dist/cli/routes/events/index.js +60 -2
- package/dist/cli/routes/events/ls.d.ts +5 -0
- package/dist/cli/routes/events/ls.js +1798 -0
- package/dist/cli/routes/events/since.js +58 -1
- package/dist/cli/routes/events/tail.js +58 -1
- package/dist/cli/routes/init.js +58 -1
- package/dist/cli/routes/keygen.js +58 -1
- package/dist/cli/routes/mcp.js +108 -1
- package/dist/cli/routes/sessions/index.js +58 -1
- package/dist/cli/routes/sessions/link.js +58 -1
- package/dist/cli/routes/sessions/ls.js +58 -1
- package/dist/cli/routes/sessions/revoke.js +58 -1
- package/dist/cli/routes/tools/call.js +58 -1
- package/dist/cli/routes/tools/describe.js +58 -1
- package/dist/cli/routes/tools/index.js +58 -1
- package/dist/cli/routes/tools/ls.js +58 -1
- package/dist/commands/events-ls.d.ts +21 -0
- package/dist/daemon/calls.d.ts +3 -1
- package/dist/daemon/event-bus.d.ts +10 -0
- package/dist/daemon/registry.d.ts +20 -12
- package/dist/daemon/rpc-errors.d.ts +3 -0
- package/dist/daemon/sessions.d.ts +4 -2
- package/dist/index.js +61 -4
- package/dist/mcp/app-tools.d.ts +19 -0
- package/dist/mcp/events-tool.d.ts +39 -0
- package/package.json +2 -2
|
@@ -28,6 +28,16 @@ export type ProjectAppEventOptions = {
|
|
|
28
28
|
/** Caps the payload's JSON to this many UTF-8 bytes (issue #113); see {@link truncateToUtf8Bytes}. */
|
|
29
29
|
payloadMaxBytes?: number;
|
|
30
30
|
};
|
|
31
|
+
/** Whether `name` matches the whole-name glob `pattern` (`*` matches any run of characters,
|
|
32
|
+
* everything else literally), in time linear in the two strings' lengths.
|
|
33
|
+
*
|
|
34
|
+
* No regex and no backtracking: the daemon is single-threaded, so a match that blows up blocks
|
|
35
|
+
* every session and RPC client (issue #112 review). Compiling `*` to `.*` was exponential in the
|
|
36
|
+
* star count, and a two-pointer matcher that backtracks to the last `*` was still quadratic.
|
|
37
|
+
* Instead the pattern is split on `*`: the first piece must be a prefix, the last a suffix, and
|
|
38
|
+
* each middle piece is found in order with `indexOf`. Taking the leftmost occurrence of each
|
|
39
|
+
* middle piece is always safe, because it leaves the most room for the pieces after it. */
|
|
40
|
+
export declare const matchesNameGlob: (pattern: string, name: string) => boolean;
|
|
31
41
|
/**
|
|
32
42
|
* Whether `event` survives `options.name`'s glob, and truncates its payload to `options
|
|
33
43
|
* .payloadMaxBytes` when given — the one implementation `since()`'s drain and `daemon.ts`'s
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-session
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Per-session registry of what an app declared, by name (ARCHITECTURE.md §7): one instance holds a
|
|
3
|
+
* session's tools, another its events (issue #124). Callers (sessions.ts) must validate incoming
|
|
4
|
+
* `tool_registry_*`/`event_registry_*` wire messages with the shared guards *before* calling into
|
|
5
|
+
* this module — v1 crashed by indexing into an unvalidated `tools: [null]` snapshot; this module
|
|
6
|
+
* only ever stores already-validated descriptors.
|
|
6
7
|
*/
|
|
7
|
-
import type { ToolDescriptor } from "@appduct/shared";
|
|
8
|
-
export type
|
|
8
|
+
import type { EventDescriptor, ToolDescriptor } from "@appduct/shared";
|
|
9
|
+
export type Registry<T extends {
|
|
10
|
+
name: string;
|
|
11
|
+
}> = {
|
|
9
12
|
/** Authoritative replace: used for the initial snapshot and every post-resume re-sync. */
|
|
10
|
-
snapshot: (
|
|
11
|
-
upsert: (
|
|
13
|
+
snapshot: (items: T[]) => void;
|
|
14
|
+
upsert: (item: T) => void;
|
|
12
15
|
remove: (name: string) => void;
|
|
13
|
-
list: () =>
|
|
14
|
-
get: (name: string) =>
|
|
16
|
+
list: () => T[];
|
|
17
|
+
get: (name: string) => T | undefined;
|
|
15
18
|
count: () => number;
|
|
16
19
|
};
|
|
17
|
-
|
|
18
|
-
export
|
|
20
|
+
export type ToolRegistry = Registry<ToolDescriptor>;
|
|
21
|
+
export type EventRegistry = Registry<EventDescriptor>;
|
|
22
|
+
/** `onChange` fires after every mutation (snapshot/upsert/remove) — used to emit `tools_changed`
|
|
23
|
+
* and `events_changed`. */
|
|
24
|
+
export declare const createRegistry: <T extends {
|
|
25
|
+
name: string;
|
|
26
|
+
}>(onChange?: () => void) => Registry<T>;
|
|
@@ -14,3 +14,6 @@ export declare class RpcApplicationError extends Error {
|
|
|
14
14
|
/** JSON-RPC error code; defaults to the shared "server error" range. */
|
|
15
15
|
code?: number, details?: unknown | undefined);
|
|
16
16
|
}
|
|
17
|
+
/** The `session_suspended` message for an app that closed its socket because it went to the
|
|
18
|
+
* background: says why and how to recover. */
|
|
19
|
+
export declare const appBackgroundedMessage: (alias: string) => string;
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
* §6: "terminal states free the alias" — there is no persisted history, matching §3/§13's "no
|
|
11
11
|
* per-session files, all state in-daemon-memory").
|
|
12
12
|
*/
|
|
13
|
-
import { type EventMessage, type SessionClaimMessage, type SessionResumeMessage, type SessionSummary, type ToolCallMessage, type ToolCallProgressMessage, type ToolCancelMessage, type ToolErrorMessage, type ToolResultMessage } from "@appduct/shared";
|
|
13
|
+
import { type EventMessage, type SessionClaimMessage, type SessionResumeMessage, type SessionSummary, type SessionSuspendReason, type ToolCallMessage, type ToolCallProgressMessage, type ToolCancelMessage, type ToolErrorMessage, type ToolResultMessage } from "@appduct/shared";
|
|
14
14
|
import type { WebSocket } from "ws";
|
|
15
15
|
import type { Clock } from "../cli/types.js";
|
|
16
16
|
import type { EventBus } from "./event-bus.js";
|
|
17
17
|
import { type CreatedLink } from "./links.js";
|
|
18
|
-
import { type ToolRegistry } from "./registry.js";
|
|
18
|
+
import { type EventRegistry, type ToolRegistry } from "./registry.js";
|
|
19
19
|
import { type TimerFns } from "./timers.js";
|
|
20
20
|
/** Wire message types allowed *after* claim (session_claim/session_resume are pre-claim only). */
|
|
21
21
|
export declare const POST_CLAIM_MESSAGE_TYPES: Set<string>;
|
|
@@ -69,7 +69,9 @@ export type SessionManager = {
|
|
|
69
69
|
sessionId: string;
|
|
70
70
|
alias: string;
|
|
71
71
|
state: "active" | "suspended";
|
|
72
|
+
suspendReason?: SessionSuspendReason;
|
|
72
73
|
registry: ToolRegistry;
|
|
74
|
+
eventRegistry: EventRegistry;
|
|
73
75
|
};
|
|
74
76
|
/** Sends a `tool_call` frame to the session's active socket; `false` if the session has no
|
|
75
77
|
* active socket right now (caller has typically already checked ACTIVE state via
|
package/dist/index.js
CHANGED
|
@@ -138,10 +138,10 @@ var createCli = () => {
|
|
|
138
138
|
"--timeout <ms>",
|
|
139
139
|
"call: call timeout in milliseconds. Shortens the deadline; it cannot extend one past the app's own timer, which is the tool's declared timeoutMs (else 10000)."
|
|
140
140
|
);
|
|
141
|
-
cli.command("events [...args]", "
|
|
141
|
+
cli.command("events [...args]", "List the events an app declares, stream them, or replay them since a cursor: ls, tail, or since.").usage("events <ls|tail|since> [selector] [args]").option("--follow", "tail: accepted for script readability; the default behavior already follows.").option(
|
|
142
142
|
"--name <glob>",
|
|
143
|
-
|
|
144
|
-
).option(
|
|
143
|
+
`Only events whose name matches this whole-name, case-sensitive glob ("*" matches any run of characters). ls: an exact name prints that event's full payload schema.`
|
|
144
|
+
).option("--limit <n>", "ls: show at most n events.").option("--offset <n>", "ls: skip the first n events of the name-sorted list.").option(
|
|
145
145
|
"--payload-max-bytes <n>",
|
|
146
146
|
"Cap each event's payload at this many UTF-8 bytes of its JSON; over the cap, the event carries payloadPreview, payloadBytes and truncated: true instead of payload."
|
|
147
147
|
);
|
|
@@ -546,6 +546,7 @@ var getExitCodeForError = (error) => {
|
|
|
546
546
|
import pc from "picocolors";
|
|
547
547
|
import {
|
|
548
548
|
formatAgentWebSocketUrl,
|
|
549
|
+
renderEventSignature,
|
|
549
550
|
renderToolSignature,
|
|
550
551
|
summarizeToolDescription
|
|
551
552
|
} from "@appduct/shared";
|
|
@@ -641,6 +642,13 @@ var formatDevice = (device) => {
|
|
|
641
642
|
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
642
643
|
return device.os ? `${label} (${device.os})` : label;
|
|
643
644
|
};
|
|
645
|
+
var SUSPEND_REASON_LABELS = {
|
|
646
|
+
app_backgrounded: "app in background",
|
|
647
|
+
connection_lost: "connection lost"
|
|
648
|
+
};
|
|
649
|
+
var formatSessionState = (session) => {
|
|
650
|
+
return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
|
|
651
|
+
};
|
|
644
652
|
var renderLsData = (colors, data, now) => {
|
|
645
653
|
if (data.length === 0) {
|
|
646
654
|
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
@@ -649,7 +657,7 @@ var renderLsData = (colors, data, now) => {
|
|
|
649
657
|
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
650
658
|
const rows = data.map((session) => [
|
|
651
659
|
session.alias,
|
|
652
|
-
session
|
|
660
|
+
formatSessionState(session),
|
|
653
661
|
formatDevice(session.device),
|
|
654
662
|
String(session.toolCount),
|
|
655
663
|
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
@@ -823,6 +831,53 @@ var renderToolsListData = (colors, data, flags, full) => {
|
|
|
823
831
|
}
|
|
824
832
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
825
833
|
};
|
|
834
|
+
var renderEventsTruncationLine = (data) => {
|
|
835
|
+
const offset = data.offset ?? 0;
|
|
836
|
+
if (data.events.length >= data.total) {
|
|
837
|
+
return [];
|
|
838
|
+
}
|
|
839
|
+
return [
|
|
840
|
+
"",
|
|
841
|
+
`Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
|
|
842
|
+
];
|
|
843
|
+
};
|
|
844
|
+
var renderEventsListData = (colors, data, flags) => {
|
|
845
|
+
const [only] = data.events;
|
|
846
|
+
if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
|
|
847
|
+
return renderFields(
|
|
848
|
+
colors.green(`Event: ${only.name}`),
|
|
849
|
+
[
|
|
850
|
+
["Signature", renderEventSignature(only)],
|
|
851
|
+
["Description", only.description],
|
|
852
|
+
["Payload schema", only.payload_schema]
|
|
853
|
+
],
|
|
854
|
+
flags
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
if (data.events.length === 0 && data.total > 0) {
|
|
858
|
+
return [
|
|
859
|
+
colors.green("Events"),
|
|
860
|
+
` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
|
|
861
|
+
];
|
|
862
|
+
}
|
|
863
|
+
if (data.events.length === 0) {
|
|
864
|
+
return [
|
|
865
|
+
colors.green("Events"),
|
|
866
|
+
data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
|
|
867
|
+
];
|
|
868
|
+
}
|
|
869
|
+
const lines = data.events.flatMap((event) => [
|
|
870
|
+
` ${renderEventSignature(event)}`,
|
|
871
|
+
` ${summarizeToolDescription(event.description)}`
|
|
872
|
+
]);
|
|
873
|
+
return [
|
|
874
|
+
colors.green("Events"),
|
|
875
|
+
...lines,
|
|
876
|
+
...renderEventsTruncationLine(data),
|
|
877
|
+
"",
|
|
878
|
+
"Run `appduct events ls --name <name>` for an event's full payload schema."
|
|
879
|
+
];
|
|
880
|
+
};
|
|
826
881
|
var renderInvokeData = (colors, data, flags) => {
|
|
827
882
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
828
883
|
};
|
|
@@ -1014,6 +1069,8 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
1014
1069
|
return renderToolsListData(colors, data, flags, options.full);
|
|
1015
1070
|
case "tools describe":
|
|
1016
1071
|
return renderToolDetail(colors, data, flags);
|
|
1072
|
+
case "events ls":
|
|
1073
|
+
return renderEventsListData(colors, data, flags);
|
|
1017
1074
|
case "tools call":
|
|
1018
1075
|
return renderInvokeData(colors, data, flags);
|
|
1019
1076
|
case "sessions revoke":
|
package/dist/mcp/app-tools.d.ts
CHANGED
|
@@ -107,12 +107,30 @@ export type ResolvedAppTool = {
|
|
|
107
107
|
descriptor: ListedToolDescriptor;
|
|
108
108
|
policy: EffectivePolicyDecision;
|
|
109
109
|
};
|
|
110
|
+
type ResolvedSession = {
|
|
111
|
+
sessionId: string;
|
|
112
|
+
alias: string;
|
|
113
|
+
};
|
|
110
114
|
export type CallToolArgs = {
|
|
111
115
|
selector?: string;
|
|
112
116
|
name: string;
|
|
113
117
|
args: Record<string, unknown>;
|
|
114
118
|
timeoutMs?: number;
|
|
115
119
|
};
|
|
120
|
+
/** Every key an agent sends must be one the tool declares. A misspelled key (`arguments` for
|
|
121
|
+
* `args`, `timeout_ms` for `timeoutMs`) would otherwise be dropped silently, and the tool would run
|
|
122
|
+
* without what the agent meant to pass. */
|
|
123
|
+
export declare const rejectUnknownKeys: (args: Record<string, unknown>, tool: string, allowed: readonly string[]) => void;
|
|
124
|
+
/** `null` counts as absent for every optional field: some clients fill unset optional
|
|
125
|
+
* parameters with `null` rather than leaving them out. */
|
|
126
|
+
export declare const asOptionalString: (value: unknown, field: string) => string | undefined;
|
|
127
|
+
/** Resolves `selector` (an alias, a session id, or nothing) to one concrete session first, with
|
|
128
|
+
* the daemon's own rules and errors (`no_session`, `ambiguous_session`, `unknown_session`).
|
|
129
|
+
* Everything after that is routed by the **session id**, never the alias: the daemon frees an
|
|
130
|
+
* alias when a session ends and gives it to the next device of the same model, so routing by alias
|
|
131
|
+
* could land a call — one the user may already have approved — on a different device. If the
|
|
132
|
+
* session goes away mid-call, the next daemon call fails with `unknown_session` instead. */
|
|
133
|
+
export declare const resolveSession: (call: DaemonCall, selector: string | undefined) => Promise<ResolvedSession>;
|
|
116
134
|
export declare const handleListToolsTool: (rawArgs: unknown, call: DaemonCall) => Promise<{
|
|
117
135
|
groups?: import("@appduct/shared").ToolGroupSummary[] | undefined;
|
|
118
136
|
tools: {
|
|
@@ -152,3 +170,4 @@ export declare const parseCallToolArgs: (rawArgs: unknown) => CallToolArgs;
|
|
|
152
170
|
*/
|
|
153
171
|
export declare const resolveCallDeadline: (tool: ResolvedAppTool, requestedTimeoutMs: number | undefined) => number;
|
|
154
172
|
export declare const resolveAppTool: (call: DaemonCall, selector: string | undefined, name: string) => Promise<ResolvedAppTool>;
|
|
173
|
+
export {};
|
|
@@ -12,12 +12,40 @@
|
|
|
12
12
|
import { type AppEvent } from "@appduct/shared";
|
|
13
13
|
import { type SpawnFn } from "../rpc/client.js";
|
|
14
14
|
import type { DaemonCall } from "./daemon-tools.js";
|
|
15
|
+
export declare const LIST_EVENTS_TOOL_NAME = "appduct_list_events";
|
|
15
16
|
export declare const EVENTS_TOOL_NAME = "appduct_events";
|
|
16
17
|
export declare const WAIT_FOR_EVENT_TOOL_NAME = "appduct_wait_for_event";
|
|
17
18
|
/** Server-side cap on `timeoutMs` (issue #6's "Cap timeoutMs server-side below the idle window
|
|
18
19
|
* rather than trusting the caller") — a pure function so the cap's boundary behavior is
|
|
19
20
|
* unit-testable without spinning up a daemon or actually waiting out either bound. */
|
|
20
21
|
export declare const clampWaitForEventTimeoutMs: (requestedMs: number | undefined) => number;
|
|
22
|
+
export declare const LIST_EVENTS_TOOL_DESCRIPTOR: {
|
|
23
|
+
readonly name: "appduct_list_events";
|
|
24
|
+
readonly description: string;
|
|
25
|
+
readonly inputSchema: {
|
|
26
|
+
readonly type: "object";
|
|
27
|
+
readonly properties: {
|
|
28
|
+
readonly selector: {
|
|
29
|
+
readonly type: "string";
|
|
30
|
+
readonly minLength: 1;
|
|
31
|
+
readonly description: "Session alias or id. Omit to target the sole active/suspended session.";
|
|
32
|
+
};
|
|
33
|
+
readonly name: {
|
|
34
|
+
readonly type: "string";
|
|
35
|
+
readonly minLength: 1;
|
|
36
|
+
};
|
|
37
|
+
readonly limit: {
|
|
38
|
+
readonly type: "integer";
|
|
39
|
+
readonly exclusiveMinimum: 0;
|
|
40
|
+
};
|
|
41
|
+
readonly offset: {
|
|
42
|
+
readonly type: "integer";
|
|
43
|
+
readonly minimum: 0;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
readonly additionalProperties: false;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
21
49
|
export declare const EVENTS_TOOL_DESCRIPTOR: {
|
|
22
50
|
readonly name: "appduct_events";
|
|
23
51
|
readonly description: string;
|
|
@@ -78,6 +106,17 @@ export declare const WAIT_FOR_EVENT_TOOL_DESCRIPTOR: {
|
|
|
78
106
|
export type EventsToolDeps = {
|
|
79
107
|
call: DaemonCall;
|
|
80
108
|
};
|
|
109
|
+
export declare const handleListEventsTool: (rawArgs: unknown, call: DaemonCall) => Promise<{
|
|
110
|
+
session: string;
|
|
111
|
+
total: number;
|
|
112
|
+
limit: {};
|
|
113
|
+
events: {
|
|
114
|
+
payload_schema?: import("@appduct/shared").EventSchemaDescriptor | undefined;
|
|
115
|
+
name: string;
|
|
116
|
+
signature: string;
|
|
117
|
+
description: string;
|
|
118
|
+
}[];
|
|
119
|
+
}>;
|
|
81
120
|
export type EventsToolResult = {
|
|
82
121
|
events: AppEvent[];
|
|
83
122
|
cursor: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appduct",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Driving React Native tools without in-app debug UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"reflect-metadata": "^0.2.2",
|
|
46
46
|
"toqr": "^0.1.1",
|
|
47
47
|
"ws": "^8.18.3",
|
|
48
|
-
"@appduct/shared": "0.
|
|
48
|
+
"@appduct/shared": "0.14.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.10.0",
|