remote-pi-app 0.7.1

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 (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +675 -0
  3. package/dist/actions/handlers.d.ts +138 -0
  4. package/dist/actions/handlers.js +189 -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 +58 -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 +62 -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 +72 -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 +194 -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 +547 -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 +389 -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 +596 -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 +213 -0
  46. package/dist/index.js +5090 -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 +327 -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 +61 -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 +125 -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 +216 -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 +401 -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 +125 -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 +604 -0
  83. package/dist/pairing/storage.js.map +1 -0
  84. package/dist/protocol/codec.d.ts +7 -0
  85. package/dist/protocol/codec.js +53 -0
  86. package/dist/protocol/codec.js.map +1 -0
  87. package/dist/protocol/types.d.ts +432 -0
  88. package/dist/protocol/types.js +2 -0
  89. package/dist/protocol/types.js.map +1 -0
  90. package/dist/rooms.d.ts +29 -0
  91. package/dist/rooms.js +57 -0
  92. package/dist/rooms.js.map +1 -0
  93. package/dist/session/bridge.d.ts +37 -0
  94. package/dist/session/bridge.js +130 -0
  95. package/dist/session/bridge.js.map +1 -0
  96. package/dist/session/broker.d.ts +189 -0
  97. package/dist/session/broker.js +507 -0
  98. package/dist/session/broker.js.map +1 -0
  99. package/dist/session/broker_remote.d.ts +74 -0
  100. package/dist/session/broker_remote.js +583 -0
  101. package/dist/session/broker_remote.js.map +1 -0
  102. package/dist/session/cwd_lock.d.ts +33 -0
  103. package/dist/session/cwd_lock.js +126 -0
  104. package/dist/session/cwd_lock.js.map +1 -0
  105. package/dist/session/envelope.d.ts +33 -0
  106. package/dist/session/envelope.js +117 -0
  107. package/dist/session/envelope.js.map +1 -0
  108. package/dist/session/global_config.d.ts +33 -0
  109. package/dist/session/global_config.js +73 -0
  110. package/dist/session/global_config.js.map +1 -0
  111. package/dist/session/ipc.d.ts +27 -0
  112. package/dist/session/ipc.js +22 -0
  113. package/dist/session/ipc.js.map +1 -0
  114. package/dist/session/leader_election.d.ts +32 -0
  115. package/dist/session/leader_election.js +106 -0
  116. package/dist/session/leader_election.js.map +1 -0
  117. package/dist/session/local_config.d.ts +55 -0
  118. package/dist/session/local_config.js +157 -0
  119. package/dist/session/local_config.js.map +1 -0
  120. package/dist/session/mesh_node.d.ts +116 -0
  121. package/dist/session/mesh_node.js +573 -0
  122. package/dist/session/mesh_node.js.map +1 -0
  123. package/dist/session/peer.d.ts +139 -0
  124. package/dist/session/peer.js +451 -0
  125. package/dist/session/peer.js.map +1 -0
  126. package/dist/session/peer_inventory.d.ts +13 -0
  127. package/dist/session/peer_inventory.js +48 -0
  128. package/dist/session/peer_inventory.js.map +1 -0
  129. package/dist/session/peer_limits.d.ts +9 -0
  130. package/dist/session/peer_limits.js +21 -0
  131. package/dist/session/peer_limits.js.map +1 -0
  132. package/dist/session/setup_wizard.d.ts +39 -0
  133. package/dist/session/setup_wizard.js +63 -0
  134. package/dist/session/setup_wizard.js.map +1 -0
  135. package/dist/session/tools.d.ts +24 -0
  136. package/dist/session/tools.js +252 -0
  137. package/dist/session/tools.js.map +1 -0
  138. package/dist/session/wizard.d.ts +13 -0
  139. package/dist/session/wizard.js +20 -0
  140. package/dist/session/wizard.js.map +1 -0
  141. package/dist/transport/peer_channel.d.ts +37 -0
  142. package/dist/transport/peer_channel.js +98 -0
  143. package/dist/transport/peer_channel.js.map +1 -0
  144. package/dist/transport/pi_forward_client.d.ts +29 -0
  145. package/dist/transport/pi_forward_client.js +62 -0
  146. package/dist/transport/pi_forward_client.js.map +1 -0
  147. package/dist/transport/relay_client.d.ts +91 -0
  148. package/dist/transport/relay_client.js +206 -0
  149. package/dist/transport/relay_client.js.map +1 -0
  150. package/dist/ui/footer.d.ts +32 -0
  151. package/dist/ui/footer.js +34 -0
  152. package/dist/ui/footer.js.map +1 -0
  153. package/docs/daemon.md +289 -0
  154. package/package.json +90 -0
  155. package/service-templates/launchd.plist.template +35 -0
  156. package/service-templates/systemd.service.template +19 -0
  157. package/service-templates/task-launcher.vbs.template +10 -0
  158. package/service-templates/task-scheduler.xml.template +38 -0
  159. package/skills/agent-network/SKILL.md +310 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Plan/28 Wave B — typed action handlers.
3
+ *
4
+ * Each handler maps one `ClientMessage` action to a public Pi SDK call,
5
+ * and replies with `action_ok` or `action_error`. Handlers take their
6
+ * dependencies as parameters so the index.ts wiring is one-liner and
7
+ * unit tests can pass fakes without touching global state.
8
+ *
9
+ * `models_list` lives next door because it shares the `ModelRegistry`
10
+ * helper and the same wire vocabulary.
11
+ *
12
+ * SDK API surface used (see plan/28 Wave 0 for the full table):
13
+ *
14
+ * - `ctx.compact()` — non-blocking, fires `session_compact`
15
+ * event when done
16
+ * - `ctx.newSession()` — only on `ExtensionCommandContext`;
17
+ * resolves with `{cancelled}` flag
18
+ * - `pi.setModel(model)` — returns `false` if no auth configured
19
+ * - `pi.setThinkingLevel(lvl)` — synchronous
20
+ * - `ctx.getModel()` — optional, undefined before first turn
21
+ * - `ModelRegistry.{refresh,getAvailable,find}` — see `registry.ts`
22
+ */
23
+ import type { ClientMessage, ServerMessage, WireModel } from "../protocol/types.js";
24
+ /**
25
+ * Structural subset of the SDK's `Model<Api>` interface (defined in
26
+ * `@earendil-works/pi-ai`, which is a transitive dep — not re-exported by
27
+ * `@earendil-works/pi-coding-agent`'s main entry). Capturing just the
28
+ * fields we touch keeps the handler decoupled from the SDK's full Model
29
+ * surface and avoids a direct dep on `pi-ai`.
30
+ */
31
+ export interface SdkModelLike {
32
+ id: string;
33
+ name: string;
34
+ provider: string;
35
+ reasoning: boolean;
36
+ contextWindow: number;
37
+ /** Plan/30: accepted input modalities. The SDK's `Model.input` is
38
+ * `("text" | "image")[]`; we read `includes("image")` for the `vision`
39
+ * flag. Optional here so tests can omit it (treated as text-only). */
40
+ input?: ("text" | "image")[];
41
+ }
42
+ type Model<_TApi = unknown> = SdkModelLike;
43
+ /**
44
+ * Minimal channel surface needed to reply. Mirrors `PlainPeerChannel`'s
45
+ * `.send` signature; tests pass an array-backed fake.
46
+ */
47
+ export interface ActionReplySender {
48
+ send(msg: ServerMessage): void;
49
+ }
50
+ /**
51
+ * Narrow shape of the `ExtensionAPI` surface action handlers actually
52
+ * call. Lets the test layer stub just these without rebuilding the full
53
+ * SDK type (which has 30+ methods we don't use here).
54
+ */
55
+ export interface ActionPi {
56
+ setModel(model: Model<any>): Promise<boolean>;
57
+ setThinkingLevel(level: import("../protocol/types.js").ThinkingLevel): void;
58
+ }
59
+ /**
60
+ * Narrow shape of the per-call context. Drawn from the union of
61
+ * `ExtensionContextActions` (compact, getModel) and
62
+ * `ExtensionCommandContextActions` (newSession), since index.ts caches
63
+ * the most-recent ctx and that's typically the command one.
64
+ *
65
+ * All fields are optional so a missing method (e.g. when only a plain
66
+ * `ExtensionContext` was seen) becomes a typed `action_error` instead of
67
+ * a runtime TypeError.
68
+ */
69
+ export interface ActionCtx {
70
+ compact?: (options?: object) => void;
71
+ /**
72
+ * Starts a new session. `withSession` is the SDK's blessed hook for
73
+ * post-replacement work: it receives a FRESH, command-capable ctx bound to
74
+ * the new session. The SDK marks any ctx captured BEFORE this call stale, so
75
+ * callers must re-capture via `withSession` rather than reuse the old ctx.
76
+ */
77
+ newSession?: (options?: {
78
+ withSession?: (ctx: ActionCtx) => Promise<void>;
79
+ }) => Promise<{
80
+ cancelled: boolean;
81
+ }>;
82
+ getModel?: () => Model<any> | undefined;
83
+ /**
84
+ * Live session registry from Pi's extension ctx. Includes providers/models
85
+ * registered dynamically via `pi.registerProvider(...)`, unlike the fallback
86
+ * disk-backed registry remote-pi can build on its own.
87
+ */
88
+ modelRegistry?: ActionModelRegistry;
89
+ }
90
+ /**
91
+ * Minimal shape of the registry surface. Maps 1:1 onto `ModelRegistry`
92
+ * but lets tests fake catalogs without instantiating the real one.
93
+ */
94
+ export interface ActionModelRegistry {
95
+ refresh(): void;
96
+ getAvailable(): Model<any>[];
97
+ find(provider: string, modelId: string): Model<any> | undefined;
98
+ }
99
+ /** Project a SDK `Model<Api>` onto the wire schema. Shared by list_models
100
+ * and the `current` echo, so both stay in lockstep. */
101
+ export declare function wireFromModel(model: Model<any>): WireModel;
102
+ type SessionCompactMsg = Extract<ClientMessage, {
103
+ type: "session_compact";
104
+ }>;
105
+ type SessionNewMsg = Extract<ClientMessage, {
106
+ type: "session_new";
107
+ }>;
108
+ type ModelSetMsg = Extract<ClientMessage, {
109
+ type: "model_set";
110
+ }>;
111
+ type ThinkingSetMsg = Extract<ClientMessage, {
112
+ type: "thinking_set";
113
+ }>;
114
+ type ListModelsMsg = Extract<ClientMessage, {
115
+ type: "list_models";
116
+ }>;
117
+ type ListCommandsMsg = Extract<ClientMessage, {
118
+ type: "list_commands";
119
+ }>;
120
+ export declare function handleSessionCompact(ctx: ActionCtx | null, sender: ActionReplySender, msg: SessionCompactMsg): void;
121
+ export declare function handleSessionNew(ctx: ActionCtx | null, sender: ActionReplySender, msg: SessionNewMsg, onReplaced?: (freshCtx: ActionCtx) => void): Promise<boolean>;
122
+ export declare function handleThinkingSet(pi: ActionPi, sender: ActionReplySender, msg: ThinkingSetMsg): void;
123
+ export declare function handleModelSet(pi: ActionPi, ctx: ActionCtx | null, reg: ActionModelRegistry, sender: ActionReplySender, msg: ModelSetMsg, onPersist?: (provider: string, modelId: string) => void): Promise<void>;
124
+ export declare function handleListModels(ctx: ActionCtx | null, reg: ActionModelRegistry, sender: ActionReplySender, msg: ListModelsMsg): void;
125
+ /**
126
+ * Plan/58 (primitive A): enumerate the slash commands pi currently exposes
127
+ * (remote-pi's own, plus any registered by third-party plugins) so remote
128
+ * clients can offer a picker. Read-only — execution goes through the daemon's
129
+ * RPC `prompt` path (see the relay `user_message` handler in index.ts).
130
+ */
131
+ export declare function handleListCommands(pi: {
132
+ getCommands?: () => Array<{
133
+ name: string;
134
+ description?: string;
135
+ source?: string;
136
+ }>;
137
+ } | null, sender: ActionReplySender, msg: ListCommandsMsg): void;
138
+ export {};
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Plan/28 Wave B — typed action handlers.
3
+ *
4
+ * Each handler maps one `ClientMessage` action to a public Pi SDK call,
5
+ * and replies with `action_ok` or `action_error`. Handlers take their
6
+ * dependencies as parameters so the index.ts wiring is one-liner and
7
+ * unit tests can pass fakes without touching global state.
8
+ *
9
+ * `models_list` lives next door because it shares the `ModelRegistry`
10
+ * helper and the same wire vocabulary.
11
+ *
12
+ * SDK API surface used (see plan/28 Wave 0 for the full table):
13
+ *
14
+ * - `ctx.compact()` — non-blocking, fires `session_compact`
15
+ * event when done
16
+ * - `ctx.newSession()` — only on `ExtensionCommandContext`;
17
+ * resolves with `{cancelled}` flag
18
+ * - `pi.setModel(model)` — returns `false` if no auth configured
19
+ * - `pi.setThinkingLevel(lvl)` — synchronous
20
+ * - `ctx.getModel()` — optional, undefined before first turn
21
+ * - `ModelRegistry.{refresh,getAvailable,find}` — see `registry.ts`
22
+ */
23
+ /** Project a SDK `Model<Api>` onto the wire schema. Shared by list_models
24
+ * and the `current` echo, so both stay in lockstep. */
25
+ export function wireFromModel(model) {
26
+ return {
27
+ id: model.id,
28
+ name: model.name,
29
+ provider: model.provider,
30
+ reasoning: model.reasoning,
31
+ context_window: model.contextWindow,
32
+ // Plan/30: vision = model accepts image input. `Model.input` is
33
+ // `("text" | "image")[]` at runtime (confirmed against pi-ai). `?.` guards
34
+ // a fake/partial model in tests → treated as text-only.
35
+ vision: model.input?.includes("image") ?? false,
36
+ };
37
+ }
38
+ // ── ack helpers ────────────────────────────────────────────────────────────
39
+ function ok(sender, msg, action) {
40
+ sender.send({ type: "action_ok", in_reply_to: msg.id, action });
41
+ }
42
+ function fail(sender, msg, action, err) {
43
+ const error = err instanceof Error ? err.message : String(err);
44
+ sender.send({ type: "action_error", in_reply_to: msg.id, action, error });
45
+ }
46
+ /** Run a synchronous action with uniform success/failure replies. */
47
+ function runSync(sender, msg, action, body) {
48
+ try {
49
+ body();
50
+ ok(sender, msg, action);
51
+ }
52
+ catch (e) {
53
+ fail(sender, msg, action, e);
54
+ }
55
+ }
56
+ /** Run an async action with uniform success/failure replies. */
57
+ async function runAsync(sender, msg, action, body) {
58
+ try {
59
+ await body();
60
+ ok(sender, msg, action);
61
+ return true;
62
+ }
63
+ catch (e) {
64
+ fail(sender, msg, action, e);
65
+ return false;
66
+ }
67
+ }
68
+ export function handleSessionCompact(ctx, sender, msg) {
69
+ runSync(sender, msg, "session_compact", () => {
70
+ if (!ctx?.compact)
71
+ throw new Error("compact unavailable (no active session ctx)");
72
+ // Force the summary to English regardless of the conversation language —
73
+ // the summary is surfaced to the app via the `compaction` message, which
74
+ // is an English-only surface. `customInstructions` is appended to the SDK's
75
+ // compaction prompt (best-effort: the model writes the summary).
76
+ ctx.compact({
77
+ customInstructions: "Always write the compaction summary in English, even if the conversation is in another language.",
78
+ });
79
+ });
80
+ }
81
+ export async function handleSessionNew(ctx, sender, msg, onReplaced) {
82
+ // Returns true only when a fresh session was actually created. index.ts
83
+ // keys the Pi-side reset (clear _messageBuffer, restamp _sessionStartedAt,
84
+ // fan out an empty session_history) off this signal — a `cancelled`/errored
85
+ // new-session must NOT reset, so we return runAsync's success boolean.
86
+ return runAsync(sender, msg, "session_new", async () => {
87
+ if (!ctx?.newSession)
88
+ throw new Error("newSession unavailable (no command ctx yet)");
89
+ // newSession marks the caller's captured ctx (index.ts's `_lastCtx`) STALE
90
+ // — reusing it later throws "stale after session replacement" (the
91
+ // compact-after-New-session crash). `withSession` hands back a fresh,
92
+ // command-capable ctx bound to the new session; forward it via onReplaced
93
+ // so the caller re-captures and keeps later actions off the stale ctx.
94
+ const result = await ctx.newSession({
95
+ withSession: async (freshCtx) => { onReplaced?.(freshCtx); },
96
+ });
97
+ // `cancelled: true` happens when the SDK's hook chain vetoes the new
98
+ // session (e.g. an extension's `session_before_switch` returned a
99
+ // refusal). Surface as a typed error rather than silent success.
100
+ if (result.cancelled)
101
+ throw new Error("cancelled by extension hook");
102
+ });
103
+ }
104
+ export function handleThinkingSet(pi, sender, msg) {
105
+ runSync(sender, msg, "thinking_set", () => {
106
+ pi.setThinkingLevel(msg.level);
107
+ });
108
+ }
109
+ export async function handleModelSet(pi, ctx, reg, sender, msg, onPersist) {
110
+ await runAsync(sender, msg, "model_set", async () => {
111
+ // Prefer Pi's LIVE session registry when available so the app sees models
112
+ // registered dynamically by extensions via `pi.registerProvider(...)`.
113
+ // Fall back to remote-pi's own disk-backed registry when no ctx exists.
114
+ const liveReg = ctx?.modelRegistry ?? reg;
115
+ // Refresh first so a model just-added via `/login` is visible.
116
+ liveReg.refresh();
117
+ const model = liveReg.find(msg.provider, msg.model_id);
118
+ if (!model) {
119
+ throw new Error(`model "${msg.provider}/${msg.model_id}" not in registry`);
120
+ }
121
+ const success = await pi.setModel(model);
122
+ if (!success)
123
+ throw new Error("no auth configured for this model");
124
+ // `pi.setModel` only sets the LIVE model — it does NOT persist. Without
125
+ // this, a model picked from the app reverts to the saved default on the
126
+ // next Pi/daemon restart (the TUI persists because AgentSession.setModel
127
+ // writes the default; this path doesn't). `onPersist` writes the new
128
+ // default so the app's choice survives. Best-effort — the caller's writer
129
+ // must not throw, so a failed settings write never fails the model change.
130
+ onPersist?.(model.provider, model.id);
131
+ });
132
+ }
133
+ export function handleListModels(ctx, reg, sender, msg) {
134
+ // refresh() can throw if `models.json` is malformed — wrap in try so the
135
+ // app gets an explicit error reply instead of a silent drop.
136
+ try {
137
+ // Prefer Pi's LIVE session registry when available so the app sees models
138
+ // registered dynamically by extensions via `pi.registerProvider(...)`.
139
+ // Fall back to remote-pi's own disk-backed registry when no ctx exists.
140
+ const liveReg = ctx?.modelRegistry ?? reg;
141
+ liveReg.refresh();
142
+ const models = liveReg.getAvailable().map(wireFromModel);
143
+ const current = ctx?.getModel?.();
144
+ sender.send({
145
+ type: "models_list",
146
+ in_reply_to: msg.id,
147
+ models,
148
+ current: current ? wireFromModel(current) : undefined,
149
+ });
150
+ }
151
+ catch (e) {
152
+ sender.send({
153
+ type: "error",
154
+ in_reply_to: msg.id,
155
+ code: "internal_error",
156
+ message: e instanceof Error ? e.message : String(e),
157
+ });
158
+ }
159
+ }
160
+ /**
161
+ * Plan/58 (primitive A): enumerate the slash commands pi currently exposes
162
+ * (remote-pi's own, plus any registered by third-party plugins) so remote
163
+ * clients can offer a picker. Read-only — execution goes through the daemon's
164
+ * RPC `prompt` path (see the relay `user_message` handler in index.ts).
165
+ */
166
+ export function handleListCommands(pi, sender, msg) {
167
+ try {
168
+ const commands = (pi?.getCommands?.() ?? []).map((command) => {
169
+ const source = command.source === "extension" || command.source === "prompt" || command.source === "skill"
170
+ ? command.source
171
+ : "extension";
172
+ return {
173
+ name: command.name,
174
+ ...(command.description ? { description: command.description } : {}),
175
+ source,
176
+ };
177
+ });
178
+ sender.send({ type: "commands_list", in_reply_to: msg.id, commands });
179
+ }
180
+ catch (e) {
181
+ sender.send({
182
+ type: "error",
183
+ in_reply_to: msg.id,
184
+ code: "internal_error",
185
+ message: e instanceof Error ? e.message : String(e),
186
+ });
187
+ }
188
+ }
189
+ //# sourceMappingURL=handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.js","sourceRoot":"","sources":["../../src/actions/handlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Plan/28 — ModelRegistry access for the action handlers.
3
+ *
4
+ * The host hands every extension a ready `ModelRegistry` on
5
+ * `ExtensionContext.modelRegistry` — the same instance `AgentSession` uses
6
+ * internally. That is the ONLY source we read.
7
+ *
8
+ * History (issue #112): this module used to build its own registry via
9
+ * `ModelRegistry.create(AuthStorage.create())`. Both factories were removed
10
+ * from `@earendil-works/pi-coding-agent` in pi 0.83, so the import crashed the
11
+ * whole pi process on the first `model_set`/`list_models` from the app
12
+ * (`undefined is not an object (evaluating 'AuthStorage.create')`, thrown from
13
+ * the WebSocket line handler). Reading the registry off the live ctx drops the
14
+ * dependency on that package's factory surface entirely, so a future breaking
15
+ * change there cannot crash the extension the same way again.
16
+ *
17
+ * Two hazards this handles:
18
+ *
19
+ * 1. A ctx captured before a session replacement/reload is **stale**, and even
20
+ * *reading* `.modelRegistry` on it throws (`assertActive`). Every read is
21
+ * wrapped.
22
+ * 2. There may be no live ctx at all (daemon boot, control channel). Rather
23
+ * than throw from a WS callback — an uncaught exception that exits pi — we
24
+ * fall back to the last registry that worked, then to a stub whose methods
25
+ * never throw and report an empty catalog.
26
+ */
27
+ import type { ActionModelRegistry } from "./handlers.js";
28
+ /** Anything carrying the host registry — `ExtensionContext` and friends. */
29
+ interface RegistryBearingCtx {
30
+ modelRegistry?: ActionModelRegistry;
31
+ }
32
+ /**
33
+ * Resolve the live host `ModelRegistry` from the most recent extension ctx.
34
+ * Falls back to the last one that worked, then to an inert stub.
35
+ */
36
+ export declare function ensureModelRegistry(ctx?: RegistryBearingCtx | null): ActionModelRegistry;
37
+ /** Test seam — drop the cached registry so tests can rebuild with fakes. */
38
+ export declare function _resetModelRegistryForTests(): void;
39
+ export {};
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Plan/28 — ModelRegistry access for the action handlers.
3
+ *
4
+ * The host hands every extension a ready `ModelRegistry` on
5
+ * `ExtensionContext.modelRegistry` — the same instance `AgentSession` uses
6
+ * internally. That is the ONLY source we read.
7
+ *
8
+ * History (issue #112): this module used to build its own registry via
9
+ * `ModelRegistry.create(AuthStorage.create())`. Both factories were removed
10
+ * from `@earendil-works/pi-coding-agent` in pi 0.83, so the import crashed the
11
+ * whole pi process on the first `model_set`/`list_models` from the app
12
+ * (`undefined is not an object (evaluating 'AuthStorage.create')`, thrown from
13
+ * the WebSocket line handler). Reading the registry off the live ctx drops the
14
+ * dependency on that package's factory surface entirely, so a future breaking
15
+ * change there cannot crash the extension the same way again.
16
+ *
17
+ * Two hazards this handles:
18
+ *
19
+ * 1. A ctx captured before a session replacement/reload is **stale**, and even
20
+ * *reading* `.modelRegistry` on it throws (`assertActive`). Every read is
21
+ * wrapped.
22
+ * 2. There may be no live ctx at all (daemon boot, control channel). Rather
23
+ * than throw from a WS callback — an uncaught exception that exits pi — we
24
+ * fall back to the last registry that worked, then to a stub whose methods
25
+ * never throw and report an empty catalog.
26
+ */
27
+ let _lastGoodRegistry = null;
28
+ /**
29
+ * Never-throwing empty catalog. Used only when no ctx has ever been seen —
30
+ * callers get an empty model list instead of a crashed agent.
31
+ */
32
+ const _stubRegistry = {
33
+ refresh() { },
34
+ getAvailable() { return []; },
35
+ find() { return undefined; },
36
+ };
37
+ /**
38
+ * Resolve the live host `ModelRegistry` from the most recent extension ctx.
39
+ * Falls back to the last one that worked, then to an inert stub.
40
+ */
41
+ export function ensureModelRegistry(ctx) {
42
+ try {
43
+ const live = ctx?.modelRegistry;
44
+ if (live) {
45
+ _lastGoodRegistry = live;
46
+ return live;
47
+ }
48
+ }
49
+ catch {
50
+ // stale ctx after session replacement — reading the property throws.
51
+ }
52
+ return _lastGoodRegistry ?? _stubRegistry;
53
+ }
54
+ /** Test seam — drop the cached registry so tests can rebuild with fakes. */
55
+ export function _resetModelRegistryForTests() {
56
+ _lastGoodRegistry = null;
57
+ }
58
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/actions/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AASH,IAAI,iBAAiB,GAA+B,IAAI,CAAC;AAEzD;;;GAGG;AACH,MAAM,aAAa,GAAwB;IACzC,OAAO,KAAI,CAAC;IACZ,YAAY,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC;IAC7B,IAAI,KAAK,OAAO,SAAS,CAAC,CAAC,CAAC;CAC7B,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAA+B;IACjE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,GAAG,EAAE,aAAa,CAAC;QAChC,IAAI,IAAI,EAAE,CAAC;YACT,iBAAiB,GAAG,IAAI,CAAC;YACzB,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,qEAAqE;IACvE,CAAC;IACD,OAAO,iBAAiB,IAAI,aAAa,CAAC;AAC5C,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,2BAA2B;IACzC,iBAAiB,GAAG,IAAI,CAAC;AAC3B,CAAC"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `pi-supervisord` — long-running daemon supervisor.
4
+ *
5
+ * Entry point of the `pi-supervisord` binary (plan/26 W2). Run by
6
+ * systemd/launchd in production, or directly during dev:
7
+ *
8
+ * pnpm build
9
+ * node dist/bin/supervisord.js
10
+ *
11
+ * Once running, it:
12
+ * - Reads `~/.pi/remote/daemons.json`
13
+ * - Spawns `pi --mode rpc -e <remote-pi/dist/index.js>` per entry
14
+ * - Listens on `~/.pi/remote/supervisor.sock` for CLI control requests
15
+ * - Restarts crashed children with exponential backoff
16
+ *
17
+ * Exits cleanly on SIGTERM/SIGINT (used by `remote-pi uninstall`).
18
+ */
19
+ import { fileURLToPath } from "node:url";
20
+ import { dirname, join } from "node:path";
21
+ import { Supervisor, SupervisorAlreadyRunningError } from "../daemon/supervisor.js";
22
+ const HELP_TEXT = `pi-supervisord — Remote Pi daemon supervisor
23
+
24
+ Usage: pi-supervisord
25
+
26
+ Runs the long-lived supervisor: reads ~/.pi/remote/daemons.json, spawns one
27
+ \`pi --mode rpc\` child per entry, and listens on ~/.pi/remote/supervisor.sock
28
+ for control requests from the \`remote-pi\` CLI.
29
+
30
+ This binary takes NO arguments — it is normally launched by systemd/launchd
31
+ (via \`remote-pi install\`), not by hand. Manage the fleet with:
32
+ remote-pi daemon start | stop | restart | status
33
+ remote-pi daemons
34
+
35
+ Options:
36
+ -h, --help Show this help and exit
37
+ -v, --version Print version and exit
38
+ `;
39
+ async function main() {
40
+ // Guard: any stray argument used to fall through and start a FULL
41
+ // supervisor (a `pi-supervisord --help` once ran for days). Handle the
42
+ // conventional flags explicitly and reject unknown args instead of
43
+ // silently spawning the daemon fleet.
44
+ const args = process.argv.slice(2);
45
+ if (args.includes("-h") || args.includes("--help")) {
46
+ process.stdout.write(HELP_TEXT);
47
+ return;
48
+ }
49
+ if (args.includes("-v") || args.includes("--version")) {
50
+ process.stdout.write("pi-supervisord (remote-pi)\n");
51
+ return;
52
+ }
53
+ if (args.length > 0) {
54
+ process.stderr.write(`pi-supervisord: unexpected argument(s): ${args.join(" ")}\n` +
55
+ "This binary takes no arguments. Run `pi-supervisord --help`.\n");
56
+ process.exit(2);
57
+ }
58
+ // The supervisor needs to point each spawned Pi at the extension
59
+ // entry it's bundled with. We're at `dist/bin/supervisord.js` after
60
+ // build; the extension is the sibling `dist/index.js`.
61
+ const here = fileURLToPath(import.meta.url);
62
+ const distRoot = dirname(dirname(here)); // dist/bin → dist
63
+ const extensionPath = join(distRoot, "index.js");
64
+ const supervisor = new Supervisor({ extensionPath });
65
+ await supervisor.start();
66
+ process.stderr.write(`[pi-supervisord] up — UDS: ~/.pi/remote/supervisor.sock, extension: ${extensionPath}\n`);
67
+ const shutdown = async (signal) => {
68
+ process.stderr.write(`[pi-supervisord] received ${signal}, shutting down\n`);
69
+ await supervisor.stop();
70
+ process.exit(0);
71
+ };
72
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
73
+ process.on("SIGINT", () => void shutdown("SIGINT"));
74
+ }
75
+ main().catch((err) => {
76
+ // "Already running" is a normal, expected condition (systemd/launchd may
77
+ // race a manual start, or the user double-launches) — not a crash. Report
78
+ // it calmly and exit 0 so service managers don't flag a failure loop.
79
+ if (err instanceof SupervisorAlreadyRunningError) {
80
+ process.stderr.write(`[pi-supervisord] ${err.message}\n`);
81
+ process.exit(0);
82
+ }
83
+ process.stderr.write(`[pi-supervisord] fatal: ${String(err)}\n`);
84
+ process.exit(1);
85
+ });
86
+ //# sourceMappingURL=supervisord.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"supervisord.js","sourceRoot":"","sources":["../../src/bin/supervisord.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Default community relay. Stored in canonical http(s):// form — conversion
3
+ * to ws(s):// happens at the transport layer (see `toWebSocketUrl`). The
4
+ * community relay's reverse proxy maps `:443 → :3000` (the WS port), so the
5
+ * URL has no explicit port and the WebSocket upgrade rides on the same TLS
6
+ * connection as the HTTPS endpoints used by the mesh client.
7
+ */
8
+ export declare const kDefaultRelayUrl = "https://relay-rp1.jacobmoura.work";
9
+ export type RemotePiConfig = {
10
+ relay?: string;
11
+ };
12
+ export declare function loadConfig(): RemotePiConfig;
13
+ export declare function saveConfig(patch: Partial<RemotePiConfig>): void;
14
+ export type RelayResolution = {
15
+ url: string;
16
+ source: "env" | "config" | "default";
17
+ };
18
+ /**
19
+ * Resolves the effective relay URL in **canonical http(s):// form**.
20
+ *
21
+ * Precedence:
22
+ * 1. `REMOTE_PI_RELAY` env var (ops/CI escape hatch)
23
+ * 2. `~/.pi/remote/config.json` `relay` field (set via /remote-pi set-relay)
24
+ * 3. `kDefaultRelayUrl` (community default)
25
+ *
26
+ * Any ws(s):// values found (legacy configs or env overrides) are coerced
27
+ * to http(s):// defensively — the canonical form across the codebase is
28
+ * http(s)://, and the transport layer converts to ws(s):// at WS-open time.
29
+ */
30
+ export declare function resolveRelayUrl(): RelayResolution;
31
+ /**
32
+ * Strict validator for **user-provided** relay URLs (via `/remote-pi
33
+ * set-relay` or `/remote-pi relay url`).
34
+ *
35
+ * Only accepts `http://` and `https://`. `ws://`/`wss://` are deliberately
36
+ * **rejected** — the canonical form stored in config is http(s):// and the
37
+ * extension converts to ws(s):// internally when opening the WebSocket.
38
+ * Forcing a single scheme at the user boundary avoids two-form drift.
39
+ */
40
+ export declare function isValidRelayUrl(url: string): boolean;
41
+ /**
42
+ * Returns true if the URL uses ws:// or wss:// scheme — for emitting a
43
+ * targeted error message when the user pastes a WebSocket URL by mistake.
44
+ */
45
+ export declare function isWebSocketScheme(url: string): boolean;
46
+ /**
47
+ * Converts an http(s):// URL to the corresponding ws(s):// form. Used by
48
+ * the transport layer right before opening the WebSocket — config storage
49
+ * and the mesh HTTP client both stay on http(s)://.
50
+ *
51
+ * https://host → wss://host
52
+ * http://host → ws://host
53
+ * ws(s)://host → pass-through (defensive — env overrides or legacy
54
+ * configs may still carry ws(s)://)
55
+ */
56
+ export declare function toWebSocketUrl(url: string): string;
57
+ /**
58
+ * Inverse of `toWebSocketUrl`. Used by `resolveRelayUrl` to coerce any
59
+ * ws(s):// values back to canonical http(s):// before returning them to
60
+ * the rest of the codebase.
61
+ */
62
+ export declare function toHttpUrl(url: string): string;