@krimto-labs/krimto 0.2.20 → 0.2.22
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 +43 -1
- package/package.json +1 -1
- package/src/cli/editors.ts +6 -4
- package/src/cli/help.ts +4 -0
- package/src/cli/init.ts +50 -1
- package/src/cli/join.ts +8 -5
- package/src/cli/setIdentity.ts +232 -0
- package/src/cli/status.ts +4 -3
- package/src/cli/whoami.ts +172 -0
- package/src/cli/wizard.ts +21 -9
- package/src/server/index.ts +1 -1
package/bin/krimto.mjs
CHANGED
|
@@ -13,7 +13,9 @@ try {
|
|
|
13
13
|
const cmd =
|
|
14
14
|
rawCmd === "team" && typeof process.argv[3] === "string"
|
|
15
15
|
? `team ${process.argv[3]}`
|
|
16
|
-
: rawCmd
|
|
16
|
+
: rawCmd === "set" && typeof process.argv[3] === "string"
|
|
17
|
+
? `set ${process.argv[3]}`
|
|
18
|
+
: rawCmd;
|
|
17
19
|
|
|
18
20
|
// Guard: `krimto team` alone (or with an unknown subverb) shouldn't fall through to the stdio
|
|
19
21
|
// MCP server. Print usage and exit instead.
|
|
@@ -27,6 +29,16 @@ try {
|
|
|
27
29
|
process.exit(2);
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
// Same guard for `krimto set <subverb>`.
|
|
33
|
+
const knownSetCmds = ["set identity"];
|
|
34
|
+
if (rawCmd === "set" && !knownSetCmds.includes(cmd)) {
|
|
35
|
+
process.stderr.write(
|
|
36
|
+
"Usage: krimto set <identity> <value>\n" +
|
|
37
|
+
" identity <email> Change the identity used for new fact writes\n",
|
|
38
|
+
);
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
31
43
|
// `krimto --help` — surface every subcommand so a user who didn't read the README can still
|
|
32
44
|
// discover them. Version is read from the server module so it never drifts from KRIMTO_VERSION.
|
|
@@ -351,6 +363,36 @@ try {
|
|
|
351
363
|
// doesn't have to chase the README. Honors KRIMTO_IDENTITY when set.
|
|
352
364
|
const { formatConnect } = await tsImport("../src/cli/connect.ts", import.meta.url);
|
|
353
365
|
process.stdout.write(formatConnect({ identity: process.env.KRIMTO_IDENTITY }));
|
|
366
|
+
} else if (cmd === "whoami") {
|
|
367
|
+
// `krimto whoami` — show the active KRIMTO_IDENTITY and every place it's currently set
|
|
368
|
+
// (each editor's MCP config + the always-running service unit). Surfaces drift between
|
|
369
|
+
// sources so users notice before notes start splitting across two scopes.
|
|
370
|
+
const { runWhoami } = await tsImport("../src/cli/whoami.ts", import.meta.url);
|
|
371
|
+
const result = await runWhoami();
|
|
372
|
+
process.stdout.write(result.message);
|
|
373
|
+
if (result.mismatch) process.exitCode = 1;
|
|
374
|
+
} else if (cmd === "set identity") {
|
|
375
|
+
// `krimto set identity <email>` — update KRIMTO_IDENTITY across every registered editor
|
|
376
|
+
// and the always-running service. Preserves other env keys (KRIMTO_EMBED_*). Existing
|
|
377
|
+
// notes do NOT move — that's an intentional separate step.
|
|
378
|
+
const newIdentity = process.argv[4];
|
|
379
|
+
const flags = process.argv.slice(5);
|
|
380
|
+
const yes = flags.includes("--yes");
|
|
381
|
+
if (!newIdentity) {
|
|
382
|
+
process.stderr.write(
|
|
383
|
+
"Usage: krimto set identity <email> [--yes]\n e.g. krimto set identity alice@acme.com\n",
|
|
384
|
+
);
|
|
385
|
+
process.exit(2);
|
|
386
|
+
}
|
|
387
|
+
const { runSetIdentity } = await tsImport("../src/cli/setIdentity.ts", import.meta.url);
|
|
388
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
389
|
+
const result = await runSetIdentity({
|
|
390
|
+
identity: newIdentity,
|
|
391
|
+
dataDir: resolveDataDir(),
|
|
392
|
+
yes,
|
|
393
|
+
});
|
|
394
|
+
process.stdout.write(result.message);
|
|
395
|
+
if (result.status === "error") process.exitCode = 1;
|
|
354
396
|
} else {
|
|
355
397
|
const mod = await tsImport("../src/server/index.ts", import.meta.url);
|
|
356
398
|
await mod.main();
|
package/package.json
CHANGED
package/src/cli/editors.ts
CHANGED
|
@@ -126,10 +126,12 @@ async function askEditorsList(
|
|
|
126
126
|
description: current.includes(env.editor)
|
|
127
127
|
? "currently connected"
|
|
128
128
|
: env.present
|
|
129
|
-
? "detected
|
|
130
|
-
: env.
|
|
131
|
-
? "not
|
|
132
|
-
:
|
|
129
|
+
? "detected in this project"
|
|
130
|
+
: env.installed
|
|
131
|
+
? "installed on this machine (not in this project yet)"
|
|
132
|
+
: env.mcpWire === null
|
|
133
|
+
? "not detected — manual snippet only"
|
|
134
|
+
: "not detected — toggle on if you want anyway",
|
|
133
135
|
checked: current.includes(env.editor),
|
|
134
136
|
})),
|
|
135
137
|
});
|
package/src/cli/help.ts
CHANGED
|
@@ -37,6 +37,10 @@ export function formatHelp(version: string): string {
|
|
|
37
37
|
" rm <id> Delete a fact (file + index + git deletion commit)",
|
|
38
38
|
" reindex Rebuild index.db from markdown (fixes manual-delete orphans)",
|
|
39
39
|
"",
|
|
40
|
+
" Identity:",
|
|
41
|
+
" whoami Show which email Krimto is using (and where it's set)",
|
|
42
|
+
" set identity <e> Change the identity used for new fact writes",
|
|
43
|
+
"",
|
|
40
44
|
" Diagnose:",
|
|
41
45
|
" verify-connection Is my agent actually calling Krimto? (live status + last 5 calls)",
|
|
42
46
|
" setup-remote <url> Wire data dir to a git remote, verify the push works",
|
package/src/cli/init.ts
CHANGED
|
@@ -106,14 +106,52 @@ export type McpWireMethod =
|
|
|
106
106
|
|
|
107
107
|
export interface EditorEnvironment {
|
|
108
108
|
editor: EditorKind;
|
|
109
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* True when local **project-level** signals (file/dir presence inside `cwd`) indicate this
|
|
111
|
+
* editor is in use *for this project*. Narrow scope — `.cursor/`, `CLAUDE.md`, etc.
|
|
112
|
+
*/
|
|
110
113
|
present: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* v0.2.21: True when **machine-level** signals (files/dirs in `homeDir`) indicate this editor
|
|
116
|
+
* is installed and has been used at least once on this machine. Broader than `present` — catches
|
|
117
|
+
* the case where a user is editing in Cursor but the project folder hasn't acquired a `.cursor/`
|
|
118
|
+
* directory yet. The wizard preselects an editor when `present || installed`.
|
|
119
|
+
*/
|
|
120
|
+
installed: boolean;
|
|
111
121
|
/** Project-relative rules-file path (the CLAUDE.md / .cursor/rules/krimto.mdc / etc.). */
|
|
112
122
|
rulesPath: string;
|
|
113
123
|
/** How to wire MCP config for this editor, or `null` when wiring isn't automated yet. */
|
|
114
124
|
mcpWire: McpWireMethod | null;
|
|
115
125
|
}
|
|
116
126
|
|
|
127
|
+
/**
|
|
128
|
+
* v0.2.21: Machine-level installation check. Returns true when this editor's home-dir footprint
|
|
129
|
+
* suggests it's been installed + run on this machine. Used by `detectEditorEnvironments` to
|
|
130
|
+
* populate `EditorEnvironment.installed` so the wizard can preselect editors even when the
|
|
131
|
+
* current project folder has no editor-specific signals yet.
|
|
132
|
+
*
|
|
133
|
+
* The home-dir paths checked are well-known per editor: each is created on first launch.
|
|
134
|
+
* • Cursor — `~/.cursor/`
|
|
135
|
+
* • Claude Code — `~/.claude.json` (user config) or `~/.claude/` (project data)
|
|
136
|
+
* • Codex — `~/.codex/`
|
|
137
|
+
* • Gemini CLI — `~/.gemini/`
|
|
138
|
+
*
|
|
139
|
+
* False positives (folder exists from a previous uninstalled tool) are harmless: the user can
|
|
140
|
+
* untoggle. False negatives (editor installed but never launched) are equally rare in practice.
|
|
141
|
+
*/
|
|
142
|
+
async function isInstalledOnMachine(editor: EditorKind, homeDir: string): Promise<boolean> {
|
|
143
|
+
if (editor === "cursor") return exists(path.join(homeDir, ".cursor"));
|
|
144
|
+
if (editor === "claude-code") {
|
|
145
|
+
return (
|
|
146
|
+
(await exists(path.join(homeDir, ".claude.json"))) ||
|
|
147
|
+
(await exists(path.join(homeDir, ".claude")))
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (editor === "codex") return exists(path.join(homeDir, ".codex"));
|
|
151
|
+
if (editor === "gemini-cli") return exists(path.join(homeDir, ".gemini"));
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
117
155
|
/**
|
|
118
156
|
* Resolve every editor Krimto can reason about into an `EditorEnvironment` triple
|
|
119
157
|
* (editor + rules-file path + mcp-wiring method). `present` reflects detection signals;
|
|
@@ -136,10 +174,18 @@ export async function detectEditorEnvironments(
|
|
|
136
174
|
(await exists(path.join(cwd, ".gemini")));
|
|
137
175
|
const cursorPresent = await exists(path.join(cwd, ".cursor"));
|
|
138
176
|
|
|
177
|
+
// v0.2.21: machine-level installation signals — catch the case where the user is editing in
|
|
178
|
+
// Cursor (or Claude Code) but the project folder hasn't acquired editor-specific files yet.
|
|
179
|
+
const cursorInstalled = await isInstalledOnMachine("cursor", homeDir);
|
|
180
|
+
const claudeInstalled = await isInstalledOnMachine("claude-code", homeDir);
|
|
181
|
+
const codexInstalled = await isInstalledOnMachine("codex", homeDir);
|
|
182
|
+
const geminiInstalled = await isInstalledOnMachine("gemini-cli", homeDir);
|
|
183
|
+
|
|
139
184
|
return [
|
|
140
185
|
{
|
|
141
186
|
editor: "cursor",
|
|
142
187
|
present: cursorPresent,
|
|
188
|
+
installed: cursorInstalled,
|
|
143
189
|
rulesPath: path.join(".cursor", "rules", "krimto.mdc"),
|
|
144
190
|
mcpWire: {
|
|
145
191
|
method: "json",
|
|
@@ -150,6 +196,7 @@ export async function detectEditorEnvironments(
|
|
|
150
196
|
{
|
|
151
197
|
editor: "claude-code",
|
|
152
198
|
present: claudePresent,
|
|
199
|
+
installed: claudeInstalled,
|
|
153
200
|
rulesPath: "CLAUDE.md",
|
|
154
201
|
// `claude mcp add krimto ...` is the supported invocation. Shelling out to the editor's
|
|
155
202
|
// own CLI sidesteps the question of which exact file Claude Code persists MCP config in
|
|
@@ -163,6 +210,7 @@ export async function detectEditorEnvironments(
|
|
|
163
210
|
{
|
|
164
211
|
editor: "gemini-cli",
|
|
165
212
|
present: geminiPresent,
|
|
213
|
+
installed: geminiInstalled,
|
|
166
214
|
rulesPath: "GEMINI.md",
|
|
167
215
|
// Deferred — Gemini CLI's MCP config path needs empirical confirmation before we write
|
|
168
216
|
// to it. v0.2.17 prints a copy-paste snippet instead.
|
|
@@ -171,6 +219,7 @@ export async function detectEditorEnvironments(
|
|
|
171
219
|
{
|
|
172
220
|
editor: "codex",
|
|
173
221
|
present: codexPresent,
|
|
222
|
+
installed: codexInstalled,
|
|
174
223
|
rulesPath: "AGENTS.md",
|
|
175
224
|
// Deferred — Codex's config is TOML (`~/.codex/config.toml`); writing TOML safely needs
|
|
176
225
|
// a parser we haven't added yet. v0.2.17 prints a copy-paste snippet instead.
|
package/src/cli/join.ts
CHANGED
|
@@ -180,11 +180,14 @@ async function askEditors(envs: EditorEnvironment[]): Promise<EditorKind[]> {
|
|
|
180
180
|
value: env.editor,
|
|
181
181
|
name: EDITOR_LABEL[env.editor],
|
|
182
182
|
description: env.present
|
|
183
|
-
? "detected
|
|
184
|
-
: env.
|
|
185
|
-
? "not
|
|
186
|
-
:
|
|
187
|
-
|
|
183
|
+
? "detected in this project"
|
|
184
|
+
: env.installed
|
|
185
|
+
? "installed on this machine (not in this project yet)"
|
|
186
|
+
: env.mcpWire === null
|
|
187
|
+
? "not detected — manual snippet only"
|
|
188
|
+
: "not detected — toggle on if you want anyway",
|
|
189
|
+
// v0.2.21: preselect on either signal — installed-on-machine still counts.
|
|
190
|
+
checked: env.present || env.installed,
|
|
188
191
|
})),
|
|
189
192
|
});
|
|
190
193
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// `krimto set identity <email>` — change KRIMTO_IDENTITY everywhere the wizard wrote it.
|
|
2
|
+
//
|
|
3
|
+
// Surfaces touched (in order):
|
|
4
|
+
// 1. Each registered editor's MCP config — for JSON-method editors, mutate
|
|
5
|
+
// `env.KRIMTO_IDENTITY` in place (other env keys like KRIMTO_EMBED_* are preserved).
|
|
6
|
+
// For CLI-method editors (Claude Code), rebuild a fresh stdio entry via writeMcpConfig
|
|
7
|
+
// (extra env keys are lost — known limitation noted below).
|
|
8
|
+
// 2. The always-running service (launchd/systemd/schtasks) — uninstall + reinstall with
|
|
9
|
+
// the new env. Same dataDir + port as before, mirroring serviceCmd.applyService.
|
|
10
|
+
//
|
|
11
|
+
// HTTP-transport MCP entries don't carry identity (it lives in the service env); they are
|
|
12
|
+
// left untouched.
|
|
13
|
+
//
|
|
14
|
+
// Existing notes do NOT migrate. The folder for the old identity stays under
|
|
15
|
+
// `~/.krimto/user/<old-email>/` — moving them is intentionally a separate step (would need
|
|
16
|
+
// a git rename + reindex; out of scope for this command).
|
|
17
|
+
|
|
18
|
+
import { confirm } from "@inquirer/prompts";
|
|
19
|
+
import { promises as fs } from "node:fs";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
|
|
22
|
+
import { stdioMcpEntry, type KrimtoMcpEntry } from "../server/connect";
|
|
23
|
+
import {
|
|
24
|
+
detectEditorEnvironments,
|
|
25
|
+
detectExistingSetup,
|
|
26
|
+
type EditorEnvironment,
|
|
27
|
+
type EditorKind,
|
|
28
|
+
} from "./init";
|
|
29
|
+
import { writeMcpConfig } from "./mcpConfig";
|
|
30
|
+
import {
|
|
31
|
+
detectPlatform,
|
|
32
|
+
installService,
|
|
33
|
+
isServiceInstalled,
|
|
34
|
+
uninstallService,
|
|
35
|
+
} from "./service";
|
|
36
|
+
import { defaultIO, isExitPrompt, type WizardIO } from "./promptHelpers";
|
|
37
|
+
import { runWhoami } from "./whoami";
|
|
38
|
+
|
|
39
|
+
const EDITOR_LABEL: Record<EditorKind, string> = {
|
|
40
|
+
cursor: "Cursor",
|
|
41
|
+
"claude-code": "Claude Code",
|
|
42
|
+
codex: "Codex",
|
|
43
|
+
"gemini-cli": "Gemini CLI",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Same permissive shape used by defaultIdentity() — accepts anything that looks like name@host.
|
|
47
|
+
const EMAIL_REGEX = /^[^@\s]+@[^@\s]+$/;
|
|
48
|
+
|
|
49
|
+
export interface SetIdentityOptions {
|
|
50
|
+
identity: string;
|
|
51
|
+
io?: WizardIO;
|
|
52
|
+
cwd?: string;
|
|
53
|
+
homeDir?: string;
|
|
54
|
+
dataDir?: string;
|
|
55
|
+
/** Skip the confirmation prompt (CI / scripted runs). */
|
|
56
|
+
yes?: boolean;
|
|
57
|
+
/** Forwarded to install/uninstallService for tests. */
|
|
58
|
+
dryRun?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SetIdentityResult {
|
|
62
|
+
status: "ok" | "no-change" | "error";
|
|
63
|
+
message: string;
|
|
64
|
+
updatedEditors: EditorKind[];
|
|
65
|
+
serviceUpdated: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runSetIdentity(opts: SetIdentityOptions): Promise<SetIdentityResult> {
|
|
69
|
+
const io = opts.io ?? defaultIO;
|
|
70
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
71
|
+
|
|
72
|
+
if (!EMAIL_REGEX.test(opts.identity)) {
|
|
73
|
+
return {
|
|
74
|
+
status: "error",
|
|
75
|
+
message:
|
|
76
|
+
`\n❌ Not a valid email: "${opts.identity}"\n` +
|
|
77
|
+
` Expected something like name@example.com\n\n`,
|
|
78
|
+
updatedEditors: [],
|
|
79
|
+
serviceUpdated: false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const current = await runWhoami({ cwd, homeDir: opts.homeDir });
|
|
84
|
+
const snapshot = await detectExistingSetup(cwd, opts.homeDir);
|
|
85
|
+
|
|
86
|
+
if (!snapshot.configured) {
|
|
87
|
+
return {
|
|
88
|
+
status: "error",
|
|
89
|
+
message: "\n❌ Krimto isn't set up here yet. Run `krimto init` first.\n\n",
|
|
90
|
+
updatedEditors: [],
|
|
91
|
+
serviceUpdated: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (current.activeIdentity === opts.identity && !current.mismatch) {
|
|
96
|
+
return {
|
|
97
|
+
status: "no-change",
|
|
98
|
+
message: `\nNo change — identity is already ${opts.identity}.\n\n`,
|
|
99
|
+
updatedEditors: [],
|
|
100
|
+
serviceUpdated: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!opts.yes) {
|
|
105
|
+
io.out("\nKrimto — Set identity\n\n");
|
|
106
|
+
io.out(` Current identity: ${current.activeIdentity}\n`);
|
|
107
|
+
io.out(` New identity: ${opts.identity}\n\n`);
|
|
108
|
+
io.out(" Will update:\n");
|
|
109
|
+
for (const e of snapshot.registeredEditors) {
|
|
110
|
+
io.out(` • ${EDITOR_LABEL[e]} MCP config\n`);
|
|
111
|
+
}
|
|
112
|
+
if (snapshot.runMode === "always-running") {
|
|
113
|
+
io.out(" • Background service (launchd/systemd/schtasks)\n");
|
|
114
|
+
}
|
|
115
|
+
io.out("\n ⚠️ Existing notes will NOT move — they stay under\n");
|
|
116
|
+
io.out(` ~/.krimto/user/${current.activeIdentity}/\n`);
|
|
117
|
+
io.out(` New facts will go to ~/.krimto/user/${opts.identity}/\n\n`);
|
|
118
|
+
try {
|
|
119
|
+
const ok = await confirm({ message: "Apply this change?", default: true });
|
|
120
|
+
if (!ok) {
|
|
121
|
+
return {
|
|
122
|
+
status: "no-change",
|
|
123
|
+
message: "\nAborted. No changes were made.\n\n",
|
|
124
|
+
updatedEditors: [],
|
|
125
|
+
serviceUpdated: false,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
if (isExitPrompt(e)) {
|
|
130
|
+
return {
|
|
131
|
+
status: "no-change",
|
|
132
|
+
message: "\nAborted. No changes were made.\n\n",
|
|
133
|
+
updatedEditors: [],
|
|
134
|
+
serviceUpdated: false,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
throw e;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const envs = await detectEditorEnvironments(cwd, opts.homeDir);
|
|
142
|
+
const updatedEditors: EditorKind[] = [];
|
|
143
|
+
for (const env of envs) {
|
|
144
|
+
if (!snapshot.registeredEditors.includes(env.editor)) continue;
|
|
145
|
+
const ok = await updateEditorIdentity(env, opts.identity);
|
|
146
|
+
if (ok) updatedEditors.push(env.editor);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let serviceUpdated = false;
|
|
150
|
+
const platform = detectPlatform();
|
|
151
|
+
const service = await isServiceInstalled(platform, opts.homeDir);
|
|
152
|
+
if (service.installed) {
|
|
153
|
+
const dataDir = opts.dataDir ?? path.join(opts.homeDir ?? "", ".krimto");
|
|
154
|
+
await uninstallService({ platform, homeDir: opts.homeDir, dryRun: opts.dryRun });
|
|
155
|
+
await installService(
|
|
156
|
+
{
|
|
157
|
+
binPath: process.execPath,
|
|
158
|
+
args: [process.argv[1] ?? "krimto", "serve"],
|
|
159
|
+
env: { KRIMTO_IDENTITY: opts.identity, KRIMTO_DATA: dataDir, KRIMTO_HTTP_PORT: "8080" },
|
|
160
|
+
homeDir: opts.homeDir,
|
|
161
|
+
},
|
|
162
|
+
{ dryRun: opts.dryRun, platform },
|
|
163
|
+
);
|
|
164
|
+
serviceUpdated = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const lines: string[] = ["", `✅ Identity changed → ${opts.identity}`, ""];
|
|
168
|
+
for (const e of updatedEditors) {
|
|
169
|
+
lines.push(` • Updated ${EDITOR_LABEL[e]} MCP config`);
|
|
170
|
+
}
|
|
171
|
+
if (serviceUpdated) lines.push(" • Restarted background service");
|
|
172
|
+
if (updatedEditors.length === 0 && !serviceUpdated) {
|
|
173
|
+
lines.push(" (Nothing to update — no editors registered + no service installed)");
|
|
174
|
+
}
|
|
175
|
+
lines.push("");
|
|
176
|
+
lines.push("Restart your editor(s) so they pick up the new identity.");
|
|
177
|
+
lines.push("");
|
|
178
|
+
lines.push(`New facts → ~/.krimto/user/${opts.identity}/`);
|
|
179
|
+
lines.push(`Old facts stay at ~/.krimto/user/${current.activeIdentity}/`);
|
|
180
|
+
lines.push("");
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
status: "ok",
|
|
184
|
+
message: lines.join("\n"),
|
|
185
|
+
updatedEditors,
|
|
186
|
+
serviceUpdated,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Update KRIMTO_IDENTITY in this editor's MCP entry.
|
|
192
|
+
* • JSON method: surgical mutation preserves any other env keys (KRIMTO_EMBED_*).
|
|
193
|
+
* • CLI method (Claude Code): rebuild + re-add via writeMcpConfig. Extra env keys are lost.
|
|
194
|
+
* • HTTP entry (url-based): no-op — identity lives in the service env.
|
|
195
|
+
* • mcpWire === null: no automated wiring; skip silently.
|
|
196
|
+
*/
|
|
197
|
+
async function updateEditorIdentity(env: EditorEnvironment, identity: string): Promise<boolean> {
|
|
198
|
+
if (env.mcpWire === null) return false;
|
|
199
|
+
if (env.mcpWire.method === "json") {
|
|
200
|
+
return updateIdentityInJson(env.mcpWire.path, env.mcpWire.key, identity);
|
|
201
|
+
}
|
|
202
|
+
const entry: KrimtoMcpEntry = { transport: "stdio", ...stdioMcpEntry({ identity }) };
|
|
203
|
+
await writeMcpConfig(env, entry);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function updateIdentityInJson(
|
|
208
|
+
filePath: string,
|
|
209
|
+
key: string,
|
|
210
|
+
identity: string,
|
|
211
|
+
): Promise<boolean> {
|
|
212
|
+
let text: string;
|
|
213
|
+
try {
|
|
214
|
+
text = await fs.readFile(filePath, "utf8");
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
let parsed: Record<string, unknown>;
|
|
219
|
+
try {
|
|
220
|
+
parsed = JSON.parse(text) as Record<string, unknown>;
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const servers = parsed[key] as Record<string, unknown> | undefined;
|
|
225
|
+
if (!servers || !("krimto" in servers)) return false;
|
|
226
|
+
const krimto = servers.krimto as { env?: Record<string, string>; url?: string };
|
|
227
|
+
if (krimto.url) return false;
|
|
228
|
+
const nextEnv = { ...(krimto.env ?? {}), KRIMTO_IDENTITY: identity };
|
|
229
|
+
(servers.krimto as { env: Record<string, string> }).env = nextEnv;
|
|
230
|
+
await fs.writeFile(filePath, JSON.stringify(parsed, null, 2) + "\n", "utf8");
|
|
231
|
+
return true;
|
|
232
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { promisify } from "node:util";
|
|
|
17
17
|
|
|
18
18
|
import { ActivityLog, type ActivityEntry } from "../server/activity";
|
|
19
19
|
import { isProcessAlive, type LockInfo } from "../server/lock";
|
|
20
|
+
import { KRIMTO_VERSION } from "../server/index";
|
|
20
21
|
import {
|
|
21
22
|
detectEditorEnvironments,
|
|
22
23
|
detectExistingSetup,
|
|
@@ -174,12 +175,12 @@ function headerLine(
|
|
|
174
175
|
): string {
|
|
175
176
|
if (status === "ok") {
|
|
176
177
|
if (lock?.alive) {
|
|
177
|
-
return `\n✅ Krimto is working ·
|
|
178
|
+
return `\n✅ Krimto is working · v${KRIMTO_VERSION}\n PID ${lock.info.pid} (${lock.info.mode}), started ${humanAgo(lock.info.started, now)}\n`;
|
|
178
179
|
}
|
|
179
|
-
return `\n✅ Krimto is configured ·
|
|
180
|
+
return `\n✅ Krimto is configured · v${KRIMTO_VERSION}\n No active server right now — it will be launched on demand by your editor.\n`;
|
|
180
181
|
}
|
|
181
182
|
if (status === "warning") {
|
|
182
|
-
return `\n⚠️ Krimto needs attention ·
|
|
183
|
+
return `\n⚠️ Krimto needs attention · v${KRIMTO_VERSION}\n`;
|
|
183
184
|
}
|
|
184
185
|
return `\n🔴 Krimto isn't set up on this machine\n Run: $ npx @krimto-labs/krimto init\n`;
|
|
185
186
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// `krimto whoami` — show the active KRIMTO_IDENTITY and every place it's set.
|
|
2
|
+
//
|
|
3
|
+
// Motivation: the v0.2.21 "data-location surprise" bug class — users end up with two scopes
|
|
4
|
+
// (`user/lpdthemes@gmail.com` and `user/user@localhost`) because different surfaces saw
|
|
5
|
+
// different identities and they only noticed weeks later. `whoami` makes drift visible
|
|
6
|
+
// before it leads to split notes.
|
|
7
|
+
|
|
8
|
+
import { promises as fs } from "node:fs";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
defaultIdentity,
|
|
12
|
+
detectEditorEnvironments,
|
|
13
|
+
type EditorEnvironment,
|
|
14
|
+
type EditorKind,
|
|
15
|
+
} from "./init";
|
|
16
|
+
import { detectPlatform, isServiceInstalled, type ServicePlatform } from "./service";
|
|
17
|
+
|
|
18
|
+
const EDITOR_LABEL: Record<EditorKind, string> = {
|
|
19
|
+
cursor: "Cursor",
|
|
20
|
+
"claude-code": "Claude Code",
|
|
21
|
+
codex: "Codex",
|
|
22
|
+
"gemini-cli": "Gemini CLI",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface WhoamiOptions {
|
|
26
|
+
cwd?: string;
|
|
27
|
+
homeDir?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IdentitySource {
|
|
31
|
+
label: string;
|
|
32
|
+
/** The literal value found; `null` when nothing readable, `"(http)"` for HTTP entries with no inline identity. */
|
|
33
|
+
identity: string | null;
|
|
34
|
+
detail?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WhoamiResult {
|
|
38
|
+
/** The identity Krimto would use for a write right now (best inference). */
|
|
39
|
+
activeIdentity: string;
|
|
40
|
+
sources: IdentitySource[];
|
|
41
|
+
/** True when registered surfaces disagree on identity. */
|
|
42
|
+
mismatch: boolean;
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runWhoami(opts: WhoamiOptions = {}): Promise<WhoamiResult> {
|
|
47
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
48
|
+
const homeDir = opts.homeDir;
|
|
49
|
+
const envs = await detectEditorEnvironments(cwd, homeDir);
|
|
50
|
+
|
|
51
|
+
const sources: IdentitySource[] = [];
|
|
52
|
+
const distinctIdentities = new Set<string>();
|
|
53
|
+
|
|
54
|
+
for (const env of envs) {
|
|
55
|
+
const found = await readIdentityFromEditor(env);
|
|
56
|
+
if (found === null) continue;
|
|
57
|
+
sources.push(found);
|
|
58
|
+
if (found.identity && found.identity !== "(http)") distinctIdentities.add(found.identity);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const platform = detectPlatform();
|
|
62
|
+
const service = await isServiceInstalled(platform, homeDir);
|
|
63
|
+
if (service.installed && service.unitPath) {
|
|
64
|
+
const id = await readIdentityFromServiceUnit(service.unitPath, platform);
|
|
65
|
+
if (id) {
|
|
66
|
+
sources.push({ label: "Background service", identity: id, detail: service.unitPath });
|
|
67
|
+
distinctIdentities.add(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const envOverride = process.env.KRIMTO_IDENTITY ?? null;
|
|
72
|
+
const gitDefault = await defaultIdentity();
|
|
73
|
+
|
|
74
|
+
// Active identity = first concrete source we found; fall back through env → git → server default.
|
|
75
|
+
const firstConcrete = sources.find((s) => s.identity && s.identity !== "(http)")?.identity;
|
|
76
|
+
const activeIdentity = firstConcrete ?? envOverride ?? gitDefault;
|
|
77
|
+
const mismatch = distinctIdentities.size > 1;
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
activeIdentity,
|
|
81
|
+
sources,
|
|
82
|
+
mismatch,
|
|
83
|
+
message: formatWhoami({ activeIdentity, sources, mismatch, envOverride, gitDefault }),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readIdentityFromEditor(env: EditorEnvironment): Promise<IdentitySource | null> {
|
|
88
|
+
if (env.mcpWire === null || env.mcpWire.method !== "json") return null;
|
|
89
|
+
let text: string;
|
|
90
|
+
try {
|
|
91
|
+
text = await fs.readFile(env.mcpWire.path, "utf8");
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
let parsed: Record<string, unknown>;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(text) as Record<string, unknown>;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const servers = parsed[env.mcpWire.key] as Record<string, unknown> | undefined;
|
|
102
|
+
if (!servers || !("krimto" in servers)) return null;
|
|
103
|
+
const krimto = servers.krimto as { env?: Record<string, string>; url?: string };
|
|
104
|
+
const label = `${EDITOR_LABEL[env.editor]} MCP config`;
|
|
105
|
+
if (krimto.env?.KRIMTO_IDENTITY) {
|
|
106
|
+
return { label, identity: krimto.env.KRIMTO_IDENTITY, detail: env.mcpWire.path };
|
|
107
|
+
}
|
|
108
|
+
if (krimto.url) {
|
|
109
|
+
return { label, identity: "(http)", detail: env.mcpWire.path };
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function readIdentityFromServiceUnit(
|
|
115
|
+
unitPath: string,
|
|
116
|
+
platform: ServicePlatform,
|
|
117
|
+
): Promise<string | null> {
|
|
118
|
+
let text: string;
|
|
119
|
+
try {
|
|
120
|
+
text = await fs.readFile(unitPath, "utf8");
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (platform === "darwin") {
|
|
125
|
+
// plist XML pattern written by installService — KRIMTO_IDENTITY appears as a <key>/<string> pair.
|
|
126
|
+
const m = text.match(/<key>KRIMTO_IDENTITY<\/key>\s*<string>([^<]+)<\/string>/);
|
|
127
|
+
return m && m[1] ? m[1] : null;
|
|
128
|
+
}
|
|
129
|
+
if (platform === "linux") {
|
|
130
|
+
const m = text.match(/KRIMTO_IDENTITY=([^"\s\n]+)/);
|
|
131
|
+
return m && m[1] ? m[1] : null;
|
|
132
|
+
}
|
|
133
|
+
// Windows: schtasks doesn't store env in a readable file we own. Skip.
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function formatWhoami(opts: {
|
|
138
|
+
activeIdentity: string;
|
|
139
|
+
sources: IdentitySource[];
|
|
140
|
+
mismatch: boolean;
|
|
141
|
+
envOverride: string | null;
|
|
142
|
+
gitDefault: string;
|
|
143
|
+
}): string {
|
|
144
|
+
const lines: string[] = ["", "Krimto — Identity", ""];
|
|
145
|
+
lines.push(` Active identity: ${opts.activeIdentity}`, "");
|
|
146
|
+
lines.push(" Where it's set:");
|
|
147
|
+
if (opts.sources.length === 0) {
|
|
148
|
+
lines.push(" (no editors registered, no service installed — run `krimto init` first)");
|
|
149
|
+
} else {
|
|
150
|
+
for (const s of opts.sources) {
|
|
151
|
+
const display = s.identity === "(http)" ? "(uses service identity)" : (s.identity ?? "(unset)");
|
|
152
|
+
const marker = s.identity === "(http)" || s.identity === opts.activeIdentity ? "✓" : "⚠";
|
|
153
|
+
lines.push(` ${marker} ${s.label.padEnd(28)} ${display}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
lines.push("");
|
|
157
|
+
lines.push(" Fallback chain (used when no source above sets it):");
|
|
158
|
+
lines.push(` KRIMTO_IDENTITY env ${opts.envOverride ?? "(unset)"}`);
|
|
159
|
+
lines.push(` git config user.email ${opts.gitDefault}`);
|
|
160
|
+
lines.push(" Server default user@localhost");
|
|
161
|
+
lines.push("");
|
|
162
|
+
if (opts.mismatch) {
|
|
163
|
+
lines.push(" ⚠️ Mismatch — different sources disagree on identity.");
|
|
164
|
+
lines.push(" Run `krimto set identity <email>` to sync them.");
|
|
165
|
+
} else if (opts.sources.length > 0) {
|
|
166
|
+
lines.push(" ✅ All sources agree.");
|
|
167
|
+
}
|
|
168
|
+
lines.push("");
|
|
169
|
+
lines.push("To change it: krimto set identity <email>");
|
|
170
|
+
lines.push("");
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
package/src/cli/wizard.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type WizardAnswers,
|
|
25
25
|
} from "./init";
|
|
26
26
|
import { runSetupEmbeddings } from "./setupEmbeddings";
|
|
27
|
+
import { KRIMTO_VERSION } from "../server/index";
|
|
27
28
|
|
|
28
29
|
const EDITOR_LABEL: Record<EditorKind, string> = {
|
|
29
30
|
cursor: "Cursor",
|
|
@@ -149,7 +150,7 @@ async function runFreshWizard(
|
|
|
149
150
|
io: WizardIO,
|
|
150
151
|
snapshot: SetupSnapshot | null = null,
|
|
151
152
|
): Promise<ApplyResult | null> {
|
|
152
|
-
io.out(
|
|
153
|
+
io.out(`\nKrimto — Setting up your AI's memory · v${KRIMTO_VERSION}\n\n`);
|
|
153
154
|
const envs = await detectEditorEnvironments(cwd, opts.homeDir);
|
|
154
155
|
printScan(envs, io);
|
|
155
156
|
|
|
@@ -193,13 +194,16 @@ async function askEditors(
|
|
|
193
194
|
value: env.editor,
|
|
194
195
|
name: EDITOR_LABEL[env.editor],
|
|
195
196
|
description: env.present
|
|
196
|
-
? "detected
|
|
197
|
-
: env.
|
|
198
|
-
? "not
|
|
199
|
-
:
|
|
197
|
+
? "detected in this project"
|
|
198
|
+
: env.installed
|
|
199
|
+
? "installed on this machine (not in this project yet)"
|
|
200
|
+
: env.mcpWire === null
|
|
201
|
+
? "not detected — manual snippet only"
|
|
202
|
+
: "not detected — toggle on if you want anyway",
|
|
203
|
+
// v0.2.21: preselect on either project-level (present) OR machine-level (installed) signal.
|
|
200
204
|
checked: snapshot
|
|
201
205
|
? snapshot.registeredEditors.includes(env.editor)
|
|
202
|
-
: env.present,
|
|
206
|
+
: env.present || env.installed,
|
|
203
207
|
})),
|
|
204
208
|
});
|
|
205
209
|
}
|
|
@@ -317,8 +321,14 @@ async function askSearch(
|
|
|
317
321
|
function printScan(envs: EditorEnvironment[], io: WizardIO): void {
|
|
318
322
|
io.out(" Scanning your machine ...\n\n");
|
|
319
323
|
for (const env of envs) {
|
|
320
|
-
|
|
321
|
-
|
|
324
|
+
// v0.2.21: three states — project-level, machine-level only, not found.
|
|
325
|
+
const dot = env.present ? "✓" : env.installed ? "~" : "–";
|
|
326
|
+
const note = env.present
|
|
327
|
+
? "detected (in this project)"
|
|
328
|
+
: env.installed
|
|
329
|
+
? "installed (machine-wide)"
|
|
330
|
+
: "not found";
|
|
331
|
+
io.out(` ${dot} ${EDITOR_LABEL[env.editor].padEnd(14)} ${note}\n`);
|
|
322
332
|
}
|
|
323
333
|
io.out("\n");
|
|
324
334
|
}
|
|
@@ -430,7 +440,9 @@ export async function runInitNonInteractive(
|
|
|
430
440
|
opts: NonInteractiveOptions = {},
|
|
431
441
|
): Promise<ApplyResult> {
|
|
432
442
|
const envs = await detectEditorEnvironments(cwd, opts.homeDir);
|
|
433
|
-
|
|
443
|
+
// v0.2.21: count both project-level (`present`) AND machine-level (`installed`) signals so
|
|
444
|
+
// the --yes path matches the interactive wizard's preselect logic.
|
|
445
|
+
const detected = envs.filter((e) => e.present || e.installed).map((e) => e.editor);
|
|
434
446
|
const editors = opts.editors ?? (detected.length > 0 ? detected : envs.map((e) => e.editor));
|
|
435
447
|
const runMode = opts.runMode ?? (editors.length >= 2 ? "always-running" : "as-needed");
|
|
436
448
|
const search = opts.search ?? "keyword";
|
package/src/server/index.ts
CHANGED
|
@@ -48,7 +48,7 @@ import { type Requester } from "../access/scope";
|
|
|
48
48
|
|
|
49
49
|
export type RequesterResolver = (extra: { authInfo?: AuthInfo }) => Requester;
|
|
50
50
|
|
|
51
|
-
export const KRIMTO_VERSION = "0.2.
|
|
51
|
+
export const KRIMTO_VERSION = "0.2.22";
|
|
52
52
|
|
|
53
53
|
export function resolveDataDir(): string {
|
|
54
54
|
return process.env.KRIMTO_DATA ?? path.join(homedir(), ".krimto");
|