pi-kanban 0.2.0 → 0.3.1
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 +51 -46
- package/extensions/kanban.ts +465 -465
- package/lib/git-branch.js +112 -112
- package/lib/pi-parsers.js +1000 -901
- package/lib/task-store.js +152 -136
- package/package.json +66 -59
- package/public/app.js +5859 -5750
- package/public/icons/icon-svg.svg +6 -6
- package/public/index.html +674 -674
- package/public/manifest.json +26 -26
- package/public/style.css +3760 -3616
- package/public/sw.js +60 -60
- package/server.js +602 -491
- package/themes/kanban-dark-default.json +21 -21
- package/themes/kanban-default.json +21 -21
- package/themes/pi-dark.json +21 -21
- package/themes/pi-light.json +21 -21
package/lib/task-store.js
CHANGED
|
@@ -1,136 +1,152 @@
|
|
|
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
|
|
28
|
-
try { return fs.
|
|
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
|
|
49
|
-
const dir = sessionDir(sessionId);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function
|
|
101
|
-
if (!sessionId
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
async function listSessionIdsAsync() {
|
|
28
|
+
try { return await fs.promises.readdir(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
|
+
async function listTasksAsync(sessionId) {
|
|
49
|
+
const dir = sessionDir(sessionId);
|
|
50
|
+
let entries;
|
|
51
|
+
try { entries = await fs.promises.readdir(dir); }
|
|
52
|
+
catch { return []; }
|
|
53
|
+
const jsons = entries.filter((n) => n.endsWith('.json'));
|
|
54
|
+
const tasks = await Promise.all(jsons.map(async (name) => {
|
|
55
|
+
try {
|
|
56
|
+
const raw = await fs.promises.readFile(path.join(dir, name), 'utf8');
|
|
57
|
+
return JSON.parse(raw);
|
|
58
|
+
} catch { return null; }
|
|
59
|
+
}));
|
|
60
|
+
return tasks.filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function writeTask(sessionId, task) {
|
|
64
|
+
const dir = sessionDir(sessionId);
|
|
65
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
66
|
+
const final = path.join(dir, `${task.id}.json`);
|
|
67
|
+
const tmp = `${final}.tmp`;
|
|
68
|
+
fs.writeFileSync(tmp, JSON.stringify(task, null, 2));
|
|
69
|
+
fs.renameSync(tmp, final);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function deleteTask(sessionId, taskId) {
|
|
73
|
+
try { fs.unlinkSync(taskPath(sessionId, taskId)); return true; }
|
|
74
|
+
catch (e) {
|
|
75
|
+
if (e.code === 'ENOENT') return false;
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeTask(snapTask, existing) {
|
|
81
|
+
const e = existing || {};
|
|
82
|
+
return {
|
|
83
|
+
id: String(snapTask.id),
|
|
84
|
+
subject: snapTask.subject ?? snapTask.text ?? '',
|
|
85
|
+
description: snapTask.description ?? null,
|
|
86
|
+
activeForm: snapTask.activeForm ?? e.activeForm ?? null,
|
|
87
|
+
status: snapTask.status ?? 'pending',
|
|
88
|
+
blocks: Array.isArray(e.blocks) ? e.blocks : [],
|
|
89
|
+
blockedBy: Array.isArray(e.blockedBy) ? e.blockedBy : [],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function tasksEqual(a, b) {
|
|
94
|
+
return a.subject === b.subject
|
|
95
|
+
&& a.description === b.description
|
|
96
|
+
&& a.activeForm === b.activeForm
|
|
97
|
+
&& a.status === b.status;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function cleanupIfAllCompleted(sessionId) {
|
|
101
|
+
if (!sessionId) return false;
|
|
102
|
+
const tasks = listTasks(sessionId);
|
|
103
|
+
if (!tasks.length) return false;
|
|
104
|
+
const live = tasks.filter((t) => t.status !== 'deleted');
|
|
105
|
+
if (!live.length) return false;
|
|
106
|
+
if (!live.every((t) => t.status === 'completed')) return false;
|
|
107
|
+
const dir = sessionDir(sessionId);
|
|
108
|
+
for (const t of tasks) {
|
|
109
|
+
try { fs.unlinkSync(path.join(dir, `${t.id}.json`)); } catch {}
|
|
110
|
+
}
|
|
111
|
+
try { fs.rmdirSync(dir); } catch {}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function reconcileFromSnapshot(sessionId, snapshot) {
|
|
116
|
+
if (!sessionId || !Array.isArray(snapshot)) return;
|
|
117
|
+
const dir = sessionDir(sessionId);
|
|
118
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
119
|
+
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
for (const t of snapshot) {
|
|
122
|
+
if (t == null || t.id === undefined || t.id === null) continue;
|
|
123
|
+
const id = String(t.id);
|
|
124
|
+
seen.add(id);
|
|
125
|
+
const existing = readTask(sessionId, id);
|
|
126
|
+
const merged = normalizeTask(t, existing);
|
|
127
|
+
if (existing && tasksEqual(existing, merged)) continue;
|
|
128
|
+
writeTask(sessionId, merged);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let entries;
|
|
132
|
+
try { entries = fs.readdirSync(dir); } catch { return; }
|
|
133
|
+
for (const name of entries) {
|
|
134
|
+
if (!name.endsWith('.json')) continue;
|
|
135
|
+
const id = name.slice(0, -5);
|
|
136
|
+
if (seen.has(id)) continue;
|
|
137
|
+
try { fs.unlinkSync(path.join(dir, name)); } catch {}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
getTasksDir,
|
|
143
|
+
sessionDir,
|
|
144
|
+
listSessionIdsAsync,
|
|
145
|
+
listTasks,
|
|
146
|
+
listTasksAsync,
|
|
147
|
+
readTask,
|
|
148
|
+
writeTask,
|
|
149
|
+
deleteTask,
|
|
150
|
+
reconcileFromSnapshot,
|
|
151
|
+
cleanupIfAllCompleted,
|
|
152
|
+
};
|
package/package.json
CHANGED
|
@@ -1,59 +1,66 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-kanban",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Workspace for the pi coding agent — sessions, todos, subagents, and more.",
|
|
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
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
},
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-kanban",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Workspace for the pi coding agent — sessions, todos, subagents, and more.",
|
|
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
|
+
"docs:dev": "vitepress dev docs",
|
|
15
|
+
"docs:build": "vitepress build docs",
|
|
16
|
+
"docs:preview": "vitepress preview docs",
|
|
17
|
+
"release:local": "npm version \"$(git describe --tags --exact-match)\" --no-git-tag-version --allow-same-version && npm publish --access public && git checkout -- package.json package-lock.json"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"vitepress": "^1.6.4"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi-package",
|
|
24
|
+
"kanban",
|
|
25
|
+
"dashboard",
|
|
26
|
+
"observability",
|
|
27
|
+
"sessions",
|
|
28
|
+
"subagents"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"server.js",
|
|
32
|
+
"lib/",
|
|
33
|
+
"public/",
|
|
34
|
+
"extensions/",
|
|
35
|
+
"themes/",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"author": "nikiforovall",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/NikiforovAll/pi-kanban.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/NikiforovAll/pi-kanban/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/NikiforovAll/pi-kanban#readme",
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"chokidar": "^3.5.3",
|
|
50
|
+
"express": "^4.18.2",
|
|
51
|
+
"open": "^11.0.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@mariozechner/pi-coding-agent": "*"
|
|
55
|
+
},
|
|
56
|
+
"pi": {
|
|
57
|
+
"extensions": [
|
|
58
|
+
"./extensions"
|
|
59
|
+
],
|
|
60
|
+
"video": "https://raw.githubusercontent.com/NikiforovAll/pi-kanban/main/assets/pi-kanban-demo.mp4",
|
|
61
|
+
"image": "https://raw.githubusercontent.com/NikiforovAll/pi-kanban/main/assets/pi-kanban-poster.png"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=20.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|