@prih/mcp-graph-memory 1.0.3
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/LICENSE +15 -0
- package/README.md +512 -0
- package/dist/api/index.js +473 -0
- package/dist/api/rest/code.js +78 -0
- package/dist/api/rest/docs.js +80 -0
- package/dist/api/rest/files.js +64 -0
- package/dist/api/rest/graph.js +56 -0
- package/dist/api/rest/index.js +117 -0
- package/dist/api/rest/knowledge.js +238 -0
- package/dist/api/rest/skills.js +284 -0
- package/dist/api/rest/tasks.js +272 -0
- package/dist/api/rest/tools.js +126 -0
- package/dist/api/rest/validation.js +191 -0
- package/dist/api/rest/websocket.js +65 -0
- package/dist/api/tools/code/get-file-symbols.js +30 -0
- package/dist/api/tools/code/get-symbol.js +22 -0
- package/dist/api/tools/code/list-files.js +18 -0
- package/dist/api/tools/code/search-code.js +27 -0
- package/dist/api/tools/code/search-files.js +22 -0
- package/dist/api/tools/context/get-context.js +19 -0
- package/dist/api/tools/docs/cross-references.js +76 -0
- package/dist/api/tools/docs/explain-symbol.js +55 -0
- package/dist/api/tools/docs/find-examples.js +52 -0
- package/dist/api/tools/docs/get-node.js +24 -0
- package/dist/api/tools/docs/get-toc.js +22 -0
- package/dist/api/tools/docs/list-snippets.js +46 -0
- package/dist/api/tools/docs/list-topics.js +18 -0
- package/dist/api/tools/docs/search-files.js +22 -0
- package/dist/api/tools/docs/search-snippets.js +43 -0
- package/dist/api/tools/docs/search.js +27 -0
- package/dist/api/tools/file-index/get-file-info.js +21 -0
- package/dist/api/tools/file-index/list-all-files.js +28 -0
- package/dist/api/tools/file-index/search-all-files.js +24 -0
- package/dist/api/tools/knowledge/add-attachment.js +31 -0
- package/dist/api/tools/knowledge/create-note.js +20 -0
- package/dist/api/tools/knowledge/create-relation.js +29 -0
- package/dist/api/tools/knowledge/delete-note.js +19 -0
- package/dist/api/tools/knowledge/delete-relation.js +23 -0
- package/dist/api/tools/knowledge/find-linked-notes.js +25 -0
- package/dist/api/tools/knowledge/get-note.js +20 -0
- package/dist/api/tools/knowledge/list-notes.js +18 -0
- package/dist/api/tools/knowledge/list-relations.js +17 -0
- package/dist/api/tools/knowledge/remove-attachment.js +19 -0
- package/dist/api/tools/knowledge/search-notes.js +25 -0
- package/dist/api/tools/knowledge/update-note.js +34 -0
- package/dist/api/tools/skills/add-attachment.js +31 -0
- package/dist/api/tools/skills/bump-usage.js +19 -0
- package/dist/api/tools/skills/create-skill-link.js +25 -0
- package/dist/api/tools/skills/create-skill.js +26 -0
- package/dist/api/tools/skills/delete-skill-link.js +23 -0
- package/dist/api/tools/skills/delete-skill.js +20 -0
- package/dist/api/tools/skills/find-linked-skills.js +25 -0
- package/dist/api/tools/skills/get-skill.js +21 -0
- package/dist/api/tools/skills/link-skill.js +23 -0
- package/dist/api/tools/skills/list-skills.js +20 -0
- package/dist/api/tools/skills/recall-skills.js +18 -0
- package/dist/api/tools/skills/remove-attachment.js +19 -0
- package/dist/api/tools/skills/search-skills.js +25 -0
- package/dist/api/tools/skills/update-skill.js +58 -0
- package/dist/api/tools/tasks/add-attachment.js +31 -0
- package/dist/api/tools/tasks/create-task-link.js +25 -0
- package/dist/api/tools/tasks/create-task.js +25 -0
- package/dist/api/tools/tasks/delete-task-link.js +23 -0
- package/dist/api/tools/tasks/delete-task.js +20 -0
- package/dist/api/tools/tasks/find-linked-tasks.js +25 -0
- package/dist/api/tools/tasks/get-task.js +20 -0
- package/dist/api/tools/tasks/link-task.js +23 -0
- package/dist/api/tools/tasks/list-tasks.js +24 -0
- package/dist/api/tools/tasks/move-task.js +38 -0
- package/dist/api/tools/tasks/remove-attachment.js +19 -0
- package/dist/api/tools/tasks/search-tasks.js +25 -0
- package/dist/api/tools/tasks/update-task.js +55 -0
- package/dist/cli/index.js +451 -0
- package/dist/cli/indexer.js +277 -0
- package/dist/graphs/attachment-types.js +74 -0
- package/dist/graphs/code-types.js +10 -0
- package/dist/graphs/code.js +172 -0
- package/dist/graphs/docs.js +198 -0
- package/dist/graphs/file-index-types.js +10 -0
- package/dist/graphs/file-index.js +310 -0
- package/dist/graphs/file-lang.js +119 -0
- package/dist/graphs/knowledge-types.js +32 -0
- package/dist/graphs/knowledge.js +764 -0
- package/dist/graphs/manager-types.js +87 -0
- package/dist/graphs/skill-types.js +10 -0
- package/dist/graphs/skill.js +1013 -0
- package/dist/graphs/task-types.js +17 -0
- package/dist/graphs/task.js +960 -0
- package/dist/lib/embedder.js +101 -0
- package/dist/lib/events-log.js +400 -0
- package/dist/lib/file-import.js +327 -0
- package/dist/lib/file-mirror.js +446 -0
- package/dist/lib/frontmatter.js +17 -0
- package/dist/lib/mirror-watcher.js +637 -0
- package/dist/lib/multi-config.js +254 -0
- package/dist/lib/parsers/code.js +246 -0
- package/dist/lib/parsers/codeblock.js +66 -0
- package/dist/lib/parsers/docs.js +196 -0
- package/dist/lib/project-manager.js +418 -0
- package/dist/lib/promise-queue.js +22 -0
- package/dist/lib/search/bm25.js +167 -0
- package/dist/lib/search/code.js +103 -0
- package/dist/lib/search/docs.js +108 -0
- package/dist/lib/search/file-index.js +31 -0
- package/dist/lib/search/files.js +61 -0
- package/dist/lib/search/knowledge.js +101 -0
- package/dist/lib/search/skills.js +104 -0
- package/dist/lib/search/tasks.js +103 -0
- package/dist/lib/watcher.js +67 -0
- package/package.json +83 -0
- package/ui/README.md +54 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadModel = loadModel;
|
|
7
|
+
exports.embed = embed;
|
|
8
|
+
exports.embedQuery = embedQuery;
|
|
9
|
+
exports.embedBatch = embedBatch;
|
|
10
|
+
exports.resetEmbedder = resetEmbedder;
|
|
11
|
+
exports.cosineSimilarity = cosineSimilarity;
|
|
12
|
+
const transformers_1 = require("@huggingface/transformers");
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
15
|
+
const _models = new Map(); // name → { pipe, config }
|
|
16
|
+
const _pipeCache = new Map(); // "model|dtype" → pipe (dedup)
|
|
17
|
+
let _maxChars = 4000;
|
|
18
|
+
async function loadModel(config, modelsDir, maxChars, name = 'default') {
|
|
19
|
+
_maxChars = maxChars;
|
|
20
|
+
// Cache key includes dtype since same model with different dtype = different pipeline
|
|
21
|
+
const cacheKey = `${config.model}|${config.dtype ?? ''}`;
|
|
22
|
+
// Reuse pipeline if same model+dtype already loaded
|
|
23
|
+
const cached = _pipeCache.get(cacheKey);
|
|
24
|
+
if (cached) {
|
|
25
|
+
_models.set(name, { pipe: cached, config });
|
|
26
|
+
process.stderr.write(`[embedder] Reusing model ${config.model} for "${name}"\n`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
transformers_1.env.cacheDir = modelsDir;
|
|
30
|
+
const modelDir = path_1.default.join(modelsDir, config.model.replace('/', path_1.default.sep));
|
|
31
|
+
if (fs_1.default.existsSync(modelDir)) {
|
|
32
|
+
transformers_1.env.allowRemoteModels = false;
|
|
33
|
+
process.stderr.write(`[embedder] Using local model at ${modelDir}\n`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
transformers_1.env.allowRemoteModels = true;
|
|
37
|
+
process.stderr.write(`[embedder] Downloading model ${config.model} to ${modelsDir}...\n`);
|
|
38
|
+
}
|
|
39
|
+
const pipeOpts = {};
|
|
40
|
+
if (config.dtype)
|
|
41
|
+
pipeOpts.dtype = config.dtype;
|
|
42
|
+
const pipe = await (0, transformers_1.pipeline)('feature-extraction', config.model, pipeOpts);
|
|
43
|
+
_pipeCache.set(cacheKey, pipe);
|
|
44
|
+
_models.set(name, { pipe, config });
|
|
45
|
+
process.stderr.write(`[embedder] Model "${name}" ready\n`);
|
|
46
|
+
}
|
|
47
|
+
function getEntry(modelName) {
|
|
48
|
+
const entry = _models.get(modelName);
|
|
49
|
+
if (!entry)
|
|
50
|
+
throw new Error(`Model "${modelName}" not loaded. Call loadModel() first.`);
|
|
51
|
+
return entry;
|
|
52
|
+
}
|
|
53
|
+
/** Embed a document (indexing). Applies documentPrefix and configured pooling. */
|
|
54
|
+
async function embed(title, content, modelName = 'default') {
|
|
55
|
+
const { pipe, config } = getEntry(modelName);
|
|
56
|
+
const raw = `${title}\n${content}`;
|
|
57
|
+
const text = `${config.documentPrefix}${raw}`.slice(0, _maxChars);
|
|
58
|
+
const tensor = await pipe._call(text, { pooling: config.pooling, normalize: config.normalize });
|
|
59
|
+
return Array.from(tensor.data);
|
|
60
|
+
}
|
|
61
|
+
/** Embed a search query. Applies queryPrefix and configured pooling. */
|
|
62
|
+
async function embedQuery(query, modelName = 'default') {
|
|
63
|
+
const { pipe, config } = getEntry(modelName);
|
|
64
|
+
const text = `${config.queryPrefix}${query}`.slice(0, _maxChars);
|
|
65
|
+
const tensor = await pipe._call(text, { pooling: config.pooling, normalize: config.normalize });
|
|
66
|
+
return Array.from(tensor.data);
|
|
67
|
+
}
|
|
68
|
+
/** Batch-embed documents (indexing). Applies documentPrefix and configured pooling. */
|
|
69
|
+
async function embedBatch(inputs, modelName = 'default') {
|
|
70
|
+
const { pipe, config } = getEntry(modelName);
|
|
71
|
+
if (inputs.length === 0)
|
|
72
|
+
return [];
|
|
73
|
+
if (inputs.length === 1)
|
|
74
|
+
return [await embed(inputs[0].title, inputs[0].content, modelName)];
|
|
75
|
+
const texts = inputs.map(({ title, content }) => `${config.documentPrefix}${title}\n${content}`.slice(0, _maxChars));
|
|
76
|
+
const batchSize = config.batchSize;
|
|
77
|
+
const result = [];
|
|
78
|
+
for (let start = 0; start < texts.length; start += batchSize) {
|
|
79
|
+
const chunk = texts.slice(start, start + batchSize);
|
|
80
|
+
const tensor = await pipe._call(chunk, { pooling: config.pooling, normalize: config.normalize });
|
|
81
|
+
const dim = tensor.dims[1];
|
|
82
|
+
const data = tensor.data;
|
|
83
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
84
|
+
result.push(Array.from(data.slice(i * dim, (i + 1) * dim)));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
function resetEmbedder() {
|
|
90
|
+
_models.clear();
|
|
91
|
+
_pipeCache.clear();
|
|
92
|
+
}
|
|
93
|
+
// Vectors are L2-normalized → dot product = cosine similarity
|
|
94
|
+
function cosineSimilarity(a, b) {
|
|
95
|
+
if (a.length !== b.length)
|
|
96
|
+
return 0;
|
|
97
|
+
let dot = 0;
|
|
98
|
+
for (let i = 0; i < a.length; i++)
|
|
99
|
+
dot += a[i] * b[i];
|
|
100
|
+
return dot;
|
|
101
|
+
}
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.appendEvent = appendEvent;
|
|
37
|
+
exports.readEvents = readEvents;
|
|
38
|
+
exports.replayNoteEvents = replayNoteEvents;
|
|
39
|
+
exports.replayTaskEvents = replayTaskEvents;
|
|
40
|
+
exports.replaySkillEvents = replaySkillEvents;
|
|
41
|
+
exports.ensureGitignore = ensureGitignore;
|
|
42
|
+
exports.ensureGitattributes = ensureGitattributes;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Core functions
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/** Append a single event as a JSON line to the events.jsonl file. */
|
|
49
|
+
function appendEvent(eventsPath, event) {
|
|
50
|
+
try {
|
|
51
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...event }) + '\n';
|
|
52
|
+
fs.appendFileSync(eventsPath, line, 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
process.stderr.write(`[events-log] failed to append event: ${err}\n`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Read and parse all events from a JSONL file. Invalid lines are skipped. */
|
|
59
|
+
function readEvents(eventsPath) {
|
|
60
|
+
try {
|
|
61
|
+
if (!fs.existsSync(eventsPath))
|
|
62
|
+
return [];
|
|
63
|
+
const content = fs.readFileSync(eventsPath, 'utf-8');
|
|
64
|
+
const events = [];
|
|
65
|
+
for (const line of content.split('\n')) {
|
|
66
|
+
const trimmed = line.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
events.push(JSON.parse(trimmed));
|
|
71
|
+
}
|
|
72
|
+
catch { /* skip invalid lines */ }
|
|
73
|
+
}
|
|
74
|
+
return events;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Sort events by timestamp (ISO strings are lexicographically sortable). */
|
|
81
|
+
function sortByTs(events) {
|
|
82
|
+
return [...events].sort((a, b) => (a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0));
|
|
83
|
+
}
|
|
84
|
+
function isoToMs(value) {
|
|
85
|
+
if (value == null)
|
|
86
|
+
return null;
|
|
87
|
+
const d = new Date(value);
|
|
88
|
+
return isNaN(d.getTime()) ? null : d.getTime();
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Replay functions
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
/**
|
|
94
|
+
* Replay note events + content to reconstruct a ParsedNoteFile.
|
|
95
|
+
* Returns null if no 'created' event is found.
|
|
96
|
+
*/
|
|
97
|
+
function replayNoteEvents(events, content) {
|
|
98
|
+
const sorted = sortByTs(events);
|
|
99
|
+
const created = sorted.find(e => e.op === 'created');
|
|
100
|
+
if (!created)
|
|
101
|
+
return null;
|
|
102
|
+
let title = created.title;
|
|
103
|
+
let tags = created.tags ?? [];
|
|
104
|
+
let updatedBy = null;
|
|
105
|
+
const relations = [];
|
|
106
|
+
const attachments = [];
|
|
107
|
+
for (const ev of sorted) {
|
|
108
|
+
if (ev.op === 'update') {
|
|
109
|
+
const u = ev;
|
|
110
|
+
if (typeof u.title === 'string')
|
|
111
|
+
title = u.title;
|
|
112
|
+
if (Array.isArray(u.tags))
|
|
113
|
+
tags = u.tags;
|
|
114
|
+
if (typeof u.by === 'string')
|
|
115
|
+
updatedBy = u.by;
|
|
116
|
+
}
|
|
117
|
+
else if (ev.op === 'relation') {
|
|
118
|
+
const r = ev;
|
|
119
|
+
const key = `${r.to}:${r.kind}:${r.graph ?? ''}`;
|
|
120
|
+
if (r.action === 'add') {
|
|
121
|
+
if (!relations.some(x => `${x.to}:${x.kind}:${x.graph ?? ''}` === key)) {
|
|
122
|
+
const entry = { to: r.to, kind: r.kind };
|
|
123
|
+
if (r.graph)
|
|
124
|
+
entry.graph = r.graph;
|
|
125
|
+
relations.push(entry);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const idx = relations.findIndex(x => `${x.to}:${x.kind}:${x.graph ?? ''}` === key);
|
|
130
|
+
if (idx !== -1)
|
|
131
|
+
relations.splice(idx, 1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (ev.op === 'attachment') {
|
|
135
|
+
const a = ev;
|
|
136
|
+
if (a.action === 'add') {
|
|
137
|
+
if (!attachments.some(x => x.filename === a.file)) {
|
|
138
|
+
attachments.push({ filename: a.file, mimeType: 'application/octet-stream', size: 0, addedAt: 0 });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const idx = attachments.findIndex(x => x.filename === a.file);
|
|
143
|
+
if (idx !== -1)
|
|
144
|
+
attachments.splice(idx, 1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const version = sorted.length;
|
|
149
|
+
// updatedAt = ts of last event; createdAt from created event
|
|
150
|
+
const lastEvent = sorted[sorted.length - 1];
|
|
151
|
+
const updatedAt = isoToMs(lastEvent?.ts);
|
|
152
|
+
const createdAt = isoToMs(created.ts) ?? created.createdAt;
|
|
153
|
+
return {
|
|
154
|
+
id: created.id,
|
|
155
|
+
title,
|
|
156
|
+
content,
|
|
157
|
+
tags,
|
|
158
|
+
createdAt: created.createdAt ?? createdAt,
|
|
159
|
+
updatedAt: updatedAt ?? created.createdAt,
|
|
160
|
+
version,
|
|
161
|
+
createdBy: created.createdBy ?? null,
|
|
162
|
+
updatedBy,
|
|
163
|
+
relations,
|
|
164
|
+
attachments,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Replay task events + description to reconstruct a ParsedTaskFile.
|
|
169
|
+
* Returns null if no 'created' event is found.
|
|
170
|
+
*/
|
|
171
|
+
function replayTaskEvents(events, description) {
|
|
172
|
+
const sorted = sortByTs(events);
|
|
173
|
+
const created = sorted.find(e => e.op === 'created');
|
|
174
|
+
if (!created)
|
|
175
|
+
return null;
|
|
176
|
+
let title = created.title;
|
|
177
|
+
let status = created.status;
|
|
178
|
+
let priority = created.priority;
|
|
179
|
+
let tags = created.tags ?? [];
|
|
180
|
+
let dueDate = created.dueDate;
|
|
181
|
+
let estimate = created.estimate;
|
|
182
|
+
let completedAt = created.completedAt;
|
|
183
|
+
let updatedBy = null;
|
|
184
|
+
const relations = [];
|
|
185
|
+
const attachments = [];
|
|
186
|
+
for (const ev of sorted) {
|
|
187
|
+
if (ev.op === 'update') {
|
|
188
|
+
const u = ev;
|
|
189
|
+
if (typeof u.title === 'string')
|
|
190
|
+
title = u.title;
|
|
191
|
+
if (typeof u.status === 'string')
|
|
192
|
+
status = u.status;
|
|
193
|
+
if (typeof u.priority === 'string')
|
|
194
|
+
priority = u.priority;
|
|
195
|
+
if (Array.isArray(u.tags))
|
|
196
|
+
tags = u.tags;
|
|
197
|
+
if ('dueDate' in u)
|
|
198
|
+
dueDate = u.dueDate;
|
|
199
|
+
if ('estimate' in u)
|
|
200
|
+
estimate = u.estimate;
|
|
201
|
+
if ('completedAt' in u)
|
|
202
|
+
completedAt = u.completedAt;
|
|
203
|
+
if (typeof u.by === 'string')
|
|
204
|
+
updatedBy = u.by;
|
|
205
|
+
}
|
|
206
|
+
else if (ev.op === 'relation') {
|
|
207
|
+
const r = ev;
|
|
208
|
+
const key = `${r.to}:${r.kind}:${r.graph ?? ''}`;
|
|
209
|
+
if (r.action === 'add') {
|
|
210
|
+
if (!relations.some(x => `${x.to}:${x.kind}:${x.graph ?? ''}` === key)) {
|
|
211
|
+
const entry = { to: r.to, kind: r.kind };
|
|
212
|
+
if (r.graph)
|
|
213
|
+
entry.graph = r.graph;
|
|
214
|
+
relations.push(entry);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const idx = relations.findIndex(x => `${x.to}:${x.kind}:${x.graph ?? ''}` === key);
|
|
219
|
+
if (idx !== -1)
|
|
220
|
+
relations.splice(idx, 1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (ev.op === 'attachment') {
|
|
224
|
+
const a = ev;
|
|
225
|
+
if (a.action === 'add') {
|
|
226
|
+
if (!attachments.some(x => x.filename === a.file)) {
|
|
227
|
+
attachments.push({ filename: a.file, mimeType: 'application/octet-stream', size: 0, addedAt: 0 });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const idx = attachments.findIndex(x => x.filename === a.file);
|
|
232
|
+
if (idx !== -1)
|
|
233
|
+
attachments.splice(idx, 1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const version = sorted.length;
|
|
238
|
+
const lastEvent = sorted[sorted.length - 1];
|
|
239
|
+
const updatedAt = isoToMs(lastEvent?.ts);
|
|
240
|
+
return {
|
|
241
|
+
id: created.id,
|
|
242
|
+
title,
|
|
243
|
+
description,
|
|
244
|
+
status,
|
|
245
|
+
priority,
|
|
246
|
+
tags,
|
|
247
|
+
dueDate,
|
|
248
|
+
estimate,
|
|
249
|
+
completedAt,
|
|
250
|
+
createdAt: created.createdAt,
|
|
251
|
+
updatedAt: updatedAt ?? created.createdAt,
|
|
252
|
+
version,
|
|
253
|
+
createdBy: created.createdBy ?? null,
|
|
254
|
+
updatedBy,
|
|
255
|
+
relations,
|
|
256
|
+
attachments,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Replay skill events + description to reconstruct a ParsedSkillFile.
|
|
261
|
+
* Returns null if no 'created' event is found.
|
|
262
|
+
*/
|
|
263
|
+
function replaySkillEvents(events, description) {
|
|
264
|
+
const sorted = sortByTs(events);
|
|
265
|
+
const created = sorted.find(e => e.op === 'created');
|
|
266
|
+
if (!created)
|
|
267
|
+
return null;
|
|
268
|
+
let title = created.title;
|
|
269
|
+
let tags = created.tags ?? [];
|
|
270
|
+
let steps = created.steps ?? [];
|
|
271
|
+
let triggers = created.triggers ?? [];
|
|
272
|
+
let inputHints = created.inputHints ?? [];
|
|
273
|
+
let filePatterns = created.filePatterns ?? [];
|
|
274
|
+
let source = created.source;
|
|
275
|
+
let confidence = created.confidence;
|
|
276
|
+
let usageCount = created.usageCount;
|
|
277
|
+
let lastUsedAt = created.lastUsedAt;
|
|
278
|
+
let updatedBy = null;
|
|
279
|
+
const relations = [];
|
|
280
|
+
const attachments = [];
|
|
281
|
+
for (const ev of sorted) {
|
|
282
|
+
if (ev.op === 'update') {
|
|
283
|
+
const u = ev;
|
|
284
|
+
if (typeof u.title === 'string')
|
|
285
|
+
title = u.title;
|
|
286
|
+
if (Array.isArray(u.tags))
|
|
287
|
+
tags = u.tags;
|
|
288
|
+
if (Array.isArray(u.steps))
|
|
289
|
+
steps = u.steps;
|
|
290
|
+
if (Array.isArray(u.triggers))
|
|
291
|
+
triggers = u.triggers;
|
|
292
|
+
if (Array.isArray(u.inputHints))
|
|
293
|
+
inputHints = u.inputHints;
|
|
294
|
+
if (Array.isArray(u.filePatterns))
|
|
295
|
+
filePatterns = u.filePatterns;
|
|
296
|
+
if (typeof u.source === 'string')
|
|
297
|
+
source = u.source;
|
|
298
|
+
if (typeof u.confidence === 'number')
|
|
299
|
+
confidence = u.confidence;
|
|
300
|
+
if (typeof u.usageCount === 'number')
|
|
301
|
+
usageCount = u.usageCount;
|
|
302
|
+
if ('lastUsedAt' in u)
|
|
303
|
+
lastUsedAt = u.lastUsedAt;
|
|
304
|
+
if (typeof u.by === 'string')
|
|
305
|
+
updatedBy = u.by;
|
|
306
|
+
}
|
|
307
|
+
else if (ev.op === 'relation') {
|
|
308
|
+
const r = ev;
|
|
309
|
+
const key = `${r.to}:${r.kind}:${r.graph ?? ''}`;
|
|
310
|
+
if (r.action === 'add') {
|
|
311
|
+
if (!relations.some(x => `${x.to}:${x.kind}:${x.graph ?? ''}` === key)) {
|
|
312
|
+
const entry = { to: r.to, kind: r.kind };
|
|
313
|
+
if (r.graph)
|
|
314
|
+
entry.graph = r.graph;
|
|
315
|
+
relations.push(entry);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
const idx = relations.findIndex(x => `${x.to}:${x.kind}:${x.graph ?? ''}` === key);
|
|
320
|
+
if (idx !== -1)
|
|
321
|
+
relations.splice(idx, 1);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else if (ev.op === 'attachment') {
|
|
325
|
+
const a = ev;
|
|
326
|
+
if (a.action === 'add') {
|
|
327
|
+
if (!attachments.some(x => x.filename === a.file)) {
|
|
328
|
+
attachments.push({ filename: a.file, mimeType: 'application/octet-stream', size: 0, addedAt: 0 });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
const idx = attachments.findIndex(x => x.filename === a.file);
|
|
333
|
+
if (idx !== -1)
|
|
334
|
+
attachments.splice(idx, 1);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const version = sorted.length;
|
|
339
|
+
const lastEvent = sorted[sorted.length - 1];
|
|
340
|
+
const updatedAt = isoToMs(lastEvent?.ts);
|
|
341
|
+
return {
|
|
342
|
+
id: created.id,
|
|
343
|
+
title,
|
|
344
|
+
description,
|
|
345
|
+
steps,
|
|
346
|
+
triggers,
|
|
347
|
+
inputHints,
|
|
348
|
+
filePatterns,
|
|
349
|
+
tags,
|
|
350
|
+
source,
|
|
351
|
+
confidence,
|
|
352
|
+
usageCount,
|
|
353
|
+
lastUsedAt,
|
|
354
|
+
createdAt: created.createdAt,
|
|
355
|
+
updatedAt: updatedAt ?? created.createdAt,
|
|
356
|
+
version,
|
|
357
|
+
createdBy: created.createdBy ?? null,
|
|
358
|
+
updatedBy,
|
|
359
|
+
relations,
|
|
360
|
+
attachments,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Git helpers
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
/** Ensure a .gitignore file in parentDir contains the given pattern line. */
|
|
367
|
+
function ensureGitignore(parentDir, pattern) {
|
|
368
|
+
try {
|
|
369
|
+
const gitignorePath = path.join(parentDir, '.gitignore');
|
|
370
|
+
let content = '';
|
|
371
|
+
if (fs.existsSync(gitignorePath)) {
|
|
372
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
373
|
+
}
|
|
374
|
+
if (!content.split('\n').some(l => l.trim() === pattern)) {
|
|
375
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
376
|
+
fs.writeFileSync(gitignorePath, content + (content && !content.endsWith('\n') ? '\n' : '') + pattern + '\n', 'utf-8');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
process.stderr.write(`[events-log] failed to write .gitignore: ${err}\n`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/** Ensure a .gitattributes file in entityParentDir contains the merge=union line for events.jsonl. */
|
|
384
|
+
function ensureGitattributes(entityParentDir) {
|
|
385
|
+
const pattern = '*/events.jsonl merge=union';
|
|
386
|
+
try {
|
|
387
|
+
const gitattrsPath = path.join(entityParentDir, '.gitattributes');
|
|
388
|
+
let content = '';
|
|
389
|
+
if (fs.existsSync(gitattrsPath)) {
|
|
390
|
+
content = fs.readFileSync(gitattrsPath, 'utf-8');
|
|
391
|
+
}
|
|
392
|
+
if (!content.split('\n').some(l => l.trim() === pattern)) {
|
|
393
|
+
fs.mkdirSync(entityParentDir, { recursive: true });
|
|
394
|
+
fs.writeFileSync(gitattrsPath, content + (content && !content.endsWith('\n') ? '\n' : '') + pattern + '\n', 'utf-8');
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
process.stderr.write(`[events-log] failed to write .gitattributes: ${err}\n`);
|
|
399
|
+
}
|
|
400
|
+
}
|