engrm 0.1.0 → 0.2.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 (98) hide show
  1. package/README.md +214 -73
  2. package/bin/build.mjs +97 -0
  3. package/bin/engrm.mjs +13 -0
  4. package/dist/cli.js +2712 -0
  5. package/dist/hooks/elicitation-result.js +1786 -0
  6. package/dist/hooks/post-tool-use.js +2357 -0
  7. package/dist/hooks/pre-compact.js +1321 -0
  8. package/dist/hooks/sentinel.js +1168 -0
  9. package/dist/hooks/session-start.js +1473 -0
  10. package/dist/hooks/stop.js +1834 -0
  11. package/dist/server.js +16628 -0
  12. package/package.json +29 -4
  13. package/packs/api-best-practices.json +182 -0
  14. package/packs/nextjs-patterns.json +68 -0
  15. package/packs/node-security.json +68 -0
  16. package/packs/python-django.json +68 -0
  17. package/packs/react-gotchas.json +182 -0
  18. package/packs/typescript-patterns.json +67 -0
  19. package/packs/web-security.json +182 -0
  20. package/.mcp.json +0 -9
  21. package/AUTH-DESIGN.md +0 -436
  22. package/BRIEF.md +0 -197
  23. package/CLAUDE.md +0 -44
  24. package/COMPETITIVE.md +0 -174
  25. package/CONTEXT-OPTIMIZATION.md +0 -305
  26. package/INFRASTRUCTURE.md +0 -252
  27. package/MARKET.md +0 -230
  28. package/PLAN.md +0 -278
  29. package/SENTINEL.md +0 -293
  30. package/SERVER-API-PLAN.md +0 -553
  31. package/SPEC.md +0 -843
  32. package/SWOT.md +0 -148
  33. package/SYNC-ARCHITECTURE.md +0 -294
  34. package/VIBE-CODER-STRATEGY.md +0 -250
  35. package/bun.lock +0 -375
  36. package/hooks/post-tool-use.ts +0 -144
  37. package/hooks/session-start.ts +0 -64
  38. package/hooks/stop.ts +0 -131
  39. package/mem-page.html +0 -1305
  40. package/src/capture/dedup.test.ts +0 -103
  41. package/src/capture/dedup.ts +0 -76
  42. package/src/capture/extractor.test.ts +0 -245
  43. package/src/capture/extractor.ts +0 -330
  44. package/src/capture/quality.test.ts +0 -168
  45. package/src/capture/quality.ts +0 -104
  46. package/src/capture/retrospective.test.ts +0 -115
  47. package/src/capture/retrospective.ts +0 -121
  48. package/src/capture/scanner.test.ts +0 -131
  49. package/src/capture/scanner.ts +0 -100
  50. package/src/capture/scrubber.test.ts +0 -144
  51. package/src/capture/scrubber.ts +0 -181
  52. package/src/cli.ts +0 -517
  53. package/src/config.ts +0 -238
  54. package/src/context/inject.test.ts +0 -940
  55. package/src/context/inject.ts +0 -382
  56. package/src/embeddings/backfill.ts +0 -50
  57. package/src/embeddings/embedder.test.ts +0 -76
  58. package/src/embeddings/embedder.ts +0 -139
  59. package/src/lifecycle/aging.test.ts +0 -103
  60. package/src/lifecycle/aging.ts +0 -36
  61. package/src/lifecycle/compaction.test.ts +0 -264
  62. package/src/lifecycle/compaction.ts +0 -190
  63. package/src/lifecycle/purge.test.ts +0 -100
  64. package/src/lifecycle/purge.ts +0 -37
  65. package/src/lifecycle/scheduler.test.ts +0 -120
  66. package/src/lifecycle/scheduler.ts +0 -101
  67. package/src/provisioning/browser-auth.ts +0 -172
  68. package/src/provisioning/provision.test.ts +0 -198
  69. package/src/provisioning/provision.ts +0 -94
  70. package/src/register.test.ts +0 -167
  71. package/src/register.ts +0 -178
  72. package/src/server.ts +0 -436
  73. package/src/storage/migrations.test.ts +0 -244
  74. package/src/storage/migrations.ts +0 -261
  75. package/src/storage/outbox.test.ts +0 -229
  76. package/src/storage/outbox.ts +0 -131
  77. package/src/storage/projects.test.ts +0 -137
  78. package/src/storage/projects.ts +0 -184
  79. package/src/storage/sqlite.test.ts +0 -798
  80. package/src/storage/sqlite.ts +0 -934
  81. package/src/storage/vec.test.ts +0 -198
  82. package/src/sync/auth.test.ts +0 -76
  83. package/src/sync/auth.ts +0 -68
  84. package/src/sync/client.ts +0 -183
  85. package/src/sync/engine.test.ts +0 -94
  86. package/src/sync/engine.ts +0 -127
  87. package/src/sync/pull.test.ts +0 -279
  88. package/src/sync/pull.ts +0 -170
  89. package/src/sync/push.test.ts +0 -117
  90. package/src/sync/push.ts +0 -230
  91. package/src/tools/get.ts +0 -34
  92. package/src/tools/pin.ts +0 -47
  93. package/src/tools/save.test.ts +0 -301
  94. package/src/tools/save.ts +0 -231
  95. package/src/tools/search.test.ts +0 -69
  96. package/src/tools/search.ts +0 -181
  97. package/src/tools/timeline.ts +0 -64
  98. package/tsconfig.json +0 -22
@@ -1,94 +0,0 @@
1
- /**
2
- * Provisioning: exchange a cmt_ token (or OAuth code) for cvk_ API credentials.
3
- *
4
- * Calls POST /v1/mem/provision on Candengo Vector.
5
- * Returns everything needed to write ~/.engrm/settings.json.
6
- */
7
-
8
- export const DEFAULT_CANDENGO_URL = "https://www.candengo.com";
9
-
10
- export interface ProvisionRequest {
11
- /** cmt_ provisioning token from web signup */
12
- token?: string;
13
- /** OAuth authorization code from browser flow */
14
- code?: string;
15
- /** Device name for identification (e.g. "MacBook Pro") */
16
- device_name?: string;
17
- }
18
-
19
- export interface TeamInfo {
20
- id: string;
21
- name: string;
22
- namespace: string;
23
- }
24
-
25
- export interface ProvisionResponse {
26
- api_key: string; // cvk_...
27
- site_id: string;
28
- namespace: string;
29
- user_id: string;
30
- user_email: string;
31
- teams: TeamInfo[];
32
- }
33
-
34
- export class ProvisionError extends Error {
35
- constructor(
36
- readonly status: number,
37
- readonly detail: string
38
- ) {
39
- super(detail);
40
- this.name = "ProvisionError";
41
- }
42
- }
43
-
44
- /**
45
- * Exchange a provisioning token or OAuth code for API credentials.
46
- */
47
- export async function provision(
48
- baseUrl: string,
49
- request: ProvisionRequest
50
- ): Promise<ProvisionResponse> {
51
- const url = `${baseUrl.replace(/\/$/, "")}/v1/mem/provision`;
52
-
53
- const response = await fetch(url, {
54
- method: "POST",
55
- headers: { "Content-Type": "application/json" },
56
- body: JSON.stringify(request),
57
- });
58
-
59
- if (!response.ok) {
60
- let detail: string;
61
- try {
62
- const body = (await response.json()) as { detail?: string };
63
- detail = body.detail ?? `HTTP ${response.status}`;
64
- } catch {
65
- detail = `HTTP ${response.status}`;
66
- }
67
-
68
- if (response.status === 401 || response.status === 403) {
69
- throw new ProvisionError(
70
- response.status,
71
- "Invalid or expired provisioning token"
72
- );
73
- }
74
- if (response.status === 409) {
75
- throw new ProvisionError(
76
- response.status,
77
- "Token has already been used"
78
- );
79
- }
80
- throw new ProvisionError(response.status, detail);
81
- }
82
-
83
- const data = (await response.json()) as ProvisionResponse;
84
-
85
- // Validate response
86
- if (!data.api_key?.startsWith("cvk_")) {
87
- throw new ProvisionError(0, "Server returned invalid API key format");
88
- }
89
- if (!data.site_id || !data.namespace || !data.user_id) {
90
- throw new ProvisionError(0, "Server returned incomplete credentials");
91
- }
92
-
93
- return data;
94
- }
@@ -1,167 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "bun:test";
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
- import { randomBytes } from "node:crypto";
6
-
7
- // We test the internal logic by importing helpers and mocking the file paths.
8
- // Since register.ts uses hardcoded paths (~/.claude.json, ~/.claude/settings.json),
9
- // we test the JSON merge logic directly.
10
-
11
- describe("register: JSON merge logic", () => {
12
- let tmpDir: string;
13
- let claudeJson: string;
14
- let claudeSettings: string;
15
-
16
- beforeEach(() => {
17
- tmpDir = join(tmpdir(), `engrm-register-test-${randomBytes(4).toString("hex")}`);
18
- mkdirSync(tmpDir, { recursive: true });
19
- mkdirSync(join(tmpDir, ".claude"), { recursive: true });
20
- claudeJson = join(tmpDir, ".claude.json");
21
- claudeSettings = join(tmpDir, ".claude", "settings.json");
22
- });
23
-
24
- afterEach(() => {
25
- rmSync(tmpDir, { recursive: true, force: true });
26
- });
27
-
28
- it("creates claude.json from scratch with MCP server", () => {
29
- // Simulate what registerMcpServer does
30
- const config: Record<string, unknown> = {};
31
- const servers: Record<string, unknown> = {};
32
- servers["engrm"] = {
33
- type: "stdio",
34
- command: "/usr/local/bin/bun",
35
- args: ["run", "/path/to/server.ts"],
36
- };
37
- config["mcpServers"] = servers;
38
-
39
- writeFileSync(claudeJson, JSON.stringify(config, null, 2));
40
- const result = JSON.parse(readFileSync(claudeJson, "utf-8"));
41
-
42
- expect(result.mcpServers.engrm).toBeDefined();
43
- expect(result.mcpServers.engrm.command).toBe("/usr/local/bin/bun");
44
- expect(result.mcpServers.engrm.type).toBe("stdio");
45
- });
46
-
47
- it("preserves existing MCP servers when adding engrm", () => {
48
- const existing = {
49
- numStartups: 5,
50
- mcpServers: {
51
- "other-server": {
52
- type: "stdio",
53
- command: "node",
54
- args: ["other.js"],
55
- },
56
- },
57
- };
58
- writeFileSync(claudeJson, JSON.stringify(existing));
59
-
60
- // Read, merge, write
61
- const config = JSON.parse(readFileSync(claudeJson, "utf-8"));
62
- const servers = config.mcpServers ?? {};
63
- servers["engrm"] = { type: "stdio", command: "bun", args: ["run", "server.ts"] };
64
- config.mcpServers = servers;
65
- writeFileSync(claudeJson, JSON.stringify(config, null, 2));
66
-
67
- const result = JSON.parse(readFileSync(claudeJson, "utf-8"));
68
- expect(result.numStartups).toBe(5);
69
- expect(result.mcpServers["other-server"]).toBeDefined();
70
- expect(result.mcpServers.engrm).toBeDefined();
71
- });
72
-
73
- it("replaces existing engrm MCP config with new paths", () => {
74
- const existing = {
75
- mcpServers: {
76
- engrm: { type: "stdio", command: "bun", args: ["run", "/old/path/server.ts"] },
77
- },
78
- };
79
- writeFileSync(claudeJson, JSON.stringify(existing));
80
-
81
- const config = JSON.parse(readFileSync(claudeJson, "utf-8"));
82
- config.mcpServers.engrm = { type: "stdio", command: "bun", args: ["run", "/new/path/server.ts"] };
83
- writeFileSync(claudeJson, JSON.stringify(config, null, 2));
84
-
85
- const result = JSON.parse(readFileSync(claudeJson, "utf-8"));
86
- expect(result.mcpServers.engrm.args[1]).toBe("/new/path/server.ts");
87
- });
88
-
89
- it("creates hooks config from scratch", () => {
90
- const settings: Record<string, unknown> = {};
91
- settings["hooks"] = {
92
- SessionStart: [
93
- { hooks: [{ type: "command", command: "bun run /path/hooks/session-start.ts" }] },
94
- ],
95
- PostToolUse: [
96
- {
97
- matcher: "Edit|Write|Bash",
98
- hooks: [{ type: "command", command: "bun run /path/hooks/post-tool-use.ts" }],
99
- },
100
- ],
101
- Stop: [
102
- { hooks: [{ type: "command", command: "bun run /path/hooks/stop.ts" }] },
103
- ],
104
- };
105
- writeFileSync(claudeSettings, JSON.stringify(settings, null, 2));
106
-
107
- const result = JSON.parse(readFileSync(claudeSettings, "utf-8"));
108
- expect(result.hooks.SessionStart).toHaveLength(1);
109
- expect(result.hooks.PostToolUse).toHaveLength(1);
110
- expect(result.hooks.Stop).toHaveLength(1);
111
- });
112
-
113
- it("preserves non-engrm hooks when adding engrm hooks", () => {
114
- const existing = {
115
- hooks: {
116
- PostToolUse: [
117
- {
118
- matcher: "Bash",
119
- hooks: [{ type: "command", command: "/path/to/other-hook.sh" }],
120
- },
121
- ],
122
- },
123
- };
124
- writeFileSync(claudeSettings, JSON.stringify(existing));
125
-
126
- const settings = JSON.parse(readFileSync(claudeSettings, "utf-8"));
127
- const hooks = settings.hooks ?? {};
128
-
129
- // Add engrm hooks — keep others
130
- const postToolUse = hooks.PostToolUse ?? [];
131
- const otherHooks = postToolUse.filter(
132
- (e: { hooks: { command: string }[] }) =>
133
- !e.hooks?.some((h: { command: string }) => h.command.includes("engrm"))
134
- );
135
- hooks.PostToolUse = [
136
- ...otherHooks,
137
- {
138
- matcher: "Edit|Write|Bash",
139
- hooks: [{ type: "command", command: "bun run /path/engrm/hooks/post-tool-use.ts" }],
140
- },
141
- ];
142
- settings.hooks = hooks;
143
- writeFileSync(claudeSettings, JSON.stringify(settings, null, 2));
144
-
145
- const result = JSON.parse(readFileSync(claudeSettings, "utf-8"));
146
- expect(result.hooks.PostToolUse).toHaveLength(2);
147
- expect(result.hooks.PostToolUse[0].hooks[0].command).toBe("/path/to/other-hook.sh");
148
- expect(result.hooks.PostToolUse[1].hooks[0].command).toContain("engrm");
149
- });
150
-
151
- it("preserves other settings fields", () => {
152
- const existing = {
153
- skipDangerousModePermissionPrompt: true,
154
- attribution: { commit: "", pr: "" },
155
- };
156
- writeFileSync(claudeSettings, JSON.stringify(existing));
157
-
158
- const settings = JSON.parse(readFileSync(claudeSettings, "utf-8"));
159
- settings.hooks = { Stop: [{ hooks: [{ type: "command", command: "test" }] }] };
160
- writeFileSync(claudeSettings, JSON.stringify(settings, null, 2));
161
-
162
- const result = JSON.parse(readFileSync(claudeSettings, "utf-8"));
163
- expect(result.skipDangerousModePermissionPrompt).toBe(true);
164
- expect(result.attribution).toBeDefined();
165
- expect(result.hooks.Stop).toHaveLength(1);
166
- });
167
- });
package/src/register.ts DELETED
@@ -1,178 +0,0 @@
1
- /**
2
- * Auto-register Engrm MCP server + hooks in Claude Code config.
3
- *
4
- * - MCP server → ~/.claude.json (mcpServers.engrm)
5
- * - Hooks → ~/.claude/settings.json (hooks.SessionStart, PostToolUse, Stop)
6
- *
7
- * Merges into existing config — never overwrites other servers or hooks.
8
- */
9
-
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
- import { homedir } from "node:os";
12
- import { join, dirname } from "node:path";
13
-
14
- // --- Paths ---
15
-
16
- const CLAUDE_JSON = join(homedir(), ".claude.json");
17
- const CLAUDE_SETTINGS = join(homedir(), ".claude", "settings.json");
18
-
19
- /**
20
- * Resolve the absolute path to the bun binary.
21
- * Checks common locations; falls back to "bun" (assumes PATH).
22
- */
23
- function findBun(): string {
24
- const candidates = [
25
- join(homedir(), ".bun", "bin", "bun"),
26
- "/usr/local/bin/bun",
27
- "/opt/homebrew/bin/bun",
28
- ];
29
- for (const p of candidates) {
30
- if (existsSync(p)) return p;
31
- }
32
- // If running under bun, process.execPath is the bun binary
33
- if (process.execPath && process.execPath.endsWith("bun")) {
34
- return process.execPath;
35
- }
36
- return "bun";
37
- }
38
-
39
- /**
40
- * Resolve the package root directory (where src/ and hooks/ live).
41
- * Works both in dev (running from checkout) and when installed via npx.
42
- */
43
- function findPackageRoot(): string {
44
- // __dirname equivalent for ESM: import.meta.dir (Bun) or derive from import.meta.url
45
- // This file is at src/register.ts, so package root is one level up
46
- const thisDir =
47
- typeof import.meta.dir === "string"
48
- ? import.meta.dir
49
- : dirname(new URL(import.meta.url).pathname);
50
- return join(thisDir, "..");
51
- }
52
-
53
- // --- JSON helpers ---
54
-
55
- function readJsonFile(path: string): Record<string, unknown> {
56
- if (!existsSync(path)) return {};
57
- try {
58
- const raw = readFileSync(path, "utf-8");
59
- const parsed = JSON.parse(raw);
60
- if (typeof parsed === "object" && parsed !== null) {
61
- return parsed as Record<string, unknown>;
62
- }
63
- } catch {
64
- // Corrupt or empty file — start fresh
65
- }
66
- return {};
67
- }
68
-
69
- function writeJsonFile(path: string, data: Record<string, unknown>): void {
70
- const dir = dirname(path);
71
- if (!existsSync(dir)) {
72
- mkdirSync(dir, { recursive: true });
73
- }
74
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
75
- }
76
-
77
- // --- MCP registration ---
78
-
79
- export function registerMcpServer(): { path: string; added: boolean } {
80
- const bun = findBun();
81
- const root = findPackageRoot();
82
- const serverPath = join(root, "src", "server.ts");
83
-
84
- const config = readJsonFile(CLAUDE_JSON);
85
- const servers = (config["mcpServers"] ?? {}) as Record<string, unknown>;
86
-
87
- servers["engrm"] = {
88
- type: "stdio",
89
- command: bun,
90
- args: ["run", serverPath],
91
- };
92
-
93
- config["mcpServers"] = servers;
94
- writeJsonFile(CLAUDE_JSON, config);
95
-
96
- return { path: CLAUDE_JSON, added: true };
97
- }
98
-
99
- // --- Hooks registration ---
100
-
101
- interface HookEntry {
102
- matcher?: string;
103
- hooks: { type: string; command: string }[];
104
- }
105
-
106
- export function registerHooks(): { path: string; added: boolean } {
107
- const bun = findBun();
108
- const root = findPackageRoot();
109
-
110
- const sessionStartCmd = `${bun} run ${join(root, "hooks", "session-start.ts")}`;
111
- const postToolUseCmd = `${bun} run ${join(root, "hooks", "post-tool-use.ts")}`;
112
- const stopCmd = `${bun} run ${join(root, "hooks", "stop.ts")}`;
113
-
114
- const settings = readJsonFile(CLAUDE_SETTINGS);
115
- const hooks = (settings["hooks"] ?? {}) as Record<string, HookEntry[]>;
116
-
117
- // Replace any existing engrm hooks, preserve others
118
- hooks["SessionStart"] = replaceEngrmHook(
119
- hooks["SessionStart"],
120
- { hooks: [{ type: "command", command: sessionStartCmd }] },
121
- "session-start.ts"
122
- );
123
-
124
- hooks["PostToolUse"] = replaceEngrmHook(
125
- hooks["PostToolUse"],
126
- {
127
- matcher: "Edit|Write|Bash",
128
- hooks: [{ type: "command", command: postToolUseCmd }],
129
- },
130
- "post-tool-use.ts"
131
- );
132
-
133
- hooks["Stop"] = replaceEngrmHook(
134
- hooks["Stop"],
135
- { hooks: [{ type: "command", command: stopCmd }] },
136
- "stop.ts"
137
- );
138
-
139
- settings["hooks"] = hooks;
140
- writeJsonFile(CLAUDE_SETTINGS, settings);
141
-
142
- return { path: CLAUDE_SETTINGS, added: true };
143
- }
144
-
145
- /**
146
- * Replace any existing engrm hook entry in the array, or append.
147
- * Identifies engrm hooks by checking if the command contains our hook filename.
148
- */
149
- function replaceEngrmHook(
150
- existing: HookEntry[] | undefined,
151
- newEntry: HookEntry,
152
- hookFilename: string
153
- ): HookEntry[] {
154
- if (!existing || !Array.isArray(existing)) return [newEntry];
155
-
156
- const isEngrmHook = (entry: HookEntry): boolean =>
157
- entry.hooks?.some(
158
- (h) =>
159
- h.command?.includes("engrm") || h.command?.includes(hookFilename)
160
- ) ?? false;
161
-
162
- // Remove old engrm entries, add new one
163
- const others = existing.filter((e) => !isEngrmHook(e));
164
- return [...others, newEntry];
165
- }
166
-
167
- /**
168
- * Register both MCP server and hooks. Returns summary for CLI output.
169
- */
170
- export function registerAll(): {
171
- mcp: { path: string; added: boolean };
172
- hooks: { path: string; added: boolean };
173
- } {
174
- return {
175
- mcp: registerMcpServer(),
176
- hooks: registerHooks(),
177
- };
178
- }