@onebrain-ai/cli 2.2.0 → 2.2.2

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 +2 -2
  2. package/dist/onebrain +111 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -103,7 +103,7 @@ Obsidian becomes your dispatch hub for everything you do:
103
103
  <p align="center">
104
104
  <picture>
105
105
  <source media="(prefers-color-scheme: dark)" srcset="assets/diagrams/vault-hub-dark.svg">
106
- <img alt="Obsidian as command center — eight spokes radiate from the vault to CLI/repo, website, cloud infra, social media, office docs, project notes, research, and MCP server" src="assets/diagrams/vault-hub-light.svg" width="640">
106
+ <img alt="Obsidian as command center — eight spokes radiate from the vault to CLI/repo, website, cloud infra, social media, office docs, project notes, research, and MCP server" src="assets/diagrams/vault-hub-light.svg" width="460">
107
107
  </picture>
108
108
  </p>
109
109
 
@@ -118,7 +118,7 @@ OneBrain runs as a tight 3-step loop. Each cycle, both sides sharpen.
118
118
  <p align="center">
119
119
  <picture>
120
120
  <source media="(prefers-color-scheme: dark)" srcset="assets/diagrams/coevo-loop-dark.svg">
121
- <img alt="Co-Evolution loop — three nodes (01 CAPTURE at top, 02 EVOLVE at bottom-right, 03 WRAPUP at bottom-left) connected by curved arrows flowing clockwise" src="assets/diagrams/coevo-loop-light.svg" width="540">
121
+ <img alt="Co-Evolution loop — three nodes (01 CAPTURE at top, 02 EVOLVE at bottom-right, 03 WRAPUP at bottom-left) connected by curved arrows flowing clockwise" src="assets/diagrams/coevo-loop-light.svg" width="350">
122
122
  </picture>
123
123
  </p>
124
124
 
package/dist/onebrain CHANGED
@@ -8909,7 +8909,7 @@ async function loadVaultConfig(vaultRoot) {
8909
8909
  const file = Bun.file(vaultYmlPath);
8910
8910
  const exists = await file.exists();
8911
8911
  if (!exists) {
8912
- throw new Error(`vault.yml not found at ${vaultYmlPath}. Run onebrain init to set up this vault.`);
8912
+ throw new Error(`${VAULT_YML_NOT_FOUND_PREFIX}${vaultYmlPath}. Run onebrain init to set up this vault.`);
8913
8913
  }
8914
8914
  const text = await file.text();
8915
8915
  const parsed = import_yaml.parse(text) ?? {};
@@ -8952,7 +8952,7 @@ async function loadVaultConfig(vaultRoot) {
8952
8952
  }
8953
8953
  return config;
8954
8954
  }
8955
- var import_yaml, DEFAULT_FOLDERS, DEFAULT_CHECKPOINT;
8955
+ var import_yaml, DEFAULT_FOLDERS, DEFAULT_CHECKPOINT, VAULT_YML_NOT_FOUND_PREFIX = "vault.yml not found at ";
8956
8956
  var init_parser = __esm(() => {
8957
8957
  import_yaml = __toESM(require_dist(), 1);
8958
8958
  DEFAULT_FOLDERS = {
@@ -9545,7 +9545,9 @@ __export(exports_lib, {
9545
9545
  checkOrphanCheckpoints: () => checkOrphanCheckpoints,
9546
9546
  checkFolders: () => checkFolders,
9547
9547
  checkClaudeSettings: () => checkClaudeSettings,
9548
- atomicWrite: () => atomicWrite
9548
+ atomicWrite: () => atomicWrite,
9549
+ VAULT_YML_NOT_FOUND_PREFIX: () => VAULT_YML_NOT_FOUND_PREFIX,
9550
+ DEFAULT_CHECKPOINT: () => DEFAULT_CHECKPOINT
9549
9551
  });
9550
9552
  var init_lib = __esm(() => {
9551
9553
  init_parser();
@@ -9558,7 +9560,7 @@ var init_lib = __esm(() => {
9558
9560
  var require_package = __commonJS((exports, module) => {
9559
9561
  module.exports = {
9560
9562
  name: "@onebrain-ai/cli",
9561
- version: "2.2.0",
9563
+ version: "2.2.2",
9562
9564
  description: "CLI for OneBrain \u2014 personal AI OS for Obsidian with persistent memory, 24+ skills, and Claude Code integration",
9563
9565
  keywords: [
9564
9566
  "onebrain",
@@ -10989,7 +10991,7 @@ var import_picocolors5 = __toESM(require_picocolors(), 1);
10989
10991
  var import_picocolors = __toESM(require_picocolors(), 1);
10990
10992
  function resolveBinaryVersion() {
10991
10993
  if (true)
10992
- return "2.2.0";
10994
+ return "2.2.2";
10993
10995
  try {
10994
10996
  const pkg = require_package();
10995
10997
  return pkg.version ?? "dev";
@@ -11481,7 +11483,8 @@ async function runDoctor(opts = {}) {
11481
11483
  agent: "05-agent",
11482
11484
  archive: "06-archive",
11483
11485
  logs: "07-logs"
11484
- }
11486
+ },
11487
+ checkpoint: { ...DEFAULT_CHECKPOINT }
11485
11488
  };
11486
11489
  const sp1 = createStep("\uD83D\uDCCB", "vault.yml");
11487
11490
  const vaultYmlResult = await checkVaultYmlFn(vaultDir);
@@ -12408,9 +12411,7 @@ function loadVaultSettings(vaultRoot) {
12408
12411
  }
12409
12412
  }
12410
12413
  function maxCheckpointNnSync(vaultRoot, date, token, logsFolder) {
12411
- const yyyy = date.slice(0, 4);
12412
- const mm = date.slice(5, 7);
12413
- const dir = join7(vaultRoot, logsFolder, yyyy, mm);
12414
+ const dir = join7(vaultRoot, logsFolder, "checkpoint");
12414
12415
  const prefix = `${date}-${token}-checkpoint-`;
12415
12416
  try {
12416
12417
  let max = 0;
@@ -12525,14 +12526,15 @@ async function runBackfillRecapped(logsFolder, cutoffDate) {
12525
12526
  const today = new Date().toISOString().slice(0, 10);
12526
12527
  let backfilled = 0;
12527
12528
  let skipped = 0;
12529
+ const sessionRoot = join8(logsFolder, "session");
12528
12530
  let yearDirs = [];
12529
12531
  try {
12530
- yearDirs = await readdir3(logsFolder);
12532
+ yearDirs = await readdir3(sessionRoot);
12531
12533
  } catch {
12532
12534
  return { backfilled: 0, skipped: 0 };
12533
12535
  }
12534
12536
  for (const yearDir of yearDirs) {
12535
- const yearPath = join8(logsFolder, yearDir);
12537
+ const yearPath = join8(sessionRoot, yearDir);
12536
12538
  let monthDirs = [];
12537
12539
  try {
12538
12540
  monthDirs = await readdir3(yearPath);
@@ -12603,9 +12605,12 @@ async function migrateCommand(migrationName, cutoffDate, vaultDir) {
12603
12605
  }
12604
12606
 
12605
12607
  // src/commands/internal/orphan-scan.ts
12608
+ init_parser();
12606
12609
  var import_yaml6 = __toESM(require_dist(), 1);
12607
- import { readFile as readFile5, readdir as readdir4 } from "fs/promises";
12610
+ import { readFile as readFile5, readdir as readdir4, stat as stat6 } from "fs/promises";
12608
12611
  import { join as join9 } from "path";
12612
+ var MIN_GUARD_MINUTES = 60;
12613
+ var DEFAULT_ACTIVE_SESSION_GUARD_MS = 60 * 60 * 1000;
12609
12614
  function parseFrontmatter(rawText) {
12610
12615
  const text = rawText.replace(/\r\n/g, `
12611
12616
  `);
@@ -12623,14 +12628,6 @@ function parseFrontmatter(rawText) {
12623
12628
  return null;
12624
12629
  }
12625
12630
  }
12626
- function getMonthParts(now = new Date) {
12627
- const thisYear = String(now.getFullYear());
12628
- const thisMonth = String(now.getMonth() + 1).padStart(2, "0");
12629
- const prevDate = new Date(now.getFullYear(), now.getMonth() - 1, 1);
12630
- const prevYear = String(prevDate.getFullYear());
12631
- const prevMonth = String(prevDate.getMonth() + 1).padStart(2, "0");
12632
- return { thisYear, thisMonth, prevYear, prevMonth };
12633
- }
12634
12631
  async function listMdFiles2(dir) {
12635
12632
  try {
12636
12633
  const entries = await readdir4(dir);
@@ -12639,6 +12636,61 @@ async function listMdFiles2(dir) {
12639
12636
  return [];
12640
12637
  }
12641
12638
  }
12639
+ async function getActiveSessionGuardMs(vaultRoot) {
12640
+ try {
12641
+ const config = await loadVaultConfig(vaultRoot);
12642
+ const cpMinutes = config.checkpoint.minutes;
12643
+ if (typeof cpMinutes !== "number" || !Number.isFinite(cpMinutes) || cpMinutes <= 0) {
12644
+ return DEFAULT_ACTIVE_SESSION_GUARD_MS;
12645
+ }
12646
+ const minutes = Math.max(MIN_GUARD_MINUTES, 2 * cpMinutes);
12647
+ return minutes * 60 * 1000;
12648
+ } catch (err) {
12649
+ const msg = err instanceof Error ? err.message : String(err);
12650
+ const isExpectedAbsence = msg.startsWith(VAULT_YML_NOT_FOUND_PREFIX);
12651
+ if (!isExpectedAbsence) {
12652
+ try {
12653
+ process.stderr.write(`onebrain orphan-scan: vault.yml unreadable, using ${MIN_GUARD_MINUTES}-min Active-Session Guard default (${msg})
12654
+ `);
12655
+ } catch {}
12656
+ }
12657
+ return DEFAULT_ACTIVE_SESSION_GUARD_MS;
12658
+ }
12659
+ }
12660
+ async function getMtimeMs(path) {
12661
+ try {
12662
+ const s = await stat6(path);
12663
+ if (typeof s.mtimeMs !== "number" || !Number.isFinite(s.mtimeMs))
12664
+ return null;
12665
+ return s.mtimeMs;
12666
+ } catch {
12667
+ return null;
12668
+ }
12669
+ }
12670
+ async function getNewestMtimeMs(filePaths) {
12671
+ if (filePaths.length === 0)
12672
+ return null;
12673
+ let newest = Number.NEGATIVE_INFINITY;
12674
+ for (const p2 of filePaths) {
12675
+ const m3 = await getMtimeMs(p2);
12676
+ if (m3 === null)
12677
+ return null;
12678
+ if (m3 > newest)
12679
+ newest = m3;
12680
+ }
12681
+ return Number.isFinite(newest) ? newest : null;
12682
+ }
12683
+ async function isGroupActiveOrAmbiguous(filePaths, nowMs, guardMs) {
12684
+ if (!Number.isFinite(guardMs) || guardMs <= 0)
12685
+ return true;
12686
+ const newest = await getNewestMtimeMs(filePaths);
12687
+ if (newest === null)
12688
+ return true;
12689
+ const ageMs = nowMs - newest;
12690
+ if (ageMs < 0)
12691
+ return true;
12692
+ return ageMs < guardMs;
12693
+ }
12642
12694
  async function hasManualSessionLog(monthDir, date) {
12643
12695
  const files = await listMdFiles2(monthDir);
12644
12696
  const sessionLogs = files.filter((f2) => f2.startsWith(date) && f2.includes("-session-") && f2.endsWith(".md"));
@@ -12653,10 +12705,22 @@ async function hasManualSessionLog(monthDir, date) {
12653
12705
  }
12654
12706
  return false;
12655
12707
  }
12656
- async function scanMonthDir(monthDir, currentToken, today, seenTokens) {
12657
- const files = await listMdFiles2(monthDir);
12708
+ async function collectCandidateGroups(checkpointDir, sessionDir, currentToken, today) {
12709
+ const groups = new Map;
12710
+ const files = await listMdFiles2(checkpointDir);
12658
12711
  const checkpoints = files.filter((f2) => f2.includes("-checkpoint-") && f2.endsWith(".md"));
12659
- let count = 0;
12712
+ const manualLogCache = new Map;
12713
+ async function dateHasManualLog(date) {
12714
+ const cached = manualLogCache.get(date);
12715
+ if (cached !== undefined)
12716
+ return cached;
12717
+ const year = date.slice(0, 4);
12718
+ const month = date.slice(5, 7);
12719
+ const sessionMonthDir = join9(sessionDir, year, month);
12720
+ const result = await hasManualSessionLog(sessionMonthDir, date);
12721
+ manualLogCache.set(date, result);
12722
+ return result;
12723
+ }
12660
12724
  for (const fname of checkpoints) {
12661
12725
  const dateMatch = fname.match(/^(\d{4}-\d{2}-\d{2})-/);
12662
12726
  if (!dateMatch)
@@ -12673,32 +12737,37 @@ async function scanMonthDir(monthDir, currentToken, today, seenTokens) {
12673
12737
  continue;
12674
12738
  if (ftoken === currentToken)
12675
12739
  continue;
12676
- if (seenTokens.has(ftoken))
12677
- continue;
12678
- if (await hasManualSessionLog(monthDir, fdate))
12740
+ if (await dateHasManualLog(fdate))
12679
12741
  continue;
12680
- seenTokens.add(ftoken);
12681
- count++;
12742
+ const fpath = join9(checkpointDir, fname);
12743
+ const existing = groups.get(ftoken);
12744
+ if (existing)
12745
+ existing.push(fpath);
12746
+ else
12747
+ groups.set(ftoken, [fpath]);
12682
12748
  }
12683
- return count;
12749
+ return groups;
12684
12750
  }
12685
- async function runOrphanScan(logsFolder, sessionToken, now) {
12751
+ async function runOrphanScan(logsFolder, sessionToken, now, vaultRoot) {
12752
+ if (!vaultRoot) {
12753
+ throw new Error("runOrphanScan: vaultRoot is required and must be a non-empty path");
12754
+ }
12686
12755
  const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
12687
- const { thisYear, thisMonth, prevYear, prevMonth } = getMonthParts(now);
12688
- const monthDirs = [
12689
- { year: thisYear, month: thisMonth },
12690
- { year: prevYear, month: prevMonth }
12691
- ];
12692
- const seenTokens = new Set;
12756
+ const checkpointDir = join9(logsFolder, "checkpoint");
12757
+ const sessionDir = join9(logsFolder, "session");
12758
+ const allGroups = await collectCandidateGroups(checkpointDir, sessionDir, sessionToken, today);
12759
+ const guardMs = await getActiveSessionGuardMs(vaultRoot);
12760
+ const nowMs = now.getTime();
12693
12761
  let totalOrphans = 0;
12694
- for (const { year, month } of monthDirs) {
12695
- const monthDir = join9(logsFolder, year, month);
12696
- totalOrphans += await scanMonthDir(monthDir, sessionToken, today, seenTokens);
12762
+ for (const [, files] of allGroups) {
12763
+ if (await isGroupActiveOrAmbiguous(files, nowMs, guardMs))
12764
+ continue;
12765
+ totalOrphans++;
12697
12766
  }
12698
12767
  return { orphan_count: totalOrphans };
12699
12768
  }
12700
12769
  async function orphanScanCommand(logsFolder, sessionToken) {
12701
- const result = await runOrphanScan(logsFolder, sessionToken, new Date);
12770
+ const result = await runOrphanScan(logsFolder, sessionToken, new Date, process.cwd());
12702
12771
  process.stdout.write(`${JSON.stringify(result)}
12703
12772
  `);
12704
12773
  }
@@ -13220,8 +13289,8 @@ function patchUtf8(stream) {
13220
13289
  }
13221
13290
 
13222
13291
  // src/index.ts
13223
- var VERSION = "2.2.0";
13224
- var RELEASE_DATE = "2026-05-07";
13292
+ var VERSION = "2.2.2";
13293
+ var RELEASE_DATE = "2026-05-10";
13225
13294
  patchUtf8(process.stdout);
13226
13295
  patchUtf8(process.stderr);
13227
13296
  var VERSION_STRING = `OneBrain v${VERSION} \u2014 released ${RELEASE_DATE}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebrain-ai/cli",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
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",