@holt-os/holt 0.14.3 → 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 +6 -1
- package/dist/cli.js +401 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,7 +195,7 @@ A brain is an agent CLI installed and logged in on your machine. No API keys to
|
|
|
195
195
|
|
|
196
196
|
### Direct API brains
|
|
197
197
|
|
|
198
|
-
A brain can also be a direct provider connection: no CLI install, your own key. Run `holt setting` and pick `[c] connect API brain`, or say yes to the API-brain question in `holt init`. Choose a provider (anthropic, openai, gemini), a model (suggestions offered, type anything), and a short name. For the key, paste a raw key (stored in `~/.holt/credentials.json`, mode 600) or give the name of an env var that holds it. Resolution order: the brain's `keyEnv`, the credentials file, then the standard env var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`). API brains stream and share memory exactly like CLI brains; switch with `/brain <name>`.
|
|
198
|
+
A brain can also be a direct provider connection: no CLI install, your own key. Run `holt setting` and pick `[c] connect API brain`, or say yes to the API-brain question in `holt init`. Choose a provider (anthropic, openai, gemini, kimi, grok), a model (suggestions offered, type anything), and a short name. Kimi (Moonshot) and Grok (xAI) speak the OpenAI wire format at their own endpoints; any other OpenAI-compatible endpoint (a gateway, a self-hosted server) works too via an optional `baseUrl` on the brain in `.holt/config.json`. For the key, paste a raw key (stored in `~/.holt/credentials.json`, mode 600) or give the name of an env var that holds it. Resolution order: the brain's `keyEnv`, the credentials file, then the standard env var (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, `MOONSHOT_API_KEY`, `XAI_API_KEY`). API brains stream and share memory exactly like CLI brains; switch with `/brain <name>`.
|
|
199
199
|
|
|
200
200
|
## See your memory: `holt graph`
|
|
201
201
|
|
|
@@ -556,6 +556,11 @@ holt routine named, reusable, scheduled jobs: add | run | list | show |
|
|
|
556
556
|
holt telegram chat with Holt from your phone: telegram [setup]
|
|
557
557
|
holt notify [msg] push a message to your phone over Telegram (stdin-friendly)
|
|
558
558
|
holt doctor check this machine and recommend how best to run Holt here
|
|
559
|
+
("holt doctor --fix" also repairs the common problems it finds:
|
|
560
|
+
starts Ollama, pulls the embed model, mends a damaged config)
|
|
561
|
+
holt tour a 2-minute guided first run: ask, teach one fact, watch it remember
|
|
562
|
+
holt update get the latest Holt in one word (npm or Homebrew, auto-detected)
|
|
563
|
+
holt reset start this folder over (typed confirmation; your files untouched)
|
|
559
564
|
holt voice teach Holt your writing voice: add <file> | show | edit | clear
|
|
560
565
|
holt write <what> draft content in your voice with anti-AI checks (--type, --out, --fast)
|
|
561
566
|
holt graph see your memory as an interactive knowledge graph
|
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;
|
|
234
|
+
try {
|
|
235
|
+
raw = readFileSync2(path, "utf8");
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
207
239
|
try {
|
|
208
|
-
const cfg = JSON.parse(
|
|
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}`
|
|
@@ -4210,6 +4261,9 @@ async function chat() {
|
|
|
4210
4261
|
if (embedOk && stats.withEmbeddings < stats.turns) {
|
|
4211
4262
|
console.log(c.dim(` ${stats.turns - stats.withEmbeddings} older moments lack embeddings. Run "holt memory embed" to upgrade them.`));
|
|
4212
4263
|
}
|
|
4264
|
+
if (!embedOk) {
|
|
4265
|
+
console.log(c.dim(' Recall works, but semantic memory is off. "holt doctor --fix" turns it on.'));
|
|
4266
|
+
}
|
|
4213
4267
|
console.log(c.dim("Type a message. Commands: /brain /memory /output /save /allow /setting /clear /help /exit"));
|
|
4214
4268
|
showStatus();
|
|
4215
4269
|
while (true) {
|
|
@@ -4688,6 +4742,9 @@ async function memoryCmd(sub, rest = []) {
|
|
|
4688
4742
|
}
|
|
4689
4743
|
const hits = await recall(q, "__none__", 8);
|
|
4690
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
|
+
}
|
|
4691
4748
|
if (hits.length === 0) console.log(c.dim(" Nothing relevant found."));
|
|
4692
4749
|
else for (const h of hits) {
|
|
4693
4750
|
const when = new Date(h.turn.ts).toISOString().slice(0, 10);
|
|
@@ -7327,7 +7384,7 @@ function holtTools() {
|
|
|
7327
7384
|
}
|
|
7328
7385
|
|
|
7329
7386
|
// src/version.ts
|
|
7330
|
-
var VERSION = "0.
|
|
7387
|
+
var VERSION = "0.15.0";
|
|
7331
7388
|
|
|
7332
7389
|
// src/commands/mcp.ts
|
|
7333
7390
|
function printSetup() {
|
|
@@ -7471,7 +7528,7 @@ function parseArgs2(args) {
|
|
|
7471
7528
|
let out;
|
|
7472
7529
|
let noStore = false;
|
|
7473
7530
|
let noRecall = false;
|
|
7474
|
-
let
|
|
7531
|
+
let quiet2 = false;
|
|
7475
7532
|
for (let i = 0; i < args.length; i++) {
|
|
7476
7533
|
const a = args[i];
|
|
7477
7534
|
if (a === "--brain") {
|
|
@@ -7491,12 +7548,12 @@ function parseArgs2(args) {
|
|
|
7491
7548
|
continue;
|
|
7492
7549
|
}
|
|
7493
7550
|
if (a === "--quiet") {
|
|
7494
|
-
|
|
7551
|
+
quiet2 = true;
|
|
7495
7552
|
continue;
|
|
7496
7553
|
}
|
|
7497
7554
|
words2.push(a);
|
|
7498
7555
|
}
|
|
7499
|
-
return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet };
|
|
7556
|
+
return { task: words2.join(" ").trim(), brainId, out, noStore, noRecall, quiet: quiet2 };
|
|
7500
7557
|
}
|
|
7501
7558
|
async function run(args) {
|
|
7502
7559
|
const p = parseArgs2(args);
|
|
@@ -7986,7 +8043,7 @@ function buildRoutinePrompt(r) {
|
|
|
7986
8043
|
return { prompt };
|
|
7987
8044
|
}
|
|
7988
8045
|
async function runCmd(rest) {
|
|
7989
|
-
let
|
|
8046
|
+
let quiet2 = false;
|
|
7990
8047
|
let outOverride;
|
|
7991
8048
|
let notifyOverride = false;
|
|
7992
8049
|
let brainOverride;
|
|
@@ -7994,7 +8051,7 @@ async function runCmd(rest) {
|
|
|
7994
8051
|
for (let i = 0; i < rest.length; i++) {
|
|
7995
8052
|
const a = rest[i];
|
|
7996
8053
|
if (a === void 0) continue;
|
|
7997
|
-
if (a === "--quiet")
|
|
8054
|
+
if (a === "--quiet") quiet2 = true;
|
|
7998
8055
|
else if (a === "--notify") notifyOverride = true;
|
|
7999
8056
|
else if (a === "--out") outOverride = rest[++i];
|
|
8000
8057
|
else if (a === "--brain") brainOverride = rest[++i];
|
|
@@ -8035,7 +8092,7 @@ async function runCmd(rest) {
|
|
|
8035
8092
|
let streamed = false;
|
|
8036
8093
|
const result = await runTask(built.prompt, {
|
|
8037
8094
|
...brainId ? { brainId } : {},
|
|
8038
|
-
onChunk:
|
|
8095
|
+
onChunk: quiet2 ? void 0 : (ch) => {
|
|
8039
8096
|
streamed = true;
|
|
8040
8097
|
process.stdout.write(ch);
|
|
8041
8098
|
}
|
|
@@ -8046,13 +8103,13 @@ async function runCmd(rest) {
|
|
|
8046
8103
|
process.exitCode = 1;
|
|
8047
8104
|
return;
|
|
8048
8105
|
}
|
|
8049
|
-
if (!
|
|
8050
|
-
if (!
|
|
8106
|
+
if (!quiet2 && !streamed) process.stdout.write(result.text);
|
|
8107
|
+
if (!quiet2 && (streamed || result.text) && !result.text.endsWith("\n")) process.stdout.write("\n");
|
|
8051
8108
|
if (outTarget) {
|
|
8052
8109
|
const target = resolve7(routine2.workspace, outTarget);
|
|
8053
8110
|
try {
|
|
8054
8111
|
writeFileSync19(target, result.text, "utf8");
|
|
8055
|
-
if (!
|
|
8112
|
+
if (!quiet2) process.stderr.write(c.dim(`saved to ${target}`) + "\n");
|
|
8056
8113
|
} catch (e) {
|
|
8057
8114
|
process.stderr.write(c.red(`Could not write ${target}: ${e.message}`) + "\n");
|
|
8058
8115
|
process.exitCode = 1;
|
|
@@ -8228,6 +8285,10 @@ function readStdin3() {
|
|
|
8228
8285
|
}
|
|
8229
8286
|
}
|
|
8230
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
|
+
|
|
8231
8292
|
// src/specs.ts
|
|
8232
8293
|
import os from "os";
|
|
8233
8294
|
var BYTES_PER_GB = 1024 ** 3;
|
|
@@ -8347,10 +8408,63 @@ function good(text) {
|
|
|
8347
8408
|
function warn(text) {
|
|
8348
8409
|
console.log(" " + c.red(text));
|
|
8349
8410
|
}
|
|
8350
|
-
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 = [];
|
|
8351
8465
|
const specs = detect();
|
|
8352
8466
|
const steps = [];
|
|
8353
|
-
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"));
|
|
8354
8468
|
header("Machine");
|
|
8355
8469
|
row("platform", `${specs.platform} / ${specs.arch}`);
|
|
8356
8470
|
row("cpu", `${specs.cpuModel}${specs.cpuCount ? ` (${specs.cpuCount} cores)` : ""}`);
|
|
@@ -8387,13 +8501,29 @@ async function doctor() {
|
|
|
8387
8501
|
} catch {
|
|
8388
8502
|
embedOk = false;
|
|
8389
8503
|
}
|
|
8504
|
+
if (!embedOk && fix) {
|
|
8505
|
+
embedOk = await fixSemanticMemory(fixed);
|
|
8506
|
+
}
|
|
8390
8507
|
if (embedOk) {
|
|
8391
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
|
+
}
|
|
8392
8520
|
} else {
|
|
8393
8521
|
note(`Ollama with "${EMBED_MODEL}" not reachable at ${OLLAMA_URL3}.`);
|
|
8394
8522
|
note("Without it, Holt still remembers, but recall falls back to keyword match.");
|
|
8395
|
-
|
|
8396
|
-
|
|
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
|
+
}
|
|
8397
8527
|
}
|
|
8398
8528
|
header("Knowledge wiki maintainer");
|
|
8399
8529
|
const rec = recommendLocalModel2(specs.totalRamGB);
|
|
@@ -8419,6 +8549,12 @@ async function doctor() {
|
|
|
8419
8549
|
}
|
|
8420
8550
|
note("Hosting tip: a low-power always-on machine (an old laptop, a Pi) is ideal for the bot and scheduled runs.");
|
|
8421
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
|
+
}
|
|
8422
8558
|
header("Recommended next steps");
|
|
8423
8559
|
if (!cfg) {
|
|
8424
8560
|
steps.unshift('Set up this folder: run "holt init".');
|
|
@@ -8432,6 +8568,165 @@ async function doctor() {
|
|
|
8432
8568
|
console.log("");
|
|
8433
8569
|
}
|
|
8434
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
|
+
|
|
8435
8730
|
// src/commands/write.ts
|
|
8436
8731
|
import { writeFileSync as writeFileSync20 } from "fs";
|
|
8437
8732
|
import { resolve as resolve8 } from "path";
|
|
@@ -8623,6 +8918,37 @@ async function write(args) {
|
|
|
8623
8918
|
}
|
|
8624
8919
|
}
|
|
8625
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
|
+
|
|
8626
8952
|
// src/cli.ts
|
|
8627
8953
|
var BANNER = `
|
|
8628
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
|
|
@@ -8636,40 +8962,53 @@ var BANNER = `
|
|
|
8636
8962
|
var HELP = `${BANNER}
|
|
8637
8963
|
Usage: holt <command>
|
|
8638
8964
|
|
|
8639
|
-
|
|
8640
|
-
(no command) Start your assistant: sets up if needed, then launches
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
telegram Chat with Holt from your phone: holt telegram [setup]
|
|
8649
|
-
notify [msg] Push a message to your phone over Telegram (stdin-friendly)
|
|
8650
|
-
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"
|
|
8651
8974
|
write <what> Draft content in your voice, with anti-AI checks: holt write "..." [--type]
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
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)
|
|
8660
8989
|
login <brain> Sign in to a brain: claude, codex, or gemini
|
|
8661
|
-
|
|
8662
|
-
|
|
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
|
|
8663
8998
|
|
|
8664
8999
|
Holt runs in the folder you launch it from and asks to trust it first.
|
|
8665
|
-
|
|
8666
|
-
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.
|
|
8667
9002
|
|
|
8668
9003
|
Docs: https://productsdecoded.com/holt
|
|
8669
9004
|
Repo: https://github.com/holt-os/holt
|
|
8670
9005
|
`;
|
|
8671
9006
|
async function main() {
|
|
8672
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
|
+
}
|
|
8673
9012
|
switch (cmd) {
|
|
8674
9013
|
case void 0:
|
|
8675
9014
|
await launch();
|
|
@@ -8738,7 +9077,16 @@ async function main() {
|
|
|
8738
9077
|
await notify(process.argv.slice(3));
|
|
8739
9078
|
break;
|
|
8740
9079
|
case "doctor":
|
|
8741
|
-
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();
|
|
8742
9090
|
break;
|
|
8743
9091
|
case "voice":
|
|
8744
9092
|
await voice(process.argv[3], process.argv.slice(4));
|
|
@@ -8754,4 +9102,6 @@ async function main() {
|
|
|
8754
9102
|
process.exitCode = 1;
|
|
8755
9103
|
}
|
|
8756
9104
|
}
|
|
8757
|
-
|
|
9105
|
+
process.on("uncaughtException", handleCrash);
|
|
9106
|
+
process.on("unhandledRejection", handleCrash);
|
|
9107
|
+
main().catch(handleCrash);
|