@holt-os/holt 0.14.2 → 0.15.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 +57 -14
- package/dist/cli.js +932 -244
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/config.ts
|
|
4
|
-
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
4
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, renameSync } from "fs";
|
|
5
5
|
import { join as join2 } from "path";
|
|
6
6
|
|
|
7
7
|
// src/workspace.ts
|
|
@@ -11,6 +11,13 @@ import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
11
11
|
|
|
12
12
|
// src/ui.ts
|
|
13
13
|
import readline from "readline";
|
|
14
|
+
var quiet = false;
|
|
15
|
+
function setQuietMode(q) {
|
|
16
|
+
quiet = q;
|
|
17
|
+
}
|
|
18
|
+
function isQuietMode() {
|
|
19
|
+
return quiet;
|
|
20
|
+
}
|
|
14
21
|
var on = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
15
22
|
var wrap = (code) => (s) => on ? `\x1B[${code}m${s}\x1B[0m` : s;
|
|
16
23
|
var c = {
|
|
@@ -162,10 +169,24 @@ function ensureWsDir() {
|
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
// src/config.ts
|
|
172
|
+
var PROVIDER_WIRE = {
|
|
173
|
+
anthropic: "anthropic",
|
|
174
|
+
openai: "openai",
|
|
175
|
+
gemini: "gemini",
|
|
176
|
+
kimi: "openai",
|
|
177
|
+
grok: "openai"
|
|
178
|
+
};
|
|
179
|
+
var PROVIDER_BASE_URL = {
|
|
180
|
+
openai: "https://api.openai.com/v1",
|
|
181
|
+
kimi: "https://api.moonshot.ai/v1",
|
|
182
|
+
grok: "https://api.x.ai/v1"
|
|
183
|
+
};
|
|
165
184
|
var WIKI_DEFAULT_LOCAL_MODEL = "qwen2.5:7b";
|
|
166
185
|
var BRAIN_DEFS = {
|
|
167
186
|
claude: { label: "Claude Code", command: "claude", args: ["-p"] },
|
|
168
|
-
|
|
187
|
+
// --skip-git-repo-check: codex exec otherwise refuses to run in folders that
|
|
188
|
+
// are not git repositories, which is nearly every non-developer folder.
|
|
189
|
+
codex: { label: "Codex (OpenAI)", command: "codex", args: ["exec", "--skip-git-repo-check"] },
|
|
169
190
|
gemini: { label: "Gemini CLI", command: "gemini", args: ["-p"] }
|
|
170
191
|
};
|
|
171
192
|
var BRAIN_SETUP = {
|
|
@@ -174,16 +195,20 @@ var BRAIN_SETUP = {
|
|
|
174
195
|
gemini: { install: ["npm", "install", "-g", "@google/gemini-cli"], login: ["gemini"] }
|
|
175
196
|
};
|
|
176
197
|
var BRAIN_IDS = ["claude", "codex", "gemini"];
|
|
177
|
-
var PROVIDERS = ["anthropic", "openai", "gemini"];
|
|
198
|
+
var PROVIDERS = ["anthropic", "openai", "gemini", "kimi", "grok"];
|
|
178
199
|
var PROVIDER_MODEL_SUGGESTION = {
|
|
179
200
|
anthropic: "claude-sonnet-4-6",
|
|
180
201
|
openai: "gpt-5",
|
|
181
|
-
gemini: "gemini-2.5-flash"
|
|
202
|
+
gemini: "gemini-2.5-flash",
|
|
203
|
+
kimi: "kimi-k2-turbo-preview",
|
|
204
|
+
grok: "grok-4"
|
|
182
205
|
};
|
|
183
206
|
var PROVIDER_ENV = {
|
|
184
207
|
anthropic: "ANTHROPIC_API_KEY",
|
|
185
208
|
openai: "OPENAI_API_KEY",
|
|
186
|
-
gemini: "GEMINI_API_KEY"
|
|
209
|
+
gemini: "GEMINI_API_KEY",
|
|
210
|
+
kimi: "MOONSHOT_API_KEY",
|
|
211
|
+
grok: "XAI_API_KEY"
|
|
187
212
|
};
|
|
188
213
|
function defaultConfig() {
|
|
189
214
|
const brains = {};
|
|
@@ -201,14 +226,25 @@ function defaultConfig() {
|
|
|
201
226
|
wiki: { maintainer: "brain", localModel: WIKI_DEFAULT_LOCAL_MODEL }
|
|
202
227
|
};
|
|
203
228
|
}
|
|
229
|
+
var lastConfigRepair = null;
|
|
204
230
|
function loadConfig() {
|
|
205
231
|
const path = wsConfigPath();
|
|
206
232
|
if (!existsSync2(path)) return null;
|
|
233
|
+
let raw;
|
|
207
234
|
try {
|
|
208
|
-
|
|
235
|
+
raw = readFileSync2(path, "utf8");
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const cfg = JSON.parse(raw);
|
|
209
241
|
const base = defaultConfig();
|
|
210
242
|
const brains = cfg.brains ?? {};
|
|
211
243
|
for (const id of BRAIN_IDS) if (!brains[id]) brains[id] = base.brains[id];
|
|
244
|
+
const cx = brains.codex;
|
|
245
|
+
if (cx && cx.command === "codex" && Array.isArray(cx.args) && !cx.args.includes("--skip-git-repo-check")) {
|
|
246
|
+
cx.args = [...cx.args, "--skip-git-repo-check"];
|
|
247
|
+
}
|
|
212
248
|
const wikiIn = cfg.wiki ?? {};
|
|
213
249
|
const wiki = {
|
|
214
250
|
maintainer: wikiIn.maintainer === "local" ? "local" : "brain",
|
|
@@ -227,6 +263,18 @@ function loadConfig() {
|
|
|
227
263
|
wiki
|
|
228
264
|
};
|
|
229
265
|
} catch {
|
|
266
|
+
const backup2 = `${path}.broken-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[:T]/g, "")}`;
|
|
267
|
+
try {
|
|
268
|
+
renameSync(path, backup2);
|
|
269
|
+
lastConfigRepair = backup2;
|
|
270
|
+
if (!isQuietMode()) {
|
|
271
|
+
console.error(
|
|
272
|
+
c.red("\n Your Holt settings file was damaged, so it was moved aside.") + c.dim(`
|
|
273
|
+
Saved at: ${backup2}`) + c.dim("\n Holt will set this folder up fresh; your memory is untouched.\n")
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
230
278
|
return null;
|
|
231
279
|
}
|
|
232
280
|
}
|
|
@@ -310,7 +358,7 @@ function renderPrompt(history, message, memory = []) {
|
|
|
310
358
|
function looksLikeAuthError(text) {
|
|
311
359
|
try {
|
|
312
360
|
if (!text) return false;
|
|
313
|
-
const re = /run\s+\/login|not\s+logged\s+in|invalid\s+api\s+key|please\s+(?:log|sign)\s+in|authentication\s+(?:failed|required)|unauthorized|session\s+(?:has\s+)?expired|no\s+api\s+key|login\s+required|credit\s+balance\s+is\s+too\s+low/i;
|
|
361
|
+
const re = /run\s+\/login|not\s+logged\s+in|invalid\s+api\s+key|please\s+(?:log|sign)\s+in|authentication\s+(?:failed|required)|unauthorized|session\s+(?:has\s+)?expired|no\s+api\s+key|login\s+required|credit\s+balance\s+is\s+too\s+low|set\s+an\s+auth\s+method|GEMINI_API_KEY/i;
|
|
314
362
|
return re.test(text);
|
|
315
363
|
} catch {
|
|
316
364
|
return false;
|
|
@@ -967,7 +1015,8 @@ function dataLines(event) {
|
|
|
967
1015
|
}
|
|
968
1016
|
return out;
|
|
969
1017
|
}
|
|
970
|
-
function extractDelta(
|
|
1018
|
+
function extractDelta(providerIn, json) {
|
|
1019
|
+
const provider = PROVIDER_WIRE[providerIn];
|
|
971
1020
|
const j = json;
|
|
972
1021
|
if (provider === "anthropic") {
|
|
973
1022
|
if (j["type"] === "content_block_delta") {
|
|
@@ -987,7 +1036,8 @@ function extractDelta(provider, json) {
|
|
|
987
1036
|
return parts.map((p) => typeof p.text === "string" ? p.text : "").join("");
|
|
988
1037
|
}
|
|
989
1038
|
function buildRequest(brain, key, prompt) {
|
|
990
|
-
|
|
1039
|
+
const wire = PROVIDER_WIRE[brain.provider];
|
|
1040
|
+
if (wire === "anthropic") {
|
|
991
1041
|
return {
|
|
992
1042
|
url: "https://api.anthropic.com/v1/messages",
|
|
993
1043
|
headers: {
|
|
@@ -1003,9 +1053,10 @@ function buildRequest(brain, key, prompt) {
|
|
|
1003
1053
|
})
|
|
1004
1054
|
};
|
|
1005
1055
|
}
|
|
1006
|
-
if (
|
|
1056
|
+
if (wire === "openai") {
|
|
1057
|
+
const base = (brain.baseUrl ?? PROVIDER_BASE_URL[brain.provider] ?? "https://api.openai.com/v1").replace(/\/$/, "");
|
|
1007
1058
|
return {
|
|
1008
|
-
url:
|
|
1059
|
+
url: `${base}/chat/completions`,
|
|
1009
1060
|
headers: {
|
|
1010
1061
|
"content-type": "application/json",
|
|
1011
1062
|
authorization: `Bearer ${key}`
|
|
@@ -2220,9 +2271,9 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
|
|
|
2220
2271
|
}
|
|
2221
2272
|
|
|
2222
2273
|
// src/commands/launch.ts
|
|
2223
|
-
import { readFileSync as
|
|
2224
|
-
import { homedir as
|
|
2225
|
-
import { join as
|
|
2274
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync12, mkdirSync as mkdirSync11, copyFileSync as copyFileSync2 } from "fs";
|
|
2275
|
+
import { homedir as homedir7 } from "os";
|
|
2276
|
+
import { join as join13, dirname as dirname6 } from "path";
|
|
2226
2277
|
|
|
2227
2278
|
// src/scheduler.ts
|
|
2228
2279
|
import { homedir as homedir3 } from "os";
|
|
@@ -2363,11 +2414,340 @@ function ensureLogsDir() {
|
|
|
2363
2414
|
mkdirSync7(logsDir(), { recursive: true });
|
|
2364
2415
|
}
|
|
2365
2416
|
|
|
2417
|
+
// src/skillcompiler.ts
|
|
2418
|
+
import {
|
|
2419
|
+
readFileSync as readFileSync10,
|
|
2420
|
+
writeFileSync as writeFileSync8,
|
|
2421
|
+
existsSync as existsSync8,
|
|
2422
|
+
mkdirSync as mkdirSync8,
|
|
2423
|
+
readdirSync as readdirSync2,
|
|
2424
|
+
statSync as statSync3,
|
|
2425
|
+
rmSync as rmSync4,
|
|
2426
|
+
cpSync
|
|
2427
|
+
} from "fs";
|
|
2428
|
+
import { join as join9, dirname as dirname3 } from "path";
|
|
2429
|
+
import { homedir as homedir4 } from "os";
|
|
2430
|
+
import { createHash as createHash2 } from "crypto";
|
|
2431
|
+
var MARKER = "holt-managed skill";
|
|
2432
|
+
var CODEX_PREFIX = "holt-";
|
|
2433
|
+
function manifestPath(ws = workspace()) {
|
|
2434
|
+
return join9(wsHoltDir(ws), "skill-commands.json");
|
|
2435
|
+
}
|
|
2436
|
+
function emptyManifest() {
|
|
2437
|
+
return { version: 1, brains: {} };
|
|
2438
|
+
}
|
|
2439
|
+
function readManifest(ws = workspace()) {
|
|
2440
|
+
const p = manifestPath(ws);
|
|
2441
|
+
if (!existsSync8(p)) return emptyManifest();
|
|
2442
|
+
try {
|
|
2443
|
+
const parsed = JSON.parse(readFileSync10(p, "utf8"));
|
|
2444
|
+
const brains = parsed.brains && typeof parsed.brains === "object" ? parsed.brains : {};
|
|
2445
|
+
return { version: 1, brains };
|
|
2446
|
+
} catch {
|
|
2447
|
+
return emptyManifest();
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
function writeManifest(m, ws = workspace()) {
|
|
2451
|
+
mkdirSync8(wsHoltDir(ws), { recursive: true });
|
|
2452
|
+
writeFileSync8(manifestPath(ws), JSON.stringify(m, null, 2) + "\n", "utf8");
|
|
2453
|
+
}
|
|
2454
|
+
function hashString(s) {
|
|
2455
|
+
return createHash2("sha256").update(s).digest("hex");
|
|
2456
|
+
}
|
|
2457
|
+
function hashDir(dir) {
|
|
2458
|
+
const h = createHash2("sha256");
|
|
2459
|
+
const walk = (rel) => {
|
|
2460
|
+
const abs = join9(dir, rel);
|
|
2461
|
+
let entries;
|
|
2462
|
+
try {
|
|
2463
|
+
entries = readdirSync2(abs).sort();
|
|
2464
|
+
} catch {
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
for (const name of entries) {
|
|
2468
|
+
const childRel = rel ? join9(rel, name) : name;
|
|
2469
|
+
const childAbs = join9(dir, childRel);
|
|
2470
|
+
let st;
|
|
2471
|
+
try {
|
|
2472
|
+
st = statSync3(childAbs);
|
|
2473
|
+
} catch {
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
if (st.isDirectory()) {
|
|
2477
|
+
h.update("D:" + childRel + "\n");
|
|
2478
|
+
walk(childRel);
|
|
2479
|
+
} else {
|
|
2480
|
+
h.update("F:" + childRel + "\n");
|
|
2481
|
+
try {
|
|
2482
|
+
h.update(readFileSync10(childAbs));
|
|
2483
|
+
} catch {
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
walk("");
|
|
2489
|
+
return h.digest("hex");
|
|
2490
|
+
}
|
|
2491
|
+
function readSkillSource(skill) {
|
|
2492
|
+
const file = join9(skill.dir, "SKILL.md");
|
|
2493
|
+
let raw = "";
|
|
2494
|
+
try {
|
|
2495
|
+
raw = readFileSync10(file, "utf8");
|
|
2496
|
+
} catch {
|
|
2497
|
+
return null;
|
|
2498
|
+
}
|
|
2499
|
+
const { data, body } = parseFrontmatter(raw);
|
|
2500
|
+
const description = (data.description || skill.description || "").trim();
|
|
2501
|
+
return { name: skill.name, description, body: body.trim() };
|
|
2502
|
+
}
|
|
2503
|
+
function tomlBasicString(s) {
|
|
2504
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, " ").trim();
|
|
2505
|
+
}
|
|
2506
|
+
function tomlMultiline(s) {
|
|
2507
|
+
return s.replace(/\\/g, "\\\\").replace(/"""/g, '\\"\\"\\"');
|
|
2508
|
+
}
|
|
2509
|
+
function renderGeminiToml(src) {
|
|
2510
|
+
const instructions = src.body || src.description;
|
|
2511
|
+
if (!instructions.trim()) return null;
|
|
2512
|
+
const prompt = [
|
|
2513
|
+
instructions,
|
|
2514
|
+
"",
|
|
2515
|
+
"Apply the skill above to this request:",
|
|
2516
|
+
"{{args}}"
|
|
2517
|
+
].join("\n");
|
|
2518
|
+
const lines = [
|
|
2519
|
+
`# ${MARKER}: ${src.name}`,
|
|
2520
|
+
"# Generated by Holt from a SKILL.md. Edit the skill, not this file: Holt",
|
|
2521
|
+
'# will overwrite it on the next launch. Delete via "holt skill remove".'
|
|
2522
|
+
];
|
|
2523
|
+
if (src.description) lines.push(`description = "${tomlBasicString(src.description)}"`);
|
|
2524
|
+
lines.push('prompt = """');
|
|
2525
|
+
lines.push(tomlMultiline(prompt));
|
|
2526
|
+
lines.push('"""');
|
|
2527
|
+
return lines.join("\n") + "\n";
|
|
2528
|
+
}
|
|
2529
|
+
function renderCodexPrompt(src) {
|
|
2530
|
+
const instructions = src.body || src.description;
|
|
2531
|
+
if (!instructions.trim()) return null;
|
|
2532
|
+
const front = ["---"];
|
|
2533
|
+
if (src.description) front.push(`description: ${src.description.replace(/\n/g, " ").trim()}`);
|
|
2534
|
+
front.push("argument-hint: [request]");
|
|
2535
|
+
front.push("---");
|
|
2536
|
+
const parts = [
|
|
2537
|
+
front.join("\n"),
|
|
2538
|
+
"",
|
|
2539
|
+
`<!-- ${MARKER}: ${src.name} (generated by Holt from a SKILL.md; do not edit) -->`,
|
|
2540
|
+
"",
|
|
2541
|
+
instructions,
|
|
2542
|
+
"",
|
|
2543
|
+
"Apply the skill above to this request:",
|
|
2544
|
+
"",
|
|
2545
|
+
"$ARGUMENTS",
|
|
2546
|
+
""
|
|
2547
|
+
];
|
|
2548
|
+
return parts.join("\n");
|
|
2549
|
+
}
|
|
2550
|
+
function claudeSkillsDir(ws) {
|
|
2551
|
+
return join9(ws, ".claude", "skills");
|
|
2552
|
+
}
|
|
2553
|
+
function geminiCommandsDir(ws) {
|
|
2554
|
+
return join9(ws, ".gemini", "commands");
|
|
2555
|
+
}
|
|
2556
|
+
function codexPromptsDir() {
|
|
2557
|
+
return join9(homedir4(), ".codex", "prompts");
|
|
2558
|
+
}
|
|
2559
|
+
function fileIsOurs(path) {
|
|
2560
|
+
try {
|
|
2561
|
+
return readFileSync10(path, "utf8").includes(MARKER);
|
|
2562
|
+
} catch {
|
|
2563
|
+
return false;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
function emptyResult() {
|
|
2567
|
+
return { written: [], skippedExisting: [], skippedUnexpressible: [], removed: [] };
|
|
2568
|
+
}
|
|
2569
|
+
function priorEntry(m, brain, path) {
|
|
2570
|
+
return (m.brains[brain] || []).find((e) => e.path === path);
|
|
2571
|
+
}
|
|
2572
|
+
function compileForBrain(brain, ws = workspace()) {
|
|
2573
|
+
const res = emptyResult();
|
|
2574
|
+
const manifest = readManifest(ws);
|
|
2575
|
+
const prior = manifest.brains[brain] || [];
|
|
2576
|
+
const nextEntries = [];
|
|
2577
|
+
let skills;
|
|
2578
|
+
try {
|
|
2579
|
+
skills = listSkills();
|
|
2580
|
+
} catch {
|
|
2581
|
+
return res;
|
|
2582
|
+
}
|
|
2583
|
+
const enabledNames = new Set(skills.map((s) => s.name));
|
|
2584
|
+
for (const skill of skills) {
|
|
2585
|
+
try {
|
|
2586
|
+
if (brain === "claude") {
|
|
2587
|
+
compileClaude(skill, ws, manifest, res, nextEntries);
|
|
2588
|
+
} else if (brain === "gemini") {
|
|
2589
|
+
compileGemini(skill, ws, manifest, res, nextEntries);
|
|
2590
|
+
} else {
|
|
2591
|
+
compileCodex(skill, manifest, res, nextEntries);
|
|
2592
|
+
}
|
|
2593
|
+
} catch {
|
|
2594
|
+
res.skippedUnexpressible.push(skill.name);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
for (const e of prior) {
|
|
2598
|
+
if (enabledNames.has(e.skill) && nextEntries.some((n) => n.path === e.path)) continue;
|
|
2599
|
+
if (removeArtifact(e.path)) res.removed.push(e.path);
|
|
2600
|
+
}
|
|
2601
|
+
manifest.brains[brain] = nextEntries;
|
|
2602
|
+
try {
|
|
2603
|
+
writeManifest(manifest, ws);
|
|
2604
|
+
} catch {
|
|
2605
|
+
}
|
|
2606
|
+
return res;
|
|
2607
|
+
}
|
|
2608
|
+
function compileClaude(skill, ws, manifest, res, next) {
|
|
2609
|
+
const dest = join9(claudeSkillsDir(ws), skill.name);
|
|
2610
|
+
const known = priorEntry(manifest, "claude", dest);
|
|
2611
|
+
if (existsSync8(dest) && !known) {
|
|
2612
|
+
res.skippedExisting.push(dest);
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
const srcHash = hashDir(skill.dir);
|
|
2616
|
+
if (known && known.hash === srcHash && existsSync8(dest)) {
|
|
2617
|
+
next.push(known);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (existsSync8(dest)) rmSync4(dest, { recursive: true, force: true });
|
|
2621
|
+
mkdirSync8(dirname3(dest), { recursive: true });
|
|
2622
|
+
cpSync(skill.dir, dest, { recursive: true });
|
|
2623
|
+
next.push({ skill: skill.name, path: dest, hash: hashDir(dest) });
|
|
2624
|
+
res.written.push(dest);
|
|
2625
|
+
}
|
|
2626
|
+
function compileGemini(skill, ws, manifest, res, next) {
|
|
2627
|
+
const src = readSkillSource(skill);
|
|
2628
|
+
if (!src) {
|
|
2629
|
+
res.skippedUnexpressible.push(skill.name);
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
const content = renderGeminiToml(src);
|
|
2633
|
+
if (content === null) {
|
|
2634
|
+
res.skippedUnexpressible.push(skill.name);
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
const dest = join9(geminiCommandsDir(ws), `${skill.name}.toml`);
|
|
2638
|
+
writeTextArtifact("gemini", skill.name, dest, content, manifest, res, next);
|
|
2639
|
+
}
|
|
2640
|
+
function compileCodex(skill, manifest, res, next) {
|
|
2641
|
+
const src = readSkillSource(skill);
|
|
2642
|
+
if (!src) {
|
|
2643
|
+
res.skippedUnexpressible.push(skill.name);
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
const content = renderCodexPrompt(src);
|
|
2647
|
+
if (content === null) {
|
|
2648
|
+
res.skippedUnexpressible.push(skill.name);
|
|
2649
|
+
return;
|
|
2650
|
+
}
|
|
2651
|
+
const dest = join9(codexPromptsDir(), `${CODEX_PREFIX}${skill.name}.md`);
|
|
2652
|
+
writeTextArtifact("codex", skill.name, dest, content, manifest, res, next);
|
|
2653
|
+
}
|
|
2654
|
+
function writeTextArtifact(brain, skillName, dest, content, manifest, res, next) {
|
|
2655
|
+
const known = priorEntry(manifest, brain, dest);
|
|
2656
|
+
if (existsSync8(dest) && !known && !fileIsOurs(dest)) {
|
|
2657
|
+
res.skippedExisting.push(dest);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
const hash = hashString(content);
|
|
2661
|
+
if (existsSync8(dest)) {
|
|
2662
|
+
let cur = "";
|
|
2663
|
+
try {
|
|
2664
|
+
cur = readFileSync10(dest, "utf8");
|
|
2665
|
+
} catch {
|
|
2666
|
+
cur = "";
|
|
2667
|
+
}
|
|
2668
|
+
if (hashString(cur) === hash) {
|
|
2669
|
+
next.push({ skill: skillName, path: dest, hash });
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
mkdirSync8(dirname3(dest), { recursive: true });
|
|
2674
|
+
writeFileSync8(dest, content, "utf8");
|
|
2675
|
+
next.push({ skill: skillName, path: dest, hash });
|
|
2676
|
+
res.written.push(dest);
|
|
2677
|
+
}
|
|
2678
|
+
function removeArtifact(path) {
|
|
2679
|
+
try {
|
|
2680
|
+
if (!existsSync8(path)) return false;
|
|
2681
|
+
rmSync4(path, { recursive: true, force: true });
|
|
2682
|
+
return true;
|
|
2683
|
+
} catch {
|
|
2684
|
+
return false;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
function syncSkillCommands(brain, ws = workspace()) {
|
|
2688
|
+
let res;
|
|
2689
|
+
try {
|
|
2690
|
+
res = compileForBrain(brain, ws);
|
|
2691
|
+
} catch {
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
reportQuiet(brain, res);
|
|
2695
|
+
}
|
|
2696
|
+
function syncAllBrains(enabled, ws = workspace()) {
|
|
2697
|
+
for (const brain of enabled) {
|
|
2698
|
+
try {
|
|
2699
|
+
const res = compileForBrain(brain, ws);
|
|
2700
|
+
reportQuiet(brain, res);
|
|
2701
|
+
} catch {
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
function removeSkillArtifacts(skillName, ws = workspace()) {
|
|
2706
|
+
const manifest = readManifest(ws);
|
|
2707
|
+
let removed = 0;
|
|
2708
|
+
for (const brain of Object.keys(manifest.brains)) {
|
|
2709
|
+
const entries = manifest.brains[brain] || [];
|
|
2710
|
+
const keep = [];
|
|
2711
|
+
for (const e of entries) {
|
|
2712
|
+
if (e.skill === skillName) {
|
|
2713
|
+
if (removeArtifact(e.path)) removed++;
|
|
2714
|
+
} else {
|
|
2715
|
+
keep.push(e);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
manifest.brains[brain] = keep;
|
|
2719
|
+
}
|
|
2720
|
+
try {
|
|
2721
|
+
writeManifest(manifest, ws);
|
|
2722
|
+
} catch {
|
|
2723
|
+
}
|
|
2724
|
+
return removed;
|
|
2725
|
+
}
|
|
2726
|
+
var BRAIN_LABEL = {
|
|
2727
|
+
claude: "Claude Code",
|
|
2728
|
+
gemini: "Gemini CLI",
|
|
2729
|
+
codex: "Codex"
|
|
2730
|
+
};
|
|
2731
|
+
function reportQuiet(brain, res) {
|
|
2732
|
+
const changed = res.written.length + res.removed.length;
|
|
2733
|
+
if (changed === 0 && res.skippedExisting.length === 0) return;
|
|
2734
|
+
const label = BRAIN_LABEL[brain];
|
|
2735
|
+
const parts = [];
|
|
2736
|
+
if (res.written.length) parts.push(`${res.written.length} synced`);
|
|
2737
|
+
if (res.removed.length) parts.push(`${res.removed.length} removed`);
|
|
2738
|
+
if (parts.length) {
|
|
2739
|
+
console.log(c.dim(` Skills as ${label} commands: ${parts.join(", ")}.`));
|
|
2740
|
+
}
|
|
2741
|
+
for (const p of res.skippedExisting) {
|
|
2742
|
+
console.log(c.dim(` Skipped ${p} (already exists and not written by Holt).`));
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2366
2746
|
// src/commands/hook.ts
|
|
2367
|
-
import { readFileSync as
|
|
2747
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync10, existsSync as existsSync10, mkdirSync as mkdirSync10, copyFileSync, realpathSync as realpathSync2, appendFileSync as appendFileSync2, statSync as statSync5 } from "fs";
|
|
2368
2748
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
2369
|
-
import { join as
|
|
2370
|
-
import { homedir as
|
|
2749
|
+
import { join as join11, dirname as dirname4 } from "path";
|
|
2750
|
+
import { homedir as homedir5 } from "os";
|
|
2371
2751
|
|
|
2372
2752
|
// src/facts.ts
|
|
2373
2753
|
var MIN_EXCHANGES_FOR_EXTRACTION = 3;
|
|
@@ -2483,15 +2863,15 @@ async function extractAndSaveFacts(active, cfg, history, session) {
|
|
|
2483
2863
|
|
|
2484
2864
|
// src/wiki.ts
|
|
2485
2865
|
import {
|
|
2486
|
-
readFileSync as
|
|
2487
|
-
writeFileSync as
|
|
2488
|
-
existsSync as
|
|
2489
|
-
mkdirSync as
|
|
2490
|
-
rmSync as
|
|
2491
|
-
readdirSync as
|
|
2492
|
-
statSync as
|
|
2866
|
+
readFileSync as readFileSync11,
|
|
2867
|
+
writeFileSync as writeFileSync9,
|
|
2868
|
+
existsSync as existsSync9,
|
|
2869
|
+
mkdirSync as mkdirSync9,
|
|
2870
|
+
rmSync as rmSync5,
|
|
2871
|
+
readdirSync as readdirSync3,
|
|
2872
|
+
statSync as statSync4
|
|
2493
2873
|
} from "fs";
|
|
2494
|
-
import { join as
|
|
2874
|
+
import { join as join10 } from "path";
|
|
2495
2875
|
import { totalmem } from "os";
|
|
2496
2876
|
|
|
2497
2877
|
// src/localmodel.ts
|
|
@@ -2546,28 +2926,28 @@ function pullHint(model) {
|
|
|
2546
2926
|
|
|
2547
2927
|
// src/wiki.ts
|
|
2548
2928
|
function wikiDir() {
|
|
2549
|
-
return
|
|
2929
|
+
return join10(wsHoltDir(), "wiki");
|
|
2550
2930
|
}
|
|
2551
2931
|
function wikiStatePath() {
|
|
2552
|
-
return
|
|
2932
|
+
return join10(wikiDir(), ".state.json");
|
|
2553
2933
|
}
|
|
2554
2934
|
function wikiIndexPath() {
|
|
2555
|
-
return
|
|
2935
|
+
return join10(wikiDir(), "index.md");
|
|
2556
2936
|
}
|
|
2557
2937
|
function pagePath(slug) {
|
|
2558
|
-
return
|
|
2938
|
+
return join10(wikiDir(), `${slug}.md`);
|
|
2559
2939
|
}
|
|
2560
2940
|
function loadState() {
|
|
2561
2941
|
try {
|
|
2562
|
-
const s = JSON.parse(
|
|
2942
|
+
const s = JSON.parse(readFileSync11(wikiStatePath(), "utf8"));
|
|
2563
2943
|
return { lastSyncTs: typeof s.lastSyncTs === "number" ? s.lastSyncTs : 0, updatedAt: s.updatedAt ?? 0 };
|
|
2564
2944
|
} catch {
|
|
2565
2945
|
return { lastSyncTs: 0, updatedAt: 0 };
|
|
2566
2946
|
}
|
|
2567
2947
|
}
|
|
2568
2948
|
function saveState(s) {
|
|
2569
|
-
|
|
2570
|
-
|
|
2949
|
+
mkdirSync9(wikiDir(), { recursive: true });
|
|
2950
|
+
writeFileSync9(wikiStatePath(), JSON.stringify(s, null, 2) + "\n", "utf8");
|
|
2571
2951
|
}
|
|
2572
2952
|
function saveSyncMarker(ts) {
|
|
2573
2953
|
saveState({ lastSyncTs: ts, updatedAt: Date.now() });
|
|
@@ -2577,19 +2957,19 @@ function isPageFile(name) {
|
|
|
2577
2957
|
}
|
|
2578
2958
|
function listPageSlugs() {
|
|
2579
2959
|
const dir = wikiDir();
|
|
2580
|
-
if (!
|
|
2960
|
+
if (!existsSync9(dir)) return [];
|
|
2581
2961
|
try {
|
|
2582
|
-
return
|
|
2962
|
+
return readdirSync3(dir).filter(isPageFile).map((f) => f.slice(0, -3)).sort();
|
|
2583
2963
|
} catch {
|
|
2584
2964
|
return [];
|
|
2585
2965
|
}
|
|
2586
2966
|
}
|
|
2587
2967
|
function readPage(slug) {
|
|
2588
2968
|
const p = pagePath(slug);
|
|
2589
|
-
if (!
|
|
2969
|
+
if (!existsSync9(p)) return null;
|
|
2590
2970
|
let raw = "";
|
|
2591
2971
|
try {
|
|
2592
|
-
raw =
|
|
2972
|
+
raw = readFileSync11(p, "utf8");
|
|
2593
2973
|
} catch {
|
|
2594
2974
|
return null;
|
|
2595
2975
|
}
|
|
@@ -2631,8 +3011,8 @@ function renderPage(pg) {
|
|
|
2631
3011
|
return fm;
|
|
2632
3012
|
}
|
|
2633
3013
|
function writePage(pg) {
|
|
2634
|
-
|
|
2635
|
-
|
|
3014
|
+
mkdirSync9(wikiDir(), { recursive: true });
|
|
3015
|
+
writeFileSync9(pagePath(pg.slug), renderPage(pg), "utf8");
|
|
2636
3016
|
}
|
|
2637
3017
|
var WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
2638
3018
|
function extractLinks(body) {
|
|
@@ -2909,8 +3289,8 @@ function renderIndex(pages) {
|
|
|
2909
3289
|
return lines.join("\n");
|
|
2910
3290
|
}
|
|
2911
3291
|
function writeIndex(pages) {
|
|
2912
|
-
|
|
2913
|
-
|
|
3292
|
+
mkdirSync9(wikiDir(), { recursive: true });
|
|
3293
|
+
writeFileSync9(wikiIndexPath(), renderIndex(pages), "utf8");
|
|
2914
3294
|
}
|
|
2915
3295
|
var WIKI_SESSION = "wiki";
|
|
2916
3296
|
function wikiRowId(slug) {
|
|
@@ -2925,9 +3305,9 @@ async function indexPagesForRecall(pages) {
|
|
|
2925
3305
|
if (pages.length === 0) return;
|
|
2926
3306
|
const slugs = new Set(pages.map((p) => sanitizeName(p.slug).slice(0, 24)));
|
|
2927
3307
|
const kept = dropWikiRows(slugs);
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
3308
|
+
mkdirSync9(join10(wsHoltDir(), "memory"), { recursive: true });
|
|
3309
|
+
writeFileSync9(
|
|
3310
|
+
join10(wsHoltDir(), "memory", "turns.jsonl"),
|
|
2931
3311
|
kept.map((t) => JSON.stringify(t)).join("\n") + (kept.length ? "\n" : ""),
|
|
2932
3312
|
"utf8"
|
|
2933
3313
|
);
|
|
@@ -2949,9 +3329,9 @@ ${pg.body}`.slice(0, 4e3);
|
|
|
2949
3329
|
function clearWikiRecallRows() {
|
|
2950
3330
|
try {
|
|
2951
3331
|
const kept = loadTurns().filter((t) => t.session !== WIKI_SESSION);
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3332
|
+
mkdirSync9(join10(wsHoltDir(), "memory"), { recursive: true });
|
|
3333
|
+
writeFileSync9(
|
|
3334
|
+
join10(wsHoltDir(), "memory", "turns.jsonl"),
|
|
2955
3335
|
kept.map((t) => JSON.stringify(t)).join("\n") + (kept.length ? "\n" : ""),
|
|
2956
3336
|
"utf8"
|
|
2957
3337
|
);
|
|
@@ -2959,14 +3339,14 @@ function clearWikiRecallRows() {
|
|
|
2959
3339
|
}
|
|
2960
3340
|
}
|
|
2961
3341
|
function wipeWiki() {
|
|
2962
|
-
if (
|
|
3342
|
+
if (existsSync9(wikiDir())) rmSync5(wikiDir(), { recursive: true, force: true });
|
|
2963
3343
|
}
|
|
2964
3344
|
function wikiStats() {
|
|
2965
3345
|
const slugs = listPageSlugs();
|
|
2966
3346
|
let bytes = 0;
|
|
2967
3347
|
for (const slug of slugs) {
|
|
2968
3348
|
try {
|
|
2969
|
-
bytes +=
|
|
3349
|
+
bytes += statSync4(pagePath(slug)).size;
|
|
2970
3350
|
} catch {
|
|
2971
3351
|
}
|
|
2972
3352
|
}
|
|
@@ -3005,26 +3385,26 @@ var HOOK_CMD = {
|
|
|
3005
3385
|
capture: "holt hook capture"
|
|
3006
3386
|
};
|
|
3007
3387
|
function globalSettingsPath() {
|
|
3008
|
-
return
|
|
3388
|
+
return join11(homedir5(), ".claude", "settings.json");
|
|
3009
3389
|
}
|
|
3010
3390
|
function projectSettingsPath() {
|
|
3011
|
-
return
|
|
3391
|
+
return join11(process.cwd(), ".claude", "settings.json");
|
|
3012
3392
|
}
|
|
3013
3393
|
function readSettings(path) {
|
|
3014
|
-
if (!
|
|
3394
|
+
if (!existsSync10(path)) return {};
|
|
3015
3395
|
try {
|
|
3016
|
-
const parsed = JSON.parse(
|
|
3396
|
+
const parsed = JSON.parse(readFileSync12(path, "utf8"));
|
|
3017
3397
|
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3018
3398
|
} catch {
|
|
3019
3399
|
throw new Error(`could not parse ${path} (not valid JSON) - leaving it untouched`);
|
|
3020
3400
|
}
|
|
3021
3401
|
}
|
|
3022
3402
|
function writeSettings(path, settings) {
|
|
3023
|
-
|
|
3024
|
-
|
|
3403
|
+
mkdirSync10(dirname4(path), { recursive: true });
|
|
3404
|
+
writeFileSync10(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
3025
3405
|
}
|
|
3026
3406
|
function backup(path) {
|
|
3027
|
-
if (!
|
|
3407
|
+
if (!existsSync10(path)) return null;
|
|
3028
3408
|
const dest = `${path}.holt-bak`;
|
|
3029
3409
|
try {
|
|
3030
3410
|
copyFileSync(path, dest);
|
|
@@ -3114,7 +3494,7 @@ function remove(rest) {
|
|
|
3114
3494
|
const paths = project ? [projectSettingsPath()] : [globalSettingsPath(), projectSettingsPath()];
|
|
3115
3495
|
let touchedAny = false;
|
|
3116
3496
|
for (const path of paths) {
|
|
3117
|
-
if (!
|
|
3497
|
+
if (!existsSync10(path)) continue;
|
|
3118
3498
|
let settings;
|
|
3119
3499
|
try {
|
|
3120
3500
|
settings = readSettings(path);
|
|
@@ -3144,7 +3524,7 @@ function status(rest) {
|
|
|
3144
3524
|
];
|
|
3145
3525
|
console.log(c.accent(" Holt ambient hooks"));
|
|
3146
3526
|
for (const e of entries) {
|
|
3147
|
-
if (!
|
|
3527
|
+
if (!existsSync10(e.path)) {
|
|
3148
3528
|
console.log(c.dim(` ${e.label.padEnd(8)} ${e.path} (no settings file)`));
|
|
3149
3529
|
continue;
|
|
3150
3530
|
}
|
|
@@ -3194,7 +3574,7 @@ function parseHookInput(raw) {
|
|
|
3194
3574
|
}
|
|
3195
3575
|
}
|
|
3196
3576
|
function enterFolder(cwd) {
|
|
3197
|
-
if (!cwd || !
|
|
3577
|
+
if (!cwd || !existsSync10(cwd)) return null;
|
|
3198
3578
|
try {
|
|
3199
3579
|
const real = realpathSync2(cwd);
|
|
3200
3580
|
process.chdir(real);
|
|
@@ -3205,13 +3585,13 @@ function enterFolder(cwd) {
|
|
|
3205
3585
|
}
|
|
3206
3586
|
function guardTrusted(real) {
|
|
3207
3587
|
if (!isTrusted(real)) return false;
|
|
3208
|
-
if (!
|
|
3588
|
+
if (!existsSync10(memDir())) return false;
|
|
3209
3589
|
return true;
|
|
3210
3590
|
}
|
|
3211
3591
|
function hookLog(msg) {
|
|
3212
3592
|
try {
|
|
3213
|
-
|
|
3214
|
-
appendFileSync2(
|
|
3593
|
+
mkdirSync10(GLOBAL_DIR, { recursive: true });
|
|
3594
|
+
appendFileSync2(join11(GLOBAL_DIR, "hooks.log"), `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
3215
3595
|
`, "utf8");
|
|
3216
3596
|
} catch {
|
|
3217
3597
|
}
|
|
@@ -3219,18 +3599,18 @@ function hookLog(msg) {
|
|
|
3219
3599
|
function fileBytes(path) {
|
|
3220
3600
|
if (!path) return 0;
|
|
3221
3601
|
try {
|
|
3222
|
-
return
|
|
3602
|
+
return statSync5(path).size;
|
|
3223
3603
|
} catch {
|
|
3224
3604
|
return 0;
|
|
3225
3605
|
}
|
|
3226
3606
|
}
|
|
3227
3607
|
var THROTTLE_MIN_NEW_EXCHANGES = 2;
|
|
3228
3608
|
function hookStatePath() {
|
|
3229
|
-
return
|
|
3609
|
+
return join11(GLOBAL_DIR, "hook-state.json");
|
|
3230
3610
|
}
|
|
3231
3611
|
function readHookState() {
|
|
3232
3612
|
try {
|
|
3233
|
-
const parsed = JSON.parse(
|
|
3613
|
+
const parsed = JSON.parse(readFileSync12(hookStatePath(), "utf8"));
|
|
3234
3614
|
if (!parsed || typeof parsed !== "object") return {};
|
|
3235
3615
|
return parsed;
|
|
3236
3616
|
} catch {
|
|
@@ -3243,10 +3623,10 @@ function lastProcessedExchanges(state, sessionId) {
|
|
|
3243
3623
|
}
|
|
3244
3624
|
function saveHookState(sessionId, exchanges) {
|
|
3245
3625
|
try {
|
|
3246
|
-
|
|
3626
|
+
mkdirSync10(GLOBAL_DIR, { recursive: true });
|
|
3247
3627
|
const state = readHookState();
|
|
3248
3628
|
state[sessionId] = { lastExchanges: exchanges };
|
|
3249
|
-
|
|
3629
|
+
writeFileSync10(hookStatePath(), JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
3250
3630
|
} catch {
|
|
3251
3631
|
}
|
|
3252
3632
|
}
|
|
@@ -3320,7 +3700,7 @@ function readTranscript(path) {
|
|
|
3320
3700
|
const out = [];
|
|
3321
3701
|
let raw;
|
|
3322
3702
|
try {
|
|
3323
|
-
raw =
|
|
3703
|
+
raw = readFileSync12(path, "utf8");
|
|
3324
3704
|
} catch {
|
|
3325
3705
|
return out;
|
|
3326
3706
|
}
|
|
@@ -3348,28 +3728,28 @@ function readTranscript(path) {
|
|
|
3348
3728
|
return out;
|
|
3349
3729
|
}
|
|
3350
3730
|
function resolveCommandPath(command) {
|
|
3351
|
-
if (command.startsWith("/") &&
|
|
3731
|
+
if (command.startsWith("/") && existsSync10(command)) return command;
|
|
3352
3732
|
try {
|
|
3353
3733
|
const shell = process.env.SHELL || "/bin/sh";
|
|
3354
3734
|
const res = spawnSync3(shell, ["-lc", "command -v " + command], { encoding: "utf8" });
|
|
3355
3735
|
if (res.status === 0) {
|
|
3356
3736
|
const p = (res.stdout || "").trim().split("\n")[0]?.trim();
|
|
3357
|
-
if (p && p.startsWith("/") &&
|
|
3737
|
+
if (p && p.startsWith("/") && existsSync10(p)) return p;
|
|
3358
3738
|
}
|
|
3359
3739
|
} catch {
|
|
3360
3740
|
}
|
|
3361
|
-
const home =
|
|
3741
|
+
const home = homedir5();
|
|
3362
3742
|
const dirs = [
|
|
3363
|
-
|
|
3743
|
+
join11(home, ".local", "bin"),
|
|
3364
3744
|
"/opt/homebrew/bin",
|
|
3365
3745
|
"/usr/local/bin",
|
|
3366
|
-
|
|
3746
|
+
join11(home, ".npm-global", "bin"),
|
|
3367
3747
|
"/usr/bin",
|
|
3368
3748
|
"/bin"
|
|
3369
3749
|
];
|
|
3370
3750
|
for (const dir of dirs) {
|
|
3371
|
-
const candidate =
|
|
3372
|
-
if (
|
|
3751
|
+
const candidate = join11(dir, command);
|
|
3752
|
+
if (existsSync10(candidate)) return candidate;
|
|
3373
3753
|
}
|
|
3374
3754
|
return null;
|
|
3375
3755
|
}
|
|
@@ -3443,7 +3823,7 @@ async function capture() {
|
|
|
3443
3823
|
let bytes = 0;
|
|
3444
3824
|
let lines = 0;
|
|
3445
3825
|
try {
|
|
3446
|
-
const rawT =
|
|
3826
|
+
const rawT = readFileSync12(data.transcript_path, "utf8");
|
|
3447
3827
|
bytes = Buffer.byteLength(rawT, "utf8");
|
|
3448
3828
|
lines = countLines(rawT);
|
|
3449
3829
|
} catch {
|
|
@@ -3541,8 +3921,8 @@ async function hook(sub, rest = []) {
|
|
|
3541
3921
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
3542
3922
|
|
|
3543
3923
|
// src/output.ts
|
|
3544
|
-
import { writeFileSync as
|
|
3545
|
-
import { join as
|
|
3924
|
+
import { writeFileSync as writeFileSync11 } from "fs";
|
|
3925
|
+
import { join as join12, isAbsolute } from "path";
|
|
3546
3926
|
function escapeHtml(s) {
|
|
3547
3927
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3548
3928
|
}
|
|
@@ -3682,8 +4062,8 @@ function saveReply(reply, format, name) {
|
|
|
3682
4062
|
throw new Error('name must be a plain file name in this folder (no "/", "\\", or "..").');
|
|
3683
4063
|
}
|
|
3684
4064
|
if (!/\.[a-z0-9]+$/i.test(base)) base += ext;
|
|
3685
|
-
const path =
|
|
3686
|
-
|
|
4065
|
+
const path = join12(process.cwd(), base);
|
|
4066
|
+
writeFileSync11(path, renderReply(reply, format), "utf8");
|
|
3687
4067
|
return path;
|
|
3688
4068
|
}
|
|
3689
4069
|
|
|
@@ -3711,12 +4091,12 @@ async function login(which) {
|
|
|
3711
4091
|
}
|
|
3712
4092
|
|
|
3713
4093
|
// src/access.ts
|
|
3714
|
-
import { existsSync as
|
|
3715
|
-
import { homedir as
|
|
3716
|
-
import { dirname as
|
|
4094
|
+
import { existsSync as existsSync11, realpathSync as realpathSync3, statSync as statSync6 } from "fs";
|
|
4095
|
+
import { homedir as homedir6 } from "os";
|
|
4096
|
+
import { dirname as dirname5, isAbsolute as isAbsolute2, resolve as resolve2, sep } from "path";
|
|
3717
4097
|
function expandHome(p) {
|
|
3718
|
-
if (p === "~") return
|
|
3719
|
-
if (p.startsWith("~/")) return resolve2(
|
|
4098
|
+
if (p === "~") return homedir6();
|
|
4099
|
+
if (p.startsWith("~/")) return resolve2(homedir6(), p.slice(2));
|
|
3720
4100
|
return p;
|
|
3721
4101
|
}
|
|
3722
4102
|
function isInside(child, parent) {
|
|
@@ -3727,7 +4107,7 @@ function isInside(child, parent) {
|
|
|
3727
4107
|
function resolveExisting(token) {
|
|
3728
4108
|
const expanded = expandHome(token);
|
|
3729
4109
|
const abs = isAbsolute2(expanded) ? expanded : resolve2(process.cwd(), expanded);
|
|
3730
|
-
if (!
|
|
4110
|
+
if (!existsSync11(abs)) return null;
|
|
3731
4111
|
try {
|
|
3732
4112
|
return realpathSync3(abs);
|
|
3733
4113
|
} catch {
|
|
@@ -3736,9 +4116,9 @@ function resolveExisting(token) {
|
|
|
3736
4116
|
}
|
|
3737
4117
|
function containingDir(absPath) {
|
|
3738
4118
|
try {
|
|
3739
|
-
return
|
|
4119
|
+
return statSync6(absPath).isDirectory() ? absPath : dirname5(absPath);
|
|
3740
4120
|
} catch {
|
|
3741
|
-
return
|
|
4121
|
+
return dirname5(absPath);
|
|
3742
4122
|
}
|
|
3743
4123
|
}
|
|
3744
4124
|
function resolveGrantDir(token) {
|
|
@@ -3881,6 +4261,9 @@ async function chat() {
|
|
|
3881
4261
|
if (embedOk && stats.withEmbeddings < stats.turns) {
|
|
3882
4262
|
console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
|
|
3883
4263
|
}
|
|
4264
|
+
if (!embedOk) {
|
|
4265
|
+
console.log(c.dim(' Recall works, but semantic memory is off. "holt doctor --fix" turns it on.'));
|
|
4266
|
+
}
|
|
3884
4267
|
console.log(c.dim("Type a message. Commands: /brain /memory /output /save /allow /setting /clear /help /exit"));
|
|
3885
4268
|
showStatus();
|
|
3886
4269
|
while (true) {
|
|
@@ -4166,13 +4549,13 @@ var CONTEXT_MARKER = "<!-- holt-managed identity -->";
|
|
|
4166
4549
|
function brandContextFile(id) {
|
|
4167
4550
|
const name = CONTEXT_FILE[id];
|
|
4168
4551
|
if (!name) return null;
|
|
4169
|
-
const path =
|
|
4552
|
+
const path = join13(workspace(), name);
|
|
4170
4553
|
try {
|
|
4171
|
-
if (
|
|
4172
|
-
const cur =
|
|
4554
|
+
if (existsSync12(path)) {
|
|
4555
|
+
const cur = readFileSync13(path, "utf8");
|
|
4173
4556
|
if (!cur.includes(CONTEXT_MARKER)) return null;
|
|
4174
4557
|
}
|
|
4175
|
-
|
|
4558
|
+
writeFileSync12(path, `${CONTEXT_MARKER}
|
|
4176
4559
|
# Holt
|
|
4177
4560
|
|
|
4178
4561
|
${HOLT_IDENTITY}
|
|
@@ -4183,11 +4566,11 @@ ${HOLT_IDENTITY}
|
|
|
4183
4566
|
}
|
|
4184
4567
|
}
|
|
4185
4568
|
function brandStatusLine() {
|
|
4186
|
-
const path =
|
|
4569
|
+
const path = join13(workspace(), ".claude", "settings.json");
|
|
4187
4570
|
let settings = {};
|
|
4188
4571
|
try {
|
|
4189
|
-
if (
|
|
4190
|
-
const parsed = JSON.parse(
|
|
4572
|
+
if (existsSync12(path)) {
|
|
4573
|
+
const parsed = JSON.parse(readFileSync13(path, "utf8"));
|
|
4191
4574
|
if (parsed && typeof parsed === "object") settings = parsed;
|
|
4192
4575
|
}
|
|
4193
4576
|
} catch {
|
|
@@ -4195,13 +4578,13 @@ function brandStatusLine() {
|
|
|
4195
4578
|
}
|
|
4196
4579
|
if (settings.statusLine) return false;
|
|
4197
4580
|
try {
|
|
4198
|
-
if (
|
|
4581
|
+
if (existsSync12(path)) copyFileSync2(path, `${path}.holt-bak`);
|
|
4199
4582
|
settings.statusLine = {
|
|
4200
4583
|
type: "command",
|
|
4201
4584
|
command: `${resolveHoltPath()} statusline`
|
|
4202
4585
|
};
|
|
4203
|
-
|
|
4204
|
-
|
|
4586
|
+
mkdirSync11(dirname6(path), { recursive: true });
|
|
4587
|
+
writeFileSync12(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
4205
4588
|
return true;
|
|
4206
4589
|
} catch {
|
|
4207
4590
|
return false;
|
|
@@ -4223,7 +4606,7 @@ function resolveTarget(cfg) {
|
|
|
4223
4606
|
async function ensureSetup() {
|
|
4224
4607
|
const ws = workspace();
|
|
4225
4608
|
if (!isTrusted(ws)) trustDir(ws);
|
|
4226
|
-
|
|
4609
|
+
mkdirSync11(memDir(), { recursive: true });
|
|
4227
4610
|
if (!hooksAlreadyInstalled()) {
|
|
4228
4611
|
try {
|
|
4229
4612
|
await hook("install", []);
|
|
@@ -4233,8 +4616,8 @@ async function ensureSetup() {
|
|
|
4233
4616
|
}
|
|
4234
4617
|
function hooksAlreadyInstalled() {
|
|
4235
4618
|
try {
|
|
4236
|
-
const p =
|
|
4237
|
-
return
|
|
4619
|
+
const p = join13(homedir7(), ".claude", "settings.json");
|
|
4620
|
+
return existsSync12(p) && readFileSync13(p, "utf8").includes("holt hook");
|
|
4238
4621
|
} catch {
|
|
4239
4622
|
return false;
|
|
4240
4623
|
}
|
|
@@ -4283,6 +4666,7 @@ async function launch() {
|
|
|
4283
4666
|
if (ctx) console.log(c.dim(` Holt identity: ${ctx}`));
|
|
4284
4667
|
}
|
|
4285
4668
|
if (target.id === "claude") brandStatusLine();
|
|
4669
|
+
syncSkillCommands(target.id);
|
|
4286
4670
|
const interactive = INTERACTIVE_ARGS[target.id];
|
|
4287
4671
|
const args = [...interactive, ...flags];
|
|
4288
4672
|
console.log(c.dim(' Starting your Holt session. Type "exit" to leave.\n'));
|
|
@@ -4291,7 +4675,7 @@ async function launch() {
|
|
|
4291
4675
|
}
|
|
4292
4676
|
|
|
4293
4677
|
// src/commands/memory.ts
|
|
4294
|
-
import { existsSync as
|
|
4678
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
|
|
4295
4679
|
import { basename } from "path";
|
|
4296
4680
|
async function memoryCmd(sub, rest = []) {
|
|
4297
4681
|
const { ask, close } = createReader();
|
|
@@ -4339,12 +4723,12 @@ async function memoryCmd(sub, rest = []) {
|
|
|
4339
4723
|
return;
|
|
4340
4724
|
}
|
|
4341
4725
|
if (action === "facts") {
|
|
4342
|
-
if (!
|
|
4726
|
+
if (!existsSync13(factsMdPath())) {
|
|
4343
4727
|
console.log(c.dim("\n No facts distilled yet. They form when you end a chat session.\n"));
|
|
4344
4728
|
close();
|
|
4345
4729
|
return;
|
|
4346
4730
|
}
|
|
4347
|
-
const body =
|
|
4731
|
+
const body = readFileSync14(factsMdPath(), "utf8").trim();
|
|
4348
4732
|
console.log("\n" + body + "\n");
|
|
4349
4733
|
close();
|
|
4350
4734
|
return;
|
|
@@ -4358,6 +4742,9 @@ async function memoryCmd(sub, rest = []) {
|
|
|
4358
4742
|
}
|
|
4359
4743
|
const hits = await recall(q, "__none__", 8);
|
|
4360
4744
|
console.log("");
|
|
4745
|
+
if (!await embeddingsAvailable()) {
|
|
4746
|
+
console.log(c.dim(' (memory is in keyword mode; run "holt doctor --fix" to turn semantic recall back on)'));
|
|
4747
|
+
}
|
|
4361
4748
|
if (hits.length === 0) console.log(c.dim(" Nothing relevant found."));
|
|
4362
4749
|
else for (const h of hits) {
|
|
4363
4750
|
const when = new Date(h.turn.ts).toISOString().slice(0, 10);
|
|
@@ -4458,8 +4845,8 @@ function globalCmd(act, rest) {
|
|
|
4458
4845
|
}
|
|
4459
4846
|
|
|
4460
4847
|
// src/commands/wiki.ts
|
|
4461
|
-
import { existsSync as
|
|
4462
|
-
import { join as
|
|
4848
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
|
|
4849
|
+
import { join as join14 } from "path";
|
|
4463
4850
|
import { spawn as spawn3 } from "child_process";
|
|
4464
4851
|
function titleToSlug(title) {
|
|
4465
4852
|
return proposeSlug(title);
|
|
@@ -4652,7 +5039,7 @@ function listCmd() {
|
|
|
4652
5039
|
for (const slug of slugs) {
|
|
4653
5040
|
const pg = readPage(slug);
|
|
4654
5041
|
if (!pg) continue;
|
|
4655
|
-
const size =
|
|
5042
|
+
const size = existsSync14(join14(wikiDir(), slug + ".md")) ? (readFileSync15(join14(wikiDir(), slug + ".md"), "utf8").length / 1024).toFixed(1) : "0.0";
|
|
4656
5043
|
console.log(` ${c.cyan(pg.title.padEnd(28).slice(0, 28))} ${c.dim(pg.updated || " ")} ${size} KB`);
|
|
4657
5044
|
}
|
|
4658
5045
|
console.log("");
|
|
@@ -4670,7 +5057,7 @@ function showCmd(name) {
|
|
|
4670
5057
|
`));
|
|
4671
5058
|
return;
|
|
4672
5059
|
}
|
|
4673
|
-
const raw =
|
|
5060
|
+
const raw = readFileSync15(join14(wikiDir(), slug + ".md"), "utf8");
|
|
4674
5061
|
console.log("\n" + raw.trim() + "\n");
|
|
4675
5062
|
}
|
|
4676
5063
|
async function statusCmd(cfg) {
|
|
@@ -4740,8 +5127,8 @@ function setupCmd(cfg) {
|
|
|
4740
5127
|
`));
|
|
4741
5128
|
}
|
|
4742
5129
|
function openCmd() {
|
|
4743
|
-
const target =
|
|
4744
|
-
if (!
|
|
5130
|
+
const target = existsSync14(wikiIndexPath()) ? wikiIndexPath() : wikiDir();
|
|
5131
|
+
if (!existsSync14(target)) {
|
|
4745
5132
|
console.log(c.dim('\n No wiki yet. Run "holt wiki sync" first.\n'));
|
|
4746
5133
|
return;
|
|
4747
5134
|
}
|
|
@@ -4828,29 +5215,29 @@ async function wikiCmd(sub, rest = []) {
|
|
|
4828
5215
|
|
|
4829
5216
|
// src/commands/skill.ts
|
|
4830
5217
|
import {
|
|
4831
|
-
mkdirSync as
|
|
4832
|
-
writeFileSync as
|
|
4833
|
-
readFileSync as
|
|
4834
|
-
existsSync as
|
|
4835
|
-
readdirSync as
|
|
4836
|
-
statSync as
|
|
4837
|
-
rmSync as
|
|
4838
|
-
cpSync,
|
|
5218
|
+
mkdirSync as mkdirSync13,
|
|
5219
|
+
writeFileSync as writeFileSync14,
|
|
5220
|
+
readFileSync as readFileSync17,
|
|
5221
|
+
existsSync as existsSync16,
|
|
5222
|
+
readdirSync as readdirSync4,
|
|
5223
|
+
statSync as statSync8,
|
|
5224
|
+
rmSync as rmSync6,
|
|
5225
|
+
cpSync as cpSync2,
|
|
4839
5226
|
mkdtempSync
|
|
4840
5227
|
} from "fs";
|
|
4841
|
-
import { join as
|
|
5228
|
+
import { join as join16, resolve as resolve4, sep as sep2, isAbsolute as isAbsolute4 } from "path";
|
|
4842
5229
|
import { tmpdir } from "os";
|
|
4843
5230
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
4844
5231
|
|
|
4845
5232
|
// src/registry.ts
|
|
4846
|
-
import { readFileSync as
|
|
4847
|
-
import { join as
|
|
5233
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync13, existsSync as existsSync15, mkdirSync as mkdirSync12, statSync as statSync7 } from "fs";
|
|
5234
|
+
import { join as join15, isAbsolute as isAbsolute3, resolve as resolve3 } from "path";
|
|
4848
5235
|
import { pathToFileURL, fileURLToPath as fileURLToPath2 } from "url";
|
|
4849
5236
|
var DEFAULT_REGISTRY_URL = "https://raw.githubusercontent.com/holt-os/registry/main/registry.json";
|
|
4850
5237
|
var REGISTRY_REPO_URL = "https://github.com/holt-os/registry";
|
|
4851
5238
|
var CACHE_TTL_MS = 60 * 60 * 1e3;
|
|
4852
5239
|
function registryCachePath() {
|
|
4853
|
-
return
|
|
5240
|
+
return join15(GLOBAL_DIR, "registry-cache.json");
|
|
4854
5241
|
}
|
|
4855
5242
|
function registryUrl() {
|
|
4856
5243
|
const env = (process.env.HOLT_REGISTRY_URL || "").trim();
|
|
@@ -4889,8 +5276,8 @@ function parseRegistry(text) {
|
|
|
4889
5276
|
function readCache() {
|
|
4890
5277
|
try {
|
|
4891
5278
|
const path = registryCachePath();
|
|
4892
|
-
if (!
|
|
4893
|
-
const raw = JSON.parse(
|
|
5279
|
+
if (!existsSync15(path)) return null;
|
|
5280
|
+
const raw = JSON.parse(readFileSync16(path, "utf8"));
|
|
4894
5281
|
if (!raw || typeof raw.fetchedAt !== "number" || typeof raw.url !== "string") return null;
|
|
4895
5282
|
const registry = coerceRegistry(raw.registry);
|
|
4896
5283
|
if (!registry) return null;
|
|
@@ -4901,9 +5288,9 @@ function readCache() {
|
|
|
4901
5288
|
}
|
|
4902
5289
|
function writeCache(url, registry) {
|
|
4903
5290
|
try {
|
|
4904
|
-
|
|
5291
|
+
mkdirSync12(GLOBAL_DIR, { recursive: true });
|
|
4905
5292
|
const body = { fetchedAt: Date.now(), url, registry };
|
|
4906
|
-
|
|
5293
|
+
writeFileSync13(registryCachePath(), JSON.stringify(body, null, 2) + "\n", "utf8");
|
|
4907
5294
|
} catch {
|
|
4908
5295
|
}
|
|
4909
5296
|
}
|
|
@@ -4921,10 +5308,10 @@ function localPathFor(loc) {
|
|
|
4921
5308
|
var UNREACHABLE_MSG = "no registry reachable; set HOLT_REGISTRY_URL or the community registry is not live yet";
|
|
4922
5309
|
function loadLocal(path) {
|
|
4923
5310
|
try {
|
|
4924
|
-
if (!
|
|
5311
|
+
if (!existsSync15(path)) {
|
|
4925
5312
|
return { registry: null, fromCache: false, error: UNREACHABLE_MSG };
|
|
4926
5313
|
}
|
|
4927
|
-
const registry = parseRegistry(
|
|
5314
|
+
const registry = parseRegistry(readFileSync16(path, "utf8"));
|
|
4928
5315
|
if (!registry) {
|
|
4929
5316
|
return { registry: null, fromCache: false, error: "registry file is not valid JSON" };
|
|
4930
5317
|
}
|
|
@@ -4993,6 +5380,12 @@ function buildPublishEntry(input) {
|
|
|
4993
5380
|
}
|
|
4994
5381
|
|
|
4995
5382
|
// src/commands/skill.ts
|
|
5383
|
+
function enabledBrains() {
|
|
5384
|
+
const cfg = loadConfig();
|
|
5385
|
+
if (!cfg) return [...BRAIN_IDS];
|
|
5386
|
+
const on2 = BRAIN_IDS.filter((b) => cfg.brains[b].enabled);
|
|
5387
|
+
return on2.length ? on2 : [...BRAIN_IDS];
|
|
5388
|
+
}
|
|
4996
5389
|
function usage() {
|
|
4997
5390
|
console.log(c.dim([
|
|
4998
5391
|
"",
|
|
@@ -5053,12 +5446,12 @@ function resolveSubpathDir(fetched, subpath) {
|
|
|
5053
5446
|
return target;
|
|
5054
5447
|
}
|
|
5055
5448
|
function findSkillDir(root) {
|
|
5056
|
-
if (
|
|
5449
|
+
if (existsSync16(join16(root, "SKILL.md"))) return root;
|
|
5057
5450
|
let subs = [];
|
|
5058
5451
|
try {
|
|
5059
|
-
subs =
|
|
5452
|
+
subs = readdirSync4(root).filter((f) => {
|
|
5060
5453
|
try {
|
|
5061
|
-
return
|
|
5454
|
+
return statSync8(join16(root, f)).isDirectory();
|
|
5062
5455
|
} catch {
|
|
5063
5456
|
return false;
|
|
5064
5457
|
}
|
|
@@ -5066,8 +5459,8 @@ function findSkillDir(root) {
|
|
|
5066
5459
|
} catch {
|
|
5067
5460
|
return null;
|
|
5068
5461
|
}
|
|
5069
|
-
const withSkill = subs.filter((f) =>
|
|
5070
|
-
if (withSkill.length === 1) return
|
|
5462
|
+
const withSkill = subs.filter((f) => existsSync16(join16(root, f, "SKILL.md")));
|
|
5463
|
+
if (withSkill.length === 1) return join16(root, withSkill[0]);
|
|
5071
5464
|
return null;
|
|
5072
5465
|
}
|
|
5073
5466
|
function cmdList() {
|
|
@@ -5108,7 +5501,7 @@ function cmdShow(name) {
|
|
|
5108
5501
|
const { skill, body } = loaded;
|
|
5109
5502
|
console.log("\n" + c.accent(skill.name) + c.dim(` (${skill.scope})`));
|
|
5110
5503
|
if (skill.description) console.log(" " + skill.description);
|
|
5111
|
-
console.log(c.dim(" " +
|
|
5504
|
+
console.log(c.dim(" " + join16(skill.dir, "SKILL.md")));
|
|
5112
5505
|
console.log("");
|
|
5113
5506
|
console.log(c.dim(body));
|
|
5114
5507
|
console.log("");
|
|
@@ -5120,19 +5513,19 @@ function cmdCreate(name, global) {
|
|
|
5120
5513
|
return;
|
|
5121
5514
|
}
|
|
5122
5515
|
const scope = global ? "global" : "workspace";
|
|
5123
|
-
const dir =
|
|
5124
|
-
if (
|
|
5516
|
+
const dir = join16(skillsRoot(scope), clean);
|
|
5517
|
+
if (existsSync16(dir)) {
|
|
5125
5518
|
console.error(c.red(`
|
|
5126
5519
|
A ${scope} skill named "${clean}" already exists.`));
|
|
5127
5520
|
console.error(c.dim(" " + dir + "\n"));
|
|
5128
5521
|
process.exitCode = 1;
|
|
5129
5522
|
return;
|
|
5130
5523
|
}
|
|
5131
|
-
|
|
5132
|
-
|
|
5524
|
+
mkdirSync13(dir, { recursive: true });
|
|
5525
|
+
writeFileSync14(join16(dir, "SKILL.md"), SKILL_TEMPLATE(clean), "utf8");
|
|
5133
5526
|
console.log(c.green(`
|
|
5134
5527
|
Created ${scope} skill "${clean}".`));
|
|
5135
|
-
console.log(c.dim(" " +
|
|
5528
|
+
console.log(c.dim(" " + join16(dir, "SKILL.md")));
|
|
5136
5529
|
console.log(c.dim(' Edit it, then run it in chat with "/skill ' + clean + '".\n'));
|
|
5137
5530
|
}
|
|
5138
5531
|
function installFromSource(source, scope) {
|
|
@@ -5141,7 +5534,7 @@ function installFromSource(source, scope) {
|
|
|
5141
5534
|
let tempDir = "";
|
|
5142
5535
|
try {
|
|
5143
5536
|
if (isGitUrl(base)) {
|
|
5144
|
-
tempDir = mkdtempSync(
|
|
5537
|
+
tempDir = mkdtempSync(join16(tmpdir(), "holt-skill-"));
|
|
5145
5538
|
console.log(c.dim(`
|
|
5146
5539
|
Cloning ${base} ...`));
|
|
5147
5540
|
const res = spawnSync4("git", ["clone", "--depth", "1", base, tempDir], { stdio: "ignore" });
|
|
@@ -5153,7 +5546,7 @@ function installFromSource(source, scope) {
|
|
|
5153
5546
|
fetched = tempDir;
|
|
5154
5547
|
} else {
|
|
5155
5548
|
fetched = resolve4(base);
|
|
5156
|
-
if (!
|
|
5549
|
+
if (!existsSync16(fetched)) {
|
|
5157
5550
|
console.error(c.red(`
|
|
5158
5551
|
No such path: ${fetched}
|
|
5159
5552
|
`));
|
|
@@ -5171,7 +5564,7 @@ function installFromSource(source, scope) {
|
|
|
5171
5564
|
process.exitCode = 1;
|
|
5172
5565
|
return false;
|
|
5173
5566
|
}
|
|
5174
|
-
if (!
|
|
5567
|
+
if (!existsSync16(target) || !statSync8(target).isDirectory()) {
|
|
5175
5568
|
console.error(c.red(`
|
|
5176
5569
|
Subpath "${subpath}" does not exist in the source.
|
|
5177
5570
|
`));
|
|
@@ -5191,7 +5584,7 @@ function installFromSource(source, scope) {
|
|
|
5191
5584
|
}
|
|
5192
5585
|
let skillName = "";
|
|
5193
5586
|
try {
|
|
5194
|
-
const { data } = parseFrontmatter(
|
|
5587
|
+
const { data } = parseFrontmatter(readFileSync17(join16(skillDir, "SKILL.md"), "utf8"));
|
|
5195
5588
|
skillName = sanitizeName(data.name || "");
|
|
5196
5589
|
} catch {
|
|
5197
5590
|
skillName = "";
|
|
@@ -5202,26 +5595,26 @@ function installFromSource(source, scope) {
|
|
|
5202
5595
|
process.exitCode = 1;
|
|
5203
5596
|
return false;
|
|
5204
5597
|
}
|
|
5205
|
-
const dest =
|
|
5206
|
-
if (
|
|
5598
|
+
const dest = join16(skillsRoot(scope), skillName);
|
|
5599
|
+
if (existsSync16(dest)) {
|
|
5207
5600
|
console.error(c.red(`
|
|
5208
5601
|
A ${scope} skill named "${skillName}" already exists.`));
|
|
5209
5602
|
console.error(c.dim(" " + dest + "\n"));
|
|
5210
5603
|
process.exitCode = 1;
|
|
5211
5604
|
return false;
|
|
5212
5605
|
}
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
const nestedGit =
|
|
5216
|
-
if (
|
|
5606
|
+
mkdirSync13(skillsRoot(scope), { recursive: true });
|
|
5607
|
+
cpSync2(skillDir, dest, { recursive: true });
|
|
5608
|
+
const nestedGit = join16(dest, ".git");
|
|
5609
|
+
if (existsSync16(nestedGit)) rmSync6(nestedGit, { recursive: true, force: true });
|
|
5217
5610
|
console.log(c.green(`
|
|
5218
5611
|
Installed ${scope} skill "${skillName}".`));
|
|
5219
|
-
console.log(c.dim(" " +
|
|
5612
|
+
console.log(c.dim(" " + join16(dest, "SKILL.md") + "\n"));
|
|
5220
5613
|
return true;
|
|
5221
5614
|
} finally {
|
|
5222
|
-
if (tempDir &&
|
|
5615
|
+
if (tempDir && existsSync16(tempDir)) {
|
|
5223
5616
|
try {
|
|
5224
|
-
|
|
5617
|
+
rmSync6(tempDir, { recursive: true, force: true });
|
|
5225
5618
|
} catch {
|
|
5226
5619
|
}
|
|
5227
5620
|
}
|
|
@@ -5234,8 +5627,8 @@ async function cmdAdd(source, global, refresh) {
|
|
|
5234
5627
|
}
|
|
5235
5628
|
const scope = global ? "global" : "workspace";
|
|
5236
5629
|
const { base } = splitSubpath(source);
|
|
5237
|
-
if (isGitUrl(base) ||
|
|
5238
|
-
installFromSource(source, scope);
|
|
5630
|
+
if (isGitUrl(base) || existsSync16(resolve4(base))) {
|
|
5631
|
+
if (installFromSource(source, scope)) syncAllBrains(enabledBrains());
|
|
5239
5632
|
return;
|
|
5240
5633
|
}
|
|
5241
5634
|
const { registry, error } = await loadRegistry({ refresh });
|
|
@@ -5256,7 +5649,7 @@ async function cmdAdd(source, global, refresh) {
|
|
|
5256
5649
|
}
|
|
5257
5650
|
console.log(c.dim(`
|
|
5258
5651
|
Resolved "${hit.name}" -> ${hit.source}`));
|
|
5259
|
-
installFromSource(hit.source, scope);
|
|
5652
|
+
if (installFromSource(hit.source, scope)) syncAllBrains(enabledBrains());
|
|
5260
5653
|
}
|
|
5261
5654
|
async function cmdSearch(query, refresh) {
|
|
5262
5655
|
const { registry, error, fromCache } = await loadRegistry({ refresh });
|
|
@@ -5306,7 +5699,7 @@ function cmdPublish(name) {
|
|
|
5306
5699
|
}
|
|
5307
5700
|
let raw = "";
|
|
5308
5701
|
try {
|
|
5309
|
-
raw =
|
|
5702
|
+
raw = readFileSync17(join16(loaded.skill.dir, "SKILL.md"), "utf8");
|
|
5310
5703
|
} catch {
|
|
5311
5704
|
console.error(c.red("\n Could not read the skill's SKILL.md.\n"));
|
|
5312
5705
|
process.exitCode = 1;
|
|
@@ -5316,14 +5709,14 @@ function cmdPublish(name) {
|
|
|
5316
5709
|
skillName = sanitizeName(data.name || loaded.skill.name);
|
|
5317
5710
|
description = (data.description || loaded.skill.description || "").trim();
|
|
5318
5711
|
} else {
|
|
5319
|
-
const file =
|
|
5320
|
-
if (!
|
|
5712
|
+
const file = join16(process.cwd(), "SKILL.md");
|
|
5713
|
+
if (!existsSync16(file)) {
|
|
5321
5714
|
console.error(c.red("\n No SKILL.md in the current directory, and no skill name given."));
|
|
5322
5715
|
console.error(c.dim(" Usage: holt skill publish [<name>]\n"));
|
|
5323
5716
|
process.exitCode = 1;
|
|
5324
5717
|
return;
|
|
5325
5718
|
}
|
|
5326
|
-
const { data } = parseFrontmatter(
|
|
5719
|
+
const { data } = parseFrontmatter(readFileSync17(file, "utf8"));
|
|
5327
5720
|
skillName = sanitizeName(data.name || process.cwd().split(/[\\/]/).pop() || "");
|
|
5328
5721
|
description = (data.description || "").trim();
|
|
5329
5722
|
sourceHint = process.cwd();
|
|
@@ -5359,11 +5752,11 @@ async function cmdRemove(name, ask) {
|
|
|
5359
5752
|
console.log(c.dim("\n Usage: holt skill remove <name>\n"));
|
|
5360
5753
|
return;
|
|
5361
5754
|
}
|
|
5362
|
-
const wsDir =
|
|
5363
|
-
const globalDir =
|
|
5364
|
-
const target =
|
|
5755
|
+
const wsDir = join16(skillsRoot("workspace"), clean);
|
|
5756
|
+
const globalDir = join16(skillsRoot("global"), clean);
|
|
5757
|
+
const target = existsSync16(wsDir) ? wsDir : existsSync16(globalDir) ? globalDir : "";
|
|
5365
5758
|
if (!target) {
|
|
5366
|
-
if (
|
|
5759
|
+
if (existsSync16(join16(skillsRoot("builtin"), clean))) {
|
|
5367
5760
|
console.error(c.red(`
|
|
5368
5761
|
"${clean}" is a built-in skill and cannot be removed; it ships inside Holt.`));
|
|
5369
5762
|
console.error(c.dim(" Override it instead: create a workspace or global skill with the same name.\n"));
|
|
@@ -5380,7 +5773,9 @@ async function cmdRemove(name, ask) {
|
|
|
5380
5773
|
const a = (await ask(`
|
|
5381
5774
|
Delete ${scope} skill "${clean}"? [y/N] `) ?? "").trim().toLowerCase();
|
|
5382
5775
|
if (a === "y" || a === "yes") {
|
|
5383
|
-
|
|
5776
|
+
rmSync6(target, { recursive: true, force: true });
|
|
5777
|
+
removeSkillArtifacts(clean);
|
|
5778
|
+
syncAllBrains(enabledBrains());
|
|
5384
5779
|
console.log(c.green(" Removed.\n"));
|
|
5385
5780
|
} else {
|
|
5386
5781
|
console.log(c.dim(" Kept.\n"));
|
|
@@ -5436,8 +5831,8 @@ async function skillCmd(sub, rest = []) {
|
|
|
5436
5831
|
}
|
|
5437
5832
|
|
|
5438
5833
|
// src/commands/graph.ts
|
|
5439
|
-
import { writeFileSync as
|
|
5440
|
-
import { join as
|
|
5834
|
+
import { writeFileSync as writeFileSync15, mkdirSync as mkdirSync14, existsSync as existsSync17 } from "fs";
|
|
5835
|
+
import { join as join18, dirname as dirname8, resolve as resolve5 } from "path";
|
|
5441
5836
|
import { spawn as spawn4 } from "child_process";
|
|
5442
5837
|
|
|
5443
5838
|
// src/communities.ts
|
|
@@ -6234,8 +6629,8 @@ function renderGraphHtml(graph2, meta) {
|
|
|
6234
6629
|
}
|
|
6235
6630
|
|
|
6236
6631
|
// src/ingest.ts
|
|
6237
|
-
import { readdirSync as
|
|
6238
|
-
import { join as
|
|
6632
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync18, statSync as statSync9 } from "fs";
|
|
6633
|
+
import { join as join17, relative, sep as sep3, extname, dirname as dirname7, basename as basename2 } from "path";
|
|
6239
6634
|
var CODE_EXT = /* @__PURE__ */ new Set([
|
|
6240
6635
|
"js",
|
|
6241
6636
|
"jsx",
|
|
@@ -6297,7 +6692,7 @@ function ingestFolder(root, want = { code: true, docs: true }) {
|
|
|
6297
6692
|
const dir = stack.pop();
|
|
6298
6693
|
let entries;
|
|
6299
6694
|
try {
|
|
6300
|
-
entries =
|
|
6695
|
+
entries = readdirSync5(dir).sort((a, b) => a.localeCompare(b));
|
|
6301
6696
|
} catch {
|
|
6302
6697
|
skipped.unreadable++;
|
|
6303
6698
|
continue;
|
|
@@ -6308,10 +6703,10 @@ function ingestFolder(root, want = { code: true, docs: true }) {
|
|
|
6308
6703
|
skipped.hitFileCap = true;
|
|
6309
6704
|
break;
|
|
6310
6705
|
}
|
|
6311
|
-
const full =
|
|
6706
|
+
const full = join17(dir, name);
|
|
6312
6707
|
let st;
|
|
6313
6708
|
try {
|
|
6314
|
-
st =
|
|
6709
|
+
st = statSync9(full);
|
|
6315
6710
|
} catch {
|
|
6316
6711
|
skipped.unreadable++;
|
|
6317
6712
|
continue;
|
|
@@ -6341,7 +6736,7 @@ function ingestFolder(root, want = { code: true, docs: true }) {
|
|
|
6341
6736
|
}
|
|
6342
6737
|
let content;
|
|
6343
6738
|
try {
|
|
6344
|
-
content =
|
|
6739
|
+
content = readFileSync18(full, "utf8");
|
|
6345
6740
|
} catch {
|
|
6346
6741
|
skipped.unreadable++;
|
|
6347
6742
|
continue;
|
|
@@ -6475,7 +6870,7 @@ function resolvePySpec(f, spec, byId) {
|
|
|
6475
6870
|
return null;
|
|
6476
6871
|
}
|
|
6477
6872
|
function toPosixDir(id) {
|
|
6478
|
-
const d = toPosix(
|
|
6873
|
+
const d = toPosix(dirname7(id));
|
|
6479
6874
|
return d === "" || d === "." ? "." : d;
|
|
6480
6875
|
}
|
|
6481
6876
|
var MD_LINK_RE = /\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
|
|
@@ -6647,7 +7042,7 @@ async function graph(args = []) {
|
|
|
6647
7042
|
const ingestCode = opts.code;
|
|
6648
7043
|
const ingestDocs = opts.docs;
|
|
6649
7044
|
const turns = loadTurns();
|
|
6650
|
-
const includeWiki = opts.wiki !== void 0 ? opts.wiki :
|
|
7045
|
+
const includeWiki = opts.wiki !== void 0 ? opts.wiki : existsSync17(wikiDir());
|
|
6651
7046
|
const wikiPages = includeWiki ? wikiGraphPages() : [];
|
|
6652
7047
|
let fileNodeCount = 0;
|
|
6653
7048
|
let docNodeCount = 0;
|
|
@@ -6677,9 +7072,9 @@ async function graph(args = []) {
|
|
|
6677
7072
|
if (codeGraph) assignCommunities(g);
|
|
6678
7073
|
if (opts.report) {
|
|
6679
7074
|
const md = renderReport(g, { workspace: workspace() });
|
|
6680
|
-
const reportPath = opts.out ? resolve5(opts.out) :
|
|
6681
|
-
|
|
6682
|
-
|
|
7075
|
+
const reportPath = opts.out ? resolve5(opts.out) : join18(workspace(), "GRAPH_REPORT.md");
|
|
7076
|
+
mkdirSync14(dirname8(reportPath), { recursive: true });
|
|
7077
|
+
writeFileSync15(reportPath, md, "utf8");
|
|
6683
7078
|
const communities = new Set(g.nodes.map((n) => n.community ?? "0")).size;
|
|
6684
7079
|
console.log("\n" + c.accent("Graph report written") + c.dim(" (this folder)"));
|
|
6685
7080
|
console.log(` nodes ${g.nodes.length} (${fileNodeCount} files, ${docNodeCount} docs)`);
|
|
@@ -6699,9 +7094,9 @@ async function graph(args = []) {
|
|
|
6699
7094
|
concepts: conceptCount,
|
|
6700
7095
|
edges: g.edges.length
|
|
6701
7096
|
});
|
|
6702
|
-
const outPath = opts.out ? resolve5(opts.out) :
|
|
6703
|
-
|
|
6704
|
-
|
|
7097
|
+
const outPath = opts.out ? resolve5(opts.out) : join18(wsHoltDir(), "graph.html");
|
|
7098
|
+
mkdirSync14(dirname8(outPath), { recursive: true });
|
|
7099
|
+
writeFileSync15(outPath, html, "utf8");
|
|
6705
7100
|
console.log("\n" + c.accent("Memory graph built") + c.dim(" (this folder)"));
|
|
6706
7101
|
const parts = [`${turns.length} turns`, `${conceptCount} concepts`];
|
|
6707
7102
|
if (wikiCount) parts.push(`${wikiCount} wiki pages`);
|
|
@@ -6989,7 +7384,7 @@ function holtTools() {
|
|
|
6989
7384
|
}
|
|
6990
7385
|
|
|
6991
7386
|
// src/version.ts
|
|
6992
|
-
var VERSION = "0.
|
|
7387
|
+
var VERSION = "0.15.0";
|
|
6993
7388
|
|
|
6994
7389
|
// src/commands/mcp.ts
|
|
6995
7390
|
function printSetup() {
|
|
@@ -7111,7 +7506,7 @@ async function statusline() {
|
|
|
7111
7506
|
}
|
|
7112
7507
|
|
|
7113
7508
|
// src/commands/run.ts
|
|
7114
|
-
import { writeFileSync as
|
|
7509
|
+
import { writeFileSync as writeFileSync16 } from "fs";
|
|
7115
7510
|
import { resolve as resolve6 } from "path";
|
|
7116
7511
|
var USAGE3 = [
|
|
7117
7512
|
" holt run <task> [options]",
|
|
@@ -7133,7 +7528,7 @@ function parseArgs2(args) {
|
|
|
7133
7528
|
let out;
|
|
7134
7529
|
let noStore = false;
|
|
7135
7530
|
let noRecall = false;
|
|
7136
|
-
let
|
|
7531
|
+
let quiet2 = false;
|
|
7137
7532
|
for (let i = 0; i < args.length; i++) {
|
|
7138
7533
|
const a = args[i];
|
|
7139
7534
|
if (a === "--brain") {
|
|
@@ -7153,12 +7548,12 @@ function parseArgs2(args) {
|
|
|
7153
7548
|
continue;
|
|
7154
7549
|
}
|
|
7155
7550
|
if (a === "--quiet") {
|
|
7156
|
-
|
|
7551
|
+
quiet2 = true;
|
|
7157
7552
|
continue;
|
|
7158
7553
|
}
|
|
7159
7554
|
words2.push(a);
|
|
7160
7555
|
}
|
|
7161
|
-
return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet };
|
|
7556
|
+
return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet: quiet2 };
|
|
7162
7557
|
}
|
|
7163
7558
|
async function run(args) {
|
|
7164
7559
|
const p = parseArgs2(args);
|
|
@@ -7203,7 +7598,7 @@ async function run(args) {
|
|
|
7203
7598
|
if (p.out) {
|
|
7204
7599
|
const target = resolve6(process.cwd(), p.out);
|
|
7205
7600
|
try {
|
|
7206
|
-
|
|
7601
|
+
writeFileSync16(target, result.text, "utf8");
|
|
7207
7602
|
process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|
|
7208
7603
|
} catch (e) {
|
|
7209
7604
|
process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
|
|
@@ -7214,8 +7609,8 @@ async function run(args) {
|
|
|
7214
7609
|
|
|
7215
7610
|
// src/commands/schedule.ts
|
|
7216
7611
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
7217
|
-
import { writeFileSync as
|
|
7218
|
-
import { dirname as
|
|
7612
|
+
import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync15, rmSync as rmSync7, existsSync as existsSync18 } from "fs";
|
|
7613
|
+
import { dirname as dirname9 } from "path";
|
|
7219
7614
|
function parseAdd(rest) {
|
|
7220
7615
|
const positional = [];
|
|
7221
7616
|
const opts = { notify: false };
|
|
@@ -7238,8 +7633,8 @@ function parseAdd(rest) {
|
|
|
7238
7633
|
}
|
|
7239
7634
|
function installDarwin(job, holtPath) {
|
|
7240
7635
|
const path = plistPath(job.id);
|
|
7241
|
-
|
|
7242
|
-
|
|
7636
|
+
mkdirSync15(dirname9(path), { recursive: true });
|
|
7637
|
+
writeFileSync17(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
7243
7638
|
const res = spawnSync5("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
7244
7639
|
if (res.status === 0) {
|
|
7245
7640
|
console.log(c.green(" Loaded into launchd."));
|
|
@@ -7327,10 +7722,10 @@ function listCmd2() {
|
|
|
7327
7722
|
}
|
|
7328
7723
|
function removeDarwin(id) {
|
|
7329
7724
|
const path = plistPath(id);
|
|
7330
|
-
if (
|
|
7725
|
+
if (existsSync18(path)) {
|
|
7331
7726
|
spawnSync5("launchctl", ["unload", path], { encoding: "utf8" });
|
|
7332
7727
|
try {
|
|
7333
|
-
|
|
7728
|
+
rmSync7(path);
|
|
7334
7729
|
} catch {
|
|
7335
7730
|
}
|
|
7336
7731
|
}
|
|
@@ -7390,29 +7785,29 @@ async function schedule(sub, rest = []) {
|
|
|
7390
7785
|
|
|
7391
7786
|
// src/commands/routine.ts
|
|
7392
7787
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
7393
|
-
import { writeFileSync as
|
|
7394
|
-
import { dirname as
|
|
7788
|
+
import { writeFileSync as writeFileSync19, mkdirSync as mkdirSync17, rmSync as rmSync8, existsSync as existsSync19 } from "fs";
|
|
7789
|
+
import { dirname as dirname10, resolve as resolve7 } from "path";
|
|
7395
7790
|
|
|
7396
7791
|
// src/routines.ts
|
|
7397
|
-
import { readFileSync as
|
|
7398
|
-
import { join as
|
|
7792
|
+
import { readFileSync as readFileSync19, writeFileSync as writeFileSync18, mkdirSync as mkdirSync16 } from "fs";
|
|
7793
|
+
import { join as join19 } from "path";
|
|
7399
7794
|
function sanitizeRoutineName(name) {
|
|
7400
7795
|
return (name || "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-");
|
|
7401
7796
|
}
|
|
7402
7797
|
function routinesPath() {
|
|
7403
|
-
return
|
|
7798
|
+
return join19(GLOBAL_DIR, "routines.json");
|
|
7404
7799
|
}
|
|
7405
7800
|
function loadRoutines() {
|
|
7406
7801
|
try {
|
|
7407
|
-
const raw = JSON.parse(
|
|
7802
|
+
const raw = JSON.parse(readFileSync19(routinesPath(), "utf8"));
|
|
7408
7803
|
return Array.isArray(raw) ? raw : [];
|
|
7409
7804
|
} catch {
|
|
7410
7805
|
return [];
|
|
7411
7806
|
}
|
|
7412
7807
|
}
|
|
7413
7808
|
function saveRoutines(routines) {
|
|
7414
|
-
|
|
7415
|
-
|
|
7809
|
+
mkdirSync16(GLOBAL_DIR, { recursive: true });
|
|
7810
|
+
writeFileSync18(routinesPath(), JSON.stringify(routines, null, 2) + "\n", "utf8");
|
|
7416
7811
|
}
|
|
7417
7812
|
function findRoutine(name) {
|
|
7418
7813
|
const want = sanitizeRoutineName(name);
|
|
@@ -7472,8 +7867,8 @@ function routineJobId(name) {
|
|
|
7472
7867
|
}
|
|
7473
7868
|
function installDarwin2(job, holtPath) {
|
|
7474
7869
|
const path = plistPath(job.id);
|
|
7475
|
-
|
|
7476
|
-
|
|
7870
|
+
mkdirSync17(dirname10(path), { recursive: true });
|
|
7871
|
+
writeFileSync19(path, buildLaunchdPlist(job, holtPath), "utf8");
|
|
7477
7872
|
const res = spawnSync6("launchctl", ["load", "-w", path], { encoding: "utf8" });
|
|
7478
7873
|
if (res.status === 0) {
|
|
7479
7874
|
console.log(c.green(" Loaded into launchd."));
|
|
@@ -7517,10 +7912,10 @@ function installSchedule(r) {
|
|
|
7517
7912
|
}
|
|
7518
7913
|
function removeScheduleDarwin(id) {
|
|
7519
7914
|
const path = plistPath(id);
|
|
7520
|
-
if (
|
|
7915
|
+
if (existsSync19(path)) {
|
|
7521
7916
|
spawnSync6("launchctl", ["unload", path], { encoding: "utf8" });
|
|
7522
7917
|
try {
|
|
7523
|
-
|
|
7918
|
+
rmSync8(path);
|
|
7524
7919
|
} catch {
|
|
7525
7920
|
}
|
|
7526
7921
|
}
|
|
@@ -7648,7 +8043,7 @@ function buildRoutinePrompt(r) {
|
|
|
7648
8043
|
return { prompt };
|
|
7649
8044
|
}
|
|
7650
8045
|
async function runCmd(rest) {
|
|
7651
|
-
let
|
|
8046
|
+
let quiet2 = false;
|
|
7652
8047
|
let outOverride;
|
|
7653
8048
|
let notifyOverride = false;
|
|
7654
8049
|
let brainOverride;
|
|
@@ -7656,7 +8051,7 @@ async function runCmd(rest) {
|
|
|
7656
8051
|
for (let i = 0; i < rest.length; i++) {
|
|
7657
8052
|
const a = rest[i];
|
|
7658
8053
|
if (a === void 0) continue;
|
|
7659
|
-
if (a === "--quiet")
|
|
8054
|
+
if (a === "--quiet") quiet2 = true;
|
|
7660
8055
|
else if (a === "--notify") notifyOverride = true;
|
|
7661
8056
|
else if (a === "--out") outOverride = rest[++i];
|
|
7662
8057
|
else if (a === "--brain") brainOverride = rest[++i];
|
|
@@ -7697,7 +8092,7 @@ async function runCmd(rest) {
|
|
|
7697
8092
|
let streamed = false;
|
|
7698
8093
|
const result = await runTask(built.prompt, {
|
|
7699
8094
|
...brainId ? { brainId } : {},
|
|
7700
|
-
onChunk:
|
|
8095
|
+
onChunk: quiet2 ? void 0 : (ch) => {
|
|
7701
8096
|
streamed = true;
|
|
7702
8097
|
process.stdout.write(ch);
|
|
7703
8098
|
}
|
|
@@ -7708,13 +8103,13 @@ async function runCmd(rest) {
|
|
|
7708
8103
|
process.exitCode = 1;
|
|
7709
8104
|
return;
|
|
7710
8105
|
}
|
|
7711
|
-
if (!
|
|
7712
|
-
if (!
|
|
8106
|
+
if (!quiet2 && !streamed) process.stdout.write(result.text);
|
|
8107
|
+
if (!quiet2 && (streamed || result.text) && !result.text.endsWith("\n")) process.stdout.write("\n");
|
|
7713
8108
|
if (outTarget) {
|
|
7714
8109
|
const target = resolve7(routine2.workspace, outTarget);
|
|
7715
8110
|
try {
|
|
7716
|
-
|
|
7717
|
-
if (!
|
|
8111
|
+
writeFileSync19(target, result.text, "utf8");
|
|
8112
|
+
if (!quiet2) process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|
|
7718
8113
|
} catch (e) {
|
|
7719
8114
|
process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
|
|
7720
8115
|
process.exitCode = 1;
|
|
@@ -7851,7 +8246,7 @@ async function routine(sub, rest = []) {
|
|
|
7851
8246
|
}
|
|
7852
8247
|
|
|
7853
8248
|
// src/commands/notify.ts
|
|
7854
|
-
import { readFileSync as
|
|
8249
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
7855
8250
|
async function notify(args) {
|
|
7856
8251
|
const nonFlag = args.filter((a) => a !== "-" && !a.startsWith("--"));
|
|
7857
8252
|
const wantsStdin = args.includes("-") || !process.stdin.isTTY;
|
|
@@ -7884,12 +8279,16 @@ async function notify(args) {
|
|
|
7884
8279
|
}
|
|
7885
8280
|
function readStdin3() {
|
|
7886
8281
|
try {
|
|
7887
|
-
return
|
|
8282
|
+
return readFileSync20(0, "utf8");
|
|
7888
8283
|
} catch {
|
|
7889
8284
|
return "";
|
|
7890
8285
|
}
|
|
7891
8286
|
}
|
|
7892
8287
|
|
|
8288
|
+
// src/commands/doctor.ts
|
|
8289
|
+
import { spawn as spawn5, spawnSync as spawnSync7 } from "child_process";
|
|
8290
|
+
import { existsSync as existsSync20 } from "fs";
|
|
8291
|
+
|
|
7893
8292
|
// src/specs.ts
|
|
7894
8293
|
import os from "os";
|
|
7895
8294
|
var BYTES_PER_GB = 1024 ** 3;
|
|
@@ -8009,10 +8408,63 @@ function good(text) {
|
|
|
8009
8408
|
function warn(text) {
|
|
8010
8409
|
console.log(" " + c.red(text));
|
|
8011
8410
|
}
|
|
8012
|
-
async function
|
|
8411
|
+
async function ollamaTags() {
|
|
8412
|
+
const url = process.env.HOLT_OLLAMA_URL || "http://127.0.0.1:11434";
|
|
8413
|
+
try {
|
|
8414
|
+
const res = await fetch(`${url}/api/tags`, { signal: AbortSignal.timeout(1500) });
|
|
8415
|
+
if (!res.ok) return null;
|
|
8416
|
+
const data = await res.json();
|
|
8417
|
+
return (data.models ?? []).map((m) => String(m.name ?? ""));
|
|
8418
|
+
} catch {
|
|
8419
|
+
return null;
|
|
8420
|
+
}
|
|
8421
|
+
}
|
|
8422
|
+
async function fixSemanticMemory(fixed) {
|
|
8423
|
+
let tags = await ollamaTags();
|
|
8424
|
+
if (tags === null) {
|
|
8425
|
+
if (!isInstalled("ollama")) {
|
|
8426
|
+
note("Ollama is not installed, so there is nothing to start.");
|
|
8427
|
+
note(
|
|
8428
|
+
process.platform === "darwin" ? 'Install it with: brew install ollama (then run "holt doctor --fix" again)' : 'Install it from https://ollama.com/download, then run "holt doctor --fix" again.'
|
|
8429
|
+
);
|
|
8430
|
+
return false;
|
|
8431
|
+
}
|
|
8432
|
+
note("Ollama is installed but not running. Starting it...");
|
|
8433
|
+
try {
|
|
8434
|
+
const child = spawn5("ollama", ["serve"], { detached: true, stdio: "ignore" });
|
|
8435
|
+
child.unref();
|
|
8436
|
+
} catch {
|
|
8437
|
+
warn('Could not start Ollama. Start it yourself (open the Ollama app or run "ollama serve").');
|
|
8438
|
+
return false;
|
|
8439
|
+
}
|
|
8440
|
+
for (let i = 0; i < 16 && tags === null; i++) {
|
|
8441
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
8442
|
+
tags = await ollamaTags();
|
|
8443
|
+
}
|
|
8444
|
+
if (tags === null) {
|
|
8445
|
+
warn('Ollama did not come up. Open the Ollama app once, then run "holt doctor --fix" again.');
|
|
8446
|
+
return false;
|
|
8447
|
+
}
|
|
8448
|
+
fixed.push("Started the Ollama daemon.");
|
|
8449
|
+
}
|
|
8450
|
+
if (!tags.some((t) => t.startsWith(EMBED_MODEL))) {
|
|
8451
|
+
note(`The "${EMBED_MODEL}" model is missing. Downloading it (one-time, ~275 MB)...`);
|
|
8452
|
+
const r = spawnSync7("ollama", ["pull", EMBED_MODEL], { stdio: "inherit" });
|
|
8453
|
+
if (r.status !== 0) {
|
|
8454
|
+
warn(`Could not download ${EMBED_MODEL}. Check your internet connection and retry.`);
|
|
8455
|
+
return false;
|
|
8456
|
+
}
|
|
8457
|
+
fixed.push(`Downloaded the "${EMBED_MODEL}" model.`);
|
|
8458
|
+
}
|
|
8459
|
+
resetEmbedProbe();
|
|
8460
|
+
return embeddingsAvailable();
|
|
8461
|
+
}
|
|
8462
|
+
async function doctor(args = []) {
|
|
8463
|
+
const fix = args.includes("--fix");
|
|
8464
|
+
const fixed = [];
|
|
8013
8465
|
const specs = detect();
|
|
8014
8466
|
const steps = [];
|
|
8015
|
-
console.log("\n" + c.bold("Holt doctor") + c.dim("
|
|
8467
|
+
console.log("\n" + c.bold("Holt doctor") + c.dim(" \xB7 a look at this machine and how best to run Holt on it"));
|
|
8016
8468
|
header("Machine");
|
|
8017
8469
|
row("platform", `${specs.platform} / ${specs.arch}`);
|
|
8018
8470
|
row("cpu", `${specs.cpuModel}${specs.cpuCount ? ` (${specs.cpuCount} cores)` : ""}`);
|
|
@@ -8049,13 +8501,29 @@ async function doctor() {
|
|
|
8049
8501
|
} catch {
|
|
8050
8502
|
embedOk = false;
|
|
8051
8503
|
}
|
|
8504
|
+
if (!embedOk && fix) {
|
|
8505
|
+
embedOk = await fixSemanticMemory(fixed);
|
|
8506
|
+
}
|
|
8052
8507
|
if (embedOk) {
|
|
8053
8508
|
good(`Ollama reachable and "${EMBED_MODEL}" is present. Memory recall is semantic, private, and local.`);
|
|
8509
|
+
if (fix && existsSync20(wsHoltDir())) {
|
|
8510
|
+
const missing = loadTurns().filter((t) => !Array.isArray(t.emb)).length;
|
|
8511
|
+
if (missing > 0) {
|
|
8512
|
+
note(`${missing} memories in this folder lack embeddings. Upgrading them...`);
|
|
8513
|
+
const r = await backfillEmbeddings((done, total) => {
|
|
8514
|
+
process.stdout.write(`\r embedding ${done}/${total}...`);
|
|
8515
|
+
});
|
|
8516
|
+
console.log("");
|
|
8517
|
+
if (r.embedded > 0) fixed.push(`Upgraded ${r.embedded} memories to semantic recall.`);
|
|
8518
|
+
}
|
|
8519
|
+
}
|
|
8054
8520
|
} else {
|
|
8055
8521
|
note(`Ollama with "${EMBED_MODEL}" not reachable at ${OLLAMA_URL3}.`);
|
|
8056
8522
|
note("Without it, Holt still remembers, but recall falls back to keyword match.");
|
|
8057
|
-
|
|
8058
|
-
|
|
8523
|
+
if (!fix) {
|
|
8524
|
+
warn('Recommendation: run "holt doctor --fix" to set this up automatically.');
|
|
8525
|
+
steps.push("Enable semantic memory: holt doctor --fix");
|
|
8526
|
+
}
|
|
8059
8527
|
}
|
|
8060
8528
|
header("Knowledge wiki maintainer");
|
|
8061
8529
|
const rec = recommendLocalModel2(specs.totalRamGB);
|
|
@@ -8081,6 +8549,12 @@ async function doctor() {
|
|
|
8081
8549
|
}
|
|
8082
8550
|
note("Hosting tip: a low-power always-on machine (an old laptop, a Pi) is ideal for the bot and scheduled runs.");
|
|
8083
8551
|
note("Heavy local models want more RAM, so keep those on your bigger machine and let the small box relay.");
|
|
8552
|
+
if (fix) {
|
|
8553
|
+
header("Repairs");
|
|
8554
|
+
if (lastConfigRepair) fixed.push(`Moved a damaged settings file aside (${lastConfigRepair}).`);
|
|
8555
|
+
if (fixed.length === 0) good("Nothing needed repair.");
|
|
8556
|
+
else for (const f of fixed) good(f);
|
|
8557
|
+
}
|
|
8084
8558
|
header("Recommended next steps");
|
|
8085
8559
|
if (!cfg) {
|
|
8086
8560
|
steps.unshift('Set up this folder: run "holt init".');
|
|
@@ -8094,8 +8568,167 @@ async function doctor() {
|
|
|
8094
8568
|
console.log("");
|
|
8095
8569
|
}
|
|
8096
8570
|
|
|
8571
|
+
// src/commands/tour.ts
|
|
8572
|
+
function step(n, title) {
|
|
8573
|
+
console.log("\n" + c.accent(`Step ${n} of 3`) + c.bold(` ${title}`));
|
|
8574
|
+
}
|
|
8575
|
+
async function tour() {
|
|
8576
|
+
const { ask, close } = createReader();
|
|
8577
|
+
try {
|
|
8578
|
+
if (!await ensureTrusted(ask)) return;
|
|
8579
|
+
const cfg = loadConfig();
|
|
8580
|
+
if (!cfg || !cfg.defaultBrain) {
|
|
8581
|
+
console.log(c.dim('\n This folder is not set up yet. Run "holt" once, then come back for the tour.\n'));
|
|
8582
|
+
return;
|
|
8583
|
+
}
|
|
8584
|
+
console.log("\n" + c.bold("Welcome. This takes two minutes and shows you the one thing that matters."));
|
|
8585
|
+
step(1, "Ask about your files");
|
|
8586
|
+
console.log(c.dim(" Holt reads the folder it runs in. Asking it what it sees here...\n"));
|
|
8587
|
+
const r = await runTask(
|
|
8588
|
+
"Look at the files in this folder and describe, in two short plain sentences, what this folder seems to be about. If it is empty or nearly empty, say that cheerfully.",
|
|
8589
|
+
{ store: false, onChunk: (t) => process.stdout.write(t) }
|
|
8590
|
+
);
|
|
8591
|
+
if (!r.ok) {
|
|
8592
|
+
console.log(c.dim(" (Your brain is not reachable right now, so we will skip this step. The next two work offline.)"));
|
|
8593
|
+
if (r.text) console.log(c.dim(` ${r.text.trim()}`));
|
|
8594
|
+
}
|
|
8595
|
+
console.log("");
|
|
8596
|
+
step(2, "Teach Holt one thing about you");
|
|
8597
|
+
console.log(c.dim(' Type one fact you want Holt to keep. Examples: "I run a bakery in Pune",'));
|
|
8598
|
+
console.log(c.dim(' "My big project this quarter is the Delhi launch", "I hate long emails".\n'));
|
|
8599
|
+
let fact = (await ask(" Your fact: ") ?? "").trim();
|
|
8600
|
+
if (!fact) fact = "I just finished the Holt tour.";
|
|
8601
|
+
const session = newSessionId();
|
|
8602
|
+
await saveFact(fact, session);
|
|
8603
|
+
console.log(c.green("\n Kept.") + c.dim(" That went into this folder\u2019s memory, on this laptop, nowhere else."));
|
|
8604
|
+
step(3, "Watch it remember");
|
|
8605
|
+
console.log(c.dim(' Asking Holt\u2019s memory: "what do you know about me?"...\n'));
|
|
8606
|
+
const hits = await recall("what do you know about me? who am I? my work", "__tour__", 4);
|
|
8607
|
+
const shown = hits.filter((h) => h.turn.content.trim().length > 0).slice(0, 3);
|
|
8608
|
+
if (shown.length === 0) {
|
|
8609
|
+
console.log(" " + c.accent("remembered") + ` ${fact}`);
|
|
8610
|
+
} else {
|
|
8611
|
+
for (const h of shown) {
|
|
8612
|
+
console.log(" " + c.accent("remembered") + ` ${h.turn.content.slice(0, 110).replace(/\s+/g, " ")}`);
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
8615
|
+
const s = memStats();
|
|
8616
|
+
console.log(
|
|
8617
|
+
"\n" + c.bold(" Here is the point: ") + `close this terminal. Come back tomorrow, or next month, and ask again.
|
|
8618
|
+
Still there. This folder now holds ${s.turns} memor${s.turns === 1 ? "y" : "ies"}, and every chat adds more.
|
|
8619
|
+
`
|
|
8620
|
+
);
|
|
8621
|
+
console.log(c.dim(' Next: run "holt" to start a real conversation, or "holt help" to see everything it does.\n'));
|
|
8622
|
+
} finally {
|
|
8623
|
+
close();
|
|
8624
|
+
}
|
|
8625
|
+
}
|
|
8626
|
+
|
|
8627
|
+
// src/commands/reset.ts
|
|
8628
|
+
import { existsSync as existsSync21, rmSync as rmSync9 } from "fs";
|
|
8629
|
+
async function reset() {
|
|
8630
|
+
const { ask, close } = createReader();
|
|
8631
|
+
try {
|
|
8632
|
+
const dir = wsHoltDir();
|
|
8633
|
+
if (!existsSync21(dir)) {
|
|
8634
|
+
console.log(c.dim("\n Nothing to reset: Holt has no settings or memory in this folder.\n"));
|
|
8635
|
+
return;
|
|
8636
|
+
}
|
|
8637
|
+
let turns = 0;
|
|
8638
|
+
try {
|
|
8639
|
+
turns = memStats().turns;
|
|
8640
|
+
} catch {
|
|
8641
|
+
}
|
|
8642
|
+
console.log("\n" + c.bold("Reset Holt in this folder"));
|
|
8643
|
+
console.log(c.dim(` Folder: ${workspace()}`));
|
|
8644
|
+
console.log(c.dim(` This deletes Holt's settings and all ${turns} remembered moment${turns === 1 ? "" : "s"} here.`));
|
|
8645
|
+
console.log(c.dim(" Your own files are not touched. Other folders are not touched."));
|
|
8646
|
+
const a = (await ask(`
|
|
8647
|
+
Type ${c.bold("reset")} to confirm, or press enter to cancel: `) ?? "").trim().toLowerCase();
|
|
8648
|
+
if (a !== "reset") {
|
|
8649
|
+
console.log(c.dim(" Cancelled. Nothing was changed.\n"));
|
|
8650
|
+
return;
|
|
8651
|
+
}
|
|
8652
|
+
rmSync9(dir, { recursive: true, force: true });
|
|
8653
|
+
console.log(c.green("\n Done. This folder is a clean slate.") + c.dim(' Run "holt" to set it up again.\n'));
|
|
8654
|
+
} finally {
|
|
8655
|
+
close();
|
|
8656
|
+
}
|
|
8657
|
+
}
|
|
8658
|
+
|
|
8659
|
+
// src/commands/update.ts
|
|
8660
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
8661
|
+
import { realpathSync as realpathSync4 } from "fs";
|
|
8662
|
+
async function latestVersion() {
|
|
8663
|
+
try {
|
|
8664
|
+
const res = await fetch("https://registry.npmjs.org/@holt-os/holt/latest", {
|
|
8665
|
+
signal: AbortSignal.timeout(4e3)
|
|
8666
|
+
});
|
|
8667
|
+
if (!res.ok) return null;
|
|
8668
|
+
const data = await res.json();
|
|
8669
|
+
return typeof data.version === "string" ? data.version : null;
|
|
8670
|
+
} catch {
|
|
8671
|
+
return null;
|
|
8672
|
+
}
|
|
8673
|
+
}
|
|
8674
|
+
function isBrewInstall() {
|
|
8675
|
+
try {
|
|
8676
|
+
const p = realpathSync4(process.argv[1] ?? "");
|
|
8677
|
+
return p.includes("/Cellar/") || p.includes("/homebrew/") || p.includes("/linuxbrew/");
|
|
8678
|
+
} catch {
|
|
8679
|
+
return false;
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
async function update() {
|
|
8683
|
+
const { ask, close } = createReader();
|
|
8684
|
+
try {
|
|
8685
|
+
console.log(c.dim(`
|
|
8686
|
+
You have holt ${VERSION}. Checking for a newer one...`));
|
|
8687
|
+
const latest = await latestVersion();
|
|
8688
|
+
if (!latest) {
|
|
8689
|
+
console.log(c.dim(" Could not reach the registry. Check your internet connection and try again.\n"));
|
|
8690
|
+
return;
|
|
8691
|
+
}
|
|
8692
|
+
if (latest === VERSION) {
|
|
8693
|
+
console.log(c.green(` You are on the latest version (${VERSION}).`) + "\n");
|
|
8694
|
+
return;
|
|
8695
|
+
}
|
|
8696
|
+
console.log(` Newer version available: ${c.bold(latest)}`);
|
|
8697
|
+
if (isBrewInstall()) {
|
|
8698
|
+
const a2 = (await ask(" Update now via Homebrew? [Y/n] ") ?? "").trim().toLowerCase();
|
|
8699
|
+
if (a2 === "n" || a2 === "no") {
|
|
8700
|
+
console.log(c.dim(" Later, then. The command is: brew upgrade holt\n"));
|
|
8701
|
+
return;
|
|
8702
|
+
}
|
|
8703
|
+
const r2 = spawnSync8("brew", ["upgrade", "holt"], { stdio: "inherit" });
|
|
8704
|
+
if (r2.status === 0) console.log(c.green(`
|
|
8705
|
+
Updated to ${latest}.`) + "\n");
|
|
8706
|
+
else console.log(c.dim("\n Homebrew reported a problem above. You can retry with: brew upgrade holt\n"));
|
|
8707
|
+
return;
|
|
8708
|
+
}
|
|
8709
|
+
const a = (await ask(" Update now? [Y/n] ") ?? "").trim().toLowerCase();
|
|
8710
|
+
if (a === "n" || a === "no") {
|
|
8711
|
+
console.log(c.dim(" Later, then. The command is: npm install -g @holt-os/holt\n"));
|
|
8712
|
+
return;
|
|
8713
|
+
}
|
|
8714
|
+
const r = spawnSync8("npm", ["install", "-g", "@holt-os/holt"], { stdio: "inherit" });
|
|
8715
|
+
if (r.status === 0) {
|
|
8716
|
+
console.log(c.green(`
|
|
8717
|
+
Updated to ${latest}.`) + "\n");
|
|
8718
|
+
} else {
|
|
8719
|
+
console.log(
|
|
8720
|
+
c.dim(
|
|
8721
|
+
"\n The install hit a permission problem. Two easy ways out:\n 1. Run it once with sudo: sudo npm install -g @holt-os/holt\n 2. Or install Node with nvm (https://nvm.sh) so no sudo is ever needed.\n"
|
|
8722
|
+
)
|
|
8723
|
+
);
|
|
8724
|
+
}
|
|
8725
|
+
} finally {
|
|
8726
|
+
close();
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
|
|
8097
8730
|
// src/commands/write.ts
|
|
8098
|
-
import { writeFileSync as
|
|
8731
|
+
import { writeFileSync as writeFileSync20 } from "fs";
|
|
8099
8732
|
import { resolve as resolve8 } from "path";
|
|
8100
8733
|
|
|
8101
8734
|
// src/antiai.ts
|
|
@@ -8276,7 +8909,7 @@ async function write(args) {
|
|
|
8276
8909
|
if (p.out) {
|
|
8277
8910
|
const target = resolve8(process.cwd(), p.out);
|
|
8278
8911
|
try {
|
|
8279
|
-
|
|
8912
|
+
writeFileSync20(target, draft + "\n", "utf8");
|
|
8280
8913
|
process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|
|
8281
8914
|
} catch (e) {
|
|
8282
8915
|
process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
|
|
@@ -8285,6 +8918,37 @@ async function write(args) {
|
|
|
8285
8918
|
}
|
|
8286
8919
|
}
|
|
8287
8920
|
|
|
8921
|
+
// src/crash.ts
|
|
8922
|
+
import { mkdirSync as mkdirSync18, appendFileSync as appendFileSync3 } from "fs";
|
|
8923
|
+
import { join as join20 } from "path";
|
|
8924
|
+
function handleCrash(err2) {
|
|
8925
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
8926
|
+
const detail = err2 instanceof Error ? err2.stack || err2.message : String(err2);
|
|
8927
|
+
let logPath2 = null;
|
|
8928
|
+
const entry = `[${stamp}] holt ${VERSION} argv: ${process.argv.slice(2).join(" ")}
|
|
8929
|
+
${detail}
|
|
8930
|
+
|
|
8931
|
+
`;
|
|
8932
|
+
for (const base of [wsHoltDir(), GLOBAL_DIR]) {
|
|
8933
|
+
try {
|
|
8934
|
+
const dir = join20(base, "logs");
|
|
8935
|
+
mkdirSync18(dir, { recursive: true });
|
|
8936
|
+
const p = join20(dir, `crash-${stamp.slice(0, 10)}.log`);
|
|
8937
|
+
appendFileSync3(p, entry, "utf8");
|
|
8938
|
+
logPath2 = p;
|
|
8939
|
+
break;
|
|
8940
|
+
} catch {
|
|
8941
|
+
}
|
|
8942
|
+
}
|
|
8943
|
+
if (!isQuietMode()) {
|
|
8944
|
+
console.error(c.red("\n Something went wrong, and it is not your fault."));
|
|
8945
|
+
if (logPath2) console.error(c.dim(` Details saved to: ${logPath2}`));
|
|
8946
|
+
console.error(c.dim(" Try: holt doctor --fix (it repairs the common causes)"));
|
|
8947
|
+
console.error(c.dim(" Still stuck? https://github.com/holt-os/holt/issues\n"));
|
|
8948
|
+
}
|
|
8949
|
+
process.exit(1);
|
|
8950
|
+
}
|
|
8951
|
+
|
|
8288
8952
|
// src/cli.ts
|
|
8289
8953
|
var BANNER = `
|
|
8290
8954
|
\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
@@ -8298,40 +8962,53 @@ var BANNER = `
|
|
|
8298
8962
|
var HELP = `${BANNER}
|
|
8299
8963
|
Usage: holt <command>
|
|
8300
8964
|
|
|
8301
|
-
|
|
8302
|
-
(no command) Start your assistant: sets up if needed, then launches
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
telegram Chat with Holt from your phone: holt telegram [setup]
|
|
8311
|
-
notify [msg] Push a message to your phone over Telegram (stdin-friendly)
|
|
8312
|
-
voice Teach Holt how you write: holt voice [add <file> | show | edit | clear]
|
|
8965
|
+
Start here:
|
|
8966
|
+
(no command) Start your assistant: sets up if needed, then launches your
|
|
8967
|
+
brain (Claude Code, Codex, Gemini), branded as Holt
|
|
8968
|
+
tour A 2-minute guided first run: ask, teach it one fact, watch it remember
|
|
8969
|
+
doctor Health check for this machine
|
|
8970
|
+
("holt doctor --fix" also repairs the common problems for you)
|
|
8971
|
+
|
|
8972
|
+
Every day:
|
|
8973
|
+
run <task> Do one task and remember it: holt run "draft my status update"
|
|
8313
8974
|
write <what> Draft content in your voice, with anti-AI checks: holt write "..." [--type]
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8975
|
+
voice Teach Holt how you write: holt voice [add <file> | show | edit | clear]
|
|
8976
|
+
memory What Holt knows here: holt memory [search <q> | facts | embed | clear]
|
|
8977
|
+
wiki A tidy, linked notebook Holt keeps from your memory: holt wiki [sync | show]
|
|
8978
|
+
graph See everything Holt knows as a visual map in your browser
|
|
8979
|
+
skill Reusable how-tos you can share: holt skill [list | show | create | add]
|
|
8980
|
+
|
|
8981
|
+
From your phone, while you sleep:
|
|
8982
|
+
telegram Chat with Holt from your phone: holt telegram setup
|
|
8983
|
+
notify [msg] Send yourself a phone message from any script (stdin-friendly)
|
|
8984
|
+
schedule Run a task on a timer: holt schedule [add | list | remove]
|
|
8985
|
+
routine Named, reusable, scheduled jobs: holt routine [add | run | list | remove]
|
|
8986
|
+
|
|
8987
|
+
Housekeeping:
|
|
8988
|
+
setting Change brains, API connections, and your launch word (per folder)
|
|
8322
8989
|
login <brain> Sign in to a brain: claude, codex, or gemini
|
|
8323
|
-
|
|
8324
|
-
|
|
8990
|
+
update Get the latest Holt, in one word
|
|
8991
|
+
reset Start this folder over (asks first; your own files are untouched)
|
|
8992
|
+
hook Ambient memory inside plain Claude Code: holt hook [install | status]
|
|
8993
|
+
mcp Share Holt's memory with other AI apps (Claude Code, Cursor, Codex)
|
|
8994
|
+
chat Lightweight built-in chat, used for API brains
|
|
8995
|
+
init Full setup by hand (bare "holt" runs this for you when needed)
|
|
8996
|
+
launch Same as bare "holt": start your assistant
|
|
8997
|
+
version | help Print the version / show this help
|
|
8325
8998
|
|
|
8326
8999
|
Holt runs in the folder you launch it from and asks to trust it first.
|
|
8327
|
-
|
|
8328
|
-
API
|
|
9000
|
+
A "brain" is the AI doing the thinking: an agent CLI on your machine
|
|
9001
|
+
(claude, codex, gemini) or a direct API connection you add in settings.
|
|
8329
9002
|
|
|
8330
9003
|
Docs: https://productsdecoded.com/holt
|
|
8331
9004
|
Repo: https://github.com/holt-os/holt
|
|
8332
9005
|
`;
|
|
8333
9006
|
async function main() {
|
|
8334
9007
|
const cmd = process.argv[2];
|
|
9008
|
+
const sub = process.argv[3];
|
|
9009
|
+
if (cmd === "statusline" || cmd === "mcp" || cmd === "notify" || cmd === "hook" && (sub === "inject" || sub === "capture")) {
|
|
9010
|
+
setQuietMode(true);
|
|
9011
|
+
}
|
|
8335
9012
|
switch (cmd) {
|
|
8336
9013
|
case void 0:
|
|
8337
9014
|
await launch();
|
|
@@ -8400,7 +9077,16 @@ async function main() {
|
|
|
8400
9077
|
await notify(process.argv.slice(3));
|
|
8401
9078
|
break;
|
|
8402
9079
|
case "doctor":
|
|
8403
|
-
await doctor();
|
|
9080
|
+
await doctor(process.argv.slice(3));
|
|
9081
|
+
break;
|
|
9082
|
+
case "tour":
|
|
9083
|
+
await tour();
|
|
9084
|
+
break;
|
|
9085
|
+
case "reset":
|
|
9086
|
+
await reset();
|
|
9087
|
+
break;
|
|
9088
|
+
case "update":
|
|
9089
|
+
await update();
|
|
8404
9090
|
break;
|
|
8405
9091
|
case "voice":
|
|
8406
9092
|
await voice(process.argv[3], process.argv.slice(4));
|
|
@@ -8416,4 +9102,6 @@ async function main() {
|
|
|
8416
9102
|
process.exitCode = 1;
|
|
8417
9103
|
}
|
|
8418
9104
|
}
|
|
8419
|
-
|
|
9105
|
+
process.on("uncaughtException", handleCrash);
|
|
9106
|
+
process.on("unhandledRejection", handleCrash);
|
|
9107
|
+
main().catch(handleCrash);
|