@holt-os/holt 0.11.0 → 0.12.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 -3
- package/dist/cli.js +346 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,14 @@ Holt is an open-source, self-hosted personal agent OS. Clone it, pick your skill
|
|
|
6
6
|
|
|
7
7
|
> A *holt* is a small wood: a sheltered place where things are kept and grow. That's the idea. A private home for your knowledge that compounds over time.
|
|
8
8
|
|
|
9
|
-
> **Status:
|
|
9
|
+
> **Status: shipping and usable today.** Working now:
|
|
10
|
+
>
|
|
11
|
+
> - `holt init` + `holt chat` with any brain (Claude Code, Codex, Gemini, or a direct API key), and mid-conversation brain switching that keeps context.
|
|
12
|
+
> - Per-folder memory with semantic recall and distilled facts, ambient memory for Claude Code (`holt hook`), and opt-in memory shared across folders (`holt memory global`).
|
|
13
|
+
> - A self-maintaining knowledge wiki (`holt wiki`) and an interactive knowledge graph (`holt graph`) that can also ingest your code and docs into communities.
|
|
14
|
+
> - Run a task once, on a schedule, or as a named routine (`holt run` / `schedule` / `routine`), and reach it from your phone over Telegram.
|
|
15
|
+
> - Draft in your own voice with anti-AI checks (`holt write`), and size your setup with `holt doctor`.
|
|
16
|
+
> - Ten built-in skills plus a community registry (`holt skill search`), and an MCP server so Claude Code, Cursor, or Codex can read this folder's memory.
|
|
10
17
|
|
|
11
18
|
---
|
|
12
19
|
|
|
@@ -213,11 +220,13 @@ holt skill list list installed skills
|
|
|
213
220
|
holt skill show <name> print a skill
|
|
214
221
|
holt skill create <name> [--global] scaffold a new skill
|
|
215
222
|
holt skill search <query> find skills in the registry
|
|
216
|
-
holt skill add <src|name> [--global] install from a git URL, path, or registry name
|
|
223
|
+
holt skill add <src|name> [--global] install from a git URL, git-url#subdir, path, or registry name
|
|
217
224
|
holt skill publish [<name>] prepare a skill for the registry (prints a PR entry)
|
|
218
225
|
holt skill remove <name> delete a skill
|
|
219
226
|
```
|
|
220
227
|
|
|
228
|
+
A source may carry a `#<subdir>` suffix (split on the first `#`) that points at one skill folder inside a repo or path: `holt skill add https://github.com/holt-os/registry#skills/pm-prd`. The subdir is a path, relative to the source root, that contains a `SKILL.md`. This lets a single repo hold many skills (one monorepo), which is exactly how the community registry is laid out. The subdir is validated to stay inside the source (absolute paths and `..` traversal are refused), since a registry entry is remote-controlled. Without a `#`, behaviour is unchanged: Holt looks for a `SKILL.md` at the source root or in exactly one immediate subfolder.
|
|
229
|
+
|
|
221
230
|
In chat, run one with `/skill <name> [your input]`. Available skills are also listed to the brain each turn, so it knows what it can be asked to follow.
|
|
222
231
|
|
|
223
232
|
### Built-in skills
|
|
@@ -256,7 +265,7 @@ The index is a JSON document:
|
|
|
256
265
|
}
|
|
257
266
|
```
|
|
258
267
|
|
|
259
|
-
`source` is anything `holt skill add` already accepts: a git URL (its `SKILL.md` may sit in one subfolder) or a
|
|
268
|
+
`source` is anything `holt skill add` already accepts: a git URL (its `SKILL.md` may sit in one subfolder), a local path, or either of those with a `#<subdir>` suffix pointing at one skill folder inside the repo. That last form lets one repo hold many skills, so the community registry can be a single monorepo (e.g. `"source": "https://github.com/holt-os/registry#skills/pm-prd"`). `tags` is optional. Only `name` and `source` are required per entry; malformed rows are skipped rather than failing the whole index.
|
|
260
269
|
|
|
261
270
|
- **Find:** `holt skill search <query>` fetches the index and lists skills whose name, description, or tags match (case-insensitive substring, name matches first). An empty query lists everything. It prints name, author, description, and source.
|
|
262
271
|
- **Install by name:** `holt skill add <name>` resolves the name in the registry to its `source`, then installs through the exact same clone/copy path as `holt skill add <url|path>`. A git URL or an existing local path is still installed directly, unchanged.
|
|
@@ -317,6 +326,8 @@ Install **merges** into any existing `hooks` config, never clobbers unrelated se
|
|
|
317
326
|
|
|
318
327
|
**Trusted-folder guard (important).** The two runtime hooks (`holt hook inject` and `holt hook capture`, invoked by Claude Code, not you) no-op **silently** unless the folder is a trusted Holt workspace with an existing `.holt/memory`. So Holt never injects your private notes into an unrelated project, and never creates memory in a folder you never set up. To make a folder ambient, run `holt init` (or `holt chat`) there once so it becomes trusted and gets its `.holt/memory`. The inject hook keeps stdout clean (only the context block is printed, since Claude Code injects it verbatim); all diagnostics go to stderr, and both hooks always exit 0 so a memory step never blocks or slows your prompt.
|
|
319
328
|
|
|
329
|
+
**Seeing what the hooks did (`~/.holt/hooks.log`).** Since the hooks stay silent, every `holt hook capture` run appends **one** line to `~/.holt/hooks.log` with the outcome or the exact skip reason (untrusted folder, no config, `extractFacts` off, no usable brain, missing or empty transcript, too few exchanges, or `saved N facts`), preceded by a line of the raw hook fields it received. Check this file first if capture seems to save nothing. `inject` uses the same log but stays quiet by default; set `HOLT_HOOK_DEBUG=1` to make it log too.
|
|
330
|
+
|
|
320
331
|
To uninstall, run `holt hook remove` (add `--project` if you installed with `--project`).
|
|
321
332
|
|
|
322
333
|
## Output format
|
|
@@ -433,6 +444,8 @@ holt voice clear # remove it
|
|
|
433
444
|
|
|
434
445
|
The interview asks you to pick a **depth** up front: `quick` (a few questions) or `detailed` (more). Questions are about **writing and communication style only**: tone, sentence length, first vs third person, emoji habits, words you love or ban, who you write for, and what you want to sound like and not sound like.
|
|
435
446
|
|
|
447
|
+
Changed your mind at the first prompt? Answer `n`, `skip`, `cancel`, or just press enter to back out cleanly. Nothing is saved and no profile is built; run `holt voice` again whenever you like. Inside the question list, an empty answer skips only that one question.
|
|
448
|
+
|
|
436
449
|
Privacy is a hard rule. The interview never asks for personal details (no name, job, location, or life story). If you happen to mention something personal in an answer, Holt stores only what you typed and never asks follow ups.
|
|
437
450
|
|
|
438
451
|
You can also feed it real writing. `holt voice add <file>` reads a file; inside the interview you can paste a sample instead. Samples are stored as a hash and length by default; an excerpt is kept only if you say yes, so Holt can match your rhythm directly.
|
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,70 @@ function bar(fraction, width = 12) {
|
|
|
27
27
|
const filled = Math.round(f * width);
|
|
28
28
|
return c.green("\u2588".repeat(filled)) + c.dim("\u2591".repeat(width - filled));
|
|
29
29
|
}
|
|
30
|
+
var CSI = "\x1B[";
|
|
31
|
+
function createStatusBar() {
|
|
32
|
+
const out = process.stdout;
|
|
33
|
+
if (!out.isTTY) {
|
|
34
|
+
return { active: false, set: () => {
|
|
35
|
+
}, detach: () => {
|
|
36
|
+
} };
|
|
37
|
+
}
|
|
38
|
+
let text = "";
|
|
39
|
+
let detached = false;
|
|
40
|
+
const realWrite = out.write.bind(out);
|
|
41
|
+
const rows = () => out.rows || 24;
|
|
42
|
+
const paint = () => {
|
|
43
|
+
if (detached) return;
|
|
44
|
+
const r = rows();
|
|
45
|
+
realWrite(
|
|
46
|
+
`\x1B7${CSI}${r};1H${CSI}2K` + // clear the line
|
|
47
|
+
text + `\x1B8`
|
|
48
|
+
// restore cursor
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
const reserve = () => {
|
|
52
|
+
const r = rows();
|
|
53
|
+
realWrite(
|
|
54
|
+
`${CSI}1;${Math.max(1, r - 1)}r${CSI}${Math.max(1, r - 1)};1H`
|
|
55
|
+
// move cursor into the region
|
|
56
|
+
);
|
|
57
|
+
paint();
|
|
58
|
+
};
|
|
59
|
+
const onResize = () => {
|
|
60
|
+
if (!detached) reserve();
|
|
61
|
+
};
|
|
62
|
+
const detach = () => {
|
|
63
|
+
if (detached) return;
|
|
64
|
+
detached = true;
|
|
65
|
+
out.removeListener("resize", onResize);
|
|
66
|
+
process.removeListener("exit", detach);
|
|
67
|
+
process.removeListener("SIGINT", onSignal);
|
|
68
|
+
process.removeListener("SIGTERM", onSignal);
|
|
69
|
+
const r = rows();
|
|
70
|
+
realWrite(
|
|
71
|
+
`${CSI}r\x1B7${CSI}${r};1H${CSI}2K\x1B8${CSI}?25h`
|
|
72
|
+
// ensure cursor visible
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
const onSignal = (sig) => {
|
|
76
|
+
detach();
|
|
77
|
+
process.removeListener(sig, onSignal);
|
|
78
|
+
process.kill(process.pid, sig);
|
|
79
|
+
};
|
|
80
|
+
reserve();
|
|
81
|
+
out.on("resize", onResize);
|
|
82
|
+
process.once("exit", detach);
|
|
83
|
+
process.once("SIGINT", onSignal);
|
|
84
|
+
process.once("SIGTERM", onSignal);
|
|
85
|
+
return {
|
|
86
|
+
active: true,
|
|
87
|
+
set(next) {
|
|
88
|
+
text = next;
|
|
89
|
+
paint();
|
|
90
|
+
},
|
|
91
|
+
detach
|
|
92
|
+
};
|
|
93
|
+
}
|
|
30
94
|
function createReader() {
|
|
31
95
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
96
|
const buffer = [];
|
|
@@ -225,6 +289,7 @@ function renderPrompt(history, message, memory = []) {
|
|
|
225
289
|
const parts = [];
|
|
226
290
|
if (recent.length || memory.length) {
|
|
227
291
|
parts.push(
|
|
292
|
+
"You are the user's assistant, running through Holt, a tool that gives you durable memory of this user across sessions. Notes recalled from their past sessions may appear below; use them naturally and answer as their assistant.",
|
|
228
293
|
"You are continuing an ongoing conversation. Use the context below and reply only as the assistant to the final User message."
|
|
229
294
|
);
|
|
230
295
|
}
|
|
@@ -1756,10 +1821,19 @@ var QUESTIONS = [
|
|
|
1756
1821
|
{ key: "avoid", q: "What do you NOT want to sound like? (e.g. corporate, salesy, robotic)", depth: "detailed" },
|
|
1757
1822
|
{ key: "signature", q: "Any signature moves? (openings you like, how you end, humor, analogies)", depth: "detailed" }
|
|
1758
1823
|
];
|
|
1824
|
+
function isSkip(a) {
|
|
1825
|
+
if (a === null) return true;
|
|
1826
|
+
const s = a.trim().toLowerCase();
|
|
1827
|
+
return s === "" || s === "n" || s === "no" || s === "skip" || s === "cancel" || s === "q" || s === "quit";
|
|
1828
|
+
}
|
|
1829
|
+
function skipNotice() {
|
|
1830
|
+
console.log(c.dim("\n Skipped. Run ") + c.accent("holt voice") + c.dim(" anytime.\n"));
|
|
1831
|
+
}
|
|
1759
1832
|
async function askDepth(ask) {
|
|
1760
1833
|
const a = await ask('Depth? "quick" (a few questions) or "detailed" (more): [quick] ');
|
|
1761
1834
|
if (a === null) return null;
|
|
1762
1835
|
const s = a.trim().toLowerCase();
|
|
1836
|
+
if (s === "n" || s === "no" || s === "skip" || s === "cancel" || s === "q" || s === "quit") return null;
|
|
1763
1837
|
return s === "detailed" || s === "d" ? "detailed" : "quick";
|
|
1764
1838
|
}
|
|
1765
1839
|
async function runQuestions(ask, depth) {
|
|
@@ -1887,13 +1961,21 @@ async function voiceInterview() {
|
|
|
1887
1961
|
const v = existing ?? emptyVoice();
|
|
1888
1962
|
console.log("\n" + c.accent("Teach Holt your writing voice"));
|
|
1889
1963
|
console.log(c.dim(" Style only. No personal questions. You can do the interview, share samples, or both.\n"));
|
|
1890
|
-
const
|
|
1891
|
-
|
|
1964
|
+
const modeRaw = await ask("Do the [i]nterview, add [s]amples, or [b]oth? [i] ");
|
|
1965
|
+
if (isSkip(modeRaw)) {
|
|
1966
|
+
close();
|
|
1967
|
+
skipNotice();
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const mode = (modeRaw ?? "").trim().toLowerCase();
|
|
1971
|
+
const wantInterview = mode === "i" || mode === "b" || mode === "interview" || mode === "both";
|
|
1892
1972
|
const wantSamples = mode === "s" || mode === "b" || mode === "samples" || mode === "both";
|
|
1893
|
-
|
|
1973
|
+
const wantInterviewOrDefault = wantInterview || !wantSamples;
|
|
1974
|
+
if (wantInterviewOrDefault) {
|
|
1894
1975
|
const depth = await askDepth(ask);
|
|
1895
1976
|
if (depth === null) {
|
|
1896
1977
|
close();
|
|
1978
|
+
skipNotice();
|
|
1897
1979
|
return;
|
|
1898
1980
|
}
|
|
1899
1981
|
v.depth = depth;
|
|
@@ -1917,6 +1999,11 @@ async function voiceInterview() {
|
|
|
1917
1999
|
}
|
|
1918
2000
|
}
|
|
1919
2001
|
close();
|
|
2002
|
+
const gotAnswers = v.answers.some((a) => a.answer.trim());
|
|
2003
|
+
if (!gotAnswers && v.samples.length === 0) {
|
|
2004
|
+
skipNotice();
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
1920
2007
|
saveVoice(v);
|
|
1921
2008
|
console.log(c.dim("\n Building your style profile from a brain..."));
|
|
1922
2009
|
const synthed = await synthesizeVoice(v);
|
|
@@ -2218,7 +2305,10 @@ async function extractAndSaveFacts(active, cfg, history, session) {
|
|
|
2218
2305
|
try {
|
|
2219
2306
|
if (Math.floor(history.length / 2) < MIN_EXCHANGES_FOR_EXTRACTION) return 0;
|
|
2220
2307
|
const prompt = buildExtractionPrompt(history);
|
|
2221
|
-
const res = active.kind === "cli" ? await runBrain(
|
|
2308
|
+
const res = active.kind === "cli" ? await runBrain(
|
|
2309
|
+
active.commandOverride ? { ...cfg.brains[active.id], command: active.commandOverride } : cfg.brains[active.id],
|
|
2310
|
+
prompt
|
|
2311
|
+
) : await runApiBrain(active.brain, prompt);
|
|
2222
2312
|
if (!res.ok) return 0;
|
|
2223
2313
|
const facts = parseFacts(res.text);
|
|
2224
2314
|
let saved = 0;
|
|
@@ -2896,9 +2986,8 @@ function saveReply(reply, format, name) {
|
|
|
2896
2986
|
function statusLine(brainLabel, history) {
|
|
2897
2987
|
const live = Math.min(history.length, MAX_REPLAY_TURNS);
|
|
2898
2988
|
const frac = MAX_REPLAY_TURNS ? live / MAX_REPLAY_TURNS : 0;
|
|
2899
|
-
const pct = Math.round(frac * 100);
|
|
2900
2989
|
const mem = memStats().turns;
|
|
2901
|
-
return c.dim(" " + brainLabel + " ") + c.dim("[") + bar(frac) + c.dim("]") + c.dim(` ${
|
|
2990
|
+
return c.dim(" " + brainLabel + " ") + c.dim("[") + bar(frac) + c.dim("]") + c.dim(` recent ${live}/${MAX_REPLAY_TURNS} \xB7 ${mem} in memory`);
|
|
2902
2991
|
}
|
|
2903
2992
|
function help() {
|
|
2904
2993
|
console.log(c.dim([
|
|
@@ -2954,6 +3043,12 @@ async function chat() {
|
|
|
2954
3043
|
const session = newSessionId();
|
|
2955
3044
|
const history = [];
|
|
2956
3045
|
let lastReply = "";
|
|
3046
|
+
const sb = createStatusBar();
|
|
3047
|
+
const showStatus = () => {
|
|
3048
|
+
const line = statusLine(active.label, history);
|
|
3049
|
+
if (sb.active) sb.set(line);
|
|
3050
|
+
else console.log(line + "\n");
|
|
3051
|
+
};
|
|
2957
3052
|
const embedOk = await embeddingsAvailable();
|
|
2958
3053
|
const stats = memStats();
|
|
2959
3054
|
console.log("\n" + c.accent("Holt") + c.dim(` brain: ${active.label}`));
|
|
@@ -2964,7 +3059,7 @@ async function chat() {
|
|
|
2964
3059
|
console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
|
|
2965
3060
|
}
|
|
2966
3061
|
console.log(c.dim("Type a message. Commands: /brain /memory /output /save /setting /clear /help /exit"));
|
|
2967
|
-
|
|
3062
|
+
showStatus();
|
|
2968
3063
|
while (true) {
|
|
2969
3064
|
const raw = await ask(c.accent("\u203A "));
|
|
2970
3065
|
if (raw === null) break;
|
|
@@ -3107,11 +3202,12 @@ async function chat() {
|
|
|
3107
3202
|
const now = Date.now();
|
|
3108
3203
|
appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session, role: "user", content: line, emb: await embed(line) ?? void 0 });
|
|
3109
3204
|
appendTurn({ id: randomUUID3().slice(0, 8), ts: now, session, role: "assistant", content: res.text, emb: await embed(res.text) ?? void 0 });
|
|
3110
|
-
|
|
3205
|
+
showStatus();
|
|
3111
3206
|
} else {
|
|
3112
3207
|
console.log(c.red("\n " + res.text + "\n"));
|
|
3113
3208
|
}
|
|
3114
3209
|
}
|
|
3210
|
+
sb.detach();
|
|
3115
3211
|
if (cfg.memory?.extractFacts !== false) {
|
|
3116
3212
|
try {
|
|
3117
3213
|
const arg = active.kind === "cli" ? { kind: "cli", id: active.id } : { kind: "api", brain: active.brain };
|
|
@@ -3702,7 +3798,7 @@ import {
|
|
|
3702
3798
|
cpSync,
|
|
3703
3799
|
mkdtempSync
|
|
3704
3800
|
} from "fs";
|
|
3705
|
-
import { join as join12, resolve as resolve3 } from "path";
|
|
3801
|
+
import { join as join12, resolve as resolve3, sep, isAbsolute as isAbsolute3 } from "path";
|
|
3706
3802
|
import { tmpdir } from "os";
|
|
3707
3803
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3708
3804
|
|
|
@@ -3866,7 +3962,7 @@ function usage() {
|
|
|
3866
3962
|
" holt skill show <name> print a skill",
|
|
3867
3963
|
" holt skill create <name> [--global] scaffold a new skill",
|
|
3868
3964
|
" holt skill search <query> find skills in the registry",
|
|
3869
|
-
" holt skill add <src|name> [--global] install from a git URL, path, or registry name",
|
|
3965
|
+
" holt skill add <src|name> [--global] install from a git URL, git-url#subdir, path, or registry name",
|
|
3870
3966
|
" holt skill publish [<name>] prepare a skill for the registry (prints a PR entry)",
|
|
3871
3967
|
" holt skill remove <name> delete a skill",
|
|
3872
3968
|
"",
|
|
@@ -3902,6 +3998,20 @@ var SKILL_TEMPLATE = (name) => [
|
|
|
3902
3998
|
function isGitUrl(s) {
|
|
3903
3999
|
return s.includes("://") || s.endsWith(".git") || s.startsWith("git@");
|
|
3904
4000
|
}
|
|
4001
|
+
function splitSubpath(source) {
|
|
4002
|
+
const i = source.indexOf("#");
|
|
4003
|
+
if (i === -1) return { base: source, subpath: "" };
|
|
4004
|
+
return { base: source.slice(0, i), subpath: source.slice(i + 1) };
|
|
4005
|
+
}
|
|
4006
|
+
function resolveSubpathDir(fetched, subpath) {
|
|
4007
|
+
if (isAbsolute3(subpath)) return null;
|
|
4008
|
+
const segments = subpath.split(/[\\/]/);
|
|
4009
|
+
if (segments.some((s) => s === "..")) return null;
|
|
4010
|
+
const target = resolve3(fetched, subpath);
|
|
4011
|
+
const root = resolve3(fetched);
|
|
4012
|
+
if (target !== root && !target.startsWith(root + sep)) return null;
|
|
4013
|
+
return target;
|
|
4014
|
+
}
|
|
3905
4015
|
function findSkillDir(root) {
|
|
3906
4016
|
if (existsSync12(join12(root, "SKILL.md"))) return root;
|
|
3907
4017
|
let subs = [];
|
|
@@ -3986,14 +4096,15 @@ function cmdCreate(name, global) {
|
|
|
3986
4096
|
console.log(c.dim(' Edit it, then run it in chat with "/skill ' + clean + '".\n'));
|
|
3987
4097
|
}
|
|
3988
4098
|
function installFromSource(source, scope) {
|
|
4099
|
+
const { base, subpath } = splitSubpath(source);
|
|
3989
4100
|
let fetched = "";
|
|
3990
4101
|
let tempDir = "";
|
|
3991
4102
|
try {
|
|
3992
|
-
if (isGitUrl(
|
|
4103
|
+
if (isGitUrl(base)) {
|
|
3993
4104
|
tempDir = mkdtempSync(join12(tmpdir(), "holt-skill-"));
|
|
3994
4105
|
console.log(c.dim(`
|
|
3995
|
-
Cloning ${
|
|
3996
|
-
const res = spawnSync2("git", ["clone", "--depth", "1",
|
|
4106
|
+
Cloning ${base} ...`));
|
|
4107
|
+
const res = spawnSync2("git", ["clone", "--depth", "1", base, tempDir], { stdio: "ignore" });
|
|
3997
4108
|
if (res.status !== 0) {
|
|
3998
4109
|
console.error(c.red(" Clone failed. Check the URL and that git is installed.\n"));
|
|
3999
4110
|
process.exitCode = 1;
|
|
@@ -4001,7 +4112,7 @@ function installFromSource(source, scope) {
|
|
|
4001
4112
|
}
|
|
4002
4113
|
fetched = tempDir;
|
|
4003
4114
|
} else {
|
|
4004
|
-
fetched = resolve3(
|
|
4115
|
+
fetched = resolve3(base);
|
|
4005
4116
|
if (!existsSync12(fetched)) {
|
|
4006
4117
|
console.error(c.red(`
|
|
4007
4118
|
No such path: ${fetched}
|
|
@@ -4010,9 +4121,31 @@ function installFromSource(source, scope) {
|
|
|
4010
4121
|
return false;
|
|
4011
4122
|
}
|
|
4012
4123
|
}
|
|
4013
|
-
|
|
4124
|
+
let searchRoot = fetched;
|
|
4125
|
+
if (subpath) {
|
|
4126
|
+
const target = resolveSubpathDir(fetched, subpath);
|
|
4127
|
+
if (!target) {
|
|
4128
|
+
console.error(c.red(`
|
|
4129
|
+
Refusing unsafe subpath "${subpath}": it must stay inside the source (no absolute or "..").
|
|
4130
|
+
`));
|
|
4131
|
+
process.exitCode = 1;
|
|
4132
|
+
return false;
|
|
4133
|
+
}
|
|
4134
|
+
if (!existsSync12(target) || !statSync5(target).isDirectory()) {
|
|
4135
|
+
console.error(c.red(`
|
|
4136
|
+
Subpath "${subpath}" does not exist in the source.
|
|
4137
|
+
`));
|
|
4138
|
+
process.exitCode = 1;
|
|
4139
|
+
return false;
|
|
4140
|
+
}
|
|
4141
|
+
searchRoot = target;
|
|
4142
|
+
}
|
|
4143
|
+
const skillDir = findSkillDir(searchRoot);
|
|
4014
4144
|
if (!skillDir) {
|
|
4015
|
-
|
|
4145
|
+
const where = subpath ? `subpath "${subpath}"` : "the source root or in a single subfolder";
|
|
4146
|
+
console.error(c.red(`
|
|
4147
|
+
Could not find a SKILL.md at ${where}.
|
|
4148
|
+
`));
|
|
4016
4149
|
process.exitCode = 1;
|
|
4017
4150
|
return false;
|
|
4018
4151
|
}
|
|
@@ -4056,11 +4189,12 @@ function installFromSource(source, scope) {
|
|
|
4056
4189
|
}
|
|
4057
4190
|
async function cmdAdd(source, global, refresh) {
|
|
4058
4191
|
if (!source) {
|
|
4059
|
-
console.log(c.dim("\n Usage: holt skill add <git-url|path|name> [--global]\n"));
|
|
4192
|
+
console.log(c.dim("\n Usage: holt skill add <git-url|git-url#subdir|path|name> [--global]\n"));
|
|
4060
4193
|
return;
|
|
4061
4194
|
}
|
|
4062
4195
|
const scope = global ? "global" : "workspace";
|
|
4063
|
-
|
|
4196
|
+
const { base } = splitSubpath(source);
|
|
4197
|
+
if (isGitUrl(base) || existsSync12(resolve3(base))) {
|
|
4064
4198
|
installFromSource(source, scope);
|
|
4065
4199
|
return;
|
|
4066
4200
|
}
|
|
@@ -5061,7 +5195,7 @@ function renderGraphHtml(graph2, meta) {
|
|
|
5061
5195
|
|
|
5062
5196
|
// src/ingest.ts
|
|
5063
5197
|
import { readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as statSync6 } from "fs";
|
|
5064
|
-
import { join as join13, relative, sep, extname, dirname as dirname3, basename as basename2 } from "path";
|
|
5198
|
+
import { join as join13, relative, sep as sep2, extname, dirname as dirname3, basename as basename2 } from "path";
|
|
5065
5199
|
var CODE_EXT = /* @__PURE__ */ new Set([
|
|
5066
5200
|
"js",
|
|
5067
5201
|
"jsx",
|
|
@@ -5112,7 +5246,7 @@ var MAX_FILES = 2e3;
|
|
|
5112
5246
|
var MAX_FILE_BYTES = 400 * 1024;
|
|
5113
5247
|
var MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
5114
5248
|
function toPosix(p) {
|
|
5115
|
-
return
|
|
5249
|
+
return sep2 === "/" ? p : p.split(sep2).join("/");
|
|
5116
5250
|
}
|
|
5117
5251
|
function ingestFolder(root, want = { code: true, docs: true }) {
|
|
5118
5252
|
const files = [];
|
|
@@ -5815,7 +5949,7 @@ function holtTools() {
|
|
|
5815
5949
|
}
|
|
5816
5950
|
|
|
5817
5951
|
// src/version.ts
|
|
5818
|
-
var VERSION = "0.
|
|
5952
|
+
var VERSION = "0.12.1";
|
|
5819
5953
|
|
|
5820
5954
|
// src/commands/mcp.ts
|
|
5821
5955
|
function printSetup() {
|
|
@@ -5861,7 +5995,8 @@ async function mcp(sub, rest = []) {
|
|
|
5861
5995
|
}
|
|
5862
5996
|
|
|
5863
5997
|
// src/commands/hook.ts
|
|
5864
|
-
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync11, copyFileSync, realpathSync as realpathSync2 } from "fs";
|
|
5998
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync14, mkdirSync as mkdirSync11, copyFileSync, realpathSync as realpathSync2, appendFileSync as appendFileSync2, statSync as statSync7 } from "fs";
|
|
5999
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
5865
6000
|
import { join as join15, dirname as dirname5 } from "path";
|
|
5866
6001
|
import { homedir as homedir3 } from "os";
|
|
5867
6002
|
var EVENT = {
|
|
@@ -6076,25 +6211,87 @@ function guardTrusted(real) {
|
|
|
6076
6211
|
if (!existsSync14(memDir())) return false;
|
|
6077
6212
|
return true;
|
|
6078
6213
|
}
|
|
6214
|
+
function hookLog(msg) {
|
|
6215
|
+
try {
|
|
6216
|
+
mkdirSync11(GLOBAL_DIR, { recursive: true });
|
|
6217
|
+
appendFileSync2(join15(GLOBAL_DIR, "hooks.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
6218
|
+
`, "utf8");
|
|
6219
|
+
} catch {
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
function fileBytes(path) {
|
|
6223
|
+
if (!path) return 0;
|
|
6224
|
+
try {
|
|
6225
|
+
return statSync7(path).size;
|
|
6226
|
+
} catch {
|
|
6227
|
+
return 0;
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6079
6230
|
async function inject() {
|
|
6231
|
+
const debug = !!process.env.HOLT_HOOK_DEBUG;
|
|
6232
|
+
const dlog = (m) => {
|
|
6233
|
+
if (debug) hookLog(m);
|
|
6234
|
+
};
|
|
6080
6235
|
try {
|
|
6081
6236
|
const raw = await readStdin(2e3);
|
|
6082
6237
|
const data = parseHookInput(raw);
|
|
6238
|
+
dlog(
|
|
6239
|
+
`inject cwd=${data.cwd ?? "(none)"} event=${data.hook_event_name ?? "UserPromptSubmit"} hasCwd=${!!data.cwd} promptLen=${(data.prompt || "").trim().length}`
|
|
6240
|
+
);
|
|
6083
6241
|
const prompt = (data.prompt || "").trim();
|
|
6084
|
-
if (prompt.length < 3)
|
|
6242
|
+
if (prompt.length < 3) {
|
|
6243
|
+
dlog("inject -> prompt too short");
|
|
6244
|
+
return;
|
|
6245
|
+
}
|
|
6085
6246
|
const real = enterFolder(data.cwd);
|
|
6086
|
-
if (!real)
|
|
6087
|
-
|
|
6247
|
+
if (!real) {
|
|
6248
|
+
dlog("inject -> no cwd");
|
|
6249
|
+
return;
|
|
6250
|
+
}
|
|
6251
|
+
if (!guardTrusted(real)) {
|
|
6252
|
+
dlog(`inject -> untrusted or no .holt/memory (real=${real})`);
|
|
6253
|
+
return;
|
|
6254
|
+
}
|
|
6088
6255
|
const session = data.session_id || newSessionId();
|
|
6089
6256
|
const hits = await recall(prompt, session, 4);
|
|
6090
|
-
if (!hits.length)
|
|
6257
|
+
if (!hits.length) {
|
|
6258
|
+
dlog("inject -> recalled 0 notes");
|
|
6259
|
+
return;
|
|
6260
|
+
}
|
|
6091
6261
|
const lines = hits.map((h) => `- ${h.turn.content.replace(/\s+/g, " ").trim().slice(0, 500)}`);
|
|
6092
6262
|
process.stdout.write(
|
|
6093
|
-
[
|
|
6263
|
+
[
|
|
6264
|
+
"Background on this user, recalled from earlier sessions (use it directly, do not mention looking it up):",
|
|
6265
|
+
...lines,
|
|
6266
|
+
""
|
|
6267
|
+
].join("\n")
|
|
6094
6268
|
);
|
|
6269
|
+
dlog(`inject -> injected ${hits.length} note(s)`);
|
|
6095
6270
|
} catch (e) {
|
|
6096
6271
|
process.stderr.write("holt hook inject: " + (e instanceof Error ? e.message : String(e)) + "\n");
|
|
6272
|
+
dlog("inject -> error: " + (e instanceof Error ? e.message : String(e)));
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
function countLines(raw) {
|
|
6276
|
+
let n = 0;
|
|
6277
|
+
for (const line of raw.split("\n")) if (line.trim()) n++;
|
|
6278
|
+
return n;
|
|
6279
|
+
}
|
|
6280
|
+
function textFromContent(content) {
|
|
6281
|
+
if (typeof content === "string") return content.trim();
|
|
6282
|
+
if (!Array.isArray(content)) return "";
|
|
6283
|
+
const parts = [];
|
|
6284
|
+
for (const b of content) {
|
|
6285
|
+
if (typeof b === "string") {
|
|
6286
|
+
parts.push(b);
|
|
6287
|
+
continue;
|
|
6288
|
+
}
|
|
6289
|
+
if (!b || typeof b !== "object") continue;
|
|
6290
|
+
const blk = b;
|
|
6291
|
+
if (blk.type === "tool_use" || blk.type === "tool_result" || blk.type === "thinking") continue;
|
|
6292
|
+
if (typeof blk.text === "string") parts.push(blk.text);
|
|
6097
6293
|
}
|
|
6294
|
+
return parts.join(" ").trim();
|
|
6098
6295
|
}
|
|
6099
6296
|
function readTranscript(path) {
|
|
6100
6297
|
const out = [];
|
|
@@ -6112,56 +6309,133 @@ function readTranscript(path) {
|
|
|
6112
6309
|
} catch {
|
|
6113
6310
|
continue;
|
|
6114
6311
|
}
|
|
6312
|
+
if (!rec || typeof rec !== "object") continue;
|
|
6115
6313
|
const d = rec;
|
|
6116
|
-
|
|
6117
|
-
const m = d.message;
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
if (
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
text = m.content.map((b) => {
|
|
6124
|
-
if (typeof b === "string") return b;
|
|
6125
|
-
if (b && typeof b.text === "string") return b.text;
|
|
6126
|
-
return "";
|
|
6127
|
-
}).join(" ").trim();
|
|
6128
|
-
}
|
|
6314
|
+
const topType = d.type === "user" || d.type === "assistant" ? d.type : void 0;
|
|
6315
|
+
const m = d.message && typeof d.message === "object" ? d.message : void 0;
|
|
6316
|
+
const msgRole = m && (m.role === "user" || m.role === "assistant") ? m.role : void 0;
|
|
6317
|
+
const role = msgRole ?? topType;
|
|
6318
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
6319
|
+
let text = m ? textFromContent(m.content) : "";
|
|
6320
|
+
if (!text) text = textFromContent(d.content);
|
|
6129
6321
|
text = text.trim();
|
|
6130
6322
|
if (text.length < 3) continue;
|
|
6131
|
-
out.push({ role
|
|
6323
|
+
out.push({ role, content: text });
|
|
6132
6324
|
}
|
|
6133
6325
|
return out;
|
|
6134
6326
|
}
|
|
6327
|
+
function resolveCommandPath(command) {
|
|
6328
|
+
if (command.startsWith("/") && existsSync14(command)) return command;
|
|
6329
|
+
try {
|
|
6330
|
+
const shell = process.env.SHELL || "/bin/sh";
|
|
6331
|
+
const res = spawnSync3(shell, ["-lc", "command -v " + command], { encoding: "utf8" });
|
|
6332
|
+
if (res.status === 0) {
|
|
6333
|
+
const p = (res.stdout || "").trim().split("\n")[0]?.trim();
|
|
6334
|
+
if (p && p.startsWith("/") && existsSync14(p)) return p;
|
|
6335
|
+
}
|
|
6336
|
+
} catch {
|
|
6337
|
+
}
|
|
6338
|
+
const home = homedir3();
|
|
6339
|
+
const dirs = [
|
|
6340
|
+
join15(home, ".local", "bin"),
|
|
6341
|
+
"/opt/homebrew/bin",
|
|
6342
|
+
"/usr/local/bin",
|
|
6343
|
+
join15(home, ".npm-global", "bin"),
|
|
6344
|
+
"/usr/bin",
|
|
6345
|
+
"/bin"
|
|
6346
|
+
];
|
|
6347
|
+
for (const dir of dirs) {
|
|
6348
|
+
const candidate = join15(dir, command);
|
|
6349
|
+
if (existsSync14(candidate)) return candidate;
|
|
6350
|
+
}
|
|
6351
|
+
return null;
|
|
6352
|
+
}
|
|
6135
6353
|
function resolveExtractionBrain(cfg) {
|
|
6136
6354
|
const id = cfg.defaultBrain;
|
|
6137
|
-
|
|
6355
|
+
const diag = { defaultBrain: id ?? "(none)", command: "(none)", whichFound: false, resolvedPath: null };
|
|
6356
|
+
if (!id) return { brain: null, diag };
|
|
6138
6357
|
if (BRAIN_IDS.includes(id)) {
|
|
6139
6358
|
const bid = id;
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
return {
|
|
6359
|
+
const command = cfg.brains[bid].command;
|
|
6360
|
+
diag.command = command;
|
|
6361
|
+
if (!cfg.brains[bid].enabled) return { brain: null, diag };
|
|
6362
|
+
const whichFound = isInstalled(command);
|
|
6363
|
+
diag.whichFound = whichFound;
|
|
6364
|
+
if (whichFound) {
|
|
6365
|
+
return { brain: { kind: "cli", id: bid }, diag };
|
|
6366
|
+
}
|
|
6367
|
+
const resolved = resolveCommandPath(command);
|
|
6368
|
+
diag.resolvedPath = resolved;
|
|
6369
|
+
if (resolved) {
|
|
6370
|
+
return { brain: { kind: "cli", id: bid, commandOverride: resolved }, diag };
|
|
6371
|
+
}
|
|
6372
|
+
return { brain: null, diag };
|
|
6143
6373
|
}
|
|
6144
6374
|
const api = findApiBrain(cfg, id);
|
|
6145
|
-
if (api && resolveApiKey(api)) return { kind: "api", brain: api };
|
|
6146
|
-
return null;
|
|
6375
|
+
if (api && resolveApiKey(api)) return { brain: { kind: "api", brain: api }, diag };
|
|
6376
|
+
return { brain: null, diag };
|
|
6147
6377
|
}
|
|
6148
6378
|
async function capture() {
|
|
6379
|
+
const cwdForLog = process.cwd();
|
|
6149
6380
|
try {
|
|
6150
6381
|
const raw = await readStdin(3e3);
|
|
6151
6382
|
const data = parseHookInput(raw);
|
|
6383
|
+
hookLog(
|
|
6384
|
+
`capture-fields event=${data.hook_event_name ?? "Stop"} hasCwd=${!!data.cwd} hasTranscriptPath=${!!data.transcript_path} transcriptBytes=${fileBytes(data.transcript_path)} stdinBytes=${raw.length}`
|
|
6385
|
+
);
|
|
6152
6386
|
const real = enterFolder(data.cwd);
|
|
6153
|
-
if (!real)
|
|
6154
|
-
|
|
6387
|
+
if (!real) {
|
|
6388
|
+
hookLog(`capture cwd=${data.cwd ?? cwdForLog} event=Stop -> no cwd`);
|
|
6389
|
+
return;
|
|
6390
|
+
}
|
|
6391
|
+
if (!guardTrusted(real)) {
|
|
6392
|
+
hookLog(`capture cwd=${real} event=Stop -> untrusted or no .holt/memory (real=${real})`);
|
|
6393
|
+
return;
|
|
6394
|
+
}
|
|
6155
6395
|
const cfg = loadConfig();
|
|
6156
|
-
if (!cfg)
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
if (
|
|
6396
|
+
if (!cfg) {
|
|
6397
|
+
hookLog(`capture cwd=${real} event=Stop -> no config`);
|
|
6398
|
+
return;
|
|
6399
|
+
}
|
|
6400
|
+
if (cfg.memory?.extractFacts === false) {
|
|
6401
|
+
hookLog(`capture cwd=${real} event=Stop -> extractFacts off`);
|
|
6402
|
+
return;
|
|
6403
|
+
}
|
|
6404
|
+
const { brain, diag } = resolveExtractionBrain(cfg);
|
|
6405
|
+
if (!brain) {
|
|
6406
|
+
hookLog(
|
|
6407
|
+
`capture cwd=${real} event=Stop -> no usable brain (defaultBrain=${diag.defaultBrain} command=${diag.command} whichFound=${diag.whichFound} resolvedPath=${diag.resolvedPath ?? "none"})`
|
|
6408
|
+
);
|
|
6409
|
+
return;
|
|
6410
|
+
}
|
|
6411
|
+
if (brain.kind === "cli" && brain.commandOverride) {
|
|
6412
|
+
hookLog(`capture cwd=${real} event=Stop -> brain resolved via absolute path (resolvedPath=${brain.commandOverride})`);
|
|
6413
|
+
}
|
|
6414
|
+
if (!data.transcript_path) {
|
|
6415
|
+
hookLog(`capture cwd=${real} event=Stop -> no transcript_path`);
|
|
6416
|
+
return;
|
|
6417
|
+
}
|
|
6161
6418
|
const history = readTranscript(data.transcript_path);
|
|
6162
|
-
if (!history.length)
|
|
6419
|
+
if (!history.length) {
|
|
6420
|
+
let bytes = 0;
|
|
6421
|
+
let lines = 0;
|
|
6422
|
+
try {
|
|
6423
|
+
const rawT = readFileSync15(data.transcript_path, "utf8");
|
|
6424
|
+
bytes = Buffer.byteLength(rawT, "utf8");
|
|
6425
|
+
lines = countLines(rawT);
|
|
6426
|
+
} catch {
|
|
6427
|
+
}
|
|
6428
|
+
hookLog(`capture cwd=${real} event=Stop -> transcript parsed 0 turns (path=${data.transcript_path} bytes=${bytes} lines=${lines})`);
|
|
6429
|
+
return;
|
|
6430
|
+
}
|
|
6431
|
+
const exchanges = Math.floor(history.length / 2);
|
|
6432
|
+
if (exchanges < MIN_EXCHANGES_FOR_EXTRACTION) {
|
|
6433
|
+
hookLog(`capture cwd=${real} event=Stop -> only ${exchanges} exchange(s) (<${MIN_EXCHANGES_FOR_EXTRACTION}, skipped)`);
|
|
6434
|
+
return;
|
|
6435
|
+
}
|
|
6163
6436
|
const session = "hook-" + (data.session_id || newSessionId());
|
|
6164
6437
|
const n = await extractAndSaveFacts(brain, cfg, history, session);
|
|
6438
|
+
hookLog(`capture cwd=${real} event=Stop -> saved ${n} fact${n === 1 ? "" : "s"}`);
|
|
6165
6439
|
if (n > 0) process.stderr.write(`holt hook capture: saved ${n} fact${n === 1 ? "" : "s"} to ${memDir()}
|
|
6166
6440
|
`);
|
|
6167
6441
|
if (cfg.wiki?.autoSync) {
|
|
@@ -6174,7 +6448,9 @@ async function capture() {
|
|
|
6174
6448
|
}
|
|
6175
6449
|
}
|
|
6176
6450
|
} catch (e) {
|
|
6177
|
-
|
|
6451
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
6452
|
+
process.stderr.write("holt hook capture: " + msg + "\n");
|
|
6453
|
+
hookLog(`capture cwd=${cwdForLog} event=Stop -> error: ${msg}`);
|
|
6178
6454
|
}
|
|
6179
6455
|
}
|
|
6180
6456
|
var USAGE2 = [
|
|
@@ -6332,7 +6608,7 @@ async function run(args) {
|
|
|
6332
6608
|
}
|
|
6333
6609
|
|
|
6334
6610
|
// src/commands/schedule.ts
|
|
6335
|
-
import { spawnSync as
|
|
6611
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
6336
6612
|
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync13, rmSync as rmSync6, existsSync as existsSync15 } from "fs";
|
|
6337
6613
|
import { dirname as dirname6 } from "path";
|
|
6338
6614
|
|
|
@@ -6341,7 +6617,7 @@ import { homedir as homedir4 } from "os";
|
|
|
6341
6617
|
import { join as join16 } from "path";
|
|
6342
6618
|
import { readFileSync as readFileSync16, writeFileSync as writeFileSync14, mkdirSync as mkdirSync12 } from "fs";
|
|
6343
6619
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
6344
|
-
import { spawnSync as
|
|
6620
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
6345
6621
|
function schedulesPath() {
|
|
6346
6622
|
return join16(GLOBAL_DIR, "schedules.json");
|
|
6347
6623
|
}
|
|
@@ -6397,7 +6673,7 @@ function shQuote(s) {
|
|
|
6397
6673
|
function resolveHoltPath() {
|
|
6398
6674
|
try {
|
|
6399
6675
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
6400
|
-
const res =
|
|
6676
|
+
const res = spawnSync4(finder, ["holt"], { encoding: "utf8" });
|
|
6401
6677
|
if (res.status === 0 && typeof res.stdout === "string") {
|
|
6402
6678
|
const first = res.stdout.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
6403
6679
|
if (first) return first;
|
|
@@ -6500,7 +6776,7 @@ function installDarwin(job, holtPath) {
|
|
|
6500
6776
|
const path = plistPath(job.id);
|
|
6501
6777
|
mkdirSync13(dirname6(path), { recursive: true });
|
|
6502
6778
|
writeFileSync15(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
6503
|
-
const res =
|
|
6779
|
+
const res = spawnSync5("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
6504
6780
|
if (res.status === 0) {
|
|
6505
6781
|
console.log(c.green(" Loaded into launchd."));
|
|
6506
6782
|
} else {
|
|
@@ -6511,11 +6787,11 @@ function installDarwin(job, holtPath) {
|
|
|
6511
6787
|
console.log(c.dim(` plist: ${path}`));
|
|
6512
6788
|
}
|
|
6513
6789
|
function currentCrontab() {
|
|
6514
|
-
const res =
|
|
6790
|
+
const res = spawnSync5("crontab", ["-l"], { encoding: "utf8" });
|
|
6515
6791
|
return res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
|
|
6516
6792
|
}
|
|
6517
6793
|
function writeCrontab(body) {
|
|
6518
|
-
const res =
|
|
6794
|
+
const res = spawnSync5("crontab", ["-"], { input: body, encoding: "utf8" });
|
|
6519
6795
|
return res.status === 0;
|
|
6520
6796
|
}
|
|
6521
6797
|
function installLinux(job, holtPath) {
|
|
@@ -6588,7 +6864,7 @@ function listCmd2() {
|
|
|
6588
6864
|
function removeDarwin(id) {
|
|
6589
6865
|
const path = plistPath(id);
|
|
6590
6866
|
if (existsSync15(path)) {
|
|
6591
|
-
|
|
6867
|
+
spawnSync5("launchctl", ["unload", path], { encoding: "utf8" });
|
|
6592
6868
|
try {
|
|
6593
6869
|
rmSync6(path);
|
|
6594
6870
|
} catch {
|
|
@@ -6596,10 +6872,10 @@ function removeDarwin(id) {
|
|
|
6596
6872
|
}
|
|
6597
6873
|
}
|
|
6598
6874
|
function removeLinux(id) {
|
|
6599
|
-
const res =
|
|
6875
|
+
const res = spawnSync5("crontab", ["-l"], { encoding: "utf8" });
|
|
6600
6876
|
const body = res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
|
|
6601
6877
|
const next = stripCronLines(body, id);
|
|
6602
|
-
|
|
6878
|
+
spawnSync5("crontab", ["-"], { input: next, encoding: "utf8" });
|
|
6603
6879
|
}
|
|
6604
6880
|
function removeCmd(rest) {
|
|
6605
6881
|
const id = rest[0];
|
|
@@ -6649,7 +6925,7 @@ async function schedule(sub, rest = []) {
|
|
|
6649
6925
|
}
|
|
6650
6926
|
|
|
6651
6927
|
// src/commands/routine.ts
|
|
6652
|
-
import { spawnSync as
|
|
6928
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
6653
6929
|
import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, rmSync as rmSync7, existsSync as existsSync16 } from "fs";
|
|
6654
6930
|
import { dirname as dirname7, resolve as resolve6 } from "path";
|
|
6655
6931
|
|
|
@@ -6734,7 +7010,7 @@ function installDarwin2(job, holtPath) {
|
|
|
6734
7010
|
const path = plistPath(job.id);
|
|
6735
7011
|
mkdirSync15(dirname7(path), { recursive: true });
|
|
6736
7012
|
writeFileSync17(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
6737
|
-
const res =
|
|
7013
|
+
const res = spawnSync6("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
6738
7014
|
if (res.status === 0) {
|
|
6739
7015
|
console.log(c.green(" Loaded into launchd."));
|
|
6740
7016
|
} else {
|
|
@@ -6745,11 +7021,11 @@ function installDarwin2(job, holtPath) {
|
|
|
6745
7021
|
console.log(c.dim(` plist: ${path}`));
|
|
6746
7022
|
}
|
|
6747
7023
|
function currentCrontab2() {
|
|
6748
|
-
const res =
|
|
7024
|
+
const res = spawnSync6("crontab", ["-l"], { encoding: "utf8" });
|
|
6749
7025
|
return res.status === 0 && typeof res.stdout === "string" ? res.stdout : "";
|
|
6750
7026
|
}
|
|
6751
7027
|
function writeCrontab2(body) {
|
|
6752
|
-
const res =
|
|
7028
|
+
const res = spawnSync6("crontab", ["-"], { input: body, encoding: "utf8" });
|
|
6753
7029
|
return res.status === 0;
|
|
6754
7030
|
}
|
|
6755
7031
|
function installLinux2(job, holtPath) {
|
|
@@ -6778,7 +7054,7 @@ function installSchedule(r) {
|
|
|
6778
7054
|
function removeScheduleDarwin(id) {
|
|
6779
7055
|
const path = plistPath(id);
|
|
6780
7056
|
if (existsSync16(path)) {
|
|
6781
|
-
|
|
7057
|
+
spawnSync6("launchctl", ["unload", path], { encoding: "utf8" });
|
|
6782
7058
|
try {
|
|
6783
7059
|
rmSync7(path);
|
|
6784
7060
|
} catch {
|