@geohar/un-bien 0.7.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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +726 -0
  3. package/dist/actions/handlers.d.ts +77 -0
  4. package/dist/actions/handlers.js +26 -0
  5. package/dist/actions/handlers.js.map +1 -0
  6. package/dist/actions/registry.d.ts +39 -0
  7. package/dist/actions/registry.js +62 -0
  8. package/dist/actions/registry.js.map +1 -0
  9. package/dist/bin/supervisord.d.ts +2 -0
  10. package/dist/bin/supervisord.js +86 -0
  11. package/dist/bin/supervisord.js.map +1 -0
  12. package/dist/config.d.ts +84 -0
  13. package/dist/config.js +115 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/daemon/client.d.ts +20 -0
  16. package/dist/daemon/client.js +131 -0
  17. package/dist/daemon/client.js.map +1 -0
  18. package/dist/daemon/control_protocol.d.ts +176 -0
  19. package/dist/daemon/control_protocol.js +63 -0
  20. package/dist/daemon/control_protocol.js.map +1 -0
  21. package/dist/daemon/cron_log.d.ts +45 -0
  22. package/dist/daemon/cron_log.js +71 -0
  23. package/dist/daemon/cron_log.js.map +1 -0
  24. package/dist/daemon/cron_registry.d.ts +80 -0
  25. package/dist/daemon/cron_registry.js +198 -0
  26. package/dist/daemon/cron_registry.js.map +1 -0
  27. package/dist/daemon/id.d.ts +24 -0
  28. package/dist/daemon/id.js +36 -0
  29. package/dist/daemon/id.js.map +1 -0
  30. package/dist/daemon/install.d.ts +170 -0
  31. package/dist/daemon/install.js +603 -0
  32. package/dist/daemon/install.js.map +1 -0
  33. package/dist/daemon/registry.d.ts +63 -0
  34. package/dist/daemon/registry.js +153 -0
  35. package/dist/daemon/registry.js.map +1 -0
  36. package/dist/daemon/rpc_child.d.ts +194 -0
  37. package/dist/daemon/rpc_child.js +419 -0
  38. package/dist/daemon/rpc_child.js.map +1 -0
  39. package/dist/daemon/supervisor.d.ts +93 -0
  40. package/dist/daemon/supervisor.js +666 -0
  41. package/dist/daemon/supervisor.js.map +1 -0
  42. package/dist/extension_ui_bridge.d.ts +26 -0
  43. package/dist/extension_ui_bridge.js +341 -0
  44. package/dist/extension_ui_bridge.js.map +1 -0
  45. package/dist/index.d.ts +197 -0
  46. package/dist/index.js +5750 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/mcp/mesh_result.d.ts +10 -0
  49. package/dist/mcp/mesh_result.js +42 -0
  50. package/dist/mcp/mesh_result.js.map +1 -0
  51. package/dist/mcp/mesh_server.d.ts +16 -0
  52. package/dist/mcp/mesh_server.js +373 -0
  53. package/dist/mcp/mesh_server.js.map +1 -0
  54. package/dist/mesh/canonical.d.ts +30 -0
  55. package/dist/mesh/canonical.js +63 -0
  56. package/dist/mesh/canonical.js.map +1 -0
  57. package/dist/mesh/client.d.ts +18 -0
  58. package/dist/mesh/client.js +127 -0
  59. package/dist/mesh/client.js.map +1 -0
  60. package/dist/mesh/encoding.d.ts +38 -0
  61. package/dist/mesh/encoding.js +217 -0
  62. package/dist/mesh/encoding.js.map +1 -0
  63. package/dist/mesh/self_revoke.d.ts +73 -0
  64. package/dist/mesh/self_revoke.js +407 -0
  65. package/dist/mesh/self_revoke.js.map +1 -0
  66. package/dist/mesh/siblings.d.ts +49 -0
  67. package/dist/mesh/siblings.js +159 -0
  68. package/dist/mesh/siblings.js.map +1 -0
  69. package/dist/mesh/types.d.ts +34 -0
  70. package/dist/mesh/types.js +11 -0
  71. package/dist/mesh/types.js.map +1 -0
  72. package/dist/mesh/verify.d.ts +17 -0
  73. package/dist/mesh/verify.js +86 -0
  74. package/dist/mesh/verify.js.map +1 -0
  75. package/dist/pairing/crypto.d.ts +8 -0
  76. package/dist/pairing/crypto.js +22 -0
  77. package/dist/pairing/crypto.js.map +1 -0
  78. package/dist/pairing/qr.d.ts +59 -0
  79. package/dist/pairing/qr.js +127 -0
  80. package/dist/pairing/qr.js.map +1 -0
  81. package/dist/pairing/storage.d.ts +120 -0
  82. package/dist/pairing/storage.js +614 -0
  83. package/dist/pairing/storage.js.map +1 -0
  84. package/dist/panel_bridge.d.ts +15 -0
  85. package/dist/panel_bridge.js +409 -0
  86. package/dist/panel_bridge.js.map +1 -0
  87. package/dist/paths.d.ts +39 -0
  88. package/dist/paths.js +51 -0
  89. package/dist/paths.js.map +1 -0
  90. package/dist/protocol/codec.d.ts +7 -0
  91. package/dist/protocol/codec.js +48 -0
  92. package/dist/protocol/codec.js.map +1 -0
  93. package/dist/protocol/types.d.ts +404 -0
  94. package/dist/protocol/types.js +2 -0
  95. package/dist/protocol/types.js.map +1 -0
  96. package/dist/rooms.d.ts +42 -0
  97. package/dist/rooms.js +78 -0
  98. package/dist/rooms.js.map +1 -0
  99. package/dist/session/bridge.d.ts +37 -0
  100. package/dist/session/bridge.js +134 -0
  101. package/dist/session/bridge.js.map +1 -0
  102. package/dist/session/broker.d.ts +189 -0
  103. package/dist/session/broker.js +527 -0
  104. package/dist/session/broker.js.map +1 -0
  105. package/dist/session/broker_remote.d.ts +74 -0
  106. package/dist/session/broker_remote.js +581 -0
  107. package/dist/session/broker_remote.js.map +1 -0
  108. package/dist/session/cwd_lock.d.ts +33 -0
  109. package/dist/session/cwd_lock.js +127 -0
  110. package/dist/session/cwd_lock.js.map +1 -0
  111. package/dist/session/debug_log.d.ts +3 -0
  112. package/dist/session/debug_log.js +32 -0
  113. package/dist/session/debug_log.js.map +1 -0
  114. package/dist/session/envelope.d.ts +33 -0
  115. package/dist/session/envelope.js +117 -0
  116. package/dist/session/envelope.js.map +1 -0
  117. package/dist/session/global_config.d.ts +33 -0
  118. package/dist/session/global_config.js +73 -0
  119. package/dist/session/global_config.js.map +1 -0
  120. package/dist/session/ipc.d.ts +27 -0
  121. package/dist/session/ipc.js +22 -0
  122. package/dist/session/ipc.js.map +1 -0
  123. package/dist/session/leader_election.d.ts +32 -0
  124. package/dist/session/leader_election.js +108 -0
  125. package/dist/session/leader_election.js.map +1 -0
  126. package/dist/session/local_config.d.ts +67 -0
  127. package/dist/session/local_config.js +193 -0
  128. package/dist/session/local_config.js.map +1 -0
  129. package/dist/session/mesh_node.d.ts +116 -0
  130. package/dist/session/mesh_node.js +596 -0
  131. package/dist/session/mesh_node.js.map +1 -0
  132. package/dist/session/peer.d.ts +139 -0
  133. package/dist/session/peer.js +474 -0
  134. package/dist/session/peer.js.map +1 -0
  135. package/dist/session/peer_inventory.d.ts +13 -0
  136. package/dist/session/peer_inventory.js +48 -0
  137. package/dist/session/peer_inventory.js.map +1 -0
  138. package/dist/session/peer_limits.d.ts +9 -0
  139. package/dist/session/peer_limits.js +24 -0
  140. package/dist/session/peer_limits.js.map +1 -0
  141. package/dist/session/rpc_envelope.d.ts +108 -0
  142. package/dist/session/rpc_envelope.js +221 -0
  143. package/dist/session/rpc_envelope.js.map +1 -0
  144. package/dist/session/rpc_inbound.d.ts +55 -0
  145. package/dist/session/rpc_inbound.js +136 -0
  146. package/dist/session/rpc_inbound.js.map +1 -0
  147. package/dist/session/setup_wizard.d.ts +39 -0
  148. package/dist/session/setup_wizard.js +63 -0
  149. package/dist/session/setup_wizard.js.map +1 -0
  150. package/dist/session/tools.d.ts +24 -0
  151. package/dist/session/tools.js +272 -0
  152. package/dist/session/tools.js.map +1 -0
  153. package/dist/session/wizard.d.ts +13 -0
  154. package/dist/session/wizard.js +20 -0
  155. package/dist/session/wizard.js.map +1 -0
  156. package/dist/transport/peer_channel.d.ts +52 -0
  157. package/dist/transport/peer_channel.js +145 -0
  158. package/dist/transport/peer_channel.js.map +1 -0
  159. package/dist/transport/pi_forward_client.d.ts +29 -0
  160. package/dist/transport/pi_forward_client.js +68 -0
  161. package/dist/transport/pi_forward_client.js.map +1 -0
  162. package/dist/transport/relay_client.d.ts +96 -0
  163. package/dist/transport/relay_client.js +210 -0
  164. package/dist/transport/relay_client.js.map +1 -0
  165. package/dist/ui/footer.d.ts +32 -0
  166. package/dist/ui/footer.js +34 -0
  167. package/dist/ui/footer.js.map +1 -0
  168. package/docs/daemon.md +289 -0
  169. package/package.json +92 -0
  170. package/service-templates/launchd.plist.template +35 -0
  171. package/service-templates/systemd.service.template +19 -0
  172. package/service-templates/task-launcher.vbs.template +10 -0
  173. package/service-templates/task-scheduler.xml.template +38 -0
  174. package/skills/agent-network/SKILL.md +310 -0
@@ -0,0 +1,39 @@
1
+ import type { LocalConfig } from "./local_config.js";
2
+ /**
3
+ * Pi SDK UI surface needed by the wizard. Subset of `ExtensionUIContext` —
4
+ * declared inline so tests can mock cleanly without dragging the full
5
+ * ExtensionContext shape.
6
+ */
7
+ export interface WizardUI {
8
+ /** Free-text prompt. Returns the entered string, or undefined if cancelled. */
9
+ input?: (title: string, options?: {
10
+ defaultValue?: string;
11
+ }) => Promise<string | undefined>;
12
+ /** Picker. Returns the picked option, or undefined if cancelled. */
13
+ select: (title: string, options: string[]) => Promise<string | undefined>;
14
+ /** Non-blocking notification. Used for inline validation feedback. */
15
+ notify?: (msg: string, kind: "info" | "warning" | "error") => void;
16
+ }
17
+ export interface WizardDefaults {
18
+ agent_name: string;
19
+ use_relay: boolean;
20
+ }
21
+ /**
22
+ * Runs the 2-question setup wizard. Returns the chosen config on confirm, or
23
+ * null when the user cancels any prompt.
24
+ *
25
+ * Prompts:
26
+ * 1. Agent name (default: parent/folder of cwd)
27
+ * 2. Use the relay on this terminal? (yes/no) — gates connection to the
28
+ * remote mesh (mobile devices + other PCs over the relay). "No" means
29
+ * local-only: this Pi joins the UDS mesh but doesn't open WSS.
30
+ * Final: review + confirm "Save and activate?" yes/no
31
+ *
32
+ * Daemon mode (run agents 24/7 via systemd/launchd) is intentionally NOT in
33
+ * the wizard — it's an explicit, separate opt-in via `/unbien install`.
34
+ *
35
+ * The local UDS mesh is always single per machine ("local" session) — no
36
+ * session question. All Pis on the same machine see each other through
37
+ * the same broker.
38
+ */
39
+ export declare function runSetupWizard(ui: WizardUI, defaults: WizardDefaults): Promise<LocalConfig | null>;
@@ -0,0 +1,63 @@
1
+ const YES = "Yes";
2
+ const NO = "No";
3
+ const CANCEL_TOKEN = "__cancel__";
4
+ /**
5
+ * Runs the 2-question setup wizard. Returns the chosen config on confirm, or
6
+ * null when the user cancels any prompt.
7
+ *
8
+ * Prompts:
9
+ * 1. Agent name (default: parent/folder of cwd)
10
+ * 2. Use the relay on this terminal? (yes/no) — gates connection to the
11
+ * remote mesh (mobile devices + other PCs over the relay). "No" means
12
+ * local-only: this Pi joins the UDS mesh but doesn't open WSS.
13
+ * Final: review + confirm "Save and activate?" yes/no
14
+ *
15
+ * Daemon mode (run agents 24/7 via systemd/launchd) is intentionally NOT in
16
+ * the wizard — it's an explicit, separate opt-in via `/unbien install`.
17
+ *
18
+ * The local UDS mesh is always single per machine ("local" session) — no
19
+ * session question. All Pis on the same machine see each other through
20
+ * the same broker.
21
+ */
22
+ export async function runSetupWizard(ui, defaults) {
23
+ const agent_name = await _askText(ui, "Agent name:", defaults.agent_name);
24
+ if (agent_name === null)
25
+ return null;
26
+ ui.notify?.("The relay forwards encrypted messages to the Remote Pi mobile app and other PCs in your mesh. Skip this if you only want a local-only mesh on this machine.", "info");
27
+ const useRelayChoice = await ui.select("Use the relay on this terminal to connect to the remote mesh (mobile + PCs)?", defaults.use_relay ? [YES, NO] : [NO, YES]);
28
+ if (!useRelayChoice)
29
+ return null;
30
+ const auto_start_relay = useRelayChoice === YES;
31
+ // Review + confirm
32
+ const summary = [
33
+ ` Agent name: ${agent_name}`,
34
+ ` Use relay: ${auto_start_relay ? YES : NO}`,
35
+ ].join("\n");
36
+ ui.notify?.(`Summary:\n${summary}`, "info");
37
+ const confirm = await ui.select("Save and activate?", [YES, NO]);
38
+ if (confirm !== YES)
39
+ return null;
40
+ return { agent_name, auto_start_relay };
41
+ }
42
+ /**
43
+ * Asks the user for free text. The Pi SDK's `ui.input` does not pre-fill the
44
+ * field with `defaultValue` (the SDK ignores that option), so we surface the
45
+ * default in the prompt label and treat an empty submission as "accept the
46
+ * default" — the standard CLI convention. Falls back to `select` when the
47
+ * SDK doesn't expose `input` at all.
48
+ */
49
+ async function _askText(ui, title, defaultValue) {
50
+ const titleWithHint = `${title} (default: ${defaultValue})`;
51
+ const raw = ui.input
52
+ ? await ui.input(titleWithHint, { defaultValue })
53
+ : await ui.select(titleWithHint, [defaultValue, CANCEL_TOKEN]);
54
+ if (raw === undefined)
55
+ return null;
56
+ if (raw === CANCEL_TOKEN)
57
+ return null;
58
+ const trimmed = raw.trim();
59
+ // Empty submission = accept the default. No re-prompt, no warning — the
60
+ // user explicitly asked for the default by hitting enter.
61
+ return trimmed.length > 0 ? trimmed : defaultValue;
62
+ }
63
+ //# sourceMappingURL=setup_wizard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup_wizard.js","sourceRoot":"","sources":["../../src/session/setup_wizard.ts"],"names":[],"mappings":"AAwBA,MAAM,GAAG,GAAG,KAAK,CAAC;AAClB,MAAM,EAAE,GAAG,IAAI,CAAC;AAChB,MAAM,YAAY,GAAG,YAAY,CAAC;AAElC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,EAAY,EACZ,QAAwB;IAExB,MAAM,UAAU,GAAG,MAAM,QAAQ,CAAC,EAAE,EAAE,aAAa,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC1E,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAErC,EAAE,CAAC,MAAM,EAAE,CACT,6JAA6J,EAC7J,MAAM,CACP,CAAC;IACF,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,MAAM,CACpC,8EAA8E,EAC9E,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAC3C,CAAC;IACF,IAAI,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,gBAAgB,GAAG,cAAc,KAAK,GAAG,CAAC;IAEhD,mBAAmB;IACnB,MAAM,OAAO,GAAG;QACd,oBAAoB,UAAU,EAAE;QAChC,oBAAoB,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;KAClD,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,EAAE,CAAC,MAAM,EAAE,CAAC,aAAa,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC;IAE5C,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,oBAAoB,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IACjE,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAEjC,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,QAAQ,CACrB,EAAY,EACZ,KAAa,EACb,YAAoB;IAEpB,MAAM,aAAa,GAAG,GAAG,KAAK,cAAc,YAAY,GAAG,CAAC;IAC5D,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK;QAClB,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,aAAa,EAAE,EAAE,YAAY,EAAE,CAAC;QACjD,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC;IACjE,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,GAAG,KAAK,YAAY;QAAE,OAAO,IAAI,CAAC;IACtC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,wEAAwE;IACxE,0DAA0D;IAC1D,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;AACrD,CAAC"}
@@ -0,0 +1,24 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { SessionPeer } from "./peer.js";
3
+ /**
4
+ * Registers the native tools the Pi LLM uses to talk to other agents in the
5
+ * same UDS session (plano 19 transport + plan/25 Wave 0 ACK protocol):
6
+ *
7
+ * - `agent_send` — unified delivery with broker-level ACK. Returns
8
+ * a status so the LLM can decide whether to retry.
9
+ * For unicast targets the broker auto-replies with
10
+ * `received | busy | denied`. For broadcast/multicast
11
+ * the tool is fire-and-forget (status `sent`).
12
+ * - `agent_request` — **deprecated**. Still works (send + block on reply
13
+ * via `re` correlation) but the LLM should migrate
14
+ * to the event-driven send+inbox pattern. Each call
15
+ * emits a one-shot warning to stderr.
16
+ *
17
+ * Reply pattern (new world): when you receive a message you want to answer,
18
+ * send back another envelope with `re=<original-id>`. The original sender
19
+ * sees that reply in its inbox during a future turn.
20
+ *
21
+ * `getSessionPeer` is a getter (not a captured value) so changes to the
22
+ * underlying `_sessionPeer` module variable are observed live.
23
+ */
24
+ export declare function registerAgentTools(pi: ExtensionAPI, getSessionPeer: () => SessionPeer | null): void;
@@ -0,0 +1,272 @@
1
+ import { Type } from "typebox";
2
+ const NOT_IN_SESSION = "Not in a session. Run /unbien join first";
3
+ const ACK_TIMEOUT_MS = 5_000;
4
+ const LEGACY_REQUEST_TIMEOUT_MS = 30_000;
5
+ const LIST_PEERS_TIMEOUT_MS = 2_000;
6
+ /**
7
+ * Registers the native tools the Pi LLM uses to talk to other agents in the
8
+ * same UDS session (plano 19 transport + plan/25 Wave 0 ACK protocol):
9
+ *
10
+ * - `agent_send` — unified delivery with broker-level ACK. Returns
11
+ * a status so the LLM can decide whether to retry.
12
+ * For unicast targets the broker auto-replies with
13
+ * `received | busy | denied`. For broadcast/multicast
14
+ * the tool is fire-and-forget (status `sent`).
15
+ * - `agent_request` — **deprecated**. Still works (send + block on reply
16
+ * via `re` correlation) but the LLM should migrate
17
+ * to the event-driven send+inbox pattern. Each call
18
+ * emits a one-shot warning to stderr.
19
+ *
20
+ * Reply pattern (new world): when you receive a message you want to answer,
21
+ * send back another envelope with `re=<original-id>`. The original sender
22
+ * sees that reply in its inbox during a future turn.
23
+ *
24
+ * `getSessionPeer` is a getter (not a captured value) so changes to the
25
+ * underlying `_sessionPeer` module variable are observed live.
26
+ */
27
+ export function registerAgentTools(pi, getSessionPeer) {
28
+ const SendParams = Type.Object({
29
+ to: Type.String({
30
+ description: "Recipient agent name (e.g. 'backend'), 'broadcast', or array of names. " +
31
+ "Broadcast/multicast are fire-and-forget; unicast returns an ACK status.",
32
+ }),
33
+ body: Type.Unknown({
34
+ description: "Free-form JSON payload. String or object — your choice.",
35
+ }),
36
+ re: Type.Optional(Type.String({
37
+ description: "Set this to the `id` of an incoming message when you are REPLYING to it. " +
38
+ "The peer correlates your answer with their original send via this field.",
39
+ })),
40
+ });
41
+ const RequestParams = Type.Object({
42
+ to: Type.String({
43
+ description: "Recipient agent name. Must be a single peer (not broadcast).",
44
+ }),
45
+ body: Type.Unknown({ description: "Free-form JSON payload to send." }),
46
+ timeout_ms: Type.Optional(Type.Number({
47
+ description: "Optional override of the default 30s reply timeout. Per-request.",
48
+ })),
49
+ });
50
+ pi.registerTool({
51
+ name: "agent_send",
52
+ label: "Agent Send",
53
+ description: "Send a message to another Pi agent in the current local session and " +
54
+ "wait for the broker's delivery ACK. Delivery is reliable: a peer that " +
55
+ "is mid-turn still receives the message (its harness enqueues it for " +
56
+ "the next turn), so you never need to retry. Returns one of: `received` " +
57
+ "(delivered; peer will process it in an upcoming turn), `denied` (peer " +
58
+ "refused), `timeout` (no ACK in 5s — peer offline / transport error), " +
59
+ "`sent` (broadcast/multicast — no ACK semantics). Use `re` to mark this " +
60
+ "message as a reply to an incoming envelope's `id` (correlation only).",
61
+ promptSnippet: "agent_send({to, body, re?}): unicast → returns {status: received|denied|timeout} (delivery is reliable; no retry-on-busy). Broadcast/multicast → fire-and-forget ({status:'sent'}).",
62
+ parameters: SendParams,
63
+ execute: async (_toolCallId, params) => {
64
+ const peer = getSessionPeer();
65
+ if (!peer) {
66
+ const details = {
67
+ status: "refused",
68
+ ok: false,
69
+ error: NOT_IN_SESSION,
70
+ };
71
+ return {
72
+ content: [{ type: "text", text: NOT_IN_SESSION }],
73
+ details,
74
+ };
75
+ }
76
+ const { to, body, re } = params;
77
+ if (to === peer.address() || to === peer.name()) {
78
+ const msg = `Refused: cannot agent_send to yourself ("${to}"). Just do the work directly.`;
79
+ const details = {
80
+ status: "refused",
81
+ ok: false,
82
+ error: msg,
83
+ };
84
+ return {
85
+ content: [{ type: "text", text: msg }],
86
+ details,
87
+ };
88
+ }
89
+ const isUnicast = to !== "broadcast";
90
+ // Broadcast: fire-and-forget. Broker doesn't ACK multi-target sends.
91
+ if (!isUnicast) {
92
+ try {
93
+ await peer.send(to, body, re ?? null);
94
+ const details = { status: "sent", ok: true };
95
+ return {
96
+ content: [{ type: "text", text: `Broadcast sent.` }],
97
+ details,
98
+ };
99
+ }
100
+ catch (err) {
101
+ const msg = err instanceof Error ? err.message : String(err);
102
+ const details = {
103
+ status: "timeout",
104
+ ok: false,
105
+ error: msg,
106
+ };
107
+ return {
108
+ content: [{ type: "text", text: `Broadcast failed: ${msg}` }],
109
+ details,
110
+ };
111
+ }
112
+ }
113
+ // Unicast: wait for broker ACK.
114
+ try {
115
+ const ack = await peer.sendWithAck(to, body, re ?? null, ACK_TIMEOUT_MS);
116
+ const ok = ack.status === "received";
117
+ const details = {
118
+ status: ack.status,
119
+ ok,
120
+ ...(ack.target === undefined ? {} : { target: ack.target }),
121
+ ...(ack.error === undefined ? {} : { error: ack.error }),
122
+ ...(ack.reason === undefined ? {} : { reason: ack.reason }),
123
+ };
124
+ const text = _formatAck(to, ack, re);
125
+ return { content: [{ type: "text", text }], details };
126
+ }
127
+ catch (err) {
128
+ const msg = err instanceof Error ? err.message : String(err);
129
+ const details = {
130
+ status: "timeout",
131
+ ok: false,
132
+ error: msg,
133
+ };
134
+ return {
135
+ content: [{ type: "text", text: `Failed to send: ${msg}` }],
136
+ details,
137
+ };
138
+ }
139
+ },
140
+ });
141
+ const ListPeersParams = Type.Object({});
142
+ pi.registerTool({
143
+ name: "list_peers",
144
+ label: "List Peers",
145
+ description: "Returns the current peer inventory in this session as ADDRESSES of the " +
146
+ "form `<cwd>@<name>` (cross-PC peers prefixed `<pc>:`). An address is an " +
147
+ "opaque routing key — pass it to `agent_send`/`agent_request` VERBATIM, " +
148
+ "never build one by hand. Use BEFORE sending whenever you're unsure who's " +
149
+ "available, or after a `peer_joined` / `peer_left` notification to refresh " +
150
+ "your mental model. Resolves in milliseconds — a metadata query to the " +
151
+ "broker, not a turn of another agent.",
152
+ promptSnippet: "list_peers(): returns {peers: string[]} of addresses `<cwd>@<name>` (`<pc>:` prefix cross-PC). Echo an address verbatim to agent_send; never compose one. Cheap; call freely.",
153
+ parameters: ListPeersParams,
154
+ execute: async (_toolCallId) => {
155
+ const peer = getSessionPeer();
156
+ if (!peer) {
157
+ return {
158
+ content: [{ type: "text", text: NOT_IN_SESSION }],
159
+ details: { peers: [] },
160
+ };
161
+ }
162
+ try {
163
+ // Internal use of the request/reply primitive is fine — broker
164
+ // replies are synthesised in-process (`_handleBrokerMessage`)
165
+ // without going through `_route`, so they bypass the ACK path.
166
+ const reply = await peer.request("broker", { type: "list_peers" }, LIST_PEERS_TIMEOUT_MS);
167
+ const body = reply.body;
168
+ const peers = Array.isArray(body?.peers)
169
+ ? body.peers.filter((p) => typeof p === "string")
170
+ : [];
171
+ // Drop self from the list — the caller is the only one who can't
172
+ // address itself anyway, so listing it is noise. Peers are ADDRESSES
173
+ // (plan/38), so filter by address.
174
+ const selfAddress = peer.address();
175
+ const filtered = peers.filter((p) => p !== selfAddress);
176
+ const text = filtered.length === 0 ? "(no peers)" : filtered.join("\n");
177
+ return {
178
+ content: [{ type: "text", text }],
179
+ details: { peers: filtered },
180
+ };
181
+ }
182
+ catch (err) {
183
+ const msg = err instanceof Error ? err.message : String(err);
184
+ return {
185
+ content: [{ type: "text", text: `list_peers failed: ${msg}` }],
186
+ details: { peers: [] },
187
+ };
188
+ }
189
+ },
190
+ });
191
+ pi.registerTool({
192
+ name: "agent_request",
193
+ label: "Agent Request (deprecated)",
194
+ description: "DEPRECATED — prefer `agent_send` + observing your inbox for the " +
195
+ "reply (correlated by `re=<your-send-id>`). This tool still works: " +
196
+ "it sends a message and synchronously blocks until the peer replies " +
197
+ "or the timeout fires. Default 30s. Will be removed in a future " +
198
+ "release; migrate to the event-driven pattern in the agent-network skill.",
199
+ promptSnippet: "agent_request({to, body, timeout_ms?}): DEPRECATED synchronous request/reply (blocks current turn). Prefer agent_send + inbox observation.",
200
+ parameters: RequestParams,
201
+ execute: async (_toolCallId, params) => {
202
+ const peer = getSessionPeer();
203
+ if (!peer) {
204
+ return {
205
+ content: [{ type: "text", text: NOT_IN_SESSION }],
206
+ details: { error: NOT_IN_SESSION },
207
+ };
208
+ }
209
+ const { to, body, timeout_ms } = params;
210
+ if (to === peer.address() || to === peer.name()) {
211
+ const msg = `Refused: cannot agent_request to yourself ("${to}"). Just do the work directly.`;
212
+ return {
213
+ content: [{ type: "text", text: msg }],
214
+ details: { error: msg },
215
+ };
216
+ }
217
+ const timeout = typeof timeout_ms === "number" && timeout_ms > 0
218
+ ? timeout_ms
219
+ : LEGACY_REQUEST_TIMEOUT_MS;
220
+ try {
221
+ const reply = await peer.request(to, body, timeout);
222
+ const text = typeof reply.body === "string"
223
+ ? reply.body
224
+ : JSON.stringify(reply.body);
225
+ return {
226
+ content: [{ type: "text", text }],
227
+ details: reply.body,
228
+ };
229
+ }
230
+ catch (err) {
231
+ const msg = err instanceof Error ? err.message : String(err);
232
+ return {
233
+ content: [{ type: "text", text: `Request failed: ${msg}` }],
234
+ details: { error: msg },
235
+ };
236
+ }
237
+ },
238
+ });
239
+ }
240
+ function _formatAck(to, ack, re) {
241
+ const reSuffix = re ? ` (re=${re})` : "";
242
+ const errorSuffix = ack.error ? ` (${ack.error})` : "";
243
+ switch (ack.status) {
244
+ case "received":
245
+ return `Delivered to "${to}"${reSuffix} — peer will process it in an upcoming turn.`;
246
+ case "busy":
247
+ // plan/34 removed busy-drop: the current broker never returns `busy`. If
248
+ // we still see it, an OUT-OF-DATE broker leader dropped the message — so
249
+ // be honest that it was NOT delivered, and point at the fix.
250
+ return (`NOT delivered — "${to}"${reSuffix} came back BUSY, which only ` +
251
+ `happens when an out-of-date broker leader dropped the message. ` +
252
+ `Restart the agent leading the local broker (oldest Pi/unbien process) ` +
253
+ `to pick up the new build, then resend.`);
254
+ case "denied":
255
+ if (ack.reason === "not_authorized") {
256
+ return (`Relay did not authorize delivery to "${to}"${reSuffix}${errorSuffix}. ` +
257
+ `Do not blindly retry; verify authorization first.`);
258
+ }
259
+ if (ack.reason === "bad_envelope") {
260
+ return (`Relay rejected the envelope for "${to}"${reSuffix}${errorSuffix}. ` +
261
+ `Do not blindly retry; correct the envelope first.`);
262
+ }
263
+ return `"${to}" denied the message${reSuffix}. Do not retry; report to user.`;
264
+ case "timeout":
265
+ if (ack.reason === "offline") {
266
+ return (`Immediate Relay offline transport error for "${to}"${reSuffix}${errorSuffix}; ` +
267
+ `reported as timeout without waiting for the ACK deadline.`);
268
+ }
269
+ return `No ACK from "${to}" within ${ACK_TIMEOUT_MS}ms${reSuffix} — transport error. Investigate or retry later.`;
270
+ }
271
+ }
272
+ //# sourceMappingURL=tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../../src/session/tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Minimal wizard for `/unbien join` with no argument.
3
+ *
4
+ * The Pi SDK `ExtensionUIContext.select` accepts a title and string options
5
+ * and returns the chosen option. Wizard offers existing sessions + an "explicit
6
+ * create" sentinel. If user picks the sentinel, prompts for a fresh name via
7
+ * `select`-with-option-other (Pi's "Other" escape hatch). Returns the picked
8
+ * session name, or null if user cancelled.
9
+ */
10
+ export interface WizardUI {
11
+ select(title: string, options: string[]): Promise<string | undefined>;
12
+ }
13
+ export declare function joinWizard(ui: WizardUI, defaultName: string): Promise<string | null>;
@@ -0,0 +1,20 @@
1
+ import { listSessions, sessionHasSock } from "./global_config.js";
2
+ const CREATE_SENTINEL = "━━━ Create new session ━━━";
3
+ export async function joinWizard(ui, defaultName) {
4
+ const sessions = listSessions();
5
+ const liveSessions = sessions.filter(sessionHasSock);
6
+ const options = [...liveSessions, CREATE_SENTINEL];
7
+ const picked = await ui.select(liveSessions.length
8
+ ? "Choose a session to join, or create a new one"
9
+ : "No active sessions. Create one?", options);
10
+ if (!picked)
11
+ return null;
12
+ if (picked === CREATE_SENTINEL) {
13
+ // Caller is expected to follow up with a name prompt via ctx.ui.select
14
+ // (the Pi SDK input dialog). For non-interactive contexts we return the
15
+ // default name.
16
+ return defaultName;
17
+ }
18
+ return picked;
19
+ }
20
+ //# sourceMappingURL=wizard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wizard.js","sourceRoot":"","sources":["../../src/session/wizard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAelE,MAAM,eAAe,GAAG,4BAA4B,CAAC;AAErD,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,EAAY,EACZ,WAAmB;IAEnB,MAAM,QAAQ,GAAG,YAAY,EAAE,CAAC;IAChC,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;IACrD,MAAM,OAAO,GAAG,CAAC,GAAG,YAAY,EAAE,eAAe,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAC5B,YAAY,CAAC,MAAM;QACjB,CAAC,CAAC,+CAA+C;QACjD,CAAC,CAAC,iCAAiC,EACrC,OAAO,CACR,CAAC;IACF,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,IAAI,MAAM,KAAK,eAAe,EAAE,CAAC;QAC/B,uEAAuE;QACvE,wEAAwE;QACxE,gBAAgB;QAChB,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,52 @@
1
+ import type { ClientMessage, ServerMessage } from "../protocol/types.js";
2
+ import { type EnvelopeMessage } from "../session/rpc_envelope.js";
3
+ import type { RelayClient } from "./relay_client.js";
4
+ /** Sink for ServerMessage outbound to the remote app. */
5
+ export interface PeerChannel {
6
+ send(msg: ServerMessage): void;
7
+ }
8
+ /**
9
+ * Plaintext PeerChannel backed by a RelayClient WebSocket.
10
+ *
11
+ * Usage (after pair_request handshake completes):
12
+ * const channel = new PlainPeerChannel(relay, appPeerId, myRoomId, onMsg)
13
+ * channel.send(serverMessage) // base64-encodes JSON, routes via relay
14
+ * // incoming relay messages destined for appPeerId are auto-decoded
15
+ * // and delivered via onMessage callback
16
+ *
17
+ * `myRoomId` is the *local* Pi's room id — sent on every outbound envelope
18
+ * so the app can correlate which Pi sent it (multi-pi support, plano 17).
19
+ */
20
+ export declare class PlainPeerChannel implements PeerChannel {
21
+ private readonly relay;
22
+ private readonly remotePeerId;
23
+ private readonly onMessage;
24
+ /** Route an inbound mesh-envelope ({rpc|evt}, docs/rpc-envelope.md) to the
25
+ * new-protocol dispatcher. Absent → the channel ignores envelope frames. */
26
+ private readonly onRpc?;
27
+ private readonly _unsubscribe;
28
+ constructor(relay: RelayClient, remotePeerId: string,
29
+ /**
30
+ * This Pi's room id. Currently NOT injected in the outer envelope
31
+ * (defensive — relay/app not yet ready). Kept in the constructor for
32
+ * forward-compat so callers don't need to change again when we re-enable.
33
+ */
34
+ myRoomId: string | undefined, onMessage: (msg: ClientMessage) => void,
35
+ /** Called when this specific peer connection is considered lost. */
36
+ _onDisconnect?: () => void,
37
+ /** Route an inbound mesh-envelope ({rpc|evt}, docs/rpc-envelope.md) to the
38
+ * new-protocol dispatcher. Absent → the channel ignores envelope frames. */
39
+ onRpc?: ((env: EnvelopeMessage, sender: PlainPeerChannel) => void) | undefined);
40
+ send(msg: ServerMessage): void;
41
+ /**
42
+ * Send an rpc-envelope message (`{ rpc | evt }`, docs/rpc-envelope.md) to the
43
+ * app. Same base64-into-`ct` outer wire as `send` — the relay stays opaque —
44
+ * but the inner payload is an `EnvelopeMessage`, not a `ServerMessage`.
45
+ * Best-effort, mirroring `send`: a relay mid-reconnect must not throw an
46
+ * uncaught exception out of the SDK event callback that drives it.
47
+ */
48
+ sendEnvelope(env: EnvelopeMessage): void;
49
+ /** Detaches from relay (does not close the relay itself). */
50
+ detach(): void;
51
+ private _onLine;
52
+ }
@@ -0,0 +1,145 @@
1
+ import { UB_KIND, RPC_KIND, EVT_KIND, isEnvelopeFrame, } from "../session/rpc_envelope.js";
2
+ import { envLog } from "../session/debug_log.js";
3
+ /**
4
+ * Plaintext PeerChannel backed by a RelayClient WebSocket.
5
+ *
6
+ * Usage (after pair_request handshake completes):
7
+ * const channel = new PlainPeerChannel(relay, appPeerId, myRoomId, onMsg)
8
+ * channel.send(serverMessage) // base64-encodes JSON, routes via relay
9
+ * // incoming relay messages destined for appPeerId are auto-decoded
10
+ * // and delivered via onMessage callback
11
+ *
12
+ * `myRoomId` is the *local* Pi's room id — sent on every outbound envelope
13
+ * so the app can correlate which Pi sent it (multi-pi support, plano 17).
14
+ */
15
+ export class PlainPeerChannel {
16
+ relay;
17
+ remotePeerId;
18
+ onMessage;
19
+ onRpc;
20
+ _unsubscribe;
21
+ constructor(relay, remotePeerId,
22
+ /**
23
+ * This Pi's room id. Currently NOT injected in the outer envelope
24
+ * (defensive — relay/app not yet ready). Kept in the constructor for
25
+ * forward-compat so callers don't need to change again when we re-enable.
26
+ */
27
+ myRoomId, onMessage,
28
+ /** Called when this specific peer connection is considered lost. */
29
+ _onDisconnect,
30
+ /** Route an inbound mesh-envelope ({rpc|evt}, docs/rpc-envelope.md) to the
31
+ * new-protocol dispatcher. Absent → the channel ignores envelope frames. */
32
+ onRpc) {
33
+ this.relay = relay;
34
+ this.remotePeerId = remotePeerId;
35
+ this.onMessage = onMessage;
36
+ this.onRpc = onRpc;
37
+ const listener = (line) => this._onLine(line);
38
+ relay.on("message", listener);
39
+ this._unsubscribe = () => relay.off("message", listener);
40
+ void _onDisconnect;
41
+ void myRoomId; // intentionally unused — see send() comment
42
+ }
43
+ // ── PeerChannel interface ──────────────────────────────────────────────────
44
+ send(msg) {
45
+ const ct = Buffer.from(JSON.stringify(msg)).toString("base64");
46
+ // NOTE: `room` removed from the outer envelope until relay (W1.A) + app
47
+ // (W1.C) accept the field. Multi-Pi multiplexing already works via
48
+ // `room_id`/`room_meta` in the WS-level `hello` — outer routing stays by
49
+ // `peer` alone. Re-add the field once downstream is ready.
50
+ const outer = { peer: this.remotePeerId, ct };
51
+ // Best-effort delivery. The relay WS can be mid-reconnect (idle/NAT drop, or
52
+ // a session_new/session-replacement teardown) when we push a server→app frame
53
+ // — notably the action_ok/action_error ack a handler emits right after
54
+ // newSession. `relay.send` throws "relay: not connected" in that window; since
55
+ // this runs inside an async SDK event callback, letting it propagate becomes an
56
+ // uncaughtException that kills the whole pi process. The relay auto-reconnects
57
+ // and the app re-syncs via session_sync, so a dropped frame is recoverable — a
58
+ // crash is not. Mirrors RelayClient.sendControl's no-op-when-closed policy.
59
+ try {
60
+ this.relay.send(JSON.stringify(outer));
61
+ }
62
+ catch {
63
+ /* relay down — drop this frame; reconnect + session_sync will recover */
64
+ }
65
+ }
66
+ /**
67
+ * Send an rpc-envelope message (`{ rpc | evt }`, docs/rpc-envelope.md) to the
68
+ * app. Same base64-into-`ct` outer wire as `send` — the relay stays opaque —
69
+ * but the inner payload is an `EnvelopeMessage`, not a `ServerMessage`.
70
+ * Best-effort, mirroring `send`: a relay mid-reconnect must not throw an
71
+ * uncaught exception out of the SDK event callback that drives it.
72
+ */
73
+ sendEnvelope(env) {
74
+ // Stamp the wrapper kind + timestamp at the single outbound choke.
75
+ const wire = {
76
+ ...env,
77
+ // Namespace stamp: each plane stamps its REAL type — ub-plane -> "ub",
78
+ // evt -> "evt", rpc (the spine / default) -> "rpc". Legacy "env" is no
79
+ // longer stamped (still accepted on read one transition).
80
+ type: env.type ??
81
+ (env.ub !== undefined
82
+ ? UB_KIND
83
+ : env.evt !== undefined
84
+ ? EVT_KIND
85
+ : RPC_KIND),
86
+ ts: env.ts ?? Date.now(),
87
+ };
88
+ const ct = Buffer.from(JSON.stringify(wire)).toString("base64");
89
+ const outer = { peer: this.remotePeerId, ct };
90
+ try {
91
+ this.relay.send(JSON.stringify(outer));
92
+ }
93
+ catch {
94
+ /* relay down — drop this frame; reconnect + session_sync will recover */
95
+ }
96
+ }
97
+ /** Detaches from relay (does not close the relay itself). */
98
+ detach() {
99
+ this._unsubscribe();
100
+ }
101
+ // ── Incoming line from relay ────────────────────────────────────────────────
102
+ _onLine(line) {
103
+ let outer;
104
+ try {
105
+ outer = JSON.parse(line);
106
+ }
107
+ catch {
108
+ return; // malformed line
109
+ }
110
+ if (outer.peer !== this.remotePeerId)
111
+ return;
112
+ if (!outer.ct)
113
+ return;
114
+ let plaintext;
115
+ try {
116
+ plaintext = Buffer.from(outer.ct, "base64").toString("utf8");
117
+ }
118
+ catch {
119
+ return;
120
+ }
121
+ let msg;
122
+ try {
123
+ msg = JSON.parse(plaintext);
124
+ }
125
+ catch {
126
+ return;
127
+ }
128
+ if (!msg || typeof msg !== "object")
129
+ return;
130
+ const obj = msg;
131
+ envLog(`inbound<-${this.remotePeerId.slice(0, 8)}: type=${String(obj.type)} rpc=${obj.rpc !== undefined} evt=${obj.evt !== undefined}`);
132
+ // Mesh-envelope inbound (any plane, real-typed or legacy "env" or bare
133
+ // field-presence) routes to the envelope dispatcher, NOT the stock
134
+ // ClientMessage switch. Shared predicate with index.ts's reconnect guard so
135
+ // they can't drift; the dispatcher branches by plane (rpc vs ub) itself.
136
+ if (isEnvelopeFrame(obj)) {
137
+ this.onRpc?.(obj, this);
138
+ return;
139
+ }
140
+ if (typeof obj.type !== "string")
141
+ return;
142
+ this.onMessage(msg);
143
+ }
144
+ }
145
+ //# sourceMappingURL=peer_channel.js.map