@ia-ccun/code-agent-cli 0.0.52 → 0.0.53-beta.2

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 (89) hide show
  1. package/bin/cli.js +97 -69
  2. package/config/agent/bin/README.md +102 -0
  3. package/config/agent/extensions/aicode-advisor.ts +197 -61
  4. package/config/agent/extensions/aicode-banner.ts +101 -0
  5. package/config/agent/extensions/aicode-btw.ts +677 -183
  6. package/config/agent/extensions/aicode-cd.ts +1017 -0
  7. package/config/agent/extensions/aicode-grillMe.ts +198 -0
  8. package/config/agent/extensions/aicode-pets.ts +351 -0
  9. package/config/agent/extensions/aicode-plan.ts +2 -0
  10. package/config/agent/extensions/aicode-safety-guard.ts +555 -0
  11. package/config/agent/extensions/aicode-tips.ts +69 -30
  12. package/config/agent/extensions/aicode-todo-progress.ts +547 -0
  13. package/config/agent/extensions/context-monitor.ts +30 -15
  14. package/config/agent/extensions/context.ts +132 -60
  15. package/config/agent/extensions/custom-footer.ts +0 -5
  16. package/config/agent/extensions/custom.ts +119 -109
  17. package/config/agent/extensions/error-monitor.ts +38 -51
  18. package/config/agent/extensions/goal.ts +8 -6
  19. package/config/agent/extensions/msg-debug.ts +103 -112
  20. package/config/agent/extensions/ponytail.ts +441 -0
  21. package/config/agent/extensions/review.ts +365 -164
  22. package/config/agent/extensions/tps.ts +87 -0
  23. package/config/agent/extensions/working-msg.ts +164 -51
  24. package/config/agent/help.md +0 -7
  25. package/config/agent/models.json +7 -7
  26. package/config/agent/settings.json +2 -1
  27. package/config/agent/skills/chroma/LICENSE +21 -0
  28. package/config/agent/skills/chroma/chroma-local/SKILL.md +165 -0
  29. package/config/agent/skills/chroma/chroma-local/cli.md +48 -0
  30. package/config/agent/skills/chroma/chroma-local/data-model.md +103 -0
  31. package/config/agent/skills/chroma/chroma-local/error-handling/python.md +166 -0
  32. package/config/agent/skills/chroma/chroma-local/error-handling/typescript.md +231 -0
  33. package/config/agent/skills/chroma/chroma-local/local-chroma/python.md +95 -0
  34. package/config/agent/skills/chroma/chroma-local/local-chroma/typescript.md +101 -0
  35. package/config/agent/skills/chroma/chroma-local/metadata/python.md +154 -0
  36. package/config/agent/skills/chroma/chroma-local/metadata/typescript.md +153 -0
  37. package/config/agent/skills/chroma/chroma-local/querying/python.md +147 -0
  38. package/config/agent/skills/chroma/chroma-local/querying/typescript.md +157 -0
  39. package/config/agent/skills/chroma/chroma-local/regex/python.md +69 -0
  40. package/config/agent/skills/chroma/chroma-local/regex/typescript.md +71 -0
  41. package/config/agent/skills/chroma/chroma-local/understanding-a-codebase.md +80 -0
  42. package/config/agent/skills/chroma/chroma-local/updating-deleting/python.md +159 -0
  43. package/config/agent/skills/chroma/chroma-local/updating-deleting/typescript.md +228 -0
  44. package/config/agent/skills/milvus-skill/LICENSE +202 -0
  45. package/config/agent/skills/milvus-skill/README.md +37 -0
  46. package/config/agent/skills/milvus-skill/SKILL.md +259 -0
  47. package/config/agent/skills/milvus-skill/references/collection.md +86 -0
  48. package/config/agent/skills/milvus-skill/references/database.md +24 -0
  49. package/config/agent/skills/milvus-skill/references/index.md +69 -0
  50. package/config/agent/skills/milvus-skill/references/partition.md +29 -0
  51. package/config/agent/skills/milvus-skill/references/patterns.md +150 -0
  52. package/config/agent/skills/milvus-skill/references/user-role.md +90 -0
  53. package/config/agent/skills/milvus-skill/references/vector.md +247 -0
  54. package/config/agent/skills/ponytail/SKILL.md +100 -0
  55. package/config/agent/skills/ponytail-audit/SKILL.md +40 -0
  56. package/config/agent/skills/ponytail-debt/SKILL.md +44 -0
  57. package/config/agent/skills/ponytail-help/SKILL.md +68 -0
  58. package/config/agent/skills/ponytail-review/SKILL.md +56 -0
  59. package/config/agent/skills/zread/README.md +115 -0
  60. package/config/agent/skills/zread/SKILL.md +103 -0
  61. package/config/agent/skills/zread/references/stdio-protocol.md +312 -0
  62. package/dist/banner.d.ts.map +1 -1
  63. package/dist/banner.js +8 -1
  64. package/dist/banner.js.map +1 -1
  65. package/dist/config-loader.d.ts.map +1 -1
  66. package/dist/config-loader.js +9 -2
  67. package/dist/config-loader.js.map +1 -1
  68. package/package.json +7 -7
  69. package/scripts/postinstall.js +58 -1
  70. package/scripts/regression/README.md +224 -0
  71. package/scripts/regression/cases/01-version-and-deps.sh +94 -0
  72. package/scripts/regression/cases/02-extension-loading.sh +130 -0
  73. package/scripts/regression/cases/03-rpc-protocol.sh +259 -0
  74. package/scripts/regression/cases/04-prompt-stream.sh +130 -0
  75. package/scripts/regression/cases/05-slash-commands-and-skills.sh +119 -0
  76. package/scripts/regression/cases/06-tool-execution.sh +115 -0
  77. package/scripts/regression/cases/07-pi-new-features.sh +141 -0
  78. package/scripts/regression/lib/common.sh +287 -0
  79. package/scripts/regression/results/.last_pid +1 -0
  80. package/scripts/regression/results/.last_workdir +1 -0
  81. package/scripts/regression/results/02-extension-loading.log +18 -0
  82. package/scripts/regression/results/03-prompt-stream.log +16 -0
  83. package/scripts/regression/results/03-rpc-protocol.log +27 -0
  84. package/scripts/regression/results/04-slash-commands-and-skills.log +11 -0
  85. package/scripts/regression/results/05-tool-execution.log +8 -0
  86. package/scripts/regression/results/06-pi-new-features.log +29 -0
  87. package/scripts/regression/run-all.sh +187 -0
  88. package/config/agent/dangerous-patterns.json +0 -106
  89. package/config/agent/extensions/bash-check.ts +0 -227
@@ -9,9 +9,21 @@
9
9
  * - current context window usage + session totals (tokens/cost)
10
10
  */
11
11
 
12
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, ToolResultEvent } from "@earendil-works/pi-coding-agent";
12
+ import type {
13
+ ExtensionAPI,
14
+ ExtensionCommandContext,
15
+ ExtensionContext,
16
+ ToolResultEvent,
17
+ } from "@earendil-works/pi-coding-agent";
13
18
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
14
- import { Container, Key, Text, matchesKey, type Component, type TUI } from "@earendil-works/pi-tui";
19
+ import {
20
+ Container,
21
+ Key,
22
+ Text,
23
+ matchesKey,
24
+ type Component,
25
+ type TUI,
26
+ } from "@earendil-works/pi-tui";
15
27
  import os from "node:os";
16
28
  import path from "node:path";
17
29
  import fs from "node:fs/promises";
@@ -66,17 +78,25 @@ function getAgentDir(): string {
66
78
  return path.join(os.homedir(), ".pi", "agent");
67
79
  }
68
80
 
69
- async function readFileIfExists(filePath: string): Promise<{ path: string; content: string; bytes: number } | null> {
81
+ async function readFileIfExists(
82
+ filePath: string,
83
+ ): Promise<{ path: string; content: string; bytes: number } | null> {
70
84
  if (!existsSync(filePath)) return null;
71
85
  try {
72
86
  const buf = await fs.readFile(filePath);
73
- return { path: filePath, content: buf.toString("utf8"), bytes: buf.byteLength };
87
+ return {
88
+ path: filePath,
89
+ content: buf.toString("utf8"),
90
+ bytes: buf.byteLength,
91
+ };
74
92
  } catch {
75
93
  return null;
76
94
  }
77
95
  }
78
96
 
79
- async function loadProjectContextFiles(cwd: string): Promise<Array<{ path: string; tokens: number; bytes: number }>> {
97
+ async function loadProjectContextFiles(
98
+ cwd: string,
99
+ ): Promise<Array<{ path: string; tokens: number; bytes: number }>> {
80
100
  const out: Array<{ path: string; tokens: number; bytes: number }> = [];
81
101
  const seen = new Set<string>();
82
102
 
@@ -86,7 +106,11 @@ async function loadProjectContextFiles(cwd: string): Promise<Array<{ path: strin
86
106
  const f = await readFileIfExists(p);
87
107
  if (f && !seen.has(f.path)) {
88
108
  seen.add(f.path);
89
- out.push({ path: f.path, tokens: estimateTokens(f.content), bytes: f.bytes });
109
+ out.push({
110
+ path: f.path,
111
+ tokens: estimateTokens(f.content),
112
+ bytes: f.bytes,
113
+ });
90
114
  // pi loads at most one of those per dir
91
115
  return;
92
116
  }
@@ -125,7 +149,8 @@ function buildSkillIndex(pi: ExtensionAPI, cwd: string): SkillIndexEntry[] {
125
149
  .getCommands()
126
150
  .filter((c) => c.source === "skill")
127
151
  .map((c) => {
128
- const p = c.path ? normalizeReadPath(c.path, cwd) : "";
152
+ // pi 1.1.0: SlashCommandInfo 不再含 path 字段,用 sourceInfo.path 替代
153
+ const p = normalizeReadPath(c.sourceInfo.path, cwd);
129
154
  return {
130
155
  name: normalizeSkillName(c.name),
131
156
  skillFilePath: p,
@@ -225,9 +250,9 @@ function renderUsageBar(
225
250
  if (total <= 0) return "";
226
251
 
227
252
  const toCols = (n: number) => Math.round((n / total) * w);
228
- let sys = toCols(parts.system);
229
- let tools = toCols(parts.tools);
230
- let con = toCols(parts.convo);
253
+ const sys = toCols(parts.system);
254
+ const tools = toCols(parts.tools);
255
+ const con = toCols(parts.convo);
231
256
  let rem = w - sys - tools - con;
232
257
  if (rem < 0) rem = 0;
233
258
  // adjust rounding drift
@@ -246,26 +271,28 @@ function joinComma(items: string[]): string {
246
271
  return items.join(", ");
247
272
  }
248
273
 
249
- function joinCommaStyled(items: string[], renderItem: (item: string) => string, sep: string): string {
274
+ function joinCommaStyled(
275
+ items: string[],
276
+ renderItem: (item: string) => string,
277
+ sep: string,
278
+ ): string {
250
279
  return items.map(renderItem).join(sep);
251
280
  }
252
281
 
253
282
  type ContextViewData = {
254
- usage:
255
- | {
256
- // message-based context usage estimate from ctx.getContextUsage()
257
- messageTokens: number;
258
- contextWindow: number;
259
- // effective usage incl. a rough tool-definition estimate
260
- effectiveTokens: number;
261
- percent: number;
262
- remainingTokens: number;
263
- systemPromptTokens: number;
264
- agentTokens: number;
265
- toolsTokens: number;
266
- activeTools: number;
267
- }
268
- | null;
283
+ usage: {
284
+ // message-based context usage estimate from ctx.getContextUsage()
285
+ messageTokens: number;
286
+ contextWindow: number;
287
+ // effective usage incl. a rough tool-definition estimate
288
+ effectiveTokens: number;
289
+ percent: number;
290
+ remainingTokens: number;
291
+ systemPromptTokens: number;
292
+ agentTokens: number;
293
+ toolsTokens: number;
294
+ activeTools: number;
295
+ } | null;
269
296
  agentFiles: string[];
270
297
  extensions: string[];
271
298
  skills: string[];
@@ -274,16 +301,13 @@ type ContextViewData = {
274
301
  };
275
302
 
276
303
  class ContextView implements Component {
277
- private tui: TUI;
278
304
  private theme: any;
279
305
  private onDone: () => void;
280
306
  private data: ContextViewData;
281
307
  private container: Container;
282
308
  private body: Text;
283
309
  private cachedWidth?: number;
284
-
285
- constructor(tui: TUI, theme: any, data: ContextViewData, onDone: () => void) {
286
- this.tui = tui;
310
+ constructor(_tui: TUI, theme: any, data: ContextViewData, onDone: () => void) {
287
311
  this.theme = theme;
288
312
  this.data = data;
289
313
  this.onDone = onDone;
@@ -292,7 +316,8 @@ class ContextView implements Component {
292
316
  this.container.addChild(new DynamicBorder((s) => theme.fg("accent", s)));
293
317
  this.container.addChild(
294
318
  new Text(
295
- theme.fg("accent", theme.bold("Context")) + theme.fg("dim", " (Esc/q/Enter to close)"),
319
+ theme.fg("accent", theme.bold("Context")) +
320
+ theme.fg("dim", " (Esc/q/Enter to close)"),
296
321
  1,
297
322
  0,
298
323
  ),
@@ -320,8 +345,12 @@ class ContextView implements Component {
320
345
  const u = this.data.usage;
321
346
  lines.push(
322
347
  muted("Window: ") +
323
- text(`~${u.effectiveTokens.toLocaleString()} / ${u.contextWindow.toLocaleString()}`) +
324
- muted(` (${u.percent.toFixed(1)}% used, ~${u.remainingTokens.toLocaleString()} left)`),
348
+ text(
349
+ `~${u.effectiveTokens.toLocaleString()} / ${u.contextWindow.toLocaleString()}`,
350
+ ) +
351
+ muted(
352
+ ` (${u.percent.toFixed(1)}% used, ~${u.remainingTokens.toLocaleString()} left)`,
353
+ ),
325
354
  );
326
355
 
327
356
  // bar width tries to fit within the viewport
@@ -374,15 +403,28 @@ class ContextView implements Component {
374
403
  );
375
404
  }
376
405
 
377
- lines.push(muted(`AGENTS (${this.data.agentFiles.length}): `) + text(this.data.agentFiles.length ? joinComma(this.data.agentFiles) : "(none)"));
406
+ lines.push(
407
+ muted(`AGENTS (${this.data.agentFiles.length}): `) +
408
+ text(
409
+ this.data.agentFiles.length ? joinComma(this.data.agentFiles) : "(none)",
410
+ ),
411
+ );
378
412
  lines.push("");
379
- lines.push(muted(`Extensions (${this.data.extensions.length}): `) + text(this.data.extensions.length ? joinComma(this.data.extensions) : "(none)"));
413
+ lines.push(
414
+ muted(`Extensions (${this.data.extensions.length}): `) +
415
+ text(
416
+ this.data.extensions.length ? joinComma(this.data.extensions) : "(none)",
417
+ ),
418
+ );
380
419
 
381
420
  const loaded = new Set(this.data.loadedSkills);
382
421
  const skillsRendered = this.data.skills.length
383
422
  ? joinCommaStyled(
384
423
  this.data.skills,
385
- (name) => (loaded.has(name) ? this.theme.fg("success", name) : this.theme.fg("muted", name)),
424
+ (name) =>
425
+ loaded.has(name)
426
+ ? this.theme.fg("success", name)
427
+ : this.theme.fg("muted", name),
386
428
  this.theme.fg("muted", ", "),
387
429
  )
388
430
  : "(none)";
@@ -444,7 +486,10 @@ export default function contextExtension(pi: ExtensionAPI) {
444
486
  let best: SkillIndexEntry | null = null;
445
487
  for (const s of cachedSkillIndex) {
446
488
  if (!s.skillDir) continue;
447
- if (absPath === s.skillFilePath || absPath.startsWith(s.skillDir + path.sep)) {
489
+ if (
490
+ absPath === s.skillFilePath ||
491
+ absPath.startsWith(s.skillDir + path.sep)
492
+ ) {
448
493
  if (!best || s.skillDir.length > best.skillDir.length) best = s;
449
494
  }
450
495
  }
@@ -467,7 +512,10 @@ export default function contextExtension(pi: ExtensionAPI) {
467
512
 
468
513
  if (!cachedLoadedSkills.has(skillName)) {
469
514
  cachedLoadedSkills.add(skillName);
470
- pi.appendEntry<SkillLoadedEntryData>(SKILL_LOADED_ENTRY, { name: skillName, path: abs });
515
+ pi.appendEntry<SkillLoadedEntryData>(SKILL_LOADED_ENTRY, {
516
+ name: skillName,
517
+ path: abs,
518
+ });
471
519
  }
472
520
  });
473
521
 
@@ -480,7 +528,8 @@ export default function contextExtension(pi: ExtensionAPI) {
480
528
 
481
529
  const extensionsByPath = new Map<string, string[]>();
482
530
  for (const c of extensionCmds) {
483
- const p = c.path ?? "<unknown>";
531
+ // pi 1.1.0: SlashCommandInfo 不再含 path 字段
532
+ const p = c.sourceInfo.path;
484
533
  const arr = extensionsByPath.get(p) ?? [];
485
534
  arr.push(c.name);
486
535
  extensionsByPath.set(p, arr);
@@ -509,7 +558,9 @@ export default function contextExtension(pi: ExtensionAPI) {
509
558
  // factor to account for parameters/schema/formatting.
510
559
  const TOOL_FUDGE = 1.5;
511
560
  const activeToolNames = pi.getActiveTools();
512
- const toolInfoByName = new Map(pi.getAllTools().map((t) => [t.name, t] as const));
561
+ const toolInfoByName = new Map(
562
+ pi.getAllTools().map((t) => [t.name, t] as const),
563
+ );
513
564
  let toolsTokens = 0;
514
565
  for (const name of activeToolNames) {
515
566
  const info = toolInfoByName.get(name);
@@ -520,7 +571,8 @@ export default function contextExtension(pi: ExtensionAPI) {
520
571
 
521
572
  const effectiveTokens = messageTokens + toolsTokens;
522
573
  const percent = ctxWindow > 0 ? (effectiveTokens / ctxWindow) * 100 : 0;
523
- const remainingTokens = ctxWindow > 0 ? Math.max(0, ctxWindow - effectiveTokens) : 0;
574
+ const remainingTokens =
575
+ ctxWindow > 0 ? Math.max(0, ctxWindow - effectiveTokens) : 0;
524
576
 
525
577
  const sessionUsage = sumSessionUsage(ctx);
526
578
 
@@ -534,41 +586,61 @@ export default function contextExtension(pi: ExtensionAPI) {
534
586
  } else {
535
587
  lines.push("Window: (unknown)");
536
588
  }
537
- lines.push(`System: ~${systemPromptTokens.toLocaleString()} tok (AGENTS ~${agentTokens.toLocaleString()})`);
538
- lines.push(`Tools: ~${toolsTokens.toLocaleString()} tok (${activeToolNames.length} active)`);
539
- lines.push(`AGENTS: ${agentFilePaths.length ? joinComma(agentFilePaths) : "(none)"}`);
540
- lines.push(`Extensions (${extensionFiles.length}): ${extensionFiles.length ? joinComma(extensionFiles) : "(none)"}`);
541
- lines.push(`Skills (${skills.length}): ${skills.length ? joinComma(skills) : "(none)"}`);
542
- lines.push(`Session: ${sessionUsage.totalTokens.toLocaleString()} tokens · ${formatUsd(sessionUsage.totalCost)}`);
589
+ lines.push(
590
+ `System: ~${systemPromptTokens.toLocaleString()} tok (AGENTS ~${agentTokens.toLocaleString()})`,
591
+ );
592
+ lines.push(
593
+ `Tools: ~${toolsTokens.toLocaleString()} tok (${activeToolNames.length} active)`,
594
+ );
595
+ lines.push(
596
+ `AGENTS: ${agentFilePaths.length ? joinComma(agentFilePaths) : "(none)"}`,
597
+ );
598
+ lines.push(
599
+ `Extensions (${extensionFiles.length}): ${extensionFiles.length ? joinComma(extensionFiles) : "(none)"}`,
600
+ );
601
+ lines.push(
602
+ `Skills (${skills.length}): ${skills.length ? joinComma(skills) : "(none)"}`,
603
+ );
604
+ lines.push(
605
+ `Session: ${sessionUsage.totalTokens.toLocaleString()} tokens · ${formatUsd(sessionUsage.totalCost)}`,
606
+ );
543
607
  return lines.join("\n");
544
608
  };
545
609
 
546
610
  if (!ctx.hasUI) {
547
- pi.sendMessage({ customType: "context", content: makePlainText(), display: true }, { triggerTurn: false });
611
+ pi.sendMessage(
612
+ { customType: "context", content: makePlainText(), display: true },
613
+ { triggerTurn: false },
614
+ );
548
615
  return;
549
616
  }
550
617
 
551
- const loadedSkills = Array.from(getLoadedSkillsFromSession(ctx)).sort((a, b) => a.localeCompare(b));
618
+ const loadedSkills = Array.from(getLoadedSkillsFromSession(ctx)).sort(
619
+ (a, b) => a.localeCompare(b),
620
+ );
552
621
 
553
622
  const viewData: ContextViewData = {
554
623
  usage: usage
555
624
  ? {
556
- messageTokens,
557
- contextWindow: ctxWindow,
558
- effectiveTokens,
559
- percent,
560
- remainingTokens,
561
- systemPromptTokens,
562
- agentTokens,
563
- toolsTokens,
564
- activeTools: activeToolNames.length,
565
- }
625
+ messageTokens,
626
+ contextWindow: ctxWindow,
627
+ effectiveTokens,
628
+ percent,
629
+ remainingTokens,
630
+ systemPromptTokens,
631
+ agentTokens,
632
+ toolsTokens,
633
+ activeTools: activeToolNames.length,
634
+ }
566
635
  : null,
567
636
  agentFiles: agentFilePaths,
568
637
  extensions: extensionFiles,
569
638
  skills,
570
639
  loadedSkills,
571
- session: { totalTokens: sessionUsage.totalTokens, totalCost: sessionUsage.totalCost },
640
+ session: {
641
+ totalTokens: sessionUsage.totalTokens,
642
+ totalCost: sessionUsage.totalCost,
643
+ },
572
644
  };
573
645
 
574
646
  await ctx.ui.custom<void>((tui, theme, _kb, done) => {
@@ -108,11 +108,6 @@ export default function (pi: ExtensionAPI) {
108
108
 
109
109
  // ── Assemble: figure out what fits ──────────────────────
110
110
  // Always show: path | ctx | model. Stats only if room.
111
- const fixedRight = ctxRaw + modelRaw;
112
- const fixedRightWidth = visibleWidth(fixedRight);
113
- const sepsNeeded = statsRaw ? 3 : 2; // number of separators
114
- const sepsWidth = sepsNeeded * sepWidth;
115
-
116
111
  // Available width for path after reserving right segments
117
112
  const rightBlockWidth = (statsRaw ? visibleWidth(statsRaw) + sepWidth : 0)
118
113
  + visibleWidth(ctxRaw) + sepWidth + visibleWidth(modelRaw);
@@ -2,126 +2,108 @@
2
2
  * @author: xujianjiang
3
3
  * 自定义扩展示例
4
4
  *
5
- * 放置位置: config/agent/extensions/
5
+ * 放置位置:config/agent/extensions/
6
6
  * 安装后会复制到 ~/.pi/agent/extensions/
7
7
  *
8
8
  * 可以拦截内置工具、注册自定义工具和命令
9
9
  */
10
10
 
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
- import type { AssistantMessage } from "@earendil-works/pi-ai";
13
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
14
12
 
15
13
  export default function (pi: ExtensionAPI) {
16
- // === 记录开始时间 ===
17
- let agentStartTime = 0;
18
-
19
- pi.on("agent_start", async () => {
20
- agentStartTime = Date.now();
21
- });
22
-
23
- // === 消息完成通知(只在最后一条显示)===
14
+ // === 拦截工具调用 ===
24
15
 
25
- pi.on("agent_end", async (event, ctx) => {
16
+ pi.on("tool_call", async (event, ctx) => {
26
17
  try {
27
- // 计算耗时
28
- const elapsed = Date.now() - agentStartTime;
29
- let timeStr: string;
30
- if (elapsed < 1000) {
31
- timeStr = `${elapsed}ms`;
32
- } else if (elapsed < 60000) {
33
- timeStr = `${(elapsed / 1000).toFixed(1)}s`;
34
- } else {
35
- const mins = Math.floor(elapsed / 60000);
36
- const secs = Math.floor((elapsed % 60000) / 1000);
37
- timeStr = `${mins}m ${secs}s`;
18
+ // 拦截 login 工具 - 阻止执行
19
+ if (event.toolName === "login") {
20
+ await ctx.ui.notify("⚠️ login 功能已禁用", "warning");
21
+ return { block: true, reason: "login 功能已禁用" };
38
22
  }
39
23
 
40
- // 格式化完整时间
41
- const now = new Date();
42
- const timeStr2 = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
43
-
44
- // agent_end 表示一轮对话结束,显示完成提示
45
- ctx.ui.notify(`${timeStr2} | ✨ 消息处理完成 (${timeStr})`, "info");
24
+ // 拦截 logout 工具 - 阻止执行
25
+ if (event.toolName === "logout") {
26
+ await ctx.ui.notify("⚠️ logout 功能已禁用", "warning");
27
+ return { block: true, reason: "logout 功能已禁用" };
28
+ }
46
29
  } catch (e) {
47
30
  // 忽略错误(ctx 可能已过期)
48
31
  }
49
- });
50
-
51
- // === 拦截工具调用 ===
52
-
53
- pi.on("tool_call", async (event, ctx) => {
54
- // 拦截 login 工具 - 阻止执行
55
- if (event.toolName === "login") {
56
- await ctx.ui.notify("⚠️ login 功能已禁用", "warning");
57
- return { block: true, reason: "login 功能已禁用" };
58
- }
59
-
60
- // 拦截 logout 工具 - 阻止执行
61
- if (event.toolName === "logout") {
62
- await ctx.ui.notify("⚠️ logout 功能已禁用", "warning");
63
- return { block: true, reason: "logout 功能已禁用" };
64
- }
32
+ return;
65
33
  });
66
34
 
67
35
  // === 注册自定义命令 ===
68
36
 
69
37
  pi.registerCommand("hello", {
70
38
  description: "打招呼命令",
71
- usage: "/hello [名字]",
72
39
  handler: async (args, ctx) => {
73
- const name = args || "大佬,您吃了吗";
74
- ctx.ui.notify(`你好,${name}!🤝`, "info");
40
+ try {
41
+ const name = args || "大佬,您吃了吗";
42
+ ctx.ui.notify(`你好,${name}!🤝`, "info");
43
+ } catch (e) {
44
+ // 忽略错误
45
+ }
75
46
  },
76
47
  });
77
48
 
78
49
  pi.registerCommand("status", {
79
50
  description: "显示当前会话状态",
80
51
  handler: async (_args, ctx) => {
81
- const info = `
52
+ try {
53
+ // 保存需要的数据,避免多次触发 getter
54
+ const model = ctx.model?.name || "未知";
55
+ const entries = ctx.sessionManager.getEntries();
56
+ const messages = entries.filter((e) => e.type === "message").length;
57
+
58
+ const info = `
82
59
  📊 会话状态
83
60
 
84
- - 当前模型: ${ctx.session.model?.name || "未知"}
85
- - 项目目录: ${ctx.session.workspace || "未知"}
86
- - 消息数量: ${ctx.session.messages?.length || 0}
61
+ - 当前模型:${model}
62
+ - 消息数量:${messages}
87
63
  `.trim();
88
- ctx.ui.notify(info, "info");
64
+ ctx.ui.notify(info, "info");
65
+ } catch (e) {
66
+ // 忽略错误(ctx 可能已过期)
67
+ }
89
68
  },
90
69
  });
91
70
 
92
71
  pi.registerCommand("clear", {
93
72
  description: "清理当前会话上下文",
94
- usage: "/clear",
95
73
  handler: async (_args, ctx) => {
96
- // 获取当前会话条目数
97
- const entries = ctx.sessionManager.getBranch();
98
- const msgCount = entries.filter(e => e.type === "message").length;
99
-
100
- if (msgCount === 0) {
101
- ctx.ui.notify("会话已经是空的", "info");
102
- return;
103
- }
104
-
105
- // 确认清理
106
- const ok = await ctx.ui.confirm("清理会话", `确定要清理当前会话的所有 ${msgCount} 条消息吗?`);
107
- if (!ok) {
108
- ctx.ui.notify("已取消", "info");
109
- return;
110
- }
74
+ try {
75
+ // 获取当前会话条目数(在 await 前完成所有 ctx 访问)
76
+ const entries = ctx.sessionManager.getBranch();
77
+ const msgCount = entries.filter((e) => e.type === "message").length;
78
+ const uiContext = ctx.ui; // ← 保存 UI context
79
+
80
+ if (msgCount === 0) {
81
+ uiContext.notify("会话已经是空的", "info");
82
+ return;
83
+ }
111
84
 
112
- // 创建新会话来清空上下文
113
- const result = await ctx.newSession({
114
- setup: async (_sm) => {
115
- // 新会话默认为空
116
- },
117
- });
85
+ // 确认清理
86
+ const ok = await uiContext.confirm(
87
+ "清理会话",
88
+ `确定要清理当前会话的所有 ${msgCount} 条消息吗?`,
89
+ );
90
+ if (!ok) {
91
+ uiContext.notify("已取消", "info");
92
+ return;
93
+ }
118
94
 
119
- if (result.cancelled) {
120
- ctx.ui.notify("已取消", "info");
121
- return;
95
+ // 创建新会话来清空上下文
96
+ await ctx.newSession({
97
+ withSession: async (newCtx) => {
98
+ newCtx.ui.notify(
99
+ `✅ 已清理 ${msgCount} 条消息,进入新会话`,
100
+ "info",
101
+ );
102
+ },
103
+ });
104
+ } catch (e) {
105
+ // 忽略错误(ctx 可能已过期)
122
106
  }
123
-
124
- ctx.ui.notify(`✅ 已清理 ${msgCount} 条消息,进入新会话`, "success");
125
107
  },
126
108
  });
127
109
 
@@ -129,10 +111,13 @@ export default function (pi: ExtensionAPI) {
129
111
 
130
112
  pi.registerCommand("exit", {
131
113
  description: "退出当前会话",
132
- usage: "/exit",
133
114
  handler: async (_args, ctx) => {
134
- ctx.ui.notify("👋 再见!", "info");
135
- ctx.shutdown();
115
+ try {
116
+ ctx.ui.notify("👋 再见!", "info");
117
+ ctx.shutdown();
118
+ } catch (e) {
119
+ // 忽略错误
120
+ }
136
121
  },
137
122
  });
138
123
 
@@ -140,36 +125,52 @@ export default function (pi: ExtensionAPI) {
140
125
 
141
126
  pi.registerCommand("update", {
142
127
  description: "更新 aicode-cli 工具",
143
- usage: "/update",
144
128
  handler: async (_args, ctx) => {
145
- const ok = await ctx.ui.confirm("更新确认", "确定要更新 aicode-cli 吗?");
146
- if (!ok) {
147
- ctx.ui.notify("已取消", "info");
148
- return;
149
- }
150
-
151
- ctx.ui.notify("🔄 正在更新 aicode-cli...", "info");
152
- const { exec } = await import("child_process");
153
- exec("npm install -g @ia-ccun/code-agent-cli", (error, stdout, stderr) => {
154
- if (error) {
155
- ctx.ui.notify(`❌ 更新失败: ${error.message},请重试`, "error");
156
- } else {
157
- ctx.ui.notify("✅ aicode-cli 更新成功!请重启 CLI", "success");
129
+ try {
130
+ // ← 在 await 前保存 UI context
131
+ const uiContext = ctx.ui;
132
+
133
+ const ok = await uiContext.confirm(
134
+ "更新确认",
135
+ "确定要更新 aicode-cli 吗?",
136
+ );
137
+ if (!ok) {
138
+ uiContext.notify("已取消", "info");
139
+ return;
158
140
  }
159
- });
141
+
142
+ uiContext.notify("🔄 正在更新 aicode-cli...", "info");
143
+
144
+ // 使用 Promise 包装 exec
145
+ const { exec } = await import("child_process");
146
+ await new Promise<void>((resolve) => {
147
+ exec("npm install -g @ia-ccun/code-agent-cli", (error) => {
148
+ if (error) {
149
+ uiContext.notify(
150
+ `❌ 更新失败:${error.message},请重试`,
151
+ "error",
152
+ );
153
+ } else {
154
+ uiContext.notify("✅ aicode-cli 更新成功!请重启 CLI", "info");
155
+ }
156
+ resolve();
157
+ });
158
+ });
159
+ } catch (e) {
160
+ // 忽略错误(ctx 可能已过期)
161
+ }
160
162
  },
161
163
  });
162
164
 
163
165
  // === 注册自定义工具 ===
164
166
 
165
- // 注意: 注册自定义工具需要更复杂的参数定义
166
- // 详情请参考: https://github.com/mariozechner/pi-coding-agent/docs/extensions.md
167
+ // 注意:注册自定义工具需要更复杂的参数定义
168
+ // 详情请参考:https://github.com/mariozechner/pi-coding-agent/docs/extensions.md
167
169
 
168
170
  // === 注册 /version 命令 ===
169
171
 
170
172
  pi.registerCommand("version", {
171
173
  description: "查看当前版本和最新版本",
172
- usage: "/version",
173
174
  handler: async (_args, ctx) => {
174
175
  const { execSync } = await import("child_process");
175
176
  const { readFileSync, existsSync } = await import("fs");
@@ -193,18 +194,27 @@ export default function (pi: ExtensionAPI) {
193
194
  let latestVersion = "未知";
194
195
  let updateMsg = "";
195
196
  try {
196
- const registry = execSync("npm config get registry", { encoding: "utf8" }).trim();
197
- latestVersion = execSync(`npm view @ia-ccun/code-agent-cli version --registry=${registry}`, { encoding: "utf8" }).trim();
197
+ const registry = execSync("npm config get registry", {
198
+ encoding: "utf8",
199
+ }).trim();
200
+ latestVersion = execSync(
201
+ `npm view @ia-ccun/code-agent-cli version --registry=${registry}`,
202
+ { encoding: "utf8" },
203
+ ).trim();
198
204
 
199
205
  // 比较版本
200
206
  const localParts = localVersion.split(".").map(Number);
201
207
  const latestParts = latestVersion.split(".").map(Number);
202
- const needsUpdate = latestParts[0] > localParts[0] ||
203
- (latestParts[0] === localParts[0] && latestParts[1] > localParts[1]) ||
204
- (latestParts[0] === localParts[0] && latestParts[1] === localParts[1] && latestParts[2] > localParts[2]);
208
+ const needsUpdate =
209
+ latestParts[0] > localParts[0] ||
210
+ (latestParts[0] === localParts[0] &&
211
+ latestParts[1] > localParts[1]) ||
212
+ (latestParts[0] === localParts[0] &&
213
+ latestParts[1] === localParts[1] &&
214
+ latestParts[2] > localParts[2]);
205
215
 
206
216
  if (needsUpdate) {
207
- updateMsg = `\n\n⚠️ 发现新版本 v${localVersion} → v${latestVersion}\n 运行: npm install -g @ia-ccun/code-agent-cli`;
217
+ updateMsg = `\n\n⚠️ 发现新版本 v${localVersion} → v${latestVersion}\n 运行:npm install -g @ia-ccun/code-agent-cli`;
208
218
  } else {
209
219
  updateMsg = "\n\n✅ 已是最新版本";
210
220
  }
@@ -215,8 +225,8 @@ export default function (pi: ExtensionAPI) {
215
225
  const info = `
216
226
  📦 版本信息
217
227
 
218
- 当前版本: v${localVersion}
219
- 最新版本: v${latestVersion}
228
+ 当前版本:v${localVersion}
229
+ 最新版本:v${latestVersion}
220
230
  ${updateMsg}
221
231
  `.trim();
222
232