@co0ontty/wand 4.27.0 → 4.27.1-beta.gafcd211

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/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  ### Overview
13
13
 
14
- Wand is a web console for remotely accessing and managing local CLI tools from a browser. It supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai/), [Grok Build](https://grok.com/), and [Qoder CLI](https://docs.qoder.com/en/cli/quick-start), with terminal and structured conversation views, persistent resumable sessions, permission controls, file browsing, and native clients for multiple platforms.
14
+ Wand is a web console for remotely accessing and managing local CLI tools from a browser. It supports [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), [OpenCode](https://opencode.ai/), [Grok Build](https://grok.com/), [Qoder CLI](https://docs.qoder.com/en/cli/quick-start), and [Pi](https://pi.dev/), with terminal and structured conversation views, persistent resumable sessions, permission controls, file browsing, and native clients for multiple platforms.
15
15
 
16
16
  The browser password manager extension source lives in `browser-extension/`; setup and supported baseline features are documented in [docs/browser-extension.md](docs/browser-extension.md).
17
17
 
@@ -56,7 +56,7 @@ wand config:password
56
56
  #### Core
57
57
 
58
58
  - **Dual view modes** — switch between raw terminal output and a structured conversation view for the same session
59
- - **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, OpenCode, Grok, and Qoder CLI
59
+ - **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, OpenCode, Grok, Qoder CLI, and Pi
60
60
  - **Session management** — create, archive, and resume Claude, Codex, OpenCode, Grok, and Qoder sessions with their provider-native context; restore Claude/Codex native history; show summaries in the session list
61
61
  - **Permission control** — visual permission prompts with one-time approval, per-turn memory, and related policies
62
62
 
@@ -73,7 +73,7 @@ wand config:password
73
73
  - **Native clients** — Android, iOS, and macOS clients with encrypted connection codes and update checks
74
74
  - **HTTPS** — optional self-signed certificates for remote or mobile access
75
75
  - **Update channels** — built-in stable/beta update checks and upgrade prompts
76
- - **CLI updates** — check and quickly update Claude Code, Codex, and OpenCode from server settings, with optional automatic updates
76
+ - **CLI updates** — check and quickly update Claude Code, Codex, OpenCode, Qoder CLI, and Pi from server settings, with optional automatic updates
77
77
 
78
78
  ### Configuration
79
79
 
@@ -155,7 +155,7 @@ npm install -g opencode-ai@latest
155
155
 
156
156
  ### 概览
157
157
 
158
- 通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。支持 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Codex](https://github.com/openai/codex)、[OpenCode](https://opencode.ai/)、[Grok Build](https://grok.com/)[Qoder CLI](https://docs.qoder.com/zh/cli/quick-start),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
158
+ 通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。支持 [Claude Code](https://docs.anthropic.com/en/docs/claude-code)、[Codex](https://github.com/openai/codex)、[OpenCode](https://opencode.ai/)、[Grok Build](https://grok.com/)[Qoder CLI](https://docs.qoder.com/zh/cli/quick-start) 和 [Pi](https://pi.dev/),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
159
159
 
160
160
  <p align="center">
161
161
  <img src="docs/screenshots/clients-overview.png" width="100%" alt="Web 居中,iOS 和 Android 客户端在两侧的多端详情总览" />
@@ -205,7 +205,7 @@ npm install -g opencode-ai@latest
205
205
  #### 核心
206
206
 
207
207
  - **双视图模式** — 终端原始输出和结构化对话视图可随时切换,同一会话两种呈现
208
- - **多 Provider 支持** — Claude Code、Codex、OpenCode、Grok 和 Qoder CLI 均可创建 PTY 或结构化会话
208
+ - **多 Provider 支持** — Claude Code、Codex、OpenCode、Grok、Qoder CLI Pi 均可创建 PTY 或结构化会话
209
209
  - **会话管理** — 创建、归档并携带原生上下文恢复 Claude、Codex、OpenCode、Grok、Qoder 会话;支持恢复 Claude/Codex 原生历史记录;会话列表显示摘要
210
210
  - **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略
211
211
 
@@ -222,7 +222,7 @@ npm install -g opencode-ai@latest
222
222
  - **多平台客户端** — Android / iOS / macOS 原生客户端,支持加密连接码分发、自动更新检查
223
223
  - **HTTPS** — 可选自签证书,适合远程或移动端访问
224
224
  - **版本管理** — 内置更新检查与升级提示,支持 stable/beta 双通道
225
- - **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode,也可开启自动更新
225
+ - **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode、Qoder CLI 和 Pi,也可开启自动更新
226
226
 
227
227
  ### 截图
228
228
 
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "e553e5bcdd0f84a79e0f4cf213cb8295362b71c9",
3
- "builtAt": "2026-07-30T01:43:06.002Z",
4
- "version": "4.27.0",
5
- "channel": "stable"
2
+ "commit": "afcd2110a96cf8e1413df69d7ed1a14e2a76a6b3",
3
+ "builtAt": "2026-08-02T10:51:41.209Z",
4
+ "version": "4.27.1-beta.gafcd211",
5
+ "channel": "beta"
6
6
  }
package/dist/config.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { WandStorage } from "./storage.js";
7
7
  * 升级路径:老 JSON 里仍存有这些字段时,首次启动会被搬到 DB(见 migrateLegacyPreferencesToDb),
8
8
  * 然后下一次 saveConfig 写回 JSON 时它们会被剥离(见 stripPreferenceFields)。
9
9
  */
10
- export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
10
+ export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "defaultPiModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
11
11
  export type PreferenceKey = (typeof PREFERENCE_KEYS)[number];
12
12
  export declare function isPreferenceKey(key: string): key is PreferenceKey;
13
13
  export declare const defaultConfig: () => WandConfig;
@@ -48,11 +48,12 @@ export declare function writePreferenceToStorage(config: WandConfig, storage: Wa
48
48
  export declare function validateCommitAiConfig(_config: Pick<WandConfig, "commitAiSource" | "systemAi">): void;
49
49
  export declare function normalizeCardDefaults(input: unknown): CardExpandDefaults;
50
50
  export declare function isExecutionMode(value: unknown): value is ExecutionMode;
51
- export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel">): {
51
+ export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel">): {
52
52
  claude: string;
53
53
  codex: string;
54
54
  opencode: string;
55
55
  grok: string;
56
56
  qoder: string;
57
+ pi: string;
57
58
  };
58
- export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel">, provider: SessionProvider | undefined): string;
59
+ export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel">, provider: SessionProvider | undefined): string;
package/dist/config.js CHANGED
@@ -31,6 +31,7 @@ export const PREFERENCE_KEYS = [
31
31
  "defaultOpenCodeModel",
32
32
  "defaultGrokModel",
33
33
  "defaultQoderModel",
34
+ "defaultPiModel",
34
35
  "commitCli",
35
36
  "commitModel",
36
37
  "commitAiSource",
@@ -75,6 +76,7 @@ export const defaultConfig = () => ({
75
76
  defaultOpenCodeModel: "",
76
77
  defaultGrokModel: "",
77
78
  defaultQoderModel: "",
79
+ defaultPiModel: "",
78
80
  commitCli: "claude",
79
81
  commitModel: "",
80
82
  commitAiSource: "cli",
@@ -285,7 +287,7 @@ export function applyStoragePreferences(config, storage) {
285
287
  const defaults = defaultConfig();
286
288
  if (storage.hasPreference(preferenceStorageKey("defaultProvider"))) {
287
289
  const v = storage.getPreference(preferenceStorageKey("defaultProvider"), defaults.defaultProvider ?? "claude");
288
- if (v === "claude" || v === "codex" || v === "opencode" || v === "grok" || v === "qoder")
290
+ if (v === "claude" || v === "codex" || v === "opencode" || v === "grok" || v === "qoder" || v === "pi")
289
291
  config.defaultProvider = v;
290
292
  }
291
293
  if (storage.hasPreference(preferenceStorageKey("defaultSessionKind"))) {
@@ -328,6 +330,11 @@ export function applyStoragePreferences(config, storage) {
328
330
  if (typeof v === "string")
329
331
  config.defaultQoderModel = v.trim();
330
332
  }
333
+ if (storage.hasPreference(preferenceStorageKey("defaultPiModel"))) {
334
+ const v = storage.getPreference(preferenceStorageKey("defaultPiModel"), defaults.defaultPiModel ?? "");
335
+ if (typeof v === "string")
336
+ config.defaultPiModel = v.trim();
337
+ }
331
338
  if (storage.hasPreference(preferenceStorageKey("commitCli"))) {
332
339
  const v = storage.getPreference(preferenceStorageKey("commitCli"), defaults.commitCli ?? "claude");
333
340
  if (v === "claude" || v === "codex" || v === "opencode")
@@ -378,7 +385,7 @@ export function writePreferenceToStorage(config, storage, key, value, options =
378
385
  const dbKey = preferenceStorageKey(key);
379
386
  switch (key) {
380
387
  case "defaultProvider": {
381
- if (value !== "claude" && value !== "codex" && value !== "opencode" && value !== "grok" && value !== "qoder")
388
+ if (value !== "claude" && value !== "codex" && value !== "opencode" && value !== "grok" && value !== "qoder" && value !== "pi")
382
389
  throw new Error(`无效 Provider: ${value}`);
383
390
  storage.setPreference(dbKey, value);
384
391
  config.defaultProvider = value;
@@ -434,6 +441,12 @@ export function writePreferenceToStorage(config, storage, key, value, options =
434
441
  config.defaultQoderModel = v;
435
442
  break;
436
443
  }
444
+ case "defaultPiModel": {
445
+ const v = typeof value === "string" ? value.trim() : "";
446
+ storage.setPreference(dbKey, v);
447
+ config.defaultPiModel = v;
448
+ break;
449
+ }
437
450
  case "commitCli": {
438
451
  if (value !== "claude" && value !== "codex" && value !== "opencode")
439
452
  throw new Error(`无效 commit CLI: ${value}`);
@@ -652,13 +665,14 @@ function mergeWithDefaults(input) {
652
665
  android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
653
666
  macos: normalizeMacosDmgConfig(input.macos) ?? defaults.macos,
654
667
  cardDefaults: normalizeCardDefaults(input.cardDefaults),
655
- defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" || input.defaultProvider === "qoder" ? input.defaultProvider : "claude",
668
+ defaultProvider: input.defaultProvider === "codex" || input.defaultProvider === "opencode" || input.defaultProvider === "grok" || input.defaultProvider === "qoder" || input.defaultProvider === "pi" ? input.defaultProvider : "claude",
656
669
  defaultSessionKind: input.defaultSessionKind === "pty" ? "pty" : "structured",
657
670
  defaultModel: typeof input.defaultModel === "string" ? input.defaultModel.trim() : defaults.defaultModel,
658
671
  defaultCodexModel: typeof input.defaultCodexModel === "string" ? input.defaultCodexModel.trim() : defaults.defaultCodexModel,
659
672
  defaultOpenCodeModel: typeof input.defaultOpenCodeModel === "string" ? input.defaultOpenCodeModel.trim() : defaults.defaultOpenCodeModel,
660
673
  defaultGrokModel: typeof input.defaultGrokModel === "string" ? input.defaultGrokModel.trim() : defaults.defaultGrokModel,
661
674
  defaultQoderModel: typeof input.defaultQoderModel === "string" ? input.defaultQoderModel.trim() : defaults.defaultQoderModel,
675
+ defaultPiModel: typeof input.defaultPiModel === "string" ? input.defaultPiModel.trim() : defaults.defaultPiModel,
662
676
  commitCli: input.commitCli === "codex" || input.commitCli === "opencode" ? input.commitCli : "claude",
663
677
  commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
664
678
  commitAiSource: input.commitAiSource === "api" ? "api" : "cli",
@@ -677,6 +691,7 @@ export function getProviderDefaultModels(config) {
677
691
  opencode: (config.defaultOpenCodeModel ?? "").trim(),
678
692
  grok: (config.defaultGrokModel ?? "").trim(),
679
693
  qoder: (config.defaultQoderModel ?? "").trim(),
694
+ pi: (config.defaultPiModel ?? "").trim(),
680
695
  };
681
696
  }
682
697
  export function getDefaultModelForProvider(config, provider) {
@@ -689,6 +704,8 @@ export function getDefaultModelForProvider(config, provider) {
689
704
  return defaults.grok;
690
705
  if (provider === "qoder")
691
706
  return defaults.qoder;
707
+ if (provider === "pi")
708
+ return defaults.pi;
692
709
  return defaults.claude;
693
710
  }
694
711
  function normalizePresetCommand(command) {
@@ -4,7 +4,7 @@ import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
4
4
  import { callSystemAiTextWithFallback } from "./system-ai.js";
5
5
  import { buildChildEnv } from "./env-utils.js";
6
6
  import { runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
7
- import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, } from "./structured-provider-common.js";
7
+ import { thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, thinkingEffortToPiLevel, } from "./structured-provider-common.js";
8
8
  const GIT_TIMEOUT_MS = 1500;
9
9
  const GIT_PUSH_TIMEOUT_MS = 30_000;
10
10
  const MAX_FILE_ENTRIES = 200;
@@ -306,6 +306,7 @@ function normalizeProvider(provider) {
306
306
  || provider === "opencode"
307
307
  || provider === "grok"
308
308
  || provider === "qoder"
309
+ || provider === "pi"
309
310
  ? provider
310
311
  : "claude";
311
312
  }
@@ -406,6 +407,35 @@ function extractQoderText(stdout) {
406
407
  }
407
408
  return (resultText || assistantTexts.join("\n")).trim();
408
409
  }
410
+ function extractPiText(stdout) {
411
+ let text = "";
412
+ for (const line of stdout.split(/\r?\n/)) {
413
+ try {
414
+ const event = JSON.parse(line);
415
+ if (event.type !== "message_end" || event.message?.role !== "assistant" || !Array.isArray(event.message.content))
416
+ continue;
417
+ text = event.message.content
418
+ .filter((block) => !!block && typeof block === "object" && block.type === "text" && typeof block.text === "string")
419
+ .map((block) => block.text)
420
+ .join("\n");
421
+ }
422
+ catch { /* ignore non-protocol stdout */ }
423
+ }
424
+ return text.trim();
425
+ }
426
+ function buildPiTextArgs(prompt, opts, allowTools = false) {
427
+ const args = ["--mode", "json", "--print", "--no-session"];
428
+ if (!allowTools)
429
+ args.push("--no-tools");
430
+ const model = opts.model?.trim();
431
+ if (model && model !== "default")
432
+ args.push("--model", model);
433
+ const level = thinkingEffortToPiLevel(opts.thinkingEffort ?? "off");
434
+ if (level)
435
+ args.push("--thinking", level);
436
+ args.push(prompt);
437
+ return args;
438
+ }
409
439
  function buildGrokTextArgs(prompt, opts, allowTools = false) {
410
440
  const args = ["--no-auto-update", "-p", prompt, "--output-format", "streaming-json"];
411
441
  const model = opts.model?.trim();
@@ -532,6 +562,13 @@ async function callQoderText(prompt, cwd, opts) {
532
562
  throw new QuickCommitError("Qoder 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
533
563
  return text;
534
564
  }
565
+ async function callPiText(prompt, cwd, opts) {
566
+ const stdout = await runCliText("pi", buildPiTextArgs(prompt, opts), "", { cwd, timeoutMs: CODEX_MESSAGE_TIMEOUT_MS, inheritEnv: opts.inheritEnv });
567
+ const text = extractPiText(stdout);
568
+ if (!text)
569
+ throw new QuickCommitError("Pi 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
570
+ return text;
571
+ }
535
572
  async function callCliAiText(prompt, cwd, language, opts) {
536
573
  const provider = normalizeProvider(opts.provider);
537
574
  if (provider === "codex") {
@@ -543,6 +580,8 @@ async function callCliAiText(prompt, cwd, language, opts) {
543
580
  return callGrokText(prompt, cwd, opts);
544
581
  if (provider === "qoder")
545
582
  return callQoderText(prompt, cwd, opts);
583
+ if (provider === "pi")
584
+ return callPiText(prompt, cwd, opts);
546
585
  return callClaudeText(prompt, cwd, language, opts);
547
586
  }
548
587
  async function callDirectApiText(prompt, systemAi) {
@@ -1196,6 +1235,13 @@ async function runQuickCommitFallbackCli(opts, priorError) {
1196
1235
  inheritEnv: opts.inheritEnv,
1197
1236
  });
1198
1237
  }
1238
+ else if (provider === "pi") {
1239
+ await runCliText("pi", buildPiTextArgs(prompt, opts, true), "", {
1240
+ cwd: opts.cwd,
1241
+ timeoutMs: QUICK_COMMIT_CLI_TIMEOUT_MS,
1242
+ inheritEnv: opts.inheritEnv,
1243
+ });
1244
+ }
1199
1245
  else {
1200
1246
  const args = [
1201
1247
  "-p",
package/dist/models.d.ts CHANGED
@@ -42,6 +42,7 @@ export interface ModelCache {
42
42
  opencodeModels: ClaudeModelInfo[];
43
43
  grokModels: ClaudeModelInfo[];
44
44
  qoderModels: ClaudeModelInfo[];
45
+ piModels: ClaudeModelInfo[];
45
46
  claudeVersion: string | null;
46
47
  opencodeVersion: string | null;
47
48
  refreshedAt: string;
package/dist/models.js CHANGED
@@ -53,6 +53,9 @@ const QODER_FALLBACK_MODELS = [
53
53
  { id: "performance", label: "Performance" },
54
54
  { id: "ultimate", label: "Ultimate" },
55
55
  ];
56
+ const PI_FALLBACK_MODELS = [
57
+ { id: "default", label: "跟随 Pi 默认", alias: true },
58
+ ];
56
59
  function cloneModels(models) {
57
60
  return models.map((model) => ({
58
61
  ...model,
@@ -68,6 +71,7 @@ function cloneCache(cache) {
68
71
  opencodeModels: cloneModels(cache.opencodeModels),
69
72
  grokModels: cloneModels(cache.grokModels),
70
73
  qoderModels: cloneModels(cache.qoderModels),
74
+ piModels: cloneModels(cache.piModels),
71
75
  claudeVersion: cache.claudeVersion,
72
76
  opencodeVersion: cache.opencodeVersion,
73
77
  refreshedAt: cache.refreshedAt,
@@ -235,6 +239,7 @@ function createInitialCache(options) {
235
239
  opencodeModels: cloneModels(OPENCODE_FALLBACK_MODELS),
236
240
  grokModels: cloneModels(GROK_FALLBACK_MODELS),
237
241
  qoderModels: cloneModels(QODER_FALLBACK_MODELS),
242
+ piModels: cloneModels(PI_FALLBACK_MODELS),
238
243
  claudeVersion: null,
239
244
  opencodeVersion: null,
240
245
  refreshedAt: now.toISOString(),
@@ -523,6 +528,7 @@ function catalogRevision(cache) {
523
528
  opencodeModels: cache.opencodeModels,
524
529
  grokModels: cache.grokModels,
525
530
  qoderModels: cache.qoderModels,
531
+ piModels: cache.piModels,
526
532
  claudeVersion: cache.claudeVersion,
527
533
  opencodeVersion: cache.opencodeVersion,
528
534
  });
@@ -603,8 +609,9 @@ function parsePersistedModelCatalog(value) {
603
609
  const opencodeModels = parsePersistedModelList(catalog.opencodeModels);
604
610
  const grokModels = parsePersistedModelList(catalog.grokModels);
605
611
  const qoderModels = parsePersistedModelList(catalog.qoderModels);
612
+ const piModels = parsePersistedModelList(catalog.piModels);
606
613
  const refreshedAt = safePersistedString(catalog.refreshedAt, 64);
607
- if (!models || !codexModels || !opencodeModels || !grokModels || !qoderModels
614
+ if (!models || !codexModels || !opencodeModels || !grokModels || !qoderModels || !piModels
608
615
  || !refreshedAt || Number.isNaN(Date.parse(refreshedAt))) {
609
616
  return null;
610
617
  }
@@ -619,6 +626,7 @@ function parsePersistedModelCatalog(value) {
619
626
  opencodeModels,
620
627
  grokModels,
621
628
  qoderModels,
629
+ piModels,
622
630
  claudeVersion,
623
631
  opencodeVersion,
624
632
  refreshedAt,
@@ -696,6 +704,7 @@ async function discoverModelCache(options, previous) {
696
704
  opencodeModels: opencodeProbe.models.ok ? opencodeProbe.models.value : cloneModels(previous.opencodeModels),
697
705
  grokModels: grokProbe.ok ? grokProbe.value : cloneModels(previous.grokModels),
698
706
  qoderModels: qoderProbe.ok ? qoderProbe.value : cloneModels(previous.qoderModels),
707
+ piModels: cloneModels(previous.piModels),
699
708
  claudeVersion,
700
709
  opencodeVersion: opencodeProbe.version.ok ? opencodeProbe.version.value : previous.opencodeVersion,
701
710
  refreshedAt: now.toISOString(),
@@ -6,7 +6,7 @@ import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { spawn, spawnSync } from "node:child_process";
8
8
  /** 关键的 CLI 工具,会被诊断输出。 */
9
- const PROBE_COMMANDS = ["claude", "codex", "opencode", "grok", "qodercli"];
9
+ const PROBE_COMMANDS = ["claude", "codex", "opencode", "grok", "qodercli", "pi"];
10
10
  const DEEP_PROBE_TIMEOUT_MS = 4000;
11
11
  /**
12
12
  * 构造候选 bin 目录列表(按优先级,前面的更可信)。
@@ -15,7 +15,7 @@ import { ensureNodePtyHelperExecutable } from "./ensure-node-pty-helper.js";
15
15
  import { buildLanguageDirective, buildManagedAutonomyDirective } from "./language-prompt.js";
16
16
  import { prepareSessionWorktree } from "./git-worktree.js";
17
17
  import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
18
- import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant } from "./structured-provider-common.js";
18
+ import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel } from "./structured-provider-common.js";
19
19
  import { SessionTopicCoordinator } from "./session-topic.js";
20
20
  import { getErrorMessage } from "./error-utils.js";
21
21
  import { resolveSystemAiContext } from "./session-ai-context.js";
@@ -28,6 +28,8 @@ function resolveProviderFromCommand(command) {
28
28
  return "opencode";
29
29
  if (/^grok\b/.test(command.trim()))
30
30
  return "grok";
31
+ if (/^pi\b/.test(command.trim()))
32
+ return "pi";
31
33
  return /^qodercli\b/.test(command.trim()) ? "qoder" : "claude";
32
34
  }
33
35
  /**
@@ -2210,6 +2212,17 @@ export class ProcessManager extends EventEmitter {
2210
2212
  }
2211
2213
  return result;
2212
2214
  }
2215
+ if (provider === "pi") {
2216
+ let result = command;
2217
+ const trimmedModel = model?.trim();
2218
+ if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result)) {
2219
+ result += ` --model '${trimmedModel.replace(/'/g, "'\\''")}'`;
2220
+ }
2221
+ const level = thinkingEffortToPiLevel(thinkingEffort ?? null);
2222
+ if (level && !/--thinking(?:\s|=)/.test(result))
2223
+ result += ` --thinking '${level}'`;
2224
+ return result;
2225
+ }
2213
2226
  const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
2214
2227
  if (!isClaudeCmd)
2215
2228
  return command;
@@ -1,6 +1,6 @@
1
- import type { SystemAiConfig } from "./types.js";
1
+ import { type QuickCommitAiOptions } from "./git-quick-commit.js";
2
2
  export declare class PromptOptimizeError extends Error {
3
3
  readonly code: string;
4
4
  constructor(message: string, code: string);
5
5
  }
6
- export declare function optimizePrompt(rawText: string, language: string, cwd?: string, systemAi?: SystemAiConfig): Promise<string>;
6
+ export declare function optimizePrompt(rawText: string, language: string, cwd?: string, ai?: QuickCommitAiOptions): Promise<string>;
@@ -1,6 +1,4 @@
1
- import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
2
- import { callSystemAiTextWithFallback } from "./system-ai.js";
3
- const CLAUDE_TIMEOUT_MS = 60_000;
1
+ import { callConfiguredAiText } from "./git-quick-commit.js";
4
2
  const MAX_INPUT_LENGTH = 8000;
5
3
  export class PromptOptimizeError extends Error {
6
4
  code;
@@ -10,24 +8,6 @@ export class PromptOptimizeError extends Error {
10
8
  this.name = "PromptOptimizeError";
11
9
  }
12
10
  }
13
- async function callClaudeText(prompt, cwd, language) {
14
- try {
15
- return await runClaudePrint(prompt, { cwd, timeoutMs: CLAUDE_TIMEOUT_MS, language });
16
- }
17
- catch (error) {
18
- if (error instanceof ClaudeRunError) {
19
- // 翻译成 prompt-optimizer 自己的话术 + 错误码(与原文案保持一致)。
20
- if (error.code === "CLAUDE_TIMEOUT") {
21
- throw new PromptOptimizeError("Claude 优化超时,请稍后重试。", "CLAUDE_TIMEOUT");
22
- }
23
- if (error.code === "CLAUDE_EMPTY_RESULT") {
24
- throw new PromptOptimizeError("Claude 返回了空结果。", "EMPTY_RESULT");
25
- }
26
- throw new PromptOptimizeError(error.message, error.code);
27
- }
28
- throw error;
29
- }
30
- }
31
11
  function buildOptimizePrompt(userInput, language) {
32
12
  const lang = (language || "").trim() || "中文";
33
13
  return [
@@ -42,7 +22,7 @@ function buildOptimizePrompt(userInput, language) {
42
22
  userInput,
43
23
  ].join("\n");
44
24
  }
45
- export async function optimizePrompt(rawText, language, cwd, systemAi) {
25
+ export async function optimizePrompt(rawText, language, cwd, ai = {}) {
46
26
  const text = (rawText || "").trim();
47
27
  if (!text) {
48
28
  throw new PromptOptimizeError("请先输入要优化的内容。", "EMPTY_INPUT");
@@ -52,16 +32,15 @@ export async function optimizePrompt(rawText, language, cwd, systemAi) {
52
32
  }
53
33
  const prompt = buildOptimizePrompt(text, language);
54
34
  let raw;
55
- if (systemAi?.enabled) {
56
- try {
57
- raw = await callSystemAiTextWithFallback(prompt, systemAi);
58
- }
59
- catch {
60
- raw = await callClaudeText(prompt, cwd, language);
61
- }
35
+ try {
36
+ raw = await callConfiguredAiText(prompt, cwd ?? process.cwd(), language, ai);
62
37
  }
63
- else {
64
- raw = await callClaudeText(prompt, cwd, language);
38
+ catch (error) {
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ const code = error && typeof error === "object" && "code" in error && typeof error.code === "string"
41
+ ? error.code
42
+ : "AI_OPTIMIZE_FAILED";
43
+ throw new PromptOptimizeError(message, code);
65
44
  }
66
45
  const cleaned = raw
67
46
  .replace(/^```[a-zA-Z]*\n?/, "")
@@ -69,7 +48,7 @@ export async function optimizePrompt(rawText, language, cwd, systemAi) {
69
48
  .replace(/^["'`]+|["'`]+$/g, "")
70
49
  .trim();
71
50
  if (!cleaned) {
72
- throw new PromptOptimizeError("Claude 返回了空结果。", "EMPTY_RESULT");
51
+ throw new PromptOptimizeError("AI 返回了空结果。", "EMPTY_RESULT");
73
52
  }
74
53
  return cleaned;
75
54
  }
@@ -1,4 +1,4 @@
1
- export type ProviderCliId = "claude" | "codex" | "opencode" | "qoder";
1
+ export type ProviderCliId = "claude" | "codex" | "opencode" | "qoder" | "pi";
2
2
  export interface ProviderCliUpdateStatus {
3
3
  id: ProviderCliId;
4
4
  label: string;
@@ -44,6 +44,14 @@ const PROVIDER_CLI_SPECS = [
44
44
  versionArgs: ["--version"],
45
45
  updateArgs: ["update"],
46
46
  },
47
+ {
48
+ id: "pi",
49
+ label: "Pi CLI",
50
+ command: "pi",
51
+ npmPackage: "@mariozechner/pi-coding-agent",
52
+ versionArgs: ["--version"],
53
+ updateArgs: ["update", "self"],
54
+ },
47
55
  ];
48
56
  function childEnv(options) {
49
57
  return options.env ?? buildChildEnv(options.inheritEnv !== false);
@@ -18,7 +18,7 @@ function resumeArgumentPattern(provider) {
18
18
  if (provider === "codex") {
19
19
  return new RegExp(`(?:^|\\s)resume\\s+${SAFE_PROVIDER_SESSION_ID_SOURCE}(?=\\s|$)`, "i");
20
20
  }
21
- if (provider === "opencode") {
21
+ if (provider === "opencode" || provider === "pi") {
22
22
  return new RegExp(`(?:^|\\s)(?:--session|-s)\\s+${SAFE_PROVIDER_SESSION_ID_SOURCE}(?=\\s|$)`, "i");
23
23
  }
24
24
  return new RegExp(`(?:^|\\s)(?:--resume|-r)\\s+${SAFE_PROVIDER_SESSION_ID_SOURCE}(?=\\s|$)`, "i");
@@ -34,13 +34,13 @@ export function getProviderCommandSessionId(provider, command) {
34
34
  const resumed = getProviderResumeCommandSessionId(provider, command);
35
35
  if (resumed)
36
36
  return resumed;
37
- if (provider === "codex" || provider === "opencode")
37
+ if (provider === "codex" || provider === "opencode" || provider === "pi")
38
38
  return null;
39
39
  return assignedSessionIdPattern().exec(command)?.[1] ?? null;
40
40
  }
41
41
  function stripProviderResumeArgument(provider, command) {
42
42
  const withoutResume = command.replace(resumeArgumentPattern(provider), " ");
43
- const withoutAssignedId = provider === "codex" || provider === "opencode"
43
+ const withoutAssignedId = provider === "codex" || provider === "opencode" || provider === "pi"
44
44
  ? withoutResume
45
45
  : withoutResume.replace(assignedSessionIdPattern(), " ");
46
46
  return withoutAssignedId.replace(/\s+/g, " ").trim();
@@ -55,5 +55,7 @@ export function buildProviderResumeCommand(provider, command, providerSessionId)
55
55
  return `${base} resume ${providerSessionId}`;
56
56
  if (provider === "opencode")
57
57
  return `${base} --session ${providerSessionId}`;
58
+ if (provider === "pi")
59
+ return `${base} --session ${providerSessionId}`;
58
60
  return `${base} --resume ${providerSessionId}`;
59
61
  }
@@ -319,6 +319,8 @@ function resolvePtyResumeProvider(snapshot) {
319
319
  return "grok";
320
320
  if (/^qodercli\b/.test(command))
321
321
  return "qoder";
322
+ if (/^pi\b/.test(command))
323
+ return "pi";
322
324
  return "claude";
323
325
  }
324
326
  function isPtyProviderCommand(provider, command) {
@@ -375,7 +377,7 @@ function canAutoResumePtyForInput(snapshot, input) {
375
377
  return Boolean(snapshot
376
378
  && (snapshot.sessionKind ?? "pty") === "pty"
377
379
  && snapshot.status !== "running"
378
- && (snapshot.provider === "claude" || snapshot.provider === "codex" || snapshot.provider === "opencode" || snapshot.provider === "grok" || snapshot.provider === "qoder"
380
+ && (snapshot.provider === "claude" || snapshot.provider === "codex" || snapshot.provider === "opencode" || snapshot.provider === "grok" || snapshot.provider === "qoder" || snapshot.provider === "pi"
379
381
  || /^(?:claude|codex|opencode|grok|qodercli)\b/.test(snapshot.command.trim()))
380
382
  && snapshot.claudeSessionId
381
383
  && input);
@@ -418,11 +420,11 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
418
420
  app.post("/api/structured-sessions", asyncRoute(async (req, res) => {
419
421
  const body = req.body;
420
422
  try {
421
- if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode" && body.provider !== "grok" && body.provider !== "qoder") {
423
+ if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode" && body.provider !== "grok" && body.provider !== "qoder" && body.provider !== "pi") {
422
424
  res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、Grok 或 Qoder provider。" });
423
425
  return;
424
426
  }
425
- const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" || body.provider === "qoder" ? body.provider : "claude";
427
+ const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" || body.provider === "qoder" || body.provider === "pi" ? body.provider : "claude";
426
428
  const rawModel = typeof body.model === "string" ? body.model.trim() : "";
427
429
  const origin = parseSessionCreationOrigin(body);
428
430
  const snapshot = structured.createSession({
@@ -25,6 +25,7 @@ function publicConfig(config) {
25
25
  defaultOpenCodeModel: defaultModels.opencode,
26
26
  defaultGrokModel: defaultModels.grok,
27
27
  defaultQoderModel: defaultModels.qoder,
28
+ defaultPiModel: defaultModels.pi,
28
29
  defaultModels,
29
30
  };
30
31
  }
@@ -283,6 +284,8 @@ export function registerSettingsRoutes(app, deps) {
283
284
  stagePreference("defaultGrokModel", body.defaultModels.grok);
284
285
  if (Object.hasOwn(body.defaultModels, "qoder"))
285
286
  stagePreference("defaultQoderModel", body.defaultModels.qoder);
287
+ if (Object.hasOwn(body.defaultModels, "pi"))
288
+ stagePreference("defaultPiModel", body.defaultModels.pi);
286
289
  }
287
290
  if (body.systemAi !== undefined) {
288
291
  if (!body.systemAi || typeof body.systemAi !== "object" || Array.isArray(body.systemAi)) {
@@ -397,6 +400,7 @@ export function registerSettingsRoutes(app, deps) {
397
400
  defaultOpenCodeModel: defaults.opencode,
398
401
  defaultGrokModel: defaults.grok,
399
402
  defaultQoderModel: defaults.qoder,
403
+ defaultPiModel: defaults.pi,
400
404
  defaultModels: defaults,
401
405
  });
402
406
  });
@@ -411,6 +415,7 @@ export function registerSettingsRoutes(app, deps) {
411
415
  defaultOpenCodeModel: defaults.opencode,
412
416
  defaultGrokModel: defaults.grok,
413
417
  defaultQoderModel: defaults.qoder,
418
+ defaultPiModel: defaults.pi,
414
419
  defaultModels: defaults,
415
420
  });
416
421
  }
@@ -126,7 +126,7 @@ export function registerAdminUpdateRoutes(app, deps) {
126
126
  return;
127
127
  }
128
128
  const rawIds = Array.isArray(req.body?.ids) ? req.body.ids : [];
129
- const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode" || value === "qoder");
129
+ const ids = rawIds.filter((value) => value === "claude" || value === "codex" || value === "opencode" || value === "qoder" || value === "pi");
130
130
  state.providerCliUpdateInFlight = true;
131
131
  try {
132
132
  const before = await refreshProviderCliUpdateState(state, config);