@holt-os/holt 0.17.0 → 0.18.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 CHANGED
@@ -21,7 +21,7 @@
21
21
  - **Any brain, one assistant.** Bare `holt` launches a real interactive brain (Claude Code, Codex, or Gemini) branded as Holt, or you connect a direct API brain with your own key. Switch mid-conversation and keep the full context.
22
22
  - **Private memory you can see.** Per-folder semantic recall and distilled facts, an interactive knowledge graph (`holt graph`), and a self-maintaining wiki (`holt wiki`) over everything you tell it. Nothing leaves your machine.
23
23
  - **Ambient by default.** `holt hook` wires memory into Claude Code so it recalls and remembers with no manual step, and an MCP server exposes the same memory to Cursor and Codex.
24
- - **Runs your work.** Run a task once, on a schedule, or as a named routine, and reach it from your phone over Telegram.
24
+ - **Runs your work.** Run a task once, on a schedule, or as a named routine, and reach it from your phone over Telegram. Real OS timers on macOS, Linux, and Windows.
25
25
  - **Useful on day one.** `/skill onboard` interviews you and fills memory in about fifteen minutes, `/skill audit` shows what is still missing, and `/skill level-up` turns one manual chore a week into something Holt does.
26
26
  - **Extensible, zero-infra.** Thirteen built-in skills plus a git-based community registry (`holt skill search`) in the portable `SKILL.md` format.
27
27
  - **Writes in your voice.** `holt write` drafts in a style Holt learns from you, with built-in anti-AI checks.
@@ -63,6 +63,14 @@ That is it. Bare `holt` sets the folder up the first time (trust, brain, memory
63
63
 
64
64
  Prefer to step through setup on its own first? `holt init` still does that, then `holt` starts the session.
65
65
 
66
+ Then, in that first session, run:
67
+
68
+ ```
69
+ /skill onboard
70
+ ```
71
+
72
+ An empty Holt gives generic answers, which is the usual reason people install it and drift away. `onboard` is a fifteen minute interview that fills memory with who you are and what you are working on, and ends by doing one real piece of your work. Everything else gets better once it knows you.
73
+
66
74
  ## What bare `holt` does
67
75
 
68
76
  `holt` (with no command) is the front door: **your assistant, with memory of you.**
@@ -441,7 +449,7 @@ Options: `--brain <id>` pick a brain, `--out <file>` also write the reply to a f
441
449
 
442
450
  ### holt schedule
443
451
 
444
- Run a task automatically on a timer, using your OS scheduler (launchd on macOS, cron on Linux). A scheduled run behaves exactly like typing `holt run` in that folder.
452
+ Run a task automatically on a timer, using your OS scheduler (launchd on macOS, cron on Linux, Task Scheduler on Windows). A scheduled run behaves exactly like typing `holt run` in that folder.
445
453
 
446
454
  ```bash
447
455
  holt schedule add "summarize my inbox and flag what is urgent" 07:00 --notify
@@ -490,7 +498,7 @@ holt routine remove brief # deletes the routine and its OS timer, if any
490
498
 
491
499
  Sources: give exactly one of `--skill <name>`, `--task "<prompt>"`, or `--template <t>`. A skill routine splices that skill's `SKILL.md` body into the prompt at run time; a task routine uses the prompt as-is.
492
500
 
493
- Scheduling: `--at HH:MM` (24h, daily) installs an OS timer through the same scheduler `holt schedule` uses (launchd on macOS, cron on Linux) whose command is `holt routine run <name> --quiet`. Omit `--at` for a manual, run-on-demand routine. Removing the routine removes its timer.
501
+ Scheduling: `--at HH:MM` (24h, daily) installs an OS timer through the same scheduler `holt schedule` uses (launchd on macOS, cron on Linux, Task Scheduler on Windows) whose command is `holt routine run <name> --quiet`. Omit `--at` for a manual, run-on-demand routine. Removing the routine removes its timer.
494
502
 
495
503
  Output routing: by default the result prints to stdout. `--out <file>` also writes it to a file (relative to the routine's workspace); `--notify` pushes it to Telegram (guarded cleanly when Telegram is not set up). Both `--out` and `--notify` can also be passed to `holt routine run` for a single run. In `--quiet` mode (what the scheduler uses) stdout is suppressed but `--out` and `--notify` still fire.
496
504
 
@@ -622,7 +630,8 @@ Built in always-shippable phases toward a full-vision v1:
622
630
  4. **Knowledge graph**: see and navigate your own memory with `holt graph` *(shipped)*
623
631
  5. **Everywhere**: MCP server so other tools read Holt's memory, plus `holt run`, scheduling, and Telegram *(shipped)*
624
632
  6. **Knowledge wiki**: LLM-maintained, cross-linked, regenerable pages over your memory with `holt wiki` *(shipped)*
625
- 7. **Next**: docs site, skill registry publishing, more channels
633
+ 7. **Onboarding**: a day-one interview that fills memory, plus the weekly audit and level-up ritual *(shipped)*
634
+ 8. **Next**: docs site, skill registry publishing, more channels
626
635
 
627
636
  ## Contributing
628
637
 
package/dist/cli.js CHANGED
@@ -2515,23 +2515,37 @@ function buildCronLine(job, holtPath) {
2515
2515
  const command = buildCommand(job, holtPath);
2516
2516
  return `${minute} ${hour} * * * ${command} # ${CRON_MARKER}${job.id}`;
2517
2517
  }
2518
- function buildWindowsTaskFields(job, holtPath) {
2519
- let args;
2518
+ function taskName(id) {
2519
+ return `Holt ${id}`;
2520
+ }
2521
+ function cmdScriptPath(id) {
2522
+ return join8(GLOBAL_DIR, "tasks", `${id}.cmd`);
2523
+ }
2524
+ function cmdQuote(s) {
2525
+ return /[\s&()[\]{}^=;!'+,`~|<>"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
2526
+ }
2527
+ function buildWindowsScript(job, holtPath) {
2528
+ const holt = cmdQuote(holtPath);
2529
+ const lines = ["@echo off", `cd /d ${cmdQuote(job.workspace)}`];
2520
2530
  if (job.runArgs && job.runArgs.length > 0) {
2521
- args = [...job.runArgs, "--quiet"];
2531
+ lines.push(`${holt} ${job.runArgs.map(cmdQuote).join(" ")} --quiet`);
2522
2532
  } else {
2523
- args = ["run", job.task];
2524
- if (job.brain) args.push("--brain", job.brain);
2525
- args.push("--quiet", "--out", logPath(job.id));
2533
+ const log = cmdQuote(logPath(job.id));
2534
+ let run2 = `${holt} run ${cmdQuote(job.task)}`;
2535
+ if (job.brain) run2 += ` --brain ${cmdQuote(job.brain)}`;
2536
+ run2 += ` --quiet --out ${log}`;
2537
+ lines.push(run2);
2538
+ if (job.notify) {
2539
+ lines.push(`if errorlevel 1 exit /b 1`);
2540
+ lines.push(`type ${log} | ${holt} notify`);
2541
+ }
2526
2542
  }
2527
- const quoted = args.map((a) => /[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a);
2528
- return {
2529
- program: holtPath,
2530
- args: quoted.join(" "),
2531
- startIn: job.workspace,
2532
- when: job.when,
2533
- name: `Holt ${job.id}`
2534
- };
2543
+ return lines.join("\r\n") + "\r\n";
2544
+ }
2545
+ function buildSchtasksArgs(job, scriptPath) {
2546
+ const { hour, minute } = parseWhen(job.when);
2547
+ const hhmm = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
2548
+ return ["/Create", "/F", "/SC", "DAILY", "/ST", hhmm, "/TN", taskName(job.id), "/TR", scriptPath];
2535
2549
  }
2536
2550
  function stripCronLines(crontab, id) {
2537
2551
  const marker = `# ${CRON_MARKER}${id}`;
@@ -7533,7 +7547,7 @@ function holtTools() {
7533
7547
  }
7534
7548
 
7535
7549
  // src/version.ts
7536
- var VERSION = "0.17.0";
7550
+ var VERSION = "0.18.0";
7537
7551
 
7538
7552
  // src/commands/mcp.ts
7539
7553
  function printSetup() {
@@ -7846,29 +7860,42 @@ async function addCmd(rest) {
7846
7860
  console.log(` workspace ${job.workspace}`);
7847
7861
  if (job.notify) console.log(` notify on (pushes output to Telegram)`);
7848
7862
  console.log(` log ${logPath(job.id)}`);
7849
- if (process.platform === "darwin") installDarwin(job, holtPath);
7850
- else if (process.platform === "linux") installLinux(job, holtPath);
7851
- else printManualInstall(job, holtPath);
7863
+ installForPlatform(job, holtPath);
7852
7864
  console.log("");
7853
7865
  }
7854
- function printManualInstall(job, holtPath) {
7855
- if (process.platform !== "win32") {
7866
+ function installForPlatform(job, holtPath) {
7867
+ if (process.platform === "darwin") installDarwin(job, holtPath);
7868
+ else if (process.platform === "linux") installLinux(job, holtPath);
7869
+ else if (process.platform === "win32") installWindows(job, holtPath);
7870
+ else {
7856
7871
  console.log(c.dim("\n Your OS has no built-in installer. Install this entry yourself:"));
7857
7872
  console.log(" " + buildCronLine(job, holtPath));
7858
- return;
7859
7873
  }
7860
- const f = buildWindowsTaskFields(job, holtPath);
7861
- console.log(c.dim("\n Saved, but not started: Holt cannot install timers on Windows yet."));
7862
- console.log(c.dim(' Open Task Scheduler, choose "Create Basic Task", and fill in:'));
7863
- console.log(` Name ${f.name}`);
7864
- console.log(` Trigger Daily at ${f.when}`);
7865
- console.log(` Program ${f.program}`);
7866
- console.log(` Arguments ${f.args}`);
7867
- console.log(` Start in ${f.startIn}`);
7868
- if (job.notify) {
7874
+ }
7875
+ function installWindows(job, holtPath) {
7876
+ const script = cmdScriptPath(job.id);
7877
+ mkdirSync15(dirname9(script), { recursive: true });
7878
+ writeFileSync17(script, buildWindowsScript(job, holtPath), "utf8");
7879
+ const res = spawnSync5("schtasks", buildSchtasksArgs(job, script), { encoding: "utf8" });
7880
+ if (res.status === 0) {
7881
+ console.log(c.green(" Registered with Task Scheduler."));
7882
+ } else {
7883
+ console.log(c.dim(" Wrote the task script, but schtasks did not confirm."));
7884
+ console.log(c.dim(" Register it yourself with:"));
7869
7885
  console.log(
7870
- c.dim("\n Note: --notify does not carry over to Task Scheduler. Read the log file instead.")
7886
+ c.dim(` schtasks /Create /F /SC DAILY /ST ${job.when} /TN "${taskName(job.id)}" /TR "${script}"`)
7871
7887
  );
7888
+ const why = (res.stderr || "").trim() || (res.error ? res.error.message : "");
7889
+ if (why) console.log(c.dim(` (${why})`));
7890
+ }
7891
+ console.log(c.dim(` task: ${taskName(job.id)}`));
7892
+ console.log(c.dim(` script: ${script}`));
7893
+ }
7894
+ function removeWindows(id) {
7895
+ spawnSync5("schtasks", ["/Delete", "/F", "/TN", taskName(id)], { encoding: "utf8" });
7896
+ try {
7897
+ rmSync7(cmdScriptPath(id));
7898
+ } catch {
7872
7899
  }
7873
7900
  }
7874
7901
  function listCmd2() {
@@ -7902,6 +7929,11 @@ function removeLinux(id) {
7902
7929
  const next = stripCronLines(body, id);
7903
7930
  spawnSync5("crontab", ["-"], { input: next, encoding: "utf8" });
7904
7931
  }
7932
+ function removeForPlatform(id) {
7933
+ if (process.platform === "darwin") removeDarwin(id);
7934
+ else if (process.platform === "linux") removeLinux(id);
7935
+ else if (process.platform === "win32") removeWindows(id);
7936
+ }
7905
7937
  function removeCmd(rest) {
7906
7938
  const id = rest[0];
7907
7939
  if (!id) {
@@ -7912,8 +7944,7 @@ function removeCmd(rest) {
7912
7944
  const before = loadJobs();
7913
7945
  const existed = before.some((j) => j.id === id);
7914
7946
  removeJob(id);
7915
- if (process.platform === "darwin") removeDarwin(id);
7916
- else if (process.platform === "linux") removeLinux(id);
7947
+ removeForPlatform(id);
7917
7948
  if (existed) console.log("\n" + c.green(` Removed schedule ${id}.`) + "\n");
7918
7949
  else {
7919
7950
  console.error(c.dim(`
@@ -7950,9 +7981,8 @@ async function schedule(sub, rest = []) {
7950
7981
  }
7951
7982
 
7952
7983
  // src/commands/routine.ts
7953
- import { spawnSync as spawnSync6 } from "child_process";
7954
- import { writeFileSync as writeFileSync19, mkdirSync as mkdirSync17, rmSync as rmSync8, existsSync as existsSync19 } from "fs";
7955
- import { dirname as dirname10, resolve as resolve7 } from "path";
7984
+ import { writeFileSync as writeFileSync19 } from "fs";
7985
+ import { resolve as resolve7 } from "path";
7956
7986
 
7957
7987
  // src/routines.ts
7958
7988
  import { readFileSync as readFileSync19, writeFileSync as writeFileSync18, mkdirSync as mkdirSync16 } from "fs";
@@ -8031,70 +8061,19 @@ function jobForRoutine(r) {
8031
8061
  function routineJobId(name) {
8032
8062
  return `routine-${sanitizeRoutineName(name)}`;
8033
8063
  }
8034
- function installDarwin2(job, holtPath) {
8035
- const path = plistPath(job.id);
8036
- mkdirSync17(dirname10(path), { recursive: true });
8037
- writeFileSync19(path, buildLaunchdPlist(job, holtPath), "utf8");
8038
- const res = spawnSync6("launchctl", ["load", "-w", path], { encoding: "utf8" });
8039
- if (res.status === 0) {
8040
- console.log(c.green(" Loaded into launchd."));
8041
- } else {
8042
- console.log(c.dim(" Wrote the plist, but launchctl load did not confirm."));
8043
- console.log(c.dim(` Load it yourself with: launchctl load -w ${path}`));
8044
- if (res.stderr && res.stderr.trim()) console.log(c.dim(` (${res.stderr.trim()})`));
8045
- }
8046
- console.log(c.dim(` plist: ${path}`));
8047
- }
8048
- function currentCrontab2() {
8049
- const res = spawnSync6("crontab", ["-l"], { encoding: "utf8" });
8050
- return res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
8051
- }
8052
- function writeCrontab2(body) {
8053
- const res = spawnSync6("crontab", ["-"], { input: body, encoding: "utf8" });
8054
- return res.status === 0;
8055
- }
8056
- function installLinux2(job, holtPath) {
8057
- const line = buildCronLine(job, holtPath);
8058
- const next = appendCronLine(currentCrontab2(), line);
8059
- if (writeCrontab2(next)) {
8060
- console.log(c.green(" Added to your crontab."));
8061
- console.log(c.dim(` line: ${line}`));
8062
- } else {
8063
- console.log(c.dim(" Could not update the crontab automatically. Add this line yourself:"));
8064
- console.log(" " + line);
8065
- }
8066
- }
8067
8064
  function installSchedule(r) {
8068
8065
  const holtPath = resolveHoltPath();
8069
8066
  const job = jobForRoutine(r);
8070
8067
  removeSchedule(r.name);
8071
8068
  addJob(job);
8072
- if (process.platform === "darwin") installDarwin2(job, holtPath);
8073
- else if (process.platform === "linux") installLinux2(job, holtPath);
8074
- else printManualInstall(job, holtPath);
8075
- }
8076
- function removeScheduleDarwin(id) {
8077
- const path = plistPath(id);
8078
- if (existsSync19(path)) {
8079
- spawnSync6("launchctl", ["unload", path], { encoding: "utf8" });
8080
- try {
8081
- rmSync8(path);
8082
- } catch {
8083
- }
8084
- }
8085
- }
8086
- function removeScheduleLinux(id) {
8087
- const body = currentCrontab2();
8088
- const next = stripCronLines(body, id);
8089
- writeCrontab2(next);
8069
+ installForPlatform(job, holtPath);
8090
8070
  }
8091
8071
  function removeSchedule(name) {
8092
8072
  const id = routineJobId(name);
8093
8073
  const existed = loadJobs().some((j) => j.id === id);
8094
8074
  removeJob(id);
8095
8075
  if (!existed) return;
8096
- if (process.platform === "darwin") removeScheduleDarwin(id);
8097
- else if (process.platform === "linux") removeScheduleLinux(id);
8076
+ removeForPlatform(id);
8098
8077
  }
8099
8078
  function parseAdd2(rest) {
8100
8079
  const opts = { notify: false };
@@ -8449,8 +8428,8 @@ function readStdin3() {
8449
8428
  }
8450
8429
 
8451
8430
  // src/commands/doctor.ts
8452
- import { spawn as spawn5, spawnSync as spawnSync7 } from "child_process";
8453
- import { existsSync as existsSync20 } from "fs";
8431
+ import { spawn as spawn5, spawnSync as spawnSync6 } from "child_process";
8432
+ import { existsSync as existsSync19 } from "fs";
8454
8433
 
8455
8434
  // src/specs.ts
8456
8435
  import os from "os";
@@ -8612,7 +8591,7 @@ async function fixSemanticMemory(fixed) {
8612
8591
  }
8613
8592
  if (!tags.some((t) => t.startsWith(EMBED_MODEL))) {
8614
8593
  note(`The "${EMBED_MODEL}" model is missing. Downloading it (one-time, ~275 MB)...`);
8615
- const r = spawnSync7("ollama", ["pull", EMBED_MODEL], { stdio: "inherit" });
8594
+ const r = spawnSync6("ollama", ["pull", EMBED_MODEL], { stdio: "inherit" });
8616
8595
  if (r.status !== 0) {
8617
8596
  warn(`Could not download ${EMBED_MODEL}. Check your internet connection and retry.`);
8618
8597
  return false;
@@ -8675,7 +8654,7 @@ async function doctor(args = []) {
8675
8654
  }
8676
8655
  if (embedOk) {
8677
8656
  good(`Ollama reachable and "${EMBED_MODEL}" is present. Memory recall is semantic, private, and local.`);
8678
- if (fix && existsSync20(wsHoltDir())) {
8657
+ if (fix && existsSync19(wsHoltDir())) {
8679
8658
  const missing = loadTurns().filter((t) => !Array.isArray(t.emb)).length;
8680
8659
  if (missing > 0) {
8681
8660
  note(`${missing} memories in this folder lack embeddings. Upgrading them...`);
@@ -8794,12 +8773,12 @@ async function tour() {
8794
8773
  }
8795
8774
 
8796
8775
  // src/commands/reset.ts
8797
- import { existsSync as existsSync21, rmSync as rmSync9 } from "fs";
8776
+ import { existsSync as existsSync20, rmSync as rmSync8 } from "fs";
8798
8777
  async function reset() {
8799
8778
  const { ask, close } = createReader();
8800
8779
  try {
8801
8780
  const dir = wsHoltDir();
8802
- if (!existsSync21(dir)) {
8781
+ if (!existsSync20(dir)) {
8803
8782
  console.log(c.dim("\n Nothing to reset: Holt has no settings or memory in this folder.\n"));
8804
8783
  return;
8805
8784
  }
@@ -8818,7 +8797,7 @@ async function reset() {
8818
8797
  console.log(c.dim(" Cancelled. Nothing was changed.\n"));
8819
8798
  return;
8820
8799
  }
8821
- rmSync9(dir, { recursive: true, force: true });
8800
+ rmSync8(dir, { recursive: true, force: true });
8822
8801
  console.log(c.green("\n Done. This folder is a clean slate.") + c.dim(' Run "holt" to set it up again.\n'));
8823
8802
  } finally {
8824
8803
  close();
@@ -8826,7 +8805,7 @@ async function reset() {
8826
8805
  }
8827
8806
 
8828
8807
  // src/commands/update.ts
8829
- import { spawnSync as spawnSync8 } from "child_process";
8808
+ import { spawnSync as spawnSync7 } from "child_process";
8830
8809
  import { realpathSync as realpathSync4 } from "fs";
8831
8810
  async function latestVersion() {
8832
8811
  try {
@@ -8868,7 +8847,7 @@ async function update() {
8868
8847
  console.log(c.dim(" Later, then. The command is: brew upgrade holt\n"));
8869
8848
  return;
8870
8849
  }
8871
- const r2 = spawnSync8("brew", ["upgrade", "holt"], { stdio: "inherit" });
8850
+ const r2 = spawnSync7("brew", ["upgrade", "holt"], { stdio: "inherit" });
8872
8851
  if (r2.status === 0) console.log(c.green(`
8873
8852
  Updated to ${latest}.`) + "\n");
8874
8853
  else console.log(c.dim("\n Homebrew reported a problem above. You can retry with: brew upgrade holt\n"));
@@ -8878,7 +8857,7 @@ async function update() {
8878
8857
  console.log(c.dim(" Later, then. The command is: npm install -g @holt-os/holt\n"));
8879
8858
  return;
8880
8859
  }
8881
- const r = spawnSync8("npm", ["install", "-g", "@holt-os/holt"], { stdio: "inherit" });
8860
+ const r = spawnSync7("npm", ["install", "-g", "@holt-os/holt"], { stdio: "inherit" });
8882
8861
  if (r.status === 0) {
8883
8862
  console.log(c.green(`
8884
8863
  Updated to ${latest}.`) + "\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holt-os/holt",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "An open-source personal agent OS: any LLM, private memory you can see and walk.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "build": "tsup src/cli.ts --format esm --clean",
45
45
  "start": "node dist/cli.js",
46
46
  "typecheck": "tsc --noEmit",
47
- "test": "tsx scripts/specs.test.ts && tsx scripts/skillcompiler.test.ts",
47
+ "test": "tsx scripts/specs.test.ts && tsx scripts/skillcompiler.test.ts && tsx scripts/scheduler.test.ts",
48
48
  "release": "bash scripts/release.sh"
49
49
  },
50
50
  "devDependencies": {
@@ -86,9 +86,9 @@ an essay.
86
86
  - **Hand-edited facts.** Facts added by editing `facts.md` directly are never
87
87
  embedded, so they never come back through recall. They must be re-added with
88
88
  `holt memory add`.
89
- - **Windows and timers.** On Windows, `holt schedule` and a routine's `--at`
90
- time do not install anything. Anything on rung four there has to run another
91
- way, so check the platform before calling rung four solid.
89
+ - **Timers need the machine awake.** A scheduled job fires only if the computer
90
+ is on at that time; a missed run is skipped, not caught up. If rung four looks
91
+ solid but the log file is empty, check that first.
92
92
  - **Automation above an empty memory.** If Run looks solid while Know is thin,
93
93
  say so bluntly. That setup produces confident wrong output on a timer, and it
94
94
  is the most expensive failure on this list.