@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.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/dist/onebrain +114 -31
  3. 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.0",
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.0";
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
- if (!entry.skill)
13207
- return { valid: false, reason: "entry.skill is required" };
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, "&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
+ }
13213
13254
  function generatePlist(entry, ctx) {
13214
- const labelSafe = entry.skill.replace(/^\//, "").replace(/[^a-zA-Z0-9-]/g, "-");
13255
+ const labelSafe = labelForEntry(entry);
13215
13256
  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>
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
- 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>
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
- } else {
13230
- const calendar = cronFieldsToLaunchd(entry.cron);
13231
- calendarXml = Object.entries(calendar).map(([k2, v2]) => ` <key>${k2}</key>
13232
- <integer>${v2}</integer>`).join(`
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(skill, homedir4) {
13268
- const labelSafe = skill.replace(/^\//, "").replace(/[^a-zA-Z0-9-]/g, "-");
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
- await validateSchedulable(opts.vault, entry);
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.skill, ctx.homedir);
13373
+ const target = plistPath(labelForEntry(entry), ctx.homedir);
13317
13374
  if (seen.has(target)) {
13318
- throw new Error(`Conflict: ${entry.skill} and ${seen.get(target)?.skill} normalize to the same plist path ${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
+ }
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.skill, ctx.homedir);
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.skill, ctx.homedir);
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 (", $, backtick, \\): ${v2}`);
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.skill, homedir4());
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.skill, homedir4());
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
- console.log(` ${installed} ${tag} ${when} ${entry.skill}`);
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.0";
13731
+ var VERSION = "2.3.1";
13649
13732
  var RELEASE_DATE = "2026-05-12";
13650
13733
  patchUtf8(process.stdout);
13651
13734
  patchUtf8(process.stderr);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebrain-ai/cli",
3
- "version": "2.3.0",
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",