@krimto-labs/krimto 0.2.27 → 0.2.32
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 +176 -15
- package/package.json +1 -1
- package/src/access/scope.ts +8 -0
- package/src/agentRule.ts +30 -4
- package/src/cli/editors.ts +2 -1
- package/src/cli/folderCmd.ts +216 -0
- package/src/cli/help.ts +66 -33
- package/src/cli/init.ts +27 -18
- package/src/cli/join.ts +2 -1
- package/src/cli/remoteCmd.ts +139 -0
- package/src/cli/service.ts +54 -0
- package/src/cli/status.ts +7 -35
- 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
|
@@ -10,12 +10,18 @@ try {
|
|
|
10
10
|
// Two-word command support (v0.2.17.1): `team init`, `team disband`. Collapse argv[2]+argv[3]
|
|
11
11
|
// into one cmd string when argv[2] is one of the namespaced verbs.
|
|
12
12
|
const rawCmd = process.argv[2];
|
|
13
|
+
const sub = process.argv[3];
|
|
14
|
+
// v0.2.32 — `service stop` / `service start` are explicit, scriptable subverbs (no prompt).
|
|
15
|
+
// `service` alone still launches the interactive wizard. Mirrors the team/set two-word shape.
|
|
16
|
+
const serviceSubverbs = ["stop", "start"];
|
|
13
17
|
const cmd =
|
|
14
|
-
rawCmd === "team" && typeof
|
|
15
|
-
? `team ${
|
|
16
|
-
: rawCmd === "set" && typeof
|
|
17
|
-
? `set ${
|
|
18
|
-
: rawCmd
|
|
18
|
+
rawCmd === "team" && typeof sub === "string"
|
|
19
|
+
? `team ${sub}`
|
|
20
|
+
: rawCmd === "set" && typeof sub === "string"
|
|
21
|
+
? `set ${sub}`
|
|
22
|
+
: rawCmd === "service" && typeof sub === "string" && serviceSubverbs.includes(sub)
|
|
23
|
+
? `service ${sub}`
|
|
24
|
+
: rawCmd;
|
|
19
25
|
|
|
20
26
|
// Guard: `krimto team` alone (or with an unknown subverb) shouldn't fall through to the stdio
|
|
21
27
|
// MCP server. Print usage and exit instead.
|
|
@@ -138,8 +144,13 @@ try {
|
|
|
138
144
|
" 3. Verify it landed: $ npx @krimto-labs/krimto verify-connection\n" +
|
|
139
145
|
mcpWarning +
|
|
140
146
|
"\n" +
|
|
141
|
-
|
|
142
|
-
"
|
|
147
|
+
// v0.2.32 — three honest off-ramps, three blast radii. The old single line said
|
|
148
|
+
// "To undo: krimto uninit" which only stripped this project's rule files; users
|
|
149
|
+
// were stranded thinking they had a working stop button when the service kept
|
|
150
|
+
// running on their machine.
|
|
151
|
+
"To stop the service: $ npx @krimto-labs/krimto stop\n" +
|
|
152
|
+
"To undo this project only: $ npx @krimto-labs/krimto uninit\n" +
|
|
153
|
+
"To disconnect everything: $ npx @krimto-labs/krimto reset (notes preserved)\n\n",
|
|
143
154
|
);
|
|
144
155
|
}
|
|
145
156
|
} else if (yes) {
|
|
@@ -211,6 +222,16 @@ try {
|
|
|
211
222
|
} else if (cmd === "uninit") {
|
|
212
223
|
// `krimto uninit` — remove the always-use-Krimto rule from this project's rules files,
|
|
213
224
|
// flipping the project back from AUTO MODE to DEFAULT MODE.
|
|
225
|
+
//
|
|
226
|
+
// v0.2.32: the smoke-6 audit caught users assuming `uninit` was the full undo button —
|
|
227
|
+
// it wasn't (the background service kept running). After rule removal, if a service
|
|
228
|
+
// and/or a live krimto process is detected on this machine, we now ask whether the
|
|
229
|
+
// user also wants to stop it. Default is No (the service is machine-wide; other
|
|
230
|
+
// projects may use it). The flag `--also-stop` skips the prompt; `--keep-running`
|
|
231
|
+
// explicitly suppresses it (for scripted runs).
|
|
232
|
+
const flags = process.argv.slice(3);
|
|
233
|
+
const alsoStopFlag = flags.includes("--also-stop");
|
|
234
|
+
const keepRunningFlag = flags.includes("--keep-running");
|
|
214
235
|
const { runUninit } = await tsImport("../src/cli/uninit.ts", import.meta.url);
|
|
215
236
|
const res = await runUninit(process.cwd());
|
|
216
237
|
if (res.cleaned.length === 0) {
|
|
@@ -232,11 +253,83 @@ try {
|
|
|
232
253
|
body += " Run `krimto init` to switch back to AUTO MODE.\n";
|
|
233
254
|
body += "\n Restart your editor so it picks up the change.\n\n";
|
|
234
255
|
process.stderr.write(body);
|
|
256
|
+
|
|
257
|
+
// Now offer to stop the service. Skip the prompt if either flag was passed.
|
|
258
|
+
if (!keepRunningFlag) {
|
|
259
|
+
const { isServiceInstalled, detectPlatform } = await tsImport("../src/cli/service.ts", import.meta.url);
|
|
260
|
+
const svc = await isServiceInstalled(detectPlatform());
|
|
261
|
+
if (svc.installed) {
|
|
262
|
+
let stop = alsoStopFlag;
|
|
263
|
+
if (!alsoStopFlag && process.stdin.isTTY === true) {
|
|
264
|
+
const { confirmStop } = await tsImport("../src/cli/stopCmd.ts", import.meta.url);
|
|
265
|
+
process.stderr.write(
|
|
266
|
+
"ℹ️ The background service is still running on this machine — other projects may use it.\n",
|
|
267
|
+
);
|
|
268
|
+
stop = await confirmStop();
|
|
269
|
+
}
|
|
270
|
+
if (stop) {
|
|
271
|
+
const { runStop } = await tsImport("../src/cli/stopCmd.ts", import.meta.url);
|
|
272
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
273
|
+
const stopRes = await runStop({ dataDir: resolveDataDir() });
|
|
274
|
+
process.stdout.write(stopRes.message);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
235
278
|
}
|
|
236
279
|
} else if (cmd === "where") {
|
|
237
|
-
// `krimto where` — print the data directory
|
|
280
|
+
// `krimto where` — print the data directory. v0.2.31: deprecated in favour of
|
|
281
|
+
// `krimto status` (which shows the data dir + everything else in one screen). Output is
|
|
282
|
+
// preserved for scripts that grep for the path; deprecation hint goes to stderr so it
|
|
283
|
+
// doesn't break pipes like `cd "$(krimto where)"`.
|
|
238
284
|
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
239
285
|
process.stdout.write(`${resolveDataDir()}\n`);
|
|
286
|
+
process.stderr.write("\n→ `krimto where` is now part of `krimto status` (the data-dir is in the Storage block).\n");
|
|
287
|
+
} else if (cmd === "folder") {
|
|
288
|
+
// `krimto folder` — guided move of the data dir. v0.2.31. Stops the service (if any),
|
|
289
|
+
// moves the dir (atomic when same filesystem; cp+rm fallback for EXDEV), reinstalls the
|
|
290
|
+
// service with the new KRIMTO_DATA env, prints an export hint for the user's shell.
|
|
291
|
+
const flags = process.argv.slice(3);
|
|
292
|
+
const toIdx = flags.indexOf("--to");
|
|
293
|
+
const to = toIdx >= 0 ? flags[toIdx + 1] : undefined;
|
|
294
|
+
const yes = flags.includes("--yes");
|
|
295
|
+
const { runFolderCmd } = await tsImport("../src/cli/folderCmd.ts", import.meta.url);
|
|
296
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
297
|
+
const result = await runFolderCmd({
|
|
298
|
+
from: resolveDataDir(),
|
|
299
|
+
...(to ? { to } : {}),
|
|
300
|
+
yes,
|
|
301
|
+
});
|
|
302
|
+
if (result !== null) {
|
|
303
|
+
process.stdout.write(result.message);
|
|
304
|
+
if (result.status === "error") process.exitCode = 1;
|
|
305
|
+
}
|
|
306
|
+
} else if (cmd === "remote") {
|
|
307
|
+
// `krimto remote` — friendly wrapper around setup-remote: show current / set new / remove.
|
|
308
|
+
// v0.2.31. Reuses runSetupRemote for the set path so URL validation + first-push verification
|
|
309
|
+
// happen in one place.
|
|
310
|
+
const flags = process.argv.slice(3);
|
|
311
|
+
const action = flags.includes("--show")
|
|
312
|
+
? "show"
|
|
313
|
+
: flags.includes("--remove")
|
|
314
|
+
? "remove"
|
|
315
|
+
: flags.includes("--set")
|
|
316
|
+
? "set"
|
|
317
|
+
: undefined;
|
|
318
|
+
const setIdx = flags.indexOf("--set");
|
|
319
|
+
const url = setIdx >= 0 ? flags[setIdx + 1] : undefined;
|
|
320
|
+
const yes = flags.includes("--yes");
|
|
321
|
+
const { runRemoteCmd } = await tsImport("../src/cli/remoteCmd.ts", import.meta.url);
|
|
322
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
323
|
+
const result = await runRemoteCmd({
|
|
324
|
+
dataDir: resolveDataDir(),
|
|
325
|
+
...(action ? { action } : {}),
|
|
326
|
+
...(url ? { url } : {}),
|
|
327
|
+
yes,
|
|
328
|
+
});
|
|
329
|
+
if (result !== null) {
|
|
330
|
+
process.stdout.write(result.message);
|
|
331
|
+
if (result.setupResult && result.setupResult.status !== "ok") process.exitCode = 1;
|
|
332
|
+
}
|
|
240
333
|
} else if (cmd === "setup-remote") {
|
|
241
334
|
// `krimto setup-remote <url>` — point the data dir's git repo at a remote and verify a push.
|
|
242
335
|
// Krimto must NOT be running while this is invoked (locks the .git/ index).
|
|
@@ -260,12 +353,14 @@ try {
|
|
|
260
353
|
process.stdout.write(result.message);
|
|
261
354
|
if (result.status === "error") process.exitCode = 1;
|
|
262
355
|
} else if (cmd === "verify-connection") {
|
|
263
|
-
// `krimto verify-connection` —
|
|
264
|
-
//
|
|
356
|
+
// `krimto verify-connection` — v0.2.31: deprecated in favour of `krimto status` (which
|
|
357
|
+
// includes the same lock + activity + sync info as one of its blocks). Existing output
|
|
358
|
+
// preserved verbatim so existing scripts/READMEs keep working; deprecation hint to stderr.
|
|
265
359
|
const { runVerifyConnection } = await tsImport("../src/cli/verifyConnection.ts", import.meta.url);
|
|
266
360
|
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
267
361
|
const result = await runVerifyConnection(resolveDataDir());
|
|
268
362
|
process.stdout.write(result.message);
|
|
363
|
+
process.stderr.write("\n→ `krimto verify-connection` is now part of `krimto status` (one command, four answers).\n");
|
|
269
364
|
if (result.status === "none") process.exitCode = 1;
|
|
270
365
|
} else if (cmd === "editors") {
|
|
271
366
|
// `krimto editors` — one-question shortcut to add/remove editor connections (Phase B).
|
|
@@ -280,11 +375,49 @@ try {
|
|
|
280
375
|
if (result === null) process.exitCode = 1;
|
|
281
376
|
} else if (cmd === "service") {
|
|
282
377
|
// `krimto service` — change run mode (as-needed / always-running / manual). Installs or
|
|
283
|
-
// uninstalls the platform service to match (Phase B).
|
|
378
|
+
// uninstalls the platform service to match (Phase B). v0.2.32: accepts `--as-needed`,
|
|
379
|
+
// `--always` (alias for --always-running), or `--manual` to skip the prompt for scripts.
|
|
380
|
+
const flags = process.argv.slice(3);
|
|
381
|
+
const flagMode = flags.includes("--as-needed")
|
|
382
|
+
? "as-needed"
|
|
383
|
+
: flags.includes("--always") || flags.includes("--always-running")
|
|
384
|
+
? "always-running"
|
|
385
|
+
: flags.includes("--manual")
|
|
386
|
+
? "manual"
|
|
387
|
+
: undefined;
|
|
284
388
|
const { runServiceCmd } = await tsImport("../src/cli/serviceCmd.ts", import.meta.url);
|
|
285
389
|
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
286
|
-
const result = await runServiceCmd({
|
|
390
|
+
const result = await runServiceCmd({
|
|
391
|
+
dataDir: resolveDataDir(),
|
|
392
|
+
...(flagMode ? { mode: flagMode } : {}),
|
|
393
|
+
});
|
|
287
394
|
if (result === null) process.exitCode = 1;
|
|
395
|
+
} else if (cmd === "stop" || cmd === "service stop") {
|
|
396
|
+
// `krimto stop` — v0.2.32 first-class teardown verb. Uninstalls the launchd/systemd
|
|
397
|
+
// service (if installed) and SIGTERMs whatever PID is holding the lock. Idempotent.
|
|
398
|
+
// `service stop` is the same code path, named for users coming via `service` discovery.
|
|
399
|
+
const { runStop } = await tsImport("../src/cli/stopCmd.ts", import.meta.url);
|
|
400
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
401
|
+
const result = await runStop({ dataDir: resolveDataDir() });
|
|
402
|
+
process.stdout.write(result.message);
|
|
403
|
+
} else if (cmd === "start" || cmd === "service start") {
|
|
404
|
+
// `krimto start` — v0.2.32 counterpart to stop. If a service plist exists on disk,
|
|
405
|
+
// reinstall + bootstrap (goes through the v0.2.26 kickstart-or-bootstrap path). If no
|
|
406
|
+
// service is configured, prints an instructive message instead of doing a brittle
|
|
407
|
+
// background-detached spawn.
|
|
408
|
+
const { runStart } = await tsImport("../src/cli/stopCmd.ts", import.meta.url);
|
|
409
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
410
|
+
const result = await runStart({ dataDir: resolveDataDir() });
|
|
411
|
+
process.stdout.write(result.message);
|
|
412
|
+
if (result.status === "no-service-configured" || result.status === "error") process.exitCode = 1;
|
|
413
|
+
} else if (cmd === "restart") {
|
|
414
|
+
// `krimto restart` — v0.2.32. stop + start. On always-running mode this is effectively
|
|
415
|
+
// `launchctl kickstart -k` via installService's v0.2.26 reload path — atomic, no
|
|
416
|
+
// port-unbound window.
|
|
417
|
+
const { runRestart } = await tsImport("../src/cli/stopCmd.ts", import.meta.url);
|
|
418
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
419
|
+
const result = await runRestart({ dataDir: resolveDataDir() });
|
|
420
|
+
process.stdout.write(result.message);
|
|
288
421
|
} else if (cmd === "reset") {
|
|
289
422
|
// `krimto reset` — disconnect from all editors + uninstall service + wipe local key store.
|
|
290
423
|
// `--wipe-notes` adds a second confirmation and moves the data dir to a trash sibling.
|
|
@@ -403,11 +536,12 @@ try {
|
|
|
403
536
|
process.stdout.write(result.message + "\n");
|
|
404
537
|
if (result.status !== "ok") process.exitCode = 1;
|
|
405
538
|
} else if (cmd === "storage") {
|
|
406
|
-
// `krimto storage` —
|
|
407
|
-
//
|
|
539
|
+
// `krimto storage` — v0.2.31: deprecated in favour of `krimto status` (Storage block).
|
|
540
|
+
// Existing output preserved; deprecation hint to stderr.
|
|
408
541
|
const { formatStorage } = await tsImport("../src/cli/storage.ts", import.meta.url);
|
|
409
542
|
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
410
543
|
process.stdout.write(formatStorage(resolveDataDir()));
|
|
544
|
+
process.stderr.write("\n→ `krimto storage` is now part of `krimto status` (look for the Storage block).\n");
|
|
411
545
|
} else if (cmd === "serve") {
|
|
412
546
|
// `krimto serve` — boot the HTTP server (with /ui and /ui/connect) from the npx on-ramp,
|
|
413
547
|
// so a stranger doesn't have to clone the repo or install Docker just to see the dashboard.
|
|
@@ -415,11 +549,38 @@ try {
|
|
|
415
549
|
if (!process.env.KRIMTO_HTTP_PORT) process.env.KRIMTO_HTTP_PORT = "8080";
|
|
416
550
|
const mod = await tsImport("../src/server/index.ts", import.meta.url);
|
|
417
551
|
await mod.main();
|
|
552
|
+
} else if (cmd === "ui") {
|
|
553
|
+
// `krimto ui` — open the browser dashboard. The Maria-journey doc names this as one of the
|
|
554
|
+
// four user-facing verbs; the implementation is a one-liner over the platform "open this URL"
|
|
555
|
+
// command. If no krimto server is running, the browser will hit ECONNREFUSED — surface a
|
|
556
|
+
// pointer rather than a cryptic error.
|
|
557
|
+
const port = process.env.KRIMTO_HTTP_PORT ?? "8080";
|
|
558
|
+
const url = `http://localhost:${port}/ui`;
|
|
559
|
+
const { spawn } = await import("node:child_process");
|
|
560
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
561
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" }).unref();
|
|
562
|
+
process.stdout.write(`Opening ${url}\n`);
|
|
563
|
+
process.stdout.write(`If the page doesn't load, start the server first: $ krimto serve\n`);
|
|
564
|
+
} else if (cmd === "open") {
|
|
565
|
+
// `krimto open` — reveal the notes folder in the OS file manager. Companion to `krimto ui`
|
|
566
|
+
// for users who want to inspect / back up the markdown directly. macOS uses `open`, Linux
|
|
567
|
+
// `xdg-open`, Windows `explorer`. We deliberately do NOT do this from a browser button on
|
|
568
|
+
// /ui (cross-origin POST + a process running as the user can `open arbitrary://` URLs);
|
|
569
|
+
// the CLI is the right surface.
|
|
570
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
571
|
+
const dataDir = resolveDataDir();
|
|
572
|
+
const { spawn } = await import("node:child_process");
|
|
573
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
574
|
+
spawn(opener, [dataDir], { detached: true, stdio: "ignore" }).unref();
|
|
575
|
+
process.stdout.write(`Revealing ${dataDir} in your file manager.\n`);
|
|
418
576
|
} else if (cmd === "usage") {
|
|
419
|
-
// `krimto usage` — the long-form guide: the
|
|
577
|
+
// `krimto usage` — the long-form guide. v0.2.31: kept (the guide is genuinely long and
|
|
578
|
+
// doesn't fit in `krimto status`) but still flagged so users who want the dashboard know
|
|
579
|
+
// where to find it.
|
|
420
580
|
const { formatUsage } = await tsImport("../src/cli/usage.ts", import.meta.url);
|
|
421
581
|
const { KRIMTO_VERSION } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
422
582
|
process.stdout.write(formatUsage(KRIMTO_VERSION));
|
|
583
|
+
process.stderr.write("\n→ For runtime status (is Krimto running, recent calls, where data lives) use `krimto status`.\n");
|
|
423
584
|
} else if (cmd === "connect") {
|
|
424
585
|
// `krimto connect` — print stdio connect snippets (the npx on-ramp shape), so a solo user
|
|
425
586
|
// 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
|
@@ -140,7 +140,8 @@ async function askEditorsList(
|
|
|
140
140
|
async function applyRuleToFile(cwd: string, env: EditorEnvironment): Promise<boolean> {
|
|
141
141
|
const rulePath = path.join(cwd, env.rulesPath);
|
|
142
142
|
const existing = await readMaybe(rulePath);
|
|
143
|
-
|
|
143
|
+
// v0.2.29 — Cursor's .mdc rules need `alwaysApply: true` frontmatter.
|
|
144
|
+
const next = applyRule(existing, { cursorMdc: env.editor === "cursor" });
|
|
144
145
|
if (next === existing) return false;
|
|
145
146
|
await fs.mkdir(path.dirname(rulePath), { recursive: true });
|
|
146
147
|
await fs.writeFile(rulePath, next, "utf8");
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// `krimto folder` — guided move of the notes folder (`KRIMTO_DATA`) to a new location.
|
|
2
|
+
// The Maria-journey doc §06 names this as a "direct shortcut for power users". v0.2.31.
|
|
3
|
+
//
|
|
4
|
+
// What it does:
|
|
5
|
+
// 1. Reads the current data dir via resolveDataDir() (honours KRIMTO_DATA).
|
|
6
|
+
// 2. Prompts for a destination path (or accepts --to <path>).
|
|
7
|
+
// 3. Validates: destination must not exist OR be an empty directory we can take over.
|
|
8
|
+
// 4. Confirms with the user, listing the consequences.
|
|
9
|
+
// 5. If the always-running service is installed, uninstalls it first (the launchd plist
|
|
10
|
+
// / systemd unit env still points at the OLD path; we'll reinstall with the new one
|
|
11
|
+
// after the move).
|
|
12
|
+
// 6. Atomic-ish rename (`fs.rename`). When source and destination are on different
|
|
13
|
+
// filesystems and `rename` errors with EXDEV, fall back to a recursive copy + remove.
|
|
14
|
+
// 7. Reinstalls the service (if it was installed) with `KRIMTO_DATA=<new path>` baked in.
|
|
15
|
+
// 8. Prints an `export KRIMTO_DATA=<new>` hint so the user's terminal sessions and any
|
|
16
|
+
// scripts pick up the new path on next shell.
|
|
17
|
+
|
|
18
|
+
import { confirm, input } from "@inquirer/prompts";
|
|
19
|
+
import { promises as fs } from "node:fs";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
detectPlatform,
|
|
24
|
+
installService,
|
|
25
|
+
isServiceInstalled,
|
|
26
|
+
uninstallService,
|
|
27
|
+
type InstallResult,
|
|
28
|
+
} from "./service";
|
|
29
|
+
import { defaultIdentity } from "./init";
|
|
30
|
+
import { defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
31
|
+
|
|
32
|
+
export interface FolderCmdOptions {
|
|
33
|
+
io?: WizardIO;
|
|
34
|
+
/** Current data dir — usually `resolveDataDir()`. */
|
|
35
|
+
from: string;
|
|
36
|
+
/** Destination path. When omitted, the user is prompted. */
|
|
37
|
+
to?: string;
|
|
38
|
+
/** Override `os.homedir()` so installService writes the new service unit to a temp dir in tests. */
|
|
39
|
+
homeDir?: string;
|
|
40
|
+
/** Skip the confirmation prompt. */
|
|
41
|
+
yes?: boolean;
|
|
42
|
+
/** Forwarded to installService for tests so we don't really hit launchctl/systemctl. */
|
|
43
|
+
dryRun?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface FolderCmdResult {
|
|
47
|
+
status: "ok" | "no-change" | "error";
|
|
48
|
+
from: string;
|
|
49
|
+
to: string;
|
|
50
|
+
serviceReinstalled: boolean;
|
|
51
|
+
message: string;
|
|
52
|
+
reinstall?: InstallResult;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runFolderCmd(opts: FolderCmdOptions): Promise<FolderCmdResult | null> {
|
|
56
|
+
const io = opts.io ?? defaultIO;
|
|
57
|
+
const fromDir = path.resolve(opts.from);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
io.out("\nKrimto — Move the notes folder\n\n");
|
|
61
|
+
io.out(` Current location: ${fromDir}\n\n`);
|
|
62
|
+
|
|
63
|
+
const to =
|
|
64
|
+
opts.to ??
|
|
65
|
+
(await input({
|
|
66
|
+
message: "New location (absolute path):",
|
|
67
|
+
validate: (v) => {
|
|
68
|
+
const trimmed = v.trim();
|
|
69
|
+
if (trimmed.length === 0) return "Path is required";
|
|
70
|
+
if (!path.isAbsolute(trimmed)) return "Must be an absolute path (starts with /)";
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
}));
|
|
74
|
+
const toDir = path.resolve(to);
|
|
75
|
+
|
|
76
|
+
if (toDir === fromDir) {
|
|
77
|
+
return {
|
|
78
|
+
status: "no-change",
|
|
79
|
+
from: fromDir,
|
|
80
|
+
to: toDir,
|
|
81
|
+
serviceReinstalled: false,
|
|
82
|
+
message: "\n Source and destination are the same — nothing to move.\n\n",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Destination must be either absent or an empty directory. Refuse anything else so we
|
|
87
|
+
// never silently merge into an existing notes folder.
|
|
88
|
+
const destState = await classifyDestination(toDir);
|
|
89
|
+
if (destState === "non-empty") {
|
|
90
|
+
return {
|
|
91
|
+
status: "error",
|
|
92
|
+
from: fromDir,
|
|
93
|
+
to: toDir,
|
|
94
|
+
serviceReinstalled: false,
|
|
95
|
+
message:
|
|
96
|
+
`\n❌ ${toDir} exists and isn't empty.\n` +
|
|
97
|
+
` Pick an absent path or an empty directory. Krimto won't merge into an existing folder.\n\n`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
io.out("\n This will:\n");
|
|
102
|
+
io.out(` • Move every file from ${fromDir}\n`);
|
|
103
|
+
io.out(` to ${toDir}\n`);
|
|
104
|
+
io.out(" • Reinstall the background service (if installed) with the new path\n\n");
|
|
105
|
+
io.out(" This will NOT touch:\n");
|
|
106
|
+
io.out(" • Editor MCP configs (they point at the HTTP server, not the dir)\n");
|
|
107
|
+
io.out(" • Project rule files (.cursor/rules/*.mdc, CLAUDE.md, etc.)\n\n");
|
|
108
|
+
io.out(" ⚠️ After the move, set KRIMTO_DATA in your shell so any new krimto\n");
|
|
109
|
+
io.out(" processes (CLI or editor stdio launches) pick up the new path:\n\n");
|
|
110
|
+
io.out(` export KRIMTO_DATA="${toDir}"\n\n`);
|
|
111
|
+
|
|
112
|
+
if (!opts.yes) {
|
|
113
|
+
const ok = await confirm({ message: "Proceed?", default: false });
|
|
114
|
+
if (!ok) {
|
|
115
|
+
return {
|
|
116
|
+
status: "no-change",
|
|
117
|
+
from: fromDir,
|
|
118
|
+
to: toDir,
|
|
119
|
+
serviceReinstalled: false,
|
|
120
|
+
message: "\nAborted. Nothing moved.\n\n",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Uninstall the service first — its plist/unit env still names the OLD KRIMTO_DATA.
|
|
126
|
+
// After the move we reinstall with the new one. We do this BEFORE the rename so launchd
|
|
127
|
+
// / systemd isn't holding any handles into the (about-to-vanish) source dir.
|
|
128
|
+
const platform = detectPlatform();
|
|
129
|
+
const svc = await isServiceInstalled(platform, opts.homeDir);
|
|
130
|
+
let serviceWasInstalled = false;
|
|
131
|
+
if (svc.installed) {
|
|
132
|
+
serviceWasInstalled = true;
|
|
133
|
+
await uninstallService({ platform, homeDir: opts.homeDir, dryRun: opts.dryRun });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// The move itself. fs.rename is atomic when src + dst share a filesystem. When they
|
|
137
|
+
// don't, Node throws EXDEV — fall back to copy-then-remove.
|
|
138
|
+
try {
|
|
139
|
+
await fs.mkdir(path.dirname(toDir), { recursive: true });
|
|
140
|
+
await fs.rename(fromDir, toDir);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
const code = (e as NodeJS.ErrnoException).code;
|
|
143
|
+
if (code === "EXDEV") {
|
|
144
|
+
// Cross-device fallback: cp -R then rm -rf, both via fs primitives.
|
|
145
|
+
await fs.cp(fromDir, toDir, { recursive: true, preserveTimestamps: true });
|
|
146
|
+
await fs.rm(fromDir, { recursive: true, force: true });
|
|
147
|
+
} else {
|
|
148
|
+
return {
|
|
149
|
+
status: "error",
|
|
150
|
+
from: fromDir,
|
|
151
|
+
to: toDir,
|
|
152
|
+
serviceReinstalled: false,
|
|
153
|
+
message: `\n❌ Move failed: ${e instanceof Error ? e.message : String(e)}\n\n`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Reinstall the service (if it was) with the new data dir baked into its env.
|
|
159
|
+
let reinstall: InstallResult | undefined;
|
|
160
|
+
let serviceReinstalled = false;
|
|
161
|
+
if (serviceWasInstalled) {
|
|
162
|
+
const identity = await defaultIdentity();
|
|
163
|
+
reinstall = await installService(
|
|
164
|
+
{
|
|
165
|
+
binPath: process.execPath,
|
|
166
|
+
args: [process.argv[1] ?? "krimto", "serve"],
|
|
167
|
+
env: { KRIMTO_IDENTITY: identity, KRIMTO_DATA: toDir, KRIMTO_HTTP_PORT: "8080" },
|
|
168
|
+
homeDir: opts.homeDir,
|
|
169
|
+
},
|
|
170
|
+
{ dryRun: opts.dryRun, platform },
|
|
171
|
+
);
|
|
172
|
+
serviceReinstalled = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const lines: string[] = [
|
|
176
|
+
"",
|
|
177
|
+
`✅ Notes folder moved.`,
|
|
178
|
+
` from: ${fromDir}`,
|
|
179
|
+
` to: ${toDir}`,
|
|
180
|
+
"",
|
|
181
|
+
];
|
|
182
|
+
if (serviceReinstalled) lines.push(" • Background service reinstalled with the new path.");
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push("Add this to your shell so future krimto runs find the new location:");
|
|
185
|
+
lines.push(` export KRIMTO_DATA="${toDir}"`);
|
|
186
|
+
lines.push("");
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
status: "ok",
|
|
190
|
+
from: fromDir,
|
|
191
|
+
to: toDir,
|
|
192
|
+
serviceReinstalled,
|
|
193
|
+
message: lines.join("\n"),
|
|
194
|
+
...(reinstall ? { reinstall } : {}),
|
|
195
|
+
};
|
|
196
|
+
} catch (e) {
|
|
197
|
+
if (isExitPrompt(e)) {
|
|
198
|
+
io.err("\nAborted.\n");
|
|
199
|
+
process.exitCode = 130;
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
throw e;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Categorise the destination: "absent" (we'll create it), "empty" (take over), or "non-empty" (refuse). */
|
|
207
|
+
async function classifyDestination(dir: string): Promise<"absent" | "empty" | "non-empty"> {
|
|
208
|
+
try {
|
|
209
|
+
const entries = await fs.readdir(dir);
|
|
210
|
+
return entries.length === 0 ? "empty" : "non-empty";
|
|
211
|
+
} catch (e) {
|
|
212
|
+
if ((e as NodeJS.ErrnoException).code === "ENOENT") return "absent";
|
|
213
|
+
// Other errors (permission, not-a-dir) — treat as non-empty so we refuse.
|
|
214
|
+
return "non-empty";
|
|
215
|
+
}
|
|
216
|
+
}
|