kcode-pi 0.1.12 → 0.1.14

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
@@ -59,6 +59,13 @@ kcode context --refresh
59
59
 
60
60
  ```powershell
61
61
  kcode doctor
62
+ kcode doctor --deep
63
+ ```
64
+
65
+ 修复项目级 KCode 配置:
66
+
67
+ ```powershell
68
+ kcode repair
62
69
  ```
63
70
 
64
71
  查看当前 KCode 版本:
@@ -240,6 +247,17 @@ kcode context --refresh
240
247
 
241
248
  ```powershell
242
249
  kcode doctor
250
+ kcode doctor --deep
251
+ ```
252
+
253
+ `--deep` 会额外检查 Node 版本、npm 全局目录、项目 `.pi/settings.json` 中的重复/旧 KCode package 路径、项目上下文和 active run。
254
+
255
+ ### `kcode repair`
256
+
257
+ 清理当前项目 `.pi/settings.json` 中旧 Node/nvm 版本下的 KCode package 路径,只保留当前安装路径,并刷新项目上下文。
258
+
259
+ ```powershell
260
+ kcode repair
243
261
  ```
244
262
 
245
263
  ### `kcode version`
@@ -276,6 +294,7 @@ kcode start --provider openai --model gpt-4o
276
294
  /kd-status
277
295
  /kd-runs
278
296
  /kd-switch <run-id>
297
+ /kd-resume [补充说明]
279
298
  /kd-finish
280
299
  /kd-gate
281
300
  /kd-advance [phase]
@@ -291,6 +310,15 @@ kcode start --provider openai --model gpt-4o
291
310
 
292
311
  `/kd-start` 会创建新的功能点 run,并立即触发 Agent 进入 `discuss` 阶段。如果未识别出产品画像,例如显示 `(unknown/unknown)`,下一步应先确认产品、版本和技术栈,而不是直接写代码。
293
312
 
313
+ 暂停后接续:
314
+
315
+ ```text
316
+ /kd-resume
317
+ /kd-resume 继续确认采购入库单即时库存检查插件
318
+ ```
319
+
320
+ 重新 `kcode start` 时,如果当前项目存在未完成 active run,KCode 会提示当前功能点和阶段。`/kd-resume` 会读取项目常驻上下文、active run 状态和已落盘的阶段文档,再把完整工作流上下文交给 Agent 继续。
321
+
294
322
  支持的产品画像:
295
323
 
296
324
  ```text
@@ -348,7 +376,7 @@ KCode 会把金蝶开发需求纳入 Harness 工作流。你可以直接输入
348
376
  .pi/kd/runs/<run-id>/evidence/
349
377
  ```
350
378
 
351
- 下次重新 `kcode start` 时,KCode 会读取当前项目的 active run、项目常驻上下文和已生成的阶段文档,再继续当前功能点的当前阶段。已完成或暂停的功能点仍保留在 `.pi/kd/runs/<run-id>/`,可用 `/kd-runs` 查看,用 `/kd-switch <run-id>` 切回。
379
+ 下次重新 `kcode start` 时,KCode 会提示当前项目的 active run。执行 `/kd-resume` 后,KCode 会读取项目常驻上下文、active run 状态和已生成的阶段文档,再继续当前功能点的当前阶段。已完成或暂停的功能点仍保留在 `.pi/kd/runs/<run-id>/`,可用 `/kd-runs` 查看,用 `/kd-switch <run-id>` 切回。
352
380
 
353
381
  阶段含义:
354
382
 
@@ -512,7 +540,7 @@ npm install -g kcode-pi@latest
512
540
 
513
541
  ```powershell
514
542
  kcode init
515
- kcode doctor
543
+ kcode doctor --deep
516
544
  ```
517
545
 
518
546
  ## nvm 与 Node 版本切换
@@ -636,7 +664,7 @@ type .pi\settings.json
636
664
  如果 `packages` 中有多条 `kcode-pi` 路径,运行:
637
665
 
638
666
  ```powershell
639
- kcode init
667
+ kcode repair
640
668
  ```
641
669
 
642
670
  如果使用的是 `0.1.1` 或更早版本,请手工删除旧路径,或升级到最新版。
@@ -11,7 +11,8 @@ export interface PiCliCommand {
11
11
  export declare function runKcodeCli(args: string[], cwd?: string): KcodeCliResult;
12
12
  export declare function initProject(cwd: string): KcodeCliResult;
13
13
  export declare function context(cwd: string, args: string[]): KcodeCliResult;
14
- export declare function doctor(cwd: string): KcodeCliResult;
14
+ export declare function doctor(cwd: string, args?: string[]): KcodeCliResult;
15
+ export declare function repair(cwd: string): KcodeCliResult;
15
16
  export declare function version(): KcodeCliResult;
16
17
  export declare function start(cwd: string, piArgs: string[]): KcodeCliResult;
17
18
  export declare function resolvePiCliCommand(piArgs?: string[]): PiCliCommand | undefined;
package/dist/cli/kcode.js CHANGED
@@ -17,7 +17,9 @@ export function runKcodeCli(args, cwd = process.cwd()) {
17
17
  case "context":
18
18
  return context(cwd, args.slice(1));
19
19
  case "doctor":
20
- return doctor(cwd);
20
+ return doctor(cwd, args.slice(1));
21
+ case "repair":
22
+ return repair(cwd);
21
23
  case "version":
22
24
  case "--version":
23
25
  case "-v":
@@ -55,28 +57,96 @@ export function context(cwd, args) {
55
57
  output: [`项目上下文已${refresh ? "刷新" : "就绪"}:${projectContext.path}`, "", projectContext.content].join("\n"),
56
58
  };
57
59
  }
58
- export function doctor(cwd) {
60
+ export function doctor(cwd, args = []) {
61
+ const deep = args.includes("--deep");
59
62
  const lines = [];
60
63
  const node = spawnSync("node", ["--version"], { encoding: "utf8" });
61
64
  const piCli = resolvePiCliCommand(["--version"]);
62
65
  const pi = piCli ? spawnSync(piCli.command, piCli.args, { encoding: "utf8" }) : undefined;
63
66
  const settingsPath = projectSettingsPath(cwd);
67
+ const projectContextPath = join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md");
68
+ let errors = 0;
69
+ let warnings = 0;
64
70
  lines.push(`Node:${node.status === 0 ? node.stdout.trim() : "未找到"}`);
65
71
  lines.push(`Pi CLI:${formatPiCliStatus(piCli, pi)}`);
66
72
  lines.push(`KCode version:${packageName}@${packageVersion}`);
67
73
  lines.push(`KCode package:${packageRoot}`);
68
74
  lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,请先运行 kcode init"}`);
69
- lines.push(`项目上下文:${existsSync(join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md")) ? join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md") : "未创建,请运行 kcode context"}`);
75
+ lines.push(`项目上下文:${existsSync(projectContextPath) ? projectContextPath : "未创建,请运行 kcode context"}`);
70
76
  if (existsSync(settingsPath)) {
71
- const settings = readSettings(settingsPath);
72
- const hasKcode = (settings.packages ?? []).includes(normalizePath(packageRoot));
73
- lines.push(`KCode package 已登记:${hasKcode ? "是" : "否"}`);
77
+ const settingsResult = readSettingsSafe(settingsPath);
78
+ if (!settingsResult.ok) {
79
+ errors++;
80
+ lines.push(`[ERROR] 项目配置无法解析:${settingsResult.error}`);
81
+ }
82
+ else {
83
+ const packages = settingsResult.settings.packages ?? [];
84
+ const hasKcode = packages.includes(normalizePath(packageRoot));
85
+ lines.push(`KCode package 已登记:${hasKcode ? "是" : "否"}`);
86
+ if (!hasKcode)
87
+ warnings++;
88
+ if (deep) {
89
+ const kcodePackages = packages.filter((pkg) => isSameKcodePackage(pkg, normalizePath(packageRoot)));
90
+ const stalePackages = kcodePackages.filter((pkg) => normalizePath(pkg) !== normalizePath(packageRoot));
91
+ lines.push(`KCode package 条目数:${kcodePackages.length}`);
92
+ if (stalePackages.length > 0) {
93
+ warnings++;
94
+ lines.push(`[WARN] 发现旧 KCode package 路径:${stalePackages.join(" | ")}`);
95
+ lines.push(" 运行 kcode repair 可清理旧路径并刷新项目上下文。");
96
+ }
97
+ }
98
+ }
74
99
  }
100
+ else {
101
+ warnings++;
102
+ }
103
+ if (deep) {
104
+ if (!isSupportedNodeVersion(process.version)) {
105
+ errors++;
106
+ lines.push(`[ERROR] 当前 Node ${process.version} 不满足要求:>=22.19.0`);
107
+ }
108
+ if (!existsSync(projectContextPath)) {
109
+ warnings++;
110
+ lines.push("[WARN] 项目上下文不存在,运行 kcode context --refresh 或 kcode repair。");
111
+ }
112
+ const npmPrefix = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8" });
113
+ const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf8" });
114
+ lines.push(`npm prefix -g:${npmPrefix.status === 0 ? npmPrefix.stdout.trim() : "不可用"}`);
115
+ lines.push(`npm root -g:${npmRoot.status === 0 ? npmRoot.stdout.trim() : "不可用"}`);
116
+ lines.push(`active run:${existsSync(join(cwd, ".pi", "kd", "active-run.json")) ? "存在" : "无"}`);
117
+ }
118
+ if (deep)
119
+ lines.push(`诊断汇总:errors=${errors} warnings=${warnings}`);
75
120
  return {
76
- exitCode: pi?.status === 0 ? 0 : 1,
121
+ exitCode: pi?.status === 0 && errors === 0 ? 0 : 1,
77
122
  output: lines.join("\n"),
78
123
  };
79
124
  }
125
+ export function repair(cwd) {
126
+ const settingsPath = projectSettingsPath(cwd);
127
+ const settingsResult = readSettingsSafe(settingsPath);
128
+ if (!settingsResult.ok) {
129
+ return {
130
+ exitCode: 1,
131
+ output: `项目配置无法解析,未自动覆盖:${settingsPath}\n原因:${settingsResult.error}`,
132
+ };
133
+ }
134
+ const currentPackage = normalizePath(packageRoot);
135
+ const packages = (settingsResult.settings.packages ?? []).filter((pkg) => !isSameKcodePackage(pkg, currentPackage));
136
+ packages.unshift(currentPackage);
137
+ settingsResult.settings.packages = packages;
138
+ mkdirSync(dirname(settingsPath), { recursive: true });
139
+ writeFileSync(settingsPath, `${JSON.stringify(settingsResult.settings, null, 2)}\n`, "utf8");
140
+ const projectContext = writeProjectContext(cwd);
141
+ return {
142
+ exitCode: 0,
143
+ output: [
144
+ `已修复项目级 Pi 配置:${settingsPath}`,
145
+ `已保留当前 KCode package:${currentPackage}`,
146
+ `已刷新项目上下文:${projectContext.path}`,
147
+ ].join("\n"),
148
+ };
149
+ }
80
150
  export function version() {
81
151
  const piCli = resolvePiCliCommand(["--version"]);
82
152
  const pi = piCli ? spawnSync(piCli.command, piCli.args, { encoding: "utf8" }) : undefined;
@@ -136,6 +206,14 @@ function readSettings(path) {
136
206
  return {};
137
207
  return JSON.parse(readFileSync(path, "utf8"));
138
208
  }
209
+ function readSettingsSafe(path) {
210
+ try {
211
+ return { ok: true, settings: readSettings(path) };
212
+ }
213
+ catch (error) {
214
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
215
+ }
216
+ }
139
217
  function normalizePath(path) {
140
218
  return resolve(path);
141
219
  }
@@ -184,6 +262,24 @@ function formatPiCliStatus(piCli, result) {
184
262
  const source = piCli.source === "bundled" ? "随包" : "全局";
185
263
  return `${version}(${source}:${piCli.displayPath})`;
186
264
  }
265
+ function isSupportedNodeVersion(version) {
266
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
267
+ if (!match)
268
+ return false;
269
+ const [, majorText, minorText, patchText] = match;
270
+ const major = Number(majorText);
271
+ const minor = Number(minorText);
272
+ const patch = Number(patchText);
273
+ if (major > 22)
274
+ return true;
275
+ if (major < 22)
276
+ return false;
277
+ if (minor > 19)
278
+ return true;
279
+ if (minor < 19)
280
+ return false;
281
+ return patch >= 0;
282
+ }
187
283
  function piCliPackageVersion(piCli) {
188
284
  if (piCli.source !== "bundled")
189
285
  return undefined;
@@ -204,6 +300,7 @@ function helpText() {
204
300
  " kcode init 初始化当前项目的 .pi/settings.json",
205
301
  " kcode context 生成或刷新 .pi/kd/PROJECT_CONTEXT.md",
206
302
  " kcode doctor 检查 Node、随包 Pi CLI、KCode package 和项目级配置",
303
+ " kcode repair 清理旧 KCode package 路径并刷新项目上下文",
207
304
  " kcode version 显示 KCode、随包 Pi CLI 和 Node 版本",
208
305
  " kcode start 初始化项目配置后启动 KCode 工作环境",
209
306
  ].join("\n");
@@ -35,6 +35,7 @@ npm run smoke:build-debug
35
35
  npm run smoke:sdk-signature
36
36
  npm run smoke:package
37
37
  npm run smoke:kcode-cli
38
+ npm run release:check
38
39
  ```
39
40
 
40
41
  这些 smoke 分别验证:
@@ -107,11 +108,7 @@ npm run build:cli
107
108
  发布检查:
108
109
 
109
110
  ```powershell
110
- npm run check
111
- npm run build:cli
112
- npm run smoke:package
113
- npm run smoke:kcode-cli
114
- npm pack --dry-run
111
+ npm run release:check
115
112
  ```
116
113
 
117
114
  发布:
@@ -77,6 +77,16 @@ function shouldStartHarnessFromInput(text: string): boolean {
77
77
  return KINGDEE_INTENT_PATTERN.test(text);
78
78
  }
79
79
 
80
+ function sendWorkflowPrompt(pi: ExtensionAPI, ctx: ExtensionContext, run: NonNullable<ReturnType<typeof readActiveRun>>, userText: string): void {
81
+ const prompt = workflowPromptForRun(ctx.cwd, run, userText);
82
+ if (ctx.isIdle()) {
83
+ pi.sendUserMessage(prompt);
84
+ return;
85
+ }
86
+ pi.sendUserMessage(prompt, { deliverAs: "followUp" });
87
+ if (ctx.hasUI) ctx.ui.notify("KCode harness message queued.", "info");
88
+ }
89
+
80
90
  function workflowPromptForRun(cwd: string, run: NonNullable<ReturnType<typeof readActiveRun>>, userText: string): string {
81
91
  const status = formatStatus(cwd, run);
82
92
  const memory = workflowMemoryForRun(cwd, run);
@@ -243,7 +253,7 @@ const kdQuestionTool = defineTool({
243
253
  "请先问第一个必须确认的问题,例如:",
244
254
  "kd_question action=ask question=\"采购入库单 Form ID 是否为 pur_receivebill?\" choices=[\"是\", \"不是\"]",
245
255
  "",
246
- "注意:Pi 当前不会弹出表单;问题会显示在对话里,用户在对话中回答后再用 kd_question action=answer 记录。",
256
+ "注意:交互模式下会弹出选择/输入对话并自动记录;非交互模式下用户在对话中回答后再用 kd_question action=answer 记录。",
247
257
  ].join("\n"),
248
258
  },
249
259
  ],
@@ -279,6 +289,19 @@ export default function (pi: ExtensionAPI) {
279
289
  pi.registerTool(kdPlanStatusTool);
280
290
  pi.registerTool(kdQuestionTool);
281
291
 
292
+ pi.on("session_start", async (_event, ctx) => {
293
+ const run = readActiveRun(ctx.cwd);
294
+ if (!run || !ctx.hasUI) return;
295
+ ctx.ui.notify(
296
+ [
297
+ `发现未完成 KCode run:${run.goal ?? run.id}`,
298
+ `阶段:${run.phase},产品:${run.profile?.product ?? run.product ?? "unknown"}`,
299
+ "输入 /kd-resume 可接续;输入 /kd-runs 查看其他功能点。",
300
+ ].join("\n"),
301
+ "info",
302
+ );
303
+ });
304
+
282
305
  pi.on("input", async (event, ctx) => {
283
306
  if (event.source === "extension") return { action: "continue" };
284
307
 
@@ -357,13 +380,27 @@ export default function (pi: ExtensionAPI) {
357
380
 
358
381
  const run = createActiveRun(ctx.cwd, goal, parsed.product, parsed.version);
359
382
  ctx.ui.notify(`Started Kingdee harness run: ${run.id} (${run.profile?.product}/${run.profile?.techStack})`, "info");
360
- const kickoff = `继续 KCode Harness run ${run.id}:${goal}`;
361
- if (ctx.isIdle()) {
362
- pi.sendUserMessage(kickoff);
363
- } else {
364
- pi.sendUserMessage(kickoff, { deliverAs: "followUp" });
365
- ctx.ui.notify("KCode harness kickoff queued.", "info");
383
+ sendWorkflowPrompt(pi, ctx, run, `继续 KCode Harness run ${run.id}:${goal}`);
384
+ },
385
+ });
386
+
387
+ pi.registerCommand("kd-resume", {
388
+ description: "Resume the active Kingdee harness run and inject persisted context",
389
+ handler: async (args, ctx) => {
390
+ const run = requireRun(ctx.cwd);
391
+ if (!run) {
392
+ ctx.ui.notify("No active Kingdee harness run. Use /kd-start <goal> or /kd-runs.", "error");
393
+ return;
366
394
  }
395
+
396
+ const note = args.trim();
397
+ const message = [
398
+ `继续 KCode Harness run ${run.id}:${run.goal ?? "unknown goal"}`,
399
+ note ? `用户补充:${note}` : undefined,
400
+ ]
401
+ .filter(Boolean)
402
+ .join("\n");
403
+ sendWorkflowPrompt(pi, ctx, run, message);
367
404
  },
368
405
  });
369
406
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcode-pi",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Kingdee-specific package and harness for Pi Coding Agent",
5
5
  "type": "module",
6
6
  "private": false,
@@ -64,6 +64,7 @@
64
64
  "smoke:sdk-signature": "tsx scripts/smoke-sdk-signature.ts",
65
65
  "smoke:package": "tsx scripts/smoke-package.ts",
66
66
  "smoke:kcode-cli": "tsx scripts/smoke-kcode-cli.ts",
67
+ "release:check": "tsx scripts/release-check.ts",
67
68
  "kcode": "tsx scripts/kcode.ts",
68
69
  "prepack": "npm run build:cli && npm run smoke:package"
69
70
  },
package/src/cli/kcode.ts CHANGED
@@ -37,7 +37,9 @@ export function runKcodeCli(args: string[], cwd = process.cwd()): KcodeCliResult
37
37
  case "context":
38
38
  return context(cwd, args.slice(1));
39
39
  case "doctor":
40
- return doctor(cwd);
40
+ return doctor(cwd, args.slice(1));
41
+ case "repair":
42
+ return repair(cwd);
41
43
  case "version":
42
44
  case "--version":
43
45
  case "-v":
@@ -81,32 +83,101 @@ export function context(cwd: string, args: string[]): KcodeCliResult {
81
83
  };
82
84
  }
83
85
 
84
- export function doctor(cwd: string): KcodeCliResult {
86
+ export function doctor(cwd: string, args: string[] = []): KcodeCliResult {
87
+ const deep = args.includes("--deep");
85
88
  const lines: string[] = [];
86
89
  const node = spawnSync("node", ["--version"], { encoding: "utf8" });
87
90
  const piCli = resolvePiCliCommand(["--version"]);
88
91
  const pi = piCli ? spawnSync(piCli.command, piCli.args, { encoding: "utf8" }) : undefined;
89
92
  const settingsPath = projectSettingsPath(cwd);
93
+ const projectContextPath = join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md");
94
+ let errors = 0;
95
+ let warnings = 0;
90
96
 
91
97
  lines.push(`Node:${node.status === 0 ? node.stdout.trim() : "未找到"}`);
92
98
  lines.push(`Pi CLI:${formatPiCliStatus(piCli, pi)}`);
93
99
  lines.push(`KCode version:${packageName}@${packageVersion}`);
94
100
  lines.push(`KCode package:${packageRoot}`);
95
101
  lines.push(`项目配置:${existsSync(settingsPath) ? settingsPath : "未创建,请先运行 kcode init"}`);
96
- lines.push(`项目上下文:${existsSync(join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md")) ? join(cwd, ".pi", "kd", "PROJECT_CONTEXT.md") : "未创建,请运行 kcode context"}`);
102
+ lines.push(`项目上下文:${existsSync(projectContextPath) ? projectContextPath : "未创建,请运行 kcode context"}`);
97
103
 
98
104
  if (existsSync(settingsPath)) {
99
- const settings = readSettings(settingsPath);
100
- const hasKcode = (settings.packages ?? []).includes(normalizePath(packageRoot));
101
- lines.push(`KCode package 已登记:${hasKcode ? "是" : "否"}`);
105
+ const settingsResult = readSettingsSafe(settingsPath);
106
+ if (!settingsResult.ok) {
107
+ errors++;
108
+ lines.push(`[ERROR] 项目配置无法解析:${settingsResult.error}`);
109
+ } else {
110
+ const packages = settingsResult.settings.packages ?? [];
111
+ const hasKcode = packages.includes(normalizePath(packageRoot));
112
+ lines.push(`KCode package 已登记:${hasKcode ? "是" : "否"}`);
113
+ if (!hasKcode) warnings++;
114
+ if (deep) {
115
+ const kcodePackages = packages.filter((pkg) => isSameKcodePackage(pkg, normalizePath(packageRoot)));
116
+ const stalePackages = kcodePackages.filter((pkg) => normalizePath(pkg) !== normalizePath(packageRoot));
117
+ lines.push(`KCode package 条目数:${kcodePackages.length}`);
118
+ if (stalePackages.length > 0) {
119
+ warnings++;
120
+ lines.push(`[WARN] 发现旧 KCode package 路径:${stalePackages.join(" | ")}`);
121
+ lines.push(" 运行 kcode repair 可清理旧路径并刷新项目上下文。");
122
+ }
123
+ }
124
+ }
125
+ } else {
126
+ warnings++;
102
127
  }
103
128
 
129
+ if (deep) {
130
+ if (!isSupportedNodeVersion(process.version)) {
131
+ errors++;
132
+ lines.push(`[ERROR] 当前 Node ${process.version} 不满足要求:>=22.19.0`);
133
+ }
134
+ if (!existsSync(projectContextPath)) {
135
+ warnings++;
136
+ lines.push("[WARN] 项目上下文不存在,运行 kcode context --refresh 或 kcode repair。");
137
+ }
138
+ const npmPrefix = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8" });
139
+ const npmRoot = spawnSync("npm", ["root", "-g"], { encoding: "utf8" });
140
+ lines.push(`npm prefix -g:${npmPrefix.status === 0 ? npmPrefix.stdout.trim() : "不可用"}`);
141
+ lines.push(`npm root -g:${npmRoot.status === 0 ? npmRoot.stdout.trim() : "不可用"}`);
142
+ lines.push(`active run:${existsSync(join(cwd, ".pi", "kd", "active-run.json")) ? "存在" : "无"}`);
143
+ }
144
+
145
+ if (deep) lines.push(`诊断汇总:errors=${errors} warnings=${warnings}`);
146
+
104
147
  return {
105
- exitCode: pi?.status === 0 ? 0 : 1,
148
+ exitCode: pi?.status === 0 && errors === 0 ? 0 : 1,
106
149
  output: lines.join("\n"),
107
150
  };
108
151
  }
109
152
 
153
+ export function repair(cwd: string): KcodeCliResult {
154
+ const settingsPath = projectSettingsPath(cwd);
155
+ const settingsResult = readSettingsSafe(settingsPath);
156
+ if (!settingsResult.ok) {
157
+ return {
158
+ exitCode: 1,
159
+ output: `项目配置无法解析,未自动覆盖:${settingsPath}\n原因:${settingsResult.error}`,
160
+ };
161
+ }
162
+
163
+ const currentPackage = normalizePath(packageRoot);
164
+ const packages = (settingsResult.settings.packages ?? []).filter((pkg) => !isSameKcodePackage(pkg, currentPackage));
165
+ packages.unshift(currentPackage);
166
+ settingsResult.settings.packages = packages;
167
+ mkdirSync(dirname(settingsPath), { recursive: true });
168
+ writeFileSync(settingsPath, `${JSON.stringify(settingsResult.settings, null, 2)}\n`, "utf8");
169
+ const projectContext = writeProjectContext(cwd);
170
+
171
+ return {
172
+ exitCode: 0,
173
+ output: [
174
+ `已修复项目级 Pi 配置:${settingsPath}`,
175
+ `已保留当前 KCode package:${currentPackage}`,
176
+ `已刷新项目上下文:${projectContext.path}`,
177
+ ].join("\n"),
178
+ };
179
+ }
180
+
110
181
  export function version(): KcodeCliResult {
111
182
  const piCli = resolvePiCliCommand(["--version"]);
112
183
  const pi = piCli ? spawnSync(piCli.command, piCli.args, { encoding: "utf8" }) : undefined;
@@ -176,6 +247,14 @@ function readSettings(path: string): PiSettings {
176
247
  return JSON.parse(readFileSync(path, "utf8")) as PiSettings;
177
248
  }
178
249
 
250
+ function readSettingsSafe(path: string): { ok: true; settings: PiSettings } | { ok: false; error: string } {
251
+ try {
252
+ return { ok: true, settings: readSettings(path) };
253
+ } catch (error) {
254
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
255
+ }
256
+ }
257
+
179
258
  function normalizePath(path: string): string {
180
259
  return resolve(path);
181
260
  }
@@ -233,6 +312,20 @@ function formatPiCliStatus(
233
312
  return `${version}(${source}:${piCli.displayPath})`;
234
313
  }
235
314
 
315
+ function isSupportedNodeVersion(version: string): boolean {
316
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
317
+ if (!match) return false;
318
+ const [, majorText, minorText, patchText] = match;
319
+ const major = Number(majorText);
320
+ const minor = Number(minorText);
321
+ const patch = Number(patchText);
322
+ if (major > 22) return true;
323
+ if (major < 22) return false;
324
+ if (minor > 19) return true;
325
+ if (minor < 19) return false;
326
+ return patch >= 0;
327
+ }
328
+
236
329
  function piCliPackageVersion(piCli: PiCliCommand): string | undefined {
237
330
  if (piCli.source !== "bundled") return undefined;
238
331
 
@@ -253,6 +346,7 @@ function helpText(): string {
253
346
  " kcode init 初始化当前项目的 .pi/settings.json",
254
347
  " kcode context 生成或刷新 .pi/kd/PROJECT_CONTEXT.md",
255
348
  " kcode doctor 检查 Node、随包 Pi CLI、KCode package 和项目级配置",
349
+ " kcode repair 清理旧 KCode package 路径并刷新项目上下文",
256
350
  " kcode version 显示 KCode、随包 Pi CLI 和 Node 版本",
257
351
  " kcode start 初始化项目配置后启动 KCode 工作环境",
258
352
  ].join("\n");