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.
@@ -0,0 +1,116 @@
1
+ # Agent Kanban — Task Management Skill
2
+
3
+ Use the `agent-kanban` CLI (alias `ak`) to manage tasks on your kanban board.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ npm install -g agent-kanban
9
+ ak config set api-url https://your-instance.pages.dev
10
+ ak config set api-key <your-api-key>
11
+ ```
12
+
13
+ ## Workflow
14
+
15
+ When the daemon assigns you a task, you receive the task ID. Follow this flow:
16
+
17
+ ### 1. View the task
18
+
19
+ ```bash
20
+ ak task list --format json
21
+ ```
22
+
23
+ Find your assigned task and read the details.
24
+
25
+ ### 2. Claim the task
26
+
27
+ ```bash
28
+ ak task claim <task-id> --agent-name <your-name>
29
+ ```
30
+
31
+ This confirms you are starting work and moves the task to "In Progress."
32
+
33
+ ### 3. Do the work
34
+
35
+ Read the task description, implement the changes, run tests, etc.
36
+
37
+ ### 4. Log progress
38
+
39
+ ```bash
40
+ ak task log <task-id> "Investigating the auth flow..."
41
+ ak task log <task-id> "Root cause: breaking change in v2.3"
42
+ ```
43
+
44
+ ### 5. Complete the task
45
+
46
+ ```bash
47
+ ak task complete <task-id> \
48
+ --result "Fixed JWT claim namespace" \
49
+ --pr-url "https://github.com/org/repo/pull/42" \
50
+ --agent-name <your-name>
51
+ ```
52
+
53
+ ## Task Lifecycle
54
+
55
+ ```
56
+ Todo ──assign(daemon)──→ Todo (assigned) ──claim(agent)──→ In Progress
57
+ → In Review (review) → Done (complete)
58
+ → Cancelled (cancel at any stage)
59
+ → Todo (release — on crash or timeout)
60
+ ```
61
+
62
+ - **assign**: Daemon locks the task to you. Status stays `todo`, but no other agent can take it.
63
+ - **claim**: You confirm you're starting. Status moves to `in_progress`.
64
+ - **complete**: You're done. Status moves to `done`.
65
+ - **review**: Move to `in_review` for human review before completing.
66
+
67
+ ## Creating Subtasks
68
+
69
+ When you discover follow-up work, create a task:
70
+
71
+ ```bash
72
+ ak task create \
73
+ --title "Fix shared-lib JWT claim namespace" \
74
+ --priority high \
75
+ --agent-name <your-name>
76
+ ```
77
+
78
+ Log the relationship:
79
+
80
+ ```bash
81
+ ak task log <original-task-id> "Created subtask for shared-lib fix"
82
+ ```
83
+
84
+ ## CLI Reference
85
+
86
+ | Command | Description |
87
+ |---------|-------------|
88
+ | `task create --title <t>` | Create a task (optional: --priority, --labels, --input) |
89
+ | `task list` | List tasks (optional: --status, --label, --format) |
90
+ | `task claim <id>` | Claim an assigned task — start working |
91
+ | `task log <id> <msg>` | Add a progress log entry |
92
+ | `task review <id>` | Move task to In Review |
93
+ | `task complete <id>` | Mark task done (optional: --result, --pr-url) |
94
+ | `task cancel <id>` | Cancel a task |
95
+ | `board list` | List all boards |
96
+ | `board view` | Show the kanban board |
97
+ | `config set <key> <val>` | Set api-url or api-key |
98
+
99
+ ## Smart Defaults
100
+
101
+ - Output is JSON when piped (not a TTY), text in interactive terminals
102
+ - Use `--format json` to force JSON output
103
+
104
+ ## Error Handling
105
+
106
+ - **401 Unauthorized**: Check your API key with `ak config get api-key`
107
+ - **409 Conflict**: Task is already claimed or not assigned to you
108
+ - **404 Not Found**: Task ID doesn't exist — check with `task list`
109
+
110
+ ## Best Practices
111
+
112
+ 1. Always claim before working — don't start without claiming
113
+ 2. Log progress frequently — humans monitor the board
114
+ 3. Create subtasks when you find dependency gaps
115
+ 4. Include PR URLs when completing — it closes the loop
116
+ 5. Use meaningful agent names for traceability
@@ -0,0 +1,11 @@
1
+ export interface UsageWindow {
2
+ utilization: number;
3
+ resets_at: string;
4
+ }
5
+ export interface UsageInfo {
6
+ five_hour?: UsageWindow;
7
+ seven_day?: UsageWindow;
8
+ seven_day_sonnet?: UsageWindow;
9
+ seven_day_opus?: UsageWindow;
10
+ updated_at: string;
11
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { UsageInfo } from "./types.js";
2
+ export declare function getUsage(): Promise<UsageInfo | null>;
package/dist/usage.js ADDED
@@ -0,0 +1,66 @@
1
+ import { execSync } from "child_process";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { homedir, platform } from "os";
5
+ const CREDENTIALS_PATH = join(homedir(), ".claude", ".credentials.json");
6
+ const USAGE_API = "https://api.anthropic.com/api/oauth/usage";
7
+ const CACHE_TTL_MS = 5 * 60 * 1000;
8
+ let cachedUsage = null;
9
+ let cachedAt = 0;
10
+ let cachedToken = null;
11
+ function parseToken(raw) {
12
+ const creds = JSON.parse(raw);
13
+ return creds.claudeAiOauth?.accessToken || null;
14
+ }
15
+ function readOAuthToken() {
16
+ if (cachedToken)
17
+ return cachedToken;
18
+ try {
19
+ if (platform() === "darwin") {
20
+ const raw = execSync('security find-generic-password -s "Claude Code-credentials" -w', { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
21
+ cachedToken = parseToken(raw);
22
+ }
23
+ else {
24
+ cachedToken = parseToken(readFileSync(CREDENTIALS_PATH, "utf-8"));
25
+ }
26
+ return cachedToken;
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ export async function getUsage() {
33
+ if (cachedUsage && Date.now() - cachedAt < CACHE_TTL_MS) {
34
+ return cachedUsage;
35
+ }
36
+ const token = readOAuthToken();
37
+ if (!token)
38
+ return cachedUsage;
39
+ try {
40
+ const res = await fetch(USAGE_API, {
41
+ headers: {
42
+ Authorization: `Bearer ${token}`,
43
+ "anthropic-beta": "oauth-2025-04-20",
44
+ },
45
+ signal: AbortSignal.timeout(5000),
46
+ });
47
+ if (!res.ok) {
48
+ console.error(`[WARN] Usage API returned ${res.status}`);
49
+ return cachedUsage;
50
+ }
51
+ const data = await res.json();
52
+ cachedUsage = {
53
+ ...data.five_hour && { five_hour: data.five_hour },
54
+ ...data.seven_day && { seven_day: data.seven_day },
55
+ ...data.seven_day_sonnet && { seven_day_sonnet: data.seven_day_sonnet },
56
+ ...data.seven_day_opus && { seven_day_opus: data.seven_day_opus },
57
+ updated_at: new Date().toISOString(),
58
+ };
59
+ cachedAt = Date.now();
60
+ return cachedUsage;
61
+ }
62
+ catch (err) {
63
+ console.error(`[WARN] Failed to fetch usage: ${err.message}`);
64
+ return cachedUsage;
65
+ }
66
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "agent-kanban",
3
+ "version": "1.0.0",
4
+ "description": "CLI for Agent Kanban — agent-first cross-project task board",
5
+ "type": "module",
6
+ "bin": {
7
+ "agent-kanban": "./dist/index.js",
8
+ "ak": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch"
13
+ },
14
+ "dependencies": {
15
+ "@auth/agent": "^0.3.0",
16
+ "commander": "^13.0.0",
17
+ "jose": "^6.2.2"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^25.5.0",
21
+ "typescript": "^5.7.0"
22
+ }
23
+ }
package/src/client.ts ADDED
@@ -0,0 +1,165 @@
1
+ import { SignJWT } from "jose";
2
+ import { randomUUID } from "crypto";
3
+ import type { UsageInfo } from "./types.js";
4
+ import { getConfigValue } from "./config.js";
5
+
6
+ export class ApiClient {
7
+ private baseUrl: string;
8
+ private apiKey: string;
9
+
10
+ constructor() {
11
+ const url = getConfigValue("api-url");
12
+ const key = getConfigValue("api-key");
13
+ if (!url) throw new Error("API URL not configured. Run: agent-kanban config set api-url <url>");
14
+ if (!key) throw new Error("API key not configured. Run: agent-kanban config set api-key <key>");
15
+ this.baseUrl = url.replace(/\/$/, "");
16
+ this.apiKey = key;
17
+ }
18
+
19
+ private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
20
+ const url = `${this.baseUrl}${path}`;
21
+ const res = await fetch(url, {
22
+ method,
23
+ headers: {
24
+ "Content-Type": "application/json",
25
+ Authorization: `Bearer ${this.apiKey}`,
26
+ },
27
+ body: body ? JSON.stringify(body) : undefined,
28
+ signal: AbortSignal.timeout(10000),
29
+ });
30
+
31
+ const data = await res.json() as T & { error?: { code: string; message: string } };
32
+
33
+ if (!res.ok) {
34
+ const msg = (data as any).error?.message || `HTTP ${res.status}`;
35
+ throw new Error(msg);
36
+ }
37
+
38
+ return data;
39
+ }
40
+
41
+ // Tasks
42
+ createTask(input: Record<string, unknown>) { return this.request("POST", "/api/tasks", input); }
43
+ listTasks(params?: Record<string, string>) {
44
+ const qs = params ? "?" + new URLSearchParams(params).toString() : "";
45
+ return this.request("GET", `/api/tasks${qs}`);
46
+ }
47
+ getTask(id: string) { return this.request("GET", `/api/tasks/${id}`); }
48
+ claimTask(id: string, agentName?: string) {
49
+ return this.request("POST", `/api/tasks/${id}/claim`, agentName ? { agent_id: agentName } : {});
50
+ }
51
+ completeTask(id: string, body: Record<string, unknown>) {
52
+ return this.request("POST", `/api/tasks/${id}/complete`, body);
53
+ }
54
+ releaseTask(id: string) {
55
+ return this.request("POST", `/api/tasks/${id}/release`);
56
+ }
57
+ cancelTask(id: string, body: Record<string, unknown> = {}) {
58
+ return this.request("POST", `/api/tasks/${id}/cancel`, body);
59
+ }
60
+ reviewTask(id: string, body: Record<string, unknown> = {}) {
61
+ return this.request("POST", `/api/tasks/${id}/review`, body);
62
+ }
63
+ assignTask(id: string, agentId: string) {
64
+ return this.request("POST", `/api/tasks/${id}/assign`, { agent_id: agentId });
65
+ }
66
+ addLog(taskId: string, detail: string, agentName?: string) {
67
+ return this.request("POST", `/api/tasks/${taskId}/logs`, { detail, agent_id: agentName });
68
+ }
69
+
70
+ // Machines
71
+ registerMachine(info: { name: string; os: string; version: string; runtimes: string[] }) {
72
+ return this.request<{ id: string; name: string }>("POST", "/api/machines", info);
73
+ }
74
+ heartbeat(machineId: string, info: { version?: string; runtimes?: string[]; usage_info?: UsageInfo | null }) {
75
+ return this.request("POST", `/api/machines/${machineId}/heartbeat`, info);
76
+ }
77
+
78
+ // Agents
79
+ registerAgent(agentId: string, publicKey?: string, runtime?: string, model?: string) {
80
+ return this.request("POST", "/api/agents", { agent_id: agentId, public_key: publicKey, runtime, model });
81
+ }
82
+ listAgents() { return this.request("GET", "/api/agents"); }
83
+
84
+ // Boards
85
+ createBoard(input: { name: string; description?: string }) {
86
+ return this.request("POST", "/api/boards", input);
87
+ }
88
+ listBoards() { return this.request<any[]>("GET", "/api/boards"); }
89
+ getBoardByName(name: string) { return this.request("GET", `/api/boards?name=${encodeURIComponent(name)}`); }
90
+ getBoard(boardId: string) { return this.request("GET", `/api/boards/${boardId}`); }
91
+
92
+ // Repositories
93
+ createRepository(input: { name: string; url: string }) {
94
+ return this.request("POST", "/api/repositories", input);
95
+ }
96
+ listRepositories() { return this.request<any[]>("GET", "/api/repositories"); }
97
+
98
+ // Agent usage
99
+ updateAgentUsage(agentId: string, usage: { input_tokens: number; output_tokens: number; cache_read_tokens: number; cache_creation_tokens: number; cost_micro_usd: number }) {
100
+ return this.request("PATCH", `/api/agents/${agentId}/usage`, usage);
101
+ }
102
+
103
+ // Messages
104
+ sendMessage(taskId: string, body: { agent_id: string; role: string; content: string }) {
105
+ return this.request("POST", `/api/tasks/${taskId}/messages`, body);
106
+ }
107
+ getMessages(taskId: string, since?: string) {
108
+ const qs = since ? `?since=${encodeURIComponent(since)}` : "";
109
+ return this.request<any[]>("GET", `/api/tasks/${taskId}/messages${qs}`);
110
+ }
111
+ }
112
+
113
+ export class AgentClient {
114
+ private baseUrl: string;
115
+ private agentId: string;
116
+ private privateKey: CryptoKey;
117
+
118
+ constructor(baseUrl: string, agentId: string, privateKey: CryptoKey) {
119
+ this.baseUrl = baseUrl.replace(/\/$/, "");
120
+ this.agentId = agentId;
121
+ this.privateKey = privateKey;
122
+ }
123
+
124
+ private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
125
+ const jwt = await new SignJWT({ sub: this.agentId, jti: randomUUID(), aud: this.baseUrl })
126
+ .setProtectedHeader({ alg: "EdDSA", typ: "agent+jwt" })
127
+ .setIssuedAt()
128
+ .setExpirationTime("60s")
129
+ .sign(this.privateKey);
130
+
131
+ const res = await fetch(`${this.baseUrl}${path}`, {
132
+ method,
133
+ headers: {
134
+ "Content-Type": "application/json",
135
+ Authorization: `Bearer ${jwt}`,
136
+ },
137
+ body: body ? JSON.stringify(body) : undefined,
138
+ signal: AbortSignal.timeout(10000),
139
+ });
140
+
141
+ const data = await res.json() as T & { error?: { code: string; message: string } };
142
+
143
+ if (!res.ok) {
144
+ const msg = (data as any).error?.message || `HTTP ${res.status}`;
145
+ throw new Error(msg);
146
+ }
147
+
148
+ return data;
149
+ }
150
+
151
+ // Tasks
152
+ releaseTask(id: string) {
153
+ return this.request("POST", `/api/tasks/${id}/release`);
154
+ }
155
+
156
+ // Agent usage
157
+ updateAgentUsage(usage: { input_tokens: number; output_tokens: number; cache_read_tokens: number; cache_creation_tokens: number; cost_micro_usd: number }) {
158
+ return this.request("PATCH", `/api/agents/${this.agentId}/usage`, usage);
159
+ }
160
+
161
+ // Messages
162
+ sendMessage(taskId: string, body: { agent_id: string; role: string; content: string }) {
163
+ return this.request("POST", `/api/tasks/${taskId}/messages`, body);
164
+ }
165
+ }
@@ -0,0 +1,60 @@
1
+ import { execSync } from "child_process";
2
+ import { basename } from "path";
3
+ import type { Command } from "commander";
4
+ import { ApiClient } from "../client.js";
5
+ import { setLink } from "../links.js";
6
+
7
+ function getGitRepoRoot(): string {
8
+ return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
9
+ }
10
+
11
+ function getGitRemoteUrl(): string | undefined {
12
+ try {
13
+ return execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
14
+ } catch {
15
+ return undefined;
16
+ }
17
+ }
18
+
19
+ export function registerLinkCommand(program: Command) {
20
+ program
21
+ .command("link")
22
+ .description("Register current repo and map local directory to it")
23
+ .action(async () => {
24
+ let repoRoot: string;
25
+ try {
26
+ repoRoot = getGitRepoRoot();
27
+ } catch {
28
+ console.error("Not a git repository. Run this command from a git repo.");
29
+ process.exit(1);
30
+ }
31
+
32
+ const remoteUrl = getGitRemoteUrl();
33
+ if (!remoteUrl) {
34
+ console.error("No git remote found. Add an origin remote first.");
35
+ process.exit(1);
36
+ }
37
+
38
+ const client = new ApiClient();
39
+ let repo: any;
40
+ try {
41
+ repo = await client.createRepository({
42
+ name: basename(repoRoot),
43
+ url: remoteUrl,
44
+ });
45
+ console.log(`Registered repository: ${remoteUrl}`);
46
+ } catch (err: any) {
47
+ if (err.message?.includes("UNIQUE")) {
48
+ // Already exists — find it
49
+ const repos = await client.listRepositories();
50
+ repo = repos.find((r: any) => r.url === remoteUrl);
51
+ console.log(`Repository already registered: ${remoteUrl}`);
52
+ } else {
53
+ throw err;
54
+ }
55
+ }
56
+
57
+ setLink(repo.id, repoRoot);
58
+ console.log(`Linked repository ${repo.id} → ${repoRoot}`);
59
+ });
60
+ }
@@ -0,0 +1,56 @@
1
+ import { createInterface } from "readline";
2
+ import type { Command } from "commander";
3
+ import { startDaemon } from "../daemon.js";
4
+ import { setConfigValue, getConfigValue, deleteConfigValue } from "../config.js";
5
+
6
+ function confirm(question: string): Promise<boolean> {
7
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
8
+ return new Promise((resolve) => {
9
+ rl.question(question, (answer) => {
10
+ rl.close();
11
+ resolve(answer.toLowerCase() === "y");
12
+ });
13
+ });
14
+ }
15
+
16
+ export function registerStartCommand(program: Command) {
17
+ program
18
+ .command("start")
19
+ .description("Start the Machine daemon — auto-claim and execute tasks")
20
+ .option("--api-url <url>", "API server URL")
21
+ .option("--api-key <key>", "Machine API key")
22
+ .option("--max-concurrent <n>", "Max concurrent agents", "3")
23
+ .option("--agent-cli <cmd>", "Agent CLI command to spawn", "claude")
24
+ .option("--poll-interval <ms>", "Poll interval in ms", "10000")
25
+ .option("--task-timeout <ms>", "Task timeout in ms (0 to disable)", "7200000")
26
+ .action(async (opts) => {
27
+ if (opts.apiUrl) setConfigValue("api-url", opts.apiUrl);
28
+ if (opts.apiKey) {
29
+ const oldKey = getConfigValue("api-key");
30
+ if (oldKey && oldKey !== opts.apiKey && getConfigValue("machine-id")) {
31
+ const machineId = getConfigValue("machine-id");
32
+ const yes = await confirm(
33
+ `This machine is already registered (${machineId}) with a different API key.\nSwitch to the new key and re-register? [y/N] `
34
+ );
35
+ if (!yes) {
36
+ console.log("Aborted.");
37
+ process.exit(0);
38
+ }
39
+ deleteConfigValue("machine-id");
40
+ }
41
+ setConfigValue("api-key", opts.apiKey);
42
+ }
43
+
44
+ if (!getConfigValue("api-url") || !getConfigValue("api-key")) {
45
+ console.error("API URL and key required. Pass --api-url and --api-key, or set via: ak config set api-url <url>");
46
+ process.exit(1);
47
+ }
48
+
49
+ await startDaemon({
50
+ maxConcurrent: parseInt(opts.maxConcurrent, 10),
51
+ agentCli: opts.agentCli,
52
+ pollInterval: parseInt(opts.pollInterval, 10),
53
+ taskTimeout: parseInt(opts.taskTimeout, 10),
54
+ });
55
+ });
56
+ }
package/src/config.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+
5
+ const CONFIG_DIR = join(homedir(), ".agent-kanban");
6
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
7
+
8
+ interface Config {
9
+ "api-url"?: string;
10
+ "api-key"?: string;
11
+ "machine-id"?: string;
12
+ }
13
+
14
+ export function readConfig(): Config {
15
+ try {
16
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
17
+ } catch {
18
+ return {};
19
+ }
20
+ }
21
+
22
+ export function writeConfig(config: Config): void {
23
+ mkdirSync(CONFIG_DIR, { recursive: true });
24
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
25
+ }
26
+
27
+ export function getConfigValue(key: string): string | undefined {
28
+ return readConfig()[key as keyof Config];
29
+ }
30
+
31
+ export function setConfigValue(key: string, value: string): void {
32
+ const config = readConfig();
33
+ (config as Record<string, string>)[key] = value;
34
+ writeConfig(config);
35
+ }
36
+
37
+ export function deleteConfigValue(key: string): void {
38
+ const config = readConfig();
39
+ delete (config as Record<string, string>)[key];
40
+ writeConfig(config);
41
+ }
42
+
43
+ export const PID_FILE = join(CONFIG_DIR, "daemon.pid");