@krimto-labs/krimto 0.2.21 → 0.2.26

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/src/cli/wizard.ts CHANGED
@@ -388,7 +388,10 @@ function printApplyResult(res: ApplyResult, io: WizardIO): void {
388
388
  }
389
389
  }
390
390
 
391
- io.out("Restart your editor once so it picks up the new rule.\n");
391
+ io.out(
392
+ "Restart your editor once so it loads the new MCP server (the krimto_*\n" +
393
+ "tools won't appear in chat until you do) and picks up the standing rule.\n",
394
+ );
392
395
  }
393
396
 
394
397
  function printRefreshSummary(res: ApplyResult, io: WizardIO): void {
@@ -74,13 +74,14 @@ export function cursorDeeplink(host: string): string {
74
74
  return `cursor://anysphere.cursor-deeplink/mcp/install?name=krimto&config=${config}`;
75
75
  }
76
76
 
77
- /** The five MCP tools Krimto exposes (kept in lockstep with src/server/index.ts registrations). */
77
+ /** The six MCP tools Krimto exposes (kept in lockstep with src/server/index.ts registrations). */
78
78
  export const MCP_TOOL_NAMES = [
79
79
  "krimto_write",
80
80
  "krimto_recall",
81
81
  "krimto_read",
82
82
  "krimto_supersede",
83
83
  "krimto_list_scopes",
84
+ "krimto_whoami",
84
85
  ] as const;
85
86
 
86
87
  /** The transport-level contract for wiring up any MCP client we haven't shipped a verified snippet for. */
@@ -41,6 +41,7 @@ import {
41
41
  krimtoRead,
42
42
  krimtoRecall,
43
43
  krimtoSupersede,
44
+ krimtoWhoami,
44
45
  krimtoWrite,
45
46
  type ToolContext,
46
47
  } from "./tools";
@@ -48,7 +49,7 @@ import { type Requester } from "../access/scope";
48
49
 
49
50
  export type RequesterResolver = (extra: { authInfo?: AuthInfo }) => Requester;
50
51
 
51
- export const KRIMTO_VERSION = "0.2.21";
52
+ export const KRIMTO_VERSION = "0.2.26";
52
53
 
53
54
  export function resolveDataDir(): string {
54
55
  return process.env.KRIMTO_DATA ?? path.join(homedir(), ".krimto");
@@ -192,6 +193,24 @@ export function buildServer(ctx: ToolContext, resolveRequester?: RequesterResolv
192
193
  },
193
194
  );
194
195
 
196
+ server.registerTool(
197
+ "krimto_whoami",
198
+ {
199
+ description:
200
+ "Report the caller's identity plus the scopes they can read and write. Call this before " +
201
+ "claiming to know the user's email or which team scopes exist — Krimto knows; the agent doesn't.",
202
+ inputSchema: {},
203
+ },
204
+ async (_args, extra) => {
205
+ try {
206
+ const requester = resolveRequester ? resolveRequester(extra) : ctx.requester;
207
+ return ok(await krimtoWhoami({ ...ctx, requester }));
208
+ } catch (e) {
209
+ return fail(e);
210
+ }
211
+ },
212
+ );
213
+
195
214
  return server;
196
215
  }
197
216
 
@@ -16,11 +16,19 @@ import { promises as fs } from "node:fs";
16
16
  import * as path from "node:path";
17
17
 
18
18
  export type LockMode = "stdio" | "http";
19
+ /**
20
+ * How this Krimto process was launched. "service" = via launchd / systemd / schtasks (the
21
+ * always-running setup); "ad-hoc" = direct invocation (`krimto serve`, editor-spawned stdio,
22
+ * tests). The service installers inject KRIMTO_LAUNCHED_BY=service into the unit env, so this
23
+ * field is just `process.env.KRIMTO_LAUNCHED_BY` at acquire time with a safe default.
24
+ */
25
+ export type LaunchedBy = "service" | "ad-hoc";
19
26
 
20
27
  export interface LockInfo {
21
28
  pid: number;
22
29
  started: string;
23
30
  mode: LockMode;
31
+ launchedBy: LaunchedBy;
24
32
  }
25
33
 
26
34
  export interface LockHandle {
@@ -73,6 +81,7 @@ export async function acquireLock(dataDir: string, mode: LockMode): Promise<Lock
73
81
  pid: existing.pid,
74
82
  started: typeof existing.started === "string" ? existing.started : "unknown",
75
83
  mode: (existing.mode as LockMode) ?? "stdio",
84
+ launchedBy: existing.launchedBy === "service" ? "service" : "ad-hoc",
76
85
  },
77
86
  file,
78
87
  );
@@ -83,7 +92,8 @@ export async function acquireLock(dataDir: string, mode: LockMode): Promise<Lock
83
92
  // File missing / unreadable / malformed — treat as no lock and continue.
84
93
  }
85
94
 
86
- const info: LockInfo = { pid: process.pid, started: new Date().toISOString(), mode };
95
+ const launchedBy: LaunchedBy = process.env.KRIMTO_LAUNCHED_BY === "service" ? "service" : "ad-hoc";
96
+ const info: LockInfo = { pid: process.pid, started: new Date().toISOString(), mode, launchedBy };
87
97
  await fs.writeFile(file, JSON.stringify(info, null, 2), "utf8");
88
98
 
89
99
  return {
@@ -109,6 +109,20 @@ export interface SupersedeResult {
109
109
 
110
110
  export interface ListScopesResult {
111
111
  scopes: { path: string; fact_count: number; last_updated: string | null }[];
112
+ /** Present only when `scopes` is empty — tells the calling agent how to make a scope appear. */
113
+ hint?: string;
114
+ }
115
+
116
+ /**
117
+ * v0.2.25 — Gap 3. The smoke-6 transcript showed an agent in chat inventing a wrong identity
118
+ * (`lpd.themes@gmail.com` instead of `lpdthemes@gmail.com`) because it had no MCP-side way to
119
+ * ask "who am I writing as?". `krimto_whoami` returns the resolved identity plus the scopes
120
+ * the caller can read and write, so the agent never has to guess.
121
+ */
122
+ export interface WhoamiResult {
123
+ identity: string;
124
+ readable_scopes: string[];
125
+ writable_scopes: string[];
112
126
  }
113
127
 
114
128
  function clock(ctx: ToolContext): Date {
@@ -329,17 +343,42 @@ export async function krimtoSupersede(
329
343
  });
330
344
  }
331
345
 
346
+ /**
347
+ * Return the caller's identity plus the scopes they can read and write. The agent in chat uses
348
+ * this to avoid hallucinating identity (Gap 3 from the smoke-6 transcript audit).
349
+ */
350
+ export async function krimtoWhoami(ctx: ToolContext): Promise<WhoamiResult> {
351
+ const readable = readableScopesFor(ctx);
352
+ const writable = writableScopesFor(ctx);
353
+ if (ctx.activity) await ctx.activity.record("krimto_whoami", ctx.requester.identity, ctx.requester.identity);
354
+ return {
355
+ identity: ctx.requester.identity,
356
+ readable_scopes: readable,
357
+ writable_scopes: writable,
358
+ };
359
+ }
360
+
332
361
  /** Discover the scopes that exist and what they contain. */
333
362
  export async function krimtoListScopes(ctx: ToolContext): Promise<ListScopesResult> {
334
363
  const scopes = ctx.index.listScopes(readableScopesFor(ctx));
335
364
  if (ctx.activity) await ctx.activity.record("krimto_list_scopes", ctx.requester.identity, `${scopes.length} scope(s)`);
336
- return {
365
+ const result: ListScopesResult = {
337
366
  scopes: scopes.map((s) => ({
338
367
  path: s.path,
339
368
  fact_count: s.factCount,
340
369
  last_updated: s.lastUpdated,
341
370
  })),
342
371
  };
372
+ // v0.2.24 — empty result is the #1 first-impression confuser ("Krimto must be broken").
373
+ // Surface the next step in the response itself, so an agent in chat can relay it verbatim
374
+ // instead of inventing a hallucinated explanation about "scopes aren't configured".
375
+ if (result.scopes.length === 0) {
376
+ result.hint =
377
+ `No scopes exist yet for ${ctx.requester.identity}. Scopes are created on the first ` +
378
+ `write — try krimto_write with a small fact (e.g. "we use pnpm in this repo") and ` +
379
+ `your user/<email> scope will appear here.`;
380
+ }
381
+ return result;
343
382
  }
344
383
 
345
384
  /** Build a Requester from a validated bearer token's AuthInfo (its `extra` carries identity+teams). */