@vemdev/mcp-server 0.1.1
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 +21 -0
- package/README.md +31 -0
- package/dist/chunk-52K33N7B.js +183 -0
- package/dist/chunk-52K33N7B.js.map +1 -0
- package/dist/chunk-CSA73CBK.js +181 -0
- package/dist/chunk-CSA73CBK.js.map +1 -0
- package/dist/chunk-WYWBLQNM.js +170 -0
- package/dist/chunk-WYWBLQNM.js.map +1 -0
- package/dist/claude-sessions-5HEECZ63.js +11 -0
- package/dist/claude-sessions-5HEECZ63.js.map +1 -0
- package/dist/copilot-sessions-LLDNCHIU.js +13 -0
- package/dist/copilot-sessions-LLDNCHIU.js.map +1 -0
- package/dist/gemini-sessions-RPV25JO4.js +11 -0
- package/dist/gemini-sessions-RPV25JO4.js.map +1 -0
- package/dist/index.js +3744 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3744 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-52K33N7B.js";
|
|
3
|
+
import "./chunk-CSA73CBK.js";
|
|
4
|
+
import "./chunk-WYWBLQNM.js";
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
import { execSync as execSync2 } from "child_process";
|
|
8
|
+
import { createHash as createHash4 } from "crypto";
|
|
9
|
+
import { readdir, readFile, readlink, writeFile } from "fs/promises";
|
|
10
|
+
import { join as join2, relative } from "path";
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import {
|
|
14
|
+
CallToolRequestSchema,
|
|
15
|
+
ListToolsRequestSchema
|
|
16
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
|
|
18
|
+
// ../../packages/core/dist/agent.js
|
|
19
|
+
import path5 from "path";
|
|
20
|
+
|
|
21
|
+
// ../../packages/schemas/dist/index.js
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
var RelatedDecisionRefSchema = z.union([
|
|
24
|
+
z.string(),
|
|
25
|
+
z.object({
|
|
26
|
+
id: z.string(),
|
|
27
|
+
title: z.string().optional(),
|
|
28
|
+
content: z.string().optional()
|
|
29
|
+
})
|
|
30
|
+
]);
|
|
31
|
+
var AgentSessionStatsSchema = z.object({
|
|
32
|
+
ended_at: z.string().datetime().optional(),
|
|
33
|
+
session_duration_ms: z.number().int().optional(),
|
|
34
|
+
turn_count: z.number().int().optional(),
|
|
35
|
+
tool_call_count: z.number().int().optional(),
|
|
36
|
+
model_breakdown: z.record(z.string(), z.number().int()).optional()
|
|
37
|
+
});
|
|
38
|
+
var TaskSessionRefSchema = z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
source: z.enum(["copilot", "claude", "gemini"]),
|
|
41
|
+
started_at: z.string().datetime(),
|
|
42
|
+
summary: z.string().optional(),
|
|
43
|
+
stats: AgentSessionStatsSchema.optional()
|
|
44
|
+
});
|
|
45
|
+
var TaskActionSchema = z.object({
|
|
46
|
+
type: z.enum([
|
|
47
|
+
"create",
|
|
48
|
+
"update_status",
|
|
49
|
+
"update_priority",
|
|
50
|
+
"update_tags",
|
|
51
|
+
"update_type",
|
|
52
|
+
"update_estimate",
|
|
53
|
+
"update_dependencies",
|
|
54
|
+
"update_recurrence",
|
|
55
|
+
"update_owner",
|
|
56
|
+
"update_reviewer",
|
|
57
|
+
"delete",
|
|
58
|
+
"comment",
|
|
59
|
+
"completion"
|
|
60
|
+
]),
|
|
61
|
+
reasoning: z.string().nullable().optional(),
|
|
62
|
+
actor: z.string().nullable().optional(),
|
|
63
|
+
created_at: z.string().datetime()
|
|
64
|
+
});
|
|
65
|
+
var TaskStatusSchema = z.enum([
|
|
66
|
+
"todo",
|
|
67
|
+
"in-progress",
|
|
68
|
+
"blocked",
|
|
69
|
+
"done"
|
|
70
|
+
]);
|
|
71
|
+
var TaskPrioritySchema = z.enum(["low", "medium", "high", "critical"]);
|
|
72
|
+
var TaskTypeSchema = z.enum(["feature", "bug", "chore"]);
|
|
73
|
+
var TaskSchema = z.object({
|
|
74
|
+
id: z.string(),
|
|
75
|
+
title: z.string(),
|
|
76
|
+
status: TaskStatusSchema,
|
|
77
|
+
assignee: z.string().optional(),
|
|
78
|
+
priority: TaskPrioritySchema.optional(),
|
|
79
|
+
tags: z.array(z.string()).optional(),
|
|
80
|
+
type: TaskTypeSchema.optional(),
|
|
81
|
+
estimate_hours: z.number().optional(),
|
|
82
|
+
depends_on: z.array(z.string()).optional(),
|
|
83
|
+
blocked_by: z.array(z.string()).optional(),
|
|
84
|
+
recurrence_rule: z.string().optional(),
|
|
85
|
+
owner_id: z.string().optional(),
|
|
86
|
+
reviewer_id: z.string().optional(),
|
|
87
|
+
parent_id: z.string().optional(),
|
|
88
|
+
subtask_order: z.number().int().optional(),
|
|
89
|
+
description: z.string().optional(),
|
|
90
|
+
task_context: z.string().optional(),
|
|
91
|
+
task_context_summary: z.string().optional(),
|
|
92
|
+
user_notes: z.string().optional(),
|
|
93
|
+
related_decisions: z.array(RelatedDecisionRefSchema).optional(),
|
|
94
|
+
evidence: z.array(z.string()).optional(),
|
|
95
|
+
validation_steps: z.array(z.string()).optional(),
|
|
96
|
+
sessions: z.array(TaskSessionRefSchema).optional(),
|
|
97
|
+
actions: z.array(TaskActionSchema).optional(),
|
|
98
|
+
created_at: z.string().datetime().optional(),
|
|
99
|
+
updated_at: z.string().datetime().optional(),
|
|
100
|
+
due_at: z.string().datetime().optional(),
|
|
101
|
+
github_issue_number: z.number().optional(),
|
|
102
|
+
deleted_at: z.string().datetime().optional()
|
|
103
|
+
});
|
|
104
|
+
var TaskListSchema = z.object({
|
|
105
|
+
tasks: z.array(TaskSchema)
|
|
106
|
+
});
|
|
107
|
+
var TaskUpdateSchema = z.object({
|
|
108
|
+
id: z.string(),
|
|
109
|
+
title: z.string().optional(),
|
|
110
|
+
description: z.string().optional(),
|
|
111
|
+
status: TaskStatusSchema.optional(),
|
|
112
|
+
assignee: z.string().optional(),
|
|
113
|
+
priority: TaskPrioritySchema.optional(),
|
|
114
|
+
tags: z.array(z.string()).optional(),
|
|
115
|
+
type: TaskTypeSchema.optional(),
|
|
116
|
+
estimate_hours: z.number().optional(),
|
|
117
|
+
depends_on: z.array(z.string()).optional(),
|
|
118
|
+
blocked_by: z.array(z.string()).optional(),
|
|
119
|
+
recurrence_rule: z.string().optional(),
|
|
120
|
+
owner_id: z.string().optional(),
|
|
121
|
+
reviewer_id: z.string().optional(),
|
|
122
|
+
parent_id: z.string().optional(),
|
|
123
|
+
subtask_order: z.number().int().optional(),
|
|
124
|
+
due_at: z.string().datetime().optional(),
|
|
125
|
+
evidence: z.array(z.string()).optional(),
|
|
126
|
+
task_context: z.string().optional(),
|
|
127
|
+
task_context_summary: z.string().optional(),
|
|
128
|
+
user_notes: z.string().optional(),
|
|
129
|
+
related_decisions: z.array(RelatedDecisionRefSchema).optional(),
|
|
130
|
+
validation_steps: z.array(z.string()).optional(),
|
|
131
|
+
reasoning: z.string().optional(),
|
|
132
|
+
actor: z.string().optional(),
|
|
133
|
+
github_issue_number: z.number().optional(),
|
|
134
|
+
deleted_at: z.string().datetime().optional()
|
|
135
|
+
});
|
|
136
|
+
var TaskCreateSchema = z.object({
|
|
137
|
+
title: z.string(),
|
|
138
|
+
description: z.string().optional(),
|
|
139
|
+
status: TaskStatusSchema.optional(),
|
|
140
|
+
assignee: z.string().optional(),
|
|
141
|
+
priority: TaskPrioritySchema.optional(),
|
|
142
|
+
tags: z.array(z.string()).optional(),
|
|
143
|
+
type: TaskTypeSchema.optional(),
|
|
144
|
+
estimate_hours: z.number().optional(),
|
|
145
|
+
depends_on: z.array(z.string()).optional(),
|
|
146
|
+
blocked_by: z.array(z.string()).optional(),
|
|
147
|
+
recurrence_rule: z.string().optional(),
|
|
148
|
+
owner_id: z.string().optional(),
|
|
149
|
+
reviewer_id: z.string().optional(),
|
|
150
|
+
parent_id: z.string().optional(),
|
|
151
|
+
subtask_order: z.number().int().optional(),
|
|
152
|
+
due_at: z.string().datetime().optional(),
|
|
153
|
+
evidence: z.array(z.string()).optional(),
|
|
154
|
+
task_context: z.string().optional(),
|
|
155
|
+
task_context_summary: z.string().optional(),
|
|
156
|
+
related_decisions: z.array(RelatedDecisionRefSchema).optional(),
|
|
157
|
+
validation_steps: z.array(z.string()).optional(),
|
|
158
|
+
reasoning: z.string().optional()
|
|
159
|
+
});
|
|
160
|
+
var VemUpdateSchema = z.object({
|
|
161
|
+
tasks: z.array(TaskUpdateSchema).optional(),
|
|
162
|
+
new_tasks: z.array(TaskCreateSchema).optional(),
|
|
163
|
+
changelog_append: z.union([z.string(), z.array(z.string())]).optional(),
|
|
164
|
+
decisions_append: z.union([z.string(), z.array(z.string())]).optional(),
|
|
165
|
+
current_state: z.string().optional(),
|
|
166
|
+
context: z.string().optional()
|
|
167
|
+
});
|
|
168
|
+
var WebhookEventSchema = z.enum([
|
|
169
|
+
"task.created",
|
|
170
|
+
"task.started",
|
|
171
|
+
"task.blocked",
|
|
172
|
+
"task.completed",
|
|
173
|
+
"task.deleted",
|
|
174
|
+
"snapshot.pushed",
|
|
175
|
+
"snapshot.verified",
|
|
176
|
+
"snapshot.failed",
|
|
177
|
+
"decision.added",
|
|
178
|
+
"changelog.updated",
|
|
179
|
+
"drift.detected",
|
|
180
|
+
"project.linked"
|
|
181
|
+
]);
|
|
182
|
+
var WebhookSchema = z.object({
|
|
183
|
+
id: z.string().uuid(),
|
|
184
|
+
org_id: z.string(),
|
|
185
|
+
project_id: z.string().nullable(),
|
|
186
|
+
url: z.string().url(),
|
|
187
|
+
secret: z.string(),
|
|
188
|
+
enabled: z.boolean(),
|
|
189
|
+
events: z.array(WebhookEventSchema),
|
|
190
|
+
created_at: z.string().datetime(),
|
|
191
|
+
updated_at: z.string().datetime(),
|
|
192
|
+
created_by: z.string().nullable()
|
|
193
|
+
});
|
|
194
|
+
var WebhookCreateSchema = z.object({
|
|
195
|
+
url: z.string().url("Must be a valid URL"),
|
|
196
|
+
events: z.array(WebhookEventSchema).min(1, "Must select at least one event")
|
|
197
|
+
});
|
|
198
|
+
var WebhookUpdateSchema = z.object({
|
|
199
|
+
url: z.string().url().optional(),
|
|
200
|
+
events: z.array(WebhookEventSchema).min(1).optional(),
|
|
201
|
+
enabled: z.boolean().optional()
|
|
202
|
+
});
|
|
203
|
+
var WebhookDeliverySchema = z.object({
|
|
204
|
+
id: z.string().uuid(),
|
|
205
|
+
webhook_id: z.string().uuid(),
|
|
206
|
+
event_type: WebhookEventSchema,
|
|
207
|
+
payload: z.any(),
|
|
208
|
+
status_code: z.number().nullable(),
|
|
209
|
+
success: z.boolean(),
|
|
210
|
+
attempt: z.number().int().min(1).max(3),
|
|
211
|
+
error_message: z.string().nullable(),
|
|
212
|
+
delivered_at: z.string().datetime()
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// ../../packages/core/dist/agent.js
|
|
216
|
+
import fs5 from "fs-extra";
|
|
217
|
+
|
|
218
|
+
// ../../packages/core/dist/fs.js
|
|
219
|
+
import path from "path";
|
|
220
|
+
import { findUp } from "find-up-simple";
|
|
221
|
+
import fs from "fs-extra";
|
|
222
|
+
var VEM_DIR = ".vem";
|
|
223
|
+
var TASKS_DIR = "tasks";
|
|
224
|
+
var DECISIONS_DIR = "decisions";
|
|
225
|
+
var CHANGELOG_DIR = "changelog";
|
|
226
|
+
var CONTEXT_FILE = "CONTEXT.md";
|
|
227
|
+
var CURRENT_STATE_FILE = "CURRENT_STATE.md";
|
|
228
|
+
async function getRepoRoot() {
|
|
229
|
+
const gitDir = await findUp(".git", { type: "directory" });
|
|
230
|
+
if (!gitDir) {
|
|
231
|
+
throw new Error("Not inside a Git repository");
|
|
232
|
+
}
|
|
233
|
+
return path.dirname(gitDir);
|
|
234
|
+
}
|
|
235
|
+
async function getVemDir() {
|
|
236
|
+
const root = await getRepoRoot();
|
|
237
|
+
return path.join(root, VEM_DIR);
|
|
238
|
+
}
|
|
239
|
+
async function ensureVemDir() {
|
|
240
|
+
const dir = await getVemDir();
|
|
241
|
+
await fs.ensureDir(dir);
|
|
242
|
+
return dir;
|
|
243
|
+
}
|
|
244
|
+
async function ensureVemFiles() {
|
|
245
|
+
const dir = await ensureVemDir();
|
|
246
|
+
await fs.ensureDir(path.join(dir, TASKS_DIR));
|
|
247
|
+
await fs.ensureDir(path.join(dir, DECISIONS_DIR));
|
|
248
|
+
await fs.ensureDir(path.join(dir, CHANGELOG_DIR));
|
|
249
|
+
const contextPath = path.join(dir, CONTEXT_FILE);
|
|
250
|
+
if (!await fs.pathExists(contextPath)) {
|
|
251
|
+
await fs.writeFile(contextPath, "# Project Context: vem\n\nDescribe the high-level project context here.\n", "utf-8");
|
|
252
|
+
}
|
|
253
|
+
const currentStatePath = path.join(dir, CURRENT_STATE_FILE);
|
|
254
|
+
if (!await fs.pathExists(currentStatePath)) {
|
|
255
|
+
await fs.writeFile(currentStatePath, "# Current State\n\nSummarize what just changed, what is in progress, and what is next.\n", "utf-8");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ../../packages/core/dist/git.js
|
|
260
|
+
import { execSync } from "child_process";
|
|
261
|
+
async function getGitHeadHash() {
|
|
262
|
+
try {
|
|
263
|
+
const root = await getRepoRoot();
|
|
264
|
+
const hash = execSync("git rev-parse HEAD", {
|
|
265
|
+
cwd: root,
|
|
266
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
267
|
+
}).toString().trim();
|
|
268
|
+
return hash || null;
|
|
269
|
+
} catch {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async function getGitLastCommitForPath(filePath) {
|
|
274
|
+
try {
|
|
275
|
+
const root = await getRepoRoot();
|
|
276
|
+
const relative2 = filePath.startsWith(root) ? filePath.slice(root.length + 1) : filePath;
|
|
277
|
+
const hash = execSync(`git log -1 --format=%H -- "${relative2.replace(/"/g, '\\"')}"`, {
|
|
278
|
+
cwd: root,
|
|
279
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
280
|
+
}).toString().trim();
|
|
281
|
+
return hash || null;
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ../../packages/core/dist/logs.js
|
|
288
|
+
import path2 from "path";
|
|
289
|
+
import fs2 from "fs-extra";
|
|
290
|
+
var ScalableLogService = class {
|
|
291
|
+
subDir;
|
|
292
|
+
constructor(subDir) {
|
|
293
|
+
this.subDir = subDir;
|
|
294
|
+
}
|
|
295
|
+
async getBaseDir() {
|
|
296
|
+
const vemDir = await getVemDir();
|
|
297
|
+
const dir = path2.join(vemDir, this.subDir);
|
|
298
|
+
await fs2.ensureDir(dir);
|
|
299
|
+
return dir;
|
|
300
|
+
}
|
|
301
|
+
async addEntry(title, content, options) {
|
|
302
|
+
const baseDir = await this.getBaseDir();
|
|
303
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
304
|
+
const id = `${timestamp.replace(/[:.]/g, "-")}-${title.toLowerCase().replace(/[^a-z0-9]/g, "-")}`;
|
|
305
|
+
const filePath = path2.join(baseDir, `${id}.md`);
|
|
306
|
+
const commitLine = options?.commitHash ? `**Commit:** ${options.commitHash}
|
|
307
|
+
|
|
308
|
+
` : "";
|
|
309
|
+
const fullContent = `# ${title}
|
|
310
|
+
|
|
311
|
+
**Date:** ${timestamp}
|
|
312
|
+
|
|
313
|
+
${commitLine}${content}
|
|
314
|
+
`;
|
|
315
|
+
await fs2.writeFile(filePath, fullContent, "utf-8");
|
|
316
|
+
return id;
|
|
317
|
+
}
|
|
318
|
+
async getAllEntries() {
|
|
319
|
+
const baseDir = await this.getBaseDir();
|
|
320
|
+
const files = await fs2.readdir(baseDir);
|
|
321
|
+
const entries = [];
|
|
322
|
+
for (const file of files) {
|
|
323
|
+
if (file.endsWith(".md")) {
|
|
324
|
+
const content = await fs2.readFile(path2.join(baseDir, file), "utf-8");
|
|
325
|
+
const id = path2.parse(file).name;
|
|
326
|
+
const titleMatch = content.match(/^#\s+(.*)/);
|
|
327
|
+
const title = titleMatch ? titleMatch[1] : id;
|
|
328
|
+
entries.push({
|
|
329
|
+
id,
|
|
330
|
+
title,
|
|
331
|
+
content,
|
|
332
|
+
created_at: id.substring(0, 19).replace(/-/g, (_m, offset) => offset === 10 ? "T" : offset === 13 || offset === 16 ? ":" : "-"),
|
|
333
|
+
file_path: path2.join(baseDir, file)
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return entries.sort((a, b) => b.id.localeCompare(a.id));
|
|
338
|
+
}
|
|
339
|
+
async getMonolithicContent() {
|
|
340
|
+
return this.getMonolithicContentWithOptions();
|
|
341
|
+
}
|
|
342
|
+
async getMonolithicContentWithOptions(options) {
|
|
343
|
+
const entries = await this.getAllEntries();
|
|
344
|
+
const normalized = await Promise.all(entries.reverse().map(async (entry) => {
|
|
345
|
+
if (!options?.includeCommitHashes)
|
|
346
|
+
return entry.content;
|
|
347
|
+
if (/^\*\*Commit:\*\*/m.test(entry.content))
|
|
348
|
+
return entry.content;
|
|
349
|
+
if (!entry.file_path)
|
|
350
|
+
return entry.content;
|
|
351
|
+
const commitHash = await getGitLastCommitForPath(entry.file_path);
|
|
352
|
+
if (!commitHash)
|
|
353
|
+
return entry.content;
|
|
354
|
+
const dateMatch = entry.content.match(/^\*\*Date:\*\*.*$/m);
|
|
355
|
+
if (!dateMatch) {
|
|
356
|
+
return `**Commit:** ${commitHash}
|
|
357
|
+
|
|
358
|
+
${entry.content}`;
|
|
359
|
+
}
|
|
360
|
+
return entry.content.replace(dateMatch[0], `${dateMatch[0]}
|
|
361
|
+
|
|
362
|
+
**Commit:** ${commitHash}`);
|
|
363
|
+
}));
|
|
364
|
+
return normalized.join("\n---\n\n");
|
|
365
|
+
}
|
|
366
|
+
async archiveEntries(options) {
|
|
367
|
+
const entries = await this.getAllEntries();
|
|
368
|
+
let toArchive = [];
|
|
369
|
+
if (options.keepCount !== void 0) {
|
|
370
|
+
toArchive = entries.slice(options.keepCount);
|
|
371
|
+
} else if (options.olderThanDays !== void 0) {
|
|
372
|
+
const now = /* @__PURE__ */ new Date();
|
|
373
|
+
const threshold = new Date(now.getTime() - options.olderThanDays * 24 * 60 * 60 * 1e3);
|
|
374
|
+
toArchive = entries.filter((e) => new Date(e.created_at) < threshold);
|
|
375
|
+
} else {
|
|
376
|
+
toArchive = entries.slice(20);
|
|
377
|
+
}
|
|
378
|
+
if (toArchive.length === 0)
|
|
379
|
+
return 0;
|
|
380
|
+
const baseDir = await this.getBaseDir();
|
|
381
|
+
const archiveBase = path2.join(baseDir, "archive");
|
|
382
|
+
await fs2.ensureDir(archiveBase);
|
|
383
|
+
for (const entry of toArchive) {
|
|
384
|
+
const date = new Date(entry.created_at);
|
|
385
|
+
const folder = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
386
|
+
const targetDir = path2.join(archiveBase, folder);
|
|
387
|
+
await fs2.ensureDir(targetDir);
|
|
388
|
+
const src = path2.join(baseDir, `${entry.id}.md`);
|
|
389
|
+
const dest = path2.join(targetDir, `${entry.id}.md`);
|
|
390
|
+
if (await fs2.pathExists(src)) {
|
|
391
|
+
await fs2.move(src, dest, { overwrite: true });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return toArchive.length;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// ../../packages/core/dist/tasks.js
|
|
399
|
+
import path4 from "path";
|
|
400
|
+
import fs4 from "fs-extra";
|
|
401
|
+
|
|
402
|
+
// ../../packages/core/dist/sharded-fs.js
|
|
403
|
+
import crypto from "crypto";
|
|
404
|
+
import path3 from "path";
|
|
405
|
+
import fs3 from "fs-extra";
|
|
406
|
+
var ShardedFileStorage = class {
|
|
407
|
+
baseDir;
|
|
408
|
+
objectsDirName;
|
|
409
|
+
constructor(baseDir, objectsDirName = "objects") {
|
|
410
|
+
this.baseDir = baseDir;
|
|
411
|
+
this.objectsDirName = objectsDirName;
|
|
412
|
+
}
|
|
413
|
+
getObjectsDir() {
|
|
414
|
+
return path3.join(this.baseDir, this.objectsDirName);
|
|
415
|
+
}
|
|
416
|
+
getShard(id) {
|
|
417
|
+
const hash = crypto.createHash("sha1").update(id).digest("hex");
|
|
418
|
+
return hash.substring(0, 2);
|
|
419
|
+
}
|
|
420
|
+
getFilePath(id) {
|
|
421
|
+
const shard = this.getShard(id);
|
|
422
|
+
return path3.join(this.getObjectsDir(), shard, `${id}.json`);
|
|
423
|
+
}
|
|
424
|
+
async save(record) {
|
|
425
|
+
const filePath = this.getFilePath(record.id);
|
|
426
|
+
await fs3.ensureDir(path3.dirname(filePath));
|
|
427
|
+
await fs3.writeJson(filePath, record, { spaces: 2 });
|
|
428
|
+
}
|
|
429
|
+
async load(id) {
|
|
430
|
+
const filePath = this.getFilePath(id);
|
|
431
|
+
if (!await fs3.pathExists(filePath)) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
return fs3.readJson(filePath);
|
|
435
|
+
}
|
|
436
|
+
async delete(id) {
|
|
437
|
+
const filePath = this.getFilePath(id);
|
|
438
|
+
if (await fs3.pathExists(filePath)) {
|
|
439
|
+
await fs3.remove(filePath);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async listIds() {
|
|
443
|
+
const objectsDir = this.getObjectsDir();
|
|
444
|
+
if (!await fs3.pathExists(objectsDir)) {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
const shards = await fs3.readdir(objectsDir);
|
|
448
|
+
const ids = [];
|
|
449
|
+
for (const shard of shards) {
|
|
450
|
+
const shardPath = path3.join(objectsDir, shard);
|
|
451
|
+
const stat = await fs3.stat(shardPath);
|
|
452
|
+
if (!stat.isDirectory())
|
|
453
|
+
continue;
|
|
454
|
+
const files = await fs3.readdir(shardPath);
|
|
455
|
+
for (const file of files) {
|
|
456
|
+
if (file.endsWith(".json")) {
|
|
457
|
+
ids.push(path3.parse(file).name);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return ids;
|
|
462
|
+
}
|
|
463
|
+
async loadAll() {
|
|
464
|
+
const ids = await this.listIds();
|
|
465
|
+
const result = [];
|
|
466
|
+
for (const id of ids) {
|
|
467
|
+
const record = await this.load(id);
|
|
468
|
+
if (record) {
|
|
469
|
+
result.push(record);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
var TaskIndex = class {
|
|
476
|
+
indexPath;
|
|
477
|
+
constructor(baseDir) {
|
|
478
|
+
this.indexPath = path3.join(baseDir, "index.json");
|
|
479
|
+
}
|
|
480
|
+
async load() {
|
|
481
|
+
if (!await fs3.pathExists(this.indexPath)) {
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const data = await fs3.readJson(this.indexPath);
|
|
486
|
+
return data.entries || [];
|
|
487
|
+
} catch {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async save(entries) {
|
|
492
|
+
await fs3.ensureDir(path3.dirname(this.indexPath));
|
|
493
|
+
await fs3.writeJson(this.indexPath, { entries }, { spaces: 2 });
|
|
494
|
+
}
|
|
495
|
+
async updateEntry(entry) {
|
|
496
|
+
const entries = await this.load();
|
|
497
|
+
const index = entries.findIndex((e) => e.id === entry.id);
|
|
498
|
+
if (index !== -1) {
|
|
499
|
+
entries[index] = entry;
|
|
500
|
+
} else {
|
|
501
|
+
entries.push(entry);
|
|
502
|
+
}
|
|
503
|
+
await this.save(entries);
|
|
504
|
+
}
|
|
505
|
+
async removeEntry(id) {
|
|
506
|
+
const entries = await this.load();
|
|
507
|
+
const filtered = entries.filter((e) => e.id !== id);
|
|
508
|
+
await this.save(filtered);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// ../../packages/core/dist/tasks.js
|
|
513
|
+
var TASK_CONTEXT_SUMMARY_MAX_CHARS = 1200;
|
|
514
|
+
function summarizeTaskContext(value) {
|
|
515
|
+
const normalized = value.trim();
|
|
516
|
+
if (normalized.length <= TASK_CONTEXT_SUMMARY_MAX_CHARS) {
|
|
517
|
+
return normalized;
|
|
518
|
+
}
|
|
519
|
+
return `${normalized.slice(0, TASK_CONTEXT_SUMMARY_MAX_CHARS - 15).trimEnd()}
|
|
520
|
+
...[truncated]`;
|
|
521
|
+
}
|
|
522
|
+
var TaskService = class {
|
|
523
|
+
storage = null;
|
|
524
|
+
index = null;
|
|
525
|
+
async init() {
|
|
526
|
+
if (this.storage && this.index) {
|
|
527
|
+
return { storage: this.storage, index: this.index };
|
|
528
|
+
}
|
|
529
|
+
const vemDir = await getVemDir();
|
|
530
|
+
const baseDir = path4.join(vemDir, TASKS_DIR);
|
|
531
|
+
await fs4.ensureDir(baseDir);
|
|
532
|
+
this.storage = new ShardedFileStorage(baseDir);
|
|
533
|
+
this.index = new TaskIndex(baseDir);
|
|
534
|
+
return { storage: this.storage, index: this.index };
|
|
535
|
+
}
|
|
536
|
+
async getTasks() {
|
|
537
|
+
const { storage } = await this.init();
|
|
538
|
+
const active = await storage.loadAll();
|
|
539
|
+
const archived = await this.loadRecentArchivedTasks(30);
|
|
540
|
+
const archivedIds = new Set(active.map((t) => t.id));
|
|
541
|
+
for (const t of archived) {
|
|
542
|
+
if (!archivedIds.has(t.id))
|
|
543
|
+
active.push(t);
|
|
544
|
+
}
|
|
545
|
+
return active;
|
|
546
|
+
}
|
|
547
|
+
async loadRecentArchivedTasks(withinDays) {
|
|
548
|
+
const vemDir = await getVemDir();
|
|
549
|
+
const archiveDir = path4.join(vemDir, TASKS_DIR, "archive");
|
|
550
|
+
if (!await fs4.pathExists(archiveDir))
|
|
551
|
+
return [];
|
|
552
|
+
const cutoff = Date.now() - withinDays * 24 * 60 * 60 * 1e3;
|
|
553
|
+
const result = [];
|
|
554
|
+
const walk = async (dir) => {
|
|
555
|
+
const entries = await fs4.readdir(dir);
|
|
556
|
+
for (const entry of entries) {
|
|
557
|
+
const fullPath = path4.join(dir, entry);
|
|
558
|
+
const stat = await fs4.stat(fullPath);
|
|
559
|
+
if (stat.isDirectory()) {
|
|
560
|
+
await walk(fullPath);
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (!entry.endsWith(".json"))
|
|
564
|
+
continue;
|
|
565
|
+
if (stat.mtimeMs < cutoff)
|
|
566
|
+
continue;
|
|
567
|
+
try {
|
|
568
|
+
const task = await fs4.readJson(fullPath);
|
|
569
|
+
if (task?.id)
|
|
570
|
+
result.push(task);
|
|
571
|
+
} catch {
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
await walk(archiveDir);
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
async getTaskIndex() {
|
|
579
|
+
const { index } = await this.init();
|
|
580
|
+
return index.load();
|
|
581
|
+
}
|
|
582
|
+
async getTask(id) {
|
|
583
|
+
const { storage } = await this.init();
|
|
584
|
+
return storage.load(id);
|
|
585
|
+
}
|
|
586
|
+
async listArchivedTaskIds() {
|
|
587
|
+
const vemDir = await getVemDir();
|
|
588
|
+
const archiveDir = path4.join(vemDir, TASKS_DIR, "archive");
|
|
589
|
+
if (!await fs4.pathExists(archiveDir)) {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
const ids = [];
|
|
593
|
+
const walk = async (dir) => {
|
|
594
|
+
const entries = await fs4.readdir(dir);
|
|
595
|
+
for (const entry of entries) {
|
|
596
|
+
const fullPath = path4.join(dir, entry);
|
|
597
|
+
const stat = await fs4.stat(fullPath);
|
|
598
|
+
if (stat.isDirectory()) {
|
|
599
|
+
await walk(fullPath);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (!entry.endsWith(".json"))
|
|
603
|
+
continue;
|
|
604
|
+
const id = path4.parse(entry).name;
|
|
605
|
+
if (/^TASK-\d{3,}$/.test(id)) {
|
|
606
|
+
ids.push(id);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
await walk(archiveDir);
|
|
611
|
+
return ids;
|
|
612
|
+
}
|
|
613
|
+
async getNextTaskId(storage) {
|
|
614
|
+
const allTasks = await storage.loadAll();
|
|
615
|
+
const archivedIds = await this.listArchivedTaskIds();
|
|
616
|
+
const allIds = /* @__PURE__ */ new Set([
|
|
617
|
+
...allTasks.map((task) => task.id),
|
|
618
|
+
...archivedIds
|
|
619
|
+
]);
|
|
620
|
+
let maxId = 0;
|
|
621
|
+
for (const id of allIds) {
|
|
622
|
+
const match = id.match(/^TASK-(\d{3,})$/);
|
|
623
|
+
if (match) {
|
|
624
|
+
const num = parseInt(match[1], 10);
|
|
625
|
+
if (!Number.isNaN(num) && num > maxId) {
|
|
626
|
+
maxId = num;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
let candidateNum = maxId + 1;
|
|
631
|
+
let candidateId = `TASK-${String(candidateNum).padStart(3, "0")}`;
|
|
632
|
+
while (allIds.has(candidateId)) {
|
|
633
|
+
candidateNum++;
|
|
634
|
+
candidateId = `TASK-${String(candidateNum).padStart(3, "0")}`;
|
|
635
|
+
}
|
|
636
|
+
return candidateId;
|
|
637
|
+
}
|
|
638
|
+
async addTask(title, description, priority = "medium", reasoning, options) {
|
|
639
|
+
const { storage, index } = await this.init();
|
|
640
|
+
const explicitId = options?.id?.trim();
|
|
641
|
+
const id = explicitId && explicitId.length > 0 ? explicitId : await this.getNextTaskId(storage);
|
|
642
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
643
|
+
const blockingIds = await this.getBlockingIds(options?.depends_on, options?.blocked_by, storage);
|
|
644
|
+
if (options?.status === "done" && blockingIds.length > 0) {
|
|
645
|
+
throw new Error("Cannot mark task as done while dependencies are incomplete.");
|
|
646
|
+
}
|
|
647
|
+
const initialStatus = blockingIds.length > 0 && options?.status !== "done" ? "blocked" : options?.status ?? "todo";
|
|
648
|
+
const newTask = {
|
|
649
|
+
id,
|
|
650
|
+
title,
|
|
651
|
+
description,
|
|
652
|
+
status: initialStatus,
|
|
653
|
+
assignee: options?.assignee,
|
|
654
|
+
priority,
|
|
655
|
+
tags: options?.tags,
|
|
656
|
+
type: options?.type,
|
|
657
|
+
estimate_hours: options?.estimate_hours,
|
|
658
|
+
depends_on: options?.depends_on,
|
|
659
|
+
blocked_by: options?.blocked_by,
|
|
660
|
+
recurrence_rule: options?.recurrence_rule,
|
|
661
|
+
owner_id: options?.owner_id,
|
|
662
|
+
reviewer_id: options?.reviewer_id,
|
|
663
|
+
parent_id: options?.parent_id,
|
|
664
|
+
subtask_order: options?.subtask_order,
|
|
665
|
+
due_at: options?.due_at,
|
|
666
|
+
task_context: options?.task_context,
|
|
667
|
+
task_context_summary: options?.task_context_summary,
|
|
668
|
+
evidence: options?.evidence,
|
|
669
|
+
validation_steps: options?.validation_steps,
|
|
670
|
+
created_at: timestamp,
|
|
671
|
+
updated_at: timestamp,
|
|
672
|
+
actions: [
|
|
673
|
+
{
|
|
674
|
+
type: "create",
|
|
675
|
+
reasoning: reasoning ?? null,
|
|
676
|
+
actor: options?.actor ?? null,
|
|
677
|
+
created_at: timestamp
|
|
678
|
+
}
|
|
679
|
+
]
|
|
680
|
+
};
|
|
681
|
+
await storage.save(newTask);
|
|
682
|
+
await index.updateEntry({
|
|
683
|
+
id: newTask.id,
|
|
684
|
+
title: newTask.title,
|
|
685
|
+
status: newTask.status,
|
|
686
|
+
assignee: newTask.assignee,
|
|
687
|
+
priority: newTask.priority,
|
|
688
|
+
updated_at: newTask.updated_at
|
|
689
|
+
});
|
|
690
|
+
return newTask;
|
|
691
|
+
}
|
|
692
|
+
async updateTask(id, patch) {
|
|
693
|
+
return this.updateTaskInternal(id, patch);
|
|
694
|
+
}
|
|
695
|
+
async updateTaskInternal(id, patch) {
|
|
696
|
+
const { actor, ...taskPatch } = patch;
|
|
697
|
+
const { storage, index } = await this.init();
|
|
698
|
+
const currentTask = await storage.load(id);
|
|
699
|
+
if (!currentTask) {
|
|
700
|
+
throw new Error(`Task ${id} not found`);
|
|
701
|
+
}
|
|
702
|
+
const normalizeEvidence2 = (value) => value?.map((entry) => entry.trim()).filter(Boolean);
|
|
703
|
+
const normalizeText = (value) => {
|
|
704
|
+
if (value === void 0)
|
|
705
|
+
return void 0;
|
|
706
|
+
const trimmed = value.trim();
|
|
707
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
708
|
+
};
|
|
709
|
+
const hasOwn = (key) => Object.hasOwn(taskPatch, key);
|
|
710
|
+
const normalizeStringArray = (value) => {
|
|
711
|
+
if (!value)
|
|
712
|
+
return value;
|
|
713
|
+
return value.map((entry) => entry.trim()).filter(Boolean);
|
|
714
|
+
};
|
|
715
|
+
const statusProvided = taskPatch.status !== void 0;
|
|
716
|
+
const tagsProvided = hasOwn("tags");
|
|
717
|
+
const dependsProvided = hasOwn("depends_on");
|
|
718
|
+
const blockedProvided = hasOwn("blocked_by");
|
|
719
|
+
const deletedProvided = hasOwn("deleted_at");
|
|
720
|
+
const validationProvided = hasOwn("validation_steps");
|
|
721
|
+
const nextStatus = taskPatch.status ?? currentTask.status;
|
|
722
|
+
const nextPriority = taskPatch.priority ?? currentTask.priority;
|
|
723
|
+
const nextEvidence = normalizeEvidence2(taskPatch.evidence) ?? currentTask.evidence;
|
|
724
|
+
const nextTags = tagsProvided ? normalizeStringArray(taskPatch.tags) ?? [] : currentTask.tags;
|
|
725
|
+
const nextType = taskPatch.type ?? currentTask.type;
|
|
726
|
+
const nextEstimate = taskPatch.estimate_hours ?? currentTask.estimate_hours;
|
|
727
|
+
const nextDependsOn = dependsProvided ? normalizeStringArray(taskPatch.depends_on) ?? [] : currentTask.depends_on;
|
|
728
|
+
const nextBlockedBy = blockedProvided ? normalizeStringArray(taskPatch.blocked_by) ?? [] : currentTask.blocked_by;
|
|
729
|
+
const nextRecurrence = taskPatch.recurrence_rule ?? currentTask.recurrence_rule;
|
|
730
|
+
const nextOwner = taskPatch.owner_id ?? currentTask.owner_id;
|
|
731
|
+
const nextReviewer = taskPatch.reviewer_id ?? currentTask.reviewer_id;
|
|
732
|
+
const taskContextProvided = taskPatch.task_context !== void 0;
|
|
733
|
+
const taskContextSummaryProvided = taskPatch.task_context_summary !== void 0;
|
|
734
|
+
const nextTaskContext = taskContextProvided ? normalizeText(taskPatch.task_context) : currentTask.task_context;
|
|
735
|
+
let nextTaskContextSummary = taskContextSummaryProvided ? normalizeText(taskPatch.task_context_summary) : currentTask.task_context_summary;
|
|
736
|
+
const nextValidationSteps = validationProvided ? normalizeStringArray(taskPatch.validation_steps) ?? [] : currentTask.validation_steps;
|
|
737
|
+
const blockingIds = await this.getBlockingIds(nextDependsOn, nextBlockedBy, storage);
|
|
738
|
+
const hasBlocking = blockingIds.length > 0;
|
|
739
|
+
if (nextStatus === "done" && hasBlocking) {
|
|
740
|
+
throw new Error("Cannot mark task as done while dependencies are incomplete.");
|
|
741
|
+
}
|
|
742
|
+
let effectiveStatus = nextStatus;
|
|
743
|
+
if (hasBlocking) {
|
|
744
|
+
effectiveStatus = "blocked";
|
|
745
|
+
} else if (!statusProvided && currentTask.status === "blocked") {
|
|
746
|
+
effectiveStatus = "todo";
|
|
747
|
+
}
|
|
748
|
+
if (effectiveStatus === "done" && currentTask.status !== "done") {
|
|
749
|
+
if (!taskPatch.reasoning) {
|
|
750
|
+
throw new Error("Reasoning is required to mark a task as done.");
|
|
751
|
+
}
|
|
752
|
+
if (!nextEvidence || nextEvidence.length === 0) {
|
|
753
|
+
throw new Error("Evidence is required to mark a task as done. Provide file paths or verification commands.");
|
|
754
|
+
}
|
|
755
|
+
if (nextValidationSteps && nextValidationSteps.length > 0) {
|
|
756
|
+
const evidenceBlob = nextEvidence.join(" ").toLowerCase();
|
|
757
|
+
const missing = nextValidationSteps.filter((step) => !evidenceBlob.includes(step.toLowerCase()));
|
|
758
|
+
if (missing.length > 0) {
|
|
759
|
+
throw new Error(`Missing validation evidence for: ${missing.join(", ")}.`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
const actions = currentTask.actions || [];
|
|
764
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
765
|
+
const actorValue = actor?.trim() || void 0;
|
|
766
|
+
if (effectiveStatus !== currentTask.status) {
|
|
767
|
+
actions.push({
|
|
768
|
+
type: effectiveStatus === "done" ? "completion" : "update_status",
|
|
769
|
+
reasoning: taskPatch.reasoning ?? null,
|
|
770
|
+
actor: actorValue ?? null,
|
|
771
|
+
created_at: timestamp
|
|
772
|
+
});
|
|
773
|
+
} else if (nextPriority !== currentTask.priority) {
|
|
774
|
+
actions.push({
|
|
775
|
+
type: "update_priority",
|
|
776
|
+
reasoning: taskPatch.reasoning ?? null,
|
|
777
|
+
actor: actorValue ?? null,
|
|
778
|
+
created_at: timestamp
|
|
779
|
+
});
|
|
780
|
+
} else if (taskPatch.reasoning) {
|
|
781
|
+
actions.push({
|
|
782
|
+
type: "comment",
|
|
783
|
+
reasoning: taskPatch.reasoning,
|
|
784
|
+
actor: actorValue ?? null,
|
|
785
|
+
created_at: timestamp
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
if (tagsProvided) {
|
|
789
|
+
const prevTags = currentTask.tags ?? [];
|
|
790
|
+
const next = nextTags ?? [];
|
|
791
|
+
if (prevTags.join("|") !== next.join("|")) {
|
|
792
|
+
actions.push({
|
|
793
|
+
type: "update_tags",
|
|
794
|
+
reasoning: taskPatch.reasoning ?? null,
|
|
795
|
+
actor: actorValue ?? null,
|
|
796
|
+
created_at: timestamp
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (nextType !== currentTask.type) {
|
|
801
|
+
actions.push({
|
|
802
|
+
type: "update_type",
|
|
803
|
+
reasoning: taskPatch.reasoning ?? null,
|
|
804
|
+
actor: actorValue ?? null,
|
|
805
|
+
created_at: timestamp
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
if (nextEstimate !== currentTask.estimate_hours) {
|
|
809
|
+
actions.push({
|
|
810
|
+
type: "update_estimate",
|
|
811
|
+
reasoning: taskPatch.reasoning ?? null,
|
|
812
|
+
actor: actorValue ?? null,
|
|
813
|
+
created_at: timestamp
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
if (dependsProvided || blockedProvided) {
|
|
817
|
+
const prevDepends = currentTask.depends_on ?? [];
|
|
818
|
+
const prevBlocked = currentTask.blocked_by ?? [];
|
|
819
|
+
const nextDepends = nextDependsOn ?? [];
|
|
820
|
+
const nextBlocked = nextBlockedBy ?? [];
|
|
821
|
+
if (prevDepends.join("|") !== nextDepends.join("|") || prevBlocked.join("|") !== nextBlocked.join("|")) {
|
|
822
|
+
actions.push({
|
|
823
|
+
type: "update_dependencies",
|
|
824
|
+
reasoning: taskPatch.reasoning ?? null,
|
|
825
|
+
actor: actorValue ?? null,
|
|
826
|
+
created_at: timestamp
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (nextRecurrence !== currentTask.recurrence_rule) {
|
|
831
|
+
actions.push({
|
|
832
|
+
type: "update_recurrence",
|
|
833
|
+
reasoning: taskPatch.reasoning ?? null,
|
|
834
|
+
actor: actorValue ?? null,
|
|
835
|
+
created_at: timestamp
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
if (nextOwner !== currentTask.owner_id) {
|
|
839
|
+
actions.push({
|
|
840
|
+
type: "update_owner",
|
|
841
|
+
reasoning: taskPatch.reasoning ?? null,
|
|
842
|
+
actor: actorValue ?? null,
|
|
843
|
+
created_at: timestamp
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (nextReviewer !== currentTask.reviewer_id) {
|
|
847
|
+
actions.push({
|
|
848
|
+
type: "update_reviewer",
|
|
849
|
+
reasoning: taskPatch.reasoning ?? null,
|
|
850
|
+
actor: actorValue ?? null,
|
|
851
|
+
created_at: timestamp
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
if (deletedProvided && taskPatch.deleted_at !== currentTask.deleted_at) {
|
|
855
|
+
actions.push({
|
|
856
|
+
type: "delete",
|
|
857
|
+
reasoning: taskPatch.reasoning ?? null,
|
|
858
|
+
actor: actorValue ?? null,
|
|
859
|
+
created_at: timestamp
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
let finalTaskContext = nextTaskContext;
|
|
863
|
+
if (effectiveStatus === "done" && finalTaskContext) {
|
|
864
|
+
if (!nextTaskContextSummary) {
|
|
865
|
+
nextTaskContextSummary = summarizeTaskContext(finalTaskContext);
|
|
866
|
+
}
|
|
867
|
+
finalTaskContext = void 0;
|
|
868
|
+
}
|
|
869
|
+
const updatedTask = {
|
|
870
|
+
...currentTask,
|
|
871
|
+
...taskPatch,
|
|
872
|
+
status: effectiveStatus,
|
|
873
|
+
tags: nextTags,
|
|
874
|
+
type: nextType,
|
|
875
|
+
estimate_hours: nextEstimate,
|
|
876
|
+
depends_on: nextDependsOn,
|
|
877
|
+
blocked_by: nextBlockedBy,
|
|
878
|
+
recurrence_rule: nextRecurrence,
|
|
879
|
+
owner_id: nextOwner,
|
|
880
|
+
reviewer_id: nextReviewer,
|
|
881
|
+
evidence: nextEvidence,
|
|
882
|
+
task_context: finalTaskContext,
|
|
883
|
+
task_context_summary: nextTaskContextSummary,
|
|
884
|
+
validation_steps: nextValidationSteps,
|
|
885
|
+
actions,
|
|
886
|
+
updated_at: timestamp
|
|
887
|
+
};
|
|
888
|
+
delete updatedTask.reasoning;
|
|
889
|
+
delete updatedTask.actor;
|
|
890
|
+
await storage.save(updatedTask);
|
|
891
|
+
await index.updateEntry({
|
|
892
|
+
id: updatedTask.id,
|
|
893
|
+
title: updatedTask.title,
|
|
894
|
+
status: updatedTask.status,
|
|
895
|
+
assignee: updatedTask.assignee,
|
|
896
|
+
priority: updatedTask.priority,
|
|
897
|
+
updated_at: updatedTask.updated_at
|
|
898
|
+
});
|
|
899
|
+
if (updatedTask.parent_id) {
|
|
900
|
+
await this.syncParentStatus(updatedTask.parent_id, storage);
|
|
901
|
+
}
|
|
902
|
+
return updatedTask;
|
|
903
|
+
}
|
|
904
|
+
async syncParentStatus(parentId, storage) {
|
|
905
|
+
const parent = await storage.load(parentId);
|
|
906
|
+
if (!parent)
|
|
907
|
+
return;
|
|
908
|
+
const allTasks = await storage.loadAll();
|
|
909
|
+
const subtasks = allTasks.filter((task) => task.parent_id === parentId && !task.deleted_at);
|
|
910
|
+
if (subtasks.length === 0)
|
|
911
|
+
return;
|
|
912
|
+
const allDone = subtasks.every((task) => task.status === "done");
|
|
913
|
+
if (allDone) {
|
|
914
|
+
if (parent.status === "done")
|
|
915
|
+
return;
|
|
916
|
+
const subtaskIds = subtasks.map((task) => task.id).join(", ");
|
|
917
|
+
await this.updateTaskInternal(parentId, {
|
|
918
|
+
status: "done",
|
|
919
|
+
reasoning: "Auto-completed because all subtasks are done.",
|
|
920
|
+
evidence: [`Subtasks completed: ${subtaskIds}`]
|
|
921
|
+
});
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (parent.status !== "done")
|
|
925
|
+
return;
|
|
926
|
+
await this.updateTaskInternal(parentId, {
|
|
927
|
+
status: "in-progress",
|
|
928
|
+
reasoning: "Auto-reopened because a subtask is not done."
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
async getBlockingIds(dependsOn, blockedBy, storage) {
|
|
932
|
+
const blockers = /* @__PURE__ */ new Set();
|
|
933
|
+
const ids = [...dependsOn ?? [], ...blockedBy ?? []].filter(Boolean);
|
|
934
|
+
if (ids.length === 0)
|
|
935
|
+
return [];
|
|
936
|
+
for (const id of ids) {
|
|
937
|
+
const task = await storage.load(id);
|
|
938
|
+
if (!task || task.deleted_at || task.status !== "done") {
|
|
939
|
+
blockers.add(id);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return Array.from(blockers);
|
|
943
|
+
}
|
|
944
|
+
async archiveTasks(options) {
|
|
945
|
+
const { storage, index } = await this.init();
|
|
946
|
+
const entries = await index.load();
|
|
947
|
+
if (!options.status && options.olderThanDays === void 0) {
|
|
948
|
+
throw new Error("Must provide at least one filter (status or olderThanDays)");
|
|
949
|
+
}
|
|
950
|
+
const now = /* @__PURE__ */ new Date();
|
|
951
|
+
const threshold = options.olderThanDays !== void 0 ? new Date(now.getTime() - options.olderThanDays * 24 * 60 * 60 * 1e3) : null;
|
|
952
|
+
const candidates = entries.filter((entry) => {
|
|
953
|
+
let matches = true;
|
|
954
|
+
if (options.status) {
|
|
955
|
+
matches = matches && entry.status === options.status;
|
|
956
|
+
}
|
|
957
|
+
if (threshold && entry.updated_at) {
|
|
958
|
+
matches = matches && new Date(entry.updated_at) < threshold;
|
|
959
|
+
}
|
|
960
|
+
return matches;
|
|
961
|
+
});
|
|
962
|
+
if (candidates.length === 0)
|
|
963
|
+
return 0;
|
|
964
|
+
const vemDir = await getVemDir();
|
|
965
|
+
const baseDir = path4.join(vemDir, TASKS_DIR);
|
|
966
|
+
const archiveBase = path4.join(baseDir, "archive");
|
|
967
|
+
await fs4.ensureDir(archiveBase);
|
|
968
|
+
let count = 0;
|
|
969
|
+
for (const entry of candidates) {
|
|
970
|
+
const task = await storage.load(entry.id);
|
|
971
|
+
if (task) {
|
|
972
|
+
const date = new Date(task.created_at || /* @__PURE__ */ new Date());
|
|
973
|
+
const folder = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
974
|
+
const targetDir = path4.join(archiveBase, folder);
|
|
975
|
+
await fs4.ensureDir(targetDir);
|
|
976
|
+
const destWithId = path4.join(targetDir, `${task.id}.json`);
|
|
977
|
+
await fs4.writeJson(destWithId, task, { spaces: 2 });
|
|
978
|
+
await storage.delete(entry.id);
|
|
979
|
+
await index.removeEntry(entry.id);
|
|
980
|
+
count++;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return count;
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// ../../packages/core/dist/agent.js
|
|
988
|
+
var DEFAULT_TASK_CONTEXT_SUMMARY_MAX_CHARS = 1200;
|
|
989
|
+
function resolveAgentActor(actor) {
|
|
990
|
+
const trimmed = actor?.trim();
|
|
991
|
+
if (trimmed)
|
|
992
|
+
return trimmed;
|
|
993
|
+
const envActor = process.env.VEM_AGENT_NAME || process.env.VEM_ACTOR || process.env.VEM_AGENT;
|
|
994
|
+
const normalized = envActor?.trim();
|
|
995
|
+
return normalized || void 0;
|
|
996
|
+
}
|
|
997
|
+
function normalizeEvidence(evidence) {
|
|
998
|
+
if (!evidence)
|
|
999
|
+
return [];
|
|
1000
|
+
return evidence.map((entry) => entry.trim()).filter(Boolean);
|
|
1001
|
+
}
|
|
1002
|
+
function addValidationEvidence(evidence, validationSteps) {
|
|
1003
|
+
if (!validationSteps || validationSteps.length === 0) {
|
|
1004
|
+
return evidence;
|
|
1005
|
+
}
|
|
1006
|
+
const merged = [...evidence];
|
|
1007
|
+
let evidenceBlob = merged.join(" ").toLowerCase();
|
|
1008
|
+
for (const step of validationSteps) {
|
|
1009
|
+
const normalizedStep = step.trim();
|
|
1010
|
+
if (!normalizedStep)
|
|
1011
|
+
continue;
|
|
1012
|
+
if (!evidenceBlob.includes(normalizedStep.toLowerCase())) {
|
|
1013
|
+
const entry = `Validated: ${normalizedStep}`;
|
|
1014
|
+
merged.push(entry);
|
|
1015
|
+
evidenceBlob = `${evidenceBlob}
|
|
1016
|
+
${entry.toLowerCase()}`;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return merged;
|
|
1020
|
+
}
|
|
1021
|
+
function summarizeTaskContext2(taskContext) {
|
|
1022
|
+
const normalized = taskContext.trim();
|
|
1023
|
+
if (normalized.length <= DEFAULT_TASK_CONTEXT_SUMMARY_MAX_CHARS) {
|
|
1024
|
+
return normalized;
|
|
1025
|
+
}
|
|
1026
|
+
return `${normalized.slice(0, DEFAULT_TASK_CONTEXT_SUMMARY_MAX_CHARS - 15).trimEnd()}
|
|
1027
|
+
...[truncated]`;
|
|
1028
|
+
}
|
|
1029
|
+
async function applyVemUpdate(update) {
|
|
1030
|
+
await ensureVemFiles();
|
|
1031
|
+
const taskService2 = new TaskService();
|
|
1032
|
+
const decisionsLog = new ScalableLogService(DECISIONS_DIR);
|
|
1033
|
+
const changelogLog = new ScalableLogService(CHANGELOG_DIR);
|
|
1034
|
+
const updatedTasks = [];
|
|
1035
|
+
const newTasks = [];
|
|
1036
|
+
if (update.tasks?.length) {
|
|
1037
|
+
for (const entry of update.tasks) {
|
|
1038
|
+
const parsed = TaskUpdateSchema.safeParse(entry);
|
|
1039
|
+
if (!parsed.success) {
|
|
1040
|
+
throw new Error(`Invalid task update for ${entry.id}: ${parsed.error.message}`);
|
|
1041
|
+
}
|
|
1042
|
+
const { id, ...patch } = parsed.data;
|
|
1043
|
+
const task = await taskService2.getTask(id);
|
|
1044
|
+
if (!task) {
|
|
1045
|
+
throw new Error(`Task ${id} not found.`);
|
|
1046
|
+
}
|
|
1047
|
+
let cleaned = stripUndefined(patch);
|
|
1048
|
+
if (cleaned.status === "done" && task.status !== "done") {
|
|
1049
|
+
const actor = resolveAgentActor(cleaned.actor);
|
|
1050
|
+
const reasoning = cleaned.reasoning?.trim() || `Completed via ${actor || "agent"} session.`;
|
|
1051
|
+
const baseEvidence = normalizeEvidence(cleaned.evidence);
|
|
1052
|
+
const evidenceSeed = baseEvidence.length > 0 ? baseEvidence : [`Completed by agent ${actor || "unknown"}`];
|
|
1053
|
+
const evidence = addValidationEvidence(evidenceSeed, task.validation_steps);
|
|
1054
|
+
const taskContextSummary = cleaned.task_context_summary ?? (task.task_context ? summarizeTaskContext2(task.task_context) : void 0);
|
|
1055
|
+
cleaned = stripUndefined({
|
|
1056
|
+
...cleaned,
|
|
1057
|
+
actor,
|
|
1058
|
+
reasoning,
|
|
1059
|
+
evidence,
|
|
1060
|
+
task_context_summary: taskContextSummary
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
const updated = await taskService2.updateTask(id, cleaned);
|
|
1064
|
+
updatedTasks.push(updated);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (update.new_tasks?.length) {
|
|
1068
|
+
for (const entry of update.new_tasks) {
|
|
1069
|
+
const parsed = TaskCreateSchema.safeParse(entry);
|
|
1070
|
+
if (!parsed.success) {
|
|
1071
|
+
throw new Error(`Invalid new task payload: ${parsed.error.message}`);
|
|
1072
|
+
}
|
|
1073
|
+
const created = await taskService2.addTask(parsed.data.title, parsed.data.description, parsed.data.priority ?? "medium", parsed.data.reasoning, stripUndefined({
|
|
1074
|
+
status: parsed.data.status,
|
|
1075
|
+
assignee: parsed.data.assignee,
|
|
1076
|
+
tags: parsed.data.tags,
|
|
1077
|
+
type: parsed.data.type,
|
|
1078
|
+
estimate_hours: parsed.data.estimate_hours,
|
|
1079
|
+
depends_on: parsed.data.depends_on,
|
|
1080
|
+
blocked_by: parsed.data.blocked_by,
|
|
1081
|
+
recurrence_rule: parsed.data.recurrence_rule,
|
|
1082
|
+
owner_id: parsed.data.owner_id,
|
|
1083
|
+
reviewer_id: parsed.data.reviewer_id,
|
|
1084
|
+
evidence: parsed.data.evidence,
|
|
1085
|
+
parent_id: parsed.data.parent_id,
|
|
1086
|
+
subtask_order: parsed.data.subtask_order,
|
|
1087
|
+
due_at: parsed.data.due_at,
|
|
1088
|
+
task_context: parsed.data.task_context,
|
|
1089
|
+
task_context_summary: parsed.data.task_context_summary
|
|
1090
|
+
}));
|
|
1091
|
+
const patch = stripUndefined({
|
|
1092
|
+
status: parsed.data.status,
|
|
1093
|
+
assignee: parsed.data.assignee,
|
|
1094
|
+
tags: parsed.data.tags,
|
|
1095
|
+
type: parsed.data.type,
|
|
1096
|
+
estimate_hours: parsed.data.estimate_hours,
|
|
1097
|
+
depends_on: parsed.data.depends_on,
|
|
1098
|
+
blocked_by: parsed.data.blocked_by,
|
|
1099
|
+
recurrence_rule: parsed.data.recurrence_rule,
|
|
1100
|
+
owner_id: parsed.data.owner_id,
|
|
1101
|
+
reviewer_id: parsed.data.reviewer_id,
|
|
1102
|
+
evidence: parsed.data.evidence,
|
|
1103
|
+
parent_id: parsed.data.parent_id,
|
|
1104
|
+
subtask_order: parsed.data.subtask_order,
|
|
1105
|
+
due_at: parsed.data.due_at,
|
|
1106
|
+
task_context: parsed.data.task_context,
|
|
1107
|
+
task_context_summary: parsed.data.task_context_summary
|
|
1108
|
+
});
|
|
1109
|
+
let updated = created;
|
|
1110
|
+
if (Object.keys(patch).length > 0) {
|
|
1111
|
+
updated = await taskService2.updateTask(created.id, patch);
|
|
1112
|
+
}
|
|
1113
|
+
newTasks.push(updated);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
const changelogLines = await appendChangelog(changelogLog, update.changelog_append);
|
|
1117
|
+
const decisionsAppendedRef = await appendDecisions(decisionsLog, update.decisions_append);
|
|
1118
|
+
const decisionsAppended = decisionsAppendedRef !== null;
|
|
1119
|
+
if (decisionsAppendedRef) {
|
|
1120
|
+
const refId = typeof decisionsAppendedRef === "string" ? decisionsAppendedRef : decisionsAppendedRef.id;
|
|
1121
|
+
const allAffectedTasks = [...updatedTasks, ...newTasks];
|
|
1122
|
+
for (const task of allAffectedTasks) {
|
|
1123
|
+
const existing = Array.isArray(task.related_decisions) ? task.related_decisions : [];
|
|
1124
|
+
const alreadyLinked = existing.some((r) => typeof r === "string" ? r === refId : r.id === refId);
|
|
1125
|
+
if (!alreadyLinked) {
|
|
1126
|
+
const updated = await taskService2.updateTask(task.id, {
|
|
1127
|
+
related_decisions: [...existing, decisionsAppendedRef]
|
|
1128
|
+
});
|
|
1129
|
+
const ui = updatedTasks.findIndex((t) => t.id === task.id);
|
|
1130
|
+
if (ui !== -1)
|
|
1131
|
+
updatedTasks[ui] = updated;
|
|
1132
|
+
const ni = newTasks.findIndex((t) => t.id === task.id);
|
|
1133
|
+
if (ni !== -1)
|
|
1134
|
+
newTasks[ni] = updated;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const currentStateUpdated = await writeCurrentState(update.current_state);
|
|
1139
|
+
const contextUpdated = await writeContext(update.context);
|
|
1140
|
+
return {
|
|
1141
|
+
updatedTasks,
|
|
1142
|
+
newTasks,
|
|
1143
|
+
changelogLines,
|
|
1144
|
+
decisionsAppended,
|
|
1145
|
+
decisionsAppendedRef,
|
|
1146
|
+
currentStateUpdated,
|
|
1147
|
+
contextUpdated
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function stripUndefined(value) {
|
|
1151
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
1152
|
+
}
|
|
1153
|
+
function normalizeLines(value) {
|
|
1154
|
+
if (!value)
|
|
1155
|
+
return [];
|
|
1156
|
+
const raw = Array.isArray(value) ? value : value.split(/\r?\n/);
|
|
1157
|
+
return raw.map((line) => line.trim()).filter(Boolean);
|
|
1158
|
+
}
|
|
1159
|
+
async function appendChangelog(log, value) {
|
|
1160
|
+
const additions = normalizeLines(value);
|
|
1161
|
+
if (additions.length === 0)
|
|
1162
|
+
return [];
|
|
1163
|
+
const content = additions.map((line) => `- ${line}`).join("\n");
|
|
1164
|
+
const commitHash = await getGitHeadHash();
|
|
1165
|
+
await log.addEntry("Agent Update", content, { commitHash });
|
|
1166
|
+
return additions;
|
|
1167
|
+
}
|
|
1168
|
+
function extractDecisionTitle(block) {
|
|
1169
|
+
const match = block.match(/^#{1,3}\s+\[([^\]]+)\]\s+(.+)$/m);
|
|
1170
|
+
if (match)
|
|
1171
|
+
return `${match[1]}: ${match[2].trim()}`;
|
|
1172
|
+
const plain = block.match(/^#{1,3}\s+(.+)$/m);
|
|
1173
|
+
return plain?.[1]?.trim();
|
|
1174
|
+
}
|
|
1175
|
+
async function appendDecisions(log, value) {
|
|
1176
|
+
if (!value)
|
|
1177
|
+
return null;
|
|
1178
|
+
const block = Array.isArray(value) ? value.join("\n").trim() : value.trim();
|
|
1179
|
+
if (!block)
|
|
1180
|
+
return null;
|
|
1181
|
+
const commitHash = await getGitHeadHash();
|
|
1182
|
+
const id = await log.addEntry("Agent Decision", block, { commitHash });
|
|
1183
|
+
const title = extractDecisionTitle(block);
|
|
1184
|
+
return title ? { id, title, content: block } : { id, content: block };
|
|
1185
|
+
}
|
|
1186
|
+
async function writeCurrentState(value) {
|
|
1187
|
+
if (value === void 0)
|
|
1188
|
+
return false;
|
|
1189
|
+
const dir = await getVemDir();
|
|
1190
|
+
const currentStatePath = path5.join(dir, CURRENT_STATE_FILE);
|
|
1191
|
+
const next = value.trim().length > 0 ? `${value.trim()}
|
|
1192
|
+
` : "";
|
|
1193
|
+
await fs5.writeFile(currentStatePath, next, "utf-8");
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
async function writeContext(value) {
|
|
1197
|
+
if (value === void 0)
|
|
1198
|
+
return false;
|
|
1199
|
+
const dir = await getVemDir();
|
|
1200
|
+
const contextPath = path5.join(dir, CONTEXT_FILE);
|
|
1201
|
+
const next = value.trim().length > 0 ? `${value.trim()}
|
|
1202
|
+
` : "";
|
|
1203
|
+
await fs5.writeFile(contextPath, next, "utf-8");
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// ../../packages/core/dist/agent-sessions.js
|
|
1208
|
+
function fromCopilotSession(s) {
|
|
1209
|
+
return {
|
|
1210
|
+
id: s.id,
|
|
1211
|
+
source: "copilot",
|
|
1212
|
+
summary: s.summary,
|
|
1213
|
+
branch: s.branch,
|
|
1214
|
+
repository: s.repository,
|
|
1215
|
+
git_root: s.git_root,
|
|
1216
|
+
cwd: s.cwd,
|
|
1217
|
+
created_at: s.created_at,
|
|
1218
|
+
updated_at: s.updated_at,
|
|
1219
|
+
intents: "intents" in s ? s.intents : [],
|
|
1220
|
+
user_messages: "user_messages" in s ? s.user_messages : []
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
async function listAllAgentSessions(gitRoot, sources = ["copilot", "claude", "gemini"]) {
|
|
1224
|
+
const [{ listCopilotSessions }, { listClaudeSessions }, { listGeminiSessions }] = await Promise.all([
|
|
1225
|
+
import("./copilot-sessions-LLDNCHIU.js"),
|
|
1226
|
+
import("./claude-sessions-5HEECZ63.js"),
|
|
1227
|
+
import("./gemini-sessions-RPV25JO4.js")
|
|
1228
|
+
]);
|
|
1229
|
+
const results = await Promise.all([
|
|
1230
|
+
sources.includes("copilot") ? listCopilotSessions(gitRoot).then((ss) => ss.map(fromCopilotSession)) : Promise.resolve([]),
|
|
1231
|
+
sources.includes("claude") ? listClaudeSessions(gitRoot) : Promise.resolve([]),
|
|
1232
|
+
sources.includes("gemini") ? listGeminiSessions(gitRoot) : Promise.resolve([])
|
|
1233
|
+
]);
|
|
1234
|
+
const all = results.flat();
|
|
1235
|
+
all.sort((a, b) => {
|
|
1236
|
+
if (!a.updated_at)
|
|
1237
|
+
return 1;
|
|
1238
|
+
if (!b.updated_at)
|
|
1239
|
+
return -1;
|
|
1240
|
+
return b.updated_at.localeCompare(a.updated_at);
|
|
1241
|
+
});
|
|
1242
|
+
return all;
|
|
1243
|
+
}
|
|
1244
|
+
async function computeSessionStats(sessionId, source) {
|
|
1245
|
+
const { computeCopilotSessionStats } = await import("./copilot-sessions-LLDNCHIU.js");
|
|
1246
|
+
const { computeClaudeSessionStats } = await import("./claude-sessions-5HEECZ63.js");
|
|
1247
|
+
const { computeGeminiSessionStats } = await import("./gemini-sessions-RPV25JO4.js");
|
|
1248
|
+
switch (source) {
|
|
1249
|
+
case "copilot":
|
|
1250
|
+
return computeCopilotSessionStats(sessionId);
|
|
1251
|
+
case "claude":
|
|
1252
|
+
return computeClaudeSessionStats(sessionId);
|
|
1253
|
+
case "gemini":
|
|
1254
|
+
return computeGeminiSessionStats(sessionId);
|
|
1255
|
+
default:
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// ../../packages/core/dist/config.js
|
|
1261
|
+
import { randomUUID } from "crypto";
|
|
1262
|
+
import { homedir, hostname } from "os";
|
|
1263
|
+
import path6 from "path";
|
|
1264
|
+
import fs6 from "fs-extra";
|
|
1265
|
+
var CONFIG_FILE = "config.json";
|
|
1266
|
+
var ConfigService = class {
|
|
1267
|
+
async getLocalPath() {
|
|
1268
|
+
const dir = await getVemDir();
|
|
1269
|
+
return path6.join(dir, CONFIG_FILE);
|
|
1270
|
+
}
|
|
1271
|
+
getGlobalPath() {
|
|
1272
|
+
return path6.join(homedir(), ".vem", CONFIG_FILE);
|
|
1273
|
+
}
|
|
1274
|
+
async readLocalConfig() {
|
|
1275
|
+
try {
|
|
1276
|
+
const filePath = await this.getLocalPath();
|
|
1277
|
+
if (!await fs6.pathExists(filePath))
|
|
1278
|
+
return {};
|
|
1279
|
+
return fs6.readJson(filePath);
|
|
1280
|
+
} catch {
|
|
1281
|
+
return {};
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
async readGlobalConfig() {
|
|
1285
|
+
try {
|
|
1286
|
+
const filePath = this.getGlobalPath();
|
|
1287
|
+
if (!await fs6.pathExists(filePath))
|
|
1288
|
+
return {};
|
|
1289
|
+
return fs6.readJson(filePath);
|
|
1290
|
+
} catch {
|
|
1291
|
+
return {};
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
async writeLocalConfig(update) {
|
|
1295
|
+
const filePath = await this.getLocalPath();
|
|
1296
|
+
const current = await this.readLocalConfig();
|
|
1297
|
+
const next = { ...current, ...update };
|
|
1298
|
+
const clean = {
|
|
1299
|
+
last_version: next.last_version,
|
|
1300
|
+
project_id: next.project_id,
|
|
1301
|
+
project_org_id: next.project_org_id,
|
|
1302
|
+
linked_remote_name: next.linked_remote_name,
|
|
1303
|
+
linked_remote_url: next.linked_remote_url,
|
|
1304
|
+
last_push_git_hash: next.last_push_git_hash,
|
|
1305
|
+
last_push_vem_hash: next.last_push_vem_hash,
|
|
1306
|
+
last_synced_vem_hash: next.last_synced_vem_hash
|
|
1307
|
+
};
|
|
1308
|
+
await fs6.outputJson(filePath, clean, { spaces: 2 });
|
|
1309
|
+
}
|
|
1310
|
+
async writeGlobalConfig(update) {
|
|
1311
|
+
const filePath = this.getGlobalPath();
|
|
1312
|
+
const current = await this.readGlobalConfig();
|
|
1313
|
+
const next = { ...current, ...update };
|
|
1314
|
+
const clean = {
|
|
1315
|
+
api_key: next.api_key,
|
|
1316
|
+
device_id: next.device_id,
|
|
1317
|
+
device_name: next.device_name
|
|
1318
|
+
};
|
|
1319
|
+
await fs6.outputJson(filePath, clean, { spaces: 2 });
|
|
1320
|
+
}
|
|
1321
|
+
// --- Global Scoped ---
|
|
1322
|
+
async getApiKey() {
|
|
1323
|
+
const config = await this.readGlobalConfig();
|
|
1324
|
+
return config.api_key || process.env.VEM_API_KEY;
|
|
1325
|
+
}
|
|
1326
|
+
async getDeviceId() {
|
|
1327
|
+
const config = await this.readGlobalConfig();
|
|
1328
|
+
return config.device_id;
|
|
1329
|
+
}
|
|
1330
|
+
async getOrCreateDeviceId() {
|
|
1331
|
+
const config = await this.readGlobalConfig();
|
|
1332
|
+
if (config.device_id && config.device_name) {
|
|
1333
|
+
return { deviceId: config.device_id, deviceName: config.device_name };
|
|
1334
|
+
}
|
|
1335
|
+
let deviceId = config.device_id;
|
|
1336
|
+
if (!deviceId) {
|
|
1337
|
+
deviceId = randomUUID();
|
|
1338
|
+
}
|
|
1339
|
+
let deviceName = config.device_name;
|
|
1340
|
+
if (!deviceName) {
|
|
1341
|
+
deviceName = hostname();
|
|
1342
|
+
}
|
|
1343
|
+
await this.writeGlobalConfig({
|
|
1344
|
+
device_id: deviceId,
|
|
1345
|
+
device_name: deviceName
|
|
1346
|
+
});
|
|
1347
|
+
return { deviceId, deviceName };
|
|
1348
|
+
}
|
|
1349
|
+
async setApiKey(key) {
|
|
1350
|
+
await this.writeGlobalConfig({ api_key: key || void 0 });
|
|
1351
|
+
}
|
|
1352
|
+
// --- Local Scoped ---
|
|
1353
|
+
async getProjectId() {
|
|
1354
|
+
const config = await this.readLocalConfig();
|
|
1355
|
+
return config.project_id || process.env.VEM_PROJECT_ID;
|
|
1356
|
+
}
|
|
1357
|
+
async getProjectOrgId() {
|
|
1358
|
+
const config = await this.readLocalConfig();
|
|
1359
|
+
return config.project_org_id;
|
|
1360
|
+
}
|
|
1361
|
+
async getLinkedRemoteName() {
|
|
1362
|
+
const config = await this.readLocalConfig();
|
|
1363
|
+
return config.linked_remote_name;
|
|
1364
|
+
}
|
|
1365
|
+
async getLinkedRemoteUrl() {
|
|
1366
|
+
const config = await this.readLocalConfig();
|
|
1367
|
+
return config.linked_remote_url;
|
|
1368
|
+
}
|
|
1369
|
+
async getLastVersion() {
|
|
1370
|
+
const config = await this.readLocalConfig();
|
|
1371
|
+
return config.last_version;
|
|
1372
|
+
}
|
|
1373
|
+
async setLastVersion(version) {
|
|
1374
|
+
await this.writeLocalConfig({ last_version: version });
|
|
1375
|
+
}
|
|
1376
|
+
async getLastPushState() {
|
|
1377
|
+
const config = await this.readLocalConfig();
|
|
1378
|
+
return {
|
|
1379
|
+
gitHash: config.last_push_git_hash,
|
|
1380
|
+
vemHash: config.last_push_vem_hash
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
async setLastPushState(state) {
|
|
1384
|
+
await this.writeLocalConfig({
|
|
1385
|
+
last_push_git_hash: state.gitHash,
|
|
1386
|
+
last_push_vem_hash: state.vemHash
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
async getLastSyncedVemHash() {
|
|
1390
|
+
const config = await this.readLocalConfig();
|
|
1391
|
+
return config.last_synced_vem_hash;
|
|
1392
|
+
}
|
|
1393
|
+
async setLastSyncedVemHash(vemHash) {
|
|
1394
|
+
await this.writeLocalConfig({
|
|
1395
|
+
last_synced_vem_hash: vemHash || void 0
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
async setProjectId(projectId) {
|
|
1399
|
+
await this.writeLocalConfig({ project_id: projectId || void 0 });
|
|
1400
|
+
}
|
|
1401
|
+
async setProjectOrgId(orgId) {
|
|
1402
|
+
await this.writeLocalConfig({ project_org_id: orgId || void 0 });
|
|
1403
|
+
}
|
|
1404
|
+
async setLinkedRemote(binding) {
|
|
1405
|
+
await this.writeLocalConfig({
|
|
1406
|
+
linked_remote_name: binding?.name || void 0,
|
|
1407
|
+
linked_remote_url: binding?.url || void 0
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
// --- Context (Local) ---
|
|
1411
|
+
async getContextPath() {
|
|
1412
|
+
try {
|
|
1413
|
+
const dir = await getVemDir();
|
|
1414
|
+
return path6.join(dir, CONTEXT_FILE);
|
|
1415
|
+
} catch {
|
|
1416
|
+
return "";
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
async getContext() {
|
|
1420
|
+
const filePath = await this.getContextPath();
|
|
1421
|
+
if (!filePath || !await fs6.pathExists(filePath)) {
|
|
1422
|
+
return "";
|
|
1423
|
+
}
|
|
1424
|
+
return fs6.readFile(filePath, "utf-8");
|
|
1425
|
+
}
|
|
1426
|
+
async updateContext(content) {
|
|
1427
|
+
const filePath = await this.getContextPath();
|
|
1428
|
+
if (!filePath)
|
|
1429
|
+
throw new Error("Cannot update context: Not in a git repository.");
|
|
1430
|
+
await fs6.writeFile(filePath, content, "utf-8");
|
|
1431
|
+
}
|
|
1432
|
+
async recordDecision(title, context, decision, relatedTasks) {
|
|
1433
|
+
const decisionsLog = new ScalableLogService(DECISIONS_DIR);
|
|
1434
|
+
let entry = `**Decision:** ${decision}
|
|
1435
|
+
|
|
1436
|
+
**Context:** ${context}`;
|
|
1437
|
+
if (relatedTasks && relatedTasks.length > 0) {
|
|
1438
|
+
entry = `**Related Tasks:** ${relatedTasks.join(", ")}
|
|
1439
|
+
|
|
1440
|
+
${entry}`;
|
|
1441
|
+
}
|
|
1442
|
+
const commitHash = await getGitHeadHash();
|
|
1443
|
+
await decisionsLog.addEntry(title, entry, { commitHash });
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
// ../../packages/core/dist/diff.js
|
|
1448
|
+
import { createHash } from "crypto";
|
|
1449
|
+
import path7 from "path";
|
|
1450
|
+
import fs7 from "fs-extra";
|
|
1451
|
+
|
|
1452
|
+
// ../../packages/core/dist/doctor.js
|
|
1453
|
+
import path8 from "path";
|
|
1454
|
+
import fs8 from "fs-extra";
|
|
1455
|
+
|
|
1456
|
+
// ../../packages/core/dist/env.js
|
|
1457
|
+
import { z as z2 } from "zod";
|
|
1458
|
+
var commonEnvSchema = {
|
|
1459
|
+
NODE_ENV: z2.enum(["development", "production", "test"]).default("development"),
|
|
1460
|
+
INTERNAL_API_SECRET: z2.string().min(1, "INTERNAL_API_SECRET is required"),
|
|
1461
|
+
DATABASE_URL: z2.string().url("DATABASE_URL must be a valid URL")
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1464
|
+
// ../../packages/core/dist/github-private-key.js
|
|
1465
|
+
import { createPrivateKey } from "crypto";
|
|
1466
|
+
|
|
1467
|
+
// ../../packages/core/dist/logger.js
|
|
1468
|
+
import pino from "pino";
|
|
1469
|
+
var isDev = process.env.NODE_ENV === "development";
|
|
1470
|
+
var logger = pino({
|
|
1471
|
+
level: process.env.LOG_LEVEL || "info",
|
|
1472
|
+
// GCP Cloud Logging uses 'severity' instead of 'level'
|
|
1473
|
+
// We map pino levels to Google Cloud severity levels
|
|
1474
|
+
formatters: {
|
|
1475
|
+
level(label) {
|
|
1476
|
+
return { severity: label.toUpperCase() };
|
|
1477
|
+
}
|
|
1478
|
+
},
|
|
1479
|
+
// In development, we use pino-pretty for readability
|
|
1480
|
+
// In production, we want raw JSON for Cloud Logging
|
|
1481
|
+
transport: isDev ? {
|
|
1482
|
+
target: "pino-pretty",
|
|
1483
|
+
options: {
|
|
1484
|
+
colorize: true,
|
|
1485
|
+
ignore: "pid,hostname",
|
|
1486
|
+
translateTime: "SYS:standard"
|
|
1487
|
+
}
|
|
1488
|
+
} : void 0,
|
|
1489
|
+
serializers: {
|
|
1490
|
+
err: pino.stdSerializers.err,
|
|
1491
|
+
error: pino.stdSerializers.err
|
|
1492
|
+
},
|
|
1493
|
+
redact: {
|
|
1494
|
+
paths: [
|
|
1495
|
+
"req.headers.authorization",
|
|
1496
|
+
"req.headers['x-api-key']",
|
|
1497
|
+
"*.password",
|
|
1498
|
+
"*.secret",
|
|
1499
|
+
"*.token"
|
|
1500
|
+
],
|
|
1501
|
+
remove: true
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
// ../../packages/core/dist/secrets.js
|
|
1506
|
+
import { createHash as createHash2, timingSafeEqual } from "crypto";
|
|
1507
|
+
var SECRET_PATTERNS = [
|
|
1508
|
+
{
|
|
1509
|
+
name: "private_key",
|
|
1510
|
+
regex: /-----BEGIN (?:RSA|EC|DSA|OPENSSH|PGP) PRIVATE KEY-----/g,
|
|
1511
|
+
replace: "[REDACTED:private_key]"
|
|
1512
|
+
},
|
|
1513
|
+
{
|
|
1514
|
+
name: "aws_access_key",
|
|
1515
|
+
regex: /\bAKIA[0-9A-Z]{16}\b/g,
|
|
1516
|
+
replace: "[REDACTED:aws_access_key]"
|
|
1517
|
+
},
|
|
1518
|
+
{
|
|
1519
|
+
name: "aws_secret_key",
|
|
1520
|
+
regex: /\b(aws_secret_access_key)\b\s*[:=]\s*([A-Za-z0-9/+=]{40})/gi,
|
|
1521
|
+
replace: (_match, key) => `${key}=[REDACTED:aws_secret_key]`
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
name: "github_token",
|
|
1525
|
+
regex: /\bghp_[A-Za-z0-9]{36}\b/g,
|
|
1526
|
+
replace: "[REDACTED:github_token]"
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
name: "github_pat",
|
|
1530
|
+
regex: /\bgithub_pat_[A-Za-z0-9_]{22,}\b/g,
|
|
1531
|
+
replace: "[REDACTED:github_pat]"
|
|
1532
|
+
},
|
|
1533
|
+
{
|
|
1534
|
+
name: "slack_token",
|
|
1535
|
+
regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
1536
|
+
replace: "[REDACTED:slack_token]"
|
|
1537
|
+
}
|
|
1538
|
+
];
|
|
1539
|
+
function redactSecrets(input) {
|
|
1540
|
+
if (!input)
|
|
1541
|
+
return input;
|
|
1542
|
+
let output = input;
|
|
1543
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
1544
|
+
if (pattern.regex.test(output)) {
|
|
1545
|
+
if (typeof pattern.replace === "string") {
|
|
1546
|
+
output = output.replace(pattern.regex, pattern.replace);
|
|
1547
|
+
} else {
|
|
1548
|
+
output = output.replace(pattern.regex, pattern.replace);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
pattern.regex.lastIndex = 0;
|
|
1552
|
+
}
|
|
1553
|
+
return output;
|
|
1554
|
+
}
|
|
1555
|
+
function detectSecrets(input) {
|
|
1556
|
+
if (!input)
|
|
1557
|
+
return [];
|
|
1558
|
+
const matches = /* @__PURE__ */ new Set();
|
|
1559
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
1560
|
+
if (pattern.regex.test(input)) {
|
|
1561
|
+
matches.add(pattern.name);
|
|
1562
|
+
}
|
|
1563
|
+
pattern.regex.lastIndex = 0;
|
|
1564
|
+
}
|
|
1565
|
+
return Array.from(matches);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// ../../packages/core/dist/sync.js
|
|
1569
|
+
import { createHash as createHash3 } from "crypto";
|
|
1570
|
+
import path9 from "path";
|
|
1571
|
+
import fs9 from "fs-extra";
|
|
1572
|
+
var KNOWN_AGENT_INSTRUCTION_FILES = [
|
|
1573
|
+
"AGENTS.md",
|
|
1574
|
+
"CLAUDE.md",
|
|
1575
|
+
"GEMINI.md",
|
|
1576
|
+
"CURSOR.md",
|
|
1577
|
+
"copilot-instructions.md",
|
|
1578
|
+
"COPILOT_INSTRUCTIONS.md",
|
|
1579
|
+
".github/copilot-instructions.md"
|
|
1580
|
+
];
|
|
1581
|
+
var KNOWN_AGENT_INSTRUCTION_FILE_SET = new Set(KNOWN_AGENT_INSTRUCTION_FILES);
|
|
1582
|
+
function normalizeInstructionPath(value) {
|
|
1583
|
+
if (typeof value !== "string")
|
|
1584
|
+
return null;
|
|
1585
|
+
const normalized = value.trim().replace(/\\/g, "/");
|
|
1586
|
+
if (!normalized)
|
|
1587
|
+
return null;
|
|
1588
|
+
const collapsed = path9.posix.normalize(normalized);
|
|
1589
|
+
if (collapsed === "." || collapsed === ".." || collapsed.startsWith("../") || collapsed.startsWith("/")) {
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
return KNOWN_AGENT_INSTRUCTION_FILE_SET.has(collapsed) ? collapsed : null;
|
|
1593
|
+
}
|
|
1594
|
+
var DEFAULT_AGENT_PACK_OPTIONS = {
|
|
1595
|
+
contextMaxChars: 16e3,
|
|
1596
|
+
currentStateMaxChars: 8e3,
|
|
1597
|
+
decisionEntryLimit: 8,
|
|
1598
|
+
decisionMaxChars: 9e3,
|
|
1599
|
+
decisionEntryMaxChars: 1200,
|
|
1600
|
+
changelogEntryLimit: 12,
|
|
1601
|
+
changelogMaxChars: 12e3,
|
|
1602
|
+
changelogEntryMaxChars: 900,
|
|
1603
|
+
activeTaskLimit: 20,
|
|
1604
|
+
recentDoneTaskLimit: 5,
|
|
1605
|
+
taskTextMaxChars: 600,
|
|
1606
|
+
taskEvidenceLimit: 5,
|
|
1607
|
+
taskValidationLimit: 8
|
|
1608
|
+
};
|
|
1609
|
+
function truncateText(value, maxChars) {
|
|
1610
|
+
if (value.length <= maxChars)
|
|
1611
|
+
return value;
|
|
1612
|
+
return `${value.slice(0, Math.max(0, maxChars - 15)).trimEnd()}
|
|
1613
|
+
...[truncated]`;
|
|
1614
|
+
}
|
|
1615
|
+
function normalizeTaskText(value, maxChars) {
|
|
1616
|
+
if (!value)
|
|
1617
|
+
return void 0;
|
|
1618
|
+
const normalized = value.trim();
|
|
1619
|
+
if (!normalized)
|
|
1620
|
+
return void 0;
|
|
1621
|
+
return truncateText(normalized, maxChars);
|
|
1622
|
+
}
|
|
1623
|
+
function sortByUpdatedAtDesc(tasks) {
|
|
1624
|
+
return [...tasks].sort((a, b) => (b.updated_at ?? b.created_at ?? "").localeCompare(a.updated_at ?? a.created_at ?? ""));
|
|
1625
|
+
}
|
|
1626
|
+
var SyncService = class {
|
|
1627
|
+
taskService = new TaskService();
|
|
1628
|
+
decisionsLog = new ScalableLogService(DECISIONS_DIR);
|
|
1629
|
+
changelogLog = new ScalableLogService(CHANGELOG_DIR);
|
|
1630
|
+
async getQueueDir() {
|
|
1631
|
+
const dir = await getVemDir();
|
|
1632
|
+
const queueDir = path9.join(dir, "queue");
|
|
1633
|
+
await fs9.ensureDir(queueDir);
|
|
1634
|
+
return queueDir;
|
|
1635
|
+
}
|
|
1636
|
+
async getContextPath() {
|
|
1637
|
+
const dir = await getVemDir();
|
|
1638
|
+
return path9.join(dir, CONTEXT_FILE);
|
|
1639
|
+
}
|
|
1640
|
+
async getCurrentStatePath() {
|
|
1641
|
+
const dir = await getVemDir();
|
|
1642
|
+
return path9.join(dir, CURRENT_STATE_FILE);
|
|
1643
|
+
}
|
|
1644
|
+
async collectAgentInstructionFiles() {
|
|
1645
|
+
const repoRoot = await getRepoRoot();
|
|
1646
|
+
const files = [];
|
|
1647
|
+
for (const relativePath of KNOWN_AGENT_INSTRUCTION_FILES) {
|
|
1648
|
+
const absolutePath = path9.join(repoRoot, relativePath);
|
|
1649
|
+
if (!await fs9.pathExists(absolutePath))
|
|
1650
|
+
continue;
|
|
1651
|
+
const stat = await fs9.stat(absolutePath);
|
|
1652
|
+
if (!stat.isFile())
|
|
1653
|
+
continue;
|
|
1654
|
+
const content = await fs9.readFile(absolutePath, "utf-8");
|
|
1655
|
+
files.push({ path: relativePath, content });
|
|
1656
|
+
}
|
|
1657
|
+
return files;
|
|
1658
|
+
}
|
|
1659
|
+
async unpackAgentInstructionFiles(entries) {
|
|
1660
|
+
if (!Array.isArray(entries) || entries.length === 0)
|
|
1661
|
+
return;
|
|
1662
|
+
const repoRoot = await getRepoRoot();
|
|
1663
|
+
const resolvedRoot = path9.resolve(repoRoot);
|
|
1664
|
+
for (const entry of entries) {
|
|
1665
|
+
const normalizedPath = normalizeInstructionPath(entry?.path);
|
|
1666
|
+
if (!normalizedPath)
|
|
1667
|
+
continue;
|
|
1668
|
+
if (typeof entry.content !== "string")
|
|
1669
|
+
continue;
|
|
1670
|
+
const destination = path9.resolve(repoRoot, normalizedPath);
|
|
1671
|
+
if (destination !== resolvedRoot && !destination.startsWith(`${resolvedRoot}${path9.sep}`)) {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
await fs9.ensureDir(path9.dirname(destination));
|
|
1675
|
+
await fs9.writeFile(destination, entry.content, "utf-8");
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
async pack() {
|
|
1679
|
+
const tasks = await this.taskService.getTasks();
|
|
1680
|
+
const decisions = await this.decisionsLog.getMonolithicContentWithOptions({
|
|
1681
|
+
includeCommitHashes: true
|
|
1682
|
+
});
|
|
1683
|
+
const changelog = await this.changelogLog.getMonolithicContentWithOptions({
|
|
1684
|
+
includeCommitHashes: true
|
|
1685
|
+
});
|
|
1686
|
+
const secretMatches = [];
|
|
1687
|
+
const addSecretMatch = (path10, value) => {
|
|
1688
|
+
if (!value)
|
|
1689
|
+
return;
|
|
1690
|
+
const types = detectSecrets(value);
|
|
1691
|
+
if (types.length > 0) {
|
|
1692
|
+
secretMatches.push({ path: path10, types });
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
const contextPath = await this.getContextPath();
|
|
1696
|
+
let context = "";
|
|
1697
|
+
if (await fs9.pathExists(contextPath)) {
|
|
1698
|
+
const raw = await fs9.readFile(contextPath, "utf-8");
|
|
1699
|
+
addSecretMatch(".vem/CONTEXT.md", raw);
|
|
1700
|
+
context = redactSecrets(raw);
|
|
1701
|
+
}
|
|
1702
|
+
const currentStatePath = await this.getCurrentStatePath();
|
|
1703
|
+
let currentState = "";
|
|
1704
|
+
if (await fs9.pathExists(currentStatePath)) {
|
|
1705
|
+
const raw = await fs9.readFile(currentStatePath, "utf-8");
|
|
1706
|
+
addSecretMatch(".vem/CURRENT_STATE.md", raw);
|
|
1707
|
+
currentState = redactSecrets(raw);
|
|
1708
|
+
}
|
|
1709
|
+
addSecretMatch(".vem/DECISIONS.md", decisions);
|
|
1710
|
+
addSecretMatch(".vem/CHANGELOG.md", changelog);
|
|
1711
|
+
const agentInstructions = await this.collectAgentInstructionFiles();
|
|
1712
|
+
const redactedAgentInstructions = agentInstructions.map((entry) => {
|
|
1713
|
+
addSecretMatch(entry.path, entry.content);
|
|
1714
|
+
return {
|
|
1715
|
+
path: entry.path,
|
|
1716
|
+
content: redactSecrets(entry.content)
|
|
1717
|
+
};
|
|
1718
|
+
});
|
|
1719
|
+
const redactedTasks = {
|
|
1720
|
+
tasks: tasks.map((task) => ({
|
|
1721
|
+
...task,
|
|
1722
|
+
title: redactSecrets(task.title),
|
|
1723
|
+
description: task.description ? redactSecrets(task.description) : void 0,
|
|
1724
|
+
task_context: task.task_context ? redactSecrets(task.task_context) : void 0,
|
|
1725
|
+
task_context_summary: task.task_context_summary ? redactSecrets(task.task_context_summary) : void 0,
|
|
1726
|
+
evidence: task.evidence?.map((entry) => redactSecrets(entry))
|
|
1727
|
+
}))
|
|
1728
|
+
};
|
|
1729
|
+
for (const task of tasks) {
|
|
1730
|
+
const basePath = `.vem/tasks/objects/${task.id}.json`;
|
|
1731
|
+
addSecretMatch(`${basePath}#title`, task.title);
|
|
1732
|
+
if (task.description) {
|
|
1733
|
+
addSecretMatch(`${basePath}#description`, task.description);
|
|
1734
|
+
}
|
|
1735
|
+
if (task.task_context) {
|
|
1736
|
+
addSecretMatch(`${basePath}#task_context`, task.task_context);
|
|
1737
|
+
}
|
|
1738
|
+
if (task.task_context_summary) {
|
|
1739
|
+
addSecretMatch(`${basePath}#task_context_summary`, task.task_context_summary);
|
|
1740
|
+
}
|
|
1741
|
+
if (task.evidence) {
|
|
1742
|
+
task.evidence.forEach((entry, index) => {
|
|
1743
|
+
addSecretMatch(`${basePath}#evidence[${index}]`, entry);
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return {
|
|
1748
|
+
tasks: redactedTasks,
|
|
1749
|
+
context,
|
|
1750
|
+
decisions: redactSecrets(decisions),
|
|
1751
|
+
changelog: redactSecrets(changelog),
|
|
1752
|
+
current_state: currentState,
|
|
1753
|
+
agent_instructions: redactedAgentInstructions,
|
|
1754
|
+
secret_scan_report: {
|
|
1755
|
+
scanned_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1756
|
+
matches: secretMatches,
|
|
1757
|
+
total: secretMatches.length
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
async buildCompactLog(log, label, entryLimit, entryMaxChars, totalMaxChars) {
|
|
1762
|
+
const entries = await log.getAllEntries();
|
|
1763
|
+
if (entries.length === 0)
|
|
1764
|
+
return "";
|
|
1765
|
+
const selected = entries.slice(0, entryLimit);
|
|
1766
|
+
const blocks = selected.map((entry) => {
|
|
1767
|
+
const body = truncateText(entry.content.trim(), entryMaxChars);
|
|
1768
|
+
return `## ${entry.title}
|
|
1769
|
+
**Date:** ${entry.created_at}
|
|
1770
|
+
|
|
1771
|
+
${body}`;
|
|
1772
|
+
});
|
|
1773
|
+
const header = `_Showing ${selected.length} of ${entries.length} ${label} entries._`;
|
|
1774
|
+
const combined = [header, ...blocks].join("\n\n---\n\n");
|
|
1775
|
+
return truncateText(combined, totalMaxChars);
|
|
1776
|
+
}
|
|
1777
|
+
buildCompactTaskList(tasks, options) {
|
|
1778
|
+
const visibleTasks = tasks.filter((task) => !task.deleted_at);
|
|
1779
|
+
const active = sortByUpdatedAtDesc(visibleTasks.filter((task) => task.status !== "done")).slice(0, options.activeTaskLimit);
|
|
1780
|
+
const recentDone = sortByUpdatedAtDesc(visibleTasks.filter((task) => task.status === "done")).slice(0, options.recentDoneTaskLimit);
|
|
1781
|
+
const selectedById = /* @__PURE__ */ new Map();
|
|
1782
|
+
for (const task of [...active, ...recentDone]) {
|
|
1783
|
+
selectedById.set(task.id, task);
|
|
1784
|
+
}
|
|
1785
|
+
return {
|
|
1786
|
+
tasks: Array.from(selectedById.values()).map((task) => ({
|
|
1787
|
+
id: task.id,
|
|
1788
|
+
title: task.title,
|
|
1789
|
+
status: task.status,
|
|
1790
|
+
priority: task.priority,
|
|
1791
|
+
assignee: task.assignee,
|
|
1792
|
+
type: task.type,
|
|
1793
|
+
description: normalizeTaskText(task.description, options.taskTextMaxChars),
|
|
1794
|
+
task_context_summary: normalizeTaskText(task.task_context_summary, options.taskTextMaxChars),
|
|
1795
|
+
evidence: task.evidence?.slice(0, options.taskEvidenceLimit).map((entry) => truncateText(entry, options.taskTextMaxChars)),
|
|
1796
|
+
validation_steps: task.validation_steps?.slice(0, options.taskValidationLimit),
|
|
1797
|
+
depends_on: task.depends_on,
|
|
1798
|
+
blocked_by: task.blocked_by,
|
|
1799
|
+
created_at: task.created_at,
|
|
1800
|
+
updated_at: task.updated_at,
|
|
1801
|
+
due_at: task.due_at
|
|
1802
|
+
}))
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
async packForAgent(options = {}) {
|
|
1806
|
+
const merged = {
|
|
1807
|
+
...DEFAULT_AGENT_PACK_OPTIONS,
|
|
1808
|
+
...options
|
|
1809
|
+
};
|
|
1810
|
+
const full = await this.pack();
|
|
1811
|
+
const compactTasks = this.buildCompactTaskList(full.tasks.tasks, merged);
|
|
1812
|
+
const compactDecisions = await this.buildCompactLog(this.decisionsLog, "decision", merged.decisionEntryLimit, merged.decisionEntryMaxChars, merged.decisionMaxChars);
|
|
1813
|
+
const compactChangelog = await this.buildCompactLog(this.changelogLog, "changelog", merged.changelogEntryLimit, merged.changelogEntryMaxChars, merged.changelogMaxChars);
|
|
1814
|
+
return {
|
|
1815
|
+
...full,
|
|
1816
|
+
tasks: compactTasks,
|
|
1817
|
+
context: truncateText(full.context, merged.contextMaxChars),
|
|
1818
|
+
current_state: truncateText(full.current_state, merged.currentStateMaxChars),
|
|
1819
|
+
decisions: compactDecisions,
|
|
1820
|
+
changelog: compactChangelog
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
async unpack(payload) {
|
|
1824
|
+
const vemDir = await getVemDir();
|
|
1825
|
+
await fs9.ensureDir(vemDir);
|
|
1826
|
+
const { storage, index } = await this.taskService.init();
|
|
1827
|
+
const taskIds = await storage.listIds();
|
|
1828
|
+
for (const id of taskIds) {
|
|
1829
|
+
await storage.delete(id);
|
|
1830
|
+
}
|
|
1831
|
+
const newIndexEntries = [];
|
|
1832
|
+
for (const task of payload.tasks.tasks) {
|
|
1833
|
+
await storage.save(task);
|
|
1834
|
+
newIndexEntries.push({
|
|
1835
|
+
id: task.id,
|
|
1836
|
+
title: task.title,
|
|
1837
|
+
status: task.status,
|
|
1838
|
+
assignee: task.assignee,
|
|
1839
|
+
priority: task.priority,
|
|
1840
|
+
updated_at: task.updated_at
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
await index.save(newIndexEntries);
|
|
1844
|
+
const contextPath = await this.getContextPath();
|
|
1845
|
+
await fs9.writeFile(contextPath, payload.context, "utf-8");
|
|
1846
|
+
if (payload.decisions) {
|
|
1847
|
+
await this.decisionsLog.addEntry("Imported from Sync", payload.decisions);
|
|
1848
|
+
}
|
|
1849
|
+
if (payload.changelog) {
|
|
1850
|
+
await this.changelogLog.addEntry("Imported from Sync", payload.changelog);
|
|
1851
|
+
}
|
|
1852
|
+
const currentStatePath = await this.getCurrentStatePath();
|
|
1853
|
+
await fs9.writeFile(currentStatePath, payload.current_state ?? "", "utf-8");
|
|
1854
|
+
await this.unpackAgentInstructionFiles(payload.agent_instructions);
|
|
1855
|
+
}
|
|
1856
|
+
async enqueue(payload) {
|
|
1857
|
+
const queueDir = await this.getQueueDir();
|
|
1858
|
+
const id = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}.json`;
|
|
1859
|
+
const filePath = path9.join(queueDir, id);
|
|
1860
|
+
await fs9.writeJson(filePath, payload, { spaces: 2 });
|
|
1861
|
+
return id;
|
|
1862
|
+
}
|
|
1863
|
+
async getQueue() {
|
|
1864
|
+
const queueDir = await this.getQueueDir();
|
|
1865
|
+
const files = await fs9.readdir(queueDir);
|
|
1866
|
+
const queue = [];
|
|
1867
|
+
for (const file of files) {
|
|
1868
|
+
if (file.endsWith(".json")) {
|
|
1869
|
+
try {
|
|
1870
|
+
const payload = await fs9.readJson(path9.join(queueDir, file));
|
|
1871
|
+
queue.push({ id: file, payload });
|
|
1872
|
+
} catch (error) {
|
|
1873
|
+
console.error(`Error reading queued snapshot ${file}:`, error);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return queue.sort((a, b) => a.id.localeCompare(b.id));
|
|
1878
|
+
}
|
|
1879
|
+
async removeFromQueue(id) {
|
|
1880
|
+
const queueDir = await this.getQueueDir();
|
|
1881
|
+
const filePath = path9.join(queueDir, id);
|
|
1882
|
+
if (await fs9.pathExists(filePath)) {
|
|
1883
|
+
await fs9.remove(filePath);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
};
|
|
1887
|
+
|
|
1888
|
+
// ../../packages/core/dist/usage-metrics.js
|
|
1889
|
+
import { join } from "path";
|
|
1890
|
+
import fs10 from "fs-extra";
|
|
1891
|
+
var UsageMetricsService = class _UsageMetricsService {
|
|
1892
|
+
metricsPath = null;
|
|
1893
|
+
baseDir;
|
|
1894
|
+
/**
|
|
1895
|
+
* Power score weights for various features
|
|
1896
|
+
*/
|
|
1897
|
+
static POWER_SCORES = {
|
|
1898
|
+
agent: 30,
|
|
1899
|
+
strict_memory: 20,
|
|
1900
|
+
task_driven: 20,
|
|
1901
|
+
finalize: 15,
|
|
1902
|
+
search: 10,
|
|
1903
|
+
ask: 10,
|
|
1904
|
+
archive: 5
|
|
1905
|
+
};
|
|
1906
|
+
static DEFAULT_SYNC_INTERVAL_MS = 5 * 60 * 1e3;
|
|
1907
|
+
static DEFAULT_SYNC_TIMEOUT_MS = 7e3;
|
|
1908
|
+
constructor(baseDir) {
|
|
1909
|
+
this.baseDir = baseDir;
|
|
1910
|
+
if (baseDir) {
|
|
1911
|
+
this.metricsPath = join(baseDir, ".usage-metrics.json");
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
async getMetricsPath() {
|
|
1915
|
+
if (this.metricsPath) {
|
|
1916
|
+
return this.metricsPath;
|
|
1917
|
+
}
|
|
1918
|
+
const vemDir = await getVemDir();
|
|
1919
|
+
this.metricsPath = join(vemDir, ".usage-metrics.json");
|
|
1920
|
+
return this.metricsPath;
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Track a command execution
|
|
1924
|
+
*/
|
|
1925
|
+
async trackCommand(command) {
|
|
1926
|
+
try {
|
|
1927
|
+
const data = await this.loadMetrics();
|
|
1928
|
+
data.commandCounts[command] = (data.commandCounts[command] || 0) + 1;
|
|
1929
|
+
if (command === "agent") {
|
|
1930
|
+
data.lastAgentRun = Date.now();
|
|
1931
|
+
} else if (command === "push") {
|
|
1932
|
+
data.lastPush = Date.now();
|
|
1933
|
+
}
|
|
1934
|
+
await this.saveMetrics(data);
|
|
1935
|
+
} catch (_error) {
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Track a feature flag usage
|
|
1940
|
+
*/
|
|
1941
|
+
async trackFeature(feature) {
|
|
1942
|
+
try {
|
|
1943
|
+
const data = await this.loadMetrics();
|
|
1944
|
+
data.featureFlags[feature] = true;
|
|
1945
|
+
await this.saveMetrics(data);
|
|
1946
|
+
} catch (_error) {
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Check if a feature has been used
|
|
1951
|
+
*/
|
|
1952
|
+
async hasUsedFeature(feature) {
|
|
1953
|
+
try {
|
|
1954
|
+
const data = await this.loadMetrics();
|
|
1955
|
+
return data.commandCounts[feature] > 0 || data.featureFlags[feature] === true;
|
|
1956
|
+
} catch (_error) {
|
|
1957
|
+
return false;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
/**
|
|
1961
|
+
* Get usage statistics including power score
|
|
1962
|
+
*/
|
|
1963
|
+
async getStats() {
|
|
1964
|
+
try {
|
|
1965
|
+
const data = await this.loadMetrics();
|
|
1966
|
+
const powerScore = this.calculatePowerScore(data);
|
|
1967
|
+
const totalCommands = Object.values(data.commandCounts).reduce((sum, count) => sum + count, 0);
|
|
1968
|
+
return {
|
|
1969
|
+
commandCounts: data.commandCounts,
|
|
1970
|
+
featureFlags: data.featureFlags,
|
|
1971
|
+
lastAgentRun: data.lastAgentRun,
|
|
1972
|
+
lastPush: data.lastPush,
|
|
1973
|
+
powerScore,
|
|
1974
|
+
totalCommands
|
|
1975
|
+
};
|
|
1976
|
+
} catch (_error) {
|
|
1977
|
+
return {
|
|
1978
|
+
commandCounts: {},
|
|
1979
|
+
featureFlags: {},
|
|
1980
|
+
lastAgentRun: null,
|
|
1981
|
+
lastPush: null,
|
|
1982
|
+
powerScore: 0,
|
|
1983
|
+
totalCommands: 0
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* Sync usage metrics to cloud API.
|
|
1989
|
+
* This is best-effort and should never throw.
|
|
1990
|
+
*/
|
|
1991
|
+
async syncToCloud(options) {
|
|
1992
|
+
try {
|
|
1993
|
+
if (!this.isCloudSyncEnabled()) {
|
|
1994
|
+
return { synced: false, reason: "disabled_by_privacy" };
|
|
1995
|
+
}
|
|
1996
|
+
if (!options.apiUrl || !options.apiKey) {
|
|
1997
|
+
return { synced: false, reason: "missing_credentials" };
|
|
1998
|
+
}
|
|
1999
|
+
const data = await this.loadMetrics();
|
|
2000
|
+
const stats = await this.getStats();
|
|
2001
|
+
const signature = JSON.stringify({
|
|
2002
|
+
commandCounts: data.commandCounts,
|
|
2003
|
+
featureFlags: data.featureFlags,
|
|
2004
|
+
lastAgentRun: data.lastAgentRun,
|
|
2005
|
+
lastPush: data.lastPush
|
|
2006
|
+
});
|
|
2007
|
+
const now = Date.now();
|
|
2008
|
+
const minIntervalMs = options.minIntervalMs ?? _UsageMetricsService.DEFAULT_SYNC_INTERVAL_MS;
|
|
2009
|
+
if (!options.force) {
|
|
2010
|
+
if (data.lastSyncedAt && now - data.lastSyncedAt < minIntervalMs) {
|
|
2011
|
+
return { synced: false, reason: "throttled" };
|
|
2012
|
+
}
|
|
2013
|
+
if (data.lastSyncedSignature === signature) {
|
|
2014
|
+
return { synced: false, reason: "unchanged" };
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
2018
|
+
const endpoint = `${apiUrl}/api/metrics/usage`;
|
|
2019
|
+
const timeoutMs = options.timeoutMs ?? _UsageMetricsService.DEFAULT_SYNC_TIMEOUT_MS;
|
|
2020
|
+
const controller = new AbortController();
|
|
2021
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
2022
|
+
const events = options.event && (options.event.command || options.event.featureFlag) ? [
|
|
2023
|
+
{
|
|
2024
|
+
command: options.event.command,
|
|
2025
|
+
feature_flag: options.event.featureFlag,
|
|
2026
|
+
metadata: options.event.metadata,
|
|
2027
|
+
timestamp: options.event.timestamp ?? now
|
|
2028
|
+
}
|
|
2029
|
+
] : [];
|
|
2030
|
+
const response = await fetch(endpoint, {
|
|
2031
|
+
method: "POST",
|
|
2032
|
+
headers: {
|
|
2033
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
2034
|
+
"Content-Type": "application/json",
|
|
2035
|
+
...options.headers ?? {}
|
|
2036
|
+
},
|
|
2037
|
+
body: JSON.stringify({
|
|
2038
|
+
project_id: options.projectId,
|
|
2039
|
+
events,
|
|
2040
|
+
stats: {
|
|
2041
|
+
commandCounts: stats.commandCounts,
|
|
2042
|
+
featureFlags: stats.featureFlags,
|
|
2043
|
+
lastAgentRun: stats.lastAgentRun,
|
|
2044
|
+
lastPush: stats.lastPush,
|
|
2045
|
+
powerScore: stats.powerScore,
|
|
2046
|
+
totalCommands: stats.totalCommands
|
|
2047
|
+
}
|
|
2048
|
+
}),
|
|
2049
|
+
signal: controller.signal
|
|
2050
|
+
}).finally(() => {
|
|
2051
|
+
clearTimeout(timeout);
|
|
2052
|
+
});
|
|
2053
|
+
if (!response.ok) {
|
|
2054
|
+
return {
|
|
2055
|
+
synced: false,
|
|
2056
|
+
reason: `http_${response.status}`
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
data.lastSyncedAt = now;
|
|
2060
|
+
data.lastSyncedSignature = signature;
|
|
2061
|
+
await this.saveMetrics(data);
|
|
2062
|
+
return { synced: true };
|
|
2063
|
+
} catch {
|
|
2064
|
+
return { synced: false, reason: "network_error" };
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Calculate power score based on feature usage
|
|
2069
|
+
*/
|
|
2070
|
+
calculatePowerScore(data) {
|
|
2071
|
+
let score = 0;
|
|
2072
|
+
for (const [command, count] of Object.entries(data.commandCounts)) {
|
|
2073
|
+
if (count > 0 && _UsageMetricsService.POWER_SCORES[command]) {
|
|
2074
|
+
score += _UsageMetricsService.POWER_SCORES[command];
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
for (const [feature, enabled] of Object.entries(data.featureFlags)) {
|
|
2078
|
+
if (enabled && _UsageMetricsService.POWER_SCORES[feature]) {
|
|
2079
|
+
score += _UsageMetricsService.POWER_SCORES[feature];
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
return score;
|
|
2083
|
+
}
|
|
2084
|
+
/**
|
|
2085
|
+
* Load metrics from disk
|
|
2086
|
+
*/
|
|
2087
|
+
async loadMetrics() {
|
|
2088
|
+
try {
|
|
2089
|
+
const metricsPath = await this.getMetricsPath();
|
|
2090
|
+
if (await fs10.pathExists(metricsPath)) {
|
|
2091
|
+
const content = await fs10.readFile(metricsPath, "utf-8");
|
|
2092
|
+
return JSON.parse(content);
|
|
2093
|
+
}
|
|
2094
|
+
} catch (_error) {
|
|
2095
|
+
}
|
|
2096
|
+
return {
|
|
2097
|
+
commandCounts: {},
|
|
2098
|
+
featureFlags: {},
|
|
2099
|
+
lastAgentRun: null,
|
|
2100
|
+
lastPush: null,
|
|
2101
|
+
lastSyncedAt: null,
|
|
2102
|
+
lastSyncedSignature: null
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Save metrics to disk
|
|
2107
|
+
*/
|
|
2108
|
+
async saveMetrics(data) {
|
|
2109
|
+
const metricsPath = await this.getMetricsPath();
|
|
2110
|
+
await fs10.writeFile(metricsPath, JSON.stringify(data, null, 2), "utf-8");
|
|
2111
|
+
}
|
|
2112
|
+
isCloudSyncEnabled() {
|
|
2113
|
+
const disabled = (process.env.VEM_DISABLE_METRICS || "").toLowerCase();
|
|
2114
|
+
if (disabled === "1" || disabled === "true" || disabled === "yes") {
|
|
2115
|
+
return false;
|
|
2116
|
+
}
|
|
2117
|
+
const privacyMode = (process.env.VEM_PRIVACY_MODE || "").toLowerCase();
|
|
2118
|
+
if (privacyMode === "strict" || privacyMode === "local-only") {
|
|
2119
|
+
return false;
|
|
2120
|
+
}
|
|
2121
|
+
return true;
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
|
|
2125
|
+
// ../../packages/core/dist/webhook.js
|
|
2126
|
+
import crypto2 from "crypto";
|
|
2127
|
+
import dns from "dns";
|
|
2128
|
+
|
|
2129
|
+
// src/index.ts
|
|
2130
|
+
var server = new Server(
|
|
2131
|
+
{
|
|
2132
|
+
name: "vem-mcp",
|
|
2133
|
+
version: "0.1.0"
|
|
2134
|
+
},
|
|
2135
|
+
{
|
|
2136
|
+
capabilities: {
|
|
2137
|
+
tools: {}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
);
|
|
2141
|
+
var taskService = new TaskService();
|
|
2142
|
+
var configService = new ConfigService();
|
|
2143
|
+
var syncService = new SyncService();
|
|
2144
|
+
var metricsService = new UsageMetricsService();
|
|
2145
|
+
var API_URL = process.env.VEM_API_URL || "http://localhost:3002";
|
|
2146
|
+
async function trackHeartbeat(toolName, taskId) {
|
|
2147
|
+
try {
|
|
2148
|
+
const apiKey = await configService.getApiKey();
|
|
2149
|
+
if (!apiKey) return;
|
|
2150
|
+
const projectId = await configService.getProjectId();
|
|
2151
|
+
const agentName = process.env.VEM_AGENT_NAME || "mcp-agent";
|
|
2152
|
+
await metricsService.syncToCloud({
|
|
2153
|
+
apiUrl: API_URL,
|
|
2154
|
+
apiKey,
|
|
2155
|
+
projectId,
|
|
2156
|
+
headers: await buildDeviceHeaders(configService),
|
|
2157
|
+
force: true,
|
|
2158
|
+
event: {
|
|
2159
|
+
featureFlag: "agent_heartbeat",
|
|
2160
|
+
metadata: {
|
|
2161
|
+
agentName,
|
|
2162
|
+
taskId,
|
|
2163
|
+
command: `mcp:${toolName}`
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
});
|
|
2167
|
+
} catch {
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
function getGitHash() {
|
|
2171
|
+
try {
|
|
2172
|
+
return execSync2("git rev-parse HEAD").toString().trim() || null;
|
|
2173
|
+
} catch {
|
|
2174
|
+
return null;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
function getGitRemote() {
|
|
2178
|
+
try {
|
|
2179
|
+
return execSync2("git remote get-url origin").toString().trim() || null;
|
|
2180
|
+
} catch {
|
|
2181
|
+
return null;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function getCommits(limit = 50) {
|
|
2185
|
+
try {
|
|
2186
|
+
const output = execSync2(
|
|
2187
|
+
`git log -n ${limit} --pretty=format:"%H|%an|%cI|%s"`
|
|
2188
|
+
).toString();
|
|
2189
|
+
return output.split("\n").map((line) => {
|
|
2190
|
+
const [hash, author, date, ...msgParts] = line.split("|");
|
|
2191
|
+
return {
|
|
2192
|
+
hash,
|
|
2193
|
+
author_name: author,
|
|
2194
|
+
committed_at: date,
|
|
2195
|
+
message: msgParts.join("|")
|
|
2196
|
+
};
|
|
2197
|
+
}).filter((c) => c.hash && c.message);
|
|
2198
|
+
} catch {
|
|
2199
|
+
return [];
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
async function isVemDirty() {
|
|
2203
|
+
try {
|
|
2204
|
+
const root = await getRepoRoot();
|
|
2205
|
+
const status = execSync2("git status --porcelain .vem", { cwd: root }).toString().trim();
|
|
2206
|
+
return status.length > 0;
|
|
2207
|
+
} catch {
|
|
2208
|
+
return false;
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
async function computeVemHash() {
|
|
2212
|
+
try {
|
|
2213
|
+
const vemDir = await getVemDir();
|
|
2214
|
+
const hash = createHash4("sha256");
|
|
2215
|
+
const walk = async (currentDir) => {
|
|
2216
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
2217
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2218
|
+
for (const entry of entries) {
|
|
2219
|
+
if (entry.name === "queue") continue;
|
|
2220
|
+
const fullPath = join2(currentDir, entry.name);
|
|
2221
|
+
const relPath = relative(vemDir, fullPath).split("\\").join("/");
|
|
2222
|
+
if (relPath === "queue" || relPath.startsWith("queue/") || relPath === "config.json" || relPath === "current_context.md" || relPath === "task_context.md") {
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
if (entry.isDirectory()) {
|
|
2226
|
+
hash.update(`dir:${relPath}\0`);
|
|
2227
|
+
await walk(fullPath);
|
|
2228
|
+
} else if (entry.isFile()) {
|
|
2229
|
+
hash.update(`file:${relPath}\0`);
|
|
2230
|
+
const data = await readFile(fullPath);
|
|
2231
|
+
hash.update(data);
|
|
2232
|
+
} else if (entry.isSymbolicLink()) {
|
|
2233
|
+
const target = await readlink(fullPath);
|
|
2234
|
+
hash.update(`link:${relPath}\0${target}\0`);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
};
|
|
2238
|
+
await walk(vemDir);
|
|
2239
|
+
return hash.digest("hex");
|
|
2240
|
+
} catch {
|
|
2241
|
+
return null;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
function normalizeForSnapshotHash(value) {
|
|
2245
|
+
if (typeof value === "string") return value.normalize("NFC");
|
|
2246
|
+
if (Array.isArray(value))
|
|
2247
|
+
return value.map((entry) => normalizeForSnapshotHash(entry));
|
|
2248
|
+
if (value && typeof value === "object") {
|
|
2249
|
+
const record = value;
|
|
2250
|
+
const normalized = {};
|
|
2251
|
+
for (const key of Object.keys(record).sort((a, b) => a.localeCompare(b))) {
|
|
2252
|
+
const next = normalizeForSnapshotHash(record[key]);
|
|
2253
|
+
if (next !== void 0) normalized[key] = next;
|
|
2254
|
+
}
|
|
2255
|
+
return normalized;
|
|
2256
|
+
}
|
|
2257
|
+
if (typeof value === "number" && !Number.isFinite(value)) return null;
|
|
2258
|
+
return value;
|
|
2259
|
+
}
|
|
2260
|
+
function computeSnapshotHashFromPayload(snapshot) {
|
|
2261
|
+
const sortedTasks = Array.isArray(snapshot?.tasks?.tasks) ? [...snapshot.tasks.tasks].sort(
|
|
2262
|
+
(a, b) => String(a?.id || "").localeCompare(String(b?.id || ""))
|
|
2263
|
+
) : [];
|
|
2264
|
+
const canonical = normalizeForSnapshotHash({
|
|
2265
|
+
tasks: { tasks: sortedTasks },
|
|
2266
|
+
context: snapshot?.context || "",
|
|
2267
|
+
decisions: snapshot?.decisions || "",
|
|
2268
|
+
changelog: snapshot?.changelog || "",
|
|
2269
|
+
current_state: snapshot?.current_state || ""
|
|
2270
|
+
});
|
|
2271
|
+
return createHash4("sha256").update(JSON.stringify(canonical), "utf8").digest("hex");
|
|
2272
|
+
}
|
|
2273
|
+
async function buildDeviceHeaders(cs) {
|
|
2274
|
+
const { deviceId, deviceName } = await cs.getOrCreateDeviceId();
|
|
2275
|
+
const projectOrgId = await cs.getProjectOrgId();
|
|
2276
|
+
return {
|
|
2277
|
+
"X-Vem-Device-Id": deviceId,
|
|
2278
|
+
"X-Vem-Device-Name": deviceName,
|
|
2279
|
+
"X-Vem-Client": "mcp-server",
|
|
2280
|
+
...projectOrgId ? { "X-Org-Id": projectOrgId } : {}
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2284
|
+
return {
|
|
2285
|
+
tools: [
|
|
2286
|
+
{
|
|
2287
|
+
name: "get_active_tasks",
|
|
2288
|
+
description: "Get list of VEM tasks from project memory. Use this to know what to do. Defaults to active tasks only.",
|
|
2289
|
+
inputSchema: {
|
|
2290
|
+
type: "object",
|
|
2291
|
+
properties: {
|
|
2292
|
+
status: {
|
|
2293
|
+
type: "string",
|
|
2294
|
+
enum: ["todo", "in-progress", "done", "all"],
|
|
2295
|
+
description: "Filter tasks by status. Default is to exclude done tasks."
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
},
|
|
2300
|
+
{
|
|
2301
|
+
name: "get_task_details",
|
|
2302
|
+
description: "Get detailed information about a specific task by its ID.",
|
|
2303
|
+
inputSchema: {
|
|
2304
|
+
type: "object",
|
|
2305
|
+
properties: {
|
|
2306
|
+
id: { type: "string", description: "The TASK-XXX id" }
|
|
2307
|
+
},
|
|
2308
|
+
required: ["id"]
|
|
2309
|
+
}
|
|
2310
|
+
},
|
|
2311
|
+
{
|
|
2312
|
+
name: "add_task",
|
|
2313
|
+
description: "Add a new task to VEM memory.",
|
|
2314
|
+
inputSchema: {
|
|
2315
|
+
type: "object",
|
|
2316
|
+
properties: {
|
|
2317
|
+
title: { type: "string" },
|
|
2318
|
+
description: { type: "string" },
|
|
2319
|
+
priority: {
|
|
2320
|
+
type: "string",
|
|
2321
|
+
enum: ["low", "medium", "high", "critical"]
|
|
2322
|
+
},
|
|
2323
|
+
type: {
|
|
2324
|
+
type: "string",
|
|
2325
|
+
enum: ["feature", "bug", "chore"]
|
|
2326
|
+
},
|
|
2327
|
+
parent_id: {
|
|
2328
|
+
type: "string",
|
|
2329
|
+
description: "Optional parent task ID"
|
|
2330
|
+
},
|
|
2331
|
+
validation_steps: {
|
|
2332
|
+
type: "array",
|
|
2333
|
+
items: { type: "string" },
|
|
2334
|
+
description: "List of commands or steps to validate completion"
|
|
2335
|
+
},
|
|
2336
|
+
reasoning: {
|
|
2337
|
+
type: "string",
|
|
2338
|
+
description: "Reasoning for creating this task."
|
|
2339
|
+
}
|
|
2340
|
+
},
|
|
2341
|
+
required: ["title"]
|
|
2342
|
+
}
|
|
2343
|
+
},
|
|
2344
|
+
{
|
|
2345
|
+
name: "complete_task",
|
|
2346
|
+
description: "Mark a task as done in VEM memory.",
|
|
2347
|
+
inputSchema: {
|
|
2348
|
+
type: "object",
|
|
2349
|
+
properties: {
|
|
2350
|
+
id: { type: "string", description: "The TASK-XXX id" },
|
|
2351
|
+
evidence: {
|
|
2352
|
+
type: "string",
|
|
2353
|
+
description: "Proof of completion (file path, test command, etc)"
|
|
2354
|
+
},
|
|
2355
|
+
reasoning: {
|
|
2356
|
+
type: "string",
|
|
2357
|
+
description: "Why is this task considered done? Explain your reasoning."
|
|
2358
|
+
}
|
|
2359
|
+
},
|
|
2360
|
+
required: ["id", "evidence", "reasoning"]
|
|
2361
|
+
}
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
name: "start_task",
|
|
2365
|
+
description: "Mark a task as in-progress and attach the current agent session to it. Call this when you start working on a task. Returns the full task object so you can get its context.",
|
|
2366
|
+
inputSchema: {
|
|
2367
|
+
type: "object",
|
|
2368
|
+
properties: {
|
|
2369
|
+
id: {
|
|
2370
|
+
type: "string",
|
|
2371
|
+
description: "The TASK-XXX id to start"
|
|
2372
|
+
},
|
|
2373
|
+
session_id: {
|
|
2374
|
+
type: "string",
|
|
2375
|
+
description: "The current agent session ID"
|
|
2376
|
+
},
|
|
2377
|
+
source: {
|
|
2378
|
+
type: "string",
|
|
2379
|
+
enum: ["copilot", "claude", "gemini"],
|
|
2380
|
+
description: "Which AI tool is running (copilot | claude | gemini)"
|
|
2381
|
+
},
|
|
2382
|
+
session_summary: {
|
|
2383
|
+
type: "string",
|
|
2384
|
+
description: "Optional brief summary of what this session intends to do"
|
|
2385
|
+
}
|
|
2386
|
+
},
|
|
2387
|
+
required: ["id"]
|
|
2388
|
+
}
|
|
2389
|
+
},
|
|
2390
|
+
{
|
|
2391
|
+
name: "get_context",
|
|
2392
|
+
description: "Read the project's CONTEXT.md and CURRENT_STATE.md. Always call this at the start of a session to load project context.",
|
|
2393
|
+
inputSchema: {
|
|
2394
|
+
type: "object",
|
|
2395
|
+
properties: {}
|
|
2396
|
+
}
|
|
2397
|
+
},
|
|
2398
|
+
{
|
|
2399
|
+
name: "list_decisions",
|
|
2400
|
+
description: "List architectural decisions recorded in VEM memory. Read these before making significant design choices to avoid conflicts.",
|
|
2401
|
+
inputSchema: {
|
|
2402
|
+
type: "object",
|
|
2403
|
+
properties: {
|
|
2404
|
+
limit: {
|
|
2405
|
+
type: "number",
|
|
2406
|
+
description: "Number of recent decisions to return (default: 20, max: 50)",
|
|
2407
|
+
default: 20
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
},
|
|
2412
|
+
{
|
|
2413
|
+
name: "update_current_state",
|
|
2414
|
+
description: "Update CURRENT_STATE.md with a summary of latest progress and next steps. Call this after completing meaningful work.",
|
|
2415
|
+
inputSchema: {
|
|
2416
|
+
type: "object",
|
|
2417
|
+
properties: {
|
|
2418
|
+
content: {
|
|
2419
|
+
type: "string",
|
|
2420
|
+
description: "The new content for CURRENT_STATE.md (replaces existing content)"
|
|
2421
|
+
}
|
|
2422
|
+
},
|
|
2423
|
+
required: ["content"]
|
|
2424
|
+
}
|
|
2425
|
+
},
|
|
2426
|
+
{
|
|
2427
|
+
name: "search_memory",
|
|
2428
|
+
description: "Semantic search over project memory (tasks, context, decisions).",
|
|
2429
|
+
inputSchema: {
|
|
2430
|
+
type: "object",
|
|
2431
|
+
properties: {
|
|
2432
|
+
query: {
|
|
2433
|
+
type: "string",
|
|
2434
|
+
description: "The search query (e.g. 'auth tasks', 'database schema')"
|
|
2435
|
+
}
|
|
2436
|
+
},
|
|
2437
|
+
required: ["query"]
|
|
2438
|
+
}
|
|
2439
|
+
},
|
|
2440
|
+
{
|
|
2441
|
+
name: "ask_question",
|
|
2442
|
+
description: "Ask a natural language question about the project codebase and memory (commits, diffs, tasks).",
|
|
2443
|
+
inputSchema: {
|
|
2444
|
+
type: "object",
|
|
2445
|
+
properties: {
|
|
2446
|
+
question: {
|
|
2447
|
+
type: "string",
|
|
2448
|
+
description: "The question to ask (e.g. 'How does authentication work?')"
|
|
2449
|
+
},
|
|
2450
|
+
path: {
|
|
2451
|
+
type: "string",
|
|
2452
|
+
description: "Optional file path to limit the scope of the search"
|
|
2453
|
+
}
|
|
2454
|
+
},
|
|
2455
|
+
required: ["question"]
|
|
2456
|
+
}
|
|
2457
|
+
},
|
|
2458
|
+
{
|
|
2459
|
+
name: "update_task",
|
|
2460
|
+
description: "Update a task's status, priority, or other fields. Use this to start working on a task, block it, or update its priority.",
|
|
2461
|
+
inputSchema: {
|
|
2462
|
+
type: "object",
|
|
2463
|
+
properties: {
|
|
2464
|
+
id: {
|
|
2465
|
+
type: "string",
|
|
2466
|
+
description: "The TASK-XXX id"
|
|
2467
|
+
},
|
|
2468
|
+
status: {
|
|
2469
|
+
type: "string",
|
|
2470
|
+
enum: ["todo", "in-progress", "blocked"],
|
|
2471
|
+
description: "New status for the task. Use 'in-progress' to start working, 'blocked' to mark as blocked, 'todo' to unblock."
|
|
2472
|
+
},
|
|
2473
|
+
priority: {
|
|
2474
|
+
type: "string",
|
|
2475
|
+
enum: ["low", "medium", "high", "critical"],
|
|
2476
|
+
description: "New priority for the task."
|
|
2477
|
+
},
|
|
2478
|
+
reasoning: {
|
|
2479
|
+
type: "string",
|
|
2480
|
+
description: "Explanation for the update. Required when blocking a task."
|
|
2481
|
+
},
|
|
2482
|
+
blocked_by: {
|
|
2483
|
+
type: "array",
|
|
2484
|
+
items: { type: "string" },
|
|
2485
|
+
description: "Task IDs that are blocking this task (when setting status to blocked)."
|
|
2486
|
+
},
|
|
2487
|
+
validation_steps: {
|
|
2488
|
+
type: "array",
|
|
2489
|
+
items: { type: "string" },
|
|
2490
|
+
description: "New list of validation steps (replaces existing)"
|
|
2491
|
+
}
|
|
2492
|
+
},
|
|
2493
|
+
required: ["id"]
|
|
2494
|
+
}
|
|
2495
|
+
},
|
|
2496
|
+
{
|
|
2497
|
+
name: "add_decision",
|
|
2498
|
+
description: "Record an architectural decision in VEM memory. Use this when making significant technical choices (e.g., choosing libraries, changing architecture, setting patterns).",
|
|
2499
|
+
inputSchema: {
|
|
2500
|
+
type: "object",
|
|
2501
|
+
properties: {
|
|
2502
|
+
title: {
|
|
2503
|
+
type: "string",
|
|
2504
|
+
description: "Short decision title (e.g., 'Use Zod for validation')"
|
|
2505
|
+
},
|
|
2506
|
+
context: {
|
|
2507
|
+
type: "string",
|
|
2508
|
+
description: "Why this decision was needed - the problem or situation"
|
|
2509
|
+
},
|
|
2510
|
+
decision: {
|
|
2511
|
+
type: "string",
|
|
2512
|
+
description: "What was decided and key rationale"
|
|
2513
|
+
},
|
|
2514
|
+
related_tasks: {
|
|
2515
|
+
type: "array",
|
|
2516
|
+
items: { type: "string" },
|
|
2517
|
+
description: "Optional TASK-XXX references that motivated or implement this decision"
|
|
2518
|
+
}
|
|
2519
|
+
},
|
|
2520
|
+
required: ["title", "context", "decision"]
|
|
2521
|
+
}
|
|
2522
|
+
},
|
|
2523
|
+
{
|
|
2524
|
+
name: "get_changelog",
|
|
2525
|
+
description: "Get recent changelog entries from VEM memory. Use this to understand what work has been done recently.",
|
|
2526
|
+
inputSchema: {
|
|
2527
|
+
type: "object",
|
|
2528
|
+
properties: {
|
|
2529
|
+
limit: {
|
|
2530
|
+
type: "number",
|
|
2531
|
+
description: "Number of recent entries to return (default: 10, max: 50)",
|
|
2532
|
+
default: 10
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
},
|
|
2537
|
+
{
|
|
2538
|
+
name: "delete_task",
|
|
2539
|
+
description: "Soft delete a task from VEM memory.",
|
|
2540
|
+
inputSchema: {
|
|
2541
|
+
type: "object",
|
|
2542
|
+
properties: {
|
|
2543
|
+
id: { type: "string", description: "The TASK-XXX id" },
|
|
2544
|
+
reasoning: { type: "string", description: "Reason for deletion" }
|
|
2545
|
+
},
|
|
2546
|
+
required: ["id"]
|
|
2547
|
+
}
|
|
2548
|
+
},
|
|
2549
|
+
{
|
|
2550
|
+
name: "apply_vem_update",
|
|
2551
|
+
description: "Apply a complete vem_update block containing current_state, context, changelog_append, decisions_append, and task updates.",
|
|
2552
|
+
inputSchema: {
|
|
2553
|
+
type: "object",
|
|
2554
|
+
properties: {
|
|
2555
|
+
current_state: { type: "string" },
|
|
2556
|
+
context: { type: "string" },
|
|
2557
|
+
changelog_append: {
|
|
2558
|
+
oneOf: [
|
|
2559
|
+
{ type: "string" },
|
|
2560
|
+
{ type: "array", items: { type: "string" } }
|
|
2561
|
+
]
|
|
2562
|
+
},
|
|
2563
|
+
decisions_append: {
|
|
2564
|
+
oneOf: [
|
|
2565
|
+
{ type: "string" },
|
|
2566
|
+
{ type: "array", items: { type: "string" } }
|
|
2567
|
+
]
|
|
2568
|
+
},
|
|
2569
|
+
tasks: {
|
|
2570
|
+
type: "array",
|
|
2571
|
+
items: {
|
|
2572
|
+
type: "object",
|
|
2573
|
+
properties: {
|
|
2574
|
+
id: { type: "string" },
|
|
2575
|
+
status: { type: "string" },
|
|
2576
|
+
reasoning: { type: "string" },
|
|
2577
|
+
priority: { type: "string" }
|
|
2578
|
+
},
|
|
2579
|
+
required: ["id"]
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
},
|
|
2585
|
+
{
|
|
2586
|
+
name: "sync_push",
|
|
2587
|
+
description: "Push local VEM snapshot to the cloud. Syncs tasks, context, decisions, and changelog to the remote project.",
|
|
2588
|
+
inputSchema: {
|
|
2589
|
+
type: "object",
|
|
2590
|
+
properties: {
|
|
2591
|
+
force: {
|
|
2592
|
+
type: "boolean",
|
|
2593
|
+
description: "Push even if no changes detected since last push."
|
|
2594
|
+
},
|
|
2595
|
+
dry_run: {
|
|
2596
|
+
type: "boolean",
|
|
2597
|
+
description: "Preview what would be pushed without actually pushing."
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
},
|
|
2602
|
+
{
|
|
2603
|
+
name: "sync_pull",
|
|
2604
|
+
description: "Pull the latest VEM snapshot from the cloud. Updates local tasks, context, decisions, and changelog.",
|
|
2605
|
+
inputSchema: {
|
|
2606
|
+
type: "object",
|
|
2607
|
+
properties: {
|
|
2608
|
+
force: {
|
|
2609
|
+
type: "boolean",
|
|
2610
|
+
description: "Overwrite local changes without warning."
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
},
|
|
2615
|
+
{
|
|
2616
|
+
name: "get_task_context",
|
|
2617
|
+
description: "Read a task's working context and summary. Use this to retrieve scratchpad notes for a specific task.",
|
|
2618
|
+
inputSchema: {
|
|
2619
|
+
type: "object",
|
|
2620
|
+
properties: {
|
|
2621
|
+
id: {
|
|
2622
|
+
type: "string",
|
|
2623
|
+
description: "The TASK-XXX id"
|
|
2624
|
+
}
|
|
2625
|
+
},
|
|
2626
|
+
required: ["id"]
|
|
2627
|
+
}
|
|
2628
|
+
},
|
|
2629
|
+
{
|
|
2630
|
+
name: "update_task_context",
|
|
2631
|
+
description: "Set, append to, or clear a task's working context. Use this to maintain scratchpad notes across sessions.",
|
|
2632
|
+
inputSchema: {
|
|
2633
|
+
type: "object",
|
|
2634
|
+
properties: {
|
|
2635
|
+
id: {
|
|
2636
|
+
type: "string",
|
|
2637
|
+
description: "The TASK-XXX id"
|
|
2638
|
+
},
|
|
2639
|
+
operation: {
|
|
2640
|
+
type: "string",
|
|
2641
|
+
enum: ["set", "append", "clear"],
|
|
2642
|
+
description: "'set' replaces context, 'append' adds to it, 'clear' removes it."
|
|
2643
|
+
},
|
|
2644
|
+
text: {
|
|
2645
|
+
type: "string",
|
|
2646
|
+
description: "The text content. Required for 'set' and 'append' operations."
|
|
2647
|
+
}
|
|
2648
|
+
},
|
|
2649
|
+
required: ["id", "operation"]
|
|
2650
|
+
}
|
|
2651
|
+
},
|
|
2652
|
+
{
|
|
2653
|
+
name: "get_subtasks",
|
|
2654
|
+
description: "Get a parent task and all its subtasks. Use this to view the task hierarchy.",
|
|
2655
|
+
inputSchema: {
|
|
2656
|
+
type: "object",
|
|
2657
|
+
properties: {
|
|
2658
|
+
parent_id: {
|
|
2659
|
+
type: "string",
|
|
2660
|
+
description: "The parent TASK-XXX id"
|
|
2661
|
+
}
|
|
2662
|
+
},
|
|
2663
|
+
required: ["parent_id"]
|
|
2664
|
+
}
|
|
2665
|
+
},
|
|
2666
|
+
{
|
|
2667
|
+
name: "list_agent_sessions",
|
|
2668
|
+
description: "List recent agent sessions for this repository from Copilot CLI, Claude CLI, and Gemini CLI. Call this at the start of a session to understand what previous sessions worked on and avoid duplicating effort.",
|
|
2669
|
+
inputSchema: {
|
|
2670
|
+
type: "object",
|
|
2671
|
+
properties: {
|
|
2672
|
+
limit: {
|
|
2673
|
+
type: "number",
|
|
2674
|
+
description: "Maximum number of sessions to return (default: 10)"
|
|
2675
|
+
},
|
|
2676
|
+
branch: {
|
|
2677
|
+
type: "string",
|
|
2678
|
+
description: "Filter sessions by branch name"
|
|
2679
|
+
},
|
|
2680
|
+
sources: {
|
|
2681
|
+
type: "array",
|
|
2682
|
+
items: { type: "string", enum: ["copilot", "claude", "gemini"] },
|
|
2683
|
+
description: "Which tools to include (default: all three)"
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
},
|
|
2688
|
+
{
|
|
2689
|
+
name: "save_session_stats",
|
|
2690
|
+
description: "Save statistics for an agent session onto its task. Call this when finishing a task to record session duration, turns, tool calls, and model usage. Stats are computed from the agent's session files.",
|
|
2691
|
+
inputSchema: {
|
|
2692
|
+
type: "object",
|
|
2693
|
+
properties: {
|
|
2694
|
+
task_id: {
|
|
2695
|
+
type: "string",
|
|
2696
|
+
description: "The TASK-XXX id to attach stats to"
|
|
2697
|
+
},
|
|
2698
|
+
session_id: {
|
|
2699
|
+
type: "string",
|
|
2700
|
+
description: "The agent session ID to compute stats for"
|
|
2701
|
+
},
|
|
2702
|
+
source: {
|
|
2703
|
+
type: "string",
|
|
2704
|
+
enum: ["copilot", "claude", "gemini"],
|
|
2705
|
+
description: "Which AI tool the session belongs to (default: copilot)"
|
|
2706
|
+
}
|
|
2707
|
+
},
|
|
2708
|
+
required: ["task_id", "session_id"]
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
]
|
|
2712
|
+
};
|
|
2713
|
+
});
|
|
2714
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2715
|
+
const { name, arguments: args } = request.params;
|
|
2716
|
+
const taskId = args?.id || args?.taskId || process.env.VEM_ACTIVE_TASK;
|
|
2717
|
+
void trackHeartbeat(name, taskId);
|
|
2718
|
+
if (name === "get_active_tasks") {
|
|
2719
|
+
const tasks = await taskService.getTasks();
|
|
2720
|
+
const status = args?.status;
|
|
2721
|
+
let filtered = tasks;
|
|
2722
|
+
if (status === "all") {
|
|
2723
|
+
} else if (status === "done") {
|
|
2724
|
+
filtered = tasks.filter((t) => t.status === "done");
|
|
2725
|
+
} else if (status === "todo") {
|
|
2726
|
+
filtered = tasks.filter((t) => t.status === "todo");
|
|
2727
|
+
} else if (status === "in-progress") {
|
|
2728
|
+
filtered = tasks.filter((t) => t.status === "in-progress");
|
|
2729
|
+
} else {
|
|
2730
|
+
filtered = tasks.filter((t) => t.status !== "done");
|
|
2731
|
+
}
|
|
2732
|
+
const annotated = filtered.map((t) => {
|
|
2733
|
+
const sessions = t.sessions || [];
|
|
2734
|
+
if (sessions.length === 0) return t;
|
|
2735
|
+
const last = sessions[sessions.length - 1];
|
|
2736
|
+
const hint = last.summary ? `Last worked by ${last.source} on ${last.started_at.slice(0, 10)}: ${last.summary}` : `Last worked by ${last.source} on ${last.started_at.slice(0, 10)}`;
|
|
2737
|
+
return { ...t, _last_session_hint: hint };
|
|
2738
|
+
});
|
|
2739
|
+
return {
|
|
2740
|
+
content: [
|
|
2741
|
+
{
|
|
2742
|
+
type: "text",
|
|
2743
|
+
text: JSON.stringify(annotated, null, 2)
|
|
2744
|
+
}
|
|
2745
|
+
]
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
if (name === "get_task_details") {
|
|
2749
|
+
const id = args?.id;
|
|
2750
|
+
if (!id) {
|
|
2751
|
+
return {
|
|
2752
|
+
isError: true,
|
|
2753
|
+
content: [{ type: "text", text: "Task ID is required." }]
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
const task = await taskService.getTask(id);
|
|
2757
|
+
if (!task) {
|
|
2758
|
+
return {
|
|
2759
|
+
isError: true,
|
|
2760
|
+
content: [{ type: "text", text: `Task ${id} not found.` }]
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
return {
|
|
2764
|
+
content: [{ type: "text", text: JSON.stringify(task, null, 2) }]
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
if (name === "add_task") {
|
|
2768
|
+
const title = args?.title;
|
|
2769
|
+
const description = args?.description || void 0;
|
|
2770
|
+
const priority = args?.priority || "medium";
|
|
2771
|
+
const reasoning = args?.reasoning || void 0;
|
|
2772
|
+
const type = args?.type || void 0;
|
|
2773
|
+
const parentId = args?.parent_id || void 0;
|
|
2774
|
+
const validationSteps = args?.validation_steps || void 0;
|
|
2775
|
+
const task = await taskService.addTask(
|
|
2776
|
+
title,
|
|
2777
|
+
description,
|
|
2778
|
+
priority,
|
|
2779
|
+
reasoning,
|
|
2780
|
+
{
|
|
2781
|
+
type,
|
|
2782
|
+
parent_id: parentId,
|
|
2783
|
+
validation_steps: validationSteps
|
|
2784
|
+
}
|
|
2785
|
+
);
|
|
2786
|
+
return {
|
|
2787
|
+
content: [
|
|
2788
|
+
{ type: "text", text: `Task Added: ${task.id} (${task.title})` }
|
|
2789
|
+
]
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
if (name === "complete_task") {
|
|
2793
|
+
const id = args?.id;
|
|
2794
|
+
const evidence = String(args?.evidence || "").trim();
|
|
2795
|
+
const reasoning = String(args?.reasoning || "").trim();
|
|
2796
|
+
if (!evidence) {
|
|
2797
|
+
return {
|
|
2798
|
+
isError: true,
|
|
2799
|
+
content: [
|
|
2800
|
+
{
|
|
2801
|
+
type: "text",
|
|
2802
|
+
text: "Evidence is required to complete a task."
|
|
2803
|
+
}
|
|
2804
|
+
]
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
if (!reasoning) {
|
|
2808
|
+
return {
|
|
2809
|
+
isError: true,
|
|
2810
|
+
content: [
|
|
2811
|
+
{
|
|
2812
|
+
type: "text",
|
|
2813
|
+
text: "Reasoning is required to complete a task."
|
|
2814
|
+
}
|
|
2815
|
+
]
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
await taskService.updateTask(id, {
|
|
2819
|
+
status: "done",
|
|
2820
|
+
evidence: [evidence],
|
|
2821
|
+
reasoning
|
|
2822
|
+
});
|
|
2823
|
+
const vemDir = await getVemDir();
|
|
2824
|
+
await writeFile(join2(vemDir, "exit_signal"), "", "utf-8");
|
|
2825
|
+
return {
|
|
2826
|
+
content: [
|
|
2827
|
+
{
|
|
2828
|
+
type: "text",
|
|
2829
|
+
text: `Task ${id} marked as DONE. CLI will auto-close when the agent session ends.`
|
|
2830
|
+
}
|
|
2831
|
+
]
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
if (name === "start_task") {
|
|
2835
|
+
const id = args?.id;
|
|
2836
|
+
const sessionId = args?.session_id;
|
|
2837
|
+
const source = args?.source || "copilot";
|
|
2838
|
+
const sessionSummary = args?.session_summary;
|
|
2839
|
+
if (!id) {
|
|
2840
|
+
return {
|
|
2841
|
+
isError: true,
|
|
2842
|
+
content: [{ type: "text", text: "Task ID is required." }]
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
const task = await taskService.getTask(id);
|
|
2846
|
+
if (!task) {
|
|
2847
|
+
return {
|
|
2848
|
+
isError: true,
|
|
2849
|
+
content: [{ type: "text", text: `Task ${id} not found.` }]
|
|
2850
|
+
};
|
|
2851
|
+
}
|
|
2852
|
+
const sessionRef = sessionId ? {
|
|
2853
|
+
id: sessionId,
|
|
2854
|
+
source,
|
|
2855
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2856
|
+
...sessionSummary ? { summary: sessionSummary } : {}
|
|
2857
|
+
} : null;
|
|
2858
|
+
const existingSessions = task.sessions || [];
|
|
2859
|
+
const patch = {
|
|
2860
|
+
status: "in-progress",
|
|
2861
|
+
reasoning: sessionSummary ? `Started by ${source} session: ${sessionSummary}` : `Started by ${source} agent`,
|
|
2862
|
+
sessions: sessionRef ? [...existingSessions, sessionRef] : existingSessions
|
|
2863
|
+
};
|
|
2864
|
+
if (!task.description && sessionSummary) {
|
|
2865
|
+
patch.description = sessionSummary;
|
|
2866
|
+
}
|
|
2867
|
+
await taskService.updateTask(id, patch);
|
|
2868
|
+
const updated = await taskService.getTask(id);
|
|
2869
|
+
return {
|
|
2870
|
+
content: [
|
|
2871
|
+
{
|
|
2872
|
+
type: "text",
|
|
2873
|
+
text: `Task ${id} is now IN PROGRESS.
|
|
2874
|
+
|
|
2875
|
+
${JSON.stringify(updated, null, 2)}`
|
|
2876
|
+
}
|
|
2877
|
+
]
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
if (name === "search_memory") {
|
|
2881
|
+
const query = args?.query;
|
|
2882
|
+
const apiKey = await configService.getApiKey();
|
|
2883
|
+
if (!apiKey) {
|
|
2884
|
+
return {
|
|
2885
|
+
isError: true,
|
|
2886
|
+
content: [
|
|
2887
|
+
{
|
|
2888
|
+
type: "text",
|
|
2889
|
+
text: "Error: No API Key configured. Please run `vem login` in the CLI."
|
|
2890
|
+
}
|
|
2891
|
+
]
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
try {
|
|
2895
|
+
const { deviceId, deviceName } = await configService.getOrCreateDeviceId();
|
|
2896
|
+
const apiUrl = process.env.VEM_API_URL || "http://localhost:3002";
|
|
2897
|
+
const res = await fetch(
|
|
2898
|
+
`${apiUrl}/search?q=${encodeURIComponent(query)}`,
|
|
2899
|
+
{
|
|
2900
|
+
headers: {
|
|
2901
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2902
|
+
"X-Vem-Device-Id": deviceId,
|
|
2903
|
+
"X-Vem-Device-Name": deviceName
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
);
|
|
2907
|
+
if (!res.ok) {
|
|
2908
|
+
return {
|
|
2909
|
+
isError: true,
|
|
2910
|
+
content: [
|
|
2911
|
+
{
|
|
2912
|
+
type: "text",
|
|
2913
|
+
text: `Search API Error: ${res.status} ${res.statusText}`
|
|
2914
|
+
}
|
|
2915
|
+
]
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
const data = await res.json();
|
|
2919
|
+
return {
|
|
2920
|
+
content: [
|
|
2921
|
+
{
|
|
2922
|
+
type: "text",
|
|
2923
|
+
text: JSON.stringify(data.results || [], null, 2)
|
|
2924
|
+
}
|
|
2925
|
+
]
|
|
2926
|
+
};
|
|
2927
|
+
} catch (error) {
|
|
2928
|
+
return {
|
|
2929
|
+
isError: true,
|
|
2930
|
+
content: [
|
|
2931
|
+
{ type: "text", text: `Search Request Failed: ${error.message}` }
|
|
2932
|
+
]
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
if (name === "ask_question") {
|
|
2937
|
+
const question = args?.question;
|
|
2938
|
+
const path10 = args?.path;
|
|
2939
|
+
const apiKey = await configService.getApiKey();
|
|
2940
|
+
const projectId = await configService.getProjectId();
|
|
2941
|
+
if (!apiKey) {
|
|
2942
|
+
return {
|
|
2943
|
+
isError: true,
|
|
2944
|
+
content: [
|
|
2945
|
+
{
|
|
2946
|
+
type: "text",
|
|
2947
|
+
text: "Error: No API Key configured. Please run `vem login` in the CLI."
|
|
2948
|
+
}
|
|
2949
|
+
]
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
if (!projectId) {
|
|
2953
|
+
return {
|
|
2954
|
+
isError: true,
|
|
2955
|
+
content: [
|
|
2956
|
+
{
|
|
2957
|
+
type: "text",
|
|
2958
|
+
text: "Error: No project linked. Please run `vem link` in the CLI."
|
|
2959
|
+
}
|
|
2960
|
+
]
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
try {
|
|
2964
|
+
const { deviceId, deviceName } = await configService.getOrCreateDeviceId();
|
|
2965
|
+
const apiUrl = process.env.VEM_API_URL || "http://localhost:3002";
|
|
2966
|
+
const payload = { question };
|
|
2967
|
+
if (path10) payload.path = path10;
|
|
2968
|
+
const res = await fetch(`${apiUrl}/projects/${projectId}/ask`, {
|
|
2969
|
+
method: "POST",
|
|
2970
|
+
headers: {
|
|
2971
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2972
|
+
"Content-Type": "application/json",
|
|
2973
|
+
"X-Vem-Device-Id": deviceId,
|
|
2974
|
+
"X-Vem-Device-Name": deviceName,
|
|
2975
|
+
"X-Vem-Client": "mcp-server"
|
|
2976
|
+
},
|
|
2977
|
+
body: JSON.stringify(payload)
|
|
2978
|
+
});
|
|
2979
|
+
if (!res.ok) {
|
|
2980
|
+
return {
|
|
2981
|
+
isError: true,
|
|
2982
|
+
content: [
|
|
2983
|
+
{
|
|
2984
|
+
type: "text",
|
|
2985
|
+
text: `Ask API Error: ${res.status} ${res.statusText}`
|
|
2986
|
+
}
|
|
2987
|
+
]
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
const data = await res.json();
|
|
2991
|
+
return {
|
|
2992
|
+
content: [
|
|
2993
|
+
{
|
|
2994
|
+
type: "text",
|
|
2995
|
+
text: JSON.stringify(data, null, 2)
|
|
2996
|
+
}
|
|
2997
|
+
]
|
|
2998
|
+
};
|
|
2999
|
+
} catch (error) {
|
|
3000
|
+
return {
|
|
3001
|
+
isError: true,
|
|
3002
|
+
content: [
|
|
3003
|
+
{ type: "text", text: `Ask Request Failed: ${error.message}` }
|
|
3004
|
+
]
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
if (name === "get_context") {
|
|
3009
|
+
const vemDir = await getVemDir();
|
|
3010
|
+
const context = await configService.getContext();
|
|
3011
|
+
let currentState = "";
|
|
3012
|
+
try {
|
|
3013
|
+
currentState = await readFile(join2(vemDir, CURRENT_STATE_FILE), "utf-8");
|
|
3014
|
+
} catch {
|
|
3015
|
+
}
|
|
3016
|
+
const parts = [];
|
|
3017
|
+
if (context) parts.push(`## CONTEXT.md
|
|
3018
|
+
|
|
3019
|
+
${context}`);
|
|
3020
|
+
if (currentState) parts.push(`## CURRENT_STATE.md
|
|
3021
|
+
|
|
3022
|
+
${currentState}`);
|
|
3023
|
+
return {
|
|
3024
|
+
content: [{ type: "text", text: parts.join("\n\n---\n\n") || "(no context yet)" }]
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
if (name === "list_decisions") {
|
|
3028
|
+
const requestedLimit = args?.limit || 20;
|
|
3029
|
+
const limit = Math.min(Math.max(1, requestedLimit), 50);
|
|
3030
|
+
const decisionsService = new ScalableLogService(DECISIONS_DIR);
|
|
3031
|
+
const entries = await decisionsService.getAllEntries();
|
|
3032
|
+
const limitedEntries = entries.slice(0, limit);
|
|
3033
|
+
const formatted = limitedEntries.map((entry) => ({
|
|
3034
|
+
id: entry.id,
|
|
3035
|
+
title: entry.title,
|
|
3036
|
+
date: entry.created_at,
|
|
3037
|
+
content: entry.content
|
|
3038
|
+
}));
|
|
3039
|
+
return {
|
|
3040
|
+
content: [
|
|
3041
|
+
{
|
|
3042
|
+
type: "text",
|
|
3043
|
+
text: formatted.length > 0 ? JSON.stringify(formatted, null, 2) : "No decisions recorded yet."
|
|
3044
|
+
}
|
|
3045
|
+
]
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
3048
|
+
if (name === "update_current_state") {
|
|
3049
|
+
const content = args?.content;
|
|
3050
|
+
if (!content) {
|
|
3051
|
+
return {
|
|
3052
|
+
isError: true,
|
|
3053
|
+
content: [{ type: "text", text: "Content is required." }]
|
|
3054
|
+
};
|
|
3055
|
+
}
|
|
3056
|
+
const vemDir = await getVemDir();
|
|
3057
|
+
await writeFile(join2(vemDir, CURRENT_STATE_FILE), content, "utf-8");
|
|
3058
|
+
return {
|
|
3059
|
+
content: [{ type: "text", text: "CURRENT_STATE.md updated." }]
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
3062
|
+
if (name === "update_task") {
|
|
3063
|
+
const id = args?.id;
|
|
3064
|
+
const status = args?.status;
|
|
3065
|
+
const priority = args?.priority;
|
|
3066
|
+
const reasoning = args?.reasoning;
|
|
3067
|
+
const blockedBy = args?.blocked_by;
|
|
3068
|
+
const validationSteps = args?.validation_steps;
|
|
3069
|
+
if (!id) {
|
|
3070
|
+
return {
|
|
3071
|
+
isError: true,
|
|
3072
|
+
content: [{ type: "text", text: "Task ID is required." }]
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
const task = await taskService.getTask(id);
|
|
3076
|
+
if (!task) {
|
|
3077
|
+
return {
|
|
3078
|
+
isError: true,
|
|
3079
|
+
content: [{ type: "text", text: `Task ${id} not found.` }]
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
if (status === "blocked" && !reasoning) {
|
|
3083
|
+
return {
|
|
3084
|
+
isError: true,
|
|
3085
|
+
content: [
|
|
3086
|
+
{
|
|
3087
|
+
type: "text",
|
|
3088
|
+
text: "Reasoning is required when blocking a task."
|
|
3089
|
+
}
|
|
3090
|
+
]
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
if (task.status === "done" && status) {
|
|
3094
|
+
return {
|
|
3095
|
+
isError: true,
|
|
3096
|
+
content: [
|
|
3097
|
+
{
|
|
3098
|
+
type: "text",
|
|
3099
|
+
text: "Cannot change status of a completed task. Use complete_task to mark as done."
|
|
3100
|
+
}
|
|
3101
|
+
]
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
const patch = {};
|
|
3105
|
+
if (status) patch.status = status;
|
|
3106
|
+
if (priority) patch.priority = priority;
|
|
3107
|
+
if (reasoning) patch.reasoning = reasoning;
|
|
3108
|
+
if (blockedBy) patch.blocked_by = blockedBy;
|
|
3109
|
+
if (validationSteps) patch.validation_steps = validationSteps;
|
|
3110
|
+
if (status && !reasoning) {
|
|
3111
|
+
if (status === "in-progress") {
|
|
3112
|
+
patch.reasoning = "Started working on task";
|
|
3113
|
+
} else if (status === "todo") {
|
|
3114
|
+
patch.reasoning = "Unblocked task";
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
await taskService.updateTask(id, patch);
|
|
3118
|
+
const statusMsg = status ? ` Status: ${status.toUpperCase()}.` : "";
|
|
3119
|
+
const priorityMsg = priority ? ` Priority: ${priority}.` : "";
|
|
3120
|
+
return {
|
|
3121
|
+
content: [
|
|
3122
|
+
{
|
|
3123
|
+
type: "text",
|
|
3124
|
+
text: `Task ${id} updated.${statusMsg}${priorityMsg}`
|
|
3125
|
+
}
|
|
3126
|
+
]
|
|
3127
|
+
};
|
|
3128
|
+
}
|
|
3129
|
+
if (name === "add_decision") {
|
|
3130
|
+
const title = args?.title;
|
|
3131
|
+
const context = args?.context;
|
|
3132
|
+
const decision = args?.decision;
|
|
3133
|
+
const relatedTasks = args?.related_tasks;
|
|
3134
|
+
if (!title || !context || !decision) {
|
|
3135
|
+
return {
|
|
3136
|
+
isError: true,
|
|
3137
|
+
content: [
|
|
3138
|
+
{
|
|
3139
|
+
type: "text",
|
|
3140
|
+
text: "Title, context, and decision are required."
|
|
3141
|
+
}
|
|
3142
|
+
]
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
await configService.recordDecision(title, context, decision, relatedTasks);
|
|
3146
|
+
const taskInfo = relatedTasks && relatedTasks.length > 0 ? ` (related to: ${relatedTasks.join(", ")})` : "";
|
|
3147
|
+
return {
|
|
3148
|
+
content: [
|
|
3149
|
+
{
|
|
3150
|
+
type: "text",
|
|
3151
|
+
text: `Decision recorded: ${title}${taskInfo}`
|
|
3152
|
+
}
|
|
3153
|
+
]
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
3156
|
+
if (name === "get_changelog") {
|
|
3157
|
+
const requestedLimit = args?.limit || 10;
|
|
3158
|
+
const limit = Math.min(Math.max(1, requestedLimit), 50);
|
|
3159
|
+
const changelogService = new ScalableLogService(CHANGELOG_DIR);
|
|
3160
|
+
const entries = await changelogService.getAllEntries();
|
|
3161
|
+
const limitedEntries = entries.slice(0, limit);
|
|
3162
|
+
const formatted = limitedEntries.map((entry) => ({
|
|
3163
|
+
id: entry.id,
|
|
3164
|
+
title: entry.title,
|
|
3165
|
+
date: entry.created_at,
|
|
3166
|
+
content: entry.content
|
|
3167
|
+
}));
|
|
3168
|
+
return {
|
|
3169
|
+
content: [
|
|
3170
|
+
{
|
|
3171
|
+
type: "text",
|
|
3172
|
+
text: JSON.stringify(formatted, null, 2)
|
|
3173
|
+
}
|
|
3174
|
+
]
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
if (name === "delete_task") {
|
|
3178
|
+
const id = args?.id;
|
|
3179
|
+
const reasoning = args?.reasoning;
|
|
3180
|
+
if (!id) {
|
|
3181
|
+
return {
|
|
3182
|
+
isError: true,
|
|
3183
|
+
content: [{ type: "text", text: "Task ID is required." }]
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
await taskService.updateTask(id, {
|
|
3187
|
+
deleted_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3188
|
+
reasoning
|
|
3189
|
+
});
|
|
3190
|
+
return {
|
|
3191
|
+
content: [{ type: "text", text: `Task ${id} soft deleted.` }]
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
if (name === "apply_vem_update") {
|
|
3195
|
+
try {
|
|
3196
|
+
const update = args;
|
|
3197
|
+
const result = await applyVemUpdate(update);
|
|
3198
|
+
return {
|
|
3199
|
+
content: [
|
|
3200
|
+
{
|
|
3201
|
+
type: "text",
|
|
3202
|
+
text: `Successfully applied vem_update:
|
|
3203
|
+
- Tasks updated: ${result.updatedTasks.length}
|
|
3204
|
+
- New tasks: ${result.newTasks.length}
|
|
3205
|
+
- Changelog lines: ${result.changelogLines.length}
|
|
3206
|
+
- Decisions: ${result.decisionsAppended ? "Appended" : "No change"}
|
|
3207
|
+
- Context: ${result.contextUpdated ? "Updated" : "No change"}
|
|
3208
|
+
- State: ${result.currentStateUpdated ? "Updated" : "No change"}`
|
|
3209
|
+
}
|
|
3210
|
+
]
|
|
3211
|
+
};
|
|
3212
|
+
} catch (error) {
|
|
3213
|
+
return {
|
|
3214
|
+
isError: true,
|
|
3215
|
+
content: [
|
|
3216
|
+
{ type: "text", text: `Failed to apply update: ${error.message}` }
|
|
3217
|
+
]
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
if (name === "sync_push") {
|
|
3222
|
+
const force = args?.force;
|
|
3223
|
+
const dryRun = args?.dry_run;
|
|
3224
|
+
try {
|
|
3225
|
+
const projectId = await configService.getProjectId();
|
|
3226
|
+
if (!projectId) {
|
|
3227
|
+
return {
|
|
3228
|
+
isError: true,
|
|
3229
|
+
content: [
|
|
3230
|
+
{
|
|
3231
|
+
type: "text",
|
|
3232
|
+
text: "Error: Project not linked. Run `vem link` in the CLI first."
|
|
3233
|
+
}
|
|
3234
|
+
]
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
const apiKey = await configService.getApiKey();
|
|
3238
|
+
if (!apiKey) {
|
|
3239
|
+
return {
|
|
3240
|
+
isError: true,
|
|
3241
|
+
content: [
|
|
3242
|
+
{
|
|
3243
|
+
type: "text",
|
|
3244
|
+
text: "Error: No API Key configured. Run `vem login` in the CLI."
|
|
3245
|
+
}
|
|
3246
|
+
]
|
|
3247
|
+
};
|
|
3248
|
+
}
|
|
3249
|
+
const baseVersion = await configService.getLastVersion();
|
|
3250
|
+
const gitHash = getGitHash();
|
|
3251
|
+
if (!gitHash) {
|
|
3252
|
+
return {
|
|
3253
|
+
isError: true,
|
|
3254
|
+
content: [
|
|
3255
|
+
{
|
|
3256
|
+
type: "text",
|
|
3257
|
+
text: "Error: git HEAD not found. Create at least one commit before syncing snapshots."
|
|
3258
|
+
}
|
|
3259
|
+
]
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
const vemHash = await computeVemHash();
|
|
3263
|
+
const lastPush = await configService.getLastPushState();
|
|
3264
|
+
const hasChanges = !(vemHash && lastPush.gitHash === gitHash && lastPush.vemHash === vemHash);
|
|
3265
|
+
if (!hasChanges && !force) {
|
|
3266
|
+
return {
|
|
3267
|
+
content: [
|
|
3268
|
+
{
|
|
3269
|
+
type: "text",
|
|
3270
|
+
text: "No changes since last push (git HEAD and .vem unchanged). Use force=true to push anyway."
|
|
3271
|
+
}
|
|
3272
|
+
]
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
const snapshot = await syncService.pack();
|
|
3276
|
+
const snapshotHash = computeSnapshotHashFromPayload(snapshot);
|
|
3277
|
+
if (dryRun) {
|
|
3278
|
+
const taskCount = snapshot.tasks?.tasks?.length || 0;
|
|
3279
|
+
return {
|
|
3280
|
+
content: [
|
|
3281
|
+
{
|
|
3282
|
+
type: "text",
|
|
3283
|
+
text: `Dry run preview:
|
|
3284
|
+
- Project: ${projectId}
|
|
3285
|
+
- Git hash: ${gitHash}
|
|
3286
|
+
- Snapshot hash: ${snapshotHash}
|
|
3287
|
+
- Base version: ${baseVersion || "none"}
|
|
3288
|
+
- Tasks: ${taskCount}
|
|
3289
|
+
- Context: ${snapshot.context ? "yes" : "no"}
|
|
3290
|
+
- Current state: ${snapshot.current_state ? "yes" : "no"}
|
|
3291
|
+
|
|
3292
|
+
Verification remains pending until Git webhook linkage is confirmed.
|
|
3293
|
+
|
|
3294
|
+
No changes pushed. Remove dry_run to push for real.`
|
|
3295
|
+
}
|
|
3296
|
+
]
|
|
3297
|
+
};
|
|
3298
|
+
}
|
|
3299
|
+
const repoUrl = getGitRemote();
|
|
3300
|
+
const commits = getCommits(50);
|
|
3301
|
+
const payload = {
|
|
3302
|
+
...snapshot,
|
|
3303
|
+
...repoUrl ? { repo_url: repoUrl } : {},
|
|
3304
|
+
base_version: baseVersion,
|
|
3305
|
+
commits,
|
|
3306
|
+
project_id: projectId,
|
|
3307
|
+
git_hash: gitHash,
|
|
3308
|
+
snapshot_hash: snapshotHash
|
|
3309
|
+
};
|
|
3310
|
+
const apiUrl = process.env.VEM_API_URL || "http://localhost:3002";
|
|
3311
|
+
const res = await fetch(`${apiUrl}/snapshots`, {
|
|
3312
|
+
method: "POST",
|
|
3313
|
+
headers: {
|
|
3314
|
+
Authorization: `Bearer ${apiKey}`,
|
|
3315
|
+
"Content-Type": "application/json",
|
|
3316
|
+
...await buildDeviceHeaders(configService)
|
|
3317
|
+
},
|
|
3318
|
+
body: JSON.stringify(payload)
|
|
3319
|
+
});
|
|
3320
|
+
if (res.ok) {
|
|
3321
|
+
const json = await res.json();
|
|
3322
|
+
if (json.version) {
|
|
3323
|
+
await configService.setLastVersion(json.version);
|
|
3324
|
+
}
|
|
3325
|
+
if (gitHash && vemHash) {
|
|
3326
|
+
await configService.setLastPushState({ gitHash, vemHash });
|
|
3327
|
+
}
|
|
3328
|
+
try {
|
|
3329
|
+
await taskService.archiveTasks({ status: "done" });
|
|
3330
|
+
} catch {
|
|
3331
|
+
}
|
|
3332
|
+
return {
|
|
3333
|
+
content: [
|
|
3334
|
+
{
|
|
3335
|
+
type: "text",
|
|
3336
|
+
text: `Snapshot pushed successfully. Version: ${json.version || "v1"}`
|
|
3337
|
+
}
|
|
3338
|
+
]
|
|
3339
|
+
};
|
|
3340
|
+
}
|
|
3341
|
+
const data = await res.json().catch(() => ({}));
|
|
3342
|
+
if (res.status === 409) {
|
|
3343
|
+
if (data.latest_version) {
|
|
3344
|
+
return {
|
|
3345
|
+
isError: true,
|
|
3346
|
+
content: [
|
|
3347
|
+
{
|
|
3348
|
+
type: "text",
|
|
3349
|
+
text: `Conflict: local base version ${baseVersion || "none"} does not match latest ${data.latest_version}. Run sync_pull first, then retry.`
|
|
3350
|
+
}
|
|
3351
|
+
]
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
if (data.expected_repo_url) {
|
|
3355
|
+
return {
|
|
3356
|
+
isError: true,
|
|
3357
|
+
content: [
|
|
3358
|
+
{
|
|
3359
|
+
type: "text",
|
|
3360
|
+
text: `Conflict: project is linked to ${data.expected_repo_url}, but local repo is ${repoUrl || "(no git remote)"}. Update git remote or re-link the project.`
|
|
3361
|
+
}
|
|
3362
|
+
]
|
|
3363
|
+
};
|
|
3364
|
+
}
|
|
3365
|
+
return {
|
|
3366
|
+
isError: true,
|
|
3367
|
+
content: [
|
|
3368
|
+
{
|
|
3369
|
+
type: "text",
|
|
3370
|
+
text: `Conflict: ${data.error || "Unknown conflict"}`
|
|
3371
|
+
}
|
|
3372
|
+
]
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
if (res.status === 403) {
|
|
3376
|
+
return {
|
|
3377
|
+
isError: true,
|
|
3378
|
+
content: [
|
|
3379
|
+
{
|
|
3380
|
+
type: "text",
|
|
3381
|
+
text: data.error || "Device limit reached. Disconnect a device or upgrade your plan."
|
|
3382
|
+
}
|
|
3383
|
+
]
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
if (res.status === 404) {
|
|
3387
|
+
return {
|
|
3388
|
+
isError: true,
|
|
3389
|
+
content: [
|
|
3390
|
+
{
|
|
3391
|
+
type: "text",
|
|
3392
|
+
text: data.error || "Project not found. It may have been deleted. Run `vem unlink` then `vem link` to reconnect."
|
|
3393
|
+
}
|
|
3394
|
+
]
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
await syncService.enqueue(payload);
|
|
3398
|
+
return {
|
|
3399
|
+
isError: true,
|
|
3400
|
+
content: [
|
|
3401
|
+
{
|
|
3402
|
+
type: "text",
|
|
3403
|
+
text: `Push failed (${data.error || res.statusText}). Snapshot queued for later retry.`
|
|
3404
|
+
}
|
|
3405
|
+
]
|
|
3406
|
+
};
|
|
3407
|
+
} catch (error) {
|
|
3408
|
+
return {
|
|
3409
|
+
isError: true,
|
|
3410
|
+
content: [{ type: "text", text: `Push failed: ${error.message}` }]
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
if (name === "sync_pull") {
|
|
3415
|
+
const force = args?.force;
|
|
3416
|
+
try {
|
|
3417
|
+
const apiKey = await configService.getApiKey();
|
|
3418
|
+
if (!apiKey) {
|
|
3419
|
+
return {
|
|
3420
|
+
isError: true,
|
|
3421
|
+
content: [
|
|
3422
|
+
{
|
|
3423
|
+
type: "text",
|
|
3424
|
+
text: "Error: No API Key configured. Run `vem login` in the CLI."
|
|
3425
|
+
}
|
|
3426
|
+
]
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
if (await isVemDirty() && !force) {
|
|
3430
|
+
return {
|
|
3431
|
+
isError: true,
|
|
3432
|
+
content: [
|
|
3433
|
+
{
|
|
3434
|
+
type: "text",
|
|
3435
|
+
text: "Local .vem has uncommitted changes. Use force=true to overwrite, or commit your changes first."
|
|
3436
|
+
}
|
|
3437
|
+
]
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
const repoUrl = getGitRemote();
|
|
3441
|
+
const projectId = await configService.getProjectId();
|
|
3442
|
+
if (!repoUrl && !projectId) {
|
|
3443
|
+
return {
|
|
3444
|
+
isError: true,
|
|
3445
|
+
content: [
|
|
3446
|
+
{
|
|
3447
|
+
type: "text",
|
|
3448
|
+
text: "Error: No git remote or linked project found. Run `vem link` in the CLI."
|
|
3449
|
+
}
|
|
3450
|
+
]
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
const apiUrl = process.env.VEM_API_URL || "http://localhost:3002";
|
|
3454
|
+
const query = new URLSearchParams();
|
|
3455
|
+
if (repoUrl) query.set("repo_url", repoUrl);
|
|
3456
|
+
if (projectId) query.set("project_id", projectId);
|
|
3457
|
+
const res = await fetch(`${apiUrl}/snapshots/latest?${query}`, {
|
|
3458
|
+
headers: {
|
|
3459
|
+
Authorization: `Bearer ${apiKey}`,
|
|
3460
|
+
...await buildDeviceHeaders(configService)
|
|
3461
|
+
}
|
|
3462
|
+
});
|
|
3463
|
+
if (!res.ok) {
|
|
3464
|
+
if (res.status === 404) {
|
|
3465
|
+
return {
|
|
3466
|
+
isError: true,
|
|
3467
|
+
content: [
|
|
3468
|
+
{
|
|
3469
|
+
type: "text",
|
|
3470
|
+
text: "Project not found. It may have been deleted."
|
|
3471
|
+
}
|
|
3472
|
+
]
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
if (res.status === 409) {
|
|
3476
|
+
const data2 = await res.json().catch(() => ({}));
|
|
3477
|
+
return {
|
|
3478
|
+
isError: true,
|
|
3479
|
+
content: [
|
|
3480
|
+
{
|
|
3481
|
+
type: "text",
|
|
3482
|
+
text: data2.expected_repo_url ? `Repo URL mismatch. Expected ${data2.expected_repo_url}.` : data2.error || "Conflict detected."
|
|
3483
|
+
}
|
|
3484
|
+
]
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
if (res.status === 403) {
|
|
3488
|
+
const data2 = await res.json().catch(() => ({}));
|
|
3489
|
+
return {
|
|
3490
|
+
isError: true,
|
|
3491
|
+
content: [
|
|
3492
|
+
{ type: "text", text: data2.error || "Device limit reached." }
|
|
3493
|
+
]
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
const err = await res.text();
|
|
3497
|
+
return {
|
|
3498
|
+
isError: true,
|
|
3499
|
+
content: [
|
|
3500
|
+
{ type: "text", text: `Pull API Error: ${res.status} ${err}` }
|
|
3501
|
+
]
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
const data = await res.json();
|
|
3505
|
+
if (!data.snapshot) {
|
|
3506
|
+
return {
|
|
3507
|
+
content: [{ type: "text", text: "No snapshot data available." }]
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
await syncService.unpack(data.snapshot);
|
|
3511
|
+
if (data.version) {
|
|
3512
|
+
await configService.setLastVersion(data.version);
|
|
3513
|
+
}
|
|
3514
|
+
return {
|
|
3515
|
+
content: [
|
|
3516
|
+
{
|
|
3517
|
+
type: "text",
|
|
3518
|
+
text: `Snapshot pulled and unpacked. Version: ${data.version || "unknown"}`
|
|
3519
|
+
}
|
|
3520
|
+
]
|
|
3521
|
+
};
|
|
3522
|
+
} catch (error) {
|
|
3523
|
+
return {
|
|
3524
|
+
isError: true,
|
|
3525
|
+
content: [{ type: "text", text: `Pull failed: ${error.message}` }]
|
|
3526
|
+
};
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
if (name === "get_task_context") {
|
|
3530
|
+
const id = args?.id;
|
|
3531
|
+
if (!id) {
|
|
3532
|
+
return {
|
|
3533
|
+
isError: true,
|
|
3534
|
+
content: [{ type: "text", text: "Task ID is required." }]
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3537
|
+
const task = await taskService.getTask(id);
|
|
3538
|
+
if (!task) {
|
|
3539
|
+
return {
|
|
3540
|
+
isError: true,
|
|
3541
|
+
content: [{ type: "text", text: `Task ${id} not found.` }]
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
return {
|
|
3545
|
+
content: [
|
|
3546
|
+
{
|
|
3547
|
+
type: "text",
|
|
3548
|
+
text: JSON.stringify(
|
|
3549
|
+
{
|
|
3550
|
+
id: task.id,
|
|
3551
|
+
title: task.title,
|
|
3552
|
+
task_context: task.task_context || null,
|
|
3553
|
+
task_context_summary: task.task_context_summary || null
|
|
3554
|
+
},
|
|
3555
|
+
null,
|
|
3556
|
+
2
|
|
3557
|
+
)
|
|
3558
|
+
}
|
|
3559
|
+
]
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
if (name === "update_task_context") {
|
|
3563
|
+
const id = args?.id;
|
|
3564
|
+
const operation = args?.operation;
|
|
3565
|
+
const text = args?.text;
|
|
3566
|
+
if (!id) {
|
|
3567
|
+
return {
|
|
3568
|
+
isError: true,
|
|
3569
|
+
content: [{ type: "text", text: "Task ID is required." }]
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
if (!operation || !["set", "append", "clear"].includes(operation)) {
|
|
3573
|
+
return {
|
|
3574
|
+
isError: true,
|
|
3575
|
+
content: [
|
|
3576
|
+
{
|
|
3577
|
+
type: "text",
|
|
3578
|
+
text: "Operation must be 'set', 'append', or 'clear'."
|
|
3579
|
+
}
|
|
3580
|
+
]
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
if ((operation === "set" || operation === "append") && !text) {
|
|
3584
|
+
return {
|
|
3585
|
+
isError: true,
|
|
3586
|
+
content: [
|
|
3587
|
+
{
|
|
3588
|
+
type: "text",
|
|
3589
|
+
text: `Text is required for '${operation}' operation.`
|
|
3590
|
+
}
|
|
3591
|
+
]
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
const task = await taskService.getTask(id);
|
|
3595
|
+
if (!task) {
|
|
3596
|
+
return {
|
|
3597
|
+
isError: true,
|
|
3598
|
+
content: [{ type: "text", text: `Task ${id} not found.` }]
|
|
3599
|
+
};
|
|
3600
|
+
}
|
|
3601
|
+
let nextContext = task.task_context || "";
|
|
3602
|
+
if (operation === "clear") {
|
|
3603
|
+
nextContext = "";
|
|
3604
|
+
} else if (operation === "set") {
|
|
3605
|
+
nextContext = text;
|
|
3606
|
+
} else if (operation === "append") {
|
|
3607
|
+
nextContext = [nextContext, text].filter(Boolean).join("\n");
|
|
3608
|
+
}
|
|
3609
|
+
await taskService.updateTask(id, { task_context: nextContext });
|
|
3610
|
+
return {
|
|
3611
|
+
content: [
|
|
3612
|
+
{ type: "text", text: `Task ${id} context updated (${operation}).` }
|
|
3613
|
+
]
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
if (name === "get_subtasks") {
|
|
3617
|
+
const parentId = args?.parent_id;
|
|
3618
|
+
if (!parentId) {
|
|
3619
|
+
return {
|
|
3620
|
+
isError: true,
|
|
3621
|
+
content: [{ type: "text", text: "Parent task ID is required." }]
|
|
3622
|
+
};
|
|
3623
|
+
}
|
|
3624
|
+
const allTasks = await taskService.getTasks();
|
|
3625
|
+
const parent = allTasks.find((t) => t.id === parentId);
|
|
3626
|
+
if (!parent) {
|
|
3627
|
+
return {
|
|
3628
|
+
isError: true,
|
|
3629
|
+
content: [{ type: "text", text: `Parent task ${parentId} not found.` }]
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
const subtasks = allTasks.filter((t) => t.parent_id === parentId && !t.deleted_at).sort((a, b) => (a.subtask_order ?? 0) - (b.subtask_order ?? 0));
|
|
3633
|
+
return {
|
|
3634
|
+
content: [
|
|
3635
|
+
{
|
|
3636
|
+
type: "text",
|
|
3637
|
+
text: JSON.stringify({ parent, subtasks }, null, 2)
|
|
3638
|
+
}
|
|
3639
|
+
]
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
if (name === "list_agent_sessions") {
|
|
3643
|
+
const limit = typeof args?.limit === "number" ? args.limit : 10;
|
|
3644
|
+
const branch = typeof args?.branch === "string" ? args.branch : void 0;
|
|
3645
|
+
const rawSources = args?.sources;
|
|
3646
|
+
const sources = Array.isArray(rawSources) && rawSources.length > 0 ? rawSources : void 0;
|
|
3647
|
+
let gitRoot;
|
|
3648
|
+
try {
|
|
3649
|
+
gitRoot = await getRepoRoot();
|
|
3650
|
+
} catch {
|
|
3651
|
+
}
|
|
3652
|
+
let sessions = await listAllAgentSessions(gitRoot, sources);
|
|
3653
|
+
if (branch) {
|
|
3654
|
+
sessions = sessions.filter((s) => s.branch === branch);
|
|
3655
|
+
}
|
|
3656
|
+
sessions = sessions.slice(0, limit);
|
|
3657
|
+
return {
|
|
3658
|
+
content: [
|
|
3659
|
+
{
|
|
3660
|
+
type: "text",
|
|
3661
|
+
text: JSON.stringify(
|
|
3662
|
+
sessions.map((s) => ({
|
|
3663
|
+
id: s.id,
|
|
3664
|
+
source: s.source,
|
|
3665
|
+
summary: s.summary,
|
|
3666
|
+
branch: s.branch,
|
|
3667
|
+
repository: s.repository,
|
|
3668
|
+
created_at: s.created_at,
|
|
3669
|
+
updated_at: s.updated_at,
|
|
3670
|
+
intents: s.intents,
|
|
3671
|
+
user_messages: s.user_messages.slice(0, 3)
|
|
3672
|
+
})),
|
|
3673
|
+
null,
|
|
3674
|
+
2
|
|
3675
|
+
)
|
|
3676
|
+
}
|
|
3677
|
+
]
|
|
3678
|
+
};
|
|
3679
|
+
}
|
|
3680
|
+
if (name === "save_session_stats") {
|
|
3681
|
+
const taskId2 = args?.task_id;
|
|
3682
|
+
const sessionId = args?.session_id;
|
|
3683
|
+
const source = args?.source || "copilot";
|
|
3684
|
+
if (!taskId2 || !sessionId) {
|
|
3685
|
+
return {
|
|
3686
|
+
isError: true,
|
|
3687
|
+
content: [
|
|
3688
|
+
{
|
|
3689
|
+
type: "text",
|
|
3690
|
+
text: "task_id and session_id are required."
|
|
3691
|
+
}
|
|
3692
|
+
]
|
|
3693
|
+
};
|
|
3694
|
+
}
|
|
3695
|
+
const task = await taskService.getTask(taskId2);
|
|
3696
|
+
if (!task) {
|
|
3697
|
+
return {
|
|
3698
|
+
isError: true,
|
|
3699
|
+
content: [{ type: "text", text: `Task ${taskId2} not found.` }]
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
const stats = await computeSessionStats(sessionId, source);
|
|
3703
|
+
if (!stats) {
|
|
3704
|
+
return {
|
|
3705
|
+
isError: true,
|
|
3706
|
+
content: [
|
|
3707
|
+
{
|
|
3708
|
+
type: "text",
|
|
3709
|
+
text: `Could not compute stats for ${source} session ${sessionId}. Session file may not exist or is unreadable.`
|
|
3710
|
+
}
|
|
3711
|
+
]
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
const existingSessions = (task.sessions || []).map((s) => {
|
|
3715
|
+
if (s.id === sessionId) {
|
|
3716
|
+
return { ...s, stats };
|
|
3717
|
+
}
|
|
3718
|
+
return s;
|
|
3719
|
+
});
|
|
3720
|
+
const hasMatch = existingSessions.some((s) => s.id === sessionId);
|
|
3721
|
+
if (!hasMatch) {
|
|
3722
|
+
existingSessions.push({
|
|
3723
|
+
id: sessionId,
|
|
3724
|
+
source,
|
|
3725
|
+
started_at: stats.ended_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
3726
|
+
stats
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
await taskService.updateTask(taskId2, { sessions: existingSessions });
|
|
3730
|
+
return {
|
|
3731
|
+
content: [
|
|
3732
|
+
{
|
|
3733
|
+
type: "text",
|
|
3734
|
+
text: `Stats saved for session ${sessionId} on task ${taskId2}:
|
|
3735
|
+
${JSON.stringify(stats, null, 2)}`
|
|
3736
|
+
}
|
|
3737
|
+
]
|
|
3738
|
+
};
|
|
3739
|
+
}
|
|
3740
|
+
throw new Error(`Tool not found: ${name}`);
|
|
3741
|
+
});
|
|
3742
|
+
var transport = new StdioServerTransport();
|
|
3743
|
+
await server.connect(transport);
|
|
3744
|
+
//# sourceMappingURL=index.js.map
|