@pipod/cli 0.1.6 → 0.1.8

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/dist/cli.js CHANGED
@@ -6,8 +6,8 @@ import {
6
6
  } from "./chunk-GNKF75X7.js";
7
7
 
8
8
  // src/cli.ts
9
- import * as fs35 from "fs";
10
- import * as path33 from "path";
9
+ import * as fs36 from "fs";
10
+ import * as path34 from "path";
11
11
 
12
12
  // src/errors.ts
13
13
  var EXIT = {
@@ -583,7 +583,7 @@ import * as path5 from "path";
583
583
  import * as fs from "fs";
584
584
  import * as path from "path";
585
585
  var HOST_PI_AGENT_SUBPATH = path.join(".pi", "agent");
586
- var HOST_COUPLED_SETTINGS_KEYS = ["hooks", "mcpServers"];
586
+ var HOST_COUPLED_SETTINGS_KEYS = ["hooks", "mcpServers", "deviceId"];
587
587
  var DEFAULT_HOST_CONFIG = {
588
588
  settings: true,
589
589
  packages: true
@@ -4436,7 +4436,7 @@ function parseResourcePath(flag, value) {
4436
4436
  function sameResourceList(requested, echoed) {
4437
4437
  const want = requested ?? [];
4438
4438
  const got = echoed ?? [];
4439
- return want.length === got.length && want.every((path34, index) => got[index] === path34);
4439
+ return want.length === got.length && want.every((path35, index) => got[index] === path35);
4440
4440
  }
4441
4441
 
4442
4442
  // src/account/launch-seed.ts
@@ -6275,7 +6275,7 @@ function splitPiArgs(piArgs) {
6275
6275
  if (key === "model" || key === "thinking") continue;
6276
6276
  const flag = PI_RESOURCE_FLAGS[key];
6277
6277
  if (!flag || !Array.isArray(paths)) continue;
6278
- for (const path34 of paths) managedArgs.push(flag, path34);
6278
+ for (const path35 of paths) managedArgs.push(flag, path35);
6279
6279
  }
6280
6280
  }
6281
6281
  return {
@@ -6520,7 +6520,6 @@ import * as path17 from "path";
6520
6520
  import { promisify as promisify2 } from "util";
6521
6521
 
6522
6522
  // src/client/runtime/local-extensions.ts
6523
- import { randomUUID } from "crypto";
6524
6523
  import * as fs17 from "fs";
6525
6524
  import * as path16 from "path";
6526
6525
 
@@ -7002,19 +7001,19 @@ function branchSessionStats(entries, state) {
7002
7001
  function branchUsageTotals(entries) {
7003
7002
  const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
7004
7003
  for (const entry of entries) {
7005
- let usage;
7004
+ let usage2;
7006
7005
  if (entry.type === "message") {
7007
7006
  const message = entry.message;
7008
- if (message.role === "assistant" || message.role === "toolResult") usage = message.usage;
7007
+ if (message.role === "assistant" || message.role === "toolResult") usage2 = message.usage;
7009
7008
  } else if (entry.type === "compaction" || entry.type === "branch_summary") {
7010
- usage = entry.usage;
7009
+ usage2 = entry.usage;
7011
7010
  }
7012
- if (!usage) continue;
7013
- totals.input += usage.input ?? 0;
7014
- totals.output += usage.output ?? 0;
7015
- totals.cacheRead += usage.cacheRead ?? 0;
7016
- totals.cacheWrite += usage.cacheWrite ?? 0;
7017
- totals.cost += usage.cost?.total ?? 0;
7011
+ if (!usage2) continue;
7012
+ totals.input += usage2.input ?? 0;
7013
+ totals.output += usage2.output ?? 0;
7014
+ totals.cacheRead += usage2.cacheRead ?? 0;
7015
+ totals.cacheWrite += usage2.cacheWrite ?? 0;
7016
+ totals.cost += usage2.cost?.total ?? 0;
7018
7017
  }
7019
7018
  return totals;
7020
7019
  }
@@ -7208,7 +7207,7 @@ function branchOf(entries, fromId) {
7208
7207
  }
7209
7208
  async function createLocalExtensionHost(opts) {
7210
7209
  if (opts.paths.length === 0) return null;
7211
- const { cache, bridge, rpc } = opts;
7210
+ const { cache, bridge } = opts;
7212
7211
  const notifyWhenBound = (message, type) => {
7213
7212
  bridge.onUiBound(() => bridge.notify(message, type));
7214
7213
  };
@@ -7271,48 +7270,6 @@ async function createLocalExtensionHost(opts) {
7271
7270
  resetLeaf: throwMutator("resetLeaf")
7272
7271
  };
7273
7272
  const runner = new ExtensionRunnerClass(kept, runtime, opts.hostCwd, sessionManagerFacade, opts.modelRegistry);
7274
- let appendEntryWarned = false;
7275
- const appendEntryUnavailable = (reason) => {
7276
- if (appendEntryWarned) return;
7277
- appendEntryWarned = true;
7278
- notifyWhenBound(`pod extension appendEntry: ${reason} \u2014 the entry was not recorded`, "warning");
7279
- };
7280
- const supportsAppendEntry = () => cache.commands.some((command) => {
7281
- const candidate = command;
7282
- return candidate.name === APPEND_ENTRY_COMMAND && candidate.source === "extension";
7283
- });
7284
- const sendAppendEntry = (customType, data) => {
7285
- if (typeof customType !== "string" || customType.length === 0 || customType.length > APPEND_ENTRY_MAX_CUSTOM_TYPE_LENGTH) {
7286
- appendEntryUnavailable(`invalid custom type ${JSON.stringify(customType).slice(0, 64)}`);
7287
- return;
7288
- }
7289
- if (!supportsAppendEntry()) {
7290
- appendEntryUnavailable("this pod runs an older pi pod extension (restart Pi in the pod to update it)");
7291
- return;
7292
- }
7293
- const request = {
7294
- v: APPEND_ENTRY_PROTOCOL_VERSION,
7295
- id: randomUUID(),
7296
- op: "append-entry",
7297
- customType,
7298
- ...data !== void 0 ? { data } : {}
7299
- };
7300
- let encoded;
7301
- try {
7302
- encoded = Buffer.from(JSON.stringify(request), "utf8").toString("base64url");
7303
- } catch (error2) {
7304
- appendEntryUnavailable(`entry data is not serializable (${error2 instanceof Error ? error2.message : String(error2)})`);
7305
- return;
7306
- }
7307
- if (Buffer.byteLength(encoded, "utf8") > TREE_BRIDGE_MAX_ENCODED_REQUEST_BYTES) {
7308
- appendEntryUnavailable("entry data exceeds the bridge size limit");
7309
- return;
7310
- }
7311
- void rpc.prompt(`/${APPEND_ENTRY_COMMAND} ${encoded}`).then(() => cache.refreshTree().then(() => void 0)).catch((error2) => {
7312
- appendEntryUnavailable(error2 instanceof Error ? error2.message : String(error2));
7313
- debug(`append-entry bridge failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
7314
- });
7315
- };
7316
7273
  const actions = {
7317
7274
  sendMessage: unavailable("pi.sendMessage", "conversation context is built in the pod"),
7318
7275
  sendUserMessage: (content, sendOptions) => {
@@ -7324,7 +7281,7 @@ async function createLocalExtensionHost(opts) {
7324
7281
  )
7325
7282
  );
7326
7283
  },
7327
- appendEntry: (customType, data) => sendAppendEntry(customType, data),
7284
+ appendEntry: opts.appendEntry,
7328
7285
  setSessionName: (name) => void opts.setSessionName(name),
7329
7286
  getSessionName: () => cache.state.sessionName,
7330
7287
  setLabel: (entryId, label2) => opts.setLabel(entryId, label2),
@@ -8514,6 +8471,54 @@ import {
8514
8471
  resolveModelScopeWithDiagnostics
8515
8472
  } from "@earendil-works/pi-coding-agent";
8516
8473
 
8474
+ // src/client/runtime/append-entry-bridge.ts
8475
+ import { randomUUID } from "crypto";
8476
+ function createAppendEntryBridge(opts) {
8477
+ const { rpc, cache, bridge } = opts;
8478
+ let warned = false;
8479
+ const unavailable3 = (reason) => {
8480
+ if (warned) return;
8481
+ warned = true;
8482
+ bridge.onUiBound(() => bridge.notify(`session entry not recorded: ${reason}`, "warning"));
8483
+ };
8484
+ const supported = () => cache.commands.some((command) => {
8485
+ const candidate = command;
8486
+ return candidate.name === APPEND_ENTRY_COMMAND && candidate.source === "extension";
8487
+ });
8488
+ return (customType, data) => {
8489
+ if (typeof customType !== "string" || customType.length === 0 || customType.length > APPEND_ENTRY_MAX_CUSTOM_TYPE_LENGTH) {
8490
+ unavailable3(`invalid custom type ${JSON.stringify(customType).slice(0, 64)}`);
8491
+ return;
8492
+ }
8493
+ if (!supported()) {
8494
+ unavailable3("this pod runs an older pi pod extension (restart Pi in the pod to update it)");
8495
+ return;
8496
+ }
8497
+ const request = {
8498
+ v: APPEND_ENTRY_PROTOCOL_VERSION,
8499
+ id: randomUUID(),
8500
+ op: "append-entry",
8501
+ customType,
8502
+ ...data !== void 0 ? { data } : {}
8503
+ };
8504
+ let encoded;
8505
+ try {
8506
+ encoded = Buffer.from(JSON.stringify(request), "utf8").toString("base64url");
8507
+ } catch (error2) {
8508
+ unavailable3(`entry data is not serializable (${error2 instanceof Error ? error2.message : String(error2)})`);
8509
+ return;
8510
+ }
8511
+ if (Buffer.byteLength(encoded, "utf8") > TREE_BRIDGE_MAX_ENCODED_REQUEST_BYTES) {
8512
+ unavailable3("entry data exceeds the bridge size limit");
8513
+ return;
8514
+ }
8515
+ void rpc.prompt(`/${APPEND_ENTRY_COMMAND} ${encoded}`).then(() => cache.refreshTree().then(() => void 0)).catch((error2) => {
8516
+ unavailable3(error2 instanceof Error ? error2.message : String(error2));
8517
+ debug(`append-entry bridge failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
8518
+ });
8519
+ };
8520
+ }
8521
+
8517
8522
  // src/client/runtime/bridge.ts
8518
8523
  var HostBridge = class {
8519
8524
  /** Set exactly once, by the first ending path to fire; later writers lose. */
@@ -8632,11 +8637,11 @@ function extractAtPrefix(text) {
8632
8637
  const tokenStart = lastDelimiterIndex === -1 ? 0 : lastDelimiterIndex + 1;
8633
8638
  return text[tokenStart] === "@" ? text.slice(tokenStart) : null;
8634
8639
  }
8635
- function buildCompletionValue(path34, isQuotedPrefix) {
8636
- return isQuotedPrefix || path34.includes(" ") ? `@"${path34}"` : `@${path34}`;
8640
+ function buildCompletionValue(path35, isQuotedPrefix) {
8641
+ return isQuotedPrefix || path35.includes(" ") ? `@"${path35}"` : `@${path35}`;
8637
8642
  }
8638
- function basename7(path34) {
8639
- const trimmed = path34.endsWith("/") ? path34.slice(0, -1) : path34;
8643
+ function basename7(path35) {
8644
+ const trimmed = path35.endsWith("/") ? path35.slice(0, -1) : path35;
8640
8645
  const cut = trimmed.lastIndexOf("/");
8641
8646
  return cut === -1 ? trimmed : trimmed.slice(cut + 1);
8642
8647
  }
@@ -10588,6 +10593,7 @@ function createRemoteRuntime(opts) {
10588
10593
  const { rpc, cwd } = opts;
10589
10594
  const bridge = new HostBridge();
10590
10595
  const cache = new RemoteStateCache(rpc);
10596
+ const appendEntry = createAppendEntryBridge({ rpc, cache, bridge });
10591
10597
  const refreshThinkingLevels = async () => {
10592
10598
  cache.availableThinkingLevels = null;
10593
10599
  if (!opts.getAvailableThinkingLevels) return;
@@ -11017,6 +11023,8 @@ function createRemoteRuntime(opts) {
11017
11023
  // (including Mermaid) still compose — InteractiveMode appends these to its own chain.
11018
11024
  // Pod extension transforms remain honestly unavailable; client extensions supply theirs here.
11019
11025
  getMarkdownTransformers: () => localHost?.runner.getMarkdownTransformers() ?? [],
11026
+ // Tool renderers from client extensions wrap the built-in one, as in-process.
11027
+ resolveToolRenderers: (toolName, base) => localHost ? localHost.runner.resolveToolRenderers(toolName, base) : base(),
11020
11028
  getCommandDiagnostics: () => localHost?.runner.getCommandDiagnostics() ?? [],
11021
11029
  getShortcutDiagnostics: () => localHost?.runner.getShortcutDiagnostics() ?? [],
11022
11030
  getModelRegistry: () => localHost ? extensionModelRegistry : void 0,
@@ -11062,6 +11070,8 @@ function createRemoteRuntime(opts) {
11062
11070
  const sessionManager = {
11063
11071
  getCwd: () => cwd,
11064
11072
  getEntries: () => cache.entries,
11073
+ getBranch: () => cache.entries,
11074
+ getEntryCount: () => cache.entries.length,
11065
11075
  getLeafId: () => cache.leafId ?? void 0,
11066
11076
  getSessionDir: () => cwd,
11067
11077
  getSessionFile: () => cache.state.sessionFile,
@@ -11071,6 +11081,8 @@ function createRemoteRuntime(opts) {
11071
11081
  usesDefaultSessionDir: () => true,
11072
11082
  isPersisted: () => cache.state.sessionFile !== void 0,
11073
11083
  appendLabelChange: (entryId, label2) => trackLabelMutation(entryId, label2),
11084
+ /** pi's `/bug` records the report here; the entry belongs in the pod's session file. */
11085
+ appendCustomEntry: (customType, data) => appendEntry(customType, data),
11074
11086
  buildContextEntries: () => cache.entries
11075
11087
  };
11076
11088
  const session = {
@@ -11152,6 +11164,14 @@ function createRemoteRuntime(opts) {
11152
11164
  get hasPendingBashMessages() {
11153
11165
  return false;
11154
11166
  },
11167
+ // RPC reports neither the cache warmer nor where a virtual model routed a request, so the
11168
+ // footer and /settings show what pi shows when they are unavailable.
11169
+ get cacheWarmingStatus() {
11170
+ return void 0;
11171
+ },
11172
+ get routedModel() {
11173
+ return void 0;
11174
+ },
11155
11175
  // local-by-nature services
11156
11176
  settingsManager: remoteSettingsManager,
11157
11177
  sessionManager,
@@ -11203,6 +11223,8 @@ function createRemoteRuntime(opts) {
11203
11223
  abortBranchSummary: () => {
11204
11224
  },
11205
11225
  setAutoCompactionEnabled: (enabled) => mutate(() => rpc.setAutoCompaction(enabled)),
11226
+ /** Persisted like the /transport toggle; the warmer itself runs in the pod. */
11227
+ setCacheWarmingMode: (mode) => remoteSettingsManager.setCacheWarmingMode(mode),
11206
11228
  setAutoRetryEnabled: (enabled) => mutate(() => rpc.setAutoRetry(enabled)),
11207
11229
  abortRetry: () => rpc.abortRetry(),
11208
11230
  /** `!command` bash: streamed chunks arrive as bash_execution_update events with our id (§8). */
@@ -11423,7 +11445,7 @@ function createRemoteRuntime(opts) {
11423
11445
  hostCwd: cwd,
11424
11446
  cache,
11425
11447
  bridge,
11426
- rpc,
11448
+ appendEntry,
11427
11449
  getScopedModels: () => scopedModels,
11428
11450
  prompt,
11429
11451
  setModel: (model) => setRemoteModel(() => rpc.setModel(model.provider, model.id)),
@@ -16560,8 +16582,8 @@ async function allJobs(client) {
16560
16582
  beforeId = last.id;
16561
16583
  }
16562
16584
  }
16563
- function assertArgCount(args, expected, usage) {
16564
- if (args.length !== expected) throw new PiPodError(`usage: ${usage}`);
16585
+ function assertArgCount(args, expected, usage2) {
16586
+ if (args.length !== expected) throw new PiPodError(`usage: ${usage2}`);
16565
16587
  }
16566
16588
  function shortSchedule(trigger) {
16567
16589
  if (trigger.type === "cron") return trigger.cron;
@@ -16747,8 +16769,8 @@ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16747
16769
  var CHANGE_USAGE = "pipod billing change --plan standard|pro [--yes]";
16748
16770
  var CHANGE_PREVIEW_USAGE = "pipod billing change preview --plan standard|pro";
16749
16771
  var CHANGE_CONFIRM_USAGE = "pipod billing change confirm --quote <quoteId> [--yes]";
16750
- function usageError(message, usage) {
16751
- return new PiPodError(message, { hint: `usage: ${usage}`, exitCode: EXIT.USAGE });
16772
+ function usageError(message, usage2) {
16773
+ return new PiPodError(message, { hint: `usage: ${usage2}`, exitCode: EXIT.USAGE });
16752
16774
  }
16753
16775
  function parseChangeArgs(args) {
16754
16776
  let form = "flow";
@@ -17945,11 +17967,11 @@ function formatStatus(status) {
17945
17967
  const expires = status.state === "ready" && status.expiresAt ? ` (expires ${status.expiresAt})` : "";
17946
17968
  return `${status.providerId} ${status.state.replaceAll("_", " ")}${expires}`;
17947
17969
  }
17948
- function rejectExtra(args, usage) {
17949
- if (args.length > 0) throw new PiPodError(usage);
17970
+ function rejectExtra(args, usage2) {
17971
+ if (args.length > 0) throw new PiPodError(usage2);
17950
17972
  }
17951
- function rejectProviderArgs(args, usage) {
17952
- if (args.length !== 1 || args[0].startsWith("-")) throw new PiPodError(usage);
17973
+ function rejectProviderArgs(args, usage2) {
17974
+ if (args.length !== 1 || args[0].startsWith("-")) throw new PiPodError(usage2);
17953
17975
  }
17954
17976
  function terminalInteraction() {
17955
17977
  const controller = new AbortController();
@@ -19188,9 +19210,111 @@ function setDotPath(target, dotPath, value) {
19188
19210
  cursor[parts.at(-1)] = value;
19189
19211
  }
19190
19212
 
19191
- // src/scaffold.ts
19213
+ // src/manual.ts
19192
19214
  import * as fs33 from "fs";
19193
19215
  import * as path31 from "path";
19216
+ var MAIN_PAGE = "pipod.1.md";
19217
+ var FENCE = "```";
19218
+ function usage(command) {
19219
+ const entry = command === null ? section(mainPage(), 2, "SYNOPSIS") : commandEntry(command);
19220
+ if (entry === null) throw new PiPodError(`the pipod manual has no entry for ${command}`);
19221
+ const block = openingBlock(entry);
19222
+ const more = command !== null && hasMoreThanBlock(entry) ? `
19223
+ More: pipod help ${command}
19224
+ ` : "";
19225
+ return `${block}${more}`;
19226
+ }
19227
+ function manualEntry(name) {
19228
+ const entry = commandEntry(name) ?? topicSection(name) ?? topicPage(name);
19229
+ if (entry === null) {
19230
+ throw new PiPodError(`no manual entry for "${name}"`, {
19231
+ hint: "`pipod help` lists the commands and topics",
19232
+ exitCode: EXIT.USAGE
19233
+ });
19234
+ }
19235
+ return plainText(entry);
19236
+ }
19237
+ function commandEntry(command) {
19238
+ const commands = section(mainPage(), 2, "COMMANDS");
19239
+ return commands === null ? null : section(commands, 3, `pipod ${command}`);
19240
+ }
19241
+ function topicSection(topic) {
19242
+ return /^[a-z]+$/.test(topic) ? section(mainPage(), 2, topic.toUpperCase()) : null;
19243
+ }
19244
+ function topicPage(topic) {
19245
+ const prefix = `pipod-${topic}.`;
19246
+ const file = fs33.readdirSync(manDir()).find(
19247
+ (name) => name.startsWith(prefix) && /^\d+\.md$/.test(name.slice(prefix.length))
19248
+ );
19249
+ return file === void 0 ? null : read(path31.join(manDir(), file));
19250
+ }
19251
+ function section(markdown, level, title) {
19252
+ const lines = markdown.split("\n");
19253
+ const heading = `${"#".repeat(level)} ${title}`;
19254
+ let start = -1;
19255
+ let fenced = false;
19256
+ for (let i = 0; i < lines.length; i++) {
19257
+ const line = lines[i];
19258
+ if (line.startsWith(FENCE)) fenced = !fenced;
19259
+ if (fenced) continue;
19260
+ if (start < 0) {
19261
+ if (line === heading) start = i;
19262
+ continue;
19263
+ }
19264
+ const depth2 = /^(#+) /.exec(line)?.[1]?.length;
19265
+ if (depth2 !== void 0 && depth2 <= level) return `${lines.slice(start, i).join("\n").trimEnd()}
19266
+ `;
19267
+ }
19268
+ return start < 0 ? null : `${lines.slice(start).join("\n").trimEnd()}
19269
+ `;
19270
+ }
19271
+ function openingBlock(entry) {
19272
+ const lines = entry.split("\n");
19273
+ const open = lines.findIndex((line) => line.startsWith(FENCE));
19274
+ const close = lines.findIndex((line, i) => i > open && line.startsWith(FENCE));
19275
+ if (open < 0 || close < 0) throw new PiPodError("a pipod manual entry does not open with a usage block");
19276
+ return `${lines.slice(open + 1, close).join("\n")}
19277
+ `;
19278
+ }
19279
+ function hasMoreThanBlock(entry) {
19280
+ const lines = entry.split("\n");
19281
+ const fences = lines.flatMap((line, i) => line.startsWith(FENCE) ? [i] : []);
19282
+ return lines.slice((fences[1] ?? lines.length) + 1).some((line) => line.trim() !== "");
19283
+ }
19284
+ function plainText(markdown) {
19285
+ const out2 = [];
19286
+ let fenced = false;
19287
+ for (const line of markdown.split("\n")) {
19288
+ if (line.startsWith(FENCE)) {
19289
+ fenced = !fenced;
19290
+ continue;
19291
+ }
19292
+ if (fenced) out2.push(line === "" ? "" : ` ${line}`);
19293
+ else out2.push(line.replace(/^#+ /, ""));
19294
+ }
19295
+ return `${out2.join("\n").trimEnd()}
19296
+ `;
19297
+ }
19298
+ function mainPage() {
19299
+ return read(path31.join(manDir(), MAIN_PAGE));
19300
+ }
19301
+ function manDir() {
19302
+ return path31.join(packageRoot(), "man");
19303
+ }
19304
+ function read(file) {
19305
+ try {
19306
+ return fs33.readFileSync(file, "utf8");
19307
+ } catch (cause) {
19308
+ throw new PiPodError(`the pipod manual is missing from this install (${file})`, {
19309
+ hint: "reinstall pipod: `npm install -g @pipod/cli`, or `npm run build` in a checkout",
19310
+ cause
19311
+ });
19312
+ }
19313
+ }
19314
+
19315
+ // src/scaffold.ts
19316
+ import * as fs34 from "fs";
19317
+ import * as path32 from "path";
19194
19318
  function configTemplate() {
19195
19319
  return `{
19196
19320
  // .pi-pod/config.json \u2014 optional server-side launch overrides for this project.
@@ -19232,8 +19356,8 @@ set -euo pipefail
19232
19356
  echo "init complete"
19233
19357
  `;
19234
19358
  function scaffold(opts) {
19235
- const dir = path31.join(opts.projectRoot, CONFIG_DIR);
19236
- fs33.mkdirSync(dir, { recursive: true });
19359
+ const dir = path32.join(opts.projectRoot, CONFIG_DIR);
19360
+ fs34.mkdirSync(dir, { recursive: true });
19237
19361
  const files = [
19238
19362
  { name: "config.json", contents: configTemplate() },
19239
19363
  { name: "env.example", contents: ENV_EXAMPLE_TEMPLATE },
@@ -19242,32 +19366,32 @@ function scaffold(opts) {
19242
19366
  const created = [];
19243
19367
  const skipped = [];
19244
19368
  for (const file of files) {
19245
- const target = path31.join(dir, file.name);
19246
- if (fs33.existsSync(target) && !opts.force) {
19247
- skipped.push(path31.join(CONFIG_DIR, file.name));
19369
+ const target = path32.join(dir, file.name);
19370
+ if (fs34.existsSync(target) && !opts.force) {
19371
+ skipped.push(path32.join(CONFIG_DIR, file.name));
19248
19372
  continue;
19249
19373
  }
19250
- fs33.writeFileSync(target, file.contents, file.mode ? { mode: file.mode } : void 0);
19251
- if (file.mode) fs33.chmodSync(target, file.mode);
19252
- created.push(path31.join(CONFIG_DIR, file.name));
19374
+ fs34.writeFileSync(target, file.contents, file.mode ? { mode: file.mode } : void 0);
19375
+ if (file.mode) fs34.chmodSync(target, file.mode);
19376
+ created.push(path32.join(CONFIG_DIR, file.name));
19253
19377
  }
19254
19378
  const gitignoreUpdated = ensureGitignoreEntry(opts.projectRoot, `${CONFIG_DIR}/env`);
19255
- const envPath = path31.join(dir, "env");
19256
- const envCreated = !fs33.existsSync(envPath);
19379
+ const envPath = path32.join(dir, "env");
19380
+ const envCreated = !fs34.existsSync(envPath);
19257
19381
  if (envCreated) {
19258
- fs33.writeFileSync(envPath, ENV_TEMPLATE, { mode: 384 });
19259
- fs33.chmodSync(envPath, 384);
19260
- created.push(path31.join(CONFIG_DIR, "env"));
19382
+ fs34.writeFileSync(envPath, ENV_TEMPLATE, { mode: 384 });
19383
+ fs34.chmodSync(envPath, 384);
19384
+ created.push(path32.join(CONFIG_DIR, "env"));
19261
19385
  }
19262
19386
  return { created, skipped, gitignoreUpdated, envCreated };
19263
19387
  }
19264
19388
  function ensureGitignoreEntry(projectRoot, entry) {
19265
- const gitignorePath = path31.join(projectRoot, ".gitignore");
19266
- const existing = fs33.existsSync(gitignorePath) ? fs33.readFileSync(gitignorePath, "utf8") : "";
19389
+ const gitignorePath = path32.join(projectRoot, ".gitignore");
19390
+ const existing = fs34.existsSync(gitignorePath) ? fs34.readFileSync(gitignorePath, "utf8") : "";
19267
19391
  const alreadyPresent = existing.split(/\r?\n/).map((line) => line.trim()).some((line) => line === entry || line === `/${entry}`);
19268
19392
  if (alreadyPresent) return false;
19269
19393
  const prefix = existing === "" || existing.endsWith("\n") ? "" : "\n";
19270
- fs33.appendFileSync(gitignorePath, `${prefix}
19394
+ fs34.appendFileSync(gitignorePath, `${prefix}
19271
19395
  # pi pod secrets \u2014 never commit
19272
19396
  ${entry}
19273
19397
  `);
@@ -19281,7 +19405,7 @@ function printInitResult(result, projectRoot) {
19281
19405
  const steps = [
19282
19406
  [`edit ${CONFIG_DIR}/config.json and init.sh`, "what this project's pods get"],
19283
19407
  [`edit ${CONFIG_DIR}/env`, "this project's own keys"],
19284
- ["pipod", `launch a pod for ${path31.basename(projectRoot)}; the first launch offers to keep these in a template`]
19408
+ ["pipod", `launch a pod for ${path32.basename(projectRoot)}; the first launch offers to keep these in a template`]
19285
19409
  ].map(([command, why]) => `${command.padEnd(40)} ${color.dim(`# ${why}`)}`);
19286
19410
  plain("");
19287
19411
  plain("Next steps:");
@@ -19291,8 +19415,8 @@ function printInitResult(result, projectRoot) {
19291
19415
 
19292
19416
  // src/update.ts
19293
19417
  import { spawnSync as spawnSync3 } from "child_process";
19294
- import * as fs34 from "fs";
19295
- import * as path32 from "path";
19418
+ import * as fs35 from "fs";
19419
+ import * as path33 from "path";
19296
19420
  function run(cmd, args, cwd) {
19297
19421
  const r = spawnSync3(cmd, args, { cwd, encoding: "utf8" });
19298
19422
  return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
@@ -19301,13 +19425,13 @@ function runVisible(cmd, args, cwd) {
19301
19425
  return spawnSync3(cmd, args, { cwd, stdio: "inherit" }).status === 0;
19302
19426
  }
19303
19427
  function detectInstall(root = packageRoot()) {
19304
- if (fs34.existsSync(path32.join(root, ".git"))) return { kind: "git", root };
19305
- if (!root.split(path32.sep).includes("node_modules") && run("git", ["rev-parse", "--is-inside-work-tree"], root).out === "true") {
19428
+ if (fs35.existsSync(path33.join(root, ".git"))) return { kind: "git", root };
19429
+ if (!root.split(path33.sep).includes("node_modules") && run("git", ["rev-parse", "--is-inside-work-tree"], root).out === "true") {
19306
19430
  return { kind: "git", root };
19307
19431
  }
19308
- if (root.split(path32.sep).includes("node_modules")) {
19432
+ if (root.split(path33.sep).includes("node_modules")) {
19309
19433
  try {
19310
- const pkg = JSON.parse(fs34.readFileSync(path32.join(root, "package.json"), "utf8"));
19434
+ const pkg = JSON.parse(fs35.readFileSync(path33.join(root, "package.json"), "utf8"));
19311
19435
  if (pkg.name) return { kind: "npm", root, name: pkg.name };
19312
19436
  } catch {
19313
19437
  }
@@ -19316,7 +19440,7 @@ function detectInstall(root = packageRoot()) {
19316
19440
  }
19317
19441
  function versionAt(root) {
19318
19442
  try {
19319
- const pkg = JSON.parse(fs34.readFileSync(path32.join(root, "package.json"), "utf8"));
19443
+ const pkg = JSON.parse(fs35.readFileSync(path33.join(root, "package.json"), "utf8"));
19320
19444
  return pkg.version ?? "0.0.0";
19321
19445
  } catch {
19322
19446
  return "0.0.0";
@@ -19391,16 +19515,16 @@ async function updateCheckout(root, opts) {
19391
19515
  return rebuild(root, before);
19392
19516
  }
19393
19517
  function distIsStale(root) {
19394
- const srcDir = path32.join(root, "src");
19395
- if (!fs34.existsSync(srcDir)) return false;
19396
- const dist = path32.join(root, "dist", "cli.js");
19397
- if (!fs34.existsSync(dist)) return true;
19398
- const distTime = fs34.statSync(dist).mtimeMs;
19518
+ const srcDir = path33.join(root, "src");
19519
+ if (!fs35.existsSync(srcDir)) return false;
19520
+ const dist = path33.join(root, "dist", "cli.js");
19521
+ if (!fs35.existsSync(dist)) return true;
19522
+ const distTime = fs35.statSync(dist).mtimeMs;
19399
19523
  const newest = (dir) => {
19400
19524
  let max = 0;
19401
- for (const entry of fs34.readdirSync(dir, { withFileTypes: true })) {
19402
- const p = path32.join(dir, entry.name);
19403
- max = Math.max(max, entry.isDirectory() ? newest(p) : fs34.statSync(p).mtimeMs);
19525
+ for (const entry of fs35.readdirSync(dir, { withFileTypes: true })) {
19526
+ const p = path33.join(dir, entry.name);
19527
+ max = Math.max(max, entry.isDirectory() ? newest(p) : fs35.statSync(p).mtimeMs);
19404
19528
  }
19405
19529
  return max;
19406
19530
  };
@@ -19421,8 +19545,8 @@ function rebuild(root, before) {
19421
19545
  const head = run("git", ["rev-parse", "--short", "HEAD"], root).out;
19422
19546
  plain("");
19423
19547
  info(after === before ? `now at ${color.bold(after)} (${head})` : `updated ${before} \u2192 ${color.bold(after)} (${head})`);
19424
- const upgrade = path32.join(run("git", ["rev-parse", "--show-toplevel"], root).out, "selfhost", "upgrade");
19425
- if (fs34.existsSync(upgrade)) out(`if this checkout runs your server, upgrade it to match: ${upgrade}`);
19548
+ const upgrade = path33.join(run("git", ["rev-parse", "--show-toplevel"], root).out, "selfhost", "upgrade");
19549
+ if (fs35.existsSync(upgrade)) out(`if this checkout runs your server, upgrade it to match: ${upgrade}`);
19426
19550
  out("run `pipod doctor` to check the new version against this repo");
19427
19551
  }
19428
19552
  async function updateFromRegistry(root, name, opts) {
@@ -19490,472 +19614,6 @@ var ALIASES = {
19490
19614
  a: "attach",
19491
19615
  ls: "list"
19492
19616
  };
19493
- var HELP = `pipod ${VERSION} \u2014 run pi sessions in server-managed pods
19494
-
19495
- Usage:
19496
- pipod [options] launch a pod for this project and start pi
19497
- pipod [options] -- <pi args\u2026> launch and pass arguments to pi: prompt words,
19498
- --model <id>, --thinking <level>, --tui-mode <mode>,
19499
- -e/--extension <path>, --skill <path>,
19500
- --prompt-template <path> (each repeatable)
19501
- pipod <command> [options] run a command
19502
- pipod help [command] show help (same as --help)
19503
-
19504
- Sign in first with \`pipod login\`. A pod-scoped PI_POD_SERVER_TOKEN also counts as
19505
- signed in. While signed out, only login, whoami, init, doctor, update, help, and
19506
- version are available.
19507
-
19508
- Pod commands:
19509
- list [pod] List pods; \`list <pod>\` shows one machine's group (ls)
19510
- attach [pod] Reconnect to a pod session (a)
19511
- stop [pod\u2026] Stop pods now (files kept; attach restarts them)
19512
- fork <pod> Launch a new pod forked from a pod's session
19513
- rename <pod> <name> Rename a pod
19514
- send [pod] <path> Copy host files into a pod
19515
- receive [pod] <path> Copy pod files into the current directory
19516
- archive [pod\u2026] Hide pods from the list (files kept; restore brings them back)
19517
- restore [pod\u2026] Bring archived pods back
19518
- gc Report retention state; --delete removes reclaimable pods
19519
-
19520
- Account commands:
19521
- login Sign in to a pi pod server via Zitadel
19522
- logout Revoke this session and delete the stored credentials
19523
- whoami Show the signed-in account
19524
- account Open the Zitadel self-service console
19525
- org-admin Open the Zitadel Console for this organization
19526
- billing Open Stripe Checkout or the billing portal (authenticated)
19527
- credentials Connect account model-provider sign-ins
19528
-
19529
- Configuration commands:
19530
- init Scaffold .pi-pod/ config, env, env.example, and init.sh
19531
- templates Manage account templates
19532
- push Upload a local config layer to its server bundle
19533
- pull Write a server bundle into its local config layer
19534
- diff Show local vs server drift for a config layer
19535
- secrets Manage server and project secret scopes
19536
- settings Show or edit one server settings config directly
19537
- jobs Manage server-scheduled jobs
19538
-
19539
- Maintenance commands:
19540
- doctor Check auth, server, org, and launch-resolution layers
19541
- update Upgrade pi pod itself
19542
-
19543
- Launch options:
19544
- --template <name|id> Launch from an account template
19545
- --on <pod|self> Co-locate the new pod on an existing pod's machine
19546
- --reuse Restart this project's newest stopped pod instead of creating one
19547
- --no-seed Start the pod with an empty workspace (skip workspace seeding)
19548
- --dry-run Resolve and print the launch plan; create nothing
19549
-
19550
- General options:
19551
- -y, --yes Skip supported confirmations
19552
- -v, --verbose Debug logging (secrets redacted)
19553
- --version Print CLI, Node, and bundled Pi versions
19554
- -h, --help Show help
19555
-
19556
- Settings resolve on the server: built-in \u2192 org \u2192 org-template \u2192 user \u2192 user-template
19557
- \u2192 org policy. Each bundle has a local directory as its source of truth (see
19558
- \`pipod push --help\`); local project settings apply after \`pipod push\`, and the
19559
- first launch in a project offers to bootstrap a template.
19560
-
19561
- Per-launch Pi resources (--extension, --skill, --prompt-template) name files or
19562
- directories inside the pod as absolute paths and add to the pod's configured Pi
19563
- resources for that launch; anything else Pi-related belongs in the pi.args config.
19564
-
19565
- Workspace seeding: launching from a directory with no .pi-pod/config.json seeds the
19566
- new pod from that directory (its git repository root when inside one). A clean,
19567
- pushed checkout whose remote tip matches HEAD is cloned at that exact commit;
19568
- anything else \u2014 dirty trees, unpushed commits, private remotes without usable
19569
- credentials, plain directories \u2014 is streamed in as a tar archive (git-ignored
19570
- paths, node_modules and .pi-pod/env excluded). Private-remote credentials are
19571
- forwarded only after a prompt (\`--yes\` approves it) and are never stored.
19572
- \`--dry-run\` prints the decision without prompting or creating a pod. Forks,
19573
- reused pods, \`--on\` co-location and \`--no-seed\` never seed.
19574
-
19575
- Run \`pipod <command> --help\` for command-specific usage.
19576
- `;
19577
- var SUBCOMMAND_HELP = {
19578
- login: `Usage: pipod login [--server <url>] [--issuer <url>] [--org <alias>] [--device] [--token <jwt>]
19579
-
19580
- Sign this machine in to a pi pod server via Zitadel (authorization code + PKCE,
19581
- in your browser). Where no browser can open here (over SSH, on a headless
19582
- server), it prints a page and a code to confirm on any other device instead.
19583
- The session is stored in ~/.pi-pod/auth.json.
19584
-
19585
- Options:
19586
- --server <url> Server to sign in to (default: the previous session's server,
19587
- $PI_POD_ACCOUNT_URL, or the production server)
19588
- --issuer <url> OIDC issuer (default: $PI_POD_ISSUER, else the one the
19589
- server publishes); needed only for a server too old to
19590
- publish its own
19591
- --org <alias> Only sign in to the organization with this primary domain;
19592
- omit it to sign in to your own organization
19593
- --device Sign in with a code on another device even if a browser
19594
- could open here
19595
- --token <jwt> Use an existing access token instead of the browser flow (dev/CI)
19596
- `,
19597
- account: `Usage: pipod account
19598
-
19599
- Open the Zitadel self-service console in your browser to manage profile,
19600
- password, MFA, and active sessions.
19601
- `,
19602
- "org-admin": `Usage: pipod org-admin
19603
-
19604
- Open the Zitadel Console in your browser. Organization owners manage members
19605
- and role grants there.
19606
- `,
19607
- billing: `Usage: pipod billing checkout [--plan standard|pro] [--trial]
19608
- pipod billing portal
19609
- pipod billing change --plan standard|pro [--yes]
19610
- pipod billing change preview --plan standard|pro
19611
- pipod billing change confirm --quote <quoteId> [--yes]
19612
-
19613
- Authenticated Stripe Checkout and billing portal. Checkout sends trial:false
19614
- (paid) unless --trial. Closed public trial refuses --trial with 402. Change
19615
- previews a Standard<->Pro plan change and confirms it only after an explicit
19616
- yes; stale quotes are never auto-confirmed. Return
19617
- pages do not grant a plan. Public npm is not a supported install path; use a
19618
- GitHub release tarball or git clone + npm run build.
19619
- `,
19620
- logout: `Usage: pipod logout
19621
-
19622
- Revoke the current account session and delete ~/.pi-pod/auth.json. Pod
19623
- operations are unavailable until the next \`pipod login\`.
19624
- `,
19625
- whoami: `Usage: pipod whoami
19626
-
19627
- Show the signed-in server, user, permissions, active organization, and console
19628
- links. Exits 1 when signed out.
19629
- `,
19630
- init: `Usage: pipod init [--force]
19631
-
19632
- Scaffold project configuration under .pi-pod/ in the current directory:
19633
- config.json, env.example, env (mode 600), and init.sh.
19634
-
19635
- Options:
19636
- --force Overwrite scaffold files that already exist
19637
- `,
19638
- doctor: `Usage: pipod doctor [--template <name|id>]
19639
-
19640
- Check each layer a launch depends on: account auth, server reachability and
19641
- version, organization membership and roles, secret references, and the
19642
- server-resolved settings bundle chain for this directory. Works while signed
19643
- out to diagnose auth problems. Exits non-zero when a layer fails.
19644
-
19645
- Options:
19646
- --template <name|id> Plan the checked launch with this template
19647
- `,
19648
- list: `Usage: pipod list [pod] [--all] [--archived] [--template <name|id>] [--quiet]
19649
-
19650
- Alias: ls
19651
-
19652
- List pods on the signed-in server with stable short refs (p-\u2026). The project is
19653
- the directory's git repository (or the directory, or its .pi-pod/ config): a
19654
- listing shows that project's pods when it has any \u2014 its pinned template's when
19655
- it pins one \u2014 and every pod otherwise. Co-located pods are grouped under the pod
19656
- whose machine they share, with an ON column naming it.
19657
- With a pod argument, show just that machine's group: the pod and everything
19658
- co-located on it.
19659
-
19660
- Status says where a pod's files are: a stopped pod keeps them on disk and restarts on
19661
- attach; an archived workspace is restored on next use, which takes longer the bigger it is.
19662
- Pods you archive stay hidden unless --archived is passed.
19663
-
19664
- Options:
19665
- -a, --all Every pod; ignore the current project and its template pin
19666
- --archived Include logically archived pods
19667
- --template <name|id> Only pods launched from that template (whole account)
19668
- -q, --quiet Print pod ids only, one per line
19669
- `,
19670
- attach: `Usage: pipod attach [pod] [--yes] [-- <prompt\u2026> [--tui-mode <mode>]]
19671
-
19672
- Alias: a
19673
-
19674
- Reconnect to a pod session through the server gateway. Name the pod by id,
19675
- short ref (p-\u2026), or name; omit it when this project has exactly one pod.
19676
- Quitting detaches: the pod keeps running until the server's idle policy reaps
19677
- it or \`pipod stop\` stops it. Raw pod shells are not available.
19678
- Attaching to an archived pod offers to restore it first.
19679
-
19680
- After --:
19681
- <prompt\u2026> Words are sent to the session as a startup prompt
19682
- --tui-mode <mode> Local TUI layout for this attach: regular or fullscreen
19683
-
19684
- Options:
19685
- -y, --yes Restore an archived pod without asking
19686
- `,
19687
- stop: `Usage: pipod stop [pod\u2026]
19688
-
19689
- Stop pods now by id, short ref (p-\u2026), or name; omit the argument when this
19690
- project has exactly one pod. Stopped pods keep their files and \`pipod attach\`
19691
- restarts them. Stopping a pod that hosts co-located pods stops those too.
19692
- `,
19693
- fork: `Usage: pipod fork <pod> [--session <path>] [launch options] [-- <pi args\u2026>]
19694
-
19695
- Launch a new pod whose session starts as a fork of <pod>'s current session.
19696
- <pod> is an id, short ref (p-\u2026), or name. The workspace is not copied \u2014
19697
- commit and push first if the new pod needs those changes. --on is not
19698
- supported: a fork launches a new sandbox, not a co-located pod.
19699
-
19700
- Options:
19701
- --session <path> Fork a specific session file instead of the newest
19702
- --template <name|id> Launch the new pod from an account template
19703
- --dry-run Resolve and print the launch plan; create nothing
19704
- -y, --yes Skip supported confirmations
19705
- `,
19706
- rename: `Usage: pipod rename <pod> <name>
19707
-
19708
- Rename a server-managed pod. <pod> is an id, short ref (p-\u2026), or the current
19709
- name.
19710
- `,
19711
- send: `Usage: pipod send [pod] <path> [--yes]
19712
-
19713
- Copy a host file or directory into a pod through the server. Omit the pod when
19714
- this project has exactly one. Sending to an archived pod offers to restore it
19715
- first.
19716
-
19717
- Options:
19718
- -y, --yes Restore an archived pod without asking
19719
- `,
19720
- receive: `Usage: pipod receive [pod] <path> [--yes]
19721
-
19722
- Copy a file or directory from a pod into the current local directory. <path>
19723
- may be a workdir-relative path or an absolute path inside that workdir, such
19724
- as foo or /workspace/foo. The destination must not already exist. Omit the
19725
- pod when this project has exactly one. Receiving from an archived pod offers
19726
- to restore it first.
19727
-
19728
- Options:
19729
- -y, --yes Restore an archived pod without asking
19730
- `,
19731
- archive: `Usage: pipod archive [pod\u2026] [--all] [--template <name|id>] [--idle <duration>] [--dry-run] [--yes]
19732
-
19733
- Archive pods by id, short ref (p-\u2026), or name. Archiving hides pods from
19734
- \`pipod list\` (\`pipod list --archived\` shows them); it never deletes work, and
19735
- \`pipod restore\` brings them back. Archiving a pod that hosts co-located pods
19736
- archives those too.
19737
-
19738
- Options:
19739
- -a, --all Archive every pod (cannot be combined with pod refs)
19740
- --template <name|id> Only pods launched from that template (whole account)
19741
- --idle <duration> Only pods whose last activity is older than this (4h, 2d, 30m)
19742
- --dry-run Print the matching pods; archive nothing
19743
- -y, --yes Skip the bulk confirmation
19744
- `,
19745
- restore: `Usage: pipod restore [pod\u2026] [--all] [--yes]
19746
-
19747
- Bring archived pods back by id, short ref (p-\u2026), or name. Restoring a host
19748
- restores the co-located pods archived with it. \`pipod attach\` then starts the
19749
- pod; a workspace in cold storage takes longer the bigger it is.
19750
-
19751
- Options:
19752
- -a, --all Restore every archived pod (cannot be combined with pod refs)
19753
- -y, --yes Skip the --all confirmation
19754
- `,
19755
- gc: `Usage: pipod gc [--delete] [--yes]
19756
-
19757
- List the archived and failed pods that can be deleted for good; --delete deletes
19758
- them. (The server stops idle pods and moves stopped workspaces to cold storage on
19759
- its own schedule; that never deletes anything.)
19760
-
19761
- Options:
19762
- --delete Permanently delete the reclaimable pods
19763
- -y, --yes Delete without a confirmation prompt
19764
- `,
19765
- jobs: `Usage: pipod jobs [action] [args] [--quiet] [--yes] [--project] [--org]
19766
-
19767
- Manage scheduled jobs the pi pod server runs for you: at each trigger it
19768
- launches a pod from the job's template and sends the job's prompt. Local job
19769
- files in .pi-pod/jobs/ (project) or ~/.pi-pod/jobs/ (user) are the reviewable
19770
- source; \`jobs push\` promotes them to the server. <job> is an id, short ref
19771
- (j-\u2026), or name.
19772
-
19773
- Actions:
19774
- list List server jobs (default)
19775
- show <job> Show a job's schedule, template, model, and prompt
19776
- runs <job> List a job's recent runs
19777
- run <job> Run an active job once, now; its schedule is unchanged
19778
- activate <job> Activate a job
19779
- pause <job> Pause a job's schedule
19780
- resume <job> Resume a paused job
19781
- rm <job> Remove a job; its schedule stops firing
19782
- push [job\u2026] Create or update server jobs from local job files
19783
- pull <job> Write a server job to a local job file
19784
- diff [job\u2026] Show local vs server drift; exits 1 when drifted
19785
-
19786
- Options:
19787
- --org push: create org-scoped jobs or promote matching user jobs
19788
- (promotion is one-way)
19789
- --project pull: write into this project's .pi-pod/jobs/ instead of
19790
- ~/.pi-pod/jobs/
19791
- -q, --quiet list: print job ids only, one per line
19792
- -y, --yes Skip confirmations; pull: overwrite an existing local file
19793
- `,
19794
- credentials: `Usage: pipod credentials [action]
19795
-
19796
- Manage account-scoped model-provider sign-ins. Connected credentials apply to
19797
- every pod you launch; OAuth refresh tokens stay in server custody and never
19798
- reach pods or this machine.
19799
-
19800
- Actions:
19801
- list Saved credentials and connectable providers (default)
19802
- connect Connect every provider your local pi is signed in to:
19803
- API keys are imported, everything else is a fresh
19804
- account login; ready credentials are left alone
19805
- connect <provider> Sign in to a provider (OAuth by default)
19806
- reconnect <provider> Redo the sign-in for a saved credential
19807
- test <provider> Verify a saved credential against the provider
19808
- remove <provider> [--yes] Delete a saved credential (alias: rm)
19809
-
19810
- connect options:
19811
- --api-key Enter an API key instead of the OAuth flow
19812
- --api-key --from-pi Import the API key your local pi resolves for that
19813
- provider (auth.json or env) instead of typing it.
19814
- OAuth grants are never imported.
19815
- `,
19816
- secrets: `Usage: pipod secrets [action]
19817
-
19818
- Manage the secret layers pods see. Requires account or pod-token auth. Server
19819
- scopes are org, user, and template/<name>; project is this project's dotenv
19820
- file, the highest-precedence layer for launches from the project.
19821
-
19822
- Actions:
19823
- list Every secret a pod launched here would see,
19824
- which layer wins, and who set it (default)
19825
- set <scope> <NAME> Store a secret, its value read from a hidden
19826
- prompt or stdin (never the command line)
19827
- rm <scope> <NAME> Remove a secret (aliases: remove, unset)
19828
- sync <scope> Upload a dotenv file to a server scope
19829
-
19830
- sync options (scope must be org, user, or template/<name>):
19831
- --file <path> Dotenv file to upload (default: ~/.pi-pod/secrets/<scope>.env)
19832
- --prune Also delete server secrets that are not in the file
19833
- --dry-run Show what would change without writing
19834
-
19835
- Values may be op:// references; they resolve just-in-time (at upload for
19836
- server scopes, at launch for the project file) and the references themselves
19837
- are never stored on the server.
19838
- `,
19839
- templates: `Usage: pipod templates [action]
19840
-
19841
- Manage pod templates on the pi pod server. A template stores config, init and
19842
- bake scripts, Pi settings files, named secrets, and agent instructions.
19843
- Templates are personal by default: only their owner sees and launches them.
19844
- --org at creation or \`templates share\` makes one org-wide.
19845
-
19846
- Agent instructions are added to the agent's system prompt in every pod launched
19847
- from the template, with the hosts and secrets the pod actually has: say what
19848
- access the template is meant to have. They guide the agent and enforce nothing;
19849
- the template's secrets and egress decide what a pod can reach. A pod's child
19850
- pods and the jobs it schedules launch from the pod's own template.
19851
-
19852
- Actions:
19853
- list List templates with scope and key counts (default)
19854
- show <name|id> Show config, scripts, agent instructions, Pi settings, and
19855
- secret names
19856
- create <name> Create a template
19857
- edit <name|id> Update a template's name, description, config, scripts, or
19858
- agent instructions
19859
- share <name|id> Make a personal template org-wide (cannot be undone)
19860
- rm <name|id> Delete a template (alias: delete)
19861
-
19862
- create/edit options:
19863
- --name <name> New name (edit only)
19864
- --description <text> Set the description
19865
- --config <file> Store this config file (same shape as .pi-pod/config.json)
19866
- --from-here create only: store this project's authored config,
19867
- scripts, and sanitized Pi files (contradicts --config)
19868
- --init-script <file> Store this init script
19869
- --bake-script <file> Store this bake script
19870
- --agent-instructions <file>
19871
- Store this file as the agent instructions (an empty
19872
- file removes them); \`pipod push\` leaves them alone
19873
- --org create only: make the template org-wide from the start
19874
- --with-secrets With --from-here: also upload project env values as
19875
- template secrets
19876
- -y, --yes Skip confirmations (rm)
19877
- `,
19878
- push: `Usage: pipod push [template [<name>] | user | org] [--dir <path>]
19879
- [--with-secrets] [--yes]
19880
-
19881
- Replace a server config bundle with its local source, whole. The layer names
19882
- both sides; edit the local files with any editor, then push. A diff preview
19883
- precedes the confirmation.
19884
-
19885
- Layers and their local source:
19886
- template [<name>] This project: .pi-pod/config.json, its init and bake
19887
- scripts, Pi files under .pi/, and the project env file
19888
- (secrets, with --with-secrets or a prompt). Without
19889
- <name> the project's pinned template is used.
19890
- user ~/.pi-pod/config.json and ~/.pi/agent/ (settings.json,
19891
- models.json, mcporter.json, subagents.json, agents/)
19892
- org ~/.pi-pod/org/: config.json, init.sh, bake.sh, pi/\u2026, and
19893
- an optional env file whose values become org secrets
19894
-
19895
- Inside a project the layer defaults to template; elsewhere it is required.
19896
- Provider credentials, hooks, mcpServers, and model credential values never
19897
- leave this machine.
19898
-
19899
- Options:
19900
- --dir <path> org only: read the org source from <path> instead
19901
- --with-secrets Also upload the layer's env values as secrets
19902
- -y, --yes Apply the replacement without the confirmation prompt
19903
- `,
19904
- pull: `Usage: pipod pull [template [<name>] | user | org] [--dir <path>] [--yes]
19905
-
19906
- Write a server config bundle into its local source files (see
19907
- \`pipod push --help\` for the layer \u2192 directory table). A diff preview
19908
- precedes the confirmation; nothing is written when the two already match.
19909
-
19910
- What pull keeps from the local files:
19911
- config.json client-only keys: $schema, template, secretResolver,
19912
- pi.chords, pi.sessionNaming
19913
- settings.json hooks and mcpServers (and packages when hostConfig.packages
19914
- is off)
19915
- models.json credential values the push had dropped
19916
- agents/ files the bundle no longer contains are removed
19917
-
19918
- Secrets stay on the server; their names are listed. The user layer has no
19919
- local script files, so user bundle scripts are shown but not written.
19920
-
19921
- Options:
19922
- --dir <path> org only: write the org source to <path> instead
19923
- -y, --yes Apply the local changes without the confirmation prompt
19924
- `,
19925
- diff: `Usage: pipod diff [template [<name>] | user | org] [--dir <path>]
19926
-
19927
- Compare a config layer's local source with its server bundle (see
19928
- \`pipod push --help\` for the layer \u2192 directory table). Secrets compare by
19929
- name only. Exits 1 when they differ, 0 when they match.
19930
-
19931
- Options:
19932
- --dir <path> org only: read the org source from <path> instead
19933
- `,
19934
- settings: `Usage: pipod settings <user|org> [show | edit | set <key> <value> | unset <key>]
19935
-
19936
- Show or edit a persistent settings bundle on the server: your own user bundle,
19937
- or the org-wide defaults (org scope needs the org:manage permission). Edits
19938
- change only the config; scripts and Pi files stay as they are \u2014 replace the
19939
- whole bundle from local files with \`pipod push user\` or \`pipod push org\`.
19940
- Edits use compare-and-swap on the bundle version, so concurrent changes fail
19941
- cleanly instead of overwriting each other.
19942
-
19943
- Actions:
19944
- show Print the settings bundle as JSON (default)
19945
- edit Edit the config in $VISUAL/$EDITOR, then save
19946
- set <key> <value> Set one dotted key to a JSON value, or to the text as
19947
- a string: pipod settings user set pi.model provider/model
19948
- unset <key> Remove one dotted key, so the layers below decide it
19949
- `,
19950
- update: `Usage: pipod update [--dry-run]
19951
-
19952
- Upgrade pi pod itself: a registry install updates to the latest published
19953
- version; a git checkout pulls its upstream and rebuilds.
19954
-
19955
- Options:
19956
- --dry-run Report what an update would do without changing anything
19957
- `
19958
- };
19959
19617
  var SELF_PARSING_SUBCOMMANDS = /* @__PURE__ */ new Set(["credentials", "secrets", "templates", "push", "pull", "diff", "settings", "billing"]);
19960
19618
  var UNIVERSAL_FLAGS = /* @__PURE__ */ new Set(["--verbose", "--version", "--help"]);
19961
19619
  var FLAG_SCOPE = {
@@ -20131,15 +19789,22 @@ function parseArgs(argv) {
20131
19789
  });
20132
19790
  }
20133
19791
  }
20134
- let commandPositionals = positionals;
20135
19792
  if (positionals[0] === "help") {
20136
19793
  flags.help = true;
20137
19794
  seenFlags.add("--help");
20138
- commandPositionals = positionals.slice(1);
19795
+ const name = positionals[1];
19796
+ return {
19797
+ subcommand: null,
19798
+ subcommandArgs: [],
19799
+ flags,
19800
+ piArgs,
19801
+ seenFlags,
19802
+ ...name !== void 0 ? { helpEntry: resolveSubcommand(name) ?? name } : {}
19803
+ };
20139
19804
  }
20140
- const subcommand = resolveSubcommand(commandPositionals[0]);
20141
- if (!subcommand && commandPositionals.length > 0) {
20142
- throw new PiPodError(`unknown pi pod command "${commandPositionals[0]}"`, {
19805
+ const subcommand = resolveSubcommand(positionals[0]);
19806
+ if (!subcommand && positionals.length > 0) {
19807
+ throw new PiPodError(`unknown pi pod command "${positionals[0]}"`, {
20143
19808
  hint: 'pass all Pi arguments after `--`, for example: pipod -- "review this change"',
20144
19809
  exitCode: EXIT.USAGE
20145
19810
  });
@@ -20152,7 +19817,7 @@ function parseArgs(argv) {
20152
19817
  }
20153
19818
  return {
20154
19819
  subcommand,
20155
- subcommandArgs: subcommand ? commandPositionals.slice(1) : [],
19820
+ subcommandArgs: subcommand ? positionals.slice(1) : [],
20156
19821
  flags,
20157
19822
  piArgs,
20158
19823
  seenFlags
@@ -20175,10 +19840,14 @@ function canonicalFlagName(name) {
20175
19840
  }
20176
19841
  }
20177
19842
  async function main(argv) {
20178
- const { subcommand, subcommandArgs, flags, piArgs, seenFlags } = parseArgs(argv);
19843
+ const { subcommand, subcommandArgs, flags, piArgs, seenFlags, helpEntry } = parseArgs(argv);
20179
19844
  setVerbose(flags.verbose);
20180
19845
  if (flags.help) {
20181
- process.stdout.write(subcommand ? SUBCOMMAND_HELP[subcommand] : HELP);
19846
+ if (helpEntry !== void 0) process.stdout.write(manualEntry(helpEntry));
19847
+ else if (subcommand) process.stdout.write(usage(subcommand));
19848
+ else process.stdout.write(`pipod ${VERSION} \u2014 run pi sessions in server-managed pods
19849
+
19850
+ ${usage(null)}`);
20182
19851
  return EXIT.OK;
20183
19852
  }
20184
19853
  for (const flag of checkFlagScope(subcommand, seenFlags)) {
@@ -20291,7 +19960,7 @@ function announceUserConfig() {
20291
19960
  function runInit(flags) {
20292
19961
  const cwd = process.cwd();
20293
19962
  const existing = findConfigPath(cwd);
20294
- if (existing !== null && path33.dirname(path33.dirname(existing)) !== fs35.realpathSync(cwd)) {
19963
+ if (existing !== null && path34.dirname(path34.dirname(existing)) !== fs36.realpathSync(cwd)) {
20295
19964
  info(`note: ${color.bold(displayPath(existing))} defines an enclosing project; this scaffold shadows it here`);
20296
19965
  }
20297
19966
  const result = scaffold({ projectRoot: cwd, force: flags.force });
@@ -20319,7 +19988,7 @@ function reportError(errorValue) {
20319
19988
  if (errorValue instanceof Error && errorValue.stack) plain(color.dim(errorValue.stack));
20320
19989
  return EXIT.FAILURE;
20321
19990
  }
20322
- var invokedDirectly = process.argv[1] !== void 0 && ["cli.js", "cli.ts", "pipod", "pi-pod", "pp"].includes(path33.basename(process.argv[1]));
19991
+ var invokedDirectly = process.argv[1] !== void 0 && ["cli.js", "cli.ts", "pipod", "pi-pod", "pp"].includes(path34.basename(process.argv[1]));
20323
19992
  if (invokedDirectly) {
20324
19993
  for (const stream of [process.stdout, process.stderr]) {
20325
19994
  stream.on("error", (failure) => {