agent-kanban 1.0.0
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/dist/client.d.ts +78 -0
- package/dist/client.js +143 -0
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.js +57 -0
- package/dist/commands/start.d.ts +2 -0
- package/dist/commands/start.js +50 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +31 -0
- package/dist/daemon.d.ts +7 -0
- package/dist/daemon.js +205 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +265 -0
- package/dist/links.d.ts +7 -0
- package/dist/links.js +28 -0
- package/dist/output.d.ts +7 -0
- package/dist/output.js +88 -0
- package/dist/processManager.d.ts +24 -0
- package/dist/processManager.js +189 -0
- package/dist/project.d.ts +11 -0
- package/dist/project.js +44 -0
- package/dist/skills/agent-kanban/SKILL.md +116 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +2 -0
- package/dist/usage.js +66 -0
- package/package.json +23 -0
- package/src/client.ts +165 -0
- package/src/commands/link.ts +60 -0
- package/src/commands/start.ts +56 -0
- package/src/config.ts +43 -0
- package/src/daemon.ts +244 -0
- package/src/index.ts +276 -0
- package/src/links.ts +37 -0
- package/src/output.ts +101 -0
- package/src/processManager.ts +222 -0
- package/src/types.ts +12 -0
- package/src/usage.ts +71 -0
- package/tsconfig.json +14 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { setConfigValue, getConfigValue } from "./config.js";
|
|
4
|
+
import { ApiClient } from "./client.js";
|
|
5
|
+
import { getFormat, output, formatTaskList, formatBoard, formatAgentList, formatBoardList, formatRepositoryList } from "./output.js";
|
|
6
|
+
import { registerLinkCommand } from "./commands/link.js";
|
|
7
|
+
import { registerStartCommand } from "./commands/start.js";
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program.name("agent-kanban").description("Agent-first kanban board").version("1.3.0");
|
|
10
|
+
// ─── Config ───
|
|
11
|
+
const configCmd = program.command("config").description("Manage CLI configuration");
|
|
12
|
+
configCmd
|
|
13
|
+
.command("set <key> <value>")
|
|
14
|
+
.description("Set a config value (api-url, api-key)")
|
|
15
|
+
.action((key, value) => {
|
|
16
|
+
setConfigValue(key, value);
|
|
17
|
+
console.log(`Set ${key}`);
|
|
18
|
+
});
|
|
19
|
+
configCmd
|
|
20
|
+
.command("get <key>")
|
|
21
|
+
.description("Get a config value")
|
|
22
|
+
.action((key) => {
|
|
23
|
+
const value = getConfigValue(key);
|
|
24
|
+
if (value)
|
|
25
|
+
console.log(value);
|
|
26
|
+
else {
|
|
27
|
+
console.error(`Not set: ${key}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
// ─── Task ───
|
|
32
|
+
const taskCmd = program.command("task").description("Manage tasks");
|
|
33
|
+
taskCmd
|
|
34
|
+
.command("create")
|
|
35
|
+
.description("Create a new task")
|
|
36
|
+
.requiredOption("--title <title>", "Task title")
|
|
37
|
+
.option("--description <desc>", "Task description")
|
|
38
|
+
.option("--repo <repo>", "Repository name or ID")
|
|
39
|
+
.option("--priority <priority>", "Priority (low, medium, high, urgent)")
|
|
40
|
+
.option("--labels <labels>", "Comma-separated labels")
|
|
41
|
+
.option("--input <json>", "JSON input payload")
|
|
42
|
+
.option("--agent-name <name>", "Agent identity")
|
|
43
|
+
.option("--parent <id>", "Parent task ID (creates subtask)")
|
|
44
|
+
.option("--depends-on <ids>", "Comma-separated task IDs this depends on")
|
|
45
|
+
.option("--format <format>", "Output format (json, text)")
|
|
46
|
+
.action(async (opts) => {
|
|
47
|
+
const client = new ApiClient();
|
|
48
|
+
const body = { title: opts.title };
|
|
49
|
+
if (opts.description)
|
|
50
|
+
body.description = opts.description;
|
|
51
|
+
if (opts.repo)
|
|
52
|
+
body.repository_id = opts.repo;
|
|
53
|
+
if (opts.priority)
|
|
54
|
+
body.priority = opts.priority;
|
|
55
|
+
if (opts.labels)
|
|
56
|
+
body.labels = opts.labels.split(",").map((l) => l.trim());
|
|
57
|
+
if (opts.agentName)
|
|
58
|
+
body.agent_id = opts.agentName;
|
|
59
|
+
if (opts.parent)
|
|
60
|
+
body.created_from = opts.parent;
|
|
61
|
+
if (opts.dependsOn)
|
|
62
|
+
body.depends_on = opts.dependsOn.split(",").map((id) => id.trim());
|
|
63
|
+
if (opts.input) {
|
|
64
|
+
try {
|
|
65
|
+
body.input = JSON.parse(opts.input);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
console.error("Invalid JSON for --input");
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const task = await client.createTask(body);
|
|
73
|
+
const fmt = getFormat(opts.format);
|
|
74
|
+
output(task, fmt, (t) => `Created task ${t.id}: ${t.title}`);
|
|
75
|
+
});
|
|
76
|
+
taskCmd
|
|
77
|
+
.command("list")
|
|
78
|
+
.description("List tasks")
|
|
79
|
+
.option("--repo <repo>", "Filter by repository ID")
|
|
80
|
+
.option("--status <status>", "Filter by status (column name)")
|
|
81
|
+
.option("--label <label>", "Filter by label")
|
|
82
|
+
.option("--parent <id>", "Filter subtasks of a parent task")
|
|
83
|
+
.option("--format <format>", "Output format (json, text)")
|
|
84
|
+
.action(async (opts) => {
|
|
85
|
+
const client = new ApiClient();
|
|
86
|
+
const params = {};
|
|
87
|
+
if (opts.repo)
|
|
88
|
+
params.repository_id = opts.repo;
|
|
89
|
+
if (opts.status)
|
|
90
|
+
params.status = opts.status;
|
|
91
|
+
if (opts.label)
|
|
92
|
+
params.label = opts.label;
|
|
93
|
+
if (opts.parent)
|
|
94
|
+
params.parent = opts.parent;
|
|
95
|
+
const tasks = await client.listTasks(params);
|
|
96
|
+
const fmt = getFormat(opts.format);
|
|
97
|
+
output(tasks, fmt, formatTaskList);
|
|
98
|
+
});
|
|
99
|
+
taskCmd
|
|
100
|
+
.command("claim <id>")
|
|
101
|
+
.description("Claim an assigned task — start working on it")
|
|
102
|
+
.option("--agent-name <name>", "Agent identity")
|
|
103
|
+
.option("--format <format>", "Output format (json, text)")
|
|
104
|
+
.action(async (id, opts) => {
|
|
105
|
+
const client = new ApiClient();
|
|
106
|
+
const task = await client.claimTask(id, opts.agentName);
|
|
107
|
+
const fmt = getFormat(opts.format);
|
|
108
|
+
output(task, fmt, (t) => `Claimed task ${t.id}: ${t.title} (now in progress)`);
|
|
109
|
+
});
|
|
110
|
+
taskCmd
|
|
111
|
+
.command("log <id> <message>")
|
|
112
|
+
.description("Add a log entry to a task")
|
|
113
|
+
.option("--agent-name <name>", "Agent identity")
|
|
114
|
+
.action(async (id, message, opts) => {
|
|
115
|
+
const client = new ApiClient();
|
|
116
|
+
await client.addLog(id, message, opts.agentName);
|
|
117
|
+
console.log("Log entry added.");
|
|
118
|
+
});
|
|
119
|
+
taskCmd
|
|
120
|
+
.command("cancel <id>")
|
|
121
|
+
.description("Cancel a task")
|
|
122
|
+
.option("--agent-name <name>", "Agent identity")
|
|
123
|
+
.option("--format <format>", "Output format (json, text)")
|
|
124
|
+
.action(async (id, opts) => {
|
|
125
|
+
const client = new ApiClient();
|
|
126
|
+
const body = {};
|
|
127
|
+
if (opts.agentName)
|
|
128
|
+
body.agent_name = opts.agentName;
|
|
129
|
+
const task = await client.cancelTask(id, body);
|
|
130
|
+
const fmt = getFormat(opts.format);
|
|
131
|
+
output(task, fmt, (t) => `Cancelled task ${t.id}: ${t.title}`);
|
|
132
|
+
});
|
|
133
|
+
taskCmd
|
|
134
|
+
.command("review <id>")
|
|
135
|
+
.description("Move a task to In Review")
|
|
136
|
+
.option("--pr-url <url>", "Pull request URL")
|
|
137
|
+
.option("--agent-name <name>", "Agent identity")
|
|
138
|
+
.option("--format <format>", "Output format (json, text)")
|
|
139
|
+
.action(async (id, opts) => {
|
|
140
|
+
const client = new ApiClient();
|
|
141
|
+
const body = {};
|
|
142
|
+
if (opts.prUrl)
|
|
143
|
+
body.pr_url = opts.prUrl;
|
|
144
|
+
if (opts.agentName)
|
|
145
|
+
body.agent_name = opts.agentName;
|
|
146
|
+
const task = await client.reviewTask(id, body);
|
|
147
|
+
const fmt = getFormat(opts.format);
|
|
148
|
+
output(task, fmt, (t) => `Moved task ${t.id} to review: ${t.title}`);
|
|
149
|
+
});
|
|
150
|
+
taskCmd
|
|
151
|
+
.command("complete <id>")
|
|
152
|
+
.description("Complete a task")
|
|
153
|
+
.option("--result <result>", "Completion result summary")
|
|
154
|
+
.option("--pr-url <url>", "PR URL")
|
|
155
|
+
.option("--agent-name <name>", "Agent identity")
|
|
156
|
+
.option("--format <format>", "Output format (json, text)")
|
|
157
|
+
.action(async (id, opts) => {
|
|
158
|
+
const client = new ApiClient();
|
|
159
|
+
const body = {};
|
|
160
|
+
if (opts.result)
|
|
161
|
+
body.result = opts.result;
|
|
162
|
+
if (opts.prUrl)
|
|
163
|
+
body.pr_url = opts.prUrl;
|
|
164
|
+
if (opts.agentName)
|
|
165
|
+
body.agent_id = opts.agentName;
|
|
166
|
+
const task = await client.completeTask(id, body);
|
|
167
|
+
const fmt = getFormat(opts.format);
|
|
168
|
+
output(task, fmt, (t) => `Completed task ${t.id}: ${t.title}`);
|
|
169
|
+
});
|
|
170
|
+
// ─── Agent ───
|
|
171
|
+
const agentCmd = program.command("agent").description("Manage agents");
|
|
172
|
+
agentCmd
|
|
173
|
+
.command("list")
|
|
174
|
+
.description("List all agents")
|
|
175
|
+
.option("--format <format>", "Output format (json, text)")
|
|
176
|
+
.action(async (opts) => {
|
|
177
|
+
const client = new ApiClient();
|
|
178
|
+
const agents = await client.listAgents();
|
|
179
|
+
const fmt = getFormat(opts.format);
|
|
180
|
+
output(agents, fmt, formatAgentList);
|
|
181
|
+
});
|
|
182
|
+
// ─── Board ───
|
|
183
|
+
const boardCmd = program.command("board").description("Manage boards");
|
|
184
|
+
boardCmd
|
|
185
|
+
.command("create")
|
|
186
|
+
.description("Create a new board")
|
|
187
|
+
.requiredOption("--name <name>", "Board name")
|
|
188
|
+
.option("--description <desc>", "Board description")
|
|
189
|
+
.option("--format <format>", "Output format (json, text)")
|
|
190
|
+
.action(async (opts) => {
|
|
191
|
+
const client = new ApiClient();
|
|
192
|
+
const board = await client.createBoard({ name: opts.name, description: opts.description });
|
|
193
|
+
const fmt = getFormat(opts.format);
|
|
194
|
+
output(board, fmt, (b) => `Created board ${b.id}: ${b.name}`);
|
|
195
|
+
});
|
|
196
|
+
boardCmd
|
|
197
|
+
.command("list")
|
|
198
|
+
.description("List all boards")
|
|
199
|
+
.option("--format <format>", "Output format (json, text)")
|
|
200
|
+
.action(async (opts) => {
|
|
201
|
+
const client = new ApiClient();
|
|
202
|
+
const boards = await client.listBoards();
|
|
203
|
+
const fmt = getFormat(opts.format);
|
|
204
|
+
output(boards, fmt, formatBoardList);
|
|
205
|
+
});
|
|
206
|
+
boardCmd
|
|
207
|
+
.command("view")
|
|
208
|
+
.description("View a kanban board")
|
|
209
|
+
.option("--board <name-or-id>", "Board name or ID (uses first if omitted)")
|
|
210
|
+
.option("--format <format>", "Output format (json, text)")
|
|
211
|
+
.action(async (opts) => {
|
|
212
|
+
const client = new ApiClient();
|
|
213
|
+
let boardId;
|
|
214
|
+
if (opts.board) {
|
|
215
|
+
boardId = await resolveBoardId(client, opts.board);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
const boards = await client.listBoards();
|
|
219
|
+
if (boards.length === 0) {
|
|
220
|
+
console.error("No boards. Create one first: agent-kanban board create --name 'My Board'");
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
boardId = boards[0].id;
|
|
224
|
+
}
|
|
225
|
+
const board = await client.getBoard(boardId);
|
|
226
|
+
const fmt = getFormat(opts.format);
|
|
227
|
+
output(board, fmt, formatBoard);
|
|
228
|
+
});
|
|
229
|
+
async function resolveBoardId(client, nameOrId) {
|
|
230
|
+
const boards = await client.listBoards();
|
|
231
|
+
const match = boards.find((b) => b.id === nameOrId || b.name === nameOrId);
|
|
232
|
+
if (!match) {
|
|
233
|
+
console.error(`Board not found: ${nameOrId}`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
return match.id;
|
|
237
|
+
}
|
|
238
|
+
// ─── Repo ───
|
|
239
|
+
const repoCmd = program.command("repo").description("Manage repositories");
|
|
240
|
+
repoCmd
|
|
241
|
+
.command("add")
|
|
242
|
+
.description("Add a repository")
|
|
243
|
+
.requiredOption("--name <name>", "Repository name")
|
|
244
|
+
.requiredOption("--url <url>", "Clone URL")
|
|
245
|
+
.option("--format <format>", "Output format (json, text)")
|
|
246
|
+
.action(async (opts) => {
|
|
247
|
+
const client = new ApiClient();
|
|
248
|
+
const repo = await client.createRepository({ name: opts.name, url: opts.url });
|
|
249
|
+
const fmt = getFormat(opts.format);
|
|
250
|
+
output(repo, fmt, (r) => `Added repository ${r.id}: ${r.name}`);
|
|
251
|
+
});
|
|
252
|
+
repoCmd
|
|
253
|
+
.command("list")
|
|
254
|
+
.description("List repositories")
|
|
255
|
+
.option("--format <format>", "Output format (json, text)")
|
|
256
|
+
.action(async (opts) => {
|
|
257
|
+
const client = new ApiClient();
|
|
258
|
+
const repos = await client.listRepositories();
|
|
259
|
+
const fmt = getFormat(opts.format);
|
|
260
|
+
output(repos, fmt, formatRepositoryList);
|
|
261
|
+
});
|
|
262
|
+
// ─── Link & Start ───
|
|
263
|
+
registerLinkCommand(program);
|
|
264
|
+
registerStartCommand(program);
|
|
265
|
+
program.parseAsync();
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface Links {
|
|
2
|
+
[repositoryId: string]: string;
|
|
3
|
+
}
|
|
4
|
+
export declare function setLink(repositoryId: string, localPath: string): void;
|
|
5
|
+
export declare function getLinks(): Links;
|
|
6
|
+
export declare function findPathForRepository(repositoryId: string): string | undefined;
|
|
7
|
+
export {};
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".agent-kanban");
|
|
5
|
+
const LINKS_FILE = join(CONFIG_DIR, "links.json");
|
|
6
|
+
function readLinks() {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(readFileSync(LINKS_FILE, "utf-8"));
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function writeLinks(links) {
|
|
15
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
16
|
+
writeFileSync(LINKS_FILE, JSON.stringify(links, null, 2) + "\n");
|
|
17
|
+
}
|
|
18
|
+
export function setLink(repositoryId, localPath) {
|
|
19
|
+
const links = readLinks();
|
|
20
|
+
links[repositoryId] = localPath;
|
|
21
|
+
writeLinks(links);
|
|
22
|
+
}
|
|
23
|
+
export function getLinks() {
|
|
24
|
+
return readLinks();
|
|
25
|
+
}
|
|
26
|
+
export function findPathForRepository(repositoryId) {
|
|
27
|
+
return readLinks()[repositoryId];
|
|
28
|
+
}
|
package/dist/output.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function getFormat(explicit?: string): "json" | "text";
|
|
2
|
+
export declare function output(data: unknown, format: "json" | "text", textFormatter?: (data: any) => string): void;
|
|
3
|
+
export declare function formatTaskList(tasks: any[]): string;
|
|
4
|
+
export declare function formatAgentList(agents: any[]): string;
|
|
5
|
+
export declare function formatBoardList(boards: any[]): string;
|
|
6
|
+
export declare function formatRepositoryList(repos: any[]): string;
|
|
7
|
+
export declare function formatBoard(board: any): string;
|
package/dist/output.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const isTTY = process.stdout.isTTY;
|
|
2
|
+
export function getFormat(explicit) {
|
|
3
|
+
if (explicit === "json")
|
|
4
|
+
return "json";
|
|
5
|
+
if (explicit === "text")
|
|
6
|
+
return "text";
|
|
7
|
+
return isTTY ? "text" : "json";
|
|
8
|
+
}
|
|
9
|
+
export function output(data, format, textFormatter) {
|
|
10
|
+
if (format === "json") {
|
|
11
|
+
console.log(JSON.stringify(data, null, 2));
|
|
12
|
+
}
|
|
13
|
+
else if (textFormatter) {
|
|
14
|
+
console.log(textFormatter(data));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.log(JSON.stringify(data, null, 2));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function formatTaskList(tasks) {
|
|
21
|
+
if (tasks.length === 0)
|
|
22
|
+
return "No tasks found.";
|
|
23
|
+
const lines = tasks.map((t) => {
|
|
24
|
+
const priority = t.priority ? `[${t.priority}]` : "";
|
|
25
|
+
const repo = t.repository_name ? `(${t.repository_name})` : "";
|
|
26
|
+
const agent = t.assigned_to ? `→ ${t.assigned_to}` : "";
|
|
27
|
+
return ` ${t.id} ${priority.padEnd(8)} ${t.title} ${repo} ${agent}`;
|
|
28
|
+
});
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
31
|
+
export function formatAgentList(agents) {
|
|
32
|
+
if (agents.length === 0)
|
|
33
|
+
return "No agents found.";
|
|
34
|
+
const lines = agents.map((a) => {
|
|
35
|
+
const status = `[${a.status}]`.padEnd(10);
|
|
36
|
+
const tasks = `${a.task_count} tasks`;
|
|
37
|
+
const lastActive = a.last_active_at ? `last: ${a.last_active_at}` : "never active";
|
|
38
|
+
return ` ${a.id} ${status} ${a.name} — ${tasks}, ${lastActive}`;
|
|
39
|
+
});
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
export function formatBoardList(boards) {
|
|
43
|
+
if (boards.length === 0)
|
|
44
|
+
return "No boards found.";
|
|
45
|
+
const lines = boards.map((b) => {
|
|
46
|
+
const desc = b.description ? ` — ${b.description}` : "";
|
|
47
|
+
return ` ${b.id} ${b.name}${desc}`;
|
|
48
|
+
});
|
|
49
|
+
return lines.join("\n");
|
|
50
|
+
}
|
|
51
|
+
export function formatRepositoryList(repos) {
|
|
52
|
+
if (repos.length === 0)
|
|
53
|
+
return "No repositories found.";
|
|
54
|
+
const lines = repos.map((r) => {
|
|
55
|
+
return ` ${r.id} ${r.name} ${r.url}`;
|
|
56
|
+
});
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
59
|
+
export function formatBoard(board) {
|
|
60
|
+
const cols = board.columns || [];
|
|
61
|
+
const maxWidth = 30;
|
|
62
|
+
const header = cols.map((c) => `│ ${(c.name + ` (${c.tasks.length})`).padEnd(maxWidth)} `).join("") + "│";
|
|
63
|
+
const sep = cols.map(() => "├" + "─".repeat(maxWidth + 2)).join("") + "┤";
|
|
64
|
+
const topSep = cols.map(() => "┌" + "─".repeat(maxWidth + 2)).join("") + "┐";
|
|
65
|
+
const botSep = cols.map(() => "└" + "─".repeat(maxWidth + 2)).join("") + "┘";
|
|
66
|
+
const maxRows = Math.max(...cols.map((c) => c.tasks.length), 0);
|
|
67
|
+
const rows = [];
|
|
68
|
+
for (let i = 0; i < maxRows; i++) {
|
|
69
|
+
const row = cols.map((c) => {
|
|
70
|
+
const task = c.tasks[i];
|
|
71
|
+
if (!task)
|
|
72
|
+
return `│ ${"".padEnd(maxWidth)} `;
|
|
73
|
+
const title = task.title.length > maxWidth - 2
|
|
74
|
+
? task.title.slice(0, maxWidth - 5) + "..."
|
|
75
|
+
: task.title;
|
|
76
|
+
return `│ ${title.padEnd(maxWidth)} `;
|
|
77
|
+
}).join("") + "│";
|
|
78
|
+
rows.push(row);
|
|
79
|
+
}
|
|
80
|
+
return [
|
|
81
|
+
`Board: ${board.name}`,
|
|
82
|
+
topSep,
|
|
83
|
+
header,
|
|
84
|
+
sep,
|
|
85
|
+
...rows,
|
|
86
|
+
botSep,
|
|
87
|
+
].join("\n");
|
|
88
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ChildProcess } from "child_process";
|
|
2
|
+
import type { ApiClient, AgentClient } from "./client.js";
|
|
3
|
+
export interface AgentProcess {
|
|
4
|
+
taskId: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
process: ChildProcess;
|
|
7
|
+
agentClient?: AgentClient;
|
|
8
|
+
timeoutTimer?: ReturnType<typeof setTimeout>;
|
|
9
|
+
}
|
|
10
|
+
export declare class ProcessManager {
|
|
11
|
+
private agents;
|
|
12
|
+
private client;
|
|
13
|
+
private agentCli;
|
|
14
|
+
private onSlotFreed;
|
|
15
|
+
private taskTimeoutMs;
|
|
16
|
+
constructor(client: ApiClient, agentCli: string, onSlotFreed: () => void, taskTimeoutMs?: number);
|
|
17
|
+
get activeCount(): number;
|
|
18
|
+
hasTask(taskId: string): boolean;
|
|
19
|
+
spawnAgent(taskId: string, sessionId: string, cwd: string, taskContext: string, agentClient?: AgentClient): Promise<void>;
|
|
20
|
+
killAll(): Promise<void>;
|
|
21
|
+
private handleEvent;
|
|
22
|
+
private postMessage;
|
|
23
|
+
private releaseTask;
|
|
24
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
export class ProcessManager {
|
|
3
|
+
agents = new Map();
|
|
4
|
+
client;
|
|
5
|
+
agentCli;
|
|
6
|
+
onSlotFreed;
|
|
7
|
+
taskTimeoutMs;
|
|
8
|
+
constructor(client, agentCli, onSlotFreed, taskTimeoutMs = 2 * 60 * 60 * 1000) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
this.agentCli = agentCli;
|
|
11
|
+
this.onSlotFreed = onSlotFreed;
|
|
12
|
+
this.taskTimeoutMs = taskTimeoutMs;
|
|
13
|
+
}
|
|
14
|
+
get activeCount() {
|
|
15
|
+
return this.agents.size;
|
|
16
|
+
}
|
|
17
|
+
hasTask(taskId) {
|
|
18
|
+
return this.agents.has(taskId);
|
|
19
|
+
}
|
|
20
|
+
async spawnAgent(taskId, sessionId, cwd, taskContext, agentClient) {
|
|
21
|
+
const args = [
|
|
22
|
+
"--print",
|
|
23
|
+
"--verbose",
|
|
24
|
+
"--input-format", "stream-json",
|
|
25
|
+
"--output-format", "stream-json",
|
|
26
|
+
"--dangerously-skip-permissions",
|
|
27
|
+
"--session-id", sessionId,
|
|
28
|
+
"-w",
|
|
29
|
+
];
|
|
30
|
+
let proc;
|
|
31
|
+
try {
|
|
32
|
+
proc = spawn(this.agentCli, args, {
|
|
33
|
+
cwd,
|
|
34
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
35
|
+
env: { ...process.env },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
console.error(`[ERROR] Failed to spawn ${this.agentCli}: ${err.message}`);
|
|
40
|
+
await this.releaseTask(taskId);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!proc.pid) {
|
|
44
|
+
console.error(`[ERROR] ${this.agentCli} not found or failed to start`);
|
|
45
|
+
await this.releaseTask(taskId);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const agent = { taskId, sessionId, process: proc, agentClient };
|
|
49
|
+
this.agents.set(taskId, agent);
|
|
50
|
+
// Kill agent if it exceeds the task timeout
|
|
51
|
+
if (this.taskTimeoutMs > 0) {
|
|
52
|
+
agent.timeoutTimer = setTimeout(() => {
|
|
53
|
+
console.warn(`[WARN] Agent for task ${taskId} exceeded timeout (${Math.round(this.taskTimeoutMs / 60000)}m), killing`);
|
|
54
|
+
proc.kill("SIGTERM");
|
|
55
|
+
}, this.taskTimeoutMs);
|
|
56
|
+
}
|
|
57
|
+
// Send task context as JSON message via stdin, then close
|
|
58
|
+
proc.on("spawn", () => {
|
|
59
|
+
const payload = JSON.stringify({
|
|
60
|
+
type: "user",
|
|
61
|
+
message: { role: "user", content: taskContext },
|
|
62
|
+
});
|
|
63
|
+
proc.stdin?.write(payload + "\n");
|
|
64
|
+
proc.stdin?.end();
|
|
65
|
+
});
|
|
66
|
+
// Parse stdout (stream-json): each line is a JSON event
|
|
67
|
+
let stdoutBuffer = "";
|
|
68
|
+
proc.stdout?.on("data", (chunk) => {
|
|
69
|
+
stdoutBuffer += chunk.toString();
|
|
70
|
+
const lines = stdoutBuffer.split("\n");
|
|
71
|
+
stdoutBuffer = lines.pop() || "";
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
if (!line.trim())
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const event = JSON.parse(line);
|
|
77
|
+
this.handleEvent(taskId, sessionId, event, agentClient);
|
|
78
|
+
}
|
|
79
|
+
catch { /* non-JSON line, skip */ }
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
// Capture stderr for crash diagnostics
|
|
83
|
+
let stderrBuffer = "";
|
|
84
|
+
proc.stderr?.on("data", (chunk) => {
|
|
85
|
+
stderrBuffer += chunk.toString();
|
|
86
|
+
if (stderrBuffer.length > 10000)
|
|
87
|
+
stderrBuffer = stderrBuffer.slice(-5000);
|
|
88
|
+
});
|
|
89
|
+
// Handle exit
|
|
90
|
+
proc.on("close", async (code) => {
|
|
91
|
+
// Clear timeout timer
|
|
92
|
+
if (agent.timeoutTimer)
|
|
93
|
+
clearTimeout(agent.timeoutTimer);
|
|
94
|
+
// Flush remaining stdout
|
|
95
|
+
if (stdoutBuffer.trim()) {
|
|
96
|
+
try {
|
|
97
|
+
const event = JSON.parse(stdoutBuffer);
|
|
98
|
+
this.handleEvent(taskId, sessionId, event, agentClient);
|
|
99
|
+
}
|
|
100
|
+
catch { /* skip */ }
|
|
101
|
+
}
|
|
102
|
+
this.agents.delete(taskId);
|
|
103
|
+
if (code === 0) {
|
|
104
|
+
console.log(`[INFO] Agent finished task ${taskId}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.warn(`[WARN] Agent crashed on task ${taskId} (exit ${code})`);
|
|
108
|
+
if (stderrBuffer.trim()) {
|
|
109
|
+
const lastLines = stderrBuffer.trim().split("\n").slice(-5).join("\n");
|
|
110
|
+
console.warn(` stderr: ${lastLines}`);
|
|
111
|
+
}
|
|
112
|
+
await this.releaseTask(taskId);
|
|
113
|
+
}
|
|
114
|
+
this.onSlotFreed();
|
|
115
|
+
});
|
|
116
|
+
proc.on("error", async (err) => {
|
|
117
|
+
console.error(`[ERROR] Agent process error for task ${taskId}: ${err.message}`);
|
|
118
|
+
if (agent.timeoutTimer)
|
|
119
|
+
clearTimeout(agent.timeoutTimer);
|
|
120
|
+
this.agents.delete(taskId);
|
|
121
|
+
await this.releaseTask(taskId);
|
|
122
|
+
this.onSlotFreed();
|
|
123
|
+
});
|
|
124
|
+
console.log(`[INFO] Spawned ${this.agentCli} (session=${sessionId}) for task ${taskId} in ${cwd}`);
|
|
125
|
+
}
|
|
126
|
+
async killAll() {
|
|
127
|
+
const entries = [...this.agents.entries()];
|
|
128
|
+
for (const [taskId, agent] of entries) {
|
|
129
|
+
console.log(`[INFO] Killing agent for task ${taskId}`);
|
|
130
|
+
if (agent.timeoutTimer)
|
|
131
|
+
clearTimeout(agent.timeoutTimer);
|
|
132
|
+
agent.process.kill("SIGTERM");
|
|
133
|
+
this.agents.delete(taskId);
|
|
134
|
+
await this.releaseTask(taskId);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
handleEvent(taskId, sessionId, event, agentClient) {
|
|
138
|
+
// Extract text from assistant messages → post as agent chat messages
|
|
139
|
+
if (event.type === "assistant" && Array.isArray(event.message?.content)) {
|
|
140
|
+
for (const block of event.message.content) {
|
|
141
|
+
if (block.type === "text" && block.text) {
|
|
142
|
+
this.postMessage(taskId, sessionId, "agent", block.text, agentClient).catch(() => { });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Report usage on result event
|
|
147
|
+
if (event.type === "result") {
|
|
148
|
+
const cost = event.total_cost_usd || 0;
|
|
149
|
+
const usage = event.usage || {};
|
|
150
|
+
console.log(`[INFO] Agent result for task ${taskId}: cost=$${cost.toFixed(4)}`);
|
|
151
|
+
const usageData = {
|
|
152
|
+
input_tokens: usage.input_tokens || 0,
|
|
153
|
+
output_tokens: usage.output_tokens || 0,
|
|
154
|
+
cache_read_tokens: usage.cache_read_input_tokens || 0,
|
|
155
|
+
cache_creation_tokens: usage.cache_creation_input_tokens || 0,
|
|
156
|
+
cost_micro_usd: Math.round(cost * 1_000_000),
|
|
157
|
+
};
|
|
158
|
+
if (agentClient) {
|
|
159
|
+
agentClient.updateAgentUsage(usageData).catch(() => { });
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
this.client.updateAgentUsage(sessionId, usageData).catch(() => { });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async postMessage(taskId, sessionId, role, content, agentClient) {
|
|
167
|
+
const body = { agent_id: sessionId, role, content };
|
|
168
|
+
if (agentClient) {
|
|
169
|
+
await agentClient.sendMessage(taskId, body);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
await this.client.sendMessage(taskId, body);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async releaseTask(taskId, retries = 3) {
|
|
176
|
+
for (let i = 0; i < retries; i++) {
|
|
177
|
+
try {
|
|
178
|
+
await this.client.releaseTask(taskId);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
console.error(`[WARN] Failed to release task ${taskId} (attempt ${i + 1}/${retries}): ${err.message}`);
|
|
183
|
+
if (i < retries - 1)
|
|
184
|
+
await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
console.error(`[ERROR] Could not release task ${taskId} after ${retries} attempts. Task will remain locked until stale detection.`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ApiClient } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Detect project name from context.
|
|
4
|
+
* Precedence: explicit flag > .agent-kanban.json > git repo basename > undefined
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectProjectName(explicit?: string): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve project name to project_id via API.
|
|
9
|
+
* Returns id if found, undefined if no project name detected.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectProjectId(client: ApiClient, explicit?: string): Promise<string | undefined>;
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { join, basename, dirname } from "path";
|
|
4
|
+
/**
|
|
5
|
+
* Detect project name from context.
|
|
6
|
+
* Precedence: explicit flag > .agent-kanban.json > git repo basename > undefined
|
|
7
|
+
*/
|
|
8
|
+
export function detectProjectName(explicit) {
|
|
9
|
+
if (explicit)
|
|
10
|
+
return explicit;
|
|
11
|
+
// Walk up from cwd looking for .agent-kanban.json
|
|
12
|
+
let dir = process.cwd();
|
|
13
|
+
while (dir !== dirname(dir)) {
|
|
14
|
+
const configPath = join(dir, ".agent-kanban.json");
|
|
15
|
+
if (existsSync(configPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
18
|
+
if (config.project)
|
|
19
|
+
return config.project;
|
|
20
|
+
}
|
|
21
|
+
catch { /* ignore malformed */ }
|
|
22
|
+
}
|
|
23
|
+
dir = dirname(dir);
|
|
24
|
+
}
|
|
25
|
+
// Try git repo basename
|
|
26
|
+
try {
|
|
27
|
+
const root = execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
28
|
+
return basename(root);
|
|
29
|
+
}
|
|
30
|
+
catch { /* not a git repo */ }
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve project name to project_id via API.
|
|
35
|
+
* Returns id if found, undefined if no project name detected.
|
|
36
|
+
*/
|
|
37
|
+
export async function detectProjectId(client, explicit) {
|
|
38
|
+
const name = detectProjectName(explicit);
|
|
39
|
+
if (!name)
|
|
40
|
+
return undefined;
|
|
41
|
+
const projects = await client.listProjects();
|
|
42
|
+
const match = projects.find((p) => p.name === name);
|
|
43
|
+
return match?.id;
|
|
44
|
+
}
|