@pipod/cli 0.1.20 → 0.1.22

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.
@@ -0,0 +1,41 @@
1
+ // src/client/runtime/key-encoding-guard.ts
2
+ import { decodeKittyPrintable, isKeyRelease } from "@earendil-works/pi-tui";
3
+ var CONTROL_DELETE = 127;
4
+ var DUPLICATE_WINDOW_MS = 50;
5
+ function rawTextChar(data) {
6
+ if (data.length === 0 || data.includes("\x1B")) return void 0;
7
+ if ([...data].length !== 1) return void 0;
8
+ const codepoint = data.codePointAt(0);
9
+ if (codepoint < 32 || codepoint === CONTROL_DELETE) return void 0;
10
+ return data;
11
+ }
12
+ function encodingGuardInput(state, data, now) {
13
+ if (isKeyRelease(data)) {
14
+ state.pending = void 0;
15
+ return void 0;
16
+ }
17
+ const char = rawTextChar(data);
18
+ if (char !== void 0) {
19
+ const pending = state.pending;
20
+ state.pending = void 0;
21
+ if (pending !== void 0 && pending.char === char && now - pending.at <= DUPLICATE_WINDOW_MS) {
22
+ return { consume: true };
23
+ }
24
+ return void 0;
25
+ }
26
+ const printable = decodeKittyPrintable(data);
27
+ state.pending = printable === void 0 ? void 0 : { char: printable, at: now };
28
+ return void 0;
29
+ }
30
+ function wireKeyEncodingGuard(ui) {
31
+ const subscribe = ui?.onTerminalInput;
32
+ if (!subscribe) return () => {
33
+ };
34
+ const state = { pending: void 0 };
35
+ return subscribe((data) => encodingGuardInput(state, data, Date.now()));
36
+ }
37
+ export {
38
+ encodingGuardInput,
39
+ wireKeyEncodingGuard
40
+ };
41
+ //# sourceMappingURL=key-encoding-guard-6UOKKTJQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/runtime/key-encoding-guard.ts"],"sourcesContent":["/**\n * src/client/runtime/key-encoding-guard.ts — one press, one character, whichever encodings the\n * terminal reports it in.\n *\n * A terminal with the kitty keyboard protocol enabled describes a press on two paths: the key\n * event as a `CSI-u` sequence, and the text the key produced as raw bytes. Normally it uses one\n * or the other per press, and nothing here fires. Some terminals emit *both* for the same press\n * — the case upstream pi fixed for one shape and order in 0.70.4 (#3780, \"duplicate printable\n * characters from Kitty keyboard protocol CSI-u plus raw character input on layouts such as\n * Italian\"). pi-tui guards against it in `StdinBuffer.emitDataSequence`, which remembers the\n * codepoint of the last CSI-u printable and drops a raw character that repeats it.\n *\n * That recogniser is `parseUnmodifiedKittyPrintableCodepoint`, and it accepts only the bare\n * forms — `ESC[<cp>u`, `ESC[<cp>:<shifted>u`, `ESC[<cp>::<base>u`. Every form carrying a\n * *modifier parameter* is refused, while `decodeKittyPrintable` accepts those (it masks Caps\n * Lock and Num Lock: `KITTY_PRINTABLE_ALLOWED_MODIFIERS = shift | 64 | 128`). So the dedupe\n * silently stops working the moment the terminal puts a lock bit or a layout alternate in that\n * parameter — a Num Lock that is on by default, or the AltGr characters of a non-US layout —\n * and both copies insert: one press, two characters.\n *\n * The guard is fail-open by construction. A duplicate that escapes costs one character; a\n * keystroke spent on a wrong suppression costs the user's intent, and the second is the only\n * failure here that is not instantly visible and recoverable. So it drops a raw character only\n * when all of these hold: a `CSI-u` printable for exactly that character arrived immediately\n * before it; it is the one drop that `CSI-u` is owed (a second matching character is a second\n * press); and it lands within {@link DUPLICATE_WINDOW_MS}, which the two encodings of a single\n * press always meet because the terminal writes them together. A release rearms nothing: it\n * clears the memory instead, so the release event that follows a press can never make the next\n * deliberate press of the same key disappear.\n *\n * Reads and repeats are left alone on purpose. `key-repeat-guard.ts` owns repeats, and a\n * terminal that reports repeats as CSI-u event-type 2 is only deduped against its own text.\n */\n\nimport { decodeKittyPrintable, isKeyRelease } from \"@earendil-works/pi-tui\";\nimport type { HostUiContext, TerminalInputHandler } from \"./bridge.js\";\n\nconst CONTROL_DELETE = 0x7f;\n/**\n * How long the raw half of a dual-encoded press may trail its CSI-u half. Both halves are\n * written by the terminal as one keystroke, so they arrive within a read or two; the window\n * exists only to bound the damage of a terminal that mixes encodings per press — the one way a\n * deliberate press could sit close enough behind a CSI-u to be mistaken for its text.\n */\nconst DUPLICATE_WINDOW_MS = 50;\n\ninterface EncodingState {\n /** The printable a CSI-u just delivered, and when — still owed exactly one raw drop. */\n pending: { char: string; at: number } | undefined;\n}\n\n/** The single printable character this chunk carries as plain text, if it is that. */\nfunction rawTextChar(data: string): string | undefined {\n // An escape sequence is not text: the CSI-u half of the pair arrives in this shape.\n if (data.length === 0 || data.includes(\"\\x1b\")) return undefined;\n if ([...data].length !== 1) return undefined;\n const codepoint = data.codePointAt(0)!;\n if (codepoint < 32 || codepoint === CONTROL_DELETE) return undefined;\n return data;\n}\n\n/**\n * One terminal-input step ahead of the focused component. Consumes only the second encoding of\n * a press the terminal already reported as a key event.\n */\nexport function encodingGuardInput(\n state: EncodingState,\n data: string,\n now: number,\n): { consume: true } | undefined {\n // A release is not a press: it must never arm a drop, or the key's next press would vanish.\n if (isKeyRelease(data)) {\n state.pending = undefined;\n return undefined;\n }\n\n const char = rawTextChar(data);\n if (char !== undefined) {\n const pending = state.pending;\n state.pending = undefined;\n if (pending !== undefined && pending.char === char && now - pending.at <= DUPLICATE_WINDOW_MS) {\n return { consume: true };\n }\n return undefined;\n }\n\n // `decodeKittyPrintable` is the editor's own reading of a key event, including the modifier\n // combinations the stock dedupe refuses — which is exactly the gap this guard fills.\n const printable = decodeKittyPrintable(data);\n state.pending = printable === undefined ? undefined : { char: printable, at: now };\n return undefined;\n}\n\n/**\n * Wire the guard into the bound TUI. Returns an unsubscribe; call it before re-wiring, since\n * `clearExtensionTerminalInputListeners()` drops the listener on every session rebind.\n *\n * Registered ahead of the repeat guard: the repeat guard consumes CSI-u repeats, and this one\n * needs to see a repeat's key event to recognise the text copy that follows it.\n */\nexport function wireKeyEncodingGuard(ui: HostUiContext | null | undefined): () => void {\n const subscribe: ((handler: TerminalInputHandler) => () => void) | undefined = ui?.onTerminalInput;\n if (!subscribe) return () => {};\n const state: EncodingState = { pending: undefined };\n return subscribe((data) => encodingGuardInput(state, data, Date.now()));\n}\n"],"mappings":";AAkCA,SAAS,sBAAsB,oBAAoB;AAGnD,IAAM,iBAAiB;AAOvB,IAAM,sBAAsB;AAQ5B,SAAS,YAAY,MAAkC;AAErD,MAAI,KAAK,WAAW,KAAK,KAAK,SAAS,MAAM,EAAG,QAAO;AACvD,MAAI,CAAC,GAAG,IAAI,EAAE,WAAW,EAAG,QAAO;AACnC,QAAM,YAAY,KAAK,YAAY,CAAC;AACpC,MAAI,YAAY,MAAM,cAAc,eAAgB,QAAO;AAC3D,SAAO;AACT;AAMO,SAAS,mBACd,OACA,MACA,KAC+B;AAE/B,MAAI,aAAa,IAAI,GAAG;AACtB,UAAM,UAAU;AAChB,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,YAAY,IAAI;AAC7B,MAAI,SAAS,QAAW;AACtB,UAAM,UAAU,MAAM;AACtB,UAAM,UAAU;AAChB,QAAI,YAAY,UAAa,QAAQ,SAAS,QAAQ,MAAM,QAAQ,MAAM,qBAAqB;AAC7F,aAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AACA,WAAO;AAAA,EACT;AAIA,QAAM,YAAY,qBAAqB,IAAI;AAC3C,QAAM,UAAU,cAAc,SAAY,SAAY,EAAE,MAAM,WAAW,IAAI,IAAI;AACjF,SAAO;AACT;AASO,SAAS,qBAAqB,IAAkD;AACrF,QAAM,YAAyE,IAAI;AACnF,MAAI,CAAC,UAAW,QAAO,MAAM;AAAA,EAAC;AAC9B,QAAM,QAAuB,EAAE,SAAS,OAAU;AAClD,SAAO,UAAU,CAAC,SAAS,mBAAmB,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC;AACxE;","names":[]}
@@ -1,9 +1,9 @@
1
1
  # Keyboard input in the attached TUI
2
2
 
3
- Two input defects turn one key press into several characters or into escape-sequence
4
- gibberish. Both live in the local byte path — nothing typed crosses the wire — and both are
5
- fixed in `src/client/runtime/` rather than by pinning a different pi-tui, because the pinned
6
- version is what pods and the launcher share.
3
+ Three input defects turn one key press into several characters, the same character twice, or
4
+ into escape-sequence gibberish. All live in the local byte path — nothing typed crosses the
5
+ wire — and all are fixed in `src/client/runtime/` rather than by pinning a different pi-tui,
6
+ because the pinned version is what pods and the launcher share.
7
7
 
8
8
  ## The path
9
9
 
@@ -11,6 +11,7 @@ version is what pods and the launcher share.
11
11
  terminal ──bytes──▶ ProcessTerminal ──▶ StdinBuffer ──▶ TUI input listeners ──▶ Editor
12
12
  │
13
13
  chords · disconnected-quit ·
14
+ key-encoding guard ·
14
15
  key-repeat guard
15
16
  ```
16
17
 
@@ -69,6 +70,34 @@ reply may be missed and the theme falls back, which is cosmetic; the prompt stay
69
70
  Upstream pi-tui 1.4.0 still flushes these fragments (it only rescues recognizable terminal
70
71
  *replies*), so a version bump would not fix this.
71
72
 
73
+ ## 3. A press reported in both encodings types once (`key-encoding-guard.ts`)
74
+
75
+ A terminal with the kitty keyboard protocol enabled may describe one press twice: the key
76
+ event as a `CSI-u` sequence **and** the text it produced as raw bytes. pi-tui already guards
77
+ this in `StdinBuffer.emitDataSequence` — it remembers the codepoint of the last CSI-u
78
+ printable and drops a raw character that repeats it — but its recogniser
79
+ (`parseUnmodifiedKittyPrintableCodepoint`) accepts only `ESC[<cp>u`, `ESC[<cp>:<shifted>u` and
80
+ `ESC[<cp>::<base>u`, and refuses every form carrying a *modifier parameter*. `decodeKittyPrintable`
81
+ accepts those, masking Caps Lock and Num Lock (`KITTY_PRINTABLE_ALLOWED_MODIFIERS = shift | 64
82
+ | 128`). So the stock dedupe stops working the moment the terminal puts a modifier bit in that
83
+ parameter — measured here with the lock bits a machine carries without anyone noticing, a Num
84
+ Lock that is on by default (`input:numlock_by_default = true`) or a Caps Lock — and both copies
85
+ insert. Upstream pi fixed the same class of report for one terminal order and shape in
86
+ 0.70.4 (#3780, "duplicate printable characters from Kitty keyboard protocol CSI-u plus raw
87
+ character input on layouts such as Italian"); the pinned pi-tui still has the gap.
88
+
89
+ The guard consumes a raw character only when a CSI-u printable for exactly that character
90
+ arrived immediately before it, only once per CSI-u, and only within 50 ms — the two halves of
91
+ one press are written together, so they always meet that window. It is fail-open by
92
+ construction: an escaped duplicate costs one visible, recoverable character, while a wrong
93
+ suppression would silently eat a keystroke. A release clears the memory rather than arming it,
94
+ so the release event that follows a press can never make the next deliberate press of that key
95
+ disappear. Releases and repeats otherwise stay with `key-repeat-guard.ts`, which owns them.
96
+
97
+ Left open deliberately: the reversed order (raw text first, then the CSI-u). No observed
98
+ terminal writes it, and deduping it would mean dropping a key *event* on suspicion — the one
99
+ direction where a mistake loses input.
100
+
72
101
  ## Accepted rows
73
102
 
74
103
  Measured with a pty harness against pi 1.0.2 + pi-tui 1.0.2 (the pinned pair), driving
@@ -81,6 +110,14 @@ inside pi's own `InteractiveMode`:
81
110
  | release split past the flush (`\x1b[104;1:` … `3u`) | `h3u` | `h` |
82
111
  | press split past the flush | `;1:1u` | that key |
83
112
  | OSC reply fragment, then tap `j` | keystroke lost | `j` |
113
+ | Num Lock bit on the CSI-u (`\x1b[97;129u` then `a`) | `aa` | `a` |
114
+ | layout base + Num Lock (`\x1b[97:97:97;129u` then `a`) | `aa` | `a` |
115
+ | symbol press with Num Lock (`\x1b[64;129u` then `@`) | `@@` | `@` |
116
+ | Caps Lock bit (`\x1b[65;65u` then `A`) | `AA` | `A` |
117
+ | both halves in one read (`\x1b[104;129uh`) | `hh` | `h` |
118
+ | stock dedupe already covers it: `\x1b[64:64:50u` then `@` | `@` | `@` |
119
+ | CSI-u half lost, same key 200 ms later | `aa` | `aa` |
120
+ | release between the halves, then that key again | press kept | unchanged |
84
121
 
85
122
  Everything else must stay exactly as it was, and does: a tap inserts one character, two
86
123
  deliberate taps of the same key insert two, held Backspace and arrows still repeat, a
@@ -88,4 +125,4 @@ bracketed paste arrives verbatim, a legacy terminal without releases is untouche
88
125
  stray fragment (`\x1b[1` followed later by another key) costs neither keystroke.
89
126
 
90
127
  `PI_TUI_ESC_TIMEOUT` only widens the lone-ESC window, not the 50 ms sequence window, so it is
91
- not a workaround for either defect.
128
+ not a workaround for any of these defects.
@@ -3,7 +3,8 @@
3
3
  `pipod ls` (also `pipod list`) reads every page before applying display filters or
4
4
  rendering the oldest-activity-first tree. Quiet output contains each full pod ID
5
5
  once. Project, template, account and machine-group scopes use the same page walk;
6
- so do short-ref/name resolution and bulk archive/restore/GC selection.
6
+ so does bulk archive/restore/GC selection. Short-ref/name resolution uses the
7
+ bounded lookup below; only an older server without that route uses pagination.
7
8
 
8
9
  ## API contract
9
10
 
@@ -29,6 +30,32 @@ pod ahead of an already-read boundary. The CLI deduplicates IDs if it sees a pod
29
30
  again and fails explicitly on a repeated cursor rather than reporting a silently
30
31
  truncated roster. Rerun a listing after concurrent activity for a fresh view.
31
32
 
33
+ ## Pod-reference lookup
34
+
35
+ `GET /v1/pods/by-ref/:ref` resolves a short reference or exact name within the
36
+ caller's own pods, including archived pods but excluding gone rows. Pod tokens
37
+ are additionally limited to their own subtree. Matching uses the same shared
38
+ parser as the CLI: case-sensitive exact names precede hexadecimal references;
39
+ `p-`/`j-` references match UUID tails, and bare hex matches legacy leading prefixes
40
+ or tails. Short hex references require at least four characters. Exact names can
41
+ still contain text that would otherwise be an invalid short reference.
42
+
43
+ The response is `{ pod, matches, total }`. A unique result includes one full pod;
44
+ no match returns `pod: null, matches: [], total: 0`. Ambiguity returns no full pod,
45
+ the exact total, and at most twenty `{ id, name, state }` choices. It never sends
46
+ the account's launch reports just to resolve one reference. All values are bound
47
+ SQL parameters, and lookup does not grant permission to perform an action.
48
+
49
+ A full UUID continues to use the ordinary single-pod route. Only a 404 from the
50
+ new route triggers the CLI's old-server pagination fallback; auth, validation,
51
+ network and server failures do not. Missing matches on a supporting server are
52
+ 200 responses, not capability failures.
53
+
54
+ Attach also shares one in-flight semantic model catalog between catalog and
55
+ thinking-level consumers and overlaps independent auth/thinking initialization.
56
+ It still validates readiness, settings and extension trust before accepting input;
57
+ there is no persistent model cache or new classic-RPC passthrough.
58
+
32
59
  ## Manual acceptance (no unit tests)
33
60
 
34
61
  Use an explicit disposable migrated database, never a production fixture insert.
@@ -45,6 +72,14 @@ Run the actual pod routes over loopback HTTP and the built CLI by absolute path:
45
72
  hide active pods on later pages. Check quiet and rendered output.
46
73
  - Delete the last pod of one page, then follow its cursor successfully.
47
74
  - Malformed cursor and simultaneous `cursor`/`before` return 400.
75
+ - Resolve short refs and names against more than 800 history rows without any
76
+ roster requests. Check exact-name precedence, case-insensitive hex, minimum
77
+ length, prefix/tail collisions, archived/gone rows, owner/org isolation and
78
+ pod-token lineage. Ambiguity must return the exact count but at most twenty
79
+ metadata choices, never a full report for each match.
80
+ - A missing lookup route falls back to the old paginated selection. A missing
81
+ match, invalid reference, 401/403, network failure or 5xx must not trigger that
82
+ fallback. Full UUIDs still go directly to the single-pod route.
48
83
  - A legacy server omitting `nextCursor` is still traversed with `before`.
49
84
  A server repeating a cursor fails without printing an incomplete quiet roster.
50
85
 
@@ -1,4 +1,4 @@
1
- .TH "PIPOD\-CONFIG" "5" "October 2026" "0.1.20"
1
+ .TH "PIPOD\-CONFIG" "5" "November 2026" "0.1.22"
2
2
  .SH "NAME"
3
3
  \fBpipod-config\fR \- pi pod configuration files
4
4
  .SH SYNOPSIS
package/man/pipod.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH "PIPOD" "1" "October 2026" "0.1.20"
1
+ .TH "PIPOD" "1" "November 2026" "0.1.22"
2
2
  .SH "NAME"
3
3
  \fBpipod\fR \- run pi coding\-agent sessions in server\-managed pods
4
4
  .SH SYNOPSIS
@@ -713,8 +713,9 @@ Quitting detaches: the pod keeps running until the server's idle policy reaps
713
713
  it or `pipod stop` stops it\. Raw pod shells are not available\.
714
714
  Attaching to an archived pod offers to restore it first\.
715
715
 
716
- Holding a character key types it once: key repeat stays available for
717
- Backspace, Tab and the arrow keys, where it is useful while editing\.
716
+ Holding a character key types it once, and a press the terminal reports in both of its
717
+ encodings types once too: key repeat stays available for Backspace, Tab and the arrow keys,
718
+ where it is useful while editing\.
718
719
 
719
720
  After \-\-:
720
721
  <prompt…> Words are sent to the session as a startup prompt
package/man/pipod.1.md CHANGED
@@ -703,8 +703,9 @@ Quitting detaches: the pod keeps running until the server's idle policy reaps
703
703
  it or `pipod stop` stops it. Raw pod shells are not available.
704
704
  Attaching to an archived pod offers to restore it first.
705
705
 
706
- Holding a character key types it once: key repeat stays available for
707
- Backspace, Tab and the arrow keys, where it is useful while editing.
706
+ Holding a character key types it once, and a press the terminal reports in both of its
707
+ encodings types once too: key repeat stays available for Backspace, Tab and the arrow keys,
708
+ where it is useful while editing.
708
709
 
709
710
  After --:
710
711
  <prompt…> Words are sent to the session as a startup prompt
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pipod/cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pipod/cli",
9
- "version": "0.1.20",
9
+ "version": "0.1.22",
10
10
  "license": "AGPL-3.0-only",
11
11
  "dependencies": {
12
12
  "@earendil-works/pi-coding-agent": "1.0.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipod/cli",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Run pi coding-agent sessions in server-managed pods",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",