abelworkflow 1.1.3 → 1.2.1

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 -496
  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,393 @@ 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: "low",
101
+ low: null,
102
+ medium: null,
103
+ high: null,
104
+ xhigh: "xhigh",
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),
140
+ defaultProvider: piProviderId,
418
141
  defaultModel,
419
- defaultThinkingLevel: settings.defaultThinkingLevel || "high",
420
- enableSkillCommands: settings.enableSkillCommands ?? true
142
+ defaultThinkingLevel: "xhigh"
421
143
  };
422
144
  }
423
145
 
424
146
  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, {}));
147
+ const readAuth = operations.readAuth ?? ((path) => readJsonFileSafe(path, {}, {
148
+ safeRoot: paths.homeDir,
149
+ sensitive: true
150
+ }));
151
+ const readModels = operations.readModels ?? ((path) => readJsoncFileSafe(path, {}, {
152
+ safeRoot: paths.homeDir,
153
+ sensitive: true
154
+ }));
155
+ const readSettings = operations.readSettings ?? ((path) => readJsonFileSafe(path, {}, {
156
+ safeRoot: paths.homeDir
157
+ }));
428
158
  const auth = await readAuth(paths.piAuthPath);
429
159
  const models = await readModels(paths.piModelsPath);
430
160
  const settings = await readSettings(paths.piSettingsPath);
161
+ assertPlainObject(auth, "Pi auth");
162
+ assertPlainObject(models, "Pi models");
163
+ assertPlainObject(settings, "Pi settings");
431
164
  return { auth, models, settings };
432
165
  }
433
166
 
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
- };
167
+ function skipJsoncTrivia(content, index) {
168
+ while (index < content.length) {
169
+ if (/\s/u.test(content[index])) {
170
+ index += 1;
171
+ } else if (content.startsWith("//", index)) {
172
+ index = content.indexOf("\n", index + 2);
173
+ if (index === -1) return content.length;
174
+ } else if (content.startsWith("/*", index)) {
175
+ const end = content.indexOf("*/", index + 2);
176
+ if (end === -1) throw new SyntaxError("Unterminated JSONC block comment");
177
+ index = end + 2;
178
+ } else {
179
+ break;
180
+ }
181
+ }
182
+ return index;
455
183
  }
456
184
 
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);
185
+ function parseJsoncStringNode(content, index) {
186
+ const start = index;
187
+ index += 1;
188
+ while (index < content.length) {
189
+ if (content[index] === "\\") index += 2;
190
+ else if (content[index] === '"') {
191
+ index += 1;
192
+ return {
193
+ end: index,
194
+ start,
195
+ type: "string",
196
+ value: JSON.parse(content.slice(start, index))
197
+ };
198
+ } else index += 1;
199
+ }
200
+ throw new SyntaxError("Unterminated JSON string");
468
201
  }
469
202
 
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;
203
+ function parseJsoncNode(content, startIndex = 0) {
204
+ let index = skipJsoncTrivia(content, startIndex);
205
+ const start = index;
206
+ if (content[index] === '"') return parseJsoncStringNode(content, index);
207
+ if (content[index] === "{") {
208
+ index += 1;
209
+ const properties = [];
210
+ while (true) {
211
+ index = skipJsoncTrivia(content, index);
212
+ if (content[index] === "}") {
213
+ return { end: index + 1, properties, start, type: "object" };
214
+ }
215
+ const key = parseJsoncStringNode(content, index);
216
+ index = skipJsoncTrivia(content, key.end);
217
+ if (content[index] !== ":") throw new SyntaxError("Invalid JSONC object property");
218
+ const value = parseJsoncNode(content, index + 1);
219
+ index = skipJsoncTrivia(content, value.end);
220
+ let comma = false;
221
+ if (content[index] === ",") {
222
+ comma = true;
223
+ index += 1;
224
+ }
225
+ properties.push({ comma, key, value });
226
+ if (!comma) {
227
+ index = skipJsoncTrivia(content, index);
228
+ if (content[index] !== "}") throw new SyntaxError("Invalid JSONC object");
229
+ }
230
+ }
480
231
  }
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 当前有效模型");
232
+ if (content[index] === "[") {
233
+ index += 1;
234
+ while (true) {
235
+ index = skipJsoncTrivia(content, index);
236
+ if (content[index] === "]") return { end: index + 1, start, type: "array" };
237
+ const value = parseJsoncNode(content, index);
238
+ index = skipJsoncTrivia(content, value.end);
239
+ if (content[index] === ",") index += 1;
240
+ else if (content[index] !== "]") throw new SyntaxError("Invalid JSONC array");
241
+ }
501
242
  }
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);
243
+ while (index < content.length
244
+ && !/[\s,\]}]/u.test(content[index])
245
+ && !content.startsWith("//", index)
246
+ && !content.startsWith("/*", index)) {
247
+ index += 1;
509
248
  }
510
- if (effectiveModel
511
- && (settings.defaultProvider !== existing.providerId || settings.defaultModel !== existing.defaultModel)) {
512
- log.warn(`Pi 保存的默认模型 ${settings.defaultProvider || "未知"}/${settings.defaultModel || "未知"} 与当前有效模型 ${existing.providerId}/${existing.defaultModel} 不同;将配置当前有效模型。`);
249
+ if (index === start) throw new SyntaxError("Invalid JSONC value");
250
+ return { end: index, start, type: "scalar" };
251
+ }
252
+
253
+ function lineIndentAt(content, index) {
254
+ const lineStart = content.lastIndexOf("\n", index - 1) + 1;
255
+ return content.slice(lineStart, index).match(/^[ \t]*/u)?.[0] ?? "";
256
+ }
257
+
258
+ function formatJsoncValue(value, indent, lineEnding) {
259
+ return JSON.stringify(value, null, 2).replace(/\n/gu, `${lineEnding}${indent}`);
260
+ }
261
+
262
+ function findJsoncProperty(objectNode, key) {
263
+ return objectNode.properties.find((property) => property.key.value === key);
264
+ }
265
+
266
+ function findUniqueJsoncProperty(objectNode, key, label) {
267
+ const matches = objectNode.properties.filter((property) => property.key.value === key);
268
+ if (matches.length > 1) throw new SyntaxError(`Duplicate Pi ${label} key`);
269
+ return matches[0];
270
+ }
271
+
272
+ function setJsoncObjectProperty(content, objectNode, key, value) {
273
+ const existing = findJsoncProperty(objectNode, key);
274
+ const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
275
+ if (existing) {
276
+ const indent = lineIndentAt(content, existing.key.start);
277
+ return `${content.slice(0, existing.value.start)}${formatJsoncValue(value, indent, lineEnding)}${content.slice(existing.value.end)}`;
513
278
  }
514
- const providerLabel = `Pi ${providerId}`;
515
279
 
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);
280
+ const close = objectNode.end - 1;
281
+ let insertionPoint = close;
282
+ while (insertionPoint > objectNode.start && /[ \t]/u.test(content[insertionPoint - 1])) insertionPoint -= 1;
283
+ const objectIndent = lineIndentAt(content, objectNode.start);
284
+ const childIndent = objectNode.properties.length
285
+ ? lineIndentAt(content, objectNode.properties[0].key.start)
286
+ : `${objectIndent} `;
287
+ const last = objectNode.properties.at(-1);
288
+ const needsComma = last && !last.comma;
289
+ const gapStart = last?.value.end ?? objectNode.start + 1;
290
+ const gap = content.slice(gapStart, insertionPoint);
291
+ const needsLineBreak = !gap.includes("\n") && !gap.includes("\r");
292
+ const property = `${childIndent}${JSON.stringify(key)}: ${formatJsoncValue(value, childIndent, lineEnding)}${lineEnding}`;
293
+ return `${content.slice(0, gapStart)}${needsComma ? "," : ""}${gap}${needsLineBreak ? lineEnding : ""}${property}${content.slice(insertionPoint)}`;
294
+ }
524
295
 
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
- });
296
+ function updatePiModelsContent(content, provider) {
297
+ const root = parseJsoncNode(content);
298
+ if (root.type !== "object") throw new TypeError("Pi models must contain a JSON object");
299
+ const providersProperty = findUniqueJsoncProperty(root, "providers", "providers");
300
+ if (!providersProperty) {
301
+ return setJsoncObjectProperty(content, root, "providers", { [piProviderId]: provider });
302
+ }
303
+ if (providersProperty.value.type !== "object") {
304
+ throw new TypeError("Pi models providers must contain a JSON object");
305
+ }
306
+ findUniqueJsoncProperty(providersProperty.value, piProviderId, `providers.${piProviderId}`);
307
+ return setJsoncObjectProperty(content, providersProperty.value, piProviderId, provider);
308
+ }
534
309
 
535
- const finalApiKey = await passwordOrExisting({
536
- message: `${providerLabel} API Key`,
537
- existingValue: existing.apiKey
310
+ async function persistPiConfiguration(paths, intent, operations = {}) {
311
+ return withFileTransaction([
312
+ { path: paths.piAuthPath, safeRoot: paths.homeDir, sensitive: true },
313
+ { path: paths.piModelsPath, safeRoot: paths.homeDir, sensitive: true },
314
+ { path: paths.piSettingsPath, safeRoot: paths.homeDir }
315
+ ], async (transaction) => {
316
+ const auth = transaction.readJson(paths.piAuthPath, {});
317
+ const models = transaction.readJson(paths.piModelsPath, {}, { jsonc: true });
318
+ const settings = transaction.readJson(paths.piSettingsPath, {});
319
+ assertPlainObject(auth, "Pi auth");
320
+ assertPlainObject(models, "Pi models");
321
+ assertPlainObject(settings, "Pi settings");
322
+ const nextModels = buildPiModelsConfig(models, intent);
323
+ const currentModelsContent = transaction.readText(paths.piModelsPath, "{}\n");
324
+ const nextAuth = buildPiAuthConfig(auth, intent.apiKey);
325
+ const nextSettings = buildPiSettingsConfig(settings, intent.defaultModel);
326
+ const nextModelsContent = updatePiModelsContent(
327
+ currentModelsContent,
328
+ nextModels.providers[piProviderId]
329
+ );
330
+ const validatedModels = JSON.parse(stripJsonComments(nextModelsContent));
331
+ assertPlainObject(validatedModels, "Pi models");
332
+
333
+ await transaction.writeJson(paths.piAuthPath, nextAuth, {
334
+ ...(operations.writeAuth ? { writer: operations.writeAuth } : {})
335
+ });
336
+ await transaction.writeText(
337
+ paths.piModelsPath,
338
+ nextModelsContent,
339
+ { ...(operations.writeModels ? { writer: operations.writeModels } : {}) }
340
+ );
341
+ await transaction.writeJson(paths.piSettingsPath, nextSettings, {
342
+ ...(operations.writeSettings ? { writer: operations.writeSettings } : {})
343
+ });
538
344
  });
345
+ }
539
346
 
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);
347
+ async function configurePiApi(paths, withExtensionsPresent, promptApi, runtime = {}) {
348
+ if (typeof withExtensionsPresent !== "function") {
349
+ throw new TypeError("Expected a Pi Extensions deployment function");
350
+ }
351
+ return withExtensionsPresent(paths, async (guard) => {
352
+ await guard();
353
+ const text = runtime.text ?? p.text;
354
+ const {
355
+ assertNotCancelled,
356
+ passwordOrExisting,
357
+ selectOrCancel
358
+ } = promptApi;
359
+ const { auth, models, settings } = await readExistingPiConfiguration(paths);
360
+ const existing = resolveExistingPiApiConfig(models, settings, auth);
361
+ const providerLabel = `Pi ${piProviderId}`;
362
+
363
+ const baseUrlInput = await text({
364
+ message: `${providerLabel} Base URL`,
365
+ initialValue: existing.baseUrl || undefined,
366
+ validate: (value) => {
367
+ try {
368
+ normalizeOpenAiBaseUrl(value);
369
+ } catch (error) {
370
+ return error.message;
371
+ }
372
+ }
373
+ });
374
+ assertNotCancelled(baseUrlInput);
375
+ const inferredApi = inferPiApiFromBaseUrl(baseUrlInput);
376
+ const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
377
+
378
+ const apiOptions = getPiApiPromptOptions();
379
+ const initialApi = apiOptions.some((option) => option.value === existing.api)
380
+ ? existing.api
381
+ : undefined;
382
+ const api = inferredApi || await selectOrCancel({
383
+ message: `${providerLabel} API 类型`,
384
+ options: apiOptions,
385
+ ...(initialApi ? { initialValue: initialApi } : {})
386
+ });
387
+ const apiKey = await passwordOrExisting({
388
+ message: `${providerLabel} API Key`,
389
+ existingValue: existing.apiKey
390
+ });
549
391
 
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);
392
+ const modelIdsText = await text({
393
+ message: `${providerLabel} 模型 ID(多个用逗号分隔)`,
394
+ initialValue: existing.modelIds.join(",") || undefined,
395
+ validate: (value) => parsePiModelIds(value).length ? undefined : "至少需要一个模型 ID"
396
+ });
397
+ assertNotCancelled(modelIdsText);
398
+ const modelIds = parsePiModelIds(modelIdsText);
399
+ const defaultModel = await text({
400
+ message: "Pi 默认模型",
401
+ initialValue: modelIds.includes(existing.defaultModel) ? existing.defaultModel : modelIds[0],
402
+ validate: (value) => modelIds.includes(String(value || "").trim())
403
+ ? undefined
404
+ : "默认模型必须在模型 ID 列表中"
405
+ });
406
+ assertNotCancelled(defaultModel);
556
407
 
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
408
+ await guard();
409
+ await persistPiConfiguration(paths, {
410
+ apiKey,
411
+ baseUrl,
412
+ api,
413
+ modelIds,
414
+ defaultModel: String(defaultModel).trim()
415
+ });
565
416
  });
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
417
  }
574
418
 
575
419
  export {
576
- assertConfigurablePiProvider,
577
- assertSupportedPiVersion,
578
420
  buildPiAuthConfig,
579
- buildPiConfiguration,
580
421
  buildPiModelsConfig,
581
422
  buildPiSettingsConfig,
582
423
  configurePiApi,
583
- detectPiEffectiveModel,
584
424
  getPiApiPromptOptions,
585
- getPiProcessInvocation,
586
425
  inferPiApiFromBaseUrl,
587
426
  normalizeOpenAiBaseUrl,
588
- parsePiRpcEffectiveModel,
589
427
  parsePiModelIds,
590
428
  persistPiConfiguration,
591
429
  readExistingPiConfiguration,
592
- resolveExistingPiApiConfig,
593
- resolvePiApiTarget,
594
- runPiRpcCommand,
595
- updatePiAuthFile
430
+ resolveExistingPiApiConfig
596
431
  };