bonecode 1.3.0 → 1.4.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 (32) hide show
  1. package/README.md +42 -0
  2. package/compat/opencode_adapter.ts +69 -8
  3. package/dist/compat/opencode_adapter.js +63 -7
  4. package/dist/compat/opencode_adapter.js.map +1 -1
  5. package/dist/src/db_adapter.js +30 -0
  6. package/dist/src/db_adapter.js.map +1 -1
  7. package/dist/src/engine/session/build_mode.d.ts +83 -0
  8. package/dist/src/engine/session/build_mode.js +800 -0
  9. package/dist/src/engine/session/build_mode.js.map +1 -0
  10. package/dist/src/engine/session/build_mode_helpers.d.ts +6 -0
  11. package/dist/src/engine/session/build_mode_helpers.js +61 -0
  12. package/dist/src/engine/session/build_mode_helpers.js.map +1 -0
  13. package/dist/src/engine/session/leaked_tool_call.d.ts +49 -0
  14. package/dist/src/engine/session/leaked_tool_call.js +174 -0
  15. package/dist/src/engine/session/leaked_tool_call.js.map +1 -0
  16. package/dist/src/engine/session/prompt/bonescript.txt +11 -0
  17. package/dist/src/engine/session/prompt.js +173 -2
  18. package/dist/src/engine/session/prompt.js.map +1 -1
  19. package/dist/src/tui.js +146 -9
  20. package/dist/src/tui.js.map +1 -1
  21. package/package.json +1 -1
  22. package/scripts/debug_extract.js +40 -0
  23. package/scripts/test_build_fallback.js +221 -0
  24. package/scripts/test_build_mode.js +301 -0
  25. package/scripts/test_leaked_tool_call.js +269 -0
  26. package/src/db_adapter.ts +29 -0
  27. package/src/engine/session/build_mode.ts +906 -0
  28. package/src/engine/session/build_mode_helpers.ts +72 -0
  29. package/src/engine/session/leaked_tool_call.ts +166 -0
  30. package/src/engine/session/prompt/bonescript.txt +11 -0
  31. package/src/engine/session/prompt.ts +219 -2
  32. package/src/tui.ts +147 -9
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for the leaked tool-call parser. Loads the compiled module directly
4
+ * so tests run against the same code that ships.
5
+ *
6
+ * Patterns tested are taken from real model outputs:
7
+ * - gemma: <|tool_call>call:edit{file_path:<|"|>foo.bone<|"|>}<tool_call|>
8
+ * - qwen: <tool_call>{"name":"write","arguments":{"path":"x"}}</tool_call>
9
+ * - llama3: <|python_tag|>write({"path":"x"})<|/python_tag|>
10
+ * - openai-style fenced: ```tool_code\nname(arg=val)\n```
11
+ *
12
+ * Also re-tests isBuildPrompt against the prompts that previously slipped
13
+ * through (e.g. "using BoneScript as the backend, write a python ...").
14
+ */
15
+
16
+ "use strict";
17
+ const fs = require("fs");
18
+ const path = require("path");
19
+
20
+ const G = "\x1b[32m"; const R = "\x1b[31m"; const C = "\x1b[36m";
21
+ const B = "\x1b[1m"; const D = "\x1b[2m"; const N = "\x1b[0m";
22
+
23
+ let passed = 0;
24
+ let failed = 0;
25
+ const failures = [];
26
+
27
+ function ok(name, info = "") {
28
+ passed++;
29
+ console.log(` ${G}✓${N} ${name}${info ? ` ${D}${info}${N}` : ""}`);
30
+ }
31
+ function fail(name, msg) {
32
+ failed++;
33
+ failures.push(`${name}: ${msg}`);
34
+ console.log(` ${R}✗${N} ${name} ${R}${msg}${N}`);
35
+ }
36
+ function header(s) { console.log(`\n${C}${B}${s}${N}`); }
37
+
38
+ const ROOT = path.resolve(__dirname, "..");
39
+ const modulePath = path.join(ROOT, "dist", "src", "engine", "session", "leaked_tool_call.js");
40
+
41
+ if (!fs.existsSync(modulePath)) {
42
+ console.error(`${R}Compiled module not found at ${modulePath}.${N}`);
43
+ console.error(`Run \`npm run build\` first.`);
44
+ process.exit(1);
45
+ }
46
+
47
+ const {
48
+ extractLeakedToolCall,
49
+ parseLeakedBody,
50
+ parseKwargs,
51
+ parseLooseObject,
52
+ } = require(modulePath);
53
+
54
+ // ─── Tests: gemma-style markers ───────────────────────────────────────────────
55
+
56
+ header("[1] Gemma-style leaked calls (the user's exact bug)");
57
+
58
+ (() => {
59
+ const text = `I'll create the file.\n<|tool_call>call:edit{file_path:<|"|>medieval_market.bone<|"|>}<tool_call|>\nDone.`;
60
+ const r = extractLeakedToolCall(text);
61
+ if (r && r.toolName === "edit" && r.toolInput.file_path === "medieval_market.bone") {
62
+ ok("gemma <|tool_call>call:name{...}<tool_call|>", `→ edit(file_path="${r.toolInput.file_path}")`);
63
+ } else {
64
+ fail("gemma exact bug", JSON.stringify(r));
65
+ }
66
+ })();
67
+
68
+ (() => {
69
+ const text = `<|tool_call|>{"name":"write","arguments":{"path":"foo.ts","content":"hello"}}<|/tool_call|>`;
70
+ const r = extractLeakedToolCall(text);
71
+ if (r && r.toolName === "write" && r.toolInput.path === "foo.ts" && r.toolInput.content === "hello") {
72
+ ok("gemma <|tool_call|>{json}<|/tool_call|>");
73
+ } else {
74
+ fail("gemma JSON form", JSON.stringify(r));
75
+ }
76
+ })();
77
+
78
+ // ─── Tests: qwen-style markers ────────────────────────────────────────────────
79
+
80
+ header("[2] Qwen-style leaked calls");
81
+
82
+ (() => {
83
+ const text = `<tool_call>{"name":"bash","arguments":{"command":"ls -la"}}</tool_call>`;
84
+ const r = extractLeakedToolCall(text);
85
+ if (r && r.toolName === "bash" && r.toolInput.command === "ls -la") {
86
+ ok("<tool_call>{json}</tool_call>");
87
+ } else {
88
+ fail("qwen", JSON.stringify(r));
89
+ }
90
+ })();
91
+
92
+ (() => {
93
+ const text = `<tool_call>{"tool":"read","args":{"path":"src/main.ts"}}</tool_call>`;
94
+ const r = extractLeakedToolCall(text);
95
+ if (r && r.toolName === "read" && r.toolInput.path === "src/main.ts") {
96
+ ok("<tool_call>{tool: ..., args: ...}</tool_call>");
97
+ } else {
98
+ fail("qwen alt keys", JSON.stringify(r));
99
+ }
100
+ })();
101
+
102
+ // ─── Tests: llama3-style python_tag ───────────────────────────────────────────
103
+
104
+ header("[3] llama3-style <|python_tag|>");
105
+
106
+ (() => {
107
+ const text = `<|python_tag|>write({"path":"x.txt","content":"y"})<|/python_tag|>`;
108
+ const r = extractLeakedToolCall(text);
109
+ if (r && r.toolName === "write" && r.toolInput.path === "x.txt" && r.toolInput.content === "y") {
110
+ ok("llama3 python_tag with JSON arg");
111
+ } else {
112
+ fail("llama3", JSON.stringify(r));
113
+ }
114
+ })();
115
+
116
+ // ─── Tests: function-call kwargs ──────────────────────────────────────────────
117
+
118
+ header("[4] Function-call kwargs syntax");
119
+
120
+ (() => {
121
+ const args = parseKwargs(`path="foo.ts", content="hello world"`);
122
+ if (args && args.path === "foo.ts" && args.content === "hello world") ok("string kwargs");
123
+ else fail("string kwargs", JSON.stringify(args));
124
+ })();
125
+
126
+ (() => {
127
+ const args = parseKwargs(`count=42, ratio=3.14, enabled=true, missing=null`);
128
+ if (args && args.count === 42 && args.ratio === 3.14 && args.enabled === true && args.missing === null) {
129
+ ok("typed kwargs (number, float, bool, null)");
130
+ } else {
131
+ fail("typed kwargs", JSON.stringify(args));
132
+ }
133
+ })();
134
+
135
+ (() => {
136
+ const args = parseKwargs(`file_path=<|"|>medieval_market.bone<|"|>`);
137
+ if (args && args.file_path === "medieval_market.bone") ok(`<|"|> escapes are stripped`);
138
+ else fail("escape markers", JSON.stringify(args));
139
+ })();
140
+
141
+ // ─── Tests: loose-object form ─────────────────────────────────────────────────
142
+
143
+ header("[5] Loose-object form (pseudo-JSON)");
144
+
145
+ (() => {
146
+ const o = parseLooseObject(`file_path:"foo.bone", count:3`);
147
+ if (o && o.file_path === "foo.bone" && o.count === 3) ok("colon-separated loose object");
148
+ else fail("loose object", JSON.stringify(o));
149
+ })();
150
+
151
+ // ─── Tests: fenced tool_code ──────────────────────────────────────────────────
152
+
153
+ header("[6] Fenced tool_code blocks");
154
+
155
+ (() => {
156
+ const text = "Some prose\n```tool_code\nwrite(path=\"x\", content=\"y\")\n```\nMore prose.";
157
+ const r = extractLeakedToolCall(text);
158
+ if (r && r.toolName === "write" && r.toolInput.path === "x" && r.toolInput.content === "y") {
159
+ ok("```tool_code\\nname(args)\\n```");
160
+ } else {
161
+ fail("fenced tool_code", JSON.stringify(r));
162
+ }
163
+ })();
164
+
165
+ // ─── Tests: false-positives ───────────────────────────────────────────────────
166
+
167
+ header("[7] No false-positives on plain text");
168
+
169
+ const cleanCases = [
170
+ "I'll create a file called foo.bone now.",
171
+ "Use the `write` tool to save the file.",
172
+ "Here's how you'd do it: write(path, content) — but that's pseudocode.",
173
+ "<not_a_tool_call>just text</not_a_tool_call>",
174
+ "",
175
+ "<tool_call></tool_call>",
176
+ ];
177
+ for (const c of cleanCases) {
178
+ const r = extractLeakedToolCall(c);
179
+ if (r === null) ok(`clean: "${c.slice(0, 50)}..."`);
180
+ else fail(`false positive`, `"${c}" → ${JSON.stringify(r)}`);
181
+ }
182
+
183
+ // ─── Tests: stripping positions ───────────────────────────────────────────────
184
+
185
+ header("[8] startIndex/endIndex enable text stripping");
186
+
187
+ (() => {
188
+ const text = `Before <|tool_call|>{"name":"write","arguments":{}}<|/tool_call|> after`;
189
+ const r = extractLeakedToolCall(text);
190
+ if (!r) {
191
+ fail("strip positions", "no match");
192
+ } else {
193
+ const stripped = text.slice(0, r.startIndex) + text.slice(r.endIndex);
194
+ if (stripped === "Before after") ok("text stripped cleanly", `"${stripped}"`);
195
+ else fail("strip", `got "${stripped}"`);
196
+ }
197
+ })();
198
+
199
+ // ─── Tests: build mode trigger detection ──────────────────────────────────────
200
+
201
+ header("[9] isBuildPrompt covers the previously-missed prompts");
202
+
203
+ const bmModulePath = path.join(ROOT, "dist", "src", "engine", "session", "build_mode.js");
204
+ if (!fs.existsSync(bmModulePath)) {
205
+ fail("build_mode module", "compiled file missing");
206
+ } else {
207
+ const { isBuildPrompt } = require(bmModulePath);
208
+
209
+ const newCases = [
210
+ // The exact prompt that previously failed in the user's session
211
+ "using BoneScript as the backend, write a python 2d mideveal copper silver gold platinum transaction market simulation",
212
+ "with bonescript, build a chat app",
213
+ "in BoneScript, design a multi-tenant CRM",
214
+ "BoneScript backend for a music streaming service",
215
+ "write me a REST API for a todo list",
216
+ "develop a graphql api for users",
217
+ "scaffold a web application with auth",
218
+ ];
219
+ for (const p of newCases) {
220
+ if (isBuildPrompt(p)) ok(`triggers: "${p.slice(0, 60)}..."`);
221
+ else fail(`missed`, p);
222
+ }
223
+
224
+ const negative = [
225
+ "what does this function do",
226
+ "explain the difference between let and const",
227
+ "fix the typo on line 5",
228
+ ];
229
+ for (const p of negative) {
230
+ if (!isBuildPrompt(p)) ok(`not triggered: "${p}"`);
231
+ else fail(`over-matched`, p);
232
+ }
233
+ }
234
+
235
+ // ─── Tests: parseLeakedBody handles edge cases ────────────────────────────────
236
+
237
+ header("[10] parseLeakedBody edge cases");
238
+
239
+ (() => {
240
+ const r = parseLeakedBody("");
241
+ if (r === null) ok("empty body returns null");
242
+ else fail("empty", JSON.stringify(r));
243
+ })();
244
+
245
+ (() => {
246
+ const r = parseLeakedBody("not a tool call at all");
247
+ if (r === null) ok("garbage body returns null");
248
+ else fail("garbage", JSON.stringify(r));
249
+ })();
250
+
251
+ (() => {
252
+ // Function call with JSON arg
253
+ const r = parseLeakedBody('write({"path": "a.txt", "content": "b"})');
254
+ if (r && r.toolName === "write" && r.toolInput.path === "a.txt" && r.toolInput.content === "b") {
255
+ ok("function with JSON arg");
256
+ } else {
257
+ fail("function JSON arg", JSON.stringify(r));
258
+ }
259
+ })();
260
+
261
+ console.log();
262
+ if (failed === 0) {
263
+ console.log(`${G}${B}✓ All ${passed} tests passed${N}`);
264
+ process.exit(0);
265
+ } else {
266
+ console.log(`${R}${B}✗ ${failed} failed, ${passed} passed${N}`);
267
+ for (const f of failures) console.log(` ${R}- ${f}${N}`);
268
+ process.exit(1);
269
+ }
package/src/db_adapter.ts CHANGED
@@ -164,6 +164,7 @@ CREATE TABLE IF NOT EXISTS sessions (
164
164
  summary_deletions INTEGER,
165
165
  share_url TEXT,
166
166
  workspace_id TEXT,
167
+ build_state TEXT,
167
168
  created_at TEXT DEFAULT (datetime('now')),
168
169
  updated_at TEXT DEFAULT (datetime('now'))
169
170
  );
@@ -309,6 +310,7 @@ export async function initDatabase(silent = false): Promise<{
309
310
  process.stderr.write(`[DB] Pool error: ${err.message}\n`);
310
311
  }
311
312
  });
313
+ await ensurePostgresColumns(pgPool);
312
314
  return { pool: pgPool, mode: "postgres", pgAvailable: true };
313
315
  }
314
316
  log(`\x1b[33m[BoneCode] Postgres not reachable at ${dbUrl.replace(/:[^:@]+@/, ":***@")} — trying Docker...\x1b[0m`);
@@ -330,6 +332,7 @@ export async function initDatabase(silent = false): Promise<{
330
332
  }
331
333
  });
332
334
  log(`\x1b[32m[BoneCode] PostgreSQL ready (Docker) — full RAG enabled\x1b[0m`);
335
+ await ensurePostgresColumns(pgPool);
333
336
  return { pool: pgPool, mode: "postgres", pgAvailable: true };
334
337
  }
335
338
  }
@@ -348,7 +351,33 @@ export async function initDatabase(silent = false): Promise<{
348
351
  const Database = require("better-sqlite3");
349
352
  const db = new Database(dbPath);
350
353
  db.exec(SQLITE_SCHEMA);
354
+ // Idempotent migrations for older databases — add new columns if missing.
355
+ // SQLite has no IF NOT EXISTS for columns, so we swallow the duplicate-column error.
356
+ const safeAlter = (sql: string) => { try { db.exec(sql); } catch { /* column exists */ } };
357
+ safeAlter(`ALTER TABLE sessions ADD COLUMN build_state TEXT`);
351
358
  db.close();
352
359
 
353
360
  return { pool: sqlitePool, mode: "sqlite", pgAvailable: false };
354
361
  }
362
+
363
+
364
+ // ─── Postgres column migrations ───────────────────────────────────────────────
365
+
366
+ /**
367
+ * Idempotently add columns the BoneScript-generated schema doesn't include.
368
+ * Each ALTER is run with IF NOT EXISTS / pg_class probing so it's safe to run
369
+ * on every server startup.
370
+ */
371
+ async function ensurePostgresColumns(pool: any): Promise<void> {
372
+ const migrations: Array<{ table: string; column: string; type: string }> = [
373
+ { table: "sessions", column: "build_state", type: "JSONB" },
374
+ ];
375
+ for (const m of migrations) {
376
+ try {
377
+ await pool.query(`ALTER TABLE ${m.table} ADD COLUMN IF NOT EXISTS ${m.column} ${m.type}`);
378
+ } catch {
379
+ // Either the table doesn't exist yet (first run) or the user has
380
+ // restricted permissions. Build mode falls back to permission_ruleset.build.
381
+ }
382
+ }
383
+ }