@onebrain-ai/cli 2.0.2 → 2.0.4

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.
Files changed (3) hide show
  1. package/README.md +48 -0
  2. package/dist/onebrain +134 -30
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @onebrain-ai/cli
2
+
3
+ The CLI binary for [OneBrain](https://github.com/kengio/onebrain) — a personal AI OS built on Obsidian with persistent memory, 24+ skills, and Claude Code integration.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # with bun (recommended)
9
+ bun install -g @onebrain-ai/cli
10
+
11
+ # or with npm
12
+ npm install -g @onebrain-ai/cli
13
+ ```
14
+
15
+ Verify: `onebrain --version`
16
+
17
+ ## What it does
18
+
19
+ The `onebrain` binary handles the low-level operations that keep your vault running.
20
+
21
+ **User-facing commands:**
22
+
23
+ | Command | Purpose |
24
+ |---------|---------|
25
+ | `onebrain init` | First-time vault initialization |
26
+ | `onebrain update` | Pull latest plugin files from GitHub |
27
+ | `onebrain doctor` | Audit vault health — orphan checkpoints, version drift, qmd embedding status, missing config |
28
+ | `onebrain help` | List all available commands |
29
+
30
+ **Internal commands** (not meant to be run directly):
31
+
32
+ `session-init` · `orphan-scan` · `checkpoint` · `qmd-reindex` · `vault-sync` · `register-hooks` · `migrate`
33
+
34
+ ## Requirements
35
+
36
+ - macOS, Linux, or Windows (Git Bash)
37
+ - Bun or Node.js required (used as the runtime for the npm package)
38
+ - For a self-contained binary with no runtime dependency, download from [GitHub Releases](https://github.com/kengio/onebrain/releases)
39
+
40
+ ## OneBrain
41
+
42
+ OneBrain gives your AI agent persistent memory across sessions, a structured Markdown vault, and 24+ pre-built skills — so every session picks up exactly where the last one left off.
43
+
44
+ **Full documentation and vault setup:** [github.com/kengio/onebrain](https://github.com/kengio/onebrain)
45
+
46
+ ## License
47
+
48
+ [MIT](https://github.com/kengio/onebrain/blob/main/LICENSE)
package/dist/onebrain CHANGED
@@ -10742,7 +10742,7 @@ init_dist();
10742
10742
  import { mkdir as mkdir3, readFile as readFile3, rename as rename3, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
10743
10743
  import { homedir as homedir3 } from "node:os";
10744
10744
  import { dirname as dirname3, join as join5 } from "node:path";
10745
- var binaryVersion = "2.0.2";
10745
+ var binaryVersion = "2.0.4";
10746
10746
  var STANDARD_FOLDERS = [
10747
10747
  "00-inbox",
10748
10748
  "01-projects",
@@ -11228,8 +11228,8 @@ async function updateCommand(opts = {}) {
11228
11228
  }
11229
11229
 
11230
11230
  // src/internal/checkpoint.ts
11231
- import { readFileSync, writeFileSync } from "node:fs";
11232
- import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
11231
+ import { readFileSync, readdirSync, writeFileSync } from "node:fs";
11232
+ import { mkdir as mkdir4, readdir as readdir2, writeFile as writeFile5 } from "node:fs/promises";
11233
11233
  import { tmpdir as osTmpdir } from "node:os";
11234
11234
  import { join as join7 } from "node:path";
11235
11235
  var SKIP_WINDOW = 60;
@@ -11282,13 +11282,15 @@ function writeState(token, state, tmpDir = osTmpdir()) {
11282
11282
  `);
11283
11283
  }
11284
11284
  }
11285
- function loadThresholds(vaultRoot) {
11285
+ var DEFAULT_LOGS_FOLDER = "07-logs";
11286
+ function loadVaultSettings(vaultRoot) {
11286
11287
  try {
11287
11288
  const vaultYml = join7(vaultRoot, "vault.yml");
11288
11289
  const raw = readFileSync(vaultYml, "utf8");
11289
- const checkpointBlock = raw.match(/^checkpoint:\s*\n((?:[ \t]+[^\n]+\n?)*)/m);
11290
11290
  let messages = DEFAULT_MESSAGES_THRESHOLD;
11291
11291
  let minutes = DEFAULT_MINUTES_THRESHOLD;
11292
+ let logsFolder = DEFAULT_LOGS_FOLDER;
11293
+ const checkpointBlock = raw.match(/^checkpoint:\s*\n((?:[ \t]+[^\n]+\n?)*)/m);
11292
11294
  if (checkpointBlock?.[1]) {
11293
11295
  const block = checkpointBlock[1];
11294
11296
  const msgMatch = block.match(/messages:\s*(\d+)/);
@@ -11298,14 +11300,40 @@ function loadThresholds(vaultRoot) {
11298
11300
  if (minMatch?.[1])
11299
11301
  minutes = Number(minMatch[1]);
11300
11302
  }
11301
- return { messagesThreshold: messages, minutesThreshold: minutes };
11303
+ const foldersBlock = raw.match(/^folders:\s*\n((?:[ \t]+[^\n]+\n?)*)/m);
11304
+ if (foldersBlock?.[1]) {
11305
+ const logsMatch = foldersBlock[1].match(/logs:\s*['"]?([^'"\s]+)['"]?/);
11306
+ if (logsMatch?.[1])
11307
+ logsFolder = logsMatch[1];
11308
+ }
11309
+ return { messagesThreshold: messages, minutesThreshold: minutes, logsFolder };
11302
11310
  } catch {
11303
11311
  return {
11304
11312
  messagesThreshold: DEFAULT_MESSAGES_THRESHOLD,
11305
- minutesThreshold: DEFAULT_MINUTES_THRESHOLD
11313
+ minutesThreshold: DEFAULT_MINUTES_THRESHOLD,
11314
+ logsFolder: DEFAULT_LOGS_FOLDER
11306
11315
  };
11307
11316
  }
11308
11317
  }
11318
+ function maxCheckpointNnSync(vaultRoot, date, token, logsFolder) {
11319
+ const yyyy = date.slice(0, 4);
11320
+ const mm = date.slice(5, 7);
11321
+ const dir = join7(vaultRoot, logsFolder, yyyy, mm);
11322
+ const prefix = `${date}-${token}-checkpoint-`;
11323
+ try {
11324
+ let max = 0;
11325
+ for (const f2 of readdirSync(dir)) {
11326
+ if (!f2.startsWith(prefix) || !f2.endsWith(".md"))
11327
+ continue;
11328
+ const m3 = f2.match(/-checkpoint-(\d{2})\.md$/);
11329
+ if (m3)
11330
+ max = Math.max(max, Number(m3[1]));
11331
+ }
11332
+ return max;
11333
+ } catch {
11334
+ return 0;
11335
+ }
11336
+ }
11309
11337
  function formatDate(epochSeconds) {
11310
11338
  const d2 = new Date(epochSeconds * 1000);
11311
11339
  const yyyy = d2.getFullYear().toString();
@@ -11332,7 +11360,7 @@ function handleStop(token, vaultRoot, now = Math.floor(Date.now() / 1000), tmpDi
11332
11360
  return;
11333
11361
  }
11334
11362
  state.count += 1;
11335
- const { messagesThreshold, minutesThreshold } = loadThresholds(vaultRoot);
11363
+ const { messagesThreshold, minutesThreshold, logsFolder } = loadVaultSettings(vaultRoot);
11336
11364
  const timeThreshold = minutesThreshold * 60;
11337
11365
  const elapsed = state.last_ts === 0 ? 0 : now - state.last_ts;
11338
11366
  const thresholdMet = state.count >= messagesThreshold || elapsed >= timeThreshold;
@@ -11344,12 +11372,13 @@ function handleStop(token, vaultRoot, now = Math.floor(Date.now() / 1000), tmpDi
11344
11372
  writeState(token, { count: state.count, last_ts: state.last_ts, last_stop_nn: state.last_stop_nn }, tmpDir);
11345
11373
  return;
11346
11374
  }
11347
- const nextNn = String(Number(state.last_stop_nn) + 1).padStart(2, "0");
11348
11375
  const date = formatDate(now);
11376
+ const maxNn = maxCheckpointNnSync(vaultRoot, date, token, logsFolder);
11377
+ const nextNn = String(maxNn + 1).padStart(2, "0");
11378
+ const since = maxNn === 0 ? " since start" : ` since checkpoint-${String(maxNn).padStart(2, "0")}`;
11349
11379
  const filename = `${date}-${token}-checkpoint-${nextNn}.md`;
11350
- const since = state.last_stop_nn === "00" ? " since start" : ` since checkpoint-${state.last_stop_nn}`;
11351
11380
  emitBlock(`${filename}${since}`);
11352
- writeState(token, { count: 0, last_ts: now, last_stop_nn: nextNn }, tmpDir);
11381
+ writeState(token, { count: 0, last_ts: now, last_stop_nn: nextNn, pending_stub: state.pending_stub }, tmpDir);
11353
11382
  }
11354
11383
  var PRECOMPACT_STUB_TEMPLATE = (date, nn) => `---
11355
11384
  tags: [checkpoint, session-log]
@@ -11388,10 +11417,11 @@ async function handlePrecompact(token, vaultRoot, now = Math.floor(Date.now() /
11388
11417
  if (state.last_ts > 0 && now - state.last_ts < PRECOMPACT_RECENCY) {
11389
11418
  return;
11390
11419
  }
11391
- const stubNn = String(Number(state.last_stop_nn) + 1).padStart(2, "0");
11420
+ if (state.pending_stub) {
11421
+ return;
11422
+ }
11392
11423
  const date = formatDate(now);
11393
- const stubFilename = `${date}-${token}-checkpoint-${stubNn}.md`;
11394
- let logsFolder = "07-logs";
11424
+ let logsFolder = DEFAULT_LOGS_FOLDER;
11395
11425
  try {
11396
11426
  const config = await loadVaultConfig(vaultRoot);
11397
11427
  logsFolder = config.folders.logs;
@@ -11399,6 +11429,16 @@ async function handlePrecompact(token, vaultRoot, now = Math.floor(Date.now() /
11399
11429
  const yyyy = formatYYYY(now);
11400
11430
  const mm = formatMM(now);
11401
11431
  const stubDir = join7(vaultRoot, logsFolder, yyyy, mm);
11432
+ const existingFiles = await readdir2(stubDir).catch(() => []);
11433
+ const prefix = `${date}-${token}-checkpoint-`;
11434
+ const maxNn = existingFiles.reduce((max, f2) => {
11435
+ if (!f2.startsWith(prefix) || !f2.endsWith(".md"))
11436
+ return max;
11437
+ const m3 = f2.match(/-checkpoint-(\d{2})\.md$/);
11438
+ return m3 ? Math.max(max, Number(m3[1])) : max;
11439
+ }, 0);
11440
+ const stubNn = String(maxNn + 1).padStart(2, "0");
11441
+ const stubFilename = `${date}-${token}-checkpoint-${stubNn}.md`;
11402
11442
  const stubPath = join7(stubDir, stubFilename);
11403
11443
  try {
11404
11444
  await mkdir4(stubDir, { recursive: true });
@@ -11415,15 +11455,79 @@ async function handlePrecompact(token, vaultRoot, now = Math.floor(Date.now() /
11415
11455
  pending_stub: stubFilename
11416
11456
  }, tmpDir);
11417
11457
  }
11418
- function handlePostcompact(token, _now = Math.floor(Date.now() / 1000), tmpDir = osTmpdir()) {
11458
+ function handlePostcompact(token, vaultRoot, now = Math.floor(Date.now() / 1000), tmpDir = osTmpdir()) {
11419
11459
  const state = readState(token, tmpDir);
11420
11460
  if (!state.pending_stub) {
11421
11461
  writeState(token, { count: 0, last_ts: state.last_ts, last_stop_nn: state.last_stop_nn }, tmpDir);
11422
11462
  return;
11423
11463
  }
11424
- const since = state.last_stop_nn === "00" ? " since start" : ` since checkpoint-${state.last_stop_nn}`;
11464
+ const stubNnMatch = state.pending_stub.match(/-checkpoint-(\d{2})\.md$/);
11465
+ const stubNn = stubNnMatch?.[1] ?? "01";
11466
+ const stubNnNum = Number(stubNn);
11467
+ const { logsFolder } = loadVaultSettings(vaultRoot);
11468
+ const date = state.pending_stub.slice(0, 10);
11469
+ const yyyy = date.slice(0, 4);
11470
+ const mm = date.slice(5, 7);
11471
+ const dir = join7(vaultRoot, logsFolder, yyyy, mm);
11472
+ const prefix = `${date}-${token}-checkpoint-`;
11473
+ let predecessorNn = 0;
11474
+ try {
11475
+ for (const f2 of readdirSync(dir)) {
11476
+ if (!f2.startsWith(prefix) || !f2.endsWith(".md"))
11477
+ continue;
11478
+ const m3 = f2.match(/-checkpoint-(\d{2})\.md$/);
11479
+ if (m3) {
11480
+ const nn = Number(m3[1]);
11481
+ if (nn < stubNnNum)
11482
+ predecessorNn = Math.max(predecessorNn, nn);
11483
+ }
11484
+ }
11485
+ } catch {}
11486
+ const since = predecessorNn === 0 ? " since start" : ` since checkpoint-${String(predecessorNn).padStart(2, "0")}`;
11425
11487
  emitBlock(`fill-checkpoint: ${state.pending_stub}${since}`);
11426
- writeState(token, { count: 0, last_ts: 0, last_stop_nn: state.last_stop_nn }, tmpDir);
11488
+ writeState(token, { count: 0, last_ts: now, last_stop_nn: stubNn }, tmpDir);
11489
+ }
11490
+ function postcompactFallback(token, vaultRoot, now = Math.floor(Date.now() / 1000), tmpDir = osTmpdir()) {
11491
+ const state = readState(token, tmpDir);
11492
+ if (state.pending_stub) {
11493
+ handlePostcompact(token, vaultRoot, now, tmpDir);
11494
+ return;
11495
+ }
11496
+ const { logsFolder } = loadVaultSettings(vaultRoot);
11497
+ const date = formatDate(now);
11498
+ const yyyy = date.slice(0, 4);
11499
+ const mm = date.slice(5, 7);
11500
+ const dir = join7(vaultRoot, logsFolder, yyyy, mm);
11501
+ const prefix = `${date}-${token}-checkpoint-`;
11502
+ const stubs = [];
11503
+ const allNns = [];
11504
+ try {
11505
+ for (const f2 of readdirSync(dir)) {
11506
+ if (!f2.startsWith(prefix) || !f2.endsWith(".md"))
11507
+ continue;
11508
+ const m3 = f2.match(/-checkpoint-(\d{2})\.md$/);
11509
+ if (!m3)
11510
+ continue;
11511
+ allNns.push(Number(m3[1]));
11512
+ const content = readFileSync(join7(dir, f2), "utf8");
11513
+ if (/^trigger:\s*precompact/m.test(content) && !/^merged:\s*true/m.test(content)) {
11514
+ stubs.push(f2);
11515
+ }
11516
+ }
11517
+ } catch {}
11518
+ if (stubs.length === 0) {
11519
+ writeState(token, { count: 0, last_ts: state.last_ts, last_stop_nn: state.last_stop_nn }, tmpDir);
11520
+ return;
11521
+ }
11522
+ stubs.sort();
11523
+ const stubFilename = stubs[stubs.length - 1];
11524
+ const stubNnMatch = stubFilename.match(/-checkpoint-(\d{2})\.md$/);
11525
+ const stubNn = stubNnMatch?.[1] ?? "01";
11526
+ const stubNnNum = Number(stubNn);
11527
+ const predecessorNn = allNns.filter((n) => n < stubNnNum).reduce((max, n) => Math.max(max, n), 0);
11528
+ const since = predecessorNn === 0 ? " since start" : ` since checkpoint-${String(predecessorNn).padStart(2, "0")}`;
11529
+ emitBlock(`fill-checkpoint: ${stubFilename}${since}`);
11530
+ writeState(token, { count: 0, last_ts: now, last_stop_nn: stubNn }, tmpDir);
11427
11531
  }
11428
11532
  async function checkpointCommand(mode, token, vaultRoot) {
11429
11533
  try {
@@ -11435,7 +11539,7 @@ async function checkpointCommand(mode, token, vaultRoot) {
11435
11539
  await handlePrecompact(token, vaultRoot);
11436
11540
  break;
11437
11541
  case "postcompact":
11438
- handlePostcompact(token);
11542
+ postcompactFallback(token, vaultRoot);
11439
11543
  break;
11440
11544
  case "reset":
11441
11545
  handleReset(token);
@@ -11451,7 +11555,7 @@ async function checkpointCommand(mode, token, vaultRoot) {
11451
11555
  }
11452
11556
 
11453
11557
  // src/internal/migrate.ts
11454
- import { readFile as readFile5, readdir as readdir2, writeFile as writeFile6 } from "node:fs/promises";
11558
+ import { readFile as readFile5, readdir as readdir3, writeFile as writeFile6 } from "node:fs/promises";
11455
11559
  import { join as join8 } from "node:path";
11456
11560
  init_dist();
11457
11561
  function parseFrontmatterWithRest(rawText) {
@@ -11480,7 +11584,7 @@ function parseFrontmatterWithRest(rawText) {
11480
11584
  }
11481
11585
  async function listMdFiles(dir) {
11482
11586
  try {
11483
- const entries = await readdir2(dir);
11587
+ const entries = await readdir3(dir);
11484
11588
  return entries.filter((e2) => e2.endsWith(".md"));
11485
11589
  } catch {
11486
11590
  return [];
@@ -11492,7 +11596,7 @@ async function runBackfillRecapped(logsFolder) {
11492
11596
  let skipped = 0;
11493
11597
  let yearDirs = [];
11494
11598
  try {
11495
- yearDirs = await readdir2(logsFolder);
11599
+ yearDirs = await readdir3(logsFolder);
11496
11600
  } catch {
11497
11601
  return { backfilled: 0, skipped: 0 };
11498
11602
  }
@@ -11500,7 +11604,7 @@ async function runBackfillRecapped(logsFolder) {
11500
11604
  const yearPath = join8(logsFolder, yearDir);
11501
11605
  let monthDirs = [];
11502
11606
  try {
11503
- monthDirs = await readdir2(yearPath);
11607
+ monthDirs = await readdir3(yearPath);
11504
11608
  } catch {
11505
11609
  continue;
11506
11610
  }
@@ -11563,7 +11667,7 @@ async function migrateCommand(migrationName) {
11563
11667
 
11564
11668
  // src/internal/orphan-scan.ts
11565
11669
  init_dist();
11566
- import { readFile as readFile6, readdir as readdir3 } from "node:fs/promises";
11670
+ import { readFile as readFile6, readdir as readdir4 } from "node:fs/promises";
11567
11671
  import { join as join9 } from "node:path";
11568
11672
  function parseFrontmatter(rawText) {
11569
11673
  const text = rawText.replace(/\r\n/g, `
@@ -11592,7 +11696,7 @@ function getMonthParts(now = new Date) {
11592
11696
  }
11593
11697
  async function listMdFiles2(dir) {
11594
11698
  try {
11595
- const entries = await readdir3(dir);
11699
+ const entries = await readdir4(dir);
11596
11700
  return entries.filter((e2) => e2.endsWith(".md"));
11597
11701
  } catch {
11598
11702
  return [];
@@ -12115,7 +12219,7 @@ import {
12115
12219
  mkdir as mkdir6,
12116
12220
  mkdtemp as mkdtemp2,
12117
12221
  readFile as readFile8,
12118
- readdir as readdir4,
12222
+ readdir as readdir5,
12119
12223
  rename as rename6,
12120
12224
  rm as rm2,
12121
12225
  stat as stat4,
@@ -12156,7 +12260,7 @@ async function extractTarball2(tarball, destDir) {
12156
12260
  throw new Error(`tar extraction failed (exit ${exitCode}): ${errText.trim()}`);
12157
12261
  }
12158
12262
  await unlink3(tarPath);
12159
- const entries = await readdir4(destDir);
12263
+ const entries = await readdir5(destDir);
12160
12264
  const topLevel = entries.find((e2) => e2 !== "bundle.tar.gz");
12161
12265
  if (!topLevel) {
12162
12266
  throw new Error("Extracted tarball contains no top-level directory");
@@ -12170,7 +12274,7 @@ async function listFilesRecursive2(dir) {
12170
12274
  const current = queue.pop();
12171
12275
  let entries;
12172
12276
  try {
12173
- entries = await readdir4(current);
12277
+ entries = await readdir5(current);
12174
12278
  } catch {
12175
12279
  continue;
12176
12280
  }
@@ -12395,7 +12499,7 @@ async function cleanPluginCache2(installedPluginsPath, installedPluginsCacheDir)
12395
12499
  } catch {}
12396
12500
  if (onebrainDirs.length === 0) {
12397
12501
  try {
12398
- const marketplaceDirs = await readdir4(cacheDir);
12502
+ const marketplaceDirs = await readdir5(cacheDir);
12399
12503
  for (const mp of marketplaceDirs) {
12400
12504
  const candidate = join12(cacheDir, mp, "onebrain");
12401
12505
  try {
@@ -12411,7 +12515,7 @@ async function cleanPluginCache2(installedPluginsPath, installedPluginsCacheDir)
12411
12515
  for (const pluginDir of onebrainDirs) {
12412
12516
  let versionDirs;
12413
12517
  try {
12414
- versionDirs = await readdir4(pluginDir);
12518
+ versionDirs = await readdir5(pluginDir);
12415
12519
  } catch {
12416
12520
  continue;
12417
12521
  }
@@ -12598,7 +12702,7 @@ async function vaultSyncCommand2(vaultRoot, opts = {}) {
12598
12702
  }
12599
12703
 
12600
12704
  // src/index.ts
12601
- var VERSION = "2.0.2";
12705
+ var VERSION = "2.0.4";
12602
12706
  var RELEASE_DATE = "2026-04-25";
12603
12707
  var VERSION_STRING = `OneBrain v${VERSION} \u2014 released ${RELEASE_DATE}`;
12604
12708
  if (process.argv.slice(2).length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebrain-ai/cli",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "CLI for OneBrain — personal AI OS for Obsidian with persistent memory, 24+ skills, and Claude Code integration",
5
5
  "keywords": [
6
6
  "onebrain",