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.
- package/LICENSE +21 -0
- package/README.md +675 -0
- package/dist/actions/handlers.d.ts +138 -0
- package/dist/actions/handlers.js +189 -0
- package/dist/actions/handlers.js.map +1 -0
- package/dist/actions/registry.d.ts +39 -0
- package/dist/actions/registry.js +58 -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 +62 -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 +72 -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 +194 -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 +547 -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 +389 -0
- package/dist/daemon/rpc_child.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +93 -0
- package/dist/daemon/supervisor.js +596 -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 +213 -0
- package/dist/index.js +5090 -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 +327 -0
- package/dist/mcp/mesh_server.js.map +1 -0
- package/dist/mesh/canonical.d.ts +30 -0
- package/dist/mesh/canonical.js +61 -0
- package/dist/mesh/canonical.js.map +1 -0
- package/dist/mesh/client.d.ts +18 -0
- package/dist/mesh/client.js +125 -0
- package/dist/mesh/client.js.map +1 -0
- package/dist/mesh/encoding.d.ts +38 -0
- package/dist/mesh/encoding.js +216 -0
- package/dist/mesh/encoding.js.map +1 -0
- package/dist/mesh/self_revoke.d.ts +73 -0
- package/dist/mesh/self_revoke.js +401 -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 +125 -0
- package/dist/pairing/qr.js.map +1 -0
- package/dist/pairing/storage.d.ts +120 -0
- package/dist/pairing/storage.js +604 -0
- package/dist/pairing/storage.js.map +1 -0
- package/dist/protocol/codec.d.ts +7 -0
- package/dist/protocol/codec.js +53 -0
- package/dist/protocol/codec.js.map +1 -0
- package/dist/protocol/types.d.ts +432 -0
- package/dist/protocol/types.js +2 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/rooms.d.ts +29 -0
- package/dist/rooms.js +57 -0
- package/dist/rooms.js.map +1 -0
- package/dist/session/bridge.d.ts +37 -0
- package/dist/session/bridge.js +130 -0
- package/dist/session/bridge.js.map +1 -0
- package/dist/session/broker.d.ts +189 -0
- package/dist/session/broker.js +507 -0
- package/dist/session/broker.js.map +1 -0
- package/dist/session/broker_remote.d.ts +74 -0
- package/dist/session/broker_remote.js +583 -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 +126 -0
- package/dist/session/cwd_lock.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 +106 -0
- package/dist/session/leader_election.js.map +1 -0
- package/dist/session/local_config.d.ts +55 -0
- package/dist/session/local_config.js +157 -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 +573 -0
- package/dist/session/mesh_node.js.map +1 -0
- package/dist/session/peer.d.ts +139 -0
- package/dist/session/peer.js +451 -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 +21 -0
- package/dist/session/peer_limits.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 +252 -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 +37 -0
- package/dist/transport/peer_channel.js +98 -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 +62 -0
- package/dist/transport/pi_forward_client.js.map +1 -0
- package/dist/transport/relay_client.d.ts +91 -0
- package/dist/transport/relay_client.js +206 -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 +90 -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,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,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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"}
|
package/dist/config.d.ts
ADDED
|
@@ -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;
|