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,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
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/usage.d.ts
ADDED
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");
|