@gzmagyari/kanbanboard 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/.env.example +48 -0
- package/API.md +1256 -0
- package/README.md +138 -0
- package/bin/cli.mjs +437 -0
- package/cron-sync.mjs +9 -0
- package/db.mjs +378 -0
- package/docs/project-manager-chat.md +202 -0
- package/kanban-mcp-server.mjs +377 -0
- package/kanban.mjs +127 -0
- package/lib/paths.mjs +136 -0
- package/llm.mjs +307 -0
- package/package.json +52 -0
- package/public/index.html +4747 -0
- package/repo-grounding.mjs +417 -0
- package/server.mjs +8607 -0
package/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Jarvis Kanban Dashboard
|
|
2
|
+
|
|
3
|
+
A lightweight kanban board for managing tasks for Jarvis/Clawdbot — now with Projects + basic task hierarchy (sub-tickets).
|
|
4
|
+
|
|
5
|
+
- Frontend: **Vue 2 + Vuetify 2** (CDN, no build step)
|
|
6
|
+
- Backend: **Node.js (Express) + SQLite**
|
|
7
|
+
- Drag & drop: SortableJS via vuedraggable
|
|
8
|
+
|
|
9
|
+
## Columns
|
|
10
|
+
- Ideas
|
|
11
|
+
- To do
|
|
12
|
+
- In Progress
|
|
13
|
+
- Review
|
|
14
|
+
- Testing
|
|
15
|
+
- Done
|
|
16
|
+
|
|
17
|
+
Jarvis should only pick up tasks in **To do** and **In Progress**.
|
|
18
|
+
|
|
19
|
+
## Run
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd /home/clawdbot/clawd/kanban-dashboard
|
|
23
|
+
npm install
|
|
24
|
+
HOST=0.0.0.0 PORT=3000 npm start
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Open: `http://<server-ip>:3000/`
|
|
28
|
+
|
|
29
|
+
## API (summary)
|
|
30
|
+
- `GET /api/health`
|
|
31
|
+
- `GET /api/columns`
|
|
32
|
+
|
|
33
|
+
- `GET /api/ai/health` (shows whether LLM is configured/enabled)
|
|
34
|
+
- `POST /api/ai/tasks/merge` (AI create: enhance + optional breakdown into sub-tickets; returns plan + created tasks when applied)
|
|
35
|
+
- `POST /api/ai/projects/:id/evolve` (AI: propose new tickets based on current state)
|
|
36
|
+
- `POST /api/ai/projects/:id/remove-concept` (AI: remove/unmerge a concept from the hierarchy)
|
|
37
|
+
- `POST /api/ai/projects/:id/init` (AI: generate/overwrite project scope from a repo path; optional seed tickets)
|
|
38
|
+
|
|
39
|
+
- `GET /api/tasks/:id/ancestors` (optional `?include_self=1`)
|
|
40
|
+
- `GET /api/tasks/:id/descendants` (optional `?mode=flat|tree&include_self=1`)
|
|
41
|
+
- `GET /api/tasks/:id/context`
|
|
42
|
+
- `POST /api/tasks/:id/next` (select next leaf sub-ticket; LLM-assisted when enabled, heuristic fallback)
|
|
43
|
+
|
|
44
|
+
- `GET /api/projects`
|
|
45
|
+
- `POST /api/projects`
|
|
46
|
+
- `GET /api/projects/:id`
|
|
47
|
+
- `PATCH /api/projects/:id`
|
|
48
|
+
- `DELETE /api/projects/:id` (reassigns tickets to default project, then deletes)
|
|
49
|
+
|
|
50
|
+
- `GET /api/tasks` (optional `?status=todo&project_id=default&parent_id=<id>`)
|
|
51
|
+
- `POST /api/tasks` (supports `project_id` + `parent_id`)
|
|
52
|
+
- `GET /api/tasks/:id`
|
|
53
|
+
- `PATCH /api/tasks/:id` (supports `status`, `project_id`, `parent_id`)
|
|
54
|
+
- `POST /api/tasks/:id/comments`
|
|
55
|
+
- `POST /api/tasks/:id/updates` (append-only progress log)
|
|
56
|
+
- `GET /api/tasks/:id/updates?limit=5`
|
|
57
|
+
|
|
58
|
+
## Database
|
|
59
|
+
SQLite file: `kanban-dashboard/data/kanban.db`
|
|
60
|
+
|
|
61
|
+
Tables:
|
|
62
|
+
- `projects` (project scope/context lives here)
|
|
63
|
+
- `tasks`
|
|
64
|
+
- `automation_tasks` (recurring/scheduled; now also linked to projects)
|
|
65
|
+
- `comments`
|
|
66
|
+
- `progress_updates`
|
|
67
|
+
- `llm_plans` (stores validated AI "plans" and whether they were applied)
|
|
68
|
+
- `llm_runs` (stores raw LLM request/response/error logs for auditing)
|
|
69
|
+
|
|
70
|
+
## LLM / AI (Phase 3)
|
|
71
|
+
Phase 3 adds:
|
|
72
|
+
- `llm_runs` table (logs LLM requests/responses/errors)
|
|
73
|
+
- `/api/ai/health`
|
|
74
|
+
- `POST /api/tasks/:id/next` (LLM-assisted next-leaf selection)
|
|
75
|
+
|
|
76
|
+
### Environment variables
|
|
77
|
+
The backend uses an **OpenAI-compatible** Chat Completions endpoint (works with LiteLLM Proxy, OpenRouter direct, etc).
|
|
78
|
+
|
|
79
|
+
Common setup (LiteLLM proxy):
|
|
80
|
+
```bash
|
|
81
|
+
LLM_ENABLED=1
|
|
82
|
+
LLM_BASE_URL=http://127.0.0.1:4000
|
|
83
|
+
LLM_API_KEY=sk-... # optional depending on your proxy
|
|
84
|
+
LLM_MODEL=openrouter/google/gemini-3-pro-preview
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Optional tuning:
|
|
88
|
+
```bash
|
|
89
|
+
LLM_TIMEOUT_MS=45000
|
|
90
|
+
LLM_TEMPERATURE=0
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## AI create (Phase 4)
|
|
94
|
+
`POST /api/ai/tasks/merge` accepts:
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"project_id": "default",
|
|
98
|
+
"parent_id": null,
|
|
99
|
+
"status": "todo",
|
|
100
|
+
"text": "Add OAuth login...\n\nMore details...",
|
|
101
|
+
"breakdown": true,
|
|
102
|
+
"max_depth": 3,
|
|
103
|
+
"apply": true
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Response (when `apply=true`) returns:
|
|
108
|
+
- `plan_id`
|
|
109
|
+
- `llm_run_id`
|
|
110
|
+
- `root_task`
|
|
111
|
+
- `created_tasks[]`
|
|
112
|
+
|
|
113
|
+
## Guardrails / safety (Phase 5)
|
|
114
|
+
### Optional API key for AI endpoints
|
|
115
|
+
If set, all `/api/ai/*` endpoints require `X-API-Key: <value>`.
|
|
116
|
+
```bash
|
|
117
|
+
AI_API_KEY=changeme
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Rate limiting (in-memory)
|
|
121
|
+
Simple per-IP rate limiting for AI/LLM calls.
|
|
122
|
+
```bash
|
|
123
|
+
AI_RATE_LIMIT_PER_MIN=60
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Project init repo scanning safety
|
|
127
|
+
By default, init can only read paths under the server working directory. You can allowlist additional roots:
|
|
128
|
+
```bash
|
|
129
|
+
AI_INIT_ALLOWED_ROOTS=/home/clawdbot,/mnt/data
|
|
130
|
+
AI_INIT_MAX_FILES=400
|
|
131
|
+
AI_INIT_MAX_FILE_BYTES=64000
|
|
132
|
+
AI_INIT_MAX_TOTAL_BYTES=600000
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
To allow any path (dangerous on shared machines):
|
|
136
|
+
```bash
|
|
137
|
+
AI_INIT_ALLOW_ANY=1
|
|
138
|
+
```
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* kanbanboard CLI — manage KanbanBoard from anywhere.
|
|
4
|
+
*
|
|
5
|
+
* Usage: kanbanboard <command>
|
|
6
|
+
*
|
|
7
|
+
* Commands:
|
|
8
|
+
* init Set up ~/.kanbanboard/ with config and empty database
|
|
9
|
+
* run Start the server in the foreground (Ctrl+C to stop)
|
|
10
|
+
* start Start the server in the background (detached)
|
|
11
|
+
* stop Stop a running background server
|
|
12
|
+
* restart Stop + start (background)
|
|
13
|
+
* status Show whether the server is running
|
|
14
|
+
* doctor Check system dependencies and configuration
|
|
15
|
+
* paths Show resolved file paths (debug)
|
|
16
|
+
* mcp Run the MCP server (stdio, for Claude/agent integration)
|
|
17
|
+
* version Print version
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import os from 'node:os';
|
|
23
|
+
import net from 'node:net';
|
|
24
|
+
import readline from 'node:readline';
|
|
25
|
+
import { spawn, execSync } from 'node:child_process';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
getHome, isDevelopment, getPackageRoot,
|
|
31
|
+
loadEnv, ensureDirectories, printPathSummary,
|
|
32
|
+
getDataDir, getDbPath, getRuntimeDir, getEnvPath,
|
|
33
|
+
getMcpJsonPath, getPidFile, getLogFile, getPublicDir,
|
|
34
|
+
getServerScript, getQueueLogPath,
|
|
35
|
+
} from '../lib/paths.mjs';
|
|
36
|
+
|
|
37
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
38
|
+
const require = createRequire(import.meta.url);
|
|
39
|
+
const pkg = require('../package.json');
|
|
40
|
+
|
|
41
|
+
const command = (process.argv[2] || '').toLowerCase();
|
|
42
|
+
|
|
43
|
+
switch (command) {
|
|
44
|
+
case 'init':
|
|
45
|
+
await cmdInit();
|
|
46
|
+
break;
|
|
47
|
+
case 'run':
|
|
48
|
+
await cmdRun();
|
|
49
|
+
break;
|
|
50
|
+
case 'start':
|
|
51
|
+
await cmdStart();
|
|
52
|
+
break;
|
|
53
|
+
case 'stop':
|
|
54
|
+
await cmdStop();
|
|
55
|
+
break;
|
|
56
|
+
case 'restart':
|
|
57
|
+
await cmdStop();
|
|
58
|
+
await sleep(1000);
|
|
59
|
+
await cmdStart();
|
|
60
|
+
break;
|
|
61
|
+
case 'status':
|
|
62
|
+
cmdStatus();
|
|
63
|
+
break;
|
|
64
|
+
case 'doctor':
|
|
65
|
+
await cmdDoctor();
|
|
66
|
+
break;
|
|
67
|
+
case 'paths':
|
|
68
|
+
printPathSummary();
|
|
69
|
+
break;
|
|
70
|
+
case 'mcp':
|
|
71
|
+
await cmdMcp();
|
|
72
|
+
break;
|
|
73
|
+
case 'version':
|
|
74
|
+
case '--version':
|
|
75
|
+
case '-v':
|
|
76
|
+
console.log(`kanbanboard v${pkg.version}`);
|
|
77
|
+
break;
|
|
78
|
+
case 'help':
|
|
79
|
+
case '--help':
|
|
80
|
+
case '-h':
|
|
81
|
+
case '':
|
|
82
|
+
printHelp();
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
console.error(`Unknown command: ${command}`);
|
|
86
|
+
printHelp();
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Helpers ──
|
|
91
|
+
|
|
92
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
93
|
+
|
|
94
|
+
function ask(question, defaultVal = '') {
|
|
95
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
96
|
+
return new Promise(resolve => {
|
|
97
|
+
const suffix = defaultVal ? ` [${defaultVal}]` : '';
|
|
98
|
+
rl.question(`${question}${suffix}: `, answer => {
|
|
99
|
+
rl.close();
|
|
100
|
+
resolve(answer.trim() || defaultVal);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readPid() {
|
|
106
|
+
try {
|
|
107
|
+
const pid = parseInt(fs.readFileSync(getPidFile(), 'utf8').trim(), 10);
|
|
108
|
+
return Number.isFinite(pid) ? pid : null;
|
|
109
|
+
} catch { return null; }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isProcessRunning(pid) {
|
|
113
|
+
if (process.platform === 'win32') {
|
|
114
|
+
try {
|
|
115
|
+
const out = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
|
|
116
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
|
|
117
|
+
});
|
|
118
|
+
return out.includes(String(pid));
|
|
119
|
+
} catch { return false; }
|
|
120
|
+
}
|
|
121
|
+
// Unix: signal 0
|
|
122
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function printHelp() {
|
|
126
|
+
console.log(`
|
|
127
|
+
kanbanboard v${pkg.version} — AI-powered Kanban Dashboard
|
|
128
|
+
|
|
129
|
+
Usage: kanbanboard <command>
|
|
130
|
+
|
|
131
|
+
Commands:
|
|
132
|
+
init Set up ~/.kanbanboard/ with config and empty database
|
|
133
|
+
run Start the server in the foreground (Ctrl+C to stop)
|
|
134
|
+
start Start the server in the background (detached)
|
|
135
|
+
stop Stop a running background server
|
|
136
|
+
restart Stop + start (background)
|
|
137
|
+
status Show whether the server is running
|
|
138
|
+
doctor Check system dependencies and configuration
|
|
139
|
+
paths Show resolved file paths (debug)
|
|
140
|
+
mcp Run the MCP server (stdio, for Claude/agent integration)
|
|
141
|
+
version Print version
|
|
142
|
+
|
|
143
|
+
Environment:
|
|
144
|
+
KANBANBOARD_HOME Override the config directory (default: ~/.kanbanboard/)
|
|
145
|
+
PORT Server port (default: 3000)
|
|
146
|
+
HOST Server bind address (default: 127.0.0.1)
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Commands ──
|
|
151
|
+
|
|
152
|
+
async function cmdInit() {
|
|
153
|
+
const home = getHome();
|
|
154
|
+
console.log(`\nKanbanBoard Init`);
|
|
155
|
+
console.log(`================`);
|
|
156
|
+
console.log(`Config directory: ${home}\n`);
|
|
157
|
+
|
|
158
|
+
// 1. Create directories
|
|
159
|
+
ensureDirectories();
|
|
160
|
+
console.log(`[ok] Created data directory: ${getDataDir()}`);
|
|
161
|
+
console.log(`[ok] Created runtime directory: ${getRuntimeDir()}`);
|
|
162
|
+
|
|
163
|
+
// 2. Create .env if it doesn't exist
|
|
164
|
+
const envPath = getEnvPath();
|
|
165
|
+
if (fs.existsSync(envPath)) {
|
|
166
|
+
console.log(`[skip] .env already exists at ${envPath}`);
|
|
167
|
+
} else {
|
|
168
|
+
console.log(`\n--- LLM Configuration (optional, press Enter to skip) ---`);
|
|
169
|
+
const llmEnabled = await ask('Enable LLM features? (1/0)', '0');
|
|
170
|
+
const llmBaseUrl = await ask('LLM base URL (OpenAI-compatible)');
|
|
171
|
+
const llmApiKey = await ask('LLM API key');
|
|
172
|
+
const llmModel = await ask('LLM model name', 'openrouter/google/gemini-3-pro-preview');
|
|
173
|
+
|
|
174
|
+
let envContent = `# KanbanBoard Configuration\n`;
|
|
175
|
+
envContent += `# See .env.example for all available options\n\n`;
|
|
176
|
+
envContent += `# Server\n`;
|
|
177
|
+
envContent += `PORT=3000\n`;
|
|
178
|
+
envContent += `HOST=127.0.0.1\n\n`;
|
|
179
|
+
envContent += `# LLM (OpenAI-compatible API)\n`;
|
|
180
|
+
envContent += `LLM_ENABLED=${llmEnabled || '0'}\n`;
|
|
181
|
+
if (llmBaseUrl) envContent += `LLM_BASE_URL=${llmBaseUrl}\n`;
|
|
182
|
+
if (llmApiKey) envContent += `LLM_API_KEY=${llmApiKey}\n`;
|
|
183
|
+
if (llmModel) envContent += `LLM_MODEL=${llmModel}\n`;
|
|
184
|
+
envContent += `\n# AI API protection (set a key to require X-API-Key header)\n`;
|
|
185
|
+
envContent += `# AI_API_KEY=\n`;
|
|
186
|
+
envContent += `# AI_INIT_ALLOW_ANY=1\n`;
|
|
187
|
+
|
|
188
|
+
fs.writeFileSync(envPath, envContent, 'utf8');
|
|
189
|
+
console.log(`\n[ok] Created .env at ${envPath}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 3. Initialize database
|
|
193
|
+
console.log(`\nInitializing database...`);
|
|
194
|
+
await loadEnv();
|
|
195
|
+
const { migrate } = await import('../db.mjs');
|
|
196
|
+
migrate();
|
|
197
|
+
console.log(`[ok] Database initialized at ${getDbPath()}`);
|
|
198
|
+
|
|
199
|
+
console.log(`\n--- Setup Complete ---`);
|
|
200
|
+
console.log(`Run 'kanbanboard doctor' to verify your environment.`);
|
|
201
|
+
console.log(`Run 'kanbanboard run' to start the server.\n`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function cmdRun() {
|
|
205
|
+
ensureDirectories();
|
|
206
|
+
await loadEnv();
|
|
207
|
+
await import('../server.mjs');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function cmdStart() {
|
|
211
|
+
const existing = readPid();
|
|
212
|
+
if (existing && isProcessRunning(existing)) {
|
|
213
|
+
console.log(`Server already running (PID ${existing}). Use 'restart' to bounce.`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (existing) try { fs.unlinkSync(getPidFile()); } catch {}
|
|
217
|
+
|
|
218
|
+
ensureDirectories();
|
|
219
|
+
|
|
220
|
+
const logFile = getLogFile();
|
|
221
|
+
const out = fs.openSync(logFile, 'a');
|
|
222
|
+
const err = fs.openSync(logFile, 'a');
|
|
223
|
+
|
|
224
|
+
// Spawn the CLI itself with 'run' so loadEnv + paths are set up
|
|
225
|
+
const child = spawn(process.execPath, [__filename, 'run'], {
|
|
226
|
+
detached: true,
|
|
227
|
+
stdio: ['ignore', out, err],
|
|
228
|
+
env: { ...process.env, KANBANBOARD_HOME: getHome() },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
child.unref();
|
|
232
|
+
fs.writeFileSync(getPidFile(), String(child.pid), 'utf8');
|
|
233
|
+
console.log(`Server started (PID ${child.pid}). Logs: ${logFile}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function cmdStop() {
|
|
237
|
+
const pid = readPid();
|
|
238
|
+
if (!pid) {
|
|
239
|
+
console.log('Server is not running (no PID file).');
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
if (!isProcessRunning(pid)) {
|
|
243
|
+
console.log(`PID ${pid} is not running. Cleaning stale PID file.`);
|
|
244
|
+
try { fs.unlinkSync(getPidFile()); } catch {}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
if (process.platform === 'win32') {
|
|
249
|
+
execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'pipe' });
|
|
250
|
+
} else {
|
|
251
|
+
process.kill(pid, 'SIGTERM');
|
|
252
|
+
}
|
|
253
|
+
console.log(`Stopped server (PID ${pid}).`);
|
|
254
|
+
} catch (e) {
|
|
255
|
+
console.error(`Failed to stop PID ${pid}: ${e.message}`);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
try { fs.unlinkSync(getPidFile()); } catch {}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function cmdStatus() {
|
|
263
|
+
const pid = readPid();
|
|
264
|
+
if (!pid) {
|
|
265
|
+
console.log('Server is NOT running (no PID file).');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (isProcessRunning(pid)) {
|
|
269
|
+
console.log(`Server is RUNNING (PID ${pid}).`);
|
|
270
|
+
} else {
|
|
271
|
+
console.log(`Server is NOT running (stale PID ${pid}). Cleaning up.`);
|
|
272
|
+
try { fs.unlinkSync(getPidFile()); } catch {}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function cmdMcp() {
|
|
277
|
+
await loadEnv();
|
|
278
|
+
await import('../kanban-mcp-server.mjs');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function cmdDoctor() {
|
|
282
|
+
console.log(`\nKanbanBoard Doctor`);
|
|
283
|
+
console.log(`==================\n`);
|
|
284
|
+
|
|
285
|
+
let issues = 0;
|
|
286
|
+
|
|
287
|
+
// 1. Mode and paths
|
|
288
|
+
console.log(`Mode: ${isDevelopment() ? 'development (repo)' : 'installed'}`);
|
|
289
|
+
console.log(`Home: ${getHome()}\n`);
|
|
290
|
+
|
|
291
|
+
// 2. Node.js version
|
|
292
|
+
const nodeVersion = process.version;
|
|
293
|
+
const major = parseInt(nodeVersion.slice(1), 10);
|
|
294
|
+
if (major >= 18) {
|
|
295
|
+
console.log(`[ok] Node.js ${nodeVersion} (>= 18 required)`);
|
|
296
|
+
} else {
|
|
297
|
+
console.log(`[FAIL] Node.js ${nodeVersion} -- version 18+ required`);
|
|
298
|
+
issues++;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 3. Data directory
|
|
302
|
+
if (fs.existsSync(getDataDir())) {
|
|
303
|
+
console.log(`[ok] Data directory exists: ${getDataDir()}`);
|
|
304
|
+
} else {
|
|
305
|
+
console.log(`[FAIL] Data directory missing: ${getDataDir()}`);
|
|
306
|
+
console.log(` Run 'kanbanboard init' to create it.`);
|
|
307
|
+
issues++;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 4. Database
|
|
311
|
+
if (fs.existsSync(getDbPath())) {
|
|
312
|
+
console.log(`[ok] Database exists: ${getDbPath()}`);
|
|
313
|
+
try {
|
|
314
|
+
const { default: Database } = await import('better-sqlite3');
|
|
315
|
+
const testDb = new Database(getDbPath(), { readonly: true });
|
|
316
|
+
const ver = testDb.pragma('user_version', { simple: true });
|
|
317
|
+
testDb.close();
|
|
318
|
+
console.log(`[ok] Database schema version: ${ver}`);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
console.log(`[WARN] Database may be corrupted: ${e.message}`);
|
|
321
|
+
issues++;
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
console.log(`[WARN] Database not found: ${getDbPath()}`);
|
|
325
|
+
console.log(` It will be created on first server start.`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 5. .env file
|
|
329
|
+
if (fs.existsSync(getEnvPath())) {
|
|
330
|
+
console.log(`[ok] .env file exists: ${getEnvPath()}`);
|
|
331
|
+
} else {
|
|
332
|
+
console.log(`[WARN] .env file not found: ${getEnvPath()}`);
|
|
333
|
+
console.log(` AI features will be disabled without configuration.`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 6. LLM configuration
|
|
337
|
+
await loadEnv();
|
|
338
|
+
const llmEnabled = process.env.LLM_ENABLED;
|
|
339
|
+
const llmBaseUrl = process.env.LLM_BASE_URL;
|
|
340
|
+
const llmApiKey = process.env.LLM_API_KEY;
|
|
341
|
+
if (llmEnabled === '1' || llmEnabled === 'true') {
|
|
342
|
+
console.log(`[ok] LLM enabled`);
|
|
343
|
+
if (llmBaseUrl) {
|
|
344
|
+
console.log(`[ok] LLM base URL: ${llmBaseUrl}`);
|
|
345
|
+
// Try to reach it
|
|
346
|
+
try {
|
|
347
|
+
const url = new URL(llmBaseUrl);
|
|
348
|
+
const mod = url.protocol === 'https:' ? await import('node:https') : await import('node:http');
|
|
349
|
+
await new Promise((resolve, reject) => {
|
|
350
|
+
const req = mod.default.get(url, { timeout: 5000 }, res => {
|
|
351
|
+
res.resume();
|
|
352
|
+
resolve();
|
|
353
|
+
});
|
|
354
|
+
req.on('error', reject);
|
|
355
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
356
|
+
});
|
|
357
|
+
console.log(`[ok] LLM base URL is reachable`);
|
|
358
|
+
} catch (e) {
|
|
359
|
+
console.log(`[WARN] LLM base URL not reachable: ${e.message}`);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
console.log(`[FAIL] LLM enabled but LLM_BASE_URL not set`);
|
|
363
|
+
issues++;
|
|
364
|
+
}
|
|
365
|
+
if (llmApiKey) {
|
|
366
|
+
console.log(`[ok] LLM API key is set (${llmApiKey.slice(0, 8)}...)`);
|
|
367
|
+
} else {
|
|
368
|
+
console.log(`[WARN] LLM_API_KEY not set -- some providers require it`);
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
console.log(`[info] LLM disabled (set LLM_ENABLED=1 to enable)`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 7. CLI tools
|
|
375
|
+
console.log('');
|
|
376
|
+
for (const [name, desc] of [
|
|
377
|
+
['claude', 'Claude Code CLI (Anthropic)'],
|
|
378
|
+
['codex', 'Codex CLI (OpenAI)'],
|
|
379
|
+
['gemini', 'Gemini CLI (Google)'],
|
|
380
|
+
]) {
|
|
381
|
+
try {
|
|
382
|
+
const versionCmd = name === 'gemini' ? `${name} --version` : `${name} --version`;
|
|
383
|
+
const version = execSync(versionCmd, {
|
|
384
|
+
encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
385
|
+
}).trim().split('\n')[0];
|
|
386
|
+
console.log(`[ok] ${desc}: ${version}`);
|
|
387
|
+
} catch {
|
|
388
|
+
console.log(`[info] ${desc}: not found in PATH`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// 8. Port availability
|
|
393
|
+
console.log('');
|
|
394
|
+
const port = Number(process.env.PORT || 3000);
|
|
395
|
+
try {
|
|
396
|
+
await new Promise((resolve, reject) => {
|
|
397
|
+
const server = net.createServer();
|
|
398
|
+
server.once('error', (e) => {
|
|
399
|
+
if (e.code === 'EADDRINUSE') reject(new Error(`Port ${port} in use`));
|
|
400
|
+
else reject(e);
|
|
401
|
+
});
|
|
402
|
+
server.once('listening', () => { server.close(); resolve(); });
|
|
403
|
+
server.listen(port, '127.0.0.1');
|
|
404
|
+
});
|
|
405
|
+
console.log(`[ok] Port ${port} is available`);
|
|
406
|
+
} catch (e) {
|
|
407
|
+
console.log(`[WARN] ${e.message} -- the server may already be running`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 9. Chrome (for MCP agent DevTools)
|
|
411
|
+
let chromeFound = false;
|
|
412
|
+
if (process.platform === 'win32') {
|
|
413
|
+
for (const envVar of ['ProgramFiles', 'ProgramFiles(x86)', 'LocalAppData']) {
|
|
414
|
+
const base = process.env[envVar];
|
|
415
|
+
if (base) {
|
|
416
|
+
const candidate = path.join(base, 'Google', 'Chrome', 'Application', 'chrome.exe');
|
|
417
|
+
if (fs.existsSync(candidate)) { chromeFound = true; break; }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} else if (process.platform === 'darwin') {
|
|
421
|
+
chromeFound = fs.existsSync('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome');
|
|
422
|
+
} else {
|
|
423
|
+
try {
|
|
424
|
+
execSync('which google-chrome || which chromium', { stdio: 'pipe' });
|
|
425
|
+
chromeFound = true;
|
|
426
|
+
} catch {}
|
|
427
|
+
}
|
|
428
|
+
if (chromeFound) {
|
|
429
|
+
console.log(`[ok] Chrome browser found (for agent MCP DevTools)`);
|
|
430
|
+
} else {
|
|
431
|
+
console.log(`[info] Chrome browser not found (optional, for agent MCP)`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Summary
|
|
435
|
+
console.log(`\n${issues === 0 ? 'All checks passed!' : `${issues} issue(s) found.`}\n`);
|
|
436
|
+
process.exit(issues > 0 ? 1 : 0);
|
|
437
|
+
}
|
package/cron-sync.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder for the cron sync agent.
|
|
3
|
+
*
|
|
4
|
+
* This repo currently ships the Kanban UI + API, but does not include the
|
|
5
|
+
* Gateway cron integration client. If you intend to run a sync worker,
|
|
6
|
+
* implement your cron tooling here (or add a local ./lib/cron-tool-shim.mjs).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Intentionally empty.
|