@agentmeshhq/agent 0.1.6 → 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/config.d.ts +1 -0
- package/dist/cli/config.js +129 -0
- package/dist/cli/config.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 +76 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.d.ts +4 -0
- package/dist/cli/logs.js +54 -0
- package/dist/cli/logs.js.map +1 -0
- package/dist/cli/restart.d.ts +1 -0
- package/dist/cli/restart.js +41 -0
- package/dist/cli/restart.js.map +1 -0
- 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/cli/status.d.ts +1 -0
- package/dist/cli/status.js +73 -0
- package/dist/cli/status.js.map +1 -0
- 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/config.ts +148 -0
- package/src/cli/context.ts +232 -0
- package/src/cli/index.ts +76 -0
- package/src/cli/logs.ts +64 -0
- package/src/cli/restart.ts +50 -0
- package/src/cli/start.ts +11 -9
- package/src/cli/status.ts +83 -0
- 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,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
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -4,10 +4,15 @@ import { createRequire } from "node:module";
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import pc from "picocolors";
|
|
6
6
|
import { attach } from "./attach.js";
|
|
7
|
+
import { configCmd } from "./config.js";
|
|
8
|
+
import { contextCmd } from "./context.js";
|
|
7
9
|
import { init } from "./init.js";
|
|
8
10
|
import { list } from "./list.js";
|
|
11
|
+
import { logs } from "./logs.js";
|
|
9
12
|
import { nudge } from "./nudge.js";
|
|
13
|
+
import { restart } from "./restart.js";
|
|
10
14
|
import { start } from "./start.js";
|
|
15
|
+
import { status } from "./status.js";
|
|
11
16
|
import { stop } from "./stop.js";
|
|
12
17
|
import { token } from "./token.js";
|
|
13
18
|
import { whoami } from "./whoami.js";
|
|
@@ -42,6 +47,7 @@ program
|
|
|
42
47
|
.option("-w, --workdir <path>", "Working directory")
|
|
43
48
|
.option("-m, --model <model>", "Model identifier")
|
|
44
49
|
.option("-f, --foreground", "Run in foreground (blocking)")
|
|
50
|
+
.option("--no-context", "Start fresh without restoring previous context")
|
|
45
51
|
.action(async (options) => {
|
|
46
52
|
try {
|
|
47
53
|
await start(options);
|
|
@@ -131,4 +137,74 @@ program
|
|
|
131
137
|
}
|
|
132
138
|
});
|
|
133
139
|
|
|
140
|
+
program
|
|
141
|
+
.command("status")
|
|
142
|
+
.description("Show AgentMesh status and health")
|
|
143
|
+
.action(async () => {
|
|
144
|
+
try {
|
|
145
|
+
await status();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(pc.red((error as Error).message));
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
program
|
|
153
|
+
.command("logs")
|
|
154
|
+
.description("View agent session logs")
|
|
155
|
+
.argument("<name>", "Agent name")
|
|
156
|
+
.option("-f, --follow", "Follow log output (attach read-only)")
|
|
157
|
+
.option("-n, --lines <number>", "Number of lines to show", "50")
|
|
158
|
+
.action(async (name, options) => {
|
|
159
|
+
try {
|
|
160
|
+
await logs(name, { follow: options.follow, lines: parseInt(options.lines, 10) });
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error(pc.red((error as Error).message));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
program
|
|
168
|
+
.command("restart")
|
|
169
|
+
.description("Restart an agent (preserves agent ID)")
|
|
170
|
+
.argument("<name>", "Agent name")
|
|
171
|
+
.action(async (name) => {
|
|
172
|
+
try {
|
|
173
|
+
await restart(name);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(pc.red((error as Error).message));
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
program
|
|
181
|
+
.command("config")
|
|
182
|
+
.description("View or edit configuration")
|
|
183
|
+
.argument("[action]", "Action: show (default), get, set, edit, path")
|
|
184
|
+
.argument("[key]", "Config key (for get/set)")
|
|
185
|
+
.argument("[value]", "Config value (for set)")
|
|
186
|
+
.action(async (action, key, value) => {
|
|
187
|
+
try {
|
|
188
|
+
await configCmd(action || "show", key, value);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(pc.red((error as Error).message));
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
program
|
|
196
|
+
.command("context")
|
|
197
|
+
.description("Manage agent context persistence")
|
|
198
|
+
.argument("[action]", "Action: show (default), clear, export, import, list, path")
|
|
199
|
+
.argument("[name]", "Agent name or file path (for import)")
|
|
200
|
+
.option("-o, --output <path>", "Output file path (for export)")
|
|
201
|
+
.action(async (action, name, options) => {
|
|
202
|
+
try {
|
|
203
|
+
await contextCmd(action || "show", name, options);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error(pc.red((error as Error).message));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
134
210
|
program.parse();
|
package/src/cli/logs.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { getAgentState, loadConfig } from "../config/loader.js";
|
|
4
|
+
import { getSessionName, sessionExists } from "../core/tmux.js";
|
|
5
|
+
|
|
6
|
+
export async function logs(
|
|
7
|
+
name: string,
|
|
8
|
+
options: { follow?: boolean; lines?: number },
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const config = loadConfig();
|
|
11
|
+
|
|
12
|
+
if (!config) {
|
|
13
|
+
console.log(pc.red("No config found. Run 'agentmesh init' first."));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const agent = getAgentState(name);
|
|
18
|
+
if (!agent) {
|
|
19
|
+
console.log(pc.red(`Agent "${name}" not found.`));
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sessionName = getSessionName(name);
|
|
24
|
+
if (!sessionExists(sessionName)) {
|
|
25
|
+
console.log(pc.red(`Session "${sessionName}" not running.`));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lines = options.lines || 50;
|
|
30
|
+
|
|
31
|
+
// Capture pane content using tmux capture-pane
|
|
32
|
+
if (options.follow) {
|
|
33
|
+
// Follow mode - attach in view-only mode
|
|
34
|
+
console.log(pc.dim(`Following logs for "${name}"... (Ctrl+C to exit)`));
|
|
35
|
+
console.log(pc.dim("─".repeat(60)));
|
|
36
|
+
|
|
37
|
+
// Use tmux pipe-pane to stream output
|
|
38
|
+
const tmux = spawn("tmux", ["attach-session", "-t", sessionName, "-r"], {
|
|
39
|
+
stdio: "inherit",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
tmux.on("exit", (code) => {
|
|
43
|
+
process.exit(code ?? 0);
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
// Static mode - capture and print
|
|
47
|
+
try {
|
|
48
|
+
const { execSync } = await import("node:child_process");
|
|
49
|
+
|
|
50
|
+
// Capture the pane history
|
|
51
|
+
const output = execSync(`tmux capture-pane -t "${sessionName}" -p -S -${lines}`, {
|
|
52
|
+
encoding: "utf-8",
|
|
53
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
console.log(pc.dim(`Last ${lines} lines from "${name}":`));
|
|
57
|
+
console.log(pc.dim("─".repeat(60)));
|
|
58
|
+
console.log(output);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.log(pc.red(`Failed to capture logs: ${(error as Error).message}`));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { getAgentState, loadConfig } from "../config/loader.js";
|
|
3
|
+
import { destroySession, getSessionName, sessionExists } from "../core/tmux.js";
|
|
4
|
+
import { start } from "./start.js";
|
|
5
|
+
import { stop } from "./stop.js";
|
|
6
|
+
|
|
7
|
+
export async function restart(name: string): Promise<void> {
|
|
8
|
+
const config = loadConfig();
|
|
9
|
+
|
|
10
|
+
if (!config) {
|
|
11
|
+
console.log(pc.red("No config found. Run 'agentmesh init' first."));
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const agent = getAgentState(name);
|
|
16
|
+
if (!agent) {
|
|
17
|
+
console.log(pc.red(`Agent "${name}" not found in state.`));
|
|
18
|
+
console.log(pc.dim("Use 'agentmesh start' to create a new agent."));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(pc.dim(`Restarting agent "${name}"...`));
|
|
23
|
+
|
|
24
|
+
// Stop the agent (but preserve the agent ID for re-registration)
|
|
25
|
+
const sessionName = getSessionName(name);
|
|
26
|
+
if (sessionExists(sessionName)) {
|
|
27
|
+
console.log(pc.dim("Stopping current session..."));
|
|
28
|
+
await stop(name);
|
|
29
|
+
|
|
30
|
+
// Small delay to ensure clean shutdown
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Find agent config to get workdir and other settings
|
|
35
|
+
const agentConfig = config.agents.find((a) => a.name === name);
|
|
36
|
+
|
|
37
|
+
console.log(pc.dim("Starting new session..."));
|
|
38
|
+
|
|
39
|
+
// Start with the same settings, agent ID will be reused from state
|
|
40
|
+
await start({
|
|
41
|
+
name,
|
|
42
|
+
command: agentConfig?.command,
|
|
43
|
+
workdir: agentConfig?.workdir,
|
|
44
|
+
model: agentConfig?.model,
|
|
45
|
+
foreground: false,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(pc.green(`Agent "${name}" restarted.`));
|
|
49
|
+
console.log(pc.dim(`Agent ID preserved: ${agent.agentId}`));
|
|
50
|
+
}
|
package/src/cli/start.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { loadConfig, getAgentState } from "../config/loader.js";
|
|
6
|
-
import { sessionExists, getSessionName } from "../core/tmux.js";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
7
4
|
import pc from "picocolors";
|
|
5
|
+
import { getAgentState, loadConfig } from "../config/loader.js";
|
|
6
|
+
import { AgentDaemon } from "../core/daemon.js";
|
|
7
|
+
import { getSessionName, sessionExists } from "../core/tmux.js";
|
|
8
8
|
|
|
9
9
|
export interface StartOptions {
|
|
10
10
|
name: string;
|
|
@@ -12,6 +12,7 @@ export interface StartOptions {
|
|
|
12
12
|
workdir?: string;
|
|
13
13
|
model?: string;
|
|
14
14
|
foreground?: boolean;
|
|
15
|
+
noContext?: boolean;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export async function start(options: StartOptions): Promise<void> {
|
|
@@ -40,7 +41,10 @@ export async function start(options: StartOptions): Promise<void> {
|
|
|
40
41
|
// If --foreground flag is set, run in foreground (blocking)
|
|
41
42
|
if (options.foreground) {
|
|
42
43
|
try {
|
|
43
|
-
const daemon = new AgentDaemon(
|
|
44
|
+
const daemon = new AgentDaemon({
|
|
45
|
+
...options,
|
|
46
|
+
restoreContext: !options.noContext,
|
|
47
|
+
});
|
|
44
48
|
await daemon.start();
|
|
45
49
|
// Keep process alive
|
|
46
50
|
await new Promise(() => {});
|
|
@@ -55,16 +59,14 @@ export async function start(options: StartOptions): Promise<void> {
|
|
|
55
59
|
console.log(`Starting agent "${options.name}" in background...`);
|
|
56
60
|
|
|
57
61
|
// Get the path to this CLI
|
|
58
|
-
const cliPath = path.resolve(
|
|
59
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
60
|
-
"../cli/index.js"
|
|
61
|
-
);
|
|
62
|
+
const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../cli/index.js");
|
|
62
63
|
|
|
63
64
|
// Build args for the background process
|
|
64
65
|
const args = ["start", "--name", options.name, "--foreground"];
|
|
65
66
|
if (options.command) args.push("--command", options.command);
|
|
66
67
|
if (options.workdir) args.push("--workdir", options.workdir);
|
|
67
68
|
if (options.model) args.push("--model", options.model);
|
|
69
|
+
if (options.noContext) args.push("--no-context");
|
|
68
70
|
|
|
69
71
|
// Spawn detached background process
|
|
70
72
|
const child = spawn("node", [cliPath, ...args], {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { loadConfig, loadState } from "../config/loader.js";
|
|
3
|
+
import { getSessionName, sessionExists } from "../core/tmux.js";
|
|
4
|
+
|
|
5
|
+
interface HealthResponse {
|
|
6
|
+
status: string;
|
|
7
|
+
service?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function status(): Promise<void> {
|
|
12
|
+
const config = loadConfig();
|
|
13
|
+
|
|
14
|
+
if (!config) {
|
|
15
|
+
console.log(pc.red("No config found. Run 'agentmesh init' first."));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(pc.bold("AgentMesh Status\n"));
|
|
20
|
+
|
|
21
|
+
// Check hub connectivity
|
|
22
|
+
console.log(pc.dim("Hub:"));
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(`${config.hubUrl}/health`, {
|
|
25
|
+
signal: AbortSignal.timeout(5000),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (response.ok) {
|
|
29
|
+
const data = (await response.json()) as HealthResponse;
|
|
30
|
+
console.log(` URL: ${pc.cyan(config.hubUrl)}`);
|
|
31
|
+
console.log(` Status: ${pc.green("connected")}`);
|
|
32
|
+
if (data.version) {
|
|
33
|
+
console.log(` Version: ${pc.dim(data.version)}`);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
console.log(` URL: ${pc.cyan(config.hubUrl)}`);
|
|
37
|
+
console.log(` Status: ${pc.yellow(`HTTP ${response.status}`)}`);
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.log(` URL: ${pc.cyan(config.hubUrl)}`);
|
|
41
|
+
console.log(` Status: ${pc.red("unreachable")}`);
|
|
42
|
+
console.log(` Error: ${pc.dim((error as Error).message)}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check local agents
|
|
46
|
+
const state = loadState();
|
|
47
|
+
const runningAgents = state.agents.filter((a) => {
|
|
48
|
+
if (!a.pid) return false;
|
|
49
|
+
try {
|
|
50
|
+
process.kill(a.pid, 0);
|
|
51
|
+
return sessionExists(getSessionName(a.name));
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log();
|
|
58
|
+
console.log(pc.dim("Local Agents:"));
|
|
59
|
+
console.log(` Running: ${pc.cyan(String(runningAgents.length))}`);
|
|
60
|
+
console.log(` Total: ${pc.dim(String(state.agents.length))}`);
|
|
61
|
+
|
|
62
|
+
if (runningAgents.length > 0) {
|
|
63
|
+
console.log(` Names: ${pc.dim(runningAgents.map((a) => a.name).join(", "))}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check tmux
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(pc.dim("Dependencies:"));
|
|
69
|
+
try {
|
|
70
|
+
const { execSync } = await import("node:child_process");
|
|
71
|
+
const tmuxVersion = execSync("tmux -V", { encoding: "utf-8" }).trim();
|
|
72
|
+
console.log(` tmux: ${pc.green(tmuxVersion)}`);
|
|
73
|
+
} catch {
|
|
74
|
+
console.log(` tmux: ${pc.red("not found")}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Config info
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(pc.dim("Config:"));
|
|
80
|
+
console.log(` Workspace: ${pc.cyan(config.workspace)}`);
|
|
81
|
+
console.log(` Command: ${pc.dim(config.defaults.command)}`);
|
|
82
|
+
console.log(` Model: ${pc.dim(config.defaults.model)}`);
|
|
83
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Handoff Module
|
|
3
|
+
* Functions for sharing context between agents via handoffs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AgentContext } from "./schema.js";
|
|
7
|
+
import { loadContext } from "./storage.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Minimal context for handoff (excludes sensitive/large data)
|
|
11
|
+
*/
|
|
12
|
+
export interface HandoffContext {
|
|
13
|
+
/** Working directory */
|
|
14
|
+
workdir: string;
|
|
15
|
+
/** Git branch if in a repo */
|
|
16
|
+
gitBranch?: string;
|
|
17
|
+
/** Current goal being worked on */
|
|
18
|
+
currentGoal?: string;
|
|
19
|
+
/** Active tasks (in_progress and pending) */
|
|
20
|
+
activeTasks: Array<{
|
|
21
|
+
content: string;
|
|
22
|
+
status: "in_progress" | "pending";
|
|
23
|
+
priority: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** Recent accomplishments */
|
|
26
|
+
recentAccomplishments: string[];
|
|
27
|
+
/** Key topics from conversation */
|
|
28
|
+
topics: string[];
|
|
29
|
+
/** Any custom context data */
|
|
30
|
+
custom?: Record<string, unknown>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extracts handoff-relevant context from full agent context
|
|
35
|
+
*/
|
|
36
|
+
export function extractHandoffContext(context: AgentContext): HandoffContext {
|
|
37
|
+
return {
|
|
38
|
+
workdir: context.workingState.workdir,
|
|
39
|
+
gitBranch: context.workingState.gitBranch,
|
|
40
|
+
currentGoal: context.tasks.currentGoal,
|
|
41
|
+
activeTasks: context.tasks.tasks
|
|
42
|
+
.filter((t) => t.status === "in_progress" || t.status === "pending")
|
|
43
|
+
.map((t) => ({
|
|
44
|
+
content: t.content,
|
|
45
|
+
status: t.status as "in_progress" | "pending",
|
|
46
|
+
priority: t.priority,
|
|
47
|
+
})),
|
|
48
|
+
recentAccomplishments: context.conversation.accomplishments.slice(0, 5),
|
|
49
|
+
topics: context.conversation.topics.slice(0, 10),
|
|
50
|
+
custom: Object.keys(context.custom).length > 0 ? context.custom : undefined,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Gets handoff context for an agent by ID
|
|
56
|
+
*/
|
|
57
|
+
export function getHandoffContextForAgent(agentId: string): HandoffContext | null {
|
|
58
|
+
const context = loadContext(agentId);
|
|
59
|
+
if (!context) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return extractHandoffContext(context);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Serializes handoff context to a string for inclusion in API calls
|
|
67
|
+
*/
|
|
68
|
+
export function serializeHandoffContext(context: HandoffContext): string {
|
|
69
|
+
return JSON.stringify(context);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses handoff context from a string (received from API)
|
|
74
|
+
*/
|
|
75
|
+
export function parseHandoffContext(contextString: string): HandoffContext | null {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(contextString) as HandoffContext;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generates a human-readable summary of handoff context
|
|
85
|
+
*/
|
|
86
|
+
export function formatHandoffContextSummary(context: HandoffContext): string {
|
|
87
|
+
const lines: string[] = [];
|
|
88
|
+
|
|
89
|
+
if (context.workdir) {
|
|
90
|
+
lines.push(`Working directory: ${context.workdir}`);
|
|
91
|
+
}
|
|
92
|
+
if (context.gitBranch) {
|
|
93
|
+
lines.push(`Git branch: ${context.gitBranch}`);
|
|
94
|
+
}
|
|
95
|
+
if (context.currentGoal) {
|
|
96
|
+
lines.push(`Current goal: ${context.currentGoal}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (context.activeTasks.length > 0) {
|
|
100
|
+
lines.push("");
|
|
101
|
+
lines.push("Active tasks:");
|
|
102
|
+
for (const task of context.activeTasks) {
|
|
103
|
+
const icon = task.status === "in_progress" ? ">" : "-";
|
|
104
|
+
lines.push(` ${icon} ${task.content}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (context.recentAccomplishments.length > 0) {
|
|
109
|
+
lines.push("");
|
|
110
|
+
lines.push("Recent accomplishments:");
|
|
111
|
+
for (const acc of context.recentAccomplishments) {
|
|
112
|
+
lines.push(` - ${acc}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (context.topics.length > 0) {
|
|
117
|
+
lines.push("");
|
|
118
|
+
lines.push(`Topics: ${context.topics.join(", ")}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|