@krimto-labs/krimto 0.2.32 → 0.2.35
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 +115 -50
- package/bin/krimto.mjs +93 -7
- package/package.json +1 -1
- package/src/cli/editors.ts +64 -1
- package/src/cli/folderCmd.ts +12 -1
- package/src/cli/help.ts +14 -0
- package/src/cli/mcpConfig.ts +54 -17
- package/src/cli/promptHelpers.ts +24 -0
- package/src/cli/remoteCmd.ts +14 -1
- package/src/cli/reset.ts +41 -26
- package/src/cli/searchSettings.ts +13 -1
- package/src/cli/serviceCmd.ts +14 -1
- package/src/cli/status.ts +13 -2
- package/src/cli/wizard.ts +134 -28
- package/src/server/index.ts +1 -1
package/README.md
CHANGED
|
@@ -9,28 +9,55 @@ place and reads the right slice of it — Alice's preferences override the team'
|
|
|
9
9
|
conventions override the org's standards, and every fact carries a paper trail (author, source,
|
|
10
10
|
timestamp, reviewer).
|
|
11
11
|
|
|
12
|
-
> **Where we are:** **v0.2.
|
|
13
|
-
>
|
|
14
|
-
> (markdown-in-git storage, `user → team → org` hierarchy,
|
|
15
|
-
> two-way git sync, MCP over stdio + HTTP, the Docker
|
|
16
|
-
>
|
|
12
|
+
> **Where we are:** **v0.2.35** is the current release — the v0.2.17 wizard redesign is now
|
|
13
|
+
> shipped end-to-end, plus eighteen patch releases of correctness fixes and agent-friendly
|
|
14
|
+
> surface. The v0.2.16 architecture (markdown-in-git storage, `user → team → org` hierarchy,
|
|
15
|
+
> hybrid retrieval, server-enforced access, two-way git sync, MCP over stdio + HTTP, the Docker
|
|
16
|
+
> image, the web UI) is unchanged. What you get on top of v0.2.16:
|
|
17
17
|
>
|
|
18
|
-
>
|
|
19
|
-
>
|
|
20
|
-
>
|
|
21
|
-
>
|
|
22
|
-
> - **
|
|
23
|
-
>
|
|
24
|
-
>
|
|
25
|
-
> thing without re-running the whole wizard. `reset` cleanly disconnects + uninstalls; `--wipe-notes`
|
|
26
|
-
> moves data to a recoverable trash sibling, never `rm -rf`.
|
|
27
|
-
> - **Notes-app `/ui`** — plain-English scope labels (Just me / Team name / Org name), inline
|
|
28
|
-
> Edit + Move + Delete on every note, and a consolidated **Settings** page for the engineering
|
|
29
|
-
> panels.
|
|
18
|
+
> **The setup story (v0.2.17 → v0.2.21).**
|
|
19
|
+
> - **One-command interactive setup wizard** (`krimto init`) — five questions, preselected
|
|
20
|
+
> defaults. Reads `git config user.email` for identity; detects editors at both project
|
|
21
|
+
> level (`.cursor/`, `CLAUDE.md`) and machine level (`~/.cursor/`, `~/.claude.json`).
|
|
22
|
+
> - **Reconfigure menu** — re-runs of `krimto init` show a "Krimto on this machine:"
|
|
23
|
+
> summary with a real **Service:** line driven by lock + launchctl reality (not just
|
|
24
|
+
> config snapshot). "Keep current" prompts let you Enter-through.
|
|
30
25
|
>
|
|
31
|
-
>
|
|
32
|
-
>
|
|
33
|
-
>
|
|
26
|
+
> **The teardown story (v0.2.32 → v0.2.33).**
|
|
27
|
+
> - **`krimto stop` / `start` / `restart`** — first-class verbs for the off-ramp the original
|
|
28
|
+
> wizard forgot. Idempotent. `stop` keeps the plist on disk so `start` can reload it.
|
|
29
|
+
> - **`krimto reset [--yes] [--wipe-notes]`** — aggressive sweep across all editors (Cursor
|
|
30
|
+
> JSON + Claude Code CLI scopes user/project/local) + launchd/systemd uninstall + lock
|
|
31
|
+
> cleanup. `--wipe-notes` collapses to one named-consequence prompt.
|
|
32
|
+
>
|
|
33
|
+
> **The runtime story (v0.2.26 → v0.2.30).**
|
|
34
|
+
> - **`/ui` notes-app redesign** — warm-paper Fraunces serif aesthetic, scope cards with
|
|
35
|
+
> emoji icons (📔 Just me / 📓 Team / 🏢 Org), notes timeline with per-row Edit / Move /
|
|
36
|
+
> Delete / View file. `krimto ui` opens it.
|
|
37
|
+
> - **Single reconciled runtime view** (`inspectRuntime`) — every read-side command (status,
|
|
38
|
+
> verify-connection, whoami, reconfigure menu) routes through the same lock + launchctl +
|
|
39
|
+
> editor-config probe. No more "status says one thing, verify says another."
|
|
40
|
+
> - **Service-first install ordering + port-ready probe** — wizard installs the service
|
|
41
|
+
> first, waits for `:8080` to accept TCP, then writes editor configs. Cursor's file
|
|
42
|
+
> watcher never fires into an unbound port (the v0.2.27/28 ECONNREFUSED fix).
|
|
43
|
+
>
|
|
44
|
+
> **The agent story (v0.2.34 → v0.2.35).**
|
|
45
|
+
> - **Phase B agent flags** — `editors --add cursor`, `service --always`, `search --keyword`,
|
|
46
|
+
> `reset --yes`, `remote --set <url>`, `folder --to <path>`. Every command that used to
|
|
47
|
+
> open an interactive prompt now has a flag form.
|
|
48
|
+
> - **Non-TTY guards** — interactive commands without flags exit 2 with copy-pasteable
|
|
49
|
+
> usage instead of hanging on an unanswerable prompt.
|
|
50
|
+
> - **`krimto whoami` + `set identity <email>`** + a **`krimto_whoami` MCP tool** so agents
|
|
51
|
+
> stop hallucinating identity.
|
|
52
|
+
> - **Editor attribution via User-Agent sniffing** — facts saved over HTTP automatically
|
|
53
|
+
> carry `source: "cursor"` / `"claude-code"` / etc., so the dashboard's "saved from a
|
|
54
|
+
> Cursor chat" line works without prompting agents.
|
|
55
|
+
> - **Cursor `alwaysApply: true` frontmatter** so `.cursor/rules/krimto.mdc` auto-attaches
|
|
56
|
+
> instead of requiring the user to type "krimto" first.
|
|
57
|
+
>
|
|
58
|
+
> See [ROADMAP.md](ROADMAP.md), [CHANGELOG.md](CHANGELOG.md), and the proposal-vs-reality
|
|
59
|
+
> diff in [docs/krimto-v0.2.17-maria-journey.html §09](docs/krimto-v0.2.17-maria-journey.html)
|
|
60
|
+
> for the design rationale + what each patch caught.
|
|
34
61
|
|
|
35
62
|
## Try it in 90 seconds (solo, no account)
|
|
36
63
|
|
|
@@ -76,6 +103,33 @@ what your agent has been calling.
|
|
|
76
103
|
**Power-user / CI:** `npx @krimto-labs/krimto init --yes` skips all prompts and applies
|
|
77
104
|
defaults non-interactively. `--all` and `--minimal` keep their v0.2.16 meaning.
|
|
78
105
|
|
|
106
|
+
### Setting Krimto up programmatically (AI agents, CI)
|
|
107
|
+
|
|
108
|
+
Krimto is designed to be driven by Claude Code, Cursor, Codex, and similar AI agents that
|
|
109
|
+
don't have a stdin TTY. Every interactive command has a flag form:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# The one-command full install (defaults: detected editors, as-needed run mode):
|
|
113
|
+
krimto init --yes
|
|
114
|
+
|
|
115
|
+
# Or pick the pieces:
|
|
116
|
+
krimto editors --add cursor --add claude-code --yes # wire editors
|
|
117
|
+
krimto service --always # install background service
|
|
118
|
+
krimto search --keyword # keyword search (default)
|
|
119
|
+
krimto search --openai --api-key sk-... # semantic search
|
|
120
|
+
krimto remote --set git@github.com:acme/krimto.git # cross-machine sync
|
|
121
|
+
krimto status # machine-readable check
|
|
122
|
+
krimto stop / start / restart # idempotent service control
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Run any interactive command (`editors`, `service`, `search`, `remote`, `folder`, `reset`)
|
|
126
|
+
without a flag from a non-TTY shell and you get `exit 2` with copy-pasteable flag usage,
|
|
127
|
+
not a hung-prompt warning. `krimto --help` has a dedicated **For AI agents (no TTY)**
|
|
128
|
+
block at the top.
|
|
129
|
+
|
|
130
|
+
The HTTP MCP handler also **sniffs `User-Agent`** to auto-stamp facts with `source:
|
|
131
|
+
"cursor"` / `"claude-code"` / etc. — no agent-prompt convention needed.
|
|
132
|
+
|
|
79
133
|
## Connect your agent
|
|
80
134
|
|
|
81
135
|
Krimto is one MCP server — point any client at `http://localhost:8080/mcp`. Verified for **Claude Code**
|
|
@@ -136,66 +190,77 @@ The in-product **Connect** page (`/ui/connect`) shows this same rule with a copy
|
|
|
136
190
|
|
|
137
191
|
Everything is reachable via `npx`. Run `npx @krimto-labs/krimto --help` for the full list. All
|
|
138
192
|
commands print clean, sectioned output with ✅ / ⚠️ / 🟢 status indicators and copy-paste shell
|
|
139
|
-
commands.
|
|
193
|
+
commands. The seven groups below match `--help`'s structure.
|
|
140
194
|
|
|
141
195
|
**Get connected (start here)**
|
|
142
196
|
|
|
143
197
|
| Command | What it does |
|
|
144
198
|
|---|---|
|
|
145
|
-
| `init` | Interactive setup wizard — five questions, preselected defaults. Connects detected editors, applies the standing rule, optionally installs a background service. `--yes` skips prompts; legacy `--all` / `--minimal` still work. |
|
|
146
|
-
| `
|
|
147
|
-
| `
|
|
148
|
-
| `uninit` | Strip the standing rule from this project's rules files |
|
|
199
|
+
| `init [--yes]` | Interactive setup wizard — five questions, preselected defaults. Connects detected editors, applies the standing rule, optionally installs a background service. `--yes` skips prompts; legacy `--all` / `--minimal` still work. |
|
|
200
|
+
| `connect` | Print copy-paste config for Claude Code & Cursor (manual path) — substitutes your real `git config user.email` for the identity. |
|
|
201
|
+
| `uninit [--also-stop | --keep-running]` | Strip the standing rule from this project's rules files. Offers to stop the service too (or skip the prompt with the flags). |
|
|
149
202
|
|
|
150
|
-
**
|
|
203
|
+
**Look at your notes**
|
|
151
204
|
|
|
152
205
|
| Command | What it does |
|
|
153
206
|
|---|---|
|
|
154
207
|
| `notes [query]` | List notes grouped by plain-English scope (`Just me` / team name / org name). With a query: ranked search via `krimto_recall`. |
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
| `
|
|
158
|
-
| `
|
|
208
|
+
| `ui` | Open `http://localhost:8080/ui` in your browser. |
|
|
209
|
+
| `open` | Reveal the notes folder in your OS file manager (`open` / `xdg-open` / `explorer`). |
|
|
210
|
+
| `edit <id>` | Open the fact's `.md` in `$EDITOR`; reindexes on save. |
|
|
211
|
+
| `mv <id> <scope>` | Move a note between scopes (id preserved). |
|
|
212
|
+
| `supersede <id>` | Replace a note with a new version. Old stays in git. |
|
|
213
|
+
| `tag <id> +new -old ...` | Add or remove tags via frontmatter rewrite. |
|
|
214
|
+
| `rm <id>` | Delete a fact (file + index + git deletion commit). |
|
|
159
215
|
|
|
160
|
-
**
|
|
216
|
+
**Stop & reset** (v0.2.32+)
|
|
161
217
|
|
|
162
218
|
| Command | What it does |
|
|
163
219
|
|---|---|
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
220
|
+
| `stop` | Stop the running krimto (uninstalls/unloads service if installed, SIGTERMs an ad-hoc PID). Plist stays on disk so `start` can reload it. Idempotent. |
|
|
221
|
+
| `start` | Start krimto (re-bootstraps the existing plist). Honest message when no service is configured. |
|
|
222
|
+
| `restart` | `stop` + `start`. Atomic on always-running mode via `launchctl kickstart -k`. |
|
|
223
|
+
| `uninit` | Project-only undo (rule files only). Offers to stop the service too. |
|
|
224
|
+
| `reset [--yes] [--wipe-notes]` | Disconnect every editor (Cursor JSON + Claude Code CLI across user/project/local scopes) + uninstall service + wipe API-key store. `--wipe-notes` collapses to one named-consequence prompt. |
|
|
167
225
|
|
|
168
|
-
**
|
|
226
|
+
**Is it working?**
|
|
169
227
|
|
|
170
228
|
| Command | What it does |
|
|
171
229
|
|---|---|
|
|
172
|
-
| `
|
|
173
|
-
| `
|
|
174
|
-
| `
|
|
175
|
-
| `reset [--yes] [--wipe-notes]` | Disconnect from all editors + uninstall service + wipe local keys. `--wipe-notes` atomically moves the data dir to a timestamped trash sibling (recoverable). |
|
|
230
|
+
| `status` | One-screen consolidator: running PID + mode + launched-by, editors, storage, add-ons, recent activity, hijack warning. Nudges toward `service --always` when running ad-hoc. |
|
|
231
|
+
| `whoami` | Print the active `KRIMTO_IDENTITY` and every place it's set (each editor's MCP config + the service unit env). Flags mismatches. |
|
|
232
|
+
| `verify-connection` / `where` / `storage` / `usage` | Back-compat aliases — preserve their original stdout, point at `status` on stderr. |
|
|
176
233
|
|
|
177
|
-
**
|
|
234
|
+
**Configure (after first run)** — every command accepts flags so agents can drive them non-interactively.
|
|
178
235
|
|
|
179
236
|
| Command | What it does |
|
|
180
237
|
|---|---|
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
238
|
+
| `editors --add <name> / --remove <name> / --set <list>` | Wire / unwire editors (Cursor / Claude Code / Codex / Gemini). `--list` prints current connections. No-flag form prompts interactively when a TTY is available. |
|
|
239
|
+
| `service --as-needed / --always / --manual` | Switch run mode. Flag form skips the prompt. `service stop` / `service start` are aliases for `stop` / `start`. |
|
|
240
|
+
| `search --keyword / --openai --api-key sk-...` | Switch search provider. The OpenAI path verifies the key before persisting. |
|
|
241
|
+
| `remote --show / --set <url> / --remove` | Manage the git remote (also reachable as `setup-remote <url>`). |
|
|
242
|
+
| `folder --to <path> [--yes]` | Guided move of the data dir. Stops service, atomic rename (cross-fs fallback), reinstalls service with new env, prints `export KRIMTO_DATA=` hint. |
|
|
243
|
+
| `set identity <email>` | Change `KRIMTO_IDENTITY` everywhere atomically (editor MCP configs + service env). Preserves other env keys. |
|
|
185
244
|
|
|
186
|
-
**
|
|
245
|
+
**Team**
|
|
187
246
|
|
|
188
247
|
| Command | What it does |
|
|
189
248
|
|---|---|
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
249
|
+
| `team init` | Admin-side wizard: admin email, team slug, optional git remote, initial teammates. Prints admin key + per-teammate keys + a DM template. |
|
|
250
|
+
| `team disband [--yes]` | Per-machine step-back to solo mode. Notes / `members.yaml` / git history untouched. |
|
|
251
|
+
| `join --server <url> --key <key>` | Teammate-side: detects editors, writes HTTP MCP config with the bearer header + the standing rule. |
|
|
192
252
|
|
|
193
|
-
**
|
|
253
|
+
**Advanced / scripting**
|
|
194
254
|
|
|
195
255
|
| Command | What it does |
|
|
196
256
|
|---|---|
|
|
197
|
-
|
|
|
198
|
-
|
|
|
257
|
+
| `serve` | Start the HTTP server in the foreground (port 8080) + browser `/ui` dashboard. |
|
|
258
|
+
| `setup-remote <url>` | One-shot git remote setup (verifies the initial push). |
|
|
259
|
+
| `setup-embeddings` | Send a real test embedding to verify a `KRIMTO_EMBED_*` config. |
|
|
260
|
+
| `reindex` | Rebuild `index.db` from markdown (fixes orphans). |
|
|
261
|
+
| `delete <id>` | Alias for `rm`. |
|
|
262
|
+
| (no args) | Start the stdio MCP server (default; for MCP clients to launch). |
|
|
263
|
+
| `--help`, `-h` | Show the full CLI surface — includes a `For AI agents (no TTY)` block at the top with copy-paste flag examples. |
|
|
199
264
|
|
|
200
265
|
The stdio entrypoint enforces a **single-writer lock** on the data dir (`.krimto/lock.json`) — two
|
|
201
266
|
Krimto processes can no longer race on the same `~/.krimto`. A second `serve`/stdio launch is
|
package/bin/krimto.mjs
CHANGED
|
@@ -6,6 +6,24 @@
|
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { tsImport } from "tsx/esm/api";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* v0.2.34 — collect every value passed via a repeating flag. Used by `editors --add cursor
|
|
11
|
+
* --add codex` and similar. Accepts both `--flag value` and `--flag=value` forms; ignores
|
|
12
|
+
* the flag itself.
|
|
13
|
+
*/
|
|
14
|
+
function collectFlagValues(flags, name) {
|
|
15
|
+
const values = [];
|
|
16
|
+
for (let i = 0; i < flags.length; i++) {
|
|
17
|
+
const f = flags[i];
|
|
18
|
+
if (f === name) {
|
|
19
|
+
if (typeof flags[i + 1] === "string") values.push(flags[i + 1]);
|
|
20
|
+
} else if (typeof f === "string" && f.startsWith(`${name}=`)) {
|
|
21
|
+
values.push(f.slice(name.length + 1));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return values;
|
|
25
|
+
}
|
|
26
|
+
|
|
9
27
|
try {
|
|
10
28
|
// Two-word command support (v0.2.17.1): `team init`, `team disband`. Collapse argv[2]+argv[3]
|
|
11
29
|
// into one cmd string when argv[2] is one of the namespaced verbs.
|
|
@@ -363,16 +381,84 @@ try {
|
|
|
363
381
|
process.stderr.write("\n→ `krimto verify-connection` is now part of `krimto status` (one command, four answers).\n");
|
|
364
382
|
if (result.status === "none") process.exitCode = 1;
|
|
365
383
|
} else if (cmd === "editors") {
|
|
366
|
-
// `krimto editors` —
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
384
|
+
// `krimto editors` — Phase B shortcut. v0.2.34 added flag forms for AI-agent + CI use:
|
|
385
|
+
// --add <name> Connect one editor (merge with current set). Repeatable.
|
|
386
|
+
// --remove <name> Disconnect one editor (merge with current set). Repeatable.
|
|
387
|
+
// --set <list> Replace the entire connected set (comma-separated).
|
|
388
|
+
// --list Print current connected editors, one per line.
|
|
389
|
+
// No flags + TTY → interactive checkbox wizard (unchanged).
|
|
390
|
+
// No flags + no TTY → the new assertInteractiveOrUsage guard prints flag usage and exits 2.
|
|
391
|
+
const flags = process.argv.slice(3);
|
|
392
|
+
if (flags.includes("--list")) {
|
|
393
|
+
const { listConnectedEditors } = await tsImport("../src/cli/editors.ts", import.meta.url);
|
|
394
|
+
const connected = await listConnectedEditors();
|
|
395
|
+
for (const e of connected) process.stdout.write(`${e}\n`);
|
|
396
|
+
} else {
|
|
397
|
+
const adds = collectFlagValues(flags, "--add");
|
|
398
|
+
const removes = collectFlagValues(flags, "--remove");
|
|
399
|
+
const setIdx = flags.indexOf("--set");
|
|
400
|
+
const setValue = setIdx >= 0 ? flags[setIdx + 1] : undefined;
|
|
401
|
+
const yes = flags.includes("--yes");
|
|
402
|
+
const { runEditors, parseEditorList, listConnectedEditors } = await tsImport(
|
|
403
|
+
"../src/cli/editors.ts",
|
|
404
|
+
import.meta.url,
|
|
405
|
+
);
|
|
406
|
+
if (adds.length > 0 || removes.length > 0 || setValue !== undefined) {
|
|
407
|
+
// Programmatic path — compute the target set and call applyEditors directly through
|
|
408
|
+
// the wrapper. `editors` option short-circuits the prompt.
|
|
409
|
+
let target;
|
|
410
|
+
try {
|
|
411
|
+
if (setValue !== undefined) {
|
|
412
|
+
target = parseEditorList([setValue]);
|
|
413
|
+
} else {
|
|
414
|
+
const current = await listConnectedEditors();
|
|
415
|
+
const toAdd = parseEditorList(adds);
|
|
416
|
+
const toRemove = new Set(parseEditorList(removes));
|
|
417
|
+
target = [...current];
|
|
418
|
+
for (const a of toAdd) if (!target.includes(a)) target.push(a);
|
|
419
|
+
target = target.filter((e) => !toRemove.has(e));
|
|
420
|
+
}
|
|
421
|
+
} catch (err) {
|
|
422
|
+
process.stderr.write(`krimto editors: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
423
|
+
process.exit(2);
|
|
424
|
+
}
|
|
425
|
+
const result = await runEditors({ editors: target, ...(yes ? { yes: true } : {}) });
|
|
426
|
+
if (result === null) process.exitCode = 1;
|
|
427
|
+
} else {
|
|
428
|
+
// No flags — TTY user gets the interactive checkbox; agents get the guard's usage.
|
|
429
|
+
const result = await runEditors();
|
|
430
|
+
if (result === null) process.exitCode = 1;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
370
433
|
} else if (cmd === "search") {
|
|
371
434
|
// `krimto search` — change the search provider (Keyword vs OpenAI) without re-running the
|
|
372
|
-
// whole setup wizard (Phase B).
|
|
435
|
+
// whole setup wizard (Phase B). v0.2.34 added flag forms for agent / CI use:
|
|
436
|
+
// --keyword Switch to keyword search (default, no API key).
|
|
437
|
+
// --openai --api-key sk-... Switch to OpenAI semantic search (key verified first).
|
|
438
|
+
// No flags + TTY → interactive select; no flags + no TTY → guard prints usage + exit 2.
|
|
439
|
+
const flags = process.argv.slice(3);
|
|
440
|
+
const keyword = flags.includes("--keyword");
|
|
441
|
+
const openai = flags.includes("--openai");
|
|
442
|
+
const apiKeyIdx = flags.indexOf("--api-key");
|
|
443
|
+
const keyIdx = flags.indexOf("--key");
|
|
444
|
+
const apiKey =
|
|
445
|
+
apiKeyIdx >= 0 ? flags[apiKeyIdx + 1] : keyIdx >= 0 ? flags[keyIdx + 1] : undefined;
|
|
373
446
|
const { runSearchSettings } = await tsImport("../src/cli/searchSettings.ts", import.meta.url);
|
|
374
|
-
|
|
375
|
-
|
|
447
|
+
if (keyword) {
|
|
448
|
+
const result = await runSearchSettings({ provider: "keyword" });
|
|
449
|
+
if (result === null) process.exitCode = 1;
|
|
450
|
+
} else if (openai) {
|
|
451
|
+
if (!apiKey) {
|
|
452
|
+
process.stderr.write("krimto search --openai requires --api-key <sk-...>\n");
|
|
453
|
+
process.exit(2);
|
|
454
|
+
}
|
|
455
|
+
const result = await runSearchSettings({ provider: "openai", apiKey });
|
|
456
|
+
if (result === null) process.exitCode = 1;
|
|
457
|
+
} else {
|
|
458
|
+
// No flags — TTY user gets the interactive select; agents get the guard's usage.
|
|
459
|
+
const result = await runSearchSettings();
|
|
460
|
+
if (result === null) process.exitCode = 1;
|
|
461
|
+
}
|
|
376
462
|
} else if (cmd === "service") {
|
|
377
463
|
// `krimto service` — change run mode (as-needed / always-running / manual). Installs or
|
|
378
464
|
// uninstalls the platform service to match (Phase B). v0.2.32: accepts `--as-needed`,
|
package/package.json
CHANGED
package/src/cli/editors.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
writeMcpConfig,
|
|
25
25
|
type WriteAction,
|
|
26
26
|
} from "./mcpConfig";
|
|
27
|
-
import { defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
27
|
+
import { assertInteractiveOrUsage, defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
28
28
|
|
|
29
29
|
const EDITOR_LABEL: Record<EditorKind, string> = {
|
|
30
30
|
cursor: "Cursor",
|
|
@@ -89,6 +89,13 @@ export async function applyEditors(
|
|
|
89
89
|
|
|
90
90
|
export async function runEditors(opts: EditorsOptions = {}): Promise<EditorsResult | null> {
|
|
91
91
|
const io = opts.io ?? defaultIO;
|
|
92
|
+
// v0.2.34 — when no editor list was supplied programmatically, we'd open the checkbox
|
|
93
|
+
// prompt. Without a TTY (AI-agent Bash, CI) that prompt would hang then crash with the
|
|
94
|
+
// cryptic "unsettled top-level await" warning. Detect and surface the right flags
|
|
95
|
+
// instead, so agents get a clean exit + actionable usage.
|
|
96
|
+
if (!opts.editors) {
|
|
97
|
+
assertInteractiveOrUsage(EDITORS_USAGE);
|
|
98
|
+
}
|
|
92
99
|
try {
|
|
93
100
|
const cwd = opts.cwd ?? process.cwd();
|
|
94
101
|
const envs = await detectEditorEnvironments(cwd, opts.homeDir);
|
|
@@ -114,6 +121,62 @@ export async function runEditors(opts: EditorsOptions = {}): Promise<EditorsResu
|
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
|
|
124
|
+
/** Non-interactive usage shown by the TTY guard when an agent runs `krimto editors` cold. */
|
|
125
|
+
const EDITORS_USAGE =
|
|
126
|
+
"For non-interactive use (AI agents / CI):\n" +
|
|
127
|
+
" krimto editors --add cursor [--yes] Connect one editor (repeat or comma-list ok)\n" +
|
|
128
|
+
" krimto editors --remove cursor [--yes] Disconnect one editor\n" +
|
|
129
|
+
" krimto editors --set cursor,claude-code [--yes] Replace the full connected set\n" +
|
|
130
|
+
" krimto editors --list Print current connections (one per line)";
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* v0.2.34 — parse a comma-separated / repeated CLI value into a deduped EditorKind list.
|
|
134
|
+
* Accepts the canonical slugs plus common variants. Throws (with a clear message) on
|
|
135
|
+
* unknown names so an agent passing a typo learns immediately instead of silently no-op'ing.
|
|
136
|
+
*/
|
|
137
|
+
export function parseEditorList(values: string[]): EditorKind[] {
|
|
138
|
+
const aliases: Record<string, EditorKind> = {
|
|
139
|
+
cursor: "cursor",
|
|
140
|
+
"claude-code": "claude-code",
|
|
141
|
+
claudecode: "claude-code",
|
|
142
|
+
claude_code: "claude-code",
|
|
143
|
+
claude: "claude-code",
|
|
144
|
+
codex: "codex",
|
|
145
|
+
gemini: "gemini-cli",
|
|
146
|
+
"gemini-cli": "gemini-cli",
|
|
147
|
+
geminicli: "gemini-cli",
|
|
148
|
+
};
|
|
149
|
+
const out: EditorKind[] = [];
|
|
150
|
+
const seen = new Set<EditorKind>();
|
|
151
|
+
for (const raw of values) {
|
|
152
|
+
for (const part of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
153
|
+
const key = part.toLowerCase();
|
|
154
|
+
const kind = aliases[key];
|
|
155
|
+
if (!kind) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Unknown editor "${part}". Expected one of: cursor, claude-code, codex, gemini-cli.`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
if (!seen.has(kind)) {
|
|
161
|
+
out.push(kind);
|
|
162
|
+
seen.add(kind);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* v0.2.34 — programmatic helpers the bin uses to compute the target editor set without
|
|
171
|
+
* spawning the checkbox prompt. `--add` / `--remove` are merge ops over the current
|
|
172
|
+
* snapshot; `--set` replaces the list outright.
|
|
173
|
+
*/
|
|
174
|
+
export async function listConnectedEditors(opts: { cwd?: string; homeDir?: string } = {}): Promise<EditorKind[]> {
|
|
175
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
176
|
+
const snapshot = await detectExistingSetup(cwd, opts.homeDir);
|
|
177
|
+
return snapshot.registeredEditors;
|
|
178
|
+
}
|
|
179
|
+
|
|
117
180
|
async function askEditorsList(
|
|
118
181
|
envs: EditorEnvironment[],
|
|
119
182
|
current: EditorKind[],
|
package/src/cli/folderCmd.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
type InstallResult,
|
|
28
28
|
} from "./service";
|
|
29
29
|
import { defaultIdentity } from "./init";
|
|
30
|
-
import { defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
30
|
+
import { assertInteractiveOrUsage, defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
31
31
|
|
|
32
32
|
export interface FolderCmdOptions {
|
|
33
33
|
io?: WizardIO;
|
|
@@ -56,6 +56,12 @@ export async function runFolderCmd(opts: FolderCmdOptions): Promise<FolderCmdRes
|
|
|
56
56
|
const io = opts.io ?? defaultIO;
|
|
57
57
|
const fromDir = path.resolve(opts.from);
|
|
58
58
|
|
|
59
|
+
// v0.2.34 — when --to was NOT supplied we'd open an input prompt for the destination.
|
|
60
|
+
// Without a TTY (AI agent / CI) that hangs. Surface the flag form.
|
|
61
|
+
if (!opts.to) {
|
|
62
|
+
assertInteractiveOrUsage(FOLDER_USAGE);
|
|
63
|
+
}
|
|
64
|
+
|
|
59
65
|
try {
|
|
60
66
|
io.out("\nKrimto — Move the notes folder\n\n");
|
|
61
67
|
io.out(` Current location: ${fromDir}\n\n`);
|
|
@@ -214,3 +220,8 @@ async function classifyDestination(dir: string): Promise<"absent" | "empty" | "n
|
|
|
214
220
|
return "non-empty";
|
|
215
221
|
}
|
|
216
222
|
}
|
|
223
|
+
|
|
224
|
+
/** v0.2.34 — non-interactive usage shown by the TTY guard. */
|
|
225
|
+
const FOLDER_USAGE =
|
|
226
|
+
"For non-interactive use (AI agents / CI):\n" +
|
|
227
|
+
" krimto folder --to /new/absolute/path --yes Move the notes folder";
|
package/src/cli/help.ts
CHANGED
|
@@ -19,6 +19,20 @@ export function formatHelp(version: string): string {
|
|
|
19
19
|
" AUTO MODE Your agent uses Krimto automatically (no prefix needed).",
|
|
20
20
|
" Setup: `init` walks the whole flow in one command.",
|
|
21
21
|
"",
|
|
22
|
+
"━━ For AI agents (no TTY) ━━",
|
|
23
|
+
"",
|
|
24
|
+
" Setting Krimto up programmatically? Most commands accept flags so you can",
|
|
25
|
+
" skip the interactive prompts. The canonical one-shot for a full install:",
|
|
26
|
+
"",
|
|
27
|
+
" $ krimto init --yes Full setup, all defaults",
|
|
28
|
+
" $ krimto status Machine-readable check (stdout)",
|
|
29
|
+
" $ krimto editors --add cursor [--yes] Wire one editor",
|
|
30
|
+
" $ krimto service --always | --as-needed | --manual Switch run mode",
|
|
31
|
+
" $ krimto search --keyword | --openai --api-key sk-...",
|
|
32
|
+
" $ krimto stop / start / restart Service control (idempotent)",
|
|
33
|
+
"",
|
|
34
|
+
" Interactive commands without flags will exit 2 with usage instead of hanging.",
|
|
35
|
+
"",
|
|
22
36
|
"━━ Usage ━━",
|
|
23
37
|
"",
|
|
24
38
|
" $ npx @krimto-labs/krimto [command]",
|
package/src/cli/mcpConfig.ts
CHANGED
|
@@ -148,25 +148,62 @@ export async function writeMcpConfig(
|
|
|
148
148
|
* matching `mcp remove`, but we don't depend on it).
|
|
149
149
|
*/
|
|
150
150
|
export async function removeMcpConfig(env: EditorEnvironment): Promise<{ removed: boolean }> {
|
|
151
|
-
if (env.mcpWire === null
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
text
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
if (env.mcpWire === null) return { removed: false };
|
|
152
|
+
|
|
153
|
+
if (env.mcpWire.method === "json") {
|
|
154
|
+
let text: string;
|
|
155
|
+
try {
|
|
156
|
+
text = await fs.readFile(env.mcpWire.path, "utf8");
|
|
157
|
+
} catch {
|
|
158
|
+
return { removed: false };
|
|
159
|
+
}
|
|
160
|
+
let parsed: Record<string, unknown>;
|
|
161
|
+
try {
|
|
162
|
+
parsed = JSON.parse(text) as Record<string, unknown>;
|
|
163
|
+
} catch {
|
|
164
|
+
return { removed: false };
|
|
165
|
+
}
|
|
166
|
+
const servers = parsed[env.mcpWire.key] as Record<string, unknown> | undefined;
|
|
167
|
+
if (!servers || !("krimto" in servers)) return { removed: false };
|
|
168
|
+
const { krimto: _krimto, ...rest } = servers; // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
169
|
+
parsed[env.mcpWire.key] = rest;
|
|
170
|
+
await fs.writeFile(env.mcpWire.path, JSON.stringify(parsed, null, 2) + "\n", "utf8");
|
|
171
|
+
return { removed: true };
|
|
157
172
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
173
|
+
|
|
174
|
+
// v0.2.35 — CLI method (Claude Code). The smoke-6 transcript caught reset reporting "No
|
|
175
|
+
// editors were connected" while `claude mcp list` still showed krimto. Root cause: this
|
|
176
|
+
// function used to bail at the top with `method !== "json"`, so the CLI path was never
|
|
177
|
+
// exercised. The reset SWEEP rule (always run cleanup, never trust detection) covers the
|
|
178
|
+
// intent here too — we shell out to `claude mcp remove krimto` across the three scopes
|
|
179
|
+
// Claude Code supports (local / user / project). Each call is idempotent: if krimto
|
|
180
|
+
// isn't registered at a scope, claude prints "MCP server krimto not found" and exits
|
|
181
|
+
// non-zero, which we swallow. We mark removed=true when ANY scope succeeds.
|
|
182
|
+
if (env.mcpWire.method === "cli") {
|
|
183
|
+
return { removed: await removeClaudeMcpAllScopes(env.mcpWire.command) };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { removed: false };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* v0.2.35 — try `claude mcp remove krimto -s <scope>` for each known scope. Returns true
|
|
191
|
+
* when at least one succeeds. The user's krimto entry may live in any scope depending on
|
|
192
|
+
* which directory they were in when they originally ran `claude mcp add`, so we sweep all
|
|
193
|
+
* three. Errors are best-effort (no-op when nothing's registered at that scope).
|
|
194
|
+
*/
|
|
195
|
+
async function removeClaudeMcpAllScopes(claudeBinary: string): Promise<boolean> {
|
|
196
|
+
const scopes = ["local", "user", "project"] as const;
|
|
197
|
+
let anyRemoved = false;
|
|
198
|
+
for (const scope of scopes) {
|
|
199
|
+
try {
|
|
200
|
+
await exec(claudeBinary, ["mcp", "remove", "krimto", "-s", scope]);
|
|
201
|
+
anyRemoved = true;
|
|
202
|
+
} catch {
|
|
203
|
+
// "MCP server krimto not found" at this scope — fine, try the next.
|
|
204
|
+
}
|
|
163
205
|
}
|
|
164
|
-
|
|
165
|
-
if (!servers || !("krimto" in servers)) return { removed: false };
|
|
166
|
-
const { krimto: _krimto, ...rest } = servers; // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
167
|
-
parsed[env.mcpWire.key] = rest;
|
|
168
|
-
await fs.writeFile(env.mcpWire.path, JSON.stringify(parsed, null, 2) + "\n", "utf8");
|
|
169
|
-
return { removed: true };
|
|
206
|
+
return anyRemoved;
|
|
170
207
|
}
|
|
171
208
|
|
|
172
209
|
// --- internal helpers -------------------------------------------------------
|
package/src/cli/promptHelpers.ts
CHANGED
|
@@ -22,3 +22,27 @@ export const defaultIO: WizardIO = {
|
|
|
22
22
|
export function isExitPrompt(e: unknown): boolean {
|
|
23
23
|
return e instanceof Error && e.name === "ExitPromptError";
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* v0.2.34 — guard for the Phase B commands (`editors`, `service`, `search`, `reset`,
|
|
28
|
+
* `remote`, `folder`) when about to spawn an `@inquirer/prompts` UI. If the process has no
|
|
29
|
+
* TTY on stdin (typical for an AI-agent shell tool), the prompt would hang forever, then
|
|
30
|
+
* Node would surface the cryptic "Detected unsettled top-level await" warning and abort.
|
|
31
|
+
*
|
|
32
|
+
* Instead, we detect the no-TTY case here, print a copy-pasteable usage block, and exit
|
|
33
|
+
* 2 cleanly. The TTY case is a no-op — the interactive wizard runs as before. Each caller
|
|
34
|
+
* supplies its own `usage` text naming the flags an agent should use.
|
|
35
|
+
*
|
|
36
|
+
* NOTE: this is only called BEFORE the first prompt would run. When a flag like
|
|
37
|
+
* `--always` / `--add cursor` / `--keyword` IS passed, the caller skips the prompt entirely
|
|
38
|
+
* and never invokes this — the non-interactive path stays open.
|
|
39
|
+
*/
|
|
40
|
+
export function assertInteractiveOrUsage(usage: string): void {
|
|
41
|
+
if (process.stdin.isTTY === true) return;
|
|
42
|
+
process.stderr.write(
|
|
43
|
+
"\nℹ️ No interactive terminal detected — this command needs flags for non-interactive use.\n\n" +
|
|
44
|
+
usage +
|
|
45
|
+
"\n\nSee `krimto --help` for the full list.\n",
|
|
46
|
+
);
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
package/src/cli/remoteCmd.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { promisify } from "node:util";
|
|
|
10
10
|
|
|
11
11
|
import { readDataDirGitInfo } from "../storage/git";
|
|
12
12
|
import { runSetupRemote, type SetupRemoteResult } from "./setupRemote";
|
|
13
|
-
import { defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
13
|
+
import { assertInteractiveOrUsage, defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
14
14
|
|
|
15
15
|
const exec = promisify(execFile);
|
|
16
16
|
|
|
@@ -40,6 +40,12 @@ export async function runRemoteCmd(opts: RemoteCmdOptions): Promise<RemoteCmdRes
|
|
|
40
40
|
const io = opts.io ?? defaultIO;
|
|
41
41
|
const dataDir = opts.dataDir;
|
|
42
42
|
|
|
43
|
+
// v0.2.34 — guard against agents calling `krimto remote` cold. Without an action
|
|
44
|
+
// we open a select prompt; without a TTY that hangs. Surface the flag forms.
|
|
45
|
+
if (!opts.action) {
|
|
46
|
+
assertInteractiveOrUsage(REMOTE_USAGE);
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
try {
|
|
44
50
|
const gitInfo = await readDataDirGitInfo(dataDir);
|
|
45
51
|
const current = gitInfo.remote;
|
|
@@ -137,3 +143,10 @@ export async function runRemoteCmd(opts: RemoteCmdOptions): Promise<RemoteCmdRes
|
|
|
137
143
|
throw e;
|
|
138
144
|
}
|
|
139
145
|
}
|
|
146
|
+
|
|
147
|
+
/** v0.2.34 — non-interactive usage shown by the TTY guard. */
|
|
148
|
+
const REMOTE_USAGE =
|
|
149
|
+
"For non-interactive use (AI agents / CI):\n" +
|
|
150
|
+
" krimto remote --show Print the current remote URL\n" +
|
|
151
|
+
" krimto remote --set git@host:repo.git Wire a remote (verifies the first push)\n" +
|
|
152
|
+
" krimto remote --remove --yes Unwire the remote";
|
package/src/cli/reset.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
type EditorKind,
|
|
25
25
|
} from "./init";
|
|
26
26
|
import { removeMcpConfig } from "./mcpConfig";
|
|
27
|
-
import { defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
27
|
+
import { assertInteractiveOrUsage, defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
28
28
|
import { detectPlatform, uninstallService } from "./service";
|
|
29
29
|
|
|
30
30
|
const EDITOR_LABEL: Record<EditorKind, string> = {
|
|
@@ -152,41 +152,50 @@ export async function applyReset(opts: ResetOptions = {}): Promise<ResetResult>
|
|
|
152
152
|
|
|
153
153
|
export async function runReset(opts: ResetOptions = {}): Promise<ResetResult | null> {
|
|
154
154
|
const io = opts.io ?? defaultIO;
|
|
155
|
+
// v0.2.34 — when --yes was NOT passed we'd open a confirm prompt. Without a TTY (AI
|
|
156
|
+
// agent / CI) the prompt would hang. Surface the flag form instead.
|
|
157
|
+
if (!opts.yes) {
|
|
158
|
+
assertInteractiveOrUsage(RESET_USAGE);
|
|
159
|
+
}
|
|
155
160
|
try {
|
|
156
161
|
const dataDir = opts.dataDir ?? path.join(opts.homeDir ?? "", ".krimto");
|
|
157
162
|
|
|
163
|
+
// v0.2.33 — single-prompt UX. The original flow asked "Proceed with reset?" first
|
|
164
|
+
// (default N), and only AFTER that asked the wipe-notes-specific confirmation. Users
|
|
165
|
+
// who passed `--wipe-notes` were tripped up by the first prompt: they'd typed the flag,
|
|
166
|
+
// hit Enter at "Proceed?", and got "No changes made" without realising the flag they
|
|
167
|
+
// passed had no consent baked in. The fix: when `--wipe-notes` is passed, collapse the
|
|
168
|
+
// two confirmations into ONE that names the worst thing explicitly. Without the flag,
|
|
169
|
+
// the single-prompt flow stays exactly as it was.
|
|
158
170
|
io.out("\nKrimto — Reset machine-level config\n\n");
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
171
|
+
if (opts.wipeNotes) {
|
|
172
|
+
io.out(" ⚠️ --wipe-notes — this will:\n");
|
|
173
|
+
io.out(" • Disconnect Krimto from all editors (MCP config + standing rule)\n");
|
|
174
|
+
io.out(" • Stop and uninstall the background service (if installed)\n");
|
|
175
|
+
io.out(" • Wipe the local API-key store\n");
|
|
176
|
+
io.out(` • MOVE your notes folder (${dataDir}) to a timestamped trash sibling\n`);
|
|
177
|
+
io.out(` ${dataDir}.trash-<ts> stays on disk until you delete it manually.\n\n`);
|
|
178
|
+
io.out(" This will NOT touch:\n");
|
|
179
|
+
io.out(" • The team's git history or members.yaml on the remote\n\n");
|
|
180
|
+
} else {
|
|
181
|
+
io.out(" This will:\n");
|
|
182
|
+
io.out(" • Disconnect Krimto from all editors (MCP config + standing rule)\n");
|
|
183
|
+
io.out(" • Stop and uninstall the background service (if installed)\n");
|
|
184
|
+
io.out(" • Wipe the local API-key store\n\n");
|
|
185
|
+
io.out(" This will NOT touch:\n");
|
|
186
|
+
io.out(` • Your notes folder (${dataDir}) — pass --wipe-notes to also move it\n`);
|
|
187
|
+
io.out(" • The team's git history or members.yaml\n\n");
|
|
188
|
+
}
|
|
166
189
|
|
|
167
|
-
const
|
|
190
|
+
const promptMessage = opts.wipeNotes
|
|
191
|
+
? "Wipe notes folder AND disconnect everything?"
|
|
192
|
+
: "Proceed with reset?";
|
|
193
|
+
const ok = opts.yes ?? (await confirm({ message: promptMessage, default: false }));
|
|
168
194
|
if (!ok) {
|
|
169
195
|
io.out("\nNo changes made.\n");
|
|
170
196
|
return null;
|
|
171
197
|
}
|
|
172
198
|
|
|
173
|
-
// The --wipe-notes path needs its OWN confirmation — the default reset is reversible
|
|
174
|
-
// (just re-run `krimto init`), but wiping notes is data loss.
|
|
175
|
-
if (opts.wipeNotes && !opts.yes) {
|
|
176
|
-
io.out(
|
|
177
|
-
`\n⚠️ --wipe-notes will MOVE ${dataDir} to a timestamped trash sibling.\n` +
|
|
178
|
-
` The notes stay on disk (recoverable) until you delete the trash dir manually.\n`,
|
|
179
|
-
);
|
|
180
|
-
const wipeOk = await confirm({
|
|
181
|
-
message: `Confirm: also move ${dataDir} to ${dataDir}.trash-<ts>?`,
|
|
182
|
-
default: false,
|
|
183
|
-
});
|
|
184
|
-
if (!wipeOk) {
|
|
185
|
-
io.out("\n(Reset proceeding without --wipe-notes — notes preserved.)\n");
|
|
186
|
-
opts = { ...opts, wipeNotes: false };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
199
|
const result = await applyReset(opts);
|
|
191
200
|
printResetResult(result, io);
|
|
192
201
|
return result;
|
|
@@ -272,3 +281,9 @@ function printResetResult(res: ResetResult, io: WizardIO): void {
|
|
|
272
281
|
}
|
|
273
282
|
io.out("\nRestart your editor(s) so they pick up the changes.\n");
|
|
274
283
|
}
|
|
284
|
+
|
|
285
|
+
/** v0.2.34 — non-interactive usage shown by the TTY guard. */
|
|
286
|
+
const RESET_USAGE =
|
|
287
|
+
"For non-interactive use (AI agents / CI):\n" +
|
|
288
|
+
" krimto reset --yes Disconnect everything (notes preserved)\n" +
|
|
289
|
+
" krimto reset --yes --wipe-notes Also MOVE notes to a trash sibling";
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
detectExistingSetup,
|
|
15
15
|
type SearchProvider,
|
|
16
16
|
} from "./init";
|
|
17
|
-
import { defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
17
|
+
import { assertInteractiveOrUsage, defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
18
18
|
import { runSetupEmbeddings } from "./setupEmbeddings";
|
|
19
19
|
|
|
20
20
|
export interface SearchOptions {
|
|
@@ -83,6 +83,11 @@ export async function applySearch(
|
|
|
83
83
|
|
|
84
84
|
export async function runSearchSettings(opts: SearchOptions = {}): Promise<SearchResult | null> {
|
|
85
85
|
const io = opts.io ?? defaultIO;
|
|
86
|
+
// v0.2.34 — when no provider was supplied programmatically we'd open a select prompt.
|
|
87
|
+
// Without a TTY (AI agent / CI) that prompt hangs. Surface the flag form instead.
|
|
88
|
+
if (!opts.provider) {
|
|
89
|
+
assertInteractiveOrUsage(SEARCH_USAGE);
|
|
90
|
+
}
|
|
86
91
|
try {
|
|
87
92
|
const snapshot = await detectExistingSetup(opts.cwd ?? process.cwd(), opts.homeDir);
|
|
88
93
|
io.out("\nKrimto — Search settings\n\n");
|
|
@@ -145,3 +150,10 @@ export async function runSearchSettings(opts: SearchOptions = {}): Promise<Searc
|
|
|
145
150
|
throw e;
|
|
146
151
|
}
|
|
147
152
|
}
|
|
153
|
+
|
|
154
|
+
/** Non-interactive usage shown by the v0.2.34 TTY guard. */
|
|
155
|
+
const SEARCH_USAGE =
|
|
156
|
+
"For non-interactive use (AI agents / CI):\n" +
|
|
157
|
+
" krimto search --keyword Use keyword search (default, free)\n" +
|
|
158
|
+
" krimto search --openai --api-key sk-... Use OpenAI semantic search (key verified)";
|
|
159
|
+
|
package/src/cli/serviceCmd.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type UninstallResult,
|
|
19
19
|
} from "./service";
|
|
20
20
|
import { defaultIdentity, type RunMode } from "./init";
|
|
21
|
-
import { defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
21
|
+
import { assertInteractiveOrUsage, defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
22
22
|
|
|
23
23
|
export interface ServiceCmdOptions {
|
|
24
24
|
io?: WizardIO;
|
|
@@ -75,6 +75,12 @@ export async function applyService(
|
|
|
75
75
|
|
|
76
76
|
export async function runServiceCmd(opts: ServiceCmdOptions = {}): Promise<ServiceCmdResult | null> {
|
|
77
77
|
const io = opts.io ?? defaultIO;
|
|
78
|
+
// v0.2.34 — guard against agents calling `krimto service` cold. Without a mode flag we
|
|
79
|
+
// would spawn a select prompt; without a TTY that hangs and then crashes with "unsettled
|
|
80
|
+
// top-level await". Surface the flag forms instead.
|
|
81
|
+
if (!opts.mode) {
|
|
82
|
+
assertInteractiveOrUsage(SERVICE_USAGE);
|
|
83
|
+
}
|
|
78
84
|
try {
|
|
79
85
|
const platform = detectPlatform();
|
|
80
86
|
const current = await isServiceInstalled(platform, opts.homeDir);
|
|
@@ -144,3 +150,10 @@ function runModeLabel(m: RunMode): string {
|
|
|
144
150
|
return "Manual (`krimto serve`)";
|
|
145
151
|
}
|
|
146
152
|
}
|
|
153
|
+
|
|
154
|
+
/** v0.2.34 — non-interactive usage shown by the TTY guard. */
|
|
155
|
+
const SERVICE_USAGE =
|
|
156
|
+
"For non-interactive use (AI agents / CI):\n" +
|
|
157
|
+
" krimto service --as-needed Editor launches Krimto on demand (stdio)\n" +
|
|
158
|
+
" krimto service --always Run continuously as a background service\n" +
|
|
159
|
+
" krimto service --manual Don't auto-start; user runs `krimto serve`";
|
package/src/cli/status.ts
CHANGED
|
@@ -134,9 +134,20 @@ function headerLine(
|
|
|
134
134
|
): string {
|
|
135
135
|
if (status === "ok") {
|
|
136
136
|
if (lock?.alive) {
|
|
137
|
-
|
|
137
|
+
// v0.2.34 — when the running process is ad-hoc (a foreground `krimto serve` or an
|
|
138
|
+
// editor's stdio launcher), nudge the user toward `krimto service --always` so the
|
|
139
|
+
// service survives reboots without them having to dig for the verb.
|
|
140
|
+
const adHocHint =
|
|
141
|
+
(effectiveLaunchedBy ?? lock.info.launchedBy) === "service"
|
|
142
|
+
? ""
|
|
143
|
+
: " → To run continuously across reboots: krimto service --always\n";
|
|
144
|
+
return `\n✅ Krimto is working · v${KRIMTO_VERSION}\n PID ${lock.info.pid} (${lock.info.mode}, ${effectiveLaunchedBy ?? lock.info.launchedBy}), started ${humanAgo(lock.info.started, now)}\n${adHocHint}`;
|
|
138
145
|
}
|
|
139
|
-
return
|
|
146
|
+
return (
|
|
147
|
+
`\n✅ Krimto is configured · v${KRIMTO_VERSION}\n` +
|
|
148
|
+
` No active server right now — it will be launched on demand by your editor.\n` +
|
|
149
|
+
` → To run continuously across reboots: krimto service --always\n`
|
|
150
|
+
);
|
|
140
151
|
}
|
|
141
152
|
if (status === "warning") {
|
|
142
153
|
return `\n⚠️ Krimto needs attention · v${KRIMTO_VERSION}\n`;
|
package/src/cli/wizard.ts
CHANGED
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
} from "./init";
|
|
26
26
|
import { runSetupEmbeddings } from "./setupEmbeddings";
|
|
27
27
|
import { KRIMTO_VERSION } from "../server/index";
|
|
28
|
+
import { inspectRuntime, type RuntimeState } from "./inspectRuntime";
|
|
29
|
+
import { applyService } from "./serviceCmd";
|
|
30
|
+
import * as os from "node:os";
|
|
31
|
+
import * as path from "node:path";
|
|
28
32
|
|
|
29
33
|
const EDITOR_LABEL: Record<EditorKind, string> = {
|
|
30
34
|
cursor: "Cursor",
|
|
@@ -68,46 +72,88 @@ export async function runInitWizard(
|
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* §06 — reconfigure menu shown when `krimto init` is re-run on a configured machine.
|
|
77
|
+
*
|
|
78
|
+
* v0.2.35: drops the ambiguous "Krimto is already set up on this machine" header (users
|
|
79
|
+
* read "set up" as "running"). The new header is neutral — "Krimto on this machine:" —
|
|
80
|
+
* and adds a real **Service:** line driven by `inspectRuntime` (config-snapshot ≠ runtime).
|
|
81
|
+
* That way the user always knows whether a process is actually serving right now, not just
|
|
82
|
+
* whether a config exists on disk. A fifth menu choice — "Start it running continuously"
|
|
83
|
+
* — appears only when no service is currently loaded, so users who want a daemon find the
|
|
84
|
+
* button without leaving the menu.
|
|
85
|
+
*/
|
|
72
86
|
async function runReconfigureMenu(
|
|
73
87
|
cwd: string,
|
|
74
88
|
snapshot: SetupSnapshot,
|
|
75
89
|
opts: RunWizardOptions,
|
|
76
90
|
io: WizardIO,
|
|
77
91
|
): Promise<ApplyResult | null> {
|
|
92
|
+
// Match applyWizardAnswers's pattern: derive dataDir from homeDir when not explicitly
|
|
93
|
+
// passed. Tests rely on this so the runtime probe hits a test temp dir, not the user's
|
|
94
|
+
// real ~/.krimto.
|
|
95
|
+
const homeDir = opts.homeDir ?? os.homedir();
|
|
96
|
+
const dataDir = opts.dataDir ?? path.join(homeDir, ".krimto");
|
|
97
|
+
const runtime = await inspectRuntime(dataDir, { cwd, homeDir });
|
|
98
|
+
|
|
78
99
|
io.out("\n");
|
|
79
|
-
io.out("Krimto
|
|
80
|
-
io.out(
|
|
81
|
-
|
|
100
|
+
io.out("Krimto on this machine:\n");
|
|
101
|
+
io.out(
|
|
102
|
+
` Editors: ${
|
|
103
|
+
snapshot.registeredEditors.map((e) => EDITOR_LABEL[e]).join(", ") || "(none)"
|
|
104
|
+
}\n`,
|
|
105
|
+
);
|
|
106
|
+
io.out(` Service: ${formatServiceLine(runtime)}\n`);
|
|
82
107
|
io.out(` Search: ${searchLabel(snapshot.searchProvider)}\n`);
|
|
83
108
|
io.out("\n");
|
|
84
109
|
|
|
110
|
+
// The "Start it running continuously" choice only makes sense when no daemon is alive.
|
|
111
|
+
// (When already running as a service, there's nothing to start; when running ad-hoc, the
|
|
112
|
+
// user is better served by `krimto service --always` AFTER stopping the ad-hoc one, which
|
|
113
|
+
// is more state than this menu wants to manage.)
|
|
114
|
+
const canStartService =
|
|
115
|
+
!runtime.service.loaded && !(runtime.lock?.alive ?? false);
|
|
116
|
+
|
|
117
|
+
const choices: Array<{
|
|
118
|
+
value: "refresh" | "reconfigure" | "status" | "start-service" | "quit";
|
|
119
|
+
name: string;
|
|
120
|
+
description: string;
|
|
121
|
+
}> = [
|
|
122
|
+
{
|
|
123
|
+
value: "refresh",
|
|
124
|
+
name: "Just refresh the standing rule in this project",
|
|
125
|
+
description:
|
|
126
|
+
"Re-applies the 'always use Krimto' rule to CLAUDE.md / .cursor/rules/ here.\nUse this when you just cloned a new repo.",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
value: "reconfigure",
|
|
130
|
+
name: "Change settings (reconfigure)",
|
|
131
|
+
description:
|
|
132
|
+
"Re-runs the setup wizard with your current answers pre-filled.\nSkip anything you don't want to change.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
value: "status",
|
|
136
|
+
name: "View status",
|
|
137
|
+
description: "Show what's working, what's configured, recent activity.",
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
if (canStartService) {
|
|
141
|
+
choices.push({
|
|
142
|
+
value: "start-service",
|
|
143
|
+
name: "Start it running continuously (install as a background service)",
|
|
144
|
+
description:
|
|
145
|
+
"Runs Krimto as a launchd / systemd-user / schtasks service so it stays up\nacross reboots. Equivalent to `krimto service --always`.",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
choices.push({
|
|
149
|
+
value: "quit",
|
|
150
|
+
name: "Quit",
|
|
151
|
+
description: "Leave Krimto as it is.",
|
|
152
|
+
});
|
|
153
|
+
|
|
85
154
|
const choice = await select({
|
|
86
155
|
message: "What would you like to do?",
|
|
87
|
-
choices
|
|
88
|
-
{
|
|
89
|
-
value: "refresh" as const,
|
|
90
|
-
name: "Just refresh the standing rule in this project",
|
|
91
|
-
description:
|
|
92
|
-
"Re-applies the 'always use Krimto' rule to CLAUDE.md / .cursor/rules/ here.\nUse this when you just cloned a new repo.",
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
value: "reconfigure" as const,
|
|
96
|
-
name: "Change settings (reconfigure)",
|
|
97
|
-
description:
|
|
98
|
-
"Re-runs the setup wizard with your current answers pre-filled.\nSkip anything you don't want to change.",
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
value: "status" as const,
|
|
102
|
-
name: "View status",
|
|
103
|
-
description: "Show what's working, what's configured, recent activity.",
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
value: "quit" as const,
|
|
107
|
-
name: "Quit",
|
|
108
|
-
description: "Leave Krimto as it is.",
|
|
109
|
-
},
|
|
110
|
-
],
|
|
156
|
+
choices,
|
|
111
157
|
});
|
|
112
158
|
|
|
113
159
|
if (choice === "quit") {
|
|
@@ -139,10 +185,70 @@ async function runReconfigureMenu(
|
|
|
139
185
|
return null;
|
|
140
186
|
}
|
|
141
187
|
|
|
188
|
+
if (choice === "start-service") {
|
|
189
|
+
// v0.2.35 — install + load the always-running service from inside the menu so users
|
|
190
|
+
// who want a daemon don't have to drop back to the shell and remember the flag name.
|
|
191
|
+
// Delegates to applyService which handles the platform-specific install + port probe.
|
|
192
|
+
io.out("\nInstalling background service...\n");
|
|
193
|
+
const installResult = await applyService("always-running", {
|
|
194
|
+
...(opts.dataDir ? { dataDir: opts.dataDir } : {}),
|
|
195
|
+
...(opts.homeDir ? { homeDir: opts.homeDir } : {}),
|
|
196
|
+
...(opts.dryRun ? { dryRun: true } : {}),
|
|
197
|
+
});
|
|
198
|
+
if (installResult.install?.activated) {
|
|
199
|
+
const portMsg =
|
|
200
|
+
installResult.install.portReady === false
|
|
201
|
+
? " ⚠ Service installed but the HTTP port didn't come up within 10s. Check\n /tmp/com.krimto.server.err.log.\n"
|
|
202
|
+
: " ✓ Service started, port accepting connections.\n";
|
|
203
|
+
io.out("\n✅ Background service is now running.\n" + portMsg + "\n");
|
|
204
|
+
} else {
|
|
205
|
+
io.out("\n(Service was configured but not activated — likely a dry-run.)\n");
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
142
210
|
// "reconfigure" — re-run the five questions with snapshot as defaults
|
|
143
211
|
return runFreshWizard(cwd, opts, io, snapshot);
|
|
144
212
|
}
|
|
145
213
|
|
|
214
|
+
/**
|
|
215
|
+
* v0.2.35 — turn the reconciled runtime view into one human line for the reconfigure menu.
|
|
216
|
+
* Four cases (in priority order):
|
|
217
|
+
* 1. A live process holds the lock AND launched-by="service" → "Running as background service (PID …, started Nm ago)"
|
|
218
|
+
* 2. A live process holds the lock, launched-by="ad-hoc" → "Running ad-hoc (PID … — started by `krimto serve` or an editor)"
|
|
219
|
+
* 3. Service unit on disk but not loaded → "⚠ Installed but not running — `krimto start` to load it"
|
|
220
|
+
* 4. Nothing running, no unit on disk → "Not running (your editor launches it on demand via stdio)"
|
|
221
|
+
*
|
|
222
|
+
* This is the line that the smoke-6 user wanted: "is Krimto serving RIGHT NOW?" — driven
|
|
223
|
+
* by lock + launchctl/systemctl reality, not by the static config snapshot.
|
|
224
|
+
*/
|
|
225
|
+
function formatServiceLine(runtime: RuntimeState): string {
|
|
226
|
+
const lock = runtime.lock;
|
|
227
|
+
if (lock?.alive) {
|
|
228
|
+
const ago = humanAgoForLock(lock.started);
|
|
229
|
+
if (runtime.effectiveLaunchedBy === "service") {
|
|
230
|
+
return `Running as background service (PID ${lock.pid}, started ${ago})`;
|
|
231
|
+
}
|
|
232
|
+
return `Running ad-hoc (PID ${lock.pid} — started ${ago} by \`krimto serve\` or an editor)`;
|
|
233
|
+
}
|
|
234
|
+
if (runtime.service.installed) {
|
|
235
|
+
return "⚠ Installed but not running — `krimto start` to load it";
|
|
236
|
+
}
|
|
237
|
+
return "Not running (your editor launches it on demand via stdio)";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function humanAgoForLock(iso: string): string {
|
|
241
|
+
const t = Date.parse(iso);
|
|
242
|
+
if (Number.isNaN(t)) return iso;
|
|
243
|
+
const secs = Math.max(0, Math.round((Date.now() - t) / 1000));
|
|
244
|
+
if (secs < 60) return `${secs}s ago`;
|
|
245
|
+
const mins = Math.round(secs / 60);
|
|
246
|
+
if (mins < 60) return `${mins}m ago`;
|
|
247
|
+
const hrs = Math.round(mins / 60);
|
|
248
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
249
|
+
return `${Math.round(hrs / 24)}d ago`;
|
|
250
|
+
}
|
|
251
|
+
|
|
146
252
|
/** §03 — the fresh five-question flow. `snapshot` (when given) seeds the defaults. */
|
|
147
253
|
async function runFreshWizard(
|
|
148
254
|
cwd: string,
|
package/src/server/index.ts
CHANGED
|
@@ -49,7 +49,7 @@ import { type Requester } from "../access/scope";
|
|
|
49
49
|
|
|
50
50
|
export type RequesterResolver = (extra: { authInfo?: AuthInfo }) => Requester;
|
|
51
51
|
|
|
52
|
-
export const KRIMTO_VERSION = "0.2.
|
|
52
|
+
export const KRIMTO_VERSION = "0.2.35";
|
|
53
53
|
|
|
54
54
|
export function resolveDataDir(): string {
|
|
55
55
|
return process.env.KRIMTO_DATA ?? path.join(homedir(), ".krimto");
|