@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/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.