remote-pi 0.1.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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/dist/config.d.ts +18 -0
  4. package/dist/config.js +51 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/index.d.ts +77 -0
  7. package/dist/index.js +1285 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/pairing/crypto.d.ts +8 -0
  10. package/dist/pairing/crypto.js +22 -0
  11. package/dist/pairing/crypto.js.map +1 -0
  12. package/dist/pairing/handshake.d.ts +28 -0
  13. package/dist/pairing/handshake.js +113 -0
  14. package/dist/pairing/handshake.js.map +1 -0
  15. package/dist/pairing/noise-sha256.d.ts +16 -0
  16. package/dist/pairing/noise-sha256.js +103 -0
  17. package/dist/pairing/noise-sha256.js.map +1 -0
  18. package/dist/pairing/qr.d.ts +41 -0
  19. package/dist/pairing/qr.js +96 -0
  20. package/dist/pairing/qr.js.map +1 -0
  21. package/dist/pairing/storage.d.ts +10 -0
  22. package/dist/pairing/storage.js +65 -0
  23. package/dist/pairing/storage.js.map +1 -0
  24. package/dist/protocol/codec.d.ts +7 -0
  25. package/dist/protocol/codec.js +46 -0
  26. package/dist/protocol/codec.js.map +1 -0
  27. package/dist/protocol/types.d.ts +119 -0
  28. package/dist/protocol/types.js +2 -0
  29. package/dist/protocol/types.js.map +1 -0
  30. package/dist/rooms.d.ts +9 -0
  31. package/dist/rooms.js +22 -0
  32. package/dist/rooms.js.map +1 -0
  33. package/dist/session/agent_bridge.d.ts +55 -0
  34. package/dist/session/agent_bridge.js +146 -0
  35. package/dist/session/agent_bridge.js.map +1 -0
  36. package/dist/session/broker.d.ts +37 -0
  37. package/dist/session/broker.js +206 -0
  38. package/dist/session/broker.js.map +1 -0
  39. package/dist/session/envelope.d.ts +24 -0
  40. package/dist/session/envelope.js +89 -0
  41. package/dist/session/envelope.js.map +1 -0
  42. package/dist/session/global_config.d.ts +14 -0
  43. package/dist/session/global_config.js +51 -0
  44. package/dist/session/global_config.js.map +1 -0
  45. package/dist/session/leader_election.d.ts +16 -0
  46. package/dist/session/leader_election.js +78 -0
  47. package/dist/session/leader_election.js.map +1 -0
  48. package/dist/session/local_config.d.ts +18 -0
  49. package/dist/session/local_config.js +47 -0
  50. package/dist/session/local_config.js.map +1 -0
  51. package/dist/session/peer.d.ts +80 -0
  52. package/dist/session/peer.js +268 -0
  53. package/dist/session/peer.js.map +1 -0
  54. package/dist/session/setup_wizard.d.ts +32 -0
  55. package/dist/session/setup_wizard.js +60 -0
  56. package/dist/session/setup_wizard.js.map +1 -0
  57. package/dist/session/tool_gate.d.ts +5 -0
  58. package/dist/session/tool_gate.js +11 -0
  59. package/dist/session/tool_gate.js.map +1 -0
  60. package/dist/session/tools.d.ts +16 -0
  61. package/dist/session/tools.js +123 -0
  62. package/dist/session/tools.js.map +1 -0
  63. package/dist/session/wizard.d.ts +13 -0
  64. package/dist/session/wizard.js +20 -0
  65. package/dist/session/wizard.js.map +1 -0
  66. package/dist/settings.d.ts +15 -0
  67. package/dist/settings.js +52 -0
  68. package/dist/settings.js.map +1 -0
  69. package/dist/transport/peer_channel.d.ts +37 -0
  70. package/dist/transport/peer_channel.js +85 -0
  71. package/dist/transport/peer_channel.js.map +1 -0
  72. package/dist/transport/relay_client.d.ts +81 -0
  73. package/dist/transport/relay_client.js +154 -0
  74. package/dist/transport/relay_client.js.map +1 -0
  75. package/dist/ui/footer.d.ts +32 -0
  76. package/dist/ui/footer.js +32 -0
  77. package/dist/ui/footer.js.map +1 -0
  78. package/package.json +77 -0
  79. package/skills/agent-network/SKILL.md +429 -0
@@ -0,0 +1,65 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import keytar from "keytar";
5
+ import { generateEd25519Keypair } from "./crypto.js";
6
+ const KEYCHAIN_SERVICE = "dev.remotepi.mac";
7
+ const PEERS_PATH = join(homedir(), ".pi", "remote", "peers.json");
8
+ // ── Keychain ──────────────────────────────────────────────────────────────────
9
+ async function loadKeypairFromKeychain(account) {
10
+ const stored = await keytar.getPassword(KEYCHAIN_SERVICE, account);
11
+ if (!stored)
12
+ return null;
13
+ const parsed = JSON.parse(stored);
14
+ return {
15
+ publicKey: Buffer.from(parsed.pk, "base64"),
16
+ secretKey: Buffer.from(parsed.sk, "base64"),
17
+ };
18
+ }
19
+ async function saveKeypairToKeychain(account, kp) {
20
+ const data = JSON.stringify({
21
+ pk: Buffer.from(kp.publicKey).toString("base64"),
22
+ sk: Buffer.from(kp.secretKey).toString("base64"),
23
+ });
24
+ await keytar.setPassword(KEYCHAIN_SERVICE, account, data);
25
+ }
26
+ export async function getOrCreateEd25519Keypair() {
27
+ const existing = await loadKeypairFromKeychain("longterm-ed25519");
28
+ if (existing)
29
+ return existing;
30
+ const kp = generateEd25519Keypair();
31
+ await saveKeypairToKeychain("longterm-ed25519", kp);
32
+ return kp;
33
+ }
34
+ export async function listPeers() {
35
+ try {
36
+ const raw = await readFile(PEERS_PATH, "utf8");
37
+ const parsed = JSON.parse(raw);
38
+ return parsed.peers ?? [];
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
44
+ export async function addPeer(record) {
45
+ const peers = await listPeers();
46
+ const idx = peers.findIndex((p) => p.remote_epk === record.remote_epk);
47
+ if (idx >= 0) {
48
+ peers[idx] = record; // idempotent re-pair
49
+ }
50
+ else {
51
+ peers.push(record);
52
+ }
53
+ await mkdir(dirname(PEERS_PATH), { recursive: true });
54
+ await writeFile(PEERS_PATH, JSON.stringify({ peers }, null, 2));
55
+ }
56
+ export async function removePeer(remoteEpk) {
57
+ const peers = await listPeers();
58
+ const filtered = peers.filter((p) => p.remote_epk !== remoteEpk);
59
+ if (filtered.length === peers.length)
60
+ return false;
61
+ await mkdir(dirname(PEERS_PATH), { recursive: true });
62
+ await writeFile(PEERS_PATH, JSON.stringify({ peers: filtered }, null, 2));
63
+ return true;
64
+ }
65
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/pairing/storage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import type { ClientMessage, ServerMessage } from "./types.js";
2
+ export declare class DecodeError extends Error {
3
+ readonly code: "invalid_message" | "unsupported_type";
4
+ constructor(code: "invalid_message" | "unsupported_type", message: string);
5
+ }
6
+ export declare function encodeClient(msg: ClientMessage): string;
7
+ export declare function decodeServer(line: string): ServerMessage;
@@ -0,0 +1,46 @@
1
+ const SERVER_TYPES = new Set([
2
+ "pair_ok",
3
+ "pair_error",
4
+ "user_input",
5
+ "agent_chunk",
6
+ "agent_done",
7
+ "agent_message",
8
+ "tool_request",
9
+ "tool_result",
10
+ "error",
11
+ "cancelled",
12
+ "pong",
13
+ "bye",
14
+ "session_history",
15
+ ]);
16
+ export class DecodeError extends Error {
17
+ code;
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.code = code;
21
+ this.name = "DecodeError";
22
+ }
23
+ }
24
+ export function encodeClient(msg) {
25
+ return JSON.stringify(msg) + "\n";
26
+ }
27
+ export function decodeServer(line) {
28
+ let obj;
29
+ try {
30
+ obj = JSON.parse(line.trim());
31
+ }
32
+ catch (e) {
33
+ throw new DecodeError("invalid_message", `not JSON: ${e.message}`);
34
+ }
35
+ if (!obj ||
36
+ typeof obj !== "object" ||
37
+ typeof obj.type !== "string") {
38
+ throw new DecodeError("invalid_message", "missing 'type'");
39
+ }
40
+ const t = obj.type;
41
+ if (!SERVER_TYPES.has(t)) {
42
+ throw new DecodeError("unsupported_type", `unknown type: ${t}`);
43
+ }
44
+ return obj;
45
+ }
46
+ //# sourceMappingURL=codec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.js","sourceRoot":"","sources":["../../src/protocol/codec.ts"],"names":[],"mappings":"AAEA,MAAM,YAAY,GAAG,IAAI,GAAG,CAAwB;IAClD,SAAS;IACT,YAAY;IACZ,YAAY;IACZ,aAAa;IACb,YAAY;IACZ,eAAe;IACf,cAAc;IACd,aAAa;IACb,OAAO;IACP,WAAW;IACX,MAAM;IACN,KAAK;IACL,iBAAiB;CAClB,CAAC,CAAC;AAEH,MAAM,OAAO,WAAY,SAAQ,KAAK;IAElB;IADlB,YACkB,IAA4C,EAC5D,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,SAAI,GAAJ,IAAI,CAAwC;QAI5D,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF;AAED,MAAM,UAAU,YAAY,CAAC,GAAkB;IAC7C,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,WAAW,CAAC,iBAAiB,EAAE,aAAc,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IAChF,CAAC;IACD,IACE,CAAC,GAAG;QACJ,OAAO,GAAG,KAAK,QAAQ;QACvB,OAAQ,GAA+B,CAAC,IAAI,KAAK,QAAQ,EACzD,CAAC;QACD,MAAM,IAAI,WAAW,CAAC,iBAAiB,EAAE,gBAAgB,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,CAAC,GAAI,GAA+B,CAAC,IAAc,CAAC;IAC1D,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAA0B,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,WAAW,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,GAAoB,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,119 @@
1
+ export type PairErrorCode = "token_expired" | "token_consumed" | "token_unknown" | "internal_error";
2
+ export type ClientMessage = {
3
+ type: "pair_request";
4
+ id: string;
5
+ token: string;
6
+ device_name: string;
7
+ } | {
8
+ type: "user_message";
9
+ id: string;
10
+ text: string;
11
+ } | {
12
+ type: "approve_tool";
13
+ id: string;
14
+ tool_call_id: string;
15
+ decision: "allow" | "deny";
16
+ } | {
17
+ type: "cancel";
18
+ id: string;
19
+ target_id: string;
20
+ } | {
21
+ type: "ping";
22
+ id: string;
23
+ } | {
24
+ type: "session_sync";
25
+ id: string;
26
+ limit?: number;
27
+ };
28
+ export type Usage = {
29
+ input_tokens: number;
30
+ output_tokens: number;
31
+ };
32
+ export type KnownErrorCode = "tool_approval_required" | "invalid_message" | "unsupported_type" | "too_large" | "rate_limited" | "timeout" | "internal_error";
33
+ export type ErrorCode = KnownErrorCode | (string & {});
34
+ export type SessionHistoryEvent = {
35
+ ts: number;
36
+ type: "user_input";
37
+ id: string;
38
+ text: string;
39
+ } | {
40
+ ts: number;
41
+ type: "tool_request";
42
+ tool_call_id: string;
43
+ tool: string;
44
+ args: Record<string, unknown>;
45
+ } | {
46
+ ts: number;
47
+ type: "tool_result";
48
+ tool_call_id: string;
49
+ result?: unknown;
50
+ error?: string;
51
+ } | {
52
+ ts: number;
53
+ type: "agent_message";
54
+ in_reply_to: string;
55
+ text: string;
56
+ usage?: Usage;
57
+ };
58
+ export type ServerMessage = {
59
+ type: "pair_ok";
60
+ in_reply_to: string;
61
+ session_name: string;
62
+ session_started_at: number;
63
+ room_id: string;
64
+ } | {
65
+ type: "pair_error";
66
+ in_reply_to: string;
67
+ code: PairErrorCode;
68
+ message: string;
69
+ } | {
70
+ type: "user_input";
71
+ id: string;
72
+ text: string;
73
+ } | {
74
+ type: "agent_chunk";
75
+ in_reply_to: string;
76
+ delta: string;
77
+ } | {
78
+ type: "agent_done";
79
+ in_reply_to: string;
80
+ usage?: Usage;
81
+ } | {
82
+ type: "agent_message";
83
+ in_reply_to: string;
84
+ text: string;
85
+ usage?: Usage;
86
+ } | {
87
+ type: "tool_request";
88
+ tool_call_id: string;
89
+ tool: string;
90
+ args: Record<string, unknown>;
91
+ } | {
92
+ type: "tool_result";
93
+ tool_call_id: string;
94
+ result?: unknown;
95
+ error?: string;
96
+ } | {
97
+ type: "error";
98
+ in_reply_to?: string;
99
+ code: ErrorCode;
100
+ message: string;
101
+ } | {
102
+ type: "cancelled";
103
+ in_reply_to: string;
104
+ target_id: string;
105
+ } | {
106
+ type: "pong";
107
+ in_reply_to: string;
108
+ } | {
109
+ type: "bye";
110
+ reason: ByeReason;
111
+ } | {
112
+ type: "session_history";
113
+ in_reply_to: string;
114
+ session_started_at: number;
115
+ events: SessionHistoryEvent[];
116
+ eos: boolean;
117
+ truncated: boolean;
118
+ };
119
+ export type ByeReason = "peer_stop" | "session_replaced" | "shutdown";
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/protocol/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Deterministic room id derived from a cwd. Two Pi processes in the same
3
+ * directory produce the same id; different cwds produce different ids
4
+ * (with cryptographic-strength collision resistance). Symlinks are resolved
5
+ * via `realpath` so `/a` and `/symlink-to-a` map to the same room.
6
+ *
7
+ * Format: first 12 chars of base64url(sha256(realpath)).
8
+ */
9
+ export declare function roomIdForCwd(cwd: string): string;
package/dist/rooms.js ADDED
@@ -0,0 +1,22 @@
1
+ import { createHash } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ /**
4
+ * Deterministic room id derived from a cwd. Two Pi processes in the same
5
+ * directory produce the same id; different cwds produce different ids
6
+ * (with cryptographic-strength collision resistance). Symlinks are resolved
7
+ * via `realpath` so `/a` and `/symlink-to-a` map to the same room.
8
+ *
9
+ * Format: first 12 chars of base64url(sha256(realpath)).
10
+ */
11
+ export function roomIdForCwd(cwd) {
12
+ let target;
13
+ try {
14
+ target = realpathSync(cwd);
15
+ }
16
+ catch {
17
+ // cwd doesn't exist (unlikely in production) — fallback to raw path.
18
+ target = cwd;
19
+ }
20
+ return createHash("sha256").update(target).digest("base64url").slice(0, 12);
21
+ }
22
+ //# sourceMappingURL=rooms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rooms.js","sourceRoot":"","sources":["../src/rooms.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,IAAI,MAAc,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,qEAAqE;QACrE,MAAM,GAAG,GAAG,CAAC;IACf,CAAC;IACD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC9E,CAAC"}
@@ -0,0 +1,55 @@
1
+ import type { AgentSession } from "@mariozechner/pi-coding-agent";
2
+ import type { ClientMessage, ServerMessage } from "../protocol/types.js";
3
+ /**
4
+ * Abstraction over the Noise-encrypted transport.
5
+ * Wave 3 plugs the real channel; tests use a mock.
6
+ */
7
+ export interface PeerChannel {
8
+ send(msg: ServerMessage): void;
9
+ }
10
+ /**
11
+ * Minimal subset of BeforeToolCallContext consumed by the bridge.
12
+ * Shape-compatible with @mariozechner/pi-agent-core BeforeToolCallContext.
13
+ */
14
+ export interface BridgeToolCallContext {
15
+ toolCall: {
16
+ toolCallId: string;
17
+ toolCall: {
18
+ name: string;
19
+ arguments: Record<string, unknown>;
20
+ };
21
+ };
22
+ args: unknown;
23
+ }
24
+ /**
25
+ * Bridges an AgentSession (Pi SDK) and a PeerChannel (remote app).
26
+ *
27
+ * Wire-up at session creation:
28
+ * const bridge = new AgentBridge(session, channel)
29
+ * // pass bridge.beforeToolCallHook to createAgentSession as beforeToolCall
30
+ *
31
+ * Then route incoming client messages:
32
+ * channel.on('message', msg => bridge.onClientMessage(msg))
33
+ */
34
+ export declare class AgentBridge {
35
+ private readonly session;
36
+ private readonly channel;
37
+ /** ID of the active user_message turn, used as in_reply_to for streaming. */
38
+ private currentTurnId;
39
+ /** Pending tool approvals keyed by tool_call_id. */
40
+ private readonly pendingApprovals;
41
+ private readonly unsubscribe;
42
+ constructor(session: AgentSession, channel: PeerChannel);
43
+ /**
44
+ * Pass this as `beforeToolCall` in createAgentSession options.
45
+ * Auto-approves whitelisted tools; pauses everything else for user approval.
46
+ */
47
+ readonly beforeToolCallHook: (ctx: BridgeToolCallContext, signal?: AbortSignal) => Promise<{
48
+ block?: boolean;
49
+ reason?: string;
50
+ } | undefined>;
51
+ onClientMessage(msg: ClientMessage): void;
52
+ dispose(): void;
53
+ private awaitApproval;
54
+ private onSessionEvent;
55
+ }
@@ -0,0 +1,146 @@
1
+ import { decide } from "./tool_gate.js";
2
+ const APPROVAL_TIMEOUT_MS = 60_000;
3
+ // ── AgentBridge ───────────────────────────────────────────────────────────────
4
+ /**
5
+ * Bridges an AgentSession (Pi SDK) and a PeerChannel (remote app).
6
+ *
7
+ * Wire-up at session creation:
8
+ * const bridge = new AgentBridge(session, channel)
9
+ * // pass bridge.beforeToolCallHook to createAgentSession as beforeToolCall
10
+ *
11
+ * Then route incoming client messages:
12
+ * channel.on('message', msg => bridge.onClientMessage(msg))
13
+ */
14
+ export class AgentBridge {
15
+ session;
16
+ channel;
17
+ /** ID of the active user_message turn, used as in_reply_to for streaming. */
18
+ currentTurnId = null;
19
+ /** Pending tool approvals keyed by tool_call_id. */
20
+ pendingApprovals = new Map();
21
+ unsubscribe;
22
+ constructor(session, channel) {
23
+ this.session = session;
24
+ this.channel = channel;
25
+ this.unsubscribe = session.subscribe(this.onSessionEvent.bind(this));
26
+ }
27
+ // ── beforeToolCall hook ────────────────────────────────────────────────────
28
+ /**
29
+ * Pass this as `beforeToolCall` in createAgentSession options.
30
+ * Auto-approves whitelisted tools; pauses everything else for user approval.
31
+ */
32
+ beforeToolCallHook = async (ctx, signal) => {
33
+ const toolName = ctx.toolCall.toolCall.name;
34
+ const toolCallId = ctx.toolCall.toolCallId;
35
+ if (decide(toolName) === "auto") {
36
+ return undefined; // proceed immediately
37
+ }
38
+ // Needs user approval: emit tool_request and block until response
39
+ this.channel.send({
40
+ type: "tool_request",
41
+ tool_call_id: toolCallId,
42
+ tool: toolName,
43
+ args: ctx.args,
44
+ });
45
+ const decision = await this.awaitApproval(toolCallId, signal);
46
+ if (decision === "deny") {
47
+ // Cancel the whole turn — user denied execution
48
+ void this.session.abort();
49
+ return { block: true, reason: "denied by user" };
50
+ }
51
+ return undefined; // allow
52
+ };
53
+ // ── Incoming client messages ───────────────────────────────────────────────
54
+ onClientMessage(msg) {
55
+ switch (msg.type) {
56
+ case "user_message":
57
+ this.currentTurnId = msg.id;
58
+ void this.session.prompt(msg.text);
59
+ break;
60
+ case "approve_tool": {
61
+ const resolve = this.pendingApprovals.get(msg.tool_call_id);
62
+ if (resolve)
63
+ resolve(msg.decision);
64
+ break;
65
+ }
66
+ case "cancel":
67
+ void this.session.abort();
68
+ this.channel.send({
69
+ type: "cancelled",
70
+ in_reply_to: msg.id,
71
+ target_id: msg.target_id,
72
+ });
73
+ break;
74
+ case "ping":
75
+ this.channel.send({ type: "pong", in_reply_to: msg.id });
76
+ break;
77
+ }
78
+ }
79
+ // ── Cleanup ────────────────────────────────────────────────────────────────
80
+ dispose() {
81
+ this.unsubscribe();
82
+ for (const resolve of this.pendingApprovals.values()) {
83
+ resolve("deny");
84
+ }
85
+ this.pendingApprovals.clear();
86
+ }
87
+ // ── Private ────────────────────────────────────────────────────────────────
88
+ awaitApproval(toolCallId, signal) {
89
+ return new Promise((resolve) => {
90
+ let settled = false;
91
+ const settle = (d) => {
92
+ if (settled)
93
+ return;
94
+ settled = true;
95
+ clearTimeout(timer);
96
+ this.pendingApprovals.delete(toolCallId);
97
+ resolve(d);
98
+ };
99
+ // 60 s timeout → auto-deny + inform remote peer
100
+ const timer = setTimeout(() => {
101
+ this.channel.send({
102
+ type: "error",
103
+ code: "timeout",
104
+ message: `tool approval timeout: ${toolCallId}`,
105
+ });
106
+ settle("deny");
107
+ }, APPROVAL_TIMEOUT_MS);
108
+ this.pendingApprovals.set(toolCallId, settle);
109
+ signal?.addEventListener("abort", () => settle("deny"), { once: true });
110
+ });
111
+ }
112
+ onSessionEvent(event) {
113
+ // ── text streaming ─────────────────────────────────────────────────────
114
+ if (event.type === "message_update" && this.currentTurnId !== null) {
115
+ const ae = event.assistantMessageEvent;
116
+ if (ae.type === "text_delta") {
117
+ this.channel.send({
118
+ type: "agent_chunk",
119
+ in_reply_to: this.currentTurnId,
120
+ delta: ae.delta,
121
+ });
122
+ }
123
+ }
124
+ // ── tool result ────────────────────────────────────────────────────────
125
+ if (event.type === "tool_execution_end") {
126
+ const toolResult = event.isError
127
+ ? {
128
+ type: "tool_result",
129
+ tool_call_id: event.toolCallId,
130
+ error: String(event.result),
131
+ }
132
+ : {
133
+ type: "tool_result",
134
+ tool_call_id: event.toolCallId,
135
+ result: event.result,
136
+ };
137
+ this.channel.send(toolResult);
138
+ }
139
+ // ── turn complete ──────────────────────────────────────────────────────
140
+ if (event.type === "agent_end" && this.currentTurnId !== null) {
141
+ this.channel.send({ type: "agent_done", in_reply_to: this.currentTurnId });
142
+ this.currentTurnId = null;
143
+ }
144
+ }
145
+ }
146
+ //# sourceMappingURL=agent_bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent_bridge.js","sourceRoot":"","sources":["../../src/session/agent_bridge.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AA2BxC,MAAM,mBAAmB,GAAG,MAAM,CAAC;AAEnC,iFAAiF;AAEjF;;;;;;;;;GASG;AACH,MAAM,OAAO,WAAW;IACL,OAAO,CAAe;IACtB,OAAO,CAAc;IACtC,6EAA6E;IACrE,aAAa,GAAkB,IAAI,CAAC;IAC5C,oDAAoD;IACnC,gBAAgB,GAAG,IAAI,GAAG,EAGxC,CAAC;IACa,WAAW,CAAa;IAEzC,YAAY,OAAqB,EAAE,OAAoB;QACrD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACvE,CAAC;IAED,8EAA8E;IAE9E;;;OAGG;IACM,kBAAkB,GAAG,KAAK,EACjC,GAA0B,EAC1B,MAAoB,EACuC,EAAE;QAC7D,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC5C,MAAM,UAAU,GAAG,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC;QAE3C,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,MAAM,EAAE,CAAC;YAChC,OAAO,SAAS,CAAC,CAAC,sBAAsB;QAC1C,CAAC;QAED,kEAAkE;QAClE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;YAChB,IAAI,EAAE,cAAc;YACpB,YAAY,EAAE,UAAU;YACxB,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,GAAG,CAAC,IAA+B;SAC1C,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QAE9D,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;YACxB,gDAAgD;YAChD,KAAK,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YAC1B,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QACnD,CAAC;QAED,OAAO,SAAS,CAAC,CAAC,QAAQ;IAC5B,CAAC,CAAC;IAEF,8EAA8E;IAE9E,eAAe,CAAC,GAAkB;QAChC,QAAQ,GAAG,CAAC,IAAI,EAAE,CAAC;YACjB,KAAK,cAAc;gBACjB,IAAI,CAAC,aAAa,GAAG,GAAG,CAAC,EAAE,CAAC;gBAC5B,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;gBACnC,MAAM;YAER,KAAK,cAAc,CAAC,CAAC,CAAC;gBACpB,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBAC5D,IAAI,OAAO;oBAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;gBACnC,MAAM;YACR,CAAC;YAED,KAAK,QAAQ;gBACX,KAAK,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;gBAC1B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;oBAChB,IAAI,EAAE,WAAW;oBACjB,WAAW,EAAE,GAAG,CAAC,EAAE;oBACnB,SAAS,EAAE,GAAG,CAAC,SAAS;iBACzB,CAAC,CAAC;gBACH,MAAM;YAER,KAAK,MAAM;gBACT,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;gBACzD,MAAM;QACV,CAAC;IACH,CAAC;IAED,8EAA8E;IAE9E,OAAO;QACL,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,gBAAgB,CAAC,MAAM,EAAE,EAAE,CAAC;YACrD,OAAO,CAAC,MAAM,CAAC,CAAC;QAClB,CAAC;QACD,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;IAChC,CAAC;IAED,8EAA8E;IAEtE,aAAa,CACnB,UAAkB,EAClB,MAAoB;QAEpB,OAAO,IAAI,OAAO,CAAmB,CAAC,OAAO,EAAE,EAAE;YAC/C,IAAI,OAAO,GAAG,KAAK,CAAC;YAEpB,MAAM,MAAM,GAAG,CAAC,CAAmB,EAAE,EAAE;gBACrC,IAAI,OAAO;oBAAE,OAAO;gBACpB,OAAO,GAAG,IAAI,CAAC;gBACf,YAAY,CAAC,KAAK,CAAC,CAAC;gBACpB,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;gBACzC,OAAO,CAAC,CAAC,CAAC,CAAC;YACb,CAAC,CAAC;YAEF,gDAAgD;YAChD,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;oBAChB,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,SAAS;oBACf,OAAO,EAAE,0BAA0B,UAAU,EAAE;iBAChD,CAAC,CAAC;gBACH,MAAM,CAAC,MAAM,CAAC,CAAC;YACjB,CAAC,EAAE,mBAAmB,CAAC,CAAC;YAExB,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;YAE9C,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,cAAc,CAAC,KAAwB;QAC7C,0EAA0E;QAC1E,IAAI,KAAK,CAAC,IAAI,KAAK,gBAAgB,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;YACnE,MAAM,EAAE,GAAG,KAAK,CAAC,qBAAqB,CAAC;YACvC,IAAI,EAAE,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC7B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;oBAChB,IAAI,EAAE,aAAa;oBACnB,WAAW,EAAE,IAAI,CAAC,aAAa;oBAC/B,KAAK,EAAE,EAAE,CAAC,KAAK;iBAChB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,0EAA0E;QAC1E,IAAI,KAAK,CAAC,IAAI,KAAK,oBAAoB,EAAE,CAAC;YACxC,MAAM,UAAU,GAAkB,KAAK,CAAC,OAAO;gBAC7C,CAAC,CAAC;oBACE,IAAI,EAAE,aAAa;oBACnB,YAAY,EAAE,KAAK,CAAC,UAAU;oBAC9B,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;iBAC5B;gBACH,CAAC,CAAC;oBACE,IAAI,EAAE,aAAa;oBACnB,YAAY,EAAE,KAAK,CAAC,UAAU;oBAC9B,MAAM,EAAE,KAAK,CAAC,MAAiB;iBAChC,CAAC;YACN,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAChC,CAAC;QAED,0EAA0E;QAC1E,IAAI,KAAK,CAAC,IAAI,KAAK,WAAW,IAAI,IAAI,CAAC,aAAa,KAAK,IAAI,EAAE,CAAC;YAC9D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;YAC3E,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC5B,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,37 @@
1
+ import type { Server } from "node:net";
2
+ import { type Envelope } from "./envelope.js";
3
+ /**
4
+ * Broker hosted by the session leader. Accepts UDS connections, maintains a
5
+ * `name → connection` map, routes envelopes per the `to` field, and appends
6
+ * each routed message to an `audit.jsonl` log.
7
+ *
8
+ * Auto-suffix on name collision: when a peer registers a name already taken,
9
+ * the broker assigns `<name>#N` and returns it in the register ack.
10
+ */
11
+ export interface BrokerOptions {
12
+ server: Server;
13
+ auditPath?: string;
14
+ /** Optional callback invoked after each successful route (testing/observability). */
15
+ onRouted?: (env: Envelope, deliveredTo: string[]) => void;
16
+ }
17
+ export declare class Broker {
18
+ private readonly peers;
19
+ private readonly auditPath?;
20
+ private readonly onRouted?;
21
+ private readonly server;
22
+ constructor(opts: BrokerOptions);
23
+ /** Peers currently registered. Snapshot, safe to read. */
24
+ peerNames(): string[];
25
+ close(): Promise<void>;
26
+ private _handleConnection;
27
+ private _onData;
28
+ private _handleLine;
29
+ private _handleRegister;
30
+ private _uniqueName;
31
+ private _onClose;
32
+ private _route;
33
+ private _resolveTargets;
34
+ private _handleBrokerMessage;
35
+ private _broadcastSystem;
36
+ private _appendAudit;
37
+ }