amalgm 0.1.36 → 0.1.38
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/package.json +2 -2
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +29 -2
- package/runtime/scripts/amalgm-mcp/events/executor.js +6 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +13 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +13 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +6 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +9 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +25 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +10 -0
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +9 -3
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +13 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-store.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +15 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +162 -22
- package/runtime/scripts/amalgm-mcp/workspace/store.js +278 -0
- package/runtime/scripts/chat-core/adapters/claude.js +2 -1
- package/runtime/scripts/chat-core/adapters/codex.js +44 -4
- package/runtime/scripts/chat-core/adapters/opencode.js +2 -1
- package/runtime/scripts/chat-core/contract.js +57 -0
- package/runtime/scripts/chat-core/engine.js +5 -5
- package/runtime/scripts/chat-core/server.js +17 -4
- package/runtime/scripts/chat-core/sse.js +8 -1
- package/runtime/scripts/chat-core/stores.js +3 -1
- package/runtime/scripts/chat-core/tooling/active-memory.js +396 -0
- package/runtime/scripts/chat-core/tooling/package-import.js +108 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +3 -0
- package/runtime/scripts/chat-server/db.js +38 -9
- package/runtime/scripts/local-gateway.js +158 -0
|
@@ -190,6 +190,7 @@ function frozenRuntimeFields(input) {
|
|
|
190
190
|
authMethod: input.authMethod,
|
|
191
191
|
auth: authIdentity,
|
|
192
192
|
systemPrompt: fingerprint(input.systemPrompt || ''),
|
|
193
|
+
origin: fingerprint(JSON.stringify(input.origin || null)),
|
|
193
194
|
mcpServers: fingerprint(JSON.stringify(mcpServers)),
|
|
194
195
|
};
|
|
195
196
|
}
|
|
@@ -254,6 +255,59 @@ function nullableString(value) {
|
|
|
254
255
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
function normalizeOrigin(payload = {}) {
|
|
259
|
+
const raw = payload.origin && typeof payload.origin === 'object' ? payload.origin : {};
|
|
260
|
+
const type = nullableString(raw.type || payload.originType || payload.origin);
|
|
261
|
+
const id = nullableString(
|
|
262
|
+
raw.id
|
|
263
|
+
|| raw.originId
|
|
264
|
+
|| payload.originId
|
|
265
|
+
|| payload.taskId
|
|
266
|
+
|| payload.triggerId,
|
|
267
|
+
);
|
|
268
|
+
if (!type || !id) return null;
|
|
269
|
+
return {
|
|
270
|
+
type,
|
|
271
|
+
id,
|
|
272
|
+
name: nullableString(raw.name || raw.originName || payload.originName),
|
|
273
|
+
projectPath: nullableString(raw.projectPath || payload.projectPath || payload.cwd),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeConstruct(input, fallback = {}) {
|
|
278
|
+
if (!input || typeof input !== 'object') return null;
|
|
279
|
+
const type = nullableString(input.type || input.scopeType);
|
|
280
|
+
if (!type) return null;
|
|
281
|
+
const id = nullableString(input.id || input.scopeId || input.originId);
|
|
282
|
+
const projectPath = nullableString(input.projectPath || input.path || fallback.projectPath);
|
|
283
|
+
const item = {
|
|
284
|
+
type,
|
|
285
|
+
id,
|
|
286
|
+
name: nullableString(input.name || input.originName),
|
|
287
|
+
projectPath,
|
|
288
|
+
path: nullableString(input.path),
|
|
289
|
+
};
|
|
290
|
+
if (!item.id && !item.projectPath && !item.path) return null;
|
|
291
|
+
return item;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function normalizeConstructs(payload = {}, cwd = null) {
|
|
295
|
+
const constructs = [];
|
|
296
|
+
for (const item of Array.isArray(payload.constructs) ? payload.constructs : []) {
|
|
297
|
+
const normalized = normalizeConstruct(item, { projectPath: cwd });
|
|
298
|
+
if (normalized) constructs.push(normalized);
|
|
299
|
+
}
|
|
300
|
+
const origin = normalizeOrigin(payload);
|
|
301
|
+
if (origin) constructs.push(origin);
|
|
302
|
+
const seen = new Set();
|
|
303
|
+
return constructs.filter((item) => {
|
|
304
|
+
const key = `${item.type}:${item.id || item.projectPath || item.path || ''}`;
|
|
305
|
+
if (seen.has(key)) return false;
|
|
306
|
+
seen.add(key);
|
|
307
|
+
return true;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
257
311
|
function createContract(payload, options = {}) {
|
|
258
312
|
const sessionId = payload.codeSessionId || payload.sessionId;
|
|
259
313
|
if (!sessionId) throw new Error('codeSessionId is required');
|
|
@@ -270,6 +324,7 @@ function createContract(payload, options = {}) {
|
|
|
270
324
|
const cliModel = cliModelFor({ harness, modelId, cliModel: payload.cliModel, reasoningEffort });
|
|
271
325
|
const cwd = resolveCwd(payload.cwd, options);
|
|
272
326
|
const localBaseUrl = options.localBaseUrl || `http://127.0.0.1:${options.port || process.env.CHAT_SERVER_PORT || 8084}`;
|
|
327
|
+
const origin = normalizeOrigin({ ...payload, cwd });
|
|
273
328
|
const auth = authEnvelope({
|
|
274
329
|
harness,
|
|
275
330
|
authMethod,
|
|
@@ -299,6 +354,8 @@ function createContract(payload, options = {}) {
|
|
|
299
354
|
contextWindow: positiveOrNull(payload.contextWindow),
|
|
300
355
|
maxTokens: positiveOrNull(payload.maxTokens),
|
|
301
356
|
cwd,
|
|
357
|
+
origin,
|
|
358
|
+
constructs: normalizeConstructs({ ...payload, origin }, cwd),
|
|
302
359
|
localBaseUrl,
|
|
303
360
|
mcpServers: Array.isArray(payload.mcpServers) ? payload.mcpServers : [],
|
|
304
361
|
systemPrompt: payload.systemPrompt || '',
|
|
@@ -4,7 +4,7 @@ const { PartAccumulator } = require('./parts');
|
|
|
4
4
|
const { assertSameContract, createContract } = require('./contract');
|
|
5
5
|
const { done, errorEvent } = require('./events');
|
|
6
6
|
const { normalizeInput } = require('./input');
|
|
7
|
-
const { frameFor, titleFrame } = require('./sse');
|
|
7
|
+
const { frameFor, titleFrame, withSseId } = require('./sse');
|
|
8
8
|
const { beginProxyTurn, logTurnUsage } = require('./usage');
|
|
9
9
|
|
|
10
10
|
function warnIfDbNotSaved(result, label) {
|
|
@@ -168,8 +168,8 @@ class ChatCore {
|
|
|
168
168
|
if (typeof this.db.generateAndSaveTitle === 'function' && input.text) {
|
|
169
169
|
this.db.generateAndSaveTitle(contract.sessionId, input.text, (title) => {
|
|
170
170
|
const frame = titleFrame({ sessionId: contract.sessionId, title });
|
|
171
|
-
this.turns.addChunk(turnId, frame);
|
|
172
|
-
if (writeFrame) writeFrame(
|
|
171
|
+
const chunk = this.turns.addChunk(turnId, (index) => withSseId(frame, index));
|
|
172
|
+
if (writeFrame && chunk) writeFrame(chunk.data);
|
|
173
173
|
}).catch((err) => {
|
|
174
174
|
console.warn('[ChatCore] Title generation failed:', err.message);
|
|
175
175
|
});
|
|
@@ -194,8 +194,8 @@ class ChatCore {
|
|
|
194
194
|
}
|
|
195
195
|
parts.apply(e);
|
|
196
196
|
const frame = frameFor(e, { sessionId: contract.sessionId, providerSessionId, usage });
|
|
197
|
-
this.turns.addChunk(turnId, frame);
|
|
198
|
-
if (writeFrame) writeFrame(
|
|
197
|
+
const chunk = this.turns.addChunk(turnId, (index) => withSseId(frame, index));
|
|
198
|
+
if (writeFrame && chunk) writeFrame(chunk.data);
|
|
199
199
|
};
|
|
200
200
|
|
|
201
201
|
try {
|
|
@@ -56,23 +56,36 @@ function writeRawTemp(core, route, res) {
|
|
|
56
56
|
const entry = core.turns.latest(route.id) || core.turns.get(route.id);
|
|
57
57
|
if (!entry) return sendJson(res, 404, { error: 'No active stream', codeSessionId: route.id });
|
|
58
58
|
writeSseHeaders(res);
|
|
59
|
+
let open = true;
|
|
60
|
+
const heartbeat = setInterval(() => {
|
|
61
|
+
if (open && !res.writableEnded) res.write(': keep-alive\n\n');
|
|
62
|
+
}, 15_000);
|
|
63
|
+
heartbeat.unref?.();
|
|
64
|
+
const cleanup = () => {
|
|
65
|
+
open = false;
|
|
66
|
+
clearInterval(heartbeat);
|
|
67
|
+
};
|
|
59
68
|
const chunks = entry.chunks.filter((chunk) => chunk.index > (Number.isFinite(route.afterIndex) ? route.afterIndex : -1));
|
|
60
|
-
|
|
61
|
-
res.write(`event: existing\ndata: ${JSON.stringify({ codeSessionId: entry.sessionId, assistantMessageId: entry.assistantMessageId, chunks, total: chunks.length })}\n\n`);
|
|
62
|
-
}
|
|
69
|
+
res.write(`event: existing\ndata: ${JSON.stringify({ codeSessionId: entry.sessionId, assistantMessageId: entry.assistantMessageId, chunks, total: chunks.length })}\n\n`);
|
|
63
70
|
if (entry.status !== 'streaming') {
|
|
64
71
|
res.write(`event: complete\ndata: ${JSON.stringify({ codeSessionId: entry.sessionId, total: entry.chunks.length })}\n\n`);
|
|
72
|
+
cleanup();
|
|
65
73
|
res.end();
|
|
66
74
|
return;
|
|
67
75
|
}
|
|
68
76
|
const off = core.turns.subscribe(entry.turnId, (chunk, current) => {
|
|
77
|
+
if (!open || res.writableEnded) return;
|
|
69
78
|
if (chunk) res.write(`event: chunk\ndata: ${JSON.stringify({ codeSessionId: current.sessionId, chunk, total: current.chunks.length })}\n\n`);
|
|
70
79
|
else {
|
|
71
80
|
res.write(`event: complete\ndata: ${JSON.stringify({ codeSessionId: current.sessionId, total: current.chunks.length })}\n\n`);
|
|
81
|
+
cleanup();
|
|
72
82
|
res.end();
|
|
73
83
|
}
|
|
74
84
|
});
|
|
75
|
-
res.on('close',
|
|
85
|
+
res.on('close', () => {
|
|
86
|
+
cleanup();
|
|
87
|
+
off();
|
|
88
|
+
});
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
function createCore(options = {}) {
|
|
@@ -185,6 +185,13 @@ function titleFrame({ sessionId, title }) {
|
|
|
185
185
|
return `data: ${JSON.stringify({ _type: 'title_generated', sessionId, title })}\n\n`;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
function withSseId(frame, id) {
|
|
189
|
+
const cleanId = Number(id);
|
|
190
|
+
if (!Number.isInteger(cleanId) || cleanId < 0) return frame;
|
|
191
|
+
if (String(frame || '').startsWith('id:')) return frame;
|
|
192
|
+
return `id: ${cleanId}\n${frame}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
188
195
|
function writeSseHeaders(res) {
|
|
189
196
|
res.writeHead(200, {
|
|
190
197
|
'Content-Type': 'text/event-stream',
|
|
@@ -193,4 +200,4 @@ function writeSseHeaders(res) {
|
|
|
193
200
|
});
|
|
194
201
|
}
|
|
195
202
|
|
|
196
|
-
module.exports = { frameFor, titleFrame, toAgentStreamEvent, toPromptUsage, writeSseHeaders };
|
|
203
|
+
module.exports = { frameFor, titleFrame, toAgentStreamEvent, toPromptUsage, withSseId, writeSseHeaders };
|
|
@@ -44,7 +44,9 @@ class TurnStore {
|
|
|
44
44
|
addChunk(turnId, frame) {
|
|
45
45
|
const entry = this.turns.get(turnId);
|
|
46
46
|
if (!entry || !['streaming', 'cancelling'].includes(entry.status)) return null;
|
|
47
|
-
const
|
|
47
|
+
const index = entry.chunks.length;
|
|
48
|
+
const data = typeof frame === 'function' ? frame(index) : frame;
|
|
49
|
+
const chunk = { index, data, timestamp: Date.now() };
|
|
48
50
|
entry.chunks.push(chunk);
|
|
49
51
|
entry.updatedAt = Date.now();
|
|
50
52
|
this.emit(turnId, chunk, entry);
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
9
|
+
const ACTIVE_MEMORY_DIRNAME = '.memories';
|
|
10
|
+
const ACTIVE_MEMORY_ROOT = path.join(AMALGM_DIR, ACTIVE_MEMORY_DIRNAME);
|
|
11
|
+
const MAX_CONTEXT_FILE_CHARS = 12_000;
|
|
12
|
+
|
|
13
|
+
const BASE_FILES = [
|
|
14
|
+
{ scopeType: 'global', title: 'Global Active Memory', relativePath: 'active.md' },
|
|
15
|
+
{ scopeType: 'tasks', title: 'Task Active Memory', relativePath: 'tasks.md' },
|
|
16
|
+
{ scopeType: 'events', title: 'Event Active Memory', relativePath: 'events.md' },
|
|
17
|
+
{ scopeType: 'artifacts', title: 'Artifact Active Memory', relativePath: 'artifacts.md' },
|
|
18
|
+
{ scopeType: 'tools', title: 'Tool Active Memory', relativePath: 'tools.md' },
|
|
19
|
+
{ scopeType: 'projects', title: 'Project Active Memory', relativePath: 'projects.md' },
|
|
20
|
+
{ scopeType: 'folders', title: 'Folder Active Memory', relativePath: 'folders.md' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const CONSTRUCT_FOLDERS = {
|
|
24
|
+
artifact: 'artifacts',
|
|
25
|
+
event: 'events',
|
|
26
|
+
folder: 'folders',
|
|
27
|
+
project: 'projects',
|
|
28
|
+
task: 'tasks',
|
|
29
|
+
tool: 'tools',
|
|
30
|
+
workspace: 'projects',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function hash(value) {
|
|
34
|
+
return crypto.createHash('sha256').update(String(value || '')).digest('hex').slice(0, 20);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function cleanString(value) {
|
|
38
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function safeSegment(value, fallback = 'memory') {
|
|
42
|
+
const cleaned = cleanString(value)
|
|
43
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
44
|
+
.replace(/^-+|-+$/g, '')
|
|
45
|
+
.slice(0, 80);
|
|
46
|
+
return cleaned || `${fallback}-${hash(value)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isWithin(root, target) {
|
|
50
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
51
|
+
return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function activeMemoryRoot() {
|
|
55
|
+
return ACTIVE_MEMORY_ROOT;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function activeMemoryTemplate(title, details = {}) {
|
|
59
|
+
const lines = [
|
|
60
|
+
`# ${title}`,
|
|
61
|
+
'',
|
|
62
|
+
'Agents maintain this active memory file. Keep durable, currently useful facts here and remove stale notes.',
|
|
63
|
+
];
|
|
64
|
+
if (details.scopeType) lines.push(`scope: ${details.scopeType}`);
|
|
65
|
+
if (details.scopeId) lines.push(`scope_id: ${details.scopeId}`);
|
|
66
|
+
if (details.name) lines.push(`name: ${details.name}`);
|
|
67
|
+
if (details.projectPath) lines.push(`project_path: ${details.projectPath}`);
|
|
68
|
+
lines.push('', '- No durable notes yet.', '');
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ensureFile(filePath, title, details = {}) {
|
|
73
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
74
|
+
if (fs.existsSync(filePath)) return false;
|
|
75
|
+
fs.writeFileSync(filePath, activeMemoryTemplate(title, details), 'utf8');
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function statFile(filePath) {
|
|
80
|
+
try {
|
|
81
|
+
return fs.statSync(filePath);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readFile(filePath, maxChars = MAX_CONTEXT_FILE_CHARS) {
|
|
88
|
+
try {
|
|
89
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
90
|
+
if (text.length <= maxChars) return text;
|
|
91
|
+
return `${text.slice(0, maxChars).trim()}\n[truncated]`;
|
|
92
|
+
} catch {
|
|
93
|
+
return '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function basename(filePath) {
|
|
98
|
+
return path.basename(filePath);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function titleFromPath(filePath) {
|
|
102
|
+
return basename(filePath).replace(/\.md$/i, '').replace(/[-_]+/g, ' ') || 'Active Memory';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fileRecord(filePath, extra = {}, options = {}) {
|
|
106
|
+
const stat = statFile(filePath);
|
|
107
|
+
if (!stat || !stat.isFile()) return null;
|
|
108
|
+
const record = {
|
|
109
|
+
id: `memory_${hash(path.resolve(filePath))}`,
|
|
110
|
+
path: path.resolve(filePath),
|
|
111
|
+
name: basename(filePath),
|
|
112
|
+
title: extra.title || titleFromPath(filePath),
|
|
113
|
+
kind: 'active',
|
|
114
|
+
scopeType: extra.scopeType || null,
|
|
115
|
+
scopeId: extra.scopeId || null,
|
|
116
|
+
modTime: stat.mtime.toISOString(),
|
|
117
|
+
size: stat.size,
|
|
118
|
+
writable: true,
|
|
119
|
+
generated: true,
|
|
120
|
+
};
|
|
121
|
+
if (options.includeContent) record.content = readFile(filePath, options.maxChars);
|
|
122
|
+
return record;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function publishMemoriesChange(source = 'active-memory') {
|
|
126
|
+
try {
|
|
127
|
+
const { appendStateEvent } = require('../../amalgm-mcp/state/events');
|
|
128
|
+
appendStateEvent({
|
|
129
|
+
resource: 'memories',
|
|
130
|
+
op: 'replace',
|
|
131
|
+
value: collectActiveMemoryFiles({ publish: false }),
|
|
132
|
+
source,
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
const message = error instanceof Error ? error.message.split('\n')[0] : String(error);
|
|
136
|
+
console.warn('[active-memory] Local Live Store publish failed:', message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function ensureActiveMemoryLibrary(options = {}) {
|
|
141
|
+
const created = [];
|
|
142
|
+
fs.mkdirSync(ACTIVE_MEMORY_ROOT, { recursive: true });
|
|
143
|
+
for (const item of BASE_FILES) {
|
|
144
|
+
const filePath = path.join(ACTIVE_MEMORY_ROOT, item.relativePath);
|
|
145
|
+
if (ensureFile(filePath, item.title, { scopeType: item.scopeType })) {
|
|
146
|
+
created.push(filePath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const dir of ['tasks', 'events', 'artifacts', 'tools', 'projects', 'folders']) {
|
|
150
|
+
fs.mkdirSync(path.join(ACTIVE_MEMORY_ROOT, dir), { recursive: true });
|
|
151
|
+
}
|
|
152
|
+
if (created.length > 0 && options.publish) {
|
|
153
|
+
publishMemoriesChange(options.source || 'active-memory:init');
|
|
154
|
+
}
|
|
155
|
+
return created;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function collectMarkdownFiles(root, out = []) {
|
|
159
|
+
let entries = [];
|
|
160
|
+
try {
|
|
161
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
162
|
+
} catch {
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
const fullPath = path.join(root, entry.name);
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
collectMarkdownFiles(fullPath, out);
|
|
169
|
+
} else if (entry.isFile() && /\.md$/i.test(entry.name)) {
|
|
170
|
+
out.push(fullPath);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function collectActiveMemoryFiles(options = {}) {
|
|
177
|
+
ensureActiveMemoryLibrary({ publish: false });
|
|
178
|
+
const files = collectMarkdownFiles(ACTIVE_MEMORY_ROOT)
|
|
179
|
+
.map((filePath) => fileRecord(filePath, {}, options))
|
|
180
|
+
.filter(Boolean);
|
|
181
|
+
files.sort((left, right) => {
|
|
182
|
+
const leftTime = left.modTime ? new Date(left.modTime).getTime() : 0;
|
|
183
|
+
const rightTime = right.modTime ? new Date(right.modTime).getTime() : 0;
|
|
184
|
+
if (leftTime !== rightTime) return rightTime - leftTime;
|
|
185
|
+
return left.path.localeCompare(right.path);
|
|
186
|
+
});
|
|
187
|
+
return files;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isActiveMemoryPath(filePath) {
|
|
191
|
+
return !!cleanString(filePath) && isWithin(ACTIVE_MEMORY_ROOT, filePath);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function notifyMemoryPathChanged(filePath, source = 'fs') {
|
|
195
|
+
if (isActiveMemoryPath(filePath)) publishMemoriesChange(source);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function workspaceForProjectPath(projectPath) {
|
|
199
|
+
const resolved = cleanString(projectPath) ? path.resolve(projectPath) : '';
|
|
200
|
+
if (!resolved) return null;
|
|
201
|
+
try {
|
|
202
|
+
const workspaceStore = require('../../amalgm-mcp/workspace/store');
|
|
203
|
+
const workspaces = workspaceStore.listWorkspaces();
|
|
204
|
+
return workspaces
|
|
205
|
+
.filter((workspace) => workspace?.path && isWithin(workspace.path, resolved))
|
|
206
|
+
.sort((left, right) => String(right.path).length - String(left.path).length)[0] || null;
|
|
207
|
+
} catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function projectScope(projectPath) {
|
|
213
|
+
return constructScope({ type: 'project', projectPath });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function originFromContract(contract) {
|
|
217
|
+
const raw = contract?.origin && typeof contract.origin === 'object' ? contract.origin : {};
|
|
218
|
+
const type = cleanString(raw.type || contract?.originType);
|
|
219
|
+
const id = cleanString(raw.id || raw.originId || contract?.originId || contract?.taskId || contract?.triggerId);
|
|
220
|
+
if (!type || !id) return null;
|
|
221
|
+
return {
|
|
222
|
+
type,
|
|
223
|
+
id,
|
|
224
|
+
name: cleanString(raw.name || raw.originName || contract?.originName),
|
|
225
|
+
projectPath: cleanString(raw.projectPath || contract?.projectPath || contract?.cwd),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function originScope(origin) {
|
|
230
|
+
if (!origin) return null;
|
|
231
|
+
if (origin.type !== 'task' && origin.type !== 'event') return null;
|
|
232
|
+
return constructScope(origin);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function constructScope(input = {}) {
|
|
236
|
+
const rawType = cleanString(input.type || input.scopeType).toLowerCase();
|
|
237
|
+
const folder = CONSTRUCT_FOLDERS[rawType];
|
|
238
|
+
if (!folder) return null;
|
|
239
|
+
|
|
240
|
+
if (rawType === 'project' || rawType === 'workspace') {
|
|
241
|
+
const resolved = cleanString(input.projectPath || input.path) ? path.resolve(input.projectPath || input.path) : '';
|
|
242
|
+
const workspace = resolved ? workspaceForProjectPath(resolved) : null;
|
|
243
|
+
const scopeId = cleanString(input.id || input.scopeId) || workspace?.id || (resolved ? `path-${hash(resolved)}` : '');
|
|
244
|
+
if (!scopeId) return null;
|
|
245
|
+
const name = cleanString(input.name) || workspace?.name || (resolved ? path.basename(resolved) : '') || 'Project';
|
|
246
|
+
return {
|
|
247
|
+
scopeType: 'project',
|
|
248
|
+
scopeId,
|
|
249
|
+
title: `${name} Active Memory`,
|
|
250
|
+
relativePath: path.join(folder, `${safeSegment(scopeId, 'project')}.md`),
|
|
251
|
+
name,
|
|
252
|
+
projectPath: workspace?.path || resolved || cleanString(input.projectPath || input.path),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (rawType === 'folder') {
|
|
257
|
+
const folderPath = cleanString(input.path || input.projectPath);
|
|
258
|
+
const scopeId = cleanString(input.id || input.scopeId) || (folderPath ? `path-${hash(path.resolve(folderPath))}` : '');
|
|
259
|
+
if (!scopeId) return null;
|
|
260
|
+
const name = cleanString(input.name) || (folderPath ? path.basename(path.resolve(folderPath)) : '') || 'Folder';
|
|
261
|
+
return {
|
|
262
|
+
scopeType: 'folder',
|
|
263
|
+
scopeId,
|
|
264
|
+
title: `${name} Active Memory`,
|
|
265
|
+
relativePath: path.join(folder, `${safeSegment(scopeId, 'folder')}.md`),
|
|
266
|
+
name,
|
|
267
|
+
projectPath: folderPath ? path.resolve(folderPath) : '',
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const scopeId = cleanString(input.id || input.scopeId);
|
|
272
|
+
if (!scopeId) return null;
|
|
273
|
+
const titleType = rawType.charAt(0).toUpperCase() + rawType.slice(1);
|
|
274
|
+
const name = cleanString(input.name) || titleType;
|
|
275
|
+
return {
|
|
276
|
+
scopeType: rawType,
|
|
277
|
+
scopeId,
|
|
278
|
+
title: `${name} Active Memory`,
|
|
279
|
+
relativePath: path.join(folder, `${safeSegment(scopeId, rawType)}.md`),
|
|
280
|
+
name,
|
|
281
|
+
projectPath: cleanString(input.projectPath || input.path),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function ensureScopedFile(scope, options = {}) {
|
|
286
|
+
const filePath = path.join(ACTIVE_MEMORY_ROOT, scope.relativePath);
|
|
287
|
+
const created = ensureFile(filePath, scope.title, scope);
|
|
288
|
+
if (created && options.publish) {
|
|
289
|
+
publishMemoriesChange(options.source || 'active-memory:scope');
|
|
290
|
+
}
|
|
291
|
+
const record = fileRecord(filePath, scope, { includeContent: options.includeContent, maxChars: options.maxChars });
|
|
292
|
+
if (record && created) record.created = true;
|
|
293
|
+
return record;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function ensureConstructMemory(input, options = {}) {
|
|
297
|
+
const scope = constructScope(input);
|
|
298
|
+
if (!scope) return null;
|
|
299
|
+
return ensureScopedFile(scope, {
|
|
300
|
+
includeContent: options.includeContent,
|
|
301
|
+
maxChars: options.maxChars,
|
|
302
|
+
publish: options.publish !== false,
|
|
303
|
+
source: options.source || `active-memory:${scope.scopeType}`,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function scopesForContract(contract) {
|
|
308
|
+
const scopes = [
|
|
309
|
+
{ scopeType: 'global', title: 'Global Active Memory', relativePath: 'active.md' },
|
|
310
|
+
];
|
|
311
|
+
const origin = originFromContract(contract);
|
|
312
|
+
const projectPath = cleanString(origin?.projectPath || contract?.projectPath || contract?.cwd);
|
|
313
|
+
const project = projectScope(projectPath);
|
|
314
|
+
if (project) {
|
|
315
|
+
scopes.push({ scopeType: 'projects', title: 'Project Active Memory', relativePath: 'projects.md' });
|
|
316
|
+
scopes.push(project);
|
|
317
|
+
}
|
|
318
|
+
for (const construct of Array.isArray(contract?.constructs) ? contract.constructs : []) {
|
|
319
|
+
const scope = constructScope(construct);
|
|
320
|
+
if (!scope) continue;
|
|
321
|
+
const folder = CONSTRUCT_FOLDERS[scope.scopeType];
|
|
322
|
+
if (folder) {
|
|
323
|
+
scopes.push({
|
|
324
|
+
scopeType: folder,
|
|
325
|
+
title: `${scope.scopeType.charAt(0).toUpperCase()}${scope.scopeType.slice(1)} Active Memory`,
|
|
326
|
+
relativePath: `${folder}.md`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
scopes.push(scope);
|
|
330
|
+
}
|
|
331
|
+
const originMemory = originScope(origin);
|
|
332
|
+
if (originMemory) {
|
|
333
|
+
scopes.push({
|
|
334
|
+
scopeType: origin.type === 'task' ? 'tasks' : 'events',
|
|
335
|
+
title: origin.type === 'task' ? 'Task Active Memory' : 'Event Active Memory',
|
|
336
|
+
relativePath: origin.type === 'task' ? 'tasks.md' : 'events.md',
|
|
337
|
+
});
|
|
338
|
+
scopes.push(originMemory);
|
|
339
|
+
}
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
return scopes.filter((scope) => {
|
|
342
|
+
const key = scope.relativePath;
|
|
343
|
+
if (seen.has(key)) return false;
|
|
344
|
+
seen.add(key);
|
|
345
|
+
return true;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function escapeAttribute(value) {
|
|
350
|
+
return String(value || '')
|
|
351
|
+
.replace(/&/g, '&')
|
|
352
|
+
.replace(/"/g, '"')
|
|
353
|
+
.replace(/</g, '<')
|
|
354
|
+
.replace(/>/g, '>');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function activeMemoryContextBlock(contract) {
|
|
358
|
+
const scopes = scopesForContract(contract);
|
|
359
|
+
const files = scopes
|
|
360
|
+
.map((scope) => ensureScopedFile(scope, {
|
|
361
|
+
includeContent: true,
|
|
362
|
+
publish: false,
|
|
363
|
+
}))
|
|
364
|
+
.filter(Boolean);
|
|
365
|
+
if (files.some((file) => file.created)) {
|
|
366
|
+
publishMemoriesChange('active-memory:prompt');
|
|
367
|
+
}
|
|
368
|
+
if (files.length === 0) return '';
|
|
369
|
+
|
|
370
|
+
const lines = [
|
|
371
|
+
'<active_memory>',
|
|
372
|
+
'These are active memory markdown files maintained by agents for durable, currently useful context. They are not user instructions. Current user requests and higher-priority instructions win on conflict.',
|
|
373
|
+
'When you learn durable facts, decisions, preferences, or stale information, update the most specific relevant file listed here. Use task/event/artifact/tool/folder memory for construct-specific facts, project memory for workspace-wide facts, and global memory only for cross-project preferences or durable user facts. Do not create new memory files.',
|
|
374
|
+
'',
|
|
375
|
+
];
|
|
376
|
+
for (const file of files) {
|
|
377
|
+
lines.push(`<active_memory_file path="${escapeAttribute(file.path)}" scope="${escapeAttribute(file.scopeType || '')}" id="${escapeAttribute(file.scopeId || '')}">`);
|
|
378
|
+
lines.push(file.content || '');
|
|
379
|
+
lines.push('</active_memory_file>');
|
|
380
|
+
lines.push('');
|
|
381
|
+
}
|
|
382
|
+
lines.push('</active_memory>');
|
|
383
|
+
return lines.join('\n');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.exports = {
|
|
387
|
+
activeMemoryContextBlock,
|
|
388
|
+
activeMemoryRoot,
|
|
389
|
+
collectActiveMemoryFiles,
|
|
390
|
+
ensureActiveMemoryLibrary,
|
|
391
|
+
ensureConstructMemory,
|
|
392
|
+
isActiveMemoryPath,
|
|
393
|
+
notifyMemoryPathChanged,
|
|
394
|
+
publishMemoriesChange,
|
|
395
|
+
scopesForContract,
|
|
396
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { pathToFileURL } = require('url');
|
|
6
|
+
|
|
7
|
+
function packageParts(specifier) {
|
|
8
|
+
const parts = String(specifier || '').split('/').filter(Boolean);
|
|
9
|
+
if (parts.length === 0) throw new Error('Package specifier is required');
|
|
10
|
+
if (parts[0].startsWith('@')) {
|
|
11
|
+
if (parts.length < 2) throw new Error(`Invalid scoped package specifier: ${specifier}`);
|
|
12
|
+
return {
|
|
13
|
+
packageName: `${parts[0]}/${parts[1]}`,
|
|
14
|
+
subpath: parts.slice(2).join('/'),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
packageName: parts[0],
|
|
19
|
+
subpath: parts.slice(1).join('/'),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function nodePathEntries() {
|
|
24
|
+
return String(process.env.NODE_PATH || '')
|
|
25
|
+
.split(path.delimiter)
|
|
26
|
+
.map((entry) => entry.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ancestorNodeModules() {
|
|
31
|
+
const entries = [];
|
|
32
|
+
let current = __dirname;
|
|
33
|
+
for (;;) {
|
|
34
|
+
entries.push(path.join(current, 'node_modules'));
|
|
35
|
+
const next = path.dirname(current);
|
|
36
|
+
if (next === current) break;
|
|
37
|
+
current = next;
|
|
38
|
+
}
|
|
39
|
+
return entries;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function candidateNodeModules() {
|
|
43
|
+
const resourcesPath = process.resourcesPath || '';
|
|
44
|
+
const entries = [
|
|
45
|
+
...nodePathEntries(),
|
|
46
|
+
...ancestorNodeModules(),
|
|
47
|
+
resourcesPath ? path.join(resourcesPath, 'app.asar', 'node_modules') : '',
|
|
48
|
+
resourcesPath ? path.join(resourcesPath, 'app.asar.unpacked', 'node_modules') : '',
|
|
49
|
+
resourcesPath ? path.join(resourcesPath, 'cli', 'node_modules') : '',
|
|
50
|
+
resourcesPath ? path.join(resourcesPath, 'runtime', 'node_modules') : '',
|
|
51
|
+
].filter(Boolean);
|
|
52
|
+
return Array.from(new Set(entries));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readPackageJson(packageRoot) {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function exportedPath(exportsField, subpath) {
|
|
64
|
+
if (!exportsField) return '';
|
|
65
|
+
const key = subpath ? `./${subpath}` : '.';
|
|
66
|
+
const value = typeof exportsField === 'object' && !Array.isArray(exportsField)
|
|
67
|
+
? exportsField[key]
|
|
68
|
+
: (key === '.' ? exportsField : null);
|
|
69
|
+
if (!value) return '';
|
|
70
|
+
if (typeof value === 'string') return value;
|
|
71
|
+
if (typeof value !== 'object' || Array.isArray(value)) return '';
|
|
72
|
+
for (const condition of ['import', 'module', 'default', 'node', 'require']) {
|
|
73
|
+
if (typeof value[condition] === 'string') return value[condition];
|
|
74
|
+
}
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolvePackageEntry(specifier) {
|
|
79
|
+
const { packageName, subpath } = packageParts(specifier);
|
|
80
|
+
for (const nodeModules of candidateNodeModules()) {
|
|
81
|
+
const packageRoot = path.join(nodeModules, packageName);
|
|
82
|
+
const pkg = readPackageJson(packageRoot);
|
|
83
|
+
if (!pkg) continue;
|
|
84
|
+
|
|
85
|
+
const target = (
|
|
86
|
+
exportedPath(pkg.exports, subpath)
|
|
87
|
+
|| (subpath ? subpath : '')
|
|
88
|
+
|| pkg.module
|
|
89
|
+
|| pkg.main
|
|
90
|
+
|| 'index.js'
|
|
91
|
+
);
|
|
92
|
+
const resolved = path.join(packageRoot, target);
|
|
93
|
+
if (fs.existsSync(resolved)) return resolved;
|
|
94
|
+
}
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function importPackage(specifier) {
|
|
99
|
+
try {
|
|
100
|
+
return await import(specifier);
|
|
101
|
+
} catch (originalError) {
|
|
102
|
+
const resolved = resolvePackageEntry(specifier);
|
|
103
|
+
if (!resolved) throw originalError;
|
|
104
|
+
return import(pathToFileURL(resolved).href);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { importPackage, resolvePackageEntry };
|