@onebrain-ai/cli 2.3.0 → 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.
- package/README.md +16 -0
- package/dist/onebrain +114 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -528,6 +528,22 @@ Or use the interactive wizards from inside your vault:
|
|
|
528
528
|
|
|
529
529
|
Output goes to `[logs_folder]/scheduler/YYYY/MM/YYYY-MM-DD-{skill}.md` as readable markdown.
|
|
530
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
|
+
|
|
531
547
|
CLI flags:
|
|
532
548
|
|
|
533
549
|
| Flag | Purpose |
|
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.3.
|
|
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",
|
|
@@ -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.3.
|
|
11051
|
+
return "2.3.1";
|
|
11052
11052
|
try {
|
|
11053
11053
|
const pkg = require_package();
|
|
11054
11054
|
return pkg.version ?? "dev";
|
|
@@ -13197,40 +13197,94 @@ function atToLaunchd(at) {
|
|
|
13197
13197
|
function isOneShot(entry) {
|
|
13198
13198
|
return entry.at !== undefined;
|
|
13199
13199
|
}
|
|
13200
|
+
function isSkillMode(entry) {
|
|
13201
|
+
return entry.skill !== undefined;
|
|
13202
|
+
}
|
|
13203
|
+
function isCommandMode(entry) {
|
|
13204
|
+
return entry.command !== undefined;
|
|
13205
|
+
}
|
|
13200
13206
|
function validateEntry(entry) {
|
|
13201
13207
|
const hasCron = entry.cron !== undefined;
|
|
13202
13208
|
const hasAt = entry.at !== undefined;
|
|
13203
13209
|
if (hasCron === hasAt) {
|
|
13204
13210
|
return { valid: false, reason: "entry must have exactly one of `cron` or `at`" };
|
|
13205
13211
|
}
|
|
13206
|
-
|
|
13207
|
-
|
|
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
|
+
}
|
|
13208
13245
|
return { valid: true };
|
|
13209
13246
|
}
|
|
13210
13247
|
|
|
13211
13248
|
// src/lib/scheduler/launchd.ts
|
|
13212
13249
|
var xmlEscape = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
13250
|
+
function labelForEntry(entry) {
|
|
13251
|
+
const raw = isCommandMode(entry) ? entry.command : (entry.skill ?? "").replace(/^\//, "");
|
|
13252
|
+
return raw.replace(/[^a-zA-Z0-9-]/g, "-");
|
|
13253
|
+
}
|
|
13213
13254
|
function generatePlist(entry, ctx) {
|
|
13214
|
-
const labelSafe = entry
|
|
13255
|
+
const labelSafe = labelForEntry(entry);
|
|
13215
13256
|
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>
|
|
13257
|
+
const calendar = isOneShot(entry) ? atToLaunchd(entry.at) : cronFieldsToLaunchd(entry.cron);
|
|
13258
|
+
const calendarXml = Object.entries(calendar).map(([k2, v2]) => ` <key>${k2}</key>
|
|
13221
13259
|
<integer>${v2}</integer>`).join(`
|
|
13222
13260
|
`);
|
|
13223
|
-
|
|
13224
|
-
|
|
13225
|
-
|
|
13226
|
-
|
|
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>
|
|
13227
13270
|
<string>-c</string>
|
|
13228
13271
|
<string>${shellLine}</string>`;
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
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(`
|
|
13233
13286
|
`);
|
|
13287
|
+
} else {
|
|
13234
13288
|
const argsBlock = entry.args ? `
|
|
13235
13289
|
${Object.entries(entry.args).map(([k2, v2]) => ` <string>--${xmlEscape(k2)}=${xmlEscape(v2)}</string>`).join(`
|
|
13236
13290
|
`)}` : "";
|
|
@@ -13238,7 +13292,7 @@ ${Object.entries(entry.args).map(([k2, v2]) => ` <string>--${xmlEscape(k2
|
|
|
13238
13292
|
<string>--vault</string>
|
|
13239
13293
|
<string>${xmlEscape(ctx.vaultPath)}</string>
|
|
13240
13294
|
<string>--skill</string>
|
|
13241
|
-
<string>${xmlEscape(entry.skill)}</string>
|
|
13295
|
+
<string>${xmlEscape(entry.skill ?? "")}</string>
|
|
13242
13296
|
<string>--headless</string>${argsBlock}`;
|
|
13243
13297
|
}
|
|
13244
13298
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
@@ -13264,8 +13318,8 @@ ${calendarXml}
|
|
|
13264
13318
|
</dict>
|
|
13265
13319
|
</plist>`;
|
|
13266
13320
|
}
|
|
13267
|
-
function plistPath(
|
|
13268
|
-
const labelSafe =
|
|
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, "-");
|
|
13269
13323
|
return `${homedir4}/Library/LaunchAgents/com.onebrain.${labelSafe}.plist`;
|
|
13270
13324
|
}
|
|
13271
13325
|
|
|
@@ -13296,12 +13350,15 @@ async function registerSchedule(opts) {
|
|
|
13296
13350
|
const va = validateAt(entry.at);
|
|
13297
13351
|
if (!va.valid)
|
|
13298
13352
|
throw new Error(`Invalid at "${entry.at}": ${va.reason}`);
|
|
13353
|
+
sanitizeArgsForOneShot(entry);
|
|
13299
13354
|
} else if (entry.cron !== undefined) {
|
|
13300
13355
|
const vc = validateCron(entry.cron);
|
|
13301
13356
|
if (!vc.valid)
|
|
13302
13357
|
throw new Error(`Invalid cron "${entry.cron}": ${vc.reason}`);
|
|
13303
13358
|
}
|
|
13304
|
-
|
|
13359
|
+
if (isSkillMode(entry)) {
|
|
13360
|
+
await validateSchedulable(opts.vault, entry);
|
|
13361
|
+
}
|
|
13305
13362
|
}
|
|
13306
13363
|
const skillCliPath = process.argv[1] ?? "onebrain";
|
|
13307
13364
|
const ctx = {
|
|
@@ -13313,15 +13370,20 @@ async function registerSchedule(opts) {
|
|
|
13313
13370
|
};
|
|
13314
13371
|
const seen = new Map;
|
|
13315
13372
|
for (const entry of entries) {
|
|
13316
|
-
const target = plistPath(entry
|
|
13373
|
+
const target = plistPath(labelForEntry(entry), ctx.homedir);
|
|
13317
13374
|
if (seen.has(target)) {
|
|
13318
|
-
|
|
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
|
+
}
|
|
13319
13381
|
}
|
|
13320
13382
|
seen.set(target, entry);
|
|
13321
13383
|
}
|
|
13322
13384
|
for (const entry of entries) {
|
|
13323
13385
|
const plistContent = generatePlist(entry, ctx);
|
|
13324
|
-
const targetPath = plistPath(entry
|
|
13386
|
+
const targetPath = plistPath(labelForEntry(entry), ctx.homedir);
|
|
13325
13387
|
if (opts.dryRun) {
|
|
13326
13388
|
console.log(import_picocolors8.default.cyan(`--- ${targetPath} ---`));
|
|
13327
13389
|
console.log(plistContent);
|
|
@@ -13334,7 +13396,7 @@ async function registerSchedule(opts) {
|
|
|
13334
13396
|
Registered ${entries.length} schedule entries.`));
|
|
13335
13397
|
console.log(import_picocolors8.default.dim("Use launchctl to load (or restart launchd):"));
|
|
13336
13398
|
for (const entry of entries) {
|
|
13337
|
-
const target = plistPath(entry
|
|
13399
|
+
const target = plistPath(labelForEntry(entry), ctx.homedir);
|
|
13338
13400
|
console.log(import_picocolors8.default.dim(` launchctl load ${target}`));
|
|
13339
13401
|
}
|
|
13340
13402
|
}
|
|
@@ -13345,7 +13407,18 @@ async function readVaultConfig(vault) {
|
|
|
13345
13407
|
const raw = await readFile6(yamlPath, "utf8");
|
|
13346
13408
|
return import_yaml7.parse(raw) ?? {};
|
|
13347
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
|
+
}
|
|
13348
13418
|
async function validateSchedulable(vault, entry) {
|
|
13419
|
+
if (!entry.skill) {
|
|
13420
|
+
throw new Error("validateSchedulable invoked on non-skill entry \u2014 caller bug");
|
|
13421
|
+
}
|
|
13349
13422
|
const skillName = entry.skill.replace(/^\//, "");
|
|
13350
13423
|
const skillPath = join11(vault, ".claude/plugins/onebrain/skills", skillName, "SKILL.md");
|
|
13351
13424
|
if (!existsSync(skillPath)) {
|
|
@@ -13373,7 +13446,7 @@ async function validateSchedulable(vault, entry) {
|
|
|
13373
13446
|
if (entry.args) {
|
|
13374
13447
|
for (const [k2, v2] of Object.entries(entry.args)) {
|
|
13375
13448
|
if (/["$`\\]/.test(v2)) {
|
|
13376
|
-
throw new Error(`Arg "${k2}" value must not contain shell-special chars (", $,
|
|
13449
|
+
throw new Error(`Arg "${k2}" value must not contain shell-special chars (", $, \`, \\): ${v2}`);
|
|
13377
13450
|
}
|
|
13378
13451
|
}
|
|
13379
13452
|
}
|
|
@@ -13382,7 +13455,7 @@ async function removeAll(vault) {
|
|
|
13382
13455
|
const config = await readVaultConfig(vault);
|
|
13383
13456
|
const entries = config.schedule ?? [];
|
|
13384
13457
|
for (const entry of entries) {
|
|
13385
|
-
const target = plistPath(entry
|
|
13458
|
+
const target = plistPath(labelForEntry(entry), homedir4());
|
|
13386
13459
|
if (existsSync(target)) {
|
|
13387
13460
|
await unlink4(target);
|
|
13388
13461
|
console.log(import_picocolors8.default.green(`\u2713 Removed ${target}`));
|
|
@@ -13394,11 +13467,21 @@ async function printStatus(vault) {
|
|
|
13394
13467
|
const entries = config.schedule ?? [];
|
|
13395
13468
|
console.log(import_picocolors8.default.cyan(`Registered schedules: ${entries.length}`));
|
|
13396
13469
|
for (const entry of entries) {
|
|
13397
|
-
const target = plistPath(entry
|
|
13470
|
+
const target = plistPath(labelForEntry(entry), homedir4());
|
|
13398
13471
|
const installed = existsSync(target) ? "\u2713" : "\u2717";
|
|
13399
13472
|
const when = entry.at ?? entry.cron ?? "?";
|
|
13400
13473
|
const tag = entry.at ? import_picocolors8.default.magenta("[once]") : import_picocolors8.default.dim("[cron]");
|
|
13401
|
-
|
|
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}`);
|
|
13402
13485
|
}
|
|
13403
13486
|
}
|
|
13404
13487
|
async function testRun(vault, skill) {
|
|
@@ -13645,7 +13728,7 @@ function patchUtf8(stream) {
|
|
|
13645
13728
|
}
|
|
13646
13729
|
|
|
13647
13730
|
// src/index.ts
|
|
13648
|
-
var VERSION = "2.3.
|
|
13731
|
+
var VERSION = "2.3.1";
|
|
13649
13732
|
var RELEASE_DATE = "2026-05-12";
|
|
13650
13733
|
patchUtf8(process.stdout);
|
|
13651
13734
|
patchUtf8(process.stderr);
|