@co0ontty/wand 4.23.0 → 4.24.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.
package/dist/system-ai.js CHANGED
@@ -35,7 +35,9 @@ export function normalizeSystemAiConfig(value, fallback) {
35
35
  apiKey: typeof raw.apiKey === "string" ? raw.apiKey.trim() : fallback?.apiKey ?? "",
36
36
  model: typeof raw.model === "string" ? raw.model.trim() : fallback?.model ?? "",
37
37
  authHeader: raw.authHeader === "x-api-key" ? "x-api-key" : "bearer",
38
- source: raw.source === "claude" || raw.source === "codex" || raw.source === "opencode" ? raw.source : "custom",
38
+ source: raw.source === "claude" || raw.source === "codex" || raw.source === "opencode" || raw.source === "grok"
39
+ ? raw.source
40
+ : "custom",
39
41
  };
40
42
  if (Array.isArray(raw.fallbacks)) {
41
43
  normalized.fallbacks = raw.fallbacks
@@ -57,6 +59,25 @@ export function normalizeSystemAiConfig(value, fallback) {
57
59
  }
58
60
  return normalized;
59
61
  }
62
+ function tryNormalizeSystemAiConfig(value) {
63
+ try {
64
+ return normalizeSystemAiConfig(value);
65
+ }
66
+ catch {
67
+ // A stale or partially edited tool profile must not prevent the remaining
68
+ // APIs—or the final current-session CLI fallback—from being tried.
69
+ return null;
70
+ }
71
+ }
72
+ function systemAiProfileKey(profile) {
73
+ return [
74
+ profile.protocol,
75
+ profile.baseUrl,
76
+ profile.apiKey,
77
+ profile.model,
78
+ profile.authHeader ?? "bearer",
79
+ ].join("\0");
80
+ }
60
81
  function readJson(filePath) {
61
82
  if (!existsSync(filePath))
62
83
  return null;
@@ -67,6 +88,54 @@ function readJson(filePath) {
67
88
  return null;
68
89
  }
69
90
  }
91
+ function parseTomlString(raw) {
92
+ const value = raw.trim();
93
+ if (value.startsWith('"')) {
94
+ const quoted = /^"(?:\\.|[^"\\])*"/.exec(value)?.[0];
95
+ if (!quoted)
96
+ return null;
97
+ try {
98
+ const parsed = JSON.parse(quoted);
99
+ return typeof parsed === "string" ? parsed : null;
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ if (value.startsWith("'")) {
106
+ const end = value.indexOf("'", 1);
107
+ return end < 0 ? null : value.slice(1, end);
108
+ }
109
+ return null;
110
+ }
111
+ function readTomlStringSections(filePath) {
112
+ if (!existsSync(filePath))
113
+ return null;
114
+ try {
115
+ const sections = new Map();
116
+ let section = "";
117
+ sections.set(section, new Map());
118
+ for (const line of readFileSync(filePath, "utf8").split(/\r?\n/)) {
119
+ const sectionMatch = /^\s*\[([^\]]+)\]\s*(?:#.*)?$/.exec(line);
120
+ if (sectionMatch) {
121
+ section = sectionMatch[1].trim();
122
+ if (!sections.has(section))
123
+ sections.set(section, new Map());
124
+ continue;
125
+ }
126
+ const assignment = /^\s*([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
127
+ if (!assignment)
128
+ continue;
129
+ const value = parseTomlString(assignment[2]);
130
+ if (value !== null)
131
+ sections.get(section).set(assignment[1], value);
132
+ }
133
+ return sections;
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ }
70
139
  function discoverClaude(home) {
71
140
  const settings = readJson(path.join(home, ".claude", "settings.json"));
72
141
  const env = settings?.env && typeof settings.env === "object" ? settings.env : {};
@@ -76,7 +145,7 @@ function discoverClaude(home) {
76
145
  const model = typeof settings?.model === "string" ? settings.model : "";
77
146
  if (!apiKey || !model)
78
147
  return null;
79
- return normalizeSystemAiConfig({
148
+ return tryNormalizeSystemAiConfig({
80
149
  enabled: true, protocol: "anthropic", baseUrl, apiKey, model,
81
150
  authHeader: typeof env.ANTHROPIC_AUTH_TOKEN === "string" ? "bearer" : "x-api-key",
82
151
  source: "claude",
@@ -100,7 +169,7 @@ function discoverOpenCode(home) {
100
169
  : typeof provider?.model === "string" ? provider.model : Object.keys(models)[0] ?? "";
101
170
  if (!apiKey || !baseUrl || !configuredModel)
102
171
  continue;
103
- found.push(normalizeSystemAiConfig({
172
+ const profile = tryNormalizeSystemAiConfig({
104
173
  enabled: true,
105
174
  protocol: "openai",
106
175
  baseUrl,
@@ -108,7 +177,9 @@ function discoverOpenCode(home) {
108
177
  model: configuredModel,
109
178
  authHeader: "bearer",
110
179
  source: "opencode",
111
- }));
180
+ });
181
+ if (profile)
182
+ found.push(profile);
112
183
  }
113
184
  return found;
114
185
  }
@@ -127,7 +198,40 @@ function discoverCodex(home) {
127
198
  catch { /* optional config */ }
128
199
  if (!model)
129
200
  return null;
130
- return normalizeSystemAiConfig({ enabled: true, protocol: "openai", baseUrl, apiKey, model, authHeader: "bearer", source: "codex" });
201
+ return tryNormalizeSystemAiConfig({ enabled: true, protocol: "openai", baseUrl, apiKey, model, authHeader: "bearer", source: "codex" });
202
+ }
203
+ function discoverGrok(home) {
204
+ const sections = readTomlStringSections(path.join(home, ".grok", "config.toml"));
205
+ if (!sections)
206
+ return [];
207
+ const defaultProfile = sections.get("models")?.get("default") ?? "";
208
+ const modelSections = [...sections.entries()].filter(([name]) => name.startsWith("model."));
209
+ const orderedSections = [
210
+ ...modelSections.filter(([name]) => name === `model.${defaultProfile}`),
211
+ ...modelSections.filter(([name]) => name !== `model.${defaultProfile}`),
212
+ ];
213
+ const found = [];
214
+ for (const [section, values] of orderedSections) {
215
+ if (values.get("api_backend")?.trim().toLowerCase() !== "chat_completions")
216
+ continue;
217
+ const apiKey = values.get("api_key") ?? "";
218
+ const baseUrl = values.get("base_url") ?? "";
219
+ const model = values.get("model") ?? section.slice("model.".length);
220
+ if (!apiKey || !baseUrl || !model)
221
+ continue;
222
+ const profile = tryNormalizeSystemAiConfig({
223
+ enabled: true,
224
+ protocol: "openai",
225
+ baseUrl,
226
+ apiKey,
227
+ model,
228
+ authHeader: "bearer",
229
+ source: "grok",
230
+ });
231
+ if (profile)
232
+ found.push(profile);
233
+ }
234
+ return found;
131
235
  }
132
236
  /** Copy every usable direct-API profile from the user's configured CLIs. */
133
237
  export function discoverCliSystemAiConfigs(preferred, home = os.homedir()) {
@@ -135,15 +239,23 @@ export function discoverCliSystemAiConfigs(preferred, home = os.homedir()) {
135
239
  claude: (dir) => [discoverClaude(dir)].filter((item) => item !== null),
136
240
  codex: (dir) => [discoverCodex(dir)].filter((item) => item !== null),
137
241
  opencode: discoverOpenCode,
242
+ grok: discoverGrok,
138
243
  };
139
- const order = [preferred ?? "claude", "claude", "opencode", "codex"];
244
+ const order = [preferred ?? "claude", "claude", "opencode", "grok", "codex"];
140
245
  const found = [];
141
246
  const seen = new Set();
142
247
  for (const provider of [...new Set(order)]) {
143
- if (provider === "grok" || provider === "qoder")
248
+ if (provider === "qoder")
144
249
  continue;
145
- for (const profile of discoverers[provider](home)) {
146
- const key = [profile.protocol, profile.baseUrl, profile.apiKey, profile.model].join("\0");
250
+ let discovered;
251
+ try {
252
+ discovered = discoverers[provider](home);
253
+ }
254
+ catch {
255
+ continue;
256
+ }
257
+ for (const profile of discovered) {
258
+ const key = systemAiProfileKey(profile);
147
259
  if (seen.has(key))
148
260
  continue;
149
261
  seen.add(key);
@@ -163,25 +275,53 @@ export function systemAiProfiles(config, forceEnabled = false) {
163
275
  const candidates = [config, ...(config.fallbacks ?? [])];
164
276
  const seen = new Set();
165
277
  return candidates.flatMap((candidate) => {
166
- const normalized = normalizeSystemAiConfig({ ...candidate, enabled: true, fallbacks: undefined });
278
+ const normalized = tryNormalizeSystemAiConfig({ ...candidate, enabled: true, fallbacks: undefined });
279
+ if (!normalized)
280
+ return [];
167
281
  if (!normalized.baseUrl || !normalized.apiKey || !normalized.model)
168
282
  return [];
169
- const key = [normalized.protocol, normalized.baseUrl, normalized.apiKey, normalized.model].join("\0");
283
+ const key = systemAiProfileKey(normalized);
170
284
  if (seen.has(key))
171
285
  return [];
172
286
  seen.add(key);
173
287
  return [normalized];
174
288
  });
175
289
  }
290
+ /**
291
+ * Flatten and combine direct-API groups in priority order. Dynamically discovered
292
+ * profiles can be passed first, followed by stored/legacy settings.
293
+ */
294
+ export function mergeSystemAiConfigs(...groups) {
295
+ const profiles = [];
296
+ const seen = new Set();
297
+ for (const group of groups) {
298
+ for (const config of Array.isArray(group) ? group : group ? [group] : []) {
299
+ for (const profile of systemAiProfiles(config, true)) {
300
+ const key = systemAiProfileKey(profile);
301
+ if (seen.has(key))
302
+ continue;
303
+ seen.add(key);
304
+ profiles.push({ ...profile, enabled: true, fallbacks: undefined });
305
+ }
306
+ }
307
+ }
308
+ const [primary, ...fallbacks] = profiles;
309
+ if (!primary)
310
+ return undefined;
311
+ const merged = { ...primary, enabled: true };
312
+ if (fallbacks.length)
313
+ merged.fallbacks = fallbacks;
314
+ return merged;
315
+ }
176
316
  function endpoint(baseUrl, protocol) {
177
317
  const url = new URL(baseUrl);
178
318
  const pathName = url.pathname.replace(/\/+$/, "");
179
319
  const fullSuffix = protocol === "anthropic" ? "/v1/messages" : "/v1/chat/completions";
180
320
  const shortSuffix = protocol === "anthropic" ? "/messages" : "/chat/completions";
181
- if (pathName.toLowerCase().endsWith(fullSuffix)) {
321
+ if (pathName.toLowerCase().endsWith(shortSuffix)) {
182
322
  url.pathname = pathName;
183
323
  }
184
- else if (pathName.toLowerCase().endsWith("/v1")) {
324
+ else if (/\/v\d+(?:\.\d+)?$/i.test(pathName)) {
185
325
  url.pathname = `${pathName}${shortSuffix}`;
186
326
  }
187
327
  else {
package/dist/types.d.ts CHANGED
@@ -145,7 +145,7 @@ export interface SystemAiConfig {
145
145
  model: string;
146
146
  authHeader?: SystemAiAuthHeader;
147
147
  /** 自动导入时记录来源,仅用于设置页说明。 */
148
- source?: "claude" | "codex" | "opencode" | "custom";
148
+ source?: "claude" | "codex" | "opencode" | "grok" | "custom";
149
149
  /**
150
150
  * 其余可直连 API,按数组顺序依次尝试。保留顶层字段作为首选项,
151
151
  * 以兼容已有配置与手工编辑入口。