abelworkflow 1.2.4 → 1.3.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 (63) hide show
  1. package/README.md +26 -25
  2. package/extensions/gpt-responses-compat.ts +1 -1
  3. package/lib/cli/main.mjs +18 -86
  4. package/lib/cli/pi.mjs +43 -0
  5. package/lib/cli/prompts.mjs +3 -3
  6. package/lib/installer/assets.mjs +35 -31
  7. package/lib/installer/install.mjs +29 -9
  8. package/lib/installer/links.mjs +97 -84
  9. package/lib/paths.mjs +0 -2
  10. package/lib/providers/claude.mjs +14 -3
  11. package/lib/providers/codex.mjs +14 -9
  12. package/lib/providers/pi.mjs +61 -68
  13. package/lib/providers/skills.mjs +22 -22
  14. package/lib/providers/url.mjs +32 -1
  15. package/lib/templates/codex/agents/default.toml +16 -54
  16. package/lib/templates/codex/agents/explorer.toml +25 -52
  17. package/lib/templates/codex/agents/planner.toml +23 -77
  18. package/lib/templates/codex/agents/reviewer.toml +16 -62
  19. package/lib/templates/codex/agents/worker.toml +30 -64
  20. package/lib/templates/codex/config-base.toml +0 -6
  21. package/lib/templates/{workflow/gitignore.template → gitignore.template} +0 -3
  22. package/lib/tools/cli-installer.mjs +40 -12
  23. package/package.json +13 -18
  24. package/lib/templates/workflow/AGENTS.md +0 -50
  25. package/lib/templates/workflow/commands/abel-design.md +0 -175
  26. package/lib/templates/workflow/commands/abel-diagnose.md +0 -63
  27. package/lib/templates/workflow/commands/abel-implement.md +0 -170
  28. package/lib/templates/workflow/commands/abel-init.md +0 -25
  29. package/skills/dev-browser/SKILL.md +0 -281
  30. package/skills/dev-browser/dist/scripts/start.d.ts +0 -1
  31. package/skills/dev-browser/dist/scripts/start.js +0 -90
  32. package/skills/dev-browser/dist/src/client.d.ts +0 -92
  33. package/skills/dev-browser/dist/src/client.js +0 -310
  34. package/skills/dev-browser/dist/src/entrypoint.d.ts +0 -29
  35. package/skills/dev-browser/dist/src/entrypoint.js +0 -113
  36. package/skills/dev-browser/dist/src/index.d.ts +0 -3
  37. package/skills/dev-browser/dist/src/index.js +0 -1
  38. package/skills/dev-browser/dist/src/page-api.d.ts +0 -24
  39. package/skills/dev-browser/dist/src/page-api.js +0 -103
  40. package/skills/dev-browser/dist/src/relay.d.ts +0 -26
  41. package/skills/dev-browser/dist/src/relay.js +0 -567
  42. package/skills/dev-browser/dist/src/runtime.d.ts +0 -34
  43. package/skills/dev-browser/dist/src/runtime.js +0 -44
  44. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +0 -22
  45. package/skills/dev-browser/dist/src/snapshot/browser-script.js +0 -868
  46. package/skills/dev-browser/dist/src/snapshot/index.d.ts +0 -13
  47. package/skills/dev-browser/dist/src/snapshot/index.js +0 -13
  48. package/skills/dev-browser/dist/src/snapshot/inject.d.ts +0 -12
  49. package/skills/dev-browser/dist/src/snapshot/inject.js +0 -12
  50. package/skills/dev-browser/dist/src/standalone.d.ts +0 -31
  51. package/skills/dev-browser/dist/src/standalone.js +0 -173
  52. package/skills/dev-browser/dist/src/startup.d.ts +0 -46
  53. package/skills/dev-browser/dist/src/startup.js +0 -77
  54. package/skills/dev-browser/dist/src/target-registry.d.ts +0 -28
  55. package/skills/dev-browser/dist/src/target-registry.js +0 -134
  56. package/skills/dev-browser/dist/src/types.d.ts +0 -26
  57. package/skills/dev-browser/dist/src/types.js +0 -1
  58. package/skills/dev-browser/package-lock.json +0 -1545
  59. package/skills/dev-browser/package.json +0 -35
  60. package/skills/dev-browser/references/scraping.md +0 -144
  61. package/skills/git-commit/SKILL.md +0 -124
  62. package/skills/time/SKILL.md +0 -119
  63. package/skills/time/scripts/time_cli.py +0 -143
@@ -228,29 +228,6 @@ async function ensureProviderRootDirectory(homeDir, segments, options = {}) {
228
228
  return directory;
229
229
  }
230
230
 
231
- async function getDirectoryNames(root) {
232
- if (!(await pathIsDirectory(root))) return [];
233
- const entries = await fs.readdir(root, { withFileTypes: true });
234
- const names = await Promise.all(entries.map(async (entry) => {
235
- if (entry.isDirectory()) return entry.name;
236
- if (entry.isSymbolicLink() && await pathIsDirectory(join(root, entry.name))) return entry.name;
237
- return null;
238
- }));
239
- return names.filter(Boolean);
240
- }
241
-
242
- async function getCommandNames(commandsDir) {
243
- if (!(await pathIsDirectory(commandsDir))) return [];
244
- const entries = await fs.readdir(commandsDir, { withFileTypes: true });
245
- const names = await Promise.all(entries.map(async (entry) => {
246
- if (!entry.name.endsWith(".md")) return null;
247
- if (entry.isFile()) return entry.name;
248
- if (entry.isSymbolicLink() && await pathIsFile(join(commandsDir, entry.name))) return entry.name;
249
- return null;
250
- }));
251
- return names.filter(Boolean);
252
- }
253
-
254
231
  async function getPiExtensionNames(extensionsDir) {
255
232
  if (!(await pathIsDirectory(extensionsDir))) return [];
256
233
  const entries = await fs.readdir(extensionsDir, { withFileTypes: true });
@@ -289,10 +266,48 @@ async function pruneManagedTargets(targetDir, managedSourceRoot, expectedNames,
289
266
  return results;
290
267
  }
291
268
 
292
- async function linkSkillDirectories(baseDir, paths, previousLinkedTargets, options) {
269
+ const cadenceOwnedSkills = new Set(["grok-search", "context7-auto-research"]);
270
+
271
+ function shouldLinkSkillToPi(skillName) {
272
+ return skillName !== ".system"
273
+ && !skillName.startsWith("abel-")
274
+ && !cadenceOwnedSkills.has(skillName);
275
+ }
276
+
277
+ function managedSkillNames(managedFiles = {}) {
278
+ const names = new Set();
279
+ for (const relativePath of Object.keys(managedFiles)) {
280
+ if (!relativePath.startsWith("skills/")) continue;
281
+ const skillName = relativePath.slice("skills/".length).split("/")[0];
282
+ if (shouldLinkSkillToPi(skillName)) names.add(skillName);
283
+ }
284
+ return names;
285
+ }
286
+
287
+ function selectedLinkedSkillNames({
288
+ previousLinkedTargets = {},
289
+ skillsRoot,
290
+ managedFiles,
291
+ extraName = null
292
+ } = {}) {
293
+ const names = managedFiles ? managedSkillNames(managedFiles) : new Set();
294
+ if (!managedFiles && skillsRoot) {
295
+ const resolvedSkillsRoot = resolve(skillsRoot);
296
+ for (const previousState of Object.values(previousLinkedTargets)) {
297
+ if (!previousState || previousState.kind !== "dir") continue;
298
+ const sourcePath = resolve(previousState.sourcePath);
299
+ if (dirname(sourcePath) !== resolvedSkillsRoot) continue;
300
+ const skillName = basename(sourcePath);
301
+ if (shouldLinkSkillToPi(skillName)) names.add(skillName);
302
+ }
303
+ }
304
+ if (extraName && shouldLinkSkillToPi(extraName)) names.add(extraName);
305
+ return [...names];
306
+ }
307
+
308
+ async function linkSkillDirectories(baseDir, paths, previousLinkedTargets, options, skillNames) {
293
309
  const results = [];
294
310
  const skillsRoot = join(paths.agentsDir, "skills");
295
- const skillNames = (await getDirectoryNames(skillsRoot)).filter((skillName) => skillName !== ".system");
296
311
  results.push(...await pruneManagedTargets(
297
312
  join(baseDir, "skills"),
298
313
  skillsRoot,
@@ -312,57 +327,54 @@ async function linkSkillDirectories(baseDir, paths, previousLinkedTargets, optio
312
327
  return results;
313
328
  }
314
329
 
315
- async function linkCommands(targetDir, paths, previousLinkedTargets, options) {
316
- const commandsRoot = join(paths.agentsDir, "commands");
317
- const commandFiles = await getCommandNames(commandsRoot);
318
- const results = await pruneManagedTargets(targetDir, commandsRoot, commandFiles, previousLinkedTargets, options);
319
- for (const fileName of commandFiles) {
320
- recordLinkResult(results, options, await ensureManagedLink(
321
- join(targetDir, fileName),
322
- join(commandsRoot, fileName),
323
- "file",
330
+ async function linkPiSkill(paths, skillName, previousLinkedTargets = {}, options = {}) {
331
+ if (!shouldLinkSkillToPi(skillName)) return [];
332
+ const piAgentDir = await ensureProviderRootDirectory(paths.homeDir, [".pi", "agent"], options);
333
+ await ensureManagedContainerDirectories([join(piAgentDir, "skills")], options);
334
+ const skillsRoot = join(paths.agentsDir, "skills");
335
+ return linkSkillDirectories(
336
+ piAgentDir,
337
+ paths,
338
+ previousLinkedTargets,
339
+ options,
340
+ selectedLinkedSkillNames({
324
341
  previousLinkedTargets,
325
- options
326
- ));
327
- }
328
- return results;
342
+ skillsRoot,
343
+ managedFiles: options.managedFiles,
344
+ extraName: skillName
345
+ })
346
+ );
329
347
  }
330
348
 
331
- async function linkClaude(paths, previousLinkedTargets = {}, options = {}) {
332
- const claudeDir = await ensureProviderRootDirectory(paths.homeDir, [".claude"], options);
333
- await ensureManagedContainerDirectories([
334
- join(claudeDir, "commands"),
335
- join(claudeDir, "skills")
336
- ], options);
337
- const results = [];
338
- recordLinkResult(results, options, await ensureManagedLink(
339
- join(claudeDir, "CLAUDE.md"),
340
- join(paths.agentsDir, "AGENTS.md"),
341
- "file",
342
- previousLinkedTargets,
343
- options
344
- ));
345
- results.push(...await linkCommands(join(claudeDir, "commands"), paths, previousLinkedTargets, options));
346
- results.push(...await linkSkillDirectories(claudeDir, paths, previousLinkedTargets, options));
347
- return results;
349
+ async function pruneProviderInstructions(baseDir, commandsName, paths, previousLinkedTargets, options) {
350
+ const cleanupOptions = {
351
+ ...options,
352
+ validateTargetDir: (directory) => ensureManagedContainerDirectories([directory], options)
353
+ };
354
+ return [
355
+ ...await pruneManagedTargets(baseDir, paths.agentsDir, [], previousLinkedTargets, cleanupOptions),
356
+ ...await pruneManagedTargets(join(baseDir, commandsName), join(paths.agentsDir, "commands"), [], previousLinkedTargets, cleanupOptions)
357
+ ];
348
358
  }
349
359
 
350
- async function linkCodex(paths, previousLinkedTargets = {}, options = {}) {
351
- const codexDir = await ensureProviderRootDirectory(paths.homeDir, [".codex"], options);
352
- await ensureManagedContainerDirectories([
353
- join(codexDir, "skills"),
354
- join(codexDir, "prompts")
355
- ], options);
360
+ async function pruneRetiredProviderLinks(paths, previousLinkedTargets = {}, options = {}) {
356
361
  const results = [];
357
- recordLinkResult(results, options, await ensureManagedLink(
358
- join(codexDir, "AGENTS.md"),
359
- join(paths.agentsDir, "AGENTS.md"),
360
- "file",
361
- previousLinkedTargets,
362
- options
363
- ));
364
- results.push(...await linkSkillDirectories(codexDir, paths, previousLinkedTargets, options));
365
- results.push(...await linkCommands(join(codexDir, "prompts"), paths, previousLinkedTargets, options));
362
+ for (const targetDir of [
363
+ join(paths.homeDir, ".claude"),
364
+ join(paths.homeDir, ".claude", "commands"),
365
+ join(paths.homeDir, ".claude", "skills"),
366
+ join(paths.homeDir, ".codex"),
367
+ join(paths.homeDir, ".codex", "prompts"),
368
+ join(paths.homeDir, ".codex", "skills")
369
+ ]) {
370
+ results.push(...await pruneManagedTargets(
371
+ targetDir,
372
+ paths.agentsDir,
373
+ [],
374
+ previousLinkedTargets,
375
+ options
376
+ ));
377
+ }
366
378
  return results;
367
379
  }
368
380
 
@@ -404,19 +416,21 @@ async function linkPi(paths, previousLinkedTargets = {}, options = {}) {
404
416
  const piAgentDir = await ensureProviderRootDirectory(paths.homeDir, [".pi", "agent"], options);
405
417
  await ensureManagedContainerDirectories([
406
418
  join(piAgentDir, "extensions"),
407
- join(piAgentDir, "skills"),
408
- join(piAgentDir, "prompts")
419
+ join(piAgentDir, "skills")
409
420
  ], options);
410
- const results = [];
411
- recordLinkResult(results, options, await ensureManagedLink(
412
- join(piAgentDir, "AGENTS.md"),
413
- join(paths.agentsDir, "AGENTS.md"),
414
- "file",
421
+ const results = await pruneRetiredProviderLinks(paths, previousLinkedTargets, options);
422
+ results.push(...await pruneProviderInstructions(piAgentDir, "prompts", paths, previousLinkedTargets, options));
423
+ results.push(...await linkSkillDirectories(
424
+ piAgentDir,
425
+ paths,
415
426
  previousLinkedTargets,
416
- options
427
+ options,
428
+ selectedLinkedSkillNames({
429
+ previousLinkedTargets,
430
+ skillsRoot: join(paths.agentsDir, "skills"),
431
+ managedFiles: options.managedFiles
432
+ })
417
433
  ));
418
- results.push(...await linkSkillDirectories(piAgentDir, paths, previousLinkedTargets, options));
419
- results.push(...await linkCommands(join(piAgentDir, "prompts"), paths, previousLinkedTargets, options));
420
434
  results.push(...await linkPiExtensionsInDirectory(
421
435
  join(piAgentDir, "extensions"),
422
436
  join(paths.agentsDir, "extensions"),
@@ -429,11 +443,10 @@ async function linkPi(paths, previousLinkedTargets = {}, options = {}) {
429
443
  export {
430
444
  ensureManagedLink,
431
445
  ensureProviderRootDirectory,
432
- getCommandNames,
433
446
  isWithinManagedRoot,
434
- linkClaude,
435
- linkCodex,
436
447
  linkPi,
437
448
  linkPiExtensions,
438
- pruneManagedTargets
449
+ linkPiSkill,
450
+ pruneManagedTargets,
451
+ shouldLinkSkillToPi
439
452
  };
package/lib/paths.mjs CHANGED
@@ -16,13 +16,11 @@ function createPaths({
16
16
  assertSeparateSourceAndDeployment(absolutePackageRoot, absoluteAgentsDir);
17
17
 
18
18
  const codexTemplateRoot = join(absolutePackageRoot, "lib", "templates", "codex");
19
- const workflowTemplateRoot = join(absolutePackageRoot, "lib", "templates", "workflow");
20
19
  const piAgentDir = join(absoluteHomeDir, ".pi", "agent");
21
20
  return {
22
21
  homeDir: absoluteHomeDir,
23
22
  packageRoot: absolutePackageRoot,
24
23
  agentsDir: absoluteAgentsDir,
25
- workflowTemplateRoot,
26
24
  installMetadataName: ".abelworkflow-install.json",
27
25
  claudeSettingsPath: join(absoluteHomeDir, ".claude", "settings.json"),
28
26
  codexConfigPath: join(absoluteHomeDir, ".codex", "config.toml"),
@@ -50,8 +50,15 @@ const defaultClaudeSettings = {
50
50
  language: "Chinese"
51
51
  };
52
52
 
53
+ function assertClaudeApiKey(value, label) {
54
+ if (value !== undefined && typeof value !== "string") {
55
+ throw new TypeError(`${label} must be a string or undefined`);
56
+ }
57
+ }
58
+
53
59
  function getExistingClaudeApiConfig(settings) {
54
60
  const env = settings.env === undefined ? {} : assertPlainObject(settings.env, "Claude settings env");
61
+ assertClaudeApiKey(env.ANTHROPIC_API_KEY, "Claude settings env ANTHROPIC_API_KEY");
55
62
  return {
56
63
  baseUrl: env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
57
64
  key: env.ANTHROPIC_API_KEY || "",
@@ -66,6 +73,8 @@ function buildClaudeApiSettings(settings, {
66
73
  }) {
67
74
  assertPlainObject(settings, "Claude settings");
68
75
  const env = settings.env === undefined ? {} : assertPlainObject(settings.env, "Claude settings env");
76
+ assertClaudeApiKey(env.ANTHROPIC_API_KEY, "Claude settings env ANTHROPIC_API_KEY");
77
+ assertClaudeApiKey(key, "Claude API key");
69
78
  const permissions = settings.permissions === undefined
70
79
  ? {}
71
80
  : assertPlainObject(settings.permissions, "Claude settings permissions");
@@ -82,8 +91,7 @@ function buildClaudeApiSettings(settings, {
82
91
  env: {
83
92
  ...defaultClaudeSettings.env,
84
93
  ...env,
85
- ANTHROPIC_BASE_URL: normalizeHttpBaseUrl(baseUrl),
86
- ANTHROPIC_API_KEY: key
94
+ ANTHROPIC_BASE_URL: normalizeHttpBaseUrl(baseUrl)
87
95
  },
88
96
  permissions: {
89
97
  ...defaultClaudeSettings.permissions,
@@ -92,6 +100,8 @@ function buildClaudeApiSettings(settings, {
92
100
  deny: Array.isArray(permissions.deny) ? permissions.deny : [...defaultClaudeSettings.permissions.deny]
93
101
  }
94
102
  };
103
+ if (key === undefined) delete nextSettings.env.ANTHROPIC_API_KEY;
104
+ else nextSettings.env.ANTHROPIC_API_KEY = key;
95
105
  delete nextSettings.includeCoAuthoredBy;
96
106
  delete nextSettings.env.ANTHROPIC_AUTH_TOKEN;
97
107
  delete nextSettings.env.DISABLE_TELEMETRY;
@@ -144,7 +154,8 @@ async function configureClaudeApi(paths, promptApi, runtime = {}) {
144
154
 
145
155
  const finalKey = await passwordOrExisting({
146
156
  message: "Claude Code API Key",
147
- existingValue: existing.key
157
+ existingValue: existing.key,
158
+ requiredMessage: existing.key ? null : "API Key 不能为空"
148
159
  });
149
160
 
150
161
  const model = await text({
@@ -62,7 +62,10 @@ function mergeCodexTemplateDefaults(content, templateContent) {
62
62
  ? current.developer_instructions
63
63
  : "";
64
64
 
65
- if (publishedCodexDeveloperInstructionHashes.has(hashBytes(currentInstructions))) {
65
+ const instructionHash = hashBytes(currentInstructions);
66
+ const usesLegacyDefaults = publishedCodexDeveloperInstructionHashes.has(instructionHash);
67
+ if (usesLegacyDefaults
68
+ || instructionHash === "c84017d17fa425175f6dd7df9766b5de74e289420f63fea87054e2f41822a8bc") {
66
69
  const legacyDefaults = {
67
70
  approvals_reviewer: "guardian_subagent",
68
71
  approval_policy: "on-request",
@@ -71,7 +74,7 @@ function mergeCodexTemplateDefaults(content, templateContent) {
71
74
  };
72
75
  const replacementFields = new Set(["developer_instructions"]);
73
76
  for (const [field, value] of Object.entries(legacyDefaults)) {
74
- if (current[field] === value) replacementFields.add(field);
77
+ if (usesLegacyDefaults && current[field] === value) replacementFields.add(field);
75
78
  }
76
79
  for (const field of replacementFields) {
77
80
  nextContent = removeTopLevelTomlField(nextContent, field);
@@ -375,15 +378,17 @@ function buildCodexConfigContent(currentContent, {
375
378
 
376
379
  function mergeCodexAuthData(auth, apiKey) {
377
380
  assertPlainObject(auth, "Codex auth");
378
- const nextAuth = { ...auth };
379
- assertCodexAuthKeyAvailable(nextAuth, CODEX_ENV_KEY);
381
+ assertCodexAuthKeyAvailable(auth, CODEX_ENV_KEY);
380
382
  if (apiKey) {
381
- nextAuth.auth_mode = "apikey";
382
- nextAuth[CODEX_ENV_KEY] = apiKey;
383
- } else {
384
- delete nextAuth[CODEX_ENV_KEY];
385
- if (nextAuth.auth_mode === "apikey") delete nextAuth.auth_mode;
383
+ return {
384
+ auth_mode: "apikey",
385
+ [CODEX_ENV_KEY]: apiKey
386
+ };
386
387
  }
388
+
389
+ const nextAuth = { ...auth };
390
+ delete nextAuth[CODEX_ENV_KEY];
391
+ if (nextAuth.auth_mode === "apikey") delete nextAuth.auth_mode;
387
392
  return nextAuth;
388
393
  }
389
394
 
@@ -5,7 +5,11 @@ import {
5
5
  readJsoncFileSafe,
6
6
  withFileTransaction
7
7
  } from "../config/store.mjs";
8
- import { normalizeOpenAiBaseUrl } from "./url.mjs";
8
+ import {
9
+ normalizeOpenAiBaseUrl,
10
+ promptBaseUrlOptions,
11
+ resolvePromptedBaseUrl
12
+ } from "./url.mjs";
9
13
  import { stripJsonComments } from "../config/jsonc.mjs";
10
14
 
11
15
  const piProviderId = "abelworkflow";
@@ -349,83 +353,72 @@ async function persistPiConfiguration(paths, intent, operations = {}) {
349
353
  });
350
354
  }
351
355
 
352
- async function configurePiApi(paths, withExtensionsPresent, promptApi, runtime = {}) {
353
- if (typeof withExtensionsPresent !== "function") {
354
- throw new TypeError("Expected a Pi Extensions deployment function");
355
- }
356
- return withExtensionsPresent(paths, async (guard) => {
357
- await guard();
358
- const text = runtime.text ?? p.text;
359
- const {
360
- assertNotCancelled,
361
- passwordOrExisting,
362
- selectOrCancel
363
- } = promptApi;
364
- const { auth, models, settings } = await readExistingPiConfiguration(paths);
365
- const existing = resolveExistingPiApiConfig(models, settings, auth);
366
- const providerLabel = `Pi ${piProviderId}`;
367
-
368
- const baseUrlInput = await text({
369
- message: `${providerLabel} Base URL`,
370
- initialValue: existing.baseUrl || undefined,
371
- validate: (value) => {
372
- try {
373
- normalizeOpenAiBaseUrl(value);
374
- } catch (error) {
375
- return error.message;
376
- }
377
- }
378
- });
379
- assertNotCancelled(baseUrlInput);
380
- const inferredApi = inferPiApiFromBaseUrl(baseUrlInput);
381
- const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
356
+ async function promptPiConfiguration(paths, promptApi, runtime = {}) {
357
+ const text = runtime.text ?? p.text;
358
+ const {
359
+ assertNotCancelled,
360
+ passwordOrExisting,
361
+ selectOrCancel
362
+ } = promptApi;
363
+ const { auth, models, settings } = await readExistingPiConfiguration(paths);
364
+ const existing = resolveExistingPiApiConfig(models, settings, auth);
365
+ const providerLabel = `Pi ${piProviderId}`;
382
366
 
383
- const apiOptions = getPiApiPromptOptions();
384
- const initialApi = apiOptions.some((option) => option.value === existing.api)
385
- ? existing.api
386
- : undefined;
387
- const api = inferredApi || await selectOrCancel({
388
- message: `${providerLabel} API 类型`,
389
- options: apiOptions,
390
- ...(initialApi ? { initialValue: initialApi } : {})
391
- });
392
- const apiKey = await passwordOrExisting({
393
- message: `${providerLabel} API Key`,
394
- existingValue: existing.apiKey
395
- });
367
+ const baseUrlPrompt = {
368
+ message: `${providerLabel} Base URL`,
369
+ existingValue: existing.baseUrl,
370
+ placeholder: "https://api.openai.com/v1",
371
+ normalize: normalizeOpenAiBaseUrl
372
+ };
373
+ const baseUrlInput = await text(promptBaseUrlOptions(baseUrlPrompt));
374
+ assertNotCancelled(baseUrlInput);
375
+ const inferredApi = inferPiApiFromBaseUrl(baseUrlInput);
376
+ const baseUrl = resolvePromptedBaseUrl(baseUrlInput, baseUrlPrompt);
396
377
 
397
- const modelIdsText = await text({
398
- message: `${providerLabel} 模型 ID(多个用逗号分隔)`,
399
- initialValue: existing.modelIds.join(",") || undefined,
400
- validate: (value) => parsePiModelIds(value).length ? undefined : "至少需要一个模型 ID"
401
- });
402
- assertNotCancelled(modelIdsText);
403
- const modelIds = parsePiModelIds(modelIdsText);
404
- const defaultModel = await text({
405
- message: "Pi 默认模型",
406
- initialValue: modelIds.includes(existing.defaultModel) ? existing.defaultModel : modelIds[0],
407
- validate: (value) => modelIds.includes(String(value || "").trim())
408
- ? undefined
409
- : "默认模型必须在模型 ID 列表中"
410
- });
411
- assertNotCancelled(defaultModel);
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
+ });
412
391
 
413
- await guard();
414
- await persistPiConfiguration(paths, {
415
- apiKey,
416
- baseUrl,
417
- api,
418
- modelIds,
419
- defaultModel: String(defaultModel).trim()
420
- });
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 列表中"
421
405
  });
406
+ assertNotCancelled(defaultModel);
407
+
408
+ return {
409
+ apiKey,
410
+ baseUrl,
411
+ api,
412
+ modelIds,
413
+ defaultModel: String(defaultModel).trim()
414
+ };
422
415
  }
423
416
 
424
417
  export {
425
418
  buildPiAuthConfig,
426
419
  buildPiModelsConfig,
427
420
  buildPiSettingsConfig,
428
- configurePiApi,
421
+ promptPiConfiguration,
429
422
  getPiApiPromptOptions,
430
423
  inferPiApiFromBaseUrl,
431
424
  normalizeOpenAiBaseUrl,
@@ -3,7 +3,11 @@ import { join } from "node:path";
3
3
  import * as p from "@clack/prompts";
4
4
  import { readDotenvFile, updateDotenvFile } from "../config/store.mjs";
5
5
  import { pathToLabel } from "../paths.mjs";
6
- import { normalizeHttpBaseUrl } from "./url.mjs";
6
+ import {
7
+ normalizeHttpBaseUrl,
8
+ promptBaseUrlOptions,
9
+ resolvePromptedBaseUrl
10
+ } from "./url.mjs";
7
11
 
8
12
  const grokDefaults = Object.freeze(JSON.parse(readFileSync(
9
13
  new URL("../../skills/grok-search/defaults.json", import.meta.url),
@@ -18,14 +22,6 @@ async function updateSkillEnvFile(path, updates, safeRoot) {
18
22
  return updateDotenvFile(path, updates, { safeRoot, sensitive: true });
19
23
  }
20
24
 
21
- function validateHttpBaseUrl(value) {
22
- try {
23
- normalizeHttpBaseUrl(value);
24
- } catch (error) {
25
- return error.message;
26
- }
27
- }
28
-
29
25
  async function configureGrokSearchEnv(paths, withSkillPresent, promptApi, runtime = {}) {
30
26
  const {
31
27
  assertNotCancelled,
@@ -39,13 +35,21 @@ async function configureGrokSearchEnv(paths, withSkillPresent, promptApi, runtim
39
35
  await guard();
40
36
  const envPath = join(paths.agentsDir, "skills", "grok-search", ".env");
41
37
  const existing = await readSkillEnvFile(envPath, paths.agentsDir);
42
- const baseUrlInput = await text({
38
+ const grokApiUrlPrompt = {
43
39
  message: "Grok API URL",
44
- initialValue: existing.GROK_API_URL || "https://api.x.ai/v1",
45
- validate: validateHttpBaseUrl
46
- });
40
+ existingValue: existing.GROK_API_URL,
41
+ placeholder: "https://api.x.ai/v1",
42
+ normalize: normalizeHttpBaseUrl
43
+ };
44
+ const tavilyApiUrlPrompt = {
45
+ message: "Tavily API URL",
46
+ existingValue: existing.TAVILY_API_URL,
47
+ placeholder: "https://api.tavily.com",
48
+ normalize: normalizeHttpBaseUrl
49
+ };
50
+ const baseUrlInput = await text(promptBaseUrlOptions(grokApiUrlPrompt));
47
51
  assertNotCancelled(baseUrlInput);
48
- const baseUrl = normalizeHttpBaseUrl(baseUrlInput);
52
+ const baseUrl = resolvePromptedBaseUrl(baseUrlInput, grokApiUrlPrompt);
49
53
 
50
54
  const finalApiKey = await passwordOrExisting({
51
55
  message: "Grok API Key",
@@ -65,15 +69,11 @@ async function configureGrokSearchEnv(paths, withSkillPresent, promptApi, runtim
65
69
  initialValue: Boolean(existing.TAVILY_API_KEY)
66
70
  });
67
71
 
68
- const tavilyUrl = useTavily
69
- ? await text({
70
- message: "Tavily API URL",
71
- initialValue: existing.TAVILY_API_URL || "https://api.tavily.com",
72
- validate: validateHttpBaseUrl
73
- })
74
- : null;
72
+ const tavilyUrl = useTavily ? await text(promptBaseUrlOptions(tavilyApiUrlPrompt)) : null;
75
73
  if (useTavily) assertNotCancelled(tavilyUrl);
76
- const normalizedTavilyUrl = useTavily ? normalizeHttpBaseUrl(tavilyUrl) : null;
74
+ const normalizedTavilyUrl = useTavily
75
+ ? resolvePromptedBaseUrl(tavilyUrl, tavilyApiUrlPrompt)
76
+ : null;
77
77
 
78
78
  const finalTavilyKey = useTavily
79
79
  ? await passwordOrExisting({
@@ -32,4 +32,35 @@ function normalizeOpenAiBaseUrl(value) {
32
32
  return url.toString();
33
33
  }
34
34
 
35
- export { normalizeHttpBaseUrl, normalizeOpenAiBaseUrl };
35
+ function resolvePromptedBaseUrl(value, { existingValue = "", placeholder, normalize }) {
36
+ const existing = typeof existingValue === "string" ? existingValue.trim() : "";
37
+ return normalize(String(value || "").trim() || existing || placeholder);
38
+ }
39
+
40
+ function promptBaseUrlOptions({
41
+ message,
42
+ existingValue,
43
+ placeholder,
44
+ normalize
45
+ }) {
46
+ const existing = typeof existingValue === "string" ? existingValue.trim() : "";
47
+ return {
48
+ message,
49
+ placeholder,
50
+ ...(existing ? { initialValue: existing } : { defaultValue: placeholder }),
51
+ validate(value) {
52
+ try {
53
+ resolvePromptedBaseUrl(value, { existingValue: existing, placeholder, normalize });
54
+ } catch (error) {
55
+ return error.message;
56
+ }
57
+ }
58
+ };
59
+ }
60
+
61
+ export {
62
+ normalizeHttpBaseUrl,
63
+ normalizeOpenAiBaseUrl,
64
+ promptBaseUrlOptions,
65
+ resolvePromptedBaseUrl
66
+ };