kantban-cli 0.1.14 → 0.1.16
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/chunk-4IUZAIFL.js +102 -0
- package/dist/chunk-4IUZAIFL.js.map +1 -0
- package/dist/chunk-CQP4B53A.js +140 -0
- package/dist/chunk-CQP4B53A.js.map +1 -0
- package/dist/chunk-FF77FM7X.js +1159 -0
- package/dist/chunk-FF77FM7X.js.map +1 -0
- package/dist/chunk-MN4H5NSU.js +3149 -0
- package/dist/chunk-MN4H5NSU.js.map +1 -0
- package/dist/{cron-AZPDPON3.js → cron-FJVZR2JW.js} +10 -18
- package/dist/cron-FJVZR2JW.js.map +1 -0
- package/dist/index.js +6 -138
- package/dist/index.js.map +1 -1
- package/dist/lib/gate-proxy-server.d.ts +1 -0
- package/dist/lib/gate-proxy-server.js +462 -0
- package/dist/lib/gate-proxy-server.js.map +1 -0
- package/dist/{pipeline-7OFX75AU.js → pipeline-6SDPVNFK.js} +494 -386
- package/dist/pipeline-6SDPVNFK.js.map +1 -0
- package/package.json +3 -1
- package/dist/chunk-KGS3M2MY.js +0 -4067
- package/dist/chunk-KGS3M2MY.js.map +0 -1
- package/dist/cron-AZPDPON3.js.map +0 -1
- package/dist/pipeline-7OFX75AU.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/cron.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport type { KantBanCLIClient } from '../client.js';\nimport { generateMcpConfig, cleanupMcpConfig } from '../lib/mcp-config.js';\nimport { RalphLoop, type RalphLoopDeps, type LoopConfig } from '../lib/ralph-loop.js';\nimport type { ColumnContext, TicketContext } from '../lib/prompt-composer.js';\nimport type { TicketFingerprint } from '@kantban/types';\nimport { ClaudeProvider } from '../providers/claude-provider.js';\nimport type { McpConfig } from '../providers/types.js';\n\nfunction parseDuration(input: string): number {\n const match = input.match(/^(\\d+)(s|m|h)$/);\n if (!match) throw new Error(`Invalid duration: ${input}. Use format like 5m, 30s, 1h`);\n const value = Number(match[1]);\n const unit = match[2];\n switch (unit) {\n case 's': return value * 1000;\n case 'm': return value * 60 * 1000;\n case 'h': return value * 60 * 60 * 1000;\n default: throw new Error(`Unknown unit: ${unit}`);\n }\n}\n\nexport async function runCron(client: KantBanCLIClient, args: string[]): Promise<void> {\n const columnId = args[0];\n if (!columnId) {\n console.error('Usage: kantban cron <column-id> [--interval 5m]');\n process.exit(1);\n }\n\n // Parse interval\n let intervalMs = 5 * 60 * 1000; // default 5m\n const intervalIdx = args.indexOf('--interval');\n const intervalArg = intervalIdx !== -1 ? args[intervalIdx + 1] : undefined;\n if (intervalArg) {\n intervalMs = parseDuration(intervalArg);\n }\n\n // Resolve project ID\n const projectId = process.env['KANTBAN_PROJECT_ID'];\n if (!projectId) {\n console.error('Error: KANTBAN_PROJECT_ID required');\n process.exit(1);\n }\n\n // Generate MCP config (stable path keyed by column)\n const mcpConfigPath = generateMcpConfig(client.baseUrl, client.token, `cron-${columnId}`);\n\n // Create provider and read MCP config\n const provider = new ClaudeProvider();\n const raw = JSON.parse(readFileSync(mcpConfigPath, 'utf-8')) as { mcpServers: McpConfig['servers'] };\n const mcpConfig: McpConfig = { servers: raw.mcpServers };\n\n let stopped = false;\n\n // Graceful shutdown\n const shutdown = () => {\n stopped = true;\n cleanupMcpConfig(mcpConfigPath);\n console.log('\\nCron stopped.');\n process.exit(0);\n };\n process.on('SIGTERM', shutdown);\n process.on('SIGINT', shutdown);\n\n console.log(`Cron: column ${columnId}, interval ${String(intervalMs / 1000)}s`);\n console.log('Press Ctrl+C to stop.\\n');\n\n // Main cron tick\n const tick = async () => {\n if (stopped) return;\n\n try {\n // Fetch column scope\n const columnScope = await client.get<ColumnContext>(\n `/projects/${projectId}/pipeline-context`,\n { columnId },\n );\n\n const tickets = columnScope.tickets ?? [];\n if (tickets.length === 0) {\n console.log(`[${new Date().toLocaleTimeString('en-US', { hour12: false })}] No tickets in column. Sleeping...`);\n return;\n }\n\n console.log(`[${new Date().toLocaleTimeString('en-US', { hour12: false })}] Processing ${String(tickets.length)} ticket(s)...`);\n\n // Process each ticket sequentially (one iteration each for cron)\n for (const ticket of tickets) {\n if (stopped) break;\n\n const deps: RalphLoopDeps = {\n fetchTicketContext: (tid) => client.get<TicketContext>(\n `/projects/${projectId}/pipeline-context`, { ticketId: tid },\n ),\n fetchColumnContext: (cid) => client.get<ColumnContext>(\n `/projects/${projectId}/pipeline-context`, { columnId: cid },\n ),\n fetchFingerprint: (tid) => client.getFingerprint(projectId, tid) as Promise<TicketFingerprint>,\n provider,\n mcpConfig,\n projectId,\n };\n\n const config: LoopConfig = { maxIterations: 1, gutterThreshold: 1 };\n\n const loop = new RalphLoop(ticket.id, columnId, config, deps);\n const result = await loop.run();\n console.log(` ${String(ticket.ticket_number)}: ${result.reason} (${String(result.iterations)} iter)`);\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n console.error(`Cron tick error: ${message}`);\n }\n };\n\n // Run immediately, then on interval\n await tick();\n setInterval(() => void tick(), intervalMs);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,139 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var MAX_RETRIES = 2;
|
|
6
|
-
var RETRY_BASE_MS = 500;
|
|
7
|
-
function isRetryableStatus(status) {
|
|
8
|
-
return status >= 500 || status === 429;
|
|
9
|
-
}
|
|
10
|
-
async function fetchWithRetry(url, init, retries = MAX_RETRIES, _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
11
|
-
let lastError;
|
|
12
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
13
|
-
const controller = new AbortController();
|
|
14
|
-
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
15
|
-
try {
|
|
16
|
-
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
17
|
-
clearTimeout(timer);
|
|
18
|
-
if (res.ok || !isRetryableStatus(res.status)) {
|
|
19
|
-
return res;
|
|
20
|
-
}
|
|
21
|
-
lastError = new Error(`API error ${res.status}: ${await res.text()}`);
|
|
22
|
-
} catch (err) {
|
|
23
|
-
clearTimeout(timer);
|
|
24
|
-
lastError = err;
|
|
25
|
-
}
|
|
26
|
-
if (attempt < retries) {
|
|
27
|
-
const delay = RETRY_BASE_MS * 2 ** attempt * (1 + Math.random() * 0.5);
|
|
28
|
-
await _sleep(delay);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
throw lastError;
|
|
32
|
-
}
|
|
33
|
-
var KantBanCLIClient = class {
|
|
34
|
-
constructor(apiUrl2, apiToken2) {
|
|
35
|
-
this.apiUrl = apiUrl2;
|
|
36
|
-
this.apiToken = apiToken2;
|
|
37
|
-
}
|
|
38
|
-
get baseUrl() {
|
|
39
|
-
return this.apiUrl;
|
|
40
|
-
}
|
|
41
|
-
get token() {
|
|
42
|
-
return this.apiToken;
|
|
43
|
-
}
|
|
44
|
-
async get(path, params) {
|
|
45
|
-
const url = new URL(path, this.apiUrl);
|
|
46
|
-
if (params) {
|
|
47
|
-
for (const [k, v] of Object.entries(params)) {
|
|
48
|
-
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const res = await fetchWithRetry(url.toString(), {
|
|
52
|
-
headers: {
|
|
53
|
-
"Authorization": `Bearer ${this.apiToken}`,
|
|
54
|
-
"X-KantBan-Via": "cli"
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
|
|
58
|
-
const json = await res.json();
|
|
59
|
-
if (!json.success) throw new Error(`API responded with success: false`);
|
|
60
|
-
return json.data;
|
|
61
|
-
}
|
|
62
|
-
async post(path, body) {
|
|
63
|
-
const url = new URL(path, this.apiUrl);
|
|
64
|
-
const headers = {
|
|
65
|
-
"Authorization": `Bearer ${this.apiToken}`,
|
|
66
|
-
"X-KantBan-Via": "cli"
|
|
67
|
-
};
|
|
68
|
-
const init = { method: "POST", headers };
|
|
69
|
-
if (body) {
|
|
70
|
-
headers["Content-Type"] = "application/json";
|
|
71
|
-
init.body = JSON.stringify(body);
|
|
72
|
-
}
|
|
73
|
-
const res = await fetchWithRetry(url.toString(), init);
|
|
74
|
-
if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
|
|
75
|
-
const json = await res.json();
|
|
76
|
-
if (!json.success) throw new Error(`API responded with success: false`);
|
|
77
|
-
return json.data;
|
|
78
|
-
}
|
|
79
|
-
async claimTicket(projectId, ticketId) {
|
|
80
|
-
await this.post(`/projects/${projectId}/tickets/${ticketId}/start`, {});
|
|
81
|
-
}
|
|
82
|
-
async getFingerprint(projectId, ticketId) {
|
|
83
|
-
return this.get(`/projects/${projectId}/tickets/${ticketId}/fingerprint`);
|
|
84
|
-
}
|
|
85
|
-
async put(path, body) {
|
|
86
|
-
const url = new URL(path, this.apiUrl);
|
|
87
|
-
const headers = {
|
|
88
|
-
"Authorization": `Bearer ${this.apiToken}`,
|
|
89
|
-
"X-KantBan-Via": "cli"
|
|
90
|
-
};
|
|
91
|
-
const init = { method: "PUT", headers };
|
|
92
|
-
if (body) {
|
|
93
|
-
headers["Content-Type"] = "application/json";
|
|
94
|
-
init.body = JSON.stringify(body);
|
|
95
|
-
}
|
|
96
|
-
const res = await fetchWithRetry(url.toString(), init);
|
|
97
|
-
if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
|
|
98
|
-
const json = await res.json();
|
|
99
|
-
if (!json.success) throw new Error(`API responded with success: false`);
|
|
100
|
-
return json.data;
|
|
101
|
-
}
|
|
102
|
-
async patch(path, body) {
|
|
103
|
-
const url = new URL(path, this.apiUrl);
|
|
104
|
-
const headers = {
|
|
105
|
-
"Authorization": `Bearer ${this.apiToken}`,
|
|
106
|
-
"X-KantBan-Via": "cli"
|
|
107
|
-
};
|
|
108
|
-
const init = { method: "PATCH", headers };
|
|
109
|
-
if (body) {
|
|
110
|
-
headers["Content-Type"] = "application/json";
|
|
111
|
-
init.body = JSON.stringify(body);
|
|
112
|
-
}
|
|
113
|
-
const res = await fetchWithRetry(url.toString(), init);
|
|
114
|
-
if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
|
|
115
|
-
const json = await res.json();
|
|
116
|
-
if (!json.success) throw new Error(`API responded with success: false`);
|
|
117
|
-
return json.data;
|
|
118
|
-
}
|
|
119
|
-
async delete(path) {
|
|
120
|
-
const url = new URL(path, this.apiUrl);
|
|
121
|
-
const res = await fetchWithRetry(url.toString(), {
|
|
122
|
-
method: "DELETE",
|
|
123
|
-
headers: {
|
|
124
|
-
"Authorization": `Bearer ${this.apiToken}`,
|
|
125
|
-
"X-KantBan-Via": "cli"
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
|
|
129
|
-
const json = await res.json();
|
|
130
|
-
if (!json.success) throw new Error(`API responded with success: false`);
|
|
131
|
-
return json.data;
|
|
132
|
-
}
|
|
133
|
-
async getBoardProject(boardId) {
|
|
134
|
-
return this.get(`/boards/${boardId}/project`);
|
|
135
|
-
}
|
|
136
|
-
};
|
|
2
|
+
import {
|
|
3
|
+
KantBanCLIClient
|
|
4
|
+
} from "./chunk-CQP4B53A.js";
|
|
137
5
|
|
|
138
6
|
// src/index.ts
|
|
139
7
|
var apiToken = process.env["KANTBAN_API_TOKEN"];
|
|
@@ -163,16 +31,16 @@ async function main() {
|
|
|
163
31
|
}
|
|
164
32
|
case "pipeline": {
|
|
165
33
|
if (args[0] === "stop") {
|
|
166
|
-
const { stopPipeline } = await import("./pipeline-
|
|
34
|
+
const { stopPipeline } = await import("./pipeline-6SDPVNFK.js");
|
|
167
35
|
await stopPipeline(args.slice(1));
|
|
168
36
|
} else {
|
|
169
|
-
const { runPipeline } = await import("./pipeline-
|
|
37
|
+
const { runPipeline } = await import("./pipeline-6SDPVNFK.js");
|
|
170
38
|
await runPipeline(client, args);
|
|
171
39
|
}
|
|
172
40
|
break;
|
|
173
41
|
}
|
|
174
42
|
case "cron": {
|
|
175
|
-
const { runCron } = await import("./cron-
|
|
43
|
+
const { runCron } = await import("./cron-FJVZR2JW.js");
|
|
176
44
|
await runCron(client, args);
|
|
177
45
|
break;
|
|
178
46
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/index.ts"],"sourcesContent":["// ── retry / timeout constants ─────────────────────────────────────────────\n\nexport const REQUEST_TIMEOUT_MS = 30_000;\nexport const MAX_RETRIES = 2;\nexport const RETRY_BASE_MS = 500;\n\n/** Returns true for statuses that are worth retrying (server errors + rate limit). */\nexport function isRetryableStatus(status: number): boolean {\n return status >= 500 || status === 429;\n}\n\n/**\n * Wraps `fetch` with:\n * - Per-request AbortController timeout (REQUEST_TIMEOUT_MS)\n * - Exponential backoff with jitter on retryable failures\n * - Up to MAX_RETRIES retries (so MAX_RETRIES + 1 total attempts)\n *\n * @param _sleep — optional override for the backoff sleep (used in tests to avoid real delays)\n */\nexport async function fetchWithRetry(\n url: string,\n init: RequestInit,\n retries = MAX_RETRIES,\n _sleep: (ms: number) => Promise<void> = (ms) =>\n new Promise((resolve) => setTimeout(resolve, ms)),\n): Promise<Response> {\n let lastError: unknown;\n\n for (let attempt = 0; attempt <= retries; attempt++) {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n\n try {\n const res = await fetch(url, { ...init, signal: controller.signal });\n clearTimeout(timer);\n\n // Non-retryable: 2xx (ok) or any non-retryable error status\n if (res.ok || !isRetryableStatus(res.status)) {\n return res;\n }\n\n // Retryable status — treat as a transient failure\n lastError = new Error(`API error ${res.status}: ${await res.text()}`);\n } catch (err) {\n clearTimeout(timer);\n lastError = err;\n // Only retry on network/abort errors, not on logic errors\n }\n\n if (attempt < retries) {\n // Exponential backoff with up to 50% random jitter\n const delay = RETRY_BASE_MS * 2 ** attempt * (1 + Math.random() * 0.5);\n await _sleep(delay);\n }\n }\n\n throw lastError;\n}\n\n// ── client ────────────────────────────────────────────────────────────────\n\nexport class KantBanCLIClient {\n constructor(\n private apiUrl: string,\n private apiToken: string,\n ) {}\n\n get baseUrl(): string {\n return this.apiUrl;\n }\n\n get token(): string {\n return this.apiToken;\n }\n\n async get<T>(path: string, params?: Record<string, string | number | boolean | undefined>): Promise<T> {\n const url = new URL(path, this.apiUrl);\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n if (v !== undefined) url.searchParams.set(k, String(v));\n }\n }\n const res = await fetchWithRetry(url.toString(), {\n headers: {\n 'Authorization': `Bearer ${this.apiToken}`,\n 'X-KantBan-Via': 'cli',\n },\n });\n if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);\n const json = (await res.json()) as { success: boolean; data: T };\n if (!json.success) throw new Error(`API responded with success: false`);\n return json.data;\n }\n\n async post<T>(path: string, body?: Record<string, unknown>): Promise<T> {\n const url = new URL(path, this.apiUrl);\n const headers: Record<string, string> = {\n 'Authorization': `Bearer ${this.apiToken}`,\n 'X-KantBan-Via': 'cli',\n };\n const init: RequestInit = { method: 'POST', headers };\n if (body) {\n headers['Content-Type'] = 'application/json';\n init.body = JSON.stringify(body);\n }\n const res = await fetchWithRetry(url.toString(), init);\n if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);\n const json = (await res.json()) as { success: boolean; data: T };\n if (!json.success) throw new Error(`API responded with success: false`);\n return json.data;\n }\n\n async claimTicket(projectId: string, ticketId: string): Promise<void> {\n await this.post(`/projects/${projectId}/tickets/${ticketId}/start`, {});\n }\n\n async getFingerprint(\n projectId: string,\n ticketId: string,\n ): Promise<{\n column_id: string | null;\n updated_at: string;\n comment_count: number;\n signal_count: number;\n field_value_count: number;\n }> {\n return this.get(`/projects/${projectId}/tickets/${ticketId}/fingerprint`);\n }\n\n async put<T>(path: string, body?: Record<string, unknown>): Promise<T> {\n const url = new URL(path, this.apiUrl);\n const headers: Record<string, string> = {\n 'Authorization': `Bearer ${this.apiToken}`,\n 'X-KantBan-Via': 'cli',\n };\n const init: RequestInit = { method: 'PUT', headers };\n if (body) {\n headers['Content-Type'] = 'application/json';\n init.body = JSON.stringify(body);\n }\n const res = await fetchWithRetry(url.toString(), init);\n if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);\n const json = (await res.json()) as { success: boolean; data: T };\n if (!json.success) throw new Error(`API responded with success: false`);\n return json.data;\n }\n\n async patch<T>(path: string, body?: Record<string, unknown>): Promise<T> {\n const url = new URL(path, this.apiUrl);\n const headers: Record<string, string> = {\n 'Authorization': `Bearer ${this.apiToken}`,\n 'X-KantBan-Via': 'cli',\n };\n const init: RequestInit = { method: 'PATCH', headers };\n if (body) {\n headers['Content-Type'] = 'application/json';\n init.body = JSON.stringify(body);\n }\n const res = await fetchWithRetry(url.toString(), init);\n if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);\n const json = (await res.json()) as { success: boolean; data: T };\n if (!json.success) throw new Error(`API responded with success: false`);\n return json.data;\n }\n\n async delete<T = unknown>(path: string): Promise<T> {\n const url = new URL(path, this.apiUrl);\n const res = await fetchWithRetry(url.toString(), {\n method: 'DELETE',\n headers: {\n 'Authorization': `Bearer ${this.apiToken}`,\n 'X-KantBan-Via': 'cli',\n },\n });\n if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);\n const json = (await res.json()) as { success: boolean; data: T };\n if (!json.success) throw new Error(`API responded with success: false`);\n return json.data;\n }\n\n async getBoardProject(boardId: string): Promise<{ project_id: string }> {\n return this.get(`/boards/${boardId}/project`);\n }\n}\n","#!/usr/bin/env node\nimport { KantBanCLIClient } from './client.js';\n\nconst apiToken = process.env['KANTBAN_API_TOKEN'];\nconst apiUrl = process.env['KANTBAN_API_URL'];\n\nif (!apiToken || !apiUrl) {\n console.error('Error: KANTBAN_API_TOKEN and KANTBAN_API_URL environment variables are required');\n process.exit(1);\n}\n\nconst client = new KantBanCLIClient(apiUrl, apiToken);\nconst [command, ...args] = process.argv.slice(2);\n\nasync function main() {\n switch (command) {\n case 'context': {\n const { runContext } = await import('./commands/context.js');\n await runContext(client, args);\n break;\n }\n case 'status': {\n const { runStatus } = await import('./commands/status.js');\n await runStatus(client, args);\n break;\n }\n case 'work': {\n const { runWork } = await import('./commands/work.js');\n await runWork(client, args);\n break;\n }\n case 'pipeline': {\n if (args[0] === 'stop') {\n const { stopPipeline } = await import('./commands/pipeline.js');\n await stopPipeline(args.slice(1));\n } else {\n const { runPipeline } = await import('./commands/pipeline.js');\n await runPipeline(client, args);\n }\n break;\n }\n case 'cron': {\n const { runCron } = await import('./commands/cron.js');\n await runCron(client, args);\n break;\n }\n default:\n console.log(`kantban CLI — Pipeline orchestration for KantBan\n\nUsage:\n kantban context <scope-type> <scope-id> Output scoped pipeline context to stdout\n kantban status <board-id> Pipeline health at a glance\n kantban work <ticket-id> Start a Claude session for a ticket\n kantban pipeline <board-id> Persistent pipeline orchestrator\n kantban pipeline stop <board-id> Stop running pipeline\n kantban cron <column-id> [--interval 5m] Run single column on a timer\n\nEnvironment:\n KANTBAN_API_TOKEN API token (required)\n KANTBAN_API_URL API URL (required)\n KANTBAN_PROJECT_ID Default project ID (optional)`);\n }\n}\n\nmain().catch((err: Error) => {\n console.error('Error:', err.message);\n process.exit(1);\n});\n"],"mappings":"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","names":["apiUrl","apiToken"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { KantBanCLIClient } from './client.js';\n\nconst apiToken = process.env['KANTBAN_API_TOKEN'];\nconst apiUrl = process.env['KANTBAN_API_URL'];\n\nif (!apiToken || !apiUrl) {\n console.error('Error: KANTBAN_API_TOKEN and KANTBAN_API_URL environment variables are required');\n process.exit(1);\n}\n\nconst client = new KantBanCLIClient(apiUrl, apiToken);\nconst [command, ...args] = process.argv.slice(2);\n\nasync function main() {\n switch (command) {\n case 'context': {\n const { runContext } = await import('./commands/context.js');\n await runContext(client, args);\n break;\n }\n case 'status': {\n const { runStatus } = await import('./commands/status.js');\n await runStatus(client, args);\n break;\n }\n case 'work': {\n const { runWork } = await import('./commands/work.js');\n await runWork(client, args);\n break;\n }\n case 'pipeline': {\n if (args[0] === 'stop') {\n const { stopPipeline } = await import('./commands/pipeline.js');\n await stopPipeline(args.slice(1));\n } else {\n const { runPipeline } = await import('./commands/pipeline.js');\n await runPipeline(client, args);\n }\n break;\n }\n case 'cron': {\n const { runCron } = await import('./commands/cron.js');\n await runCron(client, args);\n break;\n }\n default:\n console.log(`kantban CLI — Pipeline orchestration for KantBan\n\nUsage:\n kantban context <scope-type> <scope-id> Output scoped pipeline context to stdout\n kantban status <board-id> Pipeline health at a glance\n kantban work <ticket-id> Start a Claude session for a ticket\n kantban pipeline <board-id> Persistent pipeline orchestrator\n kantban pipeline stop <board-id> Stop running pipeline\n kantban cron <column-id> [--interval 5m] Run single column on a timer\n\nEnvironment:\n KANTBAN_API_TOKEN API token (required)\n KANTBAN_API_URL API URL (required)\n KANTBAN_PROJECT_ID Default project ID (optional)`);\n }\n}\n\nmain().catch((err: Error) => {\n console.error('Error:', err.message);\n process.exit(1);\n});\n"],"mappings":";;;;;;AAGA,IAAM,WAAW,QAAQ,IAAI,mBAAmB;AAChD,IAAM,SAAS,QAAQ,IAAI,iBAAiB;AAE5C,IAAI,CAAC,YAAY,CAAC,QAAQ;AACxB,UAAQ,MAAM,iFAAiF;AAC/F,UAAQ,KAAK,CAAC;AAChB;AAEA,IAAM,SAAS,IAAI,iBAAiB,QAAQ,QAAQ;AACpD,IAAM,CAAC,SAAS,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC;AAE/C,eAAe,OAAO;AACpB,UAAQ,SAAS;AAAA,IACf,KAAK,WAAW;AACd,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,uBAAuB;AAC3D,YAAM,WAAW,QAAQ,IAAI;AAC7B;AAAA,IACF;AAAA,IACA,KAAK,UAAU;AACb,YAAM,EAAE,UAAU,IAAI,MAAM,OAAO,sBAAsB;AACzD,YAAM,UAAU,QAAQ,IAAI;AAC5B;AAAA,IACF;AAAA,IACA,KAAK,QAAQ;AACX,YAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,oBAAoB;AACrD,YAAM,QAAQ,QAAQ,IAAI;AAC1B;AAAA,IACF;AAAA,IACA,KAAK,YAAY;AACf,UAAI,KAAK,CAAC,MAAM,QAAQ;AACtB,cAAM,EAAE,aAAa,IAAI,MAAM,OAAO,wBAAwB;AAC9D,cAAM,aAAa,KAAK,MAAM,CAAC,CAAC;AAAA,MAClC,OAAO;AACL,cAAM,EAAE,YAAY,IAAI,MAAM,OAAO,wBAAwB;AAC7D,cAAM,YAAY,QAAQ,IAAI;AAAA,MAChC;AACA;AAAA,IACF;AAAA,IACA,KAAK,QAAQ;AACX,YAAM,EAAE,QAAQ,IAAI,MAAM,OAAO,oBAAoB;AACrD,YAAM,QAAQ,QAAQ,IAAI;AAC1B;AAAA,IACF;AAAA,IACA;AACE,cAAQ,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qDAamC;AAAA,EACnD;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,QAAe;AAC3B,UAAQ,MAAM,UAAU,IAAI,OAAO;AACnC,UAAQ,KAAK,CAAC;AAChB,CAAC;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
fetchWithRetry
|
|
4
|
+
} from "../chunk-CQP4B53A.js";
|
|
5
|
+
import {
|
|
6
|
+
formatGateErrors,
|
|
7
|
+
runGates
|
|
8
|
+
} from "../chunk-4IUZAIFL.js";
|
|
9
|
+
import {
|
|
10
|
+
parseGateConfig,
|
|
11
|
+
parseTimeout,
|
|
12
|
+
resolveGatesForColumn
|
|
13
|
+
} from "../chunk-MN4H5NSU.js";
|
|
14
|
+
|
|
15
|
+
// src/lib/gate-proxy-server.ts
|
|
16
|
+
import { readFileSync } from "fs";
|
|
17
|
+
import { createInterface } from "readline";
|
|
18
|
+
|
|
19
|
+
// src/lib/gate-proxy.ts
|
|
20
|
+
var GateProxy = class {
|
|
21
|
+
config;
|
|
22
|
+
deps;
|
|
23
|
+
constructor(config, deps2) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.deps = deps2;
|
|
26
|
+
}
|
|
27
|
+
buildRunOptions() {
|
|
28
|
+
const opts = {};
|
|
29
|
+
if (this.config.settings?.cwd !== void 0) opts.cwd = this.config.settings.cwd;
|
|
30
|
+
if (this.config.settings?.env !== void 0) opts.env = this.config.settings.env;
|
|
31
|
+
if (this.config.settings?.total_timeout !== void 0) {
|
|
32
|
+
opts.totalTimeoutMs = parseTimeout(this.config.settings.total_timeout);
|
|
33
|
+
}
|
|
34
|
+
return opts;
|
|
35
|
+
}
|
|
36
|
+
async handleRunGates(columnName2, ticketId) {
|
|
37
|
+
const allGates = resolveGatesForColumn(this.config, columnName2);
|
|
38
|
+
let gates = allGates;
|
|
39
|
+
if (ticketId) {
|
|
40
|
+
const waivers = await this.deps.getTicketGateWaivers(ticketId);
|
|
41
|
+
const waiverSet = new Set(waivers);
|
|
42
|
+
gates = allGates.filter((g) => !waiverSet.has(g.name));
|
|
43
|
+
}
|
|
44
|
+
const results = await this.deps.runGates(gates, this.buildRunOptions());
|
|
45
|
+
return {
|
|
46
|
+
passed: results.filter((r) => r.required).every((r) => r.passed),
|
|
47
|
+
results
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async handleMoveTicket(move) {
|
|
51
|
+
const allGates = resolveGatesForColumn(this.config, move.currentColumnName);
|
|
52
|
+
const waivers = await this.deps.getTicketGateWaivers(move.ticketId);
|
|
53
|
+
const waiverSet = new Set(waivers);
|
|
54
|
+
const gates = allGates.filter((g) => !waiverSet.has(g.name));
|
|
55
|
+
let results;
|
|
56
|
+
try {
|
|
57
|
+
results = await this.deps.runGates(gates, this.buildRunOptions());
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
error: "GATE_FAILURE",
|
|
61
|
+
message: `Gate evaluation error: ${err instanceof Error ? err.message : String(err)}`,
|
|
62
|
+
hint: "Fix the gate environment and retry"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const requiredFailures = results.filter((r) => r.required && !r.passed);
|
|
66
|
+
if (requiredFailures.length > 0) {
|
|
67
|
+
return {
|
|
68
|
+
error: "GATE_FAILURE",
|
|
69
|
+
message: `Cannot move ticket \u2014 ${requiredFailures.length} required gate(s) failed`,
|
|
70
|
+
formatted: formatGateErrors(results.filter((r) => r.required)),
|
|
71
|
+
results,
|
|
72
|
+
hint: "Fix the failing gate(s) and try move_ticket again"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const forwardResult = await this.deps.forwardMoveTicket({
|
|
76
|
+
...move.args,
|
|
77
|
+
projectId: move.projectId,
|
|
78
|
+
ticketId: move.ticketId,
|
|
79
|
+
column_id: move.columnId
|
|
80
|
+
});
|
|
81
|
+
return { forwardResult };
|
|
82
|
+
}
|
|
83
|
+
async handleCompleteTask(complete) {
|
|
84
|
+
const allGates = resolveGatesForColumn(this.config, complete.currentColumnName);
|
|
85
|
+
const waivers = await this.deps.getTicketGateWaivers(complete.ticketId);
|
|
86
|
+
const waiverSet = new Set(waivers);
|
|
87
|
+
const gates = allGates.filter((g) => !waiverSet.has(g.name));
|
|
88
|
+
let results;
|
|
89
|
+
try {
|
|
90
|
+
results = await this.deps.runGates(gates, this.buildRunOptions());
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return {
|
|
93
|
+
error: "GATE_FAILURE",
|
|
94
|
+
message: `Gate evaluation error: ${err instanceof Error ? err.message : String(err)}`,
|
|
95
|
+
hint: "Fix the gate environment and retry"
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const requiredFailures = results.filter((r) => r.required && !r.passed);
|
|
99
|
+
if (requiredFailures.length > 0) {
|
|
100
|
+
return {
|
|
101
|
+
error: "GATE_FAILURE",
|
|
102
|
+
message: `Cannot complete task \u2014 ${requiredFailures.length} required gate(s) failed`,
|
|
103
|
+
formatted: formatGateErrors(results.filter((r) => r.required)),
|
|
104
|
+
results,
|
|
105
|
+
hint: "Fix the failing gate(s) and try complete_task again"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const forwardResult = await this.deps.forwardCompleteTask({
|
|
109
|
+
...complete.args,
|
|
110
|
+
projectId: complete.projectId,
|
|
111
|
+
ticketId: complete.ticketId
|
|
112
|
+
});
|
|
113
|
+
return { forwardResult };
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// src/lib/gate-proxy-server.ts
|
|
118
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
119
|
+
function validateUuid(value, name) {
|
|
120
|
+
if (typeof value !== "string" || !UUID_RE.test(value)) {
|
|
121
|
+
throw new Error(`Invalid ${name}: expected UUID, got "${String(value)}"`);
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
var GATE_CONFIG_PATH = process.env["GATE_CONFIG_PATH"];
|
|
126
|
+
var COLUMN_ID = process.env["COLUMN_ID"];
|
|
127
|
+
var COLUMN_NAME = process.env["COLUMN_NAME"];
|
|
128
|
+
var PROJECT_ID = process.env["PROJECT_ID"];
|
|
129
|
+
var API_TOKEN = process.env["KANTBAN_API_TOKEN"];
|
|
130
|
+
var API_URL = process.env["KANTBAN_API_URL"];
|
|
131
|
+
function requireEnv(name, value) {
|
|
132
|
+
if (!value) {
|
|
133
|
+
process.stderr.write(`gate-proxy-server: missing required env var ${name}
|
|
134
|
+
`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
var gateConfigPath = requireEnv("GATE_CONFIG_PATH", GATE_CONFIG_PATH);
|
|
140
|
+
var columnId = requireEnv("COLUMN_ID", COLUMN_ID);
|
|
141
|
+
var columnName = requireEnv("COLUMN_NAME", COLUMN_NAME);
|
|
142
|
+
var projectId = requireEnv("PROJECT_ID", PROJECT_ID);
|
|
143
|
+
var apiToken = requireEnv("KANTBAN_API_TOKEN", API_TOKEN);
|
|
144
|
+
var apiUrl = requireEnv("KANTBAN_API_URL", API_URL);
|
|
145
|
+
var yamlContent = readFileSync(gateConfigPath, "utf-8");
|
|
146
|
+
var gateConfig = parseGateConfig(yamlContent);
|
|
147
|
+
function apiHeaders() {
|
|
148
|
+
return {
|
|
149
|
+
"Authorization": `Bearer ${apiToken}`,
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
"X-KantBan-Via": "cli-gate-proxy"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function apiPost(path, body) {
|
|
155
|
+
const url = new URL(path, apiUrl);
|
|
156
|
+
const res = await fetchWithRetry(url.toString(), {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: apiHeaders(),
|
|
159
|
+
body: JSON.stringify(body)
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const text = await res.text();
|
|
163
|
+
throw new Error(`API error ${res.status}: ${text}`);
|
|
164
|
+
}
|
|
165
|
+
const json = await res.json();
|
|
166
|
+
if (!json.success) throw new Error("API responded with success: false");
|
|
167
|
+
return json.data;
|
|
168
|
+
}
|
|
169
|
+
async function apiPatch(path, body) {
|
|
170
|
+
const url = new URL(path, apiUrl);
|
|
171
|
+
const res = await fetchWithRetry(url.toString(), {
|
|
172
|
+
method: "PATCH",
|
|
173
|
+
headers: apiHeaders(),
|
|
174
|
+
body: JSON.stringify(body)
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const text = await res.text();
|
|
178
|
+
throw new Error(`API error ${res.status}: ${text}`);
|
|
179
|
+
}
|
|
180
|
+
const json = await res.json();
|
|
181
|
+
if (!json.success) throw new Error("API responded with success: false");
|
|
182
|
+
return json.data;
|
|
183
|
+
}
|
|
184
|
+
async function apiGet(path, params) {
|
|
185
|
+
const url = new URL(path, apiUrl);
|
|
186
|
+
if (params) {
|
|
187
|
+
for (const [k, v] of Object.entries(params)) {
|
|
188
|
+
url.searchParams.set(k, v);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const res = await fetchWithRetry(url.toString(), {
|
|
192
|
+
headers: apiHeaders()
|
|
193
|
+
});
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
const text = await res.text();
|
|
196
|
+
throw new Error(`API error ${res.status}: ${text}`);
|
|
197
|
+
}
|
|
198
|
+
const json = await res.json();
|
|
199
|
+
if (!json.success) throw new Error("API responded with success: false");
|
|
200
|
+
return json.data;
|
|
201
|
+
}
|
|
202
|
+
var deps = {
|
|
203
|
+
runGates,
|
|
204
|
+
async forwardMoveTicket(args) {
|
|
205
|
+
const ticketProjectId = validateUuid(args["projectId"], "projectId");
|
|
206
|
+
const ticketId = validateUuid(args["ticketId"], "ticketId");
|
|
207
|
+
const body = { ...args };
|
|
208
|
+
delete body["projectId"];
|
|
209
|
+
delete body["ticketId"];
|
|
210
|
+
return apiPatch(`/projects/${ticketProjectId}/tickets/${ticketId}/move`, body);
|
|
211
|
+
},
|
|
212
|
+
async forwardCompleteTask(args) {
|
|
213
|
+
const projectId2 = validateUuid(args["projectId"], "projectId");
|
|
214
|
+
const ticketId = validateUuid(args["ticketId"], "ticketId");
|
|
215
|
+
const body = { ...args };
|
|
216
|
+
delete body["projectId"];
|
|
217
|
+
delete body["ticketId"];
|
|
218
|
+
return apiPost(`/projects/${projectId2}/tickets/${ticketId}/complete`, body);
|
|
219
|
+
},
|
|
220
|
+
async getTicketGateWaivers(ticketId) {
|
|
221
|
+
try {
|
|
222
|
+
validateUuid(ticketId, "ticketId");
|
|
223
|
+
const data = await apiGet(`/projects/${projectId}/tickets/${ticketId}/field-values`);
|
|
224
|
+
const waiver = data.find((fv) => fv.field_name === "gate_waiver");
|
|
225
|
+
if (!waiver) return [];
|
|
226
|
+
if (Array.isArray(waiver.value)) {
|
|
227
|
+
return waiver.value.filter((v) => typeof v === "string" && v.length > 0);
|
|
228
|
+
}
|
|
229
|
+
return [];
|
|
230
|
+
} catch (err) {
|
|
231
|
+
process.stderr.write(`gate-proxy-server: waiver fetch failed (running all gates): ${err instanceof Error ? err.message : String(err)}
|
|
232
|
+
`);
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
var proxy = new GateProxy(gateConfig, deps);
|
|
238
|
+
var TOOLS = [
|
|
239
|
+
{
|
|
240
|
+
name: "kantban_run_gates",
|
|
241
|
+
description: "Run all configured gates for the current column and report results. Use this to check gate status before attempting to move a ticket. Pass ticketId to filter out waived gates for that ticket.",
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties: {
|
|
245
|
+
ticketId: { type: "string", description: "Ticket ID (UUID) \u2014 filters out waived gates for this ticket" }
|
|
246
|
+
},
|
|
247
|
+
required: []
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "kantban_move_ticket",
|
|
252
|
+
description: "Move a ticket to a different column. Gates are automatically enforced \u2014 if any required gate fails, the move is blocked and failure details are returned.",
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: "object",
|
|
255
|
+
properties: {
|
|
256
|
+
projectId: { type: "string", description: "Project ID (UUID)" },
|
|
257
|
+
ticketId: { type: "string", description: "Ticket ID (UUID)" },
|
|
258
|
+
columnId: { type: "string", description: "Target column ID (UUID)" },
|
|
259
|
+
currentColumnName: { type: "string", description: "Current column name" },
|
|
260
|
+
handoff: {
|
|
261
|
+
type: "object",
|
|
262
|
+
description: "Structured handoff data for the next pipeline stage",
|
|
263
|
+
additionalProperties: true
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
required: ["projectId", "ticketId", "columnId"]
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "kantban_complete_task",
|
|
271
|
+
description: "Mark a ticket as complete. Gates are automatically enforced \u2014 if any required gate fails, the completion is blocked and failure details are returned.",
|
|
272
|
+
inputSchema: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
projectId: { type: "string", description: "Project ID (UUID)" },
|
|
276
|
+
ticketId: { type: "string", description: "Ticket ID (UUID)" },
|
|
277
|
+
currentColumnName: { type: "string", description: "Current column name" },
|
|
278
|
+
moveToColumn: { type: "string", description: 'Column name to move the ticket to (e.g. "Done")' },
|
|
279
|
+
completionComment: { type: "string", description: "Comment to add upon completion" },
|
|
280
|
+
handoff: {
|
|
281
|
+
type: "object",
|
|
282
|
+
description: "Structured handoff data for the next pipeline stage",
|
|
283
|
+
additionalProperties: true
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
required: ["projectId", "ticketId"]
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
];
|
|
290
|
+
async function handleToolCall(name, args) {
|
|
291
|
+
try {
|
|
292
|
+
switch (name) {
|
|
293
|
+
case "kantban_run_gates": {
|
|
294
|
+
const ticketIdArg = args["ticketId"];
|
|
295
|
+
if (ticketIdArg) validateUuid(ticketIdArg, "ticketId");
|
|
296
|
+
const result = await proxy.handleRunGates(columnName, ticketIdArg);
|
|
297
|
+
return {
|
|
298
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
case "kantban_move_ticket": {
|
|
302
|
+
const moveProjectId = validateUuid(args["projectId"], "projectId");
|
|
303
|
+
const moveTicketId = validateUuid(args["ticketId"], "ticketId");
|
|
304
|
+
const moveColumnId = validateUuid(args["columnId"], "columnId");
|
|
305
|
+
const result = await proxy.handleMoveTicket({
|
|
306
|
+
projectId: moveProjectId,
|
|
307
|
+
ticketId: moveTicketId,
|
|
308
|
+
columnId: moveColumnId,
|
|
309
|
+
currentColumnName: args["currentColumnName"] ?? columnName,
|
|
310
|
+
args: {
|
|
311
|
+
...args["handoff"] !== void 0 ? { handoff: args["handoff"] } : {},
|
|
312
|
+
...args["column_id"] !== void 0 ? { column_id: args["column_id"] } : {}
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
const isError = result.error === "GATE_FAILURE";
|
|
316
|
+
if (isError && result.results) {
|
|
317
|
+
process.stderr.write(`gate-proxy-server: move blocked
|
|
318
|
+
${formatGateErrors(result.results)}`);
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
322
|
+
...isError ? { isError: true } : {}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
case "kantban_complete_task": {
|
|
326
|
+
const completeProjectId = validateUuid(args["projectId"], "projectId");
|
|
327
|
+
const completeTicketId = validateUuid(args["ticketId"], "ticketId");
|
|
328
|
+
const result = await proxy.handleCompleteTask({
|
|
329
|
+
projectId: completeProjectId,
|
|
330
|
+
ticketId: completeTicketId,
|
|
331
|
+
currentColumnName: args["currentColumnName"] ?? columnName,
|
|
332
|
+
args: {
|
|
333
|
+
...args["moveToColumn"] !== void 0 ? { moveToColumn: args["moveToColumn"] } : {},
|
|
334
|
+
...args["completionComment"] !== void 0 ? { completionComment: args["completionComment"] } : {},
|
|
335
|
+
...args["handoff"] !== void 0 ? { handoff: args["handoff"] } : {}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
const isError = result.error === "GATE_FAILURE";
|
|
339
|
+
if (isError && result.results) {
|
|
340
|
+
process.stderr.write(`gate-proxy-server: complete blocked
|
|
341
|
+
${formatGateErrors(result.results)}`);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
345
|
+
...isError ? { isError: true } : {}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
default:
|
|
349
|
+
return {
|
|
350
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
351
|
+
isError: true
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
358
|
+
isError: true
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function sendResponse(response) {
|
|
363
|
+
const json = JSON.stringify(response);
|
|
364
|
+
process.stdout.write(json + "\n");
|
|
365
|
+
}
|
|
366
|
+
async function handleMessage(msg) {
|
|
367
|
+
const { id, method, params } = msg;
|
|
368
|
+
switch (method) {
|
|
369
|
+
case "initialize": {
|
|
370
|
+
sendResponse({
|
|
371
|
+
jsonrpc: "2.0",
|
|
372
|
+
id,
|
|
373
|
+
result: {
|
|
374
|
+
protocolVersion: "2024-11-05",
|
|
375
|
+
capabilities: { tools: {} },
|
|
376
|
+
serverInfo: {
|
|
377
|
+
name: "kantban-gates",
|
|
378
|
+
version: "0.1.0"
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
case "notifications/initialized": {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
case "tools/list": {
|
|
388
|
+
sendResponse({
|
|
389
|
+
jsonrpc: "2.0",
|
|
390
|
+
id,
|
|
391
|
+
result: { tools: TOOLS }
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
case "tools/call": {
|
|
396
|
+
const toolName = params?.["name"];
|
|
397
|
+
const toolArgs = params?.["arguments"] ?? {};
|
|
398
|
+
if (!toolName) {
|
|
399
|
+
sendResponse({
|
|
400
|
+
jsonrpc: "2.0",
|
|
401
|
+
id,
|
|
402
|
+
error: { code: -32602, message: "Missing tool name" }
|
|
403
|
+
});
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const result = await handleToolCall(toolName, toolArgs);
|
|
407
|
+
sendResponse({
|
|
408
|
+
jsonrpc: "2.0",
|
|
409
|
+
id,
|
|
410
|
+
result
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
case "ping": {
|
|
415
|
+
sendResponse({ jsonrpc: "2.0", id, result: {} });
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
default: {
|
|
419
|
+
if (method.startsWith("notifications/")) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
sendResponse({
|
|
423
|
+
jsonrpc: "2.0",
|
|
424
|
+
id,
|
|
425
|
+
error: { code: -32601, message: `Method not found: ${method}` }
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
var rl = createInterface({ input: process.stdin });
|
|
431
|
+
var messageQueue = Promise.resolve();
|
|
432
|
+
rl.on("line", (line) => {
|
|
433
|
+
const trimmed = line.trim();
|
|
434
|
+
if (!trimmed) return;
|
|
435
|
+
let msg;
|
|
436
|
+
try {
|
|
437
|
+
msg = JSON.parse(trimmed);
|
|
438
|
+
} catch {
|
|
439
|
+
sendResponse({
|
|
440
|
+
jsonrpc: "2.0",
|
|
441
|
+
id: null,
|
|
442
|
+
error: { code: -32700, message: "Parse error" }
|
|
443
|
+
});
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
messageQueue = messageQueue.then(() => handleMessage(msg)).catch((err) => {
|
|
447
|
+
process.stderr.write(`gate-proxy-server: unhandled error in message handler: ${err instanceof Error ? err.message : String(err)}
|
|
448
|
+
`);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
rl.on("close", () => {
|
|
452
|
+
process.exit(0);
|
|
453
|
+
});
|
|
454
|
+
process.on("unhandledRejection", (err) => {
|
|
455
|
+
process.stderr.write(
|
|
456
|
+
`gate-proxy-server: unhandled rejection: ${err instanceof Error ? err.message : String(err)}
|
|
457
|
+
`
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
process.stderr.write(`gate-proxy-server: started (column="${columnName}", gates=${gateConfig.default.length})
|
|
461
|
+
`);
|
|
462
|
+
//# sourceMappingURL=gate-proxy-server.js.map
|