@onebrain-ai/cli 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -8
- package/dist/onebrain +150 -37
- 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>
|
|
@@ -528,6 +534,32 @@ Or use the interactive wizards from inside your vault:
|
|
|
528
534
|
|
|
529
535
|
Output goes to `[logs_folder]/scheduler/YYYY/MM/YYYY-MM-DD-{skill}.md` as readable markdown.
|
|
530
536
|
|
|
537
|
+
### Command mode (CLI binaries, hook-style)
|
|
538
|
+
|
|
539
|
+
For CLI maintenance tasks that aren't OneBrain skills, use the `command + args[]` shape:
|
|
540
|
+
|
|
541
|
+
```yaml
|
|
542
|
+
schedule:
|
|
543
|
+
- cron: "0 3 * * 0"
|
|
544
|
+
command: onebrain
|
|
545
|
+
args: [qmd-reindex]
|
|
546
|
+
- cron: "0 5 * * *"
|
|
547
|
+
command: rsync
|
|
548
|
+
args: [-av, /vault, /backup]
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
This matches the same shape Claude Code uses for `hooks` in `settings.json` — direct binary invocation with positional argv. No wrapper skill needed.
|
|
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
|
+
|
|
531
563
|
CLI flags:
|
|
532
564
|
|
|
533
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.2",
|
|
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",
|
|
@@ -11048,7 +11078,7 @@ var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
|
11048
11078
|
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
11049
11079
|
function resolveBinaryVersion() {
|
|
11050
11080
|
if (true)
|
|
11051
|
-
return "2.3.
|
|
11081
|
+
return "2.3.2";
|
|
11052
11082
|
try {
|
|
11053
11083
|
const pkg = require_package();
|
|
11054
11084
|
return pkg.version ?? "dev";
|
|
@@ -13197,40 +13227,94 @@ function atToLaunchd(at) {
|
|
|
13197
13227
|
function isOneShot(entry) {
|
|
13198
13228
|
return entry.at !== undefined;
|
|
13199
13229
|
}
|
|
13230
|
+
function isSkillMode(entry) {
|
|
13231
|
+
return entry.skill !== undefined;
|
|
13232
|
+
}
|
|
13233
|
+
function isCommandMode(entry) {
|
|
13234
|
+
return entry.command !== undefined;
|
|
13235
|
+
}
|
|
13200
13236
|
function validateEntry(entry) {
|
|
13201
13237
|
const hasCron = entry.cron !== undefined;
|
|
13202
13238
|
const hasAt = entry.at !== undefined;
|
|
13203
13239
|
if (hasCron === hasAt) {
|
|
13204
13240
|
return { valid: false, reason: "entry must have exactly one of `cron` or `at`" };
|
|
13205
13241
|
}
|
|
13206
|
-
|
|
13207
|
-
|
|
13242
|
+
const hasSkill = entry.skill !== undefined;
|
|
13243
|
+
const hasCommand = entry.command !== undefined;
|
|
13244
|
+
if (hasSkill === hasCommand) {
|
|
13245
|
+
return { valid: false, reason: "entry must have exactly one of `skill` or `command`" };
|
|
13246
|
+
}
|
|
13247
|
+
if (hasSkill && !entry.skill) {
|
|
13248
|
+
return { valid: false, reason: "entry.skill must not be empty" };
|
|
13249
|
+
}
|
|
13250
|
+
if (hasCommand && !entry.command) {
|
|
13251
|
+
return { valid: false, reason: "entry.command must not be empty" };
|
|
13252
|
+
}
|
|
13253
|
+
if (entry.args !== undefined) {
|
|
13254
|
+
const isArray = Array.isArray(entry.args);
|
|
13255
|
+
if (hasSkill && isArray) {
|
|
13256
|
+
return {
|
|
13257
|
+
valid: false,
|
|
13258
|
+
reason: "skill-mode entries require `args` as a map (Record<string, string>), not an array"
|
|
13259
|
+
};
|
|
13260
|
+
}
|
|
13261
|
+
if (hasCommand && !isArray) {
|
|
13262
|
+
return {
|
|
13263
|
+
valid: false,
|
|
13264
|
+
reason: "command-mode entries require `args` as a string array, not a map"
|
|
13265
|
+
};
|
|
13266
|
+
}
|
|
13267
|
+
if (isArray) {
|
|
13268
|
+
for (const v2 of entry.args) {
|
|
13269
|
+
if (typeof v2 !== "string") {
|
|
13270
|
+
return { valid: false, reason: "command-mode `args` must contain only strings" };
|
|
13271
|
+
}
|
|
13272
|
+
}
|
|
13273
|
+
}
|
|
13274
|
+
}
|
|
13208
13275
|
return { valid: true };
|
|
13209
13276
|
}
|
|
13210
13277
|
|
|
13211
13278
|
// src/lib/scheduler/launchd.ts
|
|
13212
13279
|
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13280
|
+
function labelForEntry(entry) {
|
|
13281
|
+
const raw = isCommandMode(entry) ? entry.command : (entry.skill ?? "").replace(/^\//, "");
|
|
13282
|
+
return raw.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
13283
|
+
}
|
|
13213
13284
|
function generatePlist(entry, ctx) {
|
|
13214
|
-
const labelSafe = entry
|
|
13285
|
+
const labelSafe = labelForEntry(entry);
|
|
13215
13286
|
const label = `com.onebrain.${labelSafe}`;
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
if (entry.at !== undefined) {
|
|
13219
|
-
const calendar = atToLaunchd(entry.at);
|
|
13220
|
-
calendarXml = Object.entries(calendar).map(([k2, v2]) => ` <key>${k2}</key>
|
|
13287
|
+
const calendar = isOneShot(entry) ? atToLaunchd(entry.at) : cronFieldsToLaunchd(entry.cron);
|
|
13288
|
+
const calendarXml = Object.entries(calendar).map(([k2, v2]) => ` <key>${k2}</key>
|
|
13221
13289
|
<integer>${v2}</integer>`).join(`
|
|
13222
13290
|
`);
|
|
13223
|
-
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
|
|
13291
|
+
let programArgumentsBlock;
|
|
13292
|
+
if (isOneShot(entry)) {
|
|
13293
|
+
if (isCommandMode(entry)) {
|
|
13294
|
+
const argv = entry.args ?? [];
|
|
13295
|
+
const quotedArgs = argv.map((a2) => `"${a2}"`).join(" ");
|
|
13296
|
+
const innerCommand = `"${entry.command}"${quotedArgs ? ` ${quotedArgs}` : ""}`;
|
|
13297
|
+
const plistFilePath = `${ctx.homedir}/Library/LaunchAgents/${label}.plist`;
|
|
13298
|
+
const shellLine = xmlEscape(`${innerCommand}; launchctl bootout gui/${ctx.uid}/${label}; rm -f "${plistFilePath}"`);
|
|
13299
|
+
programArgumentsBlock = ` <string>/bin/sh</string>
|
|
13227
13300
|
<string>-c</string>
|
|
13228
13301
|
<string>${shellLine}</string>`;
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13302
|
+
} else {
|
|
13303
|
+
const plistFilePath = plistPath(entry.skill ?? "", ctx.homedir);
|
|
13304
|
+
const argsFlags = entry.args ? ` ${Object.entries(entry.args).map(([k2, v2]) => `--${k2}="${v2}"`).join(" ")}` : "";
|
|
13305
|
+
const shellLine = xmlEscape(`"${ctx.skillCliPath}" --vault="${ctx.vaultPath}" --skill="${entry.skill}" --headless${argsFlags}; launchctl bootout gui/${ctx.uid}/${label}; rm -f "${plistFilePath}"`);
|
|
13306
|
+
programArgumentsBlock = ` <string>/bin/sh</string>
|
|
13307
|
+
<string>-c</string>
|
|
13308
|
+
<string>${shellLine}</string>`;
|
|
13309
|
+
}
|
|
13310
|
+
} else if (isCommandMode(entry)) {
|
|
13311
|
+
const argv = entry.args ?? [];
|
|
13312
|
+
programArgumentsBlock = [
|
|
13313
|
+
` <string>${xmlEscape(entry.command)}</string>`,
|
|
13314
|
+
...argv.map((a2) => ` <string>${xmlEscape(a2)}</string>`)
|
|
13315
|
+
].join(`
|
|
13233
13316
|
`);
|
|
13317
|
+
} else {
|
|
13234
13318
|
const argsBlock = entry.args ? `
|
|
13235
13319
|
${Object.entries(entry.args).map(([k2, v2]) => ` <string>--${xmlEscape(k2)}=${xmlEscape(v2)}</string>`).join(`
|
|
13236
13320
|
`)}` : "";
|
|
@@ -13238,7 +13322,7 @@ ${Object.entries(entry.args).map(([k2, v2]) => ` <string>--${xmlEscape(k2
|
|
|
13238
13322
|
<string>--vault</string>
|
|
13239
13323
|
<string>${xmlEscape(ctx.vaultPath)}</string>
|
|
13240
13324
|
<string>--skill</string>
|
|
13241
|
-
<string>${xmlEscape(entry.skill)}</string>
|
|
13325
|
+
<string>${xmlEscape(entry.skill ?? "")}</string>
|
|
13242
13326
|
<string>--headless</string>${argsBlock}`;
|
|
13243
13327
|
}
|
|
13244
13328
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -13264,8 +13348,8 @@ ${calendarXml}
|
|
|
13264
13348
|
</dict>
|
|
13265
13349
|
</plist>`;
|
|
13266
13350
|
}
|
|
13267
|
-
function plistPath(
|
|
13268
|
-
const labelSafe =
|
|
13351
|
+
function plistPath(skillOrLabel, homedir4) {
|
|
13352
|
+
const labelSafe = skillOrLabel.startsWith("/") ? skillOrLabel.replace(/^\//, "").replace(/[^a-zA-Z0-9-]/g, "-") : skillOrLabel.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
13269
13353
|
return `${homedir4}/Library/LaunchAgents/com.onebrain.${labelSafe}.plist`;
|
|
13270
13354
|
}
|
|
13271
13355
|
|
|
@@ -13296,12 +13380,15 @@ async function registerSchedule(opts) {
|
|
|
13296
13380
|
const va = validateAt(entry.at);
|
|
13297
13381
|
if (!va.valid)
|
|
13298
13382
|
throw new Error(`Invalid at "${entry.at}": ${va.reason}`);
|
|
13383
|
+
sanitizeArgsForOneShot(entry);
|
|
13299
13384
|
} else if (entry.cron !== undefined) {
|
|
13300
13385
|
const vc = validateCron(entry.cron);
|
|
13301
13386
|
if (!vc.valid)
|
|
13302
13387
|
throw new Error(`Invalid cron "${entry.cron}": ${vc.reason}`);
|
|
13303
13388
|
}
|
|
13304
|
-
|
|
13389
|
+
if (isSkillMode(entry)) {
|
|
13390
|
+
await validateSchedulable(opts.vault, entry);
|
|
13391
|
+
}
|
|
13305
13392
|
}
|
|
13306
13393
|
const skillCliPath = process.argv[1] ?? "onebrain";
|
|
13307
13394
|
const ctx = {
|
|
@@ -13313,15 +13400,20 @@ async function registerSchedule(opts) {
|
|
|
13313
13400
|
};
|
|
13314
13401
|
const seen = new Map;
|
|
13315
13402
|
for (const entry of entries) {
|
|
13316
|
-
const target = plistPath(entry
|
|
13403
|
+
const target = plistPath(labelForEntry(entry), ctx.homedir);
|
|
13317
13404
|
if (seen.has(target)) {
|
|
13318
|
-
|
|
13405
|
+
const existing = seen.get(target);
|
|
13406
|
+
if (existing) {
|
|
13407
|
+
const existingLabel = isCommandMode(existing) ? `command:${existing.command}` : `skill:${existing.skill}`;
|
|
13408
|
+
const newLabel = isCommandMode(entry) ? `command:${entry.command}` : `skill:${entry.skill}`;
|
|
13409
|
+
throw new Error(`Conflict: ${newLabel} and ${existingLabel} normalize to the same plist path ${target}`);
|
|
13410
|
+
}
|
|
13319
13411
|
}
|
|
13320
13412
|
seen.set(target, entry);
|
|
13321
13413
|
}
|
|
13322
13414
|
for (const entry of entries) {
|
|
13323
13415
|
const plistContent = generatePlist(entry, ctx);
|
|
13324
|
-
const targetPath = plistPath(entry
|
|
13416
|
+
const targetPath = plistPath(labelForEntry(entry), ctx.homedir);
|
|
13325
13417
|
if (opts.dryRun) {
|
|
13326
13418
|
console.log(import_picocolors8.default.cyan(`--- ${targetPath} ---`));
|
|
13327
13419
|
console.log(plistContent);
|
|
@@ -13334,7 +13426,7 @@ async function registerSchedule(opts) {
|
|
|
13334
13426
|
Registered ${entries.length} schedule entries.`));
|
|
13335
13427
|
console.log(import_picocolors8.default.dim("Use launchctl to load (or restart launchd):"));
|
|
13336
13428
|
for (const entry of entries) {
|
|
13337
|
-
const target = plistPath(entry
|
|
13429
|
+
const target = plistPath(labelForEntry(entry), ctx.homedir);
|
|
13338
13430
|
console.log(import_picocolors8.default.dim(` launchctl load ${target}`));
|
|
13339
13431
|
}
|
|
13340
13432
|
}
|
|
@@ -13345,7 +13437,18 @@ async function readVaultConfig(vault) {
|
|
|
13345
13437
|
const raw = await readFile6(yamlPath, "utf8");
|
|
13346
13438
|
return import_yaml7.parse(raw) ?? {};
|
|
13347
13439
|
}
|
|
13440
|
+
function sanitizeArgsForOneShot(entry) {
|
|
13441
|
+
const values = isCommandMode(entry) ? entry.args ?? [] : Object.values(entry.args ?? {});
|
|
13442
|
+
for (const v2 of values) {
|
|
13443
|
+
if (/["$`\\]/.test(v2)) {
|
|
13444
|
+
throw new Error(`Arg value must not contain shell-special chars (", $, \`, \\): ${v2}`);
|
|
13445
|
+
}
|
|
13446
|
+
}
|
|
13447
|
+
}
|
|
13348
13448
|
async function validateSchedulable(vault, entry) {
|
|
13449
|
+
if (!entry.skill) {
|
|
13450
|
+
throw new Error("validateSchedulable invoked on non-skill entry \u2014 caller bug");
|
|
13451
|
+
}
|
|
13349
13452
|
const skillName = entry.skill.replace(/^\//, "");
|
|
13350
13453
|
const skillPath = join11(vault, ".claude/plugins/onebrain/skills", skillName, "SKILL.md");
|
|
13351
13454
|
if (!existsSync(skillPath)) {
|
|
@@ -13373,7 +13476,7 @@ async function validateSchedulable(vault, entry) {
|
|
|
13373
13476
|
if (entry.args) {
|
|
13374
13477
|
for (const [k2, v2] of Object.entries(entry.args)) {
|
|
13375
13478
|
if (/["$`\\]/.test(v2)) {
|
|
13376
|
-
throw new Error(`Arg "${k2}" value must not contain shell-special chars (", $,
|
|
13479
|
+
throw new Error(`Arg "${k2}" value must not contain shell-special chars (", $, \`, \\): ${v2}`);
|
|
13377
13480
|
}
|
|
13378
13481
|
}
|
|
13379
13482
|
}
|
|
@@ -13382,7 +13485,7 @@ async function removeAll(vault) {
|
|
|
13382
13485
|
const config = await readVaultConfig(vault);
|
|
13383
13486
|
const entries = config.schedule ?? [];
|
|
13384
13487
|
for (const entry of entries) {
|
|
13385
|
-
const target = plistPath(entry
|
|
13488
|
+
const target = plistPath(labelForEntry(entry), homedir4());
|
|
13386
13489
|
if (existsSync(target)) {
|
|
13387
13490
|
await unlink4(target);
|
|
13388
13491
|
console.log(import_picocolors8.default.green(`\u2713 Removed ${target}`));
|
|
@@ -13394,11 +13497,21 @@ async function printStatus(vault) {
|
|
|
13394
13497
|
const entries = config.schedule ?? [];
|
|
13395
13498
|
console.log(import_picocolors8.default.cyan(`Registered schedules: ${entries.length}`));
|
|
13396
13499
|
for (const entry of entries) {
|
|
13397
|
-
const target = plistPath(entry
|
|
13500
|
+
const target = plistPath(labelForEntry(entry), homedir4());
|
|
13398
13501
|
const installed = existsSync(target) ? "\u2713" : "\u2717";
|
|
13399
13502
|
const when = entry.at ?? entry.cron ?? "?";
|
|
13400
13503
|
const tag = entry.at ? import_picocolors8.default.magenta("[once]") : import_picocolors8.default.dim("[cron]");
|
|
13401
|
-
|
|
13504
|
+
let targetLabel;
|
|
13505
|
+
if (isCommandMode(entry)) {
|
|
13506
|
+
const argv = entry.args ?? [];
|
|
13507
|
+
const argStr = argv.length ? ` ${argv.join(" ")}` : "";
|
|
13508
|
+
targetLabel = `${import_picocolors8.default.yellow("cmd:")} ${entry.command}${argStr}`;
|
|
13509
|
+
} else {
|
|
13510
|
+
const argsMap = entry.args ?? {};
|
|
13511
|
+
const argStr = Object.keys(argsMap).length ? ` (${Object.entries(argsMap).map(([k2, v2]) => `${k2}=${v2}`).join(", ")})` : "";
|
|
13512
|
+
targetLabel = `${import_picocolors8.default.green("skill:")} ${entry.skill}${argStr}`;
|
|
13513
|
+
}
|
|
13514
|
+
console.log(` ${installed} ${tag} ${when} ${targetLabel}`);
|
|
13402
13515
|
}
|
|
13403
13516
|
}
|
|
13404
13517
|
async function testRun(vault, skill) {
|
|
@@ -13645,7 +13758,7 @@ function patchUtf8(stream) {
|
|
|
13645
13758
|
}
|
|
13646
13759
|
|
|
13647
13760
|
// src/index.ts
|
|
13648
|
-
var VERSION = "2.3.
|
|
13761
|
+
var VERSION = "2.3.2";
|
|
13649
13762
|
var RELEASE_DATE = "2026-05-12";
|
|
13650
13763
|
patchUtf8(process.stdout);
|
|
13651
13764
|
patchUtf8(process.stderr);
|