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.
- package/.claude-plugin/marketplace.json +15 -0
- package/README.md +182 -0
- package/package.json +63 -0
- package/plugin/.claude-plugin/plugin.json +13 -0
- package/plugin/.mcp.json +12 -0
- package/plugin/dist/analysis/worker.d.ts +1 -0
- package/plugin/dist/analysis/worker.js +233 -0
- package/plugin/dist/analysis/worker.js.map +1 -0
- package/plugin/dist/config-t8LZeB-u.mjs +90 -0
- package/plugin/dist/config-t8LZeB-u.mjs.map +1 -0
- package/plugin/dist/hooks/handler.d.ts +284 -0
- package/plugin/dist/hooks/handler.d.ts.map +1 -0
- package/plugin/dist/hooks/handler.js +2125 -0
- package/plugin/dist/hooks/handler.js.map +1 -0
- package/plugin/dist/index.d.ts +445 -0
- package/plugin/dist/index.d.ts.map +1 -0
- package/plugin/dist/index.js +5831 -0
- package/plugin/dist/index.js.map +1 -0
- package/plugin/dist/observations-Ch0nc47i.d.mts +170 -0
- package/plugin/dist/observations-Ch0nc47i.d.mts.map +1 -0
- package/plugin/dist/tool-registry-CZ3mJ4iR.mjs +2655 -0
- package/plugin/dist/tool-registry-CZ3mJ4iR.mjs.map +1 -0
- package/plugin/hooks/hooks.json +78 -0
- package/plugin/scripts/README.md +47 -0
- package/plugin/scripts/bump-version.sh +44 -0
- package/plugin/scripts/ensure-deps.sh +12 -0
- package/plugin/scripts/install.sh +63 -0
- package/plugin/scripts/local-install.sh +103 -0
- package/plugin/scripts/setup-tmpdir.sh +65 -0
- package/plugin/scripts/uninstall.sh +95 -0
- package/plugin/scripts/update.sh +88 -0
- package/plugin/scripts/verify-install.sh +43 -0
- package/plugin/ui/activity.js +185 -0
- package/plugin/ui/app.js +1642 -0
- package/plugin/ui/graph.js +2333 -0
- package/plugin/ui/help.js +228 -0
- package/plugin/ui/index.html +492 -0
- package/plugin/ui/settings.js +650 -0
- package/plugin/ui/styles.css +2910 -0
- 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
|
+
}
|
package/plugin/.mcp.json
ADDED
|
@@ -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"}
|