@co0ontty/wand 4.89.5 → 4.90.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 (197) hide show
  1. package/README.md +5 -3
  2. package/dist/agent-dispatch.js +4 -9
  3. package/dist/ai-team-employee-binding.js +10 -9
  4. package/dist/ai-team-runner.d.ts +2 -1
  5. package/dist/ai-team-runner.js +38 -2
  6. package/dist/ai-team-types.js +3 -2
  7. package/dist/ai-text-runner.d.ts +61 -0
  8. package/dist/ai-text-runner.js +653 -0
  9. package/dist/avatar-input.d.ts +2 -0
  10. package/dist/avatar-input.js +19 -0
  11. package/dist/bounded-process-output.d.ts +5 -0
  12. package/dist/bounded-process-output.js +9 -0
  13. package/dist/build-info.json +3 -3
  14. package/dist/command-error.d.ts +6 -0
  15. package/dist/command-error.js +8 -0
  16. package/dist/config.js +11 -42
  17. package/dist/conversation-service.d.ts +12 -2
  18. package/dist/conversation-service.js +104 -25
  19. package/dist/conversation-session-preview.d.ts +2 -0
  20. package/dist/conversation-session-preview.js +4 -0
  21. package/dist/decision-client.js +22 -6
  22. package/dist/decision-expert-employee.js +2 -2
  23. package/dist/decision-expert-identity.d.ts +1 -1
  24. package/dist/decision-expert-identity.js +1 -1
  25. package/dist/decision-expert-service.d.ts +1 -0
  26. package/dist/decision-expert-service.js +82 -76
  27. package/dist/decision-hardware.js +4 -3
  28. package/dist/decision-types.d.ts +4 -0
  29. package/dist/decision-types.js +4 -0
  30. package/dist/deployment-lock.d.ts +13 -0
  31. package/dist/deployment-lock.js +189 -0
  32. package/dist/employee-text.js +1 -1
  33. package/dist/fixed-employee-avatar.d.ts +7 -0
  34. package/dist/fixed-employee-avatar.js +13 -0
  35. package/dist/git-quick-commit.d.ts +5 -40
  36. package/dist/git-quick-commit.js +8 -636
  37. package/dist/hr-employee-identity.d.ts +5 -0
  38. package/dist/hr-employee-identity.js +5 -0
  39. package/dist/hr-employee.d.ts +5 -0
  40. package/dist/hr-employee.js +21 -0
  41. package/dist/middleware/path-safety.js +1 -1
  42. package/dist/milestone-scope.js +1 -1
  43. package/dist/model-auto-assign.d.ts +1 -1
  44. package/dist/model-auto-assign.js +1 -1
  45. package/dist/models.d.ts +4 -0
  46. package/dist/models.js +21 -1
  47. package/dist/npm-update-utils.js +5 -44
  48. package/dist/pi-auto-resources.js +6 -3
  49. package/dist/plush-avatar.d.ts +10 -1
  50. package/dist/plush-avatar.js +13 -1
  51. package/dist/plush-cat-appearance.d.ts +97 -0
  52. package/dist/plush-cat-appearance.js +118 -0
  53. package/dist/plush-cat-whiskers.d.ts +6 -0
  54. package/dist/plush-cat-whiskers.js +42 -0
  55. package/dist/process-manager.d.ts +22 -21
  56. package/dist/process-manager.js +49 -83
  57. package/dist/prompt-optimizer.d.ts +1 -1
  58. package/dist/prompt-optimizer.js +1 -1
  59. package/dist/render-host.js +1 -19
  60. package/dist/scheduled-task-service.d.ts +41 -0
  61. package/dist/scheduled-task-service.js +294 -0
  62. package/dist/scheduled-task-state.d.ts +11 -0
  63. package/dist/scheduled-task-state.js +65 -0
  64. package/dist/scheduled-task-time.d.ts +4 -0
  65. package/dist/scheduled-task-time.js +82 -0
  66. package/dist/scheduled-task-types.d.ts +71 -0
  67. package/dist/scheduled-task-types.js +1 -0
  68. package/dist/server-ai-team-routes.js +3 -17
  69. package/dist/server-employee-routes.d.ts +3 -0
  70. package/dist/server-employee-routes.js +20 -38
  71. package/dist/server-file-routes.js +44 -1
  72. package/dist/server-pi-recommendation-routes.js +13 -4
  73. package/dist/server-request.d.ts +2 -0
  74. package/dist/server-request.js +9 -0
  75. package/dist/server-resume-routes.js +1 -1
  76. package/dist/server-scheduled-task-routes.d.ts +4 -0
  77. package/dist/server-scheduled-task-routes.js +34 -0
  78. package/dist/server-session-routes.d.ts +5 -13
  79. package/dist/server-session-routes.js +35 -70
  80. package/dist/server-task-routes.d.ts +6 -21
  81. package/dist/server-task-routes.js +25 -120
  82. package/dist/server-team-dispatch-routes.js +12 -2
  83. package/dist/server-workspace-routes.d.ts +1 -1
  84. package/dist/server-workspace-routes.js +33 -78
  85. package/dist/server.js +55 -20
  86. package/dist/session-model.d.ts +15 -0
  87. package/dist/session-model.js +23 -0
  88. package/dist/session-registry.d.ts +5 -1
  89. package/dist/session-registry.js +20 -11
  90. package/dist/session-request.d.ts +9 -0
  91. package/dist/session-request.js +27 -0
  92. package/dist/session-topic.d.ts +1 -1
  93. package/dist/session-topic.js +1 -1
  94. package/dist/session-transport.d.ts +3 -1
  95. package/dist/session-transport.js +11 -5
  96. package/dist/silicon-employee-draft.d.ts +2 -2
  97. package/dist/silicon-employee-draft.js +3 -3
  98. package/dist/socket-probe.d.ts +2 -0
  99. package/dist/socket-probe.js +20 -0
  100. package/dist/storage-schedules.d.ts +20 -0
  101. package/dist/storage-schedules.js +78 -0
  102. package/dist/storage-schema.d.ts +3 -0
  103. package/dist/storage-schema.js +702 -0
  104. package/dist/storage.d.ts +42 -8
  105. package/dist/storage.js +182 -715
  106. package/dist/structured-claude-adapter.js +12 -0
  107. package/dist/structured-claude-protocol.d.ts +4 -0
  108. package/dist/structured-claude-protocol.js +18 -4
  109. package/dist/structured-codex-adapter.js +11 -0
  110. package/dist/structured-exec-pump.js +79 -38
  111. package/dist/structured-failure.d.ts +21 -0
  112. package/dist/structured-failure.js +14 -0
  113. package/dist/structured-gemini-adapter.js +12 -2
  114. package/dist/structured-grok-adapter.js +12 -2
  115. package/dist/structured-opencode-adapter.js +8 -1
  116. package/dist/structured-qoder-adapter.js +14 -0
  117. package/dist/structured-session-manager.d.ts +13 -18
  118. package/dist/structured-session-manager.js +147 -517
  119. package/dist/system-employee.d.ts +1 -1
  120. package/dist/system-employee.js +1 -1
  121. package/dist/task-archive-policy.js +1 -1
  122. package/dist/task-read-model.d.ts +27 -0
  123. package/dist/task-read-model.js +47 -0
  124. package/dist/task-retention.d.ts +1 -1
  125. package/dist/task-retention.js +1 -1
  126. package/dist/task-title-service.d.ts +30 -0
  127. package/dist/task-title-service.js +122 -0
  128. package/dist/task-title.d.ts +1 -1
  129. package/dist/task-title.js +1 -1
  130. package/dist/terminal-daemon-client.js +1 -18
  131. package/dist/user-memory.js +1 -1
  132. package/dist/web-ui/asset-runtime.d.ts +12 -0
  133. package/dist/web-ui/asset-runtime.js +6 -0
  134. package/dist/web-ui/composer-avatar-activity.d.ts +6 -0
  135. package/dist/web-ui/composer-avatar-activity.js +29 -0
  136. package/dist/web-ui/content/ai-teams.js +13 -13
  137. package/dist/web-ui/content/module-manifest.json +1 -0
  138. package/dist/web-ui/content/modules/5478be410b112154/chat-render-PY6DIXNZ.js +24 -0
  139. package/dist/web-ui/content/modules/5478be410b112154/chunk-2YZLCGYN.js +1 -0
  140. package/dist/web-ui/content/modules/5478be410b112154/chunk-6CFHW3FA.js +111 -0
  141. package/dist/web-ui/content/modules/5478be410b112154/chunk-6XQEBIEX.js +1 -0
  142. package/dist/web-ui/content/modules/5478be410b112154/chunk-DDSO7OX7.js +1 -0
  143. package/dist/web-ui/content/modules/5478be410b112154/chunk-G3CXNMA2.js +20 -0
  144. package/dist/web-ui/content/modules/5478be410b112154/chunk-LGVAPHRS.js +36 -0
  145. package/dist/web-ui/content/modules/5478be410b112154/chunk-NURCNMRU.js +1 -0
  146. package/dist/web-ui/content/modules/5478be410b112154/chunk-PCMFGZKX.js +1 -0
  147. package/dist/web-ui/content/modules/5478be410b112154/chunk-QRNA4D7E.js +1 -0
  148. package/dist/web-ui/content/modules/5478be410b112154/chunk-TMRPHNJO.js +1 -0
  149. package/dist/web-ui/content/modules/5478be410b112154/chunk-WC7RK7LP.js +1 -0
  150. package/dist/web-ui/content/modules/5478be410b112154/chunk-YOXXNXOX.js +2 -0
  151. package/dist/web-ui/content/modules/5478be410b112154/chunk-YX475LXO.js +1 -0
  152. package/dist/web-ui/content/modules/5478be410b112154/employee-card-MDWKU4V3.js +1 -0
  153. package/dist/web-ui/content/modules/5478be410b112154/host-56WPSON2.js +1 -0
  154. package/dist/web-ui/content/modules/5478be410b112154/host-5QOHKHNC.js +1 -0
  155. package/dist/web-ui/content/modules/5478be410b112154/host-AWIJ5L2K.js +1 -0
  156. package/dist/web-ui/content/modules/5478be410b112154/host-BOZLGIT5.js +69 -0
  157. package/dist/web-ui/content/modules/5478be410b112154/host-EZV22Q7Y.js +4 -0
  158. package/dist/web-ui/content/modules/5478be410b112154/host-IW43Z5ZF.js +1 -0
  159. package/dist/web-ui/content/modules/5478be410b112154/host-JT7NIUQL.js +1 -0
  160. package/dist/web-ui/content/modules/5478be410b112154/host-JWEIDB55.js +3 -0
  161. package/dist/web-ui/content/modules/5478be410b112154/host-OU5TRYH6.js +2 -0
  162. package/dist/web-ui/content/modules/5478be410b112154/host-PRSTCPVE.js +5 -0
  163. package/dist/web-ui/content/modules/5478be410b112154/host-TWPJI3HE.js +1 -0
  164. package/dist/web-ui/content/modules/5478be410b112154/host-WZGJSNML.js +1 -0
  165. package/dist/web-ui/content/modules/5478be410b112154/host-X6KQVFD6.js +1 -0
  166. package/dist/web-ui/content/modules/5478be410b112154/host-YKCQFMQR.js +1 -0
  167. package/dist/web-ui/content/modules/5478be410b112154/host-bridge-2LZX6UUZ.js +1 -0
  168. package/dist/web-ui/content/modules/5478be410b112154/library-bridge-MQFJGTGY.js +4 -0
  169. package/dist/web-ui/content/modules/5478be410b112154/page-T3VUCU2W.js +3 -0
  170. package/dist/web-ui/content/modules/5478be410b112154/panel-HJBGBBPY.js +1 -0
  171. package/dist/web-ui/content/modules/5478be410b112154/runtime.js +920 -0
  172. package/dist/web-ui/content/modules/5478be410b112154/sha256-ANXV72UC.js +1 -0
  173. package/dist/web-ui/content/modules/5478be410b112154/task-board-host-AVMDIHIV.js +2 -0
  174. package/dist/web-ui/content/modules/5478be410b112154/workspace-agent-picker-KI7QVSK3.js +1 -0
  175. package/dist/web-ui/content/modules/5478be410b112154/workspace-window-L542FGP2.js +1 -0
  176. package/dist/web-ui/content/plush-avatar.js +177 -177
  177. package/dist/web-ui/content/scripts.js +1 -994
  178. package/dist/web-ui/content/styles.css +1 -1
  179. package/dist/web-ui/content/tailwind.css +1 -1
  180. package/dist/web-ui/embedded-assets.d.ts +32 -0
  181. package/dist/web-ui/embedded-assets.js +10 -8
  182. package/dist/web-ui/markdown.d.ts +3 -2
  183. package/dist/web-ui/markdown.js +61 -48
  184. package/dist/web-ui/message-file-target.d.ts +13 -0
  185. package/dist/web-ui/message-file-target.js +85 -0
  186. package/dist/web-ui/provider-identity.d.ts +1 -1
  187. package/dist/web-ui/provider-identity.js +3 -4
  188. package/dist/web-ui/react/local-preview/controller.d.ts +2 -0
  189. package/dist/web-ui/react/local-preview/controller.js +12 -0
  190. package/dist/web-ui/scripts.d.ts +10 -0
  191. package/dist/web-ui/scripts.js +42 -5
  192. package/package.json +6 -3
  193. package/browser-extension/icons/wand-128.png +0 -0
  194. package/browser-extension/icons/wand-16.png +0 -0
  195. package/browser-extension/icons/wand-32.png +0 -0
  196. package/browser-extension/icons/wand-48.png +0 -0
  197. package/dist/web-ui/content/licenses/codex-LICENSE.txt +0 -21
@@ -0,0 +1,653 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { StringDecoder } from "node:string_decoder";
7
+ import { appendDiagnosticTail, MAX_AI_TEXT_BYTES } from "./bounded-process-output.js";
8
+ import { QuickCommitError } from "./command-error.js";
9
+ import { abortableEmployeeText, prepareEmployeeTextCandidate } from "./employee-text.js";
10
+ import { buildChildEnv } from "./env-utils.js";
11
+ import { getGitErrorMessage } from "./git-utils.js";
12
+ import { buildLanguageDirective } from "./language-prompt.js";
13
+ import { isSessionProvider, providerCliInstalled } from "./session-provider.js";
14
+ import { asRecord } from "./structured-content.js";
15
+ import { composeSystemFallback, systemPromptFlag, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel, thinkingEffortToQoderEffort } from "./structured-provider-common.js";
16
+ export { QuickCommitError } from "./command-error.js";
17
+ const CLAUDE_MESSAGE_TIMEOUT_MS = 60_000;
18
+ const CODEX_MESSAGE_TIMEOUT_MS = 60_000;
19
+ /** Neutral one-shot text execution. Tool-running Git fallback never uses the retry chain. */
20
+ export function defaultProvider(provider) {
21
+ return isSessionProvider(provider) ? provider : "claude";
22
+ }
23
+ function cliTextTimeoutMs(defaultMs, opts) {
24
+ return opts.deadline === undefined ? defaultMs : Math.min(defaultMs, Math.max(1, opts.deadline - Date.now()));
25
+ }
26
+ /**
27
+ * 一次性 Claude 文本生成(commit message / tag / 提示词优化…)。
28
+ *
29
+ * 直接 spawn 本机 `claude -p`,与 PTY / 结构化会话同源:认证、模型、`~/.claude`
30
+ * 配置完全一致。对齐原来 SDK 时代的行为:
31
+ * - `--tools ""` 关掉全部工具:纯文本生成不需要工具,也避免一次性短调用顺手读文件;
32
+ * - `--no-session-persistence`:不污染 `~/.claude/projects/` 的会话历史;
33
+ * - `--strict-mcp-config`(不给 --mcp-config):跳过用户配置的 MCP,省启动时间与 token。
34
+ * 语言指令与任务规则都走 `--append-system-prompt`,任务自己的输出格式排在后面。
35
+ */
36
+ async function callClaudeText(request, cwd, language, opts) {
37
+ const args = ["-p", "--output-format", "text", "--tools", "", "--no-session-persistence", "--strict-mcp-config"];
38
+ const languageDirective = language ? buildLanguageDirective(language) : "";
39
+ const systemPrompt = [languageDirective, (request.system ?? "").trim()].filter(Boolean).join("\n\n");
40
+ if (systemPrompt)
41
+ args.push("--append-system-prompt", systemPrompt);
42
+ const model = opts.model?.trim();
43
+ if (model && model !== "default")
44
+ args.push("--model", model);
45
+ const effort = thinkingEffortToClaudeCliEffort(opts.thinkingEffort ?? "off");
46
+ if (effort)
47
+ args.push("--effort", effort);
48
+ const stdout = await runCliText("claude", args, request.prompt, {
49
+ cwd,
50
+ timeoutMs: cliTextTimeoutMs(CLAUDE_MESSAGE_TIMEOUT_MS, opts),
51
+ inheritEnv: opts.inheritEnv, signal: opts.signal,
52
+ });
53
+ const text = stdout.trim();
54
+ if (!text)
55
+ throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
56
+ return text;
57
+ }
58
+ function stripFences(raw) {
59
+ return raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
60
+ }
61
+ export function normalizeAiText(raw) {
62
+ return stripFences(raw).replace(/^["'`]+|["'`]+$/g, "").trim();
63
+ }
64
+ /** Read explicit protocol failures, not error-looking prose that could be legitimate output. */
65
+ function cliProtocolErrorText(value) {
66
+ if (typeof value === "string")
67
+ return value.trim();
68
+ if (Array.isArray(value))
69
+ return value.map(cliProtocolErrorText).filter(Boolean).join("\n");
70
+ const record = asRecord(value);
71
+ if (!record)
72
+ return "";
73
+ for (const key of ["message", "error", "errors", "data", "result"]) {
74
+ const text = cliProtocolErrorText(record[key]);
75
+ if (text)
76
+ return text;
77
+ }
78
+ return "";
79
+ }
80
+ /** A zero process exit does not imply a successful model response. */
81
+ function assertCliProtocolSucceeded(stdout, provider) {
82
+ let failure = null;
83
+ const failed = (value) => cliProtocolErrorText(value) || `${provider} CLI execution failed`;
84
+ for (const line of stdout.split(/\r?\n/)) {
85
+ let event;
86
+ try {
87
+ event = asRecord(JSON.parse(line));
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ if (!event)
93
+ continue;
94
+ if (provider === "codex") {
95
+ if (event.type === "error" || event.type === "turn.failed")
96
+ failure = failed(event);
97
+ if (event.type === "turn.completed")
98
+ failure = null;
99
+ }
100
+ else if (provider === "qoder" && event.type === "result") {
101
+ failure = event.is_error === true || event.subtype !== "success" ? failed(event) : null;
102
+ }
103
+ else if (provider === "pi") {
104
+ const messages = event.type === "agent_end" && Array.isArray(event.messages)
105
+ ? event.messages
106
+ : event.type === "message_end" || event.type === "turn_end" ? [event.message] : [];
107
+ for (const raw of messages) {
108
+ const message = asRecord(raw);
109
+ if (message?.role !== "assistant")
110
+ continue;
111
+ failure = message.stopReason === "error" || message.stopReason === "aborted"
112
+ ? failed(message.errorMessage) : null;
113
+ }
114
+ }
115
+ else if (provider === "gemini") {
116
+ if (event.type === "error" && event.severity === "error")
117
+ failure = failed(event);
118
+ if (event.type === "result")
119
+ failure = event.status === "error" ? failed(event) : null;
120
+ }
121
+ else if ((provider === "grok" || provider === "opencode") && event.type === "error") {
122
+ failure = failed(event);
123
+ }
124
+ }
125
+ if (failure)
126
+ throw new QuickCommitError(`${provider} CLI 失败:${failure}`, "CLAUDE_CLI_FAILED");
127
+ }
128
+ function extractCodexText(stdout) {
129
+ assertCliProtocolSucceeded(stdout, "codex");
130
+ let lastAgentText = "";
131
+ for (const line of stdout.split(/\r?\n/)) {
132
+ const trimmed = line.trim();
133
+ if (!trimmed.startsWith("{"))
134
+ continue;
135
+ try {
136
+ const parsed = JSON.parse(trimmed);
137
+ if (parsed.type === "item.completed" && parsed.item?.type === "agent_message" && typeof parsed.item.text === "string") {
138
+ lastAgentText = parsed.item.text;
139
+ }
140
+ }
141
+ catch {
142
+ // ignore non-JSON diagnostics mixed into stdout
143
+ }
144
+ }
145
+ if (lastAgentText)
146
+ return lastAgentText.trim();
147
+ const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
148
+ const noise = /^(OpenAI Codex|[-]+$|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|codex$|tokens used$|[0-9,]+$)/i;
149
+ for (let i = lines.length - 1; i >= 0; i--) {
150
+ if (!noise.test(lines[i]))
151
+ return lines[i];
152
+ }
153
+ return "";
154
+ }
155
+ function extractOpenCodeText(stdout) {
156
+ assertCliProtocolSucceeded(stdout, "opencode");
157
+ const texts = [];
158
+ for (const line of stdout.split(/\r?\n/)) {
159
+ const trimmed = line.trim();
160
+ if (!trimmed.startsWith("{"))
161
+ continue;
162
+ try {
163
+ const parsed = JSON.parse(trimmed);
164
+ if (parsed.type === "text" && typeof parsed.part?.text === "string" && parsed.part.text.trim()) {
165
+ texts.push(parsed.part.text.trim());
166
+ }
167
+ }
168
+ catch {
169
+ // ignore diagnostics mixed into stdout
170
+ }
171
+ }
172
+ return texts.join("\n").trim();
173
+ }
174
+ function extractGrokText(stdout) {
175
+ assertCliProtocolSucceeded(stdout, "grok");
176
+ let text = "";
177
+ for (const line of stdout.split(/\r?\n/)) {
178
+ const trimmed = line.trim();
179
+ if (!trimmed.startsWith("{"))
180
+ continue;
181
+ try {
182
+ const parsed = JSON.parse(trimmed);
183
+ if (parsed.type === "text" && typeof parsed.data === "string")
184
+ text += parsed.data;
185
+ }
186
+ catch {
187
+ // ignore diagnostics mixed into stdout
188
+ }
189
+ }
190
+ return text.trim();
191
+ }
192
+ function extractQoderText(stdout) {
193
+ assertCliProtocolSucceeded(stdout, "qoder");
194
+ let resultText = "";
195
+ const assistantTexts = [];
196
+ for (const line of stdout.split(/\r?\n/)) {
197
+ const trimmed = line.trim();
198
+ if (!trimmed.startsWith("{"))
199
+ continue;
200
+ try {
201
+ const parsed = JSON.parse(trimmed);
202
+ if (parsed.type === "result" && parsed.subtype === "success" && typeof parsed.result === "string") {
203
+ resultText = parsed.result;
204
+ continue;
205
+ }
206
+ if (parsed.type !== "assistant" || !Array.isArray(parsed.message?.content))
207
+ continue;
208
+ for (const block of parsed.message.content) {
209
+ if (block
210
+ && typeof block === "object"
211
+ && block.type === "text"
212
+ && typeof block.text === "string") {
213
+ assistantTexts.push(block.text);
214
+ }
215
+ }
216
+ }
217
+ catch {
218
+ // ignore diagnostics mixed into stdout
219
+ }
220
+ }
221
+ return (resultText || assistantTexts.join("\n")).trim();
222
+ }
223
+ function extractPiText(stdout) {
224
+ assertCliProtocolSucceeded(stdout, "pi");
225
+ let text = "";
226
+ for (const line of stdout.split(/\r?\n/)) {
227
+ try {
228
+ const event = JSON.parse(line);
229
+ if (event.type !== "message_end" || event.message?.role !== "assistant" || !Array.isArray(event.message.content))
230
+ continue;
231
+ text = event.message.content
232
+ .filter((block) => !!block && typeof block === "object" && block.type === "text" && typeof block.text === "string")
233
+ .map((block) => block.text)
234
+ .join("\n");
235
+ }
236
+ catch { /* ignore non-protocol stdout */ }
237
+ }
238
+ return text.trim();
239
+ }
240
+ /** Gemini `--output-format stream-json`:assistant message 是增量 chunk,拼接即可。 */
241
+ function extractGeminiText(stdout) {
242
+ assertCliProtocolSucceeded(stdout, "gemini");
243
+ let text = "";
244
+ for (const line of stdout.split(/\r?\n/)) {
245
+ const trimmed = line.trim();
246
+ if (!trimmed.startsWith("{"))
247
+ continue;
248
+ try {
249
+ const event = JSON.parse(trimmed);
250
+ if (event.type === "message" && event.role === "assistant" && typeof event.content === "string") {
251
+ text += event.content;
252
+ }
253
+ }
254
+ catch { /* ignore non-protocol stdout */ }
255
+ }
256
+ return text.trim();
257
+ }
258
+ export function buildPiTextArgs(request, opts, allowTools = false) {
259
+ const args = ["--mode", "json", "--print", "--no-session"];
260
+ if (!allowTools)
261
+ args.push("--no-tools");
262
+ const model = opts.model?.trim();
263
+ if (model && model !== "default")
264
+ args.push("--model", model);
265
+ const level = thinkingEffortToPiLevel(opts.thinkingEffort ?? "off");
266
+ if (level)
267
+ args.push("--thinking", level);
268
+ args.push(...systemPromptArgsFrom("pi", request.system));
269
+ args.push(request.prompt);
270
+ return args;
271
+ }
272
+ export function buildGrokTextArgs(request, opts, allowTools = false) {
273
+ const args = ["--no-auto-update", "-p", request.prompt, "--output-format", "streaming-json"];
274
+ const model = opts.model?.trim();
275
+ if (model && model !== "default")
276
+ args.push("--model", model);
277
+ const effort = thinkingEffortToGrokEffort(opts.thinkingEffort ?? "off");
278
+ if (effort)
279
+ args.push("--effort", effort);
280
+ args.push(...systemPromptArgsFrom("grok", request.system));
281
+ if (allowTools)
282
+ args.push("--always-approve");
283
+ else
284
+ args.push("--tools", "", "--disallowed-tools", "run_terminal_cmd,read_file,search_replace,grep,list_dir,web_search,web_fetch,todo_write,task,Agent", "--deny", "MCPTool", "--disable-web-search", "--no-subagents");
285
+ return args;
286
+ }
287
+ /**
288
+ * Gemini CLI 没有 `--append-system-prompt`,规则走 `contentWithSystemPrompt` 并进内容;
289
+ * headless 下必须带 `--skip-trust`,否则未信任的工作目录会直接以 55 退出。
290
+ *
291
+ * `-p ""` 只把 CLI 钉在 headless 模式,真正的 prompt 走 stdin:commit diff 可能很大,
292
+ * 塞进 argv 会撞 ARG_MAX。Gemini 没有 Codex `--ephemeral` 那样的临时会话开关,
293
+ * 一次性调用会留在它自己的聊天记录里(Wand 不扫描 Gemini 历史,不会误当可恢复会话)。
294
+ */
295
+ export function buildGeminiTextArgs(opts, allowTools = false) {
296
+ const args = ["-p", "", "--output-format", "stream-json", "--skip-trust"];
297
+ const model = opts.model?.trim();
298
+ if (model && model !== "default")
299
+ args.push("--model", model);
300
+ if (allowTools)
301
+ args.push("--approval-mode", "yolo");
302
+ return args;
303
+ }
304
+ export function buildQoderTextArgs(request, opts, allowTools = false) {
305
+ const args = ["-p", request.prompt, "--output-format", "stream-json", "--no-session-persistence"];
306
+ const model = opts.model?.trim();
307
+ if (model && model !== "default")
308
+ args.push("--model", model);
309
+ const effort = thinkingEffortToQoderEffort(opts.thinkingEffort ?? "off");
310
+ if (effort)
311
+ args.push("--reasoning-effort", effort);
312
+ args.push(...systemPromptArgsFrom("qoder", request.system));
313
+ if (allowTools)
314
+ args.push("--permission-mode", "bypass_permissions");
315
+ else
316
+ args.push("--tools", "", "--strict-mcp-config");
317
+ return args;
318
+ }
319
+ /** provider 自己的系统提示开关 + 规则文本;没有开关的 provider 返回空。 */
320
+ export function systemPromptArgsFrom(provider, system) {
321
+ const text = system?.trim();
322
+ if (!text)
323
+ return [];
324
+ const flag = systemPromptFlag(provider);
325
+ return flag ? [flag, text] : [];
326
+ }
327
+ /** 没有系统提示开关的 provider(codex / opencode):规则只能并进内容前面。 */
328
+ export function contentWithSystemPrompt(provider, request) {
329
+ return systemPromptFlag(provider) ? request.prompt : composeSystemFallback(request.system, request.prompt);
330
+ }
331
+ export function runCliText(command, args, prompt, opts) {
332
+ opts.signal?.throwIfAborted();
333
+ return new Promise((resolve, reject) => {
334
+ const baseEnv = buildChildEnv(opts.inheritEnv !== false);
335
+ const child = spawn(command, args, {
336
+ cwd: opts.cwd,
337
+ env: opts.env ? opts.env(baseEnv) : baseEnv,
338
+ stdio: ["pipe", "pipe", "pipe"],
339
+ });
340
+ let stdout = "";
341
+ let stdoutBytes = 0;
342
+ const stdoutDecoder = new StringDecoder("utf8");
343
+ const stderrDecoder = new StringDecoder("utf8");
344
+ let stderr = "";
345
+ let settled = false;
346
+ const cancelled = () => {
347
+ if (settled)
348
+ return;
349
+ settled = true;
350
+ clearTimeout(timeout);
351
+ child.kill("SIGTERM");
352
+ reject(opts.signal?.reason ?? new Error("AI 文本请求已取消。"));
353
+ };
354
+ const cleanup = () => { clearTimeout(timeout); opts.signal?.removeEventListener("abort", cancelled); };
355
+ const timeout = setTimeout(() => {
356
+ settled = true;
357
+ opts.signal?.removeEventListener("abort", cancelled);
358
+ child.kill("SIGTERM");
359
+ reject(new QuickCommitError(`${command} 调用超时。`, "CLAUDE_TIMEOUT"));
360
+ }, opts.timeoutMs);
361
+ opts.signal?.addEventListener("abort", cancelled, { once: true });
362
+ child.stdout?.on("data", (chunk) => {
363
+ if (settled)
364
+ return;
365
+ stdoutBytes += chunk.length;
366
+ if (stdoutBytes > MAX_AI_TEXT_BYTES) {
367
+ settled = true;
368
+ cleanup();
369
+ child.kill("SIGTERM");
370
+ reject(new QuickCommitError("AI 文本输出超过 4 MiB 上限,未使用截断结果。", "CLAUDE_CLI_FAILED"));
371
+ return;
372
+ }
373
+ stdout += stdoutDecoder.write(chunk);
374
+ });
375
+ child.stderr?.on("data", (chunk) => {
376
+ if (!settled)
377
+ stderr = appendDiagnosticTail(stderr, stderrDecoder.write(chunk));
378
+ });
379
+ child.on("error", (error) => {
380
+ if (settled)
381
+ return;
382
+ settled = true;
383
+ cleanup();
384
+ const code = error.code === "ENOENT" ? "CLAUDE_CLI_MISSING" : "CLAUDE_CLI_FAILED";
385
+ reject(new QuickCommitError(error.code === "ENOENT" ? `未找到 ${command} CLI。` : `${command} CLI 失败:${error.message}`, code));
386
+ });
387
+ child.on("close", (code) => {
388
+ cleanup();
389
+ if (settled)
390
+ return;
391
+ settled = true;
392
+ stdout += stdoutDecoder.end();
393
+ stderr = appendDiagnosticTail(stderr, stderrDecoder.end());
394
+ if (code === 0) {
395
+ resolve(stdout);
396
+ return;
397
+ }
398
+ reject(new QuickCommitError(`${command} CLI 失败:${(stderr || stdout).trim() || `exit ${code}`}`, "CLAUDE_CLI_FAILED"));
399
+ });
400
+ // A CLI may exit before reading stdin (e.g. invalid credentials/argv).
401
+ // The writable stream emits EPIPE independently of ChildProcess.error;
402
+ // without a listener that becomes an uncaught exception after callers
403
+ // have already finished their turn or closed storage.
404
+ //
405
+ // A closed stdin is only a delivery failure when we actually had prompt
406
+ // bytes to send. Arg-driven providers (e.g. `pi`) are called with an empty
407
+ // prompt, so the child may close stdin before we end the stream; treating
408
+ // that as failure made the same run succeed or fail at random.
409
+ const expectsStdin = prompt.length > 0;
410
+ child.stdin?.on("error", (error) => {
411
+ if (settled || !expectsStdin)
412
+ return;
413
+ settled = true;
414
+ cleanup();
415
+ child.kill("SIGTERM");
416
+ reject(new QuickCommitError(`${command} CLI stdin failed: ${error.code ?? "write error"}`, "CLAUDE_CLI_FAILED"));
417
+ });
418
+ child.stdin?.end(prompt);
419
+ });
420
+ }
421
+ async function callCodexText(request, cwd, opts) {
422
+ // Quick commit is an internal one-shot request, not a user conversation. Keep
423
+ // Codex from persisting it into ~/.codex/sessions, where Wand would otherwise
424
+ // surface the generated prompt as a recoverable session.
425
+ const args = ["exec", "--ephemeral", "--json", "--color", "never", "--skip-git-repo-check", "--sandbox", "read-only"];
426
+ const model = opts.model?.trim();
427
+ if (model && model !== "default")
428
+ args.push("--model", model);
429
+ const reasoningEffort = thinkingEffortToCodexReasoningEffort(opts.thinkingEffort ?? "off");
430
+ if (reasoningEffort)
431
+ args.push("-c", `model_reasoning_effort=${reasoningEffort}`);
432
+ args.push("-");
433
+ const stdout = await runCliText("codex", args, contentWithSystemPrompt("codex", request), {
434
+ cwd,
435
+ timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
436
+ inheritEnv: opts.inheritEnv, signal: opts.signal,
437
+ });
438
+ const text = extractCodexText(stdout);
439
+ if (!text) {
440
+ throw new QuickCommitError("Codex 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
441
+ }
442
+ return text;
443
+ }
444
+ async function callOpenCodeText(request, cwd, opts) {
445
+ const agent = `wand-text-${randomUUID()}`;
446
+ const args = ["run", "--format", "json", "--pure", "--agent", agent];
447
+ const model = opts.model?.trim();
448
+ if (model && model !== "default")
449
+ args.push("--model", model);
450
+ const variant = thinkingEffortToOpenCodeVariant(opts.thinkingEffort ?? "off");
451
+ if (variant)
452
+ args.push("--variant", variant);
453
+ const stdout = await runCliText("opencode", args, contentWithSystemPrompt("opencode", request), {
454
+ cwd,
455
+ timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
456
+ inheritEnv: opts.inheritEnv, signal: opts.signal,
457
+ // A dedicated per-invocation agent and scalar deny replace rather than merge
458
+ // wildcard/specific allow rules. Preserve inherited provider/model config.
459
+ env: base => {
460
+ const configured = base.OPENCODE_CONFIG_CONTENT ? JSON.parse(base.OPENCODE_CONFIG_CONTENT) : {};
461
+ if (!configured || typeof configured !== "object" || Array.isArray(configured))
462
+ throw new Error("OpenCode 内联配置无效。");
463
+ return { ...base, OPENCODE_PERMISSION: JSON.stringify("deny"), OPENCODE_CONFIG_CONTENT: JSON.stringify({
464
+ ...configured, permission: "deny", autoupdate: false, share: "disabled",
465
+ agent: { ...configured.agent, [agent]: { mode: "primary", permission: "deny", prompt: "Return only the requested text. Do not use tools." } },
466
+ }) };
467
+ },
468
+ });
469
+ const text = extractOpenCodeText(stdout);
470
+ if (!text)
471
+ throw new QuickCommitError("OpenCode 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
472
+ return text;
473
+ }
474
+ async function callGrokText(request, cwd, opts) {
475
+ const stdout = await runCliText("grok", buildGrokTextArgs(request, opts), "", {
476
+ cwd,
477
+ timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
478
+ inheritEnv: opts.inheritEnv, signal: opts.signal,
479
+ });
480
+ const text = extractGrokText(stdout);
481
+ if (!text)
482
+ throw new QuickCommitError("Grok 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
483
+ return text;
484
+ }
485
+ async function callQoderText(request, cwd, opts) {
486
+ const stdout = await runCliText("qodercli", buildQoderTextArgs(request, opts), "", {
487
+ cwd,
488
+ timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
489
+ inheritEnv: opts.inheritEnv, signal: opts.signal,
490
+ });
491
+ const text = extractQoderText(stdout);
492
+ if (!text)
493
+ throw new QuickCommitError("Qoder 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
494
+ return text;
495
+ }
496
+ async function callPiText(request, cwd, opts) {
497
+ const stdout = await runCliText("pi", buildPiTextArgs(request, opts), "", {
498
+ cwd, timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts), inheritEnv: opts.inheritEnv, signal: opts.signal,
499
+ });
500
+ const text = extractPiText(stdout);
501
+ if (!text)
502
+ throw new QuickCommitError("Pi 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
503
+ return text;
504
+ }
505
+ export function assertGeminiTextCapability() {
506
+ // Gemini ignores supplemental admin policy when a central policy exists.
507
+ // Refuse that unsupported text-only path rather than launching without deny.
508
+ const systemPolicies = process.platform === "darwin" ? "/Library/Application Support/GeminiCli/policies"
509
+ : process.platform === "win32" ? join(process.env.ProgramData ?? "C:\\ProgramData", "gemini-cli", "policies") : "/etc/gemini-cli/policies";
510
+ if (existsSync(systemPolicies) && readdirSync(systemPolicies).some(file => file.endsWith(".toml"))) {
511
+ throw new QuickCommitError("Gemini 中央策略阻止注入纯文本工具边界,请配置可用的文本候选。", "AI_FALLBACK_FAILED");
512
+ }
513
+ }
514
+ async function callGeminiText(request, cwd, opts) {
515
+ assertGeminiTextCapability();
516
+ const root = mkdtempSync(join(tmpdir(), "wand-gemini-text-"));
517
+ try {
518
+ const policy = join(root, "text-only.toml");
519
+ writeFileSync(policy, '[[rule]]\ntoolName = "*"\ndecision = "deny"\npriority = 999\n', { mode: 0o600 });
520
+ const args = [...buildGeminiTextArgs(opts), "--admin-policy", policy, "--extensions", "none", "--allowed-mcp-server-names", ""];
521
+ const stdout = await runCliText("gemini", args, contentWithSystemPrompt("gemini", request), {
522
+ cwd, timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts), inheritEnv: opts.inheritEnv, signal: opts.signal,
523
+ });
524
+ const text = extractGeminiText(stdout);
525
+ if (!text)
526
+ throw new QuickCommitError("Gemini 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
527
+ return text;
528
+ }
529
+ finally {
530
+ rmSync(root, { recursive: true, force: true });
531
+ }
532
+ }
533
+ export async function callCliAiText(request, cwd, language, opts) {
534
+ const provider = defaultProvider(opts.provider);
535
+ if (provider === "codex") {
536
+ return callCodexText(request, cwd, opts);
537
+ }
538
+ if (provider === "opencode")
539
+ return callOpenCodeText(request, cwd, opts);
540
+ if (provider === "grok")
541
+ return callGrokText(request, cwd, opts);
542
+ if (provider === "qoder")
543
+ return callQoderText(request, cwd, opts);
544
+ if (provider === "pi")
545
+ return callPiText(request, cwd, opts);
546
+ if (provider === "gemini")
547
+ return callGeminiText(request, cwd, opts);
548
+ return callClaudeText(request, cwd, language, opts);
549
+ }
550
+ /**
551
+ * 多候选降级时给整条链的时间上限。原生客户端的请求宽限约 180s,
552
+ * 候选再多也不能把整个请求拖死;单次仍受各自 CLI 超时保护。
553
+ */
554
+ const CLI_CHAIN_BUDGET_MS = 150_000;
555
+ /** 角色设定走系统提示前缀;任务自己的输出格式与约束必须排在后面。 */
556
+ export function withOpsPersona(request, persona) {
557
+ const trimmed = (persona ?? "").trim();
558
+ if (!trimmed)
559
+ return request;
560
+ const task = (request.system ?? "").trim();
561
+ return { ...request, system: task ? `${trimmed}\n\n${task}` : trimmed };
562
+ }
563
+ /** 链上的每条候选变成一次普通的 CLI 文本调用参数。 */
564
+ function candidateOptions(base, candidate) {
565
+ return {
566
+ ...base,
567
+ provider: candidate.provider,
568
+ model: candidate.model ?? null,
569
+ thinkingEffort: candidate.thinkingEffort ?? base.thinkingEffort ?? undefined,
570
+ };
571
+ }
572
+ /** 没有候选链时,把当前 provider/model 当作唯一的候选。 */
573
+ function singleCandidate(opts) {
574
+ return {
575
+ provider: defaultProvider(opts.provider),
576
+ model: opts.model ?? undefined,
577
+ thinkingEffort: opts.thinkingEffort ?? undefined,
578
+ };
579
+ }
580
+ /**
581
+ * 按顺序尝试候选:跳过没安装的 CLI,协议与输出校验均成功才返回。
582
+ * 只有一条候选时保持原行为(错误原样抛出,不摘要)。
583
+ * 仅用于一次性文本生成;执行工具的任务不能使用这条无条件重试路径。
584
+ */
585
+ async function callTextCandidates(request, cwd, language, opts, parseOutput) {
586
+ opts.signal?.throwIfAborted();
587
+ const { resolveModelGroupModels } = await import("./model-groups.js");
588
+ const modelGroups = opts.modelGroups ?? opts.employeeText?.config.modelGroups;
589
+ const seen = new Set();
590
+ const chain = (opts.cliCandidates?.length ? opts.cliCandidates : [singleCandidate(opts)])
591
+ .flatMap((candidate) => resolveModelGroupModels(modelGroups, candidate.provider, candidate.model, { preferDefault: !candidate.model || candidate.model === "default" })
592
+ .map((model) => ({ ...candidate, model: model || undefined })))
593
+ .filter(candidate => {
594
+ const key = JSON.stringify([candidate.provider, candidate.engine ?? "cli", candidate.model ?? "", candidate.thinkingEffort ?? opts.thinkingEffort]);
595
+ if (seen.has(key))
596
+ return false;
597
+ seen.add(key);
598
+ return true;
599
+ });
600
+ if (opts.employeeChannelOnly && !opts.cliCandidates?.length) {
601
+ throw new QuickCommitError("系统员工没有可用的执行候选。", "AI_FALLBACK_FAILED");
602
+ }
603
+ const deadline = Date.now() + Math.max(1, opts.budgetMs ?? CLI_CHAIN_BUDGET_MS);
604
+ const call = async (candidate) => {
605
+ if (candidate.engine !== "sdk")
606
+ return callCliAiText(request, cwd, language, { ...candidateOptions(opts, candidate), deadline });
607
+ const controller = new AbortController();
608
+ const timer = setTimeout(() => controller.abort(new QuickCommitError("Wand Agent 调用超时。", "CLAUDE_TIMEOUT")), Math.max(1, deadline - Date.now()));
609
+ const signal = opts.signal ? AbortSignal.any([opts.signal, controller.signal]) : controller.signal;
610
+ try {
611
+ const generate = await abortableEmployeeText(prepareEmployeeTextCandidate({
612
+ ...opts.employeeText,
613
+ config: { ...opts.employeeText?.config, defaultCwd: cwd, inheritEnv: opts.inheritEnv ?? opts.employeeText?.config.inheritEnv, modelGroups },
614
+ }, { ...candidate, model: candidate.model ?? "default", thinkingEffort: candidate.thinkingEffort ?? opts.thinkingEffort ?? "off", mode: "default", kind: "structured" }, request, signal), signal);
615
+ signal.throwIfAborted();
616
+ return await abortableEmployeeText(generate(), signal);
617
+ }
618
+ finally {
619
+ clearTimeout(timer);
620
+ }
621
+ };
622
+ if (chain.length === 1)
623
+ return parseOutput(await call(chain[0]));
624
+ const installed = chain.filter((candidate) => candidate.engine === "sdk" || providerCliInstalled(candidate.provider));
625
+ const attempts = installed.length ? installed : chain;
626
+ const errors = [];
627
+ for (const candidate of attempts) {
628
+ opts.signal?.throwIfAborted();
629
+ if (Date.now() >= deadline) {
630
+ errors.push("已超过等待上限");
631
+ break;
632
+ }
633
+ try {
634
+ const text = await call(candidate);
635
+ const result = await parseOutput(text);
636
+ if (candidate !== attempts[0]) {
637
+ console.info(`[SystemAi] 候选 ${chain.indexOf(candidate) + 1}/${chain.length} (${candidate.provider}) 成功`);
638
+ }
639
+ return result;
640
+ }
641
+ catch (error) {
642
+ opts.signal?.throwIfAborted();
643
+ errors.push(`${candidate.provider}: ${getGitErrorMessage(error)}`);
644
+ // 不记录提示词、路径、CLI 原始响应或可能带鉴权信息的错误正文。
645
+ const code = error instanceof QuickCommitError ? error.code : "INVALID_AI_RESULT";
646
+ console.warn(`[SystemAi] 候选 ${chain.indexOf(candidate) + 1}/${chain.length} (${candidate.provider}) 失败: ${code}`);
647
+ }
648
+ }
649
+ throw new QuickCommitError(`所有执行候选均失败:${errors.join(";")}`, "AI_FALLBACK_FAILED");
650
+ }
651
+ export async function callConfiguredAiText(request, cwd, language, opts, parseOutput = (raw) => raw) {
652
+ return callTextCandidates(withOpsPersona(request, opts.opsPersona), cwd, language, opts, parseOutput);
653
+ }
@@ -0,0 +1,2 @@
1
+ /** One write contract for employee and unbound team-member avatars. */
2
+ export declare function parseStoredAvatar(value: unknown, label: string, maxChars: number): string;
@@ -0,0 +1,19 @@
1
+ import { parsePlushAvatar, parsePlushCatAvatar, parsePlushDogAvatar } from "./plush-avatar.js";
2
+ import { text } from "./server-request.js";
3
+ /** One write contract for employee and unbound team-member avatars. */
4
+ export function parseStoredAvatar(value, label, maxChars) {
5
+ if (value !== undefined && value !== null && typeof value !== "string") {
6
+ throw new Error(`${label}的头像格式无效。`);
7
+ }
8
+ const avatar = text(value);
9
+ if (!avatar || /^cat:\d{1,2}$/.test(avatar))
10
+ return avatar;
11
+ if (parsePlushAvatar(avatar) || parsePlushCatAvatar(avatar) || parsePlushDogAvatar(avatar))
12
+ return avatar;
13
+ if (!/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(avatar)) {
14
+ throw new Error(`${label}的头像格式无效。`);
15
+ }
16
+ if (avatar.length > maxChars)
17
+ throw new Error(`${label}的头像太大。`);
18
+ return avatar;
19
+ }