@dug-21/unimatrix 0.5.5
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/bin/unimatrix.js +62 -0
- package/lib/init.js +267 -0
- package/lib/merge-settings.js +188 -0
- package/lib/resolve-binary.js +69 -0
- package/package.json +32 -0
- package/postinstall.js +61 -0
- package/skills/knowledge-lookup/SKILL.md +120 -0
- package/skills/knowledge-search/SKILL.md +113 -0
- package/skills/query-patterns/SKILL.md +110 -0
- package/skills/record-outcome/SKILL.md +96 -0
- package/skills/retro/SKILL.md +296 -0
- package/skills/review-pr/SKILL.md +128 -0
- package/skills/store-adr/SKILL.md +142 -0
- package/skills/store-lesson/SKILL.md +109 -0
- package/skills/store-pattern/SKILL.md +124 -0
- package/skills/store-procedure/SKILL.md +114 -0
- package/skills/uni-git/SKILL.md +117 -0
- package/skills/uni-init/SKILL.md +169 -0
- package/skills/uni-knowledge-lookup/SKILL.md +120 -0
- package/skills/uni-knowledge-search/SKILL.md +113 -0
- package/skills/uni-query-patterns/SKILL.md +110 -0
- package/skills/uni-record-outcome/SKILL.md +96 -0
- package/skills/uni-release/SKILL.md +210 -0
- package/skills/uni-retro/SKILL.md +296 -0
- package/skills/uni-review-pr/SKILL.md +128 -0
- package/skills/uni-seed/SKILL.md +271 -0
- package/skills/uni-store-adr/SKILL.md +142 -0
- package/skills/uni-store-lesson/SKILL.md +109 -0
- package/skills/uni-store-pattern/SKILL.md +124 -0
- package/skills/uni-store-procedure/SKILL.md +114 -0
- package/skills/unimatrix-init/SKILL.md +171 -0
- package/skills/unimatrix-seed/SKILL.md +271 -0
package/bin/unimatrix.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { execFileSync } = require("child_process");
|
|
5
|
+
|
|
6
|
+
function main() {
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
// Route "init" to JS implementation (ADR-003)
|
|
10
|
+
if (args[0] === "init") {
|
|
11
|
+
const { init } = require("../lib/init.js");
|
|
12
|
+
const projectDirIdx = args.indexOf("--project-dir");
|
|
13
|
+
const projectDir =
|
|
14
|
+
projectDirIdx >= 0 && projectDirIdx + 1 < args.length
|
|
15
|
+
? args[projectDirIdx + 1]
|
|
16
|
+
: undefined;
|
|
17
|
+
init({ dryRun: args.includes("--dry-run"), projectDir })
|
|
18
|
+
.then(() => {
|
|
19
|
+
process.exitCode = 0;
|
|
20
|
+
})
|
|
21
|
+
.catch((error) => {
|
|
22
|
+
process.stderr.write("unimatrix init failed: " + error.message + "\n");
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// All other subcommands: resolve binary and exec
|
|
29
|
+
let binaryPath;
|
|
30
|
+
try {
|
|
31
|
+
binaryPath = require("../lib/resolve-binary.js").resolveBinary();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
process.stderr.write(error.message + "\n");
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Ensure bundled shared libraries (libonnxruntime) are found at runtime
|
|
39
|
+
const binDir = require("path").dirname(binaryPath);
|
|
40
|
+
const ldPath = process.env.LD_LIBRARY_PATH;
|
|
41
|
+
const env = Object.assign({}, process.env, {
|
|
42
|
+
LD_LIBRARY_PATH: ldPath ? binDir + ":" + ldPath : binDir,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
execFileSync(binaryPath, args, { stdio: "inherit", env: env });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// execFileSync throws on non-zero exit code
|
|
49
|
+
// error.status contains the exit code from the child process
|
|
50
|
+
if (error.status !== null && error.status !== undefined) {
|
|
51
|
+
process.exitCode = error.status;
|
|
52
|
+
} else {
|
|
53
|
+
// Signal death or spawn failure
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
"Failed to execute unimatrix: " + error.message + "\n"
|
|
56
|
+
);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
main();
|
package/lib/init.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { execFileSync } = require("child_process");
|
|
6
|
+
const { resolveBinary } = require("./resolve-binary.js");
|
|
7
|
+
const { mergeSettings } = require("./merge-settings.js");
|
|
8
|
+
|
|
9
|
+
const HOOK_EVENTS = [
|
|
10
|
+
"SessionStart",
|
|
11
|
+
"Stop",
|
|
12
|
+
"UserPromptSubmit",
|
|
13
|
+
"PreToolUse",
|
|
14
|
+
"PostToolUse",
|
|
15
|
+
"SubagentStart",
|
|
16
|
+
"SubagentStop",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect project root by walking up from startDir to find .git directory.
|
|
21
|
+
* Mirrors the Rust detect_project_root algorithm (ADR-003).
|
|
22
|
+
*
|
|
23
|
+
* @param {string} startDir - Directory to start searching from.
|
|
24
|
+
* @returns {string} Absolute path to the project root.
|
|
25
|
+
*/
|
|
26
|
+
function detectProjectRoot(startDir) {
|
|
27
|
+
let current = path.resolve(startDir);
|
|
28
|
+
for (;;) {
|
|
29
|
+
if (fs.existsSync(path.join(current, ".git"))) {
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
32
|
+
const parent = path.dirname(current);
|
|
33
|
+
if (parent === current) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
"Could not find project root (.git directory).\n" +
|
|
36
|
+
"Run this command from within a git repository."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
current = parent;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Write or merge .mcp.json with the unimatrix server entry.
|
|
45
|
+
* Preserves existing servers. Malformed JSON causes an error (ADR-004).
|
|
46
|
+
*
|
|
47
|
+
* @param {string} projectRoot - Absolute path to project root.
|
|
48
|
+
* @param {string} binaryPath - Absolute path to the unimatrix binary.
|
|
49
|
+
* @param {boolean} dryRun - If true, do not write the file.
|
|
50
|
+
* @returns {string[]} Actions taken.
|
|
51
|
+
*/
|
|
52
|
+
function writeMcpJson(projectRoot, binaryPath, dryRun) {
|
|
53
|
+
const mcpPath = path.join(projectRoot, ".mcp.json");
|
|
54
|
+
const actions = [];
|
|
55
|
+
let existing = {};
|
|
56
|
+
|
|
57
|
+
if (fs.existsSync(mcpPath)) {
|
|
58
|
+
try {
|
|
59
|
+
existing = JSON.parse(fs.readFileSync(mcpPath, "utf8"));
|
|
60
|
+
} catch (parseError) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"Malformed .mcp.json at " +
|
|
63
|
+
mcpPath +
|
|
64
|
+
": " +
|
|
65
|
+
parseError.message +
|
|
66
|
+
"\nFix the JSON syntax and re-run 'npx unimatrix init'."
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
actions.push("Updated .mcp.json (preserved existing servers)");
|
|
70
|
+
} else {
|
|
71
|
+
actions.push("Created .mcp.json");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!existing.mcpServers) {
|
|
75
|
+
existing.mcpServers = {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
existing.mcpServers.unimatrix = {
|
|
79
|
+
command: binaryPath,
|
|
80
|
+
args: [],
|
|
81
|
+
env: {},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (!dryRun) {
|
|
85
|
+
fs.writeFileSync(
|
|
86
|
+
mcpPath,
|
|
87
|
+
JSON.stringify(existing, null, 2) + "\n",
|
|
88
|
+
"utf8"
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
actions[actions.length - 1] = "[dry-run] " + actions[actions.length - 1];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return actions;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Copy bundled skill files from the package's skills/ directory
|
|
99
|
+
* into the project's .claude/skills/ directory. Overwrites existing
|
|
100
|
+
* unimatrix skills, preserves non-unimatrix skills.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} projectRoot - Absolute path to project root.
|
|
103
|
+
* @param {boolean} dryRun - If true, do not copy files.
|
|
104
|
+
* @returns {string[]} Actions taken.
|
|
105
|
+
*/
|
|
106
|
+
function copySkills(projectRoot, dryRun) {
|
|
107
|
+
const actions = [];
|
|
108
|
+
const targetDir = path.join(projectRoot, ".claude", "skills");
|
|
109
|
+
const sourceDir = path.join(__dirname, "..", "skills");
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(sourceDir)) {
|
|
112
|
+
actions.push("No bundled skills found (skipped)");
|
|
113
|
+
return actions;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!dryRun) {
|
|
117
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const skillDirs = fs
|
|
121
|
+
.readdirSync(sourceDir, { withFileTypes: true })
|
|
122
|
+
.filter((d) => d.isDirectory())
|
|
123
|
+
.map((d) => d.name);
|
|
124
|
+
|
|
125
|
+
for (const skillDir of skillDirs) {
|
|
126
|
+
const src = path.join(sourceDir, skillDir);
|
|
127
|
+
const dst = path.join(targetDir, skillDir);
|
|
128
|
+
|
|
129
|
+
if (!dryRun) {
|
|
130
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
131
|
+
|
|
132
|
+
const files = fs.readdirSync(src);
|
|
133
|
+
for (const file of files) {
|
|
134
|
+
if (file.includes("..")) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
"Path traversal detected in skill file: " + file
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const srcFile = path.join(src, file);
|
|
141
|
+
const dstFile = path.join(dst, file);
|
|
142
|
+
|
|
143
|
+
// Only copy files, not subdirectories
|
|
144
|
+
const stat = fs.statSync(srcFile);
|
|
145
|
+
if (stat.isFile()) {
|
|
146
|
+
fs.copyFileSync(srcFile, dstFile);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
actions.push("Copied skill: " + skillDir);
|
|
151
|
+
} else {
|
|
152
|
+
actions.push("[dry-run] Would copy skill: " + skillDir);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return actions;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Print summary of all actions taken during init.
|
|
161
|
+
*
|
|
162
|
+
* @param {string[]} actions - List of action descriptions.
|
|
163
|
+
* @param {boolean} dryRun - Whether this was a dry run.
|
|
164
|
+
*/
|
|
165
|
+
function printSummary(actions, dryRun) {
|
|
166
|
+
if (dryRun) {
|
|
167
|
+
console.log("\n--- Dry Run Summary ---\n");
|
|
168
|
+
} else {
|
|
169
|
+
console.log("\n--- Unimatrix Init Complete ---\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const action of actions) {
|
|
173
|
+
console.log(" " + action);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log("");
|
|
177
|
+
if (!dryRun) {
|
|
178
|
+
console.log("Next step: start a Claude Code session and run /unimatrix-init");
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Deterministic, non-interactive, idempotent project wiring.
|
|
184
|
+
* Configures MCP server, hooks, skills, and pre-creates the database.
|
|
185
|
+
* Implemented in JavaScript per ADR-003.
|
|
186
|
+
*
|
|
187
|
+
* @param {object} options
|
|
188
|
+
* @param {boolean} [options.dryRun=false] - Print actions without modifying files.
|
|
189
|
+
* @param {string} [options.projectDir] - Override project root (skip .git walk).
|
|
190
|
+
*/
|
|
191
|
+
async function init(options) {
|
|
192
|
+
const dryRun = (options && options.dryRun) || false;
|
|
193
|
+
const actions = [];
|
|
194
|
+
|
|
195
|
+
// Step 1: Resolve project root
|
|
196
|
+
let projectRoot;
|
|
197
|
+
if (options && options.projectDir) {
|
|
198
|
+
projectRoot = path.resolve(options.projectDir);
|
|
199
|
+
} else {
|
|
200
|
+
projectRoot = detectProjectRoot(process.cwd());
|
|
201
|
+
}
|
|
202
|
+
actions.push("Project root: " + projectRoot);
|
|
203
|
+
|
|
204
|
+
// Step 2: Resolve binary path
|
|
205
|
+
const binaryPath = resolveBinary();
|
|
206
|
+
actions.push("Binary: " + binaryPath);
|
|
207
|
+
|
|
208
|
+
// Step 3: Write/merge .mcp.json
|
|
209
|
+
const mcpActions = writeMcpJson(projectRoot, binaryPath, dryRun);
|
|
210
|
+
actions.push(...mcpActions);
|
|
211
|
+
|
|
212
|
+
// Step 4: Merge hooks into .claude/settings.json
|
|
213
|
+
const settingsPath = path.join(projectRoot, ".claude", "settings.json");
|
|
214
|
+
const settingsResult = mergeSettings(settingsPath, binaryPath, { dryRun });
|
|
215
|
+
actions.push(...settingsResult.actions);
|
|
216
|
+
|
|
217
|
+
// Step 5: Copy skill files
|
|
218
|
+
const skillActions = copySkills(projectRoot, dryRun);
|
|
219
|
+
actions.push(...skillActions);
|
|
220
|
+
|
|
221
|
+
// Step 6: Pre-create database (exec Rust binary)
|
|
222
|
+
if (!dryRun) {
|
|
223
|
+
try {
|
|
224
|
+
execFileSync(binaryPath, ["version", "--project-dir", projectRoot], {
|
|
225
|
+
stdio: "pipe",
|
|
226
|
+
});
|
|
227
|
+
actions.push("Database: pre-created at ~/.unimatrix/{hash}/");
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const stderr =
|
|
230
|
+
error.stderr ? error.stderr.toString() : error.message;
|
|
231
|
+
throw new Error("Database creation failed: " + stderr);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
actions.push(
|
|
235
|
+
"[dry-run] Would pre-create database via: unimatrix version --project-dir " +
|
|
236
|
+
projectRoot
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Step 7: Validate binary
|
|
241
|
+
if (!dryRun) {
|
|
242
|
+
try {
|
|
243
|
+
const versionOutput = execFileSync(binaryPath, ["version"], {
|
|
244
|
+
stdio: "pipe",
|
|
245
|
+
encoding: "utf8",
|
|
246
|
+
}).trim();
|
|
247
|
+
actions.push("Validation: " + versionOutput);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
const stderr =
|
|
250
|
+
error.stderr ? error.stderr.toString() : error.message;
|
|
251
|
+
throw new Error("Binary validation failed: " + stderr);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
actions.push("[dry-run] Would validate binary via: unimatrix version");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Step 8: Print summary
|
|
258
|
+
printSummary(actions, dryRun);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
init,
|
|
263
|
+
detectProjectRoot,
|
|
264
|
+
writeMcpJson,
|
|
265
|
+
copySkills,
|
|
266
|
+
printSummary,
|
|
267
|
+
};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Regex patterns to identify Unimatrix-owned hook entries (ADR-004).
|
|
8
|
+
* Matches both current "unimatrix" and pre-rename "unimatrix-server" commands,
|
|
9
|
+
* whether bare names or absolute paths.
|
|
10
|
+
*/
|
|
11
|
+
const UNIMATRIX_PATTERNS = [
|
|
12
|
+
/^unimatrix\s+hook\s/,
|
|
13
|
+
/^unimatrix-server\s+hook\s/,
|
|
14
|
+
/\/unimatrix\s+hook\s/,
|
|
15
|
+
/\/unimatrix-server\s+hook\s/,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const HOOK_EVENTS = [
|
|
19
|
+
"SessionStart",
|
|
20
|
+
"Stop",
|
|
21
|
+
"UserPromptSubmit",
|
|
22
|
+
"PreToolUse",
|
|
23
|
+
"PostToolUse",
|
|
24
|
+
"SubagentStart",
|
|
25
|
+
"SubagentStop",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** Matcher per event: "" for session-level, "*" for tool/agent-level */
|
|
29
|
+
const EVENT_MATCHERS = {
|
|
30
|
+
SessionStart: "",
|
|
31
|
+
Stop: "",
|
|
32
|
+
UserPromptSubmit: "",
|
|
33
|
+
PreToolUse: "*",
|
|
34
|
+
PostToolUse: "*",
|
|
35
|
+
SubagentStart: "*",
|
|
36
|
+
SubagentStop: "*",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns true if a hook entry is owned by Unimatrix, identified by
|
|
41
|
+
* prefix-matching the command field against known patterns.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} hookEntry - A hook entry object with a `command` field.
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isUnimatrixHook(hookEntry) {
|
|
47
|
+
if (!hookEntry || !hookEntry.command || typeof hookEntry.command !== "string") {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return UNIMATRIX_PATTERNS.some((pattern) => pattern.test(hookEntry.command));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Merge Unimatrix hook configuration into .claude/settings.json.
|
|
55
|
+
*
|
|
56
|
+
* Implements ADR-004 prefix-match identification. Preserves all non-unimatrix
|
|
57
|
+
* hooks, permissions, and other top-level keys. Idempotent: running twice
|
|
58
|
+
* produces the same result.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} filePath - Path to .claude/settings.json
|
|
61
|
+
* @param {string} binaryPath - Absolute path to the unimatrix binary
|
|
62
|
+
* @param {object} options - { dryRun: boolean }
|
|
63
|
+
* @returns {{ actions: string[], content: object }}
|
|
64
|
+
*/
|
|
65
|
+
function mergeSettings(filePath, binaryPath, options) {
|
|
66
|
+
const dryRun = (options && options.dryRun) || false;
|
|
67
|
+
const actions = [];
|
|
68
|
+
let content = {};
|
|
69
|
+
|
|
70
|
+
// Step 1: Read existing file
|
|
71
|
+
if (fs.existsSync(filePath)) {
|
|
72
|
+
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
73
|
+
|
|
74
|
+
if (raw === "") {
|
|
75
|
+
content = {};
|
|
76
|
+
actions.push("settings.json was empty, initializing");
|
|
77
|
+
} else {
|
|
78
|
+
try {
|
|
79
|
+
content = JSON.parse(raw);
|
|
80
|
+
} catch (parseError) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"Malformed .claude/settings.json: " +
|
|
83
|
+
parseError.message +
|
|
84
|
+
"\nFix the JSON syntax manually and re-run 'npx unimatrix init'." +
|
|
85
|
+
"\nFile: " +
|
|
86
|
+
filePath
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
actions.push("Created .claude/settings.json");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Step 2: Ensure hooks key exists
|
|
95
|
+
if (!content.hooks) {
|
|
96
|
+
content.hooks = {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Validate hooks is an object (not array, not primitive)
|
|
100
|
+
if (typeof content.hooks !== "object" || Array.isArray(content.hooks)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
".claude/settings.json 'hooks' key is not an object." +
|
|
103
|
+
'\nExpected: { "hooks": { "EventName": [...] } }' +
|
|
104
|
+
"\nFile: " +
|
|
105
|
+
filePath
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Step 3: For each hook event, merge the unimatrix entry
|
|
110
|
+
for (const event of HOOK_EVENTS) {
|
|
111
|
+
const hookCommand = binaryPath + " hook " + event;
|
|
112
|
+
const matcher = EVENT_MATCHERS[event];
|
|
113
|
+
|
|
114
|
+
const newHookEntry = {
|
|
115
|
+
type: "command",
|
|
116
|
+
command: hookCommand,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// The settings format is: hooks.EventName = [ { matcher, hooks: [...] } ]
|
|
120
|
+
if (!content.hooks[event]) {
|
|
121
|
+
content.hooks[event] = [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const eventArray = content.hooks[event];
|
|
125
|
+
let merged = false;
|
|
126
|
+
|
|
127
|
+
for (const matcherGroup of eventArray) {
|
|
128
|
+
if (matcherGroup.matcher === matcher) {
|
|
129
|
+
// Found a matcher group for our matcher value
|
|
130
|
+
if (!matcherGroup.hooks) {
|
|
131
|
+
matcherGroup.hooks = [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Look for existing unimatrix hook to update
|
|
135
|
+
let existingIndex = -1;
|
|
136
|
+
const duplicateIndices = [];
|
|
137
|
+
for (let i = 0; i < matcherGroup.hooks.length; i++) {
|
|
138
|
+
if (isUnimatrixHook(matcherGroup.hooks[i])) {
|
|
139
|
+
if (existingIndex === -1) {
|
|
140
|
+
existingIndex = i;
|
|
141
|
+
} else {
|
|
142
|
+
duplicateIndices.push(i);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Remove duplicates in reverse order (dedup on re-run, per ADR-004)
|
|
148
|
+
for (let j = duplicateIndices.length - 1; j >= 0; j--) {
|
|
149
|
+
matcherGroup.hooks.splice(duplicateIndices[j], 1);
|
|
150
|
+
actions.push("Removed duplicate unimatrix hook for " + event);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (existingIndex >= 0) {
|
|
154
|
+
matcherGroup.hooks[existingIndex] = newHookEntry;
|
|
155
|
+
actions.push("Updated hook: " + event);
|
|
156
|
+
} else {
|
|
157
|
+
matcherGroup.hooks.push(newHookEntry);
|
|
158
|
+
actions.push("Added hook: " + event);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
merged = true;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!merged) {
|
|
167
|
+
// No matcher group found for our matcher value; create one
|
|
168
|
+
eventArray.push({
|
|
169
|
+
matcher: matcher,
|
|
170
|
+
hooks: [newHookEntry],
|
|
171
|
+
});
|
|
172
|
+
actions.push("Added hook: " + event + " (new matcher group)");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Step 4: Write file (or prefix actions with [dry-run])
|
|
177
|
+
if (!dryRun) {
|
|
178
|
+
const dir = path.dirname(filePath);
|
|
179
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
180
|
+
fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + "\n", "utf8");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const finalActions = dryRun ? actions.map((a) => "[dry-run] " + a) : actions;
|
|
184
|
+
|
|
185
|
+
return { actions: finalActions, content };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = { mergeSettings, isUnimatrixHook, HOOK_EVENTS, EVENT_MATCHERS, UNIMATRIX_PATTERNS };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const PLATFORMS = {
|
|
8
|
+
"linux-x64": "@dug-21/unimatrix-linux-x64",
|
|
9
|
+
"linux-arm64": "@dug-21/unimatrix-linux-arm64",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the absolute path to the platform-specific Unimatrix binary.
|
|
14
|
+
*
|
|
15
|
+
* Resolution order:
|
|
16
|
+
* 1. UNIMATRIX_BINARY env var (development/testing override)
|
|
17
|
+
* 2. Platform package via require.resolve (optionalDependencies)
|
|
18
|
+
*
|
|
19
|
+
* @returns {string} Absolute real path to the binary
|
|
20
|
+
* @throws {Error} If binary cannot be found or env override points to missing file
|
|
21
|
+
*/
|
|
22
|
+
function resolveBinary() {
|
|
23
|
+
// 1. Check environment variable override (development/testing)
|
|
24
|
+
const envPath = process.env.UNIMATRIX_BINARY;
|
|
25
|
+
if (envPath) {
|
|
26
|
+
if (!fs.existsSync(envPath)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"UNIMATRIX_BINARY points to non-existent file: " + envPath
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return fs.realpathSync(envPath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. Determine current platform key
|
|
35
|
+
const platformKey = os.platform() + "-" + os.arch();
|
|
36
|
+
|
|
37
|
+
// 3. Look up package name for this platform
|
|
38
|
+
const packageName = PLATFORMS[platformKey];
|
|
39
|
+
if (!packageName) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Unsupported platform: " +
|
|
42
|
+
platformKey +
|
|
43
|
+
"\n" +
|
|
44
|
+
"Supported platforms: " +
|
|
45
|
+
Object.keys(PLATFORMS).join(", ") +
|
|
46
|
+
"\n" +
|
|
47
|
+
"Set UNIMATRIX_BINARY environment variable to use a custom binary path."
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 4. Resolve binary path via require.resolve
|
|
52
|
+
let binaryPath;
|
|
53
|
+
try {
|
|
54
|
+
binaryPath = require.resolve(packageName + "/bin/unimatrix");
|
|
55
|
+
} catch (_err) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"Could not find platform binary package '" +
|
|
58
|
+
packageName +
|
|
59
|
+
"'.\n" +
|
|
60
|
+
"Run 'npm install @dug-21/unimatrix' to install.\n" +
|
|
61
|
+
"If using pnpm or yarn, set UNIMATRIX_BINARY to the binary path."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 5. Resolve through symlinks to get the real absolute path (ADR-001)
|
|
66
|
+
return fs.realpathSync(binaryPath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { resolveBinary, PLATFORMS };
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dug-21/unimatrix",
|
|
3
|
+
"version": "0.5.5",
|
|
4
|
+
"description": "Unimatrix knowledge engine for multi-agent development",
|
|
5
|
+
"bin": {
|
|
6
|
+
"unimatrix": "bin/unimatrix.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"postinstall": "node postinstall.js"
|
|
10
|
+
},
|
|
11
|
+
"optionalDependencies": {
|
|
12
|
+
"@dug-21/unimatrix-linux-x64": "0.5.5",
|
|
13
|
+
"@dug-21/unimatrix-linux-arm64": "0.5.5"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"lib/",
|
|
18
|
+
"skills/",
|
|
19
|
+
"postinstall.js"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT OR Apache-2.0",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/anthropics/unimatrix"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
function main() {
|
|
5
|
+
try {
|
|
6
|
+
var resolveBinary;
|
|
7
|
+
try {
|
|
8
|
+
resolveBinary = require("./lib/resolve-binary").resolveBinary;
|
|
9
|
+
} catch (_err) {
|
|
10
|
+
console.warn(
|
|
11
|
+
"[unimatrix] postinstall: could not load resolve-binary module, skipping model download"
|
|
12
|
+
);
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
var binaryPath;
|
|
17
|
+
try {
|
|
18
|
+
binaryPath = resolveBinary();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.warn(
|
|
21
|
+
"[unimatrix] postinstall: platform binary not available (" +
|
|
22
|
+
error.message +
|
|
23
|
+
"), skipping model download"
|
|
24
|
+
);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
var execFileSync = require("child_process").execFileSync;
|
|
29
|
+
var path = require("path");
|
|
30
|
+
var binDir = path.dirname(binaryPath);
|
|
31
|
+
var ldPath = process.env.LD_LIBRARY_PATH;
|
|
32
|
+
var env = Object.assign({}, process.env, {
|
|
33
|
+
LD_LIBRARY_PATH: ldPath ? binDir + ":" + ldPath : binDir,
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
execFileSync(binaryPath, ["model-download"], {
|
|
37
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
38
|
+
timeout: 300000,
|
|
39
|
+
env: env,
|
|
40
|
+
});
|
|
41
|
+
console.log("[unimatrix] postinstall: ONNX model ready");
|
|
42
|
+
} catch (execError) {
|
|
43
|
+
console.warn(
|
|
44
|
+
"[unimatrix] postinstall: model download failed, will retry on first server start"
|
|
45
|
+
);
|
|
46
|
+
if (execError.stderr) {
|
|
47
|
+
console.warn(
|
|
48
|
+
"[unimatrix] " + execError.stderr.toString().trim()
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (outerError) {
|
|
53
|
+
console.warn(
|
|
54
|
+
"[unimatrix] postinstall: unexpected error: " + outerError.message
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
main();
|