@krimto-labs/krimto 0.2.27 → 0.2.34
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/bin/krimto.mjs +269 -22
- package/package.json +1 -1
- package/src/access/scope.ts +8 -0
- package/src/agentRule.ts +30 -4
- package/src/cli/editors.ts +66 -2
- package/src/cli/folderCmd.ts +227 -0
- package/src/cli/help.ts +78 -31
- package/src/cli/init.ts +27 -18
- package/src/cli/join.ts +2 -1
- package/src/cli/promptHelpers.ts +24 -0
- package/src/cli/remoteCmd.ts +152 -0
- package/src/cli/reset.ts +41 -26
- package/src/cli/searchSettings.ts +13 -1
- package/src/cli/service.ts +54 -0
- package/src/cli/serviceCmd.ts +14 -1
- package/src/cli/status.ts +20 -37
- package/src/cli/stopCmd.ts +240 -0
- package/src/cli/wizard.ts +84 -3
- package/src/index/factIndex.ts +3 -3
- package/src/server/http.ts +15 -3
- package/src/server/index.ts +1 -1
- package/src/server/tools.ts +5 -1
- package/src/server/userAgent.ts +22 -0
- package/src/storage/git.ts +40 -0
- package/src/web/html.ts +139 -12
- package/src/web/router.ts +53 -3
- package/src/web/views.ts +156 -43
package/bin/krimto.mjs
CHANGED
|
@@ -6,16 +6,40 @@
|
|
|
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.
|
|
12
30
|
const rawCmd = process.argv[2];
|
|
31
|
+
const sub = process.argv[3];
|
|
32
|
+
// v0.2.32 — `service stop` / `service start` are explicit, scriptable subverbs (no prompt).
|
|
33
|
+
// `service` alone still launches the interactive wizard. Mirrors the team/set two-word shape.
|
|
34
|
+
const serviceSubverbs = ["stop", "start"];
|
|
13
35
|
const cmd =
|
|
14
|
-
rawCmd === "team" && typeof
|
|
15
|
-
? `team ${
|
|
16
|
-
: rawCmd === "set" && typeof
|
|
17
|
-
? `set ${
|
|
18
|
-
: rawCmd
|
|
36
|
+
rawCmd === "team" && typeof sub === "string"
|
|
37
|
+
? `team ${sub}`
|
|
38
|
+
: rawCmd === "set" && typeof sub === "string"
|
|
39
|
+
? `set ${sub}`
|
|
40
|
+
: rawCmd === "service" && typeof sub === "string" && serviceSubverbs.includes(sub)
|
|
41
|
+
? `service ${sub}`
|
|
42
|
+
: rawCmd;
|
|
19
43
|
|
|
20
44
|
// Guard: `krimto team` alone (or with an unknown subverb) shouldn't fall through to the stdio
|
|
21
45
|
// MCP server. Print usage and exit instead.
|
|
@@ -138,8 +162,13 @@ try {
|
|
|
138
162
|
" 3. Verify it landed: $ npx @krimto-labs/krimto verify-connection\n" +
|
|
139
163
|
mcpWarning +
|
|
140
164
|
"\n" +
|
|
141
|
-
|
|
142
|
-
"
|
|
165
|
+
// v0.2.32 — three honest off-ramps, three blast radii. The old single line said
|
|
166
|
+
// "To undo: krimto uninit" which only stripped this project's rule files; users
|
|
167
|
+
// were stranded thinking they had a working stop button when the service kept
|
|
168
|
+
// running on their machine.
|
|
169
|
+
"To stop the service: $ npx @krimto-labs/krimto stop\n" +
|
|
170
|
+
"To undo this project only: $ npx @krimto-labs/krimto uninit\n" +
|
|
171
|
+
"To disconnect everything: $ npx @krimto-labs/krimto reset (notes preserved)\n\n",
|
|
143
172
|
);
|
|
144
173
|
}
|
|
145
174
|
} else if (yes) {
|
|
@@ -211,6 +240,16 @@ try {
|
|
|
211
240
|
} else if (cmd === "uninit") {
|
|
212
241
|
// `krimto uninit` — remove the always-use-Krimto rule from this project's rules files,
|
|
213
242
|
// flipping the project back from AUTO MODE to DEFAULT MODE.
|
|
243
|
+
//
|
|
244
|
+
// v0.2.32: the smoke-6 audit caught users assuming `uninit` was the full undo button —
|
|
245
|
+
// it wasn't (the background service kept running). After rule removal, if a service
|
|
246
|
+
// and/or a live krimto process is detected on this machine, we now ask whether the
|
|
247
|
+
// user also wants to stop it. Default is No (the service is machine-wide; other
|
|
248
|
+
// projects may use it). The flag `--also-stop` skips the prompt; `--keep-running`
|
|
249
|
+
// explicitly suppresses it (for scripted runs).
|
|
250
|
+
const flags = process.argv.slice(3);
|
|
251
|
+
const alsoStopFlag = flags.includes("--also-stop");
|
|
252
|
+
const keepRunningFlag = flags.includes("--keep-running");
|
|
214
253
|
const { runUninit } = await tsImport("../src/cli/uninit.ts", import.meta.url);
|
|
215
254
|
const res = await runUninit(process.cwd());
|
|
216
255
|
if (res.cleaned.length === 0) {
|
|
@@ -232,11 +271,83 @@ try {
|
|
|
232
271
|
body += " Run `krimto init` to switch back to AUTO MODE.\n";
|
|
233
272
|
body += "\n Restart your editor so it picks up the change.\n\n";
|
|
234
273
|
process.stderr.write(body);
|
|
274
|
+
|
|
275
|
+
// Now offer to stop the service. Skip the prompt if either flag was passed.
|
|
276
|
+
if (!keepRunningFlag) {
|
|
277
|
+
const { isServiceInstalled, detectPlatform } = await tsImport("../src/cli/service.ts", import.meta.url);
|
|
278
|
+
const svc = await isServiceInstalled(detectPlatform());
|
|
279
|
+
if (svc.installed) {
|
|
280
|
+
let stop = alsoStopFlag;
|
|
281
|
+
if (!alsoStopFlag && process.stdin.isTTY === true) {
|
|
282
|
+
const { confirmStop } = await tsImport("../src/cli/stopCmd.ts", import.meta.url);
|
|
283
|
+
process.stderr.write(
|
|
284
|
+
"ℹ️ The background service is still running on this machine — other projects may use it.\n",
|
|
285
|
+
);
|
|
286
|
+
stop = await confirmStop();
|
|
287
|
+
}
|
|
288
|
+
if (stop) {
|
|
289
|
+
const { runStop } = await tsImport("../src/cli/stopCmd.ts", import.meta.url);
|
|
290
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
291
|
+
const stopRes = await runStop({ dataDir: resolveDataDir() });
|
|
292
|
+
process.stdout.write(stopRes.message);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
235
296
|
}
|
|
236
297
|
} else if (cmd === "where") {
|
|
237
|
-
// `krimto where` — print the data directory
|
|
298
|
+
// `krimto where` — print the data directory. v0.2.31: deprecated in favour of
|
|
299
|
+
// `krimto status` (which shows the data dir + everything else in one screen). Output is
|
|
300
|
+
// preserved for scripts that grep for the path; deprecation hint goes to stderr so it
|
|
301
|
+
// doesn't break pipes like `cd "$(krimto where)"`.
|
|
238
302
|
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
239
303
|
process.stdout.write(`${resolveDataDir()}\n`);
|
|
304
|
+
process.stderr.write("\n→ `krimto where` is now part of `krimto status` (the data-dir is in the Storage block).\n");
|
|
305
|
+
} else if (cmd === "folder") {
|
|
306
|
+
// `krimto folder` — guided move of the data dir. v0.2.31. Stops the service (if any),
|
|
307
|
+
// moves the dir (atomic when same filesystem; cp+rm fallback for EXDEV), reinstalls the
|
|
308
|
+
// service with the new KRIMTO_DATA env, prints an export hint for the user's shell.
|
|
309
|
+
const flags = process.argv.slice(3);
|
|
310
|
+
const toIdx = flags.indexOf("--to");
|
|
311
|
+
const to = toIdx >= 0 ? flags[toIdx + 1] : undefined;
|
|
312
|
+
const yes = flags.includes("--yes");
|
|
313
|
+
const { runFolderCmd } = await tsImport("../src/cli/folderCmd.ts", import.meta.url);
|
|
314
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
315
|
+
const result = await runFolderCmd({
|
|
316
|
+
from: resolveDataDir(),
|
|
317
|
+
...(to ? { to } : {}),
|
|
318
|
+
yes,
|
|
319
|
+
});
|
|
320
|
+
if (result !== null) {
|
|
321
|
+
process.stdout.write(result.message);
|
|
322
|
+
if (result.status === "error") process.exitCode = 1;
|
|
323
|
+
}
|
|
324
|
+
} else if (cmd === "remote") {
|
|
325
|
+
// `krimto remote` — friendly wrapper around setup-remote: show current / set new / remove.
|
|
326
|
+
// v0.2.31. Reuses runSetupRemote for the set path so URL validation + first-push verification
|
|
327
|
+
// happen in one place.
|
|
328
|
+
const flags = process.argv.slice(3);
|
|
329
|
+
const action = flags.includes("--show")
|
|
330
|
+
? "show"
|
|
331
|
+
: flags.includes("--remove")
|
|
332
|
+
? "remove"
|
|
333
|
+
: flags.includes("--set")
|
|
334
|
+
? "set"
|
|
335
|
+
: undefined;
|
|
336
|
+
const setIdx = flags.indexOf("--set");
|
|
337
|
+
const url = setIdx >= 0 ? flags[setIdx + 1] : undefined;
|
|
338
|
+
const yes = flags.includes("--yes");
|
|
339
|
+
const { runRemoteCmd } = await tsImport("../src/cli/remoteCmd.ts", import.meta.url);
|
|
340
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
341
|
+
const result = await runRemoteCmd({
|
|
342
|
+
dataDir: resolveDataDir(),
|
|
343
|
+
...(action ? { action } : {}),
|
|
344
|
+
...(url ? { url } : {}),
|
|
345
|
+
yes,
|
|
346
|
+
});
|
|
347
|
+
if (result !== null) {
|
|
348
|
+
process.stdout.write(result.message);
|
|
349
|
+
if (result.setupResult && result.setupResult.status !== "ok") process.exitCode = 1;
|
|
350
|
+
}
|
|
240
351
|
} else if (cmd === "setup-remote") {
|
|
241
352
|
// `krimto setup-remote <url>` — point the data dir's git repo at a remote and verify a push.
|
|
242
353
|
// Krimto must NOT be running while this is invoked (locks the .git/ index).
|
|
@@ -260,31 +371,139 @@ try {
|
|
|
260
371
|
process.stdout.write(result.message);
|
|
261
372
|
if (result.status === "error") process.exitCode = 1;
|
|
262
373
|
} else if (cmd === "verify-connection") {
|
|
263
|
-
// `krimto verify-connection` —
|
|
264
|
-
//
|
|
374
|
+
// `krimto verify-connection` — v0.2.31: deprecated in favour of `krimto status` (which
|
|
375
|
+
// includes the same lock + activity + sync info as one of its blocks). Existing output
|
|
376
|
+
// preserved verbatim so existing scripts/READMEs keep working; deprecation hint to stderr.
|
|
265
377
|
const { runVerifyConnection } = await tsImport("../src/cli/verifyConnection.ts", import.meta.url);
|
|
266
378
|
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
267
379
|
const result = await runVerifyConnection(resolveDataDir());
|
|
268
380
|
process.stdout.write(result.message);
|
|
381
|
+
process.stderr.write("\n→ `krimto verify-connection` is now part of `krimto status` (one command, four answers).\n");
|
|
269
382
|
if (result.status === "none") process.exitCode = 1;
|
|
270
383
|
} else if (cmd === "editors") {
|
|
271
|
-
// `krimto editors` —
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
+
}
|
|
275
433
|
} else if (cmd === "search") {
|
|
276
434
|
// `krimto search` — change the search provider (Keyword vs OpenAI) without re-running the
|
|
277
|
-
// 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;
|
|
278
446
|
const { runSearchSettings } = await tsImport("../src/cli/searchSettings.ts", import.meta.url);
|
|
279
|
-
|
|
280
|
-
|
|
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
|
+
}
|
|
281
462
|
} else if (cmd === "service") {
|
|
282
463
|
// `krimto service` — change run mode (as-needed / always-running / manual). Installs or
|
|
283
|
-
// uninstalls the platform service to match (Phase B).
|
|
464
|
+
// uninstalls the platform service to match (Phase B). v0.2.32: accepts `--as-needed`,
|
|
465
|
+
// `--always` (alias for --always-running), or `--manual` to skip the prompt for scripts.
|
|
466
|
+
const flags = process.argv.slice(3);
|
|
467
|
+
const flagMode = flags.includes("--as-needed")
|
|
468
|
+
? "as-needed"
|
|
469
|
+
: flags.includes("--always") || flags.includes("--always-running")
|
|
470
|
+
? "always-running"
|
|
471
|
+
: flags.includes("--manual")
|
|
472
|
+
? "manual"
|
|
473
|
+
: undefined;
|
|
284
474
|
const { runServiceCmd } = await tsImport("../src/cli/serviceCmd.ts", import.meta.url);
|
|
285
475
|
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
286
|
-
const result = await runServiceCmd({
|
|
476
|
+
const result = await runServiceCmd({
|
|
477
|
+
dataDir: resolveDataDir(),
|
|
478
|
+
...(flagMode ? { mode: flagMode } : {}),
|
|
479
|
+
});
|
|
287
480
|
if (result === null) process.exitCode = 1;
|
|
481
|
+
} else if (cmd === "stop" || cmd === "service stop") {
|
|
482
|
+
// `krimto stop` — v0.2.32 first-class teardown verb. Uninstalls the launchd/systemd
|
|
483
|
+
// service (if installed) and SIGTERMs whatever PID is holding the lock. Idempotent.
|
|
484
|
+
// `service stop` is the same code path, named for users coming via `service` discovery.
|
|
485
|
+
const { runStop } = await tsImport("../src/cli/stopCmd.ts", import.meta.url);
|
|
486
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
487
|
+
const result = await runStop({ dataDir: resolveDataDir() });
|
|
488
|
+
process.stdout.write(result.message);
|
|
489
|
+
} else if (cmd === "start" || cmd === "service start") {
|
|
490
|
+
// `krimto start` — v0.2.32 counterpart to stop. If a service plist exists on disk,
|
|
491
|
+
// reinstall + bootstrap (goes through the v0.2.26 kickstart-or-bootstrap path). If no
|
|
492
|
+
// service is configured, prints an instructive message instead of doing a brittle
|
|
493
|
+
// background-detached spawn.
|
|
494
|
+
const { runStart } = await tsImport("../src/cli/stopCmd.ts", import.meta.url);
|
|
495
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
496
|
+
const result = await runStart({ dataDir: resolveDataDir() });
|
|
497
|
+
process.stdout.write(result.message);
|
|
498
|
+
if (result.status === "no-service-configured" || result.status === "error") process.exitCode = 1;
|
|
499
|
+
} else if (cmd === "restart") {
|
|
500
|
+
// `krimto restart` — v0.2.32. stop + start. On always-running mode this is effectively
|
|
501
|
+
// `launchctl kickstart -k` via installService's v0.2.26 reload path — atomic, no
|
|
502
|
+
// port-unbound window.
|
|
503
|
+
const { runRestart } = await tsImport("../src/cli/stopCmd.ts", import.meta.url);
|
|
504
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
505
|
+
const result = await runRestart({ dataDir: resolveDataDir() });
|
|
506
|
+
process.stdout.write(result.message);
|
|
288
507
|
} else if (cmd === "reset") {
|
|
289
508
|
// `krimto reset` — disconnect from all editors + uninstall service + wipe local key store.
|
|
290
509
|
// `--wipe-notes` adds a second confirmation and moves the data dir to a trash sibling.
|
|
@@ -403,11 +622,12 @@ try {
|
|
|
403
622
|
process.stdout.write(result.message + "\n");
|
|
404
623
|
if (result.status !== "ok") process.exitCode = 1;
|
|
405
624
|
} else if (cmd === "storage") {
|
|
406
|
-
// `krimto storage` —
|
|
407
|
-
//
|
|
625
|
+
// `krimto storage` — v0.2.31: deprecated in favour of `krimto status` (Storage block).
|
|
626
|
+
// Existing output preserved; deprecation hint to stderr.
|
|
408
627
|
const { formatStorage } = await tsImport("../src/cli/storage.ts", import.meta.url);
|
|
409
628
|
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
410
629
|
process.stdout.write(formatStorage(resolveDataDir()));
|
|
630
|
+
process.stderr.write("\n→ `krimto storage` is now part of `krimto status` (look for the Storage block).\n");
|
|
411
631
|
} else if (cmd === "serve") {
|
|
412
632
|
// `krimto serve` — boot the HTTP server (with /ui and /ui/connect) from the npx on-ramp,
|
|
413
633
|
// so a stranger doesn't have to clone the repo or install Docker just to see the dashboard.
|
|
@@ -415,11 +635,38 @@ try {
|
|
|
415
635
|
if (!process.env.KRIMTO_HTTP_PORT) process.env.KRIMTO_HTTP_PORT = "8080";
|
|
416
636
|
const mod = await tsImport("../src/server/index.ts", import.meta.url);
|
|
417
637
|
await mod.main();
|
|
638
|
+
} else if (cmd === "ui") {
|
|
639
|
+
// `krimto ui` — open the browser dashboard. The Maria-journey doc names this as one of the
|
|
640
|
+
// four user-facing verbs; the implementation is a one-liner over the platform "open this URL"
|
|
641
|
+
// command. If no krimto server is running, the browser will hit ECONNREFUSED — surface a
|
|
642
|
+
// pointer rather than a cryptic error.
|
|
643
|
+
const port = process.env.KRIMTO_HTTP_PORT ?? "8080";
|
|
644
|
+
const url = `http://localhost:${port}/ui`;
|
|
645
|
+
const { spawn } = await import("node:child_process");
|
|
646
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
647
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
|
|
648
|
+
process.stdout.write(`Opening ${url}\n`);
|
|
649
|
+
process.stdout.write(`If the page doesn't load, start the server first: $ krimto serve\n`);
|
|
650
|
+
} else if (cmd === "open") {
|
|
651
|
+
// `krimto open` — reveal the notes folder in the OS file manager. Companion to `krimto ui`
|
|
652
|
+
// for users who want to inspect / back up the markdown directly. macOS uses `open`, Linux
|
|
653
|
+
// `xdg-open`, Windows `explorer`. We deliberately do NOT do this from a browser button on
|
|
654
|
+
// /ui (cross-origin POST + a process running as the user can `open arbitrary://` URLs);
|
|
655
|
+
// the CLI is the right surface.
|
|
656
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
657
|
+
const dataDir = resolveDataDir();
|
|
658
|
+
const { spawn } = await import("node:child_process");
|
|
659
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
660
|
+
spawn(opener, [dataDir], { detached: true, stdio: "ignore" }).unref();
|
|
661
|
+
process.stdout.write(`Revealing ${dataDir} in your file manager.\n`);
|
|
418
662
|
} else if (cmd === "usage") {
|
|
419
|
-
// `krimto usage` — the long-form guide: the
|
|
663
|
+
// `krimto usage` — the long-form guide. v0.2.31: kept (the guide is genuinely long and
|
|
664
|
+
// doesn't fit in `krimto status`) but still flagged so users who want the dashboard know
|
|
665
|
+
// where to find it.
|
|
420
666
|
const { formatUsage } = await tsImport("../src/cli/usage.ts", import.meta.url);
|
|
421
667
|
const { KRIMTO_VERSION } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
422
668
|
process.stdout.write(formatUsage(KRIMTO_VERSION));
|
|
669
|
+
process.stderr.write("\n→ For runtime status (is Krimto running, recent calls, where data lives) use `krimto status`.\n");
|
|
423
670
|
} else if (cmd === "connect") {
|
|
424
671
|
// `krimto connect` — print stdio connect snippets (the npx on-ramp shape), so a solo user
|
|
425
672
|
// doesn't have to chase the README. Honors KRIMTO_IDENTITY when set.
|
package/package.json
CHANGED
package/src/access/scope.ts
CHANGED
|
@@ -58,6 +58,14 @@ export interface Requester {
|
|
|
58
58
|
identity: string;
|
|
59
59
|
/** Team slugs the requester belongs to. */
|
|
60
60
|
teams: string[];
|
|
61
|
+
/**
|
|
62
|
+
* v0.2.31 — best-effort editor attribution. Set by the HTTP MCP handler from User-Agent
|
|
63
|
+
* sniffing ("Cursor/1.x" → "cursor", "claude-code/x" → "claude-code", etc.) so that fact
|
|
64
|
+
* frontmatter can record "saved from a Cursor chat" without each MCP-tool caller needing
|
|
65
|
+
* to pass `source` explicitly. Undefined over stdio transport (no UA available) and
|
|
66
|
+
* whenever the User-Agent is unrecognised.
|
|
67
|
+
*/
|
|
68
|
+
source?: string;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
71
|
export type ScopeRelation = "own-user" | "own-team" | "org" | "other";
|
package/src/agentRule.ts
CHANGED
|
@@ -29,6 +29,17 @@ Don't save secrets, transient state, or one-off chatter.`;
|
|
|
29
29
|
const START = "<!-- krimto:start -->";
|
|
30
30
|
const END = "<!-- krimto:end -->";
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* v0.2.29 — Cursor's `.cursor/rules/*.mdc` files require YAML frontmatter to be auto-applied.
|
|
34
|
+
* Without `alwaysApply: true`, Cursor treats the rule as MANUAL-attach only — the agent only
|
|
35
|
+
* loads it when the user explicitly says "krimto" (or `@krimto`) in their prompt. The smoke-6
|
|
36
|
+
* cross-editor test showed this: Claude Code (which auto-reads CLAUDE.md with no frontmatter
|
|
37
|
+
* needed) saved facts correctly, but Cursor wouldn't recall them until the user typed "krimto".
|
|
38
|
+
* Other editors (CLAUDE.md, AGENTS.md, GEMINI.md) are plain markdown — they don't use this
|
|
39
|
+
* convention, so the frontmatter is added ONLY for the cursor target.
|
|
40
|
+
*/
|
|
41
|
+
const CURSOR_FRONTMATTER = "---\nalwaysApply: true\n---\n";
|
|
42
|
+
|
|
32
43
|
/** The rule wrapped in stable markers, so it can be found and updated in place later. */
|
|
33
44
|
export function ruleBlock(): string {
|
|
34
45
|
return `${START}\n${AGENT_RULE}\n${END}`;
|
|
@@ -40,19 +51,34 @@ export function ruleBlock(): string {
|
|
|
40
51
|
* - existing WITHOUT our markers → append the block, preserving all existing content
|
|
41
52
|
* - existing WITH our markers → replace only the marked block, preserving the rest
|
|
42
53
|
* Re-applying the same rule yields identical content (so callers can detect a no-op).
|
|
54
|
+
*
|
|
55
|
+
* `opts.cursorMdc` prepends the Cursor-required YAML frontmatter (`alwaysApply: true`) so
|
|
56
|
+
* `.cursor/rules/krimto.mdc` is auto-loaded by Cursor on every prompt instead of being
|
|
57
|
+
* manual-attach-only. Idempotent: if frontmatter already exists at the top, it's preserved.
|
|
43
58
|
*/
|
|
44
|
-
export function applyRule(
|
|
59
|
+
export function applyRule(
|
|
60
|
+
existing: string | null,
|
|
61
|
+
opts: { cursorMdc?: boolean } = {},
|
|
62
|
+
): string {
|
|
45
63
|
const block = ruleBlock();
|
|
46
|
-
|
|
64
|
+
|
|
65
|
+
// Helper: ensure the result starts with `---\nalwaysApply: true\n---\n` when requested.
|
|
66
|
+
const withFrontmatter = (content: string): string => {
|
|
67
|
+
if (!opts.cursorMdc) return content;
|
|
68
|
+
if (content.startsWith("---\n")) return content; // user-supplied frontmatter — leave alone
|
|
69
|
+
return CURSOR_FRONTMATTER + content;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (!existing || existing.trim() === "") return withFrontmatter(`${block}\n`);
|
|
47
73
|
|
|
48
74
|
const startIdx = existing.indexOf(START);
|
|
49
75
|
const endIdx = existing.indexOf(END);
|
|
50
76
|
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
51
|
-
return existing.slice(0, startIdx) + block + existing.slice(endIdx + END.length);
|
|
77
|
+
return withFrontmatter(existing.slice(0, startIdx) + block + existing.slice(endIdx + END.length));
|
|
52
78
|
}
|
|
53
79
|
|
|
54
80
|
const sep = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
55
|
-
return `${existing}${sep}${block}\n
|
|
81
|
+
return withFrontmatter(`${existing}${sep}${block}\n`);
|
|
56
82
|
}
|
|
57
83
|
|
|
58
84
|
/**
|
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[],
|
|
@@ -140,7 +203,8 @@ async function askEditorsList(
|
|
|
140
203
|
async function applyRuleToFile(cwd: string, env: EditorEnvironment): Promise<boolean> {
|
|
141
204
|
const rulePath = path.join(cwd, env.rulesPath);
|
|
142
205
|
const existing = await readMaybe(rulePath);
|
|
143
|
-
|
|
206
|
+
// v0.2.29 — Cursor's .mdc rules need `alwaysApply: true` frontmatter.
|
|
207
|
+
const next = applyRule(existing, { cursorMdc: env.editor === "cursor" });
|
|
144
208
|
if (next === existing) return false;
|
|
145
209
|
await fs.mkdir(path.dirname(rulePath), { recursive: true });
|
|
146
210
|
await fs.writeFile(rulePath, next, "utf8");
|