@co0ontty/wand 4.83.1 → 4.84.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 (188) hide show
  1. package/README.md +4 -0
  2. package/dist/agent-dispatch.js +5 -1
  3. package/dist/ai-team-availability.d.ts +1 -1
  4. package/dist/ai-team-prompts.js +13 -4
  5. package/dist/ai-team-runner.d.ts +80 -5
  6. package/dist/ai-team-runner.js +332 -55
  7. package/dist/ai-team-types.d.ts +92 -5
  8. package/dist/ai-team-types.js +9 -2
  9. package/dist/build-info.json +3 -3
  10. package/dist/cli-api.d.ts +1 -0
  11. package/dist/cli-api.js +3 -0
  12. package/dist/cli.js +18 -1
  13. package/dist/commit-task-archive.d.ts +3 -1
  14. package/dist/commit-task-archive.js +21 -10
  15. package/dist/config.d.ts +1 -1
  16. package/dist/config.js +73 -0
  17. package/dist/conversation-mentions.d.ts +16 -0
  18. package/dist/conversation-mentions.js +32 -0
  19. package/dist/conversation-service.d.ts +90 -0
  20. package/dist/conversation-service.js +759 -0
  21. package/dist/conversation-session-preview.d.ts +9 -0
  22. package/dist/conversation-session-preview.js +47 -0
  23. package/dist/conversation-task-preview.d.ts +5 -0
  24. package/dist/conversation-task-preview.js +8 -0
  25. package/dist/conversation-types.d.ts +80 -0
  26. package/dist/conversation-types.js +7 -0
  27. package/dist/core-extension-host.d.ts +27 -0
  28. package/dist/core-extension-host.js +138 -0
  29. package/dist/core-runner.d.ts +86 -0
  30. package/dist/core-runner.js +757 -0
  31. package/dist/core-status-cli.d.ts +9 -0
  32. package/dist/core-status-cli.js +280 -0
  33. package/dist/core-turn-tracker.d.ts +12 -0
  34. package/dist/core-turn-tracker.js +36 -0
  35. package/dist/decision-runner.d.ts +6 -0
  36. package/dist/decision-runner.js +3 -1
  37. package/dist/decision-tool.d.ts +2 -0
  38. package/dist/decision-tool.js +49 -22
  39. package/dist/env-utils.d.ts +7 -3
  40. package/dist/env-utils.js +30 -5
  41. package/dist/git-quick-commit.d.ts +5 -0
  42. package/dist/git-quick-commit.js +9 -3
  43. package/dist/git-utils.js +2 -1
  44. package/dist/harness-engine.d.ts +110 -0
  45. package/dist/harness-engine.js +331 -0
  46. package/dist/mission-diff.js +2 -1
  47. package/dist/model-auto-assign.d.ts +50 -0
  48. package/dist/model-auto-assign.js +163 -0
  49. package/dist/model-group-runner.d.ts +4 -0
  50. package/dist/model-group-runner.js +72 -0
  51. package/dist/model-groups.d.ts +38 -0
  52. package/dist/model-groups.js +138 -0
  53. package/dist/models.d.ts +12 -0
  54. package/dist/models.js +102 -8
  55. package/dist/npm-update-utils.js +2 -1
  56. package/dist/openrouter-free-cli.d.ts +25 -0
  57. package/dist/openrouter-free-cli.js +109 -0
  58. package/dist/openrouter-free-models.d.ts +85 -0
  59. package/dist/openrouter-free-models.js +458 -0
  60. package/dist/openrouter-free-selection.d.ts +5 -0
  61. package/dist/openrouter-free-selection.js +7 -0
  62. package/dist/path-repair.d.ts +10 -0
  63. package/dist/path-repair.js +50 -5
  64. package/dist/pi-auto-resources.d.ts +12 -0
  65. package/dist/pi-auto-resources.js +151 -0
  66. package/dist/pi-execution-types.d.ts +48 -0
  67. package/dist/pi-execution-types.js +1 -0
  68. package/dist/pi-execution.d.ts +9 -0
  69. package/dist/pi-execution.js +187 -0
  70. package/dist/pi-extension-resources.d.ts +13 -0
  71. package/dist/pi-extension-resources.js +71 -0
  72. package/dist/pi-openrouter-free-bridge.d.ts +2 -0
  73. package/dist/pi-openrouter-free-bridge.js +80 -0
  74. package/dist/pi-resource-bridge.d.ts +2 -0
  75. package/dist/pi-resource-bridge.js +73 -0
  76. package/dist/pi-resource-catalog.d.ts +18 -0
  77. package/dist/pi-resource-catalog.js +82 -0
  78. package/dist/pi-resource-recommendation.d.ts +29 -0
  79. package/dist/pi-resource-recommendation.js +169 -0
  80. package/dist/pi-resource-run.d.ts +8 -0
  81. package/dist/pi-resource-run.js +28 -0
  82. package/dist/pi-session-settings.d.ts +157 -0
  83. package/dist/pi-session-settings.js +166 -0
  84. package/dist/platform-action-catalog.d.ts +24 -0
  85. package/dist/platform-action-catalog.js +141 -0
  86. package/dist/platform-action-service.d.ts +37 -0
  87. package/dist/platform-action-service.js +138 -0
  88. package/dist/platform-action-types.d.ts +125 -0
  89. package/dist/platform-action-types.js +7 -0
  90. package/dist/process-manager.d.ts +21 -1
  91. package/dist/process-manager.js +168 -15
  92. package/dist/provider-catalog.js +1 -1
  93. package/dist/provider-cli-updater.js +29 -8
  94. package/dist/provider-history-scanner.d.ts +9 -0
  95. package/dist/provider-history-scanner.js +107 -1
  96. package/dist/retention.d.ts +19 -7
  97. package/dist/retention.js +171 -28
  98. package/dist/server-ai-team-routes.js +6 -2
  99. package/dist/server-conversation-routes.d.ts +5 -0
  100. package/dist/server-conversation-routes.js +91 -0
  101. package/dist/server-employee-routes.d.ts +2 -0
  102. package/dist/server-employee-routes.js +85 -11
  103. package/dist/server-pi-recommendation-routes.d.ts +13 -0
  104. package/dist/server-pi-recommendation-routes.js +67 -0
  105. package/dist/server-resume-routes.js +1 -1
  106. package/dist/server-session-routes.d.ts +4 -3
  107. package/dist/server-session-routes.js +182 -37
  108. package/dist/server-settings-routes.d.ts +5 -0
  109. package/dist/server-settings-routes.js +46 -0
  110. package/dist/server-task-routes.js +49 -4
  111. package/dist/server-team-dispatch-routes.d.ts +19 -0
  112. package/dist/server-team-dispatch-routes.js +71 -0
  113. package/dist/server-update-routes.d.ts +1 -1
  114. package/dist/server-update-routes.js +10 -5
  115. package/dist/server-workspace-routes.js +44 -24
  116. package/dist/server.d.ts +1 -0
  117. package/dist/server.js +148 -30
  118. package/dist/session-ai-context.d.ts +5 -1
  119. package/dist/session-ai-context.js +9 -4
  120. package/dist/session-registry.d.ts +4 -0
  121. package/dist/session-registry.js +47 -13
  122. package/dist/session-transport.js +8 -1
  123. package/dist/settings-web-access.d.ts +15 -0
  124. package/dist/settings-web-access.js +37 -0
  125. package/dist/silicon-employee-defaults.d.ts +29 -0
  126. package/dist/silicon-employee-defaults.js +41 -0
  127. package/dist/silicon-employee-dispatch.d.ts +5 -1
  128. package/dist/silicon-employee-dispatch.js +10 -4
  129. package/dist/silicon-employee-draft.d.ts +21 -4
  130. package/dist/silicon-employee-draft.js +103 -16
  131. package/dist/storage.d.ts +73 -2
  132. package/dist/storage.js +362 -26
  133. package/dist/structured-claude-protocol.js +1 -1
  134. package/dist/structured-client-protocol.js +11 -33
  135. package/dist/structured-content.d.ts +23 -0
  136. package/dist/structured-content.js +72 -0
  137. package/dist/structured-failure.d.ts +16 -0
  138. package/dist/structured-failure.js +36 -0
  139. package/dist/structured-pi-adapter.d.ts +8 -3
  140. package/dist/structured-pi-adapter.js +131 -30
  141. package/dist/structured-provider-common.d.ts +6 -0
  142. package/dist/structured-provider-common.js +23 -1
  143. package/dist/structured-runner.d.ts +45 -0
  144. package/dist/structured-session-manager.d.ts +78 -4
  145. package/dist/structured-session-manager.js +513 -129
  146. package/dist/structured-thinking.d.ts +15 -0
  147. package/dist/structured-thinking.js +46 -0
  148. package/dist/subagent-dispatch.d.ts +56 -0
  149. package/dist/subagent-dispatch.js +102 -0
  150. package/dist/system-employee.d.ts +4 -3
  151. package/dist/system-employee.js +7 -4
  152. package/dist/task-retention.d.ts +23 -0
  153. package/dist/task-retention.js +69 -0
  154. package/dist/task-types.d.ts +4 -0
  155. package/dist/task-types.js +3 -0
  156. package/dist/team-dispatch.d.ts +101 -0
  157. package/dist/team-dispatch.js +269 -0
  158. package/dist/tool-preview.js +7 -1
  159. package/dist/tui/commands.js +50 -1
  160. package/dist/tui/ipc-protocol.d.ts +1 -1
  161. package/dist/tui/ipc-server.d.ts +4 -0
  162. package/dist/tui/ipc-server.js +23 -0
  163. package/dist/turn-heartbeat.d.ts +35 -0
  164. package/dist/turn-heartbeat.js +64 -0
  165. package/dist/types.d.ts +147 -1
  166. package/dist/user-profile.d.ts +33 -0
  167. package/dist/user-profile.js +70 -0
  168. package/dist/web-ui/chat-message-meta.d.ts +11 -0
  169. package/dist/web-ui/chat-message-meta.js +47 -0
  170. package/dist/web-ui/content/ai-teams.js +13 -13
  171. package/dist/web-ui/content/scripts.js +795 -95
  172. package/dist/web-ui/content/styles.css +1 -1
  173. package/dist/web-ui/content/tailwind.css +1 -6808
  174. package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
  175. package/dist/web-ui/embedded-assets.d.ts +1 -1
  176. package/dist/web-ui/embedded-assets.js +5 -5
  177. package/dist/web-ui/index.d.ts +1 -1
  178. package/dist/web-ui/index.js +3 -3
  179. package/dist/web-ui/markdown.d.ts +2 -0
  180. package/dist/web-ui/markdown.js +111 -0
  181. package/dist/web-ui/page.d.ts +4 -0
  182. package/dist/web-ui/page.js +8 -0
  183. package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
  184. package/dist/web-ui/react/local-preview/controller.js +159 -0
  185. package/dist/web-ui/running-activity.d.ts +63 -0
  186. package/dist/web-ui/running-activity.js +134 -0
  187. package/dist/web-ui/styles.js +2 -11
  188. package/package.json +7 -2
@@ -0,0 +1,72 @@
1
+ import { getDefaultModelForProvider } from "./config.js";
2
+ import { findModelGroup, resolveModelGroupModels } from "./model-groups.js";
3
+ import { OPENROUTER_FREE_SELECTOR } from "./openrouter-free-selection.js";
4
+ import { classifyStructuredFailure, hasStructuredExecutionProgress } from "./structured-failure.js";
5
+ /** Resolve at execution time; persist the selection, normalize attempt facts even without a group. */
6
+ export function withModelGroups(runner, config) {
7
+ return { start(context, observer) {
8
+ const provider = context.session.provider ?? context.session.structuredState?.provider ?? "claude";
9
+ const selected = context.session.selectedModel?.trim();
10
+ const selector = selected && selected !== "default" ? selected : getDefaultModelForProvider(config, provider);
11
+ const groupOptions = { preferDefault: !selected || selected === "default" };
12
+ const models = resolveModelGroupModels(config.modelGroups, provider, selector, groupOptions);
13
+ const group = findModelGroup(config.modelGroups, provider, selector, groupOptions);
14
+ const grouped = !!group && models[0] !== OPENROUTER_FREE_SELECTOR;
15
+ let cancelled = false;
16
+ let current;
17
+ const start = (model) => {
18
+ let active = true;
19
+ let output = false;
20
+ let progress = false;
21
+ const isActive = () => active && !cancelled && observer.isActive();
22
+ const execution = runner.start({ ...context, ...(grouped ? { modelGroupModels: models } : {}),
23
+ session: { ...context.session, selectedModel: model || null },
24
+ }, { ...observer, isActive,
25
+ onStdout(text) { if (!isActive())
26
+ return; if (text.trim())
27
+ output = true; observer.onStdout?.(text); },
28
+ onEvent(event) {
29
+ if (!isActive())
30
+ return;
31
+ if (event.type !== "wand_pi_resource_selection")
32
+ output = true;
33
+ observer.onEvent?.(event);
34
+ },
35
+ onUpdate(state) {
36
+ if (!isActive())
37
+ return;
38
+ progress ||= hasStructuredExecutionProgress(state);
39
+ output ||= !!state.sessionId || progress;
40
+ observer.onUpdate({ ...state, model: state.model ?? model });
41
+ },
42
+ });
43
+ return { ...execution, get args() { return execution.args; }, get pid() { return execution.pid; },
44
+ completion: execution.completion.then((result) => {
45
+ const failed = result.spawnError ? { ...result, exitCode: result.exitCode ?? 1,
46
+ primaryError: result.primaryError || result.spawnError.message } : result;
47
+ const failure = classifyStructuredFailure(failed, { output, progress });
48
+ return { ...failed, failure: failure && cancelled ? { ...failure, kind: "cancelled", retryable: false } : failure,
49
+ state: { ...failed.state, model: failed.state.model ?? model } };
50
+ }).finally(() => { active = false; }) };
51
+ };
52
+ // Synchronous registration is essential for queue/recovery ownership.
53
+ current = start(models[0] ?? "");
54
+ const first = current;
55
+ const completion = (async () => {
56
+ for (let index = 0;; index++) {
57
+ const result = await current.completion;
58
+ if (!grouped || !result.failure?.retryable || cancelled || !observer.isActive()
59
+ || index + 1 >= models.length)
60
+ return result;
61
+ current = start(models[index + 1]);
62
+ }
63
+ })();
64
+ return { ...first, get args() { return first.args; }, get pid() { return first.pid; }, completion, interrupt() {
65
+ cancelled = true;
66
+ try {
67
+ current.interrupt();
68
+ }
69
+ catch { /* best effort, never replay after cancellation */ }
70
+ } };
71
+ } };
72
+ }
@@ -0,0 +1,38 @@
1
+ import { type SessionProvider } from "./provider-catalog.js";
2
+ /** Stable identities, not names, are saved in sessions/employees/default model preferences. */
3
+ export interface ModelGroup {
4
+ id: string;
5
+ provider: SessionProvider;
6
+ name: string;
7
+ /** First is preferred; later entries are pre-execution fallbacks, never replay after acceptance. */
8
+ models: string[];
9
+ }
10
+ export declare const MODEL_GROUP_PREFIX = "wand-model-group/";
11
+ /**
12
+ * 「智能分配」:不是一个分组,而是一条只能在真实提示词到手后结算的选择。
13
+ * 结构化会话的第一轮、终端会话带首条输入创建时才解析成本次真正使用的分组;
14
+ * 没有提示词的同步场景(构建命令行、一次性文本调用)退化成默认分组。
15
+ */
16
+ export declare const AUTO_ASSIGN_SELECTOR = "wand-auto-assign/auto";
17
+ export declare const AUTO_ASSIGN_LABEL = "\u667A\u80FD\u5206\u914D";
18
+ export declare const FREE_MODEL_GROUP_ID = "openrouter-free";
19
+ export declare const MODEL_GROUP_MAX_PER_PROVIDER = 32;
20
+ export declare const MODEL_GROUP_MAX_MEMBERS = 32;
21
+ export declare const DEFAULT_MODEL_GROUP_NAMES: readonly ["默认分组", "默认", "default"];
22
+ export declare function isModelGroupSelector(value: string | null | undefined): boolean;
23
+ export declare function isAutoAssignSelector(value: string | null | undefined): boolean;
24
+ export declare function modelGroupSelector(group: Pick<ModelGroup, "id" | "provider">): string;
25
+ export declare function normalizeModelGroups(value: unknown): ModelGroup[];
26
+ /** Names are exact and provider-scoped; stable selectors keep working after a rename. */
27
+ export declare function findModelGroup(groups: readonly ModelGroup[] | undefined, provider: SessionProvider, selector?: string | null, options?: {
28
+ preferDefault?: boolean;
29
+ }): ModelGroup | undefined;
30
+ /** A configured group wins; do not silently replace a deleted configured group. */
31
+ export declare function defaultModelGroupSelector(groups: readonly ModelGroup[] | undefined, provider: SessionProvider, configured?: string | null): string | undefined;
32
+ /** Native default is one executable candidate, not an empty candidate chain. */
33
+ export declare function resolveModelGroupModels(groups: readonly ModelGroup[] | undefined, provider: SessionProvider, selector: string | null | undefined, options?: {
34
+ preferDefault?: boolean;
35
+ }): string[];
36
+ export declare function freeModelOrder(groups: readonly ModelGroup[] | undefined): string[];
37
+ /** Used for free pools: saved priority first; newly verified models append, vanished models are skipped. */
38
+ export declare function orderModelIds(ids: readonly string[], preferred: readonly string[]): string[];
@@ -0,0 +1,138 @@
1
+ import { isSessionProvider, SESSION_PROVIDERS } from "./provider-catalog.js";
2
+ import { OPENROUTER_FREE_GROUP, OPENROUTER_FREE_PROVIDER, OPENROUTER_FREE_SELECTOR, isOpenRouterFreeSelector } from "./openrouter-free-selection.js";
3
+ export const MODEL_GROUP_PREFIX = "wand-model-group/";
4
+ /**
5
+ * 「智能分配」:不是一个分组,而是一条只能在真实提示词到手后结算的选择。
6
+ * 结构化会话的第一轮、终端会话带首条输入创建时才解析成本次真正使用的分组;
7
+ * 没有提示词的同步场景(构建命令行、一次性文本调用)退化成默认分组。
8
+ */
9
+ export const AUTO_ASSIGN_SELECTOR = "wand-auto-assign/auto";
10
+ export const AUTO_ASSIGN_LABEL = "智能分配";
11
+ export const FREE_MODEL_GROUP_ID = "openrouter-free";
12
+ export const MODEL_GROUP_MAX_PER_PROVIDER = 32;
13
+ export const MODEL_GROUP_MAX_MEMBERS = 32;
14
+ export const DEFAULT_MODEL_GROUP_NAMES = ["默认分组", "默认", "default"];
15
+ const GROUP_ID = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
16
+ export function isModelGroupSelector(value) {
17
+ return value?.startsWith(MODEL_GROUP_PREFIX) === true;
18
+ }
19
+ export function isAutoAssignSelector(value) {
20
+ return value?.trim() === AUTO_ASSIGN_SELECTOR;
21
+ }
22
+ /** 「智能分配」在只拿到选择器、拿不到提示词时的等价写法:按默认分组解析。 */
23
+ function modelSelectorWithoutAutoAssign(selector) {
24
+ const value = selector?.trim() ?? "";
25
+ return isAutoAssignSelector(value) ? "" : value;
26
+ }
27
+ export function modelGroupSelector(group) {
28
+ return group.id === FREE_MODEL_GROUP_ID && group.provider === "pi"
29
+ ? OPENROUTER_FREE_SELECTOR : `${MODEL_GROUP_PREFIX}${group.provider}/${group.id}`;
30
+ }
31
+ export function normalizeModelGroups(value) {
32
+ if (!Array.isArray(value) || value.length > MODEL_GROUP_MAX_PER_PROVIDER * SESSION_PROVIDERS.length + 1) {
33
+ throw new Error("模型分组必须是有界数组。");
34
+ }
35
+ const identities = new Set();
36
+ const names = new Set();
37
+ const counts = new Map();
38
+ return value.map((entry) => {
39
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
40
+ throw new Error("模型分组格式无效。");
41
+ const row = entry;
42
+ const id = typeof row.id === "string" ? row.id.trim() : "";
43
+ const name = typeof row.name === "string" ? row.name.trim() : "";
44
+ if (!GROUP_ID.test(id) || !isSessionProvider(row.provider))
45
+ throw new Error("模型分组标识或工具无效。");
46
+ const provider = row.provider;
47
+ if (!name || name.length > 40 || /[\x00-\x1f\x7f]/.test(name))
48
+ throw new Error("分组名称需为 1–40 个字符。");
49
+ const free = id === FREE_MODEL_GROUP_ID;
50
+ if (free && (provider !== "pi" || name !== OPENROUTER_FREE_GROUP))
51
+ throw new Error("免费分组的工具与名称不可修改。");
52
+ if (!free && provider === "pi" && name === OPENROUTER_FREE_GROUP)
53
+ throw new Error("免费分组是内置分组,请直接调整它的顺序。");
54
+ const identity = `${provider}/${id}`;
55
+ const named = `${provider}/${name}`;
56
+ if (identities.has(identity) || names.has(named))
57
+ throw new Error("同一工具的分组标识或名称不能重复。");
58
+ identities.add(identity);
59
+ names.add(named);
60
+ const count = (counts.get(provider) ?? 0) + (free ? 0 : 1);
61
+ if (count > MODEL_GROUP_MAX_PER_PROVIDER)
62
+ throw new Error(`每个工具最多 ${MODEL_GROUP_MAX_PER_PROVIDER} 个分组。`);
63
+ counts.set(provider, count);
64
+ if (!Array.isArray(row.models) || (!free && !row.models.length)
65
+ || row.models.length > (free ? 128 : MODEL_GROUP_MAX_MEMBERS)) {
66
+ throw new Error(`分组需包含 1–${MODEL_GROUP_MAX_MEMBERS} 个模型;免费分组可留空使用自动顺序。`);
67
+ }
68
+ const models = row.models.map((model) => {
69
+ const trimmed = typeof model === "string" ? model.trim() : "";
70
+ if (!trimmed || trimmed === "default" || trimmed.length > 256 || /[\s\x00-\x1f\x7f]/.test(trimmed)
71
+ || isModelGroupSelector(trimmed) || isAutoAssignSelector(trimmed)
72
+ || trimmed === OPENROUTER_FREE_SELECTOR || trimmed.startsWith("-")) {
73
+ throw new Error("组内必须是具体模型 ID,不能嵌套分组或使用默认值。");
74
+ }
75
+ if (isOpenRouterFreeSelector(trimmed) && provider !== "pi")
76
+ throw new Error("免费模型仅属于 one 的 Agent。");
77
+ if (free && !trimmed.startsWith(`${OPENROUTER_FREE_PROVIDER}/`))
78
+ throw new Error("免费分组只能包含免费池中的模型。");
79
+ return trimmed;
80
+ });
81
+ if (new Set(models).size !== models.length)
82
+ throw new Error("同一分组中的模型不能重复。");
83
+ return { id, provider, name, models };
84
+ });
85
+ }
86
+ /** Names are exact and provider-scoped; stable selectors keep working after a rename. */
87
+ export function findModelGroup(groups, provider, selector, options = {}) {
88
+ const candidates = (groups ?? []).filter((entry) => entry.provider === provider);
89
+ const value = modelSelectorWithoutAutoAssign(selector);
90
+ if (isModelGroupSelector(value) || value === OPENROUTER_FREE_SELECTOR) {
91
+ return candidates.find((entry) => modelGroupSelector(entry) === value);
92
+ }
93
+ if (value && value !== "default") {
94
+ const named = candidates.find((entry) => entry.name === value);
95
+ if (named || !options.preferDefault)
96
+ return named;
97
+ }
98
+ const named = DEFAULT_MODEL_GROUP_NAMES.map((name) => candidates.find((entry) => entry.name === name))
99
+ .find((entry) => entry !== undefined);
100
+ return named ?? candidates.find((entry) => entry.id !== FREE_MODEL_GROUP_ID) ?? candidates[0];
101
+ }
102
+ /** A configured group wins; do not silently replace a deleted configured group. */
103
+ export function defaultModelGroupSelector(groups, provider, configured) {
104
+ const value = configured?.trim();
105
+ // 智能分配是明确选择,必须原样带到会话上,由第一轮提示词结算。
106
+ if (isModelGroupSelector(value) || value === OPENROUTER_FREE_SELECTOR || isAutoAssignSelector(value))
107
+ return value;
108
+ const group = findModelGroup(groups, provider, value, { preferDefault: true });
109
+ return group ? modelGroupSelector(group) : undefined;
110
+ }
111
+ /** Native default is one executable candidate, not an empty candidate chain. */
112
+ export function resolveModelGroupModels(groups, provider, selector, options = {}) {
113
+ const value = modelSelectorWithoutAutoAssign(selector);
114
+ if (value === OPENROUTER_FREE_SELECTOR) {
115
+ if (provider !== "pi")
116
+ throw new Error("免费分组仅属于 one 的 Agent。");
117
+ return [value];
118
+ }
119
+ const group = findModelGroup(groups, provider, value, options);
120
+ if (group?.id === FREE_MODEL_GROUP_ID)
121
+ return [OPENROUTER_FREE_SELECTOR];
122
+ if (group) {
123
+ if (!group.models.length)
124
+ throw new Error("所选模型分组已删除、为空或不属于当前工具,请重新选择。");
125
+ return [...group.models];
126
+ }
127
+ if (isModelGroupSelector(value))
128
+ throw new Error("所选模型分组已删除、为空或不属于当前工具,请重新选择。");
129
+ return [value === "default" ? "" : value];
130
+ }
131
+ export function freeModelOrder(groups) {
132
+ return groups?.find((entry) => entry.id === FREE_MODEL_GROUP_ID && entry.provider === "pi")?.models ?? [];
133
+ }
134
+ /** Used for free pools: saved priority first; newly verified models append, vanished models are skipped. */
135
+ export function orderModelIds(ids, preferred) {
136
+ const available = new Set(ids);
137
+ return [...new Set([...preferred.filter((id) => available.has(id)), ...ids])];
138
+ }
package/dist/models.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type PiModelEndpointDiscoveryOptions } from "./pi-model-discovery.js";
2
2
  import { ClaudeModelInfo } from "./types.js";
3
+ import { type ModelGroup } from "./model-groups.js";
3
4
  /**
4
5
  * The complete server-side model catalog. Keep this separate from the Claude
5
6
  * verification cache: the former is a client-facing snapshot for every
@@ -41,6 +42,9 @@ export interface ModelRefreshOptions {
41
42
  piEndpointDiscovery?: PiModelEndpointDiscoveryOptions & {
42
43
  enabled: boolean;
43
44
  };
45
+ managedPiModels?: () => readonly ClaudeModelInfo[];
46
+ managedPiModelMembers?: () => ClaudeModelInfo[];
47
+ modelGroups?: () => readonly ModelGroup[];
44
48
  now?: () => Date;
45
49
  }
46
50
  export interface ThinkingEffortLevel {
@@ -79,6 +83,9 @@ export declare const FALLBACK_PROVIDER_THINKING: ProviderThinkingEfforts;
79
83
  */
80
84
  export declare function withConfiguredDefaultModelLabels<T extends object>(catalog: T, defaults: Readonly<Record<string, string>>): T;
81
85
  export interface ModelCatalogSnapshot extends ModelCache {
86
+ modelGroups?: ModelGroup[];
87
+ /** Verified concrete free members, used only when editing the pool order. */
88
+ freeModels?: ClaudeModelInfo[];
82
89
  /** SHA-256 of the catalog excluding `refreshedAt`. Changes only with content. */
83
90
  revision: string;
84
91
  }
@@ -150,12 +157,17 @@ export declare class ModelCatalogService {
150
157
  private readonly getOptions;
151
158
  private cache;
152
159
  private revision;
160
+ private publishedRevision;
153
161
  private hasPersistedSnapshot;
154
162
  private refreshPromise;
155
163
  private inFlightIncludesVerification;
156
164
  private readonly changeListeners;
157
165
  constructor(getOptions: () => ModelRefreshOptions);
166
+ private withManagedPiModels;
167
+ publishManagedPiModels(): void;
158
168
  snapshot(): ModelCatalogSnapshot;
169
+ /** Preference changes publish immediately without re-running CLI discovery or altering stored history. */
170
+ publishModelGroups(): void;
159
171
  /** 目录内容真正变化时通知。客户端靠它刷新下拉,不用自己再跑一遍 CLI。 */
160
172
  onChanged(listener: (result: ModelCatalogRefreshResult) => void): () => void;
161
173
  refresh(request?: ModelCatalogRefreshRequest): Promise<ModelCatalogRefreshResult>;
package/dist/models.js CHANGED
@@ -8,6 +8,8 @@ import Anthropic from "@anthropic-ai/sdk";
8
8
  import { resolveChildEnv } from "./env-utils.js";
9
9
  import { discoverPiEndpointModels } from "./pi-model-discovery.js";
10
10
  import { extractSemver } from "./version-utils.js";
11
+ import { AUTO_ASSIGN_LABEL, AUTO_ASSIGN_SELECTOR, FREE_MODEL_GROUP_ID, isAutoAssignSelector, isModelGroupSelector, modelGroupSelector, orderModelIds } from "./model-groups.js";
12
+ import { OPENROUTER_FREE_GROUP } from "./openrouter-free-selection.js";
11
13
  const execFileAsync = promisify(execFile);
12
14
  const CLAUDE_VERIFICATION_CACHE_KEY = "claude-model-verifications-v1";
13
15
  /**
@@ -59,7 +61,7 @@ const QODER_FALLBACK_MODELS = [
59
61
  { id: "ultimate", label: "Ultimate" },
60
62
  ];
61
63
  const PI_FALLBACK_MODELS = [
62
- { id: "default", label: "跟随 Pi 默认", alias: true },
64
+ { id: "default", label: "跟随 one 的 Agent 默认", alias: true },
63
65
  ];
64
66
  /**
65
67
  * Gemini CLI 没有列模型的子命令,只能给出稳定别名(`packages/core/src/config/models.ts`
@@ -124,10 +126,14 @@ export function withConfiguredDefaultModelLabels(catalog, defaults) {
124
126
  if (at < 0)
125
127
  continue;
126
128
  const entry = list[at];
127
- if (String(entry.label ?? "").includes(configured))
129
+ const selected = list.find((item) => item?.id === configured);
130
+ const name = configured.startsWith("wand-model-group/") || configured === "wand-openrouter-free/auto"
131
+ ? selected?.label || "模型分组"
132
+ : isAutoAssignSelector(configured) ? selected?.label || AUTO_ASSIGN_LABEL : configured;
133
+ if (String(entry.label ?? "").includes(name))
128
134
  continue;
129
135
  const updated = [...list];
130
- updated[at] = { ...entry, label: `跟随服务端默认(${configured})` };
136
+ updated[at] = { ...entry, label: `跟随服务端默认(${name})` };
131
137
  next ??= { ...catalog };
132
138
  next[field] = updated;
133
139
  }
@@ -168,7 +174,7 @@ function normalizeClaudeModelId(value) {
168
174
  if (typeof value !== "string")
169
175
  return null;
170
176
  const id = value.trim();
171
- return MODEL_ID_PATTERN.test(id) ? id : null;
177
+ return MODEL_ID_PATTERN.test(id) && !isModelGroupSelector(id) ? id : null;
172
178
  }
173
179
  function formatClaudeModelLabel(id, displayName) {
174
180
  const name = displayName?.trim();
@@ -389,6 +395,9 @@ async function probeQoderModels(runner, env) {
389
395
  }
390
396
  }
391
397
  async function probePiModels(runner, env, options) {
398
+ const agentDir = options.piEndpointDiscovery?.agentDir;
399
+ if (agentDir)
400
+ env = { ...env, PI_CODING_AGENT_DIR: agentDir };
392
401
  const rpcText = await commandText(runner, "pi", ["--mode", "rpc", "--no-session"], env, 12_000, `${JSON.stringify({ id: "wand-models", type: "get_available_models" })}\n`);
393
402
  const fromRpc = parsePiRpcModels(rpcText);
394
403
  let configuredModels = fromRpc.length > 1 || (fromRpc.length === 1 && fromRpc[0]?.id !== "default")
@@ -790,7 +799,7 @@ export function parsePiRpcModels(stdout) {
790
799
  return [
791
800
  {
792
801
  id: "default",
793
- label: "跟随 Pi 默认",
802
+ label: "跟随 one 的 Agent 默认",
794
803
  alias: true,
795
804
  ...(union.length ? { reasoningEfforts: union } : {}),
796
805
  },
@@ -823,7 +832,7 @@ export function parsePiModels(stdout) {
823
832
  if (!discovered.length)
824
833
  return cloneModels(PI_FALLBACK_MODELS);
825
834
  return [
826
- { id: "default", label: "跟随 Pi 默认", alias: true },
835
+ { id: "default", label: "跟随 one 的 Agent 默认", alias: true },
827
836
  ...discovered,
828
837
  ];
829
838
  }
@@ -953,6 +962,7 @@ function parsePersistedModelInfo(value) {
953
962
  id,
954
963
  label,
955
964
  ...(typeof value.alias === "boolean" ? { alias: value.alias } : {}),
965
+ ...(typeof value.group === "string" ? { group: value.group.slice(0, 80) } : {}),
956
966
  ...(source ? { source } : {}),
957
967
  ...(availability ? { availability } : {}),
958
968
  ...(note ? { note } : {}),
@@ -1150,6 +1160,7 @@ export class ModelCatalogService {
1150
1160
  getOptions;
1151
1161
  cache;
1152
1162
  revision;
1163
+ publishedRevision;
1153
1164
  hasPersistedSnapshot;
1154
1165
  refreshPromise = null;
1155
1166
  inFlightIncludesVerification = false;
@@ -1159,11 +1170,92 @@ export class ModelCatalogService {
1159
1170
  const initialOptions = getOptions();
1160
1171
  const persisted = loadPersistedModelCatalog(initialOptions.storage);
1161
1172
  this.cache = persisted ? cloneCache(persisted.catalog) : createInitialCache(initialOptions);
1162
- this.revision = persisted?.revision ?? catalogRevision(this.cache);
1173
+ this.cache.piModels = this.withManagedPiModels(this.cache.piModels);
1174
+ this.revision = catalogRevision(this.cache);
1163
1175
  this.hasPersistedSnapshot = persisted !== null;
1176
+ this.publishedRevision = this.snapshot().revision;
1177
+ }
1178
+ withManagedPiModels(models) {
1179
+ const managed = this.getOptions().managedPiModels;
1180
+ if (!managed)
1181
+ return models;
1182
+ const entries = managed();
1183
+ const ids = new Set(entries.map((model) => model.id));
1184
+ return [...models.filter((model) => !model.id.startsWith("wand-openrouter-free/") && !ids.has(model.id)), ...entries];
1185
+ }
1186
+ publishManagedPiModels() {
1187
+ const next = { ...this.cache, piModels: this.withManagedPiModels(this.cache.piModels) };
1188
+ const revision = catalogRevision(next);
1189
+ const projectionChanged = this.snapshot().revision !== this.publishedRevision;
1190
+ if (revision === this.revision && !projectionChanged)
1191
+ return;
1192
+ next.refreshedAt = new Date().toISOString();
1193
+ this.cache = next;
1194
+ this.revision = revision;
1195
+ savePersistedModelCatalog(this.getOptions().storage, this.cache, revision);
1196
+ const result = { ...this.snapshot(), changed: true, checkedAt: next.refreshedAt };
1197
+ this.publishedRevision = result.revision;
1198
+ for (const listener of this.changeListeners) {
1199
+ try {
1200
+ listener(result);
1201
+ }
1202
+ catch { /* notification must not prevent publication */ }
1203
+ }
1164
1204
  }
1165
1205
  snapshot() {
1166
- return { ...cloneCache(this.cache), revision: this.revision };
1206
+ const options = this.getOptions();
1207
+ const configured = options.modelGroups?.() ?? [];
1208
+ const catalog = cloneCache(this.cache);
1209
+ // Virtual group identities are never concrete models or CLI verification targets.
1210
+ for (const field of Object.values(PROVIDER_MODEL_LIST_FIELDS)) {
1211
+ catalog[field] = catalog[field].filter((model) => !isModelGroupSelector(model.id));
1212
+ }
1213
+ const freeModels = options.managedPiModelMembers?.() ?? [];
1214
+ const groups = structuredClone(configured.filter((group) => group.id !== FREE_MODEL_GROUP_ID));
1215
+ if (options.managedPiModelMembers) {
1216
+ const saved = configured.find((group) => group.id === FREE_MODEL_GROUP_ID && group.provider === "pi");
1217
+ groups.push({ id: FREE_MODEL_GROUP_ID, provider: "pi", name: OPENROUTER_FREE_GROUP,
1218
+ models: orderModelIds(freeModels.map((model) => model.id), saved?.models ?? []) });
1219
+ }
1220
+ for (const group of groups) {
1221
+ if (group.id === FREE_MODEL_GROUP_ID)
1222
+ continue; // Already published by OpenRouter as a single pool option.
1223
+ const list = catalog[PROVIDER_MODEL_LIST_FIELDS[group.provider]];
1224
+ const members = group.provider === "pi" ? [...list, ...freeModels] : list;
1225
+ const memberInfo = group.models.map((id) => members.find((model) => model.id === id));
1226
+ const efforts = memberInfo.every((model) => model?.reasoningEfforts?.length)
1227
+ ? memberInfo[0].reasoningEfforts.filter((level) => memberInfo.every((model) => model.reasoningEfforts.some((item) => item.effort === level.effort))) : [];
1228
+ list.push({ id: modelGroupSelector(group), label: group.name, group: "模型分组",
1229
+ ...(efforts.length ? { reasoningEfforts: structuredClone(efforts) } : {}) });
1230
+ }
1231
+ const projection = { ...catalog, ...(groups.length ? { modelGroups: groups } : {}),
1232
+ ...(options.managedPiModelMembers ? { freeModels } : {}) };
1233
+ for (const field of Object.values(PROVIDER_MODEL_LIST_FIELDS)) {
1234
+ const list = projection[field];
1235
+ if (!list.some((model) => model.id === AUTO_ASSIGN_SELECTOR)) {
1236
+ const entry = { id: AUTO_ASSIGN_SELECTOR, label: AUTO_ASSIGN_LABEL, alias: false,
1237
+ source: "builtin", availability: "default",
1238
+ note: "按第一条提示词在本工具的模型分组里自动选一个" };
1239
+ // 默认项排在最前;智能分配紧跟其后,其余具体模型与分组保持原顺序。
1240
+ if (list[0]?.id === "default")
1241
+ list.splice(1, 0, entry);
1242
+ else
1243
+ list.unshift(entry);
1244
+ }
1245
+ }
1246
+ return { ...projection, revision: groups.length
1247
+ ? createHash("sha256").update(`${this.revision}:${JSON.stringify(groups)}:${JSON.stringify(freeModels)}`).digest("hex") : this.revision };
1248
+ }
1249
+ /** Preference changes publish immediately without re-running CLI discovery or altering stored history. */
1250
+ publishModelGroups() {
1251
+ const result = { ...this.snapshot(), changed: true, checkedAt: new Date().toISOString() };
1252
+ this.publishedRevision = result.revision;
1253
+ for (const listener of this.changeListeners) {
1254
+ try {
1255
+ listener(result);
1256
+ }
1257
+ catch { /* notification must not prevent saving */ }
1258
+ }
1167
1259
  }
1168
1260
  /** 目录内容真正变化时通知。客户端靠它刷新下拉,不用自己再跑一遍 CLI。 */
1169
1261
  onChanged(listener) {
@@ -1197,6 +1289,7 @@ export class ModelCatalogService {
1197
1289
  };
1198
1290
  const checkedAt = (options.now?.() ?? new Date()).toISOString();
1199
1291
  const discovered = await discoverModelCache(options, this.cache);
1292
+ discovered.piModels = this.withManagedPiModels(discovered.piModels);
1200
1293
  const discoveredRevision = catalogRevision(discovered);
1201
1294
  const changed = !this.hasPersistedSnapshot || discoveredRevision !== this.revision;
1202
1295
  if (changed) {
@@ -1208,6 +1301,7 @@ export class ModelCatalogService {
1208
1301
  this.hasPersistedSnapshot = Boolean(options.storage);
1209
1302
  }
1210
1303
  const result = { ...this.snapshot(), changed, checkedAt };
1304
+ this.publishedRevision = result.revision;
1211
1305
  if (changed) {
1212
1306
  for (const listener of this.changeListeners) {
1213
1307
  try {
@@ -17,6 +17,7 @@ import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readF
17
17
  import path from "node:path";
18
18
  import process from "node:process";
19
19
  import { promisify } from "node:util";
20
+ import { buildChildEnv } from "./env-utils.js";
20
21
  import { whichSync } from "./path-repair.js";
21
22
  import { getErrorMessage } from "./error-utils.js";
22
23
  import { compareWandInstallOrder, extractSemver } from "./version-utils.js";
@@ -114,7 +115,7 @@ function getChildEnv() {
114
115
  pathEntries.push(entry);
115
116
  }
116
117
  return {
117
- ...process.env,
118
+ ...buildChildEnv(true),
118
119
  PATH: pathEntries.join(path.delimiter),
119
120
  };
120
121
  }
@@ -0,0 +1,25 @@
1
+ import type { Model, Api } from "@earendil-works/pi-ai";
2
+ import { OpenRouterFreeModelsService, type FreeModelRequirements } from "./openrouter-free-models.js";
3
+ import type { WandStorage } from "./storage.js";
4
+ import type { PreparedPiResources } from "./pi-resource-run.js";
5
+ import type { StructuredRunnerContext } from "./structured-runner.js";
6
+ export interface OpenRouterFreeCliPolicy {
7
+ databasePath: string;
8
+ model: Model<Api>;
9
+ }
10
+ export interface OpenRouterFreeCliDependencies {
11
+ storage: WandStorage;
12
+ service: OpenRouterFreeModelsService;
13
+ }
14
+ /** Only an opaque, revocable capability goes into the child env; never the connector Key. */
15
+ export declare function prepareOpenRouterFreeCli(deps: OpenRouterFreeCliDependencies, context: StructuredRunnerContext): PreparedPiResources;
16
+ /** Read-only projection: no migrations or shared catalog writes from a running Pi child. */
17
+ export declare function openOpenRouterFreeCliAccess(databasePath: string, token: string, fetchImpl?: typeof fetch): {
18
+ selector: string;
19
+ requirements: FreeModelRequirements;
20
+ service: OpenRouterFreeModelsService;
21
+ assertCurrent: () => void;
22
+ close: () => void;
23
+ };
24
+ /** The CLI, not its original Server owner, revokes scope on shutdown. */
25
+ export declare function revokeOpenRouterFreeCliAccess(databasePath: string, token: string): void;
@@ -0,0 +1,109 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
6
+ import { fileURLToPath } from "node:url";
7
+ import { decryptVaultSecret } from "./password-manager.js";
8
+ import { OpenRouterFreeModelsService, OPENROUTER_FREE_SELECTOR } from "./openrouter-free-models.js";
9
+ /** Only an opaque, revocable capability goes into the child env; never the connector Key. */
10
+ export function prepareOpenRouterFreeCli(deps, context) {
11
+ const selector = context.session.selectedModel;
12
+ const effort = context.session.thinkingEffort?.split(":").at(-1);
13
+ const requirements = { preferReasoning: !!effort && effort !== "off",
14
+ ...(context.modelGroupModels ? { allowedSelectors: context.modelGroupModels } : {}) };
15
+ const resolved = deps.service.resolve(selector, requirements) ?? deps.service.resolve(OPENROUTER_FREE_SELECTOR, requirements);
16
+ if (!resolved)
17
+ throw new Error("免费模型已下架或 OpenRouter Key 未配置,请同步免费分组并重新选择模型。");
18
+ const dir = mkdtempSync(path.join(os.tmpdir(), "wand-pi-free-"));
19
+ let token;
20
+ const close = () => {
21
+ if (token) {
22
+ try {
23
+ deps.storage.revokeOpenRouterFreeAccess(token);
24
+ }
25
+ catch { /* The durable CLI owns cleanup after a web restart. */ }
26
+ }
27
+ rmSync(dir, { recursive: true, force: true });
28
+ };
29
+ try {
30
+ token = deps.storage.issueOpenRouterFreeAccess(context.session.id, selector, createHash("sha256").update(resolved.apiKey).digest("hex"), requirements);
31
+ const file = path.join(dir, "model.json");
32
+ const policy = { databasePath: deps.storage.databasePath(), model: resolved.model };
33
+ writeFileSync(file, JSON.stringify(policy), { mode: 0o600 });
34
+ const js = fileURLToPath(new URL("./pi-openrouter-free-bridge.js", import.meta.url));
35
+ return { args: ["--extension", existsSync(js) ? js : js.replace(/\.js$/, ".ts")],
36
+ env: { ...context.env, WAND_PI_FREE_POLICY: file, WAND_PI_FREE_TOKEN: token }, close };
37
+ }
38
+ catch (error) {
39
+ close();
40
+ throw error;
41
+ }
42
+ }
43
+ /** Read-only projection: no migrations or shared catalog writes from a running Pi child. */
44
+ export function openOpenRouterFreeCliAccess(databasePath, token, fetchImpl = globalThis.fetch) {
45
+ if (!path.isAbsolute(databasePath) || !/^wf_[A-Za-z0-9_-]{43}$/.test(token)) {
46
+ throw new Error("免费模型调用授权无效。");
47
+ }
48
+ const db = new DatabaseSync(databasePath, { readOnly: true });
49
+ const hash = createHash("sha256").update(token).digest("hex");
50
+ const scope = () => {
51
+ const row = db.prepare(`SELECT a.* FROM openrouter_free_access a JOIN command_sessions s ON s.id = a.session_id
52
+ WHERE a.token_hash = ? AND a.expires_at > ? AND s.provider = 'pi' AND s.session_kind = 'structured'
53
+ AND s.status = 'running'`).get(hash, Date.now());
54
+ if (!row)
55
+ throw new Error("免费模型调用授权已失效,请重新发送。");
56
+ return row;
57
+ };
58
+ try {
59
+ const access = scope();
60
+ const readConfig = (key) => {
61
+ const value = db.prepare("SELECT value FROM app_config WHERE key = ?").get(key)?.value;
62
+ return typeof value === "string" ? value : null;
63
+ };
64
+ const localCache = new Map();
65
+ const storage = {
66
+ getConnectorToken: () => {
67
+ const current = scope();
68
+ const encrypted = db.prepare("SELECT token FROM connectors WHERE provider = 'openrouter'").get()?.token;
69
+ const key = typeof encrypted === "string" ? decryptVaultSecret(encrypted, readConfig("appSecret")) : null;
70
+ if (!key || createHash("sha256").update(key).digest("hex") !== current.credential_hash) {
71
+ throw new Error("OpenRouter Key 已变化,本次免费调用已停止,请重新发送。");
72
+ }
73
+ return key;
74
+ },
75
+ getConfigValue: (key) => localCache.get(key) ?? readConfig(key),
76
+ setConfigValue: (key, value) => { scope(); localCache.set(key, value); },
77
+ getPreference: (key, fallback) => {
78
+ try {
79
+ return JSON.parse(readConfig(key) ?? "null") ?? fallback;
80
+ }
81
+ catch {
82
+ return fallback;
83
+ }
84
+ },
85
+ saveConnector: () => { throw new Error("CLI 不可修改 OpenRouter Key。"); },
86
+ deleteConnector: () => { throw new Error("CLI 不可删除 OpenRouter Key。"); },
87
+ };
88
+ const service = new OpenRouterFreeModelsService(storage, fetchImpl);
89
+ return { selector: String(access.selector), requirements: JSON.parse(String(access.requirements_json)),
90
+ service, assertCurrent: () => { storage.getConnectorToken("openrouter"); },
91
+ close: () => { service.dispose(); db.close(); } };
92
+ }
93
+ catch (error) {
94
+ db.close();
95
+ throw error;
96
+ }
97
+ }
98
+ /** The CLI, not its original Server owner, revokes scope on shutdown. */
99
+ export function revokeOpenRouterFreeCliAccess(databasePath, token) {
100
+ const db = new DatabaseSync(databasePath);
101
+ try {
102
+ db.exec("PRAGMA busy_timeout = 5000");
103
+ db.prepare("DELETE FROM openrouter_free_access WHERE token_hash = ?")
104
+ .run(createHash("sha256").update(token).digest("hex"));
105
+ }
106
+ finally {
107
+ db.close();
108
+ }
109
+ }