@agentmeshhq/agent 0.1.7 → 0.1.8
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/dist/__tests__/context.test.d.ts +1 -0
- package/dist/__tests__/context.test.js +353 -0
- package/dist/__tests__/context.test.js.map +1 -0
- package/dist/cli/context.d.ts +4 -0
- package/dist/cli/context.js +190 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/start.d.ts +1 -0
- package/dist/cli/start.js +10 -5
- package/dist/cli/start.js.map +1 -1
- package/dist/context/handoff.d.ts +48 -0
- package/dist/context/handoff.js +88 -0
- package/dist/context/handoff.js.map +1 -0
- package/dist/context/index.d.ts +7 -0
- package/dist/context/index.js +8 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/schema.d.ts +82 -0
- package/dist/context/schema.js +33 -0
- package/dist/context/schema.js.map +1 -0
- package/dist/context/storage.d.ts +49 -0
- package/dist/context/storage.js +172 -0
- package/dist/context/storage.js.map +1 -0
- package/dist/core/daemon.d.ts +7 -0
- package/dist/core/daemon.js +53 -2
- package/dist/core/daemon.js.map +1 -1
- package/dist/core/heartbeat.d.ts +6 -0
- package/dist/core/heartbeat.js +8 -0
- package/dist/core/heartbeat.js.map +1 -1
- package/dist/core/injector.d.ts +9 -0
- package/dist/core/injector.js +55 -3
- package/dist/core/injector.js.map +1 -1
- package/dist/core/tmux.d.ts +13 -0
- package/dist/core/tmux.js +62 -0
- package/dist/core/tmux.js.map +1 -1
- package/package.json +11 -11
- package/src/__tests__/context.test.ts +464 -0
- package/src/cli/context.ts +232 -0
- package/src/cli/index.ts +17 -0
- package/src/cli/start.ts +11 -9
- package/src/context/handoff.ts +122 -0
- package/src/context/index.ts +8 -0
- package/src/context/schema.ts +111 -0
- package/src/context/storage.ts +197 -0
- package/src/core/daemon.ts +59 -1
- package/src/core/heartbeat.ts +13 -0
- package/src/core/injector.ts +74 -30
- package/src/core/tmux.ts +75 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import {
|
|
5
|
+
extractHandoffContext,
|
|
6
|
+
formatHandoffContextSummary,
|
|
7
|
+
parseHandoffContext,
|
|
8
|
+
serializeHandoffContext,
|
|
9
|
+
} from "../context/handoff.js";
|
|
10
|
+
import { type AgentContext, CONTEXT_VERSION, createEmptyContext } from "../context/schema.js";
|
|
11
|
+
import {
|
|
12
|
+
deleteContext,
|
|
13
|
+
exportContext,
|
|
14
|
+
getContextPath,
|
|
15
|
+
importContext,
|
|
16
|
+
listContexts,
|
|
17
|
+
loadContext,
|
|
18
|
+
loadOrCreateContext,
|
|
19
|
+
saveContext,
|
|
20
|
+
updateContext,
|
|
21
|
+
} from "../context/storage.js";
|
|
22
|
+
|
|
23
|
+
// Mock fs module
|
|
24
|
+
vi.mock("node:fs");
|
|
25
|
+
|
|
26
|
+
const mockFs = vi.mocked(fs);
|
|
27
|
+
|
|
28
|
+
describe("Context Schema", () => {
|
|
29
|
+
describe("createEmptyContext", () => {
|
|
30
|
+
it("creates context with correct version", () => {
|
|
31
|
+
const context = createEmptyContext("agent-123", "test-agent");
|
|
32
|
+
expect(context.version).toBe(CONTEXT_VERSION);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("creates context with agent info", () => {
|
|
36
|
+
const context = createEmptyContext("agent-123", "test-agent");
|
|
37
|
+
expect(context.agentId).toBe("agent-123");
|
|
38
|
+
expect(context.agentName).toBe("test-agent");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("creates context with empty conversation", () => {
|
|
42
|
+
const context = createEmptyContext("agent-123", "test-agent");
|
|
43
|
+
expect(context.conversation.messageCount).toBe(0);
|
|
44
|
+
expect(context.conversation.topics).toEqual([]);
|
|
45
|
+
expect(context.conversation.accomplishments).toEqual([]);
|
|
46
|
+
expect(context.conversation.recentMessages).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("creates context with working state", () => {
|
|
50
|
+
const context = createEmptyContext("agent-123", "test-agent");
|
|
51
|
+
expect(context.workingState.workdir).toBe(process.cwd());
|
|
52
|
+
expect(context.workingState.recentFiles).toEqual([]);
|
|
53
|
+
expect(context.workingState.openFiles).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("creates context with empty tasks", () => {
|
|
57
|
+
const context = createEmptyContext("agent-123", "test-agent");
|
|
58
|
+
expect(context.tasks.tasks).toEqual([]);
|
|
59
|
+
expect(context.tasks.currentGoal).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("Context Storage", () => {
|
|
65
|
+
const testAgentId = "test-agent-123";
|
|
66
|
+
const testContext: AgentContext = {
|
|
67
|
+
version: CONTEXT_VERSION,
|
|
68
|
+
agentId: testAgentId,
|
|
69
|
+
agentName: "test-agent",
|
|
70
|
+
savedAt: "2026-02-24T10:00:00.000Z",
|
|
71
|
+
conversation: {
|
|
72
|
+
messageCount: 5,
|
|
73
|
+
topics: ["testing", "context"],
|
|
74
|
+
accomplishments: ["wrote tests"],
|
|
75
|
+
recentMessages: [],
|
|
76
|
+
},
|
|
77
|
+
workingState: {
|
|
78
|
+
workdir: "/home/user/project",
|
|
79
|
+
recentFiles: ["file1.ts", "file2.ts"],
|
|
80
|
+
openFiles: ["file1.ts"],
|
|
81
|
+
gitBranch: "main",
|
|
82
|
+
},
|
|
83
|
+
tasks: {
|
|
84
|
+
tasks: [{ content: "test task", status: "in_progress", priority: "high" }],
|
|
85
|
+
currentGoal: "implement context",
|
|
86
|
+
},
|
|
87
|
+
custom: { key: "value" },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
vi.clearAllMocks();
|
|
92
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterEach(() => {
|
|
96
|
+
vi.resetAllMocks();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("getContextPath", () => {
|
|
100
|
+
it("returns correct path for agent ID", () => {
|
|
101
|
+
const contextPath = getContextPath("agent-123");
|
|
102
|
+
expect(contextPath).toContain("agent-123.json");
|
|
103
|
+
expect(contextPath).toContain(".agentmesh/context");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("saveContext", () => {
|
|
108
|
+
it("creates context directory if needed", () => {
|
|
109
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
110
|
+
mockFs.mkdirSync.mockReturnValue(undefined);
|
|
111
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
112
|
+
|
|
113
|
+
saveContext(testContext);
|
|
114
|
+
|
|
115
|
+
expect(mockFs.mkdirSync).toHaveBeenCalledWith(expect.stringContaining(".agentmesh/context"), {
|
|
116
|
+
recursive: true,
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("writes context to file", () => {
|
|
121
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
122
|
+
|
|
123
|
+
saveContext(testContext);
|
|
124
|
+
|
|
125
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
|
|
126
|
+
expect.stringContaining(`${testAgentId}.json`),
|
|
127
|
+
expect.any(String),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("updates savedAt timestamp", () => {
|
|
132
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
133
|
+
const before = new Date().toISOString();
|
|
134
|
+
|
|
135
|
+
saveContext(testContext);
|
|
136
|
+
|
|
137
|
+
const writtenData = JSON.parse(mockFs.writeFileSync.mock.calls[0][1] as string);
|
|
138
|
+
expect(new Date(writtenData.savedAt).getTime()).toBeGreaterThanOrEqual(
|
|
139
|
+
new Date(before).getTime(),
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("loadContext", () => {
|
|
145
|
+
it("returns null if file does not exist", () => {
|
|
146
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
147
|
+
|
|
148
|
+
const result = loadContext("nonexistent");
|
|
149
|
+
|
|
150
|
+
expect(result).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("loads and parses context file", () => {
|
|
154
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(testContext));
|
|
155
|
+
|
|
156
|
+
const result = loadContext(testAgentId);
|
|
157
|
+
|
|
158
|
+
expect(result).toEqual(testContext);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("returns null for version mismatch", () => {
|
|
162
|
+
const oldContext = { ...testContext, version: 0 };
|
|
163
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(oldContext));
|
|
164
|
+
|
|
165
|
+
const result = loadContext(testAgentId);
|
|
166
|
+
|
|
167
|
+
expect(result).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("returns null for invalid JSON", () => {
|
|
171
|
+
mockFs.readFileSync.mockReturnValue("invalid json");
|
|
172
|
+
|
|
173
|
+
const result = loadContext(testAgentId);
|
|
174
|
+
|
|
175
|
+
expect(result).toBeNull();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("loadOrCreateContext", () => {
|
|
180
|
+
it("returns existing context if found", () => {
|
|
181
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(testContext));
|
|
182
|
+
|
|
183
|
+
const result = loadOrCreateContext(testAgentId, "test-agent");
|
|
184
|
+
|
|
185
|
+
expect(result).toEqual(testContext);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("creates new context if not found", () => {
|
|
189
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
190
|
+
|
|
191
|
+
const result = loadOrCreateContext("new-agent", "new-name");
|
|
192
|
+
|
|
193
|
+
expect(result.agentId).toBe("new-agent");
|
|
194
|
+
expect(result.agentName).toBe("new-name");
|
|
195
|
+
expect(result.version).toBe(CONTEXT_VERSION);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("deleteContext", () => {
|
|
200
|
+
it("deletes context file if exists", () => {
|
|
201
|
+
mockFs.unlinkSync.mockReturnValue(undefined);
|
|
202
|
+
|
|
203
|
+
const result = deleteContext(testAgentId);
|
|
204
|
+
|
|
205
|
+
expect(result).toBe(true);
|
|
206
|
+
expect(mockFs.unlinkSync).toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("returns false if file does not exist", () => {
|
|
210
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
211
|
+
|
|
212
|
+
const result = deleteContext("nonexistent");
|
|
213
|
+
|
|
214
|
+
expect(result).toBe(false);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("listContexts", () => {
|
|
219
|
+
it("returns list of saved contexts", () => {
|
|
220
|
+
mockFs.readdirSync.mockReturnValue(["agent1.json", "agent2.json"] as unknown as ReturnType<
|
|
221
|
+
typeof fs.readdirSync
|
|
222
|
+
>);
|
|
223
|
+
mockFs.readFileSync.mockImplementation((filePath) => {
|
|
224
|
+
const id = path.basename(filePath as string, ".json");
|
|
225
|
+
return JSON.stringify({
|
|
226
|
+
...testContext,
|
|
227
|
+
agentId: id,
|
|
228
|
+
savedAt: id === "agent1" ? "2026-02-24T11:00:00.000Z" : "2026-02-24T10:00:00.000Z",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = listContexts();
|
|
233
|
+
|
|
234
|
+
expect(result).toHaveLength(2);
|
|
235
|
+
expect(result[0].agentId).toBe("agent1"); // More recent first
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("filters non-json files", () => {
|
|
239
|
+
mockFs.readdirSync.mockReturnValue([
|
|
240
|
+
"agent1.json",
|
|
241
|
+
"readme.md",
|
|
242
|
+
".gitkeep",
|
|
243
|
+
] as unknown as ReturnType<typeof fs.readdirSync>);
|
|
244
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(testContext));
|
|
245
|
+
|
|
246
|
+
const result = listContexts();
|
|
247
|
+
|
|
248
|
+
expect(result).toHaveLength(1);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("updateContext", () => {
|
|
253
|
+
it("updates context fields", () => {
|
|
254
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(testContext));
|
|
255
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
256
|
+
|
|
257
|
+
const result = updateContext(testAgentId, {
|
|
258
|
+
conversation: {
|
|
259
|
+
...testContext.conversation,
|
|
260
|
+
messageCount: 10,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
expect(result?.conversation.messageCount).toBe(10);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("returns null if context not found", () => {
|
|
268
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
269
|
+
|
|
270
|
+
const result = updateContext("nonexistent", {});
|
|
271
|
+
|
|
272
|
+
expect(result).toBeNull();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("preserves version and agentId", () => {
|
|
276
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(testContext));
|
|
277
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
278
|
+
|
|
279
|
+
const result = updateContext(testAgentId, {
|
|
280
|
+
agentName: "new-name",
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(result?.version).toBe(CONTEXT_VERSION);
|
|
284
|
+
expect(result?.agentId).toBe(testAgentId);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("exportContext", () => {
|
|
289
|
+
it("exports context to file", () => {
|
|
290
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(testContext));
|
|
291
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
292
|
+
|
|
293
|
+
const result = exportContext(testAgentId, "/tmp/export.json");
|
|
294
|
+
|
|
295
|
+
expect(result).toBe(true);
|
|
296
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith("/tmp/export.json", expect.any(String));
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("returns false if context not found", () => {
|
|
300
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
301
|
+
|
|
302
|
+
const result = exportContext("nonexistent", "/tmp/export.json");
|
|
303
|
+
|
|
304
|
+
expect(result).toBe(false);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("importContext", () => {
|
|
309
|
+
it("imports context from file", () => {
|
|
310
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify(testContext));
|
|
311
|
+
mockFs.writeFileSync.mockReturnValue(undefined);
|
|
312
|
+
|
|
313
|
+
const result = importContext("/tmp/import.json");
|
|
314
|
+
|
|
315
|
+
expect(result).not.toBeNull();
|
|
316
|
+
expect(result?.agentId).toBe(testAgentId);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("returns null if file not found", () => {
|
|
320
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
321
|
+
|
|
322
|
+
const result = importContext("/tmp/nonexistent.json");
|
|
323
|
+
|
|
324
|
+
expect(result).toBeNull();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("returns null for invalid context", () => {
|
|
328
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({ invalid: "context" }));
|
|
329
|
+
|
|
330
|
+
const result = importContext("/tmp/invalid.json");
|
|
331
|
+
|
|
332
|
+
expect(result).toBeNull();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe("Handoff Context", () => {
|
|
338
|
+
const testContext: AgentContext = {
|
|
339
|
+
version: CONTEXT_VERSION,
|
|
340
|
+
agentId: "test-agent-123",
|
|
341
|
+
agentName: "test-agent",
|
|
342
|
+
savedAt: "2026-02-24T10:00:00.000Z",
|
|
343
|
+
conversation: {
|
|
344
|
+
messageCount: 5,
|
|
345
|
+
topics: ["testing", "context", "handoff"],
|
|
346
|
+
accomplishments: ["wrote tests", "added feature"],
|
|
347
|
+
recentMessages: [],
|
|
348
|
+
},
|
|
349
|
+
workingState: {
|
|
350
|
+
workdir: "/home/user/project",
|
|
351
|
+
recentFiles: ["file1.ts", "file2.ts"],
|
|
352
|
+
openFiles: ["file1.ts"],
|
|
353
|
+
gitBranch: "feature/context",
|
|
354
|
+
},
|
|
355
|
+
tasks: {
|
|
356
|
+
tasks: [
|
|
357
|
+
{ content: "task in progress", status: "in_progress", priority: "high" },
|
|
358
|
+
{ content: "pending task", status: "pending", priority: "medium" },
|
|
359
|
+
{ content: "completed task", status: "completed", priority: "low" },
|
|
360
|
+
],
|
|
361
|
+
currentGoal: "implement context persistence",
|
|
362
|
+
},
|
|
363
|
+
custom: { key: "value" },
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
describe("extractHandoffContext", () => {
|
|
367
|
+
it("extracts workdir and git branch", () => {
|
|
368
|
+
const handoff = extractHandoffContext(testContext);
|
|
369
|
+
expect(handoff.workdir).toBe("/home/user/project");
|
|
370
|
+
expect(handoff.gitBranch).toBe("feature/context");
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("extracts current goal", () => {
|
|
374
|
+
const handoff = extractHandoffContext(testContext);
|
|
375
|
+
expect(handoff.currentGoal).toBe("implement context persistence");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("only includes active tasks", () => {
|
|
379
|
+
const handoff = extractHandoffContext(testContext);
|
|
380
|
+
expect(handoff.activeTasks).toHaveLength(2);
|
|
381
|
+
expect(handoff.activeTasks[0].content).toBe("task in progress");
|
|
382
|
+
expect(handoff.activeTasks[1].content).toBe("pending task");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("includes recent accomplishments", () => {
|
|
386
|
+
const handoff = extractHandoffContext(testContext);
|
|
387
|
+
expect(handoff.recentAccomplishments).toContain("wrote tests");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("includes topics", () => {
|
|
391
|
+
const handoff = extractHandoffContext(testContext);
|
|
392
|
+
expect(handoff.topics).toContain("testing");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("includes custom context if present", () => {
|
|
396
|
+
const handoff = extractHandoffContext(testContext);
|
|
397
|
+
expect(handoff.custom).toEqual({ key: "value" });
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
describe("serializeHandoffContext", () => {
|
|
402
|
+
it("serializes to JSON string", () => {
|
|
403
|
+
const handoff = extractHandoffContext(testContext);
|
|
404
|
+
const serialized = serializeHandoffContext(handoff);
|
|
405
|
+
expect(typeof serialized).toBe("string");
|
|
406
|
+
expect(JSON.parse(serialized)).toEqual(handoff);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe("parseHandoffContext", () => {
|
|
411
|
+
it("parses valid JSON", () => {
|
|
412
|
+
const handoff = extractHandoffContext(testContext);
|
|
413
|
+
const serialized = serializeHandoffContext(handoff);
|
|
414
|
+
const parsed = parseHandoffContext(serialized);
|
|
415
|
+
expect(parsed).toEqual(handoff);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("returns null for invalid JSON", () => {
|
|
419
|
+
const parsed = parseHandoffContext("invalid json");
|
|
420
|
+
expect(parsed).toBeNull();
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
describe("formatHandoffContextSummary", () => {
|
|
425
|
+
it("formats workdir", () => {
|
|
426
|
+
const handoff = extractHandoffContext(testContext);
|
|
427
|
+
const summary = formatHandoffContextSummary(handoff);
|
|
428
|
+
expect(summary).toContain("Working directory: /home/user/project");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("formats git branch", () => {
|
|
432
|
+
const handoff = extractHandoffContext(testContext);
|
|
433
|
+
const summary = formatHandoffContextSummary(handoff);
|
|
434
|
+
expect(summary).toContain("Git branch: feature/context");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("formats current goal", () => {
|
|
438
|
+
const handoff = extractHandoffContext(testContext);
|
|
439
|
+
const summary = formatHandoffContextSummary(handoff);
|
|
440
|
+
expect(summary).toContain("Current goal: implement context persistence");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("formats active tasks", () => {
|
|
444
|
+
const handoff = extractHandoffContext(testContext);
|
|
445
|
+
const summary = formatHandoffContextSummary(handoff);
|
|
446
|
+
expect(summary).toContain("Active tasks:");
|
|
447
|
+
expect(summary).toContain("> task in progress");
|
|
448
|
+
expect(summary).toContain("- pending task");
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it("formats accomplishments", () => {
|
|
452
|
+
const handoff = extractHandoffContext(testContext);
|
|
453
|
+
const summary = formatHandoffContextSummary(handoff);
|
|
454
|
+
expect(summary).toContain("Recent accomplishments:");
|
|
455
|
+
expect(summary).toContain("- wrote tests");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("formats topics", () => {
|
|
459
|
+
const handoff = extractHandoffContext(testContext);
|
|
460
|
+
const summary = formatHandoffContextSummary(handoff);
|
|
461
|
+
expect(summary).toContain("Topics:");
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { getAgentState, loadState } from "../config/loader.js";
|
|
3
|
+
import { extractHandoffContext, formatHandoffContextSummary } from "../context/handoff.js";
|
|
4
|
+
import {
|
|
5
|
+
CONTEXT_DIR,
|
|
6
|
+
deleteContext,
|
|
7
|
+
exportContext,
|
|
8
|
+
importContext,
|
|
9
|
+
listContexts,
|
|
10
|
+
loadContext,
|
|
11
|
+
} from "../context/index.js";
|
|
12
|
+
|
|
13
|
+
export interface ContextOptions {
|
|
14
|
+
output?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function contextCmd(
|
|
18
|
+
action: string,
|
|
19
|
+
nameOrPath?: string,
|
|
20
|
+
options?: ContextOptions,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
switch (action) {
|
|
23
|
+
case "show":
|
|
24
|
+
await showContext(nameOrPath);
|
|
25
|
+
break;
|
|
26
|
+
case "clear":
|
|
27
|
+
await clearContext(nameOrPath);
|
|
28
|
+
break;
|
|
29
|
+
case "export":
|
|
30
|
+
await exportContextCmd(nameOrPath, options?.output);
|
|
31
|
+
break;
|
|
32
|
+
case "import":
|
|
33
|
+
await importContextCmd(nameOrPath);
|
|
34
|
+
break;
|
|
35
|
+
case "list":
|
|
36
|
+
case "ls":
|
|
37
|
+
await listContextsCmd();
|
|
38
|
+
break;
|
|
39
|
+
case "path":
|
|
40
|
+
console.log(CONTEXT_DIR);
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
console.log(pc.red(`Unknown action: ${action}`));
|
|
44
|
+
console.log("Available actions: show, clear, export, import, list, path");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function showContext(name?: string): Promise<void> {
|
|
50
|
+
const agentId = await resolveAgentId(name);
|
|
51
|
+
if (!agentId) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const context = loadContext(agentId);
|
|
56
|
+
if (!context) {
|
|
57
|
+
console.log(pc.yellow(`No saved context found for agent.`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log(pc.bold(`Context for ${context.agentName} (${context.agentId}):`));
|
|
62
|
+
console.log(pc.dim(`Saved at: ${new Date(context.savedAt).toLocaleString()}`));
|
|
63
|
+
console.log();
|
|
64
|
+
|
|
65
|
+
// Working state
|
|
66
|
+
console.log(pc.bold("Working State:"));
|
|
67
|
+
console.log(` Directory: ${context.workingState.workdir}`);
|
|
68
|
+
if (context.workingState.gitBranch) {
|
|
69
|
+
console.log(` Git branch: ${context.workingState.gitBranch}`);
|
|
70
|
+
}
|
|
71
|
+
if (context.workingState.recentFiles.length > 0) {
|
|
72
|
+
console.log(` Recent files: ${context.workingState.recentFiles.slice(0, 5).join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
console.log();
|
|
75
|
+
|
|
76
|
+
// Tasks
|
|
77
|
+
const activeTasks = context.tasks.tasks.filter(
|
|
78
|
+
(t) => t.status === "in_progress" || t.status === "pending",
|
|
79
|
+
);
|
|
80
|
+
if (activeTasks.length > 0 || context.tasks.currentGoal) {
|
|
81
|
+
console.log(pc.bold("Tasks:"));
|
|
82
|
+
if (context.tasks.currentGoal) {
|
|
83
|
+
console.log(` Goal: ${context.tasks.currentGoal}`);
|
|
84
|
+
}
|
|
85
|
+
for (const task of activeTasks) {
|
|
86
|
+
const icon = task.status === "in_progress" ? pc.yellow(">") : pc.dim("-");
|
|
87
|
+
const priority =
|
|
88
|
+
task.priority === "high" ? pc.red(`[${task.priority}]`) : pc.dim(`[${task.priority}]`);
|
|
89
|
+
console.log(` ${icon} ${task.content} ${priority}`);
|
|
90
|
+
}
|
|
91
|
+
console.log();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Conversation
|
|
95
|
+
if (context.conversation.topics.length > 0 || context.conversation.accomplishments.length > 0) {
|
|
96
|
+
console.log(pc.bold("Conversation:"));
|
|
97
|
+
if (context.conversation.topics.length > 0) {
|
|
98
|
+
console.log(` Topics: ${context.conversation.topics.join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
if (context.conversation.accomplishments.length > 0) {
|
|
101
|
+
console.log(" Accomplishments:");
|
|
102
|
+
for (const acc of context.conversation.accomplishments.slice(0, 5)) {
|
|
103
|
+
console.log(` - ${acc}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Custom context
|
|
110
|
+
if (Object.keys(context.custom).length > 0) {
|
|
111
|
+
console.log(pc.bold("Custom Context:"));
|
|
112
|
+
console.log(JSON.stringify(context.custom, null, 2));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function clearContext(name?: string): Promise<void> {
|
|
117
|
+
const agentId = await resolveAgentId(name);
|
|
118
|
+
if (!agentId) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const deleted = deleteContext(agentId);
|
|
123
|
+
if (deleted) {
|
|
124
|
+
console.log(pc.green(`Context cleared for agent.`));
|
|
125
|
+
} else {
|
|
126
|
+
console.log(pc.yellow(`No context found to clear.`));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function exportContextCmd(name?: string, outputPath?: string): Promise<void> {
|
|
131
|
+
const agentId = await resolveAgentId(name);
|
|
132
|
+
if (!agentId) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const context = loadContext(agentId);
|
|
137
|
+
if (!context) {
|
|
138
|
+
console.log(pc.yellow(`No saved context found for agent.`));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const output = outputPath || `${context.agentName}-context.json`;
|
|
143
|
+
const success = exportContext(agentId, output);
|
|
144
|
+
|
|
145
|
+
if (success) {
|
|
146
|
+
console.log(pc.green(`Context exported to: ${output}`));
|
|
147
|
+
} else {
|
|
148
|
+
console.log(pc.red(`Failed to export context.`));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function importContextCmd(inputPath?: string): Promise<void> {
|
|
154
|
+
if (!inputPath) {
|
|
155
|
+
console.log(pc.red("Please specify a file path to import."));
|
|
156
|
+
console.log("Usage: agentmesh context import <file>");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const context = importContext(inputPath);
|
|
161
|
+
if (context) {
|
|
162
|
+
console.log(pc.green(`Context imported for agent: ${context.agentName} (${context.agentId})`));
|
|
163
|
+
} else {
|
|
164
|
+
console.log(pc.red(`Failed to import context from: ${inputPath}`));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function listContextsCmd(): Promise<void> {
|
|
170
|
+
const contexts = listContexts();
|
|
171
|
+
|
|
172
|
+
if (contexts.length === 0) {
|
|
173
|
+
console.log(pc.yellow("No saved contexts found."));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(pc.bold("Saved Contexts:"));
|
|
178
|
+
console.log();
|
|
179
|
+
|
|
180
|
+
for (const ctx of contexts) {
|
|
181
|
+
const context = loadContext(ctx.agentId);
|
|
182
|
+
if (!context) continue;
|
|
183
|
+
|
|
184
|
+
console.log(` ${pc.cyan(context.agentName)} (${pc.dim(ctx.agentId.slice(0, 8))}...)`);
|
|
185
|
+
console.log(` Saved: ${new Date(ctx.savedAt).toLocaleString()}`);
|
|
186
|
+
if (context.tasks.currentGoal) {
|
|
187
|
+
console.log(` Goal: ${context.tasks.currentGoal}`);
|
|
188
|
+
}
|
|
189
|
+
console.log();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function resolveAgentId(name?: string): Promise<string | null> {
|
|
194
|
+
// If no name provided, try to get from environment
|
|
195
|
+
if (!name) {
|
|
196
|
+
const envAgentId = process.env.AGENTMESH_AGENT_ID;
|
|
197
|
+
if (envAgentId) {
|
|
198
|
+
return envAgentId;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Try to find single running agent
|
|
202
|
+
const state = loadState();
|
|
203
|
+
if (state.agents.length === 1) {
|
|
204
|
+
return state.agents[0].agentId;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (state.agents.length === 0) {
|
|
208
|
+
console.log(pc.yellow("No running agents found."));
|
|
209
|
+
console.log("Usage: agentmesh context show <name>");
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(pc.yellow("Multiple agents running. Please specify a name."));
|
|
214
|
+
console.log("Usage: agentmesh context show <name>");
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Try to find by name
|
|
219
|
+
const agentState = getAgentState(name);
|
|
220
|
+
if (agentState) {
|
|
221
|
+
return agentState.agentId;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Maybe it's already an agent ID
|
|
225
|
+
const context = loadContext(name);
|
|
226
|
+
if (context) {
|
|
227
|
+
return name;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(pc.red(`Agent not found: ${name}`));
|
|
231
|
+
return null;
|
|
232
|
+
}
|