openhivemind 0.1.0 → 0.2.0
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/dist/cli.js +424 -142
- package/package.json +1 -1
- package/plugins/claude-code/dist/cli.js +424 -142
- package/plugins/codex/dist/cli.js +424 -142
- package/plugins/opencode/dist/cli.js +9008 -0
- package/plugins/opencode/openhivemind.js +29 -0
- package/plugins/opencode/skills/gist/SKILL.md +8 -0
- package/plugins/opencode/skills/search/SKILL.md +10 -0
- package/plugins/opencode/skills/setup/SKILL.md +8 -0
- package/plugins/opencode/skills/share/SKILL.md +8 -0
package/dist/cli.js
CHANGED
|
@@ -532,8 +532,8 @@ var require_graceful_fs = __commonJS({
|
|
|
532
532
|
fs2.createReadStream = createReadStream;
|
|
533
533
|
fs2.createWriteStream = createWriteStream;
|
|
534
534
|
var fs$readFile = fs2.readFile;
|
|
535
|
-
fs2.readFile =
|
|
536
|
-
function
|
|
535
|
+
fs2.readFile = readFile8;
|
|
536
|
+
function readFile8(path, options2, cb) {
|
|
537
537
|
if (typeof options2 === "function")
|
|
538
538
|
cb = options2, options2 = null;
|
|
539
539
|
return go$readFile(path, options2, cb);
|
|
@@ -709,7 +709,7 @@ var require_graceful_fs = __commonJS({
|
|
|
709
709
|
}
|
|
710
710
|
function ReadStream$open() {
|
|
711
711
|
var that = this;
|
|
712
|
-
|
|
712
|
+
open5(that.path, that.flags, that.mode, function(err, fd) {
|
|
713
713
|
if (err) {
|
|
714
714
|
if (that.autoClose)
|
|
715
715
|
that.destroy();
|
|
@@ -729,7 +729,7 @@ var require_graceful_fs = __commonJS({
|
|
|
729
729
|
}
|
|
730
730
|
function WriteStream$open() {
|
|
731
731
|
var that = this;
|
|
732
|
-
|
|
732
|
+
open5(that.path, that.flags, that.mode, function(err, fd) {
|
|
733
733
|
if (err) {
|
|
734
734
|
that.destroy();
|
|
735
735
|
that.emit("error", err);
|
|
@@ -746,8 +746,8 @@ var require_graceful_fs = __commonJS({
|
|
|
746
746
|
return new fs2.WriteStream(path, options2);
|
|
747
747
|
}
|
|
748
748
|
var fs$open = fs2.open;
|
|
749
|
-
fs2.open =
|
|
750
|
-
function
|
|
749
|
+
fs2.open = open5;
|
|
750
|
+
function open5(path, flags, mode, cb) {
|
|
751
751
|
if (typeof mode === "function")
|
|
752
752
|
cb = mode, mode = null;
|
|
753
753
|
return go$open(path, flags, mode, cb);
|
|
@@ -1623,7 +1623,7 @@ var require_proper_lockfile = __commonJS({
|
|
|
1623
1623
|
});
|
|
1624
1624
|
|
|
1625
1625
|
// src/cli.ts
|
|
1626
|
-
import { readdir as readdir6, readFile as
|
|
1626
|
+
import { readdir as readdir6, readFile as readFile7 } from "fs/promises";
|
|
1627
1627
|
import { resolve as resolve3 } from "path";
|
|
1628
1628
|
|
|
1629
1629
|
// ../node_modules/.pnpm/@sinclair+typebox@0.34.52/node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
|
|
@@ -3217,11 +3217,11 @@ function FromProperties7(value) {
|
|
|
3217
3217
|
Acc[K] = Readonly(FromValue(value[K], false));
|
|
3218
3218
|
return Acc;
|
|
3219
3219
|
}
|
|
3220
|
-
function ConditionalReadonly(T,
|
|
3221
|
-
return
|
|
3220
|
+
function ConditionalReadonly(T, root2) {
|
|
3221
|
+
return root2 === true ? T : Readonly(T);
|
|
3222
3222
|
}
|
|
3223
|
-
function FromValue(value,
|
|
3224
|
-
return IsAsyncIterator(value) ? ConditionalReadonly(Any(),
|
|
3223
|
+
function FromValue(value, root2) {
|
|
3224
|
+
return IsAsyncIterator(value) ? ConditionalReadonly(Any(), root2) : IsIterator(value) ? ConditionalReadonly(Any(), root2) : IsArray(value) ? Readonly(Tuple(FromArray3(value))) : IsUint8Array(value) ? Uint8Array2() : IsDate(value) ? Date2() : IsObject(value) ? ConditionalReadonly(Object2(FromProperties7(value)), root2) : IsFunction(value) ? ConditionalReadonly(Function([], Unknown()), root2) : IsUndefined(value) ? Undefined() : IsNull(value) ? Null() : IsSymbol(value) ? Symbol2() : IsBigInt(value) ? BigInt2() : IsNumber(value) ? Literal(value) : IsBoolean(value) ? Literal(value) : IsString(value) ? Literal(value) : Object2({});
|
|
3225
3225
|
}
|
|
3226
3226
|
function Const(T, options2) {
|
|
3227
3227
|
return CreateType(FromValue(T, true), options2);
|
|
@@ -4487,8 +4487,8 @@ var Invite = Type.Object({
|
|
|
4487
4487
|
expires_at: Timestamp
|
|
4488
4488
|
});
|
|
4489
4489
|
var empty = Type.Object({});
|
|
4490
|
-
function route(method, path, params, body, response,
|
|
4491
|
-
return { method, path, params, body, response, query, status };
|
|
4490
|
+
function route(method, path, params, body, response, query2, status = 200) {
|
|
4491
|
+
return { method, path, params, body, response, query: query2, status };
|
|
4492
4492
|
}
|
|
4493
4493
|
var sessionId = Type.Object({ id: Id });
|
|
4494
4494
|
var ok = Type.Object({ ok: Type.Literal(true) });
|
|
@@ -4548,8 +4548,10 @@ var routes = {
|
|
|
4548
4548
|
around: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
4549
4549
|
context: Type.Optional(Type.Integer({ minimum: 0, maximum: 100, default: 10 })),
|
|
4550
4550
|
last: Type.Optional(Type.Integer({ minimum: 1, maximum: 500 })),
|
|
4551
|
+
cursor: Type.Optional(string()),
|
|
4551
4552
|
kind: Type.Optional(Kind2),
|
|
4552
|
-
maxChars: Type.Optional(Type.Integer({ minimum: 1, maximum: 2e5, default: 2e4 }))
|
|
4553
|
+
maxChars: Type.Optional(Type.Integer({ minimum: 1, maximum: 2e5, default: 2e4 })),
|
|
4554
|
+
wholeMessages: Type.Optional(Type.Boolean())
|
|
4553
4555
|
})
|
|
4554
4556
|
),
|
|
4555
4557
|
search: route(
|
|
@@ -7160,6 +7162,10 @@ var record = (value) => value !== null && typeof value === "object" && !Array.is
|
|
|
7160
7162
|
var str = (value, fallback = "") => typeof value === "string" ? value : fallback;
|
|
7161
7163
|
var num = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
7162
7164
|
var list = (value) => Array.isArray(value) ? value : [];
|
|
7165
|
+
var INJECTED_WRAPPER = /<system-reminder>[\s\S]*?<\/system-reminder>|<command-name>[\s\S]*?<\/command-name>|<command-message>[\s\S]*?<\/command-message>|<command-args>[\s\S]*?<\/command-args>/g;
|
|
7166
|
+
function stripInjectedPrompt(text) {
|
|
7167
|
+
return text.replace(INJECTED_WRAPPER, "").trim();
|
|
7168
|
+
}
|
|
7163
7169
|
function parseRecords(source, rows, previous) {
|
|
7164
7170
|
const messages = previous?.messages.map((message) => ({ ...message })) ?? [];
|
|
7165
7171
|
const meta2 = {};
|
|
@@ -7194,16 +7200,27 @@ function parseRecords(source, rows, previous) {
|
|
|
7194
7200
|
if (data.cwd) meta2.cwd = str(data.cwd);
|
|
7195
7201
|
if (data.version) meta2.version = str(data.version);
|
|
7196
7202
|
if (branch) meta2.branch = branch;
|
|
7203
|
+
if (data.type === "ai-title") {
|
|
7204
|
+
const title = str(data.aiTitle, str(payload.aiTitle));
|
|
7205
|
+
if (title.trim()) meta2.title = title;
|
|
7206
|
+
return;
|
|
7207
|
+
}
|
|
7197
7208
|
if (data.type !== "user" && data.type !== "assistant") return;
|
|
7209
|
+
if (data.type === "user" && data.isMeta) return;
|
|
7198
7210
|
model = str(message.model) || model;
|
|
7199
7211
|
const kind = data.isCompactSummary ? "summary" : data.type === "user" ? "prompt" : "reply";
|
|
7212
|
+
const clean = (text) => kind === "prompt" ? stripInjectedPrompt(text) : text;
|
|
7200
7213
|
const before = messages.length;
|
|
7201
|
-
if (typeof message.content === "string")
|
|
7202
|
-
|
|
7214
|
+
if (typeof message.content === "string") {
|
|
7215
|
+
const text = clean(message.content);
|
|
7216
|
+
if (text) emit2(kind, text, "text");
|
|
7217
|
+
} else
|
|
7203
7218
|
list(message.content).forEach((value, blockIndex) => {
|
|
7204
7219
|
const block = record(value);
|
|
7205
|
-
if (block.type === "text")
|
|
7206
|
-
|
|
7220
|
+
if (block.type === "text") {
|
|
7221
|
+
const text = clean(str(block.text));
|
|
7222
|
+
if (text) emit2(kind, text, String(blockIndex));
|
|
7223
|
+
} else if (block.type === "tool_use")
|
|
7207
7224
|
emit2(
|
|
7208
7225
|
"tool_call",
|
|
7209
7226
|
toolSummary(str(block.name), block.input),
|
|
@@ -7325,10 +7342,10 @@ import { stat as stat5 } from "fs/promises";
|
|
|
7325
7342
|
|
|
7326
7343
|
// src/capture.ts
|
|
7327
7344
|
var import_proper_lockfile = __toESM(require_proper_lockfile(), 1);
|
|
7328
|
-
import { mkdir as mkdir2, readFile, open as
|
|
7345
|
+
import { mkdir as mkdir2, readFile, open as open3, readdir, stat as stat2, realpath } from "fs/promises";
|
|
7329
7346
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7330
|
-
import { homedir as
|
|
7331
|
-
import { join as
|
|
7347
|
+
import { homedir as homedir3 } from "os";
|
|
7348
|
+
import { join as join3, relative, isAbsolute, resolve } from "path";
|
|
7332
7349
|
import { execFile } from "child_process";
|
|
7333
7350
|
import { promisify } from "util";
|
|
7334
7351
|
|
|
@@ -7383,6 +7400,136 @@ function errorClass(error) {
|
|
|
7383
7400
|
return error instanceof Error ? error.name : "unknown";
|
|
7384
7401
|
}
|
|
7385
7402
|
|
|
7403
|
+
// src/harnesses/opencode.ts
|
|
7404
|
+
import { homedir as homedir2 } from "os";
|
|
7405
|
+
import { join as join2 } from "path";
|
|
7406
|
+
var dataHome = () => process.env["XDG_DATA_HOME"] || join2(homedir2(), ".local/share");
|
|
7407
|
+
var opencodeDatabase = () => join2(dataHome(), "opencode", "opencode.db");
|
|
7408
|
+
var PLACEHOLDER = /^New session\b/;
|
|
7409
|
+
function open2(path) {
|
|
7410
|
+
const { DatabaseSync } = __require(["node", "sqlite"].join(":"));
|
|
7411
|
+
return new DatabaseSync(path, { readOnly: true, timeout: 2e3 });
|
|
7412
|
+
}
|
|
7413
|
+
function query(path, run) {
|
|
7414
|
+
const database = open2(path);
|
|
7415
|
+
try {
|
|
7416
|
+
return run(database);
|
|
7417
|
+
} finally {
|
|
7418
|
+
database.close();
|
|
7419
|
+
}
|
|
7420
|
+
}
|
|
7421
|
+
function safely(fallback, run) {
|
|
7422
|
+
try {
|
|
7423
|
+
return run();
|
|
7424
|
+
} catch {
|
|
7425
|
+
return fallback;
|
|
7426
|
+
}
|
|
7427
|
+
}
|
|
7428
|
+
var str2 = (value) => typeof value === "string" ? value : "";
|
|
7429
|
+
function sessionRow(path, sessionId2) {
|
|
7430
|
+
return query(
|
|
7431
|
+
path,
|
|
7432
|
+
(database) => database.prepare("SELECT parent_id, directory, title, version FROM session WHERE id = ?").get(sessionId2)
|
|
7433
|
+
);
|
|
7434
|
+
}
|
|
7435
|
+
function opencodeTitle(path, sessionId2) {
|
|
7436
|
+
const title = safely(void 0, () => sessionRow(path, sessionId2))?.title;
|
|
7437
|
+
return title && !PLACEHOLDER.test(title) ? title : void 0;
|
|
7438
|
+
}
|
|
7439
|
+
function root(path, row) {
|
|
7440
|
+
let id = row.parent_id;
|
|
7441
|
+
for (let depth = 1; id && depth <= 8; depth++) {
|
|
7442
|
+
const parent = sessionRow(path, id);
|
|
7443
|
+
if (!parent?.parent_id) return { id, depth };
|
|
7444
|
+
id = parent.parent_id;
|
|
7445
|
+
}
|
|
7446
|
+
return void 0;
|
|
7447
|
+
}
|
|
7448
|
+
function opencodeEvent(input) {
|
|
7449
|
+
const value = input ?? {};
|
|
7450
|
+
const sessionId2 = str2(value["sessionId"]);
|
|
7451
|
+
if (!sessionId2) throw new Error("Hook input is missing sessionId");
|
|
7452
|
+
const transcriptPath = str2(value["dbPath"]) || opencodeDatabase();
|
|
7453
|
+
const row = sessionRow(transcriptPath, sessionId2);
|
|
7454
|
+
if (!row) throw new Error("No opencode session for this id");
|
|
7455
|
+
const title = PLACEHOLDER.test(row.title) ? "" : row.title;
|
|
7456
|
+
const parent = root(transcriptPath, row);
|
|
7457
|
+
return {
|
|
7458
|
+
sessionId: sessionId2,
|
|
7459
|
+
transcriptPath,
|
|
7460
|
+
cwd: row.directory || str2(value["cwd"]),
|
|
7461
|
+
source: "opencode",
|
|
7462
|
+
...row.version ? { version: row.version } : {},
|
|
7463
|
+
...title ? { title } : {},
|
|
7464
|
+
...parent ? { parentId: parent.id, spawnDepth: parent.depth } : {}
|
|
7465
|
+
};
|
|
7466
|
+
}
|
|
7467
|
+
function opencodeChildren(event) {
|
|
7468
|
+
const children2 = [];
|
|
7469
|
+
safely(
|
|
7470
|
+
void 0,
|
|
7471
|
+
() => query(event.transcriptPath, (database) => {
|
|
7472
|
+
const statement = database.prepare(
|
|
7473
|
+
"SELECT id, directory, title, version FROM session WHERE parent_id = ?"
|
|
7474
|
+
);
|
|
7475
|
+
let frontier = [event.sessionId];
|
|
7476
|
+
for (let depth = 1; frontier.length && depth <= 8; depth++) {
|
|
7477
|
+
const next = [];
|
|
7478
|
+
for (const parent of frontier)
|
|
7479
|
+
for (const row of statement.all(parent)) {
|
|
7480
|
+
const id = str2(row["id"]);
|
|
7481
|
+
const title = str2(row["title"]);
|
|
7482
|
+
next.push(id);
|
|
7483
|
+
children2.push({
|
|
7484
|
+
sessionId: id,
|
|
7485
|
+
transcriptPath: event.transcriptPath,
|
|
7486
|
+
cwd: str2(row["directory"]) || event.cwd,
|
|
7487
|
+
source: "opencode",
|
|
7488
|
+
parentId: event.sessionId,
|
|
7489
|
+
spawnDepth: depth,
|
|
7490
|
+
...str2(row["version"]) ? { version: str2(row["version"]) } : {},
|
|
7491
|
+
...title && !PLACEHOLDER.test(title) ? { title } : {},
|
|
7492
|
+
...event.completed ? { completed: true } : {}
|
|
7493
|
+
});
|
|
7494
|
+
}
|
|
7495
|
+
frontier = next;
|
|
7496
|
+
}
|
|
7497
|
+
})
|
|
7498
|
+
);
|
|
7499
|
+
return children2;
|
|
7500
|
+
}
|
|
7501
|
+
function opencodeRecords(path, sessionId2, cursor) {
|
|
7502
|
+
return query(path, (database) => {
|
|
7503
|
+
const messages = database.prepare(
|
|
7504
|
+
`SELECT m.id AS id, m.time_created AS created, m.data AS data,
|
|
7505
|
+
max(m.time_updated,
|
|
7506
|
+
coalesce((SELECT max(p.time_updated) FROM part p WHERE p.message_id = m.id), 0))
|
|
7507
|
+
AS updated
|
|
7508
|
+
FROM message m WHERE m.session_id = ? ORDER BY m.time_created, m.id`
|
|
7509
|
+
).all(sessionId2);
|
|
7510
|
+
const parts = database.prepare("SELECT id, data FROM part WHERE message_id = ? ORDER BY id");
|
|
7511
|
+
let next = cursor;
|
|
7512
|
+
const records = [];
|
|
7513
|
+
for (const message of messages) {
|
|
7514
|
+
const updated = Number(message["updated"] ?? 0);
|
|
7515
|
+
if (updated > next) next = updated;
|
|
7516
|
+
if (updated <= cursor) continue;
|
|
7517
|
+
records.push({
|
|
7518
|
+
id: str2(message["id"]),
|
|
7519
|
+
created_at: Number(message["created"] ?? 0),
|
|
7520
|
+
info: JSON.parse(str2(message["data"]) || "{}"),
|
|
7521
|
+
parts: parts.all(str2(message["id"])).map(
|
|
7522
|
+
(part) => ({
|
|
7523
|
+
...JSON.parse(str2(part["data"]) || "{}"),
|
|
7524
|
+
id: str2(part["id"])
|
|
7525
|
+
})
|
|
7526
|
+
)
|
|
7527
|
+
});
|
|
7528
|
+
}
|
|
7529
|
+
return { records, cursor: next };
|
|
7530
|
+
});
|
|
7531
|
+
}
|
|
7532
|
+
|
|
7386
7533
|
// src/capture.ts
|
|
7387
7534
|
var exec = promisify(execFile);
|
|
7388
7535
|
var TITLE_MAX = 120;
|
|
@@ -7393,8 +7540,11 @@ function deriveTitle(text) {
|
|
|
7393
7540
|
const boundary = cut.lastIndexOf(" ");
|
|
7394
7541
|
return (boundary > 0 ? cut.slice(0, boundary) : cut).trimEnd() + "\u2026";
|
|
7395
7542
|
}
|
|
7396
|
-
function
|
|
7397
|
-
|
|
7543
|
+
function placeholderTitle(title) {
|
|
7544
|
+
return !title || /^(untitled(?: session)?|new chat)$/i.test(title.trim());
|
|
7545
|
+
}
|
|
7546
|
+
function inside(path, root2) {
|
|
7547
|
+
const rel = relative(root2, path);
|
|
7398
7548
|
return rel === "" || !rel.startsWith("../") && rel !== ".." && !isAbsolute(rel);
|
|
7399
7549
|
}
|
|
7400
7550
|
async function allowed(cwd, config) {
|
|
@@ -7404,18 +7554,18 @@ async function allowed(cwd, config) {
|
|
|
7404
7554
|
resolveAll(config.roots),
|
|
7405
7555
|
resolveAll(config.exclude)
|
|
7406
7556
|
]);
|
|
7407
|
-
return !exclude.some((
|
|
7557
|
+
return !exclude.some((root2) => inside(path, root2)) && (!roots.length || roots.some((root2) => inside(path, root2)));
|
|
7408
7558
|
}
|
|
7409
7559
|
async function patterns2(cwd) {
|
|
7410
7560
|
const result = [];
|
|
7411
|
-
let
|
|
7561
|
+
let root2 = cwd;
|
|
7412
7562
|
try {
|
|
7413
|
-
|
|
7563
|
+
root2 = (await exec("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { timeout: 5e3 })).stdout.trim();
|
|
7414
7564
|
} catch {
|
|
7415
7565
|
}
|
|
7416
7566
|
for (const path of [
|
|
7417
|
-
|
|
7418
|
-
|
|
7567
|
+
join3(root2, ".openhivemind-ignore"),
|
|
7568
|
+
join3(homedir3(), ".config/openhivemind/ignore")
|
|
7419
7569
|
]) {
|
|
7420
7570
|
try {
|
|
7421
7571
|
result.push(...ignorePatterns(await readFile(path, "utf8")));
|
|
@@ -7428,7 +7578,7 @@ async function patterns2(cwd) {
|
|
|
7428
7578
|
}
|
|
7429
7579
|
async function readState(folder, event) {
|
|
7430
7580
|
try {
|
|
7431
|
-
return JSON.parse(await readFile(
|
|
7581
|
+
return JSON.parse(await readFile(join3(folder, "state.json"), "utf8"));
|
|
7432
7582
|
} catch (error) {
|
|
7433
7583
|
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT"))
|
|
7434
7584
|
throw error;
|
|
@@ -7445,7 +7595,7 @@ async function readState(folder, event) {
|
|
|
7445
7595
|
};
|
|
7446
7596
|
}
|
|
7447
7597
|
}
|
|
7448
|
-
var sessionFolder = (config, source, sessionId2) =>
|
|
7598
|
+
var sessionFolder = (config, source, sessionId2) => join3(stateRoot(config), source, digest(sessionId2));
|
|
7449
7599
|
var UNSPOOLED_STATUSES = /* @__PURE__ */ new Set(["excluded", "no origin", "sensitive locator"]);
|
|
7450
7600
|
var wasSpooled = (status) => !UNSPOOLED_STATUSES.has(status);
|
|
7451
7601
|
async function capture(event, config, options2 = {}) {
|
|
@@ -7471,7 +7621,7 @@ async function capture(event, config, options2 = {}) {
|
|
|
7471
7621
|
};
|
|
7472
7622
|
if (JSON.stringify(scrubValue(locator, extra)) !== JSON.stringify(locator))
|
|
7473
7623
|
return { status: "sensitive locator", chunks: 0, bytes: 0 };
|
|
7474
|
-
const
|
|
7624
|
+
const root2 = stateRoot(config);
|
|
7475
7625
|
const folder = sessionFolder(config, event.source, event.sessionId);
|
|
7476
7626
|
await mkdir2(folder, { recursive: true, mode: 448 });
|
|
7477
7627
|
const release = await import_proper_lockfile.default.lock(folder, {
|
|
@@ -7483,28 +7633,45 @@ async function capture(event, config, options2 = {}) {
|
|
|
7483
7633
|
let state = await readState(folder, event);
|
|
7484
7634
|
const files = (await readdir(folder)).filter((name) => name.endsWith(".batch.json")).sort();
|
|
7485
7635
|
for (const name of files) {
|
|
7486
|
-
const envelope2 = JSON.parse(await readFile(
|
|
7636
|
+
const envelope2 = JSON.parse(await readFile(join3(folder, name), "utf8"));
|
|
7487
7637
|
if (envelope2.after.generation > state.generation) state = envelope2.after;
|
|
7488
7638
|
}
|
|
7489
|
-
const info = await stat2(event.transcriptPath);
|
|
7490
|
-
const restart = info.ino !== state.inode || info.size < state.offset;
|
|
7491
|
-
const offset = restart ? 0 : state.offset;
|
|
7492
|
-
const file = await open2(event.transcriptPath, "r");
|
|
7493
|
-
let buffer;
|
|
7494
|
-
try {
|
|
7495
|
-
buffer = Buffer.alloc(Math.min(info.size - offset, options2.readWindow ?? 16 * 1024 * 1024));
|
|
7496
|
-
const result = await file.read(buffer, 0, buffer.length, offset);
|
|
7497
|
-
buffer = buffer.subarray(0, result.bytesRead);
|
|
7498
|
-
} finally {
|
|
7499
|
-
await file.close();
|
|
7500
|
-
}
|
|
7501
|
-
const end = buffer.lastIndexOf(10);
|
|
7502
|
-
if (end < 0 && offset + buffer.length < info.size)
|
|
7503
|
-
return { status: "line exceeds window", chunks: 0, bytes: 0 };
|
|
7504
7639
|
const closing = Boolean(event.completed) && Boolean(state.meta) && !state.meta?.completed;
|
|
7505
|
-
|
|
7506
|
-
const
|
|
7507
|
-
|
|
7640
|
+
const eventTitle = event.title ? deriveTitle(scrubValue(event.title, extra)) : void 0;
|
|
7641
|
+
const titleChanged = Boolean(state.meta && eventTitle && eventTitle !== state.meta.title);
|
|
7642
|
+
let records;
|
|
7643
|
+
let offset;
|
|
7644
|
+
let inode;
|
|
7645
|
+
let restart = false;
|
|
7646
|
+
if (event.source === "opencode") {
|
|
7647
|
+
const read = opencodeRecords(event.transcriptPath, event.sessionId, state.offset);
|
|
7648
|
+
records = read.records;
|
|
7649
|
+
offset = read.cursor;
|
|
7650
|
+
inode = state.inode;
|
|
7651
|
+
if (!records.length && !closing && !titleChanged)
|
|
7652
|
+
return { status: "no complete records", chunks: 0, bytes: 0 };
|
|
7653
|
+
} else {
|
|
7654
|
+
const info = await stat2(event.transcriptPath);
|
|
7655
|
+
restart = info.ino !== state.inode || info.size < state.offset;
|
|
7656
|
+
inode = info.ino;
|
|
7657
|
+
const from = restart ? 0 : state.offset;
|
|
7658
|
+
const file = await open3(event.transcriptPath, "r");
|
|
7659
|
+
let buffer;
|
|
7660
|
+
try {
|
|
7661
|
+
buffer = Buffer.alloc(Math.min(info.size - from, options2.readWindow ?? 16 * 1024 * 1024));
|
|
7662
|
+
const result = await file.read(buffer, 0, buffer.length, from);
|
|
7663
|
+
buffer = buffer.subarray(0, result.bytesRead);
|
|
7664
|
+
} finally {
|
|
7665
|
+
await file.close();
|
|
7666
|
+
}
|
|
7667
|
+
const end = buffer.lastIndexOf(10);
|
|
7668
|
+
if (end < 0 && from + buffer.length < info.size)
|
|
7669
|
+
return { status: "line exceeds window", chunks: 0, bytes: 0 };
|
|
7670
|
+
if (end < 0 && !closing && !titleChanged)
|
|
7671
|
+
return { status: "no complete records", chunks: 0, bytes: 0 };
|
|
7672
|
+
offset = from + end + 1;
|
|
7673
|
+
records = buffer.subarray(0, end + 1).toString("utf8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
7674
|
+
}
|
|
7508
7675
|
const parsed = parseRecords(event.source, records, {
|
|
7509
7676
|
messages: restart ? [] : state.pending,
|
|
7510
7677
|
seenUsage: restart ? [] : state.seenUsage,
|
|
@@ -7524,7 +7691,13 @@ async function capture(event, config, options2 = {}) {
|
|
|
7524
7691
|
cwd: event.cwd,
|
|
7525
7692
|
branch: safe.meta.branch ?? state.meta?.branch ?? "",
|
|
7526
7693
|
branches: [.../* @__PURE__ */ new Set([...state.meta?.branches ?? [], safe.meta.branch ?? ""])],
|
|
7527
|
-
|
|
7694
|
+
// Native harness names are more useful than a prompt-derived fallback and may only be
|
|
7695
|
+
// written after an earlier capture. The title is scrubbed before it is clamped.
|
|
7696
|
+
title: (() => {
|
|
7697
|
+
const native = (safe.meta.title ? deriveTitle(safe.meta.title) : void 0) ?? eventTitle;
|
|
7698
|
+
const prompt = firstPrompt ? deriveTitle(firstPrompt) : void 0;
|
|
7699
|
+
return (!placeholderTitle(native) ? native : void 0) ?? (!placeholderTitle(state.meta?.title) ? state.meta?.title : void 0) ?? prompt ?? native ?? state.meta?.title ?? "Untitled session";
|
|
7700
|
+
})(),
|
|
7528
7701
|
started_at: state.meta?.started_at ?? safe.messages[0]?.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7529
7702
|
completed: Boolean(state.meta?.completed || event.completed),
|
|
7530
7703
|
spawn_depth: state.meta?.spawn_depth ?? event.spawnDepth ?? 0,
|
|
@@ -7571,8 +7744,8 @@ async function capture(event, config, options2 = {}) {
|
|
|
7571
7744
|
}
|
|
7572
7745
|
const after = {
|
|
7573
7746
|
generation: state.generation + 1,
|
|
7574
|
-
offset
|
|
7575
|
-
inode
|
|
7747
|
+
offset,
|
|
7748
|
+
inode,
|
|
7576
7749
|
recordIndex: (restart ? 0 : state.recordIndex) + records.length,
|
|
7577
7750
|
nextSeq,
|
|
7578
7751
|
entries,
|
|
@@ -7593,24 +7766,28 @@ async function capture(event, config, options2 = {}) {
|
|
|
7593
7766
|
}
|
|
7594
7767
|
batch.push(message);
|
|
7595
7768
|
}
|
|
7596
|
-
|
|
7769
|
+
const published = Boolean(state.published || Object.keys(state.entries).length);
|
|
7770
|
+
const metaChanged = Boolean(
|
|
7771
|
+
published && state.meta && JSON.stringify(state.meta) !== JSON.stringify(meta2)
|
|
7772
|
+
);
|
|
7773
|
+
if (batch.length || metaChanged)
|
|
7597
7774
|
payloads.push({ protocolVersion: 1, chunkId: randomUUID2(), meta: meta2, messages: batch });
|
|
7598
7775
|
if (options2.dryRun) return { status: "dry run", chunks: payloads.length, bytes: 0, payloads };
|
|
7599
7776
|
const envelope = { chunks: payloads, after };
|
|
7600
7777
|
const serialized = JSON.stringify(envelope);
|
|
7601
|
-
const pendingSize = await spoolBytes(
|
|
7778
|
+
const pendingSize = await spoolBytes(root2);
|
|
7602
7779
|
if (pendingSize + Buffer.byteLength(serialized) > (config.spoolLimit ?? 256 * 1024 * 1024)) {
|
|
7603
|
-
await atomic(
|
|
7780
|
+
await atomic(join3(folder, "state.json"), { ...state, event, paused: true });
|
|
7604
7781
|
return { status: "paused", chunks: 0, bytes: 0 };
|
|
7605
7782
|
}
|
|
7606
7783
|
if (payloads.length) {
|
|
7607
7784
|
await atomic(
|
|
7608
|
-
|
|
7785
|
+
join3(folder, String(after.generation).padStart(16, "0") + ".batch.json"),
|
|
7609
7786
|
envelope
|
|
7610
7787
|
);
|
|
7611
7788
|
await options2.afterChunk?.();
|
|
7612
7789
|
}
|
|
7613
|
-
await atomic(
|
|
7790
|
+
await atomic(join3(folder, "state.json"), after);
|
|
7614
7791
|
return {
|
|
7615
7792
|
status: "captured",
|
|
7616
7793
|
chunks: payloads.length,
|
|
@@ -7620,19 +7797,19 @@ async function capture(event, config, options2 = {}) {
|
|
|
7620
7797
|
await release();
|
|
7621
7798
|
}
|
|
7622
7799
|
}
|
|
7623
|
-
async function spoolBytes(
|
|
7800
|
+
async function spoolBytes(root2) {
|
|
7624
7801
|
let total = 0;
|
|
7625
|
-
for (const file of await readdir(
|
|
7802
|
+
for (const file of await readdir(root2, { recursive: true, withFileTypes: true })) {
|
|
7626
7803
|
if (file.isFile() && file.name.endsWith(".batch.json"))
|
|
7627
|
-
total += (await stat2(
|
|
7804
|
+
total += (await stat2(join3(file.parentPath, file.name))).size;
|
|
7628
7805
|
}
|
|
7629
7806
|
return total;
|
|
7630
7807
|
}
|
|
7631
7808
|
|
|
7632
7809
|
// src/harnesses/claude-code.ts
|
|
7633
7810
|
import { readFile as readFile2, readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
7634
|
-
import { homedir as
|
|
7635
|
-
import { basename, dirname as dirname2, join as
|
|
7811
|
+
import { homedir as homedir4 } from "os";
|
|
7812
|
+
import { basename, dirname as dirname2, join as join4 } from "path";
|
|
7636
7813
|
function claudeCodeEvent(input) {
|
|
7637
7814
|
if (!input || typeof input !== "object") throw new Error("Hook input is not an object");
|
|
7638
7815
|
const value = input;
|
|
@@ -7651,8 +7828,28 @@ function claudeCodeEvent(input) {
|
|
|
7651
7828
|
...event === "SessionEnd" ? { completed: true } : {}
|
|
7652
7829
|
};
|
|
7653
7830
|
}
|
|
7831
|
+
async function claudeCodeTitle(transcriptPath) {
|
|
7832
|
+
let lines;
|
|
7833
|
+
try {
|
|
7834
|
+
lines = (await readFile2(transcriptPath, "utf8")).split("\n").filter(Boolean);
|
|
7835
|
+
} catch {
|
|
7836
|
+
return void 0;
|
|
7837
|
+
}
|
|
7838
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
7839
|
+
try {
|
|
7840
|
+
const record3 = JSON.parse(lines[index]);
|
|
7841
|
+
if (record3.type !== "ai-title") continue;
|
|
7842
|
+
const direct = record3.aiTitle;
|
|
7843
|
+
const payload = record3.payload;
|
|
7844
|
+
const title = typeof direct === "string" ? direct : payload?.aiTitle;
|
|
7845
|
+
if (typeof title === "string" && title.trim()) return title;
|
|
7846
|
+
} catch {
|
|
7847
|
+
}
|
|
7848
|
+
}
|
|
7849
|
+
return void 0;
|
|
7850
|
+
}
|
|
7654
7851
|
async function claudeCodeChildren(event) {
|
|
7655
|
-
const folder =
|
|
7852
|
+
const folder = join4(dirname2(event.transcriptPath), event.sessionId, "subagents");
|
|
7656
7853
|
const names = (await readdir2(folder).catch(() => [])).filter(
|
|
7657
7854
|
(name) => /^agent-.+\.jsonl$/.test(name)
|
|
7658
7855
|
);
|
|
@@ -7660,13 +7857,13 @@ async function claudeCodeChildren(event) {
|
|
|
7660
7857
|
for (const name of names.sort()) {
|
|
7661
7858
|
const agentId = name.slice("agent-".length, -".jsonl".length);
|
|
7662
7859
|
const meta2 = JSON.parse(
|
|
7663
|
-
await readFile2(
|
|
7860
|
+
await readFile2(join4(folder, name.replace(/\.jsonl$/, ".meta.json")), "utf8").catch(
|
|
7664
7861
|
() => "{}"
|
|
7665
7862
|
)
|
|
7666
7863
|
);
|
|
7667
7864
|
children2.push({
|
|
7668
7865
|
sessionId: `${event.sessionId}:${agentId}`,
|
|
7669
|
-
transcriptPath:
|
|
7866
|
+
transcriptPath: join4(folder, name),
|
|
7670
7867
|
cwd: event.cwd,
|
|
7671
7868
|
source: event.source,
|
|
7672
7869
|
parentId: event.sessionId,
|
|
@@ -7683,11 +7880,11 @@ async function claudeCodeLocate(input) {
|
|
|
7683
7880
|
let transcriptPath = input;
|
|
7684
7881
|
if (!input.endsWith(".jsonl")) {
|
|
7685
7882
|
if (!/^[0-9a-f-]{8,}$/i.test(input)) return void 0;
|
|
7686
|
-
const
|
|
7687
|
-
const projects = await readdir2(
|
|
7883
|
+
const root2 = join4(homedir4(), ".claude/projects");
|
|
7884
|
+
const projects = await readdir2(root2).catch(() => []);
|
|
7688
7885
|
const candidates = (await Promise.all(
|
|
7689
7886
|
projects.map(async (project) => {
|
|
7690
|
-
const path =
|
|
7887
|
+
const path = join4(root2, project, input + ".jsonl");
|
|
7691
7888
|
return await stat3(path).catch(() => void 0) ? path : void 0;
|
|
7692
7889
|
})
|
|
7693
7890
|
)).filter((path) => path !== void 0);
|
|
@@ -7738,15 +7935,45 @@ async function claudeCodeLocate(input) {
|
|
|
7738
7935
|
import { basename as basename3 } from "path";
|
|
7739
7936
|
|
|
7740
7937
|
// src/harnesses/codex.ts
|
|
7741
|
-
import { open as
|
|
7742
|
-
import { homedir as
|
|
7743
|
-
import { basename as basename2, dirname as dirname3, join as
|
|
7938
|
+
import { open as open4, readFile as readFile3, readdir as readdir3 } from "fs/promises";
|
|
7939
|
+
import { homedir as homedir5 } from "os";
|
|
7940
|
+
import { basename as basename2, dirname as dirname3, join as join5, sep } from "path";
|
|
7744
7941
|
var FIRST_LINE = 64 * 1024;
|
|
7745
7942
|
var ROLLOUT = /^rollout-.+\.jsonl$/;
|
|
7746
|
-
var sessionsRoot = () =>
|
|
7943
|
+
var sessionsRoot = () => join5(process.env["CODEX_HOME"] || join5(homedir5(), ".codex"), "sessions");
|
|
7944
|
+
var codexHome = () => process.env["CODEX_HOME"] || join5(homedir5(), ".codex");
|
|
7747
7945
|
var record2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7946
|
+
async function codexTitle(sessionId2) {
|
|
7947
|
+
try {
|
|
7948
|
+
const lines = (await readFile3(join5(codexHome(), "session_index.jsonl"), "utf8")).split("\n").filter(Boolean);
|
|
7949
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
7950
|
+
try {
|
|
7951
|
+
const item = record2(JSON.parse(lines[index]));
|
|
7952
|
+
if (item.id === sessionId2 && typeof item.thread_name === "string" && item.thread_name.trim())
|
|
7953
|
+
return item.thread_name;
|
|
7954
|
+
} catch {
|
|
7955
|
+
}
|
|
7956
|
+
}
|
|
7957
|
+
} catch {
|
|
7958
|
+
}
|
|
7959
|
+
try {
|
|
7960
|
+
const { DatabaseSync } = __require(["node", "sqlite"].join(":"));
|
|
7961
|
+
const database = new DatabaseSync(join5(codexHome(), "state_5.sqlite"), { readOnly: true });
|
|
7962
|
+
try {
|
|
7963
|
+
const row = database.prepare("SELECT title, name, first_user_message FROM threads WHERE id = ?").get(sessionId2);
|
|
7964
|
+
for (const field of ["title", "name", "first_user_message"]) {
|
|
7965
|
+
const title = row?.[field];
|
|
7966
|
+
if (typeof title === "string" && title.trim()) return title;
|
|
7967
|
+
}
|
|
7968
|
+
} finally {
|
|
7969
|
+
database.close();
|
|
7970
|
+
}
|
|
7971
|
+
} catch {
|
|
7972
|
+
}
|
|
7973
|
+
return void 0;
|
|
7974
|
+
}
|
|
7748
7975
|
async function meta(path) {
|
|
7749
|
-
const file = await
|
|
7976
|
+
const file = await open4(path, "r");
|
|
7750
7977
|
let text;
|
|
7751
7978
|
try {
|
|
7752
7979
|
const buffer = Buffer.alloc(FIRST_LINE);
|
|
@@ -7774,12 +8001,12 @@ async function meta(path) {
|
|
|
7774
8001
|
};
|
|
7775
8002
|
}
|
|
7776
8003
|
async function findRollout(sessionId2) {
|
|
7777
|
-
const
|
|
7778
|
-
const files = await readdir3(
|
|
8004
|
+
const root2 = sessionsRoot();
|
|
8005
|
+
const files = await readdir3(root2, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
7779
8006
|
const match = files.find(
|
|
7780
8007
|
(file) => file.isFile() && ROLLOUT.test(file.name) && file.name.endsWith(`-${sessionId2}.jsonl`)
|
|
7781
8008
|
);
|
|
7782
|
-
return match &&
|
|
8009
|
+
return match && join5(match.parentPath, match.name);
|
|
7783
8010
|
}
|
|
7784
8011
|
async function codexEvent(input) {
|
|
7785
8012
|
if (!input || typeof input !== "object") throw new Error("Hook input is not an object");
|
|
@@ -7794,11 +8021,13 @@ async function codexEvent(input) {
|
|
|
7794
8021
|
const given = value["transcript_path"];
|
|
7795
8022
|
const transcriptPath = typeof given === "string" && given ? given : await findRollout(sessionId2);
|
|
7796
8023
|
if (!transcriptPath) throw new Error("No rollout file for this Codex session");
|
|
8024
|
+
const title = await codexTitle(sessionId2);
|
|
7797
8025
|
return {
|
|
7798
8026
|
sessionId: sessionId2,
|
|
7799
8027
|
transcriptPath,
|
|
7800
8028
|
cwd,
|
|
7801
8029
|
source: "codex",
|
|
8030
|
+
...title ? { title } : {},
|
|
7802
8031
|
...name === "SessionEnd" ? { completed: true } : {}
|
|
7803
8032
|
};
|
|
7804
8033
|
}
|
|
@@ -7811,7 +8040,7 @@ function days(folder) {
|
|
|
7811
8040
|
const pad = (value) => String(value).padStart(2, "0");
|
|
7812
8041
|
return [
|
|
7813
8042
|
folder,
|
|
7814
|
-
|
|
8043
|
+
join5(
|
|
7815
8044
|
parts.slice(0, -3).join(sep),
|
|
7816
8045
|
String(next.getUTCFullYear()),
|
|
7817
8046
|
pad(next.getUTCMonth() + 1),
|
|
@@ -7825,7 +8054,7 @@ async function codexChildren(event) {
|
|
|
7825
8054
|
for (const folder of days(dirname3(event.transcriptPath))) {
|
|
7826
8055
|
const names = (await readdir3(folder).catch(() => [])).filter((name) => name !== own && ROLLOUT.test(name)).sort();
|
|
7827
8056
|
for (const name of names) {
|
|
7828
|
-
const path =
|
|
8057
|
+
const path = join5(folder, name);
|
|
7829
8058
|
const child = await meta(path);
|
|
7830
8059
|
if (!child.id || child.parent !== event.sessionId) continue;
|
|
7831
8060
|
children2.push({
|
|
@@ -7846,6 +8075,8 @@ async function codexChildren(event) {
|
|
|
7846
8075
|
// src/harnesses/index.ts
|
|
7847
8076
|
var ROLLOUT2 = /^rollout-.+\.jsonl$/;
|
|
7848
8077
|
function hookEvent(input) {
|
|
8078
|
+
if (input?.["source"] === "opencode")
|
|
8079
|
+
return Promise.resolve(opencodeEvent(input));
|
|
7849
8080
|
const path = input?.["transcript_path"];
|
|
7850
8081
|
const codex = typeof path !== "string" || !path || ROLLOUT2.test(basename3(path));
|
|
7851
8082
|
return codex ? codexEvent(input) : Promise.resolve(claudeCodeEvent(input));
|
|
@@ -7853,8 +8084,14 @@ function hookEvent(input) {
|
|
|
7853
8084
|
function children(event) {
|
|
7854
8085
|
if (event.parentId) return Promise.resolve([]);
|
|
7855
8086
|
if (event.source === "claude-code") return claudeCodeChildren(event);
|
|
8087
|
+
if (event.source === "opencode") return Promise.resolve(opencodeChildren(event));
|
|
7856
8088
|
return event.source === "codex" ? codexChildren(event) : Promise.resolve([]);
|
|
7857
8089
|
}
|
|
8090
|
+
async function refreshEvent(event) {
|
|
8091
|
+
if (event.parentId) return event;
|
|
8092
|
+
const title = event.source === "codex" ? await codexTitle(event.sessionId) : event.source === "claude-code" ? await claudeCodeTitle(event.transcriptPath) : opencodeTitle(event.transcriptPath, event.sessionId);
|
|
8093
|
+
return title ? { ...event, title } : event;
|
|
8094
|
+
}
|
|
7858
8095
|
|
|
7859
8096
|
// src/commands/sync.ts
|
|
7860
8097
|
var import_proper_lockfile3 = __toESM(require_proper_lockfile(), 1);
|
|
@@ -7862,8 +8099,8 @@ import { mkdir as mkdir4 } from "fs/promises";
|
|
|
7862
8099
|
|
|
7863
8100
|
// src/upload.ts
|
|
7864
8101
|
var import_proper_lockfile2 = __toESM(require_proper_lockfile(), 1);
|
|
7865
|
-
import { mkdir as mkdir3, readFile as
|
|
7866
|
-
import { join as
|
|
8102
|
+
import { mkdir as mkdir3, readFile as readFile4, readdir as readdir4, rename as rename2, rm as rm2, stat as stat4 } from "fs/promises";
|
|
8103
|
+
import { dirname as dirname4, join as join6 } from "path";
|
|
7867
8104
|
import { setTimeout as delay } from "timers/promises";
|
|
7868
8105
|
var ATTEMPTS = 4;
|
|
7869
8106
|
var REJECTIONS = 3;
|
|
@@ -7901,8 +8138,10 @@ async function deliver(api, path, envelope, externalId, options2) {
|
|
|
7901
8138
|
remaining.shift();
|
|
7902
8139
|
sent++;
|
|
7903
8140
|
}
|
|
7904
|
-
if (!remaining.length)
|
|
7905
|
-
|
|
8141
|
+
if (!remaining.length) {
|
|
8142
|
+
await atomic(join6(dirname4(path), "state.json"), { ...envelope.after, published: true });
|
|
8143
|
+
await rm2(path, { force: true });
|
|
8144
|
+
} else if (sent || outcome.kind === "permanent")
|
|
7906
8145
|
await atomic(path, {
|
|
7907
8146
|
...envelope,
|
|
7908
8147
|
chunks: remaining,
|
|
@@ -7913,8 +8152,8 @@ async function deliver(api, path, envelope, externalId, options2) {
|
|
|
7913
8152
|
async function outstanding(event, config) {
|
|
7914
8153
|
const info = await stat4(event.transcriptPath).catch(() => void 0);
|
|
7915
8154
|
if (!info) return false;
|
|
7916
|
-
const state = await
|
|
7917
|
-
|
|
8155
|
+
const state = await readFile4(
|
|
8156
|
+
join6(sessionFolder(config, event.source, event.sessionId), "state.json"),
|
|
7918
8157
|
"utf8"
|
|
7919
8158
|
).catch(() => void 0);
|
|
7920
8159
|
if (!state) return true;
|
|
@@ -7923,20 +8162,20 @@ async function outstanding(event, config) {
|
|
|
7923
8162
|
}
|
|
7924
8163
|
async function dropSession(folder, state) {
|
|
7925
8164
|
for (const name of await readdir4(folder))
|
|
7926
|
-
if (name.endsWith(".batch.json")) await rm2(
|
|
7927
|
-
await atomic(
|
|
8165
|
+
if (name.endsWith(".batch.json")) await rm2(join6(folder, name), { force: true });
|
|
8166
|
+
await atomic(join6(folder, "state.json"), { ...state, tombstoned: true });
|
|
7928
8167
|
}
|
|
7929
8168
|
async function drain(config, options2 = {}, resumed = false) {
|
|
7930
|
-
const
|
|
7931
|
-
await mkdir3(
|
|
8169
|
+
const root2 = stateRoot(config);
|
|
8170
|
+
await mkdir3(root2, { recursive: true, mode: 448 });
|
|
7932
8171
|
const api = createApi(config.server, config.token, options2.transport);
|
|
7933
8172
|
const errors = [];
|
|
7934
8173
|
let sent = 0;
|
|
7935
8174
|
let stopped = false;
|
|
7936
|
-
const files = (await readdir4(
|
|
8175
|
+
const files = (await readdir4(root2, { recursive: true, withFileTypes: true })).filter((file) => file.isFile() && file.name.endsWith(".batch.json")).sort((a, b) => a.name.localeCompare(b.name));
|
|
7937
8176
|
for (const item of files) {
|
|
7938
8177
|
if (stopped) break;
|
|
7939
|
-
const path =
|
|
8178
|
+
const path = join6(item.parentPath, item.name);
|
|
7940
8179
|
let release;
|
|
7941
8180
|
try {
|
|
7942
8181
|
release = await import_proper_lockfile2.default.lock(item.parentPath, { stale: 1e4, update: 3e3, retries: 0 });
|
|
@@ -7944,7 +8183,7 @@ async function drain(config, options2 = {}, resumed = false) {
|
|
|
7944
8183
|
continue;
|
|
7945
8184
|
}
|
|
7946
8185
|
try {
|
|
7947
|
-
const envelope = JSON.parse(await
|
|
8186
|
+
const envelope = JSON.parse(await readFile4(path, "utf8"));
|
|
7948
8187
|
const event = envelope.after.event;
|
|
7949
8188
|
if (!await allowed(event.cwd, config)) {
|
|
7950
8189
|
errors.push("excluded pending session");
|
|
@@ -7952,7 +8191,7 @@ async function drain(config, options2 = {}, resumed = false) {
|
|
|
7952
8191
|
}
|
|
7953
8192
|
const state = await readState(item.parentPath, event);
|
|
7954
8193
|
if (envelope.after.generation > state.generation)
|
|
7955
|
-
await atomic(
|
|
8194
|
+
await atomic(join6(item.parentPath, "state.json"), envelope.after);
|
|
7956
8195
|
const result = await deliver(api, path, envelope, event.sessionId, options2);
|
|
7957
8196
|
sent += result.sent;
|
|
7958
8197
|
if (result.outcome.kind === "gone") await dropSession(item.parentPath, envelope.after);
|
|
@@ -7984,27 +8223,28 @@ async function drain(config, options2 = {}, resumed = false) {
|
|
|
7984
8223
|
await release();
|
|
7985
8224
|
}
|
|
7986
8225
|
}
|
|
7987
|
-
const states = (await readdir4(
|
|
8226
|
+
const states = (await readdir4(root2, { recursive: true, withFileTypes: true })).filter(
|
|
7988
8227
|
(file) => file.isFile() && file.name === "state.json"
|
|
7989
8228
|
);
|
|
7990
8229
|
let restarted = false;
|
|
7991
8230
|
for (const item of states) {
|
|
7992
|
-
const path =
|
|
7993
|
-
const state = JSON.parse(await
|
|
7994
|
-
const
|
|
8231
|
+
const path = join6(item.parentPath, item.name);
|
|
8232
|
+
const state = JSON.parse(await readFile4(path, "utf8"));
|
|
8233
|
+
const event = await refreshEvent(state.event);
|
|
8234
|
+
const behind = event.source === "opencode" || await stat4(event.transcriptPath).then(
|
|
7995
8235
|
(info) => info.size > state.offset || info.ino !== state.inode,
|
|
7996
8236
|
() => false
|
|
7997
8237
|
);
|
|
7998
|
-
if (state.paused || behind) {
|
|
8238
|
+
if (state.paused || behind || event.title !== state.event.title) {
|
|
7999
8239
|
try {
|
|
8000
|
-
restarted = (await capture(
|
|
8240
|
+
restarted = (await capture(event, config)).chunks > 0 || restarted;
|
|
8001
8241
|
} catch (error) {
|
|
8002
8242
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
8003
8243
|
await atomic(path, { ...state, gap: true });
|
|
8004
8244
|
else errors.push("recapture failed");
|
|
8005
8245
|
}
|
|
8006
8246
|
}
|
|
8007
|
-
for (const child of await children(
|
|
8247
|
+
for (const child of await children(event)) {
|
|
8008
8248
|
if (!await outstanding(child, config)) continue;
|
|
8009
8249
|
try {
|
|
8010
8250
|
restarted = (await capture(child, config)).chunks > 0 || restarted;
|
|
@@ -8019,7 +8259,7 @@ async function drain(config, options2 = {}, resumed = false) {
|
|
|
8019
8259
|
}
|
|
8020
8260
|
return {
|
|
8021
8261
|
sent,
|
|
8022
|
-
pending: (await readdir4(
|
|
8262
|
+
pending: (await readdir4(root2, { recursive: true })).filter(
|
|
8023
8263
|
(name) => name.endsWith(".batch.json")
|
|
8024
8264
|
).length,
|
|
8025
8265
|
errors
|
|
@@ -8029,16 +8269,16 @@ async function drain(config, options2 = {}, resumed = false) {
|
|
|
8029
8269
|
// src/commands/sync.ts
|
|
8030
8270
|
var uploaderLock = { stale: 6e4, update: 1e4, retries: 0 };
|
|
8031
8271
|
async function uploaderRunning(config) {
|
|
8032
|
-
const
|
|
8033
|
-
await mkdir4(
|
|
8034
|
-
return import_proper_lockfile3.default.check(
|
|
8272
|
+
const root2 = stateRoot(config);
|
|
8273
|
+
await mkdir4(root2, { recursive: true, mode: 448 });
|
|
8274
|
+
return import_proper_lockfile3.default.check(root2, uploaderLock);
|
|
8035
8275
|
}
|
|
8036
8276
|
async function sync(config, options2 = {}) {
|
|
8037
|
-
const
|
|
8038
|
-
await mkdir4(
|
|
8277
|
+
const root2 = stateRoot(config);
|
|
8278
|
+
await mkdir4(root2, { recursive: true, mode: 448 });
|
|
8039
8279
|
let release;
|
|
8040
8280
|
try {
|
|
8041
|
-
release = await import_proper_lockfile3.default.lock(
|
|
8281
|
+
release = await import_proper_lockfile3.default.lock(root2, uploaderLock);
|
|
8042
8282
|
} catch {
|
|
8043
8283
|
return { sent: 0, pending: 0, errors: [], skipped: true };
|
|
8044
8284
|
}
|
|
@@ -8094,19 +8334,32 @@ async function beam(input, config, options2 = {}) {
|
|
|
8094
8334
|
}
|
|
8095
8335
|
|
|
8096
8336
|
// src/commands/doctor.ts
|
|
8097
|
-
import { readFile as
|
|
8098
|
-
import { homedir as
|
|
8099
|
-
import { join as
|
|
8337
|
+
import { readFile as readFile6, readdir as readdir5, stat as stat6 } from "fs/promises";
|
|
8338
|
+
import { homedir as homedir7 } from "os";
|
|
8339
|
+
import { join as join8 } from "path";
|
|
8100
8340
|
|
|
8101
8341
|
// src/config.ts
|
|
8102
|
-
import { readFile as
|
|
8103
|
-
import { homedir as
|
|
8104
|
-
import { join as
|
|
8105
|
-
var configPath = () =>
|
|
8342
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
8343
|
+
import { homedir as homedir6 } from "os";
|
|
8344
|
+
import { join as join7 } from "path";
|
|
8345
|
+
var configPath = () => join7(homedir6(), ".config/openhivemind/config.json");
|
|
8346
|
+
var NOT_CONFIGURED = "No hive configured. Connect this machine with the token on stdin: wl-paste | openhivemind setup <url> (pbpaste on macOS).";
|
|
8106
8347
|
async function loadConfig() {
|
|
8107
|
-
|
|
8348
|
+
let raw;
|
|
8349
|
+
try {
|
|
8350
|
+
raw = await readFile5(configPath(), "utf8");
|
|
8351
|
+
} catch (error) {
|
|
8352
|
+
if (error.code === "ENOENT") throw new Error(NOT_CONFIGURED);
|
|
8353
|
+
throw error;
|
|
8354
|
+
}
|
|
8355
|
+
let value;
|
|
8356
|
+
try {
|
|
8357
|
+
value = JSON.parse(raw);
|
|
8358
|
+
} catch {
|
|
8359
|
+
throw new Error(`Invalid configuration at ${configPath()}; run openhivemind setup <url> again`);
|
|
8360
|
+
}
|
|
8108
8361
|
if (!value || typeof value.server !== "string" || typeof value.token !== "string" || typeof value.org !== "string" || !Array.isArray(value.roots) || !Array.isArray(value.exclude))
|
|
8109
|
-
throw new Error(
|
|
8362
|
+
throw new Error(`Invalid configuration at ${configPath()}; run openhivemind setup <url> again`);
|
|
8110
8363
|
return value;
|
|
8111
8364
|
}
|
|
8112
8365
|
async function saveConfig(config) {
|
|
@@ -8115,7 +8368,7 @@ async function saveConfig(config) {
|
|
|
8115
8368
|
|
|
8116
8369
|
// src/commands/doctor.ts
|
|
8117
8370
|
async function spoolReport(config) {
|
|
8118
|
-
const
|
|
8371
|
+
const root2 = stateRoot(config);
|
|
8119
8372
|
const report = {
|
|
8120
8373
|
sessions: 0,
|
|
8121
8374
|
pending: 0,
|
|
@@ -8125,15 +8378,15 @@ async function spoolReport(config) {
|
|
|
8125
8378
|
sources: [],
|
|
8126
8379
|
bytes: 0
|
|
8127
8380
|
};
|
|
8128
|
-
const files = await readdir5(
|
|
8381
|
+
const files = await readdir5(root2, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
8129
8382
|
for (const file of files) {
|
|
8130
8383
|
if (!file.isFile()) continue;
|
|
8131
|
-
const path =
|
|
8384
|
+
const path = join8(file.parentPath, file.name);
|
|
8132
8385
|
if (file.name.endsWith(".dead")) report.dead++;
|
|
8133
8386
|
else if (file.name.endsWith(".batch.json")) report.pending++;
|
|
8134
8387
|
else if (file.name === "state.json") {
|
|
8135
8388
|
report.sessions++;
|
|
8136
|
-
const state = JSON.parse(await
|
|
8389
|
+
const state = JSON.parse(await readFile6(path, "utf8"));
|
|
8137
8390
|
if (!report.sources.includes(state.event.source)) report.sources.push(state.event.source);
|
|
8138
8391
|
if (state.paused) report.paused.push(state.event.sessionId);
|
|
8139
8392
|
if (state.gap) report.gap.push(state.event.sessionId);
|
|
@@ -8141,14 +8394,14 @@ async function spoolReport(config) {
|
|
|
8141
8394
|
if (!report.last || when > report.last) report.last = when;
|
|
8142
8395
|
}
|
|
8143
8396
|
}
|
|
8144
|
-
report.bytes = await spoolBytes(
|
|
8397
|
+
report.bytes = await spoolBytes(root2).catch(() => 0);
|
|
8145
8398
|
return report;
|
|
8146
8399
|
}
|
|
8147
8400
|
async function ignoreProblems() {
|
|
8148
8401
|
const problems = [];
|
|
8149
|
-
const path =
|
|
8402
|
+
const path = join8(homedir7(), ".config/openhivemind/ignore");
|
|
8150
8403
|
try {
|
|
8151
|
-
ignorePatterns(await
|
|
8404
|
+
ignorePatterns(await readFile6(path, "utf8"));
|
|
8152
8405
|
} catch (error) {
|
|
8153
8406
|
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT"))
|
|
8154
8407
|
problems.push(
|
|
@@ -8158,19 +8411,32 @@ async function ignoreProblems() {
|
|
|
8158
8411
|
return problems;
|
|
8159
8412
|
}
|
|
8160
8413
|
async function pluginInstalled() {
|
|
8161
|
-
const installed = await
|
|
8162
|
-
|
|
8414
|
+
const installed = await readFile6(
|
|
8415
|
+
join8(homedir7(), ".claude/plugins/installed_plugins.json"),
|
|
8163
8416
|
"utf8"
|
|
8164
8417
|
).catch(() => "{}");
|
|
8165
8418
|
const value = JSON.parse(installed);
|
|
8166
8419
|
return Object.keys(value.plugins ?? {}).some((name) => name.startsWith("openhivemind@"));
|
|
8167
8420
|
}
|
|
8168
8421
|
async function codexPluginEnabled() {
|
|
8169
|
-
const home = process.env["CODEX_HOME"] ||
|
|
8170
|
-
const config = await
|
|
8422
|
+
const home = process.env["CODEX_HOME"] || join8(homedir7(), ".codex");
|
|
8423
|
+
const config = await readFile6(join8(home, "config.toml"), "utf8").catch(() => "");
|
|
8171
8424
|
const section = config.split(/^\[/m).find((part) => part.startsWith('plugins."openhivemind@'));
|
|
8172
8425
|
return Boolean(section) && !/^enabled\s*=\s*false/m.test(section);
|
|
8173
8426
|
}
|
|
8427
|
+
async function opencodePluginInstalled() {
|
|
8428
|
+
const home = join8(process.env["XDG_CONFIG_HOME"] || join8(homedir7(), ".config"), "opencode");
|
|
8429
|
+
for (const folder of ["plugin", "plugins"])
|
|
8430
|
+
if ((await readdir5(join8(home, folder)).catch(() => [])).some(
|
|
8431
|
+
(name) => name.startsWith("openhivemind.")
|
|
8432
|
+
))
|
|
8433
|
+
return true;
|
|
8434
|
+
for (const name of ["opencode.json", "opencode.jsonc", "config.json"]) {
|
|
8435
|
+
const config = await readFile6(join8(home, name), "utf8").catch(() => "");
|
|
8436
|
+
if (/"plugin"\s*:\s*\[[^\]]*openhivemind/.test(config)) return true;
|
|
8437
|
+
}
|
|
8438
|
+
return false;
|
|
8439
|
+
}
|
|
8174
8440
|
async function doctor() {
|
|
8175
8441
|
const lines = [];
|
|
8176
8442
|
let ok2 = true;
|
|
@@ -8184,8 +8450,8 @@ async function doctor() {
|
|
|
8184
8450
|
const mode = ((await stat6(configPath())).mode & 511).toString(8);
|
|
8185
8451
|
lines.push(`Configuration: ${configPath()} (mode ${mode})`);
|
|
8186
8452
|
if (mode !== "600") fail(" the configuration file should be mode 600");
|
|
8187
|
-
} catch {
|
|
8188
|
-
fail(`Configuration:
|
|
8453
|
+
} catch (error) {
|
|
8454
|
+
fail(`Configuration: ${error instanceof Error ? error.message : `invalid at ${configPath()}`}`);
|
|
8189
8455
|
return { lines, ok: ok2 };
|
|
8190
8456
|
}
|
|
8191
8457
|
try {
|
|
@@ -8217,6 +8483,9 @@ async function doctor() {
|
|
|
8217
8483
|
lines.push(
|
|
8218
8484
|
`Codex CLI plugin: ${!await codexPluginEnabled() ? "not installed (run codex plugin marketplace add openhivemind/openhivemind, then codex plugin add openhivemind@openhivemind)" : spool.sources.includes("codex") ? "installed, bundled hooks fire" : "installed, but no turn has been captured yet; its hooks run only once trusted from the Codex TUI"}`
|
|
8219
8485
|
);
|
|
8486
|
+
lines.push(
|
|
8487
|
+
`opencode plugin: ${!await opencodePluginInstalled() ? "not installed (link client/plugins/opencode/openhivemind.js into ~/.config/opencode/plugin/)" : spool.sources.includes("opencode") ? "installed, session.idle fires" : "installed, but no session has been captured yet"}`
|
|
8488
|
+
);
|
|
8220
8489
|
for (const problem of await ignoreProblems()) fail(problem);
|
|
8221
8490
|
lines.push(
|
|
8222
8491
|
`Capture: ${spool.sessions} session(s), last ${spool.last ?? "never"}; spool ${spool.bytes} bytes in ${spool.pending} chunk file(s)`
|
|
@@ -8228,12 +8497,12 @@ async function doctor() {
|
|
|
8228
8497
|
if (spool.gap.length)
|
|
8229
8498
|
fail(` permanent gap, transcript gone: ${spool.gap.slice(0, 5).join(", ")}`);
|
|
8230
8499
|
if (spool.dead) fail(` ${spool.dead} chunk(s) rejected permanently and set aside`);
|
|
8231
|
-
const upgrade = await
|
|
8500
|
+
const upgrade = await readFile6(upgradePath(config), "utf8").catch(() => "");
|
|
8232
8501
|
if (upgrade) {
|
|
8233
8502
|
const value = JSON.parse(upgrade);
|
|
8234
8503
|
fail(` upgrade client: uploads stopped at ${value.at} (${value.detail})`);
|
|
8235
8504
|
}
|
|
8236
|
-
lines.push(`Log: ${
|
|
8505
|
+
lines.push(`Log: ${join8(stateHome(), "hook.log")}`);
|
|
8237
8506
|
return { lines, ok: ok2 };
|
|
8238
8507
|
}
|
|
8239
8508
|
|
|
@@ -8411,14 +8680,19 @@ function truncate(text, max = 200) {
|
|
|
8411
8680
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
8412
8681
|
return flat.length > max ? `${flat.slice(0, max)}\u2026 (truncated)` : flat;
|
|
8413
8682
|
}
|
|
8683
|
+
function describeRequestError(server, error, fallback) {
|
|
8684
|
+
if (error instanceof ApiError) return error.message;
|
|
8685
|
+
const detail = error instanceof Error ? error.message : fallback;
|
|
8686
|
+
return `Hive ${server} unreachable (${detail}); run openhivemind doctor`;
|
|
8687
|
+
}
|
|
8414
8688
|
|
|
8415
8689
|
// src/commands/search.ts
|
|
8416
8690
|
var SEARCH_LIMIT_CAP = 100;
|
|
8417
8691
|
var DEFAULT_LIMIT = 20;
|
|
8418
8692
|
var MAX_SNIPPETS_PER_SESSION = 3;
|
|
8419
8693
|
async function search(config, flags) {
|
|
8420
|
-
const
|
|
8421
|
-
if (!
|
|
8694
|
+
const query2 = flags.rest.join(" ").trim();
|
|
8695
|
+
if (!query2)
|
|
8422
8696
|
return {
|
|
8423
8697
|
lines: [
|
|
8424
8698
|
"Usage: openhivemind search <query...> [--remote --author --branch --since --until --kind --mine --limit --context --regex --case-sensitive --format plain|md]"
|
|
@@ -8434,7 +8708,7 @@ async function search(config, flags) {
|
|
|
8434
8708
|
try {
|
|
8435
8709
|
response = await createApi(config.server, config.token)(routes.search, {
|
|
8436
8710
|
body: {
|
|
8437
|
-
query,
|
|
8711
|
+
query: query2,
|
|
8438
8712
|
regex: flags.has("regex") || void 0,
|
|
8439
8713
|
caseSensitive: flags.has("case-sensitive") || void 0,
|
|
8440
8714
|
context: context.value,
|
|
@@ -8443,7 +8717,7 @@ async function search(config, flags) {
|
|
|
8443
8717
|
});
|
|
8444
8718
|
} catch (error) {
|
|
8445
8719
|
return {
|
|
8446
|
-
lines: [
|
|
8720
|
+
lines: [describeRequestError(config.server, error, "Search request failed")],
|
|
8447
8721
|
exitCode: 2
|
|
8448
8722
|
};
|
|
8449
8723
|
}
|
|
@@ -8512,13 +8786,13 @@ async function sessions(config, flags) {
|
|
|
8512
8786
|
}
|
|
8513
8787
|
const parsed = filters(flags);
|
|
8514
8788
|
if (parsed.error) return { lines: [parsed.error], exitCode: 2 };
|
|
8515
|
-
const
|
|
8789
|
+
const query2 = { ...parsed.value, since };
|
|
8516
8790
|
let response;
|
|
8517
8791
|
try {
|
|
8518
|
-
response = await createApi(config.server, config.token)(routes.sessions, { query });
|
|
8792
|
+
response = await createApi(config.server, config.token)(routes.sessions, { query: query2 });
|
|
8519
8793
|
} catch (error) {
|
|
8520
8794
|
return {
|
|
8521
|
-
lines: [
|
|
8795
|
+
lines: [describeRequestError(config.server, error, "Sessions request failed")],
|
|
8522
8796
|
exitCode: 2
|
|
8523
8797
|
};
|
|
8524
8798
|
}
|
|
@@ -8597,7 +8871,10 @@ async function show(config, flags) {
|
|
|
8597
8871
|
query: matcher ? { last: MATCH_SCAN_WINDOW, maxChars: MATCH_SCAN_MAX_CHARS } : { last }
|
|
8598
8872
|
});
|
|
8599
8873
|
} catch (error) {
|
|
8600
|
-
return {
|
|
8874
|
+
return {
|
|
8875
|
+
lines: [describeRequestError(config.server, error, "Show request failed")],
|
|
8876
|
+
exitCode: 2
|
|
8877
|
+
};
|
|
8601
8878
|
}
|
|
8602
8879
|
let messages = response.messages;
|
|
8603
8880
|
if (matcher) {
|
|
@@ -8636,7 +8913,7 @@ async function main() {
|
|
|
8636
8913
|
return;
|
|
8637
8914
|
}
|
|
8638
8915
|
if (command === "--version") {
|
|
8639
|
-
console.log("0.
|
|
8916
|
+
console.log("0.2.0");
|
|
8640
8917
|
return;
|
|
8641
8918
|
}
|
|
8642
8919
|
if (command === "skills") {
|
|
@@ -8644,7 +8921,7 @@ async function main() {
|
|
|
8644
8921
|
const names = (await readdir6(folder)).sort();
|
|
8645
8922
|
if (args[0]) {
|
|
8646
8923
|
if (!names.includes(args[0])) throw new Error("Unknown skill");
|
|
8647
|
-
console.log(await
|
|
8924
|
+
console.log(await readFile7(resolve3(folder, args[0], "SKILL.md"), "utf8"));
|
|
8648
8925
|
} else console.log(names.join("\n"));
|
|
8649
8926
|
return;
|
|
8650
8927
|
}
|
|
@@ -8656,8 +8933,13 @@ async function main() {
|
|
|
8656
8933
|
"Usage: openhivemind setup <url> [--token <pat>] [--root <dir>] [--exclude <dir>] [--read-only]"
|
|
8657
8934
|
);
|
|
8658
8935
|
const given = flags.get("token");
|
|
8936
|
+
if (!given && process.stdin.isTTY)
|
|
8937
|
+
throw new Error(
|
|
8938
|
+
`Pipe the token in: wl-paste | openhivemind ${command} ${command === "setup" ? "<url>" : "--server <url>"} (pbpaste on macOS), or pass --token <pat>`
|
|
8939
|
+
);
|
|
8659
8940
|
const token = given && given !== "-" ? given : (await readStdin()).trim();
|
|
8660
|
-
if (!token)
|
|
8941
|
+
if (!token)
|
|
8942
|
+
throw new Error("A personal access token is required on stdin or via --token <pat>");
|
|
8661
8943
|
const config = await login({
|
|
8662
8944
|
server,
|
|
8663
8945
|
token,
|