@pipod/cli 0.1.19 → 0.1.21

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,100 @@
1
+ // src/log.ts
2
+ import { stripVTControlCharacters } from "util";
3
+
4
+ // src/redact.ts
5
+ var MIN_REDACTABLE_LENGTH = 4;
6
+ var secrets = /* @__PURE__ */ new Set();
7
+ function registerSecret(value) {
8
+ if (!value) return;
9
+ const v = String(value);
10
+ if (v.length < MIN_REDACTABLE_LENGTH) return;
11
+ secrets.add(v);
12
+ }
13
+ function registerSecrets(values) {
14
+ for (const v of Object.values(values)) registerSecret(v);
15
+ }
16
+ function redact(input) {
17
+ if (secrets.size === 0) return input;
18
+ let out2 = input;
19
+ for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {
20
+ if (out2.includes(secret)) out2 = out2.split(secret).join("[redacted]");
21
+ }
22
+ return out2;
23
+ }
24
+
25
+ // src/log.ts
26
+ var verbose = false;
27
+ var colorEnabled = process.stderr.isTTY === true && !process.env.NO_COLOR;
28
+ function setVerbose(v) {
29
+ verbose = v;
30
+ }
31
+ function isVerbose() {
32
+ return verbose;
33
+ }
34
+ var CODES = {
35
+ reset: "\x1B[0m",
36
+ dim: "\x1B[2m",
37
+ bold: "\x1B[1m",
38
+ red: "\x1B[31m",
39
+ yellow: "\x1B[33m",
40
+ cyan: "\x1B[36m",
41
+ green: "\x1B[32m"
42
+ };
43
+ function paint(code, s) {
44
+ return colorEnabled ? `${CODES[code]}${s}${CODES.reset}` : s;
45
+ }
46
+ var color = {
47
+ dim: (s) => paint("dim", s),
48
+ bold: (s) => paint("bold", s),
49
+ red: (s) => paint("red", s),
50
+ yellow: (s) => paint("yellow", s),
51
+ cyan: (s) => paint("cyan", s),
52
+ green: (s) => paint("green", s)
53
+ };
54
+ function write(line) {
55
+ process.stderr.write(redact(line) + "\n");
56
+ }
57
+ function info(msg) {
58
+ write(`${paint("cyan", "pi pod")} ${msg}`);
59
+ }
60
+ function step(phase, msg) {
61
+ write(`${paint("cyan", "pi pod")} ${paint("dim", `[${phase}]`)} ${msg}`);
62
+ }
63
+ function warn(msg) {
64
+ write(`${paint("yellow", "warning")} ${msg}`);
65
+ }
66
+ function error(msg) {
67
+ write(`${paint("red", "error")} ${msg}`);
68
+ }
69
+ function hint(msg) {
70
+ const lines = msg.split("\n");
71
+ write(`${paint("dim", " hint:")} ${lines[0] ?? ""}`);
72
+ for (const line of lines.slice(1)) write(paint("dim", ` ${line}`));
73
+ }
74
+ function debug(msg) {
75
+ if (verbose) write(`${paint("dim", "debug")} ${paint("dim", msg)}`);
76
+ }
77
+ function plain(msg) {
78
+ write(msg);
79
+ }
80
+ function out(msg) {
81
+ const line = redact(msg);
82
+ process.stdout.write((process.stdout.isTTY === true ? line : stripVTControlCharacters(line)) + "\n");
83
+ }
84
+
85
+ export {
86
+ registerSecret,
87
+ registerSecrets,
88
+ setVerbose,
89
+ isVerbose,
90
+ color,
91
+ info,
92
+ step,
93
+ warn,
94
+ error,
95
+ hint,
96
+ debug,
97
+ plain,
98
+ out
99
+ };
100
+ //# sourceMappingURL=chunk-7TJWBDVX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/log.ts","../src/redact.ts"],"sourcesContent":["/**\n * src/log.ts — the launcher's logger. Everything user-visible goes through here so that\n * redaction (§7.3) is structural rather than something each call site has to remember.\n */\nimport { stripVTControlCharacters } from \"node:util\";\nimport { redact } from \"./redact.js\";\n\nlet verbose = false;\nlet colorEnabled = process.stderr.isTTY === true && !process.env.NO_COLOR;\n\nexport function setVerbose(v: boolean): void {\n verbose = v;\n}\n\nexport function isVerbose(): boolean {\n return verbose;\n}\n\nexport function setColor(enabled: boolean): void {\n colorEnabled = enabled;\n}\n\nconst CODES = {\n reset: \"\\u001b[0m\",\n dim: \"\\u001b[2m\",\n bold: \"\\u001b[1m\",\n red: \"\\u001b[31m\",\n yellow: \"\\u001b[33m\",\n cyan: \"\\u001b[36m\",\n green: \"\\u001b[32m\",\n} as const;\n\nfunction paint(code: keyof typeof CODES, s: string): string {\n return colorEnabled ? `${CODES[code]}${s}${CODES.reset}` : s;\n}\n\nexport const color = {\n dim: (s: string) => paint(\"dim\", s),\n bold: (s: string) => paint(\"bold\", s),\n red: (s: string) => paint(\"red\", s),\n yellow: (s: string) => paint(\"yellow\", s),\n cyan: (s: string) => paint(\"cyan\", s),\n green: (s: string) => paint(\"green\", s),\n};\n\n/**\n * Diagnostics go to stderr so that stdout stays clean for the PTY stream and for\n * machine-readable output (`--dry-run`, `--version`).\n */\nfunction write(line: string): void {\n process.stderr.write(redact(line) + \"\\n\");\n}\n\nexport function info(msg: string): void {\n write(`${paint(\"cyan\", \"pi pod\")} ${msg}`);\n}\n\nexport function step(phase: string, msg: string): void {\n write(`${paint(\"cyan\", \"pi pod\")} ${paint(\"dim\", `[${phase}]`)} ${msg}`);\n}\n\nexport function warn(msg: string): void {\n write(`${paint(\"yellow\", \"warning\")} ${msg}`);\n}\n\nexport function error(msg: string): void {\n write(`${paint(\"red\", \"error\")} ${msg}`);\n}\n\n/** Multi-line hints are one hint: the prefix appears once, continuation lines align under it. */\nexport function hint(msg: string): void {\n const lines = msg.split(\"\\n\");\n write(`${paint(\"dim\", \" hint:\")} ${lines[0] ?? \"\"}`);\n for (const line of lines.slice(1)) write(paint(\"dim\", ` ${line}`));\n}\n\nexport function debug(msg: string): void {\n if (verbose) write(`${paint(\"dim\", \"debug\")} ${paint(\"dim\", msg)}`);\n}\n\nexport function plain(msg: string): void {\n write(msg);\n}\n\n/**\n * Direct stdout writer for command results and machine-readable output; still redacted.\n * Color is useful at a terminal but must never leak into a pipe or redirected file.\n */\nexport function out(msg: string): void {\n const line = redact(msg);\n process.stdout.write((process.stdout.isTTY === true ? line : stripVTControlCharacters(line)) + \"\\n\");\n}\n\n/**\n * Wall-clock phase timings for one launch/attach, so the summary line can say where the\n * time went. Overlapping (parallelized) phases each record their own duration; the summary\n * total is wall time, not the sum.\n */\nexport interface PhaseTiming {\n phase: string;\n ms: number;\n}\n\nconst phaseTimings: PhaseTiming[] = [];\n\nexport function resetPhaseTimings(): void {\n phaseTimings.length = 0;\n}\n\nexport function recordPhase(phase: string, ms: number): void {\n phaseTimings.push({ phase, ms });\n}\n\nexport async function timePhase<T>(phase: string, work: () => Promise<T>): Promise<T> {\n const startedAt = Date.now();\n try {\n return await work();\n } finally {\n recordPhase(phase, Date.now() - startedAt);\n }\n}\n\nfunction fmtSeconds(ms: number): string {\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\n/** One line naming total wall time and every phase that cost at least 100ms. */\nexport function phaseSummary(label: string, totalMs: number): string {\n const parts = phaseTimings.filter((t) => t.ms >= 100).map((t) => `${t.phase} ${fmtSeconds(t.ms)}`);\n const breakdown = parts.length > 0 ? ` (${parts.join(\" · \")})` : \"\";\n return `${label} in ${fmtSeconds(totalMs)}${breakdown}`;\n}\n\n/**\n * Print a periodic \"still …\" line while a long provider operation runs, so a multi-minute\n * wait is distinguishable from a hang. The line names the elapsed time and, when known,\n * roughly how long the wait usually takes.\n */\nexport const LIVENESS_INTERVAL_MS = 15_000;\n\nexport async function withLiveness<T>(\n label: string,\n work: () => Promise<T>,\n opts: { intervalMs?: number; expectation?: string } = {},\n): Promise<T> {\n const intervalMs = opts.intervalMs ?? LIVENESS_INTERVAL_MS;\n const startedAt = Date.now();\n const timer = setInterval(() => {\n const elapsed = Math.round((Date.now() - startedAt) / 1000);\n const usually = opts.expectation ? `, ${opts.expectation}` : \"\";\n write(`${paint(\"cyan\", \"pi pod\")} ${paint(\"dim\", `still ${label} (${elapsed}s elapsed${usually})`)}`);\n }, intervalMs);\n timer.unref?.();\n try {\n return await work();\n } finally {\n clearInterval(timer);\n }\n}\n\n/**\n * Stream a child stream to the terminal with a prefix (`[init]`, §4.3). Handles partial\n * lines so a chunk boundary never splits a prefix.\n */\nexport function prefixedStreamer(prefix: string): (chunk: Uint8Array) => void {\n let pending = \"\";\n const flushLine = (line: string) => write(`${paint(\"dim\", prefix)} ${line}`);\n return (chunk: Uint8Array) => {\n pending += Buffer.from(chunk).toString(\"utf8\");\n const lines = pending.split(/\\r?\\n/);\n pending = lines.pop() ?? \"\";\n for (const line of lines) flushLine(line);\n // Guard against a runaway line with no newline ever arriving.\n if (pending.length > 8192) {\n flushLine(pending);\n pending = \"\";\n }\n };\n}\n","/**\n * src/redact.ts — secret-aware log redaction (§7.3).\n *\n * Every value originating from the env file is registered here; the logger runs all output\n * through `redact()`. The conformance suite (§14) asserts that no registered value ever\n * appears in captured logs.\n */\n\n/** Values short enough to appear coincidentally are not worth redacting (and would mangle output). */\nconst MIN_REDACTABLE_LENGTH = 4;\n\nconst secrets = new Set<string>();\n\nexport function registerSecret(value: string | undefined | null): void {\n if (!value) return;\n const v = String(value);\n if (v.length < MIN_REDACTABLE_LENGTH) return;\n secrets.add(v);\n}\n\nexport function registerSecrets(values: Record<string, string>): void {\n for (const v of Object.values(values)) registerSecret(v);\n}\n\n/** Test/reset hook. */\nexport function clearSecrets(): void {\n secrets.clear();\n}\n\nexport function registeredSecretCount(): number {\n return secrets.size;\n}\n\n/**\n * Replace every registered secret with `[redacted]`. Longest-first so that a secret which\n * contains another secret as a substring is not partially revealed.\n */\nexport function redact(input: string): string {\n if (secrets.size === 0) return input;\n let out = input;\n for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {\n if (out.includes(secret)) out = out.split(secret).join(\"[redacted]\");\n }\n return out;\n}\n\nexport function redactBytes(input: Uint8Array): Uint8Array {\n if (secrets.size === 0) return input;\n const text = Buffer.from(input).toString(\"utf8\");\n const cleaned = redact(text);\n return cleaned === text ? input : Buffer.from(cleaned, \"utf8\");\n}\n\n/** Redact an arbitrary structure for debug dumps. Keys are preserved, values scrubbed. */\nexport function redactDeep<T>(value: T): T {\n if (typeof value === \"string\") return redact(value) as unknown as T;\n if (Array.isArray(value)) return value.map((v) => redactDeep(v)) as unknown as T;\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) out[k] = redactDeep(v);\n return out as unknown as T;\n }\n return value;\n}\n"],"mappings":"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","names":["out"]}
package/dist/cli.js CHANGED
@@ -4,6 +4,21 @@ import {
4
4
  POD_SUBCOMMANDS,
5
5
  PodCommandRouter
6
6
  } from "./chunk-GNKF75X7.js";
7
+ import {
8
+ color,
9
+ debug,
10
+ error,
11
+ hint,
12
+ info,
13
+ isVerbose,
14
+ out,
15
+ plain,
16
+ registerSecret,
17
+ registerSecrets,
18
+ setVerbose,
19
+ step,
20
+ warn
21
+ } from "./chunk-7TJWBDVX.js";
7
22
 
8
23
  // src/cli.ts
9
24
  import * as fs36 from "fs";
@@ -96,90 +111,6 @@ var PodAsleepError = class extends PiPodError {
96
111
  import * as fs7 from "fs";
97
112
  import { Readable } from "stream";
98
113
 
99
- // src/log.ts
100
- import { stripVTControlCharacters } from "util";
101
-
102
- // src/redact.ts
103
- var MIN_REDACTABLE_LENGTH = 4;
104
- var secrets = /* @__PURE__ */ new Set();
105
- function registerSecret(value) {
106
- if (!value) return;
107
- const v = String(value);
108
- if (v.length < MIN_REDACTABLE_LENGTH) return;
109
- secrets.add(v);
110
- }
111
- function registerSecrets(values) {
112
- for (const v of Object.values(values)) registerSecret(v);
113
- }
114
- function redact(input) {
115
- if (secrets.size === 0) return input;
116
- let out2 = input;
117
- for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {
118
- if (out2.includes(secret)) out2 = out2.split(secret).join("[redacted]");
119
- }
120
- return out2;
121
- }
122
-
123
- // src/log.ts
124
- var verbose = false;
125
- var colorEnabled = process.stderr.isTTY === true && !process.env.NO_COLOR;
126
- function setVerbose(v) {
127
- verbose = v;
128
- }
129
- function isVerbose() {
130
- return verbose;
131
- }
132
- var CODES = {
133
- reset: "\x1B[0m",
134
- dim: "\x1B[2m",
135
- bold: "\x1B[1m",
136
- red: "\x1B[31m",
137
- yellow: "\x1B[33m",
138
- cyan: "\x1B[36m",
139
- green: "\x1B[32m"
140
- };
141
- function paint(code, s) {
142
- return colorEnabled ? `${CODES[code]}${s}${CODES.reset}` : s;
143
- }
144
- var color = {
145
- dim: (s) => paint("dim", s),
146
- bold: (s) => paint("bold", s),
147
- red: (s) => paint("red", s),
148
- yellow: (s) => paint("yellow", s),
149
- cyan: (s) => paint("cyan", s),
150
- green: (s) => paint("green", s)
151
- };
152
- function write(line) {
153
- process.stderr.write(redact(line) + "\n");
154
- }
155
- function info(msg) {
156
- write(`${paint("cyan", "pi pod")} ${msg}`);
157
- }
158
- function step(phase, msg) {
159
- write(`${paint("cyan", "pi pod")} ${paint("dim", `[${phase}]`)} ${msg}`);
160
- }
161
- function warn(msg) {
162
- write(`${paint("yellow", "warning")} ${msg}`);
163
- }
164
- function error(msg) {
165
- write(`${paint("red", "error")} ${msg}`);
166
- }
167
- function hint(msg) {
168
- const lines = msg.split("\n");
169
- write(`${paint("dim", " hint:")} ${lines[0] ?? ""}`);
170
- for (const line of lines.slice(1)) write(paint("dim", ` ${line}`));
171
- }
172
- function debug(msg) {
173
- if (verbose) write(`${paint("dim", "debug")} ${paint("dim", msg)}`);
174
- }
175
- function plain(msg) {
176
- write(msg);
177
- }
178
- function out(msg) {
179
- const line = redact(msg);
180
- process.stdout.write((process.stdout.isTTY === true ? line : stripVTControlCharacters(line)) + "\n");
181
- }
182
-
183
114
  // src/account/oidc.ts
184
115
  import { createHash, randomBytes, timingSafeEqual } from "crypto";
185
116
  import { createRemoteJWKSet, jwtVerify } from "jose";
@@ -1942,6 +1873,10 @@ var AccountClient = class {
1942
1873
  listPods(query = {}) {
1943
1874
  return this.request("/pods", { query });
1944
1875
  }
1876
+ /** Null means an older server lacks lookup, not that the ref had no matches. */
1877
+ lookupPodRef(ref) {
1878
+ return this.request(`/pods/by-ref/${encodeURIComponent(ref)}`, { allow404: true });
1879
+ }
1945
1880
  /** `waitMs` long-polls: a supporting server answers on the next pod change; older servers ignore it. */
1946
1881
  getPod(id, opts = {}) {
1947
1882
  const waitMs = opts.waitMs != null && opts.waitMs > 0 ? Math.floor(opts.waitMs) : void 0;
@@ -5137,19 +5072,24 @@ function displayRef(id, kind) {
5137
5072
  const tail = id.replaceAll("-", "").slice(-DISPLAY_TAIL_LENGTH).toLowerCase();
5138
5073
  return `${kind === "pod" ? "p" : "j"}-${tail}`;
5139
5074
  }
5140
- function matchesRef(id, ref) {
5141
- const normalizedId = id.toLowerCase();
5142
- const normalizedRef = ref.toLowerCase();
5143
- if (FULL_UUID.test(normalizedRef)) return normalizedId === normalizedRef;
5144
- const prefixed = /^[pj]-(.*)$/i.exec(normalizedRef);
5075
+ function parseRef(ref) {
5076
+ const normalized = ref.toLowerCase();
5077
+ if (FULL_UUID.test(normalized)) return { kind: "uuid", value: normalized };
5078
+ const prefixed = /^[pj]-(.*)$/i.exec(normalized);
5145
5079
  if (prefixed) {
5146
- const tail = prefixed[1];
5147
- assertUsableHexRef(ref, tail);
5148
- return normalizedId.replaceAll("-", "").endsWith(tail);
5080
+ assertUsableHexRef(ref, prefixed[1]);
5081
+ return { kind: "tail", value: prefixed[1] };
5149
5082
  }
5150
- if (!HEX.test(normalizedRef)) return false;
5151
- assertUsableHexRef(ref, normalizedRef);
5152
- return normalizedId.startsWith(normalizedRef) || normalizedId.replaceAll("-", "").endsWith(normalizedRef);
5083
+ if (!HEX.test(normalized)) return null;
5084
+ assertUsableHexRef(ref, normalized);
5085
+ return { kind: "hex", value: normalized };
5086
+ }
5087
+ function matchesRef(id, ref) {
5088
+ const pattern = parseRef(ref);
5089
+ if (!pattern) return false;
5090
+ const normalizedId = id.toLowerCase();
5091
+ if (pattern.kind === "uuid") return normalizedId === pattern.value;
5092
+ return normalizedId.replaceAll("-", "").endsWith(pattern.value) || pattern.kind === "hex" && normalizedId.startsWith(pattern.value);
5153
5093
  }
5154
5094
  function assertUsableHexRef(ref, hex) {
5155
5095
  if (!HEX.test(hex)) {
@@ -5284,19 +5224,19 @@ async function archiveIntoPod(opts, plan, flags = {}) {
5284
5224
  const ignored = gitIgnoredPaths(plan.root);
5285
5225
  const tmpDir = opts.tmpDir ?? os3.tmpdir();
5286
5226
  const archivePath = path14.join(tmpDir, `pi-pod-seed-${randomBytes3(8).toString("hex")}.tar.gz`);
5287
- const write2 = opts.tarball ?? createTarball;
5227
+ const write = opts.tarball ?? createTarball;
5288
5228
  try {
5289
5229
  let summary;
5290
5230
  let includeGit = plan.includeGit;
5291
5231
  step("seed", `archiving ${plan.root}${includeGit ? " with .git history" : ""} (${plan.reason})`);
5292
5232
  try {
5293
- summary = await write2(tarballOptions(opts, plan.root, archivePath, ignored, includeGit));
5233
+ summary = await write(tarballOptions(opts, plan.root, archivePath, ignored, includeGit));
5294
5234
  } catch (error2) {
5295
5235
  throwIfCancelled(opts, error2);
5296
5236
  if (!(error2 instanceof TarballLimitError) || !includeGit) throw error2;
5297
5237
  warn(`${error2.message} \u2014 retrying without .git history`);
5298
5238
  includeGit = false;
5299
- summary = await write2(tarballOptions(opts, plan.root, archivePath, ignored, includeGit));
5239
+ summary = await write(tarballOptions(opts, plan.root, archivePath, ignored, includeGit));
5300
5240
  }
5301
5241
  if (summary.entries === 0) {
5302
5242
  info("nothing to seed \u2014 the local directory is empty");
@@ -6646,6 +6586,7 @@ var RemoteStateCache = class {
6646
6586
  sessionRefreshRequested = false;
6647
6587
  expectedRefreshLeaf = NO_EXPECTED_LEAF;
6648
6588
  cacheGeneration = 0;
6589
+ modelsRefresh = null;
6649
6590
  replacementInProgress = false;
6650
6591
  labelStates = /* @__PURE__ */ new Map();
6651
6592
  seedBridge = null;
@@ -6881,11 +6822,22 @@ var RemoteStateCache = class {
6881
6822
  }
6882
6823
  throw lastError ?? new Error("pod session refresh could not be reconciled");
6883
6824
  }
6884
- async refreshModels() {
6885
- try {
6886
- this.availableModels = await this.rpc.getAvailableModels();
6887
- } catch {
6888
- }
6825
+ refreshModels() {
6826
+ const generation = this.cacheGeneration;
6827
+ if (this.modelsRefresh?.generation === generation) return this.modelsRefresh.promise;
6828
+ const promise = (async () => {
6829
+ try {
6830
+ const models = await this.rpc.getAvailableModels();
6831
+ if (generation === this.cacheGeneration) this.availableModels = models;
6832
+ } catch {
6833
+ }
6834
+ })();
6835
+ const entry = { generation, promise };
6836
+ this.modelsRefresh = entry;
6837
+ void promise.then(() => {
6838
+ if (this.modelsRefresh === entry) this.modelsRefresh = null;
6839
+ });
6840
+ return promise;
6889
6841
  }
6890
6842
  async refreshCommands() {
6891
6843
  try {
@@ -11503,8 +11455,7 @@ function createRemoteRuntime(opts) {
11503
11455
  treeBridge,
11504
11456
  init: async () => {
11505
11457
  await cache.init();
11506
- await refreshThinkingLevels();
11507
- await authBridge.refresh();
11458
+ await Promise.all([refreshThinkingLevels(), authBridge.refresh(), cache.refreshModels()]);
11508
11459
  await seedScopedModels();
11509
11460
  if (opts.attestedExtensionPaths?.length) {
11510
11461
  try {
@@ -12472,6 +12423,7 @@ var GatewayRpcClient = class _GatewayRpcClient extends RpcClientBase {
12472
12423
  /** False between adopting a skewed hello and readiness replacing it; ordinary RPC is gated. */
12473
12424
  piVersionReady = false;
12474
12425
  idPrefix = `cli-${randomBytes5(4).toString("hex")}-`;
12426
+ availableModelsRequest = null;
12475
12427
  modelsRequest = null;
12476
12428
  pendingSemantic = /* @__PURE__ */ new Map();
12477
12429
  /** Auxiliary completions multiplex by caller id — unlike single-slot semantic queries. */
@@ -12625,6 +12577,20 @@ var GatewayRpcClient = class _GatewayRpcClient extends RpcClientBase {
12625
12577
  }
12626
12578
  return promise;
12627
12579
  }
12580
+ /** Model and thinking consumers share one semantic catalog, not two large replies. */
12581
+ getAvailableModels() {
12582
+ if (this.availableModelsRequest) return this.availableModelsRequest;
12583
+ const task = this.getModels().then((snapshot) => snapshot.models).catch((error2) => {
12584
+ if (error2 instanceof Error && error2.message === "timed out waiting for the gateway model catalog" && !this.userClosed && this.piVersionReady) return super.getAvailableModels();
12585
+ throw error2;
12586
+ });
12587
+ this.availableModelsRequest = task;
12588
+ const clear = () => {
12589
+ if (this.availableModelsRequest === task) this.availableModelsRequest = null;
12590
+ };
12591
+ void task.then(clear, clear);
12592
+ return task;
12593
+ }
12628
12594
  /** List pod-side sessions. A timeout or unsupported marker is an absent capability. */
12629
12595
  getSessions() {
12630
12596
  const absent = { sessions: [], workdir: "", complete: false, unsupported: true };
@@ -12982,6 +12948,7 @@ var GatewayRpcClient = class _GatewayRpcClient extends RpcClientBase {
12982
12948
  }
12983
12949
  }
12984
12950
  resetHandshakeState(podId) {
12951
+ this.availableModelsRequest = null;
12985
12952
  this.currentPodId = podId;
12986
12953
  this.hello = null;
12987
12954
  this.piExitCode = void 0;
@@ -14346,9 +14313,9 @@ async function runAccountInteractive(opts) {
14346
14313
  latestConnectionState = state;
14347
14314
  noteAsleep(state);
14348
14315
  if (runtimeRef) {
14349
- const paint2 = () => applyConnectionStatus(runtimeRef?.bridge.ui, state, rpc?.podId ?? opts.pod.id);
14350
- if (runtimeRef.bridge.ui) paint2();
14351
- else runtimeRef.bridge.onUiBound(paint2);
14316
+ const paint = () => applyConnectionStatus(runtimeRef?.bridge.ui, state, rpc?.podId ?? opts.pod.id);
14317
+ if (runtimeRef.bridge.ui) paint();
14318
+ else runtimeRef.bridge.onUiBound(paint);
14352
14319
  if (state.kind === "lost") {
14353
14320
  runtimeRef.bridge.notify(
14354
14321
  state.reason ? `could not reconnect: ${state.reason}` : "connection lost",
@@ -14694,20 +14661,27 @@ ${choices.join("\n")}`,
14694
14661
  };
14695
14662
  let unwireDisconnectedQuit = () => {
14696
14663
  };
14664
+ let unwireKeyRepeat = () => {
14665
+ };
14697
14666
  let mode;
14698
14667
  try {
14699
14668
  await runtime.init();
14700
14669
  const { InteractiveMode } = await import("@earendil-works/pi-coding-agent");
14701
14670
  const { wirePodChords } = await import("./chords-SOJBLC6R.js");
14671
+ const { wireKeyRepeatGuard } = await import("./key-repeat-guard-7OVKW5KD.js");
14672
+ const { guardStdinFragments } = await import("./stdin-fragment-guard-SPIJC7GT.js");
14673
+ guardStdinFragments();
14702
14674
  const chords = context.config.pi.chords;
14703
14675
  runtime.bridge.setOnUiBound(() => {
14704
14676
  unwireChords();
14705
14677
  unwireDisconnectedQuit();
14678
+ unwireKeyRepeat();
14706
14679
  unwireChords = wirePodChords(runtime.bridge, runtime.router, chords);
14707
14680
  unwireDisconnectedQuit = wireDisconnectedQuit(
14708
14681
  runtime.bridge,
14709
14682
  () => connectionAllowsQuit(latestConnectionState)
14710
14683
  );
14684
+ unwireKeyRepeat = wireKeyRepeatGuard(runtime.bridge.ui);
14711
14685
  });
14712
14686
  const tuiMode = opts.tuiMode ?? tuiModeFromArgv(context.config.pi.args);
14713
14687
  process.env["PI_SKIP_VERSION_CHECK"] ??= "1";
@@ -14737,6 +14711,7 @@ ${choices.join("\n")}`,
14737
14711
  runtime.bridge.setOnUiBound(null);
14738
14712
  unwireChords();
14739
14713
  unwireDisconnectedQuit();
14714
+ unwireKeyRepeat();
14740
14715
  rpc.close();
14741
14716
  derived?.dispose();
14742
14717
  throw error2;
@@ -14763,6 +14738,7 @@ ${choices.join("\n")}`,
14763
14738
  runtime.bridge.setOnUiBound(null);
14764
14739
  unwireChords();
14765
14740
  unwireDisconnectedQuit();
14741
+ unwireKeyRepeat();
14766
14742
  process.off("SIGHUP", onHangUp);
14767
14743
  process.off("SIGTERM", onHangUp);
14768
14744
  }
@@ -15056,6 +15032,12 @@ function renderPodLines(pods) {
15056
15032
  async function selectAccountPod(client, podRef, action, cwd) {
15057
15033
  if (podRef) {
15058
15034
  if (isFullUuid(podRef)) return client.getPod(podRef.toLowerCase());
15035
+ const found = await client.lookupPodRef(podRef);
15036
+ if (found !== null) {
15037
+ if (found.total > 1) throw ambiguousPodRef(podRef, found.matches, found.total);
15038
+ if (found.pod) return found.pod;
15039
+ throw new PiPodError(`no pod matches "${podRef}"`, { hint: "see `pipod list --all`" });
15040
+ }
15059
15041
  const pods2 = (await listAllPods(client, {})).pods;
15060
15042
  const named = pods2.filter((pod) => pod.name === podRef);
15061
15043
  if (named.length === 1) return named[0];
@@ -15082,10 +15064,11 @@ ${podChoiceLines(pods)}`
15082
15064
  }
15083
15065
  return pods[0];
15084
15066
  }
15085
- function ambiguousPodRef(ref, pods) {
15086
- return new PiPodError(`"${ref}" matches ${pods.length} pods`, {
15067
+ function ambiguousPodRef(ref, pods, total = pods.length) {
15068
+ return new PiPodError(`"${ref}" matches ${total} pods`, {
15087
15069
  hint: `use a longer ref:
15088
- ${podChoiceLines(pods)}`
15070
+ ${podChoiceLines(pods)}` + (total > pods.length ? `
15071
+ \u2026 showing the first ${pods.length} matches` : "")
15089
15072
  });
15090
15073
  }
15091
15074
  function podChoiceLines(pods) {
@@ -18671,10 +18654,10 @@ async function showCmd(client, args) {
18671
18654
  if (t.piSettings && Object.keys(t.piSettings).length > 0) {
18672
18655
  info(`Pi settings ${describePiSettings(flatPiSettings(t.piSettings))}`);
18673
18656
  }
18674
- const secrets2 = await client.listSecrets("template", t.id).catch(() => null);
18675
- if (secrets2 && secrets2.secrets.length > 0) {
18657
+ const secrets = await client.listSecrets("template", t.id).catch(() => null);
18658
+ if (secrets && secrets.secrets.length > 0) {
18676
18659
  out("");
18677
- info(`template secrets: ${secrets2.secrets.map((s) => s.name).join(", ")}`);
18660
+ info(`template secrets: ${secrets.secrets.map((s) => s.name).join(", ")}`);
18678
18661
  }
18679
18662
  return 0;
18680
18663
  }
@@ -18684,9 +18667,9 @@ function describePiSettings(settings) {
18684
18667
  const packageCount = Array.isArray(packages) ? packages.length : 0;
18685
18668
  return `${names.join(", ") || "empty"}${packageCount > 0 ? ` (${packageCount} package(s))` : ""}`;
18686
18669
  }
18687
- async function writeTemplateSnapshot(write2) {
18670
+ async function writeTemplateSnapshot(write) {
18688
18671
  try {
18689
- return await write2();
18672
+ return await write();
18690
18673
  } catch (error2) {
18691
18674
  if (error2 instanceof PiPodError && error2.status === 400 && /(?:unrecognized|unknown|unexpected)[^\n]*piSettings|piSettings[^\n]*(?:unrecognized|unknown|unexpected)/i.test(error2.message)) {
18692
18675
  throw new PiPodError("the pi pod server does not support template-owned Pi settings", {