abelworkflow 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AbelWorkflow
2
2
 
3
- Codex、OpenCode、Claude Code 的 SkillsCommands 配置仓库。
3
+ Codex、OpenCode、Claude Code、Pi 的 SkillsCommands/Prompts 和扩展配置仓库。
4
4
 
5
5
  ## 目录结构
6
6
 
@@ -17,6 +17,7 @@ Codex、OpenCode、Claude Code 的 Skills 和 Commands 配置仓库。
17
17
  │ └── prompt-enhancer/ # 提示词优化器
18
18
  ├── commands/ # 命令目录
19
19
  │ └── oc/ # 工作流命令
20
+ ├── extensions/ # Pi 扩展
20
21
  ├── AGENTS.md # Agent 全局系统 prompts
21
22
  └── README.md
22
23
  ```
@@ -76,11 +77,12 @@ npx abelworkflow@latest
76
77
  > 说明:
77
78
  > - npm 发布包名必须使用小写,所以实际可执行命令是 `npx abelworkflow`。
78
79
  > - 交互式模式下,默认会打开初始化菜单,支持:
79
- > - 同步 `~/.agents` 并自动重建 `~/.claude` / `~/.codex` 链接
80
+ > - 同步 `~/.agents` 并自动重建 `~/.claude` / `~/.codex` / `~/.pi/agent` 链接
80
81
  > - 交互式填写 `grok-search`、`context7-auto-research`、`prompt-enhancer` 的 `.env`
81
- > - 一键安装或更新 `Claude Code`、`Codex`
82
+ > - 一键安装或更新 `Claude Code`、`Codex`、`Pi`
82
83
  > - 配置 `Claude Code` 的第三方 API 到 `~/.claude/settings.json`
83
84
  > - 配置 `Codex` 的第三方 API 到 `~/.codex/config.toml` 和 `~/.codex/auth.json`
85
+ > - 配置 `Pi` 的自定义 API 到 `~/.pi/agent/models.json` 中的 `gpt` provider,并设置 `~/.pi/agent/settings.json` 默认模型,同时携带 Pi 扩展
84
86
  > - 非交互场景请显式使用 `npx abelworkflow install`,不再保留旧的默认自动同步逻辑。
85
87
 
86
88
  ### 交互式初始化能力
@@ -97,12 +99,14 @@ npx abelworkflow --help
97
99
  其中完整初始化会按需引导你完成:
98
100
 
99
101
  1. 安装 AbelWorkflow 到 `~/.agents`
100
- 2. 自动链接到 `~/.claude/` 和 `~/.codex/`
102
+ 2. 自动链接到 `~/.claude/`、`~/.codex/` 和 `~/.pi/agent/`
101
103
  3. 可选安装 `Claude Code` CLI
102
104
  4. 可选配置 `Claude Code` 第三方 API
103
105
  5. 可选安装 `Codex` CLI
104
106
  6. 可选配置 `Codex` 第三方 API
105
- 7. 可选填写三个技能的环境变量
107
+ 7. 可选安装 `Pi` CLI
108
+ 8. 可选配置 Pi `gpt` provider 自定义 API,并链接 Pi 扩展
109
+ 9. 可选填写三个技能的环境变量
106
110
 
107
111
  ### 技能环境写入位置
108
112
 
@@ -174,11 +178,12 @@ node .\bin\abelworkflow.mjs install
174
178
 
175
179
  ### 映射关系(本仓库 → 配置目录)
176
180
 
177
- | 本仓库 | Claude Code | Codex | 说明 |
178
- |---|---|---|---|
179
- | `AGENTS.md` | `~/.claude/CLAUDE.md` | `~/.codex/AGENTS.md` | 全局系统提示词/规则 |
180
- | `skills/<skill>/` | `~/.claude/skills/<skill>/` | `~/.codex/skills/<skill>/` | Skills(每个目录一个技能) |
181
- | `commands/abel-*.md` | `~/.claude/commands/abel-*.md` | `~/.codex/prompts/abel-*.md` | 扁平化部署,避免命名冲突 |
181
+ | 本仓库 | Claude Code | Codex | Pi | 说明 |
182
+ |---|---|---|---|---|
183
+ | `AGENTS.md` | `~/.claude/CLAUDE.md` | `~/.codex/AGENTS.md` | `~/.pi/agent/AGENTS.md` | 全局系统提示词/规则 |
184
+ | `skills/<skill>/` | `~/.claude/skills/<skill>/` | `~/.codex/skills/<skill>/` | `~/.pi/agent/skills/<skill>/` | Skills(每个目录一个技能) |
185
+ | `commands/abel-*.md` | `~/.claude/commands/abel-*.md` | `~/.codex/prompts/abel-*.md` | `~/.pi/agent/prompts/abel-*.md` | 扁平化部署,避免命名冲突 |
186
+ | `extensions/*.ts` | - | - | `~/.pi/agent/extensions/*.ts` | Pi 扩展 |
182
187
 
183
188
  ### 验证(可选)
184
189
 
@@ -0,0 +1,25 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ export default function (pi: ExtensionAPI) {
4
+ pi.on("before_provider_request", (event, ctx) => {
5
+ const payload = event.payload as any;
6
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
7
+ if (ctx.model?.provider !== "gpt") return;
8
+ if (!("input" in payload)) return;
9
+
10
+ const input = Array.isArray(payload.input) ? payload.input : [];
11
+ const first = input[0];
12
+ const isPrompt = first?.role === "system" || first?.role === "developer";
13
+ const instructions =
14
+ (typeof payload.instructions === "string" && payload.instructions.trim()) ||
15
+ (isPrompt && typeof first.content === "string" && first.content.trim()) ||
16
+ ctx.getSystemPrompt().trim();
17
+
18
+ const nextPayload = { ...payload };
19
+ delete nextPayload.prompt_cache_key;
20
+ delete nextPayload.prompt_cache_retention;
21
+ delete nextPayload.max_output_tokens;
22
+
23
+ return { ...nextPayload, instructions, input: isPrompt ? input.slice(1) : input };
24
+ });
25
+ }
package/lib/cli/logic.mjs CHANGED
@@ -6,14 +6,38 @@ const interactiveMenuDescriptors = [
6
6
  { value: "grok-search", label: "配置 grok-search", hint: "技能", group: "skill" },
7
7
  { value: "context7", label: "配置 context7-auto-research", hint: "技能", group: "skill" },
8
8
  { value: "prompt-enhancer", label: "配置 prompt-enhancer", hint: "技能", group: "skill" },
9
- { value: "claude-install", label: "安装/更新 Claude Code", hint: "CLI", group: "cli" },
10
- { value: "claude-api", label: "配置 Claude API", hint: "CLI", group: "cli" },
11
- { value: "codex-install", label: "安装/更新 Codex", hint: "CLI", group: "cli" },
12
- { value: "codex-api", label: "配置 Codex API", hint: "CLI", group: "cli" },
9
+ { value: "pi-cli", label: "安装/配置 Pi", hint: "CLI", group: "cli" },
10
+ { value: "codex-cli", label: "安装/配置 Codex", hint: "CLI", group: "cli" },
11
+ { value: "claude-cli", label: "安装/配置 Claude Code", hint: "CLI", group: "cli" },
13
12
  { value: "exit", label: "退出", group: "exit" }
14
13
  ];
15
14
 
16
15
  const interactiveMenuDefaultValue = "full-init";
16
+ const cliToolMenuDescriptorMap = {
17
+ pi: [
18
+ { value: "pi-install", label: "安装/更新 Pi" },
19
+ { value: "pi-api", label: "配置 Pi API" },
20
+ { value: "back", label: "返回上一级" }
21
+ ],
22
+ codex: [
23
+ { value: "codex-install", label: "安装/更新 Codex" },
24
+ { value: "codex-api", label: "配置 Codex API" },
25
+ { value: "back", label: "返回上一级" }
26
+ ],
27
+ claude: [
28
+ { value: "claude-install", label: "安装/更新 Claude Code" },
29
+ { value: "claude-api", label: "配置 Claude Code API" },
30
+ { value: "back", label: "返回上一级" }
31
+ ]
32
+ };
33
+
34
+ function buildCliToolMenuDescriptors(tool) {
35
+ const descriptors = cliToolMenuDescriptorMap[tool];
36
+ if (!descriptors) {
37
+ throw new Error(`Unknown CLI tool: ${tool}`);
38
+ }
39
+ return descriptors.map((descriptor) => ({ ...descriptor }));
40
+ }
17
41
 
18
42
  class CancelledError extends Error {
19
43
  constructor(message = "用户取消") {
@@ -175,6 +199,7 @@ function getRunCommandSpawnOptions(platform = process.env.ABELWORKFLOW_TEST_PLAT
175
199
  export {
176
200
  assertInteractiveMenuSupported,
177
201
  assertNotCancelled,
202
+ buildCliToolMenuDescriptors,
178
203
  CancelledError,
179
204
  confirmOrCancel,
180
205
  getRunCommandSpawnOptions,
package/lib/cli.mjs CHANGED
@@ -9,6 +9,7 @@ import c from "picocolors";
9
9
  import {
10
10
  assertInteractiveMenuSupported,
11
11
  assertNotCancelled,
12
+ buildCliToolMenuDescriptors,
12
13
  CancelledError,
13
14
  confirmOrCancel,
14
15
  getRunCommandSpawnOptions,
@@ -33,6 +34,13 @@ const codexAuthPath = join(home, ".codex", "auth.json");
33
34
  const codexTemplateRoot = join(packageRoot, "lib", "templates", "codex");
34
35
  const codexTemplateConfigPath = join(codexTemplateRoot, "config-base.toml");
35
36
  const codexTemplateAgentsPath = join(codexTemplateRoot, "agents");
37
+ const piAgentDir = join(home, ".pi", "agent");
38
+ const piModelsPath = join(piAgentDir, "models.json");
39
+ const piSettingsPath = join(piAgentDir, "settings.json");
40
+ const piProviderId = "gpt";
41
+ const piDefaultApi = "openai-responses";
42
+ const piDefaultBaseUrl = "https://api.openai.com/v1";
43
+ const piDefaultModel = "gpt-5.5";
36
44
  const installBackupStamp = Date.now();
37
45
  const createdBackupPaths = new Set();
38
46
  const augmentContextEnginePermission = "mcp__augment-context-engine";
@@ -95,6 +103,7 @@ const managedEntries = [
95
103
  { target: "README.md" },
96
104
  { target: "commands", preserveExisting: true },
97
105
  { target: "skills", preserveExisting: true, filter: shouldCopySkillPath },
106
+ { target: "extensions", preserveExisting: true },
98
107
  { target: ".skill-lock.json" },
99
108
  { target: ".gitignore", sourceCandidates: [".gitignore", ".npmignore"] }
100
109
  ];
@@ -390,6 +399,39 @@ async function writeInstallMetadata(agentsDir, metadata) {
390
399
  await writeFile(join(agentsDir, installMetadataName), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
391
400
  }
392
401
 
402
+ function linkedTargetsFromResults(results) {
403
+ return Object.fromEntries(
404
+ results
405
+ .filter((result) => result.sourcePath)
406
+ .map((result) => [
407
+ result.targetPath,
408
+ {
409
+ sourcePath: result.sourcePath,
410
+ kind: result.kind,
411
+ mode: result.mode
412
+ }
413
+ ])
414
+ );
415
+ }
416
+
417
+ function mergeLinkedTargets(previousLinkedTargets = {}, results = []) {
418
+ const nextLinkedTargets = { ...previousLinkedTargets };
419
+ for (const result of results) {
420
+ if (result.sourcePath) {
421
+ nextLinkedTargets[result.targetPath] = {
422
+ sourcePath: result.sourcePath,
423
+ kind: result.kind,
424
+ mode: result.mode
425
+ };
426
+ continue;
427
+ }
428
+ if (result.status === "removed") {
429
+ delete nextLinkedTargets[result.targetPath];
430
+ }
431
+ }
432
+ return nextLinkedTargets;
433
+ }
434
+
393
435
  async function renderManagedWorkflowFile(path, augmentContextEngine) {
394
436
  if (!(await pathIsFile(path))) {
395
437
  return;
@@ -666,6 +708,18 @@ async function getCommandNames(commandsDir) {
666
708
  return names.filter(Boolean);
667
709
  }
668
710
 
711
+ async function getPiExtensionNames(extensionsDir) {
712
+ if (!(await pathIsDirectory(extensionsDir))) {
713
+ return [];
714
+ }
715
+
716
+ const entries = await readdir(extensionsDir, { withFileTypes: true });
717
+ const names = await Promise.all(
718
+ entries.map(async (entry) => ((await isPiExtensionEntry(extensionsDir, entry)) ? entry.name : null))
719
+ );
720
+ return names.filter(Boolean);
721
+ }
722
+
669
723
  async function pathIsDirectory(path) {
670
724
  try {
671
725
  return (await stat(path)).isDirectory();
@@ -710,6 +764,23 @@ async function isMarkdownFileEntry(root, entry) {
710
764
  return pathIsFile(join(root, entry.name));
711
765
  }
712
766
 
767
+ async function isPiExtensionEntry(root, entry) {
768
+ const entryPath = join(root, entry.name);
769
+ if (entry.isDirectory()) {
770
+ return pathIsFile(join(entryPath, "index.ts"));
771
+ }
772
+ if (entry.isFile()) {
773
+ return entry.name.endsWith(".ts");
774
+ }
775
+ if (!entry.isSymbolicLink()) {
776
+ return false;
777
+ }
778
+ if (entry.name.endsWith(".ts") && await pathIsFile(entryPath)) {
779
+ return true;
780
+ }
781
+ return pathIsFile(join(entryPath, "index.ts"));
782
+ }
783
+
713
784
  async function pruneManagedTargets(targetDir, managedSourceRoot, expectedNames, previousLinkedTargets) {
714
785
  if (!(await pathExists(targetDir))) {
715
786
  return [];
@@ -844,6 +915,71 @@ async function linkCodex(agentsDir, previousLinkedTargets) {
844
915
  return results;
845
916
  }
846
917
 
918
+ async function linkPi(agentsDir, previousLinkedTargets) {
919
+ const results = [];
920
+ await mkdir(piAgentDir, { recursive: true });
921
+ await removeIfNotDirectory(join(piAgentDir, "skills"));
922
+ await removeIfNotDirectory(join(piAgentDir, "prompts"));
923
+ await removeIfNotDirectory(join(piAgentDir, "extensions"));
924
+ await mkdir(join(piAgentDir, "skills"), { recursive: true });
925
+ await mkdir(join(piAgentDir, "prompts"), { recursive: true });
926
+ await mkdir(join(piAgentDir, "extensions"), { recursive: true });
927
+
928
+ results.push(
929
+ await ensureManagedLink(
930
+ join(piAgentDir, "AGENTS.md"),
931
+ join(agentsDir, "AGENTS.md"),
932
+ "file",
933
+ previousLinkedTargets
934
+ )
935
+ );
936
+ results.push(...(await linkSkillDirectories(piAgentDir, agentsDir, previousLinkedTargets)));
937
+
938
+ const commandFiles = await getCommandNames(join(agentsDir, "commands"));
939
+ results.push(
940
+ ...(await pruneManagedTargets(
941
+ join(piAgentDir, "prompts"),
942
+ join(agentsDir, "commands"),
943
+ commandFiles,
944
+ previousLinkedTargets
945
+ ))
946
+ );
947
+ for (const fileName of commandFiles) {
948
+ results.push(
949
+ await ensureManagedLink(
950
+ join(piAgentDir, "prompts", fileName),
951
+ join(agentsDir, "commands", fileName),
952
+ "file",
953
+ previousLinkedTargets
954
+ )
955
+ );
956
+ }
957
+
958
+ const extensionNames = await getPiExtensionNames(join(agentsDir, "extensions"));
959
+ results.push(
960
+ ...(await pruneManagedTargets(
961
+ join(piAgentDir, "extensions"),
962
+ join(agentsDir, "extensions"),
963
+ extensionNames,
964
+ previousLinkedTargets
965
+ ))
966
+ );
967
+ for (const entryName of extensionNames) {
968
+ const sourcePath = join(agentsDir, "extensions", entryName);
969
+ const kind = await pathIsDirectory(sourcePath) ? "dir" : "file";
970
+ results.push(
971
+ await ensureManagedLink(
972
+ join(piAgentDir, "extensions", entryName),
973
+ sourcePath,
974
+ kind,
975
+ previousLinkedTargets
976
+ )
977
+ );
978
+ }
979
+
980
+ return results;
981
+ }
982
+
847
983
  async function installManagedWorkflow(options) {
848
984
  let previousMetadata = {};
849
985
  let managedChildren = {};
@@ -870,30 +1006,21 @@ async function installManagedWorkflow(options) {
870
1006
  }
871
1007
 
872
1008
  const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
873
- s.start("正在链接 Claude / Codex...");
1009
+ s.start("正在链接 Claude / Codex / Pi...");
874
1010
  let claudeResults;
875
1011
  let codexResults;
1012
+ let piResults;
876
1013
  try {
877
1014
  claudeResults = await linkClaude(options.agentsDir, previousLinkedTargets);
878
1015
  codexResults = await linkCodex(options.agentsDir, previousLinkedTargets);
1016
+ piResults = await linkPi(options.agentsDir, previousLinkedTargets);
879
1017
  } catch (e) {
880
1018
  s.cancel(c.red(`链接失败: ${e.message}`));
881
1019
  throw e;
882
1020
  }
883
1021
  s.stop("链接完成");
884
1022
 
885
- const linkedTargets = Object.fromEntries(
886
- [...claudeResults, ...codexResults]
887
- .filter((result) => result.sourcePath)
888
- .map((result) => [
889
- result.targetPath,
890
- {
891
- sourcePath: result.sourcePath,
892
- kind: result.kind,
893
- mode: result.mode
894
- }
895
- ])
896
- );
1023
+ const linkedTargets = linkedTargetsFromResults([...claudeResults, ...codexResults, ...piResults]);
897
1024
 
898
1025
  const managedClaudePermissions = await ensureClaudeSettingsForFeature(augmentContextEngine, previousMetadata);
899
1026
 
@@ -909,7 +1036,7 @@ async function installManagedWorkflow(options) {
909
1036
  linkedTargets
910
1037
  });
911
1038
 
912
- const resultLines = [...claudeResults, ...codexResults].map((result) => {
1039
+ const resultLines = [...claudeResults, ...codexResults, ...piResults].map((result) => {
913
1040
  const icon = result.status === "unchanged" ? c.gray("=")
914
1041
  : result.status === "removed" ? c.yellow("−")
915
1042
  : c.green("+");
@@ -943,6 +1070,188 @@ async function writeJsonFileWithBackup(path, data) {
943
1070
  await writeJsonFileSafe(path, data);
944
1071
  }
945
1072
 
1073
+ function stripJsonComments(content) {
1074
+ let output = "";
1075
+ let inString = false;
1076
+ let quote = "";
1077
+ let escaped = false;
1078
+ for (let i = 0; i < content.length; i += 1) {
1079
+ const char = content[i];
1080
+ const next = content[i + 1];
1081
+
1082
+ if (inString) {
1083
+ output += char;
1084
+ if (escaped) {
1085
+ escaped = false;
1086
+ } else if (char === "\\") {
1087
+ escaped = true;
1088
+ } else if (char === quote) {
1089
+ inString = false;
1090
+ quote = "";
1091
+ }
1092
+ continue;
1093
+ }
1094
+
1095
+ if (char === "\"" || char === "'") {
1096
+ inString = true;
1097
+ quote = char;
1098
+ output += char;
1099
+ continue;
1100
+ }
1101
+
1102
+ if (char === "/" && next === "/") {
1103
+ while (i < content.length && content[i] !== "\n") {
1104
+ i += 1;
1105
+ }
1106
+ if (i < content.length) {
1107
+ output += content[i];
1108
+ }
1109
+ continue;
1110
+ }
1111
+
1112
+ if (char === "/" && next === "*") {
1113
+ i += 2;
1114
+ while (i < content.length && !(content[i] === "*" && content[i + 1] === "/")) {
1115
+ output += content[i] === "\n" ? "\n" : "";
1116
+ i += 1;
1117
+ }
1118
+ i += 1;
1119
+ continue;
1120
+ }
1121
+
1122
+ output += char;
1123
+ }
1124
+ return stripJsonTrailingCommas(output);
1125
+ }
1126
+
1127
+ function stripJsonTrailingCommas(content) {
1128
+ let output = "";
1129
+ let inString = false;
1130
+ let quote = "";
1131
+ let escaped = false;
1132
+ for (let i = 0; i < content.length; i += 1) {
1133
+ const char = content[i];
1134
+
1135
+ if (inString) {
1136
+ output += char;
1137
+ if (escaped) {
1138
+ escaped = false;
1139
+ } else if (char === "\\") {
1140
+ escaped = true;
1141
+ } else if (char === quote) {
1142
+ inString = false;
1143
+ quote = "";
1144
+ }
1145
+ continue;
1146
+ }
1147
+
1148
+ if (char === "\"" || char === "'") {
1149
+ inString = true;
1150
+ quote = char;
1151
+ output += char;
1152
+ continue;
1153
+ }
1154
+
1155
+ if (char === ",") {
1156
+ let nextIndex = i + 1;
1157
+ while (nextIndex < content.length && /\s/u.test(content[nextIndex])) {
1158
+ nextIndex += 1;
1159
+ }
1160
+ if (content[nextIndex] === "}" || content[nextIndex] === "]") {
1161
+ continue;
1162
+ }
1163
+ }
1164
+
1165
+ output += char;
1166
+ }
1167
+ return output;
1168
+ }
1169
+
1170
+ async function readJsoncFileSafe(path, fallback = {}) {
1171
+ if (!(await pathExists(path))) {
1172
+ return fallback;
1173
+ }
1174
+
1175
+ try {
1176
+ return JSON.parse(stripJsonComments(await readFile(path, "utf8")));
1177
+ } catch {
1178
+ return fallback;
1179
+ }
1180
+ }
1181
+
1182
+ function parsePiModelIds(value) {
1183
+ return [...new Set(String(value || "")
1184
+ .split(/[\n,]+/u)
1185
+ .map((item) => item.trim())
1186
+ .filter(Boolean))];
1187
+ }
1188
+
1189
+ function resolveExistingPiApiConfig(modelsConfig = {}, settings = {}) {
1190
+ const provider = modelsConfig.providers?.[piProviderId] && typeof modelsConfig.providers[piProviderId] === "object"
1191
+ ? modelsConfig.providers[piProviderId]
1192
+ : {};
1193
+ const models = Array.isArray(provider.models) ? provider.models.filter((model) => model?.id) : [];
1194
+ return {
1195
+ baseUrl: provider.baseUrl || piDefaultBaseUrl,
1196
+ api: provider.api || piDefaultApi,
1197
+ apiKey: provider.apiKey || "",
1198
+ modelIds: models.map((model) => model.id),
1199
+ defaultModel: settings.defaultProvider === piProviderId && settings.defaultModel
1200
+ ? settings.defaultModel
1201
+ : models[0]?.id || piDefaultModel
1202
+ };
1203
+ }
1204
+
1205
+ function buildPiModelConfig(modelId, existingModel = {}) {
1206
+ return {
1207
+ ...existingModel,
1208
+ id: modelId,
1209
+ name: existingModel.name || modelId,
1210
+ reasoning: existingModel.reasoning ?? true,
1211
+ input: Array.isArray(existingModel.input) ? existingModel.input : ["text", "image"],
1212
+ contextWindow: existingModel.contextWindow ?? 262144,
1213
+ maxTokens: existingModel.maxTokens ?? 64000
1214
+ };
1215
+ }
1216
+
1217
+ function buildPiModelsConfig(modelsConfig = {}, { baseUrl, api, apiKey, modelIds }) {
1218
+ const providers = modelsConfig.providers && typeof modelsConfig.providers === "object" ? modelsConfig.providers : {};
1219
+ const currentProvider = providers[piProviderId] && typeof providers[piProviderId] === "object" ? providers[piProviderId] : {};
1220
+ const existingModels = new Map(
1221
+ (Array.isArray(currentProvider.models) ? currentProvider.models : [])
1222
+ .filter((model) => model?.id)
1223
+ .map((model) => [model.id, model])
1224
+ );
1225
+
1226
+ return {
1227
+ ...modelsConfig,
1228
+ providers: {
1229
+ ...providers,
1230
+ [piProviderId]: {
1231
+ ...currentProvider,
1232
+ baseUrl,
1233
+ api,
1234
+ apiKey,
1235
+ compat: {
1236
+ ...(currentProvider.compat && typeof currentProvider.compat === "object" ? currentProvider.compat : {}),
1237
+ supportsDeveloperRole: false
1238
+ },
1239
+ models: modelIds.map((modelId) => buildPiModelConfig(modelId, existingModels.get(modelId)))
1240
+ }
1241
+ }
1242
+ };
1243
+ }
1244
+
1245
+ function buildPiSettingsConfig(settings = {}, defaultModel) {
1246
+ return {
1247
+ ...settings,
1248
+ defaultProvider: piProviderId,
1249
+ defaultModel,
1250
+ defaultThinkingLevel: settings.defaultThinkingLevel || "high",
1251
+ enableSkillCommands: settings.enableSkillCommands ?? true
1252
+ };
1253
+ }
1254
+
946
1255
  function parseDotenv(content) {
947
1256
  const values = {};
948
1257
  for (const rawLine of content.split(/\r?\n/u)) {
@@ -1010,10 +1319,33 @@ async function updateDotenvFile(path, updates) {
1010
1319
  await writeFile(path, renderDotenv(current), "utf8");
1011
1320
  }
1012
1321
 
1013
- function commandExists(command) {
1322
+ function getCommandPath(command) {
1014
1323
  const checker = isWindows() ? "where" : "which";
1015
- const result = spawnSync(checker, [command], { stdio: "ignore" });
1016
- return result.status === 0;
1324
+ const result = spawnSync(checker, [command], { encoding: "utf8" });
1325
+ if (result.status !== 0) {
1326
+ return undefined;
1327
+ }
1328
+ return result.stdout
1329
+ .split(/\r?\n/u)
1330
+ .map((line) => line.trim())
1331
+ .find(Boolean);
1332
+ }
1333
+
1334
+ function commandExists(command) {
1335
+ return Boolean(getCommandPath(command));
1336
+ }
1337
+
1338
+ function readCommandOutput(command, args) {
1339
+ const result = spawnSync(command, args, {
1340
+ encoding: "utf8",
1341
+ shell: isWindows(),
1342
+ stdio: ["ignore", "pipe", "ignore"]
1343
+ });
1344
+ if (result.status !== 0) {
1345
+ return undefined;
1346
+ }
1347
+ const outputValue = result.stdout.trim();
1348
+ return outputValue || undefined;
1017
1349
  }
1018
1350
 
1019
1351
  async function runCommand(command, args) {
@@ -1856,6 +2188,89 @@ async function getExistingCodexApiConfig() {
1856
2188
  return resolveExistingCodexApiConfig(content, auth);
1857
2189
  }
1858
2190
 
2191
+ async function ensurePiResourcesLinked(agentsDir) {
2192
+ const { previousMetadata, managedChildren } = await syncManagedFiles(agentsDir);
2193
+ const augmentContextEngine = resolveAugmentContextEngineFeature({}, previousMetadata);
2194
+ await renderManagedWorkflowFiles(agentsDir, augmentContextEngine);
2195
+
2196
+ const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
2197
+ const piResults = await linkPi(agentsDir, previousLinkedTargets);
2198
+ await writeInstallMetadata(agentsDir, {
2199
+ ...previousMetadata,
2200
+ package: previousMetadata.package || "abelworkflow",
2201
+ installedAt: previousMetadata.installedAt || new Date().toISOString(),
2202
+ features: {
2203
+ ...(previousMetadata.features && typeof previousMetadata.features === "object" ? previousMetadata.features : {}),
2204
+ augmentContextEngine
2205
+ },
2206
+ managedChildren,
2207
+ managedClaudePermissions: getPreviousManagedClaudePermissions(previousMetadata),
2208
+ linkedTargets: mergeLinkedTargets(previousLinkedTargets, piResults)
2209
+ });
2210
+ return piResults;
2211
+ }
2212
+
2213
+ async function configurePiApi(agentsDir) {
2214
+ const modelsConfig = await readJsoncFileSafe(piModelsPath, {});
2215
+ const settings = await readJsonFileSafe(piSettingsPath, {});
2216
+ const existing = resolveExistingPiApiConfig(modelsConfig, settings);
2217
+
2218
+ const baseUrl = await p.text({
2219
+ message: "Pi gpt Base URL",
2220
+ defaultValue: existing.baseUrl,
2221
+ validate: required()
2222
+ });
2223
+ assertNotCancelled(baseUrl);
2224
+
2225
+ const piApiOptions = ["openai-responses", "openai-completions"];
2226
+ const api = await selectOrCancel({
2227
+ message: "Pi gpt API 类型",
2228
+ options: [
2229
+ { value: "openai-responses", label: "OpenAI Responses API" },
2230
+ { value: "openai-completions", label: "OpenAI Chat Completions" }
2231
+ ],
2232
+ initialValue: piApiOptions.includes(existing.api) ? existing.api : piDefaultApi
2233
+ });
2234
+
2235
+ const apiKey = await p.password({
2236
+ message: "Pi gpt API Key(输入 - 清除)",
2237
+ mask: "*",
2238
+ defaultValue: existing.apiKey || undefined,
2239
+ validate: requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
2240
+ });
2241
+ assertNotCancelled(apiKey);
2242
+ const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
2243
+
2244
+ const modelIdsText = await p.text({
2245
+ message: "Pi gpt 模型 ID(多个用逗号分隔)",
2246
+ defaultValue: (existing.modelIds.length ? existing.modelIds : [existing.defaultModel]).join(","),
2247
+ validate: (value) => parsePiModelIds(value).length ? undefined : "至少需要一个模型 ID"
2248
+ });
2249
+ assertNotCancelled(modelIdsText);
2250
+ const modelIds = parsePiModelIds(modelIdsText);
2251
+
2252
+ const defaultModel = await p.text({
2253
+ message: "Pi 默认模型",
2254
+ defaultValue: modelIds.includes(existing.defaultModel) ? existing.defaultModel : modelIds[0],
2255
+ validate: (value) => modelIds.includes(String(value || "").trim()) ? undefined : "默认模型必须在模型 ID 列表中"
2256
+ });
2257
+ assertNotCancelled(defaultModel);
2258
+
2259
+ const finalDefaultModel = String(defaultModel).trim();
2260
+ await ensurePiResourcesLinked(agentsDir);
2261
+ await writeJsonFileWithBackup(piModelsPath, buildPiModelsConfig(modelsConfig, {
2262
+ baseUrl,
2263
+ api,
2264
+ apiKey: finalApiKey,
2265
+ modelIds
2266
+ }));
2267
+ await writeJsonFileWithBackup(piSettingsPath, buildPiSettingsConfig(settings, finalDefaultModel));
2268
+
2269
+ p.log.step(`已更新 ${pathToLabel(piModelsPath)} (${piProviderId}, ${baseUrl})`);
2270
+ p.log.step(`已更新 ${pathToLabel(piSettingsPath)} (默认模型: ${finalDefaultModel})`);
2271
+ p.log.step(`已链接 Pi 扩展到 ${pathToLabel(join(piAgentDir, "extensions"))}`);
2272
+ }
2273
+
1859
2274
  async function configureCodexApi() {
1860
2275
  const existing = await getExistingCodexApiConfig();
1861
2276
  const providerId = existing.providerId || "abelworkflow";
@@ -1945,14 +2360,140 @@ function buildCodexConfigContent(currentContent, {
1945
2360
  }
1946
2361
 
1947
2362
  function mergeCodexAuthData(auth, envKey, apiKey, legacyEnvKeys = []) {
1948
- const nextAuth = auth && typeof auth === "object" ? { ...auth } : {};
1949
- for (const key of legacyEnvKeys) {
1950
- if (key && key !== envKey) {
1951
- delete nextAuth[key];
2363
+ return { [envKey]: apiKey };
2364
+ }
2365
+
2366
+ function normalizeInstallPath(value) {
2367
+ if (!value) {
2368
+ return "";
2369
+ }
2370
+ return value.replace(/\\/gu, "/").replace(/\/+$/u, "").toLowerCase();
2371
+ }
2372
+
2373
+ function installPathIsWithin(path, parentPath) {
2374
+ const normalizedPath = normalizeInstallPath(path);
2375
+ const normalizedParent = normalizeInstallPath(parentPath);
2376
+ return Boolean(normalizedPath && normalizedParent)
2377
+ && (normalizedPath === normalizedParent || normalizedPath.startsWith(`${normalizedParent}/`));
2378
+ }
2379
+
2380
+ function getNpmGlobalBinDirs(npmGlobalPrefix, platform = getPlatform()) {
2381
+ if (!npmGlobalPrefix) {
2382
+ return [];
2383
+ }
2384
+ return platform === "win32" ? [npmGlobalPrefix] : [join(npmGlobalPrefix, "bin")];
2385
+ }
2386
+
2387
+ function inferPackageManagerFromCommandPath(commandPath, {
2388
+ bunGlobalBinDir,
2389
+ npmGlobalPrefix,
2390
+ platform = getPlatform()
2391
+ } = {}) {
2392
+ if (installPathIsWithin(commandPath, bunGlobalBinDir)) {
2393
+ return "bun";
2394
+ }
2395
+ if (getNpmGlobalBinDirs(npmGlobalPrefix, platform)
2396
+ .some((binDir) => installPathIsWithin(commandPath, binDir))) {
2397
+ return "npm";
2398
+ }
2399
+ return null;
2400
+ }
2401
+
2402
+ function chooseCliInstallPackageManager({
2403
+ commandPath,
2404
+ availablePackageManagers = [],
2405
+ bunGlobalBinDir,
2406
+ npmGlobalPrefix,
2407
+ platform = getPlatform(),
2408
+ nodeAvailable = true,
2409
+ supportedPackageManagers = ["bun", "npm"]
2410
+ } = {}) {
2411
+ const supported = new Set(supportedPackageManagers);
2412
+ const available = new Set(availablePackageManagers.filter((packageManager) => {
2413
+ if (!supported.has(packageManager)) {
2414
+ return false;
1952
2415
  }
2416
+ return packageManager !== "bun" || nodeAvailable;
2417
+ }));
2418
+ const bunBlockedByMissingNode = !nodeAvailable
2419
+ && supported.has("bun")
2420
+ && availablePackageManagers.includes("bun");
2421
+ const existingPackageManager = inferPackageManagerFromCommandPath(commandPath, {
2422
+ bunGlobalBinDir,
2423
+ npmGlobalPrefix,
2424
+ platform
2425
+ });
2426
+
2427
+ if (existingPackageManager && available.has(existingPackageManager)) {
2428
+ return { packageManager: existingPackageManager, source: "existing" };
1953
2429
  }
1954
- nextAuth[envKey] = apiKey;
1955
- return nextAuth;
2430
+ for (const packageManager of ["bun", "npm"]) {
2431
+ if (available.has(packageManager)) {
2432
+ return { packageManager, source: "available" };
2433
+ }
2434
+ }
2435
+ return {
2436
+ packageManager: null,
2437
+ source: bunBlockedByMissingNode ? "missing-node" : (availablePackageManagers.length > 0 ? "unsupported" : "missing")
2438
+ };
2439
+ }
2440
+
2441
+ function buildCliToolInstallCommand(packageManager, { packageName, skipScripts = false }) {
2442
+ if (packageManager === "npm") {
2443
+ return {
2444
+ command: "npm",
2445
+ args: [
2446
+ "install",
2447
+ "-g",
2448
+ ...(skipScripts ? ["--ignore-scripts"] : []),
2449
+ packageName,
2450
+ "--force"
2451
+ ]
2452
+ };
2453
+ }
2454
+ if (packageManager === "bun") {
2455
+ return {
2456
+ command: "bun",
2457
+ args: [
2458
+ "install",
2459
+ "-g",
2460
+ ...(skipScripts ? ["--ignore-scripts"] : []),
2461
+ packageName
2462
+ ]
2463
+ };
2464
+ }
2465
+ throw new Error(`Unsupported package manager: ${packageManager}`);
2466
+ }
2467
+
2468
+ function getPackageManagerInstallHelp(platform = getPlatform()) {
2469
+ const platformLabel = {
2470
+ darwin: "macOS",
2471
+ win32: "Windows",
2472
+ linux: "Linux"
2473
+ }[platform] || platform;
2474
+ return {
2475
+ platformLabel,
2476
+ mainlandUrl: "https://npmmirror.com/mirrors/node/",
2477
+ officialUrl: "https://nodejs.org/en/download/"
2478
+ };
2479
+ }
2480
+
2481
+ function getAvailablePackageManagers() {
2482
+ return ["bun", "npm"].filter((packageManager) => commandExists(packageManager));
2483
+ }
2484
+
2485
+ function getBunGlobalBinDir(availablePackageManagers = getAvailablePackageManagers()) {
2486
+ if (!availablePackageManagers.includes("bun")) {
2487
+ return undefined;
2488
+ }
2489
+ return readCommandOutput("bun", ["pm", "bin", "-g"]) || join(home, ".bun", "bin");
2490
+ }
2491
+
2492
+ function getNpmGlobalPrefix(availablePackageManagers = getAvailablePackageManagers()) {
2493
+ if (!availablePackageManagers.includes("npm")) {
2494
+ return undefined;
2495
+ }
2496
+ return readCommandOutput("npm", ["prefix", "-g"]);
1956
2497
  }
1957
2498
 
1958
2499
  async function installCliTool(tool) {
@@ -1960,12 +2501,20 @@ async function installCliTool(tool) {
1960
2501
  claude: {
1961
2502
  label: "Claude Code",
1962
2503
  command: "claude",
1963
- packageName: "@anthropic-ai/claude-code"
2504
+ packageName: "@anthropic-ai/claude-code",
2505
+ supportedPackageManagers: ["npm"],
2506
+ installRequirement: "Claude Code 安装需要执行 postinstall;Bun 默认会阻止该脚本,因此需使用 npm。"
1964
2507
  },
1965
2508
  codex: {
1966
2509
  label: "Codex",
1967
2510
  command: "codex",
1968
2511
  packageName: "@openai/codex"
2512
+ },
2513
+ pi: {
2514
+ label: "Pi",
2515
+ command: "pi",
2516
+ packageName: "@earendil-works/pi-coding-agent",
2517
+ skipScripts: true
1969
2518
  }
1970
2519
  }[tool];
1971
2520
 
@@ -1973,10 +2522,40 @@ async function installCliTool(tool) {
1973
2522
  throw new Error(`Unsupported tool: ${tool}`);
1974
2523
  }
1975
2524
 
1976
- const installed = commandExists(toolConfig.command);
2525
+ const commandPath = getCommandPath(toolConfig.command);
2526
+ const installed = Boolean(commandPath);
2527
+ const availablePackageManagers = getAvailablePackageManagers();
2528
+ const packageManagerChoice = chooseCliInstallPackageManager({
2529
+ commandPath,
2530
+ availablePackageManagers,
2531
+ bunGlobalBinDir: getBunGlobalBinDir(availablePackageManagers),
2532
+ npmGlobalPrefix: getNpmGlobalPrefix(availablePackageManagers),
2533
+ platform: getPlatform(),
2534
+ nodeAvailable: commandExists("node"),
2535
+ supportedPackageManagers: toolConfig.supportedPackageManagers
2536
+ });
2537
+
2538
+ if (!packageManagerChoice.packageManager) {
2539
+ const help = getPackageManagerInstallHelp();
2540
+ p.log.warn(`未检测到可用于安装 ${toolConfig.label} 的包管理器。`);
2541
+ if (packageManagerChoice.source === "missing-node") {
2542
+ p.log.message(`${toolConfig.label} 是 Node CLI;使用 Bun 安装前也需要先安装 Node.js。`);
2543
+ }
2544
+ if (toolConfig.installRequirement) {
2545
+ p.log.message(toolConfig.installRequirement);
2546
+ }
2547
+ p.log.message(`${help.platformLabel} 可先安装 Node.js/npm,再重新运行安装。`);
2548
+ p.log.message(`中国大陆镜像: ${help.mainlandUrl}`);
2549
+ p.log.message(`官方下载页: ${help.officialUrl}`);
2550
+ return;
2551
+ }
2552
+
1977
2553
  if (installed) {
2554
+ const managerMessage = packageManagerChoice.source === "existing"
2555
+ ? `检测到原安装方式为 ${packageManagerChoice.packageManager}`
2556
+ : `未识别原安装方式,将使用 ${packageManagerChoice.packageManager}`;
1978
2557
  const shouldUpdate = await confirmOrCancel({
1979
- message: `${toolConfig.label} 已检测到,是否继续执行 npm 强制安装/更新?`,
2558
+ message: `${toolConfig.label} 已检测到(${managerMessage}),是否继续安装/更新?`,
1980
2559
  initialValue: false
1981
2560
  });
1982
2561
  if (!shouldUpdate) {
@@ -1985,10 +2564,11 @@ async function installCliTool(tool) {
1985
2564
  }
1986
2565
  }
1987
2566
 
2567
+ const installCommand = buildCliToolInstallCommand(packageManagerChoice.packageManager, toolConfig);
1988
2568
  const s = p.spinner();
1989
- s.start(`正在安装 ${toolConfig.label}...`);
2569
+ s.start(`正在使用 ${packageManagerChoice.packageManager} 安装 ${toolConfig.label}...`);
1990
2570
  try {
1991
- await runCommand("npm", ["install", "-g", toolConfig.packageName, "--force"]);
2571
+ await runCommand(installCommand.command, installCommand.args);
1992
2572
  s.stop(`${toolConfig.label} 安装完成`);
1993
2573
  } catch (e) {
1994
2574
  s.cancel(c.red(`${toolConfig.label} 安装失败: ${e.message}`));
@@ -2020,6 +2600,12 @@ async function runFullInit(options) {
2020
2600
  if (await confirmOrCancel({ message: "是否配置 Codex 第三方 API?", initialValue: commandExists("codex") })) {
2021
2601
  await configureCodexApi();
2022
2602
  }
2603
+ if (await confirmOrCancel({ message: "是否安装或更新 Pi CLI?", initialValue: false })) {
2604
+ await installCliTool("pi");
2605
+ }
2606
+ if (await confirmOrCancel({ message: "是否配置 Pi gpt 自定义 API?", initialValue: commandExists("pi") })) {
2607
+ await configurePiApi(options.agentsDir);
2608
+ }
2023
2609
  if (await confirmOrCancel({ message: "是否填写 grok-search 环境变量?", initialValue: false })) {
2024
2610
  await configureGrokSearchEnv(options.agentsDir);
2025
2611
  }
@@ -2037,6 +2623,59 @@ async function runInteractiveMenu(options) {
2037
2623
  p.intro(c.bold(c.bgCyan(c.black(" AbelWorkflow Setup "))));
2038
2624
  p.log.message(`工作流目录: ${c.cyan(pathToLabel(options.agentsDir))}`);
2039
2625
 
2626
+ const buildOption = (d) => {
2627
+ const opt = { value: d.value, label: d.label };
2628
+ if (d.hint) {
2629
+ opt.hint = d.hint;
2630
+ }
2631
+ return opt;
2632
+ };
2633
+ const cliToolMenus = {
2634
+ "pi-cli": {
2635
+ tool: "pi",
2636
+ title: "Pi",
2637
+ actions: {
2638
+ "pi-install": async () => installCliTool("pi"),
2639
+ "pi-api": async () => configurePiApi(options.agentsDir)
2640
+ }
2641
+ },
2642
+ "codex-cli": {
2643
+ tool: "codex",
2644
+ title: "Codex",
2645
+ actions: {
2646
+ "codex-install": async () => installCliTool("codex"),
2647
+ "codex-api": async () => configureCodexApi()
2648
+ }
2649
+ },
2650
+ "claude-cli": {
2651
+ tool: "claude",
2652
+ title: "Claude Code",
2653
+ actions: {
2654
+ "claude-install": async () => installCliTool("claude"),
2655
+ "claude-api": async () => configureClaudeApi()
2656
+ }
2657
+ }
2658
+ };
2659
+ const runCliToolMenu = async ({ tool, title, actions }) => {
2660
+ while (true) {
2661
+ const choice = await p.select({
2662
+ message: `请选择 ${title} 操作`,
2663
+ options: buildCliToolMenuDescriptors(tool).map(buildOption),
2664
+ initialValue: `${tool}-install`
2665
+ });
2666
+
2667
+ if (p.isCancel(choice) || choice === "back") {
2668
+ return;
2669
+ }
2670
+
2671
+ const action = actions[choice];
2672
+ if (!action) {
2673
+ p.log.warn(`未知 CLI 工具菜单选项: ${choice}`);
2674
+ continue;
2675
+ }
2676
+ await action();
2677
+ }
2678
+ };
2040
2679
  const menuActions = {
2041
2680
  "full-init": async () => runFullInit(options),
2042
2681
  install: async () => installManagedWorkflow({
@@ -2047,18 +2686,9 @@ async function runInteractiveMenu(options) {
2047
2686
  "grok-search": async () => configureGrokSearchEnv(options.agentsDir),
2048
2687
  context7: async () => configureContext7Env(options.agentsDir),
2049
2688
  "prompt-enhancer": async () => configurePromptEnhancerEnv(options.agentsDir),
2050
- "claude-install": async () => installCliTool("claude"),
2051
- "claude-api": async () => configureClaudeApi(),
2052
- "codex-install": async () => installCliTool("codex"),
2053
- "codex-api": async () => configureCodexApi()
2054
- };
2055
-
2056
- const buildOption = (d) => {
2057
- const opt = { value: d.value, label: d.label };
2058
- if (d.hint) {
2059
- opt.hint = d.hint;
2060
- }
2061
- return opt;
2689
+ "pi-cli": async () => runCliToolMenu(cliToolMenus["pi-cli"]),
2690
+ "codex-cli": async () => runCliToolMenu(cliToolMenus["codex-cli"]),
2691
+ "claude-cli": async () => runCliToolMenu(cliToolMenus["claude-cli"])
2062
2692
  };
2063
2693
 
2064
2694
  while (true) {
@@ -2169,11 +2799,17 @@ async function main() {
2169
2799
 
2170
2800
  export {
2171
2801
  applyClaudePermissionFeature,
2802
+ buildCliToolInstallCommand,
2172
2803
  buildDefaultClaudeSettings,
2173
2804
  buildCodexConfigContent,
2805
+ buildPiModelsConfig,
2806
+ buildPiSettingsConfig,
2807
+ chooseCliInstallPackageManager,
2808
+ getPackageManagerInstallHelp,
2174
2809
  getAugmentContextEnginePromptOptions,
2175
2810
  getRunCommandSpawnOptions,
2176
2811
  hasPromptEnhancerApiConfig,
2812
+ inferPackageManagerFromCommandPath,
2177
2813
  main,
2178
2814
  mergeCodexAuthData,
2179
2815
  mergeClaudeSettingsWithDefaults,
@@ -2181,5 +2817,8 @@ export {
2181
2817
  resolveAugmentContextEngineFeature,
2182
2818
  resolvePromptEnhancerMode,
2183
2819
  resolveExistingCodexApiConfig,
2820
+ parsePiModelIds,
2821
+ resolveExistingPiApiConfig,
2822
+ stripJsonComments,
2184
2823
  updateTomlSectionFields
2185
2824
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelworkflow",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Install AbelWorkflow into ~/.agents and create Claude/Codex symlinks.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -17,6 +17,7 @@
17
17
  "README.md",
18
18
  "commands",
19
19
  "skills",
20
+ "extensions",
20
21
  ".skill-lock.json",
21
22
  ".gitignore"
22
23
  ],