@onebrain-ai/cli 2.2.4 → 2.3.0
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 +66 -6
- package/dist/onebrain +378 -68
- 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,
|
|
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
|
|
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) |
|
|
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** —
|
|
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
|
-
| ⚡ | **
|
|
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
|
-
## 📋
|
|
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,62 @@ 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
|
+
CLI flags:
|
|
532
|
+
|
|
533
|
+
| Flag | Purpose |
|
|
534
|
+
|---|---|
|
|
535
|
+
| `--dry-run` | Print plist without writing |
|
|
536
|
+
| `--remove` | Remove all OneBrain schedules |
|
|
537
|
+
| `--refresh` | Re-emit plists after vault move |
|
|
538
|
+
| `--resume <skill>` | Resume an auto-paused skill |
|
|
539
|
+
| `--status` | Show registered schedules + run history |
|
|
540
|
+
| `--test <skill>` | Manually invoke a scheduled skill once |
|
|
541
|
+
|
|
542
|
+
**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.
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
486
546
|
<details>
|
|
487
547
|
<summary><strong>⚙️ Prerequisites & Detailed Setup</strong></summary>
|
|
488
548
|
<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.
|
|
9563
|
+
version: "2.3.0",
|
|
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",
|
|
@@ -10015,23 +10015,30 @@ async function pathExists(p2) {
|
|
|
10015
10015
|
return false;
|
|
10016
10016
|
}
|
|
10017
10017
|
}
|
|
10018
|
-
async function
|
|
10018
|
+
async function detectHarnesses(vaultRoot) {
|
|
10019
10019
|
const env = process.env["ONEBRAIN_HARNESS"];
|
|
10020
10020
|
if (env) {
|
|
10021
10021
|
if (env === "claude" || env === "claude-code")
|
|
10022
|
-
return "claude";
|
|
10022
|
+
return ["claude"];
|
|
10023
10023
|
if (env === "gemini")
|
|
10024
|
-
return "gemini";
|
|
10024
|
+
return ["gemini"];
|
|
10025
10025
|
if (env === "direct")
|
|
10026
|
-
return "direct";
|
|
10026
|
+
return ["direct"];
|
|
10027
10027
|
process.stderr.write(`harness: unknown ONEBRAIN_HARNESS value "${env}" \u2014 ignoring, falling back to directory detection
|
|
10028
10028
|
`);
|
|
10029
10029
|
}
|
|
10030
|
-
|
|
10031
|
-
return "gemini";
|
|
10030
|
+
const detected = [];
|
|
10032
10031
|
if (await pathExists(join3(vaultRoot, ".claude")))
|
|
10033
|
-
|
|
10034
|
-
|
|
10032
|
+
detected.push("claude");
|
|
10033
|
+
if (await pathExists(join3(vaultRoot, ".gemini")))
|
|
10034
|
+
detected.push("gemini");
|
|
10035
|
+
if (detected.length === 0)
|
|
10036
|
+
return ["direct"];
|
|
10037
|
+
return detected;
|
|
10038
|
+
}
|
|
10039
|
+
async function detectHarness(vaultRoot) {
|
|
10040
|
+
const [first] = await detectHarnesses(vaultRoot);
|
|
10041
|
+
return first ?? "direct";
|
|
10035
10042
|
}
|
|
10036
10043
|
var init_harness = () => {};
|
|
10037
10044
|
|
|
@@ -10282,7 +10289,7 @@ ${PATH_EXPORT}
|
|
|
10282
10289
|
async function runRegisterHooks(opts = {}) {
|
|
10283
10290
|
const vaultRoot = opts.vaultDir ?? process.cwd();
|
|
10284
10291
|
const isTTY = opts.isTTY ?? process.stdout.isTTY ?? false;
|
|
10285
|
-
const
|
|
10292
|
+
const harnesses = await detectHarnesses(vaultRoot);
|
|
10286
10293
|
let qmdCollection;
|
|
10287
10294
|
try {
|
|
10288
10295
|
const vaultConfig = await loadVaultConfig(vaultRoot);
|
|
@@ -10308,52 +10315,53 @@ async function runRegisterHooks(opts = {}) {
|
|
|
10308
10315
|
let hooksSpinner = null;
|
|
10309
10316
|
let permSpinner = null;
|
|
10310
10317
|
try {
|
|
10311
|
-
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
|
|
10317
|
-
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10318
|
+
for (const harness of harnesses) {
|
|
10319
|
+
if (harness === "claude") {
|
|
10320
|
+
hooksSpinner = isTTY ? L2() : null;
|
|
10321
|
+
hooksSpinner?.start("Registering hooks...");
|
|
10322
|
+
const settings = await readSettings(settingsPath);
|
|
10323
|
+
result.hooks = applyHooks(settings);
|
|
10324
|
+
let qmdStatus;
|
|
10325
|
+
if (qmdCollection) {
|
|
10326
|
+
qmdStatus = applyQmdHook(settings);
|
|
10327
|
+
} else {
|
|
10328
|
+
const groups = settings.hooks?.["PostToolUse"] ?? [];
|
|
10329
|
+
const stripped = migrateLegacyQmdEntries(groups, false);
|
|
10330
|
+
if (stripped && groups.length === 0 && settings.hooks) {
|
|
10331
|
+
delete settings.hooks["PostToolUse"];
|
|
10332
|
+
}
|
|
10324
10333
|
}
|
|
10334
|
+
if (isTTY) {
|
|
10335
|
+
const parts = HOOK_EVENTS.map((e2) => {
|
|
10336
|
+
const status = result.hooks[e2];
|
|
10337
|
+
const icon = import_picocolors4.default.green(status === "ok" ? "\u2713" : status === "migrated" ? "\u2191" : "+");
|
|
10338
|
+
return `${import_picocolors4.default.dim(e2)} ${icon}`;
|
|
10339
|
+
});
|
|
10340
|
+
if (qmdStatus) {
|
|
10341
|
+
const qmdIcon = qmdStatus === "ok" ? "\u2713" : qmdStatus === "migrated" ? "\u2191" : "+";
|
|
10342
|
+
parts.push(`${import_picocolors4.default.dim("PostToolUse")} ${import_picocolors4.default.green(qmdIcon)}`);
|
|
10343
|
+
}
|
|
10344
|
+
hooksSpinner?.stop(`Hooks ${parts.join(" ")}`);
|
|
10345
|
+
} else {
|
|
10346
|
+
const hookLine = HOOK_EVENTS.map((e2) => {
|
|
10347
|
+
const status = result.hooks[e2] ?? "ok";
|
|
10348
|
+
return `${e2} ${status}`;
|
|
10349
|
+
}).join(" ");
|
|
10350
|
+
note(hookLine);
|
|
10351
|
+
if (qmdStatus)
|
|
10352
|
+
note(`PostToolUse ${qmdStatus}`);
|
|
10353
|
+
}
|
|
10354
|
+
permSpinner = isTTY ? L2() : null;
|
|
10355
|
+
permSpinner?.start("Updating permissions...");
|
|
10356
|
+
result.permissionsAdded = applyPermissions(settings);
|
|
10357
|
+
await writeSettings(settingsPath, settings);
|
|
10358
|
+
permSpinner?.stop("Permissions ok");
|
|
10359
|
+
if (!isTTY)
|
|
10360
|
+
note("permissions ok");
|
|
10361
|
+
}
|
|
10362
|
+
if (harness === "direct") {
|
|
10363
|
+
await registerDirectPath();
|
|
10325
10364
|
}
|
|
10326
|
-
if (isTTY) {
|
|
10327
|
-
const parts = HOOK_EVENTS.map((e2) => {
|
|
10328
|
-
const status = result.hooks[e2];
|
|
10329
|
-
const icon = import_picocolors4.default.green(status === "ok" ? "\u2713" : status === "migrated" ? "\u2191" : "+");
|
|
10330
|
-
return `${import_picocolors4.default.dim(e2)} ${icon}`;
|
|
10331
|
-
});
|
|
10332
|
-
if (qmdStatus) {
|
|
10333
|
-
const qmdIcon = qmdStatus === "ok" ? "\u2713" : qmdStatus === "migrated" ? "\u2191" : "+";
|
|
10334
|
-
parts.push(`${import_picocolors4.default.dim("PostToolUse")} ${import_picocolors4.default.green(qmdIcon)}`);
|
|
10335
|
-
}
|
|
10336
|
-
hooksSpinner?.stop(`Hooks ${parts.join(" ")}`);
|
|
10337
|
-
} else {
|
|
10338
|
-
const hookLine = HOOK_EVENTS.map((e2) => {
|
|
10339
|
-
const status = result.hooks[e2];
|
|
10340
|
-
const label = status === "ok" || status === "added" || status === "migrated" ? "ok" : status ?? "ok";
|
|
10341
|
-
return `${e2} ${label}`;
|
|
10342
|
-
}).join(" ");
|
|
10343
|
-
note(hookLine);
|
|
10344
|
-
if (qmdStatus)
|
|
10345
|
-
note(`PostToolUse ${qmdStatus}`);
|
|
10346
|
-
}
|
|
10347
|
-
permSpinner = isTTY ? L2() : null;
|
|
10348
|
-
permSpinner?.start("Updating permissions...");
|
|
10349
|
-
result.permissionsAdded = applyPermissions(settings);
|
|
10350
|
-
await writeSettings(settingsPath, settings);
|
|
10351
|
-
permSpinner?.stop("Permissions ok");
|
|
10352
|
-
if (!isTTY)
|
|
10353
|
-
note("permissions ok");
|
|
10354
|
-
}
|
|
10355
|
-
if (harness === "direct") {
|
|
10356
|
-
await registerDirectPath();
|
|
10357
10365
|
}
|
|
10358
10366
|
result.ok = true;
|
|
10359
10367
|
if (!isTTY) {
|
|
@@ -11013,8 +11021,8 @@ var init_vault_sync = __esm(() => {
|
|
|
11013
11021
|
});
|
|
11014
11022
|
|
|
11015
11023
|
// src/index.ts
|
|
11016
|
-
import { existsSync } from "fs";
|
|
11017
|
-
import { dirname as dirname4, join as
|
|
11024
|
+
import { existsSync as existsSync2 } from "fs";
|
|
11025
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
11018
11026
|
|
|
11019
11027
|
// node_modules/commander/esm.mjs
|
|
11020
11028
|
var import__ = __toESM(require_commander(), 1);
|
|
@@ -11040,7 +11048,7 @@ var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
|
11040
11048
|
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
11041
11049
|
function resolveBinaryVersion() {
|
|
11042
11050
|
if (true)
|
|
11043
|
-
return "2.
|
|
11051
|
+
return "2.3.0";
|
|
11044
11052
|
try {
|
|
11045
11053
|
const pkg = require_package();
|
|
11046
11054
|
return pkg.version ?? "dev";
|
|
@@ -13113,8 +13121,307 @@ async function sessionInitCommand(vaultRoot) {
|
|
|
13113
13121
|
// src/index.ts
|
|
13114
13122
|
init_vault_sync();
|
|
13115
13123
|
|
|
13116
|
-
// src/commands/
|
|
13124
|
+
// src/commands/register-schedule.ts
|
|
13117
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 validateEntry(entry) {
|
|
13201
|
+
const hasCron = entry.cron !== undefined;
|
|
13202
|
+
const hasAt = entry.at !== undefined;
|
|
13203
|
+
if (hasCron === hasAt) {
|
|
13204
|
+
return { valid: false, reason: "entry must have exactly one of `cron` or `at`" };
|
|
13205
|
+
}
|
|
13206
|
+
if (!entry.skill)
|
|
13207
|
+
return { valid: false, reason: "entry.skill is required" };
|
|
13208
|
+
return { valid: true };
|
|
13209
|
+
}
|
|
13210
|
+
|
|
13211
|
+
// src/lib/scheduler/launchd.ts
|
|
13212
|
+
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13213
|
+
function generatePlist(entry, ctx) {
|
|
13214
|
+
const labelSafe = entry.skill.replace(/^\//, "").replace(/[^a-zA-Z0-9-]/g, "-");
|
|
13215
|
+
const label = `com.onebrain.${labelSafe}`;
|
|
13216
|
+
let programArgumentsBlock;
|
|
13217
|
+
let calendarXml;
|
|
13218
|
+
if (entry.at !== undefined) {
|
|
13219
|
+
const calendar = atToLaunchd(entry.at);
|
|
13220
|
+
calendarXml = Object.entries(calendar).map(([k2, v2]) => ` <key>${k2}</key>
|
|
13221
|
+
<integer>${v2}</integer>`).join(`
|
|
13222
|
+
`);
|
|
13223
|
+
const plistFilePath = plistPath(entry.skill, ctx.homedir);
|
|
13224
|
+
const argsFlags = entry.args ? ` ${Object.entries(entry.args).map(([k2, v2]) => `--${k2}="${v2}"`).join(" ")}` : "";
|
|
13225
|
+
const shellLine = xmlEscape(`"${ctx.skillCliPath}" --vault="${ctx.vaultPath}" --skill="${entry.skill}" --headless${argsFlags}; launchctl bootout gui/${ctx.uid}/${label}; rm -f "${plistFilePath}"`);
|
|
13226
|
+
programArgumentsBlock = ` <string>/bin/sh</string>
|
|
13227
|
+
<string>-c</string>
|
|
13228
|
+
<string>${shellLine}</string>`;
|
|
13229
|
+
} else {
|
|
13230
|
+
const calendar = cronFieldsToLaunchd(entry.cron);
|
|
13231
|
+
calendarXml = Object.entries(calendar).map(([k2, v2]) => ` <key>${k2}</key>
|
|
13232
|
+
<integer>${v2}</integer>`).join(`
|
|
13233
|
+
`);
|
|
13234
|
+
const argsBlock = entry.args ? `
|
|
13235
|
+
${Object.entries(entry.args).map(([k2, v2]) => ` <string>--${xmlEscape(k2)}=${xmlEscape(v2)}</string>`).join(`
|
|
13236
|
+
`)}` : "";
|
|
13237
|
+
programArgumentsBlock = ` <string>${xmlEscape(ctx.skillCliPath)}</string>
|
|
13238
|
+
<string>--vault</string>
|
|
13239
|
+
<string>${xmlEscape(ctx.vaultPath)}</string>
|
|
13240
|
+
<string>--skill</string>
|
|
13241
|
+
<string>${xmlEscape(entry.skill)}</string>
|
|
13242
|
+
<string>--headless</string>${argsBlock}`;
|
|
13243
|
+
}
|
|
13244
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
13245
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
13246
|
+
<plist version="1.0">
|
|
13247
|
+
<dict>
|
|
13248
|
+
<key>Label</key>
|
|
13249
|
+
<string>${xmlEscape(label)}</string>
|
|
13250
|
+
<key>ProgramArguments</key>
|
|
13251
|
+
<array>
|
|
13252
|
+
${programArgumentsBlock}
|
|
13253
|
+
</array>
|
|
13254
|
+
<key>StartCalendarInterval</key>
|
|
13255
|
+
<dict>
|
|
13256
|
+
${calendarXml}
|
|
13257
|
+
</dict>
|
|
13258
|
+
<key>StandardOutPath</key>
|
|
13259
|
+
<string>${xmlEscape(ctx.logBasePath)}/onebrain-${labelSafe}.stdout</string>
|
|
13260
|
+
<key>StandardErrorPath</key>
|
|
13261
|
+
<string>${xmlEscape(ctx.logBasePath)}/onebrain-${labelSafe}.stderr</string>
|
|
13262
|
+
<key>RunAtLoad</key>
|
|
13263
|
+
<false/>
|
|
13264
|
+
</dict>
|
|
13265
|
+
</plist>`;
|
|
13266
|
+
}
|
|
13267
|
+
function plistPath(skill, homedir4) {
|
|
13268
|
+
const labelSafe = skill.replace(/^\//, "").replace(/[^a-zA-Z0-9-]/g, "-");
|
|
13269
|
+
return `${homedir4}/Library/LaunchAgents/com.onebrain.${labelSafe}.plist`;
|
|
13270
|
+
}
|
|
13271
|
+
|
|
13272
|
+
// src/commands/register-schedule.ts
|
|
13273
|
+
async function registerSchedule(opts) {
|
|
13274
|
+
if (opts.remove)
|
|
13275
|
+
return await removeAll(opts.vault);
|
|
13276
|
+
if (opts.status)
|
|
13277
|
+
return await printStatus(opts.vault);
|
|
13278
|
+
if (opts.test)
|
|
13279
|
+
return await testRun(opts.vault, opts.test);
|
|
13280
|
+
if (opts.resume)
|
|
13281
|
+
return await resumeSkill(opts.vault, opts.resume);
|
|
13282
|
+
if (opts.refresh) {
|
|
13283
|
+
console.log(import_picocolors8.default.dim("(--refresh: re-emitting plists with current vault path)"));
|
|
13284
|
+
}
|
|
13285
|
+
const config = await readVaultConfig(opts.vault);
|
|
13286
|
+
const entries = config.schedule ?? [];
|
|
13287
|
+
if (entries.length === 0) {
|
|
13288
|
+
console.log(import_picocolors8.default.yellow("No schedule entries in vault.yml. Nothing to register."));
|
|
13289
|
+
return;
|
|
13290
|
+
}
|
|
13291
|
+
for (const entry of entries) {
|
|
13292
|
+
const ve = validateEntry(entry);
|
|
13293
|
+
if (!ve.valid)
|
|
13294
|
+
throw new Error(`Invalid schedule entry: ${ve.reason}`);
|
|
13295
|
+
if (isOneShot(entry)) {
|
|
13296
|
+
const va = validateAt(entry.at);
|
|
13297
|
+
if (!va.valid)
|
|
13298
|
+
throw new Error(`Invalid at "${entry.at}": ${va.reason}`);
|
|
13299
|
+
} else if (entry.cron !== undefined) {
|
|
13300
|
+
const vc = validateCron(entry.cron);
|
|
13301
|
+
if (!vc.valid)
|
|
13302
|
+
throw new Error(`Invalid cron "${entry.cron}": ${vc.reason}`);
|
|
13303
|
+
}
|
|
13304
|
+
await validateSchedulable(opts.vault, entry);
|
|
13305
|
+
}
|
|
13306
|
+
const skillCliPath = process.argv[1] ?? "onebrain";
|
|
13307
|
+
const ctx = {
|
|
13308
|
+
vaultPath: opts.vault,
|
|
13309
|
+
skillCliPath,
|
|
13310
|
+
logBasePath: join11(opts.vault, "07-logs/scheduler"),
|
|
13311
|
+
uid: process.getuid?.() ?? 501,
|
|
13312
|
+
homedir: homedir4()
|
|
13313
|
+
};
|
|
13314
|
+
const seen = new Map;
|
|
13315
|
+
for (const entry of entries) {
|
|
13316
|
+
const target = plistPath(entry.skill, ctx.homedir);
|
|
13317
|
+
if (seen.has(target)) {
|
|
13318
|
+
throw new Error(`Conflict: ${entry.skill} and ${seen.get(target)?.skill} normalize to the same plist path ${target}`);
|
|
13319
|
+
}
|
|
13320
|
+
seen.set(target, entry);
|
|
13321
|
+
}
|
|
13322
|
+
for (const entry of entries) {
|
|
13323
|
+
const plistContent = generatePlist(entry, ctx);
|
|
13324
|
+
const targetPath = plistPath(entry.skill, ctx.homedir);
|
|
13325
|
+
if (opts.dryRun) {
|
|
13326
|
+
console.log(import_picocolors8.default.cyan(`--- ${targetPath} ---`));
|
|
13327
|
+
console.log(plistContent);
|
|
13328
|
+
continue;
|
|
13329
|
+
}
|
|
13330
|
+
await writeFile6(targetPath, plistContent, "utf8");
|
|
13331
|
+
console.log(import_picocolors8.default.green(`\u2713 Wrote ${targetPath}`));
|
|
13332
|
+
}
|
|
13333
|
+
console.log(import_picocolors8.default.green(`
|
|
13334
|
+
Registered ${entries.length} schedule entries.`));
|
|
13335
|
+
console.log(import_picocolors8.default.dim("Use launchctl to load (or restart launchd):"));
|
|
13336
|
+
for (const entry of entries) {
|
|
13337
|
+
const target = plistPath(entry.skill, ctx.homedir);
|
|
13338
|
+
console.log(import_picocolors8.default.dim(` launchctl load ${target}`));
|
|
13339
|
+
}
|
|
13340
|
+
}
|
|
13341
|
+
async function readVaultConfig(vault) {
|
|
13342
|
+
const yamlPath = join11(vault, "vault.yml");
|
|
13343
|
+
if (!existsSync(yamlPath))
|
|
13344
|
+
return {};
|
|
13345
|
+
const raw = await readFile6(yamlPath, "utf8");
|
|
13346
|
+
return import_yaml7.parse(raw) ?? {};
|
|
13347
|
+
}
|
|
13348
|
+
async function validateSchedulable(vault, entry) {
|
|
13349
|
+
const skillName = entry.skill.replace(/^\//, "");
|
|
13350
|
+
const skillPath = join11(vault, ".claude/plugins/onebrain/skills", skillName, "SKILL.md");
|
|
13351
|
+
if (!existsSync(skillPath)) {
|
|
13352
|
+
throw new Error(`Skill ${entry.skill} not found at ${skillPath}`);
|
|
13353
|
+
}
|
|
13354
|
+
const raw = await readFile6(skillPath, "utf8");
|
|
13355
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
13356
|
+
if (!match) {
|
|
13357
|
+
throw new Error(`Skill ${entry.skill} has no YAML frontmatter`);
|
|
13358
|
+
}
|
|
13359
|
+
const fm = import_yaml7.parse(match[1]);
|
|
13360
|
+
if (fm.schedulable === false) {
|
|
13361
|
+
throw new Error(`Skill ${entry.skill} requires user input \u2014 cannot schedule`);
|
|
13362
|
+
}
|
|
13363
|
+
if (fm.schedulable_with_args) {
|
|
13364
|
+
const required = fm.required_args ?? [];
|
|
13365
|
+
const provided = Object.keys(entry.args ?? {});
|
|
13366
|
+
const missing = required.filter((r2) => !provided.includes(r2));
|
|
13367
|
+
if (missing.length > 0) {
|
|
13368
|
+
throw new Error(`Skill ${entry.skill} requires args: [${missing.join(", ")}]`);
|
|
13369
|
+
}
|
|
13370
|
+
} else if (!fm.schedulable) {
|
|
13371
|
+
throw new Error(`Skill ${entry.skill} does not declare schedulable: true in frontmatter`);
|
|
13372
|
+
}
|
|
13373
|
+
if (entry.args) {
|
|
13374
|
+
for (const [k2, v2] of Object.entries(entry.args)) {
|
|
13375
|
+
if (/["$`\\]/.test(v2)) {
|
|
13376
|
+
throw new Error(`Arg "${k2}" value must not contain shell-special chars (", $, backtick, \\): ${v2}`);
|
|
13377
|
+
}
|
|
13378
|
+
}
|
|
13379
|
+
}
|
|
13380
|
+
}
|
|
13381
|
+
async function removeAll(vault) {
|
|
13382
|
+
const config = await readVaultConfig(vault);
|
|
13383
|
+
const entries = config.schedule ?? [];
|
|
13384
|
+
for (const entry of entries) {
|
|
13385
|
+
const target = plistPath(entry.skill, homedir4());
|
|
13386
|
+
if (existsSync(target)) {
|
|
13387
|
+
await unlink4(target);
|
|
13388
|
+
console.log(import_picocolors8.default.green(`\u2713 Removed ${target}`));
|
|
13389
|
+
}
|
|
13390
|
+
}
|
|
13391
|
+
}
|
|
13392
|
+
async function printStatus(vault) {
|
|
13393
|
+
const config = await readVaultConfig(vault);
|
|
13394
|
+
const entries = config.schedule ?? [];
|
|
13395
|
+
console.log(import_picocolors8.default.cyan(`Registered schedules: ${entries.length}`));
|
|
13396
|
+
for (const entry of entries) {
|
|
13397
|
+
const target = plistPath(entry.skill, homedir4());
|
|
13398
|
+
const installed = existsSync(target) ? "\u2713" : "\u2717";
|
|
13399
|
+
const when = entry.at ?? entry.cron ?? "?";
|
|
13400
|
+
const tag = entry.at ? import_picocolors8.default.magenta("[once]") : import_picocolors8.default.dim("[cron]");
|
|
13401
|
+
console.log(` ${installed} ${tag} ${when} ${entry.skill}`);
|
|
13402
|
+
}
|
|
13403
|
+
}
|
|
13404
|
+
async function testRun(vault, skill) {
|
|
13405
|
+
console.log(import_picocolors8.default.cyan(`Testing scheduled invocation of ${skill}...`));
|
|
13406
|
+
console.log(import_picocolors8.default.dim("(Spawns headless Claude Code. Output streams here.)"));
|
|
13407
|
+
const { spawn } = await import("child_process");
|
|
13408
|
+
const child = spawn("claude", ["--vault", vault, "--skill", skill, "--headless"], {
|
|
13409
|
+
stdio: "inherit"
|
|
13410
|
+
});
|
|
13411
|
+
await new Promise((resolve) => child.on("exit", resolve));
|
|
13412
|
+
}
|
|
13413
|
+
async function resumeSkill(vault, skill) {
|
|
13414
|
+
const marker = join11(vault, "07-logs/scheduler/.paused", `${skill.replace(/^\//, "")}.txt`);
|
|
13415
|
+
if (existsSync(marker)) {
|
|
13416
|
+
await unlink4(marker);
|
|
13417
|
+
console.log(import_picocolors8.default.green(`\u2713 Resumed ${skill}`));
|
|
13418
|
+
} else {
|
|
13419
|
+
console.log(import_picocolors8.default.yellow(`${skill} is not paused.`));
|
|
13420
|
+
}
|
|
13421
|
+
}
|
|
13422
|
+
|
|
13423
|
+
// src/commands/update.ts
|
|
13424
|
+
var import_picocolors9 = __toESM(require_picocolors(), 1);
|
|
13118
13425
|
init_cli_ui();
|
|
13119
13426
|
var GITHUB_REPO = "https://api.github.com/repos/onebrain-ai/onebrain";
|
|
13120
13427
|
var GITHUB_RELEASES_URL = `${GITHUB_REPO}/releases/latest`;
|
|
@@ -13209,7 +13516,7 @@ async function runUpdate(opts = {}) {
|
|
|
13209
13516
|
const sp1 = createStep("\uD83D\uDD0D", "Local version");
|
|
13210
13517
|
const { version: currentVersion, publishedAt: localPublishedAt } = await currentVersionFn();
|
|
13211
13518
|
result.currentVersion = currentVersion;
|
|
13212
|
-
const localVersionLabel = localPublishedAt ? `${
|
|
13519
|
+
const localVersionLabel = localPublishedAt ? `${import_picocolors9.default.dim(currentVersion)} ${import_picocolors9.default.dim("\xB7")} ${import_picocolors9.default.dim(formatReleaseDate(localPublishedAt))}` : import_picocolors9.default.dim(currentVersion);
|
|
13213
13520
|
if (sp1)
|
|
13214
13521
|
sp1.stop(localVersionLabel);
|
|
13215
13522
|
else
|
|
@@ -13221,9 +13528,9 @@ async function runUpdate(opts = {}) {
|
|
|
13221
13528
|
const release = await fetchLatestRelease(fetchFn);
|
|
13222
13529
|
latestVersion = release.version;
|
|
13223
13530
|
publishedAt = release.publishedAt;
|
|
13224
|
-
const dateSuffix = publishedAt ? ` ${
|
|
13531
|
+
const dateSuffix = publishedAt ? ` ${import_picocolors9.default.dim("\xB7")} ${import_picocolors9.default.dim(formatReleaseDate(publishedAt))}` : "";
|
|
13225
13532
|
if (sp2)
|
|
13226
|
-
sp2.stop(`${
|
|
13533
|
+
sp2.stop(`${import_picocolors9.default.green(latestVersion)}${dateSuffix}`);
|
|
13227
13534
|
else
|
|
13228
13535
|
writeLine(`latest: ${latestVersion}`);
|
|
13229
13536
|
} catch (err) {
|
|
@@ -13244,7 +13551,7 @@ async function runUpdate(opts = {}) {
|
|
|
13244
13551
|
if (check) {
|
|
13245
13552
|
if (isTTY) {
|
|
13246
13553
|
if (currentVersion !== latestVersion) {
|
|
13247
|
-
barLine(`\u2B06\uFE0F ${
|
|
13554
|
+
barLine(`\u2B06\uFE0F ${import_picocolors9.default.dim(currentVersion)} \u2192 ${import_picocolors9.default.green(latestVersion)} \xB7 binary would upgrade`);
|
|
13248
13555
|
barBlank();
|
|
13249
13556
|
}
|
|
13250
13557
|
close("Dry run complete \u2014 no changes made");
|
|
@@ -13257,7 +13564,7 @@ async function runUpdate(opts = {}) {
|
|
|
13257
13564
|
}
|
|
13258
13565
|
if (latestVersion === currentVersion) {
|
|
13259
13566
|
if (isTTY) {
|
|
13260
|
-
close(`Already up to date \u2014 @onebrain-ai/cli ${
|
|
13567
|
+
close(`Already up to date \u2014 @onebrain-ai/cli ${import_picocolors9.default.dim(latestVersion)}`);
|
|
13261
13568
|
} else {
|
|
13262
13569
|
writeLine(`already up to date: @onebrain-ai/cli ${latestVersion}`);
|
|
13263
13570
|
writeLine("done: nothing to do");
|
|
@@ -13267,14 +13574,14 @@ async function runUpdate(opts = {}) {
|
|
|
13267
13574
|
return result;
|
|
13268
13575
|
}
|
|
13269
13576
|
if (isTTY) {
|
|
13270
|
-
barLine(`\u2B06\uFE0F ${
|
|
13577
|
+
barLine(`\u2B06\uFE0F ${import_picocolors9.default.dim(currentVersion)} \u2192 ${import_picocolors9.default.green(latestVersion)}`);
|
|
13271
13578
|
barBlank();
|
|
13272
13579
|
}
|
|
13273
13580
|
const sp3 = createStep("\uD83D\uDCE6", "Installing @onebrain-ai/cli");
|
|
13274
13581
|
try {
|
|
13275
13582
|
await installBinaryFn(latestVersion);
|
|
13276
13583
|
if (sp3)
|
|
13277
|
-
sp3.stop(
|
|
13584
|
+
sp3.stop(import_picocolors9.default.green(latestVersion));
|
|
13278
13585
|
else
|
|
13279
13586
|
writeLine(`upgrading: @onebrain-ai/cli ${latestVersion} installed`);
|
|
13280
13587
|
} catch (err) {
|
|
@@ -13311,7 +13618,7 @@ async function runUpdate(opts = {}) {
|
|
|
13311
13618
|
result.ok = true;
|
|
13312
13619
|
result.exitCode = 0;
|
|
13313
13620
|
if (isTTY) {
|
|
13314
|
-
close(`Done \u2014 run ${
|
|
13621
|
+
close(`Done \u2014 run ${import_picocolors9.default.cyan("/update")} in Claude to sync vault files`);
|
|
13315
13622
|
} else {
|
|
13316
13623
|
writeLine("done: run /update in Claude to sync vault files");
|
|
13317
13624
|
}
|
|
@@ -13338,7 +13645,7 @@ function patchUtf8(stream) {
|
|
|
13338
13645
|
}
|
|
13339
13646
|
|
|
13340
13647
|
// src/index.ts
|
|
13341
|
-
var VERSION = "2.
|
|
13648
|
+
var VERSION = "2.3.0";
|
|
13342
13649
|
var RELEASE_DATE = "2026-05-12";
|
|
13343
13650
|
patchUtf8(process.stdout);
|
|
13344
13651
|
patchUtf8(process.stderr);
|
|
@@ -13353,7 +13660,7 @@ function findVaultRoot(startDir) {
|
|
|
13353
13660
|
return process.cwd();
|
|
13354
13661
|
let dir = startDir;
|
|
13355
13662
|
while (true) {
|
|
13356
|
-
if (
|
|
13663
|
+
if (existsSync2(join12(dir, "vault.yml")))
|
|
13357
13664
|
return dir;
|
|
13358
13665
|
const parent = dirname4(dir);
|
|
13359
13666
|
if (parent === dir)
|
|
@@ -13381,6 +13688,9 @@ program2.command("doctor").description("Run vault health checks and report issue
|
|
|
13381
13688
|
...opts.fix !== undefined ? { fix: opts.fix } : {}
|
|
13382
13689
|
});
|
|
13383
13690
|
});
|
|
13691
|
+
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) => {
|
|
13692
|
+
await registerSchedule(opts);
|
|
13693
|
+
});
|
|
13384
13694
|
program2.command("help").description("Show this help message").action(() => {
|
|
13385
13695
|
program2.help();
|
|
13386
13696
|
});
|