@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,63 @@
1
+ export interface DaemonEntry {
2
+ /** Absolute realpath of the cwd this daemon manages. */
3
+ cwd: string;
4
+ /**
5
+ * Display name (mesh `agent_name`). Persisted here because the supervisor
6
+ * now injects the daemon's config via `UNBIEN_DIRECT_CONFIG` at spawn
7
+ * instead of reading a per-cwd `.pi/un-bien/config.json`. Legacy entries
8
+ * (cwd only) fall back to `defaultAgentName(cwd)`.
9
+ */
10
+ name?: string;
11
+ }
12
+ export interface DaemonRegistry {
13
+ daemons: DaemonEntry[];
14
+ }
15
+ /**
16
+ * Normalizes a user-provided path: expands `~`/`~/...`, resolves relative
17
+ * components against `process.cwd()`, and runs `realpath` to canonicalize
18
+ * symlinks. Throws if the resulting path doesn't exist on disk.
19
+ *
20
+ * `/unbien create` always stores normalized paths so two registrations
21
+ * of the same logical folder via different aliases produce a single entry.
22
+ */
23
+ export declare function normalizeCwd(input: string): string;
24
+ /** Reads the registry, returning an empty one when the file is absent. */
25
+ export declare function loadRegistry(): DaemonRegistry;
26
+ export declare function saveRegistry(reg: DaemonRegistry): void;
27
+ /**
28
+ * Adds a daemon entry. Refuses duplicates (same normalized cwd already
29
+ * present). Returns the derived id + normalized cwd so the caller can
30
+ * report it back to the user.
31
+ */
32
+ export declare function addDaemon(rawCwd: string, name?: string): {
33
+ id: string;
34
+ cwd: string;
35
+ name: string;
36
+ };
37
+ /**
38
+ * Removes the daemon entry whose derived id matches `id`. Returns the
39
+ * removed cwd (if any). Does NOT touch the cwd's local config — `create`
40
+ * the same cwd later restores the registration idempotently.
41
+ */
42
+ export declare function removeDaemon(id: string): {
43
+ removed: boolean;
44
+ cwd?: string;
45
+ };
46
+ /** Snapshot of all registered daemons with derived ids. Order matches the
47
+ * file's insertion order — first-registered first. */
48
+ export declare function listDaemons(): Array<{
49
+ id: string;
50
+ cwd: string;
51
+ name: string;
52
+ }>;
53
+ /**
54
+ * One-shot migration: backfill `name` (folder-derived) into legacy entries
55
+ * that predate the name field, persisting the change to `daemons.json`.
56
+ * Idempotent — writes only when something was missing. Returns how many
57
+ * entries were backfilled. The supervisor runs this on start.
58
+ */
59
+ export declare function migrateRegistryNames(): number;
60
+ /** Test/diag-only: returns the on-disk path. Exported so tests can poke
61
+ * at it (e.g. tmpdir override is done via env, but for now the path is
62
+ * hardcoded to ~/.pi/un-bien/daemons.json). */
63
+ export declare function registryPath(): string;
@@ -0,0 +1,153 @@
1
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
4
+ import { daemonIdForCwd } from "./id.js";
5
+ import { defaultAgentName } from "../session/local_config.js";
6
+ import { unbienStateHome } from "../paths.js";
7
+ /**
8
+ * The global daemon registry: which working directories are promoted to
9
+ * always-on daemons under the supervisor.
10
+ *
11
+ * Schema rationale (decision Q in plan/26): the registry stores **only**
12
+ * `cwd` per entry. Everything else (agent_name, auto_start_relay, etc.)
13
+ * is the cwd's local config at `<cwd>/.pi/un-bien/config.json` — single
14
+ * source of truth, no duplication. The daemon `id` is *derived* from cwd
15
+ * via `daemonIdForCwd`, never persisted.
16
+ *
17
+ * Cwds are always normalized to an **absolute realpath** before storage.
18
+ * A user typing `/unbien create ~/Movies` or `/unbien create .`
19
+ * results in the same entry as `/unbien create /Users/x/Movies` — no
20
+ * surprise duplicates, symlinks collapse correctly.
21
+ */
22
+ /** Resolved at call time so tests can override via `UNBIEN_HOME`. The
23
+ * prod path is always `~/.pi/un-bien/daemons.json`. */
24
+ function registryPathInternal() {
25
+ return join(unbienStateHome(), "daemons.json");
26
+ }
27
+ /**
28
+ * Normalizes a user-provided path: expands `~`/`~/...`, resolves relative
29
+ * components against `process.cwd()`, and runs `realpath` to canonicalize
30
+ * symlinks. Throws if the resulting path doesn't exist on disk.
31
+ *
32
+ * `/unbien create` always stores normalized paths so two registrations
33
+ * of the same logical folder via different aliases produce a single entry.
34
+ */
35
+ export function normalizeCwd(input) {
36
+ if (!input || !input.trim()) {
37
+ throw new Error("cwd is required");
38
+ }
39
+ let p = input.trim();
40
+ // Expand `~` / `~/relative`. Shell wouldn't expand inside slash command args.
41
+ if (p === "~")
42
+ p = homedir();
43
+ else if (p.startsWith("~/"))
44
+ p = join(homedir(), p.slice(2));
45
+ if (!isAbsolute(p))
46
+ p = resolvePath(process.cwd(), p);
47
+ // realpath canonicalizes symlinks + throws if path doesn't exist.
48
+ return realpathSync(p);
49
+ }
50
+ /** Reads the registry, returning an empty one when the file is absent. */
51
+ export function loadRegistry() {
52
+ if (!existsSync(registryPathInternal()))
53
+ return { daemons: [] };
54
+ try {
55
+ const raw = readFileSync(registryPathInternal(), "utf8");
56
+ const parsed = JSON.parse(raw);
57
+ if (!parsed || typeof parsed !== "object")
58
+ return { daemons: [] };
59
+ const arr = parsed.daemons;
60
+ if (!Array.isArray(arr))
61
+ return { daemons: [] };
62
+ const daemons = [];
63
+ for (const item of arr) {
64
+ if (!item || typeof item !== "object")
65
+ continue;
66
+ const cwd = item.cwd;
67
+ if (typeof cwd === "string" && cwd.length > 0) {
68
+ const rawName = item.name;
69
+ const entry = { cwd };
70
+ if (typeof rawName === "string" && rawName.length > 0)
71
+ entry.name = rawName;
72
+ daemons.push(entry);
73
+ }
74
+ }
75
+ return { daemons };
76
+ }
77
+ catch {
78
+ return { daemons: [] };
79
+ }
80
+ }
81
+ export function saveRegistry(reg) {
82
+ mkdirSync(dirname(registryPathInternal()), { recursive: true });
83
+ writeFileSync(registryPathInternal(), JSON.stringify(reg, null, 2) + "\n");
84
+ }
85
+ /**
86
+ * Adds a daemon entry. Refuses duplicates (same normalized cwd already
87
+ * present). Returns the derived id + normalized cwd so the caller can
88
+ * report it back to the user.
89
+ */
90
+ export function addDaemon(rawCwd, name) {
91
+ const cwd = normalizeCwd(rawCwd);
92
+ const reg = loadRegistry();
93
+ if (reg.daemons.some((d) => d.cwd === cwd)) {
94
+ throw new Error(`Daemon already registered for cwd: ${cwd}`);
95
+ }
96
+ // Always persist a name (the registry is the source of truth now that the
97
+ // supervisor injects config via env instead of a local config file).
98
+ const resolvedName = name?.trim() || defaultAgentName(cwd);
99
+ reg.daemons.push({ cwd, name: resolvedName });
100
+ saveRegistry(reg);
101
+ return { id: daemonIdForCwd(cwd), cwd, name: resolvedName };
102
+ }
103
+ /**
104
+ * Removes the daemon entry whose derived id matches `id`. Returns the
105
+ * removed cwd (if any). Does NOT touch the cwd's local config — `create`
106
+ * the same cwd later restores the registration idempotently.
107
+ */
108
+ export function removeDaemon(id) {
109
+ const reg = loadRegistry();
110
+ const idx = reg.daemons.findIndex((d) => daemonIdForCwd(d.cwd) === id);
111
+ if (idx === -1)
112
+ return { removed: false };
113
+ const [removed] = reg.daemons.splice(idx, 1);
114
+ saveRegistry(reg);
115
+ return { removed: true, cwd: removed.cwd };
116
+ }
117
+ /** Snapshot of all registered daemons with derived ids. Order matches the
118
+ * file's insertion order — first-registered first. */
119
+ export function listDaemons() {
120
+ return loadRegistry().daemons.map((d) => ({
121
+ id: daemonIdForCwd(d.cwd),
122
+ cwd: d.cwd,
123
+ // Legacy entries (cwd only) fall back to the folder-derived name so a
124
+ // daemon is never nameless. New entries already carry an explicit name.
125
+ name: d.name ?? defaultAgentName(d.cwd),
126
+ }));
127
+ }
128
+ /**
129
+ * One-shot migration: backfill `name` (folder-derived) into legacy entries
130
+ * that predate the name field, persisting the change to `daemons.json`.
131
+ * Idempotent — writes only when something was missing. Returns how many
132
+ * entries were backfilled. The supervisor runs this on start.
133
+ */
134
+ export function migrateRegistryNames() {
135
+ const reg = loadRegistry();
136
+ let changed = 0;
137
+ for (const d of reg.daemons) {
138
+ if (!d.name) {
139
+ d.name = defaultAgentName(d.cwd);
140
+ changed++;
141
+ }
142
+ }
143
+ if (changed > 0)
144
+ saveRegistry(reg);
145
+ return changed;
146
+ }
147
+ /** Test/diag-only: returns the on-disk path. Exported so tests can poke
148
+ * at it (e.g. tmpdir override is done via env, but for now the path is
149
+ * hardcoded to ~/.pi/un-bien/daemons.json). */
150
+ export function registryPath() {
151
+ return registryPathInternal();
152
+ }
153
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/daemon/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,aAAa,GACd,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,WAAW,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;;;;;;;;;GAcG;AAEH;wDACwD;AACxD,SAAS,oBAAoB;IAC3B,OAAO,IAAI,CAAC,eAAe,EAAE,EAAE,cAAc,CAAC,CAAC;AACjD,CAAC;AAkBD;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACrC,CAAC;IACD,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IACrB,8EAA8E;IAC9E,IAAI,CAAC,KAAK,GAAG;QAAE,CAAC,GAAG,OAAO,EAAE,CAAC;SACxB,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7D,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IACtD,kEAAkE;IAClE,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC;AACzB,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,YAAY;IAC1B,IAAI,CAAC,UAAU,CAAC,oBAAoB,EAAE,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAChE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,oBAAoB,EAAE,EAAE,MAAM,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAClE,MAAM,GAAG,GAAI,MAAgC,CAAC,OAAO,CAAC;QACtD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAChD,MAAM,OAAO,GAAkB,EAAE,CAAC;QAClC,KAAK,MAAM,IAAI,IAAI,GAAG,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,SAAS;YAChD,MAAM,GAAG,GAAI,IAA0B,CAAC,GAAG,CAAC;YAC5C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC9C,MAAM,OAAO,GAAI,IAA2B,CAAC,IAAI,CAAC;gBAClD,MAAM,KAAK,GAAgB,EAAE,GAAG,EAAE,CAAC;gBACnC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;oBACnD,KAAK,CAAC,IAAI,GAAG,OAAO,CAAC;gBACvB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACtB,CAAC;QACH,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACzB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAmB;IAC9C,SAAS,CAAC,OAAO,CAAC,oBAAoB,EAAE,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChE,aAAa,CAAC,oBAAoB,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AAC7E,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CACvB,MAAc,EACd,IAAa;IAEb,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAC3B,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,sCAAsC,GAAG,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,0EAA0E;IAC1E,qEAAqE;IACrE,MAAM,YAAY,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAC3D,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;IAC9C,YAAY,CAAC,GAAG,CAAC,CAAC;IAClB,OAAO,EAAE,EAAE,EAAE,cAAc,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;AAC9D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,EAAU;IACrC,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAC3B,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;IACvE,IAAI,GAAG,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC1C,MAAM,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAC7C,YAAY,CAAC,GAAG,CAAC,CAAC;IAClB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,OAAQ,CAAC,GAAG,EAAE,CAAC;AAC9C,CAAC;AAED;uDACuD;AACvD,MAAM,UAAU,WAAW;IAKzB,OAAO,YAAY,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACxC,EAAE,EAAE,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC;QACzB,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,sEAAsE;QACtE,wEAAwE;QACxE,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC;KACxC,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,GAAG,GAAG,YAAY,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACZ,CAAC,CAAC,IAAI,GAAG,gBAAgB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACjC,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IACD,IAAI,OAAO,GAAG,CAAC;QAAE,YAAY,CAAC,GAAG,CAAC,CAAC;IACnC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;gDAEgD;AAChD,MAAM,UAAU,YAAY;IAC1B,OAAO,oBAAoB,EAAE,CAAC;AAChC,CAAC"}
@@ -0,0 +1,194 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type { DaemonState } from "./control_protocol.js";
3
+ import { type LocalConfig } from "../session/local_config.js";
4
+ /**
5
+ * Wrapper around a `pi --mode rpc -e <extension>` child process for the
6
+ * supervisor.
7
+ *
8
+ * Lifecycle:
9
+ * - `spawn()` boots the child with the daemon's cwd + `UNBIEN_DAEMON=1`
10
+ * env so the extension knows to skip the interactive wizard.
11
+ * - `sendPrompt(text)` writes a Pi RPC `prompt` command to stdin.
12
+ * - The child's stdout (Pi RPC events) is currently consumed line-by-line
13
+ * and ignored — wave 2 only needs fire-and-forget. Later waves can
14
+ * surface the events back through the supervisor's status op.
15
+ * - Exit/crash fires `exit` event with `{code, signal, isCrash}` so the
16
+ * supervisor can decide whether to auto-restart.
17
+ *
18
+ * Each `RpcChild` instance maps 1:1 to a registry entry (single cwd).
19
+ * The supervisor owns the map of these and addresses them by `id`.
20
+ */
21
+ export interface RpcChildOptions {
22
+ /** Path to the `pi` binary. Defaults to "pi" (must be on PATH). */
23
+ piBin?: string;
24
+ /** Absolute path to the un-bien `dist/index.js` to load as -e. */
25
+ extensionPath: string;
26
+ /** Working directory for the spawned process. Determines which local
27
+ * config the extension reads. */
28
+ cwd: string;
29
+ /** Additional env vars merged on top of `process.env` + the mandatory
30
+ * `UNBIEN_DAEMON=1`. */
31
+ env?: NodeJS.ProcessEnv;
32
+ /**
33
+ * Daemon config injected into the child via `UNBIEN_DIRECT_CONFIG`
34
+ * (JSON inline) instead of a per-cwd `.pi/un-bien/config.json` file. The
35
+ * supervisor builds this from the registry. When set, the child reads it
36
+ * env-first (see `loadLocalConfig`) and no config file is needed. Also the
37
+ * source of the `--name` for the session. Falls back to the on-disk config
38
+ * when omitted.
39
+ */
40
+ config?: LocalConfig;
41
+ }
42
+ export interface RpcChildExitEvent {
43
+ code: number | null;
44
+ signal: NodeJS.Signals | null;
45
+ /** True when exit was not clean (non-zero or signal). */
46
+ isCrash: boolean;
47
+ }
48
+ export declare const EXIT_DAEMON_FRESH_SESSION = 42;
49
+ /**
50
+ * Resolve the `pi` executable for `spawn` (plan/40, decision C). On Windows a
51
+ * bare `pi` is actually `pi.cmd`/`pi.ps1`, and `spawn` won't find it without an
52
+ * extension → resolve the real path via `where` (rather than `shell:true`,
53
+ * which risks shell injection). An explicit path or an already-suffixed name is
54
+ * used as-is. POSIX returns the name unchanged. Best-effort: if `where` fails,
55
+ * fall back to the bare name (spawn will surface ENOENT honestly).
56
+ *
57
+ * `where` lists every match in PATH order, which on pi-node installs puts the
58
+ * extensionless `pi` (a shell script) BEFORE `pi.cmd`. Naively taking the first
59
+ * line spawns the unrunnable script → ENOENT → "crashed". So we prefer a result
60
+ * with a real Windows executable extension and only fall back to the first line.
61
+ */
62
+ export declare function resolvePiBin(piBin: string, plat?: NodeJS.Platform): string;
63
+ /** What to pass to `spawn`: the executable and any args to prepend before the
64
+ * caller's args. On POSIX (and for `.exe`) `prefixArgs` is empty. */
65
+ export interface PiSpawnTarget {
66
+ command: string;
67
+ prefixArgs: string[];
68
+ }
69
+ /**
70
+ * Resolve `pi` to a directly-spawnable target (plan/40). On Windows `pi` is an
71
+ * npm `.cmd` shim, which modern Node refuses to `spawn` without `shell:true`
72
+ * (EINVAL, post CVE-2024-27980). Wrapping it in `cmd.exe` would (a) add a layer
73
+ * that orphans the real `node` on kill and (b) complicate stdio — both fatal for
74
+ * a long-lived RPC daemon the supervisor must stop/restart cleanly. Instead we
75
+ * parse the shim to recover the underlying `node <cli.js>` and spawn that
76
+ * directly: one process, clean signals, clean stdio.
77
+ *
78
+ * Falls back to the bare resolved path when not a parseable shim (e.g. a real
79
+ * `pi.exe`, or POSIX) so `spawn` surfaces any real error honestly.
80
+ */
81
+ export declare function resolvePiSpawn(piBin: string, plat?: NodeJS.Platform, nodeExe?: string): PiSpawnTarget;
82
+ /**
83
+ * Extract the JS entry an npm `.cmd` shim launches. npm shims invoke
84
+ * `"%_prog%" "%dp0%\<relative>\entry.js" %*`; we recover `<dir>\<relative>\entry.js`
85
+ * (dir = the shim's own folder, i.e. `%dp0%`). Returns null when the shim
86
+ * doesn't match the expected shape or the target is missing.
87
+ */
88
+ export declare function _npmShimTarget(cmdPath: string): string | null;
89
+ /**
90
+ * Maps an RPC stdout line to a busy-state transition: `message_start` → true
91
+ * (a message is streaming), `message_end` → false. Other lines → null (no
92
+ * change). Pure + exported for tests.
93
+ *
94
+ * NOTE (plan/39 detail 1): the Pi RPC stream has NO turn-level event — only
95
+ * per-message start/end (and `response{command:"prompt"}` is emitted at
96
+ * PREFLIGHT, i.e. turn START, not end). So this passive flag reflects
97
+ * "a message is being produced right now"; the authoritative turn-busy signal
98
+ * is `get_state.isStreaming` (see `RpcChild.refreshBusy`).
99
+ */
100
+ export declare function busyTransition(line: string): boolean | null;
101
+ /**
102
+ * CLI args for the daemon's `pi --mode rpc` child.
103
+ *
104
+ * `--continue` is the key bit: it resumes the **most recent** session for the
105
+ * cwd (`SessionManager.continueRecent`, non-interactive — unlike `--resume`
106
+ * which opens a picker). Without it every supervisor restart spun up a brand
107
+ * new session file, piling up thousands of JSONLs per folder. With it, a
108
+ * restart REUSES the one session; the app's `/new` (session_new) still rolls
109
+ * it over to a fresh one, which the next restart then continues. First boot
110
+ * (no prior session) just creates the first one.
111
+ *
112
+ * `--name <sessionName>`, when given, pins the session's display name to the
113
+ * daemon's identity (its `agent_name`) so every restart shows up under the
114
+ * same stable name in the picker/app instead of an auto-generated one. The
115
+ * daemon's name is set at registration (`unbien create <cwd> --name "…"`).
116
+ * Omitted when no name resolves, so the arg list stays minimal.
117
+ *
118
+ * `--approve` is mandatory for a daemon (pi ≥0.79 project trust): RPC mode is
119
+ * non-interactive, so without an override Pi resolves an untrusted project
120
+ * folder (any folder with `.pi/` or CLAUDE.md/AGENTS.md) to NOT trusted and
121
+ * silently skips its `.pi/settings.json` (model/provider/keys), instructions,
122
+ * resources and project extensions — the daemon then comes up with no model
123
+ * and fails on the first turn. The operator already authorized this folder by
124
+ * registering/launching a daemon in it, so `--approve` (trust-for-this-run) is
125
+ * the correct non-interactive stance. (Does NOT affect the separate "extension
126
+ * loaded twice" conflict, which comes from the extension being BOTH installed
127
+ * in ~/.pi/agent/extensions or cwd/.pi/extensions AND passed via `-e`.)
128
+ */
129
+ export declare function rpcSpawnArgs(extensionPath: string, sessionName?: string, useContinue?: boolean): string[];
130
+ export declare class RpcChild extends EventEmitter {
131
+ private readonly opts;
132
+ private child;
133
+ private _state;
134
+ private _startedAt;
135
+ private _restartCount;
136
+ /** True while a deliberate `stop()` is in flight. The process dies by
137
+ * signal (SIGTERM/SIGKILL), which would otherwise look like a crash and
138
+ * trip the supervisor's auto-restart — re-spawning a daemon the operator
139
+ * just stopped/removed. Gating `isCrash` on this makes a deliberate stop a
140
+ * clean exit (no restart). Reset on every `spawn()`. */
141
+ private _stopping;
142
+ /** Next spawn should create a fresh session instead of --continue. */
143
+ private forceFreshSessionOnNextSpawn;
144
+ /** Accumulates partial stdout lines while waiting for `\n`. */
145
+ private stdoutBuf;
146
+ /** Passive busy flag derived from the RPC stream (message_start/end). Hint
147
+ * only; `refreshBusy` syncs it authoritatively via get_state.isStreaming. */
148
+ private _busy;
149
+ /** In-flight `get_state` requests, keyed by request id. */
150
+ private readonly _statePending;
151
+ constructor(opts: RpcChildOptions);
152
+ get state(): DaemonState;
153
+ /** Passive busy hint from the stream. Prefer `refreshBusy()` for an
154
+ * authoritative check before acting on it (cron skip_if_busy). */
155
+ get isBusy(): boolean;
156
+ get pid(): number | undefined;
157
+ get restartCount(): number;
158
+ get uptimeMs(): number | undefined;
159
+ /**
160
+ * Spawn the child process. Idempotent for the same instance: a second
161
+ * call while already running is a no-op.
162
+ */
163
+ spawn(): void;
164
+ /**
165
+ * Sends a Pi RPC `prompt` command to the child's stdin. Fire-and-forget
166
+ * — we don't wait for the response (response would be the success ack;
167
+ * the actual agent output streams via the relay/UDS, not back through
168
+ * stdout).
169
+ *
170
+ * Returns false if the child isn't running (caller decides how to report).
171
+ */
172
+ sendPrompt(text: string, requestId?: string): boolean;
173
+ /**
174
+ * Asks the child to exit gracefully. Sends SIGTERM; if the child doesn't
175
+ * exit within `timeoutMs`, escalates to SIGKILL. Resolves when the
176
+ * `exit` event fires.
177
+ */
178
+ stop(timeoutMs?: number): Promise<void>;
179
+ private _onStdout;
180
+ private _handleStdoutLine;
181
+ /**
182
+ * Authoritative busy check: queries the child's `get_state` and syncs
183
+ * `_busy` to `isStreaming`. Resolves the passive flag on timeout (no
184
+ * response) or when the child isn't running. Used by the cron `skip_if_busy`
185
+ * gate, where a false "not busy" would pile a prompt onto a live turn.
186
+ */
187
+ refreshBusy(timeoutMs?: number): Promise<boolean>;
188
+ /** Test-only: feed a raw stdout line through the same handler. */
189
+ _ingestStdoutForTest(line: string): void;
190
+ private _onExit;
191
+ /** Bumps the restart counter — called by the supervisor when it
192
+ * decides to re-spawn after a crash. Exposed so tests can drive it. */
193
+ noteRestart(): void;
194
+ }