@onebrain-ai/cli 2.2.5 → 2.3.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 +82 -6
  2. package/dist/onebrain +400 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,7 +23,7 @@
23
23
  </p>
24
24
 
25
25
  <p align="center">
26
- <strong>Your personal AI OS</strong> — persistent memory, 25+ skills, and a full local stack<br>
26
+ <strong>Your personal AI OS</strong> — persistent memory, 29+ skills, and a full local stack<br>
27
27
  (Claude Code + Obsidian + tmux + Telegram), entirely on your own machine.
28
28
  </p>
29
29
 
@@ -35,7 +35,7 @@
35
35
 
36
36
  ## What is OneBrain?
37
37
 
38
- OneBrain is an AI operating system layer built on top of Obsidian. It gives your AI agent persistent memory, a structured knowledge vault, and 25+ pre-built skills — so every session picks up exactly where the last one left off.
38
+ OneBrain is an AI operating system layer built on top of Obsidian. It gives your AI agent persistent memory, a structured knowledge vault, and 29+ pre-built skills — so every session picks up exactly where the last one left off.
39
39
 
40
40
  Unlike chat-based AI tools, OneBrain lives in plain Markdown files you own forever. No cloud sync required. No proprietary format. Just your agent, your vault, your data.
41
41
 
@@ -70,7 +70,7 @@ OneBrain doesn't compete with Claude Code, Gemini CLI, or any other AI harness
70
70
 
71
71
  | # | Layer | Role | What lives here |
72
72
  |---|---|---|---|
73
- | 01 | **OneBrain** | OS layer (plugin + CLI) | 25+ skills · lifecycle hooks · vault sync · indexing · checkpoints · harness routing |
73
+ | 01 | **OneBrain** | OS layer (plugin + CLI) | 29+ skills · lifecycle hooks · vault sync · indexing · checkpoints · harness routing |
74
74
  | 02 | **Harness** | Agentic runtime | Bring your own — Claude Code · Gemini CLI · Codex · Qwen · ... |
75
75
  | 03 | **LLM** | Intelligence source | Local (mlx, ollama) · cloud (claude, gemini, gpt) · raw API |
76
76
  | 04 | **Obsidian Vault** | Source of truth | Plain Markdown — notes, memory, decisions, knowledge graph |
@@ -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** — 25+ vault-aware verbs (`/braindump`, `/research`, `/distill`, `/learn`, `/wrapup`, …) | Pre-built workflows the harness would otherwise need you to script every time. |
87
+ | ⚡ | **Skills** — 29+ 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
 
@@ -210,7 +210,7 @@ OneBrain doesn't just store markdown. Every feature exists to make you and the a
210
210
  |---|---|---|
211
211
  | 🧠 | **Persistent Memory** | Remembers your name, goals, preferences, and decisions across every session |
212
212
  | 🖥️ | **Personal AI OS** | Full local stack: Claude Code + Obsidian + tmux + Telegram — no cloud infra needed |
213
- | ⚡ | **25+ Skills** | Braindump, research, consolidate, bookmark, import files, daily briefing, and more |
213
+ | ⚡ | **29+ Skills** | Braindump, research, consolidate, bookmark, import files, daily briefing, and more |
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 |
@@ -340,7 +340,7 @@ Same vault. Same skills. Same memory. The LLM swaps; OneBrain doesn't notice.
340
340
 
341
341
  <a id="commands"></a>
342
342
 
343
- ## 📋 25+ Commands
343
+ ## 📋 29+ Commands
344
344
 
345
345
  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
346
 
@@ -389,6 +389,10 @@ Skills are organized by workflow phase. **Gemini CLI users:** prepend the `onebr
389
389
  | `/qmd` | Set up fast vault search index — enables semantic search across all notes |
390
390
  | `/help` | List all available commands with descriptions |
391
391
  | `/wrapup` | Wrap up session — merges any auto-checkpoints and saves full summary to session log |
392
+ | `/schedule-add` | Interactive wizard for adding a recurring scheduled skill |
393
+ | `/schedule-once` | One-shot wizard: schedule a skill to run once at a specific datetime |
394
+ | `/schedule-list` | Show all scheduled entries |
395
+ | `/schedule-remove` | Remove a scheduled entry |
392
396
 
393
397
  <details>
394
398
  <summary><strong>📁 Vault Structure</strong></summary>
@@ -483,6 +487,78 @@ Multi-device sync and hosted agent runtimes. Your unified intelligence travels w
483
487
 
484
488
  ---
485
489
 
490
+ ## Scheduling
491
+
492
+ OneBrain skills can run automatically on a schedule via your OS scheduler (macOS launchd; Linux + Windows coming soon). Configure in `vault.yml`:
493
+
494
+ ```yaml
495
+ schedule:
496
+ - cron: "0 9 * * *" # daily 9am
497
+ skill: /daily
498
+ - cron: "0 18 * * 5" # Friday 6pm
499
+ skill: /weekly
500
+ - cron: "0 12 * * 0" # Sunday noon
501
+ skill: /recap
502
+ ```
503
+
504
+ For a one-shot reminder, use `at:` instead of `cron:`:
505
+
506
+ ```yaml
507
+ schedule:
508
+ - at: "2026-05-13 14:30"
509
+ skill: /reminder
510
+ ```
511
+
512
+ After firing, the launchd plist auto-uninstalls itself.
513
+
514
+ Register schedules:
515
+
516
+ ```bash
517
+ onebrain register-schedule
518
+ ```
519
+
520
+ Or use the interactive wizards from inside your vault:
521
+
522
+ ```
523
+ /schedule-add # recurring schedule wizard
524
+ /schedule-once # one-shot wizard
525
+ /schedule-list # show all scheduled entries
526
+ /schedule-remove # remove an entry
527
+ ```
528
+
529
+ Output goes to `[logs_folder]/scheduler/YYYY/MM/YYYY-MM-DD-{skill}.md` as readable markdown.
530
+
531
+ ### Command mode (CLI binaries, hook-style)
532
+
533
+ For CLI maintenance tasks that aren't OneBrain skills, use the `command + args[]` shape:
534
+
535
+ ```yaml
536
+ schedule:
537
+ - cron: "0 3 * * 0"
538
+ command: onebrain
539
+ args: [qmd-reindex]
540
+ - cron: "0 5 * * *"
541
+ command: rsync
542
+ args: [-av, /vault, /backup]
543
+ ```
544
+
545
+ This matches the same shape Claude Code uses for `hooks` in `settings.json` — direct binary invocation with positional argv. No wrapper skill needed.
546
+
547
+ CLI flags:
548
+
549
+ | Flag | Purpose |
550
+ |---|---|
551
+ | `--dry-run` | Print plist without writing |
552
+ | `--remove` | Remove all OneBrain schedules |
553
+ | `--refresh` | Re-emit plists after vault move |
554
+ | `--resume <skill>` | Resume an auto-paused skill |
555
+ | `--status` | Show registered schedules + run history |
556
+ | `--test <skill>` | Manually invoke a scheduled skill once |
557
+
558
+ **Note:** OneBrain's scheduler is distinct from Claude Code's `/loop` (in-session) and `/schedule` (cloud-hosted). OneBrain runs locally and writes to your vault.
559
+
560
+ ---
561
+
486
562
  <details>
487
563
  <summary><strong>⚙️ Prerequisites & Detailed Setup</strong></summary>
488
564
  <br>
package/dist/onebrain CHANGED
@@ -9560,7 +9560,7 @@ var init_lib = __esm(() => {
9560
9560
  var require_package = __commonJS((exports, module) => {
9561
9561
  module.exports = {
9562
9562
  name: "@onebrain-ai/cli",
9563
- version: "2.2.5",
9563
+ version: "2.3.1",
9564
9564
  description: "CLI for OneBrain \u2014 personal AI OS for Obsidian with persistent memory, 24+ skills, and Claude Code integration",
9565
9565
  keywords: [
9566
9566
  "onebrain",
@@ -11021,8 +11021,8 @@ var init_vault_sync = __esm(() => {
11021
11021
  });
11022
11022
 
11023
11023
  // src/index.ts
11024
- import { existsSync } from "fs";
11025
- import { dirname as dirname4, join as join11 } from "path";
11024
+ import { existsSync as existsSync2 } from "fs";
11025
+ import { dirname as dirname4, join as join12 } from "path";
11026
11026
 
11027
11027
  // node_modules/commander/esm.mjs
11028
11028
  var import__ = __toESM(require_commander(), 1);
@@ -11048,7 +11048,7 @@ var import_picocolors5 = __toESM(require_picocolors(), 1);
11048
11048
  var import_picocolors = __toESM(require_picocolors(), 1);
11049
11049
  function resolveBinaryVersion() {
11050
11050
  if (true)
11051
- return "2.2.5";
11051
+ return "2.3.1";
11052
11052
  try {
11053
11053
  const pkg = require_package();
11054
11054
  return pkg.version ?? "dev";
@@ -13121,8 +13121,390 @@ async function sessionInitCommand(vaultRoot) {
13121
13121
  // src/index.ts
13122
13122
  init_vault_sync();
13123
13123
 
13124
- // src/commands/update.ts
13124
+ // src/commands/register-schedule.ts
13125
13125
  var import_picocolors8 = __toESM(require_picocolors(), 1);
13126
+ var import_yaml7 = __toESM(require_dist(), 1);
13127
+ import { existsSync } from "fs";
13128
+ import { readFile as readFile6, unlink as unlink4, writeFile as writeFile6 } from "fs/promises";
13129
+ import { homedir as homedir4 } from "os";
13130
+ import { join as join11 } from "path";
13131
+
13132
+ // src/lib/scheduler/cron-parse.ts
13133
+ var CRON_FIELD_RE = /^(\*|\d+)$/;
13134
+ function validateCron(cron) {
13135
+ const fields = cron.trim().split(/\s+/);
13136
+ if (fields.length !== 5) {
13137
+ return { valid: false, reason: `expected 5 fields, got ${fields.length}` };
13138
+ }
13139
+ for (const f2 of fields) {
13140
+ if (!CRON_FIELD_RE.test(f2)) {
13141
+ return { valid: false, reason: `invalid field syntax: "${f2}"` };
13142
+ }
13143
+ }
13144
+ return { valid: true };
13145
+ }
13146
+ function cronFieldsToLaunchd(cron) {
13147
+ const [m3, h, dom, mon, dow] = cron.trim().split(/\s+/);
13148
+ const out2 = {};
13149
+ if (m3 !== "*")
13150
+ out2.Minute = Number.parseInt(m3, 10);
13151
+ if (h !== "*")
13152
+ out2.Hour = Number.parseInt(h, 10);
13153
+ if (dom !== "*")
13154
+ out2.Day = Number.parseInt(dom, 10);
13155
+ if (mon !== "*")
13156
+ out2.Month = Number.parseInt(mon, 10);
13157
+ if (dow !== "*")
13158
+ out2.Weekday = Number.parseInt(dow, 10);
13159
+ return out2;
13160
+ }
13161
+ var AT_RE = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})$/;
13162
+ function validateAt(at) {
13163
+ const m3 = at.match(AT_RE);
13164
+ if (!m3) {
13165
+ return { valid: false, reason: `expected 'YYYY-MM-DD HH:MM', got "${at}"` };
13166
+ }
13167
+ const [, , mo, d, h, mi] = m3;
13168
+ const month = Number.parseInt(mo, 10);
13169
+ const day = Number.parseInt(d, 10);
13170
+ const hour = Number.parseInt(h, 10);
13171
+ const minute = Number.parseInt(mi, 10);
13172
+ if (month < 1 || month > 12)
13173
+ return { valid: false, reason: `month out of range: ${month}` };
13174
+ if (day < 1 || day > 31)
13175
+ return { valid: false, reason: `day out of range: ${day}` };
13176
+ if (hour > 23)
13177
+ return { valid: false, reason: `hour out of range: ${hour}` };
13178
+ if (minute > 59)
13179
+ return { valid: false, reason: `minute out of range: ${minute}` };
13180
+ return { valid: true };
13181
+ }
13182
+ function atToLaunchd(at) {
13183
+ const m3 = at.match(AT_RE);
13184
+ if (!m3)
13185
+ throw new Error(`atToLaunchd called with unvalidated input: ${at}`);
13186
+ const [, y2, mo, d, h, mi] = m3;
13187
+ return {
13188
+ Year: Number.parseInt(y2, 10),
13189
+ Month: Number.parseInt(mo, 10),
13190
+ Day: Number.parseInt(d, 10),
13191
+ Hour: Number.parseInt(h, 10),
13192
+ Minute: Number.parseInt(mi, 10)
13193
+ };
13194
+ }
13195
+
13196
+ // src/lib/scheduler/entry.ts
13197
+ function isOneShot(entry) {
13198
+ return entry.at !== undefined;
13199
+ }
13200
+ function isSkillMode(entry) {
13201
+ return entry.skill !== undefined;
13202
+ }
13203
+ function isCommandMode(entry) {
13204
+ return entry.command !== undefined;
13205
+ }
13206
+ function validateEntry(entry) {
13207
+ const hasCron = entry.cron !== undefined;
13208
+ const hasAt = entry.at !== undefined;
13209
+ if (hasCron === hasAt) {
13210
+ return { valid: false, reason: "entry must have exactly one of `cron` or `at`" };
13211
+ }
13212
+ const hasSkill = entry.skill !== undefined;
13213
+ const hasCommand = entry.command !== undefined;
13214
+ if (hasSkill === hasCommand) {
13215
+ return { valid: false, reason: "entry must have exactly one of `skill` or `command`" };
13216
+ }
13217
+ if (hasSkill && !entry.skill) {
13218
+ return { valid: false, reason: "entry.skill must not be empty" };
13219
+ }
13220
+ if (hasCommand && !entry.command) {
13221
+ return { valid: false, reason: "entry.command must not be empty" };
13222
+ }
13223
+ if (entry.args !== undefined) {
13224
+ const isArray = Array.isArray(entry.args);
13225
+ if (hasSkill && isArray) {
13226
+ return {
13227
+ valid: false,
13228
+ reason: "skill-mode entries require `args` as a map (Record<string, string>), not an array"
13229
+ };
13230
+ }
13231
+ if (hasCommand && !isArray) {
13232
+ return {
13233
+ valid: false,
13234
+ reason: "command-mode entries require `args` as a string array, not a map"
13235
+ };
13236
+ }
13237
+ if (isArray) {
13238
+ for (const v2 of entry.args) {
13239
+ if (typeof v2 !== "string") {
13240
+ return { valid: false, reason: "command-mode `args` must contain only strings" };
13241
+ }
13242
+ }
13243
+ }
13244
+ }
13245
+ return { valid: true };
13246
+ }
13247
+
13248
+ // src/lib/scheduler/launchd.ts
13249
+ var xmlEscape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13250
+ function labelForEntry(entry) {
13251
+ const raw = isCommandMode(entry) ? entry.command : (entry.skill ?? "").replace(/^\//, "");
13252
+ return raw.replace(/[^a-zA-Z0-9-]/g, "-");
13253
+ }
13254
+ function generatePlist(entry, ctx) {
13255
+ const labelSafe = labelForEntry(entry);
13256
+ const label = `com.onebrain.${labelSafe}`;
13257
+ const calendar = isOneShot(entry) ? atToLaunchd(entry.at) : cronFieldsToLaunchd(entry.cron);
13258
+ const calendarXml = Object.entries(calendar).map(([k2, v2]) => ` <key>${k2}</key>
13259
+ <integer>${v2}</integer>`).join(`
13260
+ `);
13261
+ let programArgumentsBlock;
13262
+ if (isOneShot(entry)) {
13263
+ if (isCommandMode(entry)) {
13264
+ const argv = entry.args ?? [];
13265
+ const quotedArgs = argv.map((a2) => `"${a2}"`).join(" ");
13266
+ const innerCommand = `"${entry.command}"${quotedArgs ? ` ${quotedArgs}` : ""}`;
13267
+ const plistFilePath = `${ctx.homedir}/Library/LaunchAgents/${label}.plist`;
13268
+ const shellLine = xmlEscape(`${innerCommand}; launchctl bootout gui/${ctx.uid}/${label}; rm -f "${plistFilePath}"`);
13269
+ programArgumentsBlock = ` <string>/bin/sh</string>
13270
+ <string>-c</string>
13271
+ <string>${shellLine}</string>`;
13272
+ } else {
13273
+ const plistFilePath = plistPath(entry.skill ?? "", ctx.homedir);
13274
+ const argsFlags = entry.args ? ` ${Object.entries(entry.args).map(([k2, v2]) => `--${k2}="${v2}"`).join(" ")}` : "";
13275
+ const shellLine = xmlEscape(`"${ctx.skillCliPath}" --vault="${ctx.vaultPath}" --skill="${entry.skill}" --headless${argsFlags}; launchctl bootout gui/${ctx.uid}/${label}; rm -f "${plistFilePath}"`);
13276
+ programArgumentsBlock = ` <string>/bin/sh</string>
13277
+ <string>-c</string>
13278
+ <string>${shellLine}</string>`;
13279
+ }
13280
+ } else if (isCommandMode(entry)) {
13281
+ const argv = entry.args ?? [];
13282
+ programArgumentsBlock = [
13283
+ ` <string>${xmlEscape(entry.command)}</string>`,
13284
+ ...argv.map((a2) => ` <string>${xmlEscape(a2)}</string>`)
13285
+ ].join(`
13286
+ `);
13287
+ } else {
13288
+ const argsBlock = entry.args ? `
13289
+ ${Object.entries(entry.args).map(([k2, v2]) => ` <string>--${xmlEscape(k2)}=${xmlEscape(v2)}</string>`).join(`
13290
+ `)}` : "";
13291
+ programArgumentsBlock = ` <string>${xmlEscape(ctx.skillCliPath)}</string>
13292
+ <string>--vault</string>
13293
+ <string>${xmlEscape(ctx.vaultPath)}</string>
13294
+ <string>--skill</string>
13295
+ <string>${xmlEscape(entry.skill ?? "")}</string>
13296
+ <string>--headless</string>${argsBlock}`;
13297
+ }
13298
+ return `<?xml version="1.0" encoding="UTF-8"?>
13299
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
13300
+ <plist version="1.0">
13301
+ <dict>
13302
+ <key>Label</key>
13303
+ <string>${xmlEscape(label)}</string>
13304
+ <key>ProgramArguments</key>
13305
+ <array>
13306
+ ${programArgumentsBlock}
13307
+ </array>
13308
+ <key>StartCalendarInterval</key>
13309
+ <dict>
13310
+ ${calendarXml}
13311
+ </dict>
13312
+ <key>StandardOutPath</key>
13313
+ <string>${xmlEscape(ctx.logBasePath)}/onebrain-${labelSafe}.stdout</string>
13314
+ <key>StandardErrorPath</key>
13315
+ <string>${xmlEscape(ctx.logBasePath)}/onebrain-${labelSafe}.stderr</string>
13316
+ <key>RunAtLoad</key>
13317
+ <false/>
13318
+ </dict>
13319
+ </plist>`;
13320
+ }
13321
+ function plistPath(skillOrLabel, homedir4) {
13322
+ const labelSafe = skillOrLabel.startsWith("/") ? skillOrLabel.replace(/^\//, "").replace(/[^a-zA-Z0-9-]/g, "-") : skillOrLabel.replace(/[^a-zA-Z0-9-]/g, "-");
13323
+ return `${homedir4}/Library/LaunchAgents/com.onebrain.${labelSafe}.plist`;
13324
+ }
13325
+
13326
+ // src/commands/register-schedule.ts
13327
+ async function registerSchedule(opts) {
13328
+ if (opts.remove)
13329
+ return await removeAll(opts.vault);
13330
+ if (opts.status)
13331
+ return await printStatus(opts.vault);
13332
+ if (opts.test)
13333
+ return await testRun(opts.vault, opts.test);
13334
+ if (opts.resume)
13335
+ return await resumeSkill(opts.vault, opts.resume);
13336
+ if (opts.refresh) {
13337
+ console.log(import_picocolors8.default.dim("(--refresh: re-emitting plists with current vault path)"));
13338
+ }
13339
+ const config = await readVaultConfig(opts.vault);
13340
+ const entries = config.schedule ?? [];
13341
+ if (entries.length === 0) {
13342
+ console.log(import_picocolors8.default.yellow("No schedule entries in vault.yml. Nothing to register."));
13343
+ return;
13344
+ }
13345
+ for (const entry of entries) {
13346
+ const ve = validateEntry(entry);
13347
+ if (!ve.valid)
13348
+ throw new Error(`Invalid schedule entry: ${ve.reason}`);
13349
+ if (isOneShot(entry)) {
13350
+ const va = validateAt(entry.at);
13351
+ if (!va.valid)
13352
+ throw new Error(`Invalid at "${entry.at}": ${va.reason}`);
13353
+ sanitizeArgsForOneShot(entry);
13354
+ } else if (entry.cron !== undefined) {
13355
+ const vc = validateCron(entry.cron);
13356
+ if (!vc.valid)
13357
+ throw new Error(`Invalid cron "${entry.cron}": ${vc.reason}`);
13358
+ }
13359
+ if (isSkillMode(entry)) {
13360
+ await validateSchedulable(opts.vault, entry);
13361
+ }
13362
+ }
13363
+ const skillCliPath = process.argv[1] ?? "onebrain";
13364
+ const ctx = {
13365
+ vaultPath: opts.vault,
13366
+ skillCliPath,
13367
+ logBasePath: join11(opts.vault, "07-logs/scheduler"),
13368
+ uid: process.getuid?.() ?? 501,
13369
+ homedir: homedir4()
13370
+ };
13371
+ const seen = new Map;
13372
+ for (const entry of entries) {
13373
+ const target = plistPath(labelForEntry(entry), ctx.homedir);
13374
+ if (seen.has(target)) {
13375
+ const existing = seen.get(target);
13376
+ if (existing) {
13377
+ const existingLabel = isCommandMode(existing) ? `command:${existing.command}` : `skill:${existing.skill}`;
13378
+ const newLabel = isCommandMode(entry) ? `command:${entry.command}` : `skill:${entry.skill}`;
13379
+ throw new Error(`Conflict: ${newLabel} and ${existingLabel} normalize to the same plist path ${target}`);
13380
+ }
13381
+ }
13382
+ seen.set(target, entry);
13383
+ }
13384
+ for (const entry of entries) {
13385
+ const plistContent = generatePlist(entry, ctx);
13386
+ const targetPath = plistPath(labelForEntry(entry), ctx.homedir);
13387
+ if (opts.dryRun) {
13388
+ console.log(import_picocolors8.default.cyan(`--- ${targetPath} ---`));
13389
+ console.log(plistContent);
13390
+ continue;
13391
+ }
13392
+ await writeFile6(targetPath, plistContent, "utf8");
13393
+ console.log(import_picocolors8.default.green(`\u2713 Wrote ${targetPath}`));
13394
+ }
13395
+ console.log(import_picocolors8.default.green(`
13396
+ Registered ${entries.length} schedule entries.`));
13397
+ console.log(import_picocolors8.default.dim("Use launchctl to load (or restart launchd):"));
13398
+ for (const entry of entries) {
13399
+ const target = plistPath(labelForEntry(entry), ctx.homedir);
13400
+ console.log(import_picocolors8.default.dim(` launchctl load ${target}`));
13401
+ }
13402
+ }
13403
+ async function readVaultConfig(vault) {
13404
+ const yamlPath = join11(vault, "vault.yml");
13405
+ if (!existsSync(yamlPath))
13406
+ return {};
13407
+ const raw = await readFile6(yamlPath, "utf8");
13408
+ return import_yaml7.parse(raw) ?? {};
13409
+ }
13410
+ function sanitizeArgsForOneShot(entry) {
13411
+ const values = isCommandMode(entry) ? entry.args ?? [] : Object.values(entry.args ?? {});
13412
+ for (const v2 of values) {
13413
+ if (/["$`\\]/.test(v2)) {
13414
+ throw new Error(`Arg value must not contain shell-special chars (", $, \`, \\): ${v2}`);
13415
+ }
13416
+ }
13417
+ }
13418
+ async function validateSchedulable(vault, entry) {
13419
+ if (!entry.skill) {
13420
+ throw new Error("validateSchedulable invoked on non-skill entry \u2014 caller bug");
13421
+ }
13422
+ const skillName = entry.skill.replace(/^\//, "");
13423
+ const skillPath = join11(vault, ".claude/plugins/onebrain/skills", skillName, "SKILL.md");
13424
+ if (!existsSync(skillPath)) {
13425
+ throw new Error(`Skill ${entry.skill} not found at ${skillPath}`);
13426
+ }
13427
+ const raw = await readFile6(skillPath, "utf8");
13428
+ const match = raw.match(/^---\n([\s\S]*?)\n---/);
13429
+ if (!match) {
13430
+ throw new Error(`Skill ${entry.skill} has no YAML frontmatter`);
13431
+ }
13432
+ const fm = import_yaml7.parse(match[1]);
13433
+ if (fm.schedulable === false) {
13434
+ throw new Error(`Skill ${entry.skill} requires user input \u2014 cannot schedule`);
13435
+ }
13436
+ if (fm.schedulable_with_args) {
13437
+ const required = fm.required_args ?? [];
13438
+ const provided = Object.keys(entry.args ?? {});
13439
+ const missing = required.filter((r2) => !provided.includes(r2));
13440
+ if (missing.length > 0) {
13441
+ throw new Error(`Skill ${entry.skill} requires args: [${missing.join(", ")}]`);
13442
+ }
13443
+ } else if (!fm.schedulable) {
13444
+ throw new Error(`Skill ${entry.skill} does not declare schedulable: true in frontmatter`);
13445
+ }
13446
+ if (entry.args) {
13447
+ for (const [k2, v2] of Object.entries(entry.args)) {
13448
+ if (/["$`\\]/.test(v2)) {
13449
+ throw new Error(`Arg "${k2}" value must not contain shell-special chars (", $, \`, \\): ${v2}`);
13450
+ }
13451
+ }
13452
+ }
13453
+ }
13454
+ async function removeAll(vault) {
13455
+ const config = await readVaultConfig(vault);
13456
+ const entries = config.schedule ?? [];
13457
+ for (const entry of entries) {
13458
+ const target = plistPath(labelForEntry(entry), homedir4());
13459
+ if (existsSync(target)) {
13460
+ await unlink4(target);
13461
+ console.log(import_picocolors8.default.green(`\u2713 Removed ${target}`));
13462
+ }
13463
+ }
13464
+ }
13465
+ async function printStatus(vault) {
13466
+ const config = await readVaultConfig(vault);
13467
+ const entries = config.schedule ?? [];
13468
+ console.log(import_picocolors8.default.cyan(`Registered schedules: ${entries.length}`));
13469
+ for (const entry of entries) {
13470
+ const target = plistPath(labelForEntry(entry), homedir4());
13471
+ const installed = existsSync(target) ? "\u2713" : "\u2717";
13472
+ const when = entry.at ?? entry.cron ?? "?";
13473
+ const tag = entry.at ? import_picocolors8.default.magenta("[once]") : import_picocolors8.default.dim("[cron]");
13474
+ let targetLabel;
13475
+ if (isCommandMode(entry)) {
13476
+ const argv = entry.args ?? [];
13477
+ const argStr = argv.length ? ` ${argv.join(" ")}` : "";
13478
+ targetLabel = `${import_picocolors8.default.yellow("cmd:")} ${entry.command}${argStr}`;
13479
+ } else {
13480
+ const argsMap = entry.args ?? {};
13481
+ const argStr = Object.keys(argsMap).length ? ` (${Object.entries(argsMap).map(([k2, v2]) => `${k2}=${v2}`).join(", ")})` : "";
13482
+ targetLabel = `${import_picocolors8.default.green("skill:")} ${entry.skill}${argStr}`;
13483
+ }
13484
+ console.log(` ${installed} ${tag} ${when} ${targetLabel}`);
13485
+ }
13486
+ }
13487
+ async function testRun(vault, skill) {
13488
+ console.log(import_picocolors8.default.cyan(`Testing scheduled invocation of ${skill}...`));
13489
+ console.log(import_picocolors8.default.dim("(Spawns headless Claude Code. Output streams here.)"));
13490
+ const { spawn } = await import("child_process");
13491
+ const child = spawn("claude", ["--vault", vault, "--skill", skill, "--headless"], {
13492
+ stdio: "inherit"
13493
+ });
13494
+ await new Promise((resolve) => child.on("exit", resolve));
13495
+ }
13496
+ async function resumeSkill(vault, skill) {
13497
+ const marker = join11(vault, "07-logs/scheduler/.paused", `${skill.replace(/^\//, "")}.txt`);
13498
+ if (existsSync(marker)) {
13499
+ await unlink4(marker);
13500
+ console.log(import_picocolors8.default.green(`\u2713 Resumed ${skill}`));
13501
+ } else {
13502
+ console.log(import_picocolors8.default.yellow(`${skill} is not paused.`));
13503
+ }
13504
+ }
13505
+
13506
+ // src/commands/update.ts
13507
+ var import_picocolors9 = __toESM(require_picocolors(), 1);
13126
13508
  init_cli_ui();
13127
13509
  var GITHUB_REPO = "https://api.github.com/repos/onebrain-ai/onebrain";
13128
13510
  var GITHUB_RELEASES_URL = `${GITHUB_REPO}/releases/latest`;
@@ -13217,7 +13599,7 @@ async function runUpdate(opts = {}) {
13217
13599
  const sp1 = createStep("\uD83D\uDD0D", "Local version");
13218
13600
  const { version: currentVersion, publishedAt: localPublishedAt } = await currentVersionFn();
13219
13601
  result.currentVersion = currentVersion;
13220
- const localVersionLabel = localPublishedAt ? `${import_picocolors8.default.dim(currentVersion)} ${import_picocolors8.default.dim("\xB7")} ${import_picocolors8.default.dim(formatReleaseDate(localPublishedAt))}` : import_picocolors8.default.dim(currentVersion);
13602
+ const localVersionLabel = localPublishedAt ? `${import_picocolors9.default.dim(currentVersion)} ${import_picocolors9.default.dim("\xB7")} ${import_picocolors9.default.dim(formatReleaseDate(localPublishedAt))}` : import_picocolors9.default.dim(currentVersion);
13221
13603
  if (sp1)
13222
13604
  sp1.stop(localVersionLabel);
13223
13605
  else
@@ -13229,9 +13611,9 @@ async function runUpdate(opts = {}) {
13229
13611
  const release = await fetchLatestRelease(fetchFn);
13230
13612
  latestVersion = release.version;
13231
13613
  publishedAt = release.publishedAt;
13232
- const dateSuffix = publishedAt ? ` ${import_picocolors8.default.dim("\xB7")} ${import_picocolors8.default.dim(formatReleaseDate(publishedAt))}` : "";
13614
+ const dateSuffix = publishedAt ? ` ${import_picocolors9.default.dim("\xB7")} ${import_picocolors9.default.dim(formatReleaseDate(publishedAt))}` : "";
13233
13615
  if (sp2)
13234
- sp2.stop(`${import_picocolors8.default.green(latestVersion)}${dateSuffix}`);
13616
+ sp2.stop(`${import_picocolors9.default.green(latestVersion)}${dateSuffix}`);
13235
13617
  else
13236
13618
  writeLine(`latest: ${latestVersion}`);
13237
13619
  } catch (err) {
@@ -13252,7 +13634,7 @@ async function runUpdate(opts = {}) {
13252
13634
  if (check) {
13253
13635
  if (isTTY) {
13254
13636
  if (currentVersion !== latestVersion) {
13255
- barLine(`\u2B06\uFE0F ${import_picocolors8.default.dim(currentVersion)} \u2192 ${import_picocolors8.default.green(latestVersion)} \xB7 binary would upgrade`);
13637
+ barLine(`\u2B06\uFE0F ${import_picocolors9.default.dim(currentVersion)} \u2192 ${import_picocolors9.default.green(latestVersion)} \xB7 binary would upgrade`);
13256
13638
  barBlank();
13257
13639
  }
13258
13640
  close("Dry run complete \u2014 no changes made");
@@ -13265,7 +13647,7 @@ async function runUpdate(opts = {}) {
13265
13647
  }
13266
13648
  if (latestVersion === currentVersion) {
13267
13649
  if (isTTY) {
13268
- close(`Already up to date \u2014 @onebrain-ai/cli ${import_picocolors8.default.dim(latestVersion)}`);
13650
+ close(`Already up to date \u2014 @onebrain-ai/cli ${import_picocolors9.default.dim(latestVersion)}`);
13269
13651
  } else {
13270
13652
  writeLine(`already up to date: @onebrain-ai/cli ${latestVersion}`);
13271
13653
  writeLine("done: nothing to do");
@@ -13275,14 +13657,14 @@ async function runUpdate(opts = {}) {
13275
13657
  return result;
13276
13658
  }
13277
13659
  if (isTTY) {
13278
- barLine(`\u2B06\uFE0F ${import_picocolors8.default.dim(currentVersion)} \u2192 ${import_picocolors8.default.green(latestVersion)}`);
13660
+ barLine(`\u2B06\uFE0F ${import_picocolors9.default.dim(currentVersion)} \u2192 ${import_picocolors9.default.green(latestVersion)}`);
13279
13661
  barBlank();
13280
13662
  }
13281
13663
  const sp3 = createStep("\uD83D\uDCE6", "Installing @onebrain-ai/cli");
13282
13664
  try {
13283
13665
  await installBinaryFn(latestVersion);
13284
13666
  if (sp3)
13285
- sp3.stop(import_picocolors8.default.green(latestVersion));
13667
+ sp3.stop(import_picocolors9.default.green(latestVersion));
13286
13668
  else
13287
13669
  writeLine(`upgrading: @onebrain-ai/cli ${latestVersion} installed`);
13288
13670
  } catch (err) {
@@ -13319,7 +13701,7 @@ async function runUpdate(opts = {}) {
13319
13701
  result.ok = true;
13320
13702
  result.exitCode = 0;
13321
13703
  if (isTTY) {
13322
- close(`Done \u2014 run ${import_picocolors8.default.cyan("/update")} in Claude to sync vault files`);
13704
+ close(`Done \u2014 run ${import_picocolors9.default.cyan("/update")} in Claude to sync vault files`);
13323
13705
  } else {
13324
13706
  writeLine("done: run /update in Claude to sync vault files");
13325
13707
  }
@@ -13346,7 +13728,7 @@ function patchUtf8(stream) {
13346
13728
  }
13347
13729
 
13348
13730
  // src/index.ts
13349
- var VERSION = "2.2.5";
13731
+ var VERSION = "2.3.1";
13350
13732
  var RELEASE_DATE = "2026-05-12";
13351
13733
  patchUtf8(process.stdout);
13352
13734
  patchUtf8(process.stderr);
@@ -13361,7 +13743,7 @@ function findVaultRoot(startDir) {
13361
13743
  return process.cwd();
13362
13744
  let dir = startDir;
13363
13745
  while (true) {
13364
- if (existsSync(join11(dir, "vault.yml")))
13746
+ if (existsSync2(join12(dir, "vault.yml")))
13365
13747
  return dir;
13366
13748
  const parent = dirname4(dir);
13367
13749
  if (parent === dir)
@@ -13389,6 +13771,9 @@ program2.command("doctor").description("Run vault health checks and report issue
13389
13771
  ...opts.fix !== undefined ? { fix: opts.fix } : {}
13390
13772
  });
13391
13773
  });
13774
+ program2.command("register-schedule").description("Register OneBrain scheduled skills with the OS scheduler (macOS launchd)").option("--vault <path>", "Vault path", process.cwd()).option("--dry-run", "Print plist without writing").option("--remove", "Remove all OneBrain schedule entries").option("--refresh", "Re-emit plists with current vault path").option("--resume <skill>", "Resume an auto-paused skill").option("--status", "Show registered schedules + recent run status").option("--test <skill>", "Manually invoke a scheduled skill once").action(async (opts) => {
13775
+ await registerSchedule(opts);
13776
+ });
13392
13777
  program2.command("help").description("Show this help message").action(() => {
13393
13778
  program2.help();
13394
13779
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebrain-ai/cli",
3
- "version": "2.2.5",
3
+ "version": "2.3.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",