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/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();
@@ -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
+ }
@@ -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>;
@@ -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
+ }