@geohar/unbien-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +72 -0
  2. package/dist/client.d.ts +89 -0
  3. package/dist/client.js +210 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/commands.d.ts +59 -0
  6. package/dist/commands.js +273 -0
  7. package/dist/commands.js.map +1 -0
  8. package/dist/connect.d.ts +2 -0
  9. package/dist/connect.js +475 -0
  10. package/dist/connect.js.map +1 -0
  11. package/dist/identity.d.ts +2 -0
  12. package/dist/identity.js +30 -0
  13. package/dist/identity.js.map +1 -0
  14. package/dist/invite.d.ts +10 -0
  15. package/dist/invite.js +28 -0
  16. package/dist/invite.js.map +1 -0
  17. package/dist/main.d.ts +3 -0
  18. package/dist/main.js +20 -0
  19. package/dist/main.js.map +1 -0
  20. package/dist/panels.d.ts +40 -0
  21. package/dist/panels.js +155 -0
  22. package/dist/panels.js.map +1 -0
  23. package/dist/picker.d.ts +6 -0
  24. package/dist/picker.js +95 -0
  25. package/dist/picker.js.map +1 -0
  26. package/dist/plan.d.ts +30 -0
  27. package/dist/plan.js +132 -0
  28. package/dist/plan.js.map +1 -0
  29. package/dist/reduce.d.ts +45 -0
  30. package/dist/reduce.js +123 -0
  31. package/dist/reduce.js.map +1 -0
  32. package/dist/render.d.ts +13 -0
  33. package/dist/render.js +91 -0
  34. package/dist/render.js.map +1 -0
  35. package/dist/replay.d.ts +2 -0
  36. package/dist/replay.js +57 -0
  37. package/dist/replay.js.map +1 -0
  38. package/dist/settings.d.ts +27 -0
  39. package/dist/settings.js +31 -0
  40. package/dist/settings.js.map +1 -0
  41. package/dist/store.d.ts +13 -0
  42. package/dist/store.js +20 -0
  43. package/dist/store.js.map +1 -0
  44. package/dist/theme.d.ts +7 -0
  45. package/dist/theme.js +110 -0
  46. package/dist/theme.js.map +1 -0
  47. package/dist/tui.d.ts +78 -0
  48. package/dist/tui.js +265 -0
  49. package/dist/tui.js.map +1 -0
  50. package/package.json +58 -0
package/README.md ADDED
@@ -0,0 +1,72 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/georgeharker/un-bien/main/app/icons/un-bien-macos-1024.png" alt="Un Bien" width="96" />
3
+ </p>
4
+
5
+ # unbien-cli
6
+
7
+ A terminal client for **remote [Pi coding agent](https://github.com/earendil-works/pi)
8
+ sessions**. Attach to a session running on another machine over an
9
+ [un-bien](https://github.com/georgeharker/un-bien) relay and drive it from your
10
+ shell — the same sessions the iOS/macOS app talks to.
11
+
12
+ It renders with **pi's own interactive components**, so a remote session looks
13
+ like a local one: CommonMark + syntax highlighting, native edit-diff / read /
14
+ bash tool cards, thinking blocks, and your configured pi theme (including
15
+ package-contributed ones such as `tokyo-night`).
16
+
17
+ ```bash
18
+ npm install -g @geohar/unbien-cli
19
+ ```
20
+
21
+ ## Use
22
+
23
+ Pair once per machine — run `/unbien pair` in a Pi session and copy the invite:
24
+
25
+ ```bash
26
+ unbien connect 'unbien://pair?t=…&epk=…'
27
+ ```
28
+
29
+ Pairing is machine-level, so later runs need no token:
30
+
31
+ ```bash
32
+ unbien connect --list # sessions on that machine
33
+ unbien connect # pick one interactively
34
+ unbien connect --session <id>
35
+ unbien connect --session-name un-bien
36
+ ```
37
+
38
+ `--relay` defaults to `$UNBIEN_RELAY`; a remembered machine uses the relay it
39
+ was paired on. `--theme <name>` overrides your pi theme, and `--list-themes`
40
+ shows what's available.
41
+
42
+ Type to prompt. Slash commands:
43
+
44
+ | | |
45
+ | -------------------------------------------------------------- | -------------------------------------------------------------------- |
46
+ | `/help` | list commands |
47
+ | `/tree` | browse turns — `f` forks a new session, `b` branches in place |
48
+ | `/fork`, `/branch` | same, by entry id |
49
+ | `/plan`, `/subagents` | pin a panel above the composer (`toggle`/`expand`/`collapse`/`hide`) |
50
+ | `/plan filter done`, `/plan filter context`, `/plan lines <n>` | what the plan shows |
51
+ | `/model`, `/thinking`, `/compact`, `/abort` | pi's own session verbs |
52
+ | `/set` | client settings (thinking stream, panel modes) |
53
+ | `/quit` | detach; the remote session keeps running |
54
+
55
+ Ctrl-C exits. Settings live in `~/.config/unbien-cli/settings.json`.
56
+
57
+ ## Offline rendering
58
+
59
+ `unbien replay <capture.jsonl>` renders a captured rpc-envelope stream — useful
60
+ for looking at a transcript, or for checking rendering without a live session.
61
+
62
+ ## How it relates to the app
63
+
64
+ The un-bien wire carries **byte-faithful pi rpc frames**, so this is a second
65
+ renderer over the stream the app already reads, not a second protocol. History
66
+ (`get_entries`) replays through the _same_ reducer as live frames.
67
+
68
+ Requires a paired Pi running the [`@geohar/un-bien`](https://www.npmjs.com/package/@geohar/un-bien)
69
+ extension and a relay you host. Machine administration (launcher daemon,
70
+ devices, revocation) lives in that package's `unbien-admin` CLI.
71
+
72
+ MIT © George Harker.
@@ -0,0 +1,89 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { type EnvelopeMessage, type Ed25519Keypair } from "@geohar/un-bien/client";
3
+ import type { PairingInvite } from "./invite.js";
4
+ /** One session's relay room, as announced by the pi that owns it. */
5
+ export interface RoomInfo {
6
+ room_id: string;
7
+ name?: string;
8
+ cwd?: string;
9
+ started_at?: number;
10
+ parent?: string;
11
+ sessionId?: string;
12
+ caps?: string[];
13
+ }
14
+ export interface SessionClientEvents {
15
+ /** An inner envelope frame ({rpc|evt|ub}) from the pi. */
16
+ envelope: [EnvelopeMessage];
17
+ /** A pre-attach stock frame — only `pair_ok` / `pair_error` arrive this way. */
18
+ control: [Record<string, unknown>];
19
+ /** A relay control frame (rooms / presence) — not routed to any pi. */
20
+ relayControl: [Record<string, unknown>];
21
+ close: [];
22
+ }
23
+ /**
24
+ * The owner end of a session channel: relay auth, pairing, then the envelope
25
+ * plane. The pi is addressed by its Ed25519 public key (`invite.epk`) — an
26
+ * opaque routing key that is echoed verbatim, never parsed.
27
+ */
28
+ export declare class SessionClient extends EventEmitter<SessionClientEvents> {
29
+ private readonly invite;
30
+ private readonly relay;
31
+ /** The room being driven. Mutable: one pairing reaches every room the pi
32
+ * owns, so picking a session is a routing change, not a re-pair. */
33
+ room: string;
34
+ constructor(relayUrl: string, keypair: Ed25519Keypair, invite: PairingInvite);
35
+ /** Snapshot the pi's currently-open rooms (one per live session). */
36
+ listRooms(timeoutMs?: number): Promise<RoomInfo[]>;
37
+ connect(): Promise<void>;
38
+ /** The one non-envelope frame a client ever sends — nothing exists before it. */
39
+ pair(deviceName: string): void;
40
+ /** Ask the extension to replay panels + pending UI for this session. */
41
+ requestSync(): void;
42
+ /**
43
+ * Pull the session's persisted transcript. This is pi's native history verb —
44
+ * `session_sync` covers panels and pending UI only, never the transcript.
45
+ */
46
+ /**
47
+ * Walk the whole transcript. `get_entries` is PAGED: each reply carries a
48
+ * page plus the `leafId` to continue from, and the walk ends on an empty
49
+ * page. A single call returns only the first page, which silently looks like
50
+ * a short history.
51
+ *
52
+ * Mirrors the app's walk, including its repeated-leaf circuit breaker: a
53
+ * non-empty page whose leaf did not advance would otherwise page forever.
54
+ */
55
+ getEntries(maxPages?: number): Promise<unknown[]>;
56
+ /**
57
+ * Issue a pi rpc command and await its correlated `response`. pi's verbs are
58
+ * used directly — an un-bien hop would only be a second name for the same
59
+ * thing.
60
+ */
61
+ request<T>(type: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T | undefined>;
62
+ /**
63
+ * un-bien's own plane, for the things pi has no same-process verb for.
64
+ *
65
+ * SAFETY: the exported `UbFrame` union is narrower than what the extension
66
+ * actually routes — `session_fork` / `session_navigate` are dispatched from
67
+ * `index.ts` but absent from the published type, which casts them the same
68
+ * way. Typing this parameter to the union would reject frames the receiver
69
+ * handles today.
70
+ */
71
+ sendUb(type: string, params?: Record<string, unknown>): void;
72
+ /** Drive the agent. `prompt` is pi's own rpc verb — no invented hop. */
73
+ prompt(message: string): void;
74
+ /** Interrupt a running turn with a steering message. */
75
+ steer(message: string): void;
76
+ abort(): void;
77
+ close(): void;
78
+ /**
79
+ * The single outbound choke, mirroring the extension's: stamp the plane's
80
+ * REAL wrapper kind plus a timestamp.
81
+ *
82
+ * The kind is not decoration. The extension's pre-attach listener drops any
83
+ * frame whose top-level `type` is not a string (relay_lifecycle.ts), so an
84
+ * unstamped envelope is silently ignored — it routes, arrives, and vanishes.
85
+ */
86
+ sendEnvelope(env: EnvelopeMessage): void;
87
+ private sendInner;
88
+ private onLine;
89
+ }
package/dist/client.js ADDED
@@ -0,0 +1,210 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { randomUUID } from "node:crypto";
3
+ import { EVT_KIND, RPC_KIND, UB_KIND, RelayClient, } from "@geohar/un-bien/client";
4
+ /** Config stores relay URLs as http(s); the socket needs ws(s). */
5
+ function toWsUrl(url) {
6
+ if (url.startsWith("http://"))
7
+ return `ws://${url.slice(7)}`;
8
+ if (url.startsWith("https://"))
9
+ return `wss://${url.slice(8)}`;
10
+ return url;
11
+ }
12
+ /**
13
+ * The owner end of a session channel: relay auth, pairing, then the envelope
14
+ * plane. The pi is addressed by its Ed25519 public key (`invite.epk`) — an
15
+ * opaque routing key that is echoed verbatim, never parsed.
16
+ */
17
+ export class SessionClient extends EventEmitter {
18
+ invite;
19
+ relay;
20
+ /** The room being driven. Mutable: one pairing reaches every room the pi
21
+ * owns, so picking a session is a routing change, not a re-pair. */
22
+ room;
23
+ constructor(relayUrl, keypair, invite) {
24
+ super();
25
+ this.invite = invite;
26
+ this.room = invite.roomId;
27
+ this.relay = new RelayClient(toWsUrl(relayUrl), keypair);
28
+ }
29
+ /** Snapshot the pi's currently-open rooms (one per live session). */
30
+ listRooms(timeoutMs = 5_000) {
31
+ return new Promise((resolve) => {
32
+ const timer = setTimeout(() => {
33
+ this.off("relayControl", onControl);
34
+ resolve([]);
35
+ }, timeoutMs);
36
+ const onControl = (frame) => {
37
+ if (frame.type !== "rooms" || frame.peer !== this.invite.epk)
38
+ return;
39
+ clearTimeout(timer);
40
+ this.off("relayControl", onControl);
41
+ resolve(Array.isArray(frame.rooms) ? frame.rooms : []);
42
+ };
43
+ this.on("relayControl", onControl);
44
+ this.relay.send(JSON.stringify({ type: "rooms_check", peers: [this.invite.epk] }));
45
+ });
46
+ }
47
+ async connect() {
48
+ this.relay.on("message", (line) => this.onLine(line));
49
+ this.relay.on("close", () => this.emit("close"));
50
+ // No room_id: an owner subscribes to rooms, it does not own one.
51
+ await this.relay.connect();
52
+ }
53
+ /** The one non-envelope frame a client ever sends — nothing exists before it. */
54
+ pair(deviceName) {
55
+ this.sendInner({
56
+ type: "pair_request",
57
+ id: randomUUID(),
58
+ token: this.invite.token,
59
+ device_name: deviceName,
60
+ });
61
+ }
62
+ /** Ask the extension to replay panels + pending UI for this session. */
63
+ requestSync() {
64
+ this.sendEnvelope({ ub: { type: "session_sync", id: randomUUID() } });
65
+ }
66
+ /**
67
+ * Pull the session's persisted transcript. This is pi's native history verb —
68
+ * `session_sync` covers panels and pending UI only, never the transcript.
69
+ */
70
+ /**
71
+ * Walk the whole transcript. `get_entries` is PAGED: each reply carries a
72
+ * page plus the `leafId` to continue from, and the walk ends on an empty
73
+ * page. A single call returns only the first page, which silently looks like
74
+ * a short history.
75
+ *
76
+ * Mirrors the app's walk, including its repeated-leaf circuit breaker: a
77
+ * non-empty page whose leaf did not advance would otherwise page forever.
78
+ */
79
+ async getEntries(maxPages = 200) {
80
+ const all = [];
81
+ let since;
82
+ for (let page = 0; page < maxPages; page++) {
83
+ let data;
84
+ try {
85
+ data = await this.request("get_entries", since ? { since } : {});
86
+ }
87
+ catch {
88
+ break; // timeout or error: keep whatever we already walked
89
+ }
90
+ const entries = data?.entries ?? [];
91
+ const leaf = data?.leafId;
92
+ if (entries.length === 0 || !leaf)
93
+ break;
94
+ all.push(...entries);
95
+ if (leaf === since)
96
+ break; // leaf did not advance — the walk is spinning
97
+ since = leaf;
98
+ }
99
+ return all;
100
+ }
101
+ /**
102
+ * Issue a pi rpc command and await its correlated `response`. pi's verbs are
103
+ * used directly — an un-bien hop would only be a second name for the same
104
+ * thing.
105
+ */
106
+ request(type, params = {}, timeoutMs = 15_000) {
107
+ const id = randomUUID();
108
+ return new Promise((resolve, reject) => {
109
+ const timer = setTimeout(() => {
110
+ this.off("envelope", onEnvelope);
111
+ reject(new Error(`${type}: timed out`));
112
+ }, timeoutMs);
113
+ const onEnvelope = (env) => {
114
+ const rpc = env.rpc;
115
+ if (typeof rpc !== "object" || rpc === null)
116
+ return;
117
+ const frame = rpc;
118
+ if (frame.type !== "response" || frame.id !== id)
119
+ return;
120
+ clearTimeout(timer);
121
+ this.off("envelope", onEnvelope);
122
+ if (frame.success === false) {
123
+ reject(new Error(String(frame.error ?? `${type} failed`)));
124
+ return;
125
+ }
126
+ resolve(frame.data);
127
+ };
128
+ this.on("envelope", onEnvelope);
129
+ this.sendEnvelope({ rpc: { type, id, ...params } });
130
+ });
131
+ }
132
+ /**
133
+ * un-bien's own plane, for the things pi has no same-process verb for.
134
+ *
135
+ * SAFETY: the exported `UbFrame` union is narrower than what the extension
136
+ * actually routes — `session_fork` / `session_navigate` are dispatched from
137
+ * `index.ts` but absent from the published type, which casts them the same
138
+ * way. Typing this parameter to the union would reject frames the receiver
139
+ * handles today.
140
+ */
141
+ sendUb(type, params = {}) {
142
+ const frame = { type, id: randomUUID(), ...params };
143
+ this.sendEnvelope({ ub: frame });
144
+ }
145
+ /** Drive the agent. `prompt` is pi's own rpc verb — no invented hop. */
146
+ prompt(message) {
147
+ this.sendEnvelope({
148
+ rpc: { type: "prompt", id: randomUUID(), message },
149
+ });
150
+ }
151
+ /** Interrupt a running turn with a steering message. */
152
+ steer(message) {
153
+ this.sendEnvelope({ rpc: { type: "steer", id: randomUUID(), message } });
154
+ }
155
+ abort() {
156
+ this.sendEnvelope({ rpc: { type: "abort", id: randomUUID() } });
157
+ }
158
+ close() {
159
+ this.relay.close();
160
+ }
161
+ /**
162
+ * The single outbound choke, mirroring the extension's: stamp the plane's
163
+ * REAL wrapper kind plus a timestamp.
164
+ *
165
+ * The kind is not decoration. The extension's pre-attach listener drops any
166
+ * frame whose top-level `type` is not a string (relay_lifecycle.ts), so an
167
+ * unstamped envelope is silently ignored — it routes, arrives, and vanishes.
168
+ */
169
+ sendEnvelope(env) {
170
+ const kind = env.rpc ? RPC_KIND : env.evt ? EVT_KIND : UB_KIND;
171
+ this.sendInner({ type: kind, ...env, ts: Date.now() });
172
+ }
173
+ sendInner(inner) {
174
+ const outer = {
175
+ peer: this.invite.epk,
176
+ room: this.room,
177
+ ct: Buffer.from(JSON.stringify(inner)).toString("base64"),
178
+ };
179
+ this.relay.send(JSON.stringify(outer));
180
+ }
181
+ onLine(line) {
182
+ let frame;
183
+ try {
184
+ frame = JSON.parse(line);
185
+ }
186
+ catch {
187
+ return;
188
+ }
189
+ // Relay control frames (rooms/presence) carry no `ct` — they are addressed
190
+ // to us, not routed through a pi.
191
+ const ct = frame.ct;
192
+ if (typeof ct !== "string") {
193
+ this.emit("relayControl", frame);
194
+ return;
195
+ }
196
+ let inner;
197
+ try {
198
+ inner = JSON.parse(Buffer.from(ct, "base64").toString("utf8"));
199
+ }
200
+ catch {
201
+ return;
202
+ }
203
+ if ("rpc" in inner || "evt" in inner || "ub" in inner) {
204
+ this.emit("envelope", inner);
205
+ return;
206
+ }
207
+ this.emit("control", inner);
208
+ }
209
+ }
210
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Slash commands. Where pi has a first-class rpc verb we issue that verb and pi
3
+ * acts; only fork/branch go over un-bien's own plane, because they need a pi
4
+ * command context that the rpc dispatch path doesn't carry.
5
+ */
6
+ import { SelectList, type SelectItem } from "@earendil-works/pi-tui";
7
+ import type { SessionClient } from "./client.js";
8
+ import type { CliSettings } from "./settings.js";
9
+ import type { PanelStore } from "./panels.js";
10
+ /** A session log entry, as `get_entries` returns it. */
11
+ export interface SessionEntry {
12
+ id?: string;
13
+ parentId?: string | null;
14
+ type?: string;
15
+ message?: {
16
+ role?: string;
17
+ content?: Array<{
18
+ type?: string;
19
+ text?: string;
20
+ }>;
21
+ };
22
+ }
23
+ export interface CommandContext {
24
+ client: SessionClient;
25
+ panels: PanelStore;
26
+ /**
27
+ * The walked session log. The extension bridges only a subset of pi's rpc
28
+ * verbs (`rpc_inbound.ts`) — `get_tree` and `get_fork_messages` are NOT
29
+ * among them — but entries carry `id`/`parentId`, so the tree and the fork
30
+ * points are derived here instead of asking for them.
31
+ */
32
+ entries: () => readonly SessionEntry[];
33
+ settings: CliSettings;
34
+ saveSettings: () => void;
35
+ settingsPath: string;
36
+ /** Leave the client. The remote session keeps running. */
37
+ quit: () => void;
38
+ /** Print lines into the transcript area. */
39
+ print: (lines: string[]) => void;
40
+ /** Offer a highlighted chooser; resolves to the picked value or null. */
41
+ choose: (title: string, items: SelectItem[]) => Promise<SelectItem | null> | SelectItem | null;
42
+ /** Chooser whose rows carry per-key actions (fork vs branch). */
43
+ chooseAction: (title: string, items: SelectItem[], hint: string, keys: readonly string[]) => Promise<{
44
+ item: SelectItem;
45
+ action: string;
46
+ } | null>;
47
+ }
48
+ export interface Command {
49
+ name: string;
50
+ summary: string;
51
+ run: (ctx: CommandContext, args: string) => Promise<void> | void;
52
+ }
53
+ export declare const COMMANDS: Command[];
54
+ export declare function findCommand(input: string): {
55
+ command: Command;
56
+ args: string;
57
+ } | null;
58
+ /** Chooser used when no TUI is available: prints a numbered list instead. */
59
+ export declare function selectListFor(items: SelectItem[], maxVisible?: number): SelectList;