@ia-ccun/code-agent-cli 0.0.52-beta.3 → 0.0.52-beta.5

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.
@@ -0,0 +1,198 @@
1
+ /**
2
+ * @author: xujianjiang
3
+ * /grill-me 扩展 — 确定性设计访谈,无情质问计划的每个细节,直到达成共识
4
+ *
5
+ * 功能说明:
6
+ * - 启动一个设计访谈会话,AI 无情地质问用户的计划
7
+ * - 逐个问题推进,每个问题提供推荐答案
8
+ * - 如果问题可通过代码库探索回答,则先探索代码库
9
+ * - 记录每个问答回合到状态文件
10
+ * - 最终将结果导出为 Markdown 文件
11
+ *
12
+ * 使用方法:
13
+ * /grill-me [plan] 启动访谈,传入计划描述
14
+ * grill_record_turn 记录一个问答回合(工具)
15
+ * grill_save_results 保存结果为 Markdown(工具)
16
+ */
17
+
18
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+ import { Type } from "typebox";
20
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
21
+ import { join, resolve } from "node:path";
22
+
23
+ interface GrillTurn {
24
+ question: string;
25
+ recommendedAnswer: string;
26
+ userAnswer?: string;
27
+ decisionStatus: "resolved" | "open" | "needs-codebase-check";
28
+ notes?: string;
29
+ }
30
+
31
+ interface GrillState {
32
+ createdAt: string;
33
+ updatedAt: string;
34
+ projectDir: string;
35
+ plan: string;
36
+ turns: GrillTurn[];
37
+ }
38
+
39
+ const RecordTurnParams = Type.Object({
40
+ question: Type.String({ description: "The exact question asked, one question only." }),
41
+ recommendedAnswer: Type.String({ description: "The assistant's recommended answer to the question." }),
42
+ userAnswer: Type.Optional(Type.String({ description: "The user's answer, if already provided." })),
43
+ decisionStatus: Type.Union([
44
+ Type.Literal("resolved"),
45
+ Type.Literal("open"),
46
+ Type.Literal("needs-codebase-check"),
47
+ ]),
48
+ notes: Type.Optional(Type.String({ description: "Short rationale, dependency, or follow-up notes." })),
49
+ });
50
+
51
+ const SaveResultsParams = Type.Object({
52
+ path: Type.Optional(Type.String({ description: "Relative output path. Defaults to GRILL-ME.md." })),
53
+ summary: Type.Optional(Type.String({ description: "Optional current shared-understanding summary." })),
54
+ agreedDecisions: Type.Optional(Type.Array(Type.String())),
55
+ openRisks: Type.Optional(Type.Array(Type.String())),
56
+ nextDecisionNeeded: Type.Optional(Type.String({ description: "Which decision is blocking progress next." })),
57
+ });
58
+
59
+ function stateDir(cwd: string): string {
60
+ return join(cwd, ".aicode-cli", "grill-me");
61
+ }
62
+
63
+ function statePath(cwd: string): string {
64
+ return join(stateDir(cwd), "state.json");
65
+ }
66
+
67
+ async function readState(cwd: string): Promise<GrillState | undefined> {
68
+ try {
69
+ return JSON.parse(await readFile(statePath(cwd), "utf8")) as GrillState;
70
+ } catch {
71
+ return undefined;
72
+ }
73
+ }
74
+
75
+ async function writeState(cwd: string, state: GrillState): Promise<void> {
76
+ await mkdir(stateDir(cwd), { recursive: true });
77
+ await writeFile(statePath(cwd), JSON.stringify(state, null, 2) + "\n", "utf8");
78
+ }
79
+
80
+ function renderMarkdown(
81
+ state: GrillState,
82
+ extra: { summary?: string; agreedDecisions?: string[]; openRisks?: string[]; nextDecisionNeeded?: string },
83
+ ): string {
84
+ const lines: string[] = [];
85
+ lines.push("# Grill Me Results", "");
86
+ lines.push(`Generated: ${new Date().toISOString()}`, "");
87
+ lines.push("## Plan", "", state.plan.trim() || "_(none recorded)_", "");
88
+ if (extra.summary?.trim()) {
89
+ lines.push("## Shared Understanding", "", extra.summary.trim(), "");
90
+ }
91
+ lines.push("## Questions and Answers", "");
92
+ if (state.turns.length === 0) {
93
+ lines.push("_(No turns recorded.)", "");
94
+ } else {
95
+ state.turns.forEach((turn, index) => {
96
+ lines.push(`### ${index + 1}. ${turn.question}`, "");
97
+ lines.push(`**Recommended answer:** ${turn.recommendedAnswer || "_(none)_"}`, "");
98
+ lines.push(`**User answer:** ${turn.userAnswer || "_(not recorded)_"}`, "");
99
+ lines.push(`**Status:** ${turn.decisionStatus}`, "");
100
+ if (turn.notes?.trim()) lines.push(`**Notes:** ${turn.notes.trim()}`, "");
101
+ });
102
+ }
103
+ if (extra.agreedDecisions?.length) {
104
+ lines.push("## Agreed Decisions", "", ...extra.agreedDecisions.map((d) => `- ${d}`), "");
105
+ }
106
+ if (extra.openRisks?.length) {
107
+ lines.push("## Open Risks", "", ...extra.openRisks.map((r) => `- ${r}`), "");
108
+ }
109
+ if (extra.nextDecisionNeeded?.trim()) {
110
+ lines.push("## Next Decision Needed", "", extra.nextDecisionNeeded.trim(), "");
111
+ }
112
+ return lines.join("\n");
113
+ }
114
+
115
+ function safeOutputPath(cwd: string, input?: string): string {
116
+ const requested = input?.trim() || "GRILL-ME.md";
117
+ const absolute = resolve(cwd, requested);
118
+ const root = resolve(cwd);
119
+ if (absolute !== root && !absolute.startsWith(root + "/")) {
120
+ throw new Error(`Refusing to write outside project directory: ${requested}`);
121
+ }
122
+ return absolute;
123
+ }
124
+
125
+ export default function grillMeExtension(pi: ExtensionAPI) {
126
+ pi.registerCommand("grill-me", {
127
+ description: "Start a deterministic design interview and save results to Markdown",
128
+ handler: async (args, ctx) => {
129
+ const plan = args.trim() || "(No plan supplied yet. Ask the user to paste or describe the plan first.)";
130
+ const state: GrillState = {
131
+ createdAt: new Date().toISOString(),
132
+ updatedAt: new Date().toISOString(),
133
+ projectDir: ctx.cwd,
134
+ plan,
135
+ turns: [],
136
+ };
137
+ await writeState(ctx.cwd, state);
138
+ ctx.ui.notify(`Grill session initialized: ${statePath(ctx.cwd)}`, "info");
139
+
140
+ pi.sendUserMessage(
141
+ `开始 /grill-me 访谈,计划如下:\n\n${plan}\n\n规则:\n- 无情地质问这个计划的每个细节,直到我们达成共识。\n- 沿着设计树的每个分支推进,逐个解决决策之间的依赖关系。\n- 每次只问一个问题。\n- 对每个问题,提供你的推荐答案,用户可以使用A B C 的方式快速选择。\n- 如果问题可以通过探索代码库来回答,则先探索代码库而不是直接提问。\n- 每个问答决策被捕获后,使用 grill_record_turn 记录。\n- 当达成足够共识或我要求停止/保存时,使用 grill_save_results 将结果保存为项目目录中的 .aicode-cli文件夹下的 Markdown 文件。`,
142
+ );
143
+ },
144
+ });
145
+
146
+ pi.registerTool({
147
+ name: "grill_record_turn",
148
+ label: "Grill Record Turn",
149
+ description: "Record one /grill-me question, recommended answer, user answer, and decision status in project state.",
150
+ promptSnippet: "Record structured progress for an active /grill-me design interview",
151
+ promptGuidelines: [
152
+ "Use grill_record_turn after each /grill-me interview question is answered or resolved from codebase exploration.",
153
+ "Do not use grill_record_turn for more than one question at a time.",
154
+ ],
155
+ parameters: RecordTurnParams,
156
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
157
+ const state = (await readState(ctx.cwd)) ?? {
158
+ createdAt: new Date().toISOString(),
159
+ updatedAt: new Date().toISOString(),
160
+ projectDir: ctx.cwd,
161
+ plan: "(state was created by grill_record_turn; no plan recorded)",
162
+ turns: [],
163
+ };
164
+ state.turns.push(params as GrillTurn);
165
+ state.updatedAt = new Date().toISOString();
166
+ await writeState(ctx.cwd, state);
167
+ return {
168
+ content: [{ type: "text", text: `Recorded grill turn #${state.turns.length}` }],
169
+ details: { path: statePath(ctx.cwd), count: state.turns.length },
170
+ };
171
+ },
172
+ });
173
+
174
+ pi.registerTool({
175
+ name: "grill_save_results",
176
+ label: "Grill Save Results",
177
+ description: "Save the active /grill-me interview state as a Markdown file inside the project directory.",
178
+ promptSnippet: "Save /grill-me interview decisions and risks to Markdown in the project directory",
179
+ promptGuidelines: ["Use grill_save_results when the /grill-me interview is complete or the user asks to save/stop."],
180
+ parameters: SaveResultsParams,
181
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
182
+ const state = await readState(ctx.cwd);
183
+ if (!state) {
184
+ return {
185
+ content: [{ type: "text", text: "No active /grill-me state found. Run /grill-me first." }],
186
+ isError: true,
187
+ details: { path: statePath(ctx.cwd) },
188
+ };
189
+ }
190
+ const outputPath = safeOutputPath(ctx.cwd, params.path);
191
+ await writeFile(outputPath, renderMarkdown(state, params), "utf8");
192
+ return {
193
+ content: [{ type: "text", text: `Saved grill results to ${outputPath}` }],
194
+ details: { path: outputPath, turns: state.turns.length },
195
+ };
196
+ },
197
+ });
198
+ }