@onebrain-ai/cli 2.1.16 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -5
  2. package/dist/onebrain +152 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -198,7 +198,7 @@ OneBrain has automatic behaviors that run without you doing anything:
198
198
 
199
199
  **The practical result:** Just say "bye" and everything is saved. If the session ends unexpectedly, you lose at most 15 messages — the last checkpoint recovers the rest.
200
200
 
201
- > Auto Checkpoint requires Claude Code (uses the Claude Code stop hook) and the `onebrain` CLI binary. Install with `npm install -g @onebrain-ai/cli`. Auto Session Summary works with any agent that follows INSTRUCTIONS.md.
201
+ > Auto Checkpoint runs on Claude Code (`Stop` hook) and Gemini CLI (`AfterAgent` hook), and uses the `onebrain` CLI binary. Install with `npm install -g @onebrain-ai/cli`. Auto Session Summary works with any agent that follows INSTRUCTIONS.md.
202
202
 
203
203
  ---
204
204
 
@@ -214,7 +214,7 @@ OneBrain doesn't just store markdown. Every feature exists to make you and the a
214
214
  | 📂 | **Vault-native Markdown** | Plain Markdown, no lock-in. Your data stays yours forever |
215
215
  | 🔀 | **Multi-Harness OS** | Switch between Claude Code, Gemini CLI, Codex, Qwen, or BYO LLM — context never breaks. [See architecture ↑](#the-harness-os-architecture) |
216
216
  | 🔌 | **Zero Config** | Clone, open in Obsidian, run `/onboarding`. Ready in under 2 minutes |
217
- | 📓 | **Session Logs & Checkpoints** | Every conversation saved with summaries and action items. Auto-checkpoints fire every 15 messages or 30 min so nothing is lost mid-session *(auto-checkpoint requires Claude Code)* |
217
+ | 📓 | **Session Logs & Checkpoints** | Every conversation saved with summaries and action items. Auto-checkpoints fire every 15 messages or 30 min so nothing is lost mid-session *(supported on Claude Code and Gemini CLI)* |
218
218
  | 💾 | **Auto Session Summary** | When you say "bye", the agent silently saves a complete session log — no `/wrapup` needed |
219
219
  | 🔗 | **Knowledge Synthesis** | `/consolidate` turns inbox captures into permanent connected knowledge |
220
220
  | 🔬 | **Confidence-scored Memory** | Every insight carries `[conf:high/medium/low]` + `[verified:YYYY-MM-DD]` — knowledge that grows more reliable with use |
@@ -276,7 +276,7 @@ Each harness reads OneBrain's instruction file automatically. Install it, run it
276
276
  | **OpenAI Codex** | `npm install -g @openai/codex` | `codex` | `AGENTS.md` |
277
277
  | **Qwen Code** | `npm install -g @qwen-code/qwen-code` | `qwen` | `AGENTS.md` |
278
278
 
279
- > Auto-checkpoint and the Stop hook are wired up for Claude Code today. The other harnesses get the rest of the skill surface (24+ commands) immediately, and gain hook coverage as upstream support lands.
279
+ > Auto-checkpoint and stop-hook coverage ship for Claude Code (`Stop` + optional `PostToolUse` qmd) and Gemini CLI (`AfterAgent` + optional `AfterTool` qmd) out of the box. Slash commands are namespaced on Gemini (`/onebrain:braindump`) to avoid collisions with built-ins; on Claude they invoke directly (`/braindump`). Other harnesses gain hook coverage as upstream support lands.
280
280
 
281
281
  ### 1. Install the OneBrain CLI
282
282
 
@@ -342,7 +342,7 @@ Same vault. Same skills. Same memory. The LLM swaps; OneBrain doesn't notice.
342
342
 
343
343
  ## 📋 24+ Commands
344
344
 
345
- The full skill surface, alphabetized by workflow.
345
+ The full skill surface, alphabetized by workflow. **Gemini CLI users:** prepend the `onebrain:` namespace, e.g. `/onebrain:braindump` instead of `/braindump` (avoids collisions with Gemini built-in commands like `/help` and `/tasks`).
346
346
 
347
347
  | Command | What it does |
348
348
  |---------|-------------|
@@ -402,7 +402,8 @@ onebrain/
402
402
  ├── GEMINI.md Instructions for Gemini CLI
403
403
  ├── AGENTS.md Universal agent instructions
404
404
  ├── vault.yml Your vault configuration (created during onboarding)
405
- └── .claude/plugins/ AI skills and hooks
405
+ ├── .claude/plugins/ AI skills, hooks, and shared INSTRUCTIONS (read by Claude Code)
406
+ └── .gemini/ Gemini CLI project config — hooks + namespaced slash commands
406
407
  ```
407
408
 
408
409
  The core workflow: capture everything to inbox → process with `/consolidate` → synthesize into knowledge or save as reference → archive what's done.
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.1.16",
9563
+ version: "2.2.1",
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",
@@ -10205,20 +10207,6 @@ function applyPermissions(settings) {
10205
10207
  }
10206
10208
  return added;
10207
10209
  }
10208
- async function registerGeminiHooks(vaultRoot) {
10209
- const geminiSettingsPath = join4(vaultRoot, ".gemini", "settings.json");
10210
- try {
10211
- const text = await readFile(geminiSettingsPath, "utf8");
10212
- const settings = JSON.parse(text);
10213
- applyHooks(settings);
10214
- await writeSettings(geminiSettingsPath, settings);
10215
- } catch (err) {
10216
- if (err.code !== "ENOENT") {
10217
- process.stderr.write(`register-hooks: gemini warning: ${err instanceof Error ? err.message : String(err)}
10218
- `);
10219
- }
10220
- }
10221
- }
10222
10210
  async function registerDirectPath() {
10223
10211
  const home = homedir();
10224
10212
  const candidates = [join4(home, ".zshrc"), join4(home, ".bashrc"), join4(home, ".profile")];
@@ -10316,9 +10304,6 @@ async function runRegisterHooks(opts = {}) {
10316
10304
  if (!isTTY)
10317
10305
  note("permissions ok");
10318
10306
  }
10319
- if (harness === "gemini") {
10320
- await registerGeminiHooks(vaultRoot);
10321
- }
10322
10307
  if (harness === "direct") {
10323
10308
  await registerDirectPath();
10324
10309
  }
@@ -10495,6 +10480,43 @@ async function syncPluginFiles(extractedDir, vaultRoot, unlinkFn = unlink2) {
10495
10480
  }
10496
10481
  return { filesAdded, filesRemoved };
10497
10482
  }
10483
+ async function syncGeminiConfig(extractedDir, vaultRoot, unlinkFn = unlink2) {
10484
+ const sourceGemini = join5(extractedDir, ".gemini");
10485
+ const destGemini = join5(vaultRoot, ".gemini");
10486
+ try {
10487
+ await stat4(sourceGemini);
10488
+ } catch {
10489
+ return { filesAdded: 0, filesRemoved: 0 };
10490
+ }
10491
+ await mkdirIdempotent(destGemini);
10492
+ const sourceFiles = await listFilesRecursive(sourceGemini);
10493
+ const sourceRelSet = new Set(sourceFiles.map((f2) => relative(sourceGemini, f2)));
10494
+ const destFiles = await listFilesRecursive(destGemini);
10495
+ const destRelSet = new Set(destFiles.map((f2) => relative(destGemini, f2)));
10496
+ const staleRels = [];
10497
+ for (const rel of destRelSet) {
10498
+ if (!sourceRelSet.has(rel))
10499
+ staleRels.push(rel);
10500
+ }
10501
+ let filesAdded = 0;
10502
+ for (const srcPath of sourceFiles) {
10503
+ const rel = relative(sourceGemini, srcPath);
10504
+ const destPath = join5(destGemini, rel);
10505
+ await mkdirIdempotent(dirname2(destPath));
10506
+ const content = await readFile2(srcPath);
10507
+ await writeFile3(destPath, content);
10508
+ filesAdded++;
10509
+ }
10510
+ let filesRemoved = 0;
10511
+ for (const rel of staleRels) {
10512
+ const destPath = join5(destGemini, rel);
10513
+ try {
10514
+ await unlinkFn(destPath);
10515
+ filesRemoved++;
10516
+ } catch {}
10517
+ }
10518
+ return { filesAdded, filesRemoved };
10519
+ }
10498
10520
  async function copyRootDocs(extractedDir, vaultRoot) {
10499
10521
  const docs = ["CONTRIBUTING.md", "CHANGELOG.md", "PLUGIN-CHANGELOG.md"];
10500
10522
  for (const doc of docs) {
@@ -10821,9 +10843,10 @@ async function runVaultSync(vaultRoot, opts = {}) {
10821
10843
  stopSpinner(`onebrain-ai/onebrain@${branch} (v${result.version})`);
10822
10844
  startSpinner("\uD83D\uDCC2", "Syncing files");
10823
10845
  try {
10824
- const { filesAdded, filesRemoved } = await syncPluginFiles(extractedDir, vaultRoot, unlinkFn);
10825
- result.filesAdded = filesAdded;
10826
- result.filesRemoved = filesRemoved;
10846
+ const pluginResult = await syncPluginFiles(extractedDir, vaultRoot, unlinkFn);
10847
+ const geminiResult = await syncGeminiConfig(extractedDir, vaultRoot, unlinkFn);
10848
+ result.filesAdded = pluginResult.filesAdded + geminiResult.filesAdded;
10849
+ result.filesRemoved = pluginResult.filesRemoved + geminiResult.filesRemoved;
10827
10850
  } catch (err) {
10828
10851
  stopSpinner("plugin sync failed");
10829
10852
  const msg = err instanceof Error ? err.message : String(err);
@@ -10968,7 +10991,7 @@ var import_picocolors5 = __toESM(require_picocolors(), 1);
10968
10991
  var import_picocolors = __toESM(require_picocolors(), 1);
10969
10992
  function resolveBinaryVersion() {
10970
10993
  if (true)
10971
- return "2.1.16";
10994
+ return "2.2.1";
10972
10995
  try {
10973
10996
  const pkg = require_package();
10974
10997
  return pkg.version ?? "dev";
@@ -11460,7 +11483,8 @@ async function runDoctor(opts = {}) {
11460
11483
  agent: "05-agent",
11461
11484
  archive: "06-archive",
11462
11485
  logs: "07-logs"
11463
- }
11486
+ },
11487
+ checkpoint: { ...DEFAULT_CHECKPOINT }
11464
11488
  };
11465
11489
  const sp1 = createStep("\uD83D\uDCCB", "vault.yml");
11466
11490
  const vaultYmlResult = await checkVaultYmlFn(vaultDir);
@@ -12582,9 +12606,12 @@ async function migrateCommand(migrationName, cutoffDate, vaultDir) {
12582
12606
  }
12583
12607
 
12584
12608
  // src/commands/internal/orphan-scan.ts
12609
+ init_parser();
12585
12610
  var import_yaml6 = __toESM(require_dist(), 1);
12586
- import { readFile as readFile5, readdir as readdir4 } from "fs/promises";
12611
+ import { readFile as readFile5, readdir as readdir4, stat as stat6 } from "fs/promises";
12587
12612
  import { join as join9 } from "path";
12613
+ var MIN_GUARD_MINUTES = 60;
12614
+ var DEFAULT_ACTIVE_SESSION_GUARD_MS = 60 * 60 * 1000;
12588
12615
  function parseFrontmatter(rawText) {
12589
12616
  const text = rawText.replace(/\r\n/g, `
12590
12617
  `);
@@ -12618,6 +12645,61 @@ async function listMdFiles2(dir) {
12618
12645
  return [];
12619
12646
  }
12620
12647
  }
12648
+ async function getActiveSessionGuardMs(vaultRoot) {
12649
+ try {
12650
+ const config = await loadVaultConfig(vaultRoot);
12651
+ const cpMinutes = config.checkpoint.minutes;
12652
+ if (typeof cpMinutes !== "number" || !Number.isFinite(cpMinutes) || cpMinutes <= 0) {
12653
+ return DEFAULT_ACTIVE_SESSION_GUARD_MS;
12654
+ }
12655
+ const minutes = Math.max(MIN_GUARD_MINUTES, 2 * cpMinutes);
12656
+ return minutes * 60 * 1000;
12657
+ } catch (err) {
12658
+ const msg = err instanceof Error ? err.message : String(err);
12659
+ const isExpectedAbsence = msg.startsWith(VAULT_YML_NOT_FOUND_PREFIX);
12660
+ if (!isExpectedAbsence) {
12661
+ try {
12662
+ process.stderr.write(`onebrain orphan-scan: vault.yml unreadable, using ${MIN_GUARD_MINUTES}-min Active-Session Guard default (${msg})
12663
+ `);
12664
+ } catch {}
12665
+ }
12666
+ return DEFAULT_ACTIVE_SESSION_GUARD_MS;
12667
+ }
12668
+ }
12669
+ async function getMtimeMs(path) {
12670
+ try {
12671
+ const s = await stat6(path);
12672
+ if (typeof s.mtimeMs !== "number" || !Number.isFinite(s.mtimeMs))
12673
+ return null;
12674
+ return s.mtimeMs;
12675
+ } catch {
12676
+ return null;
12677
+ }
12678
+ }
12679
+ async function getNewestMtimeMs(filePaths) {
12680
+ if (filePaths.length === 0)
12681
+ return null;
12682
+ let newest = Number.NEGATIVE_INFINITY;
12683
+ for (const p2 of filePaths) {
12684
+ const m3 = await getMtimeMs(p2);
12685
+ if (m3 === null)
12686
+ return null;
12687
+ if (m3 > newest)
12688
+ newest = m3;
12689
+ }
12690
+ return Number.isFinite(newest) ? newest : null;
12691
+ }
12692
+ async function isGroupActiveOrAmbiguous(filePaths, nowMs, guardMs) {
12693
+ if (!Number.isFinite(guardMs) || guardMs <= 0)
12694
+ return true;
12695
+ const newest = await getNewestMtimeMs(filePaths);
12696
+ if (newest === null)
12697
+ return true;
12698
+ const ageMs = nowMs - newest;
12699
+ if (ageMs < 0)
12700
+ return true;
12701
+ return ageMs < guardMs;
12702
+ }
12621
12703
  async function hasManualSessionLog(monthDir, date) {
12622
12704
  const files = await listMdFiles2(monthDir);
12623
12705
  const sessionLogs = files.filter((f2) => f2.startsWith(date) && f2.includes("-session-") && f2.endsWith(".md"));
@@ -12632,10 +12714,19 @@ async function hasManualSessionLog(monthDir, date) {
12632
12714
  }
12633
12715
  return false;
12634
12716
  }
12635
- async function scanMonthDir(monthDir, currentToken, today, seenTokens) {
12717
+ async function collectCandidateGroupsForMonth(monthDir, currentToken, today) {
12718
+ const groups = new Map;
12636
12719
  const files = await listMdFiles2(monthDir);
12637
12720
  const checkpoints = files.filter((f2) => f2.includes("-checkpoint-") && f2.endsWith(".md"));
12638
- let count = 0;
12721
+ const manualLogCache = new Map;
12722
+ async function dateHasManualLog(date) {
12723
+ const cached = manualLogCache.get(date);
12724
+ if (cached !== undefined)
12725
+ return cached;
12726
+ const result = await hasManualSessionLog(monthDir, date);
12727
+ manualLogCache.set(date, result);
12728
+ return result;
12729
+ }
12639
12730
  for (const fname of checkpoints) {
12640
12731
  const dateMatch = fname.match(/^(\d{4}-\d{2}-\d{2})-/);
12641
12732
  if (!dateMatch)
@@ -12652,32 +12743,51 @@ async function scanMonthDir(monthDir, currentToken, today, seenTokens) {
12652
12743
  continue;
12653
12744
  if (ftoken === currentToken)
12654
12745
  continue;
12655
- if (seenTokens.has(ftoken))
12656
- continue;
12657
- if (await hasManualSessionLog(monthDir, fdate))
12746
+ if (await dateHasManualLog(fdate))
12658
12747
  continue;
12659
- seenTokens.add(ftoken);
12660
- count++;
12748
+ const fpath = join9(monthDir, fname);
12749
+ const existing = groups.get(ftoken);
12750
+ if (existing)
12751
+ existing.push(fpath);
12752
+ else
12753
+ groups.set(ftoken, [fpath]);
12661
12754
  }
12662
- return count;
12755
+ return groups;
12663
12756
  }
12664
- async function runOrphanScan(logsFolder, sessionToken, now) {
12757
+ async function runOrphanScan(logsFolder, sessionToken, now, vaultRoot) {
12758
+ if (!vaultRoot) {
12759
+ throw new Error("runOrphanScan: vaultRoot is required and must be a non-empty path");
12760
+ }
12665
12761
  const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
12666
12762
  const { thisYear, thisMonth, prevYear, prevMonth } = getMonthParts(now);
12667
12763
  const monthDirs = [
12668
12764
  { year: thisYear, month: thisMonth },
12669
12765
  { year: prevYear, month: prevMonth }
12670
12766
  ];
12671
- const seenTokens = new Set;
12672
- let totalOrphans = 0;
12767
+ const allGroups = new Map;
12673
12768
  for (const { year, month } of monthDirs) {
12674
12769
  const monthDir = join9(logsFolder, year, month);
12675
- totalOrphans += await scanMonthDir(monthDir, sessionToken, today, seenTokens);
12770
+ const monthGroups = await collectCandidateGroupsForMonth(monthDir, sessionToken, today);
12771
+ for (const [token, files] of monthGroups) {
12772
+ const existing = allGroups.get(token);
12773
+ if (existing)
12774
+ existing.push(...files);
12775
+ else
12776
+ allGroups.set(token, [...files]);
12777
+ }
12778
+ }
12779
+ const guardMs = await getActiveSessionGuardMs(vaultRoot);
12780
+ const nowMs = now.getTime();
12781
+ let totalOrphans = 0;
12782
+ for (const [, files] of allGroups) {
12783
+ if (await isGroupActiveOrAmbiguous(files, nowMs, guardMs))
12784
+ continue;
12785
+ totalOrphans++;
12676
12786
  }
12677
12787
  return { orphan_count: totalOrphans };
12678
12788
  }
12679
12789
  async function orphanScanCommand(logsFolder, sessionToken) {
12680
- const result = await runOrphanScan(logsFolder, sessionToken, new Date);
12790
+ const result = await runOrphanScan(logsFolder, sessionToken, new Date, process.cwd());
12681
12791
  process.stdout.write(`${JSON.stringify(result)}
12682
12792
  `);
12683
12793
  }
@@ -13199,8 +13309,8 @@ function patchUtf8(stream) {
13199
13309
  }
13200
13310
 
13201
13311
  // src/index.ts
13202
- var VERSION = "2.1.16";
13203
- var RELEASE_DATE = "2026-05-06";
13312
+ var VERSION = "2.2.1";
13313
+ var RELEASE_DATE = "2026-05-07";
13204
13314
  patchUtf8(process.stdout);
13205
13315
  patchUtf8(process.stderr);
13206
13316
  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.1.16",
3
+ "version": "2.2.1",
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",