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 +2 -0
- package/cli.mjs +6 -1
- package/lib/daemon.mjs +1 -1
- package/lib/runtime-install.mjs +1 -1
- package/lib/store.mjs +74 -35
- package/package.json +2 -2
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.
|
|
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.
|
|
13
|
+
const DAEMON_VERSION = "0.4.1";
|
|
14
14
|
const PROTOCOL_VERSION = 1;
|
|
15
15
|
const RESCAN_MS = 60_000;
|
|
16
16
|
|
package/lib/runtime-install.mjs
CHANGED
|
@@ -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, ["
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
db.
|
|
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
|
|
53
|
-
const
|
|
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 =
|
|
64
|
-
const insertSearch =
|
|
65
|
-
const indexParsed =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
163
|
+
limit @limit`, params).map((row) => ({ ...row, message_count: Number(row.message_count) }));
|
|
129
164
|
},
|
|
130
165
|
read({ agent, sessionId }) {
|
|
131
|
-
const row =
|
|
132
|
-
from sessions where agent = ? and session_id =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
values(?, ?, ?, ?, ?, ?, ?, null)
|
|
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
|
-
?
|
|
149
|
-
from handoffs where target_agent = ? and consumed_at is null order by created_at desc limit 20
|
|
150
|
-
:
|
|
151
|
-
from handoffs where consumed_at is null order by created_at desc limit 20`)
|
|
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 =
|
|
155
|
-
if (row && consume)
|
|
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() {
|
|
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.
|
|
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
|
-
"
|
|
32
|
+
"node-sqlite3-wasm": "0.8.60",
|
|
33
33
|
"ws": "^8.18.3"
|
|
34
34
|
}
|
|
35
35
|
}
|