@onebrain-ai/cli 2.3.1 → 2.3.3
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/README.md +24 -8
- package/dist/onebrain +230 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ A great harness already knows how to talk to an LLM, edit files, and run shell c
|
|
|
84
84
|
| | What OneBrain adds | Why it matters |
|
|
85
85
|
|---|---|---|
|
|
86
86
|
| 🧠 | **Memory** — Identity, preferences, decisions, project state — promoted across four tiers as it earns trust | The harness alone starts every session from zero. OneBrain doesn't. |
|
|
87
|
-
| ⚡ | **Skills** —
|
|
87
|
+
| ⚡ | **Skills** — 31+ vault-aware verbs (`/braindump`, `/research`, `/distill`, `/learn`, `/wrapup`, …) | Pre-built workflows the harness would otherwise need you to script every time. |
|
|
88
88
|
| 🎯 | **Calibration** — Every correction, every preference, every learned habit tunes the agent to *you* | The longer you use it, the sharper it gets — your vault is the training data. |
|
|
89
89
|
| 🔀 | **Continuity** — Context lives in the vault, not the harness | Switch from Claude Code to Gemini CLI to Codex. Same memory. Same skills. Same agent. |
|
|
90
90
|
|
|
@@ -196,6 +196,8 @@ OneBrain has automatic behaviors that run without you doing anything:
|
|
|
196
196
|
|
|
197
197
|
**`/wrapup` is manual only.** Run it yourself when you want a visible, full session summary with output shown.
|
|
198
198
|
|
|
199
|
+
**Pausing long work across sessions.** For multi-day tasks that don't fit one session, run `/pause` to save a snapshot, then `/resume` in a future session to pick up seamlessly. Pause snapshots accumulate per-thread in `07-logs/pause/`; the next `/wrapup` consolidates them into one session log. This fills the gap between auto-checkpoint (involuntary) and `/wrapup` (terminal).
|
|
200
|
+
|
|
199
201
|
**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
202
|
|
|
201
203
|
> 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.
|
|
@@ -210,7 +212,7 @@ OneBrain doesn't just store markdown. Every feature exists to make you and the a
|
|
|
210
212
|
|---|---|---|
|
|
211
213
|
| 🧠 | **Persistent Memory** | Remembers your name, goals, preferences, and decisions across every session |
|
|
212
214
|
| 🖥️ | **Personal AI OS** | Full local stack: Claude Code + Obsidian + tmux + Telegram — no cloud infra needed |
|
|
213
|
-
| ⚡ | **
|
|
215
|
+
| ⚡ | **31+ Skills** | Braindump, research, consolidate, bookmark, import files, daily briefing, and more |
|
|
214
216
|
| 📂 | **Vault-native Markdown** | Plain Markdown, no lock-in. Your data stays yours forever |
|
|
215
217
|
| 🔀 | **Multi-Harness OS** | Switch between Claude Code, Gemini CLI, Codex, Qwen, or BYO LLM — context never breaks. [See architecture ↑](#the-harness-os-architecture) |
|
|
216
218
|
| 🔌 | **Zero Config** | Clone, open in Obsidian, run `/onboarding`. Ready in under 2 minutes |
|
|
@@ -340,7 +342,9 @@ Same vault. Same skills. Same memory. The LLM swaps; OneBrain doesn't notice.
|
|
|
340
342
|
|
|
341
343
|
<a id="commands"></a>
|
|
342
344
|
|
|
343
|
-
|
|
345
|
+
<!-- NEW-BADGE-CLEANUP: remove the green NEW shields.io badges from /search, /pause, /resume, /schedule-* on or after 2026-05-19 -->
|
|
346
|
+
|
|
347
|
+
## 📋 31+ Commands
|
|
344
348
|
|
|
345
349
|
Skills are organized by workflow phase. **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
350
|
|
|
@@ -373,7 +377,7 @@ Skills are organized by workflow phase. **Gemini CLI users:** prepend the `onebr
|
|
|
373
377
|
|
|
374
378
|
| Command | What it does |
|
|
375
379
|
|---------|-------------|
|
|
376
|
-
| `/search` | General vault retrieval — answers what + why questions across MEMORY, sessions, plans, decisions logs, notes |
|
|
380
|
+
| `/search`  | General vault retrieval — answers what + why questions across MEMORY, sessions, plans, decisions logs, notes |
|
|
377
381
|
| `/tasks` | Live task dashboard in Obsidian — creates/updates `TASKS.md` with always-current query sections |
|
|
378
382
|
| `/moc` | Vault portal in Obsidian — creates/updates `MOC.md` with projects, areas, knowledge, tasks, and pinned links |
|
|
379
383
|
| `/memory-review` | Interactive review of memory files — keep, update, deprecate, or delete entries |
|
|
@@ -389,10 +393,12 @@ Skills are organized by workflow phase. **Gemini CLI users:** prepend the `onebr
|
|
|
389
393
|
| `/qmd` | Set up fast vault search index — enables semantic search across all notes |
|
|
390
394
|
| `/help` | List all available commands with descriptions |
|
|
391
395
|
| `/wrapup` | Wrap up session — merges any auto-checkpoints and saves full summary to session log |
|
|
392
|
-
| `/
|
|
393
|
-
| `/
|
|
394
|
-
| `/schedule-
|
|
395
|
-
| `/schedule-
|
|
396
|
+
| `/pause`  | Save a snapshot of long-running work mid-flight so a future session can `/resume` (does NOT end the session or clear context) |
|
|
397
|
+
| `/resume`  | Load the latest snapshot of an active pause thread and pick up seamlessly in a fresh session |
|
|
398
|
+
| `/schedule-add`  | Interactive wizard for adding a recurring scheduled skill |
|
|
399
|
+
| `/schedule-once`  | One-shot wizard: schedule a skill to run once at a specific datetime |
|
|
400
|
+
| `/schedule-list`  | Show all scheduled entries |
|
|
401
|
+
| `/schedule-remove`  | Remove a scheduled entry |
|
|
396
402
|
|
|
397
403
|
<details>
|
|
398
404
|
<summary><strong>📁 Vault Structure</strong></summary>
|
|
@@ -544,6 +550,16 @@ schedule:
|
|
|
544
550
|
|
|
545
551
|
This matches the same shape Claude Code uses for `hooks` in `settings.json` — direct binary invocation with positional argv. No wrapper skill needed.
|
|
546
552
|
|
|
553
|
+
### Quick start — preset bundles
|
|
554
|
+
|
|
555
|
+
Don't want to hand-craft cron entries? OneBrain ships three preset tiers. New vaults are prompted during `/onboarding`; existing vaults can trigger the selector by running `/schedule-add` when the `schedule:` block is empty.
|
|
556
|
+
|
|
557
|
+
- **Minimal** — `/daily` briefing only
|
|
558
|
+
- **Essentials (default)** — `/daily` + `/weekly` Friday + `/recap` Sunday
|
|
559
|
+
- **Maintenance Plus** — Essentials + `/doctor` monthly + `/tasks` daily + `onebrain qmd-reindex` Sunday (mixes skill + command modes)
|
|
560
|
+
|
|
561
|
+
Canonical tier definitions live at `.claude/plugins/onebrain/skills/_shared/schedule-presets.md`.
|
|
562
|
+
|
|
547
563
|
CLI flags:
|
|
548
564
|
|
|
549
565
|
| Flag | Purpose |
|
package/dist/onebrain
CHANGED
|
@@ -9366,9 +9366,33 @@ async function checkClaudeSettings(vaultRoot) {
|
|
|
9366
9366
|
}
|
|
9367
9367
|
return { check: "claude-settings", status: "ok", message: "ok" };
|
|
9368
9368
|
}
|
|
9369
|
-
function
|
|
9369
|
+
function effectiveCommand(h) {
|
|
9370
|
+
const parts = [];
|
|
9371
|
+
if (typeof h.command === "string" && h.command.length > 0)
|
|
9372
|
+
parts.push(h.command);
|
|
9373
|
+
if (Array.isArray(h.args)) {
|
|
9374
|
+
for (const a of h.args)
|
|
9375
|
+
if (typeof a === "string" && a.length > 0)
|
|
9376
|
+
parts.push(a);
|
|
9377
|
+
}
|
|
9378
|
+
return parts.join(" ");
|
|
9379
|
+
}
|
|
9380
|
+
function detectHookForm(settings, event, cmdSubstring) {
|
|
9381
|
+
let sawLegacy = false;
|
|
9370
9382
|
const groups = settings.hooks?.[event] ?? [];
|
|
9371
|
-
|
|
9383
|
+
for (const g of groups) {
|
|
9384
|
+
for (const h of g.hooks ?? []) {
|
|
9385
|
+
if (!effectiveCommand(h).includes(cmdSubstring))
|
|
9386
|
+
continue;
|
|
9387
|
+
const isCanonical = h.command === CANONICAL_HOOK_COMMAND && (h.args?.length ?? 0) > 0;
|
|
9388
|
+
if (isCanonical)
|
|
9389
|
+
return "exec";
|
|
9390
|
+
sawLegacy = true;
|
|
9391
|
+
}
|
|
9392
|
+
}
|
|
9393
|
+
if (sawLegacy)
|
|
9394
|
+
return "legacy";
|
|
9395
|
+
return "absent";
|
|
9372
9396
|
}
|
|
9373
9397
|
async function checkSettingsHooks(vaultRoot, config) {
|
|
9374
9398
|
const settingsPath = join2(vaultRoot, ".claude", "settings.json");
|
|
@@ -9396,15 +9420,21 @@ async function checkSettingsHooks(vaultRoot, config) {
|
|
|
9396
9420
|
const confirmedHooks = [];
|
|
9397
9421
|
let permissionOk = false;
|
|
9398
9422
|
for (const { event, cmdSubstring } of REQUIRED_HOOKS) {
|
|
9399
|
-
|
|
9423
|
+
const form = detectHookForm(settings, event, cmdSubstring);
|
|
9424
|
+
if (form === "absent") {
|
|
9400
9425
|
warnings.push(`${event} hook missing`);
|
|
9426
|
+
} else if (form === "legacy") {
|
|
9427
|
+
warnings.push(`${event} hook in legacy shell form \u2014 --fix will migrate to exec form`);
|
|
9401
9428
|
} else {
|
|
9402
9429
|
confirmedHooks.push(`${event} \u2713`);
|
|
9403
9430
|
}
|
|
9404
9431
|
}
|
|
9405
9432
|
if (config.qmd_collection) {
|
|
9406
|
-
|
|
9433
|
+
const form = detectHookForm(settings, "PostToolUse", QMD_HOOK_SUBSTRING);
|
|
9434
|
+
if (form === "absent") {
|
|
9407
9435
|
warnings.push("PostToolUse (qmd) hook missing");
|
|
9436
|
+
} else if (form === "legacy") {
|
|
9437
|
+
warnings.push("PostToolUse (qmd) hook in legacy shell form \u2014 --fix will migrate to exec form");
|
|
9408
9438
|
} else {
|
|
9409
9439
|
confirmedHooks.push("PostToolUse \u2713");
|
|
9410
9440
|
}
|
|
@@ -9413,7 +9443,7 @@ async function checkSettingsHooks(vaultRoot, config) {
|
|
|
9413
9443
|
const groups = settings.hooks?.[event] ?? [];
|
|
9414
9444
|
for (const g of groups) {
|
|
9415
9445
|
for (const h of g.hooks ?? []) {
|
|
9416
|
-
const cmd = h
|
|
9446
|
+
const cmd = effectiveCommand(h);
|
|
9417
9447
|
if (!ALLOWED_HOOK_EVENTS.has(event) && cmd.includes(ONEBRAIN_COMMAND_SUBSTRING)) {
|
|
9418
9448
|
warnings.push(`stale ${event} hook found (onebrain CLI only registers Stop + PostToolUse)`);
|
|
9419
9449
|
}
|
|
@@ -9452,7 +9482,7 @@ async function checkSettingsHooks(vaultRoot, config) {
|
|
|
9452
9482
|
...okDetails.length > 0 ? { details: okDetails } : {}
|
|
9453
9483
|
};
|
|
9454
9484
|
}
|
|
9455
|
-
var import_yaml2, STANDARD_FOLDER_KEYS, REQUIRED_PLUGIN_FILES, REQUIRED_PLUGIN_DIRS, STALE_BASH_FILES, REQUIRED_VAULT_YML_KEYS, SOFT_REQUIRED_VAULT_YML_KEYS, REQUIRED_FOLDER_KEYS, STALE_MARKETPLACE_REPO = "kengio/onebrain", CANONICAL_MARKETPLACE_REPO = "onebrain-ai/onebrain", REQUIRED_HOOKS, ALLOWED_HOOK_EVENTS, QMD_HOOK_SUBSTRING = "onebrain qmd-reindex", ONEBRAIN_COMMAND_SUBSTRING = "onebrain", REQUIRED_PERMISSION = "Bash(onebrain *)", STALE_HOOK_SUBSTRINGS;
|
|
9485
|
+
var import_yaml2, STANDARD_FOLDER_KEYS, REQUIRED_PLUGIN_FILES, REQUIRED_PLUGIN_DIRS, STALE_BASH_FILES, REQUIRED_VAULT_YML_KEYS, SOFT_REQUIRED_VAULT_YML_KEYS, REQUIRED_FOLDER_KEYS, STALE_MARKETPLACE_REPO = "kengio/onebrain", CANONICAL_MARKETPLACE_REPO = "onebrain-ai/onebrain", REQUIRED_HOOKS, ALLOWED_HOOK_EVENTS, QMD_HOOK_SUBSTRING = "onebrain qmd-reindex", ONEBRAIN_COMMAND_SUBSTRING = "onebrain", REQUIRED_PERMISSION = "Bash(onebrain *)", STALE_HOOK_SUBSTRINGS, CANONICAL_HOOK_COMMAND = "onebrain";
|
|
9456
9486
|
var init_validator = __esm(() => {
|
|
9457
9487
|
import_yaml2 = __toESM(require_dist(), 1);
|
|
9458
9488
|
STANDARD_FOLDER_KEYS = [
|
|
@@ -9560,7 +9590,7 @@ var init_lib = __esm(() => {
|
|
|
9560
9590
|
var require_package = __commonJS((exports, module) => {
|
|
9561
9591
|
module.exports = {
|
|
9562
9592
|
name: "@onebrain-ai/cli",
|
|
9563
|
-
version: "2.3.
|
|
9593
|
+
version: "2.3.3",
|
|
9564
9594
|
description: "CLI for OneBrain \u2014 personal AI OS for Obsidian with persistent memory, 24+ skills, and Claude Code integration",
|
|
9565
9595
|
keywords: [
|
|
9566
9596
|
"onebrain",
|
|
@@ -11020,9 +11050,90 @@ var init_vault_sync = __esm(() => {
|
|
|
11020
11050
|
import_yaml3 = __toESM(require_dist(), 1);
|
|
11021
11051
|
});
|
|
11022
11052
|
|
|
11053
|
+
// src/commands/run-skill.ts
|
|
11054
|
+
var exports_run_skill = {};
|
|
11055
|
+
__export(exports_run_skill, {
|
|
11056
|
+
runSkillCommand: () => runSkillCommand,
|
|
11057
|
+
buildPrompt: () => buildPrompt
|
|
11058
|
+
});
|
|
11059
|
+
import { spawn } from "child_process";
|
|
11060
|
+
import { existsSync } from "fs";
|
|
11061
|
+
import { constants as osConstants } from "os";
|
|
11062
|
+
import { join as join11 } from "path";
|
|
11063
|
+
function resolveClaudeBin(override) {
|
|
11064
|
+
if (override)
|
|
11065
|
+
return override;
|
|
11066
|
+
const fromEnv = process.env["CLAUDE_BIN"];
|
|
11067
|
+
if (fromEnv) {
|
|
11068
|
+
if (existsSync(fromEnv))
|
|
11069
|
+
return fromEnv;
|
|
11070
|
+
console.error(import_picocolors8.default.yellow(`CLAUDE_BIN points to a missing file: ${fromEnv} \u2014 ignoring and probing defaults`));
|
|
11071
|
+
}
|
|
11072
|
+
for (const candidate of CLAUDE_FALLBACK_PATHS) {
|
|
11073
|
+
if (existsSync(candidate))
|
|
11074
|
+
return candidate;
|
|
11075
|
+
}
|
|
11076
|
+
return "claude";
|
|
11077
|
+
}
|
|
11078
|
+
function buildPrompt(skill, args) {
|
|
11079
|
+
const bare = skill.replace(/^\//, "");
|
|
11080
|
+
if (!bare) {
|
|
11081
|
+
throw new Error('skill name must not be empty (got "/" or "")');
|
|
11082
|
+
}
|
|
11083
|
+
const namespaced = bare.includes(":") ? bare : `onebrain:${bare}`;
|
|
11084
|
+
const slash = `/${namespaced}`;
|
|
11085
|
+
if (!args)
|
|
11086
|
+
return slash;
|
|
11087
|
+
const tokens = Object.entries(args).map(([k2, v2]) => `${k2}=${v2}`);
|
|
11088
|
+
return tokens.length ? `${slash} ${tokens.join(" ")}` : slash;
|
|
11089
|
+
}
|
|
11090
|
+
async function runSkillCommand(opts) {
|
|
11091
|
+
const vault = opts.vault;
|
|
11092
|
+
if (!existsSync(join11(vault, "vault.yml"))) {
|
|
11093
|
+
console.error(import_picocolors8.default.red(`Vault not found at ${vault} (no vault.yml present)`));
|
|
11094
|
+
return 78;
|
|
11095
|
+
}
|
|
11096
|
+
const claudeBin = resolveClaudeBin(opts.claudeBin);
|
|
11097
|
+
const prompt = buildPrompt(opts.skill, opts.args);
|
|
11098
|
+
const spawnFn = opts.spawnFn ?? spawn;
|
|
11099
|
+
const spawnOpts = {
|
|
11100
|
+
cwd: vault,
|
|
11101
|
+
stdio: "inherit"
|
|
11102
|
+
};
|
|
11103
|
+
const child = spawnFn(claudeBin, ["-p", prompt, "--add-dir", vault], spawnOpts);
|
|
11104
|
+
return await new Promise((resolve) => {
|
|
11105
|
+
child.on("exit", (code, signal) => {
|
|
11106
|
+
if (signal) {
|
|
11107
|
+
console.error(import_picocolors8.default.red(`claude terminated by signal: ${signal}`));
|
|
11108
|
+
resolve(128 + signalNumber(signal));
|
|
11109
|
+
return;
|
|
11110
|
+
}
|
|
11111
|
+
resolve(code ?? 1);
|
|
11112
|
+
});
|
|
11113
|
+
child.on("error", (err) => {
|
|
11114
|
+
console.error(import_picocolors8.default.red(`Failed to spawn claude (${claudeBin}): ${err.message}`));
|
|
11115
|
+
resolve(127);
|
|
11116
|
+
});
|
|
11117
|
+
});
|
|
11118
|
+
}
|
|
11119
|
+
function signalNumber(signal) {
|
|
11120
|
+
const sigs = osConstants.signals;
|
|
11121
|
+
return sigs[signal] ?? 0;
|
|
11122
|
+
}
|
|
11123
|
+
var import_picocolors8, HOME, CLAUDE_FALLBACK_PATHS;
|
|
11124
|
+
var init_run_skill = __esm(() => {
|
|
11125
|
+
import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
11126
|
+
HOME = process.env["HOME"] ?? "";
|
|
11127
|
+
CLAUDE_FALLBACK_PATHS = [
|
|
11128
|
+
...HOME ? [join11(HOME, ".local/bin/claude")] : [],
|
|
11129
|
+
"/opt/homebrew/bin/claude",
|
|
11130
|
+
"/usr/local/bin/claude"
|
|
11131
|
+
];
|
|
11132
|
+
});
|
|
11133
|
+
|
|
11023
11134
|
// src/index.ts
|
|
11024
|
-
import { existsSync as
|
|
11025
|
-
import { dirname as dirname4, join as
|
|
11135
|
+
import { existsSync as existsSync3 } from "fs";
|
|
11136
|
+
import { dirname as dirname4, join as join13 } from "path";
|
|
11026
11137
|
|
|
11027
11138
|
// node_modules/commander/esm.mjs
|
|
11028
11139
|
var import__ = __toESM(require_commander(), 1);
|
|
@@ -11048,7 +11159,7 @@ var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
|
11048
11159
|
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
11049
11160
|
function resolveBinaryVersion() {
|
|
11050
11161
|
if (true)
|
|
11051
|
-
return "2.3.
|
|
11162
|
+
return "2.3.3";
|
|
11052
11163
|
try {
|
|
11053
11164
|
const pkg = require_package();
|
|
11054
11165
|
return pkg.version ?? "dev";
|
|
@@ -13122,12 +13233,13 @@ async function sessionInitCommand(vaultRoot) {
|
|
|
13122
13233
|
init_vault_sync();
|
|
13123
13234
|
|
|
13124
13235
|
// src/commands/register-schedule.ts
|
|
13125
|
-
var
|
|
13236
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
13126
13237
|
var import_yaml7 = __toESM(require_dist(), 1);
|
|
13127
|
-
import {
|
|
13238
|
+
import { execFileSync } from "child_process";
|
|
13239
|
+
import { existsSync as existsSync2 } from "fs";
|
|
13128
13240
|
import { readFile as readFile6, unlink as unlink4, writeFile as writeFile6 } from "fs/promises";
|
|
13129
13241
|
import { homedir as homedir4 } from "os";
|
|
13130
|
-
import { join as
|
|
13242
|
+
import { isAbsolute, join as join12, resolve as pathResolve } from "path";
|
|
13131
13243
|
|
|
13132
13244
|
// src/lib/scheduler/cron-parse.ts
|
|
13133
13245
|
var CRON_FIELD_RE = /^(\*|\d+)$/;
|
|
@@ -13246,9 +13358,10 @@ function validateEntry(entry) {
|
|
|
13246
13358
|
}
|
|
13247
13359
|
|
|
13248
13360
|
// src/lib/scheduler/launchd.ts
|
|
13361
|
+
import { basename } from "path";
|
|
13249
13362
|
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13250
13363
|
function labelForEntry(entry) {
|
|
13251
|
-
const raw = isCommandMode(entry) ? entry.command : (entry.skill ?? "").replace(/^\//, "");
|
|
13364
|
+
const raw = isCommandMode(entry) ? basename(entry.command) : (entry.skill ?? "").replace(/^\//, "");
|
|
13252
13365
|
return raw.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
13253
13366
|
}
|
|
13254
13367
|
function generatePlist(entry, ctx) {
|
|
@@ -13270,9 +13383,9 @@ function generatePlist(entry, ctx) {
|
|
|
13270
13383
|
<string>-c</string>
|
|
13271
13384
|
<string>${shellLine}</string>`;
|
|
13272
13385
|
} else {
|
|
13273
|
-
const plistFilePath =
|
|
13274
|
-
const argsFlags = entry.args ? ` ${Object.entries(entry.args).map(([k2, v2]) =>
|
|
13275
|
-
const shellLine = xmlEscape(`"${ctx.skillCliPath}" --vault="${ctx.vaultPath}" --skill="${entry.skill}"
|
|
13386
|
+
const plistFilePath = `${ctx.homedir}/Library/LaunchAgents/${label}.plist`;
|
|
13387
|
+
const argsFlags = entry.args ? ` ${Object.entries(entry.args).map(([k2, v2]) => `--arg="${k2}=${v2}"`).join(" ")}` : "";
|
|
13388
|
+
const shellLine = xmlEscape(`"${ctx.skillCliPath}" run-skill --vault="${ctx.vaultPath}" --skill="${entry.skill}"${argsFlags}; launchctl bootout gui/${ctx.uid}/${label}; rm -f "${plistFilePath}"`);
|
|
13276
13389
|
programArgumentsBlock = ` <string>/bin/sh</string>
|
|
13277
13390
|
<string>-c</string>
|
|
13278
13391
|
<string>${shellLine}</string>`;
|
|
@@ -13286,14 +13399,17 @@ function generatePlist(entry, ctx) {
|
|
|
13286
13399
|
`);
|
|
13287
13400
|
} else {
|
|
13288
13401
|
const argsBlock = entry.args ? `
|
|
13289
|
-
${Object.entries(entry.args).
|
|
13402
|
+
${Object.entries(entry.args).flatMap(([k2, v2]) => [
|
|
13403
|
+
" <string>--arg</string>",
|
|
13404
|
+
` <string>${xmlEscape(`${k2}=${v2}`)}</string>`
|
|
13405
|
+
]).join(`
|
|
13290
13406
|
`)}` : "";
|
|
13291
13407
|
programArgumentsBlock = ` <string>${xmlEscape(ctx.skillCliPath)}</string>
|
|
13408
|
+
<string>run-skill</string>
|
|
13292
13409
|
<string>--vault</string>
|
|
13293
13410
|
<string>${xmlEscape(ctx.vaultPath)}</string>
|
|
13294
13411
|
<string>--skill</string>
|
|
13295
|
-
<string>${xmlEscape(entry.skill ?? "")}</string
|
|
13296
|
-
<string>--headless</string>${argsBlock}`;
|
|
13412
|
+
<string>${xmlEscape(entry.skill ?? "")}</string>${argsBlock}`;
|
|
13297
13413
|
}
|
|
13298
13414
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
13299
13415
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -13334,12 +13450,12 @@ async function registerSchedule(opts) {
|
|
|
13334
13450
|
if (opts.resume)
|
|
13335
13451
|
return await resumeSkill(opts.vault, opts.resume);
|
|
13336
13452
|
if (opts.refresh) {
|
|
13337
|
-
console.log(
|
|
13453
|
+
console.log(import_picocolors9.default.dim("(--refresh: re-emitting plists with current vault path)"));
|
|
13338
13454
|
}
|
|
13339
13455
|
const config = await readVaultConfig(opts.vault);
|
|
13340
13456
|
const entries = config.schedule ?? [];
|
|
13341
13457
|
if (entries.length === 0) {
|
|
13342
|
-
console.log(
|
|
13458
|
+
console.log(import_picocolors9.default.yellow("No schedule entries in vault.yml. Nothing to register."));
|
|
13343
13459
|
return;
|
|
13344
13460
|
}
|
|
13345
13461
|
for (const entry of entries) {
|
|
@@ -13360,16 +13476,17 @@ async function registerSchedule(opts) {
|
|
|
13360
13476
|
await validateSchedulable(opts.vault, entry);
|
|
13361
13477
|
}
|
|
13362
13478
|
}
|
|
13479
|
+
const resolvedEntries = entries.map((entry) => isCommandMode(entry) ? { ...entry, command: resolveCommandBinary(entry.command, opts.vault) } : entry);
|
|
13363
13480
|
const skillCliPath = process.argv[1] ?? "onebrain";
|
|
13364
13481
|
const ctx = {
|
|
13365
13482
|
vaultPath: opts.vault,
|
|
13366
13483
|
skillCliPath,
|
|
13367
|
-
logBasePath:
|
|
13484
|
+
logBasePath: join12(opts.vault, "07-logs/scheduler"),
|
|
13368
13485
|
uid: process.getuid?.() ?? 501,
|
|
13369
13486
|
homedir: homedir4()
|
|
13370
13487
|
};
|
|
13371
13488
|
const seen = new Map;
|
|
13372
|
-
for (const entry of
|
|
13489
|
+
for (const entry of resolvedEntries) {
|
|
13373
13490
|
const target = plistPath(labelForEntry(entry), ctx.homedir);
|
|
13374
13491
|
if (seen.has(target)) {
|
|
13375
13492
|
const existing = seen.get(target);
|
|
@@ -13381,28 +13498,28 @@ async function registerSchedule(opts) {
|
|
|
13381
13498
|
}
|
|
13382
13499
|
seen.set(target, entry);
|
|
13383
13500
|
}
|
|
13384
|
-
for (const entry of
|
|
13501
|
+
for (const entry of resolvedEntries) {
|
|
13385
13502
|
const plistContent = generatePlist(entry, ctx);
|
|
13386
13503
|
const targetPath = plistPath(labelForEntry(entry), ctx.homedir);
|
|
13387
13504
|
if (opts.dryRun) {
|
|
13388
|
-
console.log(
|
|
13505
|
+
console.log(import_picocolors9.default.cyan(`--- ${targetPath} ---`));
|
|
13389
13506
|
console.log(plistContent);
|
|
13390
13507
|
continue;
|
|
13391
13508
|
}
|
|
13392
13509
|
await writeFile6(targetPath, plistContent, "utf8");
|
|
13393
|
-
console.log(
|
|
13510
|
+
console.log(import_picocolors9.default.green(`\u2713 Wrote ${targetPath}`));
|
|
13394
13511
|
}
|
|
13395
|
-
console.log(
|
|
13512
|
+
console.log(import_picocolors9.default.green(`
|
|
13396
13513
|
Registered ${entries.length} schedule entries.`));
|
|
13397
|
-
console.log(
|
|
13398
|
-
for (const entry of
|
|
13514
|
+
console.log(import_picocolors9.default.dim("Use launchctl to load (or restart launchd):"));
|
|
13515
|
+
for (const entry of resolvedEntries) {
|
|
13399
13516
|
const target = plistPath(labelForEntry(entry), ctx.homedir);
|
|
13400
|
-
console.log(
|
|
13517
|
+
console.log(import_picocolors9.default.dim(` launchctl load ${target}`));
|
|
13401
13518
|
}
|
|
13402
13519
|
}
|
|
13403
13520
|
async function readVaultConfig(vault) {
|
|
13404
|
-
const yamlPath =
|
|
13405
|
-
if (!
|
|
13521
|
+
const yamlPath = join12(vault, "vault.yml");
|
|
13522
|
+
if (!existsSync2(yamlPath))
|
|
13406
13523
|
return {};
|
|
13407
13524
|
const raw = await readFile6(yamlPath, "utf8");
|
|
13408
13525
|
return import_yaml7.parse(raw) ?? {};
|
|
@@ -13420,8 +13537,8 @@ async function validateSchedulable(vault, entry) {
|
|
|
13420
13537
|
throw new Error("validateSchedulable invoked on non-skill entry \u2014 caller bug");
|
|
13421
13538
|
}
|
|
13422
13539
|
const skillName = entry.skill.replace(/^\//, "");
|
|
13423
|
-
const skillPath =
|
|
13424
|
-
if (!
|
|
13540
|
+
const skillPath = join12(vault, ".claude/plugins/onebrain/skills", skillName, "SKILL.md");
|
|
13541
|
+
if (!existsSync2(skillPath)) {
|
|
13425
13542
|
throw new Error(`Skill ${entry.skill} not found at ${skillPath}`);
|
|
13426
13543
|
}
|
|
13427
13544
|
const raw = await readFile6(skillPath, "utf8");
|
|
@@ -13456,55 +13573,85 @@ async function removeAll(vault) {
|
|
|
13456
13573
|
const entries = config.schedule ?? [];
|
|
13457
13574
|
for (const entry of entries) {
|
|
13458
13575
|
const target = plistPath(labelForEntry(entry), homedir4());
|
|
13459
|
-
if (
|
|
13576
|
+
if (existsSync2(target)) {
|
|
13460
13577
|
await unlink4(target);
|
|
13461
|
-
console.log(
|
|
13578
|
+
console.log(import_picocolors9.default.green(`\u2713 Removed ${target}`));
|
|
13462
13579
|
}
|
|
13463
13580
|
}
|
|
13464
13581
|
}
|
|
13465
13582
|
async function printStatus(vault) {
|
|
13466
13583
|
const config = await readVaultConfig(vault);
|
|
13467
13584
|
const entries = config.schedule ?? [];
|
|
13468
|
-
console.log(
|
|
13585
|
+
console.log(import_picocolors9.default.cyan(`Registered schedules: ${entries.length}`));
|
|
13469
13586
|
for (const entry of entries) {
|
|
13470
13587
|
const target = plistPath(labelForEntry(entry), homedir4());
|
|
13471
|
-
const installed =
|
|
13588
|
+
const installed = existsSync2(target) ? "\u2713" : "\u2717";
|
|
13472
13589
|
const when = entry.at ?? entry.cron ?? "?";
|
|
13473
|
-
const tag = entry.at ?
|
|
13590
|
+
const tag = entry.at ? import_picocolors9.default.magenta("[once]") : import_picocolors9.default.dim("[cron]");
|
|
13474
13591
|
let targetLabel;
|
|
13475
13592
|
if (isCommandMode(entry)) {
|
|
13476
13593
|
const argv = entry.args ?? [];
|
|
13477
13594
|
const argStr = argv.length ? ` ${argv.join(" ")}` : "";
|
|
13478
|
-
targetLabel = `${
|
|
13595
|
+
targetLabel = `${import_picocolors9.default.yellow("cmd:")} ${entry.command}${argStr}`;
|
|
13479
13596
|
} else {
|
|
13480
13597
|
const argsMap = entry.args ?? {};
|
|
13481
13598
|
const argStr = Object.keys(argsMap).length ? ` (${Object.entries(argsMap).map(([k2, v2]) => `${k2}=${v2}`).join(", ")})` : "";
|
|
13482
|
-
targetLabel = `${
|
|
13599
|
+
targetLabel = `${import_picocolors9.default.green("skill:")} ${entry.skill}${argStr}`;
|
|
13483
13600
|
}
|
|
13484
13601
|
console.log(` ${installed} ${tag} ${when} ${targetLabel}`);
|
|
13485
13602
|
}
|
|
13486
13603
|
}
|
|
13487
13604
|
async function testRun(vault, skill) {
|
|
13488
|
-
console.log(
|
|
13489
|
-
console.log(
|
|
13490
|
-
const {
|
|
13491
|
-
const
|
|
13492
|
-
|
|
13493
|
-
|
|
13494
|
-
|
|
13605
|
+
console.log(import_picocolors9.default.cyan(`Testing scheduled invocation of ${skill}...`));
|
|
13606
|
+
console.log(import_picocolors9.default.dim("(Spawns `onebrain run-skill` which shells out to Claude Code.)"));
|
|
13607
|
+
const { runSkillCommand: runSkillCommand2 } = await Promise.resolve().then(() => (init_run_skill(), exports_run_skill));
|
|
13608
|
+
const code = await runSkillCommand2({ vault, skill });
|
|
13609
|
+
if (code !== 0) {
|
|
13610
|
+
console.error(import_picocolors9.default.red(`Test run exited with code ${code}`));
|
|
13611
|
+
process.exit(code);
|
|
13612
|
+
}
|
|
13613
|
+
}
|
|
13614
|
+
var WHICH_BIN = "/usr/bin/which";
|
|
13615
|
+
function resolveCommandBinary(name, vaultRoot) {
|
|
13616
|
+
if (isAbsolute(name)) {
|
|
13617
|
+
if (!existsSync2(name)) {
|
|
13618
|
+
throw new Error(`Command not found at absolute path: ${name}. Check the path in vault.yml \u2014 launchd will silently fail at run time if the binary is missing.`);
|
|
13619
|
+
}
|
|
13620
|
+
return name;
|
|
13621
|
+
}
|
|
13622
|
+
if (name.startsWith("./") || name.startsWith("../")) {
|
|
13623
|
+
const base = vaultRoot ?? process.cwd();
|
|
13624
|
+
const resolved = pathResolve(base, name);
|
|
13625
|
+
if (!existsSync2(resolved)) {
|
|
13626
|
+
throw new Error(`Command not found at relative path: ${name} (resolved: ${resolved})`);
|
|
13627
|
+
}
|
|
13628
|
+
return resolved;
|
|
13629
|
+
}
|
|
13630
|
+
try {
|
|
13631
|
+
const out2 = execFileSync(WHICH_BIN, [name], {
|
|
13632
|
+
encoding: "utf8",
|
|
13633
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
13634
|
+
}).trim();
|
|
13635
|
+
if (out2 && existsSync2(out2))
|
|
13636
|
+
return out2;
|
|
13637
|
+
} catch {}
|
|
13638
|
+
throw new Error(`Command "${name}" not found in PATH. Use an absolute path in vault.yml (launchd's PATH is restricted to /usr/bin:/bin:/usr/sbin:/sbin and won't find ${name}).`);
|
|
13495
13639
|
}
|
|
13496
13640
|
async function resumeSkill(vault, skill) {
|
|
13497
|
-
const marker =
|
|
13498
|
-
if (
|
|
13641
|
+
const marker = join12(vault, "07-logs/scheduler/.paused", `${skill.replace(/^\//, "")}.txt`);
|
|
13642
|
+
if (existsSync2(marker)) {
|
|
13499
13643
|
await unlink4(marker);
|
|
13500
|
-
console.log(
|
|
13644
|
+
console.log(import_picocolors9.default.green(`\u2713 Resumed ${skill}`));
|
|
13501
13645
|
} else {
|
|
13502
|
-
console.log(
|
|
13646
|
+
console.log(import_picocolors9.default.yellow(`${skill} is not paused.`));
|
|
13503
13647
|
}
|
|
13504
13648
|
}
|
|
13505
13649
|
|
|
13650
|
+
// src/index.ts
|
|
13651
|
+
init_run_skill();
|
|
13652
|
+
|
|
13506
13653
|
// src/commands/update.ts
|
|
13507
|
-
var
|
|
13654
|
+
var import_picocolors10 = __toESM(require_picocolors(), 1);
|
|
13508
13655
|
init_cli_ui();
|
|
13509
13656
|
var GITHUB_REPO = "https://api.github.com/repos/onebrain-ai/onebrain";
|
|
13510
13657
|
var GITHUB_RELEASES_URL = `${GITHUB_REPO}/releases/latest`;
|
|
@@ -13599,7 +13746,7 @@ async function runUpdate(opts = {}) {
|
|
|
13599
13746
|
const sp1 = createStep("\uD83D\uDD0D", "Local version");
|
|
13600
13747
|
const { version: currentVersion, publishedAt: localPublishedAt } = await currentVersionFn();
|
|
13601
13748
|
result.currentVersion = currentVersion;
|
|
13602
|
-
const localVersionLabel = localPublishedAt ? `${
|
|
13749
|
+
const localVersionLabel = localPublishedAt ? `${import_picocolors10.default.dim(currentVersion)} ${import_picocolors10.default.dim("\xB7")} ${import_picocolors10.default.dim(formatReleaseDate(localPublishedAt))}` : import_picocolors10.default.dim(currentVersion);
|
|
13603
13750
|
if (sp1)
|
|
13604
13751
|
sp1.stop(localVersionLabel);
|
|
13605
13752
|
else
|
|
@@ -13611,9 +13758,9 @@ async function runUpdate(opts = {}) {
|
|
|
13611
13758
|
const release = await fetchLatestRelease(fetchFn);
|
|
13612
13759
|
latestVersion = release.version;
|
|
13613
13760
|
publishedAt = release.publishedAt;
|
|
13614
|
-
const dateSuffix = publishedAt ? ` ${
|
|
13761
|
+
const dateSuffix = publishedAt ? ` ${import_picocolors10.default.dim("\xB7")} ${import_picocolors10.default.dim(formatReleaseDate(publishedAt))}` : "";
|
|
13615
13762
|
if (sp2)
|
|
13616
|
-
sp2.stop(`${
|
|
13763
|
+
sp2.stop(`${import_picocolors10.default.green(latestVersion)}${dateSuffix}`);
|
|
13617
13764
|
else
|
|
13618
13765
|
writeLine(`latest: ${latestVersion}`);
|
|
13619
13766
|
} catch (err) {
|
|
@@ -13634,7 +13781,7 @@ async function runUpdate(opts = {}) {
|
|
|
13634
13781
|
if (check) {
|
|
13635
13782
|
if (isTTY) {
|
|
13636
13783
|
if (currentVersion !== latestVersion) {
|
|
13637
|
-
barLine(`\u2B06\uFE0F ${
|
|
13784
|
+
barLine(`\u2B06\uFE0F ${import_picocolors10.default.dim(currentVersion)} \u2192 ${import_picocolors10.default.green(latestVersion)} \xB7 binary would upgrade`);
|
|
13638
13785
|
barBlank();
|
|
13639
13786
|
}
|
|
13640
13787
|
close("Dry run complete \u2014 no changes made");
|
|
@@ -13647,7 +13794,7 @@ async function runUpdate(opts = {}) {
|
|
|
13647
13794
|
}
|
|
13648
13795
|
if (latestVersion === currentVersion) {
|
|
13649
13796
|
if (isTTY) {
|
|
13650
|
-
close(`Already up to date \u2014 @onebrain-ai/cli ${
|
|
13797
|
+
close(`Already up to date \u2014 @onebrain-ai/cli ${import_picocolors10.default.dim(latestVersion)}`);
|
|
13651
13798
|
} else {
|
|
13652
13799
|
writeLine(`already up to date: @onebrain-ai/cli ${latestVersion}`);
|
|
13653
13800
|
writeLine("done: nothing to do");
|
|
@@ -13657,14 +13804,14 @@ async function runUpdate(opts = {}) {
|
|
|
13657
13804
|
return result;
|
|
13658
13805
|
}
|
|
13659
13806
|
if (isTTY) {
|
|
13660
|
-
barLine(`\u2B06\uFE0F ${
|
|
13807
|
+
barLine(`\u2B06\uFE0F ${import_picocolors10.default.dim(currentVersion)} \u2192 ${import_picocolors10.default.green(latestVersion)}`);
|
|
13661
13808
|
barBlank();
|
|
13662
13809
|
}
|
|
13663
13810
|
const sp3 = createStep("\uD83D\uDCE6", "Installing @onebrain-ai/cli");
|
|
13664
13811
|
try {
|
|
13665
13812
|
await installBinaryFn(latestVersion);
|
|
13666
13813
|
if (sp3)
|
|
13667
|
-
sp3.stop(
|
|
13814
|
+
sp3.stop(import_picocolors10.default.green(latestVersion));
|
|
13668
13815
|
else
|
|
13669
13816
|
writeLine(`upgrading: @onebrain-ai/cli ${latestVersion} installed`);
|
|
13670
13817
|
} catch (err) {
|
|
@@ -13701,7 +13848,7 @@ async function runUpdate(opts = {}) {
|
|
|
13701
13848
|
result.ok = true;
|
|
13702
13849
|
result.exitCode = 0;
|
|
13703
13850
|
if (isTTY) {
|
|
13704
|
-
close(`Done \u2014 run ${
|
|
13851
|
+
close(`Done \u2014 run ${import_picocolors10.default.cyan("/update")} in Claude to sync vault files`);
|
|
13705
13852
|
} else {
|
|
13706
13853
|
writeLine("done: run /update in Claude to sync vault files");
|
|
13707
13854
|
}
|
|
@@ -13728,8 +13875,8 @@ function patchUtf8(stream) {
|
|
|
13728
13875
|
}
|
|
13729
13876
|
|
|
13730
13877
|
// src/index.ts
|
|
13731
|
-
var VERSION = "2.3.
|
|
13732
|
-
var RELEASE_DATE = "2026-05-
|
|
13878
|
+
var VERSION = "2.3.3";
|
|
13879
|
+
var RELEASE_DATE = "2026-05-13";
|
|
13733
13880
|
patchUtf8(process.stdout);
|
|
13734
13881
|
patchUtf8(process.stderr);
|
|
13735
13882
|
var VERSION_STRING = `OneBrain v${VERSION} \u2014 released ${RELEASE_DATE}`;
|
|
@@ -13743,7 +13890,7 @@ function findVaultRoot(startDir) {
|
|
|
13743
13890
|
return process.cwd();
|
|
13744
13891
|
let dir = startDir;
|
|
13745
13892
|
while (true) {
|
|
13746
|
-
if (
|
|
13893
|
+
if (existsSync3(join13(dir, "vault.yml")))
|
|
13747
13894
|
return dir;
|
|
13748
13895
|
const parent = dirname4(dir);
|
|
13749
13896
|
if (parent === dir)
|
|
@@ -13800,7 +13947,25 @@ program2.command("vault-sync", { hidden: true }).description("Sync plugin files
|
|
|
13800
13947
|
program2.command("register-hooks", { hidden: true }).description("Install Claude Code hooks into settings.json").option("--vault-dir <path>", "vault root directory (default: cwd)").action(async (opts) => {
|
|
13801
13948
|
await registerHooksCommand(opts.vaultDir);
|
|
13802
13949
|
});
|
|
13950
|
+
program2.command("run-skill", { hidden: true }).description("Run a OneBrain skill headlessly via Claude Code (used by the scheduler)").requiredOption("--vault <path>", "Vault root directory").requiredOption("--skill <name>", "Skill name (e.g. /daily)").option("--arg <key=value>", "Skill argument, repeatable (e.g. --arg topic=this-week)", collectKeyValue, {}).action(async (opts) => {
|
|
13951
|
+
const exitCode = await runSkillCommand({
|
|
13952
|
+
vault: opts.vault,
|
|
13953
|
+
skill: opts.skill,
|
|
13954
|
+
...Object.keys(opts.arg).length > 0 ? { args: opts.arg } : {}
|
|
13955
|
+
});
|
|
13956
|
+
process.exit(exitCode);
|
|
13957
|
+
});
|
|
13803
13958
|
program2.command("migrate", { hidden: true }).description("Run one-time migration scripts").argument("<name>", "migration name: backfill-recapped").argument("[cutoff_date]", "ISO date cutoff (YYYY-MM-DD) \u2014 skip logs newer than this date").action(async (name, cutoffDate) => {
|
|
13804
13959
|
await migrateCommand(name, cutoffDate);
|
|
13805
13960
|
});
|
|
13961
|
+
function collectKeyValue(value, prev) {
|
|
13962
|
+
const eq = value.indexOf("=");
|
|
13963
|
+
if (eq === -1) {
|
|
13964
|
+
throw new Error(`Invalid --arg format: "${value}" (expected key=value)`);
|
|
13965
|
+
}
|
|
13966
|
+
if (eq === 0) {
|
|
13967
|
+
throw new Error(`Invalid --arg: key is empty in "${value}"`);
|
|
13968
|
+
}
|
|
13969
|
+
return { ...prev, [value.slice(0, eq)]: value.slice(eq + 1) };
|
|
13970
|
+
}
|
|
13806
13971
|
program2.parse(process.argv);
|