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