laminark 2.21.6

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 (40) hide show
  1. package/.claude-plugin/marketplace.json +15 -0
  2. package/README.md +182 -0
  3. package/package.json +63 -0
  4. package/plugin/.claude-plugin/plugin.json +13 -0
  5. package/plugin/.mcp.json +12 -0
  6. package/plugin/dist/analysis/worker.d.ts +1 -0
  7. package/plugin/dist/analysis/worker.js +233 -0
  8. package/plugin/dist/analysis/worker.js.map +1 -0
  9. package/plugin/dist/config-t8LZeB-u.mjs +90 -0
  10. package/plugin/dist/config-t8LZeB-u.mjs.map +1 -0
  11. package/plugin/dist/hooks/handler.d.ts +284 -0
  12. package/plugin/dist/hooks/handler.d.ts.map +1 -0
  13. package/plugin/dist/hooks/handler.js +2125 -0
  14. package/plugin/dist/hooks/handler.js.map +1 -0
  15. package/plugin/dist/index.d.ts +445 -0
  16. package/plugin/dist/index.d.ts.map +1 -0
  17. package/plugin/dist/index.js +5831 -0
  18. package/plugin/dist/index.js.map +1 -0
  19. package/plugin/dist/observations-Ch0nc47i.d.mts +170 -0
  20. package/plugin/dist/observations-Ch0nc47i.d.mts.map +1 -0
  21. package/plugin/dist/tool-registry-CZ3mJ4iR.mjs +2655 -0
  22. package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +1 -0
  23. package/plugin/hooks/hooks.json +78 -0
  24. package/plugin/scripts/README.md +47 -0
  25. package/plugin/scripts/bump-version.sh +44 -0
  26. package/plugin/scripts/ensure-deps.sh +12 -0
  27. package/plugin/scripts/install.sh +63 -0
  28. package/plugin/scripts/local-install.sh +103 -0
  29. package/plugin/scripts/setup-tmpdir.sh +65 -0
  30. package/plugin/scripts/uninstall.sh +95 -0
  31. package/plugin/scripts/update.sh +88 -0
  32. package/plugin/scripts/verify-install.sh +43 -0
  33. package/plugin/ui/activity.js +185 -0
  34. package/plugin/ui/app.js +1642 -0
  35. package/plugin/ui/graph.js +2333 -0
  36. package/plugin/ui/help.js +228 -0
  37. package/plugin/ui/index.html +492 -0
  38. package/plugin/ui/settings.js +650 -0
  39. package/plugin/ui/styles.css +2910 -0
  40. package/plugin/ui/timeline.js +652 -0
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "laminark",
3
+ "owner": {
4
+ "name": "NoobyNull"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "laminark",
9
+ "source": "./plugin",
10
+ "description": "Persistent adaptive memory for Claude Code. Automatic observation capture, semantic search, topic detection, knowledge graph, and web visualization.",
11
+ "version": "2.21.6",
12
+ "category": "productivity"
13
+ }
14
+ ]
15
+ }
package/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # Laminark
2
+
3
+ Persistent adaptive memory for Claude Code. Automatically captures observations from your coding sessions, classifies them using LLM-based curation, and surfaces relevant context when you need it.
4
+
5
+ ## Features
6
+
7
+ - Automatic observation capture via Claude Code hooks (Write, Edit, Bash, etc.)
8
+ - LLM-based classification: discoveries, problems, solutions (noise filtered out)
9
+ - Full-text search with BM25 ranking
10
+ - Knowledge graph with entity and relationship tracking
11
+ - Cross-session memory scoped per project
12
+ - Web UI for browsing observations and graph
13
+ - Duplicate detection and secret redaction
14
+
15
+ ## Installation
16
+
17
+ User-level installation is recommended. This enables Laminark across all your projects with data automatically isolated per project directory.
18
+
19
+ ### UI Installation (Easiest)
20
+
21
+ To install via Claude Code's UI (`/plugin` command), first set up TMPDIR to avoid EXDEV errors:
22
+
23
+ ```bash
24
+ # One-time setup - run this once
25
+ git clone https://github.com/NoobyNull/Laminark.git
26
+ cd Laminark
27
+ ./scripts/setup-tmpdir.sh
28
+
29
+ # Then restart your terminal and restart Claude Code
30
+ ```
31
+
32
+ After setup, you can install/update via Claude's UI:
33
+ 1. In Claude Code, type `/plugin`
34
+ 2. Select "Add marketplace"
35
+ 3. Enter `NoobyNull/Laminark`
36
+ 4. Click "Install"
37
+
38
+ **Why this is needed:** Systems with separate filesystems for `/home/` and `/tmp/` (common with btrfs, Docker, or separate partitions) encounter EXDEV errors during plugin installation. Setting TMPDIR fixes this globally.
39
+
40
+ ### Local Installation (Development)
41
+
42
+ For local development or testing:
43
+
44
+ ```bash
45
+ git clone https://github.com/NoobyNull/Laminark.git
46
+ cd Laminark
47
+ npm install
48
+ npm run build
49
+ ./scripts/local-install.sh
50
+ ```
51
+
52
+ ### Marketplace Installation (End Users)
53
+
54
+ ```bash
55
+ ./scripts/install.sh
56
+ # Or: curl -fsSL https://raw.githubusercontent.com/NoobyNull/Laminark/master/scripts/install.sh | bash
57
+ ```
58
+
59
+ ### Manual Installation (Advanced)
60
+
61
+ If you need manual control or encounter issues:
62
+
63
+ ```bash
64
+ # Set TMPDIR to avoid cross-device errors
65
+ export TMPDIR=~/.claude/tmp
66
+ mkdir -p "$TMPDIR"
67
+ claude plugin add /path/to/Laminark
68
+ ```
69
+
70
+ ### Post-Installation
71
+
72
+ Enable the plugin:
73
+
74
+ ```bash
75
+ claude plugin enable laminark
76
+ ```
77
+
78
+ Verify installation:
79
+
80
+ ```bash
81
+ claude plugin list # Should show laminark
82
+ ```
83
+
84
+ Laminark will now run in every Claude Code session. Each project's memory is isolated by directory path -- Project A and Project B never share data, but each project remembers across sessions.
85
+
86
+ ### Updating
87
+
88
+ Check for and install updates:
89
+
90
+ ```bash
91
+ ./scripts/update.sh
92
+ # Or: npm run update
93
+ ```
94
+
95
+ The update script will:
96
+ - Check your current version
97
+ - Fetch the latest version from GitHub
98
+ - Prompt before updating
99
+ - Handle EXDEV errors automatically
100
+
101
+ ### Uninstalling
102
+
103
+ Remove the plugin with optional data cleanup:
104
+
105
+ ```bash
106
+ ./scripts/uninstall.sh
107
+ # Or: npm run uninstall
108
+ ```
109
+
110
+ The uninstall script will:
111
+ - Remove the plugin
112
+ - Ask if you want to keep or delete your data
113
+ - Clean up plugin cache
114
+
115
+ ### Troubleshooting: EXDEV Errors
116
+
117
+ If you see `EXDEV: cross-device link not permitted` errors:
118
+
119
+ **Cause:** Your `/home/` and `/tmp/` directories are on different filesystems (common with btrfs, Docker, or separate partitions).
120
+
121
+ **Solutions (choose one):**
122
+
123
+ 1. **For UI installation** - Run the setup script once:
124
+ ```bash
125
+ ./scripts/setup-tmpdir.sh
126
+ # Then restart terminal and Claude Code
127
+ ```
128
+ This configures TMPDIR globally, allowing you to use Claude's `/plugin` UI.
129
+
130
+ 2. **For command-line installation** - Use our scripts:
131
+ ```bash
132
+ ./scripts/local-install.sh # or ./scripts/install.sh
133
+ ```
134
+ These scripts handle EXDEV automatically without global configuration.
135
+
136
+ ## Why User-Level?
137
+
138
+ - Works in every project automatically -- no per-project `.mcp.json` needed
139
+ - Cross-session memory persists for each project
140
+ - Single database at `~/.laminark/data.db`, scoped by project hash
141
+ - Hooks and MCP tools are available everywhere
142
+
143
+ ## Data Storage
144
+
145
+ All data is stored in a single SQLite database at `~/.laminark/data.db`. Each project is identified by a SHA-256 hash of its directory path, ensuring complete isolation between projects.
146
+
147
+ ## MCP Tools
148
+
149
+ | Tool | Description |
150
+ |------|-------------|
151
+ | `save_memory` | Save an observation with optional title |
152
+ | `recall` | Search, view, purge, or restore memories |
153
+ | `query_graph` | Query the knowledge graph for entities and relationships |
154
+ | `graph_stats` | View knowledge graph statistics |
155
+ | `topic_context` | Show recently stashed context threads |
156
+ | `status` | Show Laminark status and statistics |
157
+
158
+ ## Development
159
+
160
+ ```bash
161
+ npm install
162
+ npm run build
163
+ npm test
164
+ ```
165
+
166
+ ## Release History
167
+
168
+ See [CHANGELOG.md](CHANGELOG.md) for detailed release notes.
169
+
170
+ **Versioning:** Laminark uses `MILESTONE.PHASE.SEQUENTIAL` format (e.g., v2.21.0) aligned with GSD workflow phases.
171
+
172
+ **Latest Releases:**
173
+ - **v2.21.0** (2026-02-14) - Phase 21: Graph Visualization (Milestone v2.2 complete)
174
+ - **v2.18.0** (2026-02-14) - Phase 18: Agent SDK Migration (Milestone v2.1 complete)
175
+ - **v2.16.0** (2026-02-10) - Phase 16: Staleness Management (Milestone v2.0 complete)
176
+ - **v1.8.0** (2026-02-09) - Phase 8: Web Visualization (Milestone v1.0 complete)
177
+
178
+ See [.planning/MILESTONES.md](.planning/MILESTONES.md) for comprehensive milestone documentation.
179
+
180
+ ## License
181
+
182
+ ISC
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "laminark",
3
+ "version": "2.21.6",
4
+ "description": "Persistent adaptive memory for Claude Code",
5
+ "type": "module",
6
+ "bin": {
7
+ "laminark-server": "./plugin/dist/index.js",
8
+ "laminark-hook": "./plugin/dist/hooks/handler.js"
9
+ },
10
+ "main": "./plugin/dist/index.js",
11
+ "types": "./plugin/dist/index.d.ts",
12
+ "files": [
13
+ "plugin",
14
+ ".claude-plugin"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22.0.0"
18
+ },
19
+ "scripts": {
20
+ "build": "tsdown",
21
+ "check": "tsc --noEmit",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "prepublishOnly": "npm run build",
25
+ "install:local": "./scripts/local-install.sh",
26
+ "install:marketplace": "./scripts/install.sh",
27
+ "update": "./scripts/update.sh",
28
+ "uninstall": "./scripts/uninstall.sh"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/NoobyNull/Laminark.git"
33
+ },
34
+ "keywords": [
35
+ "claude",
36
+ "memory",
37
+ "mcp",
38
+ "sqlite"
39
+ ],
40
+ "license": "ISC",
41
+ "bugs": {
42
+ "url": "https://github.com/NoobyNull/Laminark/issues"
43
+ },
44
+ "homepage": "https://github.com/NoobyNull/Laminark#readme",
45
+ "dependencies": {
46
+ "@anthropic-ai/claude-agent-sdk": "^0.2.42",
47
+ "@hono/node-server": "^1.19.9",
48
+ "@huggingface/transformers": "^3.8.1",
49
+ "@modelcontextprotocol/sdk": "^1.26.0",
50
+ "better-sqlite3": "^12.6.2",
51
+ "hono": "^4.11.9",
52
+ "sqlite-vec": "^0.1.7-alpha.2",
53
+ "zod": "^4.3.6"
54
+ },
55
+ "devDependencies": {
56
+ "@types/better-sqlite3": "^7.6.13",
57
+ "@types/node": "^25.2.2",
58
+ "tsdown": "^0.20.3",
59
+ "tsx": "^4.21.0",
60
+ "typescript": "^5.9.3",
61
+ "vitest": "^4.0.18"
62
+ }
63
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "laminark",
3
+ "description": "Persistent adaptive memory for Claude Code. Automatic observation capture, semantic search, topic detection, knowledge graph, and web visualization.",
4
+ "version": "2.21.6",
5
+ "author": {
6
+ "name": "NoobyNull"
7
+ },
8
+ "homepage": "https://github.com/NoobyNull/Laminark",
9
+ "repository": "https://github.com/NoobyNull/Laminark",
10
+ "license": "ISC",
11
+ "keywords": ["memory", "mcp", "sqlite", "knowledge-graph", "semantic-search"],
12
+ "skills": "./skills/"
13
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "mcpServers": {
3
+ "laminark": {
4
+ "command": "bash",
5
+ "args": [
6
+ "${CLAUDE_PLUGIN_ROOT}/scripts/ensure-deps.sh",
7
+ "node",
8
+ "${CLAUDE_PLUGIN_ROOT}/dist/index.js"
9
+ ]
10
+ }
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,233 @@
1
+ import { t as getConfigDir } from "../config-t8LZeB-u.mjs";
2
+ import { join } from "node:path";
3
+ import { parentPort } from "node:worker_threads";
4
+
5
+ //#region src/analysis/engines/local-onnx.ts
6
+ /**
7
+ * Local ONNX embedding engine using @huggingface/transformers.
8
+ *
9
+ * Loads BGE Small EN v1.5 (quantized q8) via dynamic import() for
10
+ * zero startup cost (DQ-04). Model files are cached in ~/.laminark/models/.
11
+ */
12
+ /**
13
+ * Embedding engine backed by BGE Small EN v1.5 running locally via ONNX Runtime.
14
+ *
15
+ * All public methods catch errors internally and return null/false.
16
+ */
17
+ var LocalOnnxEngine = class {
18
+ pipe = null;
19
+ ready = false;
20
+ /**
21
+ * Lazily loads the model via dynamic import().
22
+ *
23
+ * - Uses `@huggingface/transformers` loaded at runtime (not bundled)
24
+ * - Caches model files in ~/.laminark/models/
25
+ * - Returns false on any error (missing runtime, download failure, etc.)
26
+ */
27
+ async initialize() {
28
+ try {
29
+ const { pipeline, env } = await import("@huggingface/transformers");
30
+ env.cacheDir = join(getConfigDir(), "models");
31
+ this.pipe = await pipeline("feature-extraction", "Xenova/bge-small-en-v1.5", { dtype: "q8" });
32
+ this.ready = true;
33
+ return true;
34
+ } catch {
35
+ this.ready = false;
36
+ return false;
37
+ }
38
+ }
39
+ /**
40
+ * Embeds a single text string into a 384-dimensional vector.
41
+ *
42
+ * Returns null if:
43
+ * - Engine not initialized
44
+ * - Input is empty/whitespace
45
+ * - Pipeline throws
46
+ */
47
+ async embed(text) {
48
+ if (!this.ready || !this.pipe) return null;
49
+ if (!text || text.trim().length === 0) return null;
50
+ try {
51
+ const output = await this.pipe(text, {
52
+ pooling: "cls",
53
+ normalize: true
54
+ });
55
+ return Float32Array.from(output.data);
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ /**
61
+ * Embeds multiple texts, preserving order.
62
+ *
63
+ * Returns null for any text that was empty or failed.
64
+ */
65
+ async embedBatch(texts) {
66
+ const results = [];
67
+ for (const text of texts) if (!text || text.trim().length === 0) results.push(null);
68
+ else results.push(await this.embed(text));
69
+ return results;
70
+ }
71
+ /** BGE Small EN v1.5 produces 384-dimensional embeddings. */
72
+ dimensions() {
73
+ return 384;
74
+ }
75
+ /** Engine identifier. */
76
+ name() {
77
+ return "bge-small-en-v1.5-q8";
78
+ }
79
+ /** Whether the model loaded successfully. */
80
+ isReady() {
81
+ return this.ready;
82
+ }
83
+ };
84
+
85
+ //#endregion
86
+ //#region src/analysis/engines/keyword-only.ts
87
+ /**
88
+ * Embedding engine that produces no embeddings.
89
+ *
90
+ * Acts as a silent fallback so that the rest of the system can
91
+ * operate in keyword-only mode without special-casing missing engines.
92
+ */
93
+ var KeywordOnlyEngine = class {
94
+ /** Always returns null -- no model available. */
95
+ async embed() {
96
+ return null;
97
+ }
98
+ /** Returns array of nulls matching input length. */
99
+ async embedBatch(texts) {
100
+ return texts.map(() => null);
101
+ }
102
+ /** No dimensions -- no model. */
103
+ dimensions() {
104
+ return 0;
105
+ }
106
+ /** Engine identifier. */
107
+ name() {
108
+ return "keyword-only";
109
+ }
110
+ /** Intentionally returns false -- this engine has no model. */
111
+ async initialize() {
112
+ return false;
113
+ }
114
+ /** Always false -- no model loaded. */
115
+ isReady() {
116
+ return false;
117
+ }
118
+ };
119
+
120
+ //#endregion
121
+ //#region src/analysis/embedder.ts
122
+ /**
123
+ * EmbeddingEngine interface and factory.
124
+ *
125
+ * Defines the pluggable abstraction for text embedding.
126
+ * All consumers depend on this interface -- never on concrete engines.
127
+ */
128
+ /**
129
+ * Creates and initializes an embedding engine.
130
+ *
131
+ * Attempts LocalOnnxEngine first. If initialization fails (missing model,
132
+ * ONNX runtime unavailable, etc.), falls back to KeywordOnlyEngine.
133
+ *
134
+ * Never throws -- always returns a valid engine.
135
+ */
136
+ async function createEmbeddingEngine() {
137
+ const onnxEngine = new LocalOnnxEngine();
138
+ if (await onnxEngine.initialize()) return onnxEngine;
139
+ return new KeywordOnlyEngine();
140
+ }
141
+
142
+ //#endregion
143
+ //#region src/analysis/worker.ts
144
+ /**
145
+ * Worker thread entry point for off-main-thread embedding.
146
+ *
147
+ * Receives embed/embed_batch/shutdown messages from the main thread via
148
+ * parentPort, runs the embedding engine, and responds with Float32Array
149
+ * results using zero-copy transfer.
150
+ *
151
+ * Compiled as a separate tsdown entry point to dist/analysis/worker.js.
152
+ */
153
+ if (!parentPort) throw new Error("worker.ts must be run as a Worker thread");
154
+ const port = parentPort;
155
+ async function init() {
156
+ let engineName = "keyword-only";
157
+ let dimensions = 0;
158
+ try {
159
+ const engine = await createEmbeddingEngine();
160
+ engineName = engine.name();
161
+ dimensions = engine.dimensions();
162
+ port.postMessage({
163
+ type: "ready",
164
+ engineName,
165
+ dimensions
166
+ });
167
+ port.on("message", async (msg) => {
168
+ if (msg.type === "embed") try {
169
+ const embedding = await engine.embed(msg.text);
170
+ if (embedding === null) port.postMessage({
171
+ type: "embed_result",
172
+ id: msg.id,
173
+ embedding: null
174
+ });
175
+ else {
176
+ const buf = embedding.buffer;
177
+ port.postMessage({
178
+ type: "embed_result",
179
+ id: msg.id,
180
+ embedding
181
+ }, [buf]);
182
+ }
183
+ } catch {
184
+ port.postMessage({
185
+ type: "embed_result",
186
+ id: msg.id,
187
+ embedding: null
188
+ });
189
+ }
190
+ else if (msg.type === "embed_batch") try {
191
+ const embeddings = await engine.embedBatch(msg.texts);
192
+ const transferList = [];
193
+ for (const emb of embeddings) if (emb !== null) transferList.push(emb.buffer);
194
+ port.postMessage({
195
+ type: "embed_batch_result",
196
+ id: msg.id,
197
+ embeddings
198
+ }, transferList);
199
+ } catch {
200
+ port.postMessage({
201
+ type: "embed_batch_result",
202
+ id: msg.id,
203
+ embeddings: msg.texts.map(() => null)
204
+ });
205
+ }
206
+ else if (msg.type === "shutdown") process.exit(0);
207
+ });
208
+ } catch {
209
+ port.postMessage({
210
+ type: "ready",
211
+ engineName,
212
+ dimensions
213
+ });
214
+ port.on("message", (msg) => {
215
+ if (msg.type === "embed") port.postMessage({
216
+ type: "embed_result",
217
+ id: msg.id,
218
+ embedding: null
219
+ });
220
+ else if (msg.type === "embed_batch") port.postMessage({
221
+ type: "embed_batch_result",
222
+ id: msg.id,
223
+ embeddings: msg.texts.map(() => null)
224
+ });
225
+ else if (msg.type === "shutdown") process.exit(0);
226
+ });
227
+ }
228
+ }
229
+ init();
230
+
231
+ //#endregion
232
+ export { };
233
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","names":[],"sources":["../../../src/analysis/engines/local-onnx.ts","../../../src/analysis/engines/keyword-only.ts","../../../src/analysis/embedder.ts","../../../src/analysis/worker.ts"],"sourcesContent":["/**\n * Local ONNX embedding engine using @huggingface/transformers.\n *\n * Loads BGE Small EN v1.5 (quantized q8) via dynamic import() for\n * zero startup cost (DQ-04). Model files are cached in ~/.laminark/models/.\n */\n\nimport { join } from 'node:path';\n\nimport { getConfigDir } from '../../shared/config.js';\nimport type { EmbeddingEngine } from '../embedder.js';\n\n// Pipeline type from @huggingface/transformers -- kept as `unknown` to avoid\n// hard dependency on the library's type definitions at import time.\ntype Pipeline = (\n text: string,\n options?: { pooling?: string; normalize?: boolean },\n) => Promise<{ data: ArrayLike<number> }>;\n\n/**\n * Embedding engine backed by BGE Small EN v1.5 running locally via ONNX Runtime.\n *\n * All public methods catch errors internally and return null/false.\n */\nexport class LocalOnnxEngine implements EmbeddingEngine {\n private pipe: Pipeline | null = null;\n private ready = false;\n\n /**\n * Lazily loads the model via dynamic import().\n *\n * - Uses `@huggingface/transformers` loaded at runtime (not bundled)\n * - Caches model files in ~/.laminark/models/\n * - Returns false on any error (missing runtime, download failure, etc.)\n */\n async initialize(): Promise<boolean> {\n try {\n const { pipeline, env } = await import('@huggingface/transformers');\n\n // Cache models in user config directory\n env.cacheDir = join(getConfigDir(), 'models');\n\n this.pipe = (await pipeline('feature-extraction', 'Xenova/bge-small-en-v1.5', {\n dtype: 'q8',\n })) as unknown as Pipeline;\n\n this.ready = true;\n return true;\n } catch {\n this.ready = false;\n return false;\n }\n }\n\n /**\n * Embeds a single text string into a 384-dimensional vector.\n *\n * Returns null if:\n * - Engine not initialized\n * - Input is empty/whitespace\n * - Pipeline throws\n */\n async embed(text: string): Promise<Float32Array | null> {\n if (!this.ready || !this.pipe) {\n return null;\n }\n\n if (!text || text.trim().length === 0) {\n return null;\n }\n\n try {\n const output = await this.pipe(text, { pooling: 'cls', normalize: true });\n return Float32Array.from(output.data);\n } catch {\n return null;\n }\n }\n\n /**\n * Embeds multiple texts, preserving order.\n *\n * Returns null for any text that was empty or failed.\n */\n async embedBatch(texts: string[]): Promise<(Float32Array | null)[]> {\n const results: (Float32Array | null)[] = [];\n\n for (const text of texts) {\n if (!text || text.trim().length === 0) {\n results.push(null);\n } else {\n results.push(await this.embed(text));\n }\n }\n\n return results;\n }\n\n /** BGE Small EN v1.5 produces 384-dimensional embeddings. */\n dimensions(): number {\n return 384;\n }\n\n /** Engine identifier. */\n name(): string {\n return 'bge-small-en-v1.5-q8';\n }\n\n /** Whether the model loaded successfully. */\n isReady(): boolean {\n return this.ready;\n }\n}\n","/**\n * Null fallback embedding engine for graceful degradation (DQ-03).\n *\n * Used when the ONNX runtime or model is unavailable.\n * All embedding methods return null -- search falls back to keyword-only (FTS5).\n */\n\nimport type { EmbeddingEngine } from '../embedder.js';\n\n/**\n * Embedding engine that produces no embeddings.\n *\n * Acts as a silent fallback so that the rest of the system can\n * operate in keyword-only mode without special-casing missing engines.\n */\nexport class KeywordOnlyEngine implements EmbeddingEngine {\n /** Always returns null -- no model available. */\n async embed(): Promise<Float32Array | null> {\n return null;\n }\n\n /** Returns array of nulls matching input length. */\n async embedBatch(texts: string[]): Promise<(Float32Array | null)[]> {\n return texts.map(() => null);\n }\n\n /** No dimensions -- no model. */\n dimensions(): number {\n return 0;\n }\n\n /** Engine identifier. */\n name(): string {\n return 'keyword-only';\n }\n\n /** Intentionally returns false -- this engine has no model. */\n async initialize(): Promise<boolean> {\n return false;\n }\n\n /** Always false -- no model loaded. */\n isReady(): boolean {\n return false;\n }\n}\n","/**\n * EmbeddingEngine interface and factory.\n *\n * Defines the pluggable abstraction for text embedding.\n * All consumers depend on this interface -- never on concrete engines.\n */\n\nimport { LocalOnnxEngine } from './engines/local-onnx.js';\nimport { KeywordOnlyEngine } from './engines/keyword-only.js';\n\n/**\n * Pluggable embedding engine abstraction.\n *\n * All methods that can fail return null/false -- engines NEVER throw.\n * This is critical for graceful degradation (DQ-03).\n */\nexport interface EmbeddingEngine {\n /** Embed a single text string. Returns null on failure or empty input. */\n embed(text: string): Promise<Float32Array | null>;\n\n /** Embed multiple texts. Returns null for any that failed or were empty. */\n embedBatch(texts: string[]): Promise<(Float32Array | null)[]>;\n\n /** Embedding dimensions (384 for BGE Small, 0 for keyword-only). */\n dimensions(): number;\n\n /** Engine identifier string. */\n name(): string;\n\n /** Lazy initialization. Returns true on success, false on failure. */\n initialize(): Promise<boolean>;\n\n /** Whether initialize() has been called and succeeded. */\n isReady(): boolean;\n}\n\n/**\n * Creates and initializes an embedding engine.\n *\n * Attempts LocalOnnxEngine first. If initialization fails (missing model,\n * ONNX runtime unavailable, etc.), falls back to KeywordOnlyEngine.\n *\n * Never throws -- always returns a valid engine.\n */\nexport async function createEmbeddingEngine(): Promise<EmbeddingEngine> {\n const onnxEngine = new LocalOnnxEngine();\n const success = await onnxEngine.initialize();\n\n if (success) {\n return onnxEngine;\n }\n\n return new KeywordOnlyEngine();\n}\n","/**\n * Worker thread entry point for off-main-thread embedding.\n *\n * Receives embed/embed_batch/shutdown messages from the main thread via\n * parentPort, runs the embedding engine, and responds with Float32Array\n * results using zero-copy transfer.\n *\n * Compiled as a separate tsdown entry point to dist/analysis/worker.js.\n */\n\nimport { parentPort } from 'node:worker_threads';\nimport { createEmbeddingEngine } from './embedder.js';\n\nif (!parentPort) {\n throw new Error('worker.ts must be run as a Worker thread');\n}\n\nconst port = parentPort;\n\ninterface EmbedMessage {\n type: 'embed';\n id: string;\n text: string;\n}\n\ninterface EmbedBatchMessage {\n type: 'embed_batch';\n id: string;\n texts: string[];\n}\n\ninterface ShutdownMessage {\n type: 'shutdown';\n}\n\ntype WorkerMessage = EmbedMessage | EmbedBatchMessage | ShutdownMessage;\n\nasync function init(): Promise<void> {\n let engineName = 'keyword-only';\n let dimensions = 0;\n\n try {\n const engine = await createEmbeddingEngine();\n engineName = engine.name();\n dimensions = engine.dimensions();\n\n port.postMessage({ type: 'ready', engineName, dimensions });\n\n port.on('message', async (msg: WorkerMessage) => {\n if (msg.type === 'embed') {\n try {\n const embedding = await engine.embed(msg.text);\n\n if (embedding === null) {\n port.postMessage({ type: 'embed_result', id: msg.id, embedding: null });\n } else {\n // Zero-copy transfer of the underlying ArrayBuffer\n const buf = embedding.buffer as ArrayBuffer;\n port.postMessage(\n { type: 'embed_result', id: msg.id, embedding },\n [buf],\n );\n }\n } catch {\n port.postMessage({ type: 'embed_result', id: msg.id, embedding: null });\n }\n } else if (msg.type === 'embed_batch') {\n try {\n const embeddings = await engine.embedBatch(msg.texts);\n\n // Collect non-null buffers for zero-copy transfer\n const transferList: ArrayBuffer[] = [];\n for (const emb of embeddings) {\n if (emb !== null) {\n transferList.push(emb.buffer as ArrayBuffer);\n }\n }\n\n port.postMessage(\n { type: 'embed_batch_result', id: msg.id, embeddings },\n transferList,\n );\n } catch {\n port.postMessage({\n type: 'embed_batch_result',\n id: msg.id,\n embeddings: msg.texts.map(() => null),\n });\n }\n } else if (msg.type === 'shutdown') {\n process.exit(0);\n }\n });\n } catch {\n // Engine creation failed -- still report ready with keyword-only fallback\n port.postMessage({ type: 'ready', engineName, dimensions });\n\n port.on('message', (msg: WorkerMessage) => {\n if (msg.type === 'embed') {\n port.postMessage({ type: 'embed_result', id: msg.id, embedding: null });\n } else if (msg.type === 'embed_batch') {\n port.postMessage({\n type: 'embed_batch_result',\n id: msg.id,\n embeddings: msg.texts.map(() => null),\n });\n } else if (msg.type === 'shutdown') {\n process.exit(0);\n }\n });\n }\n}\n\ninit();\n"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import { mkdirSync, readFileSync, realpathSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { homedir } from "node:os";
5
+
6
+ //#region src/shared/config.ts
7
+ /**
8
+ * Cached debug-enabled flag.
9
+ * Resolved once per process -- debug mode does not change at runtime.
10
+ */
11
+ let _debugCached = null;
12
+ /**
13
+ * Returns whether debug logging is enabled for this process.
14
+ *
15
+ * Resolution order:
16
+ * 1. `LAMINARK_DEBUG` env var -- `"1"` or `"true"` enables debug mode
17
+ * 2. `~/.laminark/config.json` -- `{ "debug": true }` enables debug mode
18
+ * 3. Default: disabled
19
+ *
20
+ * The result is cached after the first call.
21
+ */
22
+ function isDebugEnabled() {
23
+ if (_debugCached !== null) return _debugCached;
24
+ const envVal = process.env.LAMINARK_DEBUG;
25
+ if (envVal === "1" || envVal === "true") {
26
+ _debugCached = true;
27
+ return true;
28
+ }
29
+ try {
30
+ const raw = readFileSync(join(getConfigDir(), "config.json"), "utf-8");
31
+ if (JSON.parse(raw).debug === true) {
32
+ _debugCached = true;
33
+ return true;
34
+ }
35
+ } catch {}
36
+ _debugCached = false;
37
+ return false;
38
+ }
39
+ /**
40
+ * Default busy timeout in milliseconds.
41
+ * Must be >= 5000ms to prevent SQLITE_BUSY under concurrent load.
42
+ * Source: SQLite docs + better-sqlite3 performance recommendations.
43
+ */
44
+ const DEFAULT_BUSY_TIMEOUT = 5e3;
45
+ /**
46
+ * Returns the Laminark data directory.
47
+ * Default: ~/.claude/plugins/cache/laminark/data/
48
+ * Creates the directory recursively if it does not exist.
49
+ *
50
+ * Supports LAMINARK_DATA_DIR env var override for testing --
51
+ * redirects all data storage to a custom directory without
52
+ * affecting the real plugin data.
53
+ */
54
+ function getConfigDir() {
55
+ const dir = process.env.LAMINARK_DATA_DIR || join(homedir(), ".claude", "plugins", "cache", "laminark", "data");
56
+ mkdirSync(dir, { recursive: true });
57
+ return dir;
58
+ }
59
+ /**
60
+ * Returns the path to the single Laminark database file.
61
+ * Single database at ~/.claude/plugins/cache/laminark/data/data.db for ALL projects.
62
+ */
63
+ function getDbPath() {
64
+ return join(getConfigDir(), "data.db");
65
+ }
66
+ /**
67
+ * Creates a deterministic SHA-256 hash of a project directory path.
68
+ * Uses realpathSync to canonicalize (resolves symlinks) to prevent
69
+ * multiple hashes for the same directory via different paths.
70
+ *
71
+ * @param projectDir - The project directory path to hash
72
+ * @returns First 16 hex characters of the SHA-256 hash
73
+ */
74
+ function getProjectHash(projectDir) {
75
+ const canonical = realpathSync(resolve(projectDir));
76
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
77
+ }
78
+ /**
79
+ * Returns the default database configuration.
80
+ */
81
+ function getDatabaseConfig() {
82
+ return {
83
+ dbPath: getDbPath(),
84
+ busyTimeout: DEFAULT_BUSY_TIMEOUT
85
+ };
86
+ }
87
+
88
+ //#endregion
89
+ export { isDebugEnabled as a, getProjectHash as i, getDatabaseConfig as n, getDbPath as r, getConfigDir as t };
90
+ //# sourceMappingURL=config-t8LZeB-u.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-t8LZeB-u.mjs","names":[],"sources":["../../src/shared/config.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { mkdirSync, readFileSync, realpathSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join, resolve } from 'node:path';\n\nimport type { DatabaseConfig } from './types.js';\n\n/**\n * Cached debug-enabled flag.\n * Resolved once per process -- debug mode does not change at runtime.\n */\nlet _debugCached: boolean | null = null;\n\n/**\n * Returns whether debug logging is enabled for this process.\n *\n * Resolution order:\n * 1. `LAMINARK_DEBUG` env var -- `\"1\"` or `\"true\"` enables debug mode\n * 2. `~/.laminark/config.json` -- `{ \"debug\": true }` enables debug mode\n * 3. Default: disabled\n *\n * The result is cached after the first call.\n */\nexport function isDebugEnabled(): boolean {\n if (_debugCached !== null) {\n return _debugCached;\n }\n\n // Check environment variable first\n const envVal = process.env.LAMINARK_DEBUG;\n if (envVal === '1' || envVal === 'true') {\n _debugCached = true;\n return true;\n }\n\n // Check config.json\n try {\n const configPath = join(getConfigDir(), 'config.json');\n const raw = readFileSync(configPath, 'utf-8');\n const config = JSON.parse(raw) as Record<string, unknown>;\n if (config.debug === true) {\n _debugCached = true;\n return true;\n }\n } catch {\n // Config file doesn't exist or is invalid -- that's fine\n }\n\n _debugCached = false;\n return false;\n}\n\n/**\n * Default busy timeout in milliseconds.\n * Must be >= 5000ms to prevent SQLITE_BUSY under concurrent load.\n * Source: SQLite docs + better-sqlite3 performance recommendations.\n */\nexport const DEFAULT_BUSY_TIMEOUT = 5000;\n\n/**\n * Returns the Laminark data directory.\n * Default: ~/.claude/plugins/cache/laminark/data/\n * Creates the directory recursively if it does not exist.\n *\n * Supports LAMINARK_DATA_DIR env var override for testing --\n * redirects all data storage to a custom directory without\n * affecting the real plugin data.\n */\nexport function getConfigDir(): string {\n const dir = process.env.LAMINARK_DATA_DIR || join(homedir(), '.claude', 'plugins', 'cache', 'laminark', 'data');\n mkdirSync(dir, { recursive: true });\n return dir;\n}\n\n/**\n * Returns the path to the single Laminark database file.\n * Single database at ~/.claude/plugins/cache/laminark/data/data.db for ALL projects.\n */\nexport function getDbPath(): string {\n return join(getConfigDir(), 'data.db');\n}\n\n/**\n * Creates a deterministic SHA-256 hash of a project directory path.\n * Uses realpathSync to canonicalize (resolves symlinks) to prevent\n * multiple hashes for the same directory via different paths.\n *\n * @param projectDir - The project directory path to hash\n * @returns First 16 hex characters of the SHA-256 hash\n */\nexport function getProjectHash(projectDir: string): string {\n const canonical = realpathSync(resolve(projectDir));\n return createHash('sha256').update(canonical).digest('hex').slice(0, 16);\n}\n\n/**\n * Returns the default database configuration.\n */\nexport function getDatabaseConfig(): DatabaseConfig {\n return {\n dbPath: getDbPath(),\n busyTimeout: DEFAULT_BUSY_TIMEOUT,\n };\n}\n"],"mappings":";;;;;;;;;;AAWA,IAAI,eAA+B;;;;;;;;;;;AAYnC,SAAgB,iBAA0B;AACxC,KAAI,iBAAiB,KACnB,QAAO;CAIT,MAAM,SAAS,QAAQ,IAAI;AAC3B,KAAI,WAAW,OAAO,WAAW,QAAQ;AACvC,iBAAe;AACf,SAAO;;AAIT,KAAI;EAEF,MAAM,MAAM,aADO,KAAK,cAAc,EAAE,cAAc,EACjB,QAAQ;AAE7C,MADe,KAAK,MAAM,IAAI,CACnB,UAAU,MAAM;AACzB,kBAAe;AACf,UAAO;;SAEH;AAIR,gBAAe;AACf,QAAO;;;;;;;AAQT,MAAa,uBAAuB;;;;;;;;;;AAWpC,SAAgB,eAAuB;CACrC,MAAM,MAAM,QAAQ,IAAI,qBAAqB,KAAK,SAAS,EAAE,WAAW,WAAW,SAAS,YAAY,OAAO;AAC/G,WAAU,KAAK,EAAE,WAAW,MAAM,CAAC;AACnC,QAAO;;;;;;AAOT,SAAgB,YAAoB;AAClC,QAAO,KAAK,cAAc,EAAE,UAAU;;;;;;;;;;AAWxC,SAAgB,eAAe,YAA4B;CACzD,MAAM,YAAY,aAAa,QAAQ,WAAW,CAAC;AACnD,QAAO,WAAW,SAAS,CAAC,OAAO,UAAU,CAAC,OAAO,MAAM,CAAC,MAAM,GAAG,GAAG;;;;;AAM1E,SAAgB,oBAAoC;AAClD,QAAO;EACL,QAAQ,WAAW;EACnB,aAAa;EACd"}