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
package/src/config.ts ADDED
@@ -0,0 +1,238 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir, hostname } from "node:os";
3
+ import { join } from "node:path";
4
+ import { randomBytes } from "node:crypto";
5
+
6
+ // --- Types ---
7
+
8
+ export interface SyncConfig {
9
+ enabled: boolean;
10
+ interval_seconds: number;
11
+ batch_size: number;
12
+ }
13
+
14
+ export interface SearchConfig {
15
+ default_limit: number;
16
+ local_boost: number;
17
+ scope: "personal" | "team" | "all";
18
+ }
19
+
20
+ export interface ScrubbingConfig {
21
+ enabled: boolean;
22
+ custom_patterns: string[];
23
+ default_sensitivity: "shared" | "personal" | "secret";
24
+ }
25
+
26
+ export interface TeamMembership {
27
+ id: string;
28
+ name: string;
29
+ namespace: string;
30
+ }
31
+
32
+ export interface Config {
33
+ candengo_url: string;
34
+ candengo_api_key: string;
35
+ site_id: string;
36
+ namespace: string;
37
+ user_id: string;
38
+ user_email: string;
39
+ device_id: string;
40
+ teams: TeamMembership[];
41
+ sync: SyncConfig;
42
+ search: SearchConfig;
43
+ scrubbing: ScrubbingConfig;
44
+ }
45
+
46
+ // --- Paths ---
47
+
48
+ const CONFIG_DIR = join(homedir(), ".engrm");
49
+ const SETTINGS_PATH = join(CONFIG_DIR, "settings.json");
50
+ const DB_PATH = join(CONFIG_DIR, "engrm.db");
51
+
52
+ export function getConfigDir(): string {
53
+ return CONFIG_DIR;
54
+ }
55
+
56
+ export function getSettingsPath(): string {
57
+ return SETTINGS_PATH;
58
+ }
59
+
60
+ export function getDbPath(): string {
61
+ return DB_PATH;
62
+ }
63
+
64
+ // --- Device ID ---
65
+
66
+ function generateDeviceId(): string {
67
+ const host = hostname().toLowerCase().replace(/[^a-z0-9-]/g, "");
68
+ const suffix = randomBytes(4).toString("hex");
69
+ return `${host}-${suffix}`;
70
+ }
71
+
72
+ // --- Defaults ---
73
+
74
+ function createDefaultConfig(): Config {
75
+ return {
76
+ candengo_url: "",
77
+ candengo_api_key: "",
78
+ site_id: "",
79
+ namespace: "",
80
+ user_id: "",
81
+ user_email: "",
82
+ device_id: generateDeviceId(),
83
+ teams: [],
84
+ sync: {
85
+ enabled: true,
86
+ interval_seconds: 30,
87
+ batch_size: 50,
88
+ },
89
+ search: {
90
+ default_limit: 10,
91
+ local_boost: 1.2,
92
+ scope: "all",
93
+ },
94
+ scrubbing: {
95
+ enabled: true,
96
+ custom_patterns: [],
97
+ default_sensitivity: "shared",
98
+ },
99
+ };
100
+ }
101
+
102
+ // --- Load / Save ---
103
+
104
+ export function loadConfig(): Config {
105
+ if (!existsSync(SETTINGS_PATH)) {
106
+ throw new Error(
107
+ `Config not found at ${SETTINGS_PATH}. Run 'engrm init --manual' to configure.`
108
+ );
109
+ }
110
+
111
+ const raw = readFileSync(SETTINGS_PATH, "utf-8");
112
+ let parsed: unknown;
113
+ try {
114
+ parsed = JSON.parse(raw);
115
+ } catch {
116
+ throw new Error(`Invalid JSON in ${SETTINGS_PATH}`);
117
+ }
118
+
119
+ if (typeof parsed !== "object" || parsed === null) {
120
+ throw new Error(`Config at ${SETTINGS_PATH} is not a JSON object`);
121
+ }
122
+
123
+ const config = parsed as Record<string, unknown>;
124
+
125
+ // Merge with defaults to fill any missing fields
126
+ const defaults = createDefaultConfig();
127
+ return {
128
+ candengo_url: asString(config["candengo_url"], defaults.candengo_url),
129
+ candengo_api_key: asString(config["candengo_api_key"], defaults.candengo_api_key),
130
+ site_id: asString(config["site_id"], defaults.site_id),
131
+ namespace: asString(config["namespace"], defaults.namespace),
132
+ user_id: asString(config["user_id"], defaults.user_id),
133
+ user_email: asString(config["user_email"], defaults.user_email),
134
+ device_id: asString(config["device_id"], defaults.device_id),
135
+ teams: asTeams(config["teams"], defaults.teams),
136
+ sync: {
137
+ enabled: asBool(
138
+ (config["sync"] as Record<string, unknown> | undefined)?.["enabled"],
139
+ defaults.sync.enabled
140
+ ),
141
+ interval_seconds: asNumber(
142
+ (config["sync"] as Record<string, unknown> | undefined)?.["interval_seconds"],
143
+ defaults.sync.interval_seconds
144
+ ),
145
+ batch_size: asNumber(
146
+ (config["sync"] as Record<string, unknown> | undefined)?.["batch_size"],
147
+ defaults.sync.batch_size
148
+ ),
149
+ },
150
+ search: {
151
+ default_limit: asNumber(
152
+ (config["search"] as Record<string, unknown> | undefined)?.["default_limit"],
153
+ defaults.search.default_limit
154
+ ),
155
+ local_boost: asNumber(
156
+ (config["search"] as Record<string, unknown> | undefined)?.["local_boost"],
157
+ defaults.search.local_boost
158
+ ),
159
+ scope: asScope(
160
+ (config["search"] as Record<string, unknown> | undefined)?.["scope"],
161
+ defaults.search.scope
162
+ ),
163
+ },
164
+ scrubbing: {
165
+ enabled: asBool(
166
+ (config["scrubbing"] as Record<string, unknown> | undefined)?.["enabled"],
167
+ defaults.scrubbing.enabled
168
+ ),
169
+ custom_patterns: asStringArray(
170
+ (config["scrubbing"] as Record<string, unknown> | undefined)?.["custom_patterns"],
171
+ defaults.scrubbing.custom_patterns
172
+ ),
173
+ default_sensitivity: asSensitivity(
174
+ (config["scrubbing"] as Record<string, unknown> | undefined)?.["default_sensitivity"],
175
+ defaults.scrubbing.default_sensitivity
176
+ ),
177
+ },
178
+ };
179
+ }
180
+
181
+ export function saveConfig(config: Config): void {
182
+ if (!existsSync(CONFIG_DIR)) {
183
+ mkdirSync(CONFIG_DIR, { recursive: true });
184
+ }
185
+ writeFileSync(SETTINGS_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
186
+ }
187
+
188
+ export function configExists(): boolean {
189
+ return existsSync(SETTINGS_PATH);
190
+ }
191
+
192
+ // --- Type helpers ---
193
+
194
+ function asString(value: unknown, fallback: string): string {
195
+ return typeof value === "string" ? value : fallback;
196
+ }
197
+
198
+ function asNumber(value: unknown, fallback: number): number {
199
+ return typeof value === "number" && !Number.isNaN(value) ? value : fallback;
200
+ }
201
+
202
+ function asBool(value: unknown, fallback: boolean): boolean {
203
+ return typeof value === "boolean" ? value : fallback;
204
+ }
205
+
206
+ function asStringArray(value: unknown, fallback: string[]): string[] {
207
+ return Array.isArray(value) && value.every((v) => typeof v === "string")
208
+ ? (value as string[])
209
+ : fallback;
210
+ }
211
+
212
+ function asScope(
213
+ value: unknown,
214
+ fallback: "personal" | "team" | "all"
215
+ ): "personal" | "team" | "all" {
216
+ if (value === "personal" || value === "team" || value === "all") return value;
217
+ return fallback;
218
+ }
219
+
220
+ function asSensitivity(
221
+ value: unknown,
222
+ fallback: "shared" | "personal" | "secret"
223
+ ): "shared" | "personal" | "secret" {
224
+ if (value === "shared" || value === "personal" || value === "secret") return value;
225
+ return fallback;
226
+ }
227
+
228
+ function asTeams(value: unknown, fallback: TeamMembership[]): TeamMembership[] {
229
+ if (!Array.isArray(value)) return fallback;
230
+ return value.filter(
231
+ (t): t is TeamMembership =>
232
+ typeof t === "object" &&
233
+ t !== null &&
234
+ typeof t.id === "string" &&
235
+ typeof t.name === "string" &&
236
+ typeof t.namespace === "string"
237
+ );
238
+ }