frappe-builder 1.1.0-dev.24 → 1.1.0-dev.26
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 +0 -0
- package/.frappe-builder/po-approval/implementation-artifacts/sprint-status.yaml +2 -2
- package/config/defaults.ts +0 -1
- package/config/loader.ts +18 -84
- package/dist/cli.mjs +7 -6
- package/dist/{init-Gp1MgJD2.mjs → init-CkLSZ_3g.mjs} +13 -123
- package/extensions/frappe-state.ts +0 -13
- package/extensions/frappe-tools.ts +3 -0
- package/package.json +1 -1
- package/state/db.ts +14 -2
- package/state/schema.ts +6 -0
- package/tools/frappe-query-tools.ts +36 -20
- package/tools/project-tools.ts +12 -11
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:
|
|
5
|
+
updated_at: 2026-03-28T14:38:26.634Z
|
|
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:
|
|
11
|
+
completed_at: 2026-03-28T14:38:26.633Z
|
|
12
12
|
|
|
13
13
|
progress:
|
|
14
14
|
done: 1
|
package/config/defaults.ts
CHANGED
|
@@ -11,7 +11,6 @@ export interface AppConfig {
|
|
|
11
11
|
allowSubAgents: boolean;
|
|
12
12
|
requirePermission: boolean;
|
|
13
13
|
rules: SpawnRule[];
|
|
14
|
-
frappeMcpUrl?: string; // URL of Frappe MCP server for frappe_query (e.g. "http://localhost:8000")
|
|
15
14
|
defaultMode?: "full" | "quick"; // default feature mode: "quick" skips planning phases
|
|
16
15
|
chainModel?: string; // model for chain subprocess agents (inherits parent model when unset)
|
|
17
16
|
permissionMode?: PermissionMode; // agent autonomy level: auto | default | plan
|
package/config/loader.ts
CHANGED
|
@@ -1,96 +1,16 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { defaults } from "./defaults.js";
|
|
5
5
|
import type { AppConfig } from "./defaults.js";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export interface GlobalConfig {
|
|
10
|
-
llm_api_key: string;
|
|
11
|
-
llm_provider?: string;
|
|
12
|
-
[key: string]: unknown;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ProjectConfig {
|
|
16
|
-
site_url: string;
|
|
17
|
-
api_key: string;
|
|
18
|
-
api_secret: string;
|
|
19
|
-
[key: string]: unknown;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface CredentialConfig {
|
|
23
|
-
global: GlobalConfig;
|
|
24
|
-
project: ProjectConfig;
|
|
25
|
-
sessionLogDir: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Always ~/.frappe-builder/sessions/ — never the project directory (NFR9). */
|
|
7
|
+
/** Always ~/.frappe-builder/sessions/ — never the project directory. */
|
|
29
8
|
export const SESSION_LOG_DIR = join(homedir(), ".frappe-builder", "sessions");
|
|
30
9
|
|
|
31
|
-
const GITIGNORE_ERROR =
|
|
32
|
-
"Site credentials file is not gitignored. Add .frappe-builder-config.json to .gitignore before proceeding.";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Validates that {projectRoot}/.gitignore lists .frappe-builder-config.json.
|
|
36
|
-
* Throws with the exact AC error message if missing or absent.
|
|
37
|
-
*/
|
|
38
|
-
export function validateGitignore(projectRoot: string): void {
|
|
39
|
-
const gitignorePath = join(projectRoot, ".gitignore");
|
|
40
|
-
if (!existsSync(gitignorePath)) {
|
|
41
|
-
throw new Error(GITIGNORE_ERROR);
|
|
42
|
-
}
|
|
43
|
-
const contents = readFileSync(gitignorePath, "utf8");
|
|
44
|
-
const lines = contents.split("\n").map((l) => l.trim());
|
|
45
|
-
if (!lines.includes(".frappe-builder-config.json")) {
|
|
46
|
-
throw new Error(GITIGNORE_ERROR);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Reads LLM API keys from ~/.frappe-builder/config.json (user-global, never project).
|
|
52
|
-
* Creates ~/.frappe-builder/ on first run. Returns empty defaults if file absent.
|
|
53
|
-
*/
|
|
54
|
-
export function loadGlobalConfig(): GlobalConfig {
|
|
55
|
-
const configDir = join(homedir(), ".frappe-builder");
|
|
56
|
-
mkdirSync(configDir, { recursive: true });
|
|
57
|
-
const configPath = join(configDir, "config.json");
|
|
58
|
-
if (!existsSync(configPath)) return { llm_api_key: "" };
|
|
59
|
-
try {
|
|
60
|
-
return JSON.parse(readFileSync(configPath, "utf8")) as GlobalConfig;
|
|
61
|
-
} catch {
|
|
62
|
-
return { llm_api_key: "" };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Reads Frappe site credentials from {projectRoot}/.frappe-builder-config.json.
|
|
68
|
-
* Returns empty defaults if file absent (caller should surface a warning).
|
|
69
|
-
*/
|
|
70
|
-
export function loadProjectConfig(projectRoot: string): ProjectConfig {
|
|
71
|
-
const configPath = join(projectRoot, ".frappe-builder-config.json");
|
|
72
|
-
if (!existsSync(configPath)) return { site_url: "", api_key: "", api_secret: "" };
|
|
73
|
-
try {
|
|
74
|
-
return JSON.parse(readFileSync(configPath, "utf8")) as ProjectConfig;
|
|
75
|
-
} catch {
|
|
76
|
-
return { site_url: "", api_key: "", api_secret: "" };
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Full credential loader: validates gitignore, loads global + project configs.
|
|
82
|
-
* Throws if .frappe-builder-config.json is not gitignored.
|
|
83
|
-
* API keys are only ever read from ~/.frappe-builder/ — never from projectRoot.
|
|
84
|
-
*/
|
|
85
|
-
export function loadCredentials(projectRoot: string): CredentialConfig {
|
|
86
|
-
validateGitignore(projectRoot);
|
|
87
|
-
const global = loadGlobalConfig();
|
|
88
|
-
const project = loadProjectConfig(projectRoot);
|
|
89
|
-
return { global, project, sessionLogDir: SESSION_LOG_DIR };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
10
|
/**
|
|
93
11
|
* Loads ~/.frappe-builder/config.json and merges with defaults.
|
|
12
|
+
* Stores agent behaviour preferences (permissionMode, requirePermission, etc.).
|
|
13
|
+
* Site credentials live in the session DB — not in this file.
|
|
94
14
|
* Never throws — returns defaults on any read/parse failure.
|
|
95
15
|
*/
|
|
96
16
|
export function loadConfig(): AppConfig {
|
|
@@ -103,3 +23,17 @@ export function loadConfig(): AppConfig {
|
|
|
103
23
|
return { ...defaults };
|
|
104
24
|
}
|
|
105
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Writes a partial config merged over the current config to ~/.frappe-builder/config.json.
|
|
29
|
+
* Creates the directory if needed. Non-fatal — never throws.
|
|
30
|
+
*/
|
|
31
|
+
export function saveConfig(partial: Partial<AppConfig>): void {
|
|
32
|
+
try {
|
|
33
|
+
const configDir = join(homedir(), ".frappe-builder");
|
|
34
|
+
mkdirSync(configDir, { recursive: true });
|
|
35
|
+
const configPath = join(configDir, "config.json");
|
|
36
|
+
const merged = { ...loadConfig(), ...partial };
|
|
37
|
+
writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
38
|
+
} catch { /* non-fatal */ }
|
|
39
|
+
}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { resolve } from "node:path";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
7
8
|
//#region src/cli.ts
|
|
8
9
|
/**
|
|
9
10
|
* src/cli.ts — frappe-builder CLI entry point
|
|
10
11
|
*
|
|
11
12
|
* Commands:
|
|
12
|
-
* frappe-builder init —
|
|
13
|
+
* frappe-builder init — bootstrap agent toolchain (run once per machine)
|
|
13
14
|
* frappe-builder — start a pi session with frappe-builder extensions loaded
|
|
14
15
|
*/
|
|
15
16
|
const cmd = process.argv[2];
|
|
16
17
|
if (cmd === "init") {
|
|
17
|
-
const { runInit } = await import("./init-
|
|
18
|
+
const { runInit } = await import("./init-CkLSZ_3g.mjs");
|
|
18
19
|
await runInit();
|
|
19
20
|
process.exit(0);
|
|
20
21
|
}
|
|
@@ -23,7 +24,7 @@ if (cmd === "--help" || cmd === "-h") {
|
|
|
23
24
|
Usage: frappe-builder [command]
|
|
24
25
|
|
|
25
26
|
Commands:
|
|
26
|
-
init
|
|
27
|
+
init Bootstrap agent toolchain: context-mode, mcp2cli, context7 (run once per machine)
|
|
27
28
|
(none) Start a frappe-builder session
|
|
28
29
|
|
|
29
30
|
Options:
|
|
@@ -31,8 +32,8 @@ Options:
|
|
|
31
32
|
`);
|
|
32
33
|
process.exit(0);
|
|
33
34
|
}
|
|
34
|
-
if (!existsSync(
|
|
35
|
-
console.error("frappe-builder
|
|
35
|
+
if (!existsSync(join(homedir(), ".frappe-builder", ".initialized"))) {
|
|
36
|
+
console.error("frappe-builder toolchain not set up.");
|
|
36
37
|
console.error("Run: frappe-builder init");
|
|
37
38
|
process.exit(1);
|
|
38
39
|
}
|
|
@@ -3,42 +3,16 @@ import { join } from "node:path";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
-
import { createInterface } from "node:readline";
|
|
7
6
|
//#region src/init.ts
|
|
8
7
|
/**
|
|
9
|
-
* src/init.ts —
|
|
8
|
+
* src/init.ts — toolchain setup for frappe-builder
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* and .gitignore patching.
|
|
10
|
+
* Bootstraps the agent environment: context-mode, mcp2cli, context7.
|
|
11
|
+
* No credential prompts — site credentials are collected at runtime via set_active_project.
|
|
14
12
|
*
|
|
15
13
|
* No imports from state/, extensions/, or gates/.
|
|
16
|
-
* Uses Node.js built-ins only
|
|
14
|
+
* Uses Node.js built-ins only.
|
|
17
15
|
*/
|
|
18
|
-
let cancelled = false;
|
|
19
|
-
process.on("SIGINT", () => {
|
|
20
|
-
cancelled = true;
|
|
21
|
-
});
|
|
22
|
-
function promptLine(question) {
|
|
23
|
-
return new Promise((resolve) => {
|
|
24
|
-
if (cancelled) {
|
|
25
|
-
resolve("");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const rl = createInterface({
|
|
29
|
-
input: process.stdin,
|
|
30
|
-
output: process.stdout
|
|
31
|
-
});
|
|
32
|
-
rl.question(question, (answer) => {
|
|
33
|
-
rl.close();
|
|
34
|
-
resolve(answer);
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
async function promptYN(question) {
|
|
39
|
-
const answer = await promptLine(question + " (y/N): ");
|
|
40
|
-
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
41
|
-
}
|
|
42
16
|
function writeAtomic(filePath, content) {
|
|
43
17
|
const tmp = filePath + ".tmp";
|
|
44
18
|
writeFileSync(tmp, content, "utf-8");
|
|
@@ -59,107 +33,23 @@ function patchGitignore(projectRoot, entry) {
|
|
|
59
33
|
async function runInit(opts = {}) {
|
|
60
34
|
const projectRoot = opts.projectRoot ?? process.cwd();
|
|
61
35
|
const homeDir = homedir();
|
|
62
|
-
const
|
|
63
|
-
const globalConfigPath = join(globalConfigDir, "config.json");
|
|
64
|
-
const projectConfigPath = join(projectRoot, ".frappe-builder-config.json");
|
|
36
|
+
const markerPath = join(homeDir, ".frappe-builder", ".initialized");
|
|
65
37
|
console.log("\n=== frappe-builder Setup ===\n");
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (existsSync(globalConfigPath)) {
|
|
70
|
-
try {
|
|
71
|
-
globalConfig = JSON.parse(readFileSync(globalConfigPath, "utf-8"));
|
|
72
|
-
} catch {}
|
|
73
|
-
if (!cancelled) {
|
|
74
|
-
const overwrite = await promptYN(`Overwrite existing ${globalConfigPath}?`);
|
|
75
|
-
if (cancelled) {
|
|
76
|
-
printCancelled();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (!overwrite) {
|
|
80
|
-
globalAction = "skipped";
|
|
81
|
-
console.log(" Keeping existing global config.\n");
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (!cancelled && globalAction === "written") {
|
|
86
|
-
const llmKey = await promptLine("LLM API key (leave blank to skip): ");
|
|
87
|
-
if (cancelled) {
|
|
88
|
-
printCancelled();
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
globalConfig.llm_api_key = llmKey.trim();
|
|
92
|
-
}
|
|
93
|
-
console.log(`\n[Project config: ${projectConfigPath}]`);
|
|
94
|
-
let projectConfig = {};
|
|
95
|
-
let projectAction = "written";
|
|
96
|
-
if (existsSync(projectConfigPath)) {
|
|
97
|
-
try {
|
|
98
|
-
projectConfig = JSON.parse(readFileSync(projectConfigPath, "utf-8"));
|
|
99
|
-
} catch {}
|
|
100
|
-
if (!cancelled) {
|
|
101
|
-
const overwrite = await promptYN(`Overwrite existing ${projectConfigPath}?`);
|
|
102
|
-
if (cancelled) {
|
|
103
|
-
printCancelled();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (!overwrite) {
|
|
107
|
-
projectAction = "skipped";
|
|
108
|
-
console.log(" Keeping existing project config.\n");
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (!cancelled && projectAction === "written") {
|
|
113
|
-
const siteUrl = await promptLine("Frappe site URL (e.g. http://site1.localhost): ");
|
|
114
|
-
if (cancelled) {
|
|
115
|
-
printCancelled();
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const apiKey = await promptLine("Frappe API key: ");
|
|
119
|
-
if (cancelled) {
|
|
120
|
-
printCancelled();
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
const apiSecret = await promptLine("Frappe API secret: ");
|
|
124
|
-
if (cancelled) {
|
|
125
|
-
printCancelled();
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
projectConfig = {
|
|
129
|
-
site_url: siteUrl.trim(),
|
|
130
|
-
api_key: apiKey.trim(),
|
|
131
|
-
api_secret: apiSecret.trim()
|
|
132
|
-
};
|
|
133
|
-
}
|
|
38
|
+
mkdirSync(join(homeDir, ".frappe-builder"), { recursive: true });
|
|
39
|
+
writeFileSync(markerPath, (/* @__PURE__ */ new Date()).toISOString() + "\n", "utf-8");
|
|
40
|
+
const gitignoreResult = patchGitignore(projectRoot, ".fb/");
|
|
134
41
|
const written = [];
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
mkdirSync(globalConfigDir, { recursive: true });
|
|
138
|
-
writeAtomic(globalConfigPath, JSON.stringify(globalConfig, null, 2) + "\n");
|
|
139
|
-
written.push(`~/.frappe-builder/config.json`);
|
|
140
|
-
} else skipped.push(`~/.frappe-builder/config.json`);
|
|
141
|
-
if (projectAction === "written") {
|
|
142
|
-
writeAtomic(projectConfigPath, JSON.stringify(projectConfig, null, 2) + "\n");
|
|
143
|
-
written.push(`.frappe-builder-config.json`);
|
|
144
|
-
} else skipped.push(`.frappe-builder-config.json`);
|
|
145
|
-
const gitignoreResult = patchGitignore(projectRoot, ".frappe-builder-config.json");
|
|
146
|
-
if (gitignoreResult === "patched") written.push(".gitignore (patched)");
|
|
147
|
-
else if (gitignoreResult === "created") written.push(".gitignore (created)");
|
|
148
|
-
else skipped.push(".gitignore (entry already present)");
|
|
42
|
+
if (gitignoreResult === "patched") written.push(".gitignore (patched with .fb/)");
|
|
43
|
+
else if (gitignoreResult === "created") written.push(".gitignore (created with .fb/)");
|
|
149
44
|
await setupContextMode(homeDir);
|
|
150
45
|
setupMcp2cli(homeDir);
|
|
151
46
|
setupContext7();
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
console.log("Skipped:");
|
|
156
|
-
for (const f of skipped) console.log(` - ${f}`);
|
|
47
|
+
if (written.length > 0) {
|
|
48
|
+
console.log("\nFiles written:");
|
|
49
|
+
for (const f of written) console.log(` ✓ ${f}`);
|
|
157
50
|
}
|
|
158
51
|
console.log("\nReady. Run: frappe-builder\n");
|
|
159
52
|
}
|
|
160
|
-
function printCancelled() {
|
|
161
|
-
console.log("\nSetup cancelled. No files were written.\n");
|
|
162
|
-
}
|
|
163
53
|
/**
|
|
164
54
|
* Installs and configures the context-mode pi MCP extension.
|
|
165
55
|
* Clones https://github.com/mksglu/context-mode into ~/.pi/extensions/context-mode,
|
|
@@ -3,7 +3,6 @@ import type { TextContent } from "@mariozechner/pi-ai";
|
|
|
3
3
|
import { db, getCurrentPhase } from "../state/db.js";
|
|
4
4
|
import { appendEntry } from "../state/journal.js";
|
|
5
5
|
import { buildStateContext, loadSpecialist, loadDebuggerSpecialist } from "./frappe-session.js";
|
|
6
|
-
import { loadCredentials } from "../config/loader.js";
|
|
7
6
|
|
|
8
7
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
8
|
export default function (pi: any) {
|
|
@@ -110,18 +109,6 @@ function applyPhaseTransition(featureId: string, newPhase: string): void {
|
|
|
110
109
|
})();
|
|
111
110
|
}
|
|
112
111
|
|
|
113
|
-
/**
|
|
114
|
-
* Session start hook — validates gitignore and loads credentials before any session setup.
|
|
115
|
-
* Throws with AC error message if .frappe-builder-config.json is not gitignored.
|
|
116
|
-
* Call this early in the session_start lifecycle; on throw, halt and surface the message.
|
|
117
|
-
* projectRoot defaults to process.cwd() when not supplied by the pi extension system.
|
|
118
|
-
*
|
|
119
|
-
* TODO: wire this into the pi extension system's session_start event once the
|
|
120
|
-
* session_start hook type is confirmed in @mariozechner/pi-agent-core.
|
|
121
|
-
*/
|
|
122
|
-
export function handleSessionStart(projectRoot: string = process.cwd()): void {
|
|
123
|
-
loadCredentials(projectRoot);
|
|
124
|
-
}
|
|
125
112
|
|
|
126
113
|
/**
|
|
127
114
|
* afterToolCall hook — appends a JSONL journal entry and updates the sessions
|
|
@@ -82,6 +82,9 @@ export default function (pi: any) {
|
|
|
82
82
|
parameters: Type.Object({
|
|
83
83
|
projectId: Type.String({ description: "Project identifier (e.g. 'my-frappe-site')" }),
|
|
84
84
|
sitePath: Type.String({ description: "Absolute path to the Frappe bench site (e.g. '/home/user/frappe-bench/sites/site1.local')" }),
|
|
85
|
+
siteUrl: Type.Optional(Type.String({ description: "Frappe site URL (e.g. 'http://site1.localhost'). Required for frappe_query." })),
|
|
86
|
+
apiKey: Type.Optional(Type.String({ description: "Frappe API key from User > API Access. Required for frappe_query." })),
|
|
87
|
+
apiSecret: Type.Optional(Type.String({ description: "Frappe API secret from User > API Access. Required for frappe_query." })),
|
|
85
88
|
}),
|
|
86
89
|
execute: async (_toolCallId: string, params: ToolParams) => {
|
|
87
90
|
const result = await setActiveProject(params);
|
package/package.json
CHANGED
package/state/db.ts
CHANGED
|
@@ -27,7 +27,16 @@ export function setDb(instance: DatabaseType): void {
|
|
|
27
27
|
*
|
|
28
28
|
* sitePath and appPath are optional so existing callers without them continue to work.
|
|
29
29
|
*/
|
|
30
|
-
export
|
|
30
|
+
export interface ProjectCredentials {
|
|
31
|
+
sitePath?: string;
|
|
32
|
+
appPath?: string;
|
|
33
|
+
siteUrl?: string;
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
apiSecret?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function switchProject(newProjectId: string, creds: ProjectCredentials = {}): void {
|
|
39
|
+
const { sitePath, appPath, siteUrl, apiKey, apiSecret } = creds;
|
|
31
40
|
db.transaction(() => {
|
|
32
41
|
// 1. Read current active session before closing
|
|
33
42
|
const current = db
|
|
@@ -63,13 +72,16 @@ export function switchProject(newProjectId: string, sitePath?: string, appPath?:
|
|
|
63
72
|
|
|
64
73
|
// 5. Create new session, restoring prior phase if available; feature_id defaults to NULL
|
|
65
74
|
db.prepare(
|
|
66
|
-
"INSERT INTO sessions (session_id, project_id, current_phase, site_path, app_path, started_at, is_active) VALUES (?, ?, ?, ?, ?, ?, 1)"
|
|
75
|
+
"INSERT INTO sessions (session_id, project_id, current_phase, site_path, app_path, site_url, api_key, api_secret, started_at, is_active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)"
|
|
67
76
|
).run(
|
|
68
77
|
crypto.randomUUID(),
|
|
69
78
|
newProjectId,
|
|
70
79
|
prior?.current_phase ?? "idle",
|
|
71
80
|
sitePath ?? null,
|
|
72
81
|
appPath ?? null,
|
|
82
|
+
siteUrl ?? null,
|
|
83
|
+
apiKey ?? null,
|
|
84
|
+
apiSecret ?? null,
|
|
73
85
|
new Date().toISOString()
|
|
74
86
|
);
|
|
75
87
|
})();
|
package/state/schema.ts
CHANGED
|
@@ -36,6 +36,9 @@ export function initSchema(db: Database): void {
|
|
|
36
36
|
current_phase TEXT NOT NULL DEFAULT 'idle',
|
|
37
37
|
site_path TEXT,
|
|
38
38
|
app_path TEXT,
|
|
39
|
+
site_url TEXT,
|
|
40
|
+
api_key TEXT,
|
|
41
|
+
api_secret TEXT,
|
|
39
42
|
chain_step TEXT,
|
|
40
43
|
chain_pid INTEGER,
|
|
41
44
|
feature_id TEXT,
|
|
@@ -62,6 +65,9 @@ export function migrateSchema(db: Database): void {
|
|
|
62
65
|
"ALTER TABLE sessions ADD COLUMN app_path TEXT",
|
|
63
66
|
"ALTER TABLE sessions ADD COLUMN chain_step TEXT",
|
|
64
67
|
"ALTER TABLE sessions ADD COLUMN chain_pid INTEGER",
|
|
68
|
+
"ALTER TABLE sessions ADD COLUMN site_url TEXT",
|
|
69
|
+
"ALTER TABLE sessions ADD COLUMN api_key TEXT",
|
|
70
|
+
"ALTER TABLE sessions ADD COLUMN api_secret TEXT",
|
|
65
71
|
];
|
|
66
72
|
for (const sql of alters) {
|
|
67
73
|
try { db.exec(sql); } catch { /* column already exists — safe to ignore */ }
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { routeThroughContextMode } from "./context-sandbox.js";
|
|
1
|
+
import { db } from "../state/db.js";
|
|
2
|
+
import { applyTruncationFallback } from "./context-sandbox.js";
|
|
4
3
|
|
|
5
4
|
export interface FrappeQueryArgs {
|
|
6
5
|
doctype: string;
|
|
@@ -12,33 +11,50 @@ export interface FrappeQueryResult {
|
|
|
12
11
|
error?: string;
|
|
13
12
|
}
|
|
14
13
|
|
|
14
|
+
interface SessionCredentials {
|
|
15
|
+
site_url: string | null;
|
|
16
|
+
api_key: string | null;
|
|
17
|
+
api_secret: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
21
|
+
* Queries Frappe data via direct REST API call using credentials stored in the
|
|
22
|
+
* active session (set via set_active_project).
|
|
23
|
+
* Output is truncation-guarded — never returns raw payloads over 8K tokens.
|
|
18
24
|
* Returns structured error on failure, never throws.
|
|
19
|
-
*
|
|
20
|
-
* Note: mcp2cli command syntax uses --mcp <url> --raw <tool_name> <json_args>.
|
|
21
|
-
* Adapt args array if actual mcp2cli CLI syntax differs.
|
|
22
25
|
*/
|
|
23
26
|
export async function frappeQuery({ doctype, filters }: FrappeQueryArgs): Promise<FrappeQueryResult> {
|
|
24
|
-
const
|
|
27
|
+
const session = db
|
|
28
|
+
.prepare("SELECT site_url, api_key, api_secret FROM sessions WHERE is_active = 1 LIMIT 1")
|
|
29
|
+
.get() as SessionCredentials | undefined;
|
|
25
30
|
|
|
26
|
-
if (!
|
|
27
|
-
return { error: "
|
|
31
|
+
if (!session?.site_url) {
|
|
32
|
+
return { error: "No site_url configured. Call set_active_project with siteUrl, apiKey, and apiSecret first." };
|
|
33
|
+
}
|
|
34
|
+
if (!session.api_key || !session.api_secret) {
|
|
35
|
+
return { error: "No API credentials configured. Call set_active_project with apiKey and apiSecret." };
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
const params = new URLSearchParams();
|
|
40
|
+
if (filters && Object.keys(filters).length > 0) {
|
|
41
|
+
params.set("filters", JSON.stringify(filters));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const url = `${session.site_url}/api/resource/${encodeURIComponent(doctype)}?${params.toString()}`;
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: `token ${session.api_key}:${session.api_secret}`,
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
38
51
|
|
|
39
|
-
if (
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
return { error: `Frappe API error ${response.status}: ${response.statusText}` };
|
|
54
|
+
}
|
|
40
55
|
|
|
41
|
-
const
|
|
56
|
+
const raw = await response.text();
|
|
57
|
+
const summary = applyTruncationFallback(raw);
|
|
42
58
|
return { summary };
|
|
43
59
|
} catch (err) {
|
|
44
60
|
const msg = err instanceof Error ? err.message : String(err);
|
package/tools/project-tools.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { db, switchProject } from "../state/db.js";
|
|
1
|
+
import { db, switchProject, type ProjectCredentials } from "../state/db.js";
|
|
2
2
|
import { reloadSessionContext } from "../extensions/frappe-session.js";
|
|
3
3
|
|
|
4
4
|
export interface ComponentStatus {
|
|
@@ -78,31 +78,32 @@ interface SetActiveProjectArgs {
|
|
|
78
78
|
projectId: string;
|
|
79
79
|
sitePath: string;
|
|
80
80
|
appPath?: string;
|
|
81
|
+
siteUrl?: string;
|
|
82
|
+
apiKey?: string;
|
|
83
|
+
apiSecret?: string;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
87
|
* Switches the active Frappe project and site, flushes current session state,
|
|
85
88
|
* creates a new session, and reloads the system prompt context.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* no second appendEntry() call here.
|
|
89
|
+
* Site credentials (siteUrl, apiKey, apiSecret) are stored in the session row
|
|
90
|
+
* and used by frappe_query for direct REST API calls.
|
|
89
91
|
*/
|
|
90
|
-
export async function setActiveProject({ projectId, sitePath, appPath }: SetActiveProjectArgs) {
|
|
91
|
-
|
|
92
|
-
switchProject(projectId,
|
|
92
|
+
export async function setActiveProject({ projectId, sitePath, appPath, siteUrl, apiKey, apiSecret }: SetActiveProjectArgs) {
|
|
93
|
+
const creds: ProjectCredentials = { sitePath, appPath, siteUrl, apiKey, apiSecret };
|
|
94
|
+
switchProject(projectId, creds);
|
|
93
95
|
|
|
94
|
-
// Reload system prompt with new project context
|
|
95
96
|
await reloadSessionContext();
|
|
96
97
|
|
|
97
|
-
// Read restored phase for return value
|
|
98
98
|
const session = db
|
|
99
|
-
.prepare("SELECT current_phase FROM sessions WHERE is_active = 1 LIMIT 1")
|
|
100
|
-
.get() as { current_phase: string } | undefined;
|
|
99
|
+
.prepare("SELECT current_phase, site_url FROM sessions WHERE is_active = 1 LIMIT 1")
|
|
100
|
+
.get() as { current_phase: string; site_url: string | null } | undefined;
|
|
101
101
|
|
|
102
102
|
return {
|
|
103
103
|
project_id: projectId,
|
|
104
104
|
site_path: sitePath,
|
|
105
105
|
app_path: appPath ?? null,
|
|
106
|
+
site_url: session?.site_url ?? null,
|
|
106
107
|
phase: session?.current_phase ?? "idle",
|
|
107
108
|
context_reloaded: true,
|
|
108
109
|
};
|