@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,10 +9,14 @@
9
9
  */
10
10
 
11
11
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
12
- import { join, dirname } from "node:path";
13
- import { fileURLToPath } from "node:url";
12
+ import { join } from "node:path";
14
13
  import type { Api, Model, StopReason, Usage } from "@earendil-works/pi-ai";
15
- import { completeSimple, getSupportedThinkingLevels, type Message, type ThinkingLevel } from "@earendil-works/pi-ai";
14
+ import {
15
+ completeSimple,
16
+ getSupportedThinkingLevels,
17
+ type Message,
18
+ type ThinkingLevel,
19
+ } from "@earendil-works/pi-ai/compat";
16
20
  import {
17
21
  type AgentToolResult,
18
22
  type AgentToolUpdateCallback,
@@ -24,12 +28,7 @@ import {
24
28
  } from "@earendil-works/pi-coding-agent";
25
29
  import type { SelectItem } from "@earendil-works/pi-tui";
26
30
  import { DynamicBorder, type Theme } from "@earendil-works/pi-coding-agent";
27
- import {
28
- Container,
29
- SelectList,
30
- Spacer,
31
- Text,
32
- } from "@earendil-works/pi-tui";
31
+ import { Container, SelectList, Spacer, Text } from "@earendil-works/pi-tui";
33
32
  import { Type } from "typebox";
34
33
 
35
34
  // ============== 常量 ==============
@@ -38,9 +37,20 @@ const TOOL_LABEL = "Advisor";
38
37
  const CHECKMARK = " ✓";
39
38
  const NO_ADVISOR_VALUE = "__no_advisor__";
40
39
  const OFF_VALUE = "__off__";
41
- const BASE_EFFORT_LEVELS: ThinkingLevel[] = ["minimal", "low", "medium", "high"];
40
+ const BASE_EFFORT_LEVELS: ThinkingLevel[] = [
41
+ "minimal",
42
+ "low",
43
+ "medium",
44
+ "high",
45
+ ];
42
46
  const XHIGH_EFFORT_LEVEL: ThinkingLevel = "xhigh";
43
- const EFFORT_ORDINAL: readonly ThinkingLevel[] = ["minimal", "low", "medium", "high", "xhigh"];
47
+ const EFFORT_ORDINAL: readonly ThinkingLevel[] = [
48
+ "minimal",
49
+ "low",
50
+ "medium",
51
+ "high",
52
+ "xhigh",
53
+ ];
44
54
  const DEFAULT_EFFORT: ThinkingLevel = "high";
45
55
  const RECOMMENDED_EFFORT_SUFFIX = " (recommended)";
46
56
 
@@ -49,15 +59,13 @@ const MSG_ADVISOR_DISABLED = "Advisor disabled";
49
59
  const MSG_REQUIRES_INTERACTIVE = "/advisor requires interactive mode";
50
60
  const MSG_ADVISOR_NUDGE = "Please advise on the executor's situation above.";
51
61
  const MSG_PERSIST_FAILED = "Failed to save advisor selection";
52
- const ERR_NO_MODEL = "No advisor model is configured. Use /advisor to enable one.";
62
+ const ERR_NO_MODEL =
63
+ "No advisor model is configured. Use /advisor to enable one.";
53
64
  const ERR_CALL_ABORTED = "Advisor call was cancelled.";
54
65
  const ERR_EMPTY_RESPONSE = "Advisor returned no text content.";
55
66
  const ERR_NO_MODEL_SELECTED = "no advisor model selected";
56
67
  const ERR_EMPTY_RESPONSE_DETAIL = "empty response";
57
68
  const ERR_ABORTED_DETAIL = "aborted";
58
- const ERR_UNKNOWN = "unknown error";
59
-
60
- // 系统提示词
61
69
  const ADVISOR_SYSTEM_PROMPT = [
62
70
  "You are a senior reviewer providing guidance to a working agent.",
63
71
  "The working agent will share their full conversation context with you.",
@@ -70,11 +78,18 @@ const ADVISOR_SYSTEM_PROMPT = [
70
78
  interface AdvisorConfig {
71
79
  modelKey?: string;
72
80
  effort?: ThinkingLevel;
73
- disabledForModels?: Array<string | { model: string; minEffort?: ThinkingLevel }>;
81
+ disabledForModels?: Array<
82
+ string | { model: string; minEffort?: ThinkingLevel }
83
+ >;
74
84
  }
75
85
 
76
86
  function getConfigPath(): string {
77
- const configDir = join(process.env.HOME || process.env.USERPROFILE || "~", ".aicode-cli", "agent", "aicode-advisor");
87
+ const configDir = join(
88
+ process.env.HOME || process.env.USERPROFILE || "~",
89
+ ".aicode-cli",
90
+ "agent",
91
+ "aicode-advisor",
92
+ );
78
93
  if (!existsSync(configDir)) {
79
94
  mkdirSync(configDir, { recursive: true, mode: 0o700 });
80
95
  }
@@ -95,17 +110,20 @@ function loadAdvisorConfig(): AdvisorConfig {
95
110
  return {};
96
111
  }
97
112
 
98
- function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel | undefined): boolean {
113
+ function saveAdvisorConfig(
114
+ key: string | undefined,
115
+ effort: ThinkingLevel | undefined,
116
+ ): boolean {
99
117
  const configPath = getConfigPath();
100
118
  const existing = loadAdvisorConfig();
101
119
  const config: AdvisorConfig = { ...existing };
102
-
120
+
103
121
  if (key) config.modelKey = key;
104
122
  else delete config.modelKey;
105
-
123
+
106
124
  if (effort) config.effort = effort;
107
125
  else delete config.effort;
108
-
126
+
109
127
  try {
110
128
  writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
111
129
  return true;
@@ -114,7 +132,9 @@ function saveAdvisorConfig(key: string | undefined, effort: ThinkingLevel | unde
114
132
  }
115
133
  }
116
134
 
117
- function parseModelKey(key: string): { provider: string; modelId: string } | undefined {
135
+ function parseModelKey(
136
+ key: string,
137
+ ): { provider: string; modelId: string } | undefined {
118
138
  const idx = key.indexOf(":");
119
139
  if (idx < 1) return undefined;
120
140
  return { provider: key.slice(0, idx), modelId: key.slice(idx + 1) };
@@ -123,7 +143,9 @@ function parseModelKey(key: string): { provider: string; modelId: string } | und
123
143
  // ============== 运行时状态 ==============
124
144
  let selectedAdvisor: Model<Api> | undefined;
125
145
  let selectedAdvisorEffort: ThinkingLevel | undefined;
126
- let disabledForModelsCache: Array<string | { model: string; minEffort?: ThinkingLevel }> = [];
146
+ let disabledForModelsCache: Array<
147
+ string | { model: string; minEffort?: ThinkingLevel }
148
+ > = [];
127
149
 
128
150
  function getAdvisorModel(): Model<Api> | undefined {
129
151
  return selectedAdvisor;
@@ -141,11 +163,26 @@ function setAdvisorEffort(effort: ThinkingLevel | undefined): void {
141
163
  selectedAdvisorEffort = effort;
142
164
  }
143
165
 
144
- function setDisabledForModels(models: Array<string | { model: string; minEffort?: ThinkingLevel }>): void {
166
+ function setDisabledForModels(
167
+ models: Array<string | { model: string; minEffort?: ThinkingLevel }>,
168
+ ): void {
145
169
  disabledForModelsCache = models;
146
170
  }
147
171
 
148
172
  // ============== 工具清单序列化 ==============
173
+ /**
174
+ * stableStringify - 稳定序列化对象(键排序)
175
+ *
176
+ * 用于序列化工具参数,确保参数对象的键顺序一致,便于对比和展示。
177
+ *
178
+ * 功能说明:
179
+ * - 处理 null 和非对象类型:直接 JSON.stringify
180
+ * - 处理数组:递归序列化每个元素,undefined 替换为 "null"
181
+ * - 处理对象:键排序后序列化,跳过 undefined 值
182
+ *
183
+ * @param value - 要序列化的值
184
+ * @returns 稳定的 JSON 字符串(键排序)
185
+ */
149
186
  function stableStringify(value: unknown): string {
150
187
  if (value === null || typeof value !== "object") {
151
188
  return JSON.stringify(value);
@@ -165,7 +202,10 @@ function stableStringify(value: unknown): string {
165
202
 
166
203
  function buildInventoryBlock(tools: ToolInfo[]): string {
167
204
  return tools
168
- .map((t) => `### ${t.name}\n${t.description}\n\nParameters: ${stableStringify(t.parameters)}`)
205
+ .map(
206
+ (t) =>
207
+ `### ${t.name}\n${t.description}\n\nParameters: ${stableStringify(t.parameters)}`,
208
+ )
169
209
  .join("\n\n---\n\n");
170
210
  }
171
211
 
@@ -173,7 +213,9 @@ function stripInflightAdvisorCall(messages: Message[]): Message[] {
173
213
  if (messages.length === 0) return messages;
174
214
  const last = messages[messages.length - 1];
175
215
  if (last.role !== "assistant") return messages;
176
- const filtered = last.content.filter((c) => !(c.type === "toolCall" && c.name === ADVISOR_TOOL_NAME));
216
+ const filtered = last.content.filter(
217
+ (c) => !(c.type === "toolCall" && c.name === ADVISOR_TOOL_NAME),
218
+ );
177
219
  if (filtered.length === last.content.length) return messages;
178
220
  if (filtered.length === 0) return messages.slice(0, -1);
179
221
  return [...messages.slice(0, -1), { ...last, content: filtered }];
@@ -193,7 +235,9 @@ function ensureUserTailForAdvisor(messages: Message[]): Message[] {
193
235
 
194
236
  function getInventoryMessage(tools: ToolInfo[]): Message | undefined {
195
237
  if (tools.length === 0) return undefined;
196
- const sorted = [...tools].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
238
+ const sorted = [...tools].sort((a, b) =>
239
+ a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
240
+ );
197
241
  const text = `## Available Executor Tools\n\n${buildInventoryBlock(sorted)}`;
198
242
  return {
199
243
  role: "user",
@@ -215,7 +259,10 @@ function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): void {
215
259
  const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
216
260
  if (!model) {
217
261
  if (ctx.hasUI) {
218
- ctx.ui.notify(`Previously configured advisor model ${config.modelKey} is no longer available`, "warning");
262
+ ctx.ui.notify(
263
+ `Previously configured advisor model ${config.modelKey} is no longer available`,
264
+ "warning",
265
+ );
219
266
  }
220
267
  return;
221
268
  }
@@ -227,7 +274,10 @@ function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): void {
227
274
 
228
275
  if (isExecutorBlocked(ctx, pi.getThinkingLevel())) {
229
276
  if (ctx.hasUI) {
230
- ctx.ui.notify(`Advisor restored: ${model.provider}:${model.id} (inactive for current executor)`, "info");
277
+ ctx.ui.notify(
278
+ `Advisor restored: ${model.provider}:${model.id} (inactive for current executor)`,
279
+ "info",
280
+ );
231
281
  }
232
282
  return;
233
283
  }
@@ -238,7 +288,10 @@ function restoreAdvisorState(ctx: ExtensionContext, pi: ExtensionAPI): void {
238
288
  }
239
289
 
240
290
  if (ctx.hasUI) {
241
- ctx.ui.notify(`Advisor restored: ${model.provider}:${model.id}${config.effort ? `, ${config.effort}` : ""}`, "info");
291
+ ctx.ui.notify(
292
+ `Advisor restored: ${model.provider}:${model.id}${config.effort ? `, ${config.effort}` : ""}`,
293
+ "info",
294
+ );
242
295
  }
243
296
  }
244
297
 
@@ -259,7 +312,9 @@ function buildErrorResult(
259
312
  const effort = getAdvisorEffort();
260
313
  return {
261
314
  content: [{ type: "text", text: userText }],
262
- details: advisorLabel ? { advisorModel: advisorLabel, effort, errorMessage } : { effort, errorMessage },
315
+ details: advisorLabel
316
+ ? { advisorModel: advisorLabel, effort, errorMessage }
317
+ : { effort, errorMessage },
263
318
  };
264
319
  }
265
320
 
@@ -278,22 +333,39 @@ async function executeAdvisor(
278
333
 
279
334
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
280
335
  if (!auth.ok) {
281
- return buildErrorResult(advisorLabel, `Advisor (${advisorLabel}) is misconfigured: ${auth.error}`, auth.error);
336
+ return buildErrorResult(
337
+ advisorLabel,
338
+ `Advisor (${advisorLabel}) is misconfigured: ${auth.error}`,
339
+ auth.error,
340
+ );
282
341
  }
283
342
  if (!auth.apiKey) {
284
- return buildErrorResult(advisorLabel, `Advisor (${advisorLabel}) has no API key available.`, `no API key for ${advisor.provider}`);
343
+ return buildErrorResult(
344
+ advisorLabel,
345
+ `Advisor (${advisorLabel}) has no API key available.`,
346
+ `no API key for ${advisor.provider}`,
347
+ );
285
348
  }
286
349
 
287
350
  const { messages: sessionMessages } = buildSessionContext(
288
351
  ctx.sessionManager.getEntries(),
289
352
  ctx.sessionManager.getLeafId(),
290
353
  );
291
- const branchMessages = ensureUserTailForAdvisor(stripInflightAdvisorCall(convertToLlm(sessionMessages)));
354
+ const branchMessages = ensureUserTailForAdvisor(
355
+ stripInflightAdvisorCall(convertToLlm(sessionMessages)),
356
+ );
292
357
  const inventoryMessage = getInventoryMessage(pi.getAllTools());
293
- const messages: Message[] = inventoryMessage ? [inventoryMessage, ...branchMessages] : branchMessages;
358
+ const messages: Message[] = inventoryMessage
359
+ ? [inventoryMessage, ...branchMessages]
360
+ : branchMessages;
294
361
 
295
362
  onUpdate?.({
296
- content: [{ type: "text", text: `Consulting advisor (${advisorLabel}${effort ? `, ${effort}` : ""})…` }],
363
+ content: [
364
+ {
365
+ type: "text",
366
+ text: `Consulting advisor (${advisorLabel}${effort ? `, ${effort}` : ""})…`,
367
+ },
368
+ ],
297
369
  details: { advisorModel: advisorLabel, effort },
298
370
  });
299
371
 
@@ -319,7 +391,12 @@ async function executeAdvisor(
319
391
 
320
392
  if (response.stopReason === "error") {
321
393
  return {
322
- content: [{ type: "text", text: `Advisor call failed: ${response.errorMessage}` }],
394
+ content: [
395
+ {
396
+ type: "text",
397
+ text: `Advisor call failed: ${response.errorMessage}`,
398
+ },
399
+ ],
323
400
  details: {
324
401
  advisorModel: advisorLabel,
325
402
  effort,
@@ -331,8 +408,11 @@ async function executeAdvisor(
331
408
  }
332
409
 
333
410
  const advisorText = response.content
334
- .filter((c): c is { type: "text"; text: string } => c.type === "text")
335
- .map((c) => c.text)
411
+ .filter(
412
+ (c: { type: string }): c is { type: "text"; text: string } =>
413
+ c.type === "text",
414
+ )
415
+ .map((c: { type: "text"; text: string }) => c.text)
336
416
  .join("\n")
337
417
  .trim();
338
418
 
@@ -360,7 +440,11 @@ async function executeAdvisor(
360
440
  };
361
441
  } catch (err) {
362
442
  const message = err instanceof Error ? err.message : String(err);
363
- return buildErrorResult(advisorLabel, `Advisor call threw: ${message}`, message);
443
+ return buildErrorResult(
444
+ advisorLabel,
445
+ `Advisor call threw: ${message}`,
446
+ message,
447
+ );
364
448
  }
365
449
  }
366
450
 
@@ -428,7 +512,10 @@ function modelKey(m: { provider: string; id: string }): string {
428
512
  return `${m.provider}:${m.id}`;
429
513
  }
430
514
 
431
- function isModelBlocked(model: Model<Api> | undefined, thinkingLevel?: string): boolean {
515
+ function isModelBlocked(
516
+ model: Model<Api> | undefined,
517
+ thinkingLevel?: string,
518
+ ): boolean {
432
519
  if (!model) return false;
433
520
  const key = modelKey(model);
434
521
  for (const entry of disabledForModelsCache) {
@@ -438,14 +525,19 @@ function isModelBlocked(model: Model<Api> | undefined, thinkingLevel?: string):
438
525
  if (entry.model !== key) continue;
439
526
  if (entry.minEffort === undefined) return true;
440
527
  const thresholdOrdinal = EFFORT_ORDINAL.indexOf(entry.minEffort);
441
- const executorOrdinal = EFFORT_ORDINAL.indexOf(thinkingLevel as ThinkingLevel);
528
+ const executorOrdinal = EFFORT_ORDINAL.indexOf(
529
+ thinkingLevel as ThinkingLevel,
530
+ );
442
531
  if (executorOrdinal >= thresholdOrdinal) return true;
443
532
  }
444
533
  }
445
534
  return false;
446
535
  }
447
536
 
448
- function isExecutorBlocked(ctx: ExtensionContext, thinkingLevel?: string): boolean {
537
+ function isExecutorBlocked(
538
+ ctx: ExtensionContext,
539
+ thinkingLevel?: string,
540
+ ): boolean {
449
541
  return isModelBlocked(ctx?.model, thinkingLevel);
450
542
  }
451
543
 
@@ -501,7 +593,8 @@ const EFFORT_HEADER_PROSE =
501
593
 
502
594
  function selectListTheme(theme: Theme) {
503
595
  return {
504
- selectedPrefix: (t: string) => theme.bg("selectedBg", theme.fg("accent", t)),
596
+ selectedPrefix: (t: string) =>
597
+ theme.bg("selectedBg", theme.fg("accent", t)),
505
598
  selectedText: (t: string) => theme.bg("selectedBg", theme.bold(t)),
506
599
  description: (t: string) => theme.fg("muted", t),
507
600
  scrollInfo: (t: string) => theme.fg("dim", t),
@@ -509,7 +602,13 @@ function selectListTheme(theme: Theme) {
509
602
  };
510
603
  }
511
604
 
512
- function buildSelectPanel(theme: Theme, title: string, proseLines: string[], selectList: SelectList, helpLines?: string[]): Container {
605
+ function buildSelectPanel(
606
+ theme: Theme,
607
+ title: string,
608
+ proseLines: string[],
609
+ selectList: SelectList,
610
+ helpLines?: string[],
611
+ ): Container {
513
612
  const container = new Container();
514
613
  const border = () => new DynamicBorder((s: string) => theme.fg("accent", s));
515
614
 
@@ -521,17 +620,19 @@ function buildSelectPanel(theme: Theme, title: string, proseLines: string[], sel
521
620
  container.addChild(new Text(line, 1, 0));
522
621
  container.addChild(new Spacer(1));
523
622
  }
524
-
623
+
525
624
  // 添加帮助信息(如果有)
526
625
  if (helpLines && helpLines.length > 0) {
527
- container.addChild(new Text(theme.fg("accent", theme.bold(helpLines[0])), 1, 0));
626
+ container.addChild(
627
+ new Text(theme.fg("accent", theme.bold(helpLines[0])), 1, 0),
628
+ );
528
629
  container.addChild(new Spacer(1));
529
630
  for (let i = 1; i < helpLines.length; i++) {
530
631
  container.addChild(new Text(helpLines[i], 1, 0));
531
632
  }
532
633
  container.addChild(new Spacer(1));
533
634
  }
534
-
635
+
535
636
  container.addChild(selectList);
536
637
  container.addChild(new Spacer(1));
537
638
  container.addChild(new Text(theme.fg("dim", NAV_HINT), 1, 0));
@@ -545,7 +646,11 @@ function showAdvisorPicker(
545
646
  items: SelectItem[],
546
647
  ): Promise<string | null> {
547
648
  return ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
548
- const selectList = new SelectList(items, Math.min(items.length, MAX_VISIBLE_ROWS), selectListTheme(theme));
649
+ const selectList = new SelectList(
650
+ items,
651
+ Math.min(items.length, MAX_VISIBLE_ROWS),
652
+ selectListTheme(theme),
653
+ );
549
654
  selectList.onSelect = (item) => done(item.value);
550
655
  selectList.onCancel = () => done(null);
551
656
 
@@ -575,15 +680,28 @@ function showEffortPicker(
575
680
  defaultEffort?: ThinkingLevel,
576
681
  ): Promise<string | null> {
577
682
  return ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
578
- const selectList = new SelectList(items, Math.min(items.length, MAX_VISIBLE_ROWS), selectListTheme(theme));
579
- const preferredIdx = currentEffort ? items.findIndex((item) => item.value === currentEffort) : -1;
683
+ const selectList = new SelectList(
684
+ items,
685
+ Math.min(items.length, MAX_VISIBLE_ROWS),
686
+ selectListTheme(theme),
687
+ );
688
+ const preferredIdx = currentEffort
689
+ ? items.findIndex((item) => item.value === currentEffort)
690
+ : -1;
580
691
  selectList.setSelectedIndex(
581
- preferredIdx >= 0 ? preferredIdx : items.findIndex((item) => item.value === defaultEffort),
692
+ preferredIdx >= 0
693
+ ? preferredIdx
694
+ : items.findIndex((item) => item.value === defaultEffort),
582
695
  );
583
696
  selectList.onSelect = (item) => done(item.value);
584
697
  selectList.onCancel = () => done(null);
585
698
 
586
- const container = buildSelectPanel(theme, EFFORT_HEADER_TITLE, [EFFORT_HEADER_PROSE], selectList);
699
+ const container = buildSelectPanel(
700
+ theme,
701
+ EFFORT_HEADER_TITLE,
702
+ [EFFORT_HEADER_PROSE],
703
+ selectList,
704
+ );
587
705
 
588
706
  return {
589
707
  render: (w: number) => container.render(w),
@@ -617,7 +735,8 @@ function registerAdvisorCommand(pi: ExtensionAPI): void {
617
735
  });
618
736
  items.push({
619
737
  value: NO_ADVISOR_VALUE,
620
- label: currentKey === undefined ? `No advisor${CHECKMARK}` : "No advisor",
738
+ label:
739
+ currentKey === undefined ? `No advisor${CHECKMARK}` : "No advisor",
621
740
  });
622
741
 
623
742
  const choice = await showAdvisorPicker(ctx, items);
@@ -659,15 +778,26 @@ function registerAdvisorCommand(pi: ExtensionAPI): void {
659
778
  { value: OFF_VALUE, label: "off" },
660
779
  ...levels.map((level) => ({
661
780
  value: level,
662
- label: level === DEFAULT_EFFORT ? `${level}${RECOMMENDED_EFFORT_SUFFIX}` : level,
781
+ label:
782
+ level === DEFAULT_EFFORT
783
+ ? `${level}${RECOMMENDED_EFFORT_SUFFIX}`
784
+ : level,
663
785
  })),
664
786
  ];
665
787
 
666
- const effortResult = await showEffortPicker(ctx, effortItems, getAdvisorEffort(), DEFAULT_EFFORT);
788
+ const effortResult = await showEffortPicker(
789
+ ctx,
790
+ effortItems,
791
+ getAdvisorEffort(),
792
+ DEFAULT_EFFORT,
793
+ );
667
794
  if (!effortResult) {
668
795
  return;
669
796
  }
670
- effortChoice = effortResult === OFF_VALUE ? undefined : (effortResult as ThinkingLevel);
797
+ effortChoice =
798
+ effortResult === OFF_VALUE
799
+ ? undefined
800
+ : (effortResult as ThinkingLevel);
671
801
  }
672
802
 
673
803
  if (!saveAdvisorConfig(modelKey(picked), effortChoice)) {
@@ -680,15 +810,21 @@ function registerAdvisorCommand(pi: ExtensionAPI): void {
680
810
  const activeToolsNow = pi.getActiveTools();
681
811
  const activeHasNow = activeToolsNow.includes(ADVISOR_TOOL_NAME);
682
812
  const blocked = isExecutorBlocked(ctx, pi.getThinkingLevel());
683
-
813
+
684
814
  if (!activeHasNow && !blocked) {
685
815
  pi.setActiveTools([...activeToolsNow, ADVISOR_TOOL_NAME]);
686
816
  }
687
-
817
+
688
818
  if (blocked) {
689
- ctx.ui.notify(`Advisor: ${modelKey(picked)}${effortChoice ? `, ${effortChoice}` : ""} (inactive for current executor)`, "info");
819
+ ctx.ui.notify(
820
+ `Advisor: ${modelKey(picked)}${effortChoice ? `, ${effortChoice}` : ""} (inactive for current executor)`,
821
+ "info",
822
+ );
690
823
  } else {
691
- ctx.ui.notify(`Advisor: ${modelKey(picked)}${effortChoice ? `, ${effortChoice}` : ""}`, "info");
824
+ ctx.ui.notify(
825
+ `Advisor: ${modelKey(picked)}${effortChoice ? `, ${effortChoice}` : ""}`,
826
+ "info",
827
+ );
692
828
  }
693
829
  },
694
830
  });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * @author: xujianjiang
3
+ * aicode-banner extension
4
+ *
5
+ * 在 `session_start` 触发时打印启动 banner (含快捷命令、常用提示词、技能).
6
+ * 替代原 cli.js 启动时同步 `console.log(generateBanner(...))`:
7
+ * 1) 避开 RPC 模式 stdout 污染 (banner 现在走 TUI notify, 不写 stdout)
8
+ * 2) chdir 到 homedir 之后才触发, 配合 cli.js 入口的防御性 chdir
9
+ * 3) 只在首次 startup 时打印; resume/new/fork 不重复
10
+ *
11
+ * ponytail: 用 ctx.ui.notify 走 TUI 通知路径, 不直写 console.
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+
16
+ const COLOR_THEMES = [
17
+ { name: "deep-cyan", hex: "#2e7d8c" },
18
+ { name: "dark-cyan", hex: "#399aad" },
19
+ { name: "cyan", hex: "#49bccf" },
20
+ { name: "light-cyan", hex: "#5ec9db" },
21
+ { name: "bright-cyan", hex: "#73d6e8" },
22
+ { name: "pale-cyan", hex: "#8ae3f0" },
23
+ { name: "faint-cyan", hex: "#a0f0f8" },
24
+ ];
25
+
26
+ function hexToRgb(hex: string): { r: number; g: number; b: number } {
27
+ const h = hex.replace("#", "");
28
+ return {
29
+ r: parseInt(h.slice(0, 2), 16),
30
+ g: parseInt(h.slice(2, 4), 16),
31
+ b: parseInt(h.slice(4, 6), 16),
32
+ };
33
+ }
34
+
35
+ function getRotatedColorTheme(): string {
36
+ const day = new Date().getDay();
37
+ return COLOR_THEMES[day % COLOR_THEMES.length].hex;
38
+ }
39
+
40
+ // 最小可用 banner: 走 ctx.ui.notify() 渲染, 不依赖 dist/banner.js
41
+ // 保留 cli.js 旧 banner 的核心信息密度, 但走 TUI 而非 stdout
42
+ function buildBanner(themeColor: string, version: string): string {
43
+ const theme =
44
+ themeColor === "random" || themeColor === "rotate"
45
+ ? getRotatedColorTheme()
46
+ : themeColor;
47
+ const rgb = hexToRgb(theme);
48
+ const color = (text: string) =>
49
+ `\x1b[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}\x1b[0m`;
50
+ const dim = (text: string) => `\x1b[2m${text}\x1b[0m`;
51
+ const bold = (text: string) => `\x1b[1m${text}\x1b[0m`;
52
+
53
+ // cwd 用 try/catch 防御 ENOENT (虽然 cli.js 已 chdir 到 homedir, 多一道防御)
54
+ let cwd: string;
55
+ try {
56
+ cwd = process
57
+ .cwd()
58
+ .replace(process.env.HOME || process.env.USERPROFILE || "", "~");
59
+ } catch {
60
+ cwd = "~";
61
+ }
62
+
63
+ return [
64
+ `${color(bold("AICODE CLI"))} ${dim(`v${version}`)}`,
65
+ `${dim("Dir:")} ${dim(cwd)}`,
66
+ "",
67
+ dim(
68
+ "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -",
69
+ ),
70
+ dim(
71
+ " 直接用自然语言描述需求即可 • 复杂任务先用 /plan 生成计划 • @文件路径 指定文件",
72
+ ),
73
+ dim(
74
+ "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -",
75
+ ),
76
+ "",
77
+ bold(" 常用命令"),
78
+ ` ${color("/model")} 切换模型 ${color("/settings")} 设置 ${color("/compact")} 压缩上下文`,
79
+ ` ${color("/new")} 新会话 ${color("/reload")} 重载配置 ${color("/resume")} 恢复会话`,
80
+ ` ${color("/plan")} 计划模式(只读) ${color("/bugs")} 已知bug修复 ${color("/goal")} 目标导向`,
81
+ "",
82
+ bold(" 提示词与技能"),
83
+ ` ${color("/workflow")} 六阶段工作流 ${color("/code-review")} 代码审查 ${color("/commit")} Git提交`,
84
+ ` ${color("/brainstorming")} 头脑风暴 ${color("/skill-creator")} 创建技能`,
85
+ ].join("\n");
86
+ }
87
+
88
+ export default function (pi: ExtensionAPI) {
89
+ const version = process.env.AICODE_CLI_VERSION || "";
90
+ // 主题色从 env 读 (cli.js 启动时已注入 AICODE_BANNER_THEME), 缺省用基准色
91
+ const themeColor = process.env.AICODE_BANNER_THEME || "#49bccf";
92
+
93
+ pi.on("session_start", (event, ctx) => {
94
+ // 只在 startup (新会话) 时打印, 不在 resume/new/fork 重复刷屏
95
+ if (event.reason !== "startup") return;
96
+ // 没有 TUI (纯 RPC/JSON 模式) 不打印 — 不污染 JSON 输出
97
+ if (!ctx.hasUI) return;
98
+
99
+ ctx.ui.notify(buildBanner(themeColor, version), "info");
100
+ });
101
+ }