@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.
- package/LICENSE +21 -0
- package/README.md +106 -2
- package/dist/actions/handlers.d.ts +143 -0
- package/dist/actions/handlers.js +200 -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/actions/session_new_bridge.d.ts +35 -0
- package/dist/actions/session_new_bridge.js +88 -0
- package/dist/actions/session_new_bridge.js.map +1 -0
- package/dist/commands.d.ts +29 -0
- package/dist/commands.js +169 -0
- package/dist/commands.js.map +1 -0
- package/dist/config.d.ts +61 -0
- package/dist/config.js +115 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +618 -0
- package/dist/index.js.map +1 -0
- package/dist/pairing/crypto.d.ts +15 -0
- package/dist/pairing/crypto.js +72 -0
- package/dist/pairing/crypto.js.map +1 -0
- package/dist/pairing/identity-keyring.d.ts +22 -0
- package/dist/pairing/identity-keyring.js +94 -0
- package/dist/pairing/identity-keyring.js.map +1 -0
- package/dist/pairing/identity-lock.d.ts +14 -0
- package/dist/pairing/identity-lock.js +116 -0
- package/dist/pairing/identity-lock.js.map +1 -0
- package/dist/pairing/owner_storage.d.ts +61 -0
- package/dist/pairing/owner_storage.js +263 -0
- package/dist/pairing/owner_storage.js.map +1 -0
- package/dist/pairing/qr.d.ts +39 -0
- package/dist/pairing/qr.js +113 -0
- package/dist/pairing/qr.js.map +1 -0
- package/dist/pairing/storage.d.ts +36 -0
- package/dist/pairing/storage.js +263 -0
- package/dist/pairing/storage.js.map +1 -0
- package/dist/protocol/types.d.ts +225 -0
- package/dist/protocol/types.js +2 -0
- package/dist/protocol/types.js.map +1 -0
- package/dist/protocol/v2/codec.d.ts +6 -0
- package/dist/protocol/v2/codec.js +4 -0
- package/dist/protocol/v2/codec.js.map +1 -0
- package/dist/protocol/v2/index.d.ts +4 -0
- package/dist/protocol/v2/index.js +5 -0
- package/dist/protocol/v2/index.js.map +1 -0
- package/dist/protocol/v2/marker.d.ts +45 -0
- package/dist/protocol/v2/marker.js +47 -0
- package/dist/protocol/v2/marker.js.map +1 -0
- package/dist/protocol/v2/schemas.d.ts +3 -0
- package/dist/protocol/v2/schemas.js +4 -0
- package/dist/protocol/v2/schemas.js.map +1 -0
- package/dist/protocol/v2/session_state.d.ts +79 -0
- package/dist/protocol/v2/session_state.js +166 -0
- package/dist/protocol/v2/session_state.js.map +1 -0
- package/dist/runtime/endpoint_metadata.d.ts +14 -0
- package/dist/runtime/endpoint_metadata.js +18 -0
- package/dist/runtime/endpoint_metadata.js.map +1 -0
- package/dist/runtime/extension_owner.d.ts +12 -0
- package/dist/runtime/extension_owner.js +80 -0
- package/dist/runtime/extension_owner.js.map +1 -0
- package/dist/runtime/footer.d.ts +6 -0
- package/dist/runtime/footer.js +21 -0
- package/dist/runtime/footer.js.map +1 -0
- package/dist/runtime/owner_router.d.ts +16 -0
- package/dist/runtime/owner_router.js +41 -0
- package/dist/runtime/owner_router.js.map +1 -0
- package/dist/runtime/pairing_coordinator.d.ts +42 -0
- package/dist/runtime/pairing_coordinator.js +136 -0
- package/dist/runtime/pairing_coordinator.js.map +1 -0
- package/dist/runtime/relay_lifecycle.d.ts +64 -0
- package/dist/runtime/relay_lifecycle.js +275 -0
- package/dist/runtime/relay_lifecycle.js.map +1 -0
- package/dist/session/local_config.d.ts +7 -0
- package/dist/session/local_config.js +52 -0
- package/dist/session/local_config.js.map +1 -0
- package/dist/timeline/history.d.ts +30 -0
- package/dist/timeline/history.js +170 -0
- package/dist/timeline/history.js.map +1 -0
- package/dist/timeline/partial_delta.d.ts +2 -0
- package/dist/timeline/partial_delta.js +16 -0
- package/dist/timeline/partial_delta.js.map +1 -0
- package/dist/timeline/run_end.d.ts +16 -0
- package/dist/timeline/run_end.js +23 -0
- package/dist/timeline/run_end.js.map +1 -0
- package/dist/timeline/runtime.d.ts +89 -0
- package/dist/timeline/runtime.js +546 -0
- package/dist/timeline/runtime.js.map +1 -0
- package/dist/timeline/sequence.d.ts +7 -0
- package/dist/timeline/sequence.js +16 -0
- package/dist/timeline/sequence.js.map +1 -0
- package/dist/timeline/tool_lifecycle.d.ts +36 -0
- package/dist/timeline/tool_lifecycle.js +171 -0
- package/dist/timeline/tool_lifecycle.js.map +1 -0
- package/dist/timeline/user_delivery.d.ts +96 -0
- package/dist/timeline/user_delivery.js +360 -0
- package/dist/timeline/user_delivery.js.map +1 -0
- package/dist/timeline/user_delivery_binding.d.ts +43 -0
- package/dist/timeline/user_delivery_binding.js +107 -0
- package/dist/timeline/user_delivery_binding.js.map +1 -0
- package/dist/timeline/v2_service.d.ts +82 -0
- package/dist/timeline/v2_service.js +512 -0
- package/dist/timeline/v2_service.js.map +1 -0
- package/dist/transport/peer_channel.d.ts +34 -0
- package/dist/transport/peer_channel.js +116 -0
- package/dist/transport/peer_channel.js.map +1 -0
- package/dist/transport/relay_client.d.ts +54 -0
- package/dist/transport/relay_client.js +270 -0
- package/dist/transport/relay_client.js.map +1 -0
- package/dist/vendor/protocol/constants.d.ts +4 -0
- package/dist/vendor/protocol/constants.js +4 -0
- package/dist/vendor/protocol/encoding.d.ts +9 -0
- package/dist/vendor/protocol/encoding.js +66 -0
- package/dist/vendor/protocol/outer/codec.d.ts +13 -0
- package/dist/vendor/protocol/outer/codec.js +105 -0
- package/dist/vendor/protocol/outer/index.d.ts +4 -0
- package/dist/vendor/protocol/outer/index.js +4 -0
- package/dist/vendor/protocol/outer/types.d.ts +120 -0
- package/dist/vendor/protocol/outer/types.js +1 -0
- package/dist/vendor/protocol/session/codec.d.ts +41 -0
- package/dist/vendor/protocol/session/codec.js +236 -0
- package/dist/vendor/protocol/session/frames.d.ts +5220 -0
- package/dist/vendor/protocol/session/frames.js +83 -0
- package/dist/vendor/protocol/session/index.d.ts +3 -0
- package/dist/vendor/protocol/session/index.js +3 -0
- package/dist/vendor/protocol/session/schema.d.ts +858 -0
- package/dist/vendor/protocol/session/schema.js +256 -0
- 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
|
-
|
|
1
|
+
<h1 align="center">Pi Reach</h1>
|
|
2
2
|
|
|
3
|
-
|
|
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 {};
|