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 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 = readFile7;
536
- function readFile7(path, options2, cb) {
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
- open4(that.path, that.flags, that.mode, function(err, fd) {
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
- open4(that.path, that.flags, that.mode, function(err, fd) {
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 = open4;
750
- function open4(path, flags, mode, cb) {
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 readFile6 } from "fs/promises";
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, root) {
3221
- return root === true ? T : Readonly(T);
3220
+ function ConditionalReadonly(T, root2) {
3221
+ return root2 === true ? T : Readonly(T);
3222
3222
  }
3223
- function FromValue(value, root) {
3224
- return IsAsyncIterator(value) ? ConditionalReadonly(Any(), root) : IsIterator(value) ? ConditionalReadonly(Any(), root) : IsArray(value) ? Readonly(Tuple(FromArray3(value))) : IsUint8Array(value) ? Uint8Array2() : IsDate(value) ? Date2() : IsObject(value) ? ConditionalReadonly(Object2(FromProperties7(value)), root) : IsFunction(value) ? ConditionalReadonly(Function([], Unknown()), root) : 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({});
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, query, status = 200) {
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") emit2(kind, message.content, "text");
7202
- else
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") emit2(kind, str(block.text), String(blockIndex));
7206
- else if (block.type === "tool_use")
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 open2, readdir, stat as stat2, realpath } from "fs/promises";
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 homedir2 } from "os";
7331
- import { join as join2, relative, isAbsolute, resolve } from "path";
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 inside(path, root) {
7397
- const rel = relative(root, path);
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((root) => inside(path, root)) && (!roots.length || roots.some((root) => inside(path, root)));
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 root = cwd;
7561
+ let root2 = cwd;
7412
7562
  try {
7413
- root = (await exec("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { timeout: 5e3 })).stdout.trim();
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
- join2(root, ".openhivemind-ignore"),
7418
- join2(homedir2(), ".config/openhivemind/ignore")
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(join2(folder, "state.json"), "utf8"));
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) => join2(stateRoot(config), source, digest(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 root = stateRoot(config);
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(join2(folder, name), "utf8"));
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
- if (end < 0 && !closing) return { status: "no complete records", chunks: 0, bytes: 0 };
7506
- const lines = buffer.subarray(0, end + 1).toString("utf8").split("\n").filter(Boolean);
7507
- const records = lines.map((line) => JSON.parse(line));
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
- title: state.meta?.title ?? (event.title ? deriveTitle(event.title) : void 0) ?? (firstPrompt ? deriveTitle(firstPrompt) : void 0) ?? "Untitled session",
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: offset + end + 1,
7575
- inode: info.ino,
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
- if (batch.length || state.meta?.completed !== meta2.completed)
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(root);
7778
+ const pendingSize = await spoolBytes(root2);
7602
7779
  if (pendingSize + Buffer.byteLength(serialized) > (config.spoolLimit ?? 256 * 1024 * 1024)) {
7603
- await atomic(join2(folder, "state.json"), { ...state, event, paused: true });
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
- join2(folder, String(after.generation).padStart(16, "0") + ".batch.json"),
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(join2(folder, "state.json"), after);
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(root) {
7800
+ async function spoolBytes(root2) {
7624
7801
  let total = 0;
7625
- for (const file of await readdir(root, { recursive: true, withFileTypes: true })) {
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(join2(file.parentPath, file.name))).size;
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 homedir3 } from "os";
7635
- import { basename, dirname as dirname2, join as join3 } from "path";
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 = join3(dirname2(event.transcriptPath), event.sessionId, "subagents");
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(join3(folder, name.replace(/\.jsonl$/, ".meta.json")), "utf8").catch(
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: join3(folder, name),
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 root = join3(homedir3(), ".claude/projects");
7687
- const projects = await readdir2(root).catch(() => []);
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 = join3(root, project, input + ".jsonl");
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 open3, readdir as readdir3 } from "fs/promises";
7742
- import { homedir as homedir4 } from "os";
7743
- import { basename as basename2, dirname as dirname3, join as join4, sep } from "path";
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 = () => join4(process.env["CODEX_HOME"] || join4(homedir4(), ".codex"), "sessions");
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 open3(path, "r");
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 root = sessionsRoot();
7778
- const files = await readdir3(root, { recursive: true, withFileTypes: true }).catch(() => []);
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 && join4(match.parentPath, match.name);
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
- join4(
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 = join4(folder, name);
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 readFile3, readdir as readdir4, rename as rename2, rm as rm2, stat as stat4 } from "fs/promises";
7866
- import { join as join5 } from "path";
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) await rm2(path, { force: true });
7905
- else if (sent || outcome.kind === "permanent")
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 readFile3(
7917
- join5(sessionFolder(config, event.source, event.sessionId), "state.json"),
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(join5(folder, name), { force: true });
7927
- await atomic(join5(folder, "state.json"), { ...state, tombstoned: true });
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 root = stateRoot(config);
7931
- await mkdir3(root, { recursive: true, mode: 448 });
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(root, { recursive: true, withFileTypes: true })).filter((file) => file.isFile() && file.name.endsWith(".batch.json")).sort((a, b) => a.name.localeCompare(b.name));
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 = join5(item.parentPath, item.name);
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 readFile3(path, "utf8"));
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(join5(item.parentPath, "state.json"), envelope.after);
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(root, { recursive: true, withFileTypes: true })).filter(
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 = join5(item.parentPath, item.name);
7993
- const state = JSON.parse(await readFile3(path, "utf8"));
7994
- const behind = await stat4(state.event.transcriptPath).then(
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(state.event, config)).chunks > 0 || restarted;
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(state.event)) {
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(root, { recursive: true })).filter(
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 root = stateRoot(config);
8033
- await mkdir4(root, { recursive: true, mode: 448 });
8034
- return import_proper_lockfile3.default.check(root, uploaderLock);
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 root = stateRoot(config);
8038
- await mkdir4(root, { recursive: true, mode: 448 });
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(root, uploaderLock);
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 readFile5, readdir as readdir5, stat as stat6 } from "fs/promises";
8098
- import { homedir as homedir6 } from "os";
8099
- import { join as join7 } from "path";
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 readFile4 } from "fs/promises";
8103
- import { homedir as homedir5 } from "os";
8104
- import { join as join6 } from "path";
8105
- var configPath = () => join6(homedir5(), ".config/openhivemind/config.json");
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
- const value = JSON.parse(await readFile4(configPath(), "utf8"));
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("Invalid configuration; run setup");
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 root = stateRoot(config);
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(root, { recursive: true, withFileTypes: true }).catch(() => []);
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 = join7(file.parentPath, file.name);
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 readFile5(path, "utf8"));
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(root).catch(() => 0);
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 = join7(homedir6(), ".config/openhivemind/ignore");
8402
+ const path = join8(homedir7(), ".config/openhivemind/ignore");
8150
8403
  try {
8151
- ignorePatterns(await readFile5(path, "utf8"));
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 readFile5(
8162
- join7(homedir6(), ".claude/plugins/installed_plugins.json"),
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"] || join7(homedir6(), ".codex");
8170
- const config = await readFile5(join7(home, "config.toml"), "utf8").catch(() => "");
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: missing or invalid at ${configPath()}; run openhivemind setup <url>`);
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 readFile5(upgradePath(config), "utf8").catch(() => "");
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: ${join7(stateHome(), "hook.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 query = flags.rest.join(" ").trim();
8421
- if (!query)
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: [error instanceof Error ? error.message : "Search request failed"],
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 query = { ...parsed.value, since };
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: [error instanceof Error ? error.message : "Sessions request failed"],
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 { lines: [error instanceof Error ? error.message : "Show request failed"], exitCode: 2 };
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.1.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 readFile6(resolve3(folder, args[0], "SKILL.md"), "utf8"));
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) throw new Error("A personal access token is required: --token <pat> or on stdin");
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,