@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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/dist/__tests__/context.test.d.ts +1 -0
  3. package/dist/__tests__/context.test.js +353 -0
  4. package/dist/__tests__/context.test.js.map +1 -0
  5. package/dist/cli/config.d.ts +1 -0
  6. package/dist/cli/config.js +129 -0
  7. package/dist/cli/config.js.map +1 -0
  8. package/dist/cli/context.d.ts +4 -0
  9. package/dist/cli/context.js +190 -0
  10. package/dist/cli/context.js.map +1 -0
  11. package/dist/cli/index.js +76 -0
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/logs.d.ts +4 -0
  14. package/dist/cli/logs.js +54 -0
  15. package/dist/cli/logs.js.map +1 -0
  16. package/dist/cli/restart.d.ts +1 -0
  17. package/dist/cli/restart.js +41 -0
  18. package/dist/cli/restart.js.map +1 -0
  19. package/dist/cli/start.d.ts +1 -0
  20. package/dist/cli/start.js +10 -5
  21. package/dist/cli/start.js.map +1 -1
  22. package/dist/cli/status.d.ts +1 -0
  23. package/dist/cli/status.js +73 -0
  24. package/dist/cli/status.js.map +1 -0
  25. package/dist/context/handoff.d.ts +48 -0
  26. package/dist/context/handoff.js +88 -0
  27. package/dist/context/handoff.js.map +1 -0
  28. package/dist/context/index.d.ts +7 -0
  29. package/dist/context/index.js +8 -0
  30. package/dist/context/index.js.map +1 -0
  31. package/dist/context/schema.d.ts +82 -0
  32. package/dist/context/schema.js +33 -0
  33. package/dist/context/schema.js.map +1 -0
  34. package/dist/context/storage.d.ts +49 -0
  35. package/dist/context/storage.js +172 -0
  36. package/dist/context/storage.js.map +1 -0
  37. package/dist/core/daemon.d.ts +7 -0
  38. package/dist/core/daemon.js +53 -2
  39. package/dist/core/daemon.js.map +1 -1
  40. package/dist/core/heartbeat.d.ts +6 -0
  41. package/dist/core/heartbeat.js +8 -0
  42. package/dist/core/heartbeat.js.map +1 -1
  43. package/dist/core/injector.d.ts +9 -0
  44. package/dist/core/injector.js +55 -3
  45. package/dist/core/injector.js.map +1 -1
  46. package/dist/core/tmux.d.ts +13 -0
  47. package/dist/core/tmux.js +62 -0
  48. package/dist/core/tmux.js.map +1 -1
  49. package/package.json +11 -11
  50. package/src/__tests__/context.test.ts +464 -0
  51. package/src/cli/config.ts +148 -0
  52. package/src/cli/context.ts +232 -0
  53. package/src/cli/index.ts +76 -0
  54. package/src/cli/logs.ts +64 -0
  55. package/src/cli/restart.ts +50 -0
  56. package/src/cli/start.ts +11 -9
  57. package/src/cli/status.ts +83 -0
  58. package/src/context/handoff.ts +122 -0
  59. package/src/context/index.ts +8 -0
  60. package/src/context/schema.ts +111 -0
  61. package/src/context/storage.ts +197 -0
  62. package/src/core/daemon.ts +59 -1
  63. package/src/core/heartbeat.ts +13 -0
  64. package/src/core/injector.ts +74 -30
  65. 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();
@@ -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 { AgentDaemon } from "../core/daemon.js";
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(options);
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
+ }