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,137 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- normaliseGitRemoteUrl,
4
- projectNameFromCanonicalId,
5
- detectProject,
6
- } from "./projects.js";
7
-
8
- describe("normaliseGitRemoteUrl", () => {
9
- test("SSH-style git@github.com", () => {
10
- expect(normaliseGitRemoteUrl("git@github.com:unimpossible/aimy-agent.git")).toBe(
11
- "github.com/unimpossible/aimy-agent"
12
- );
13
- });
14
-
15
- test("HTTPS with .git suffix", () => {
16
- expect(
17
- normaliseGitRemoteUrl("https://github.com/unimpossible/aimy-agent.git")
18
- ).toBe("github.com/unimpossible/aimy-agent");
19
- });
20
-
21
- test("HTTPS without .git suffix", () => {
22
- expect(
23
- normaliseGitRemoteUrl("https://github.com/unimpossible/aimy-agent")
24
- ).toBe("github.com/unimpossible/aimy-agent");
25
- });
26
-
27
- test("HTTPS with auth credentials", () => {
28
- expect(
29
- normaliseGitRemoteUrl(
30
- "https://david@github.com/unimpossible/aimy-agent"
31
- )
32
- ).toBe("github.com/unimpossible/aimy-agent");
33
- });
34
-
35
- test("SSH protocol URL", () => {
36
- expect(
37
- normaliseGitRemoteUrl(
38
- "ssh://git@github.com/unimpossible/aimy-agent.git"
39
- )
40
- ).toBe("github.com/unimpossible/aimy-agent");
41
- });
42
-
43
- test("all forms normalise to the same canonical ID", () => {
44
- const forms = [
45
- "git@github.com:unimpossible/aimy-agent.git",
46
- "https://github.com/unimpossible/aimy-agent.git",
47
- "https://david@github.com/unimpossible/aimy-agent",
48
- "ssh://git@github.com/unimpossible/aimy-agent.git",
49
- ];
50
- const ids = forms.map(normaliseGitRemoteUrl);
51
- const unique = new Set(ids);
52
- expect(unique.size).toBe(1);
53
- expect(ids[0]).toBe("github.com/unimpossible/aimy-agent");
54
- });
55
-
56
- test("lowercase host", () => {
57
- expect(normaliseGitRemoteUrl("git@GitHub.COM:Org/Repo.git")).toBe(
58
- "github.com/Org/Repo"
59
- );
60
- });
61
-
62
- test("preserves path case", () => {
63
- expect(
64
- normaliseGitRemoteUrl("https://github.com/MyOrg/MyRepo.git")
65
- ).toBe("github.com/MyOrg/MyRepo");
66
- });
67
-
68
- test("strips trailing slashes", () => {
69
- expect(
70
- normaliseGitRemoteUrl("https://github.com/org/repo/")
71
- ).toBe("github.com/org/repo");
72
- });
73
-
74
- test("handles git:// protocol", () => {
75
- expect(
76
- normaliseGitRemoteUrl("git://github.com/org/repo.git")
77
- ).toBe("github.com/org/repo");
78
- });
79
-
80
- test("does not replace port-like colons", () => {
81
- // github.com:443/org/repo should NOT treat :443 as SSH-style
82
- expect(
83
- normaliseGitRemoteUrl("https://github.com:443/org/repo")
84
- ).toBe("github.com:443/org/repo");
85
- });
86
-
87
- test("handles whitespace", () => {
88
- expect(
89
- normaliseGitRemoteUrl(" https://github.com/org/repo.git ")
90
- ).toBe("github.com/org/repo");
91
- });
92
-
93
- test("non-GitHub hosts work", () => {
94
- expect(
95
- normaliseGitRemoteUrl("git@gitlab.com:team/project.git")
96
- ).toBe("gitlab.com/team/project");
97
- });
98
-
99
- test("self-hosted GitLab", () => {
100
- expect(
101
- normaliseGitRemoteUrl("git@git.internal.company.com:team/project.git")
102
- ).toBe("git.internal.company.com/team/project");
103
- });
104
- });
105
-
106
- describe("projectNameFromCanonicalId", () => {
107
- test("extracts repo name from canonical ID", () => {
108
- expect(
109
- projectNameFromCanonicalId("github.com/unimpossible/aimy-agent")
110
- ).toBe("aimy-agent");
111
- });
112
-
113
- test("handles single-segment ID", () => {
114
- expect(projectNameFromCanonicalId("my-project")).toBe("my-project");
115
- });
116
-
117
- test("handles local/ prefix", () => {
118
- expect(projectNameFromCanonicalId("local/my-dir")).toBe("my-dir");
119
- });
120
- });
121
-
122
- describe("detectProject", () => {
123
- test("detects current repo (candengo-mem)", () => {
124
- const result = detectProject("/Volumes/Data/devs/candengo-mem");
125
- // This repo has a git remote
126
- expect(result.canonical_id).toContain("candengo-mem");
127
- expect(result.name).toBe("candengo-mem");
128
- expect(result.local_path).toBe("/Volumes/Data/devs/candengo-mem");
129
- });
130
-
131
- test("falls back to directory name for non-git directory", () => {
132
- const result = detectProject("/tmp");
133
- expect(result.canonical_id).toBe("local/tmp");
134
- expect(result.name).toBe("tmp");
135
- expect(result.remote_url).toBeNull();
136
- });
137
- });
@@ -1,184 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { basename, join } from "node:path";
4
-
5
- /**
6
- * Normalise a git remote URL to a canonical project ID.
7
- *
8
- * All of these resolve to the same canonical ID:
9
- * git@github.com:unimpossible/aimy-agent.git → github.com/unimpossible/aimy-agent
10
- * https://github.com/unimpossible/aimy-agent.git → github.com/unimpossible/aimy-agent
11
- * https://david@github.com/unimpossible/aimy-agent → github.com/unimpossible/aimy-agent
12
- * ssh://git@github.com/unimpossible/aimy-agent.git → github.com/unimpossible/aimy-agent
13
- *
14
- * Rules (from SPEC §1):
15
- * 1. Strip protocol (https://, git@, ssh://)
16
- * 2. Replace : with / (for SSH-style URLs)
17
- * 3. Strip .git suffix
18
- * 4. Strip auth credentials (user@)
19
- * 5. Lowercase the host
20
- */
21
- export function normaliseGitRemoteUrl(remoteUrl: string): string {
22
- let url = remoteUrl.trim();
23
-
24
- // Strip protocol
25
- url = url.replace(/^(?:https?|ssh|git):\/\//, "");
26
-
27
- // Strip auth credentials (anything before @ in host part)
28
- // Handle: git@github.com:... and david@github.com/...
29
- url = url.replace(/^[^@]+@/, "");
30
-
31
- // Replace : with / for SSH-style URLs (github.com:org/repo → github.com/org/repo)
32
- // But only if it looks like host:path (not a port like github.com:443/...)
33
- url = url.replace(/^([^/:]+):(?!\d)/, "$1/");
34
-
35
- // Strip .git suffix
36
- url = url.replace(/\.git$/, "");
37
-
38
- // Strip trailing slashes
39
- url = url.replace(/\/+$/, "");
40
-
41
- // Lowercase the host portion (everything before the first /)
42
- const slashIndex = url.indexOf("/");
43
- if (slashIndex !== -1) {
44
- const host = url.substring(0, slashIndex).toLowerCase();
45
- const path = url.substring(slashIndex);
46
- url = host + path;
47
- } else {
48
- url = url.toLowerCase();
49
- }
50
-
51
- return url;
52
- }
53
-
54
- /**
55
- * Extract a human-readable project name from a canonical ID.
56
- * github.com/unimpossible/aimy-agent → aimy-agent
57
- */
58
- export function projectNameFromCanonicalId(canonicalId: string): string {
59
- const parts = canonicalId.split("/");
60
- return parts[parts.length - 1] ?? canonicalId;
61
- }
62
-
63
- /**
64
- * Try to get the git remote origin URL for a directory.
65
- * Returns null if not a git repo or no remote configured.
66
- */
67
- function getGitRemoteUrl(directory: string): string | null {
68
- try {
69
- const url = execSync("git remote get-url origin", {
70
- cwd: directory,
71
- encoding: "utf-8",
72
- timeout: 5000,
73
- stdio: ["pipe", "pipe", "pipe"],
74
- }).trim();
75
- return url || null;
76
- } catch {
77
- // Not a git repo, or no origin remote
78
- // Try any remote
79
- try {
80
- const remotes = execSync("git remote", {
81
- cwd: directory,
82
- encoding: "utf-8",
83
- timeout: 5000,
84
- stdio: ["pipe", "pipe", "pipe"],
85
- })
86
- .trim()
87
- .split("\n")
88
- .filter(Boolean);
89
-
90
- if (remotes.length === 0) return null;
91
-
92
- const url = execSync(`git remote get-url ${remotes[0]}`, {
93
- cwd: directory,
94
- encoding: "utf-8",
95
- timeout: 5000,
96
- stdio: ["pipe", "pipe", "pipe"],
97
- }).trim();
98
- return url || null;
99
- } catch {
100
- return null;
101
- }
102
- }
103
- }
104
-
105
- /**
106
- * Project config file (.engrm.json) for non-git projects or overrides.
107
- */
108
- interface ProjectConfigFile {
109
- project_id: string;
110
- name?: string;
111
- }
112
-
113
- function readProjectConfigFile(directory: string): ProjectConfigFile | null {
114
- const configPath = join(directory, ".engrm.json");
115
- if (!existsSync(configPath)) return null;
116
-
117
- try {
118
- const raw = readFileSync(configPath, "utf-8");
119
- const parsed = JSON.parse(raw) as Record<string, unknown>;
120
-
121
- if (typeof parsed["project_id"] !== "string" || !parsed["project_id"]) {
122
- return null;
123
- }
124
-
125
- return {
126
- project_id: parsed["project_id"],
127
- name: typeof parsed["name"] === "string" ? parsed["name"] : undefined,
128
- };
129
- } catch {
130
- return null;
131
- }
132
- }
133
-
134
- /**
135
- * Detect the project identity for a given directory.
136
- *
137
- * Resolution order (from SPEC §1):
138
- * 1. Git remote origin URL → normalised
139
- * 2. Git remote (any remote if origin doesn't exist)
140
- * 3. Manual project_id in .engrm.json
141
- * 4. Last resort: directory name
142
- *
143
- * Returns { canonicalId, name, remoteUrl, localPath }
144
- */
145
- export interface DetectedProject {
146
- canonical_id: string;
147
- name: string;
148
- remote_url: string | null;
149
- local_path: string;
150
- }
151
-
152
- export function detectProject(directory: string): DetectedProject {
153
- // Try git remote first (covers fallback #1 and #2)
154
- const remoteUrl = getGitRemoteUrl(directory);
155
- if (remoteUrl) {
156
- const canonicalId = normaliseGitRemoteUrl(remoteUrl);
157
- return {
158
- canonical_id: canonicalId,
159
- name: projectNameFromCanonicalId(canonicalId),
160
- remote_url: remoteUrl,
161
- local_path: directory,
162
- };
163
- }
164
-
165
- // Try .engrm.json config file
166
- const configFile = readProjectConfigFile(directory);
167
- if (configFile) {
168
- return {
169
- canonical_id: configFile.project_id,
170
- name: configFile.name ?? projectNameFromCanonicalId(configFile.project_id),
171
- remote_url: null,
172
- local_path: directory,
173
- };
174
- }
175
-
176
- // Last resort: directory name
177
- const dirName = basename(directory);
178
- return {
179
- canonical_id: `local/${dirName}`,
180
- name: dirName,
181
- remote_url: null,
182
- local_path: directory,
183
- };
184
- }