@yefengr/pi-reach 0.0.0-stage → 0.0.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 (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -2
  3. package/dist/actions/handlers.d.ts +143 -0
  4. package/dist/actions/handlers.js +200 -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/actions/session_new_bridge.d.ts +35 -0
  10. package/dist/actions/session_new_bridge.js +88 -0
  11. package/dist/actions/session_new_bridge.js.map +1 -0
  12. package/dist/commands.d.ts +29 -0
  13. package/dist/commands.js +169 -0
  14. package/dist/commands.js.map +1 -0
  15. package/dist/config.d.ts +61 -0
  16. package/dist/config.js +115 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/index.d.ts +27 -0
  19. package/dist/index.js +618 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/pairing/crypto.d.ts +15 -0
  22. package/dist/pairing/crypto.js +72 -0
  23. package/dist/pairing/crypto.js.map +1 -0
  24. package/dist/pairing/identity-keyring.d.ts +22 -0
  25. package/dist/pairing/identity-keyring.js +94 -0
  26. package/dist/pairing/identity-keyring.js.map +1 -0
  27. package/dist/pairing/identity-lock.d.ts +14 -0
  28. package/dist/pairing/identity-lock.js +116 -0
  29. package/dist/pairing/identity-lock.js.map +1 -0
  30. package/dist/pairing/owner_storage.d.ts +61 -0
  31. package/dist/pairing/owner_storage.js +263 -0
  32. package/dist/pairing/owner_storage.js.map +1 -0
  33. package/dist/pairing/qr.d.ts +39 -0
  34. package/dist/pairing/qr.js +113 -0
  35. package/dist/pairing/qr.js.map +1 -0
  36. package/dist/pairing/storage.d.ts +36 -0
  37. package/dist/pairing/storage.js +263 -0
  38. package/dist/pairing/storage.js.map +1 -0
  39. package/dist/protocol/types.d.ts +225 -0
  40. package/dist/protocol/types.js +2 -0
  41. package/dist/protocol/types.js.map +1 -0
  42. package/dist/protocol/v2/codec.d.ts +6 -0
  43. package/dist/protocol/v2/codec.js +4 -0
  44. package/dist/protocol/v2/codec.js.map +1 -0
  45. package/dist/protocol/v2/index.d.ts +4 -0
  46. package/dist/protocol/v2/index.js +5 -0
  47. package/dist/protocol/v2/index.js.map +1 -0
  48. package/dist/protocol/v2/marker.d.ts +45 -0
  49. package/dist/protocol/v2/marker.js +47 -0
  50. package/dist/protocol/v2/marker.js.map +1 -0
  51. package/dist/protocol/v2/schemas.d.ts +3 -0
  52. package/dist/protocol/v2/schemas.js +4 -0
  53. package/dist/protocol/v2/schemas.js.map +1 -0
  54. package/dist/protocol/v2/session_state.d.ts +79 -0
  55. package/dist/protocol/v2/session_state.js +166 -0
  56. package/dist/protocol/v2/session_state.js.map +1 -0
  57. package/dist/runtime/endpoint_metadata.d.ts +14 -0
  58. package/dist/runtime/endpoint_metadata.js +18 -0
  59. package/dist/runtime/endpoint_metadata.js.map +1 -0
  60. package/dist/runtime/extension_owner.d.ts +12 -0
  61. package/dist/runtime/extension_owner.js +80 -0
  62. package/dist/runtime/extension_owner.js.map +1 -0
  63. package/dist/runtime/footer.d.ts +6 -0
  64. package/dist/runtime/footer.js +21 -0
  65. package/dist/runtime/footer.js.map +1 -0
  66. package/dist/runtime/owner_router.d.ts +16 -0
  67. package/dist/runtime/owner_router.js +41 -0
  68. package/dist/runtime/owner_router.js.map +1 -0
  69. package/dist/runtime/pairing_coordinator.d.ts +42 -0
  70. package/dist/runtime/pairing_coordinator.js +136 -0
  71. package/dist/runtime/pairing_coordinator.js.map +1 -0
  72. package/dist/runtime/relay_lifecycle.d.ts +64 -0
  73. package/dist/runtime/relay_lifecycle.js +275 -0
  74. package/dist/runtime/relay_lifecycle.js.map +1 -0
  75. package/dist/session/local_config.d.ts +7 -0
  76. package/dist/session/local_config.js +52 -0
  77. package/dist/session/local_config.js.map +1 -0
  78. package/dist/timeline/history.d.ts +30 -0
  79. package/dist/timeline/history.js +170 -0
  80. package/dist/timeline/history.js.map +1 -0
  81. package/dist/timeline/partial_delta.d.ts +2 -0
  82. package/dist/timeline/partial_delta.js +16 -0
  83. package/dist/timeline/partial_delta.js.map +1 -0
  84. package/dist/timeline/run_end.d.ts +16 -0
  85. package/dist/timeline/run_end.js +23 -0
  86. package/dist/timeline/run_end.js.map +1 -0
  87. package/dist/timeline/runtime.d.ts +89 -0
  88. package/dist/timeline/runtime.js +546 -0
  89. package/dist/timeline/runtime.js.map +1 -0
  90. package/dist/timeline/sequence.d.ts +7 -0
  91. package/dist/timeline/sequence.js +16 -0
  92. package/dist/timeline/sequence.js.map +1 -0
  93. package/dist/timeline/tool_lifecycle.d.ts +36 -0
  94. package/dist/timeline/tool_lifecycle.js +171 -0
  95. package/dist/timeline/tool_lifecycle.js.map +1 -0
  96. package/dist/timeline/user_delivery.d.ts +96 -0
  97. package/dist/timeline/user_delivery.js +360 -0
  98. package/dist/timeline/user_delivery.js.map +1 -0
  99. package/dist/timeline/user_delivery_binding.d.ts +43 -0
  100. package/dist/timeline/user_delivery_binding.js +107 -0
  101. package/dist/timeline/user_delivery_binding.js.map +1 -0
  102. package/dist/timeline/v2_service.d.ts +82 -0
  103. package/dist/timeline/v2_service.js +512 -0
  104. package/dist/timeline/v2_service.js.map +1 -0
  105. package/dist/transport/peer_channel.d.ts +34 -0
  106. package/dist/transport/peer_channel.js +116 -0
  107. package/dist/transport/peer_channel.js.map +1 -0
  108. package/dist/transport/relay_client.d.ts +54 -0
  109. package/dist/transport/relay_client.js +270 -0
  110. package/dist/transport/relay_client.js.map +1 -0
  111. package/dist/vendor/protocol/constants.d.ts +4 -0
  112. package/dist/vendor/protocol/constants.js +4 -0
  113. package/dist/vendor/protocol/encoding.d.ts +9 -0
  114. package/dist/vendor/protocol/encoding.js +66 -0
  115. package/dist/vendor/protocol/outer/codec.d.ts +13 -0
  116. package/dist/vendor/protocol/outer/codec.js +105 -0
  117. package/dist/vendor/protocol/outer/index.d.ts +4 -0
  118. package/dist/vendor/protocol/outer/index.js +4 -0
  119. package/dist/vendor/protocol/outer/types.d.ts +120 -0
  120. package/dist/vendor/protocol/outer/types.js +1 -0
  121. package/dist/vendor/protocol/session/codec.d.ts +41 -0
  122. package/dist/vendor/protocol/session/codec.js +236 -0
  123. package/dist/vendor/protocol/session/frames.d.ts +5220 -0
  124. package/dist/vendor/protocol/session/frames.js +83 -0
  125. package/dist/vendor/protocol/session/index.d.ts +3 -0
  126. package/dist/vendor/protocol/session/index.js +3 -0
  127. package/dist/vendor/protocol/session/schema.d.ts +858 -0
  128. package/dist/vendor/protocol/session/schema.js +256 -0
  129. package/package.json +74 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jacob Moura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,107 @@
1
- # Temporary Holding Version
1
+ <h1 align="center">Pi Reach</h1>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > A Pi Extension for controlling the current Pi process from the browser through a Relay.
4
+
5
+ `/pi-reach` connects the current Pi process to a Relay, supports Owner pairing, and exposes the live timeline plus typed session actions to the Pi Reach PWA.
6
+
7
+ ## Endpoint model
8
+
9
+ ```text
10
+ device -> endpoint -> runtime -> session / history generation
11
+ ```
12
+
13
+ - **Device**: the computer's Ed25519 identity.
14
+ - **Endpoint and runtime**: generated randomly when a Pi process loads the Extension. They remain stable across Extension reloads in that process and are regenerated for the next Pi process.
15
+ - **Session / generation**: the active Pi conversation and its current history branch. Pi Reach does not list or resume historical sessions.
16
+
17
+ The endpoint never derives from the working directory. Pairing QR codes target the current endpoint and runtime, while the resulting Owner authorization is stored at device scope. Later Pi processes on that computer are discovered without pairing again.
18
+
19
+ ## Quick start
20
+
21
+ Install the Extension once:
22
+
23
+ ```bash
24
+ pi install npm:@yefengr/pi-reach
25
+ ```
26
+
27
+ Open Pi in the project you want to control. When the session starts, the Extension automatically connects to the configured Relay. Pair the browser device from Pi:
28
+
29
+ ```text
30
+ /pi-reach pair
31
+ ```
32
+
33
+ Scan the code in your deployed [Pi Reach PWA](../pwa/README.md) at `/app`, select the endpoint card, and send a prompt. Pairings are local to the computer that creates them:
34
+
35
+ ```text
36
+ /pi-reach devices
37
+ /pi-reach revoke <shortid>
38
+ ```
39
+
40
+ ## Commands
41
+
42
+ | Command | Description |
43
+ |---|---|
44
+ | `/pi-reach` | Connect the current Pi endpoint after it was stopped |
45
+ | `/pi-reach start` / `/pi-reach stop` | Connect or disconnect this endpoint |
46
+ | `/pi-reach status` | Show Relay, endpoint, runtime, and Owner state |
47
+ | `/pi-reach pair` | Show an endpoint-aware pairing QR |
48
+ | `/pi-reach devices` | List locally paired Owners |
49
+ | `/pi-reach revoke <shortid>` | Revoke one locally stored Owner |
50
+ | `/pi-reach set-relay <url>` | Persist the Relay URL |
51
+ | `/pi-reach config` | Show the resolved Relay URL |
52
+
53
+ The Extension handles remote `session_new` requests in-process through Pi's session API. There is no standalone `pi-reach` CLI, background process, scheduler, or service installation command.
54
+
55
+ ## Relay configuration
56
+
57
+ The effective Relay URL resolves in this order:
58
+
59
+ 1. `PI_REACH_RELAY`
60
+ 2. `~/.pi/pi-reach/config.json`
61
+ 3. `https://pi-reach-relay.yefengr.cn`
62
+
63
+ Set and inspect it from Pi:
64
+
65
+ ```text
66
+ /pi-reach set-relay https://relay.example.com
67
+ /pi-reach config
68
+ ```
69
+
70
+ Only `http://` and `https://` are accepted at the command boundary; WebSocket conversion happens inside the Extension. The Relay forwards opaque payloads and retains endpoint routing state in memory.
71
+
72
+ ## Pairing and security
73
+
74
+ - `device_id` is the Host Ed25519 public key in canonical Base64 form.
75
+ - Owner messages are trusted only through the Relay-injected `source_owner_id`.
76
+ - Pairing and revocation update the Relay endpoint ACL with `authorized_owner_ids`.
77
+ - Relay loss enters reconnecting state; the Extension does not restart Pi to recover.
78
+ - Device private keys, pairing tokens, encrypted payloads, and message bodies are not logged.
79
+ - Concurrent Pi processes coordinate device identity initialization through a local lock. If initialization is interrupted, follow the [identity storage and lock recovery rules](../docs/reference/protocol/pairing.md#host); do not delete identity or pairing data to retry.
80
+
81
+ ## Local state
82
+
83
+ Pi Reach stores global configuration, identity files, and pairings under `~/.pi/pi-reach`, and project display configuration under `.pi/pi-reach`. Its platform keyring service is `dev.pireach.pi`.
84
+
85
+ ## Development
86
+
87
+ Install dependencies from the repository root with `pnpm install --frozen-lockfile`.
88
+ The root workspace owns dependency catalogs, build approvals, and the lockfile. The private workspace package [`@pi-reach/protocol`](../packages/protocol/) provides the shared protocol build artifacts; the Extension uses it as a development dependency, while installed users receive vendored artifacts and need neither the workspace nor a separately published shared package. Its module and distribution boundary is defined in [ARCHITECTURE](../docs/ARCHITECTURE.md#工程与构建边界).
89
+
90
+ The root `prepare` script and an Extension `pnpm build` build the shared package first. After changing shared sources, run `pnpm --filter @pi-reach/protocol build` from the repository root before an Extension-only `typecheck` or `test`, or use the corresponding root command.
91
+
92
+ Run these commands from `pi-extension/`, or use the `@yefengr/pi-reach` package filter:
93
+
94
+ ```bash
95
+ pnpm typecheck
96
+ pnpm test
97
+ pnpm build
98
+ ```
99
+
100
+ The development toolchain is pinned by the root configuration. The published
101
+ extension retains its Node 20+ runtime requirement. TypeScript ESM imports must
102
+ use `.js` extensions. `prepack` builds before packaging; `pnpm pack` resolves
103
+ catalog and workspace references, and `publish-npm.sh` uploads the inspected tarball.
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,143 @@
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
+ /** Identity used when a full SDK model object is unavailable. */
44
+ export interface CurrentModelRef {
45
+ provider: string;
46
+ id: string;
47
+ }
48
+ /** Legacy labels remain accepted for callers that predate structured identity. */
49
+ export type CurrentModelHint = Model<any> | CurrentModelRef | string;
50
+ /**
51
+ * Minimal channel surface needed to reply. Mirrors `PlainPeerChannel`'s
52
+ * `.send` signature; tests pass an array-backed fake.
53
+ */
54
+ export interface ActionReplySender {
55
+ send(msg: ServerMessage): void;
56
+ }
57
+ /**
58
+ * Narrow shape of the `ExtensionAPI` surface action handlers actually
59
+ * call. Lets the test layer stub just these without rebuilding the full
60
+ * SDK type (which has 30+ methods we don't use here).
61
+ */
62
+ export interface ActionPi {
63
+ setModel(model: Model<any>): Promise<boolean>;
64
+ setThinkingLevel(level: import("../protocol/types.js").ThinkingLevel): void;
65
+ }
66
+ /**
67
+ * Narrow shape of the per-call context. Drawn from the union of
68
+ * `ExtensionContextActions` (compact, getModel) and
69
+ * `ExtensionCommandContextActions` (newSession), since index.ts caches
70
+ * the most-recent ctx and that's typically the command one.
71
+ *
72
+ * All fields are optional so a missing method (e.g. when only a plain
73
+ * `ExtensionContext` was seen) becomes a typed `action_error` instead of
74
+ * a runtime TypeError.
75
+ */
76
+ export interface ActionCtx {
77
+ compact?: (options?: object) => void;
78
+ /**
79
+ * Starts a new session. `withSession` is the SDK's blessed hook for
80
+ * post-replacement work: it receives a FRESH, command-capable ctx bound to
81
+ * the new session. The SDK marks any ctx captured BEFORE this call stale, so
82
+ * callers must re-capture via `withSession` rather than reuse the old ctx.
83
+ */
84
+ newSession?: (options?: {
85
+ withSession?: (ctx: ActionCtx) => Promise<void>;
86
+ }) => Promise<{
87
+ cancelled: boolean;
88
+ }>;
89
+ /** Pi 0.84.4 exposes the active model directly on ExtensionContext. */
90
+ model?: Model<any>;
91
+ /** Compatibility path for SDK contexts that expose the getter only. */
92
+ getModel?: () => Model<any> | undefined;
93
+ /**
94
+ * Models enabled for session cycling by `--models` or `enabledModels`.
95
+ * Empty means no explicit scope, so all available models remain usable.
96
+ */
97
+ scopedModels?: readonly {
98
+ model: Model<any>;
99
+ }[];
100
+ /**
101
+ * Live session registry from Pi's extension ctx. Includes providers/models
102
+ * registered dynamically via `pi.registerProvider(...)`, unlike the fallback
103
+ * disk-backed registry Pi Reach can build on its own.
104
+ */
105
+ modelRegistry?: ActionModelRegistry;
106
+ }
107
+ /**
108
+ * Minimal shape of the registry surface. Maps 1:1 onto `ModelRegistry`
109
+ * but lets tests fake catalogs without instantiating the real one.
110
+ */
111
+ export interface ActionModelRegistry {
112
+ refresh(): void;
113
+ getAvailable(): Model<any>[];
114
+ find(provider: string, modelId: string): Model<any> | undefined;
115
+ }
116
+ /** Project a SDK `Model<Api>` onto the wire schema. Shared by list_models
117
+ * and the `current` echo, so both stay in lockstep. */
118
+ export declare function wireFromModel(model: Model<any>): WireModel;
119
+ type SessionCompactMsg = Extract<ClientMessage, {
120
+ type: "session_compact";
121
+ }>;
122
+ type SessionNewMsg = Extract<ClientMessage, {
123
+ type: "session_new";
124
+ }>;
125
+ type ModelSetMsg = Extract<ClientMessage, {
126
+ type: "model_set";
127
+ }>;
128
+ type ThinkingSetMsg = Extract<ClientMessage, {
129
+ type: "thinking_set";
130
+ }>;
131
+ type ListModelsMsg = Extract<ClientMessage, {
132
+ type: "list_models";
133
+ }>;
134
+ export declare function handleSessionCompact(ctx: ActionCtx | null, sender: ActionReplySender, msg: SessionCompactMsg, onSettled?: () => void): void;
135
+ export declare function handleSessionNew(ctx: ActionCtx | null, sender: ActionReplySender, msg: SessionNewMsg, onReplaced?: (freshCtx: ActionCtx) => void): Promise<boolean>;
136
+ export declare function handleThinkingSet(pi: ActionPi, sender: ActionReplySender, msg: ThinkingSetMsg): void;
137
+ export declare function handleModelSet(pi: ActionPi, ctx: ActionCtx | null, reg: ActionModelRegistry, sender: ActionReplySender, msg: ModelSetMsg, onPersist?: (provider: string, modelId: string) => void, onModelSet?: (model: Model<any>) => void): Promise<void>;
138
+ export declare function getModelsList(ctx: ActionCtx | null, reg: ActionModelRegistry, currentModelHint?: CurrentModelHint): {
139
+ models: WireModel[];
140
+ current?: WireModel;
141
+ };
142
+ export declare function handleListModels(ctx: ActionCtx | null, reg: ActionModelRegistry, sender: ActionReplySender, msg: ListModelsMsg): void;
143
+ export {};
@@ -0,0 +1,200 @@
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, onSettled) {
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
+ ...(onSettled ? { onComplete: onSettled, onError: onSettled } : {}),
79
+ });
80
+ });
81
+ }
82
+ export async function handleSessionNew(ctx, sender, msg, onReplaced) {
83
+ // Returns true only when a fresh session was actually created. index.ts
84
+ // keys the Pi-side reset (clear _messageBuffer, restamp _sessionStartedAt,
85
+ // fan out an empty session_history) off this signal — a `cancelled`/errored
86
+ // new-session must NOT reset, so we return runAsync's success boolean.
87
+ return runAsync(sender, msg, "session_new", async () => {
88
+ if (!ctx?.newSession)
89
+ throw new Error("newSession unavailable (no command ctx yet)");
90
+ // newSession marks the caller's captured ctx (index.ts's `_lastCtx`) STALE
91
+ // — reusing it later throws "stale after session replacement" (the
92
+ // compact-after-New-session crash). `withSession` hands back a fresh,
93
+ // command-capable ctx bound to the new session; forward it via onReplaced
94
+ // so the caller re-captures and keeps later actions off the stale ctx.
95
+ const result = await ctx.newSession({
96
+ withSession: async (freshCtx) => { onReplaced?.(freshCtx); },
97
+ });
98
+ // `cancelled: true` happens when the SDK's hook chain vetoes the new
99
+ // session (e.g. an extension's `session_before_switch` returned a
100
+ // refusal). Surface as a typed error rather than silent success.
101
+ if (result.cancelled)
102
+ throw new Error("cancelled by extension hook");
103
+ });
104
+ }
105
+ export function handleThinkingSet(pi, sender, msg) {
106
+ runSync(sender, msg, "thinking_set", () => {
107
+ pi.setThinkingLevel(msg.level);
108
+ });
109
+ }
110
+ export async function handleModelSet(pi, ctx, reg, sender, msg, onPersist, onModelSet) {
111
+ await runAsync(sender, msg, "model_set", async () => {
112
+ // Prefer Pi's LIVE session registry when available so the app sees models
113
+ // registered dynamically by extensions via `pi.registerProvider(...)`.
114
+ // Fall back to Pi Reach's own disk-backed registry when no ctx exists.
115
+ const liveReg = ctx?.modelRegistry ?? reg;
116
+ // Refresh first so a model just-added via `/login` is visible.
117
+ liveReg.refresh();
118
+ const model = liveReg.find(msg.provider, msg.model_id);
119
+ if (!model) {
120
+ throw new Error(`model "${msg.provider}/${msg.model_id}" not in registry`);
121
+ }
122
+ const success = await pi.setModel(model);
123
+ if (!success)
124
+ throw new Error("no auth configured for this model");
125
+ // `pi.setModel` only sets the LIVE model — it does NOT persist. Without
126
+ // this, a model picked from the app reverts to the saved default on the
127
+ // next Pi restart (the TUI persists because AgentSession.setModel
128
+ // writes the default; this path doesn't). `onPersist` writes the new
129
+ // default so the app's choice survives. Best-effort — the caller's writer
130
+ // must not throw, so a failed settings write never fails the model change.
131
+ try {
132
+ onModelSet?.(model);
133
+ }
134
+ catch {
135
+ // State synchronization must not turn a successful model change into an error.
136
+ }
137
+ onPersist?.(model.provider, model.id);
138
+ });
139
+ }
140
+ function uniqueModelMatch(available, predicate) {
141
+ const matches = available.filter(predicate);
142
+ return matches.length === 1 ? matches[0] : undefined;
143
+ }
144
+ function isFullModel(value) {
145
+ if (typeof value === "string" || !value || typeof value !== "object")
146
+ return false;
147
+ const candidate = value;
148
+ return typeof candidate.id === "string"
149
+ && typeof candidate.name === "string"
150
+ && typeof candidate.provider === "string"
151
+ && typeof candidate.reasoning === "boolean"
152
+ && typeof candidate.contextWindow === "number";
153
+ }
154
+ function resolveCurrentModel(ctx, available, hint) {
155
+ // A structured hint is updated by model_select/model_set and must win over
156
+ // the session-start context, whose model property can be an older snapshot.
157
+ if (hint) {
158
+ if (typeof hint !== "string") {
159
+ if (isFullModel(hint))
160
+ return hint;
161
+ return uniqueModelMatch(available, (model) => model.provider === hint.provider && model.id === hint.id);
162
+ }
163
+ // String matching is retained only as a conservative legacy fallback.
164
+ return uniqueModelMatch(available, (model) => model.id === hint || model.name === hint);
165
+ }
166
+ // The property is the canonical Pi 0.84.4 source. Keep getModel() for older
167
+ // contexts and test fakes that expose only the legacy accessor.
168
+ return ctx?.model ?? ctx?.getModel?.();
169
+ }
170
+ export function getModelsList(ctx, reg, currentModelHint) {
171
+ // Prefer Pi's LIVE session registry when available so the app sees models
172
+ // registered dynamically by extensions via `pi.registerProvider(...)`.
173
+ // Fall back to Pi Reach's own disk-backed registry when no ctx exists.
174
+ const liveReg = ctx?.modelRegistry ?? reg;
175
+ liveReg.refresh();
176
+ const available = liveReg.getAvailable();
177
+ const scopedModels = ctx?.scopedModels;
178
+ const listedModels = scopedModels && scopedModels.length > 0
179
+ ? scopedModels.map(({ model }) => model)
180
+ : available;
181
+ const models = listedModels.map(wireFromModel);
182
+ const current = resolveCurrentModel(ctx, available, currentModelHint);
183
+ return { models, ...(current ? { current: wireFromModel(current) } : {}) };
184
+ }
185
+ export function handleListModels(ctx, reg, sender, msg) {
186
+ // refresh() can throw if `models.json` is malformed — wrap in try so the
187
+ // app gets an explicit error reply instead of a silent drop.
188
+ try {
189
+ sender.send({ type: "models_list", in_reply_to: msg.id, ...getModelsList(ctx, reg) });
190
+ }
191
+ catch (e) {
192
+ sender.send({
193
+ type: "error",
194
+ in_reply_to: msg.id,
195
+ code: "internal_error",
196
+ message: e instanceof Error ? e.message : String(e),
197
+ });
198
+ }
199
+ }
200
+ //# 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 while an Extension callback runs. 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 while an Extension callback runs. 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,35 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
+ import type { V2ActionFrame } from "../timeline/v2_service.js";
3
+ import { type ActionCtx, type ActionReplySender } from "./handlers.js";
4
+ type SessionNewFrame = Extract<V2ActionFrame, {
5
+ type: "session_new";
6
+ }>;
7
+ export interface SessionNewBridgeOptions {
8
+ getPi(): ExtensionAPI | null;
9
+ onCommandContext(ctx: ExtensionCommandContext): void;
10
+ onReplaced(ctx: ActionCtx): void;
11
+ onFinished(): void;
12
+ }
13
+ /** Bridges a remote action to Pi's command-only, process-local newSession API. */
14
+ export declare class SessionNewBridge {
15
+ private readonly options;
16
+ private readonly pending;
17
+ private timeoutMs;
18
+ private running;
19
+ private switchPermit;
20
+ constructor(options: SessionNewBridgeOptions);
21
+ dispatch(sender: ActionReplySender, frame: SessionNewFrame): void;
22
+ run(token: string, ctx: ExtensionCommandContext): Promise<void>;
23
+ beforeSwitch(reason: "new" | "resume"): {
24
+ cancel: true;
25
+ } | undefined;
26
+ beforeFork(): {
27
+ cancel: true;
28
+ } | undefined;
29
+ isRunning(): boolean;
30
+ clear(error?: string): void;
31
+ setTimeoutForTest(timeoutMs: number): void;
32
+ private fail;
33
+ private complete;
34
+ }
35
+ export {};