@holt-os/holt 0.15.1 → 0.16.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 +11 -10
- package/dist/cli.js +158 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ cd ~/where-you-want-to-work
|
|
|
50
50
|
holt # start your assistant (or use your custom command, e.g. `ai`)
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
That is it. Bare `holt` sets the folder up the first time (trust, brain, memory hooks) and then launches the **real interactive brain** (Claude Code, Codex, or Gemini), branded as Holt. Run `holt help` for the full command list
|
|
53
|
+
That is it. Bare `holt` sets the folder up the first time (trust, brain, memory hooks) and then launches the **real interactive brain** (Claude Code, Codex, or Gemini), branded as Holt. Run `holt help` for the full command list.
|
|
54
54
|
|
|
55
55
|
Prefer to step through setup on its own first? `holt init` still does that, then `holt` starts the session.
|
|
56
56
|
|
|
@@ -59,10 +59,12 @@ Prefer to step through setup on its own first? `holt init` still does that, then
|
|
|
59
59
|
`holt` (with no command) is the front door: **your assistant, with memory of you.**
|
|
60
60
|
|
|
61
61
|
1. **Sets up if needed.** In a fresh folder it runs onboarding (trust, pick a brain, memory). In an already-configured folder it just makes sure the folder is trusted, the memory directory exists, and the [ambient memory hooks](#ambient-memory-for-claude-code-holt-hook) are installed.
|
|
62
|
-
|
|
62
|
+
> **Gemini note.** Personal Google accounts can no longer sign in to Gemini CLI (Google retired that login on 18 June 2026 in favour of Antigravity). Gemini still works in Holt with an API key from <https://aistudio.google.com/apikey>; `holt login gemini` walks you through connecting it.
|
|
63
|
+
|
|
64
|
+
2. **Launches the real interactive brain.** It hands the terminal to the brain's own interactive session (`claude`, `codex`, or `gemini` with no `-p` and no prompt), so you keep the brain's full power: agentic edits, tool use, the permission UI, MCP. That is different from Holt's own built-in REPL, which shells out to the brain once per turn and is used for API brains that have no TUI of their own.
|
|
63
65
|
3. **Brands the session as Holt.** See [Branding](#branding-holt-forward) below.
|
|
64
66
|
|
|
65
|
-
If your default brain is a **direct API brain** (no interactive TUI), bare `holt`
|
|
67
|
+
If your default brain is a **direct API brain** (no interactive TUI of its own), bare `holt` opens Holt's own built-in REPL instead, branded the same way. Same command, same intro, no second thing to learn.
|
|
66
68
|
|
|
67
69
|
### Branding (Holt-forward)
|
|
68
70
|
|
|
@@ -96,7 +98,7 @@ During `holt init` you:
|
|
|
96
98
|
|
|
97
99
|
## Using it
|
|
98
100
|
|
|
99
|
-
Inside
|
|
101
|
+
Inside a Holt session:
|
|
100
102
|
|
|
101
103
|
```
|
|
102
104
|
/brain list your brains and see which is active
|
|
@@ -127,7 +129,7 @@ Holt runs the brain in the folder you launched it from, so by default the brain
|
|
|
127
129
|
- `/allow <path>` grants a folder up front with no prompt.
|
|
128
130
|
- `/allowed` lists what you have granted.
|
|
129
131
|
|
|
130
|
-
Grants are **in-memory and session-scoped**: nothing is written to disk, and they reset the next time you run `holt
|
|
132
|
+
Grants are **in-memory and session-scoped**: nothing is written to disk, and they reset the next time you run `holt`. Access is **read-oriented** and only works with a **Claude Code** brain: Holt passes `--add-dir=<dir>` for each granted folder plus `--allowedTools=Read,Glob,Grep` so Claude Code can read (but not write) those files without an interactive permission prompt. With a Codex/Gemini or API brain, external file access is unavailable and Holt says so instead of adding flags. (If you need to tune the flags, they live in one place: `claudeAccessArgs` in `src/access.ts`.)
|
|
131
133
|
|
|
132
134
|
***
|
|
133
135
|
|
|
@@ -248,7 +250,7 @@ holt wiki setup # recommend a local model for this machine's RAM
|
|
|
248
250
|
|
|
249
251
|
**How sync works.** `holt wiki sync` gathers facts added since the last sync (a marker in `.holt/wiki/.state.json`), embeds each one locally, and routes it to the nearest existing page by cosine similarity. Related facts land on the same page; a fact that matches nothing new starts a fresh page. Facts are grouped by target page and the maintainer is called **once per changed page** (not once per fact), so it only rewrites the few pages that actually changed. Routing is always local and free, which is what keeps the maintainer's cost minimal. `holt wiki rebuild` folds every fact from scratch.
|
|
250
252
|
|
|
251
|
-
**Self-maintaining (auto-sync).** Turn on `holt wiki auto on` (persists `wiki.autoSync: true` for the folder) and the wiki keeps itself current with no manual command. It syncs **after fact distillation** at the end of a
|
|
253
|
+
**Self-maintaining (auto-sync).** Turn on `holt wiki auto on` (persists `wiki.autoSync: true` for the folder) and the wiki keeps itself current with no manual command. It syncs **after fact distillation** at the end of a Holt session, and ambiently when the Claude Code **Stop** hook fires (`holt hook capture`), so newly captured facts fold straight into pages. Auto-sync is silent, best-effort, and never blocks exit: it reuses the exact same sync engine as `holt wiki sync`, so there is no behavior drift. Left off (the default), nothing changes and you sync by hand. `holt wiki auto` with no argument shows the current state; `holt wiki status` surfaces it too.
|
|
252
254
|
|
|
253
255
|
**Lint that can fix.** `holt wiki lint` audits pages for contradictions, duplicates, and gaps and prints a report only (files untouched). Add `--fix` and Holt asks the maintainer to return corrected page bodies and **applies them**, rewriting the affected pages (provenance/`sources` is preserved). Because pages are derived and regenerable, applying is safe: a bad `--fix` is fully recoverable with `holt wiki rebuild`, and the command prints a git/backup nudge before writing. Only pages that actually change are rewritten.
|
|
254
256
|
|
|
@@ -378,7 +380,7 @@ Point the client at the folder whose memory you want it to use; Holt serves what
|
|
|
378
380
|
|
|
379
381
|
## Ambient memory for Claude Code (`holt hook`)
|
|
380
382
|
|
|
381
|
-
The MCP server above lets Claude Code recall and remember **when it decides to**. `holt hook` goes further: it makes Holt's per-folder memory work **ambiently**, with no
|
|
383
|
+
The MCP server above lets Claude Code recall and remember **when it decides to**. `holt hook` goes further: it makes Holt's per-folder memory work **ambiently**, with no Holt session open and no manual tool call. Two directions, both wired as [Claude Code hooks](https://docs.claude.com/en/docs/claude-code/hooks):
|
|
382
384
|
|
|
383
385
|
- **Inject** (`UserPromptSubmit`): before each prompt, Holt quietly recalls the most relevant remembered notes for the current folder and adds them to the model's context.
|
|
384
386
|
- **Capture** (`Stop`): when a session ends, Holt distills durable facts from the transcript (via this folder's configured brain) and saves them to the folder's memory.
|
|
@@ -394,7 +396,7 @@ holt hook remove # remove ONLY Holt's hooks, leaving everything
|
|
|
394
396
|
|
|
395
397
|
Install **merges** into any existing `hooks` config, never clobbers unrelated settings or other hooks, is **idempotent** (re-running does not duplicate), and backs the file up to `settings.json.holt-bak` before writing. `remove` deletes only the entries whose command is `holt hook inject` / `holt hook capture`.
|
|
396
398
|
|
|
397
|
-
**Trusted-folder guard (important).** The two runtime hooks (`holt hook inject` and `holt hook capture`, invoked by Claude Code, not you) no-op **silently** unless the folder is a trusted Holt workspace with an existing `.holt/memory`. So Holt never injects your private notes into an unrelated project, and never creates memory in a folder you never set up. To make a folder ambient, run `holt init`
|
|
399
|
+
**Trusted-folder guard (important).** The two runtime hooks (`holt hook inject` and `holt hook capture`, invoked by Claude Code, not you) no-op **silently** unless the folder is a trusted Holt workspace with an existing `.holt/memory`. So Holt never injects your private notes into an unrelated project, and never creates memory in a folder you never set up. To make a folder ambient, run `holt init` there once so it becomes trusted and gets its `.holt/memory`. The inject hook keeps stdout clean (only the context block is printed, since Claude Code injects it verbatim); all diagnostics go to stderr, and both hooks always exit 0 so a memory step never blocks or slows your prompt.
|
|
398
400
|
|
|
399
401
|
**Seeing what the hooks did (`~/.holt/hooks.log`).** Since the hooks stay silent, every `holt hook capture` run appends **one** line to `~/.holt/hooks.log` with the outcome or the exact skip reason (untrusted folder, no config, `extractFacts` off, no usable brain, missing or empty transcript, too few exchanges, or `saved N facts`), preceded by a line of the raw hook fields it received. Check this file first if capture seems to save nothing. `inject` uses the same log but stays quiet by default; set `HOLT_HOOK_DEBUG=1` to make it log too.
|
|
400
402
|
|
|
@@ -418,7 +420,7 @@ Run a single task non-interactively. It recalls relevant memory, injects your sk
|
|
|
418
420
|
holt run "summarize the open items in this folder"
|
|
419
421
|
```
|
|
420
422
|
|
|
421
|
-
Options: `--brain <id>` pick a brain, `--out <file>` also write the reply to a file, `--no-store` skip saving, `--no-recall` skip memory, `--quiet` suppress streaming. The folder must be trusted first (`holt init`
|
|
423
|
+
Options: `--brain <id>` pick a brain, `--out <file>` also write the reply to a file, `--no-store` skip saving, `--no-recall` skip memory, `--quiet` suppress streaming. The folder must be trusted first (`holt init` once); in a non-interactive context an untrusted folder exits non-zero instead of prompting.
|
|
422
424
|
|
|
423
425
|
### holt schedule
|
|
424
426
|
|
|
@@ -557,7 +559,6 @@ holt start your assistant: set up if needed, then launch the rea
|
|
|
557
559
|
interactive brain (Claude Code/Codex/Gemini), branded as Holt
|
|
558
560
|
holt launch same as bare "holt"
|
|
559
561
|
holt init set up (trust, brains, sign-in, defaults) for this folder
|
|
560
|
-
holt chat lightweight REPL that remembers past ones (used for API brains)
|
|
561
562
|
holt run <task> run one task non-interactively (recall, brain, remember)
|
|
562
563
|
holt schedule run a task on a timer: add | list | remove
|
|
563
564
|
holt routine named, reusable, scheduled jobs: add | run | list | show | remove
|
package/dist/cli.js
CHANGED
|
@@ -205,12 +205,22 @@ var BRAIN_SETUP = {
|
|
|
205
205
|
codex: { install: ["npm", "install", "-g", "@openai/codex"], login: ["codex", "login"] },
|
|
206
206
|
gemini: { install: ["npm", "install", "-g", "@google/gemini-cli"], login: ["gemini"] }
|
|
207
207
|
};
|
|
208
|
+
var BRAIN_LOGIN_UNAVAILABLE = {
|
|
209
|
+
gemini: {
|
|
210
|
+
reason: 'Google ended "Login with Google" for Gemini Code Assist individual accounts on 18 June 2026. Personal Google accounts (free, AI Pro, AI Ultra) can no longer sign in to Gemini CLI.',
|
|
211
|
+
provider: "gemini",
|
|
212
|
+
keyUrl: "https://aistudio.google.com/apikey"
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
function loginUnavailable(id) {
|
|
216
|
+
return BRAIN_LOGIN_UNAVAILABLE[id] ?? null;
|
|
217
|
+
}
|
|
208
218
|
var BRAIN_IDS = ["claude", "codex", "gemini"];
|
|
209
219
|
var PROVIDERS = ["anthropic", "openai", "gemini", "kimi", "grok"];
|
|
210
220
|
var PROVIDER_MODEL_SUGGESTION = {
|
|
211
|
-
anthropic: "claude-sonnet-
|
|
221
|
+
anthropic: "claude-sonnet-5",
|
|
212
222
|
openai: "gpt-5",
|
|
213
|
-
gemini: "gemini-
|
|
223
|
+
gemini: "gemini-flash-latest",
|
|
214
224
|
kimi: "kimi-k2-turbo-preview",
|
|
215
225
|
grok: "grok-4"
|
|
216
226
|
};
|
|
@@ -221,6 +231,11 @@ var PROVIDER_ENV = {
|
|
|
221
231
|
kimi: "MOONSHOT_API_KEY",
|
|
222
232
|
grok: "XAI_API_KEY"
|
|
223
233
|
};
|
|
234
|
+
function cliBrainUsable(id) {
|
|
235
|
+
const gone = loginUnavailable(id);
|
|
236
|
+
if (!gone) return true;
|
|
237
|
+
return Boolean(process.env[PROVIDER_ENV[gone.provider]]);
|
|
238
|
+
}
|
|
224
239
|
function defaultConfig() {
|
|
225
240
|
const brains = {};
|
|
226
241
|
for (const id of BRAIN_IDS) {
|
|
@@ -1509,7 +1524,10 @@ function printStatus(cfg) {
|
|
|
1509
1524
|
const b = cfg.brains[id];
|
|
1510
1525
|
const tags = [
|
|
1511
1526
|
b.enabled ? c.green("enabled") : isInstalled(b.command) ? c.dim("installed, off") : c.dim("not installed"),
|
|
1512
|
-
cfg.defaultBrain === id ? c.accent("default") : ""
|
|
1527
|
+
cfg.defaultBrain === id ? c.accent("default") : "",
|
|
1528
|
+
// A retired sign-in with no key in the environment is the real reason a
|
|
1529
|
+
// brain will not answer, so name it here rather than at first failure.
|
|
1530
|
+
loginUnavailable(id) && !cliBrainUsable(id) ? c.red("no sign-in, needs API key") : ""
|
|
1513
1531
|
].filter(Boolean).join(" ");
|
|
1514
1532
|
console.log(` ${id.padEnd(10)} ${b.label.padEnd(16)} ${tags}`);
|
|
1515
1533
|
}
|
|
@@ -1528,8 +1546,9 @@ function printStatus(cfg) {
|
|
|
1528
1546
|
console.log("\n " + c.dim("[d] default brain [t] toggle brain [c] connect API brain [x] remove API brain"));
|
|
1529
1547
|
console.log(" " + c.dim("[a] launch command [m] connect Telegram [enter] done"));
|
|
1530
1548
|
}
|
|
1531
|
-
async function connectApiBrain(ask, cfg) {
|
|
1532
|
-
let provider = null;
|
|
1549
|
+
async function connectApiBrain(ask, cfg, preset) {
|
|
1550
|
+
let provider = preset ?? null;
|
|
1551
|
+
if (provider) console.log(c.dim(` provider: ${provider}`));
|
|
1533
1552
|
for (let tries = 0; tries < 3 && !provider; tries++) {
|
|
1534
1553
|
const provRaw = (await ask(` provider [${PROVIDERS.join("/")}] (enter for ${PROVIDERS[0]}, or "skip"): `) ?? "skip").trim().toLowerCase();
|
|
1535
1554
|
if (provRaw === "skip" || provRaw === "q" || provRaw === "n" || provRaw === "no") {
|
|
@@ -1547,7 +1566,9 @@ async function connectApiBrain(ask, cfg) {
|
|
|
1547
1566
|
const suggestion = PROVIDER_MODEL_SUGGESTION[provider];
|
|
1548
1567
|
const modelRaw = (await ask(` model (enter for ${suggestion}): `) ?? "").trim();
|
|
1549
1568
|
const model = modelRaw || suggestion;
|
|
1550
|
-
const
|
|
1569
|
+
const idHint = preset ? `${preset}-api` : "";
|
|
1570
|
+
const idPrompt = idHint ? ` short name for this brain (enter for ${idHint}): ` : " short name for this brain (e.g. sonnet): ";
|
|
1571
|
+
const idRaw = (await ask(idPrompt) ?? "").trim() || idHint;
|
|
1551
1572
|
if (!idRaw) {
|
|
1552
1573
|
console.log(c.dim(" cancelled (no name)."));
|
|
1553
1574
|
return null;
|
|
@@ -1603,8 +1624,12 @@ async function runSettings(ask) {
|
|
|
1603
1624
|
} else if (choice === "t") {
|
|
1604
1625
|
const pick = (await ask(` toggle which brain [${BRAIN_IDS.join("/")}]: `) ?? "").trim();
|
|
1605
1626
|
if (BRAIN_IDS.includes(pick)) {
|
|
1627
|
+
const goneLogin = loginUnavailable(pick);
|
|
1606
1628
|
if (!cfg.brains[pick].enabled && !isInstalled(cfg.brains[pick].command)) {
|
|
1607
1629
|
console.log(c.dim(` ${cfg.brains[pick].label} is not installed. Run "holt init" to install it.`));
|
|
1630
|
+
} else if (!cfg.brains[pick].enabled && goneLogin && !cliBrainUsable(pick)) {
|
|
1631
|
+
console.log(c.dim(` ${cfg.brains[pick].label} has no sign-in any more and no ${goneLogin.provider.toUpperCase()}_API_KEY set.`));
|
|
1632
|
+
console.log(c.dim(` Get a key at ${goneLogin.keyUrl}, then connect it here with "c".`));
|
|
1608
1633
|
} else {
|
|
1609
1634
|
cfg.brains[pick].enabled = !cfg.brains[pick].enabled;
|
|
1610
1635
|
if (!cfg.brains[pick].enabled && cfg.defaultBrain === pick) cfg.defaultBrain = selectableIds(cfg)[0] ?? null;
|
|
@@ -2139,6 +2164,75 @@ async function voiceClear() {
|
|
|
2139
2164
|
console.log(had ? c.green("\n Voice profile removed.\n") : c.dim("\n No voice profile to remove.\n"));
|
|
2140
2165
|
}
|
|
2141
2166
|
|
|
2167
|
+
// src/commands/login.ts
|
|
2168
|
+
function printLoginUnavailable(id, info) {
|
|
2169
|
+
console.log("\n" + c.red(`${BRAIN_DEFS[id].label} sign-in is no longer available.`));
|
|
2170
|
+
console.log(c.dim(" " + info.reason));
|
|
2171
|
+
console.log("\n It still works on an API key. Two ways:");
|
|
2172
|
+
console.log(" 1. " + c.accent(`Get a key at ${info.keyUrl}`));
|
|
2173
|
+
console.log(" 2. Either export it as " + c.accent(`${info.provider.toUpperCase()}_API_KEY`) + c.dim(` (the ${BRAIN_DEFS[id].command} CLI reads it too), or connect it to Holt as a direct API brain.`));
|
|
2174
|
+
}
|
|
2175
|
+
async function offerKeyInstead(ask, id, info) {
|
|
2176
|
+
const cfg = loadConfig();
|
|
2177
|
+
if (!cfg) {
|
|
2178
|
+
console.log(c.dim('\n This folder is not set up yet. Run "holt init" and connect it as a direct API brain.\n'));
|
|
2179
|
+
return false;
|
|
2180
|
+
}
|
|
2181
|
+
if (!await askYesNo(ask, `
|
|
2182
|
+
Connect ${BRAIN_DEFS[id].label} to Holt with an API key now? [Y/n] `, true)) {
|
|
2183
|
+
console.log(c.dim(' Okay. Run "holt setting" then "c" whenever you have the key.\n'));
|
|
2184
|
+
return false;
|
|
2185
|
+
}
|
|
2186
|
+
const brain = await connectApiBrain(ask, cfg, info.provider);
|
|
2187
|
+
if (!brain) return false;
|
|
2188
|
+
if (!cfg.defaultBrain || cfg.defaultBrain === id) {
|
|
2189
|
+
cfg.defaultBrain = brain.id;
|
|
2190
|
+
console.log(c.dim(` default brain for this folder is now "${brain.id}".`));
|
|
2191
|
+
}
|
|
2192
|
+
if (cfg.brains[id]?.enabled) {
|
|
2193
|
+
cfg.brains[id].enabled = false;
|
|
2194
|
+
console.log(c.dim(` turned off the ${BRAIN_DEFS[id].label} brain (no sign-in available).`));
|
|
2195
|
+
}
|
|
2196
|
+
saveConfig(cfg);
|
|
2197
|
+
console.log(c.green("\n Done. Start Holt: holt\n"));
|
|
2198
|
+
return true;
|
|
2199
|
+
}
|
|
2200
|
+
async function login(which) {
|
|
2201
|
+
const id = (which || "").toLowerCase();
|
|
2202
|
+
if (!BRAIN_IDS.includes(id)) {
|
|
2203
|
+
if (which) {
|
|
2204
|
+
console.error(`
|
|
2205
|
+
Unknown brain "${which}". Use one of: ${BRAIN_IDS.join(", ")}
|
|
2206
|
+
`);
|
|
2207
|
+
process.exitCode = 1;
|
|
2208
|
+
} else {
|
|
2209
|
+
console.log(c.dim(`
|
|
2210
|
+
Usage: holt login <${BRAIN_IDS.join("|")}>
|
|
2211
|
+
`));
|
|
2212
|
+
}
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
const gone = loginUnavailable(id);
|
|
2216
|
+
if (gone) {
|
|
2217
|
+
printLoginUnavailable(id, gone);
|
|
2218
|
+
if (cliBrainUsable(id)) {
|
|
2219
|
+
console.log(c.green(`
|
|
2220
|
+
${PROVIDER_ENV[gone.provider]} is already set, so ${BRAIN_DEFS[id].label} works as it is. Nothing to do.
|
|
2221
|
+
`));
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
const { ask, close } = createReader();
|
|
2225
|
+
await offerKeyInstead(ask, id, gone);
|
|
2226
|
+
close();
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
const s = BRAIN_SETUP[id];
|
|
2230
|
+
console.log("\n" + c.accent(`Sign in to ${BRAIN_DEFS[id].label}`));
|
|
2231
|
+
console.log(c.dim(` Starting "${s.login.join(" ")}". Complete sign-in, then exit that tool.
|
|
2232
|
+
`));
|
|
2233
|
+
await runInteractive(s.login[0], s.login.slice(1));
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2142
2236
|
// src/commands/init.ts
|
|
2143
2237
|
function parseBrains(raw, found) {
|
|
2144
2238
|
const s = raw.trim().toLowerCase();
|
|
@@ -2159,7 +2253,8 @@ async function init() {
|
|
|
2159
2253
|
const found = [];
|
|
2160
2254
|
for (const id of BRAIN_IDS) {
|
|
2161
2255
|
const ok2 = isInstalled(BRAIN_DEFS[id].command);
|
|
2162
|
-
|
|
2256
|
+
const note2 = loginUnavailable(id) && !cliBrainUsable(id) ? c.dim(" (needs an API key, no sign-in)") : "";
|
|
2257
|
+
console.log(` ${ok2 ? c.green("found ") : c.dim("missing")} ${BRAIN_DEFS[id].label} (${BRAIN_DEFS[id].command})${note2}`);
|
|
2163
2258
|
if (ok2) found.push(id);
|
|
2164
2259
|
}
|
|
2165
2260
|
console.log("");
|
|
@@ -2171,11 +2266,24 @@ async function init() {
|
|
|
2171
2266
|
const toInstall = chosen.filter((id) => !isInstalled(BRAIN_DEFS[id].command));
|
|
2172
2267
|
const loginWanted = /* @__PURE__ */ new Set();
|
|
2173
2268
|
for (const id of toInstall) {
|
|
2269
|
+
if (loginUnavailable(id)) continue;
|
|
2174
2270
|
if (await askYesNo(ask, ` ${BRAIN_DEFS[id].label} is not installed. Sign in after install? [Y/n] `, true)) loginWanted.add(id);
|
|
2175
2271
|
}
|
|
2176
2272
|
const connectedApiBrains = [];
|
|
2273
|
+
const holder = defaultConfig();
|
|
2274
|
+
for (const id of chosen) {
|
|
2275
|
+
const gone = loginUnavailable(id);
|
|
2276
|
+
if (!gone || cliBrainUsable(id)) continue;
|
|
2277
|
+
printLoginUnavailable(id, gone);
|
|
2278
|
+
if (await askYesNo(ask, `
|
|
2279
|
+
Connect ${BRAIN_DEFS[id].label} with an API key now? [Y/n] `, true)) {
|
|
2280
|
+
const brain = await connectApiBrain(ask, holder, gone.provider);
|
|
2281
|
+
if (brain) connectedApiBrains.push(brain);
|
|
2282
|
+
} else {
|
|
2283
|
+
console.log(c.dim(' Okay. Run "holt setting" then "c" whenever you have the key.'));
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2177
2286
|
if (await askYesNo(ask, "\nAlso connect a direct API brain (raw key, no CLI needed)? [y/N] ", false)) {
|
|
2178
|
-
const holder = defaultConfig();
|
|
2179
2287
|
const brain = await connectApiBrain(ask, holder);
|
|
2180
2288
|
if (brain) connectedApiBrains.push(brain);
|
|
2181
2289
|
}
|
|
@@ -2246,7 +2354,7 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
|
|
|
2246
2354
|
}
|
|
2247
2355
|
}
|
|
2248
2356
|
const cfg = loadConfig() ?? defaultConfig();
|
|
2249
|
-
for (const id of BRAIN_IDS) cfg.brains[id].enabled = chosen.includes(id) && isInstalled(BRAIN_DEFS[id].command);
|
|
2357
|
+
for (const id of BRAIN_IDS) cfg.brains[id].enabled = chosen.includes(id) && isInstalled(BRAIN_DEFS[id].command) && cliBrainUsable(id);
|
|
2250
2358
|
for (const b of connectedApiBrains) if (!cfg.apiBrains.some((a) => a.id === b.id)) cfg.apiBrains.push(b);
|
|
2251
2359
|
cfg.defaultBrain = cfg.brains[defaultBrain].enabled ? defaultBrain : BRAIN_IDS.find((id) => cfg.brains[id].enabled) ?? cfg.apiBrains[0]?.id ?? null;
|
|
2252
2360
|
saveConfig(cfg);
|
|
@@ -2259,9 +2367,9 @@ Default brain? [${chosen.join("/")}] (${defPick}): `) ?? "").trim();
|
|
|
2259
2367
|
console.log("\nStart chatting:");
|
|
2260
2368
|
console.log(" " + c.accent(`source ${aliasNeedsSource}`) + c.dim(" (once; new terminals will not need it)"));
|
|
2261
2369
|
console.log(" " + c.accent(aliasAns) + "\n");
|
|
2262
|
-
console.log(c.dim(" Or right now, without sourcing: holt
|
|
2370
|
+
console.log(c.dim(" Or right now, without sourcing: holt\n"));
|
|
2263
2371
|
} else {
|
|
2264
|
-
console.log("Start chatting: " + c.accent("holt
|
|
2372
|
+
console.log("Start chatting: " + c.accent("holt") + "\n");
|
|
2265
2373
|
}
|
|
2266
2374
|
} else {
|
|
2267
2375
|
console.log(c.dim('No brain is ready yet. Install one, then run "holt init" again.\n'));
|
|
@@ -4073,29 +4181,6 @@ function saveReply(reply, format, name) {
|
|
|
4073
4181
|
return path;
|
|
4074
4182
|
}
|
|
4075
4183
|
|
|
4076
|
-
// src/commands/login.ts
|
|
4077
|
-
async function login(which) {
|
|
4078
|
-
const id = (which || "").toLowerCase();
|
|
4079
|
-
if (!BRAIN_IDS.includes(id)) {
|
|
4080
|
-
if (which) {
|
|
4081
|
-
console.error(`
|
|
4082
|
-
Unknown brain "${which}". Use one of: ${BRAIN_IDS.join(", ")}
|
|
4083
|
-
`);
|
|
4084
|
-
process.exitCode = 1;
|
|
4085
|
-
} else {
|
|
4086
|
-
console.log(c.dim(`
|
|
4087
|
-
Usage: holt login <${BRAIN_IDS.join("|")}>
|
|
4088
|
-
`));
|
|
4089
|
-
}
|
|
4090
|
-
return;
|
|
4091
|
-
}
|
|
4092
|
-
const s = BRAIN_SETUP[id];
|
|
4093
|
-
console.log("\n" + c.accent(`Sign in to ${BRAIN_DEFS[id].label}`));
|
|
4094
|
-
console.log(c.dim(` Starting "${s.login.join(" ")}". Complete sign-in, then exit that tool.
|
|
4095
|
-
`));
|
|
4096
|
-
await runInteractive(s.login[0], s.login.slice(1));
|
|
4097
|
-
}
|
|
4098
|
-
|
|
4099
4184
|
// src/access.ts
|
|
4100
4185
|
import { existsSync as existsSync11, realpathSync as realpathSync3, statSync as statSync6 } from "fs";
|
|
4101
4186
|
import { homedir as homedir6 } from "os";
|
|
@@ -4235,7 +4320,7 @@ async function chat() {
|
|
|
4235
4320
|
return;
|
|
4236
4321
|
}
|
|
4237
4322
|
await init();
|
|
4238
|
-
console.log(c.dim('\nSetup done. Run "holt
|
|
4323
|
+
console.log(c.dim('\nSetup done. Run "holt" to start talking.\n'));
|
|
4239
4324
|
return;
|
|
4240
4325
|
}
|
|
4241
4326
|
let active = resolveActive2(cfg, cfg.defaultBrain);
|
|
@@ -4363,7 +4448,7 @@ async function chat() {
|
|
|
4363
4448
|
sb.detach();
|
|
4364
4449
|
close();
|
|
4365
4450
|
await login(targetId);
|
|
4366
|
-
console.log(c.dim('\n Signed in. Run "holt
|
|
4451
|
+
console.log(c.dim(loginUnavailable(targetId) ? '\n Run "holt" again to continue.\n' : '\n Signed in. Run "holt" again to continue.\n'));
|
|
4367
4452
|
return;
|
|
4368
4453
|
}
|
|
4369
4454
|
if (cmd === "brain") {
|
|
@@ -4461,7 +4546,8 @@ async function chat() {
|
|
|
4461
4546
|
};
|
|
4462
4547
|
const res = active.kind === "cli" ? await runBrain(cfg.brains[active.id], prompt, onChunk, extraArgs) : await runApiBrain(active.brain, prompt, onChunk);
|
|
4463
4548
|
if (looksLikeAuthError(res.text)) {
|
|
4464
|
-
const
|
|
4549
|
+
const goneLogin = active.kind === "cli" ? loginUnavailable(active.id) : null;
|
|
4550
|
+
const hint = goneLogin ? ` ${active.label} has no sign-in any more and no ${PROVIDER_ENV[goneLogin.provider]} to fall back on. Get a key at ${goneLogin.keyUrl}, then run "holt setting" to connect it.` : active.kind === "cli" ? ` ${active.label} looks signed out. Run "holt login ${active.id}" in a terminal, or type /login here.` : ` ${active.label} has no working key. Run "holt setting" to add or fix it.`;
|
|
4465
4551
|
if (!streamed || !res.text.endsWith("\n")) console.log("");
|
|
4466
4552
|
console.log(c.red(hint) + "\n");
|
|
4467
4553
|
showStatus();
|
|
@@ -4600,11 +4686,12 @@ function resolveTarget(cfg) {
|
|
|
4600
4686
|
if (!id) return null;
|
|
4601
4687
|
if (BRAIN_IDS.includes(id)) {
|
|
4602
4688
|
const bid = id;
|
|
4603
|
-
return { kind: "cli", id: bid, label: cfg.brains[bid].label };
|
|
4689
|
+
if (cliBrainUsable(bid)) return { kind: "cli", id: bid, label: cfg.brains[bid].label };
|
|
4690
|
+
} else {
|
|
4691
|
+
const api = findApiBrain(cfg, id);
|
|
4692
|
+
if (api) return { kind: "api", label: `${id} (api: ${api.provider}/${api.model})` };
|
|
4604
4693
|
}
|
|
4605
|
-
const
|
|
4606
|
-
if (api) return { kind: "api", label: `${id} (api: ${api.provider}/${api.model})` };
|
|
4607
|
-
const fallback = BRAIN_IDS.find((b) => cfg.brains[b].enabled);
|
|
4694
|
+
const fallback = BRAIN_IDS.find((b) => cfg.brains[b].enabled && cliBrainUsable(b));
|
|
4608
4695
|
if (fallback) return { kind: "cli", id: fallback, label: cfg.brains[fallback].label };
|
|
4609
4696
|
return null;
|
|
4610
4697
|
}
|
|
@@ -4644,13 +4731,23 @@ async function launch() {
|
|
|
4644
4731
|
await ensureSetup();
|
|
4645
4732
|
const target = resolveTarget(cfg);
|
|
4646
4733
|
if (!target) {
|
|
4734
|
+
const def = cfg.defaultBrain;
|
|
4735
|
+
const gone = def && BRAIN_IDS.includes(def) ? loginUnavailable(def) : null;
|
|
4736
|
+
if (gone) {
|
|
4737
|
+
console.log("\n " + c.red(`${cfg.brains[def].label} can no longer sign in.`));
|
|
4738
|
+
console.log(c.dim(" " + gone.reason));
|
|
4739
|
+
console.log(c.dim(` Get a key at ${gone.keyUrl}, then run "holt setting" and press "c" to connect it.`));
|
|
4740
|
+
if (cfg.apiBrains.length) {
|
|
4741
|
+
console.log(c.dim(` Or switch this folder to an API brain you already have (${cfg.apiBrains.map((b) => b.id).join(", ")}): "holt setting" then "d".`));
|
|
4742
|
+
}
|
|
4743
|
+
console.log("");
|
|
4744
|
+
return;
|
|
4745
|
+
}
|
|
4647
4746
|
console.log(c.dim('\n No brain is ready. Run "holt setting" or "holt init".\n'));
|
|
4648
4747
|
return;
|
|
4649
4748
|
}
|
|
4650
4749
|
if (target.kind === "api") {
|
|
4651
|
-
console.log(
|
|
4652
|
-
launch needs a CLI brain; starting chat instead (default is ${target.label}).
|
|
4653
|
-
`));
|
|
4750
|
+
console.log(renderIntroBox());
|
|
4654
4751
|
await chat();
|
|
4655
4752
|
return;
|
|
4656
4753
|
}
|
|
@@ -7066,7 +7163,7 @@ async function graph(args = []) {
|
|
|
7066
7163
|
if (skipNotes.length) console.log(c.dim(` ingest skipped: ${skipNotes.join(", ")}`));
|
|
7067
7164
|
}
|
|
7068
7165
|
if (turns.length === 0 && wikiPages.length === 0 && !codeGraph) {
|
|
7069
|
-
console.log(c.dim('\n No memory in this folder yet.
|
|
7166
|
+
console.log(c.dim('\n No memory in this folder yet. Talk to Holt here first with "holt", then come back.\n'));
|
|
7070
7167
|
console.log(c.dim(` Or ingest this folder's code/docs: "holt graph --code" / "holt graph --docs".
|
|
7071
7168
|
`));
|
|
7072
7169
|
return;
|
|
@@ -7389,7 +7486,7 @@ function holtTools() {
|
|
|
7389
7486
|
}
|
|
7390
7487
|
|
|
7391
7488
|
// src/version.ts
|
|
7392
|
-
var VERSION = "0.
|
|
7489
|
+
var VERSION = "0.16.0";
|
|
7393
7490
|
|
|
7394
7491
|
// src/commands/mcp.ts
|
|
7395
7492
|
function printSetup() {
|
|
@@ -7573,7 +7670,7 @@ async function run(args) {
|
|
|
7573
7670
|
close();
|
|
7574
7671
|
if (!ok2) return;
|
|
7575
7672
|
} else {
|
|
7576
|
-
process.stderr.write("This folder is not trusted. Run holt init
|
|
7673
|
+
process.stderr.write("This folder is not trusted. Run holt init here once, then schedule/run.\n");
|
|
7577
7674
|
process.exitCode = 1;
|
|
7578
7675
|
return;
|
|
7579
7676
|
}
|
|
@@ -8080,7 +8177,7 @@ async function runCmd(rest) {
|
|
|
8080
8177
|
close();
|
|
8081
8178
|
if (!ok2) return;
|
|
8082
8179
|
} else {
|
|
8083
|
-
process.stderr.write("This folder is not trusted. Run holt init
|
|
8180
|
+
process.stderr.write("This folder is not trusted. Run holt init here once, then run the routine.\n");
|
|
8084
8181
|
process.exitCode = 1;
|
|
8085
8182
|
return;
|
|
8086
8183
|
}
|
|
@@ -8483,8 +8580,14 @@ async function doctor(args = []) {
|
|
|
8483
8580
|
for (const id of BRAIN_IDS) {
|
|
8484
8581
|
const def = BRAIN_DEFS[id];
|
|
8485
8582
|
const here = isInstalled(def.command);
|
|
8486
|
-
|
|
8583
|
+
const gone = loginUnavailable(id);
|
|
8584
|
+
const usable = cliBrainUsable(id);
|
|
8585
|
+
if (here && usable) installed.push(def.label);
|
|
8487
8586
|
console.log(` ${here ? c.green("installed") : c.dim("missing ")} ${def.label} ${c.dim(`(${def.command})`)}`);
|
|
8587
|
+
if (here && gone && !usable) {
|
|
8588
|
+
warn(`${def.label} cannot sign in any more. ${gone.reason}`);
|
|
8589
|
+
steps.push(`Get a ${gone.provider} key at ${gone.keyUrl}, then connect it: holt setting -> "c".`);
|
|
8590
|
+
}
|
|
8488
8591
|
}
|
|
8489
8592
|
const cfg = loadConfig();
|
|
8490
8593
|
const apiBrains = cfg?.apiBrains ?? [];
|
|
@@ -8565,7 +8668,7 @@ async function doctor(args = []) {
|
|
|
8565
8668
|
steps.unshift('Set up this folder: run "holt init".');
|
|
8566
8669
|
}
|
|
8567
8670
|
if (steps.length === 0) {
|
|
8568
|
-
good('You are in good shape. Run "holt
|
|
8671
|
+
good('You are in good shape. Run "holt" to start.');
|
|
8569
8672
|
} else {
|
|
8570
8673
|
let n = 1;
|
|
8571
8674
|
for (const s of steps) console.log(` ${c.accent(String(n++) + ".")} ${s}`);
|
|
@@ -8875,7 +8978,7 @@ async function write(args) {
|
|
|
8875
8978
|
close();
|
|
8876
8979
|
if (!ok2) return;
|
|
8877
8980
|
} else {
|
|
8878
|
-
process.stderr.write("This folder is not trusted. Run holt init
|
|
8981
|
+
process.stderr.write("This folder is not trusted. Run holt init here once first.\n");
|
|
8879
8982
|
process.exitCode = 1;
|
|
8880
8983
|
return;
|
|
8881
8984
|
}
|
|
@@ -8989,12 +9092,12 @@ From your phone, while you sleep:
|
|
|
8989
9092
|
|
|
8990
9093
|
Housekeeping:
|
|
8991
9094
|
setting Change brains, API connections, and your launch word (per folder)
|
|
8992
|
-
login <brain> Sign in to a brain: claude
|
|
9095
|
+
login <brain> Sign in to a brain: claude or codex
|
|
9096
|
+
(Gemini has no sign-in left; it routes you to an API key)
|
|
8993
9097
|
update Get the latest Holt, in one word
|
|
8994
9098
|
reset Start this folder over (asks first; your own files are untouched)
|
|
8995
9099
|
hook Ambient memory inside plain Claude Code: holt hook [install | status]
|
|
8996
9100
|
mcp Share Holt's memory with other AI apps (Claude Code, Cursor, Codex)
|
|
8997
|
-
chat Lightweight built-in chat, used for API brains
|
|
8998
9101
|
init Full setup by hand (bare "holt" runs this for you when needed)
|
|
8999
9102
|
launch Same as bare "holt": start your assistant
|
|
9000
9103
|
version | help Print the version / show this help
|
|
@@ -9032,6 +9135,8 @@ async function main() {
|
|
|
9032
9135
|
case "init":
|
|
9033
9136
|
await init();
|
|
9034
9137
|
break;
|
|
9138
|
+
// Undocumented: bare `holt` reaches the REPL by itself for API brains.
|
|
9139
|
+
// Kept as a working alias so older scripts and aliases do not break.
|
|
9035
9140
|
case "chat":
|
|
9036
9141
|
await chat();
|
|
9037
9142
|
break;
|