engrm 0.1.0

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.
Files changed (82) hide show
  1. package/.mcp.json +9 -0
  2. package/AUTH-DESIGN.md +436 -0
  3. package/BRIEF.md +197 -0
  4. package/CLAUDE.md +44 -0
  5. package/COMPETITIVE.md +174 -0
  6. package/CONTEXT-OPTIMIZATION.md +305 -0
  7. package/INFRASTRUCTURE.md +252 -0
  8. package/LICENSE +105 -0
  9. package/MARKET.md +230 -0
  10. package/PLAN.md +278 -0
  11. package/README.md +121 -0
  12. package/SENTINEL.md +293 -0
  13. package/SERVER-API-PLAN.md +553 -0
  14. package/SPEC.md +843 -0
  15. package/SWOT.md +148 -0
  16. package/SYNC-ARCHITECTURE.md +294 -0
  17. package/VIBE-CODER-STRATEGY.md +250 -0
  18. package/bun.lock +375 -0
  19. package/hooks/post-tool-use.ts +144 -0
  20. package/hooks/session-start.ts +64 -0
  21. package/hooks/stop.ts +131 -0
  22. package/mem-page.html +1305 -0
  23. package/package.json +30 -0
  24. package/src/capture/dedup.test.ts +103 -0
  25. package/src/capture/dedup.ts +76 -0
  26. package/src/capture/extractor.test.ts +245 -0
  27. package/src/capture/extractor.ts +330 -0
  28. package/src/capture/quality.test.ts +168 -0
  29. package/src/capture/quality.ts +104 -0
  30. package/src/capture/retrospective.test.ts +115 -0
  31. package/src/capture/retrospective.ts +121 -0
  32. package/src/capture/scanner.test.ts +131 -0
  33. package/src/capture/scanner.ts +100 -0
  34. package/src/capture/scrubber.test.ts +144 -0
  35. package/src/capture/scrubber.ts +181 -0
  36. package/src/cli.ts +517 -0
  37. package/src/config.ts +238 -0
  38. package/src/context/inject.test.ts +940 -0
  39. package/src/context/inject.ts +382 -0
  40. package/src/embeddings/backfill.ts +50 -0
  41. package/src/embeddings/embedder.test.ts +76 -0
  42. package/src/embeddings/embedder.ts +139 -0
  43. package/src/lifecycle/aging.test.ts +103 -0
  44. package/src/lifecycle/aging.ts +36 -0
  45. package/src/lifecycle/compaction.test.ts +264 -0
  46. package/src/lifecycle/compaction.ts +190 -0
  47. package/src/lifecycle/purge.test.ts +100 -0
  48. package/src/lifecycle/purge.ts +37 -0
  49. package/src/lifecycle/scheduler.test.ts +120 -0
  50. package/src/lifecycle/scheduler.ts +101 -0
  51. package/src/provisioning/browser-auth.ts +172 -0
  52. package/src/provisioning/provision.test.ts +198 -0
  53. package/src/provisioning/provision.ts +94 -0
  54. package/src/register.test.ts +167 -0
  55. package/src/register.ts +178 -0
  56. package/src/server.ts +436 -0
  57. package/src/storage/migrations.test.ts +244 -0
  58. package/src/storage/migrations.ts +261 -0
  59. package/src/storage/outbox.test.ts +229 -0
  60. package/src/storage/outbox.ts +131 -0
  61. package/src/storage/projects.test.ts +137 -0
  62. package/src/storage/projects.ts +184 -0
  63. package/src/storage/sqlite.test.ts +798 -0
  64. package/src/storage/sqlite.ts +934 -0
  65. package/src/storage/vec.test.ts +198 -0
  66. package/src/sync/auth.test.ts +76 -0
  67. package/src/sync/auth.ts +68 -0
  68. package/src/sync/client.ts +183 -0
  69. package/src/sync/engine.test.ts +94 -0
  70. package/src/sync/engine.ts +127 -0
  71. package/src/sync/pull.test.ts +279 -0
  72. package/src/sync/pull.ts +170 -0
  73. package/src/sync/push.test.ts +117 -0
  74. package/src/sync/push.ts +230 -0
  75. package/src/tools/get.ts +34 -0
  76. package/src/tools/pin.ts +47 -0
  77. package/src/tools/save.test.ts +301 -0
  78. package/src/tools/save.ts +231 -0
  79. package/src/tools/search.test.ts +69 -0
  80. package/src/tools/search.ts +181 -0
  81. package/src/tools/timeline.ts +64 -0
  82. package/tsconfig.json +22 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Secret scrubbing pipeline.
3
+ *
4
+ * Runs before any observation is saved (even to local SQLite).
5
+ * Patterns from SPEC §6.
6
+ *
7
+ * Pattern definitions are stored as source/flags (not RegExp instances)
8
+ * to avoid shared mutable state from global regex lastIndex.
9
+ */
10
+
11
+ export interface ScrubPatternDef {
12
+ source: string;
13
+ flags: string;
14
+ replacement: string;
15
+ description: string;
16
+ category: "api_key" | "token" | "password" | "db_url" | "custom";
17
+ severity: "critical" | "high" | "medium" | "low";
18
+ }
19
+
20
+ export const DEFAULT_PATTERNS: ScrubPatternDef[] = [
21
+ {
22
+ source: "sk-[a-zA-Z0-9]{20,}",
23
+ flags: "g",
24
+ replacement: "[REDACTED_API_KEY]",
25
+ description: "OpenAI API keys",
26
+ category: "api_key",
27
+ severity: "critical",
28
+ },
29
+ {
30
+ source: "Bearer [a-zA-Z0-9\\-._~+/]+=*",
31
+ flags: "g",
32
+ replacement: "[REDACTED_BEARER]",
33
+ description: "Bearer auth tokens",
34
+ category: "token",
35
+ severity: "medium",
36
+ },
37
+ {
38
+ source: "password[=:]\\s*\\S+",
39
+ flags: "gi",
40
+ replacement: "password=[REDACTED]",
41
+ description: "Passwords in config",
42
+ category: "password",
43
+ severity: "high",
44
+ },
45
+ {
46
+ source: "postgresql://[^\\s]+",
47
+ flags: "g",
48
+ replacement: "[REDACTED_DB_URL]",
49
+ description: "PostgreSQL connection strings",
50
+ category: "db_url",
51
+ severity: "high",
52
+ },
53
+ {
54
+ source: "mongodb://[^\\s]+",
55
+ flags: "g",
56
+ replacement: "[REDACTED_DB_URL]",
57
+ description: "MongoDB connection strings",
58
+ category: "db_url",
59
+ severity: "high",
60
+ },
61
+ {
62
+ source: "mysql://[^\\s]+",
63
+ flags: "g",
64
+ replacement: "[REDACTED_DB_URL]",
65
+ description: "MySQL connection strings",
66
+ category: "db_url",
67
+ severity: "high",
68
+ },
69
+ {
70
+ source: "AKIA[A-Z0-9]{16}",
71
+ flags: "g",
72
+ replacement: "[REDACTED_AWS_KEY]",
73
+ description: "AWS access keys",
74
+ category: "api_key",
75
+ severity: "critical",
76
+ },
77
+ {
78
+ source: "ghp_[a-zA-Z0-9]{36}",
79
+ flags: "g",
80
+ replacement: "[REDACTED_GH_TOKEN]",
81
+ description: "GitHub personal access tokens",
82
+ category: "token",
83
+ severity: "high",
84
+ },
85
+ {
86
+ source: "gho_[a-zA-Z0-9]{36}",
87
+ flags: "g",
88
+ replacement: "[REDACTED_GH_TOKEN]",
89
+ description: "GitHub OAuth tokens",
90
+ category: "token",
91
+ severity: "high",
92
+ },
93
+ {
94
+ source: "github_pat_[a-zA-Z0-9_]{22,}",
95
+ flags: "g",
96
+ replacement: "[REDACTED_GH_TOKEN]",
97
+ description: "GitHub fine-grained PATs",
98
+ category: "token",
99
+ severity: "high",
100
+ },
101
+ {
102
+ source: "cvk_[a-f0-9]{64}",
103
+ flags: "g",
104
+ replacement: "[REDACTED_CANDENGO_KEY]",
105
+ description: "Candengo API keys",
106
+ category: "api_key",
107
+ severity: "critical",
108
+ },
109
+ {
110
+ source: "xox[bpras]-[a-zA-Z0-9\\-]+",
111
+ flags: "g",
112
+ replacement: "[REDACTED_SLACK_TOKEN]",
113
+ description: "Slack tokens",
114
+ category: "token",
115
+ severity: "high",
116
+ },
117
+ ];
118
+
119
+ /**
120
+ * Compile custom patterns from config strings into pattern definitions.
121
+ * Each string is treated as a regex pattern with global flag.
122
+ */
123
+ function compileCustomPatterns(patterns: string[]): ScrubPatternDef[] {
124
+ const compiled: ScrubPatternDef[] = [];
125
+ for (const pattern of patterns) {
126
+ try {
127
+ // Validate the regex is parseable
128
+ new RegExp(pattern);
129
+ compiled.push({
130
+ source: pattern,
131
+ flags: "g",
132
+ replacement: "[REDACTED_CUSTOM]",
133
+ description: `Custom pattern: ${pattern}`,
134
+ category: "custom",
135
+ severity: "medium",
136
+ });
137
+ } catch {
138
+ // Skip invalid regex patterns — don't crash the scrubber
139
+ }
140
+ }
141
+ return compiled;
142
+ }
143
+
144
+ /**
145
+ * Scrub sensitive content from text.
146
+ * Returns the scrubbed text.
147
+ */
148
+ export function scrubSecrets(
149
+ text: string,
150
+ customPatterns: string[] = []
151
+ ): string {
152
+ let result = text;
153
+ const allPatterns = [...DEFAULT_PATTERNS, ...compileCustomPatterns(customPatterns)];
154
+
155
+ for (const pattern of allPatterns) {
156
+ // Fresh RegExp per call — no shared mutable lastIndex state
157
+ result = result.replace(
158
+ new RegExp(pattern.source, pattern.flags),
159
+ pattern.replacement
160
+ );
161
+ }
162
+
163
+ return result;
164
+ }
165
+
166
+ /**
167
+ * Check if text contains any secrets that would be scrubbed.
168
+ * Useful for sensitivity classification.
169
+ */
170
+ export function containsSecrets(
171
+ text: string,
172
+ customPatterns: string[] = []
173
+ ): boolean {
174
+ const allPatterns = [...DEFAULT_PATTERNS, ...compileCustomPatterns(customPatterns)];
175
+
176
+ for (const pattern of allPatterns) {
177
+ if (new RegExp(pattern.source, pattern.flags).test(text)) return true;
178
+ }
179
+
180
+ return false;
181
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,517 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Engrm CLI.
4
+ *
5
+ * Commands:
6
+ * init — Browser OAuth setup (default)
7
+ * init --token=cmt_x — Setup from provisioning token
8
+ * init --no-browser — Device code flow (headless/SSH)
9
+ * init --manual — Interactive manual setup
10
+ * init --config <f> — Non-interactive setup from a JSON file
11
+ * status — Show current config and database stats
12
+ */
13
+
14
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
15
+ import { hostname, homedir } from "node:os";
16
+ import { join } from "node:path";
17
+ import { randomBytes } from "node:crypto";
18
+ import {
19
+ loadConfig,
20
+ saveConfig,
21
+ configExists,
22
+ getConfigDir,
23
+ getSettingsPath,
24
+ getDbPath,
25
+ type Config,
26
+ } from "./config.js";
27
+ import { MemDatabase } from "./storage/sqlite.js";
28
+ import { getOutboxStats } from "./storage/outbox.js";
29
+ import {
30
+ provision,
31
+ ProvisionError,
32
+ DEFAULT_CANDENGO_URL,
33
+ type ProvisionResponse,
34
+ } from "./provisioning/provision.js";
35
+ import { runBrowserAuth } from "./provisioning/browser-auth.js";
36
+ import { registerAll } from "./register.js";
37
+
38
+ const args = process.argv.slice(2);
39
+ const command = args[0];
40
+
41
+ switch (command) {
42
+ case "init":
43
+ await handleInit(args.slice(1));
44
+ break;
45
+ case "status":
46
+ handleStatus();
47
+ break;
48
+ default:
49
+ printUsage();
50
+ break;
51
+ }
52
+
53
+ // --- Init ---
54
+
55
+ async function handleInit(flags: string[]): Promise<void> {
56
+ // --token=cmt_xxx or --token cmt_xxx
57
+ const tokenFlag = flags.find((f) => f.startsWith("--token"));
58
+ if (tokenFlag) {
59
+ let token: string;
60
+ if (tokenFlag.includes("=")) {
61
+ token = tokenFlag.split("=")[1]!;
62
+ } else {
63
+ const idx = flags.indexOf("--token");
64
+ token = flags[idx + 1] ?? "";
65
+ }
66
+ if (!token || !token.startsWith("cmt_")) {
67
+ console.error("Error: --token requires a cmt_ provisioning token");
68
+ process.exit(1);
69
+ }
70
+ const url = extractUrlFlag(flags) ?? DEFAULT_CANDENGO_URL;
71
+ await initWithToken(url, token);
72
+ return;
73
+ }
74
+
75
+ // --config <path>
76
+ if (flags.includes("--config")) {
77
+ const configIndex = flags.indexOf("--config");
78
+ const configPath = flags[configIndex + 1];
79
+ if (!configPath) {
80
+ console.error("Error: --config requires a file path");
81
+ process.exit(1);
82
+ }
83
+ initFromFile(configPath);
84
+ return;
85
+ }
86
+
87
+ // --manual
88
+ if (flags.includes("--manual")) {
89
+ await initManual();
90
+ return;
91
+ }
92
+
93
+ // --no-browser (device code flow — placeholder for Phase 4.1b)
94
+ if (flags.includes("--no-browser")) {
95
+ console.error("Device code flow is not yet implemented.");
96
+ console.error("Use: engrm init --token=cmt_xxx");
97
+ process.exit(1);
98
+ }
99
+
100
+ // Default: browser OAuth flow
101
+ const url = extractUrlFlag(flags) ?? DEFAULT_CANDENGO_URL;
102
+ await initWithBrowser(url);
103
+ }
104
+
105
+ /**
106
+ * Extract --url flag value from flags array.
107
+ */
108
+ function extractUrlFlag(flags: string[]): string | undefined {
109
+ const urlFlag = flags.find((f) => f.startsWith("--url"));
110
+ if (!urlFlag) return undefined;
111
+ if (urlFlag.includes("=")) return urlFlag.split("=")[1];
112
+ const idx = flags.indexOf("--url");
113
+ return flags[idx + 1];
114
+ }
115
+
116
+ // --- Flow C: Provisioning token ---
117
+
118
+ async function initWithToken(baseUrl: string, token: string): Promise<void> {
119
+ if (configExists()) {
120
+ console.log("Existing configuration found. Overwriting...\n");
121
+ }
122
+
123
+ console.log("Exchanging provisioning token...");
124
+
125
+ try {
126
+ const result = await provision(baseUrl, {
127
+ token,
128
+ device_name: hostname(),
129
+ });
130
+ writeConfigFromProvision(baseUrl, result);
131
+ console.log(`\nConnected as ${result.user_email}`);
132
+ printPostInit();
133
+ } catch (error) {
134
+ if (error instanceof ProvisionError) {
135
+ console.error(`\nProvisioning failed: ${error.detail}`);
136
+ process.exit(1);
137
+ }
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ // --- Flow A: Browser OAuth ---
143
+
144
+ async function initWithBrowser(baseUrl: string): Promise<void> {
145
+ if (configExists()) {
146
+ console.log("Existing configuration found. Overwriting...\n");
147
+ }
148
+
149
+ try {
150
+ const { code } = await runBrowserAuth(baseUrl);
151
+
152
+ console.log("Exchanging authorization code...");
153
+ const result = await provision(baseUrl, {
154
+ code,
155
+ device_name: hostname(),
156
+ });
157
+ writeConfigFromProvision(baseUrl, result);
158
+ console.log(`\nConnected as ${result.user_email}`);
159
+ printPostInit();
160
+ } catch (error) {
161
+ if (error instanceof ProvisionError) {
162
+ console.error(`\nProvisioning failed: ${error.detail}`);
163
+ process.exit(1);
164
+ }
165
+ console.error(
166
+ `\nAuthorization failed: ${error instanceof Error ? error.message : String(error)}`
167
+ );
168
+ console.error("Try: engrm init --token=cmt_xxx");
169
+ process.exit(1);
170
+ }
171
+ }
172
+
173
+ // --- Shared: write config from provision response ---
174
+
175
+ function writeConfigFromProvision(
176
+ baseUrl: string,
177
+ result: ProvisionResponse
178
+ ): void {
179
+ ensureConfigDir();
180
+
181
+ const config: Config = {
182
+ candengo_url: baseUrl,
183
+ candengo_api_key: result.api_key,
184
+ site_id: result.site_id,
185
+ namespace: result.namespace,
186
+ user_id: result.user_id,
187
+ user_email: result.user_email,
188
+ device_id: generateDeviceId(),
189
+ teams: result.teams ?? [],
190
+ sync: {
191
+ enabled: true,
192
+ interval_seconds: 30,
193
+ batch_size: 50,
194
+ },
195
+ search: {
196
+ default_limit: 10,
197
+ local_boost: 1.2,
198
+ scope: "all",
199
+ },
200
+ scrubbing: {
201
+ enabled: true,
202
+ custom_patterns: [],
203
+ default_sensitivity: "shared",
204
+ },
205
+ };
206
+
207
+ saveConfig(config);
208
+
209
+ // Initialise database
210
+ const db = new MemDatabase(getDbPath());
211
+ db.close();
212
+
213
+ console.log(`Configuration saved to ${getSettingsPath()}`);
214
+ console.log(`Database initialised at ${getDbPath()}`);
215
+ }
216
+
217
+ // --- Flow D: Manual ---
218
+
219
+ function initFromFile(configPath: string): void {
220
+ if (!existsSync(configPath)) {
221
+ console.error(`Config file not found: ${configPath}`);
222
+ process.exit(1);
223
+ }
224
+
225
+ let parsed: unknown;
226
+ try {
227
+ const raw = readFileSync(configPath, "utf-8");
228
+ parsed = JSON.parse(raw);
229
+ } catch {
230
+ console.error(`Invalid JSON in ${configPath}`);
231
+ process.exit(1);
232
+ }
233
+
234
+ if (typeof parsed !== "object" || parsed === null) {
235
+ console.error("Config file must contain a JSON object");
236
+ process.exit(1);
237
+ }
238
+
239
+ const input = parsed as Record<string, unknown>;
240
+
241
+ const required = [
242
+ "candengo_url",
243
+ "candengo_api_key",
244
+ "site_id",
245
+ "namespace",
246
+ "user_id",
247
+ ];
248
+ for (const field of required) {
249
+ if (typeof input[field] !== "string" || !(input[field] as string).trim()) {
250
+ console.error(`Missing required field: ${field}`);
251
+ process.exit(1);
252
+ }
253
+ }
254
+
255
+ ensureConfigDir();
256
+
257
+ const config: Config = {
258
+ candengo_url: (input["candengo_url"] as string).trim(),
259
+ candengo_api_key: (input["candengo_api_key"] as string).trim(),
260
+ site_id: (input["site_id"] as string).trim(),
261
+ namespace: (input["namespace"] as string).trim(),
262
+ user_id: (input["user_id"] as string).trim(),
263
+ user_email:
264
+ typeof input["user_email"] === "string"
265
+ ? (input["user_email"] as string).trim()
266
+ : "",
267
+ device_id:
268
+ typeof input["device_id"] === "string"
269
+ ? input["device_id"]
270
+ : generateDeviceId(),
271
+ teams: [],
272
+ sync: {
273
+ enabled: true,
274
+ interval_seconds: 30,
275
+ batch_size: 50,
276
+ },
277
+ search: {
278
+ default_limit: 10,
279
+ local_boost: 1.2,
280
+ scope: "all",
281
+ },
282
+ scrubbing: {
283
+ enabled: true,
284
+ custom_patterns: [],
285
+ default_sensitivity: "shared",
286
+ },
287
+ };
288
+
289
+ saveConfig(config);
290
+
291
+ const db = new MemDatabase(getDbPath());
292
+ db.close();
293
+
294
+ console.log(`Configuration saved to ${getSettingsPath()}`);
295
+ console.log(`Database initialised at ${getDbPath()}`);
296
+ printPostInit();
297
+ }
298
+
299
+ async function initManual(): Promise<void> {
300
+ const prompt = createPrompter();
301
+
302
+ console.log("Engrm — Interactive Setup\n");
303
+
304
+ if (configExists()) {
305
+ const overwrite = await prompt(
306
+ "Config already exists. Overwrite? [y/N]: "
307
+ );
308
+ if (overwrite.toLowerCase() !== "y") {
309
+ console.log("Aborted.");
310
+ return;
311
+ }
312
+ }
313
+
314
+ const candengoUrl = await prompt(
315
+ "Candengo Vector URL (e.g. https://www.candengo.com): "
316
+ );
317
+ const apiKey = await prompt("API key (cvk_...): ");
318
+ const siteId = await prompt("Site ID: ");
319
+ const namespace = await prompt("Namespace: ");
320
+ const userId = await prompt("User ID: ");
321
+ const userEmail = await prompt("Email (optional): ");
322
+
323
+ if (!candengoUrl || !apiKey || !siteId || !namespace || !userId) {
324
+ console.error("All fields (except email) are required.");
325
+ process.exit(1);
326
+ }
327
+
328
+ ensureConfigDir();
329
+
330
+ const config: Config = {
331
+ candengo_url: candengoUrl.trim(),
332
+ candengo_api_key: apiKey.trim(),
333
+ site_id: siteId.trim(),
334
+ namespace: namespace.trim(),
335
+ user_id: userId.trim(),
336
+ user_email: userEmail.trim(),
337
+ device_id: generateDeviceId(),
338
+ teams: [],
339
+ sync: {
340
+ enabled: true,
341
+ interval_seconds: 30,
342
+ batch_size: 50,
343
+ },
344
+ search: {
345
+ default_limit: 10,
346
+ local_boost: 1.2,
347
+ scope: "all",
348
+ },
349
+ scrubbing: {
350
+ enabled: true,
351
+ custom_patterns: [],
352
+ default_sensitivity: "shared",
353
+ },
354
+ };
355
+
356
+ saveConfig(config);
357
+
358
+ const db = new MemDatabase(getDbPath());
359
+ db.close();
360
+
361
+ console.log(`\nConfiguration saved to ${getSettingsPath()}`);
362
+ console.log(`Database initialised at ${getDbPath()}`);
363
+ printPostInit();
364
+ }
365
+
366
+ // --- Status ---
367
+
368
+ function handleStatus(): void {
369
+ if (!configExists()) {
370
+ console.log("Engrm is not configured.");
371
+ console.log("Run: engrm init");
372
+ return;
373
+ }
374
+
375
+ const config = loadConfig();
376
+ console.log("Engrm Status\n");
377
+ console.log(` User: ${config.user_id}`);
378
+ if (config.user_email) {
379
+ console.log(` Email: ${config.user_email}`);
380
+ }
381
+ console.log(` Device: ${config.device_id}`);
382
+ console.log(` Candengo: ${config.candengo_url || "(not set)"}`);
383
+ console.log(` Site: ${config.site_id}`);
384
+ console.log(` Namespace: ${config.namespace}`);
385
+ if (config.teams.length > 0) {
386
+ console.log(
387
+ ` Teams: ${config.teams.map((t) => t.name).join(", ")}`
388
+ );
389
+ }
390
+ console.log(
391
+ ` Sync: ${config.sync.enabled ? "enabled" : "disabled"}`
392
+ );
393
+ console.log(` Config: ${getSettingsPath()}`);
394
+ console.log(` Database: ${getDbPath()}`);
395
+
396
+ // Check Claude Code registration
397
+ const claudeJson = join(homedir(), ".claude.json");
398
+ const claudeSettings = join(homedir(), ".claude", "settings.json");
399
+ const mcpRegistered = existsSync(claudeJson) && readFileSync(claudeJson, "utf-8").includes('"engrm"');
400
+ const hooksRegistered = existsSync(claudeSettings) && readFileSync(claudeSettings, "utf-8").includes("engrm");
401
+ console.log(` MCP server: ${mcpRegistered ? "registered" : "not registered"}`);
402
+ console.log(` Hooks: ${hooksRegistered ? "registered" : "not registered"}`);
403
+
404
+ if (existsSync(getDbPath())) {
405
+ try {
406
+ const db = new MemDatabase(getDbPath());
407
+ const obsCount = db.getActiveObservationCount();
408
+ const outbox = getOutboxStats(db);
409
+
410
+ // Session summaries count
411
+ const summaryCount = db.db
412
+ .query<{ count: number }, []>(
413
+ "SELECT COUNT(*) as count FROM session_summaries"
414
+ )
415
+ .get()?.count ?? 0;
416
+
417
+ console.log(`\n Active observations: ${obsCount}`);
418
+ console.log(` Session summaries: ${summaryCount}`);
419
+ console.log(
420
+ ` Outbox: ${outbox["pending"] ?? 0} pending, ${outbox["failed"] ?? 0} failed, ${outbox["synced"] ?? 0} synced`
421
+ );
422
+
423
+ // Security findings (try — table may not exist on old schemas)
424
+ try {
425
+ const findings = db.db
426
+ .query<{ severity: string; count: number }, []>(
427
+ "SELECT severity, COUNT(*) as count FROM security_findings GROUP BY severity"
428
+ )
429
+ .all();
430
+ if (findings.length > 0) {
431
+ const bySeverity = Object.fromEntries(findings.map((f) => [f.severity, f.count]));
432
+ const parts: string[] = [];
433
+ if (bySeverity["critical"]) parts.push(`${bySeverity["critical"]} critical`);
434
+ if (bySeverity["high"]) parts.push(`${bySeverity["high"]} high`);
435
+ if (bySeverity["medium"]) parts.push(`${bySeverity["medium"]} medium`);
436
+ if (bySeverity["low"]) parts.push(`${bySeverity["low"]} low`);
437
+ console.log(` Security findings: ${parts.join(", ")}`);
438
+ }
439
+ } catch {
440
+ // security_findings table may not exist yet
441
+ }
442
+
443
+ db.close();
444
+ } catch (error) {
445
+ console.log(
446
+ `\n Database error: ${error instanceof Error ? error.message : String(error)}`
447
+ );
448
+ }
449
+ }
450
+ }
451
+
452
+ // --- Helpers ---
453
+
454
+ function ensureConfigDir(): void {
455
+ const dir = getConfigDir();
456
+ if (!existsSync(dir)) {
457
+ mkdirSync(dir, { recursive: true });
458
+ }
459
+ }
460
+
461
+ function generateDeviceId(): string {
462
+ const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
463
+ const suffix = randomBytes(4).toString("hex");
464
+ return `${host}-${suffix}`;
465
+ }
466
+
467
+ function printPostInit(): void {
468
+ console.log("\nRegistering with Claude Code...");
469
+
470
+ try {
471
+ const result = registerAll();
472
+ console.log(` MCP server registered → ${result.mcp.path}`);
473
+ console.log(` Hooks registered → ${result.hooks.path}`);
474
+ console.log("\nEngrm is ready! Start a new Claude Code session to use memory.");
475
+ } catch (error) {
476
+ // Registration failed — fall back to manual instructions
477
+ console.log("\nCould not auto-register with Claude Code.");
478
+ console.log(`Error: ${error instanceof Error ? error.message : String(error)}`);
479
+ console.log("\nManual setup — add to ~/.claude.json:");
480
+ console.log(`
481
+ {
482
+ "mcpServers": {
483
+ "engrm": {
484
+ "type": "stdio",
485
+ "command": "bun",
486
+ "args": ["run", "${process.cwd()}/src/server.ts"]
487
+ }
488
+ }
489
+ }`);
490
+ }
491
+ }
492
+
493
+ function printUsage(): void {
494
+ console.log("Engrm — Memory layer for AI coding agents\n");
495
+ console.log("Usage:");
496
+ console.log(" engrm init Setup via browser (recommended)");
497
+ console.log(" engrm init --token=cmt_xxx Setup from provisioning token");
498
+ console.log(" engrm init --no-browser Setup via device code (SSH/headless)");
499
+ console.log(" engrm init --manual Manual setup (enter all values)");
500
+ console.log(" engrm init --config <file> Setup from JSON file");
501
+ console.log(" engrm status Show status");
502
+ }
503
+
504
+ /**
505
+ * Simple line-based prompter using Bun's stdin.
506
+ */
507
+ function createPrompter(): (question: string) => Promise<string> {
508
+ const decoder = new TextDecoder();
509
+ return async (question: string): Promise<string> => {
510
+ process.stdout.write(question);
511
+ for await (const chunk of Bun.stdin.stream()) {
512
+ const line = decoder.decode(chunk).trim();
513
+ return line;
514
+ }
515
+ return "";
516
+ };
517
+ }