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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
2
|
+
import type { ApiClient, AgentClient } from "./client.js";
|
|
3
|
+
|
|
4
|
+
// Agent Process Lifecycle:
|
|
5
|
+
// SPAWN → stdin task notification → RUNNING
|
|
6
|
+
// agent claims task via CLI, works, completes via CLI
|
|
7
|
+
// stdout (stream-json) → parse events → POST /messages
|
|
8
|
+
// EXIT(0) → log success → cleanup
|
|
9
|
+
// EXIT(N) → POST /release (crash recovery) → cleanup
|
|
10
|
+
// KILL (shutdown) → POST /release → cleanup
|
|
11
|
+
|
|
12
|
+
export interface AgentProcess {
|
|
13
|
+
taskId: string;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
process: ChildProcess;
|
|
16
|
+
agentClient?: AgentClient;
|
|
17
|
+
timeoutTimer?: ReturnType<typeof setTimeout>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ProcessManager {
|
|
21
|
+
private agents = new Map<string, AgentProcess>();
|
|
22
|
+
private client: ApiClient;
|
|
23
|
+
private agentCli: string;
|
|
24
|
+
private onSlotFreed: () => void;
|
|
25
|
+
private taskTimeoutMs: number;
|
|
26
|
+
|
|
27
|
+
constructor(client: ApiClient, agentCli: string, onSlotFreed: () => void, taskTimeoutMs = 2 * 60 * 60 * 1000) {
|
|
28
|
+
this.client = client;
|
|
29
|
+
this.agentCli = agentCli;
|
|
30
|
+
this.onSlotFreed = onSlotFreed;
|
|
31
|
+
this.taskTimeoutMs = taskTimeoutMs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get activeCount(): number {
|
|
35
|
+
return this.agents.size;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
hasTask(taskId: string): boolean {
|
|
39
|
+
return this.agents.has(taskId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async spawnAgent(
|
|
43
|
+
taskId: string,
|
|
44
|
+
sessionId: string,
|
|
45
|
+
cwd: string,
|
|
46
|
+
taskContext: string,
|
|
47
|
+
agentClient?: AgentClient,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const args = [
|
|
50
|
+
"--print",
|
|
51
|
+
"--verbose",
|
|
52
|
+
"--input-format", "stream-json",
|
|
53
|
+
"--output-format", "stream-json",
|
|
54
|
+
"--dangerously-skip-permissions",
|
|
55
|
+
"--session-id", sessionId,
|
|
56
|
+
"-w",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
let proc: ChildProcess;
|
|
60
|
+
try {
|
|
61
|
+
proc = spawn(this.agentCli, args, {
|
|
62
|
+
cwd,
|
|
63
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
64
|
+
env: { ...process.env },
|
|
65
|
+
});
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
console.error(`[ERROR] Failed to spawn ${this.agentCli}: ${err.message}`);
|
|
68
|
+
await this.releaseTask(taskId);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!proc.pid) {
|
|
73
|
+
console.error(`[ERROR] ${this.agentCli} not found or failed to start`);
|
|
74
|
+
await this.releaseTask(taskId);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const agent: AgentProcess = { taskId, sessionId, process: proc, agentClient };
|
|
79
|
+
this.agents.set(taskId, agent);
|
|
80
|
+
|
|
81
|
+
// Kill agent if it exceeds the task timeout
|
|
82
|
+
if (this.taskTimeoutMs > 0) {
|
|
83
|
+
agent.timeoutTimer = setTimeout(() => {
|
|
84
|
+
console.warn(`[WARN] Agent for task ${taskId} exceeded timeout (${Math.round(this.taskTimeoutMs / 60000)}m), killing`);
|
|
85
|
+
proc.kill("SIGTERM");
|
|
86
|
+
}, this.taskTimeoutMs);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Send task context as JSON message via stdin, then close
|
|
90
|
+
proc.on("spawn", () => {
|
|
91
|
+
const payload = JSON.stringify({
|
|
92
|
+
type: "user",
|
|
93
|
+
message: { role: "user", content: taskContext },
|
|
94
|
+
});
|
|
95
|
+
proc.stdin?.write(payload + "\n");
|
|
96
|
+
proc.stdin?.end();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Parse stdout (stream-json): each line is a JSON event
|
|
100
|
+
let stdoutBuffer = "";
|
|
101
|
+
proc.stdout?.on("data", (chunk: Buffer) => {
|
|
102
|
+
stdoutBuffer += chunk.toString();
|
|
103
|
+
const lines = stdoutBuffer.split("\n");
|
|
104
|
+
stdoutBuffer = lines.pop() || "";
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (!line.trim()) continue;
|
|
107
|
+
try {
|
|
108
|
+
const event = JSON.parse(line);
|
|
109
|
+
this.handleEvent(taskId, sessionId, event, agentClient);
|
|
110
|
+
} catch { /* non-JSON line, skip */ }
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Capture stderr for crash diagnostics
|
|
115
|
+
let stderrBuffer = "";
|
|
116
|
+
proc.stderr?.on("data", (chunk: Buffer) => {
|
|
117
|
+
stderrBuffer += chunk.toString();
|
|
118
|
+
if (stderrBuffer.length > 10000) stderrBuffer = stderrBuffer.slice(-5000);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Handle exit
|
|
122
|
+
proc.on("close", async (code) => {
|
|
123
|
+
// Clear timeout timer
|
|
124
|
+
if (agent.timeoutTimer) clearTimeout(agent.timeoutTimer);
|
|
125
|
+
|
|
126
|
+
// Flush remaining stdout
|
|
127
|
+
if (stdoutBuffer.trim()) {
|
|
128
|
+
try {
|
|
129
|
+
const event = JSON.parse(stdoutBuffer);
|
|
130
|
+
this.handleEvent(taskId, sessionId, event, agentClient);
|
|
131
|
+
} catch { /* skip */ }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.agents.delete(taskId);
|
|
135
|
+
|
|
136
|
+
if (code === 0) {
|
|
137
|
+
console.log(`[INFO] Agent finished task ${taskId}`);
|
|
138
|
+
} else {
|
|
139
|
+
console.warn(`[WARN] Agent crashed on task ${taskId} (exit ${code})`);
|
|
140
|
+
if (stderrBuffer.trim()) {
|
|
141
|
+
const lastLines = stderrBuffer.trim().split("\n").slice(-5).join("\n");
|
|
142
|
+
console.warn(` stderr: ${lastLines}`);
|
|
143
|
+
}
|
|
144
|
+
await this.releaseTask(taskId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.onSlotFreed();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
proc.on("error", async (err) => {
|
|
151
|
+
console.error(`[ERROR] Agent process error for task ${taskId}: ${err.message}`);
|
|
152
|
+
if (agent.timeoutTimer) clearTimeout(agent.timeoutTimer);
|
|
153
|
+
this.agents.delete(taskId);
|
|
154
|
+
await this.releaseTask(taskId);
|
|
155
|
+
this.onSlotFreed();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
console.log(`[INFO] Spawned ${this.agentCli} (session=${sessionId}) for task ${taskId} in ${cwd}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async killAll(): Promise<void> {
|
|
162
|
+
const entries = [...this.agents.entries()];
|
|
163
|
+
for (const [taskId, agent] of entries) {
|
|
164
|
+
console.log(`[INFO] Killing agent for task ${taskId}`);
|
|
165
|
+
if (agent.timeoutTimer) clearTimeout(agent.timeoutTimer);
|
|
166
|
+
agent.process.kill("SIGTERM");
|
|
167
|
+
this.agents.delete(taskId);
|
|
168
|
+
await this.releaseTask(taskId);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private handleEvent(taskId: string, sessionId: string, event: any, agentClient?: AgentClient): void {
|
|
173
|
+
// Extract text from assistant messages → post as agent chat messages
|
|
174
|
+
if (event.type === "assistant" && Array.isArray(event.message?.content)) {
|
|
175
|
+
for (const block of event.message.content) {
|
|
176
|
+
if (block.type === "text" && block.text) {
|
|
177
|
+
this.postMessage(taskId, sessionId, "agent", block.text, agentClient).catch(() => {});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Report usage on result event
|
|
182
|
+
if (event.type === "result") {
|
|
183
|
+
const cost = event.total_cost_usd || 0;
|
|
184
|
+
const usage = event.usage || {};
|
|
185
|
+
console.log(`[INFO] Agent result for task ${taskId}: cost=$${cost.toFixed(4)}`);
|
|
186
|
+
const usageData = {
|
|
187
|
+
input_tokens: usage.input_tokens || 0,
|
|
188
|
+
output_tokens: usage.output_tokens || 0,
|
|
189
|
+
cache_read_tokens: usage.cache_read_input_tokens || 0,
|
|
190
|
+
cache_creation_tokens: usage.cache_creation_input_tokens || 0,
|
|
191
|
+
cost_micro_usd: Math.round(cost * 1_000_000),
|
|
192
|
+
};
|
|
193
|
+
if (agentClient) {
|
|
194
|
+
agentClient.updateAgentUsage(usageData).catch(() => {});
|
|
195
|
+
} else {
|
|
196
|
+
this.client.updateAgentUsage(sessionId, usageData).catch(() => {});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async postMessage(taskId: string, sessionId: string, role: string, content: string, agentClient?: AgentClient): Promise<void> {
|
|
202
|
+
const body = { agent_id: sessionId, role, content };
|
|
203
|
+
if (agentClient) {
|
|
204
|
+
await agentClient.sendMessage(taskId, body);
|
|
205
|
+
} else {
|
|
206
|
+
await this.client.sendMessage(taskId, body);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async releaseTask(taskId: string, retries = 3): Promise<void> {
|
|
211
|
+
for (let i = 0; i < retries; i++) {
|
|
212
|
+
try {
|
|
213
|
+
await this.client.releaseTask(taskId);
|
|
214
|
+
return;
|
|
215
|
+
} catch (err: any) {
|
|
216
|
+
console.error(`[WARN] Failed to release task ${taskId} (attempt ${i + 1}/${retries}): ${err.message}`);
|
|
217
|
+
if (i < retries - 1) await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
console.error(`[ERROR] Could not release task ${taskId} after ${retries} attempts. Task will remain locked until stale detection.`);
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface UsageWindow {
|
|
2
|
+
utilization: number;
|
|
3
|
+
resets_at: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface UsageInfo {
|
|
7
|
+
five_hour?: UsageWindow;
|
|
8
|
+
seven_day?: UsageWindow;
|
|
9
|
+
seven_day_sonnet?: UsageWindow;
|
|
10
|
+
seven_day_opus?: UsageWindow;
|
|
11
|
+
updated_at: string;
|
|
12
|
+
}
|
package/src/usage.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir, platform } from "os";
|
|
5
|
+
import type { UsageInfo } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const CREDENTIALS_PATH = join(homedir(), ".claude", ".credentials.json");
|
|
8
|
+
const USAGE_API = "https://api.anthropic.com/api/oauth/usage";
|
|
9
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
let cachedUsage: UsageInfo | null = null;
|
|
12
|
+
let cachedAt = 0;
|
|
13
|
+
let cachedToken: string | null = null;
|
|
14
|
+
|
|
15
|
+
function parseToken(raw: string): string | null {
|
|
16
|
+
const creds = JSON.parse(raw);
|
|
17
|
+
return creds.claudeAiOauth?.accessToken || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readOAuthToken(): string | null {
|
|
21
|
+
if (cachedToken) return cachedToken;
|
|
22
|
+
try {
|
|
23
|
+
if (platform() === "darwin") {
|
|
24
|
+
const raw = execSync('security find-generic-password -s "Claude Code-credentials" -w', { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
25
|
+
cachedToken = parseToken(raw);
|
|
26
|
+
} else {
|
|
27
|
+
cachedToken = parseToken(readFileSync(CREDENTIALS_PATH, "utf-8"));
|
|
28
|
+
}
|
|
29
|
+
return cachedToken;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getUsage(): Promise<UsageInfo | null> {
|
|
36
|
+
if (cachedUsage && Date.now() - cachedAt < CACHE_TTL_MS) {
|
|
37
|
+
return cachedUsage;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const token = readOAuthToken();
|
|
41
|
+
if (!token) return cachedUsage;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch(USAGE_API, {
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: `Bearer ${token}`,
|
|
47
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
48
|
+
},
|
|
49
|
+
signal: AbortSignal.timeout(5000),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
console.error(`[WARN] Usage API returned ${res.status}`);
|
|
54
|
+
return cachedUsage;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const data = await res.json() as Record<string, { utilization: number; resets_at: string }>;
|
|
58
|
+
cachedUsage = {
|
|
59
|
+
...data.five_hour && { five_hour: data.five_hour },
|
|
60
|
+
...data.seven_day && { seven_day: data.seven_day },
|
|
61
|
+
...data.seven_day_sonnet && { seven_day_sonnet: data.seven_day_sonnet },
|
|
62
|
+
...data.seven_day_opus && { seven_day_opus: data.seven_day_opus },
|
|
63
|
+
updated_at: new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
cachedAt = Date.now();
|
|
66
|
+
return cachedUsage;
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
console.error(`[WARN] Failed to fetch usage: ${err.message}`);
|
|
69
|
+
return cachedUsage;
|
|
70
|
+
}
|
|
71
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|