amalgm 0.1.88 → 0.1.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/lib/cli.js +16 -6
  2. package/lib/service.js +19 -0
  3. package/lib/supervisor.js +51 -0
  4. package/package.json +1 -1
  5. package/runtime/scripts/amalgm-mcp/apps/supervisor.js +0 -25
  6. package/runtime/scripts/amalgm-mcp/browser/auth-tools.js +179 -0
  7. package/runtime/scripts/amalgm-mcp/browser/cli.js +217 -0
  8. package/runtime/scripts/amalgm-mcp/browser/cua-tools.js +147 -0
  9. package/runtime/scripts/amalgm-mcp/browser/engine.js +454 -0
  10. package/runtime/scripts/amalgm-mcp/browser/recorder-tools.js +82 -0
  11. package/runtime/scripts/amalgm-mcp/browser/recorder.js +199 -0
  12. package/runtime/scripts/amalgm-mcp/browser/rest.js +5 -1
  13. package/runtime/scripts/amalgm-mcp/browser/tools.js +104 -731
  14. package/runtime/scripts/amalgm-mcp/fs/rest.js +5 -6
  15. package/runtime/scripts/amalgm-mcp/index.js +2 -0
  16. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +32 -66
  17. package/runtime/scripts/amalgm-mcp/project-context/paths.js +266 -0
  18. package/runtime/scripts/amalgm-mcp/project-context/prompt.js +232 -0
  19. package/runtime/scripts/amalgm-mcp/project-context/rest.js +100 -0
  20. package/runtime/scripts/amalgm-mcp/project-context/store.js +671 -0
  21. package/runtime/scripts/amalgm-mcp/project-context/tools.js +146 -0
  22. package/runtime/scripts/amalgm-mcp/server/core-tools.js +35 -2
  23. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  24. package/runtime/scripts/amalgm-mcp/server/routes/project-context.js +33 -0
  25. package/runtime/scripts/amalgm-mcp/state/db.js +11 -0
  26. package/runtime/scripts/amalgm-mcp/state/snapshot.js +3 -3
  27. package/runtime/scripts/amalgm-mcp/tests/core-tools.test.js +37 -4
  28. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +328 -0
  29. package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +9 -9
  30. package/runtime/scripts/amalgm-mcp/tests/toolbox-service.test.js +2 -2
  31. package/runtime/scripts/amalgm-mcp/toolbox/loadout-context.js +1 -0
  32. package/runtime/scripts/amalgm-mcp/toolbox/store.js +0 -15
  33. package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +2 -0
  34. package/runtime/scripts/amalgm-mcp/workspace/rest.js +11 -7
  35. package/runtime/scripts/chat-core/adapters/opencode.js +1 -1
  36. package/runtime/scripts/chat-core/engine.js +16 -2
  37. package/runtime/scripts/chat-core/input.js +19 -1
  38. package/runtime/scripts/chat-core/tool-shape.js +6 -4
  39. package/runtime/scripts/chat-core/tooling/system-instructions.js +51 -0
  40. package/runtime/scripts/chat-core/tooling/system-prompt.js +34 -8
  41. package/runtime/scripts/local-gateway.js +1 -0
  42. package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +0 -569
  43. package/runtime/scripts/amalgm-mcp/browser/page.js +0 -25
  44. package/runtime/scripts/chat-core/tooling/active-memory.js +0 -574
  45. package/runtime/scripts/chat-core/tooling/passive-memory.js +0 -576
@@ -1,574 +0,0 @@
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
-
10
- const ACTIVE_MEMORY_DIRNAME = '.memories';
11
- const ACTIVE_MEMORY_ROOT = path.join(AMALGM_DIR, ACTIVE_MEMORY_DIRNAME);
12
- const CONTEXT_DIRNAME = 'context';
13
- const PROJECT_CONTEXT_DIR = path.join('.amalgm', CONTEXT_DIRNAME);
14
- const INSTRUCTIONS_FILENAME = 'instructions.md';
15
- const MEMORIES_FILENAME = 'memories.md';
16
- const REFERENCES_DIRNAME = 'references';
17
- const MAX_CONTEXT_FILE_CHARS = 12_000;
18
-
19
- const BASE_FILES = [
20
- { scopeType: 'global', title: 'Global Active Memory', relativePath: 'active.md' },
21
- { scopeType: 'tasks', title: 'Task Active Memory', relativePath: 'tasks.md' },
22
- { scopeType: 'events', title: 'Event Active Memory', relativePath: 'events.md' },
23
- { scopeType: 'apps', title: 'App Active Memory', relativePath: 'apps.md' },
24
- { scopeType: 'tools', title: 'Tool Active Memory', relativePath: 'tools.md' },
25
- { scopeType: 'projects', title: 'Project Active Memory', relativePath: 'projects.md' },
26
- { scopeType: 'folders', title: 'Folder Active Memory', relativePath: 'folders.md' },
27
- ];
28
-
29
- const CONSTRUCT_FOLDERS = {
30
- app: 'apps',
31
- artifact: 'apps',
32
- event: 'events',
33
- folder: 'folders',
34
- project: 'projects',
35
- task: 'tasks',
36
- tool: 'tools',
37
- workspace: 'projects',
38
- };
39
-
40
- function hash(value) {
41
- return crypto.createHash('sha256').update(String(value || '')).digest('hex').slice(0, 20);
42
- }
43
-
44
- function cleanString(value) {
45
- return typeof value === 'string' && value.trim() ? value.trim() : '';
46
- }
47
-
48
- function safeSegment(value, fallback = 'memory') {
49
- const cleaned = cleanString(value)
50
- .replace(/[^A-Za-z0-9._-]+/g, '-')
51
- .replace(/^-+|-+$/g, '')
52
- .slice(0, 80);
53
- return cleaned || `${fallback}-${hash(value)}`;
54
- }
55
-
56
- function isWithin(root, target) {
57
- const relative = path.relative(path.resolve(root), path.resolve(target));
58
- return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
59
- }
60
-
61
- function activeMemoryRoot() {
62
- return ACTIVE_MEMORY_ROOT;
63
- }
64
-
65
- function systemContextRoot() {
66
- return path.join(AMALGM_DIR, CONTEXT_DIRNAME);
67
- }
68
-
69
- function projectContextRoot(projectPath) {
70
- const resolved = cleanString(projectPath) ? path.resolve(projectPath) : '';
71
- if (!resolved) return '';
72
- return path.join(resolved, PROJECT_CONTEXT_DIR);
73
- }
74
-
75
- function ensureContext(root, name, options = {}) {
76
- if (!root) return [];
77
- const created = [];
78
- fs.mkdirSync(root, { recursive: true });
79
- fs.mkdirSync(path.join(root, REFERENCES_DIRNAME), { recursive: true });
80
- const instructionsPath = path.join(root, INSTRUCTIONS_FILENAME);
81
- const memoriesPath = path.join(root, MEMORIES_FILENAME);
82
- if (ensurePlainFile(instructionsPath, instructionsTemplate(name || 'this context'))) {
83
- created.push(instructionsPath);
84
- }
85
- if (ensureFile(memoriesPath, `${name || 'Context'} Memories`, {
86
- scopeType: options.scopeType || 'project',
87
- scopeId: options.scopeId,
88
- name,
89
- projectPath: options.projectPath,
90
- })) {
91
- created.push(memoriesPath);
92
- }
93
- return created;
94
- }
95
-
96
- function ensureSystemContext(options = {}) {
97
- const created = ensureContext(systemContextRoot(), 'Amalgm System', {
98
- scopeType: 'system',
99
- scopeId: 'system',
100
- projectPath: AMALGM_DIR,
101
- });
102
- if (created.length > 0 && options.publish) {
103
- publishMemoriesChange(options.source || 'active-memory:system-context');
104
- }
105
- return created;
106
- }
107
-
108
- function ensureProjectContext(input = {}, options = {}) {
109
- const projectPath = cleanString(input.projectPath || input.path);
110
- const root = projectContextRoot(projectPath);
111
- if (!root) return [];
112
- const name = cleanString(input.name) || (projectPath ? path.basename(path.resolve(projectPath)) : '') || 'Project';
113
- const created = ensureContext(root, name, {
114
- scopeType: 'project',
115
- scopeId: cleanString(input.id || input.scopeId),
116
- projectPath: path.resolve(projectPath),
117
- });
118
- if (created.length > 0 && options.publish) {
119
- publishMemoriesChange(options.source || 'active-memory:project-context');
120
- }
121
- return created;
122
- }
123
-
124
- function activeMemoryTemplate(title, details = {}) {
125
- const lines = [
126
- `# ${title}`,
127
- '',
128
- 'Agents maintain this active memory file. Keep durable, currently useful facts here and remove stale notes.',
129
- ];
130
- if (details.scopeType) lines.push(`scope: ${details.scopeType}`);
131
- if (details.scopeId) lines.push(`scope_id: ${details.scopeId}`);
132
- if (details.name) lines.push(`name: ${details.name}`);
133
- if (details.projectPath) lines.push(`project_path: ${details.projectPath}`);
134
- lines.push('', '- No durable notes yet.', '');
135
- return lines.join('\n');
136
- }
137
-
138
- function instructionsTemplate(title) {
139
- return [
140
- '# Instructions',
141
- '',
142
- `No instructions for ${title} yet.`,
143
- '',
144
- ].join('\n');
145
- }
146
-
147
- function ensureFile(filePath, title, details = {}) {
148
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
149
- if (fs.existsSync(filePath)) return false;
150
- fs.writeFileSync(filePath, activeMemoryTemplate(title, details), 'utf8');
151
- return true;
152
- }
153
-
154
- function ensurePlainFile(filePath, content) {
155
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
156
- if (fs.existsSync(filePath)) return false;
157
- fs.writeFileSync(filePath, content, 'utf8');
158
- return true;
159
- }
160
-
161
- function statFile(filePath) {
162
- try {
163
- return fs.statSync(filePath);
164
- } catch {
165
- return null;
166
- }
167
- }
168
-
169
- function readFile(filePath, maxChars = MAX_CONTEXT_FILE_CHARS) {
170
- try {
171
- const text = fs.readFileSync(filePath, 'utf8');
172
- if (text.length <= maxChars) return text;
173
- return `${text.slice(0, maxChars).trim()}\n[truncated]`;
174
- } catch {
175
- return '';
176
- }
177
- }
178
-
179
- function basename(filePath) {
180
- return path.basename(filePath);
181
- }
182
-
183
- function titleFromPath(filePath) {
184
- return basename(filePath).replace(/\.md$/i, '').replace(/[-_]+/g, ' ') || 'Active Memory';
185
- }
186
-
187
- function fileRecord(filePath, extra = {}, options = {}) {
188
- const stat = statFile(filePath);
189
- if (!stat || !stat.isFile()) return null;
190
- const record = {
191
- id: `memory_${hash(path.resolve(filePath))}`,
192
- path: path.resolve(filePath),
193
- name: basename(filePath),
194
- title: extra.title || titleFromPath(filePath),
195
- kind: 'active',
196
- scopeType: extra.scopeType || null,
197
- scopeId: extra.scopeId || null,
198
- modTime: stat.mtime.toISOString(),
199
- size: stat.size,
200
- writable: true,
201
- generated: true,
202
- };
203
- if (options.includeContent) record.content = readFile(filePath, options.maxChars);
204
- return record;
205
- }
206
-
207
- function publishMemoriesChange(source = 'active-memory') {
208
- try {
209
- const { appendStateEvent } = require('../../amalgm-mcp/state/events');
210
- appendStateEvent({
211
- resource: 'memories',
212
- op: 'replace',
213
- value: collectActiveMemoryFiles({ publish: false }),
214
- source,
215
- });
216
- } catch (error) {
217
- const message = error instanceof Error ? error.message.split('\n')[0] : String(error);
218
- console.warn('[active-memory] Local Live Store publish failed:', message);
219
- }
220
- }
221
-
222
- function ensureActiveMemoryLibrary(options = {}) {
223
- const created = [];
224
- fs.mkdirSync(ACTIVE_MEMORY_ROOT, { recursive: true });
225
- for (const item of BASE_FILES) {
226
- const filePath = path.join(ACTIVE_MEMORY_ROOT, item.relativePath);
227
- if (ensureFile(filePath, item.title, { scopeType: item.scopeType })) {
228
- created.push(filePath);
229
- }
230
- }
231
- for (const dir of ['tasks', 'events', 'apps', 'tools', 'projects', 'folders']) {
232
- fs.mkdirSync(path.join(ACTIVE_MEMORY_ROOT, dir), { recursive: true });
233
- }
234
- if (created.length > 0 && options.publish) {
235
- publishMemoriesChange(options.source || 'active-memory:init');
236
- }
237
- return created;
238
- }
239
-
240
- function collectMarkdownFiles(root, out = []) {
241
- let entries = [];
242
- try {
243
- entries = fs.readdirSync(root, { withFileTypes: true });
244
- } catch {
245
- return out;
246
- }
247
- for (const entry of entries) {
248
- const fullPath = path.join(root, entry.name);
249
- if (entry.isDirectory()) {
250
- collectMarkdownFiles(fullPath, out);
251
- } else if (entry.isFile() && /\.md$/i.test(entry.name)) {
252
- out.push(fullPath);
253
- }
254
- }
255
- return out;
256
- }
257
-
258
- function collectActiveMemoryFiles(options = {}) {
259
- ensureActiveMemoryLibrary({ publish: false });
260
- ensureSystemContext({ publish: false });
261
- const contextMemoryFiles = [path.join(systemContextRoot(), MEMORIES_FILENAME)];
262
- try {
263
- const workspaceStore = require('../../amalgm-mcp/workspace/store');
264
- for (const workspace of workspaceStore.listWorkspaces()) {
265
- if (!workspace?.path) continue;
266
- ensureProjectContext(workspace, { publish: false });
267
- contextMemoryFiles.push(path.join(projectContextRoot(workspace.path), MEMORIES_FILENAME));
268
- }
269
- } catch {}
270
-
271
- const files = [
272
- ...collectMarkdownFiles(ACTIVE_MEMORY_ROOT),
273
- ...contextMemoryFiles,
274
- ]
275
- .map((filePath) => fileRecord(filePath, {}, options))
276
- .filter(Boolean);
277
- files.sort((left, right) => {
278
- const leftTime = left.modTime ? new Date(left.modTime).getTime() : 0;
279
- const rightTime = right.modTime ? new Date(right.modTime).getTime() : 0;
280
- if (leftTime !== rightTime) return rightTime - leftTime;
281
- return left.path.localeCompare(right.path);
282
- });
283
- return files;
284
- }
285
-
286
- function isActiveMemoryPath(filePath) {
287
- if (!cleanString(filePath)) return false;
288
- if (isWithin(ACTIVE_MEMORY_ROOT, filePath)) return true;
289
- const resolved = path.resolve(filePath);
290
- return path.basename(resolved) === MEMORIES_FILENAME
291
- && path.basename(path.dirname(resolved)) === CONTEXT_DIRNAME
292
- && path.basename(path.dirname(path.dirname(resolved))) === '.amalgm';
293
- }
294
-
295
- function notifyMemoryPathChanged(filePath, source = 'fs') {
296
- if (isActiveMemoryPath(filePath)) publishMemoriesChange(source);
297
- }
298
-
299
- function workspaceForProjectPath(projectPath) {
300
- const resolved = cleanString(projectPath) ? path.resolve(projectPath) : '';
301
- if (!resolved) return null;
302
- try {
303
- const workspaceStore = require('../../amalgm-mcp/workspace/store');
304
- const workspaces = workspaceStore.listWorkspaces();
305
- return workspaces
306
- .filter((workspace) => workspace?.path && isWithin(workspace.path, resolved))
307
- .sort((left, right) => String(right.path).length - String(left.path).length)[0] || null;
308
- } catch {
309
- return null;
310
- }
311
- }
312
-
313
- function projectScope(projectPath) {
314
- return constructScope({ type: 'project', projectPath });
315
- }
316
-
317
- function originFromContract(contract) {
318
- const raw = contract?.origin && typeof contract.origin === 'object' ? contract.origin : {};
319
- const type = cleanString(raw.type || contract?.originType);
320
- const id = cleanString(raw.id || raw.originId || contract?.originId || contract?.taskId || contract?.triggerId);
321
- if (!type || !id) return null;
322
- return {
323
- type,
324
- id,
325
- name: cleanString(raw.name || raw.originName || contract?.originName),
326
- projectPath: cleanString(raw.projectPath || contract?.projectPath || contract?.cwd),
327
- };
328
- }
329
-
330
- function originScope(origin) {
331
- if (!origin) return null;
332
- if (origin.type !== 'task' && origin.type !== 'event') return null;
333
- return constructScope(origin);
334
- }
335
-
336
- function constructScope(input = {}) {
337
- const rawType = cleanString(input.type || input.scopeType).toLowerCase();
338
- const folder = CONSTRUCT_FOLDERS[rawType];
339
- if (!folder) return null;
340
-
341
- if (rawType === 'project' || rawType === 'workspace') {
342
- const resolved = cleanString(input.projectPath || input.path) ? path.resolve(input.projectPath || input.path) : '';
343
- const workspace = resolved ? workspaceForProjectPath(resolved) : null;
344
- const scopeId = cleanString(input.id || input.scopeId) || workspace?.id || (resolved ? `path-${hash(resolved)}` : '');
345
- if (!scopeId) return null;
346
- const name = cleanString(input.name) || workspace?.name || (resolved ? path.basename(resolved) : '') || 'Project';
347
- const projectPath = workspace?.path || resolved || cleanString(input.projectPath || input.path);
348
- const contextRoot = projectContextRoot(projectPath);
349
- return {
350
- scopeType: 'project',
351
- scopeId,
352
- title: `${name} Active Memory`,
353
- relativePath: path.join(folder, `${safeSegment(scopeId, 'project')}.md`),
354
- filePath: contextRoot ? path.join(contextRoot, MEMORIES_FILENAME) : '',
355
- name,
356
- projectPath,
357
- };
358
- }
359
-
360
- if (rawType === 'folder') {
361
- const folderPath = cleanString(input.path || input.projectPath);
362
- const scopeId = cleanString(input.id || input.scopeId) || (folderPath ? `path-${hash(path.resolve(folderPath))}` : '');
363
- if (!scopeId) return null;
364
- const name = cleanString(input.name) || (folderPath ? path.basename(path.resolve(folderPath)) : '') || 'Folder';
365
- return {
366
- scopeType: 'folder',
367
- scopeId,
368
- title: `${name} Active Memory`,
369
- relativePath: path.join(folder, `${safeSegment(scopeId, 'folder')}.md`),
370
- name,
371
- projectPath: folderPath ? path.resolve(folderPath) : '',
372
- };
373
- }
374
-
375
- const scopeId = cleanString(input.id || input.scopeId);
376
- if (!scopeId) return null;
377
- const titleType = rawType.charAt(0).toUpperCase() + rawType.slice(1);
378
- const name = cleanString(input.name) || titleType;
379
- return {
380
- scopeType: rawType,
381
- scopeId,
382
- title: `${name} Active Memory`,
383
- relativePath: path.join(folder, `${safeSegment(scopeId, rawType)}.md`),
384
- name,
385
- projectPath: cleanString(input.projectPath || input.path),
386
- };
387
- }
388
-
389
- function ensureScopedFile(scope, options = {}) {
390
- if (scope.scopeType === 'project' && scope.projectPath) {
391
- ensureProjectContext(scope, { publish: false });
392
- }
393
- const filePath = scope.filePath || path.join(ACTIVE_MEMORY_ROOT, scope.relativePath);
394
- const created = ensureFile(filePath, scope.title, scope);
395
- if (created && options.publish) {
396
- publishMemoriesChange(options.source || 'active-memory:scope');
397
- }
398
- const record = fileRecord(filePath, scope, { includeContent: options.includeContent, maxChars: options.maxChars });
399
- if (record && created) record.created = true;
400
- return record;
401
- }
402
-
403
- function ensureConstructMemory(input, options = {}) {
404
- const scope = constructScope(input);
405
- if (!scope) return null;
406
- return ensureScopedFile(scope, {
407
- includeContent: options.includeContent,
408
- maxChars: options.maxChars,
409
- publish: options.publish !== false,
410
- source: options.source || `active-memory:${scope.scopeType}`,
411
- });
412
- }
413
-
414
- function scopesForContract(contract) {
415
- const scopes = [
416
- { scopeType: 'global', title: 'Global Active Memory', relativePath: 'active.md' },
417
- {
418
- scopeType: 'system',
419
- scopeId: 'system',
420
- title: 'Amalgm System Memories',
421
- relativePath: path.join(CONTEXT_DIRNAME, MEMORIES_FILENAME),
422
- filePath: path.join(systemContextRoot(), MEMORIES_FILENAME),
423
- name: 'Amalgm System',
424
- projectPath: AMALGM_DIR,
425
- },
426
- ];
427
- const origin = originFromContract(contract);
428
- const projectPath = cleanString(origin?.projectPath || contract?.projectPath || contract?.cwd);
429
- const project = projectScope(projectPath);
430
- if (project) {
431
- scopes.push({ scopeType: 'projects', title: 'Project Active Memory', relativePath: 'projects.md' });
432
- scopes.push(project);
433
- }
434
- for (const construct of Array.isArray(contract?.constructs) ? contract.constructs : []) {
435
- const scope = constructScope(construct);
436
- if (!scope) continue;
437
- const folder = CONSTRUCT_FOLDERS[scope.scopeType];
438
- if (folder) {
439
- scopes.push({
440
- scopeType: folder,
441
- title: `${scope.scopeType.charAt(0).toUpperCase()}${scope.scopeType.slice(1)} Active Memory`,
442
- relativePath: `${folder}.md`,
443
- });
444
- }
445
- scopes.push(scope);
446
- }
447
- const originMemory = originScope(origin);
448
- if (originMemory) {
449
- scopes.push({
450
- scopeType: origin.type === 'task' ? 'tasks' : 'events',
451
- title: origin.type === 'task' ? 'Task Active Memory' : 'Event Active Memory',
452
- relativePath: origin.type === 'task' ? 'tasks.md' : 'events.md',
453
- });
454
- scopes.push(originMemory);
455
- }
456
- const seen = new Set();
457
- return scopes.filter((scope) => {
458
- const key = scope.filePath || scope.relativePath;
459
- if (seen.has(key)) return false;
460
- seen.add(key);
461
- return true;
462
- });
463
- }
464
-
465
- function escapeAttribute(value) {
466
- return String(value || '')
467
- .replace(/&/g, '&amp;')
468
- .replace(/"/g, '&quot;')
469
- .replace(/</g, '&lt;')
470
- .replace(/>/g, '&gt;');
471
- }
472
-
473
- function activeMemoryContextBlock(contract) {
474
- const scopes = scopesForContract(contract);
475
- const files = scopes
476
- .map((scope) => ensureScopedFile(scope, {
477
- includeContent: true,
478
- publish: false,
479
- }))
480
- .filter(Boolean);
481
- if (files.some((file) => file.created)) {
482
- publishMemoriesChange('active-memory:prompt');
483
- }
484
- if (files.length === 0) return '';
485
-
486
- const lines = [
487
- '<active_memory>',
488
- '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.',
489
- 'When you learn durable facts, decisions, preferences, or stale information, update the most specific relevant file listed here. Use task/event/app/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.',
490
- '',
491
- ];
492
- for (const file of files) {
493
- lines.push(`<active_memory_file path="${escapeAttribute(file.path)}" scope="${escapeAttribute(file.scopeType || '')}" id="${escapeAttribute(file.scopeId || '')}">`);
494
- lines.push(file.content || '');
495
- lines.push('</active_memory_file>');
496
- lines.push('');
497
- }
498
- lines.push('</active_memory>');
499
- return lines.join('\n');
500
- }
501
-
502
- function instructionFilesForContract(contract) {
503
- ensureSystemContext({ publish: false });
504
- const files = [
505
- {
506
- scopeType: 'system',
507
- scopeId: 'system',
508
- path: path.join(systemContextRoot(), INSTRUCTIONS_FILENAME),
509
- title: 'Amalgm System Instructions',
510
- },
511
- ];
512
- const origin = originFromContract(contract);
513
- const projectPath = cleanString(origin?.projectPath || contract?.projectPath || contract?.cwd);
514
- const project = projectScope(projectPath);
515
- if (project?.projectPath) {
516
- ensureProjectContext(project, { publish: false });
517
- files.push({
518
- scopeType: 'project',
519
- scopeId: project.scopeId,
520
- path: path.join(projectContextRoot(project.projectPath), INSTRUCTIONS_FILENAME),
521
- title: `${project.name || 'Project'} Instructions`,
522
- });
523
- }
524
- return files;
525
- }
526
-
527
- function isEmptyInstructions(content, title) {
528
- const body = String(content || '')
529
- .replace(/^#\s*Instructions\s*/i, '')
530
- .trim();
531
- if (!body) return true;
532
- const expected = `No instructions for ${title} yet.`;
533
- return body === expected;
534
- }
535
-
536
- function instructionsContextBlock(contract) {
537
- const files = instructionFilesForContract(contract)
538
- .map((item) => ({
539
- ...item,
540
- content: readFile(item.path),
541
- }))
542
- .filter((item) => item.content && !isEmptyInstructions(item.content, item.title.replace(/\s+Instructions$/, '')));
543
-
544
- if (files.length === 0) return '';
545
-
546
- const lines = [
547
- '<project_instructions>',
548
- 'These are user-authored markdown instruction files. Treat them as durable user instructions for their scope. Current user requests and higher-priority system/developer instructions win on conflict.',
549
- '',
550
- ];
551
- for (const file of files) {
552
- lines.push(`<instructions_file path="${escapeAttribute(file.path)}" scope="${escapeAttribute(file.scopeType)}" id="${escapeAttribute(file.scopeId || '')}">`);
553
- lines.push(file.content);
554
- lines.push('</instructions_file>');
555
- lines.push('');
556
- }
557
- lines.push('</project_instructions>');
558
- return lines.join('\n');
559
- }
560
-
561
- module.exports = {
562
- activeMemoryContextBlock,
563
- activeMemoryRoot,
564
- collectActiveMemoryFiles,
565
- ensureActiveMemoryLibrary,
566
- ensureConstructMemory,
567
- ensureProjectContext,
568
- ensureSystemContext,
569
- instructionsContextBlock,
570
- isActiveMemoryPath,
571
- notifyMemoryPathChanged,
572
- publishMemoriesChange,
573
- scopesForContract,
574
- };