frappe-builder 1.1.0-dev.19 → 1.1.0-dev.22

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/.fb/state.db CHANGED
Binary file
@@ -2,13 +2,13 @@ feature_id: po-approval
2
2
  feature_name: "PO Approval"
3
3
  mode: full
4
4
  phase: testing
5
- updated_at: 2026-03-28T11:15:20.689Z
5
+ updated_at: 2026-03-28T14:14:28.658Z
6
6
 
7
7
  components:
8
8
  - id: final-comp
9
9
  sort_order: 0
10
10
  status: complete
11
- completed_at: 2026-03-28T11:15:20.689Z
11
+ completed_at: 2026-03-28T14:14:28.658Z
12
12
 
13
13
  progress:
14
14
  done: 1
package/AGENTS.md CHANGED
@@ -13,18 +13,19 @@ You are Frappe-Nexus, the primary orchestrator for frappe-builder — a Frappe/E
13
13
 
14
14
  ## Context Window Protection (MANDATORY)
15
15
 
16
- **Always use context-mode MCP tools.** Raw tool output floods the context window and degrades performance. This is non-negotiable.
16
+ **Always use context-mode via mcp2cli.** Pi has no native MCP support — call context-mode tools by running `mcp2cli @context-mode <tool>` in a Bash command. Raw output floods the context window and degrades performance. This is non-negotiable.
17
17
 
18
- | Instead of… | Use… |
18
+ | Instead of… | Run this Bash command… |
19
19
  |---|---|
20
- | Reading a file to analyse it | `ctx_execute_file(path, ...)` |
21
- | Running bash commands with long output | `ctx_execute(language: "shell", code: "...")` |
22
- | Searching the codebase | `ctx_batch_execute(commands, queries)` or `ctx_search(queries)` |
23
- | Fetching a URL | `ctx_fetch_and_index(url)` then `ctx_search(...)` |
20
+ | Reading a file to analyse it | `mcp2cli @context-mode ctx_execute_file --path /abs/path --language python --code "print(FILE_CONTENT)"` |
21
+ | Running bash commands with long output | `mcp2cli @context-mode ctx_execute --language shell --code "your-command"` |
22
+ | Searching the codebase | `mcp2cli @context-mode ctx_batch_execute --commands '["grep -r X ."]' --queries '["what does X do"]'` |
23
+ | Follow-up questions after indexing | `mcp2cli @context-mode ctx_search --queries '["question 1", "question 2"]'` |
24
+ | Fetching a URL | `mcp2cli @context-mode ctx_fetch_and_index --url https://...` then `ctx_search` |
24
25
 
25
- **Only use direct Read/Bash when you are about to Edit the file** — the Edit tool needs file content in context. All other reads must go through context-mode.
26
+ **Only use direct Read when you are about to Edit the file** — the Edit tool needs file content in context. All other reads must go through `mcp2cli @context-mode`.
26
27
 
27
- After every `ctx_batch_execute` or `ctx_execute`, summarise findings in 3–5 bullet points. Never dump raw output into your response.
28
+ After every context-mode call, summarise findings in 3–5 bullet points. Never dump raw output into your response.
28
29
 
29
30
  ## Constraints
30
31
  - Never auto-pilot: every significant action must be visible to the developer
package/dist/cli.mjs CHANGED
@@ -14,7 +14,7 @@ import { existsSync } from "node:fs";
14
14
  */
15
15
  const cmd = process.argv[2];
16
16
  if (cmd === "init") {
17
- const { runInit } = await import("./init-BYJy3ztn.mjs");
17
+ const { runInit } = await import("./init-BUkSYk2l.mjs");
18
18
  await runInit();
19
19
  process.exit(0);
20
20
  }
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { join } from "node:path";
3
+ import { spawnSync } from "node:child_process";
3
4
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
4
5
  import { homedir } from "node:os";
5
6
  import { createInterface } from "node:readline";
@@ -57,7 +58,8 @@ function patchGitignore(projectRoot, entry) {
57
58
  }
58
59
  async function runInit(opts = {}) {
59
60
  const projectRoot = opts.projectRoot ?? process.cwd();
60
- const globalConfigDir = join(homedir(), ".frappe-builder");
61
+ const homeDir = homedir();
62
+ const globalConfigDir = join(homeDir, ".frappe-builder");
61
63
  const globalConfigPath = join(globalConfigDir, "config.json");
62
64
  const projectConfigPath = join(projectRoot, ".frappe-builder-config.json");
63
65
  console.log("\n=== frappe-builder Setup ===\n");
@@ -144,6 +146,8 @@ async function runInit(opts = {}) {
144
146
  if (gitignoreResult === "patched") written.push(".gitignore (patched)");
145
147
  else if (gitignoreResult === "created") written.push(".gitignore (created)");
146
148
  else skipped.push(".gitignore (entry already present)");
149
+ await setupContextMode(homeDir);
150
+ setupMcp2cli(homeDir);
147
151
  console.log("\nFiles written:");
148
152
  for (const f of written) console.log(` ✓ ${f}`);
149
153
  if (skipped.length > 0) {
@@ -155,5 +159,125 @@ async function runInit(opts = {}) {
155
159
  function printCancelled() {
156
160
  console.log("\nSetup cancelled. No files were written.\n");
157
161
  }
162
+ /**
163
+ * Installs and configures the context-mode pi MCP extension.
164
+ * Clones https://github.com/mksglu/context-mode into ~/.pi/extensions/context-mode,
165
+ * builds it, and patches ~/.pi/settings/mcp.json with the server entry.
166
+ *
167
+ * Non-fatal — failures are logged as warnings, never abort init.
168
+ */
169
+ async function setupContextMode(homeDir) {
170
+ const extDir = join(homeDir, ".pi", "extensions", "context-mode");
171
+ const mcpSettingsDir = join(homeDir, ".pi", "settings");
172
+ const mcpSettingsPath = join(mcpSettingsDir, "mcp.json");
173
+ const startScript = join(extDir, "node_modules", "context-mode", "start.mjs");
174
+ console.log("\n[context-mode MCP extension]");
175
+ if (existsSync(extDir)) console.log(" ✓ context-mode already installed at ~/.pi/extensions/context-mode");
176
+ else {
177
+ console.log(" context-mode not found — installing (requires git + Node.js)...");
178
+ mkdirSync(join(homeDir, ".pi", "extensions"), { recursive: true });
179
+ const clone = spawnSync("git", [
180
+ "clone",
181
+ "https://github.com/mksglu/context-mode.git",
182
+ extDir
183
+ ], { stdio: "pipe" });
184
+ if (clone.status !== 0) {
185
+ console.warn(` ⚠ git clone failed: ${clone.stderr?.toString().trim()}`);
186
+ console.warn(" Skipping context-mode setup. Install manually: https://github.com/mksglu/context-mode");
187
+ return;
188
+ }
189
+ const install = spawnSync("npm", ["install"], {
190
+ cwd: extDir,
191
+ stdio: "pipe"
192
+ });
193
+ if (install.status !== 0) {
194
+ console.warn(` ⚠ npm install failed: ${install.stderr?.toString().trim()}`);
195
+ return;
196
+ }
197
+ const build = spawnSync("npm", ["run", "build"], {
198
+ cwd: extDir,
199
+ stdio: "pipe"
200
+ });
201
+ if (build.status !== 0) {
202
+ console.warn(` ⚠ npm run build failed: ${build.stderr?.toString().trim()}`);
203
+ return;
204
+ }
205
+ console.log(" ✓ context-mode installed and built");
206
+ }
207
+ mkdirSync(mcpSettingsDir, { recursive: true });
208
+ let mcpConfig = {};
209
+ if (existsSync(mcpSettingsPath)) try {
210
+ mcpConfig = JSON.parse(readFileSync(mcpSettingsPath, "utf-8"));
211
+ } catch {}
212
+ const servers = mcpConfig.mcpServers ?? {};
213
+ if (servers["context-mode"]) {
214
+ console.log(" ✓ context-mode already in ~/.pi/settings/mcp.json");
215
+ return;
216
+ }
217
+ servers["context-mode"] = {
218
+ command: "node",
219
+ args: [startScript]
220
+ };
221
+ mcpConfig.mcpServers = servers;
222
+ writeAtomic(mcpSettingsPath, JSON.stringify(mcpConfig, null, 2) + "\n");
223
+ console.log(" ✓ Added context-mode to ~/.pi/settings/mcp.json");
224
+ console.log(" Restart pi (or frappe-builder) for context-mode to activate.");
225
+ }
226
+ /**
227
+ * Installs the mcp2cli Claude Code skill and bakes the context-mode connection
228
+ * so the agent can call `mcp2cli @context-mode <tool>` without repeating flags.
229
+ *
230
+ * Non-fatal — failures are logged as warnings, never abort init.
231
+ */
232
+ function setupMcp2cli(homeDir) {
233
+ const startScript = join(homeDir, ".pi", "extensions", "context-mode", "node_modules", "context-mode", "start.mjs");
234
+ console.log("\n[mcp2cli skill + context-mode bake]");
235
+ if (spawnSync("mcp2cli", ["--version"], { stdio: "pipe" }).status === 0) console.log(" ✓ mcp2cli already installed");
236
+ else {
237
+ console.log(" Installing mcp2cli...");
238
+ if (spawnSync("uv", [
239
+ "tool",
240
+ "install",
241
+ "mcp2cli"
242
+ ], { stdio: "pipe" }).status === 0) console.log(" ✓ mcp2cli installed via uv");
243
+ else if (spawnSync("pip", ["install", "mcp2cli"], { stdio: "pipe" }).status === 0) console.log(" ✓ mcp2cli installed via pip");
244
+ else {
245
+ console.warn(" ⚠ mcp2cli install failed (tried uv and pip)");
246
+ console.warn(" Install manually: uv tool install mcp2cli");
247
+ }
248
+ }
249
+ const skillAdd = spawnSync("npx", [
250
+ "skills",
251
+ "add",
252
+ "knowsuchagency/mcp2cli",
253
+ "--skill",
254
+ "mcp2cli"
255
+ ], { stdio: "pipe" });
256
+ if (skillAdd.status !== 0) {
257
+ console.warn(` ⚠ mcp2cli skill install failed: ${skillAdd.stderr?.toString().trim()}`);
258
+ console.warn(" Install manually: npx skills add knowsuchagency/mcp2cli --skill mcp2cli");
259
+ } else console.log(" ✓ mcp2cli skill installed");
260
+ if (spawnSync("mcp2cli", [
261
+ "bake",
262
+ "show",
263
+ "context-mode"
264
+ ], { stdio: "pipe" }).status === 0) {
265
+ console.log(" ✓ mcp2cli @context-mode already baked");
266
+ return;
267
+ }
268
+ if (!existsSync(startScript)) {
269
+ console.warn(" ⚠ context-mode start.mjs not found — skipping bake (run init again after context-mode installs)");
270
+ return;
271
+ }
272
+ const bakeCreate = spawnSync("mcp2cli", [
273
+ "bake",
274
+ "create",
275
+ "context-mode",
276
+ "--mcp-stdio",
277
+ `node ${startScript}`
278
+ ], { stdio: "pipe" });
279
+ if (bakeCreate.status !== 0) console.warn(` ⚠ mcp2cli bake failed: ${bakeCreate.stderr?.toString().trim()}`);
280
+ else console.log(" ✓ mcp2cli @context-mode baked — agent can now call: mcp2cli @context-mode <tool>");
281
+ }
158
282
  //#endregion
159
283
  export { runInit };
@@ -239,8 +239,9 @@ export function buildStateInjection(ctx: SessionContext): string {
239
239
  `Progress: ${progress}`,
240
240
  `Last action: ${ctx.lastTool ?? "none"}`,
241
241
  "======================================",
242
- "CONTEXT WINDOW RULE: Use ctx_execute_file / ctx_execute / ctx_batch_execute / ctx_search for ALL",
243
- "reads and searches. Only use Read directly when you are about to Edit that file.",
242
+ "CONTEXT WINDOW RULE: Run `mcp2cli @context-mode <tool>` for ALL reads and searches.",
243
+ "Use ctx_execute_file, ctx_execute, ctx_batch_execute, ctx_search via mcp2cli.",
244
+ "Only use Read directly when you are about to Edit that file.",
244
245
  );
245
246
  return lines.join("\n");
246
247
  }
@@ -1,5 +1,5 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import { startFeature, completeComponent } from "../tools/feature-tools.js";
2
+ import { startFeature, completeComponent, createComponent } from "../tools/feature-tools.js";
3
3
  import { setActiveProject, getProjectStatus } from "../tools/project-tools.js";
4
4
  import { getAuditLog } from "../state/journal.js";
5
5
  import { invokeDebugger, endDebug } from "../tools/debug-tools.js";
@@ -54,6 +54,26 @@ export default function (pi: any) {
54
54
  },
55
55
  });
56
56
 
57
+ pi.registerTool({
58
+ name: "create_component",
59
+ label: "Create Component",
60
+ description:
61
+ "Registers a planned unit of work as an in-progress component for the active feature. Call this BEFORE writing any code — one component per logical deliverable. Updates sprint-status.yaml and sets the active component on the session. Valid in planning and implementation phases.",
62
+ parameters: Type.Object({
63
+ featureId: Type.String({ description: "Feature ID (kebab-case slug, e.g. 'po-approval')" }),
64
+ componentId: Type.String({ description: "Component ID (e.g. 'auth-module', 'po-doctype')" }),
65
+ description: Type.Optional(Type.String({ description: "Short description of what this component implements" })),
66
+ sortOrder: Type.Optional(Type.Number({ description: "Order within the feature (lower = earlier)" })),
67
+ }),
68
+ execute: async (_toolCallId: string, params: ToolParams) => {
69
+ const result = createComponent(params);
70
+ return {
71
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
72
+ details: result,
73
+ };
74
+ },
75
+ });
76
+
57
77
  pi.registerTool({
58
78
  name: "set_active_project",
59
79
  label: "Set Active Project",
@@ -126,7 +146,7 @@ export default function (pi: any) {
126
146
  name: "scaffold_doctype",
127
147
  label: "Scaffold DocType",
128
148
  description:
129
- "Creates a new Frappe DocType via bench. Valid only in the 'implementation' phase. (Story 4.2 deferred)",
149
+ "NOT YET IMPLEMENTED (Story 4.2 deferred). Returns an error. Do not call create DocType JSON fixtures directly instead.",
130
150
  parameters: Type.Object({
131
151
  name: Type.String({ description: "DocType name in PascalCase (e.g. 'Purchase Order')" }),
132
152
  module: Type.String({ description: "Frappe module name (e.g. 'Buying')" }),
@@ -144,7 +164,7 @@ export default function (pi: any) {
144
164
  name: "run_tests",
145
165
  label: "Run Tests",
146
166
  description:
147
- "Runs the Frappe test suite for the active feature via bench. Valid only in the 'testing' phase. (Epic 6)",
167
+ "NOT YET IMPLEMENTED (Epic 6 deferred). Returns an error. Use bench_execute with 'bench run-tests --app {app}' instead.",
148
168
  parameters: Type.Object({
149
169
  featureId: Type.Optional(Type.String({ description: "Feature ID to scope tests (optional)" })),
150
170
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-builder",
3
- "version": "1.1.0-dev.19",
3
+ "version": "1.1.0-dev.22",
4
4
  "description": "Frappe-native AI co-pilot for building and customising Frappe/ERPNext applications",
5
5
  "type": "module",
6
6
  "bin": {