@yeaft/webchat-agent 0.1.409 → 0.1.411
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 +1 -1
- package/unify/config.js +36 -0
- package/unify/engine.js +124 -16
- package/unify/index.js +14 -1
- package/unify/mcp.js +433 -0
- package/unify/memory/dream-prompt.js +272 -0
- package/unify/memory/dream.js +468 -0
- package/unify/memory/scan.js +273 -0
- package/unify/memory/types.js +139 -0
- package/unify/prompts.js +6 -0
- package/unify/session.js +191 -0
- package/unify/skills.js +315 -0
- package/unify/stop-hooks.js +146 -0
- package/unify/tools/enter-worktree.js +97 -0
- package/unify/tools/exit-worktree.js +131 -0
- package/unify/tools/mcp-tools.js +133 -0
- package/unify/tools/registry.js +146 -0
- package/unify/tools/skill.js +107 -0
- package/unify/tools/types.js +71 -0
package/unify/mcp.js
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp.js — MCP (Model Context Protocol) client manager
|
|
3
|
+
*
|
|
4
|
+
* Manages connections to MCP servers and provides a bridge
|
|
5
|
+
* for the mcp_list_tools and mcp_call_tool tools.
|
|
6
|
+
*
|
|
7
|
+
* MCP servers are configured in ~/.yeaft/config.md:
|
|
8
|
+
* ---
|
|
9
|
+
* mcp_servers:
|
|
10
|
+
* - name: github
|
|
11
|
+
* command: npx @mcp/github
|
|
12
|
+
* args: []
|
|
13
|
+
* env:
|
|
14
|
+
* GITHUB_TOKEN: ghp_...
|
|
15
|
+
* - name: slack
|
|
16
|
+
* command: npx @mcp/slack
|
|
17
|
+
* args: []
|
|
18
|
+
* ---
|
|
19
|
+
*
|
|
20
|
+
* Each MCP server communicates via stdio JSON-RPC (MCP protocol).
|
|
21
|
+
*
|
|
22
|
+
* Reference: yeaft-unify-design.md §8, yeaft-unify-core-systems.md §3.2
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { spawn } from 'child_process';
|
|
26
|
+
import { randomUUID } from 'crypto';
|
|
27
|
+
import { EventEmitter } from 'events';
|
|
28
|
+
|
|
29
|
+
// ─── Constants ──────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** Default tool call timeout (30 seconds). */
|
|
32
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
33
|
+
|
|
34
|
+
/** Server startup timeout (10 seconds). */
|
|
35
|
+
const STARTUP_TIMEOUT_MS = 10000;
|
|
36
|
+
|
|
37
|
+
// ─── MCP Server Connection ─────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A single MCP server connection.
|
|
41
|
+
* Communicates via stdio JSON-RPC.
|
|
42
|
+
*/
|
|
43
|
+
class MCPServerConnection extends EventEmitter {
|
|
44
|
+
#name;
|
|
45
|
+
#process;
|
|
46
|
+
#pendingRequests;
|
|
47
|
+
#tools;
|
|
48
|
+
#ready;
|
|
49
|
+
#buffer;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} name — server name (e.g. "github")
|
|
53
|
+
* @param {{ command: string, args?: string[], env?: object }} config
|
|
54
|
+
*/
|
|
55
|
+
constructor(name, config) {
|
|
56
|
+
super();
|
|
57
|
+
this.#name = name;
|
|
58
|
+
this.#process = null;
|
|
59
|
+
this.#pendingRequests = new Map();
|
|
60
|
+
this.#tools = [];
|
|
61
|
+
this.#ready = false;
|
|
62
|
+
this.#buffer = '';
|
|
63
|
+
this.config = config;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get name() { return this.#name; }
|
|
67
|
+
get tools() { return this.#tools; }
|
|
68
|
+
get ready() { return this.#ready; }
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Start the MCP server process and initialize.
|
|
72
|
+
* @returns {Promise<void>}
|
|
73
|
+
*/
|
|
74
|
+
async start() {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const timer = setTimeout(() => {
|
|
77
|
+
reject(new Error(`MCP server "${this.#name}" startup timeout (${STARTUP_TIMEOUT_MS}ms)`));
|
|
78
|
+
}, STARTUP_TIMEOUT_MS);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
this.#process = spawn(this.config.command, this.config.args || [], {
|
|
82
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
83
|
+
env: { ...process.env, ...this.config.env },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
this.#process.stdout.on('data', (data) => {
|
|
87
|
+
this.#buffer += data.toString();
|
|
88
|
+
this.#processBuffer();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
this.#process.stderr.on('data', (data) => {
|
|
92
|
+
this.emit('error', new Error(`[${this.#name}] stderr: ${data.toString().trim()}`));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
this.#process.on('close', (code) => {
|
|
96
|
+
this.#ready = false;
|
|
97
|
+
this.emit('close', code);
|
|
98
|
+
// Reject all pending requests
|
|
99
|
+
for (const [, { reject: rej }] of this.#pendingRequests) {
|
|
100
|
+
rej(new Error(`MCP server "${this.#name}" closed with code ${code}`));
|
|
101
|
+
}
|
|
102
|
+
this.#pendingRequests.clear();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
this.#process.on('error', (err) => {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
reject(err);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Initialize MCP protocol
|
|
111
|
+
this.#initialize().then(() => {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
this.#ready = true;
|
|
114
|
+
resolve();
|
|
115
|
+
}).catch((err) => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
reject(err);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
} catch (err) {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
reject(err);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Send the MCP initialize request and list tools.
|
|
129
|
+
*/
|
|
130
|
+
async #initialize() {
|
|
131
|
+
// Send initialize
|
|
132
|
+
await this.#rpcCall('initialize', {
|
|
133
|
+
protocolVersion: '2024-11-05',
|
|
134
|
+
capabilities: {},
|
|
135
|
+
clientInfo: { name: 'yeaft', version: '0.1.0' },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Send initialized notification
|
|
139
|
+
this.#rpcNotify('notifications/initialized', {});
|
|
140
|
+
|
|
141
|
+
// List tools
|
|
142
|
+
const result = await this.#rpcCall('tools/list', {});
|
|
143
|
+
this.#tools = (result?.tools || []).map(tool => ({
|
|
144
|
+
name: `${this.#name}__${tool.name}`,
|
|
145
|
+
serverName: this.#name,
|
|
146
|
+
originalName: tool.name,
|
|
147
|
+
description: tool.description || '',
|
|
148
|
+
inputSchema: tool.inputSchema || { type: 'object', properties: {} },
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Call a tool on this MCP server.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} toolName — original tool name (without server prefix)
|
|
156
|
+
* @param {object} args — tool arguments
|
|
157
|
+
* @param {number} [timeoutMs] — timeout in milliseconds
|
|
158
|
+
* @returns {Promise<object>}
|
|
159
|
+
*/
|
|
160
|
+
async callTool(toolName, args = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
161
|
+
const result = await this.#rpcCall('tools/call', {
|
|
162
|
+
name: toolName,
|
|
163
|
+
arguments: args,
|
|
164
|
+
}, timeoutMs);
|
|
165
|
+
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Send a JSON-RPC request and wait for response.
|
|
171
|
+
*/
|
|
172
|
+
async #rpcCall(method, params, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
173
|
+
const id = randomUUID();
|
|
174
|
+
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
const timer = setTimeout(() => {
|
|
177
|
+
this.#pendingRequests.delete(id);
|
|
178
|
+
reject(new Error(`RPC call "${method}" to "${this.#name}" timed out (${timeoutMs}ms)`));
|
|
179
|
+
}, timeoutMs);
|
|
180
|
+
|
|
181
|
+
this.#pendingRequests.set(id, {
|
|
182
|
+
resolve: (result) => {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
this.#pendingRequests.delete(id);
|
|
185
|
+
resolve(result);
|
|
186
|
+
},
|
|
187
|
+
reject: (err) => {
|
|
188
|
+
clearTimeout(timer);
|
|
189
|
+
this.#pendingRequests.delete(id);
|
|
190
|
+
reject(err);
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const message = JSON.stringify({ jsonrpc: '2.0', id, method, params });
|
|
195
|
+
this.#process.stdin.write(message + '\n');
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Send a JSON-RPC notification (no response expected).
|
|
201
|
+
*/
|
|
202
|
+
#rpcNotify(method, params) {
|
|
203
|
+
const message = JSON.stringify({ jsonrpc: '2.0', method, params });
|
|
204
|
+
this.#process.stdin.write(message + '\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Process the stdout buffer for complete JSON-RPC messages.
|
|
209
|
+
*/
|
|
210
|
+
#processBuffer() {
|
|
211
|
+
const lines = this.#buffer.split('\n');
|
|
212
|
+
this.#buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
213
|
+
|
|
214
|
+
for (const line of lines) {
|
|
215
|
+
const trimmed = line.trim();
|
|
216
|
+
if (!trimmed) continue;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const msg = JSON.parse(trimmed);
|
|
220
|
+
|
|
221
|
+
if (msg.id && this.#pendingRequests.has(msg.id)) {
|
|
222
|
+
const pending = this.#pendingRequests.get(msg.id);
|
|
223
|
+
if (msg.error) {
|
|
224
|
+
pending.reject(new Error(`MCP error: ${msg.error.message || JSON.stringify(msg.error)}`));
|
|
225
|
+
} else {
|
|
226
|
+
pending.resolve(msg.result);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Handle notifications from server
|
|
231
|
+
if (!msg.id && msg.method) {
|
|
232
|
+
this.emit('notification', msg);
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// Not valid JSON — ignore (could be debug output)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Stop the MCP server process.
|
|
242
|
+
*/
|
|
243
|
+
async stop() {
|
|
244
|
+
if (this.#process) {
|
|
245
|
+
this.#ready = false;
|
|
246
|
+
this.#process.kill('SIGTERM');
|
|
247
|
+
// Wait a bit then force kill
|
|
248
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
249
|
+
if (this.#process && !this.#process.killed) {
|
|
250
|
+
this.#process.kill('SIGKILL');
|
|
251
|
+
}
|
|
252
|
+
this.#process = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ─── MCP Manager ────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* MCPManager — manages multiple MCP server connections.
|
|
261
|
+
*/
|
|
262
|
+
export class MCPManager {
|
|
263
|
+
/** @type {Map<string, MCPServerConnection>} */
|
|
264
|
+
#servers = new Map();
|
|
265
|
+
|
|
266
|
+
/** @type {Map<string, object>} */
|
|
267
|
+
#toolIndex = new Map(); // fullToolName → { server, tool }
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Connect to an MCP server.
|
|
271
|
+
*
|
|
272
|
+
* @param {{ name: string, command: string, args?: string[], env?: object }} serverConfig
|
|
273
|
+
* @returns {Promise<{ name: string, toolCount: number }>}
|
|
274
|
+
*/
|
|
275
|
+
async connect(serverConfig) {
|
|
276
|
+
const { name } = serverConfig;
|
|
277
|
+
|
|
278
|
+
// Disconnect existing if any
|
|
279
|
+
if (this.#servers.has(name)) {
|
|
280
|
+
await this.disconnect(name);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const connection = new MCPServerConnection(name, serverConfig);
|
|
284
|
+
await connection.start();
|
|
285
|
+
|
|
286
|
+
this.#servers.set(name, connection);
|
|
287
|
+
|
|
288
|
+
// Index tools
|
|
289
|
+
for (const tool of connection.tools) {
|
|
290
|
+
this.#toolIndex.set(tool.name, { server: connection, tool });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { name, toolCount: connection.tools.length };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Connect to all servers from config.
|
|
298
|
+
*
|
|
299
|
+
* @param {object[]} serverConfigs — array of server configurations
|
|
300
|
+
* @returns {Promise<{ connected: string[], failed: { name: string, error: string }[] }>}
|
|
301
|
+
*/
|
|
302
|
+
async connectAll(serverConfigs) {
|
|
303
|
+
const connected = [];
|
|
304
|
+
const failed = [];
|
|
305
|
+
|
|
306
|
+
for (const config of serverConfigs) {
|
|
307
|
+
try {
|
|
308
|
+
await this.connect(config);
|
|
309
|
+
connected.push(config.name);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
failed.push({ name: config.name, error: err.message });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { connected, failed };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Disconnect from a specific MCP server.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} name — server name
|
|
322
|
+
*/
|
|
323
|
+
async disconnect(name) {
|
|
324
|
+
const connection = this.#servers.get(name);
|
|
325
|
+
if (connection) {
|
|
326
|
+
// Remove tools from index
|
|
327
|
+
for (const tool of connection.tools) {
|
|
328
|
+
this.#toolIndex.delete(tool.name);
|
|
329
|
+
}
|
|
330
|
+
await connection.stop();
|
|
331
|
+
this.#servers.delete(name);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Disconnect from all servers.
|
|
337
|
+
*/
|
|
338
|
+
async disconnectAll() {
|
|
339
|
+
const names = [...this.#servers.keys()];
|
|
340
|
+
for (const name of names) {
|
|
341
|
+
await this.disconnect(name);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* List all available tools from all connected servers.
|
|
347
|
+
*
|
|
348
|
+
* @param {string} [serverFilter] — optionally filter by server name
|
|
349
|
+
* @returns {object[]}
|
|
350
|
+
*/
|
|
351
|
+
listTools(serverFilter) {
|
|
352
|
+
const tools = [];
|
|
353
|
+
|
|
354
|
+
for (const [, { server, tool }] of this.#toolIndex) {
|
|
355
|
+
if (serverFilter && server.name !== serverFilter) continue;
|
|
356
|
+
tools.push({
|
|
357
|
+
name: tool.name,
|
|
358
|
+
server: server.name,
|
|
359
|
+
description: tool.description,
|
|
360
|
+
inputSchema: tool.inputSchema,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return tools;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Call a tool by its full name (server__toolName).
|
|
369
|
+
*
|
|
370
|
+
* @param {string} fullToolName — e.g. "github__list_prs"
|
|
371
|
+
* @param {object} [args={}] — tool arguments
|
|
372
|
+
* @param {number} [timeoutMs] — timeout
|
|
373
|
+
* @returns {Promise<object>}
|
|
374
|
+
*/
|
|
375
|
+
async callTool(fullToolName, args = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
376
|
+
const entry = this.#toolIndex.get(fullToolName);
|
|
377
|
+
if (!entry) {
|
|
378
|
+
throw new Error(`MCP tool "${fullToolName}" not found. Available: ${[...this.#toolIndex.keys()].join(', ') || '(none)'}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const { server, tool } = entry;
|
|
382
|
+
if (!server.ready) {
|
|
383
|
+
throw new Error(`MCP server "${server.name}" is not ready`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return server.callTool(tool.originalName, args, timeoutMs);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get the status of all servers.
|
|
391
|
+
*
|
|
392
|
+
* @returns {{ name: string, ready: boolean, toolCount: number }[]}
|
|
393
|
+
*/
|
|
394
|
+
status() {
|
|
395
|
+
return [...this.#servers.entries()].map(([name, conn]) => ({
|
|
396
|
+
name,
|
|
397
|
+
ready: conn.ready,
|
|
398
|
+
toolCount: conn.tools.length,
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Check if any servers are connected.
|
|
404
|
+
* @returns {boolean}
|
|
405
|
+
*/
|
|
406
|
+
get hasServers() {
|
|
407
|
+
return this.#servers.size > 0;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Total tool count across all servers.
|
|
412
|
+
* @returns {number}
|
|
413
|
+
*/
|
|
414
|
+
get toolCount() {
|
|
415
|
+
return this.#toolIndex.size;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Create an MCPManager and optionally connect to servers from config.
|
|
421
|
+
*
|
|
422
|
+
* @param {object} [config] — { mcp_servers: [...] }
|
|
423
|
+
* @returns {Promise<MCPManager>}
|
|
424
|
+
*/
|
|
425
|
+
export async function createMCPManager(config) {
|
|
426
|
+
const manager = new MCPManager();
|
|
427
|
+
|
|
428
|
+
if (config?.mcp_servers && Array.isArray(config.mcp_servers)) {
|
|
429
|
+
await manager.connectAll(config.mcp_servers);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return manager;
|
|
433
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dream-prompt.js — Dream prompt templates for each phase
|
|
3
|
+
*
|
|
4
|
+
* Dream has 5 phases:
|
|
5
|
+
* Phase 1: Orient — assess current memory state
|
|
6
|
+
* Phase 2: Gather — collect recent context
|
|
7
|
+
* Phase 3: Merge — combine duplicates, update outdated
|
|
8
|
+
* Phase 4: Prune — remove stale/low-value entries
|
|
9
|
+
* Phase 5: Promote — extract patterns, update profile
|
|
10
|
+
*
|
|
11
|
+
* Reference: yeaft-unify-core-systems.md §3.3
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build the Orient phase prompt (Phase 1).
|
|
16
|
+
* The LLM assesses the current memory state and identifies issues.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ memorySummary: string, profileContent: string, entryCount: number }} context
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
export function buildOrientPrompt({ memorySummary, profileContent, entryCount }) {
|
|
22
|
+
return `You are in Dream Mode — Phase 1: Orient.
|
|
23
|
+
|
|
24
|
+
Your task is to assess the current state of the memory store and identify what needs attention.
|
|
25
|
+
|
|
26
|
+
## Current Memory State
|
|
27
|
+
|
|
28
|
+
${memorySummary}
|
|
29
|
+
|
|
30
|
+
## MEMORY.md (User Profile)
|
|
31
|
+
|
|
32
|
+
${profileContent || '(empty)'}
|
|
33
|
+
|
|
34
|
+
## Assessment Instructions
|
|
35
|
+
|
|
36
|
+
Review the memory state and provide:
|
|
37
|
+
1. **Redundancies**: Are there entries that overlap or say the same thing?
|
|
38
|
+
2. **Outdated info**: Are there entries that might be stale or no longer relevant?
|
|
39
|
+
3. **Gaps**: Is there important context missing from MEMORY.md?
|
|
40
|
+
4. **Quality**: Are entries well-categorized (kind, scope, tags)?
|
|
41
|
+
|
|
42
|
+
Return your assessment as JSON:
|
|
43
|
+
{
|
|
44
|
+
"redundantGroups": [["entry-a", "entry-b"]],
|
|
45
|
+
"potentiallyStale": ["entry-name-1"],
|
|
46
|
+
"profileGaps": ["missing X context"],
|
|
47
|
+
"qualityIssues": ["entry-y has wrong kind"],
|
|
48
|
+
"overallHealth": "good" | "needs-attention" | "poor",
|
|
49
|
+
"suggestedActions": ["merge entries about X", "prune stale context entries"]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Return ONLY valid JSON, no other text.`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the Gather phase prompt (Phase 2).
|
|
57
|
+
* Collects recent compact summaries and completed task summaries.
|
|
58
|
+
*
|
|
59
|
+
* @param {{ recentCompact: string, completedTasks: object[], orientResult: object }} context
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function buildGatherPrompt({ recentCompact, completedTasks, orientResult }) {
|
|
63
|
+
const taskSummaries = completedTasks.length > 0
|
|
64
|
+
? completedTasks.map(t => `- [${t.id}] ${t.description}: ${t.summary || '(no summary)'}`).join('\n')
|
|
65
|
+
: '(no recently completed tasks)';
|
|
66
|
+
|
|
67
|
+
return `You are in Dream Mode — Phase 2: Gather.
|
|
68
|
+
|
|
69
|
+
Your task is to identify what new information should be incorporated into long-term memory.
|
|
70
|
+
|
|
71
|
+
## Recent Conversation Summary (compact.md)
|
|
72
|
+
|
|
73
|
+
${recentCompact || '(no recent summaries)'}
|
|
74
|
+
|
|
75
|
+
## Recently Completed Tasks
|
|
76
|
+
|
|
77
|
+
${taskSummaries}
|
|
78
|
+
|
|
79
|
+
## Orient Assessment
|
|
80
|
+
|
|
81
|
+
${JSON.stringify(orientResult, null, 2)}
|
|
82
|
+
|
|
83
|
+
## Instructions
|
|
84
|
+
|
|
85
|
+
From the recent conversations and tasks, identify:
|
|
86
|
+
1. **New facts** worth remembering (project structure, tech decisions)
|
|
87
|
+
2. **New preferences** expressed by the user
|
|
88
|
+
3. **New skills/lessons** learned during tasks
|
|
89
|
+
4. **Context updates** (project progress, status changes)
|
|
90
|
+
|
|
91
|
+
Return as JSON:
|
|
92
|
+
{
|
|
93
|
+
"newEntries": [
|
|
94
|
+
{ "name": "slug-name", "kind": "fact|preference|skill|lesson|context|relation", "scope": "path", "tags": ["tag1", "tag2"], "importance": "high|normal|low", "content": "description" }
|
|
95
|
+
],
|
|
96
|
+
"updatesToExisting": [
|
|
97
|
+
{ "entryName": "existing-slug", "updates": { "content": "updated text", "tags": ["new-tag"] } }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Return ONLY valid JSON, no other text.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build the Merge phase prompt (Phase 3).
|
|
106
|
+
*
|
|
107
|
+
* @param {{ duplicateGroups: object[][], gatherResult: object }} context
|
|
108
|
+
* @returns {string}
|
|
109
|
+
*/
|
|
110
|
+
export function buildMergePrompt({ duplicateGroups, gatherResult }) {
|
|
111
|
+
const groupDescriptions = duplicateGroups.map((group, i) => {
|
|
112
|
+
const entries = group.map(e =>
|
|
113
|
+
` - [${e.name}] kind=${e.kind}, scope=${e.scope}, tags=[${(e.tags || []).join(', ')}]\n ${(e.content || '').slice(0, 200)}`
|
|
114
|
+
).join('\n');
|
|
115
|
+
return `Group ${i + 1}:\n${entries}`;
|
|
116
|
+
}).join('\n\n');
|
|
117
|
+
|
|
118
|
+
return `You are in Dream Mode — Phase 3: Merge.
|
|
119
|
+
|
|
120
|
+
Your task is to merge duplicate/overlapping entries into single, richer entries.
|
|
121
|
+
|
|
122
|
+
## Potentially Duplicate Groups
|
|
123
|
+
|
|
124
|
+
${groupDescriptions || '(no duplicates detected)'}
|
|
125
|
+
|
|
126
|
+
## New Entries from Gather Phase
|
|
127
|
+
|
|
128
|
+
${JSON.stringify(gatherResult?.newEntries || [], null, 2)}
|
|
129
|
+
|
|
130
|
+
## Instructions
|
|
131
|
+
|
|
132
|
+
For each duplicate group:
|
|
133
|
+
1. Decide if they should be merged (combine info) or kept separate (different enough)
|
|
134
|
+
2. For merges, create a single entry that preserves all important info from both
|
|
135
|
+
3. List which old entries should be deleted after merge
|
|
136
|
+
|
|
137
|
+
Also process the new entries from Gather — check if any overlap with existing entries.
|
|
138
|
+
|
|
139
|
+
Return as JSON:
|
|
140
|
+
{
|
|
141
|
+
"merges": [
|
|
142
|
+
{
|
|
143
|
+
"merged": { "name": "new-slug", "kind": "...", "scope": "...", "tags": [], "importance": "...", "content": "..." },
|
|
144
|
+
"deleteOriginals": ["old-entry-1", "old-entry-2"]
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"newEntries": [
|
|
148
|
+
{ "name": "...", "kind": "...", "scope": "...", "tags": [], "importance": "...", "content": "..." }
|
|
149
|
+
],
|
|
150
|
+
"updates": [
|
|
151
|
+
{ "entryName": "existing-slug", "updates": { "content": "updated text" } }
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
Return ONLY valid JSON, no other text.`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Build the Prune phase prompt (Phase 4).
|
|
160
|
+
*
|
|
161
|
+
* @param {{ staleEntries: object[], entryCount: number, maxEntries: number }} context
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
export function buildPrunePrompt({ staleEntries, entryCount, maxEntries }) {
|
|
165
|
+
const staleDescriptions = staleEntries.map(e =>
|
|
166
|
+
`- [${e.name}] kind=${e.kind}, scope=${e.scope}, freq=${e.frequency || 1}, days_since_update=${e._daysSinceUpdate}\n ${(e.content || '').slice(0, 150)}`
|
|
167
|
+
).join('\n');
|
|
168
|
+
|
|
169
|
+
return `You are in Dream Mode — Phase 4: Prune.
|
|
170
|
+
|
|
171
|
+
Your task is to remove stale, low-value, or redundant entries.
|
|
172
|
+
|
|
173
|
+
## Potentially Stale Entries (${staleEntries.length} found)
|
|
174
|
+
|
|
175
|
+
${staleDescriptions || '(none detected)'}
|
|
176
|
+
|
|
177
|
+
## Capacity
|
|
178
|
+
|
|
179
|
+
Current entries: ${entryCount}
|
|
180
|
+
Maximum allowed: ${maxEntries}
|
|
181
|
+
${entryCount > maxEntries ? `⚠️ OVER CAPACITY by ${entryCount - maxEntries} entries — must prune aggressively` : 'Within capacity'}
|
|
182
|
+
|
|
183
|
+
## Prune Guidelines
|
|
184
|
+
|
|
185
|
+
Delete entries that are:
|
|
186
|
+
- **Outdated context**: Project status from weeks ago
|
|
187
|
+
- **Never recalled**: frequency=1 and old — nobody needs it
|
|
188
|
+
- **Too vague**: "user mentioned something about X" without useful detail
|
|
189
|
+
- **Redundant with profile**: If MEMORY.md already captures it
|
|
190
|
+
- **Re-derivable**: Info that can be obtained by running a command (e.g., "Node version is 20")
|
|
191
|
+
|
|
192
|
+
KEEP entries that are:
|
|
193
|
+
- High importance or high frequency
|
|
194
|
+
- Recent preferences or lessons
|
|
195
|
+
- Facts about project structure (hard to re-discover)
|
|
196
|
+
|
|
197
|
+
Return as JSON:
|
|
198
|
+
{
|
|
199
|
+
"toDelete": ["entry-name-1", "entry-name-2"],
|
|
200
|
+
"reasoning": {
|
|
201
|
+
"entry-name-1": "outdated context from 45 days ago",
|
|
202
|
+
"entry-name-2": "never recalled, too vague"
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Return ONLY valid JSON, no other text.`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build the Promote phase prompt (Phase 5).
|
|
211
|
+
*
|
|
212
|
+
* @param {{ entries: object[], profileContent: string, scopesSummary: string }} context
|
|
213
|
+
* @returns {string}
|
|
214
|
+
*/
|
|
215
|
+
export function buildPromotePrompt({ entries, profileContent, scopesSummary }) {
|
|
216
|
+
// Find entries that might form patterns
|
|
217
|
+
const highFreq = entries
|
|
218
|
+
.filter(e => (e.frequency || 1) >= 3)
|
|
219
|
+
.map(e => `- [${e.name}] kind=${e.kind}, freq=${e.frequency}, scope=${e.scope}: ${(e.content || '').slice(0, 150)}`)
|
|
220
|
+
.join('\n');
|
|
221
|
+
|
|
222
|
+
const lessons = entries
|
|
223
|
+
.filter(e => e.kind === 'lesson')
|
|
224
|
+
.map(e => `- [${e.name}] scope=${e.scope}: ${(e.content || '').slice(0, 150)}`)
|
|
225
|
+
.join('\n');
|
|
226
|
+
|
|
227
|
+
return `You are in Dream Mode — Phase 5: Promote.
|
|
228
|
+
|
|
229
|
+
Your task is to identify patterns and update the user profile.
|
|
230
|
+
|
|
231
|
+
## High-Frequency Entries (recalled ≥3 times)
|
|
232
|
+
|
|
233
|
+
${highFreq || '(none)'}
|
|
234
|
+
|
|
235
|
+
## All Lessons
|
|
236
|
+
|
|
237
|
+
${lessons || '(none)'}
|
|
238
|
+
|
|
239
|
+
## Current MEMORY.md Profile
|
|
240
|
+
|
|
241
|
+
${profileContent || '(empty)'}
|
|
242
|
+
|
|
243
|
+
## Scopes
|
|
244
|
+
|
|
245
|
+
${scopesSummary}
|
|
246
|
+
|
|
247
|
+
## Instructions
|
|
248
|
+
|
|
249
|
+
1. **Pattern promotion**: If multiple entries share a pattern, create a higher-level insight
|
|
250
|
+
- Example: 3 entries about "user corrects indentation" → 1 preference: "default to 2-space indent"
|
|
251
|
+
2. **Profile update**: Update MEMORY.md sections based on accumulated knowledge
|
|
252
|
+
- Keep MEMORY.md under 200 lines
|
|
253
|
+
- Sections: Facts, Preferences, Project Context, Skills, Lessons
|
|
254
|
+
3. **Scope promotion**: If a lesson applies across projects, promote scope to parent or global
|
|
255
|
+
|
|
256
|
+
Return as JSON:
|
|
257
|
+
{
|
|
258
|
+
"profileUpdates": {
|
|
259
|
+
"Facts": ["- New fact line 1"],
|
|
260
|
+
"Preferences": ["- New preference line"],
|
|
261
|
+
"Project Context": [],
|
|
262
|
+
"Skills": [],
|
|
263
|
+
"Lessons": []
|
|
264
|
+
},
|
|
265
|
+
"promotedEntries": [
|
|
266
|
+
{ "name": "...", "kind": "...", "scope": "global", "tags": [], "importance": "high", "content": "..." }
|
|
267
|
+
],
|
|
268
|
+
"entriesToDelete": ["entry-that-was-promoted-to-profile"]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
Return ONLY valid JSON, no other text.`;
|
|
272
|
+
}
|