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
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ Every command is `appduct <noun> <verb> [selector] [args]`, the same shape as `a
|
|
|
46
46
|
| `appduct tools ls [selector] --groups` | list a session's tool groups with their tool counts |
|
|
47
47
|
| `appduct tools describe [selector] <name>` | show one tool's full schema |
|
|
48
48
|
| `appduct tools call [selector] <name> --input '<json>' [--timeout <ms>]` | call a tool |
|
|
49
|
+
| `appduct events ls [selector] [--name <glob>] [--limit <n>] [--offset <n>]` | list the events the app declares, one signature line each; an exact `--name` prints the full payload schema; `--limit`/`--offset` page a long list, ending with a `Showing n of total events (offset o).` line when events were left out; `--json` carries the full descriptors, with `total` counting matches before paging |
|
|
49
50
|
| `appduct events tail [selector] [--follow]` | stream the events the app posts with `postEvent`; `--json` emits NDJSON |
|
|
50
51
|
| `appduct events since [selector] <cursor>` | one-shot pull the events retained since `<cursor>`; `--json` emits NDJSON |
|
|
51
52
|
| `appduct daemon run\|start\|stop\|status` | daemon lifecycle |
|
|
@@ -222,9 +223,9 @@ Your MCP client asks permission for `appduct_call_tool` as a single tool, so cho
|
|
|
222
223
|
|
|
223
224
|
Set this before you connect: the daemon reads `config.json` only when it starts, so changing it later takes `appduct daemon stop`. That disconnects every device, which then has to link again, and leaves a running `appduct mcp` without a daemon, so restart the MCP server in your client as well.
|
|
224
225
|
|
|
225
|
-
|
|
226
|
+
Five more built-in tools cover what the app's own tools can't. `appduct_connect` mints a link and, by default, delivers it to whichever `android`/`ios-sim` device it detects — pass `target`/`device` to choose, or `target: "none"` to force the human flow — falling back to a QR code, plus instructions to show it, only when there's nothing to deliver to. Delivering to `android` (chosen or detected) needs `appId`, resolved the same way as `--app-id` (see [Delivering the link to a device](#delivering-the-link-to-a-device)); passing it with `target: "ios-sim"` or `"none"` is an error. `appduct_wait_for_session` then waits for that session to be claimed. `target: "ios-device"` reaches a paired physical iPhone or iPad, with `appId` and the [prerequisites above](#--open-ios-device-experimental) — it's experimental and never auto-detected, so an agent has to ask for it by name.
|
|
226
227
|
|
|
227
|
-
The other
|
|
228
|
+
The other three give an agent a pull surface over `postEvent()`-pushed app events. `appduct_list_events` lists the events the app declared (`appduct events ls`) as signatures with descriptions, plus the payload schema when `name` is an exact name, so an agent learns the names before it waits on one. `appduct_events` drains everything retained since a cursor, and `appduct_wait_for_event` blocks for the next event whose name matches `name`, a whole-name glob (`*` waits for any name) — checking what's already retained before waiting live — rejecting with `tool_timeout` if none arrives in time.
|
|
228
229
|
|
|
229
230
|
## Test runners: `appduct/client`
|
|
230
231
|
|
package/dist/bin.js
CHANGED
|
@@ -140,10 +140,10 @@ var createCli = () => {
|
|
|
140
140
|
"--timeout <ms>",
|
|
141
141
|
"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)."
|
|
142
142
|
);
|
|
143
|
-
cli.command("events [...args]", "
|
|
143
|
+
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(
|
|
144
144
|
"--name <glob>",
|
|
145
|
-
|
|
146
|
-
).option(
|
|
145
|
+
`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.`
|
|
146
|
+
).option("--limit <n>", "ls: show at most n events.").option("--offset <n>", "ls: skip the first n events of the name-sorted list.").option(
|
|
147
147
|
"--payload-max-bytes <n>",
|
|
148
148
|
"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."
|
|
149
149
|
);
|
|
@@ -524,6 +524,7 @@ var getExitCodeForError = (error) => {
|
|
|
524
524
|
import pc from "picocolors";
|
|
525
525
|
import {
|
|
526
526
|
formatAgentWebSocketUrl,
|
|
527
|
+
renderEventSignature,
|
|
527
528
|
renderToolSignature,
|
|
528
529
|
summarizeToolDescription
|
|
529
530
|
} from "@appduct/shared";
|
|
@@ -619,6 +620,13 @@ var formatDevice = (device) => {
|
|
|
619
620
|
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
620
621
|
return device.os ? `${label} (${device.os})` : label;
|
|
621
622
|
};
|
|
623
|
+
var SUSPEND_REASON_LABELS = {
|
|
624
|
+
app_backgrounded: "app in background",
|
|
625
|
+
connection_lost: "connection lost"
|
|
626
|
+
};
|
|
627
|
+
var formatSessionState = (session) => {
|
|
628
|
+
return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
|
|
629
|
+
};
|
|
622
630
|
var renderLsData = (colors, data, now) => {
|
|
623
631
|
if (data.length === 0) {
|
|
624
632
|
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
@@ -627,7 +635,7 @@ var renderLsData = (colors, data, now) => {
|
|
|
627
635
|
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
628
636
|
const rows = data.map((session) => [
|
|
629
637
|
session.alias,
|
|
630
|
-
session
|
|
638
|
+
formatSessionState(session),
|
|
631
639
|
formatDevice(session.device),
|
|
632
640
|
String(session.toolCount),
|
|
633
641
|
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
@@ -801,6 +809,53 @@ var renderToolsListData = (colors, data, flags, full) => {
|
|
|
801
809
|
}
|
|
802
810
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
803
811
|
};
|
|
812
|
+
var renderEventsTruncationLine = (data) => {
|
|
813
|
+
const offset = data.offset ?? 0;
|
|
814
|
+
if (data.events.length >= data.total) {
|
|
815
|
+
return [];
|
|
816
|
+
}
|
|
817
|
+
return [
|
|
818
|
+
"",
|
|
819
|
+
`Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
|
|
820
|
+
];
|
|
821
|
+
};
|
|
822
|
+
var renderEventsListData = (colors, data, flags) => {
|
|
823
|
+
const [only] = data.events;
|
|
824
|
+
if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
|
|
825
|
+
return renderFields(
|
|
826
|
+
colors.green(`Event: ${only.name}`),
|
|
827
|
+
[
|
|
828
|
+
["Signature", renderEventSignature(only)],
|
|
829
|
+
["Description", only.description],
|
|
830
|
+
["Payload schema", only.payload_schema]
|
|
831
|
+
],
|
|
832
|
+
flags
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (data.events.length === 0 && data.total > 0) {
|
|
836
|
+
return [
|
|
837
|
+
colors.green("Events"),
|
|
838
|
+
` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
|
|
839
|
+
];
|
|
840
|
+
}
|
|
841
|
+
if (data.events.length === 0) {
|
|
842
|
+
return [
|
|
843
|
+
colors.green("Events"),
|
|
844
|
+
data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
|
|
845
|
+
];
|
|
846
|
+
}
|
|
847
|
+
const lines = data.events.flatMap((event) => [
|
|
848
|
+
` ${renderEventSignature(event)}`,
|
|
849
|
+
` ${summarizeToolDescription(event.description)}`
|
|
850
|
+
]);
|
|
851
|
+
return [
|
|
852
|
+
colors.green("Events"),
|
|
853
|
+
...lines,
|
|
854
|
+
...renderEventsTruncationLine(data),
|
|
855
|
+
"",
|
|
856
|
+
"Run `appduct events ls --name <name>` for an event's full payload schema."
|
|
857
|
+
];
|
|
858
|
+
};
|
|
804
859
|
var renderInvokeData = (colors, data, flags) => {
|
|
805
860
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
806
861
|
};
|
|
@@ -992,6 +1047,8 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
992
1047
|
return renderToolsListData(colors, data, flags, options.full);
|
|
993
1048
|
case "tools describe":
|
|
994
1049
|
return renderToolDetail(colors, data, flags);
|
|
1050
|
+
case "events ls":
|
|
1051
|
+
return renderEventsListData(colors, data, flags);
|
|
995
1052
|
case "tools call":
|
|
996
1053
|
return renderInvokeData(colors, data, flags);
|
|
997
1054
|
case "sessions revoke":
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* the thin-client shapes for the v2 command table.
|
|
5
5
|
*/
|
|
6
6
|
import type { OpenTarget } from "./open-target.js";
|
|
7
|
-
import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, SessionSummary, ToolGroupSummary, ToolsListEntry, ToolsListResult } from "@appduct/shared";
|
|
7
|
+
import type { AgentEndpoint, EffectivePolicyDecision, EventNotification, EventsListResult, SessionSummary, ToolGroupSummary, ToolsListEntry, ToolsListResult } from "@appduct/shared";
|
|
8
8
|
/**
|
|
9
9
|
* CLI-domain error classes, used only to pick a sysexits-style exit code (see `errors.ts`).
|
|
10
10
|
* `CliError.type` itself is *not* restricted to this union: RPC/app-originated failures preserve
|
|
@@ -135,6 +135,13 @@ export type ToolGroupsListing = {
|
|
|
135
135
|
* listing entry, picked out of `tools.list` — not a registration, so its `group` is `null` for an
|
|
136
136
|
* ungrouped tool. */
|
|
137
137
|
export type ToolsCommandData = ToolsListing | ToolGroupsListing | ToolsListEntry;
|
|
138
|
+
/** `appduct events ls`: the daemon's `events.list` result, plus the `--name`/`--limit`/`--offset`
|
|
139
|
+
* inputs that produced it (only the ones actually given). */
|
|
140
|
+
export type EventsListing = EventsListResult & {
|
|
141
|
+
name?: string;
|
|
142
|
+
limit?: number;
|
|
143
|
+
offset?: number;
|
|
144
|
+
};
|
|
138
145
|
/** `appduct tools call`: the tool's raw result payload, printed as-is. */
|
|
139
146
|
export type InvokeCommandData = unknown;
|
|
140
147
|
export type RevokeCommandData = {
|
|
@@ -213,6 +213,7 @@ var formatJson = (value, flags) => {
|
|
|
213
213
|
import pc from "picocolors";
|
|
214
214
|
import {
|
|
215
215
|
formatAgentWebSocketUrl,
|
|
216
|
+
renderEventSignature,
|
|
216
217
|
renderToolSignature,
|
|
217
218
|
summarizeToolDescription
|
|
218
219
|
} from "@appduct/shared";
|
|
@@ -308,6 +309,13 @@ var formatDevice = (device) => {
|
|
|
308
309
|
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
309
310
|
return device.os ? `${label} (${device.os})` : label;
|
|
310
311
|
};
|
|
312
|
+
var SUSPEND_REASON_LABELS = {
|
|
313
|
+
app_backgrounded: "app in background",
|
|
314
|
+
connection_lost: "connection lost"
|
|
315
|
+
};
|
|
316
|
+
var formatSessionState = (session) => {
|
|
317
|
+
return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
|
|
318
|
+
};
|
|
311
319
|
var renderLsData = (colors, data, now) => {
|
|
312
320
|
if (data.length === 0) {
|
|
313
321
|
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
@@ -316,7 +324,7 @@ var renderLsData = (colors, data, now) => {
|
|
|
316
324
|
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
317
325
|
const rows = data.map((session) => [
|
|
318
326
|
session.alias,
|
|
319
|
-
session
|
|
327
|
+
formatSessionState(session),
|
|
320
328
|
formatDevice(session.device),
|
|
321
329
|
String(session.toolCount),
|
|
322
330
|
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
@@ -490,6 +498,53 @@ var renderToolsListData = (colors, data, flags, full) => {
|
|
|
490
498
|
}
|
|
491
499
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
492
500
|
};
|
|
501
|
+
var renderEventsTruncationLine = (data) => {
|
|
502
|
+
const offset = data.offset ?? 0;
|
|
503
|
+
if (data.events.length >= data.total) {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
return [
|
|
507
|
+
"",
|
|
508
|
+
`Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
|
|
509
|
+
];
|
|
510
|
+
};
|
|
511
|
+
var renderEventsListData = (colors, data, flags) => {
|
|
512
|
+
const [only] = data.events;
|
|
513
|
+
if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
|
|
514
|
+
return renderFields(
|
|
515
|
+
colors.green(`Event: ${only.name}`),
|
|
516
|
+
[
|
|
517
|
+
["Signature", renderEventSignature(only)],
|
|
518
|
+
["Description", only.description],
|
|
519
|
+
["Payload schema", only.payload_schema]
|
|
520
|
+
],
|
|
521
|
+
flags
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
if (data.events.length === 0 && data.total > 0) {
|
|
525
|
+
return [
|
|
526
|
+
colors.green("Events"),
|
|
527
|
+
` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
|
|
528
|
+
];
|
|
529
|
+
}
|
|
530
|
+
if (data.events.length === 0) {
|
|
531
|
+
return [
|
|
532
|
+
colors.green("Events"),
|
|
533
|
+
data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
const lines = data.events.flatMap((event) => [
|
|
537
|
+
` ${renderEventSignature(event)}`,
|
|
538
|
+
` ${summarizeToolDescription(event.description)}`
|
|
539
|
+
]);
|
|
540
|
+
return [
|
|
541
|
+
colors.green("Events"),
|
|
542
|
+
...lines,
|
|
543
|
+
...renderEventsTruncationLine(data),
|
|
544
|
+
"",
|
|
545
|
+
"Run `appduct events ls --name <name>` for an event's full payload schema."
|
|
546
|
+
];
|
|
547
|
+
};
|
|
493
548
|
var renderInvokeData = (colors, data, flags) => {
|
|
494
549
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
495
550
|
};
|
|
@@ -681,6 +736,8 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
681
736
|
return renderToolsListData(colors, data, flags, options.full);
|
|
682
737
|
case "tools describe":
|
|
683
738
|
return renderToolDetail(colors, data, flags);
|
|
739
|
+
case "events ls":
|
|
740
|
+
return renderEventsListData(colors, data, flags);
|
|
684
741
|
case "tools call":
|
|
685
742
|
return renderInvokeData(colors, data, flags);
|
|
686
743
|
case "sessions revoke":
|
|
@@ -484,6 +484,7 @@ var RpcApplicationError = class extends Error {
|
|
|
484
484
|
this.name = "RpcApplicationError";
|
|
485
485
|
}
|
|
486
486
|
};
|
|
487
|
+
var appBackgroundedMessage = (alias) => `Session "${alias}" is suspended because the app is in the background. Bring the app to the foreground to resume it.`;
|
|
487
488
|
|
|
488
489
|
// src/daemon/timers.ts
|
|
489
490
|
var systemTimers = {
|
|
@@ -586,7 +587,7 @@ var createCallsManager = (options) => {
|
|
|
586
587
|
const message = { type: "tool_cancel", session_id: sessionId, id: callId, reason };
|
|
587
588
|
return options.sendCancel(sessionId, message);
|
|
588
589
|
};
|
|
589
|
-
const rejectSession = (sessionId, errorType) => {
|
|
590
|
+
const rejectSession = (sessionId, errorType, options2 = {}) => {
|
|
590
591
|
const sessionMap = pendingBySession.get(sessionId);
|
|
591
592
|
if (!sessionMap) {
|
|
592
593
|
return;
|
|
@@ -594,13 +595,19 @@ var createCallsManager = (options) => {
|
|
|
594
595
|
pendingBySession.delete(sessionId);
|
|
595
596
|
for (const pending of sessionMap.values()) {
|
|
596
597
|
timers.clearTimeout(pending.timer);
|
|
597
|
-
pending.reject(
|
|
598
|
+
pending.reject(
|
|
599
|
+
new RpcApplicationError(
|
|
600
|
+
errorType,
|
|
601
|
+
options2.backgrounded ? appBackgroundedMessage(pending.alias) : "Session transitioned while the call was pending."
|
|
602
|
+
)
|
|
603
|
+
);
|
|
598
604
|
}
|
|
599
605
|
};
|
|
600
606
|
const unsubscribeFromEventBus = options.eventBus.subscribe((event) => {
|
|
601
607
|
const errorType = TERMINATING_KIND_ERROR_TYPES[event.kind];
|
|
602
608
|
if (errorType && event.sessionId) {
|
|
603
|
-
|
|
609
|
+
const backgrounded = event.kind === "session_suspended" && event.data.reason === "app_backgrounded";
|
|
610
|
+
rejectSession(event.sessionId, errorType, { backgrounded });
|
|
604
611
|
}
|
|
605
612
|
});
|
|
606
613
|
const disposeAll = () => {
|
|
@@ -791,6 +798,8 @@ import {
|
|
|
791
798
|
isEventMessage,
|
|
792
799
|
isToolCallProgressMessage,
|
|
793
800
|
isToolErrorMessage,
|
|
801
|
+
isEventRegistryDeltaMessage,
|
|
802
|
+
isEventRegistrySnapshotMessage,
|
|
794
803
|
isToolRegistryDeltaMessage,
|
|
795
804
|
isToolRegistrySnapshotMessage,
|
|
796
805
|
isToolResultMessage,
|
|
@@ -888,7 +897,7 @@ var createPendingLinkRegistry = (options) => {
|
|
|
888
897
|
};
|
|
889
898
|
|
|
890
899
|
// src/daemon/registry.ts
|
|
891
|
-
var
|
|
900
|
+
var createRegistry = (onChange) => {
|
|
892
901
|
let tools = /* @__PURE__ */ new Map();
|
|
893
902
|
const notify = () => {
|
|
894
903
|
onChange?.();
|
|
@@ -918,6 +927,8 @@ var RESUME_TOKEN_BYTES = 32;
|
|
|
918
927
|
var POST_CLAIM_MESSAGE_TYPES = /* @__PURE__ */ new Set([
|
|
919
928
|
"tool_registry_snapshot",
|
|
920
929
|
"tool_registry_delta",
|
|
930
|
+
"event_registry_snapshot",
|
|
931
|
+
"event_registry_delta",
|
|
921
932
|
"tool_result",
|
|
922
933
|
"tool_error",
|
|
923
934
|
"tool_call_progress",
|
|
@@ -988,6 +999,7 @@ var createSessionManager = (options) => {
|
|
|
988
999
|
createdAt: session.createdAt.toISOString(),
|
|
989
1000
|
claimedAt: session.claimedAt.toISOString(),
|
|
990
1001
|
...session.suspendedAt ? { suspendedAt: session.suspendedAt.toISOString() } : {},
|
|
1002
|
+
...session.suspendReason ? { suspendReason: session.suspendReason } : {},
|
|
991
1003
|
toolCount: session.registry.count()
|
|
992
1004
|
};
|
|
993
1005
|
};
|
|
@@ -1024,7 +1036,8 @@ var createSessionManager = (options) => {
|
|
|
1024
1036
|
alias: session.alias,
|
|
1025
1037
|
resume_token: session.resumeToken.toString("base64url"),
|
|
1026
1038
|
keepalive_interval_s: options.keepaliveIntervalSeconds,
|
|
1027
|
-
grace_s: options.graceSeconds
|
|
1039
|
+
grace_s: options.graceSeconds,
|
|
1040
|
+
event_registry: true
|
|
1028
1041
|
};
|
|
1029
1042
|
socket.send(JSON.stringify(ack), (error) => {
|
|
1030
1043
|
if (error) {
|
|
@@ -1032,17 +1045,18 @@ var createSessionManager = (options) => {
|
|
|
1032
1045
|
}
|
|
1033
1046
|
});
|
|
1034
1047
|
};
|
|
1035
|
-
const suspend = (sessionId) => {
|
|
1048
|
+
const suspend = (sessionId, reason) => {
|
|
1036
1049
|
const session = sessions.get(sessionId);
|
|
1037
1050
|
if (!session || session.state !== "active") {
|
|
1038
1051
|
return;
|
|
1039
1052
|
}
|
|
1040
1053
|
session.state = "suspended";
|
|
1041
1054
|
session.suspendedAt = clock.now();
|
|
1055
|
+
session.suspendReason = reason;
|
|
1042
1056
|
session.socket = void 0;
|
|
1043
1057
|
session.missedPongs = 0;
|
|
1044
1058
|
session.graceTimer = timers.setTimeout(() => expire(sessionId), options.graceSeconds * 1e3);
|
|
1045
|
-
emit("session_suspended", session.sessionId, session.alias, {});
|
|
1059
|
+
emit("session_suspended", session.sessionId, session.alias, { reason });
|
|
1046
1060
|
};
|
|
1047
1061
|
const expire = (sessionId) => {
|
|
1048
1062
|
const session = sessions.get(sessionId);
|
|
@@ -1059,9 +1073,10 @@ var createSessionManager = (options) => {
|
|
|
1059
1073
|
socket.on("pong", () => {
|
|
1060
1074
|
session.missedPongs = 0;
|
|
1061
1075
|
});
|
|
1062
|
-
socket.on("close", () => {
|
|
1076
|
+
socket.on("close", (code, reason) => {
|
|
1063
1077
|
if (session.socket === socket) {
|
|
1064
|
-
|
|
1078
|
+
const backgrounded = code === 1001 && reason.toString("utf8") === "app_backgrounded";
|
|
1079
|
+
suspend(session.sessionId, backgrounded ? "app_backgrounded" : "connection_lost");
|
|
1065
1080
|
}
|
|
1066
1081
|
});
|
|
1067
1082
|
};
|
|
@@ -1073,7 +1088,7 @@ var createSessionManager = (options) => {
|
|
|
1073
1088
|
if (session.missedPongs >= 2) {
|
|
1074
1089
|
const staleSocket = session.socket;
|
|
1075
1090
|
session.socket = void 0;
|
|
1076
|
-
suspend(session.sessionId);
|
|
1091
|
+
suspend(session.sessionId, "connection_lost");
|
|
1077
1092
|
staleSocket.terminate();
|
|
1078
1093
|
continue;
|
|
1079
1094
|
}
|
|
@@ -1138,8 +1153,11 @@ var createSessionManager = (options) => {
|
|
|
1138
1153
|
resumeToken: randomBytes3(RESUME_TOKEN_BYTES),
|
|
1139
1154
|
socket,
|
|
1140
1155
|
missedPongs: 0,
|
|
1141
|
-
registry:
|
|
1156
|
+
registry: createRegistry(() => {
|
|
1142
1157
|
emit("tools_changed", sessionId, alias, { toolCount: session.registry.count() });
|
|
1158
|
+
}),
|
|
1159
|
+
eventRegistry: createRegistry(() => {
|
|
1160
|
+
emit("events_changed", sessionId, alias, { eventCount: session.eventRegistry.count() });
|
|
1143
1161
|
})
|
|
1144
1162
|
};
|
|
1145
1163
|
sessions.set(sessionId, session);
|
|
@@ -1171,6 +1189,7 @@ var createSessionManager = (options) => {
|
|
|
1171
1189
|
session.resumeToken = randomBytes3(RESUME_TOKEN_BYTES);
|
|
1172
1190
|
session.state = "active";
|
|
1173
1191
|
session.suspendedAt = void 0;
|
|
1192
|
+
session.suspendReason = void 0;
|
|
1174
1193
|
session.socket = socket;
|
|
1175
1194
|
session.missedPongs = 0;
|
|
1176
1195
|
attachSocketHandlers(session);
|
|
@@ -1205,6 +1224,26 @@ var createSessionManager = (options) => {
|
|
|
1205
1224
|
}
|
|
1206
1225
|
return;
|
|
1207
1226
|
}
|
|
1227
|
+
case "event_registry_snapshot": {
|
|
1228
|
+
if (!isEventRegistrySnapshotMessage(message)) {
|
|
1229
|
+
closeSocket(socket, 1008, "invalid_registry");
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
session.eventRegistry.snapshot(message.events);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
case "event_registry_delta": {
|
|
1236
|
+
if (!isEventRegistryDeltaMessage(message)) {
|
|
1237
|
+
closeSocket(socket, 1008, "invalid_registry");
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (message.operation === "upsert") {
|
|
1241
|
+
session.eventRegistry.upsert(message.event);
|
|
1242
|
+
} else {
|
|
1243
|
+
session.eventRegistry.remove(message.name);
|
|
1244
|
+
}
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1208
1247
|
case "tool_result": {
|
|
1209
1248
|
if (!isToolResultMessage(message)) {
|
|
1210
1249
|
closeSocket(socket, 1008, "invalid_message");
|
|
@@ -1272,7 +1311,14 @@ var createSessionManager = (options) => {
|
|
|
1272
1311
|
};
|
|
1273
1312
|
const resolveForTools = (selector) => {
|
|
1274
1313
|
const session = resolveSession(selector);
|
|
1275
|
-
return {
|
|
1314
|
+
return {
|
|
1315
|
+
sessionId: session.sessionId,
|
|
1316
|
+
alias: session.alias,
|
|
1317
|
+
state: session.state,
|
|
1318
|
+
suspendReason: session.suspendReason,
|
|
1319
|
+
registry: session.registry,
|
|
1320
|
+
eventRegistry: session.eventRegistry
|
|
1321
|
+
};
|
|
1276
1322
|
};
|
|
1277
1323
|
const sendToApp = (sessionId, message) => {
|
|
1278
1324
|
const session = sessions.get(sessionId);
|
|
@@ -2040,6 +2086,26 @@ var asSelectorParams = (params) => {
|
|
|
2040
2086
|
}
|
|
2041
2087
|
return { selector };
|
|
2042
2088
|
};
|
|
2089
|
+
var asPageParams = (record) => {
|
|
2090
|
+
const limit = record.limit;
|
|
2091
|
+
if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
|
|
2092
|
+
throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
|
|
2093
|
+
}
|
|
2094
|
+
const offset = record.offset;
|
|
2095
|
+
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) {
|
|
2096
|
+
throw new RpcApplicationError("invalid_request", '"offset" must be a non-negative integer.');
|
|
2097
|
+
}
|
|
2098
|
+
return { limit, offset };
|
|
2099
|
+
};
|
|
2100
|
+
var asEventsListParams = (params) => {
|
|
2101
|
+
const { selector } = asSelectorParams(params);
|
|
2102
|
+
const record = asRecordParams(params);
|
|
2103
|
+
const name = record.name;
|
|
2104
|
+
if (name !== void 0 && typeof name !== "string") {
|
|
2105
|
+
throw new RpcApplicationError("invalid_request", '"name" must be a string.');
|
|
2106
|
+
}
|
|
2107
|
+
return { selector, name, ...asPageParams(record) };
|
|
2108
|
+
};
|
|
2043
2109
|
var asToolsListParams = (params) => {
|
|
2044
2110
|
const { selector } = asSelectorParams(params);
|
|
2045
2111
|
const record = asRecordParams(params);
|
|
@@ -2057,14 +2123,7 @@ var asToolsListParams = (params) => {
|
|
|
2057
2123
|
`"filter" must be a string of at most ${MAX_TOOLS_FILTER_LENGTH} characters.`
|
|
2058
2124
|
);
|
|
2059
2125
|
}
|
|
2060
|
-
const limit = record
|
|
2061
|
-
if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit <= 0)) {
|
|
2062
|
-
throw new RpcApplicationError("invalid_request", '"limit" must be a positive integer.');
|
|
2063
|
-
}
|
|
2064
|
-
const offset = record.offset;
|
|
2065
|
-
if (offset !== void 0 && (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0)) {
|
|
2066
|
-
throw new RpcApplicationError("invalid_request", '"offset" must be a non-negative integer.');
|
|
2067
|
-
}
|
|
2126
|
+
const { limit, offset } = asPageParams(record);
|
|
2068
2127
|
return {
|
|
2069
2128
|
selector,
|
|
2070
2129
|
group,
|
|
@@ -2395,6 +2454,17 @@ var startDaemon = async (options) => {
|
|
|
2395
2454
|
activeSessionManager.revoke(selector);
|
|
2396
2455
|
return { ok: true };
|
|
2397
2456
|
},
|
|
2457
|
+
[RPC_METHODS.eventsList]: (params) => {
|
|
2458
|
+
const { selector, name, limit, offset } = asEventsListParams(params);
|
|
2459
|
+
const registered = activeSessionManager.resolveForTools(selector).eventRegistry.list();
|
|
2460
|
+
const sorted = registered.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
2461
|
+
const matching = name === void 0 ? sorted : sorted.filter((event) => matchesNameGlob(name, event.name));
|
|
2462
|
+
const start = offset ?? 0;
|
|
2463
|
+
return {
|
|
2464
|
+
events: limit === void 0 ? matching.slice(start) : matching.slice(start, start + limit),
|
|
2465
|
+
total: matching.length
|
|
2466
|
+
};
|
|
2467
|
+
},
|
|
2398
2468
|
[RPC_METHODS.toolsList]: (params) => {
|
|
2399
2469
|
const { selector, group, filter, limit, offset } = asToolsListParams(params);
|
|
2400
2470
|
const resolved = activeSessionManager.resolveForTools(selector);
|
|
@@ -2442,7 +2512,10 @@ var startDaemon = async (options) => {
|
|
|
2442
2512
|
if (resolved.state !== "active") {
|
|
2443
2513
|
const errorType = resolved.state === "suspended" ? "session_suspended" : "session_not_active";
|
|
2444
2514
|
writeAudit("error", errorType);
|
|
2445
|
-
throw new RpcApplicationError(
|
|
2515
|
+
throw new RpcApplicationError(
|
|
2516
|
+
errorType,
|
|
2517
|
+
resolved.suspendReason === "app_backgrounded" ? appBackgroundedMessage(resolved.alias) : `Session "${resolved.alias}" is not active.`
|
|
2518
|
+
);
|
|
2446
2519
|
}
|
|
2447
2520
|
const tool = resolved.registry.get(name);
|
|
2448
2521
|
if (!tool) {
|
|
@@ -2786,6 +2859,7 @@ var formatJson = (value, flags) => {
|
|
|
2786
2859
|
import pc from "picocolors";
|
|
2787
2860
|
import {
|
|
2788
2861
|
formatAgentWebSocketUrl,
|
|
2862
|
+
renderEventSignature,
|
|
2789
2863
|
renderToolSignature,
|
|
2790
2864
|
summarizeToolDescription
|
|
2791
2865
|
} from "@appduct/shared";
|
|
@@ -2881,6 +2955,13 @@ var formatDevice = (device) => {
|
|
|
2881
2955
|
const label = parts.length > 0 ? parts.join(" ") : "unknown device";
|
|
2882
2956
|
return device.os ? `${label} (${device.os})` : label;
|
|
2883
2957
|
};
|
|
2958
|
+
var SUSPEND_REASON_LABELS = {
|
|
2959
|
+
app_backgrounded: "app in background",
|
|
2960
|
+
connection_lost: "connection lost"
|
|
2961
|
+
};
|
|
2962
|
+
var formatSessionState = (session) => {
|
|
2963
|
+
return session.suspendReason ? `${session.state} (${SUSPEND_REASON_LABELS[session.suspendReason]})` : session.state;
|
|
2964
|
+
};
|
|
2884
2965
|
var renderLsData = (colors, data, now) => {
|
|
2885
2966
|
if (data.length === 0) {
|
|
2886
2967
|
return [colors.dim("Sessions"), " No Appduct sessions are registered."];
|
|
@@ -2889,7 +2970,7 @@ var renderLsData = (colors, data, now) => {
|
|
|
2889
2970
|
const headers = ["Alias", "State", "Device", "Tools", "Age"];
|
|
2890
2971
|
const rows = data.map((session) => [
|
|
2891
2972
|
session.alias,
|
|
2892
|
-
session
|
|
2973
|
+
formatSessionState(session),
|
|
2893
2974
|
formatDevice(session.device),
|
|
2894
2975
|
String(session.toolCount),
|
|
2895
2976
|
formatAge(session.claimedAt ?? session.createdAt, nowIso)
|
|
@@ -3063,6 +3144,53 @@ var renderToolsListData = (colors, data, flags, full) => {
|
|
|
3063
3144
|
}
|
|
3064
3145
|
return full ? renderToolsFullListing(colors, data, flags) : renderToolSummaryTable(colors, data);
|
|
3065
3146
|
};
|
|
3147
|
+
var renderEventsTruncationLine = (data) => {
|
|
3148
|
+
const offset = data.offset ?? 0;
|
|
3149
|
+
if (data.events.length >= data.total) {
|
|
3150
|
+
return [];
|
|
3151
|
+
}
|
|
3152
|
+
return [
|
|
3153
|
+
"",
|
|
3154
|
+
`Showing ${data.events.length} of ${data.total} events (offset ${offset}). Narrow with --name <glob> or page with --offset <n>.`
|
|
3155
|
+
];
|
|
3156
|
+
};
|
|
3157
|
+
var renderEventsListData = (colors, data, flags) => {
|
|
3158
|
+
const [only] = data.events;
|
|
3159
|
+
if (data.name !== void 0 && !data.name.includes("*") && only !== void 0) {
|
|
3160
|
+
return renderFields(
|
|
3161
|
+
colors.green(`Event: ${only.name}`),
|
|
3162
|
+
[
|
|
3163
|
+
["Signature", renderEventSignature(only)],
|
|
3164
|
+
["Description", only.description],
|
|
3165
|
+
["Payload schema", only.payload_schema]
|
|
3166
|
+
],
|
|
3167
|
+
flags
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
if (data.events.length === 0 && data.total > 0) {
|
|
3171
|
+
return [
|
|
3172
|
+
colors.green("Events"),
|
|
3173
|
+
` No events at offset ${data.offset ?? 0}; ${data.total} matching event${data.total === 1 ? "" : "s"} in total.`
|
|
3174
|
+
];
|
|
3175
|
+
}
|
|
3176
|
+
if (data.events.length === 0) {
|
|
3177
|
+
return [
|
|
3178
|
+
colors.green("Events"),
|
|
3179
|
+
data.name === void 0 ? " No events declared." : ` No events match ${JSON.stringify(data.name)}.`
|
|
3180
|
+
];
|
|
3181
|
+
}
|
|
3182
|
+
const lines = data.events.flatMap((event) => [
|
|
3183
|
+
` ${renderEventSignature(event)}`,
|
|
3184
|
+
` ${summarizeToolDescription(event.description)}`
|
|
3185
|
+
]);
|
|
3186
|
+
return [
|
|
3187
|
+
colors.green("Events"),
|
|
3188
|
+
...lines,
|
|
3189
|
+
...renderEventsTruncationLine(data),
|
|
3190
|
+
"",
|
|
3191
|
+
"Run `appduct events ls --name <name>` for an event's full payload schema."
|
|
3192
|
+
];
|
|
3193
|
+
};
|
|
3066
3194
|
var renderInvokeData = (colors, data, flags) => {
|
|
3067
3195
|
return [colors.green("Result"), formatScalar(data, flags)];
|
|
3068
3196
|
};
|
|
@@ -3254,6 +3382,8 @@ var renderSuccessData = (colors, command, data, options) => {
|
|
|
3254
3382
|
return renderToolsListData(colors, data, flags, options.full);
|
|
3255
3383
|
case "tools describe":
|
|
3256
3384
|
return renderToolDetail(colors, data, flags);
|
|
3385
|
+
case "events ls":
|
|
3386
|
+
return renderEventsListData(colors, data, flags);
|
|
3257
3387
|
case "tools call":
|
|
3258
3388
|
return renderInvokeData(colors, data, flags);
|
|
3259
3389
|
case "sessions revoke":
|