tryharness 0.4.0 → 0.4.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
@@ -6,6 +6,8 @@ Connect the coding agents installed on this machine to [Harness](https://tryharn
6
6
  npx tryharness
7
7
  ```
8
8
 
9
+ Supports the Node.js 18, 20, 22, and 24 release lines.
10
+
9
11
  That one command pairs the machine, detects Claude Code, Codex, pi, and opencode,
10
12
  configures every installed agent, installs the Harness daemon as a user-level login
11
13
  service, validates readiness, and starts indexing existing coding sessions locally in
package/cli.mjs CHANGED
@@ -12,8 +12,13 @@ import { installRuntime } from "./lib/runtime-install.mjs";
12
12
  import { installService, serviceInfo, uninstallService } from "./lib/service.mjs";
13
13
 
14
14
  const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
15
- const PACKAGE = readJson(path.join(SOURCE_ROOT, "package.json"), { version: "0.4.0" });
15
+ const PACKAGE = readJson(path.join(SOURCE_ROOT, "package.json"), { version: "0.4.1" });
16
16
  const VERSION = PACKAGE.version;
17
+ const NODE_MAJOR = Number(process.versions.node.split(".")[0]);
18
+ if (!Number.isFinite(NODE_MAJOR) || NODE_MAJOR < 18) {
19
+ console.error(`tryharness requires Node.js 18 or newer (current: ${process.version}). Upgrade Node, then rerun: npx tryharness`);
20
+ process.exit(1);
21
+ }
17
22
  const argv = process.argv.slice(2);
18
23
  const command = ["--help", "-h"].includes(argv[0])
19
24
  ? (argv.shift(), "help")
package/lib/daemon.mjs CHANGED
@@ -10,7 +10,7 @@ import { historyRoots, paths } from "./paths.mjs";
10
10
  import { openStore } from "./store.mjs";
11
11
  import { runAgentTask } from "./runner.mjs";
12
12
 
13
- const DAEMON_VERSION = "0.4.0";
13
+ const DAEMON_VERSION = "0.4.1";
14
14
  const PROTOCOL_VERSION = 1;
15
15
  const RESCAN_MS = 60_000;
16
16
 
@@ -59,7 +59,7 @@ export function installRuntime({ sourceRoot, version }) {
59
59
  if (!fs.existsSync(source)) continue;
60
60
  fs.cpSync(source, path.join(staging, entry), { recursive: true, force: true });
61
61
  }
62
- copyDependencyClosure(sourceRoot, staging, ["better-sqlite3", "ws"]);
62
+ copyDependencyClosure(sourceRoot, staging, ["node-sqlite3-wasm", "ws"]);
63
63
  fs.writeFileSync(path.join(staging, ".ready"), version + "\n", { mode: 0o600 });
64
64
  ensureDir(path.dirname(release));
65
65
  fs.rmSync(release, { recursive: true, force: true });
package/lib/store.mjs CHANGED
@@ -1,13 +1,23 @@
1
- import Database from "better-sqlite3";
1
+ import sqliteWasm from "node-sqlite3-wasm";
2
2
  import { parseSession } from "./parsers.mjs";
3
3
 
4
4
  const SCHEMA_VERSION = 2;
5
+ const { Database } = sqliteWasm;
6
+
7
+ function bindings(values) {
8
+ if (!values.length) return undefined;
9
+ const value = values.length === 1 ? values[0] : values;
10
+ if (!value || Array.isArray(value) || typeof value !== "object") return value;
11
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => (
12
+ [key.startsWith("@") || key.startsWith(":") || key.startsWith("$") ? key : `@${key}`, entry]
13
+ )));
14
+ }
5
15
 
6
16
  export function openStore(file) {
7
17
  const db = new Database(file);
8
- db.pragma("journal_mode = WAL");
9
- db.pragma("synchronous = NORMAL");
10
- db.pragma("busy_timeout = 5000");
18
+ // The daemon is the database's single writer. DELETE mode is portable across the
19
+ // WASM VFS on every supported Node LTS line; no native addon or ABI is involved.
20
+ db.exec("pragma journal_mode = DELETE; pragma synchronous = NORMAL; pragma busy_timeout = 5000;");
11
21
  db.exec(`
12
22
  create table if not exists schema_info (version integer not null);
13
23
  insert into schema_info(version) select ${SCHEMA_VERSION} where not exists (select 1 from schema_info);
@@ -49,8 +59,26 @@ export function openStore(file) {
49
59
  );
50
60
  `);
51
61
 
52
- const existing = db.prepare("select source_mtime, source_size from sessions where agent = ? and session_id = ?");
53
- const upsert = db.prepare(`
62
+ const statements = [];
63
+ const prepare = (sql) => {
64
+ const raw = db.prepare(sql);
65
+ statements.push(raw);
66
+ const call = (method, values) => {
67
+ const bound = bindings(values);
68
+ return bound === undefined ? raw[method]() : raw[method](bound);
69
+ };
70
+ return {
71
+ get: (...values) => call("get", values),
72
+ all: (...values) => call("all", values),
73
+ run: (...values) => call("run", values),
74
+ };
75
+ };
76
+ const execute = (method, sql, ...values) => {
77
+ const bound = bindings(values);
78
+ return bound === undefined ? db[method](sql) : db[method](sql, bound);
79
+ };
80
+ const existing = prepare("select source_mtime, source_size from sessions where agent = ? and session_id = ?");
81
+ const upsert = prepare(`
54
82
  insert into sessions(agent, session_id, source, cwd, title, started_at, ended_at, message_count, source_mtime, source_size, transcript, indexed_at)
55
83
  values(@agent, @sessionId, @source, @cwd, @title, @startedAt, @endedAt, @messageCount, @mtime, @size, @transcript, @indexedAt)
56
84
  on conflict(agent, session_id) do update set
@@ -60,15 +88,22 @@ export function openStore(file) {
60
88
  source_size=excluded.source_size, transcript=excluded.transcript,
61
89
  indexed_at=excluded.indexed_at
62
90
  `);
63
- const deleteSearch = db.prepare("delete from session_fts where agent = ? and session_id = ?");
64
- const insertSearch = db.prepare("insert into session_fts(agent, session_id, title, cwd, transcript) values(?, ?, ?, ?, ?)");
65
- const indexParsed = db.transaction((record) => {
66
- upsert.run(record);
67
- deleteSearch.run(record.agent, record.sessionId);
68
- insertSearch.run(record.agent, record.sessionId, record.title, record.cwd, record.transcript);
69
- });
70
- const sessionCount = Number(db.prepare("select count(*) as count from sessions").get().count);
71
- const searchCount = Number(db.prepare("select count(*) as count from session_fts").get().count);
91
+ const deleteSearch = prepare("delete from session_fts where agent = ? and session_id = ?");
92
+ const insertSearch = prepare("insert into session_fts(agent, session_id, title, cwd, transcript) values(?, ?, ?, ?, ?)");
93
+ const indexParsed = (record) => {
94
+ db.exec("begin immediate");
95
+ try {
96
+ upsert.run(record);
97
+ deleteSearch.run(record.agent, record.sessionId);
98
+ insertSearch.run(record.agent, record.sessionId, record.title, record.cwd, record.transcript);
99
+ db.exec("commit");
100
+ } catch (error) {
101
+ if (db.inTransaction) db.exec("rollback");
102
+ throw error;
103
+ }
104
+ };
105
+ const sessionCount = Number(execute("get", "select count(*) as count from sessions").count);
106
+ const searchCount = Number(execute("get", "select count(*) as count from session_fts").count);
72
107
  if (sessionCount !== searchCount) {
73
108
  db.exec(`delete from session_fts;
74
109
  insert into session_fts(agent, session_id, title, cwd, transcript)
@@ -92,7 +127,7 @@ export function openStore(file) {
92
127
  return parsed.meta;
93
128
  },
94
129
  counts() {
95
- const rows = db.prepare("select agent, count(*) as count from sessions group by agent").all();
130
+ const rows = execute("all", "select agent, count(*) as count from sessions group by agent");
96
131
  return Object.fromEntries(rows.map((row) => [row.agent, Number(row.count)]));
97
132
  },
98
133
  search({ query = "", agent, days, limit = 12 }) {
@@ -106,9 +141,9 @@ export function openStore(file) {
106
141
  const phrase = String(query).trim().toLowerCase();
107
142
  const boundedLimit = Math.max(1, Math.min(50, Number(limit) || 12));
108
143
  if (!phrase) {
109
- return db.prepare(`select sessions.agent, session_id, title, cwd, started_at, ended_at, message_count, null as excerpt
144
+ return execute("all", `select sessions.agent, session_id, title, cwd, started_at, ended_at, message_count, null as excerpt
110
145
  from sessions ${clauses.length ? `where ${clauses.join(" and ")}` : ""}
111
- order by coalesce(started_at, indexed_at) desc limit @limit`).all({ ...params, limit: boundedLimit })
146
+ order by coalesce(started_at, indexed_at) desc limit @limit`, { ...params, limit: boundedLimit })
112
147
  .map((row) => ({ ...row, message_count: Number(row.message_count) }));
113
148
  }
114
149
  const terms = [...new Set(phrase.match(/[\p{L}\p{N}_-]+/gu) || [])].filter((term) => term.length > 1).slice(0, 8);
@@ -117,7 +152,7 @@ export function openStore(file) {
117
152
  params.phrase = phrase;
118
153
  params.limit = boundedLimit;
119
154
  const where = ["session_fts match @match", ...clauses].join(" and ");
120
- return db.prepare(`select sessions.agent, sessions.session_id, sessions.title, sessions.cwd,
155
+ return execute("all", `select sessions.agent, sessions.session_id, sessions.title, sessions.cwd,
121
156
  sessions.started_at, sessions.ended_at, sessions.message_count,
122
157
  snippet(session_fts, 4, '', '', ' … ', 48) as excerpt
123
158
  from session_fts join sessions
@@ -125,36 +160,40 @@ export function openStore(file) {
125
160
  where ${where}
126
161
  order by case when instr(lower(sessions.title || ' ' || session_fts.transcript), @phrase) > 0 then 0 else 1 end,
127
162
  bm25(session_fts), coalesce(sessions.started_at, sessions.indexed_at) desc
128
- limit @limit`).all(params).map((row) => ({ ...row, message_count: Number(row.message_count) }));
163
+ limit @limit`, params).map((row) => ({ ...row, message_count: Number(row.message_count) }));
129
164
  },
130
165
  read({ agent, sessionId }) {
131
- const row = db.prepare(`select agent, session_id, title, cwd, started_at, ended_at, message_count, transcript, source
132
- from sessions where agent = ? and session_id = ?`).get(agent, sessionId);
166
+ const row = execute("get", `select agent, session_id, title, cwd, started_at, ended_at, message_count, transcript, source
167
+ from sessions where agent = ? and session_id = ?`, agent, sessionId);
133
168
  return row ? { ...row, message_count: Number(row.message_count) } : null;
134
169
  },
135
170
  findSource(agent, sessionHint) {
136
- const row = db.prepare(`select source, cwd from sessions
171
+ const found = execute("get", `select source, cwd from sessions
137
172
  where agent = ? and (session_id = ? or instr(session_id, ?) > 0)
138
- order by case when session_id = ? then 0 else 1 end, indexed_at desc limit 1`)
139
- .get(agent, sessionHint, sessionHint, sessionHint);
140
- return row || null;
173
+ order by case when session_id = ? then 0 else 1 end, indexed_at desc limit 1`, agent, sessionHint, sessionHint, sessionHint);
174
+ return found || null;
141
175
  },
142
176
  addHandoff({ id, targetAgent, targetSessionId, title, body, source }) {
143
- db.prepare(`insert or replace into handoffs(id, target_agent, target_session_id, title, body, source, created_at, consumed_at)
144
- values(?, ?, ?, ?, ?, ?, ?, null)`).run(id, targetAgent, targetSessionId || null, title, body, source || null, new Date().toISOString());
177
+ execute("run", `insert or replace into handoffs(id, target_agent, target_session_id, title, body, source, created_at, consumed_at)
178
+ values(?, ?, ?, ?, ?, ?, ?, null)`, id, targetAgent, targetSessionId || null, title, body, source || null, new Date().toISOString());
145
179
  },
146
180
  listHandoffs(targetAgent) {
147
181
  return targetAgent
148
- ? db.prepare(`select id, target_agent, target_session_id, title, source, created_at
149
- from handoffs where target_agent = ? and consumed_at is null order by created_at desc limit 20`).all(targetAgent)
150
- : db.prepare(`select id, target_agent, target_session_id, title, source, created_at
151
- from handoffs where consumed_at is null order by created_at desc limit 20`).all();
182
+ ? execute("all", `select id, target_agent, target_session_id, title, source, created_at
183
+ from handoffs where target_agent = ? and consumed_at is null order by created_at desc limit 20`, targetAgent)
184
+ : execute("all", `select id, target_agent, target_session_id, title, source, created_at
185
+ from handoffs where consumed_at is null order by created_at desc limit 20`);
152
186
  },
153
187
  getHandoff(id, consume = false) {
154
- const row = db.prepare("select * from handoffs where id = ?").get(id);
155
- if (row && consume) db.prepare("update handoffs set consumed_at = ? where id = ?").run(new Date().toISOString(), id);
188
+ const row = execute("get", "select * from handoffs where id = ?", id);
189
+ if (row && consume) execute("run", "update handoffs set consumed_at = ? where id = ?", new Date().toISOString(), id);
156
190
  return row || null;
157
191
  },
158
- close() { db.close(); },
192
+ close() {
193
+ for (const statement of statements) {
194
+ try { statement.finalize(); } catch {}
195
+ }
196
+ db.close();
197
+ },
159
198
  };
160
199
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tryharness",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "One-command local daemon for Harness memory, coding-agent history, handoffs, and delegated tasks.",
5
5
  "bin": {
6
6
  "tryharness": "cli.mjs"
@@ -29,7 +29,7 @@
29
29
  "homepage": "https://tryharness.ai",
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
- "better-sqlite3": "11.10.0",
32
+ "node-sqlite3-wasm": "0.8.60",
33
33
  "ws": "^8.18.3"
34
34
  }
35
35
  }