@krimto-labs/krimto 0.2.6 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -26
- package/bin/krimto.mjs +118 -2
- package/package.json +2 -2
- package/src/agentRule.ts +64 -0
- package/src/cli/connect.ts +49 -0
- package/src/cli/help.ts +57 -0
- package/src/cli/init.ts +110 -0
- package/src/cli/setupEmbeddings.ts +89 -0
- package/src/cli/setupRemote.ts +63 -0
- package/src/cli/storage.ts +94 -0
- package/src/cli/uninit.ts +42 -0
- package/src/cli/usage.ts +90 -0
- package/src/cli/verifyConnection.ts +93 -0
- package/src/server/activity.ts +63 -0
- package/src/server/banner.ts +46 -4
- package/src/server/connect.ts +26 -0
- package/src/server/http.ts +4 -0
- package/src/server/index.ts +38 -6
- package/src/server/lock.ts +101 -0
- package/src/server/tools.ts +51 -2
- package/src/storage/store.ts +5 -0
- package/src/web/router.ts +15 -3
- package/src/web/views.ts +175 -11
package/README.md
CHANGED
|
@@ -9,18 +9,21 @@ place and reads the right slice of it — Alice's preferences override the team'
|
|
|
9
9
|
conventions override the org's standards, and every fact carries a paper trail (author, source,
|
|
10
10
|
timestamp, reviewer).
|
|
11
11
|
|
|
12
|
-
> **Where we are:** this is the **v0.2** surface. Here today: the markdown-in-git storage layer, the
|
|
13
|
-
> `user → team → org` hierarchy, hybrid retrieval, server-enforced access, two-way git sync,
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
12
|
+
> **Where we are:** this is the **v0.2.9** surface. Here today: the markdown-in-git storage layer, the
|
|
13
|
+
> `user → team → org` hierarchy, hybrid retrieval, server-enforced access, two-way git sync, the MCP
|
|
14
|
+
> server over **stdio + HTTP** (Bearer API-key auth on HTTP), a **published multi-arch Docker image**
|
|
15
|
+
> (`ghcr.io/krimto-labs/krimto`), a **web UI** with browse/search/admin/diagnostics, and a complete
|
|
16
|
+
> **CLI** (`serve`, `connect`, `init`, `usage`, `storage`, `setup-remote`, `setup-embeddings`,
|
|
17
|
+
> `verify-connection`, `uninit`, `where`, `--help`). We claim the team-memory position now and fulfil
|
|
18
|
+
> it in the open — see [ROADMAP.md](ROADMAP.md) and [CHANGELOG.md](CHANGELOG.md) for what each release adds.
|
|
17
19
|
|
|
18
20
|
## Try it in 2 minutes (solo, no account)
|
|
19
21
|
|
|
20
|
-
1. **Run it** (data stays in `~/.krimto`):
|
|
22
|
+
1. **Run it** — one command, no clone, no Docker (data stays in `~/.krimto`):
|
|
21
23
|
```bash
|
|
22
|
-
|
|
24
|
+
npx @krimto-labs/krimto serve
|
|
23
25
|
```
|
|
26
|
+
*Prefer Docker?* `docker run -d -p 8080:8080 -v ~/.krimto:/data ghcr.io/krimto-labs/krimto:latest`
|
|
24
27
|
2. **Point Claude Code at it — one line, no key:**
|
|
25
28
|
```json
|
|
26
29
|
{ "mcpServers": { "krimto": { "url": "http://localhost:8080/mcp" } } }
|
|
@@ -28,9 +31,10 @@ timestamp, reviewer).
|
|
|
28
31
|
(or run `claude mcp add --transport http krimto http://localhost:8080/mcp`)
|
|
29
32
|
3. Tell your agent: **"remember that our staging DB resets every Sunday."** Then ask in a *new* chat:
|
|
30
33
|
**"what do you know about staging?"** — it remembers.
|
|
31
|
-
4. Open **http://localhost:8080** to browse.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
4. Open **http://localhost:8080** to browse. The dashboard shows your facts, a *Status* row (git
|
|
35
|
+
remote + embeddings health), and a *Recent activity* feed (so you can see your agent calling
|
|
36
|
+
Krimto in real time). That's the *personal* layer — Krimto's point is the **team** layer:
|
|
37
|
+
restart with `KRIMTO_BOOTSTRAP_ADMIN=you@acme.com` to turn on accounts and invite teammates.
|
|
34
38
|
|
|
35
39
|
## Connect your agent
|
|
36
40
|
|
|
@@ -62,9 +66,20 @@ mode); those are best-effort and not yet individually verified.
|
|
|
62
66
|
|
|
63
67
|
### Make it automatic
|
|
64
68
|
|
|
65
|
-
By default your agent uses Krimto only when you ask
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
By default your agent uses Krimto only when you ask ("use krimto to remember X"). One command, run
|
|
70
|
+
once in your project, makes it call Krimto **on its own** (recall before tasks, write when you say
|
|
71
|
+
"remember"):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx @krimto-labs/krimto init
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`init` auto-detects which editor you're using (`.cursor/`, `.claude/`, existing `CLAUDE.md` /
|
|
78
|
+
`AGENTS.md` / `GEMINI.md`, `gemini-extension.json`) and writes the rule only into the files that
|
|
79
|
+
match. Pass `--all` to write to every supported rules file. Change your mind later? Run
|
|
80
|
+
`npx @krimto-labs/krimto uninit` to cleanly strip the rule (and delete files that held only it).
|
|
81
|
+
|
|
82
|
+
The rule it writes:
|
|
68
83
|
|
|
69
84
|
```
|
|
70
85
|
# Krimto memory — always use
|
|
@@ -77,6 +92,29 @@ Codex: `AGENTS.md`; Gemini CLI: `GEMINI.md`:
|
|
|
77
92
|
|
|
78
93
|
The in-product **Connect** page (`/ui/connect`) shows this same rule with a copy button.
|
|
79
94
|
|
|
95
|
+
### The CLI surface
|
|
96
|
+
|
|
97
|
+
Everything is reachable via `npx`. Run `npx @krimto-labs/krimto --help` for the full list. Briefly:
|
|
98
|
+
|
|
99
|
+
| Command | What it does |
|
|
100
|
+
|---|---|
|
|
101
|
+
| (no args) | Start the stdio MCP server (default; for MCP clients to launch) |
|
|
102
|
+
| `serve` | Start the HTTP server (port 8080) — `/ui` dashboard, no clone, no Docker |
|
|
103
|
+
| `connect` | Print copy-paste Claude Code + Cursor config snippets |
|
|
104
|
+
| `init [--all]` | Switch this project to AUTO MODE — write the always-use rule (auto-detects editor; `--all` writes every file) |
|
|
105
|
+
| `uninit` | Switch back to DEFAULT MODE — remove the rule, delete files it created |
|
|
106
|
+
| `usage` | Show the five `krimto_*` tools with chat examples for both modes |
|
|
107
|
+
| `storage` | Explain where Krimto keeps your data (markdown / git / index), how to verify, optional add-ons |
|
|
108
|
+
| `setup-remote <url>` | Wire the data dir to a git remote and verify the initial push |
|
|
109
|
+
| `setup-embeddings` | Send a real test embedding to verify a `KRIMTO_EMBED_*` config |
|
|
110
|
+
| `verify-connection` | Diagnose "is my agent calling Krimto?" (lock status + last 5 calls) |
|
|
111
|
+
| `where` | Print the data directory |
|
|
112
|
+
| `--help`, `-h` | Show the full CLI surface |
|
|
113
|
+
|
|
114
|
+
The stdio entrypoint enforces a **single-writer lock** on the data dir (`.krimto/lock.json`) — two
|
|
115
|
+
Krimto processes can no longer race on the same `~/.krimto`. A second `serve`/stdio launch is
|
|
116
|
+
refused with a precise error pointing at the holder's PID.
|
|
117
|
+
|
|
80
118
|
## How it works
|
|
81
119
|
|
|
82
120
|
Three layers, one source of truth:
|
|
@@ -128,8 +166,21 @@ OpenClaw, Cline use the same shape):
|
|
|
128
166
|
```
|
|
129
167
|
|
|
130
168
|
The first run downloads dependencies (including `better-sqlite3`, which ships prebuilt binaries), then
|
|
131
|
-
starts the **stdio** server with data in `~/.krimto` (override with `KRIMTO_DATA
|
|
132
|
-
|
|
169
|
+
starts the **stdio** server with data in `~/.krimto` (override with `KRIMTO_DATA`; run
|
|
170
|
+
`npx @krimto-labs/krimto where` to print the exact path). Want the browser dashboard too? Stop the
|
|
171
|
+
stdio process and run `npx @krimto-labs/krimto serve` (Option B) — same data folder, plus `/ui`.
|
|
172
|
+
|
|
173
|
+
**Make your agent actually use Krimto.** By default an editor's agent routes "remember X" to its own
|
|
174
|
+
built-in memory. Run this once in your project so it calls Krimto instead:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx @krimto-labs/krimto init
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
It auto-detects which editor you're using and writes the idempotent "always use Krimto" rule into the
|
|
181
|
+
matching rules file (`CLAUDE.md` / `AGENTS.md` / `GEMINI.md` / `.cursor/rules/krimto.mdc`). Pass
|
|
182
|
+
`--all` to write all four. Run `npx @krimto-labs/krimto uninit` to cleanly remove the rule later.
|
|
183
|
+
Restart your editor afterward.
|
|
133
184
|
|
|
134
185
|
From a clone instead of npm, swap the command for `pnpm`:
|
|
135
186
|
|
|
@@ -148,16 +199,23 @@ From a clone instead of npm, swap the command for `pnpm`:
|
|
|
148
199
|
`KRIMTO_IDENTITY` is who the agent writes as (fact author + access scope). Stdio mode has **no network
|
|
149
200
|
auth** — run it locally/trusted.
|
|
150
201
|
|
|
151
|
-
### Option B — over HTTP
|
|
202
|
+
### Option B — over HTTP (browser dashboard + optional team auth)
|
|
152
203
|
|
|
153
|
-
For
|
|
204
|
+
For solo with a browser UI, or a shared/networked deployment with bearer auth. **No clone, no
|
|
205
|
+
Docker** — `npx` starts the HTTP server (default port 8080):
|
|
154
206
|
|
|
155
207
|
```bash
|
|
156
|
-
|
|
208
|
+
# Solo, local-only (no auth, no login on /ui)
|
|
209
|
+
npx @krimto-labs/krimto serve
|
|
210
|
+
|
|
211
|
+
# Team mode (first run prints an admin API key once)
|
|
212
|
+
KRIMTO_BOOTSTRAP_ADMIN=you@acme.com npx @krimto-labs/krimto serve
|
|
157
213
|
# → "issued admin API key for you@acme.com (shown once): krm_live_…"
|
|
158
214
|
# MCP at http://localhost:8080/mcp ; health at http://localhost:8080/health/ready
|
|
159
215
|
```
|
|
160
216
|
|
|
217
|
+
(`pnpm dev` is the dev-mode equivalent — only useful when working on Krimto itself.)
|
|
218
|
+
|
|
161
219
|
Then point your agent at it with that key:
|
|
162
220
|
|
|
163
221
|
```json
|
|
@@ -225,12 +283,25 @@ docker run -d --name krimto -p 8080:8080 \
|
|
|
225
283
|
|
|
226
284
|
### Web UI (humans)
|
|
227
285
|
|
|
228
|
-
When the HTTP server is running, open `http://localhost:8080/ui
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
286
|
+
When the HTTP server is running, open `http://localhost:8080/ui`. In local mode (no
|
|
287
|
+
`KRIMTO_BOOTSTRAP_ADMIN`), there's no login. In team mode, sign in with any Krimto API key.
|
|
288
|
+
|
|
289
|
+
The dashboard shows:
|
|
290
|
+
|
|
291
|
+
- **How Krimto works** — personal → team → org explainer for first-time users.
|
|
292
|
+
- **Behind the scenes — your data, your files** — names the data folder; reminds you it's just
|
|
293
|
+
markdown in git that you can open in any editor.
|
|
294
|
+
- **Status** — green/gold/red dots for the two optional add-ons (git remote sync; semantic embeddings).
|
|
295
|
+
- **Recent activity** — last 5 MCP tool calls (tool, detail, caller, relative timestamp). Critical
|
|
296
|
+
for diagnosing "did my agent actually search?" without grepping stderr.
|
|
297
|
+
- **Fact list / detail** — browse and search the facts you're allowed to see; the detail page shows
|
|
298
|
+
the absolute source-file path so you can open the underlying `.md` in any editor.
|
|
299
|
+
- **API keys** — issue/revoke your own keys (team mode).
|
|
300
|
+
- **Team admin** (`/ui/admin`, org admins only) — add members, manage teams, issue keys for others.
|
|
301
|
+
|
|
302
|
+
The UI reuses the same access control as the MCP tools — you only ever see what you can read. Set
|
|
303
|
+
`KRIMTO_SESSION_SECRET` to keep sessions valid across restarts (otherwise a random per-boot secret is
|
|
304
|
+
used). The UI is read-only for facts; editing with a review/approval flow lands in v0.3.
|
|
234
305
|
|
|
235
306
|
## The eight promises (current status)
|
|
236
307
|
|
|
@@ -240,9 +311,9 @@ v0.3.
|
|
|
240
311
|
| 2 | Hierarchical scope (`user`/`team`/`org`) as primary primitive | ✓ v0.2 |
|
|
241
312
|
| 3 | Cross-vendor SDK (MCP server + per-marketplace plugins) | ✓ MCP server over stdio + HTTP v0.2; native plugins planned |
|
|
242
313
|
| 4 | Attribution baked into every fact | ✓ v0.2 |
|
|
243
|
-
| 5 | Self-hostable, single Docker | ✓ v0.2 —
|
|
314
|
+
| 5 | Self-hostable, single Docker | ✓ v0.2 — published multi-arch image at `ghcr.io/krimto-labs/krimto`; also `npx krimto serve` (no Docker needed) and `pnpm dev` |
|
|
244
315
|
| 6 | Apache-2.0 — fully open, no rug-pull | ✓ |
|
|
245
|
-
| 7 | Web interface for humans, on top of git | ✓
|
|
316
|
+
| 7 | Web interface for humans, on top of git | ✓ v0.2.8 — `/ui` with browse/search, fact detail (with source-file path), status panel, recent-activity feed, API keys, team admin; full editing + PR approval in v0.3 |
|
|
246
317
|
| 8 | Zero-friction migration between self-hosted and Cloud | ⏳ full flow with v1.0 Cloud (`git clone` works today) |
|
|
247
318
|
|
|
248
319
|
## How Krimto compares
|
package/bin/krimto.mjs
CHANGED
|
@@ -7,8 +7,124 @@ import process from "node:process";
|
|
|
7
7
|
import { tsImport } from "tsx/esm/api";
|
|
8
8
|
|
|
9
9
|
try {
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const cmd = process.argv[2];
|
|
11
|
+
if (cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
12
|
+
// `krimto --help` — surface every subcommand so a user who didn't read the README can still
|
|
13
|
+
// discover them. Version is read from the server module so it never drifts from KRIMTO_VERSION.
|
|
14
|
+
const { formatHelp } = await tsImport("../src/cli/help.ts", import.meta.url);
|
|
15
|
+
const { KRIMTO_VERSION } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
16
|
+
process.stdout.write(formatHelp(KRIMTO_VERSION));
|
|
17
|
+
} else if (cmd === "init") {
|
|
18
|
+
// `krimto init` — drop the always-use-Krimto rule into this project's agent rules files.
|
|
19
|
+
// By default, auto-detects the editor (.cursor/, CLAUDE.md, etc.) and writes only matching
|
|
20
|
+
// files. Pass `--all` to write to every supported rules file regardless of signals.
|
|
21
|
+
const all = process.argv.slice(3).includes("--all");
|
|
22
|
+
const { runInit } = await tsImport("../src/cli/init.ts", import.meta.url);
|
|
23
|
+
const res = await runInit(process.cwd(), { all });
|
|
24
|
+
if (res.written.length === 0) {
|
|
25
|
+
const why = res.detected
|
|
26
|
+
? `(detected: ${res.considered.join(", ")}). Re-run with --all to also write the others.`
|
|
27
|
+
: "";
|
|
28
|
+
process.stderr.write(`krimto: agent rules already up to date — nothing to change. ${why}\n`);
|
|
29
|
+
} else {
|
|
30
|
+
const detectionLine = res.detected
|
|
31
|
+
? `Detected editor signals in this project — writing only the matching rule files.\n` +
|
|
32
|
+
`(Run with --all to write all 4 supported rule files instead.)\n\n`
|
|
33
|
+
: !all
|
|
34
|
+
? `No editor signals found — writing all supported rule files. Re-run with --all to\n` +
|
|
35
|
+
`keep this behavior explicitly, or remove any you don't need with \`krimto uninit\`.\n\n`
|
|
36
|
+
: "";
|
|
37
|
+
process.stderr.write(
|
|
38
|
+
detectionLine +
|
|
39
|
+
`krimto: wrote the always-use-Krimto rule to:\n ${res.written.join("\n ")}\n` +
|
|
40
|
+
"\n" +
|
|
41
|
+
"What changed: a short marker-delimited block was added to each file telling your\n" +
|
|
42
|
+
"agent to call krimto_recall before tasks and krimto_write when you say \"remember\".\n" +
|
|
43
|
+
"Existing content was preserved; running `init` again is a no-op.\n" +
|
|
44
|
+
"\n" +
|
|
45
|
+
"To remove the rule: delete the block between <!-- krimto:start --> and\n" +
|
|
46
|
+
"<!-- krimto:end --> in each file above. Nothing else in those files will be affected.\n" +
|
|
47
|
+
"(Or run `krimto uninit` to remove the rule from every file.)\n" +
|
|
48
|
+
"\n" +
|
|
49
|
+
"Restart your editor so the rule takes effect.\n",
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
} else if (cmd === "uninit") {
|
|
53
|
+
// `krimto uninit` — remove the always-use-Krimto rule from this project's rules files,
|
|
54
|
+
// flipping the project back from AUTO MODE to DEFAULT MODE.
|
|
55
|
+
const { runUninit } = await tsImport("../src/cli/uninit.ts", import.meta.url);
|
|
56
|
+
const res = await runUninit(process.cwd());
|
|
57
|
+
if (res.cleaned.length === 0) {
|
|
58
|
+
process.stderr.write("krimto: no Krimto rule found — nothing to remove.\n");
|
|
59
|
+
} else {
|
|
60
|
+
const rewritten = res.cleaned.filter((f) => !res.deleted.includes(f));
|
|
61
|
+
const lines = ["krimto: removed the always-use-Krimto rule."];
|
|
62
|
+
if (rewritten.length > 0) lines.push("Rule block stripped (file kept):", ...rewritten.map((f) => ` ${f}`));
|
|
63
|
+
if (res.deleted.length > 0) lines.push("File deleted (it held only our rule):", ...res.deleted.map((f) => ` ${f}`));
|
|
64
|
+
lines.push("", "Your project is back in DEFAULT MODE: Krimto tools are still wired up, but");
|
|
65
|
+
lines.push("your agent will only call them when you explicitly ask.");
|
|
66
|
+
lines.push("Restart your editor so it picks up the change.");
|
|
67
|
+
process.stderr.write(lines.join("\n") + "\n");
|
|
68
|
+
}
|
|
69
|
+
} else if (cmd === "where") {
|
|
70
|
+
// `krimto where` — print the data directory (honors KRIMTO_DATA), so files aren't a surprise.
|
|
71
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
72
|
+
process.stdout.write(`${resolveDataDir()}\n`);
|
|
73
|
+
} else if (cmd === "setup-remote") {
|
|
74
|
+
// `krimto setup-remote <url>` — point the data dir's git repo at a remote and verify a push.
|
|
75
|
+
// Krimto must NOT be running while this is invoked (locks the .git/ index).
|
|
76
|
+
const url = process.argv[3];
|
|
77
|
+
if (!url) {
|
|
78
|
+
process.stderr.write("Usage: krimto setup-remote <git-remote-url>\n e.g. krimto setup-remote git@github.com:acme/krimto-data.git\n");
|
|
79
|
+
process.exit(2);
|
|
80
|
+
}
|
|
81
|
+
const { runSetupRemote } = await tsImport("../src/cli/setupRemote.ts", import.meta.url);
|
|
82
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
83
|
+
const result = await runSetupRemote(resolveDataDir(), url);
|
|
84
|
+
process.stdout.write(result.message + "\n");
|
|
85
|
+
if (result.status !== "ok") process.exitCode = 1;
|
|
86
|
+
} else if (cmd === "verify-connection") {
|
|
87
|
+
// `krimto verify-connection` — read the lockfile + activity JSONL to answer "is my agent
|
|
88
|
+
// actually calling Krimto right now?" Works from any terminal regardless of how Krimto launched.
|
|
89
|
+
const { runVerifyConnection } = await tsImport("../src/cli/verifyConnection.ts", import.meta.url);
|
|
90
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
91
|
+
const result = await runVerifyConnection(resolveDataDir());
|
|
92
|
+
process.stdout.write(result.message);
|
|
93
|
+
if (result.status === "none") process.exitCode = 1;
|
|
94
|
+
} else if (cmd === "setup-embeddings") {
|
|
95
|
+
// `krimto setup-embeddings` — verify KRIMTO_EMBED_* config by sending one real test embedding,
|
|
96
|
+
// so the user finds out about a bad key now, not after they've turned embeddings on.
|
|
97
|
+
const { runSetupEmbeddings } = await tsImport("../src/cli/setupEmbeddings.ts", import.meta.url);
|
|
98
|
+
const result = await runSetupEmbeddings();
|
|
99
|
+
process.stdout.write(result.message + "\n");
|
|
100
|
+
if (result.status !== "ok") process.exitCode = 1;
|
|
101
|
+
} else if (cmd === "storage") {
|
|
102
|
+
// `krimto storage` — explain where Krimto keeps data (markdown / git / index) in plain English,
|
|
103
|
+
// so the "you own your data" half of Krimto's pitch is reachable without reading the README.
|
|
104
|
+
const { formatStorage } = await tsImport("../src/cli/storage.ts", import.meta.url);
|
|
105
|
+
const { resolveDataDir } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
106
|
+
process.stdout.write(formatStorage(resolveDataDir()));
|
|
107
|
+
} else if (cmd === "serve") {
|
|
108
|
+
// `krimto serve` — boot the HTTP server (with /ui and /ui/connect) from the npx on-ramp,
|
|
109
|
+
// so a stranger doesn't have to clone the repo or install Docker just to see the dashboard.
|
|
110
|
+
// Defaults to port 8080; honors an existing KRIMTO_HTTP_PORT if the caller set one.
|
|
111
|
+
if (!process.env.KRIMTO_HTTP_PORT) process.env.KRIMTO_HTTP_PORT = "8080";
|
|
112
|
+
const mod = await tsImport("../src/server/index.ts", import.meta.url);
|
|
113
|
+
await mod.main();
|
|
114
|
+
} else if (cmd === "usage") {
|
|
115
|
+
// `krimto usage` — the long-form guide: the five tools, both modes, copy-paste examples.
|
|
116
|
+
const { formatUsage } = await tsImport("../src/cli/usage.ts", import.meta.url);
|
|
117
|
+
const { KRIMTO_VERSION } = await tsImport("../src/server/index.ts", import.meta.url);
|
|
118
|
+
process.stdout.write(formatUsage(KRIMTO_VERSION));
|
|
119
|
+
} else if (cmd === "connect") {
|
|
120
|
+
// `krimto connect` — print stdio connect snippets (the npx on-ramp shape), so a solo user
|
|
121
|
+
// doesn't have to chase the README. Honors KRIMTO_IDENTITY when set.
|
|
122
|
+
const { formatConnect } = await tsImport("../src/cli/connect.ts", import.meta.url);
|
|
123
|
+
process.stdout.write(formatConnect({ identity: process.env.KRIMTO_IDENTITY }));
|
|
124
|
+
} else {
|
|
125
|
+
const mod = await tsImport("../src/server/index.ts", import.meta.url);
|
|
126
|
+
await mod.main();
|
|
127
|
+
}
|
|
12
128
|
} catch (e) {
|
|
13
129
|
process.stderr.write(`krimto: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
14
130
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krimto-labs/krimto",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Open-source team memory layer for AI agents — markdown files in git, user/team/org hierarchy, cross-vendor MCP server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/krimto-labs/krimto.git"
|
|
13
|
+
"url": "git+https://github.com/krimto-labs/krimto.git"
|
|
14
14
|
},
|
|
15
15
|
"homepage": "https://krimto.com",
|
|
16
16
|
"bin": {
|
package/src/agentRule.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// The standing "always use Krimto" rule, plus idempotent application to an agent's rules file.
|
|
2
|
+
// Single source of truth for both the CLI (`krimto init`) and the web connect panel — so the
|
|
3
|
+
// rule a user pastes from /ui/connect and the rule `krimto init` writes can never drift.
|
|
4
|
+
//
|
|
5
|
+
// This is the fix for the discovery problem: by default, an editor's agent routes "remember X" to
|
|
6
|
+
// its own built-in memory, not Krimto. Dropping this rule into the agent's rules file makes the
|
|
7
|
+
// agent call krimto_recall / krimto_write on its own.
|
|
8
|
+
|
|
9
|
+
export const AGENT_RULE = `# Krimto memory — always use
|
|
10
|
+
- Before a task, call krimto_recall to load what we already know.
|
|
11
|
+
- When I say "remember", or you learn a durable fact, call krimto_write
|
|
12
|
+
(user/me = personal, team/<slug> = shared).
|
|
13
|
+
- Respect precedence: user beats team beats org.
|
|
14
|
+
- Don't save secrets or one-off chatter.`;
|
|
15
|
+
|
|
16
|
+
const START = "<!-- krimto:start -->";
|
|
17
|
+
const END = "<!-- krimto:end -->";
|
|
18
|
+
|
|
19
|
+
/** The rule wrapped in stable markers, so it can be found and updated in place later. */
|
|
20
|
+
export function ruleBlock(): string {
|
|
21
|
+
return `${START}\n${AGENT_RULE}\n${END}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Insert or refresh the Krimto rule block in an agent rules file's content, idempotently:
|
|
26
|
+
* - null/blank existing → just the block (with a trailing newline)
|
|
27
|
+
* - existing WITHOUT our markers → append the block, preserving all existing content
|
|
28
|
+
* - existing WITH our markers → replace only the marked block, preserving the rest
|
|
29
|
+
* Re-applying the same rule yields identical content (so callers can detect a no-op).
|
|
30
|
+
*/
|
|
31
|
+
export function applyRule(existing: string | null): string {
|
|
32
|
+
const block = ruleBlock();
|
|
33
|
+
if (!existing || existing.trim() === "") return `${block}\n`;
|
|
34
|
+
|
|
35
|
+
const startIdx = existing.indexOf(START);
|
|
36
|
+
const endIdx = existing.indexOf(END);
|
|
37
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
38
|
+
return existing.slice(0, startIdx) + block + existing.slice(endIdx + END.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sep = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
42
|
+
return `${existing}${sep}${block}\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Inverse of `applyRule` — remove the marker-delimited Krimto rule block.
|
|
47
|
+
* Returns:
|
|
48
|
+
* - the original string when no markers are found (caller treats as no-op)
|
|
49
|
+
* - the cleaned content (markers + everything between them stripped) otherwise
|
|
50
|
+
* - `null` when removing the block leaves the file empty/whitespace-only,
|
|
51
|
+
* signalling "delete this file" (it had no pre-existing content)
|
|
52
|
+
*/
|
|
53
|
+
export function removeRule(existing: string | null): string | null {
|
|
54
|
+
if (existing === null) return null;
|
|
55
|
+
const startIdx = existing.indexOf(START);
|
|
56
|
+
const endIdx = existing.indexOf(END);
|
|
57
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return existing;
|
|
58
|
+
|
|
59
|
+
// Strip the block. Also consume one trailing newline so re-applying doesn't leave a blank gap.
|
|
60
|
+
const after = endIdx + END.length;
|
|
61
|
+
const trimmedAfter = after < existing.length && existing[after] === "\n" ? after + 1 : after;
|
|
62
|
+
const cleaned = existing.slice(0, startIdx) + existing.slice(trimmedAfter);
|
|
63
|
+
return cleaned.trim() === "" ? null : cleaned;
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// `krimto connect` — print the stdio connect snippets so a user on the npx on-ramp doesn't have to
|
|
2
|
+
// dig the README out of a closed browser tab. The shapes come from `stdioConnectSnippets` so this
|
|
3
|
+
// surface and the /ui/connect panel can never drift.
|
|
4
|
+
|
|
5
|
+
import { stdioConnectSnippets } from "../server/connect";
|
|
6
|
+
|
|
7
|
+
export interface ConnectOpts {
|
|
8
|
+
/** Identity to embed in the Cursor env block. Defaults to a generic placeholder. */
|
|
9
|
+
identity?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Build the multi-line, copy-pasteable output `krimto connect` prints to stdout. */
|
|
13
|
+
export function formatConnect(opts: ConnectOpts = {}): string {
|
|
14
|
+
const { claude, cursorJson } = stdioConnectSnippets(opts);
|
|
15
|
+
return [
|
|
16
|
+
"Connect your agent to Krimto (solo, stdio — no key needed):",
|
|
17
|
+
"",
|
|
18
|
+
"Claude Code:",
|
|
19
|
+
` ${claude}`,
|
|
20
|
+
"",
|
|
21
|
+
"Cursor (~/.cursor/mcp.json):",
|
|
22
|
+
...cursorJson.split("\n").map((line) => ` ${line}`),
|
|
23
|
+
"",
|
|
24
|
+
"What you get after pasting the snippet above:",
|
|
25
|
+
" The five Krimto tools (write, recall, read, supersede, list_scopes) are",
|
|
26
|
+
" available to your agent — but it only calls them when YOU explicitly ask,",
|
|
27
|
+
" e.g. \"use krimto to remember X\" or \"use krimto to recall what we know\".",
|
|
28
|
+
"",
|
|
29
|
+
"Want your agent to use Krimto AUTOMATICALLY (recall before tasks, save what",
|
|
30
|
+
"it learns when you say \"remember\")? Also run this once per project:",
|
|
31
|
+
" npx @krimto-labs/krimto init",
|
|
32
|
+
"",
|
|
33
|
+
" Skip `init` if you only want Krimto on-demand. The rule it writes can be",
|
|
34
|
+
" removed at any time — `krimto init` will explain how when you run it.",
|
|
35
|
+
"",
|
|
36
|
+
"Optional add-ons — add these to the `env` block above if you want them:",
|
|
37
|
+
' "KRIMTO_GIT_REMOTE": "git@github.com:acme/krimto-data.git"',
|
|
38
|
+
" ↑ pushes every batch to a private git remote (team sync across machines)",
|
|
39
|
+
" Set this up safely first: `krimto setup-remote <url>` verifies the push works.",
|
|
40
|
+
' "KRIMTO_EMBED_PROVIDER": "openai"',
|
|
41
|
+
' "KRIMTO_EMBED_API_KEY": "sk-..."',
|
|
42
|
+
" ↑ turns on semantic / vector search (recall matches paraphrases, not just",
|
|
43
|
+
" keywords). Verify the provider works first: `krimto setup-embeddings`.",
|
|
44
|
+
" Both are optional — Krimto works fully without them.",
|
|
45
|
+
"",
|
|
46
|
+
"For team mode (HTTP + bearer auth), start the server and open /ui/connect.",
|
|
47
|
+
"",
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
package/src/cli/help.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// `krimto --help` — list every subcommand a user could otherwise only discover by reading the README.
|
|
2
|
+
// Single source of truth so bin/krimto.mjs and any future surface can't drift.
|
|
3
|
+
|
|
4
|
+
/** Build the help text printed by `krimto --help` / `krimto -h` / `krimto help`. */
|
|
5
|
+
export function formatHelp(version: string): string {
|
|
6
|
+
return [
|
|
7
|
+
`krimto — open-source team memory layer for AI coding agents (v${version})`,
|
|
8
|
+
"",
|
|
9
|
+
"TWO WAYS TO USE KRIMTO",
|
|
10
|
+
" DEFAULT MODE — your agent uses Krimto only when you explicitly ask",
|
|
11
|
+
' ("use krimto to remember X" / "use krimto to recall what we know").',
|
|
12
|
+
" Setup: paste the `connect` snippet into your editor's MCP config.",
|
|
13
|
+
" Want the browser dashboard too? Run `serve` (no Docker / no clone needed).",
|
|
14
|
+
"",
|
|
15
|
+
" AUTO MODE — your agent uses Krimto automatically: recall before tasks,",
|
|
16
|
+
' save what it learns when you say "remember".',
|
|
17
|
+
" Setup: `connect` (as above), THEN run `init` once in your project.",
|
|
18
|
+
" Switch back to DEFAULT MODE at any time with `uninit`.",
|
|
19
|
+
"",
|
|
20
|
+
"Usage:",
|
|
21
|
+
" npx @krimto-labs/krimto [command]",
|
|
22
|
+
"",
|
|
23
|
+
"Commands:",
|
|
24
|
+
" (no args) Start the stdio MCP server (default; for MCP clients to launch)",
|
|
25
|
+
" serve Start the HTTP server (port 8080) — gives you /ui dashboard",
|
|
26
|
+
" → use this if you want the browser UI without Docker / pnpm",
|
|
27
|
+
" connect Print copy-paste config snippets for Claude Code & Cursor",
|
|
28
|
+
" → use this once per editor / per machine",
|
|
29
|
+
" init [--all] Switch this project to AUTO MODE — write the always-use rule",
|
|
30
|
+
" → use this once per project (auto-detects your editor; --all = every file)",
|
|
31
|
+
" uninit Switch this project back to DEFAULT MODE — remove the rule",
|
|
32
|
+
" → cleanly reverses `init` (deletes files it created)",
|
|
33
|
+
" usage Show the five krimto_* tools and chat examples for both modes",
|
|
34
|
+
" → read this AFTER `connect` to learn how to actually use Krimto",
|
|
35
|
+
" storage Explain where Krimto keeps your data (markdown / git / index)",
|
|
36
|
+
" → run this to see what's on your disk and what you can edit",
|
|
37
|
+
" setup-remote Wire the data dir to a git remote and verify the push works",
|
|
38
|
+
" → one-time setup for cross-machine / teammate sync",
|
|
39
|
+
" setup-embeddings",
|
|
40
|
+
" Verify a KRIMTO_EMBED_* config by sending a real test embedding",
|
|
41
|
+
" → one-time check before turning on semantic / vector search",
|
|
42
|
+
" verify-connection",
|
|
43
|
+
" Diagnose if your agent is actually calling Krimto (live status + last 5 calls)",
|
|
44
|
+
" → run this when 'I asked the agent but recall returned nothing'",
|
|
45
|
+
" where Print the Krimto data directory",
|
|
46
|
+
" --help, -h Show this help",
|
|
47
|
+
"",
|
|
48
|
+
"Environment:",
|
|
49
|
+
" KRIMTO_DATA Override the data directory (default: ~/.krimto)",
|
|
50
|
+
" KRIMTO_IDENTITY Identity used for writes (default: user@localhost)",
|
|
51
|
+
" KRIMTO_HTTP_PORT Serve MCP over HTTP on this port instead of stdio",
|
|
52
|
+
" KRIMTO_BOOTSTRAP_ADMIN First-admin email — turns on team mode + key auth",
|
|
53
|
+
"",
|
|
54
|
+
"Learn more: https://krimto.com · https://github.com/krimto-labs/krimto",
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// `krimto init` — drop the always-use-Krimto standing rule into a project's agent rules files so the
|
|
2
|
+
// agent actually uses Krimto (fix for the discovery problem). Idempotent and non-destructive: it only
|
|
3
|
+
// adds/refreshes a marker-delimited block, never clobbering other content (see ../agentRule).
|
|
4
|
+
//
|
|
5
|
+
// G4 — by default, init now auto-detects which editor is in use (`.cursor/`, `.claude/`, existing
|
|
6
|
+
// CLAUDE.md / AGENTS.md / GEMINI.md, etc.) and writes ONLY matching files instead of all four. If
|
|
7
|
+
// no signals are present, it falls back to writing all four so a brand-new project still picks up
|
|
8
|
+
// the rule for whichever editor ships next. `--all` keeps the legacy "write everything" behavior.
|
|
9
|
+
|
|
10
|
+
import { promises as fs } from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
|
|
13
|
+
import { applyRule } from "../agentRule";
|
|
14
|
+
|
|
15
|
+
/** The agent rules files `krimto init` targets, relative to the project dir. */
|
|
16
|
+
export const INIT_TARGETS = [
|
|
17
|
+
"CLAUDE.md",
|
|
18
|
+
"AGENTS.md",
|
|
19
|
+
"GEMINI.md",
|
|
20
|
+
path.join(".cursor", "rules", "krimto.mdc"),
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export interface InitResult {
|
|
24
|
+
/** Relative paths that were created or updated (empty when everything was already current). */
|
|
25
|
+
written: string[];
|
|
26
|
+
/** Targets considered (after detection/--all decisions). Exposed for the CLI's success message. */
|
|
27
|
+
considered: string[];
|
|
28
|
+
/** True when targets came from auto-detection (some editor signals matched). */
|
|
29
|
+
detected: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function exists(p: string): Promise<boolean> {
|
|
33
|
+
try {
|
|
34
|
+
await fs.access(p);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Auto-detect which editor rules files the project should get based on local signals.
|
|
43
|
+
* Returns the subset of INIT_TARGETS that has a positive signal, OR an empty array when
|
|
44
|
+
* nothing matches (caller falls back to writing everything).
|
|
45
|
+
*/
|
|
46
|
+
export async function detectEditorTargets(cwd: string): Promise<string[]> {
|
|
47
|
+
const matches: string[] = [];
|
|
48
|
+
|
|
49
|
+
// CLAUDE.md — Claude Code
|
|
50
|
+
if ((await exists(path.join(cwd, "CLAUDE.md"))) || (await exists(path.join(cwd, ".claude"))) || (await exists(path.join(cwd, ".claude-plugin")))) {
|
|
51
|
+
matches.push("CLAUDE.md");
|
|
52
|
+
}
|
|
53
|
+
// AGENTS.md — Codex CLI / generic
|
|
54
|
+
if (await exists(path.join(cwd, "AGENTS.md"))) {
|
|
55
|
+
matches.push("AGENTS.md");
|
|
56
|
+
}
|
|
57
|
+
// GEMINI.md — Gemini CLI
|
|
58
|
+
if ((await exists(path.join(cwd, "GEMINI.md"))) || (await exists(path.join(cwd, "gemini-extension.json"))) || (await exists(path.join(cwd, ".gemini")))) {
|
|
59
|
+
matches.push("GEMINI.md");
|
|
60
|
+
}
|
|
61
|
+
// .cursor/rules/krimto.mdc — Cursor
|
|
62
|
+
if (await exists(path.join(cwd, ".cursor"))) {
|
|
63
|
+
matches.push(path.join(".cursor", "rules", "krimto.mdc"));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return matches;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RunInitOptions {
|
|
70
|
+
/** Force writing all four files regardless of detection (the legacy behavior). */
|
|
71
|
+
all?: boolean;
|
|
72
|
+
/** Override the target list directly (tests; takes precedence over `all` + detection). */
|
|
73
|
+
targets?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Write/refresh the Krimto standing rule into each target rules file under `cwd`, idempotently. */
|
|
77
|
+
export async function runInit(cwd: string, opts: RunInitOptions = {}): Promise<InitResult> {
|
|
78
|
+
let targets: string[];
|
|
79
|
+
let detected = false;
|
|
80
|
+
if (opts.targets) {
|
|
81
|
+
targets = opts.targets;
|
|
82
|
+
} else if (opts.all) {
|
|
83
|
+
targets = INIT_TARGETS;
|
|
84
|
+
} else {
|
|
85
|
+
const auto = await detectEditorTargets(cwd);
|
|
86
|
+
if (auto.length > 0) {
|
|
87
|
+
targets = auto;
|
|
88
|
+
detected = true;
|
|
89
|
+
} else {
|
|
90
|
+
targets = INIT_TARGETS;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const written: string[] = [];
|
|
95
|
+
for (const rel of targets) {
|
|
96
|
+
const file = path.join(cwd, rel);
|
|
97
|
+
let existing: string | null = null;
|
|
98
|
+
try {
|
|
99
|
+
existing = await fs.readFile(file, "utf8");
|
|
100
|
+
} catch {
|
|
101
|
+
existing = null; // file doesn't exist yet — we'll create it
|
|
102
|
+
}
|
|
103
|
+
const next = applyRule(existing);
|
|
104
|
+
if (next === existing) continue; // already up to date
|
|
105
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
106
|
+
await fs.writeFile(file, next, "utf8");
|
|
107
|
+
written.push(rel);
|
|
108
|
+
}
|
|
109
|
+
return { written, considered: targets, detected };
|
|
110
|
+
}
|