@charzhu/openjaw-agent 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -51,7 +51,7 @@ Use the default command for the new TUI, `--legacy-ui` to A/B test against the p
51
51
 
52
52
  ### Prerequisites
53
53
 
54
- - **Node.js 18+** — [Download](https://nodejs.org)
54
+ - **Node.js 22.5+** — [Download](https://nodejs.org). Uses the built-in `node:sqlite` module. Node 23.5+ recommended (its bundled SQLite includes FTS5 for faster memory search; older builds fall back to a LIKE-based scan).
55
55
  - **Windows 10/11** recommended (some automation tools require Windows; cross-platform basics work elsewhere)
56
56
  - **LLM access** — Maestro proxy, Anthropic/OpenAI API keys, or GitHub Copilot via `/connect`
57
57
 
package/dist/main.js CHANGED
@@ -8978,6 +8978,19 @@ import { DatabaseSync } from "node:sqlite";
8978
8978
  import { join as join12 } from "node:path";
8979
8979
  import { homedir as homedir9 } from "node:os";
8980
8980
  import { mkdirSync as mkdirSync8, existsSync as existsSync10 } from "node:fs";
8981
+ function hasFts5() {
8982
+ return _hasFts5;
8983
+ }
8984
+ function detectFts5(database) {
8985
+ const probe = `__fts5_probe_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
8986
+ try {
8987
+ database.exec(`CREATE VIRTUAL TABLE temp.${probe} USING fts5(x)`);
8988
+ database.exec(`DROP TABLE temp.${probe}`);
8989
+ return true;
8990
+ } catch {
8991
+ return false;
8992
+ }
8993
+ }
8981
8994
  function getMemoryDb() {
8982
8995
  if (!db) {
8983
8996
  if (!existsSync10(DB_DIR)) {
@@ -8987,12 +9000,12 @@ function getMemoryDb() {
8987
9000
  db.exec("PRAGMA journal_mode=WAL");
8988
9001
  db.exec("PRAGMA busy_timeout=5000");
8989
9002
  ensureSchema(db);
8990
- logger_default.debug("Memory database opened", { path: DB_PATH });
9003
+ logger_default.debug("Memory database opened", { path: DB_PATH, hasFts5: _hasFts5 });
8991
9004
  }
8992
9005
  return db;
8993
9006
  }
8994
- function ensureSchema(db2) {
8995
- db2.exec(`
9007
+ function ensureSchema(database) {
9008
+ database.exec(`
8996
9009
  CREATE TABLE IF NOT EXISTS memories (
8997
9010
  id INTEGER PRIMARY KEY AUTOINCREMENT,
8998
9011
  content TEXT NOT NULL,
@@ -9002,36 +9015,49 @@ function ensureSchema(db2) {
9002
9015
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
9003
9016
  )
9004
9017
  `);
9005
- db2.exec(`
9006
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
9007
- content,
9008
- content='memories',
9009
- content_rowid='id',
9010
- tokenize='unicode61'
9011
- )
9012
- `);
9013
- db2.exec(`
9014
- CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
9015
- INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
9016
- END
9017
- `);
9018
- db2.exec(`
9019
- CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
9020
- INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.id, old.content);
9021
- END
9022
- `);
9023
- db2.exec(`
9024
- CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
9025
- INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.id, old.content);
9026
- INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
9027
- END
9028
- `);
9018
+ _hasFts5 = detectFts5(database);
9019
+ if (_hasFts5) {
9020
+ database.exec(`
9021
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
9022
+ content,
9023
+ content='memories',
9024
+ content_rowid='id',
9025
+ tokenize='unicode61'
9026
+ )
9027
+ `);
9028
+ database.exec(`
9029
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
9030
+ INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
9031
+ END
9032
+ `);
9033
+ database.exec(`
9034
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
9035
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.id, old.content);
9036
+ END
9037
+ `);
9038
+ database.exec(`
9039
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
9040
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES ('delete', old.id, old.content);
9041
+ INSERT INTO memories_fts(rowid, content) VALUES (new.id, new.content);
9042
+ END
9043
+ `);
9044
+ try {
9045
+ database.exec(`INSERT INTO memories_fts(memories_fts) VALUES('rebuild')`);
9046
+ } catch (err) {
9047
+ logger_default.warn("FTS5 rebuild failed (non-fatal)", { error: String(err) });
9048
+ }
9049
+ } else {
9050
+ database.exec(`DROP TRIGGER IF EXISTS memories_ai`);
9051
+ database.exec(`DROP TRIGGER IF EXISTS memories_ad`);
9052
+ database.exec(`DROP TRIGGER IF EXISTS memories_au`);
9053
+ logger_default.warn("FTS5 not available in this SQLite build \u2014 memory search will use LIKE-based fallback. For best performance, upgrade to Node 23.5+ (bundled SQLite includes FTS5 by default).");
9054
+ }
9029
9055
  try {
9030
- db2.exec("ALTER TABLE memories ADD COLUMN hrr_vector BLOB");
9056
+ database.exec("ALTER TABLE memories ADD COLUMN hrr_vector BLOB");
9031
9057
  } catch {
9032
9058
  }
9033
9059
  }
9034
- var DB_DIR, DB_PATH, db;
9060
+ var DB_DIR, DB_PATH, db, _hasFts5;
9035
9061
  var init_db = __esm({
9036
9062
  "../openjaw-mcp/dist/memory/db.js"() {
9037
9063
  "use strict";
@@ -9039,6 +9065,9 @@ var init_db = __esm({
9039
9065
  DB_DIR = join12(homedir9(), ".openjaw");
9040
9066
  DB_PATH = join12(DB_DIR, "memory.db");
9041
9067
  db = null;
9068
+ _hasFts5 = false;
9069
+ __name(hasFts5, "hasFts5");
9070
+ __name(detectFts5, "detectFts5");
9042
9071
  __name(getMemoryDb, "getMemoryDb");
9043
9072
  __name(ensureSchema, "ensureSchema");
9044
9073
  }
@@ -9135,11 +9164,15 @@ var init_hrr = __esm({
9135
9164
  });
9136
9165
 
9137
9166
  // ../openjaw-mcp/dist/memory/retrieval.js
9138
- var STOPWORDS, DEFAULT_WEIGHTS, HybridRetriever;
9167
+ function escapeLike(token) {
9168
+ return `%${token.replace(/[\\%_]/g, "\\$&")}%`;
9169
+ }
9170
+ var STOPWORDS, MAX_LIKE_TERMS, DEFAULT_WEIGHTS_FTS, DEFAULT_WEIGHTS_NO_FTS, HybridRetriever;
9139
9171
  var init_retrieval = __esm({
9140
9172
  "../openjaw-mcp/dist/memory/retrieval.js"() {
9141
9173
  "use strict";
9142
9174
  init_hrr();
9175
+ init_db();
9143
9176
  init_logger();
9144
9177
  STOPWORDS = /* @__PURE__ */ new Set([
9145
9178
  "a",
@@ -9201,23 +9234,30 @@ var init_retrieval = __esm({
9201
9234
  "should",
9202
9235
  "could"
9203
9236
  ]);
9204
- DEFAULT_WEIGHTS = {
9237
+ MAX_LIKE_TERMS = 16;
9238
+ __name(escapeLike, "escapeLike");
9239
+ DEFAULT_WEIGHTS_FTS = {
9205
9240
  fts: 0.4,
9206
9241
  jaccard: 0.3,
9207
9242
  hrr: 0.3
9208
9243
  };
9244
+ DEFAULT_WEIGHTS_NO_FTS = {
9245
+ fts: 0,
9246
+ jaccard: 0.5,
9247
+ hrr: 0.5
9248
+ };
9209
9249
  HybridRetriever = class {
9210
9250
  static {
9211
9251
  __name(this, "HybridRetriever");
9212
9252
  }
9213
9253
  db;
9214
9254
  weights;
9215
- constructor(db2, weights = DEFAULT_WEIGHTS) {
9255
+ constructor(db2, weights) {
9216
9256
  this.db = db2;
9217
- this.weights = weights;
9257
+ this.weights = weights ?? (hasFts5() ? DEFAULT_WEIGHTS_FTS : DEFAULT_WEIGHTS_NO_FTS);
9218
9258
  }
9219
9259
  /**
9220
- * Hybrid search: FTS5 candidate retrieval → Jaccard + HRR re-ranking.
9260
+ * Hybrid search: FTS5 (or LIKE fallback) candidate retrieval → Jaccard + HRR re-ranking.
9221
9261
  */
9222
9262
  search(query, limit = 10) {
9223
9263
  const queryTokens = query.toLowerCase().match(/[\w]+/g);
@@ -9228,27 +9268,47 @@ var init_retrieval = __esm({
9228
9268
  return [];
9229
9269
  const queryTokenSet = new Set(filtered);
9230
9270
  const candidateLimit = Math.max(limit * 5, 50);
9231
- const ftsQuery = filtered.join(" OR ");
9232
9271
  let hasHrrColumn = true;
9233
9272
  try {
9234
9273
  this.db.prepare("SELECT hrr_vector FROM memories LIMIT 0").all();
9235
9274
  } catch {
9236
9275
  hasHrrColumn = false;
9237
9276
  }
9238
- const selectCols = hasHrrColumn ? "m.id, m.content, m.source, m.created_at, m.hrr_vector, fts.rank" : "m.id, m.content, m.source, m.created_at, fts.rank";
9239
- let candidates;
9240
- try {
9241
- candidates = this.db.prepare(`
9242
- SELECT ${selectCols}
9243
- FROM memories_fts fts
9244
- JOIN memories m ON m.id = fts.rowid
9245
- WHERE memories_fts MATCH ?
9246
- ORDER BY fts.rank
9247
- LIMIT ?
9248
- `).all(ftsQuery, candidateLimit);
9249
- } catch (err) {
9250
- logger_default.warn("FTS5 search failed in hybrid retriever", { query: ftsQuery, error: String(err) });
9251
- return [];
9277
+ let candidates = [];
9278
+ if (hasFts5()) {
9279
+ const selectCols = hasHrrColumn ? "m.id, m.content, m.source, m.created_at, m.hrr_vector, fts.rank" : "m.id, m.content, m.source, m.created_at, fts.rank";
9280
+ const ftsQuery = filtered.join(" OR ");
9281
+ try {
9282
+ candidates = this.db.prepare(`
9283
+ SELECT ${selectCols}
9284
+ FROM memories_fts fts
9285
+ JOIN memories m ON m.id = fts.rowid
9286
+ WHERE memories_fts MATCH ?
9287
+ ORDER BY fts.rank
9288
+ LIMIT ?
9289
+ `).all(ftsQuery, candidateLimit);
9290
+ } catch (err) {
9291
+ logger_default.warn("FTS5 search failed in hybrid retriever", { query: ftsQuery, error: String(err) });
9292
+ return [];
9293
+ }
9294
+ } else {
9295
+ const terms = Array.from(new Set(filtered)).slice(0, MAX_LIKE_TERMS);
9296
+ const selectCols = hasHrrColumn ? "m.id, m.content, m.source, m.created_at, m.hrr_vector, 0 AS rank" : "m.id, m.content, m.source, m.created_at, 0 AS rank";
9297
+ const wheres = terms.map(() => `m.content LIKE ? ESCAPE '\\'`).join(" OR ");
9298
+ const params = terms.map(escapeLike);
9299
+ params.push(candidateLimit);
9300
+ try {
9301
+ candidates = this.db.prepare(`
9302
+ SELECT ${selectCols}
9303
+ FROM memories m
9304
+ WHERE ${wheres}
9305
+ ORDER BY m.created_at DESC, m.id DESC
9306
+ LIMIT ?
9307
+ `).all(...params);
9308
+ } catch (err) {
9309
+ logger_default.warn("LIKE-fallback search failed in hybrid retriever", { error: String(err) });
9310
+ return [];
9311
+ }
9252
9312
  }
9253
9313
  if (candidates.length === 0)
9254
9314
  return [];
@@ -19380,6 +19440,9 @@ import { homedir as homedir11 } from "node:os";
19380
19440
  function setMemoryPrefetchQuery(query) {
19381
19441
  currentQuery = query;
19382
19442
  }
19443
+ function escapeLike2(token) {
19444
+ return `%${token.replace(/[\\%_]/g, "\\$&")}%`;
19445
+ }
19383
19446
  function getMemorySection() {
19384
19447
  if (!currentQuery) return null;
19385
19448
  try {
@@ -19396,15 +19459,34 @@ function getMemorySection() {
19396
19459
  if (!tokens || tokens.length === 0) return null;
19397
19460
  const filtered = tokens.filter((t) => !STOPWORDS2.has(t));
19398
19461
  if (filtered.length === 0) return null;
19399
- const safeQuery = filtered.join(" OR ");
19400
- const rows = db2.prepare(`
19401
- SELECT m.content, m.source, m.created_at
19402
- FROM memories_fts fts
19403
- JOIN memories m ON m.id = fts.rowid
19404
- WHERE memories_fts MATCH ?
19405
- ORDER BY fts.rank
19406
- LIMIT ?
19407
- `).all(safeQuery, MAX_PREFETCH_RESULTS);
19462
+ let rows = [];
19463
+ const ftsQuery = filtered.join(" OR ");
19464
+ try {
19465
+ rows = db2.prepare(`
19466
+ SELECT m.content, m.source, m.created_at
19467
+ FROM memories_fts fts
19468
+ JOIN memories m ON m.id = fts.rowid
19469
+ WHERE memories_fts MATCH ?
19470
+ ORDER BY fts.rank
19471
+ LIMIT ?
19472
+ `).all(ftsQuery, MAX_PREFETCH_RESULTS);
19473
+ } catch {
19474
+ const terms = Array.from(new Set(filtered)).slice(0, MAX_LIKE_TERMS2);
19475
+ const wheres = terms.map(() => `content LIKE ? ESCAPE '\\'`).join(" OR ");
19476
+ const params = terms.map(escapeLike2);
19477
+ params.push(MAX_PREFETCH_RESULTS);
19478
+ try {
19479
+ rows = db2.prepare(`
19480
+ SELECT content, source, created_at
19481
+ FROM memories
19482
+ WHERE ${wheres}
19483
+ ORDER BY created_at DESC, id DESC
19484
+ LIMIT ?
19485
+ `).all(...params);
19486
+ } catch {
19487
+ return null;
19488
+ }
19489
+ }
19408
19490
  if (rows.length === 0) return null;
19409
19491
  let result = "# Relevant Memories\n\nRecalled from your memory database based on your current query:\n\n";
19410
19492
  for (const row of rows) {
@@ -19421,12 +19503,13 @@ function getMemorySection() {
19421
19503
  return null;
19422
19504
  }
19423
19505
  }
19424
- var MAX_PREFETCH_RESULTS, MAX_PREFETCH_CHARS, STOPWORDS2, currentQuery;
19506
+ var MAX_PREFETCH_RESULTS, MAX_PREFETCH_CHARS, MAX_LIKE_TERMS2, STOPWORDS2, currentQuery;
19425
19507
  var init_memory2 = __esm({
19426
19508
  "src/prompts/memory.ts"() {
19427
19509
  "use strict";
19428
19510
  MAX_PREFETCH_RESULTS = 10;
19429
19511
  MAX_PREFETCH_CHARS = 4e3;
19512
+ MAX_LIKE_TERMS2 = 16;
19430
19513
  STOPWORDS2 = /* @__PURE__ */ new Set([
19431
19514
  "a",
19432
19515
  "an",
@@ -19489,6 +19572,7 @@ var init_memory2 = __esm({
19489
19572
  ]);
19490
19573
  currentQuery = null;
19491
19574
  __name(setMemoryPrefetchQuery, "setMemoryPrefetchQuery");
19575
+ __name(escapeLike2, "escapeLike");
19492
19576
  __name(getMemorySection, "getMemorySection");
19493
19577
  }
19494
19578
  });