pi-kanban 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const os = require('node:os');
4
+
5
+ const PI_DIR = process.env.PI_DIR || path.join(os.homedir(), '.pi');
6
+ const TASKS_DIR = path.join(PI_DIR, 'agent', 'kanban', 'tasks');
7
+
8
+ function getTasksDir() {
9
+ return TASKS_DIR;
10
+ }
11
+
12
+ function sessionDir(sessionId) {
13
+ return path.join(TASKS_DIR, sessionId);
14
+ }
15
+
16
+ function taskPath(sessionId, taskId) {
17
+ return path.join(sessionDir(sessionId), `${taskId}.json`);
18
+ }
19
+
20
+ function readTask(sessionId, taskId) {
21
+ try {
22
+ const raw = fs.readFileSync(taskPath(sessionId, taskId), 'utf8');
23
+ return JSON.parse(raw);
24
+ } catch { return null; }
25
+ }
26
+
27
+ function listSessionIds() {
28
+ try { return fs.readdirSync(TASKS_DIR); }
29
+ catch { return []; }
30
+ }
31
+
32
+ function listTasks(sessionId) {
33
+ const dir = sessionDir(sessionId);
34
+ let entries;
35
+ try { entries = fs.readdirSync(dir); }
36
+ catch { return []; }
37
+ const out = [];
38
+ for (const name of entries) {
39
+ if (!name.endsWith('.json')) continue;
40
+ try {
41
+ const raw = fs.readFileSync(path.join(dir, name), 'utf8');
42
+ out.push(JSON.parse(raw));
43
+ } catch {}
44
+ }
45
+ return out;
46
+ }
47
+
48
+ function writeTask(sessionId, task) {
49
+ const dir = sessionDir(sessionId);
50
+ fs.mkdirSync(dir, { recursive: true });
51
+ const final = path.join(dir, `${task.id}.json`);
52
+ const tmp = `${final}.tmp`;
53
+ fs.writeFileSync(tmp, JSON.stringify(task, null, 2));
54
+ fs.renameSync(tmp, final);
55
+ }
56
+
57
+ function deleteTask(sessionId, taskId) {
58
+ try { fs.unlinkSync(taskPath(sessionId, taskId)); return true; }
59
+ catch (e) {
60
+ if (e.code === 'ENOENT') return false;
61
+ throw e;
62
+ }
63
+ }
64
+
65
+ function normalizeTask(snapTask, existing) {
66
+ const e = existing || {};
67
+ return {
68
+ id: String(snapTask.id),
69
+ subject: snapTask.subject ?? snapTask.text ?? '',
70
+ description: snapTask.description ?? null,
71
+ activeForm: snapTask.activeForm ?? e.activeForm ?? null,
72
+ status: snapTask.status ?? 'pending',
73
+ blocks: Array.isArray(e.blocks) ? e.blocks : [],
74
+ blockedBy: Array.isArray(e.blockedBy) ? e.blockedBy : [],
75
+ };
76
+ }
77
+
78
+ function tasksEqual(a, b) {
79
+ return a.subject === b.subject
80
+ && a.description === b.description
81
+ && a.activeForm === b.activeForm
82
+ && a.status === b.status;
83
+ }
84
+
85
+ function cleanupIfAllCompleted(sessionId) {
86
+ if (!sessionId) return false;
87
+ const tasks = listTasks(sessionId);
88
+ if (!tasks.length) return false;
89
+ if (!tasks.every((t) => t.status === 'completed')) return false;
90
+ const dir = sessionDir(sessionId);
91
+ for (const t of tasks) {
92
+ try { fs.unlinkSync(path.join(dir, `${t.id}.json`)); } catch {}
93
+ }
94
+ try { fs.rmdirSync(dir); } catch {}
95
+ return true;
96
+ }
97
+
98
+ function reconcileFromSnapshot(sessionId, snapshot) {
99
+ if (!sessionId || !Array.isArray(snapshot)) return;
100
+ const dir = sessionDir(sessionId);
101
+ fs.mkdirSync(dir, { recursive: true });
102
+
103
+ const seen = new Set();
104
+ for (const t of snapshot) {
105
+ if (t == null || t.id === undefined || t.id === null) continue;
106
+ const id = String(t.id);
107
+ seen.add(id);
108
+ const existing = readTask(sessionId, id);
109
+ const merged = normalizeTask(t, existing);
110
+ if (existing && tasksEqual(existing, merged)) continue;
111
+ writeTask(sessionId, merged);
112
+ }
113
+
114
+ let entries;
115
+ try { entries = fs.readdirSync(dir); } catch { return; }
116
+ for (const name of entries) {
117
+ if (!name.endsWith('.json')) continue;
118
+ const id = name.slice(0, -5);
119
+ if (seen.has(id)) continue;
120
+ try { fs.unlinkSync(path.join(dir, name)); } catch {}
121
+ }
122
+ }
123
+
124
+ module.exports = {
125
+ getTasksDir,
126
+ sessionDir,
127
+ listSessionIds,
128
+ listTasks,
129
+ readTask,
130
+ writeTask,
131
+ deleteTask,
132
+ reconcileFromSnapshot,
133
+ cleanupIfAllCompleted,
134
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "pi-kanban",
3
+ "version": "0.1.0",
4
+ "description": "Web dashboard for the pi coding agent (sessions, todos, subagents)",
5
+ "main": "server.js",
6
+ "bin": {
7
+ "pi-kanban": "./server.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node server.js",
11
+ "dev": "node --watch server.js",
12
+ "build:demo": "node scripts/build-demo.js",
13
+ "preview:demo": "npm run build:demo && npx --yes http-server dist -p 8765 -c-1"
14
+ },
15
+ "keywords": [
16
+ "pi-package",
17
+ "kanban",
18
+ "dashboard",
19
+ "observability",
20
+ "sessions",
21
+ "subagents"
22
+ ],
23
+ "files": [
24
+ "server.js",
25
+ "lib/",
26
+ "public/",
27
+ "extensions/",
28
+ "themes/",
29
+ "README.md"
30
+ ],
31
+ "license": "MIT",
32
+ "author": "nikiforovall",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/NikiforovAll/pi-kanban.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/NikiforovAll/pi-kanban/issues"
39
+ },
40
+ "homepage": "https://github.com/NikiforovAll/pi-kanban#readme",
41
+ "dependencies": {
42
+ "chokidar": "^3.5.3",
43
+ "express": "^4.18.2",
44
+ "open": "^11.0.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@mariozechner/pi-coding-agent": "*"
48
+ },
49
+ "pi": {
50
+ "extensions": ["./extensions"],
51
+ "video": "https://raw.githubusercontent.com/NikiforovAll/pi-kanban/main/assets/pi-kanban-demo.mp4",
52
+ "image": "https://raw.githubusercontent.com/NikiforovAll/pi-kanban/main/assets/pi-kanban-poster.png"
53
+ },
54
+ "engines": {
55
+ "node": ">=20.0.0"
56
+ }
57
+ }