@co0ontty/wand 4.79.0 → 4.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +18 -6
  2. package/dist/agent-dispatch.d.ts +3 -0
  3. package/dist/agent-dispatch.js +5 -0
  4. package/dist/ai-team-runner.js +4 -2
  5. package/dist/ai-team-types.d.ts +34 -0
  6. package/dist/ai-team-types.js +12 -1
  7. package/dist/build-info.json +3 -3
  8. package/dist/config.d.ts +4 -3
  9. package/dist/config.js +25 -0
  10. package/dist/git-quick-commit.d.ts +9 -8
  11. package/dist/git-quick-commit.js +123 -8
  12. package/dist/models.d.ts +3 -0
  13. package/dist/models.js +24 -2
  14. package/dist/process-manager.js +16 -3
  15. package/dist/provider-cli-updater.d.ts +1 -1
  16. package/dist/provider-cli-updater.js +12 -1
  17. package/dist/server-ai-team-routes.js +4 -1
  18. package/dist/server-employee-routes.d.ts +22 -0
  19. package/dist/server-employee-routes.js +277 -0
  20. package/dist/server-session-routes.js +38 -13
  21. package/dist/server-settings-routes.js +5 -0
  22. package/dist/server-task-routes.d.ts +2 -0
  23. package/dist/server-task-routes.js +174 -22
  24. package/dist/server-update-routes.js +1 -1
  25. package/dist/server-workspace-routes.js +3 -0
  26. package/dist/server.js +16 -3
  27. package/dist/session-ai-context.d.ts +7 -2
  28. package/dist/session-ai-context.js +36 -9
  29. package/dist/session-provider.d.ts +6 -1
  30. package/dist/session-provider.js +27 -1
  31. package/dist/session-transport.js +4 -0
  32. package/dist/silicon-employee-dispatch.d.ts +9 -0
  33. package/dist/silicon-employee-dispatch.js +13 -0
  34. package/dist/silicon-employee-draft.d.ts +36 -0
  35. package/dist/silicon-employee-draft.js +146 -0
  36. package/dist/storage.d.ts +19 -2
  37. package/dist/storage.js +158 -7
  38. package/dist/structured-gemini-adapter.d.ts +40 -0
  39. package/dist/structured-gemini-adapter.js +198 -0
  40. package/dist/structured-provider-common.d.ts +2 -1
  41. package/dist/structured-provider-common.js +7 -2
  42. package/dist/structured-session-manager.d.ts +10 -0
  43. package/dist/structured-session-manager.js +106 -14
  44. package/dist/system-ai.js +1 -1
  45. package/dist/system-employee.d.ts +35 -0
  46. package/dist/system-employee.js +81 -0
  47. package/dist/task-types.d.ts +8 -1
  48. package/dist/task-types.js +7 -0
  49. package/dist/types.d.ts +23 -4
  50. package/dist/web-ui/content/ai-teams.js +13 -13
  51. package/dist/web-ui/content/scripts.js +64 -64
  52. package/dist/web-ui/content/styles.css +1 -1
  53. package/dist/web-ui/content/tailwind.css +3 -0
  54. package/dist/web-ui/embedded-assets.js +3 -3
  55. package/dist/web-ui/provider-identity.d.ts +2 -1
  56. package/dist/web-ui/provider-identity.js +10 -1
  57. package/dist/web-ui/session-activity.js +1 -1
  58. package/package.json +1 -1
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/), [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.
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/), as well as [Gemini CLI](https://github.com/google-gemini/gemini-cli), 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/`.
17
17
 
@@ -48,7 +48,7 @@ wand config:password
48
48
  #### Core
49
49
 
50
50
  - **Dual view modes** — switch between raw terminal output and a structured conversation view for the same session
51
- - **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, OpenCode, Grok, Qoder CLI, and Pi
51
+ - **Multiple providers** — create PTY or structured sessions for Claude Code, Codex, OpenCode, Grok, Qoder CLI, Pi, and Gemini CLI
52
52
  - **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
53
53
  - **Permission control** — visual permission prompts with one-time approval, per-turn memory, and related policies
54
54
 
@@ -65,7 +65,7 @@ wand config:password
65
65
  - **Native clients** — Android, iOS, and macOS clients with encrypted connection codes and update checks
66
66
  - **HTTPS** — optional self-signed certificates for remote or mobile access
67
67
  - **Update channels** — built-in stable/beta update checks and upgrade prompts
68
- - **CLI updates** — check and quickly update Claude Code, Codex, OpenCode, Qoder CLI, and Pi from server settings, with optional automatic updates
68
+ - **CLI updates** — check and quickly update Claude Code, Codex, OpenCode, Qoder CLI, Pi, and Gemini CLI from server settings, with optional automatic updates
69
69
 
70
70
  ### Configuration
71
71
 
@@ -150,11 +150,17 @@ npm uninstall -g opencode
150
150
  npm install -g opencode-ai@latest
151
151
  ```
152
152
 
153
+ Gemini structured sessions require `@google/gemini-cli` 0.11 or newer (the release that added headless `--output-format stream-json`):
154
+
155
+ ```bash
156
+ npm install -g @google/gemini-cli@latest
157
+ ```
158
+
153
159
  ## 中文
154
160
 
155
161
  ### 概览
156
162
 
157
- 通过浏览器远程访问和管理本地 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/),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
163
+ 通过浏览器远程访问和管理本地 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/) 和 [Gemini CLI](https://github.com/google-gemini/gemini-cli),提供终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览和多平台客户端。
158
164
 
159
165
  ### 安装
160
166
 
@@ -191,12 +197,18 @@ npm uninstall -g opencode
191
197
  npm install -g opencode-ai@latest
192
198
  ```
193
199
 
200
+ Gemini 结构化会话需要 `@google/gemini-cli` 0.11 或更高版本(该版本才加入 headless 的 `--output-format stream-json`):
201
+
202
+ ```bash
203
+ npm install -g @google/gemini-cli@latest
204
+ ```
205
+
194
206
  ### 功能
195
207
 
196
208
  #### 核心
197
209
 
198
210
  - **双视图模式** — 终端原始输出和结构化对话视图可随时切换,同一会话两种呈现
199
- - **多 Provider 支持** — Claude Code、Codex、OpenCode、Grok、Qoder CLI 和 Pi 均可创建 PTY 或结构化会话
211
+ - **多 Provider 支持** — Claude Code、Codex、OpenCode、Grok、Qoder CLI、Pi 和 Gemini CLI 均可创建 PTY 或结构化会话
200
212
  - **会话管理** — 创建、归档并携带原生上下文恢复 Claude、Codex、OpenCode、Grok、Qoder 会话;支持恢复 Claude/Codex 原生历史记录;会话列表显示摘要
201
213
  - **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略
202
214
 
@@ -214,7 +226,7 @@ npm install -g opencode-ai@latest
214
226
  - **多平台客户端** — Android / iOS / macOS 原生客户端,支持加密连接码分发、自动更新检查
215
227
  - **HTTPS** — 可选自签证书,适合远程或移动端访问
216
228
  - **版本管理** — 内置更新检查与升级提示,支持 stable/beta 双通道
217
- - **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode、Qoder CLI 和 Pi,也可开启自动更新
229
+ - **CLI 更新** — 在服务端设置中检查并快速更新 Claude Code、Codex、OpenCode、Qoder CLI、Pi 和 Gemini CLI,也可开启自动更新
218
230
 
219
231
  ### 配置
220
232
 
@@ -3,6 +3,7 @@ import type { SessionRegistry } from "./session-registry.js";
3
3
  import type { WandStorage } from "./storage.js";
4
4
  import type { StructuredSessionManager } from "./structured-session-manager.js";
5
5
  import type { WandTask, WandTaskAgent } from "./task-types.js";
6
+ import type { SiliconEmployee } from "./ai-team-types.js";
6
7
  import type { SessionSnapshot, WandConfig } from "./types.js";
7
8
  export interface AgentDispatchDeps {
8
9
  storage: WandStorage;
@@ -32,6 +33,8 @@ export declare function dispatchAgentForTask(deps: AgentDispatchDeps, input: {
32
33
  prompt: string;
33
34
  automationId: string;
34
35
  systemPrompt?: string;
36
+ employee?: SiliconEmployee;
37
+ employeeCandidateIndex?: number;
35
38
  }): Promise<AgentDispatchResult>;
36
39
  /**
37
40
  * 给已有会话追加一条消息。结构化会话走 sendMessage(不等整轮结束);
@@ -50,6 +50,11 @@ export async function dispatchAgentForTask(deps, input) {
50
50
  sessionSource: "automation",
51
51
  automationId: input.automationId,
52
52
  systemPrompt,
53
+ employeeId: input.employee?.id,
54
+ employeeName: input.employee?.name,
55
+ employeeAvatar: input.employee?.avatar,
56
+ employeeCandidates: input.employee?.agents,
57
+ employeeCandidateIndex: input.employeeCandidateIndex,
53
58
  workspaceId: group.workspaceId,
54
59
  workspaceTaskId: group.workspaceTaskId,
55
60
  });
@@ -913,12 +913,14 @@ export class AiTeamRunner {
913
913
  return;
914
914
  }
915
915
  // 负责人的模型报错时停下来等用户(换模型或稍后回复即可接着来),不当成格式错误空转重试。
916
+ // 按真实角色身份发出正常对话气泡并展示完整报错,而不是次级 notice 小字。
916
917
  const leader = leaderOf(run.team);
917
918
  if (outcome.kind === "failed" && outcome.sessionError) {
918
919
  this.saveStep({ ...step, status: "failed", report: outcome.text, endedAt });
919
920
  this.postTurn(run, {
920
- role: "assistant", notice: true, author: chatAuthor(leader, step.sessionId, this.actualAgent(leader, step)),
921
- content: chatText(`负责人的模型出错:${outcome.text}\n可以去团队页换个模型,然后在群里回复一句让团队继续。`),
921
+ role: "assistant",
922
+ author: chatAuthor(leader, step.sessionId, this.actualAgent(leader, step)),
923
+ content: chatText(`我的模型调用失败,无法继续处理任务:\n\n\`\`\`\n${outcome.text}\n\`\`\`\n\n可以去团队设置页面为我更换可用模型,或者在群里回复我继续重试。`),
922
924
  });
923
925
  this.setStatus(run, "waiting_user", `负责人出错:${outcome.text}`);
924
926
  return;
@@ -55,7 +55,40 @@ export interface StepDispatchInfo {
55
55
  }>;
56
56
  }
57
57
  /** 上传头像在前端缩成小图后的上限(data URL 字符数)。 */
58
+ export interface SiliconEmployee {
59
+ id: string;
60
+ name: string;
61
+ duty: string;
62
+ prompt: string;
63
+ avatar: string;
64
+ agents: WandTaskAgent[];
65
+ /**
66
+ * 非空 = Wand 内置员工(如 "wand-ops" 系统运维):名字/职责/人设/头像锁定,
67
+ * 不可归档、不可删除,只有执行候选由用户维护。
68
+ */
69
+ systemKey?: string;
70
+ archivedAt?: string;
71
+ createdAt: string;
72
+ updatedAt: string;
73
+ }
74
+ /**
75
+ * AI 按自然语言期望起草的员工配置:只带一个首选执行候选,
76
+ * 其余手动字段(头像、备用候选)仍由表单和「高级配置」负责。
77
+ */
78
+ export interface SiliconEmployeeDraft {
79
+ name: string;
80
+ duty: string;
81
+ prompt: string;
82
+ agent: WandTaskAgent;
83
+ }
58
84
  export declare const AI_TEAM_AVATAR_MAX_CHARS = 60000;
85
+ export declare const SILICON_EMPLOYEE_AVATAR_MAX_CHARS = 60000;
86
+ export declare const SYSTEM_EMPLOYEE_KEY = "wand-ops";
87
+ export declare const SYSTEM_EMPLOYEE_ID = "e_wand_ops";
88
+ export declare const SYSTEM_EMPLOYEE_NAME = "\u52E4\u52B3\u7684\u521D\u4E8C";
89
+ /** 员工列表 / 设置页上的标识。 */
90
+ export declare const SYSTEM_EMPLOYEE_TAG = "\u7CFB\u7EDF\u8FD0\u7EF4";
91
+ export declare function isSystemSiliconEmployee(employee: Pick<SiliconEmployee, "systemKey"> | null | undefined): boolean;
59
92
  export interface AiTeam {
60
93
  id: string;
61
94
  name: string;
@@ -102,6 +135,7 @@ export interface AiTeamRun {
102
135
  */
103
136
  export interface AiTeamRunChatMarker {
104
137
  runId: string;
138
+ teamId: string;
105
139
  teamName: string;
106
140
  memberCount: number;
107
141
  }
@@ -25,8 +25,19 @@ export function memberAgents(member) {
25
25
  export function agentKey(agent) {
26
26
  return `${agent.provider}|${agent.model}|${agent.thinkingEffort}|${agent.mode}|${agent.kind}`;
27
27
  }
28
- /** 上传头像在前端缩成小图后的上限(data URL 字符数)。 */
29
28
  export const AI_TEAM_AVATAR_MAX_CHARS = 60_000;
29
+ export const SILICON_EMPLOYEE_AVATAR_MAX_CHARS = 60_000;
30
+ // ============ 内置「系统运维」员工 ============
31
+ // 纯常量与判定放在这里:浏览器端要拿 Tag 和判定,不能把 server-only 的
32
+ // system-employee.ts(依赖 node:fs)拉进前端 bundle。
33
+ export const SYSTEM_EMPLOYEE_KEY = "wand-ops";
34
+ export const SYSTEM_EMPLOYEE_ID = "e_wand_ops";
35
+ export const SYSTEM_EMPLOYEE_NAME = "勤劳的初二";
36
+ /** 员工列表 / 设置页上的标识。 */
37
+ export const SYSTEM_EMPLOYEE_TAG = "系统运维";
38
+ export function isSystemSiliconEmployee(employee) {
39
+ return employee?.systemKey === SYSTEM_EMPLOYEE_KEY;
40
+ }
30
41
  export const AI_TEAM_ACTIVE_RUN_STATUSES = ["running", "awaiting_approval", "waiting_user"];
31
42
  /** 终态:不会再有 live 变化,runner 据此清掉该 run 的推送指纹,避免长跑服务按 run 数攒字符串。 */
32
43
  export const AI_TEAM_TERMINAL_RUN_STATUSES = ["done", "failed", "stopped"];
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "bf34f01cbac723af9bc4316787cba307ea9d083b",
3
- "builtAt": "2026-09-29T12:40:12.739Z",
4
- "version": "4.79.0",
2
+ "commit": "f8e0c448bbfdf5623cc9dc79e12b46984d1d21c6",
3
+ "builtAt": "2026-09-30T06:54:45.317Z",
4
+ "version": "4.80.0",
5
5
  "channel": "stable"
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", "defaultTaskWorktree", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "defaultPiModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "systemAiCli", "systemAiModel", "defaultThinkingEffort", "structuredRunner", "language", "cardDefaults", "inheritEnv"];
10
+ export declare const PREFERENCE_KEYS: readonly ["defaultProvider", "defaultSessionKind", "defaultTaskWorktree", "defaultMode", "defaultCwd", "defaultModel", "defaultCodexModel", "defaultOpenCodeModel", "defaultGrokModel", "defaultQoderModel", "defaultPiModel", "defaultGeminiModel", "commitCli", "commitModel", "commitAiSource", "systemAi", "systemAiCli", "systemAiModel", "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
  /**
@@ -47,12 +47,13 @@ export declare function writePreferenceToStorage(config: WandConfig, storage: Wa
47
47
  */
48
48
  export declare function validateCommitAiConfig(_config: Pick<WandConfig, "commitAiSource" | "systemAi">): void;
49
49
  export declare function isExecutionMode(value: unknown): value is ExecutionMode;
50
- export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel">): {
50
+ export declare function getProviderDefaultModels(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultGeminiModel">): {
51
51
  claude: string;
52
52
  codex: string;
53
53
  opencode: string;
54
54
  grok: string;
55
55
  qoder: string;
56
56
  pi: string;
57
+ gemini: string;
57
58
  };
58
- export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel">, provider: SessionProvider | undefined): string;
59
+ export declare function getDefaultModelForProvider(config: Pick<WandConfig, "defaultModel" | "defaultCodexModel" | "defaultOpenCodeModel" | "defaultGrokModel" | "defaultQoderModel" | "defaultPiModel" | "defaultGeminiModel">, provider: SessionProvider | undefined): string;
package/dist/config.js CHANGED
@@ -29,6 +29,7 @@ export const PREFERENCE_KEYS = [
29
29
  "defaultGrokModel",
30
30
  "defaultQoderModel",
31
31
  "defaultPiModel",
32
+ "defaultGeminiModel",
32
33
  "commitCli",
33
34
  "commitModel",
34
35
  "commitAiSource",
@@ -102,6 +103,7 @@ export const defaultConfig = () => ({
102
103
  defaultGrokModel: "",
103
104
  defaultQoderModel: "",
104
105
  defaultPiModel: "",
106
+ defaultGeminiModel: "",
105
107
  commitCli: "claude",
106
108
  commitModel: "",
107
109
  commitAiSource: "cli",
@@ -225,6 +227,14 @@ export async function loadConfigWithStorage(configPath, storage) {
225
227
  reconcileAppSecret(config, storage);
226
228
  // password: DB 优先映射到 runtime config,让 config:show 与 server.ts 都看到真值
227
229
  applyStoragePassword(config, storage);
230
+ // 内置「系统运维」员工:Wand 自有 AI 调用(commit message / 标题 / 提示词优化 /
231
+ // 员工起草)都由它执行。这里只做幂等补齐——首次按用户已有的系统 AI 工具与默认
232
+ // provider 建首条候选,之后候选由用户在员工设置里维护,不再回写 config。
233
+ storage.ensureSystemSiliconEmployee({
234
+ cli: config.systemAiCli,
235
+ model: config.systemAiModel,
236
+ provider: config.defaultProvider,
237
+ });
228
238
  // 如果 JSON 里有偏好字段(说明是老版本配置或刚迁移),重写一次干净版本
229
239
  const hasLegacyPrefs = PREFERENCE_KEYS.some((key) => key in rawInput);
230
240
  const hasLegacySecrets = "password" in rawInput || "appSecret" in rawInput;
@@ -366,6 +376,11 @@ export function applyStoragePreferences(config, storage) {
366
376
  if (typeof v === "string")
367
377
  config.defaultPiModel = v.trim();
368
378
  }
379
+ if (storage.hasPreference(preferenceStorageKey("defaultGeminiModel"))) {
380
+ const v = storage.getPreference(preferenceStorageKey("defaultGeminiModel"), defaults.defaultGeminiModel ?? "");
381
+ if (typeof v === "string")
382
+ config.defaultGeminiModel = v.trim();
383
+ }
369
384
  if (storage.hasPreference(preferenceStorageKey("commitCli"))) {
370
385
  const v = storage.getPreference(preferenceStorageKey("commitCli"), defaults.commitCli ?? "claude");
371
386
  if (v === "claude" || v === "codex" || v === "opencode")
@@ -495,6 +510,12 @@ export function writePreferenceToStorage(config, storage, key, value, options =
495
510
  config.defaultPiModel = v;
496
511
  break;
497
512
  }
513
+ case "defaultGeminiModel": {
514
+ const v = typeof value === "string" ? value.trim() : "";
515
+ storage.setPreference(dbKey, v);
516
+ config.defaultGeminiModel = v;
517
+ break;
518
+ }
498
519
  case "commitCli": {
499
520
  if (value !== "claude" && value !== "codex" && value !== "opencode")
500
521
  throw new Error(`无效 commit CLI: ${value}`);
@@ -791,6 +812,7 @@ function mergeWithDefaults(input) {
791
812
  defaultGrokModel: typeof input.defaultGrokModel === "string" ? input.defaultGrokModel.trim() : defaults.defaultGrokModel,
792
813
  defaultQoderModel: typeof input.defaultQoderModel === "string" ? input.defaultQoderModel.trim() : defaults.defaultQoderModel,
793
814
  defaultPiModel: typeof input.defaultPiModel === "string" ? input.defaultPiModel.trim() : defaults.defaultPiModel,
815
+ defaultGeminiModel: typeof input.defaultGeminiModel === "string" ? input.defaultGeminiModel.trim() : defaults.defaultGeminiModel,
794
816
  commitCli: input.commitCli === "codex" || input.commitCli === "opencode" ? input.commitCli : "claude",
795
817
  commitModel: typeof input.commitModel === "string" ? input.commitModel.trim() : defaults.commitModel,
796
818
  commitAiSource: input.commitAiSource === "api" ? "api" : "cli",
@@ -820,6 +842,7 @@ export function getProviderDefaultModels(config) {
820
842
  grok: configuredModelId(config.defaultGrokModel),
821
843
  qoder: configuredModelId(config.defaultQoderModel),
822
844
  pi: configuredModelId(config.defaultPiModel),
845
+ gemini: configuredModelId(config.defaultGeminiModel),
823
846
  };
824
847
  }
825
848
  export function getDefaultModelForProvider(config, provider) {
@@ -834,6 +857,8 @@ export function getDefaultModelForProvider(config, provider) {
834
857
  return defaults.qoder;
835
858
  if (provider === "pi")
836
859
  return defaults.pi;
860
+ if (provider === "gemini")
861
+ return defaults.gemini;
837
862
  return defaults.claude;
838
863
  }
839
864
  function normalizePresetCommand(command) {
@@ -3,14 +3,9 @@ import { GitStatusResult, PushResult, QuickCommitResult, SessionProvider, Sessio
3
3
  export type QuickCommitErrorCode = "CWD_MISSING" | "NO_CWD" | "NOT_A_GIT_REPO" | "NO_COMMIT" | "NOTHING_TO_COMMIT" | "NOTHING_TO_PUSH" | "EMPTY_MESSAGE" | "EMPTY_TAG" | "EMPTY_AI_MESSAGE" | "AI_FALLBACK_FAILED" | "INVALID_AI_TAG" | "TAG_EXISTS" | "GIT_ADD_FAILED" | "GIT_DIFF_FAILED" | "GIT_COMMIT_FAILED" | "GIT_TAG_FAILED" | "CLAUDE_CLI_MISSING" | "CLAUDE_CLI_FAILED" | "CLAUDE_TIMEOUT";
4
4
  /** Non-blocking status implementation used by HTTP routes. */
5
5
  export declare function getGitStatusAsync(cwd: string): Promise<GitStatusResult>;
6
- interface QuickCommitOptions extends CommitInputOptions {
6
+ interface QuickCommitOptions extends CommitInputOptions, QuickCommitAiOptions {
7
7
  cwd: string;
8
8
  language: string;
9
- provider?: SessionProvider;
10
- model?: string | null;
11
- thinkingEffort?: SessionSnapshot["thinkingEffort"];
12
- inheritEnv?: boolean;
13
- systemAi?: import("./types.js").SystemAiConfig;
14
9
  autoMessage: boolean;
15
10
  customMessage?: string;
16
11
  tag?: string;
@@ -29,6 +24,10 @@ export interface QuickCommitAiOptions {
29
24
  thinkingEffort?: SessionSnapshot["thinkingEffort"];
30
25
  inheritEnv?: boolean;
31
26
  systemAi?: import("./types.js").SystemAiConfig;
27
+ /** 内置「系统运维」员工的角色设定:作为系统提示前缀注入。 */
28
+ opsPersona?: string;
29
+ /** CLI 降级链(按顺序);未设置时只用 provider/model 这一次调用。 */
30
+ cliCandidates?: import("./types.js").AiCliCandidate[];
32
31
  }
33
32
  export declare class QuickCommitError extends Error {
34
33
  readonly code: QuickCommitErrorCode;
@@ -44,10 +43,12 @@ export interface CommitInputOptions {
44
43
  iterationDigest?: string;
45
44
  includeDiff?: boolean;
46
45
  }
46
+ /** 角色设定走系统提示前缀;任务自己的输出格式与约束必须排在后面。 */
47
+ export declare function withOpsPersona(request: AiTextRequest, persona?: string): AiTextRequest;
47
48
  /**
48
49
  * Run a lightweight AI request through the selected Commit source. API mode
49
- * exhausts its profile chain and then falls back once to the current-session
50
- * CLI. CLI mode uses only that CLI.
50
+ * exhausts its profile chain and then falls back once to the system employee's
51
+ * CLI chain. CLI mode walks that chain in order.
51
52
  */
52
53
  export declare function callConfiguredAiText(request: AiTextRequest, cwd: string, language: string, opts: QuickCommitAiOptions): Promise<string>;
53
54
  export interface GenerateCommitMessageResult {
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
3
3
  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
- import { isSessionProvider } from "./session-provider.js";
6
+ import { isSessionProvider, providerCliInstalled } from "./session-provider.js";
7
7
  import { runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
8
8
  import { composeSystemFallback, systemPromptFlag, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, thinkingEffortToPiLevel, } from "./structured-provider-common.js";
9
9
  /** provider 缺省值:无法识别时回落到 Claude。 */
@@ -422,6 +422,23 @@ function extractPiText(stdout) {
422
422
  }
423
423
  return text.trim();
424
424
  }
425
+ /** Gemini `--output-format stream-json`:assistant message 是增量 chunk,拼接即可。 */
426
+ function extractGeminiText(stdout) {
427
+ let text = "";
428
+ for (const line of stdout.split(/\r?\n/)) {
429
+ const trimmed = line.trim();
430
+ if (!trimmed.startsWith("{"))
431
+ continue;
432
+ try {
433
+ const event = JSON.parse(trimmed);
434
+ if (event.type === "message" && event.role === "assistant" && typeof event.content === "string") {
435
+ text += event.content;
436
+ }
437
+ }
438
+ catch { /* ignore non-protocol stdout */ }
439
+ }
440
+ return text.trim();
441
+ }
425
442
  function buildPiTextArgs(request, opts, allowTools = false) {
426
443
  const args = ["--mode", "json", "--print", "--no-session"];
427
444
  if (!allowTools)
@@ -449,6 +466,23 @@ function buildGrokTextArgs(request, opts, allowTools = false) {
449
466
  args.push("--always-approve");
450
467
  return args;
451
468
  }
469
+ /**
470
+ * Gemini CLI 没有 `--append-system-prompt`,规则走 `contentWithSystemPrompt` 并进内容;
471
+ * headless 下必须带 `--skip-trust`,否则未信任的工作目录会直接以 55 退出。
472
+ *
473
+ * `-p ""` 只把 CLI 钉在 headless 模式,真正的 prompt 走 stdin:commit diff 可能很大,
474
+ * 塞进 argv 会撞 ARG_MAX。Gemini 没有 Codex `--ephemeral` 那样的临时会话开关,
475
+ * 一次性调用会留在它自己的聊天记录里(Wand 不扫描 Gemini 历史,不会误当可恢复会话)。
476
+ */
477
+ function buildGeminiTextArgs(opts, allowTools = false) {
478
+ const args = ["-p", "", "--output-format", "stream-json", "--skip-trust"];
479
+ const model = opts.model?.trim();
480
+ if (model && model !== "default")
481
+ args.push("--model", model);
482
+ if (allowTools)
483
+ args.push("--approval-mode", "yolo");
484
+ return args;
485
+ }
452
486
  function buildQoderTextArgs(request, opts, allowTools = false) {
453
487
  const args = ["-p", request.prompt, "--output-format", "stream-json", "--no-session-persistence"];
454
488
  const model = opts.model?.trim();
@@ -595,6 +629,17 @@ async function callPiText(request, cwd, opts) {
595
629
  throw new QuickCommitError("Pi 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
596
630
  return text;
597
631
  }
632
+ async function callGeminiText(request, cwd, opts) {
633
+ const stdout = await runCliText("gemini", buildGeminiTextArgs(opts), contentWithSystemPrompt("gemini", request), {
634
+ cwd,
635
+ timeoutMs: CODEX_MESSAGE_TIMEOUT_MS,
636
+ inheritEnv: opts.inheritEnv,
637
+ });
638
+ const text = extractGeminiText(stdout);
639
+ if (!text)
640
+ throw new QuickCommitError("Gemini 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
641
+ return text;
642
+ }
598
643
  async function callCliAiText(request, cwd, language, opts) {
599
644
  const provider = defaultProvider(opts.provider);
600
645
  if (provider === "codex") {
@@ -608,6 +653,8 @@ async function callCliAiText(request, cwd, language, opts) {
608
653
  return callQoderText(request, cwd, opts);
609
654
  if (provider === "pi")
610
655
  return callPiText(request, cwd, opts);
656
+ if (provider === "gemini")
657
+ return callGeminiText(request, cwd, opts);
611
658
  return callClaudeText(request, cwd, language, opts);
612
659
  }
613
660
  async function callDirectApiText(request, systemAi) {
@@ -621,28 +668,89 @@ function aiFallbackFailed(primary, primaryError, fallbackError) {
621
668
  const fallback = primary === "直连 API" ? "CLI" : "直连 API";
622
669
  return new QuickCommitError(`${primary} 与 ${fallback} 均失败:${getGitErrorMessage(primaryError)};${getGitErrorMessage(fallbackError)}`, "AI_FALLBACK_FAILED");
623
670
  }
671
+ /**
672
+ * 多候选降级时给整条链的时间上限。原生客户端的请求宽限约 180s,
673
+ * 候选再多也不能把整个请求拖死;单次仍受各自 CLI 超时保护。
674
+ */
675
+ const CLI_CHAIN_BUDGET_MS = 150_000;
676
+ /** 角色设定走系统提示前缀;任务自己的输出格式与约束必须排在后面。 */
677
+ export function withOpsPersona(request, persona) {
678
+ const trimmed = (persona ?? "").trim();
679
+ if (!trimmed)
680
+ return request;
681
+ const task = (request.system ?? "").trim();
682
+ return { ...request, system: task ? `${trimmed}\n\n${task}` : trimmed };
683
+ }
684
+ /** 链上的每条候选变成一次普通的 CLI 文本调用参数。 */
685
+ function candidateOptions(base, candidate) {
686
+ return {
687
+ ...base,
688
+ provider: candidate.provider,
689
+ model: candidate.model ?? null,
690
+ thinkingEffort: candidate.thinkingEffort ?? base.thinkingEffort ?? undefined,
691
+ };
692
+ }
693
+ /** 没有候选链时,把当前 provider/model 当作唯一的候选。 */
694
+ function singleCandidate(opts) {
695
+ return {
696
+ provider: defaultProvider(opts.provider),
697
+ model: opts.model ?? undefined,
698
+ thinkingEffort: opts.thinkingEffort ?? undefined,
699
+ };
700
+ }
701
+ /**
702
+ * 按顺序尝试候选:跳过没安装的 CLI,逐条收集错误,成功即返回。
703
+ * 只有一条候选时保持原行为(错误原样抛出,不摘要)。
704
+ */
705
+ async function callCliCandidates(request, cwd, language, opts) {
706
+ const chain = opts.cliCandidates?.length ? opts.cliCandidates : [singleCandidate(opts)];
707
+ if (chain.length === 1) {
708
+ return callCliAiText(request, cwd, language, candidateOptions(opts, chain[0]));
709
+ }
710
+ const installed = chain.filter((candidate) => providerCliInstalled(candidate.provider));
711
+ const attempts = installed.length ? installed : chain;
712
+ const deadline = Date.now() + CLI_CHAIN_BUDGET_MS;
713
+ const errors = [];
714
+ for (const candidate of attempts) {
715
+ if (Date.now() > deadline) {
716
+ errors.push("已超过等待上限");
717
+ break;
718
+ }
719
+ try {
720
+ const text = await callCliAiText(request, cwd, language, candidateOptions(opts, candidate));
721
+ if (text.trim())
722
+ return text;
723
+ errors.push(`${candidate.provider}: 返回空结果`);
724
+ }
725
+ catch (error) {
726
+ errors.push(`${candidate.provider}: ${getGitErrorMessage(error)}`);
727
+ }
728
+ }
729
+ throw new QuickCommitError(`所有 CLI 候选均失败:${errors.join(";")}`, "AI_FALLBACK_FAILED");
730
+ }
624
731
  /**
625
732
  * Run a lightweight AI request through the selected Commit source. API mode
626
- * exhausts its profile chain and then falls back once to the current-session
627
- * CLI. CLI mode uses only that CLI.
733
+ * exhausts its profile chain and then falls back once to the system employee's
734
+ * CLI chain. CLI mode walks that chain in order.
628
735
  */
629
736
  export async function callConfiguredAiText(request, cwd, language, opts) {
737
+ const effective = withOpsPersona(request, opts.opsPersona);
630
738
  if (opts.systemAi?.enabled) {
631
739
  try {
632
- return await callDirectApiText(request, opts.systemAi);
740
+ return await callDirectApiText(effective, opts.systemAi);
633
741
  }
634
742
  catch (apiError) {
635
743
  try {
636
744
  // The user selected the direct API first. If it is unavailable or
637
- // empty, retry this exact request through their selected CLI.
638
- return await callCliAiText(request, cwd, language, opts);
745
+ // empty, retry this exact request through their selected CLI chain.
746
+ return await callCliCandidates(effective, cwd, language, opts);
639
747
  }
640
748
  catch (cliError) {
641
749
  throw aiFallbackFailed("直连 API", apiError, cliError);
642
750
  }
643
751
  }
644
752
  }
645
- return callCliAiText(request, cwd, language, opts);
753
+ return callCliCandidates(effective, cwd, language, opts);
646
754
  }
647
755
  /** Read the unstaged + staged tree without touching the index. */
648
756
  async function collectWorkingTreeDiff(cwd) {
@@ -1276,7 +1384,7 @@ async function getLatestTagAtHead(cwd) {
1276
1384
  async function runQuickCommitFallbackCli(opts, priorError) {
1277
1385
  await assertGitWorkTreeAsync(opts.cwd);
1278
1386
  const beforeHead = await getHead(opts.cwd);
1279
- const request = buildFallbackPrompt(opts, priorError);
1387
+ const request = withOpsPersona(buildFallbackPrompt(opts, priorError), opts.opsPersona);
1280
1388
  const provider = defaultProvider(opts.provider);
1281
1389
  if (provider === "codex") {
1282
1390
  const args = ["exec", "--ephemeral", "--json", "--color", "never", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"];
@@ -1328,6 +1436,13 @@ async function runQuickCommitFallbackCli(opts, priorError) {
1328
1436
  inheritEnv: opts.inheritEnv,
1329
1437
  });
1330
1438
  }
1439
+ else if (provider === "gemini") {
1440
+ await runCliText("gemini", buildGeminiTextArgs(opts, true), contentWithSystemPrompt("gemini", request), {
1441
+ cwd: opts.cwd,
1442
+ timeoutMs: QUICK_COMMIT_CLI_TIMEOUT_MS,
1443
+ inheritEnv: opts.inheritEnv,
1444
+ });
1445
+ }
1331
1446
  else {
1332
1447
  const args = [
1333
1448
  "-p",
package/dist/models.d.ts CHANGED
@@ -49,6 +49,8 @@ export interface ProviderThinkingEfforts {
49
49
  grok: ThinkingEffortLevel[];
50
50
  qoder: ThinkingEffortLevel[];
51
51
  pi: ThinkingEffortLevel[];
52
+ /** Gemini CLI 没有思考档位开关,只保留「不覆盖」一项,不假装能调。 */
53
+ gemini: ThinkingEffortLevel[];
52
54
  }
53
55
  export interface ModelCache {
54
56
  models: ClaudeModelInfo[];
@@ -57,6 +59,7 @@ export interface ModelCache {
57
59
  grokModels: ClaudeModelInfo[];
58
60
  qoderModels: ClaudeModelInfo[];
59
61
  piModels: ClaudeModelInfo[];
62
+ geminiModels: ClaudeModelInfo[];
60
63
  thinkingEfforts: ProviderThinkingEfforts;
61
64
  claudeVersion: string | null;
62
65
  opencodeVersion: string | null;