frappe-builder 1.1.0-dev.21 → 1.1.0-dev.24

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-28T14:09:32.555Z
5
+ updated_at: 2026-03-28T14:23:24.878Z
6
6
 
7
7
  components:
8
8
  - id: final-comp
9
9
  sort_order: 0
10
10
  status: complete
11
- completed_at: 2026-03-28T14:09:32.555Z
11
+ completed_at: 2026-03-28T14:23:24.878Z
12
12
 
13
13
  progress:
14
14
  done: 1
package/AGENTS.md CHANGED
@@ -17,7 +17,7 @@ You are Frappe-Nexus, the primary orchestrator for frappe-builder — a Frappe/E
17
17
 
18
18
  | Instead of… | Run this Bash command… |
19
19
  |---|---|
20
- | Reading a file to analyse it | `mcp2cli @context-mode ctx_execute_file --path /abs/path --language python --code "print(open('/abs/path').read())"` |
20
+ | Reading a file to analyse it | `mcp2cli @context-mode ctx_execute_file --path /abs/path --language python --code "print(FILE_CONTENT)"` |
21
21
  | Running bash commands with long output | `mcp2cli @context-mode ctx_execute --language shell --code "your-command"` |
22
22
  | Searching the codebase | `mcp2cli @context-mode ctx_batch_execute --commands '["grep -r X ."]' --queries '["what does X do"]'` |
23
23
  | Follow-up questions after indexing | `mcp2cli @context-mode ctx_search --queries '["question 1", "question 2"]'` |
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-dY-MHOgS.mjs");
17
+ const { runInit } = await import("./init-Gp1MgJD2.mjs");
18
18
  await runInit();
19
19
  process.exit(0);
20
20
  }
@@ -148,6 +148,7 @@ async function runInit(opts = {}) {
148
148
  else skipped.push(".gitignore (entry already present)");
149
149
  await setupContextMode(homeDir);
150
150
  setupMcp2cli(homeDir);
151
+ setupContext7();
151
152
  console.log("\nFiles written:");
152
153
  for (const f of written) console.log(` ✓ ${f}`);
153
154
  if (skipped.length > 0) {
@@ -232,6 +233,20 @@ async function setupContextMode(homeDir) {
232
233
  function setupMcp2cli(homeDir) {
233
234
  const startScript = join(homeDir, ".pi", "extensions", "context-mode", "node_modules", "context-mode", "start.mjs");
234
235
  console.log("\n[mcp2cli skill + context-mode bake]");
236
+ if (spawnSync("mcp2cli", ["--version"], { stdio: "pipe" }).status === 0) console.log(" ✓ mcp2cli already installed");
237
+ else {
238
+ console.log(" Installing mcp2cli...");
239
+ if (spawnSync("uv", [
240
+ "tool",
241
+ "install",
242
+ "mcp2cli"
243
+ ], { stdio: "pipe" }).status === 0) console.log(" ✓ mcp2cli installed via uv");
244
+ else if (spawnSync("pip", ["install", "mcp2cli"], { stdio: "pipe" }).status === 0) console.log(" ✓ mcp2cli installed via pip");
245
+ else {
246
+ console.warn(" ⚠ mcp2cli install failed (tried uv and pip)");
247
+ console.warn(" Install manually: uv tool install mcp2cli");
248
+ }
249
+ }
235
250
  const skillAdd = spawnSync("npx", [
236
251
  "skills",
237
252
  "add",
@@ -262,10 +277,51 @@ function setupMcp2cli(homeDir) {
262
277
  "--mcp-stdio",
263
278
  `node ${startScript}`
264
279
  ], { stdio: "pipe" });
265
- if (bakeCreate.status !== 0) {
266
- console.warn(` mcp2cli bake failed: ${bakeCreate.stderr?.toString().trim()}`);
267
- console.warn(" Install mcp2cli first: pip install mcp2cli");
268
- } else console.log(" ✓ mcp2cli @context-mode baked — agent can now call: mcp2cli @context-mode <tool>");
280
+ if (bakeCreate.status !== 0) console.warn(` ⚠ mcp2cli bake failed: ${bakeCreate.stderr?.toString().trim()}`);
281
+ else console.log(" mcp2cli @context-mode baked — agent can now call: mcp2cli @context-mode <tool>");
282
+ }
283
+ /**
284
+ * Bakes the context7 cloud MCP server as @context7 and installs the
285
+ * netresearch context7 Claude Code skill for general-purpose library research.
286
+ *
287
+ * Non-fatal — failures are logged as warnings, never abort init.
288
+ */
289
+ function setupContext7() {
290
+ console.log("\n[context7 MCP bake + skill]");
291
+ if (spawnSync("mcp2cli", [
292
+ "bake",
293
+ "show",
294
+ "context7"
295
+ ], { stdio: "pipe" }).status === 0) console.log(" ✓ mcp2cli @context7 already baked");
296
+ else {
297
+ const bakeCreate = spawnSync("mcp2cli", [
298
+ "bake",
299
+ "create",
300
+ "context7",
301
+ "--mcp",
302
+ "https://mcp.context7.com/mcp"
303
+ ], { stdio: "pipe" });
304
+ if (bakeCreate.status !== 0) {
305
+ console.warn(` ⚠ context7 bake failed: ${bakeCreate.stderr?.toString().trim()}`);
306
+ console.warn(" Install mcp2cli first: uv tool install mcp2cli");
307
+ } else console.log(" ✓ mcp2cli @context7 baked — agent can now call: mcp2cli @context7 resolve-library-id");
308
+ }
309
+ spawnSync("npx", [
310
+ "skills",
311
+ "add",
312
+ "knowsuchagency/mcp2cli",
313
+ "--skill",
314
+ "mcp2cli"
315
+ ], { stdio: "pipe" });
316
+ if (spawnSync("claude", [
317
+ "plugin",
318
+ "marketplace",
319
+ "add",
320
+ "netresearch/claude-code-marketplace"
321
+ ], { stdio: "pipe" }).status !== 0) {
322
+ console.warn(" ⚠ context7 skill install failed (netresearch marketplace unavailable)");
323
+ console.warn(" Install manually: claude plugin marketplace add netresearch/claude-code-marketplace");
324
+ } else console.log(" ✓ context7 Claude Code skill installed — use /context7 for library research");
269
325
  }
270
326
  //#endregion
271
327
  export { runInit };
@@ -6,7 +6,7 @@ import { invokeDebugger, endDebug } from "../tools/debug-tools.js";
6
6
  import { spawnAgent } from "../tools/agent-tools.js";
7
7
  import { scaffoldDoctype, benchExecute, runTests } from "../tools/bench-tools.js";
8
8
  import { frappeQuery } from "../tools/frappe-query-tools.js";
9
- import { getFrappeDocs } from "../tools/frappe-context7.js";
9
+ import { getLibraryDocs, getFrappeDocs } from "../tools/frappe-context7.js";
10
10
 
11
11
  // pi.registerTool's execute callback is untyped (pi is `any`); params are
12
12
  // enforced at runtime via TypeBox schemas. One explicit-any alias avoids
@@ -212,12 +212,31 @@ export default function (pi: any) {
212
212
  },
213
213
  });
214
214
 
215
- // 9th tool outside the 8 core Frappe-semantic tools; bypasses phase guard via ALWAYS_ALLOWED_TOOLS
215
+ // General-purpose library docs tool via context7 bypasses phase guard via ALWAYS_ALLOWED_TOOLS
216
+ pi.registerTool({
217
+ name: "get_library_docs",
218
+ label: "Get Library Docs",
219
+ description:
220
+ "Retrieves up-to-date documentation for any library via context7 MCP (React, Next.js, Frappe, ERPNext, Python, etc.). Raw web content never enters the LLM context. Valid in any phase.",
221
+ parameters: Type.Object({
222
+ query: Type.String({ description: "What you want to know (e.g. 'how do hooks work', 'useEffect cleanup')" }),
223
+ library: Type.Optional(Type.String({ description: "Library name (e.g. 'frappe', 'react', 'nextjs'). Defaults to 'frappe'." })),
224
+ }),
225
+ execute: async (_toolCallId: string, params: ToolParams) => {
226
+ const result = await getLibraryDocs(params);
227
+ return {
228
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
229
+ details: result,
230
+ };
231
+ },
232
+ });
233
+
234
+ // Frappe-specific alias kept for backwards compatibility
216
235
  pi.registerTool({
217
236
  name: "get_frappe_docs",
218
237
  label: "Get Frappe Docs",
219
238
  description:
220
- "Retrieves compressed Frappe/ERPNext documentation via context7 MCP. Raw web content never enters the LLM context (~90% token reduction). Valid in any phase. (Story 4.4)",
239
+ "Retrieves Frappe/ERPNext documentation via context7. Alias for get_library_docs with library='frappe'. Valid in any phase.",
221
240
  parameters: Type.Object({
222
241
  topic: Type.String({ description: "Documentation topic (e.g. 'DocType', 'hooks', 'frappe.db')" }),
223
242
  version: Type.Optional(Type.String({ description: "Frappe version to scope docs (e.g. 'v15')" })),
@@ -51,7 +51,7 @@ function buildBlockedResponse(
51
51
  * Never throws — always returns a value or undefined.
52
52
  */
53
53
  // Tools valid in all phases — never blocked by the phase guard (FR34)
54
- const ALWAYS_ALLOWED_TOOLS = ["invoke_debugger", "end_debug", "spawn_agent", "get_frappe_docs", "get_audit_log", "get_project_status"];
54
+ const ALWAYS_ALLOWED_TOOLS = ["invoke_debugger", "end_debug", "spawn_agent", "get_frappe_docs", "get_library_docs", "get_audit_log", "get_project_status"];
55
55
 
56
56
  export function beforeToolCall(
57
57
  toolName: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frappe-builder",
3
- "version": "1.1.0-dev.21",
3
+ "version": "1.1.0-dev.24",
4
4
  "description": "Frappe-native AI co-pilot for building and customising Frappe/ERPNext applications",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,70 +1,66 @@
1
1
  import { execa } from "execa";
2
2
 
3
- export interface FrappeDocsArgs {
4
- topic: string;
5
- version?: string;
3
+ export interface GetLibraryDocsArgs {
4
+ query: string;
5
+ library?: string;
6
6
  }
7
7
 
8
- export interface FrappeDocsResult {
8
+ export interface GetLibraryDocsResult {
9
9
  snippet?: string;
10
- topic?: string;
10
+ query?: string;
11
+ library?: string;
11
12
  source?: "context7";
12
13
  error?: string;
13
14
  fallback?: string;
14
15
  }
15
16
 
16
- const CONTEXT7_UNAVAILABLE: FrappeDocsResult = {
17
+ const CONTEXT7_UNAVAILABLE: GetLibraryDocsResult = {
17
18
  error: "context7 unavailable",
18
- fallback: "search frappe.io manually",
19
+ fallback: "search library docs manually",
19
20
  };
20
21
 
21
22
  /**
22
- * Retrieves compressed Frappe/ERPNext documentation via context7 MCP subprocess.
23
- * Raw web content never enters the LLM context — only the compressed snippet is returned.
23
+ * Retrieves up-to-date documentation via context7 MCP for any library.
24
+ * Raw web content never enters the LLM context — only the snippet is returned.
24
25
  * Returns a graceful error on any failure (timeout, MCP unavailable, not found).
25
26
  * Never throws.
26
27
  *
27
28
  * Flow:
28
- * 1. Resolve Frappe library ID via context7 resolve-library-id
29
- * 2. Fetch compressed docs for topic via context7 get-library-docs
29
+ * 1. Resolve library ID via context7 resolve-library-id
30
+ * 2. Fetch docs for query via context7 query-docs
30
31
  */
31
- export async function getFrappeDocs({ topic, version }: FrappeDocsArgs): Promise<FrappeDocsResult> {
32
+ export async function getLibraryDocs({ query, library = "frappe" }: GetLibraryDocsArgs): Promise<GetLibraryDocsResult> {
32
33
  try {
33
- // Step 1: resolve Frappe library ID
34
- const libraryName = version ? `frappe@${version}` : "frappe";
34
+ // Step 1: resolve library ID
35
35
  const resolveResult = await execa(
36
36
  "mcp2cli",
37
- ["context7", "resolve-library-id", "--libraryName", libraryName],
37
+ ["@context7", "resolve-library-id", "--library-name", library, "--query", query],
38
38
  { timeout: 10_000 }
39
39
  );
40
40
 
41
- let libraryId: string;
42
- try {
43
- const parsed = JSON.parse(resolveResult.stdout) as { libraryId?: string };
44
- if (!parsed.libraryId) return CONTEXT7_UNAVAILABLE;
45
- libraryId = parsed.libraryId;
46
- } catch {
47
- return CONTEXT7_UNAVAILABLE;
48
- }
41
+ // Response is plain text listing — extract first library ID via regex
42
+ const idMatch = resolveResult.stdout.match(/Context7-compatible library ID:\s*(\S+)/);
43
+ if (!idMatch?.[1]) return CONTEXT7_UNAVAILABLE;
44
+ const libraryId = idMatch[1];
49
45
 
50
- // Step 2: fetch compressed docs for topic (~90% token reduction via 5000 token limit)
46
+ // Step 2: fetch docs for the query
51
47
  const docsResult = await execa(
52
48
  "mcp2cli",
53
- [
54
- "context7",
55
- "get-library-docs",
56
- "--libraryId", libraryId,
57
- "--topic", topic,
58
- "--tokens", "5000",
59
- ],
49
+ ["@context7", "query-docs", "--library-id", libraryId, "--query", query],
60
50
  { timeout: 15_000 }
61
51
  );
62
52
 
63
53
  const snippet = docsResult.stdout?.trim();
64
54
  if (!snippet) return CONTEXT7_UNAVAILABLE;
65
55
 
66
- return { snippet, topic, source: "context7" };
56
+ return { snippet, query, library, source: "context7" };
67
57
  } catch {
68
58
  return CONTEXT7_UNAVAILABLE;
69
59
  }
70
60
  }
61
+
62
+ /** Backwards-compatible alias for Frappe-specific doc lookups. */
63
+ export async function getFrappeDocs({ topic, version }: { topic: string; version?: string }): Promise<GetLibraryDocsResult> {
64
+ const library = version ? `frappe@${version}` : "frappe";
65
+ return getLibraryDocs({ query: topic, library });
66
+ }