abelworkflow 1.1.3 → 1.2.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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -29
  3. package/extensions/gpt-responses-compat.ts +213 -0
  4. package/extensions/k2think-compat.ts +40 -0
  5. package/lib/cli/args.mjs +5 -19
  6. package/lib/cli/main.mjs +105 -72
  7. package/lib/cli/prompts.mjs +9 -2
  8. package/lib/config/dotenv.mjs +1 -1
  9. package/lib/config/jsonc.mjs +1 -1
  10. package/lib/config/store.mjs +197 -45
  11. package/lib/config/toml.mjs +147 -453
  12. package/lib/installer/assets.mjs +67 -45
  13. package/lib/installer/install.mjs +202 -45
  14. package/lib/installer/links.mjs +124 -101
  15. package/lib/installer/lock.mjs +82 -0
  16. package/lib/installer/state.mjs +14 -21
  17. package/lib/paths.mjs +9 -9
  18. package/lib/providers/claude.mjs +58 -114
  19. package/lib/providers/codex.mjs +101 -198
  20. package/lib/providers/pi.mjs +331 -497
  21. package/lib/providers/skills.mjs +88 -68
  22. package/lib/providers/url.mjs +35 -0
  23. package/lib/templates/workflow/commands/abel-design.md +23 -161
  24. package/lib/templates/workflow/commands/abel-diagnose.md +30 -56
  25. package/lib/templates/workflow/commands/abel-implement.md +30 -150
  26. package/lib/templates/workflow/commands/abel-init.md +1 -1
  27. package/lib/tools/cli-installer.mjs +29 -190
  28. package/package.json +7 -9
  29. package/skills/context7-auto-research/context7-api.cjs +75 -17
  30. package/skills/dev-browser/SKILL.md +15 -1
  31. package/skills/dev-browser/dist/scripts/start.d.ts +0 -1
  32. package/skills/dev-browser/dist/scripts/start.js +8 -36
  33. package/skills/dev-browser/dist/src/client.d.ts +3 -4
  34. package/skills/dev-browser/dist/src/client.js +117 -117
  35. package/skills/dev-browser/dist/src/entrypoint.d.ts +1 -1
  36. package/skills/dev-browser/dist/src/entrypoint.js +38 -31
  37. package/skills/dev-browser/dist/src/index.d.ts +0 -1
  38. package/skills/dev-browser/dist/src/index.js +0 -1
  39. package/skills/dev-browser/dist/src/page-api.d.ts +0 -1
  40. package/skills/dev-browser/dist/src/page-api.js +0 -1
  41. package/skills/dev-browser/dist/src/relay.d.ts +0 -1
  42. package/skills/dev-browser/dist/src/relay.js +53 -7
  43. package/skills/dev-browser/dist/src/runtime.d.ts +3 -14
  44. package/skills/dev-browser/dist/src/runtime.js +5 -15
  45. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +0 -1
  46. package/skills/dev-browser/dist/src/snapshot/browser-script.js +0 -1
  47. package/skills/dev-browser/dist/src/snapshot/index.d.ts +0 -1
  48. package/skills/dev-browser/dist/src/snapshot/index.js +0 -1
  49. package/skills/dev-browser/dist/src/snapshot/inject.d.ts +0 -1
  50. package/skills/dev-browser/dist/src/snapshot/inject.js +0 -1
  51. package/skills/dev-browser/dist/src/standalone.d.ts +0 -1
  52. package/skills/dev-browser/dist/src/standalone.js +2 -3
  53. package/skills/dev-browser/dist/src/startup.d.ts +3 -12
  54. package/skills/dev-browser/dist/src/startup.js +6 -10
  55. package/skills/dev-browser/dist/src/target-registry.d.ts +0 -1
  56. package/skills/dev-browser/dist/src/target-registry.js +0 -1
  57. package/skills/dev-browser/dist/src/types.d.ts +0 -1
  58. package/skills/dev-browser/dist/src/types.js +0 -1
  59. package/skills/dev-browser/package-lock.json +1 -1
  60. package/skills/dev-browser/package.json +2 -2
  61. package/skills/grok-search/.env.example +1 -1
  62. package/skills/grok-search/SKILL.md +13 -16
  63. package/skills/grok-search/scripts/_dotenv.py +1 -1
  64. package/skills/grok-search/scripts/groksearch_cli.py +271 -195
  65. package/skills/time/SKILL.md +5 -2
  66. package/skills/time/scripts/time_cli.py +61 -22
  67. package/extensions/pi-gpt-responses-compat/index.ts +0 -25
  68. package/lib/templates/codex/config-base.toml +0 -103
  69. package/skills/dev-browser/dist/scripts/start.d.ts.map +0 -1
  70. package/skills/dev-browser/dist/scripts/start.js.map +0 -1
  71. package/skills/dev-browser/dist/src/client.d.ts.map +0 -1
  72. package/skills/dev-browser/dist/src/client.js.map +0 -1
  73. package/skills/dev-browser/dist/src/entrypoint.d.ts.map +0 -1
  74. package/skills/dev-browser/dist/src/entrypoint.js.map +0 -1
  75. package/skills/dev-browser/dist/src/index.d.ts.map +0 -1
  76. package/skills/dev-browser/dist/src/index.js.map +0 -1
  77. package/skills/dev-browser/dist/src/page-api.d.ts.map +0 -1
  78. package/skills/dev-browser/dist/src/page-api.js.map +0 -1
  79. package/skills/dev-browser/dist/src/relay.d.ts.map +0 -1
  80. package/skills/dev-browser/dist/src/relay.js.map +0 -1
  81. package/skills/dev-browser/dist/src/runtime.d.ts.map +0 -1
  82. package/skills/dev-browser/dist/src/runtime.js.map +0 -1
  83. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts.map +0 -1
  84. package/skills/dev-browser/dist/src/snapshot/browser-script.js.map +0 -1
  85. package/skills/dev-browser/dist/src/snapshot/index.d.ts.map +0 -1
  86. package/skills/dev-browser/dist/src/snapshot/index.js.map +0 -1
  87. package/skills/dev-browser/dist/src/snapshot/inject.d.ts.map +0 -1
  88. package/skills/dev-browser/dist/src/snapshot/inject.js.map +0 -1
  89. package/skills/dev-browser/dist/src/standalone.d.ts.map +0 -1
  90. package/skills/dev-browser/dist/src/standalone.js.map +0 -1
  91. package/skills/dev-browser/dist/src/startup.d.ts.map +0 -1
  92. package/skills/dev-browser/dist/src/startup.js.map +0 -1
  93. package/skills/dev-browser/dist/src/target-registry.d.ts.map +0 -1
  94. package/skills/dev-browser/dist/src/target-registry.js.map +0 -1
  95. package/skills/dev-browser/dist/src/types.d.ts.map +0 -1
  96. package/skills/dev-browser/dist/src/types.js.map +0 -1
  97. package/skills/grok-search/gitignore.template +0 -4
  98. package/skills/grok-search/scripts/groksearch_entry.py +0 -131
@@ -1,198 +1,14 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { join } from "node:path";
3
1
  import * as p from "@clack/prompts";
4
2
  import {
3
+ assertPlainObject,
5
4
  readJsonFileSafe,
6
5
  readJsoncFileSafe,
7
- updateLockedJson,
8
- writeJson,
9
- writeText
6
+ withFileTransaction
10
7
  } from "../config/store.mjs";
11
- import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
8
+ import { normalizeOpenAiBaseUrl } from "./url.mjs";
9
+ import { stripJsonComments } from "../config/jsonc.mjs";
12
10
 
13
- const minimumPiVersion = [0, 80, 0];
14
- const piBootstrapProviderId = "gpt";
15
- const piBootstrapApi = "openai-completions";
16
- const piBootstrapBaseUrl = "https://api.openai.com/v1";
17
- const piBootstrapModelId = "gpt-5.5";
18
- const piRpcRequestId = "abelworkflow-provider";
19
- const piRpcArgs = [
20
- "--mode", "rpc",
21
- "--no-session",
22
- "--offline",
23
- "--no-context-files",
24
- "--no-skills",
25
- "--no-prompt-templates",
26
- "--no-themes"
27
- ];
28
-
29
- function requirePiProviderId(value) {
30
- const providerId = typeof value === "string" ? value.trim() : "";
31
- if (!providerId) {
32
- throw new Error("未检测到 Pi 当前有效 Provider;请先在 Pi 中配置可用模型。");
33
- }
34
- return providerId;
35
- }
36
-
37
- async function updatePiAuthFile(path, providerId, apiKey) {
38
- const targetProviderId = requirePiProviderId(providerId);
39
- return updateLockedJson(path, (auth) => {
40
- if (!auth || typeof auth !== "object" || Array.isArray(auth)) {
41
- throw new TypeError("Pi auth.json must contain a JSON object");
42
- }
43
- return buildPiAuthConfig(auth, targetProviderId, apiKey);
44
- }, {
45
- sensitive: true,
46
- retries: {
47
- retries: 10,
48
- factor: 2,
49
- minTimeout: 100,
50
- maxTimeout: 10000,
51
- randomize: true
52
- }
53
- });
54
- }
55
-
56
- function parsePiVersion(value) {
57
- const match = String(value || "").match(/(?:^|\D)v?(\d+)\.(\d+)\.(\d+)(?:\D|$)/u);
58
- return match ? match.slice(1).map(Number) : null;
59
- }
60
-
61
- function assertSupportedPiVersion(value) {
62
- const version = parsePiVersion(value);
63
- if (!version) {
64
- throw new Error("无法检测 Pi 版本(可能尚未安装);请先安装 Pi 0.80.0 或更高版本。");
65
- }
66
- for (let index = 0; index < minimumPiVersion.length; index += 1) {
67
- if (version[index] > minimumPiVersion[index]) return version;
68
- if (version[index] < minimumPiVersion[index]) {
69
- throw new Error(`当前 Pi ${version.join(".")} 不支持 auth-only 自定义 Provider;请先升级到 Pi 0.80.0 或更高版本。`);
70
- }
71
- }
72
- return version;
73
- }
74
-
75
- function getPiProcessInvocation(args, {
76
- platform = process.platform,
77
- comspec = process.env.ComSpec || process.env.COMSPEC || "cmd.exe"
78
- } = {}) {
79
- return platform === "win32"
80
- ? { command: comspec, args: ["/d", "/c", "pi", ...args] }
81
- : { command: "pi", args };
82
- }
83
-
84
- function detectPiVersion() {
85
- const invocation = getPiProcessInvocation(["--version"]);
86
- const result = spawnSync(invocation.command, invocation.args, {
87
- encoding: "utf8",
88
- stdio: ["ignore", "pipe", "pipe"]
89
- });
90
- return result.status === 0 ? `${result.stdout || ""} ${result.stderr || ""}`.trim() : undefined;
91
- }
92
-
93
- function isPiUnknownModel(model) {
94
- return model?.provider === "unknown" && model?.id === "unknown" && model?.api === "unknown";
95
- }
96
-
97
- function parsePiRpcEffectiveModel(value) {
98
- for (const line of String(value || "").split(/\r?\n/u)) {
99
- let payload;
100
- try {
101
- payload = JSON.parse(line);
102
- } catch {
103
- continue;
104
- }
105
- if (payload?.type !== "response"
106
- || payload.command !== "get_state"
107
- || payload.success !== true) continue;
108
- const model = payload.data?.model;
109
- if (model === null) return null;
110
- const effectiveModel = {
111
- provider: typeof model?.provider === "string" ? model.provider.trim() : "",
112
- id: typeof model?.id === "string" ? model.id.trim() : "",
113
- api: typeof model?.api === "string" ? model.api.trim() : "",
114
- baseUrl: typeof model?.baseUrl === "string" ? model.baseUrl.trim() : ""
115
- };
116
- if (isPiUnknownModel(effectiveModel)) return null;
117
- if (effectiveModel.provider && effectiveModel.id) return effectiveModel;
118
- }
119
- }
120
-
121
- function runPiRpcCommand(command, args, {
122
- input = "",
123
- maxBuffer = 1024 * 1024,
124
- platform = process.platform,
125
- start = spawn,
126
- timeout = 20000
127
- } = {}) {
128
- return new Promise((resolve) => {
129
- let child;
130
- let stdout = "";
131
- let settled = false;
132
- let timer;
133
- const finish = (status) => {
134
- if (settled) return;
135
- settled = true;
136
- clearTimeout(timer);
137
- child?.stdin?.destroy();
138
- child?.stdout?.destroy();
139
- child?.stderr?.destroy();
140
- if (child?.exitCode === null && child.signalCode === null && !child.killed) child.kill();
141
- child?.unref();
142
- resolve({ status, stdout });
143
- };
144
-
145
- try {
146
- const env = { ...process.env };
147
- delete env.NODE_TEST_CONTEXT;
148
- const invocation = command === "pi"
149
- ? getPiProcessInvocation(args, { platform })
150
- : { command, args };
151
- child = start(invocation.command, invocation.args, {
152
- env,
153
- stdio: ["pipe", "pipe", "ignore"],
154
- windowsHide: true
155
- });
156
- } catch {
157
- finish(null);
158
- return;
159
- }
160
-
161
- timer = setTimeout(() => finish(null), timeout);
162
- child.on("error", () => finish(null));
163
- child.on("exit", (code) => finish(code));
164
- child.stdout.setEncoding("utf8");
165
- child.stdout.on("data", (chunk) => {
166
- stdout += chunk;
167
- if (Buffer.byteLength(stdout, "utf8") > maxBuffer) {
168
- finish(null);
169
- } else if (parsePiRpcEffectiveModel(stdout) !== undefined) {
170
- finish(0);
171
- }
172
- });
173
- child.stdin.on("error", () => finish(null));
174
- child.stdin.write(input, (error) => {
175
- if (error) finish(null);
176
- });
177
- });
178
- }
179
-
180
- async function detectPiEffectiveModel(run = runPiRpcCommand) {
181
- let result;
182
- try {
183
- result = await run("pi", piRpcArgs, {
184
- encoding: "utf8",
185
- input: `${JSON.stringify({ id: piRpcRequestId, type: "get_state" })}\n`,
186
- maxBuffer: 1024 * 1024,
187
- platform: process.platform,
188
- timeout: 20000
189
- });
190
- } catch {
191
- return undefined;
192
- }
193
- if (result?.error || result?.status !== 0) return undefined;
194
- return parsePiRpcEffectiveModel(result.stdout);
195
- }
11
+ const piProviderId = "abelworkflow";
196
12
 
197
13
  function parsePiModelIds(value) {
198
14
  return [...new Set(String(value || "")
@@ -202,43 +18,20 @@ function parsePiModelIds(value) {
202
18
  }
203
19
 
204
20
  function getOpenAiUrlPathname(value) {
205
- const input = String(value || "").trim();
206
21
  try {
207
- return new URL(input).pathname.replace(/\/+$/u, "");
22
+ return new URL(String(value || "").trim()).pathname.replace(/\/+$/u, "");
208
23
  } catch {
209
- return input.split(/[?#]/u)[0].replace(/\/+$/u, "");
24
+ return "";
210
25
  }
211
26
  }
212
27
 
213
28
  function inferPiApiFromBaseUrl(value) {
214
29
  const pathname = getOpenAiUrlPathname(value);
215
- if (pathname.endsWith("/v1/chat/completions")) {
216
- return "openai-completions";
217
- }
218
- if (pathname.endsWith("/v1/responses")) {
219
- return "openai-responses";
220
- }
30
+ if (pathname.endsWith("/v1/chat/completions")) return "openai-completions";
31
+ if (pathname.endsWith("/v1/responses")) return "openai-responses";
221
32
  return null;
222
33
  }
223
34
 
224
- function normalizeOpenAiBaseUrl(value) {
225
- const input = String(value || "").trim();
226
- try {
227
- const url = new URL(input);
228
- let pathname = url.pathname.replace(/\/+$/u, "")
229
- .replace(/\/v1\/(?:chat\/completions|responses)$/u, "/v1");
230
- if (!pathname.endsWith("/v1")) {
231
- pathname = `${pathname}/v1`;
232
- }
233
- url.pathname = pathname;
234
- return url.toString();
235
- } catch {
236
- const baseUrl = input.replace(/\/+$/u, "")
237
- .replace(/\/v1\/(?:chat\/completions|responses)$/u, "/v1");
238
- return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
239
- }
240
- }
241
-
242
35
  function getPiApiPromptOptions() {
243
36
  return [
244
37
  { value: "openai-completions", label: "OpenAI Chat Completions(推荐)" },
@@ -246,351 +39,392 @@ function getPiApiPromptOptions() {
246
39
  ];
247
40
  }
248
41
 
249
- function resolveExistingPiApiConfig(modelsConfig = {}, settings = {}, auth = {}, effectiveModel) {
250
- const savedProviderId = typeof settings.defaultProvider === "string" ? settings.defaultProvider.trim() : "";
251
- const savedModelId = typeof settings.defaultModel === "string" ? settings.defaultModel.trim() : "";
252
- const effectiveProviderId = typeof effectiveModel?.provider === "string" ? effectiveModel.provider.trim() : "";
253
- const effectiveModelId = typeof effectiveModel?.id === "string" ? effectiveModel.id.trim() : "";
254
- const savedProvider = savedProviderId
255
- && modelsConfig.providers?.[savedProviderId]
256
- && typeof modelsConfig.providers[savedProviderId] === "object"
257
- ? modelsConfig.providers[savedProviderId]
258
- : undefined;
259
- const savedModels = Array.isArray(savedProvider?.models) ? savedProvider.models : [];
260
- const savedTargetExists = savedProviderId
261
- && savedModelId
262
- && savedModels.some((model) => model?.id === savedModelId);
263
- const providerId = effectiveProviderId && effectiveModelId
264
- ? effectiveProviderId
265
- : savedTargetExists ? savedProviderId : "";
266
- const defaultModel = effectiveProviderId && effectiveModelId
267
- ? effectiveModelId
268
- : savedTargetExists ? savedModelId : "";
269
- const provider = providerId
270
- && modelsConfig.providers?.[providerId]
271
- && typeof modelsConfig.providers[providerId] === "object"
272
- ? modelsConfig.providers[providerId]
273
- : {};
274
- const credential = providerId && auth[providerId] && typeof auth[providerId] === "object"
275
- ? auth[providerId]
276
- : {};
277
- const authApiKey = credential.type === "api_key" && typeof credential.key === "string"
278
- ? credential.key
42
+ function resolveExistingPiApiConfig(modelsConfig = {}, settings = {}, auth = {}) {
43
+ assertPlainObject(modelsConfig, "Pi models");
44
+ assertPlainObject(settings, "Pi settings");
45
+ assertPlainObject(auth, "Pi auth");
46
+ const providers = modelsConfig.providers === undefined
47
+ ? {}
48
+ : assertPlainObject(modelsConfig.providers, "Pi models providers");
49
+ const provider = providers[piProviderId];
50
+ const target = provider === undefined ? {} : assertPlainObject(provider, `Pi provider ${piProviderId}`);
51
+ const models = Array.isArray(target.models) ? target.models.filter((model) => model?.id) : [];
52
+ const modelIds = models.map((model) => model.id);
53
+ const savedModel = settings.defaultProvider === piProviderId
54
+ && modelIds.includes(settings.defaultModel)
55
+ ? settings.defaultModel
279
56
  : "";
280
- const models = Array.isArray(provider.models) ? provider.models.filter((model) => model?.id) : [];
281
- const model = models.find((item) => item.id === defaultModel) || {};
282
- const runtimeMatches = Boolean(effectiveProviderId
283
- && effectiveModelId
284
- && effectiveProviderId === providerId
285
- && effectiveModelId === defaultModel);
286
- return {
287
- providerId,
288
- baseUrl: runtimeMatches && typeof effectiveModel.baseUrl === "string"
289
- ? effectiveModel.baseUrl
290
- : typeof model.baseUrl === "string" ? model.baseUrl
291
- : typeof provider.baseUrl === "string" ? provider.baseUrl : "",
292
- api: runtimeMatches && typeof effectiveModel.api === "string"
293
- ? effectiveModel.api
294
- : typeof model.api === "string" ? model.api
295
- : typeof provider.api === "string" ? provider.api : "",
296
- apiKey: authApiKey || (typeof provider.apiKey === "string" ? provider.apiKey : ""),
297
- modelIds: models.map((model) => model.id),
298
- defaultModel
299
- };
300
- }
301
-
302
- function resolvePiApiTarget(modelsConfig = {}, settings = {}, auth = {}, effectiveModel) {
303
- const hasNoEffectiveModel = effectiveModel === null || isPiUnknownModel(effectiveModel);
304
- const configuration = resolveExistingPiApiConfig(
305
- modelsConfig,
306
- settings,
307
- auth,
308
- hasNoEffectiveModel ? undefined : effectiveModel
309
- );
310
- if (configuration.providerId || !hasNoEffectiveModel) {
311
- return { bootstrap: false, configuration };
312
- }
313
-
314
- const provider = modelsConfig.providers?.[piBootstrapProviderId];
315
- const models = Array.isArray(provider?.models) ? provider.models.filter((model) => model?.id) : [];
316
- const credential = auth[piBootstrapProviderId];
317
- const apiKey = credential?.type === "api_key" && typeof credential.key === "string"
57
+ const credential = auth[piProviderId];
58
+ const authApiKey = credential?.type === "api_key" && typeof credential.key === "string"
318
59
  ? credential.key
319
- : typeof provider?.apiKey === "string" ? provider.apiKey : "";
320
- return {
321
- bootstrap: true,
322
- configuration: {
323
- providerId: piBootstrapProviderId,
324
- baseUrl: typeof provider?.baseUrl === "string" ? provider.baseUrl : piBootstrapBaseUrl,
325
- api: typeof provider?.api === "string" ? provider.api : piBootstrapApi,
326
- apiKey,
327
- modelIds: models.map((model) => model.id),
328
- defaultModel: models[0]?.id || piBootstrapModelId
329
- }
330
- };
331
- }
332
-
333
- function assertConfigurablePiProvider(modelsConfig = {}, configuration = {}) {
334
- const { providerId, defaultModel, api } = configuration;
335
- const provider = modelsConfig.providers?.[providerId];
336
- const models = Array.isArray(provider?.models) ? provider.models : [];
337
- if (!provider || typeof provider !== "object" || !models.some((model) => model?.id === defaultModel)) {
338
- throw new Error(`Pi 当前有效 Provider ${providerId || "未知"} 不是 models.json 中的自定义 Provider;为避免覆盖内置模型,已停止配置。`);
339
- }
340
- const supportedApis = new Set(getPiApiPromptOptions().map((option) => option.value));
341
- if (api && !supportedApis.has(api)) {
342
- throw new Error(`Pi 当前有效 Provider ${providerId} 使用不受支持的 API 类型 ${api};此配置器仅支持 OpenAI-compatible API。`);
343
- }
344
- }
345
-
346
- function buildPiModelConfig(modelId, existingModel = {}) {
347
- const model = { ...existingModel };
348
- delete model.baseUrl;
349
- delete model.api;
60
+ : "";
350
61
  return {
351
- ...model,
352
- id: modelId,
353
- name: model.name || modelId,
354
- reasoning: model.reasoning ?? true,
355
- input: Array.isArray(model.input) ? model.input : ["text", "image"],
356
- contextWindow: model.contextWindow ?? 262144,
357
- maxTokens: model.maxTokens ?? 64000
62
+ baseUrl: typeof target.baseUrl === "string" ? target.baseUrl : "",
63
+ api: typeof target.api === "string" ? target.api : "",
64
+ apiKey: authApiKey,
65
+ modelIds,
66
+ defaultModel: savedModel || modelIds[0] || ""
358
67
  };
359
68
  }
360
69
 
361
- function buildPiModelsConfig(modelsConfig = {}, {
362
- providerId,
363
- baseUrl,
364
- api,
365
- modelIds
366
- }) {
367
- const targetProviderId = requirePiProviderId(providerId);
368
- const providers = modelsConfig.providers && typeof modelsConfig.providers === "object" ? modelsConfig.providers : {};
369
- const currentProvider = providers[targetProviderId] && typeof providers[targetProviderId] === "object"
370
- ? providers[targetProviderId]
371
- : {};
70
+ function buildPiModelsConfig(modelsConfig = {}, { baseUrl, api, apiKey, modelIds }) {
71
+ assertPlainObject(modelsConfig, "Pi models");
72
+ const providers = modelsConfig.providers === undefined
73
+ ? {}
74
+ : assertPlainObject(modelsConfig.providers, "Pi models providers");
75
+ const currentProvider = providers[piProviderId] === undefined
76
+ ? {}
77
+ : assertPlainObject(providers[piProviderId], `Pi provider ${piProviderId}`);
372
78
  const existingModels = new Map(
373
79
  (Array.isArray(currentProvider.models) ? currentProvider.models : [])
374
80
  .filter((model) => model?.id)
375
81
  .map((model) => [model.id, model])
376
82
  );
377
-
83
+ const responses = api === "openai-responses";
378
84
  const provider = {
379
- ...currentProvider,
380
- baseUrl,
85
+ baseUrl: normalizeOpenAiBaseUrl(baseUrl),
381
86
  api,
382
- compat: {
383
- ...(currentProvider.compat && typeof currentProvider.compat === "object" ? currentProvider.compat : {}),
384
- supportsDeveloperRole: false
385
- },
386
- models: modelIds.map((modelId) => buildPiModelConfig(modelId, existingModels.get(modelId)))
87
+ ...(responses ? {
88
+ apiKey,
89
+ compat: { supportsDeveloperRole: false }
90
+ } : {}),
91
+ models: modelIds.map((modelId) => {
92
+ const name = existingModels.get(modelId)?.name;
93
+ const model = { id: modelId, name: typeof name === "string" && name ? name : modelId };
94
+ return responses ? {
95
+ ...model,
96
+ reasoning: true,
97
+ input: ["text", "image"],
98
+ thinkingLevelMap: {
99
+ off: null,
100
+ minimal: null,
101
+ low: null,
102
+ medium: null,
103
+ high: null,
104
+ xhigh: null,
105
+ max: "max"
106
+ },
107
+ contextWindow: 262144,
108
+ maxTokens: 131072
109
+ } : model;
110
+ })
387
111
  };
388
- delete provider.apiKey;
389
112
 
390
113
  return {
391
114
  ...modelsConfig,
392
115
  providers: {
393
116
  ...providers,
394
- [targetProviderId]: provider
117
+ [piProviderId]: provider
395
118
  }
396
119
  };
397
120
  }
398
121
 
399
- function buildPiAuthConfig(auth = {}, providerId, apiKey) {
400
- const targetProviderId = requirePiProviderId(providerId);
401
- const credential = auth[targetProviderId] && typeof auth[targetProviderId] === "object"
402
- ? auth[targetProviderId]
403
- : {};
404
- return {
405
- ...auth,
406
- [targetProviderId]: {
407
- ...credential,
408
- type: "api_key",
409
- key: apiKey
410
- }
411
- };
122
+ function buildPiAuthConfig(auth = {}, apiKey) {
123
+ assertPlainObject(auth, "Pi auth");
124
+ const nextAuth = { ...auth };
125
+ if (!apiKey) {
126
+ delete nextAuth[piProviderId];
127
+ return nextAuth;
128
+ }
129
+ const credential = auth[piProviderId] === undefined
130
+ ? {}
131
+ : assertPlainObject(auth[piProviderId], `Pi credential ${piProviderId}`);
132
+ nextAuth[piProviderId] = { ...credential, type: "api_key", key: apiKey };
133
+ return nextAuth;
412
134
  }
413
135
 
414
- function buildPiSettingsConfig(settings = {}, providerId, defaultModel) {
136
+ function buildPiSettingsConfig(settings = {}, defaultModel) {
137
+ assertPlainObject(settings, "Pi settings");
415
138
  return {
416
139
  ...settings,
417
- defaultProvider: requirePiProviderId(providerId),
418
- defaultModel,
419
- defaultThinkingLevel: settings.defaultThinkingLevel || "high",
420
- enableSkillCommands: settings.enableSkillCommands ?? true
140
+ defaultProvider: piProviderId,
141
+ defaultModel
421
142
  };
422
143
  }
423
144
 
424
145
  async function readExistingPiConfiguration(paths, operations = {}) {
425
- const readAuth = operations.readAuth ?? ((path) => readJsonFileSafe(path, {}, { sensitive: true }));
426
- const readModels = operations.readModels ?? ((path) => readJsoncFileSafe(path, {}, { sensitive: true }));
427
- const readSettings = operations.readSettings ?? ((path) => readJsonFileSafe(path, {}));
146
+ const readAuth = operations.readAuth ?? ((path) => readJsonFileSafe(path, {}, {
147
+ safeRoot: paths.homeDir,
148
+ sensitive: true
149
+ }));
150
+ const readModels = operations.readModels ?? ((path) => readJsoncFileSafe(path, {}, {
151
+ safeRoot: paths.homeDir,
152
+ sensitive: true
153
+ }));
154
+ const readSettings = operations.readSettings ?? ((path) => readJsonFileSafe(path, {}, {
155
+ safeRoot: paths.homeDir
156
+ }));
428
157
  const auth = await readAuth(paths.piAuthPath);
429
158
  const models = await readModels(paths.piModelsPath);
430
159
  const settings = await readSettings(paths.piSettingsPath);
160
+ assertPlainObject(auth, "Pi auth");
161
+ assertPlainObject(models, "Pi models");
162
+ assertPlainObject(settings, "Pi settings");
431
163
  return { auth, models, settings };
432
164
  }
433
165
 
434
- function buildPiConfiguration({ auth, models, settings }, {
435
- providerId,
436
- apiKey,
437
- baseUrl,
438
- api,
439
- modelIds,
440
- defaultModel
441
- }) {
442
- const targetProviderId = requirePiProviderId(providerId);
443
- return {
444
- providerId: targetProviderId,
445
- apiKey,
446
- auth: buildPiAuthConfig(auth, targetProviderId, apiKey),
447
- models: buildPiModelsConfig(models, {
448
- providerId: targetProviderId,
449
- baseUrl,
450
- api,
451
- modelIds
452
- }),
453
- settings: buildPiSettingsConfig(settings, targetProviderId, defaultModel)
454
- };
166
+ function skipJsoncTrivia(content, index) {
167
+ while (index < content.length) {
168
+ if (/\s/u.test(content[index])) {
169
+ index += 1;
170
+ } else if (content.startsWith("//", index)) {
171
+ index = content.indexOf("\n", index + 2);
172
+ if (index === -1) return content.length;
173
+ } else if (content.startsWith("/*", index)) {
174
+ const end = content.indexOf("*/", index + 2);
175
+ if (end === -1) throw new SyntaxError("Unterminated JSONC block comment");
176
+ index = end + 2;
177
+ } else {
178
+ break;
179
+ }
180
+ }
181
+ return index;
455
182
  }
456
183
 
457
- async function persistPiConfiguration(paths, configuration, operations = {}) {
458
- const updateAuth = operations.updateAuth ?? updatePiAuthFile;
459
- const writeModels = operations.writeModels ?? ((path, value) => writeText(
460
- path,
461
- `${JSON.stringify(value, null, 2)}\n`,
462
- { sensitive: true }
463
- ));
464
- const writeSettings = operations.writeSettings ?? ((path, value) => writeJson(path, value));
465
- await updateAuth(paths.piAuthPath, configuration.providerId, configuration.apiKey);
466
- await writeModels(paths.piModelsPath, configuration.models);
467
- await writeSettings(paths.piSettingsPath, configuration.settings);
184
+ function parseJsoncStringNode(content, index) {
185
+ const start = index;
186
+ index += 1;
187
+ while (index < content.length) {
188
+ if (content[index] === "\\") index += 2;
189
+ else if (content[index] === '"') {
190
+ index += 1;
191
+ return {
192
+ end: index,
193
+ start,
194
+ type: "string",
195
+ value: JSON.parse(content.slice(start, index))
196
+ };
197
+ } else index += 1;
198
+ }
199
+ throw new SyntaxError("Unterminated JSON string");
468
200
  }
469
201
 
470
- async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = async () => {}, promptApi, runtime = {}) {
471
- const log = runtime.log ?? p.log;
472
- const spinner = runtime.spinner ?? p.spinner;
473
- const text = runtime.text ?? p.text;
474
- const piVersion = await (runtime.getPiVersion ?? detectPiVersion)();
475
- try {
476
- assertSupportedPiVersion(piVersion);
477
- } catch (error) {
478
- log.warn(error.message || String(error));
479
- return;
202
+ function parseJsoncNode(content, startIndex = 0) {
203
+ let index = skipJsoncTrivia(content, startIndex);
204
+ const start = index;
205
+ if (content[index] === '"') return parseJsoncStringNode(content, index);
206
+ if (content[index] === "{") {
207
+ index += 1;
208
+ const properties = [];
209
+ while (true) {
210
+ index = skipJsoncTrivia(content, index);
211
+ if (content[index] === "}") {
212
+ return { end: index + 1, properties, start, type: "object" };
213
+ }
214
+ const key = parseJsoncStringNode(content, index);
215
+ index = skipJsoncTrivia(content, key.end);
216
+ if (content[index] !== ":") throw new SyntaxError("Invalid JSONC object property");
217
+ const value = parseJsoncNode(content, index + 1);
218
+ index = skipJsoncTrivia(content, value.end);
219
+ let comma = false;
220
+ if (content[index] === ",") {
221
+ comma = true;
222
+ index += 1;
223
+ }
224
+ properties.push({ comma, key, value });
225
+ if (!comma) {
226
+ index = skipJsoncTrivia(content, index);
227
+ if (content[index] !== "}") throw new SyntaxError("Invalid JSONC object");
228
+ }
229
+ }
480
230
  }
481
- const {
482
- assertNotCancelled,
483
- passwordOrExisting,
484
- required,
485
- selectOrCancel
486
- } = promptApi;
487
- const {
488
- auth,
489
- models: modelsConfig,
490
- settings
491
- } = await readExistingPiConfiguration(paths);
492
- const detectionSpinner = spinner();
493
- detectionSpinner.start("正在识别 Pi 当前有效模型");
494
- let effectiveModel;
495
- try {
496
- effectiveModel = await (runtime.getPiEffectiveModel ?? detectPiEffectiveModel)();
497
- } finally {
498
- detectionSpinner.stop(effectiveModel
499
- ? `已识别 ${effectiveModel.provider}/${effectiveModel.id}`
500
- : "未识别到 Pi 当前有效模型");
231
+ if (content[index] === "[") {
232
+ index += 1;
233
+ while (true) {
234
+ index = skipJsoncTrivia(content, index);
235
+ if (content[index] === "]") return { end: index + 1, start, type: "array" };
236
+ const value = parseJsoncNode(content, index);
237
+ index = skipJsoncTrivia(content, value.end);
238
+ if (content[index] === ",") index += 1;
239
+ else if (content[index] !== "]") throw new SyntaxError("Invalid JSONC array");
240
+ }
501
241
  }
502
- const target = resolvePiApiTarget(modelsConfig, settings, auth, effectiveModel);
503
- const existing = target.configuration;
504
- const providerId = requirePiProviderId(existing.providerId);
505
- if (target.bootstrap) {
506
- log.info(`Pi 尚未配置可用模型,将创建自定义 Provider ${providerId}。`);
507
- } else {
508
- assertConfigurablePiProvider(modelsConfig, existing);
242
+ while (index < content.length
243
+ && !/[\s,\]}]/u.test(content[index])
244
+ && !content.startsWith("//", index)
245
+ && !content.startsWith("/*", index)) {
246
+ index += 1;
509
247
  }
510
- if (effectiveModel
511
- && (settings.defaultProvider !== existing.providerId || settings.defaultModel !== existing.defaultModel)) {
512
- log.warn(`Pi 保存的默认模型 ${settings.defaultProvider || "未知"}/${settings.defaultModel || "未知"} 与当前有效模型 ${existing.providerId}/${existing.defaultModel} 不同;将配置当前有效模型。`);
248
+ if (index === start) throw new SyntaxError("Invalid JSONC value");
249
+ return { end: index, start, type: "scalar" };
250
+ }
251
+
252
+ function lineIndentAt(content, index) {
253
+ const lineStart = content.lastIndexOf("\n", index - 1) + 1;
254
+ return content.slice(lineStart, index).match(/^[ \t]*/u)?.[0] ?? "";
255
+ }
256
+
257
+ function formatJsoncValue(value, indent, lineEnding) {
258
+ return JSON.stringify(value, null, 2).replace(/\n/gu, `${lineEnding}${indent}`);
259
+ }
260
+
261
+ function findJsoncProperty(objectNode, key) {
262
+ return objectNode.properties.find((property) => property.key.value === key);
263
+ }
264
+
265
+ function findUniqueJsoncProperty(objectNode, key, label) {
266
+ const matches = objectNode.properties.filter((property) => property.key.value === key);
267
+ if (matches.length > 1) throw new SyntaxError(`Duplicate Pi ${label} key`);
268
+ return matches[0];
269
+ }
270
+
271
+ function setJsoncObjectProperty(content, objectNode, key, value) {
272
+ const existing = findJsoncProperty(objectNode, key);
273
+ const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
274
+ if (existing) {
275
+ const indent = lineIndentAt(content, existing.key.start);
276
+ return `${content.slice(0, existing.value.start)}${formatJsoncValue(value, indent, lineEnding)}${content.slice(existing.value.end)}`;
513
277
  }
514
- const providerLabel = `Pi ${providerId}`;
515
278
 
516
- const baseUrlInput = await text({
517
- message: `${providerLabel} Base URL`,
518
- initialValue: existing.baseUrl,
519
- validate: required()
520
- });
521
- assertNotCancelled(baseUrlInput);
522
- const inferredApi = inferPiApiFromBaseUrl(baseUrlInput);
523
- const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
279
+ const close = objectNode.end - 1;
280
+ let insertionPoint = close;
281
+ while (insertionPoint > objectNode.start && /[ \t]/u.test(content[insertionPoint - 1])) insertionPoint -= 1;
282
+ const objectIndent = lineIndentAt(content, objectNode.start);
283
+ const childIndent = objectNode.properties.length
284
+ ? lineIndentAt(content, objectNode.properties[0].key.start)
285
+ : `${objectIndent} `;
286
+ const last = objectNode.properties.at(-1);
287
+ const needsComma = last && !last.comma;
288
+ const gapStart = last?.value.end ?? objectNode.start + 1;
289
+ const gap = content.slice(gapStart, insertionPoint);
290
+ const needsLineBreak = !gap.includes("\n") && !gap.includes("\r");
291
+ const property = `${childIndent}${JSON.stringify(key)}: ${formatJsoncValue(value, childIndent, lineEnding)}${lineEnding}`;
292
+ return `${content.slice(0, gapStart)}${needsComma ? "," : ""}${gap}${needsLineBreak ? lineEnding : ""}${property}${content.slice(insertionPoint)}`;
293
+ }
524
294
 
525
- const piApiOptions = getPiApiPromptOptions();
526
- const initialApi = piApiOptions.some((option) => option.value === existing.api)
527
- ? existing.api
528
- : undefined;
529
- const api = inferredApi || await selectOrCancel({
530
- message: `${providerLabel} API 类型`,
531
- options: piApiOptions,
532
- ...(initialApi ? { initialValue: initialApi } : {})
533
- });
295
+ function updatePiModelsContent(content, provider) {
296
+ const root = parseJsoncNode(content);
297
+ if (root.type !== "object") throw new TypeError("Pi models must contain a JSON object");
298
+ const providersProperty = findUniqueJsoncProperty(root, "providers", "providers");
299
+ if (!providersProperty) {
300
+ return setJsoncObjectProperty(content, root, "providers", { [piProviderId]: provider });
301
+ }
302
+ if (providersProperty.value.type !== "object") {
303
+ throw new TypeError("Pi models providers must contain a JSON object");
304
+ }
305
+ findUniqueJsoncProperty(providersProperty.value, piProviderId, `providers.${piProviderId}`);
306
+ return setJsoncObjectProperty(content, providersProperty.value, piProviderId, provider);
307
+ }
534
308
 
535
- const finalApiKey = await passwordOrExisting({
536
- message: `${providerLabel} API Key`,
537
- existingValue: existing.apiKey
309
+ async function persistPiConfiguration(paths, intent, operations = {}) {
310
+ return withFileTransaction([
311
+ { path: paths.piAuthPath, safeRoot: paths.homeDir, sensitive: true },
312
+ { path: paths.piModelsPath, safeRoot: paths.homeDir, sensitive: true },
313
+ { path: paths.piSettingsPath, safeRoot: paths.homeDir }
314
+ ], async (transaction) => {
315
+ const auth = transaction.readJson(paths.piAuthPath, {});
316
+ const models = transaction.readJson(paths.piModelsPath, {}, { jsonc: true });
317
+ const settings = transaction.readJson(paths.piSettingsPath, {});
318
+ assertPlainObject(auth, "Pi auth");
319
+ assertPlainObject(models, "Pi models");
320
+ assertPlainObject(settings, "Pi settings");
321
+ const nextModels = buildPiModelsConfig(models, intent);
322
+ const currentModelsContent = transaction.readText(paths.piModelsPath, "{}\n");
323
+ const nextAuth = buildPiAuthConfig(auth, intent.apiKey);
324
+ const nextSettings = buildPiSettingsConfig(settings, intent.defaultModel);
325
+ const nextModelsContent = updatePiModelsContent(
326
+ currentModelsContent,
327
+ nextModels.providers[piProviderId]
328
+ );
329
+ const validatedModels = JSON.parse(stripJsonComments(nextModelsContent));
330
+ assertPlainObject(validatedModels, "Pi models");
331
+
332
+ await transaction.writeJson(paths.piAuthPath, nextAuth, {
333
+ ...(operations.writeAuth ? { writer: operations.writeAuth } : {})
334
+ });
335
+ await transaction.writeText(
336
+ paths.piModelsPath,
337
+ nextModelsContent,
338
+ { ...(operations.writeModels ? { writer: operations.writeModels } : {}) }
339
+ );
340
+ await transaction.writeJson(paths.piSettingsPath, nextSettings, {
341
+ ...(operations.writeSettings ? { writer: operations.writeSettings } : {})
342
+ });
538
343
  });
344
+ }
539
345
 
540
- const modelIdsText = await text({
541
- message: `${providerLabel} 模型 ID(多个用逗号分隔)`,
542
- initialValue: (existing.modelIds.length
543
- ? existing.modelIds
544
- : existing.defaultModel ? [existing.defaultModel] : []).join(","),
545
- validate: (value) => parsePiModelIds(value).length ? undefined : "至少需要一个模型 ID"
546
- });
547
- assertNotCancelled(modelIdsText);
548
- const modelIds = parsePiModelIds(modelIdsText);
346
+ async function configurePiApi(paths, withExtensionsPresent, promptApi, runtime = {}) {
347
+ if (typeof withExtensionsPresent !== "function") {
348
+ throw new TypeError("Expected a Pi Extensions deployment function");
349
+ }
350
+ return withExtensionsPresent(paths, async (guard) => {
351
+ await guard();
352
+ const text = runtime.text ?? p.text;
353
+ const {
354
+ assertNotCancelled,
355
+ passwordOrExisting,
356
+ selectOrCancel
357
+ } = promptApi;
358
+ const { auth, models, settings } = await readExistingPiConfiguration(paths);
359
+ const existing = resolveExistingPiApiConfig(models, settings, auth);
360
+ const providerLabel = `Pi ${piProviderId}`;
361
+
362
+ const baseUrlInput = await text({
363
+ message: `${providerLabel} Base URL`,
364
+ initialValue: existing.baseUrl || undefined,
365
+ validate: (value) => {
366
+ try {
367
+ normalizeOpenAiBaseUrl(value);
368
+ } catch (error) {
369
+ return error.message;
370
+ }
371
+ }
372
+ });
373
+ assertNotCancelled(baseUrlInput);
374
+ const inferredApi = inferPiApiFromBaseUrl(baseUrlInput);
375
+ const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
376
+
377
+ const apiOptions = getPiApiPromptOptions();
378
+ const initialApi = apiOptions.some((option) => option.value === existing.api)
379
+ ? existing.api
380
+ : undefined;
381
+ const api = inferredApi || await selectOrCancel({
382
+ message: `${providerLabel} API 类型`,
383
+ options: apiOptions,
384
+ ...(initialApi ? { initialValue: initialApi } : {})
385
+ });
386
+ const apiKey = await passwordOrExisting({
387
+ message: `${providerLabel} API Key`,
388
+ existingValue: existing.apiKey
389
+ });
549
390
 
550
- const defaultModel = await text({
551
- message: "Pi 默认模型",
552
- initialValue: modelIds.includes(existing.defaultModel) ? existing.defaultModel : modelIds[0],
553
- validate: (value) => modelIds.includes(String(value || "").trim()) ? undefined : "默认模型必须在模型 ID 列表中"
554
- });
555
- assertNotCancelled(defaultModel);
391
+ const modelIdsText = await text({
392
+ message: `${providerLabel} 模型 ID(多个用逗号分隔)`,
393
+ initialValue: existing.modelIds.join(",") || undefined,
394
+ validate: (value) => parsePiModelIds(value).length ? undefined : "至少需要一个模型 ID"
395
+ });
396
+ assertNotCancelled(modelIdsText);
397
+ const modelIds = parsePiModelIds(modelIdsText);
398
+ const defaultModel = await text({
399
+ message: "Pi 默认模型",
400
+ initialValue: modelIds.includes(existing.defaultModel) ? existing.defaultModel : modelIds[0],
401
+ validate: (value) => modelIds.includes(String(value || "").trim())
402
+ ? undefined
403
+ : "默认模型必须在模型 ID 列表中"
404
+ });
405
+ assertNotCancelled(defaultModel);
556
406
 
557
- const finalDefaultModel = String(defaultModel).trim();
558
- const configuration = buildPiConfiguration({ auth, models: modelsConfig, settings }, {
559
- providerId,
560
- apiKey: finalApiKey,
561
- baseUrl,
562
- api,
563
- modelIds,
564
- defaultModel: finalDefaultModel
407
+ await guard();
408
+ await persistPiConfiguration(paths, {
409
+ apiKey,
410
+ baseUrl,
411
+ api,
412
+ modelIds,
413
+ defaultModel: String(defaultModel).trim()
414
+ });
565
415
  });
566
- await ensurePiResourcesLinked(paths);
567
- await persistPiConfiguration(paths, configuration);
568
-
569
- log.step(`已更新 ${pathToLabel(paths.piModelsPath, paths.homeDir)} (${providerId}, ${baseUrl})`);
570
- log.step(`已更新 ${pathToLabel(paths.piSettingsPath, paths.homeDir)} (默认模型: ${finalDefaultModel})`);
571
- log.step(`已更新 ${pathToLabel(paths.piAuthPath, paths.homeDir)} (${maskSecret(finalApiKey)})`);
572
- log.step(`已链接 Pi 扩展到 ${pathToLabel(join(paths.piAgentDir, "extensions"), paths.homeDir)}`);
573
416
  }
574
417
 
575
418
  export {
576
- assertConfigurablePiProvider,
577
- assertSupportedPiVersion,
578
419
  buildPiAuthConfig,
579
- buildPiConfiguration,
580
420
  buildPiModelsConfig,
581
421
  buildPiSettingsConfig,
582
422
  configurePiApi,
583
- detectPiEffectiveModel,
584
423
  getPiApiPromptOptions,
585
- getPiProcessInvocation,
586
424
  inferPiApiFromBaseUrl,
587
425
  normalizeOpenAiBaseUrl,
588
- parsePiRpcEffectiveModel,
589
426
  parsePiModelIds,
590
427
  persistPiConfiguration,
591
428
  readExistingPiConfiguration,
592
- resolveExistingPiApiConfig,
593
- resolvePiApiTarget,
594
- runPiRpcCommand,
595
- updatePiAuthFile
429
+ resolveExistingPiApiConfig
596
430
  };