@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.
- package/LICENSE +21 -0
- package/README.md +726 -0
- package/dist/actions/handlers.d.ts +77 -0
- package/dist/actions/handlers.js +26 -0
- package/dist/actions/handlers.js.map +1 -0
- package/dist/actions/registry.d.ts +39 -0
- package/dist/actions/registry.js +62 -0
- package/dist/actions/registry.js.map +1 -0
- package/dist/bin/supervisord.d.ts +2 -0
- package/dist/bin/supervisord.js +86 -0
- package/dist/bin/supervisord.js.map +1 -0
- package/dist/config.d.ts +84 -0
- package/dist/config.js +115 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon/client.d.ts +20 -0
- package/dist/daemon/client.js +131 -0
- package/dist/daemon/client.js.map +1 -0
- package/dist/daemon/control_protocol.d.ts +176 -0
- package/dist/daemon/control_protocol.js +63 -0
- package/dist/daemon/control_protocol.js.map +1 -0
- package/dist/daemon/cron_log.d.ts +45 -0
- package/dist/daemon/cron_log.js +71 -0
- package/dist/daemon/cron_log.js.map +1 -0
- package/dist/daemon/cron_registry.d.ts +80 -0
- package/dist/daemon/cron_registry.js +198 -0
- package/dist/daemon/cron_registry.js.map +1 -0
- package/dist/daemon/id.d.ts +24 -0
- package/dist/daemon/id.js +36 -0
- package/dist/daemon/id.js.map +1 -0
- package/dist/daemon/install.d.ts +170 -0
- package/dist/daemon/install.js +603 -0
- package/dist/daemon/install.js.map +1 -0
- package/dist/daemon/registry.d.ts +63 -0
- package/dist/daemon/registry.js +153 -0
- package/dist/daemon/registry.js.map +1 -0
- package/dist/daemon/rpc_child.d.ts +194 -0
- package/dist/daemon/rpc_child.js +419 -0
- package/dist/daemon/rpc_child.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +93 -0
- package/dist/daemon/supervisor.js +666 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/extension_ui_bridge.d.ts +26 -0
- package/dist/extension_ui_bridge.js +341 -0
- package/dist/extension_ui_bridge.js.map +1 -0
- package/dist/index.d.ts +197 -0
- package/dist/index.js +5750 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/mesh_result.d.ts +10 -0
- package/dist/mcp/mesh_result.js +42 -0
- package/dist/mcp/mesh_result.js.map +1 -0
- package/dist/mcp/mesh_server.d.ts +16 -0
- package/dist/mcp/mesh_server.js +373 -0
- package/dist/mcp/mesh_server.js.map +1 -0
- package/dist/mesh/canonical.d.ts +30 -0
- package/dist/mesh/canonical.js +63 -0
- package/dist/mesh/canonical.js.map +1 -0
- package/dist/mesh/client.d.ts +18 -0
- package/dist/mesh/client.js +127 -0
- package/dist/mesh/client.js.map +1 -0
- package/dist/mesh/encoding.d.ts +38 -0
- package/dist/mesh/encoding.js +217 -0
- package/dist/mesh/encoding.js.map +1 -0
- package/dist/mesh/self_revoke.d.ts +73 -0
- package/dist/mesh/self_revoke.js +407 -0
- package/dist/mesh/self_revoke.js.map +1 -0
- package/dist/mesh/siblings.d.ts +49 -0
- package/dist/mesh/siblings.js +159 -0
- package/dist/mesh/siblings.js.map +1 -0
- package/dist/mesh/types.d.ts +34 -0
- package/dist/mesh/types.js +11 -0
- package/dist/mesh/types.js.map +1 -0
- package/dist/mesh/verify.d.ts +17 -0
- package/dist/mesh/verify.js +86 -0
- package/dist/mesh/verify.js.map +1 -0
- package/dist/pairing/crypto.d.ts +8 -0
- package/dist/pairing/crypto.js +22 -0
- package/dist/pairing/crypto.js.map +1 -0
- package/dist/pairing/qr.d.ts +59 -0
- package/dist/pairing/qr.js +127 -0
- package/dist/pairing/qr.js.map +1 -0
- package/dist/pairing/storage.d.ts +120 -0
- package/dist/pairing/storage.js +614 -0
- package/dist/pairing/storage.js.map +1 -0
- package/dist/panel_bridge.d.ts +15 -0
- package/dist/panel_bridge.js +409 -0
- package/dist/panel_bridge.js.map +1 -0
- package/dist/paths.d.ts +39 -0
- package/dist/paths.js +51 -0
- package/dist/paths.js.map +1 -0
- package/dist/protocol/codec.d.ts +7 -0
- package/dist/protocol/codec.js +48 -0
- package/dist/protocol/codec.js.map +1 -0
- package/dist/protocol/types.d.ts +404 -0
- package/dist/protocol/types.js +2 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/rooms.d.ts +42 -0
- package/dist/rooms.js +78 -0
- package/dist/rooms.js.map +1 -0
- package/dist/session/bridge.d.ts +37 -0
- package/dist/session/bridge.js +134 -0
- package/dist/session/bridge.js.map +1 -0
- package/dist/session/broker.d.ts +189 -0
- package/dist/session/broker.js +527 -0
- package/dist/session/broker.js.map +1 -0
- package/dist/session/broker_remote.d.ts +74 -0
- package/dist/session/broker_remote.js +581 -0
- package/dist/session/broker_remote.js.map +1 -0
- package/dist/session/cwd_lock.d.ts +33 -0
- package/dist/session/cwd_lock.js +127 -0
- package/dist/session/cwd_lock.js.map +1 -0
- package/dist/session/debug_log.d.ts +3 -0
- package/dist/session/debug_log.js +32 -0
- package/dist/session/debug_log.js.map +1 -0
- package/dist/session/envelope.d.ts +33 -0
- package/dist/session/envelope.js +117 -0
- package/dist/session/envelope.js.map +1 -0
- package/dist/session/global_config.d.ts +33 -0
- package/dist/session/global_config.js +73 -0
- package/dist/session/global_config.js.map +1 -0
- package/dist/session/ipc.d.ts +27 -0
- package/dist/session/ipc.js +22 -0
- package/dist/session/ipc.js.map +1 -0
- package/dist/session/leader_election.d.ts +32 -0
- package/dist/session/leader_election.js +108 -0
- package/dist/session/leader_election.js.map +1 -0
- package/dist/session/local_config.d.ts +67 -0
- package/dist/session/local_config.js +193 -0
- package/dist/session/local_config.js.map +1 -0
- package/dist/session/mesh_node.d.ts +116 -0
- package/dist/session/mesh_node.js +596 -0
- package/dist/session/mesh_node.js.map +1 -0
- package/dist/session/peer.d.ts +139 -0
- package/dist/session/peer.js +474 -0
- package/dist/session/peer.js.map +1 -0
- package/dist/session/peer_inventory.d.ts +13 -0
- package/dist/session/peer_inventory.js +48 -0
- package/dist/session/peer_inventory.js.map +1 -0
- package/dist/session/peer_limits.d.ts +9 -0
- package/dist/session/peer_limits.js +24 -0
- package/dist/session/peer_limits.js.map +1 -0
- package/dist/session/rpc_envelope.d.ts +108 -0
- package/dist/session/rpc_envelope.js +221 -0
- package/dist/session/rpc_envelope.js.map +1 -0
- package/dist/session/rpc_inbound.d.ts +55 -0
- package/dist/session/rpc_inbound.js +136 -0
- package/dist/session/rpc_inbound.js.map +1 -0
- package/dist/session/setup_wizard.d.ts +39 -0
- package/dist/session/setup_wizard.js +63 -0
- package/dist/session/setup_wizard.js.map +1 -0
- package/dist/session/tools.d.ts +24 -0
- package/dist/session/tools.js +272 -0
- package/dist/session/tools.js.map +1 -0
- package/dist/session/wizard.d.ts +13 -0
- package/dist/session/wizard.js +20 -0
- package/dist/session/wizard.js.map +1 -0
- package/dist/transport/peer_channel.d.ts +52 -0
- package/dist/transport/peer_channel.js +145 -0
- package/dist/transport/peer_channel.js.map +1 -0
- package/dist/transport/pi_forward_client.d.ts +29 -0
- package/dist/transport/pi_forward_client.js +68 -0
- package/dist/transport/pi_forward_client.js.map +1 -0
- package/dist/transport/relay_client.d.ts +96 -0
- package/dist/transport/relay_client.js +210 -0
- package/dist/transport/relay_client.js.map +1 -0
- package/dist/ui/footer.d.ts +32 -0
- package/dist/ui/footer.js +34 -0
- package/dist/ui/footer.js.map +1 -0
- package/docs/daemon.md +289 -0
- package/package.json +92 -0
- package/service-templates/launchd.plist.template +35 -0
- package/service-templates/systemd.service.template +19 -0
- package/service-templates/task-launcher.vbs.template +10 -0
- package/service-templates/task-scheduler.xml.template +38 -0
- 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":"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"}
|
|
@@ -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
|
+
}
|