@polderlabs/openkan 0.4.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/CHANGELOG.md +226 -0
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/agents/openkan.md +254 -0
- package/bin/install-agent.mjs +63 -0
- package/bin/ok.mjs +17 -0
- package/bin/openkan.mjs +10 -0
- package/dist/.claude/skills/ok-planning/SKILL.md +285 -0
- package/dist/.claude/skills/ok-planning/references/integration.md +153 -0
- package/dist/.claude/skills/ok-planning/references/schemas.md +270 -0
- package/dist/.claude/skills/ok-planning/references/workflows.md +185 -0
- package/dist/.claude/skills/ok-planning/scripts/ok-init.sh +14 -0
- package/dist/.claude/skills/ok-planning/scripts/ok-resume.sh +38 -0
- package/dist/.claude/skills/ok-planning/scripts/ok-status.sh +24 -0
- package/dist/agents/openkan.md +254 -0
- package/dist/bin/install-agent.mjs +76 -0
- package/dist/bin/ok-install.js +58 -0
- package/dist/bin/ok.js +138 -0
- package/dist/bin/openkan.js +804 -0
- package/dist/commands/organize.md +15 -0
- package/dist/kanban/agent-profile.js +8 -0
- package/dist/kanban/archive.js +49 -0
- package/dist/kanban/bizar.js +242 -0
- package/dist/kanban/board.js +367 -0
- package/dist/kanban/bulk.js +139 -0
- package/dist/kanban/changelog.js +186 -0
- package/dist/kanban/chat.js +1280 -0
- package/dist/kanban/claude-state.js +974 -0
- package/dist/kanban/comments.js +80 -0
- package/dist/kanban/docs.js +144 -0
- package/dist/kanban/fs.js +163 -0
- package/dist/kanban/git.js +196 -0
- package/dist/kanban/images.js +140 -0
- package/dist/kanban/import.js +295 -0
- package/dist/kanban/inputs.js +94 -0
- package/dist/kanban/insights.js +140 -0
- package/dist/kanban/io.js +75 -0
- package/dist/kanban/mdx-render.js +348 -0
- package/dist/kanban/mdx.js +231 -0
- package/dist/kanban/projects.js +545 -0
- package/dist/kanban/search.js +121 -0
- package/dist/kanban/server.js +3296 -0
- package/dist/kanban/tags.js +124 -0
- package/dist/kanban/template.js +145 -0
- package/dist/kanban/tsx-sandbox.js +187 -0
- package/dist/kanban/watcher.js +270 -0
- package/dist/ok/commands/goal.js +65 -0
- package/dist/ok/commands/index.js +87 -0
- package/dist/ok/commands/init.js +15 -0
- package/dist/ok/commands/plan.js +155 -0
- package/dist/ok/commands/prd.js +202 -0
- package/dist/ok/commands/progress.js +31 -0
- package/dist/ok/commands/task.js +377 -0
- package/dist/ok/ids.js +98 -0
- package/dist/ok/lock.js +156 -0
- package/dist/ok/migrate.js +197 -0
- package/dist/ok/schemas.js +402 -0
- package/dist/ok/storage.js +222 -0
- package/dist/skills/openkan/SKILL.md +111 -0
- package/dist/skills/openkan/agents/openai.yaml +4 -0
- package/dist/skills/openkan/examples/simple-task.mdx +34 -0
- package/dist/skills/openkan/examples/with-ask.mdx +32 -0
- package/dist/skills/openkan/examples/with-choice.mdx +51 -0
- package/dist/skills/openkan/examples/with-preview.mdx +54 -0
- package/dist/skills/openkan/references/api.md +169 -0
- package/dist/skills/openkan/templates/task.mdx +46 -0
- package/dist/web/api.js +257 -0
- package/dist/web/app.js +4251 -0
- package/dist/web/bizar.js +39 -0
- package/dist/web/brand/agent-activity-sprite.svg +1 -0
- package/dist/web/brand/banner-docs.svg +24 -0
- package/dist/web/brand/banner.svg +32 -0
- package/dist/web/brand/empty-sessions.svg +17 -0
- package/dist/web/brand/empty-tasks.svg +17 -0
- package/dist/web/brand/favicon.svg +9 -0
- package/dist/web/brand/infinity-loader-animated.svg +220 -0
- package/dist/web/brand/infinity-loader-spritesheet.svg +230 -0
- package/dist/web/brand/logo-wordmark.svg +10 -0
- package/dist/web/brand/logo.svg +9 -0
- package/dist/web/brand/pixel-infinity-track.svg +1 -0
- package/dist/web/brand/social-card.svg +26 -0
- package/dist/web/changelog-view.js +456 -0
- package/dist/web/charts.js +269 -0
- package/dist/web/chat-sidebar.js +2397 -0
- package/dist/web/chat-status-motion.js +154 -0
- package/dist/web/claude-pane.js +820 -0
- package/dist/web/command-palette.js +381 -0
- package/dist/web/contributors-view.js +317 -0
- package/dist/web/cross-tab.js +102 -0
- package/dist/web/docs-view.js +168 -0
- package/dist/web/experience.css +165 -0
- package/dist/web/goals-view.js +45 -0
- package/dist/web/home-view.js +113 -0
- package/dist/web/images.js +311 -0
- package/dist/web/index.html +485 -0
- package/dist/web/insights.js +217 -0
- package/dist/web/keyboard.js +446 -0
- package/dist/web/mdx-viewer.js +600 -0
- package/dist/web/path-picker.js +787 -0
- package/dist/web/preview-frame.html +187 -0
- package/dist/web/settings.js +582 -0
- package/dist/web/style.css +8545 -0
- package/dist/web/task-view.js +1759 -0
- package/dist/web/vendor/gsap.min.js +11 -0
- package/dist/web/workspace.css +1513 -0
- package/package.json +71 -0
- package/skills/openkan/SKILL.md +111 -0
- package/skills/openkan/agents/openai.yaml +4 -0
- package/skills/openkan/examples/simple-task.mdx +34 -0
- package/skills/openkan/examples/with-ask.mdx +32 -0
- package/skills/openkan/examples/with-choice.mdx +51 -0
- package/skills/openkan/examples/with-preview.mdx +54 -0
- package/skills/openkan/references/api.md +169 -0
- package/skills/openkan/templates/task.mdx +46 -0
package/dist/web/api.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// OpenKan — shared fetch wrapper + SSE pub/sub (M13 updates).
|
|
2
|
+
// Loaded by index.html before app.js, task-view.js, mdx-viewer.js.
|
|
3
|
+
// Exposes window.OpenKanAPI = { api, connectSSE, on, off, status }.
|
|
4
|
+
//
|
|
5
|
+
// M13 changes:
|
|
6
|
+
// - Reconnect on error uses exponential backoff (1s, 2s, 4s, 8s … capped
|
|
7
|
+
// at 30s) instead of relying on the EventSource's own backoff and falling
|
|
8
|
+
// back to polling.
|
|
9
|
+
// - Each named event is also re-broadcast on the OpenKanCrossTab channel
|
|
10
|
+
// so tabs that lost their SSE connection catch up via siblings.
|
|
11
|
+
// - On reconnect, /api/board is re-fetched and the resulting snapshot is
|
|
12
|
+
// emitted locally and broadcast to siblings — guarantees any tab that
|
|
13
|
+
// comes back online gets full state.
|
|
14
|
+
// - Added "task.changed" to the known-event list (M14 disk watcher).
|
|
15
|
+
|
|
16
|
+
(() => {
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
const POLL_MS = 5000;
|
|
20
|
+
const SSE_PATH = "/api/events";
|
|
21
|
+
const RECONNECT_BASE_MS = 1000;
|
|
22
|
+
const RECONNECT_CAP_MS = 30000;
|
|
23
|
+
const RECONNECT_GIVE_UP_MS = 60000; // after this long with no usable events, fall back to polling
|
|
24
|
+
|
|
25
|
+
// ─── fetch wrapper ─────────────────────────────────────────────────────────
|
|
26
|
+
// Match the original app.js signature so existing callers don't change.
|
|
27
|
+
// Returns parsed JSON when content-type is JSON, otherwise the raw text.
|
|
28
|
+
async function api(method, path, body) {
|
|
29
|
+
const opts = { method, headers: { "Content-Type": "application/json" } };
|
|
30
|
+
if (body !== undefined) opts.body = JSON.stringify(body);
|
|
31
|
+
const res = await fetch(path, opts);
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const t = await res.text().catch(() => "");
|
|
34
|
+
throw new Error(`${method} ${path} -> ${res.status} ${t}`);
|
|
35
|
+
}
|
|
36
|
+
const ct = res.headers.get("content-type") || "";
|
|
37
|
+
return ct.includes("application/json") ? res.json() : res.text();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── SSE pub/sub ────────────────────────────────────────────────────────────
|
|
41
|
+
// One singleton EventSource per page. Multiple consumers subscribe via on()/off().
|
|
42
|
+
// Auto-reconnects on error with exponential backoff capped at 30s. Each
|
|
43
|
+
// named event is also pushed onto window.OpenKanCrossTab so siblings stay
|
|
44
|
+
// in sync even when SSE is unreliable for one of them.
|
|
45
|
+
|
|
46
|
+
/** @type {EventSource|null} */
|
|
47
|
+
let es = null;
|
|
48
|
+
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
49
|
+
let reconnectTimer = null;
|
|
50
|
+
/** @type {ReturnType<typeof setInterval>|null} */
|
|
51
|
+
let pollTimer = null;
|
|
52
|
+
/** @type {Map<string, Set<Function>>} */
|
|
53
|
+
const handlers = new Map();
|
|
54
|
+
let connected = false;
|
|
55
|
+
/** Delay before the next reconnect attempt. Reset on a successful onopen. */
|
|
56
|
+
let reconnectDelay = RECONNECT_BASE_MS;
|
|
57
|
+
/** Wall-clock timestamp of the last successful SSE event (ms). */
|
|
58
|
+
let lastEventAt = 0;
|
|
59
|
+
const statusListeners = new Set();
|
|
60
|
+
|
|
61
|
+
// Best-effort cross-tab publish. The wrapper is loaded after api.js in some
|
|
62
|
+
// bundle orders, so defer the lookup to call time, not module init.
|
|
63
|
+
function xtPublish(event, payload) {
|
|
64
|
+
try {
|
|
65
|
+
const xt = window.OpenKanCrossTab;
|
|
66
|
+
if (xt?.publish) xt.publish(event, payload);
|
|
67
|
+
} catch { /* no-op */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function emit(event, data) {
|
|
71
|
+
const set = handlers.get(event);
|
|
72
|
+
if (!set) return;
|
|
73
|
+
for (const fn of set) {
|
|
74
|
+
try { fn(data); } catch (e) { console.error(`SSE handler for ${event} threw:`, e); }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Mirror a server SSE event into the cross-tab channel. Local subscribers
|
|
79
|
+
// already get the event via emit() so they run once. Other tabs receive
|
|
80
|
+
// the same payload via OpenKanCrossTab and call emit() locally too —
|
|
81
|
+
// handlers are idempotent (Map upsert + renderBoard), so a brief double-
|
|
82
|
+
// delivery at sync time is harmless and worth the simplicity.
|
|
83
|
+
function emitAndMirror(event, data) {
|
|
84
|
+
emit(event, data);
|
|
85
|
+
xtPublish(event, data);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function setConnected(v) {
|
|
89
|
+
if (connected === v) return;
|
|
90
|
+
connected = v;
|
|
91
|
+
for (const fn of statusListeners) {
|
|
92
|
+
try { fn(v); } catch {}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function startPoll() {
|
|
97
|
+
if (pollTimer) return;
|
|
98
|
+
pollTimer = setInterval(refresh, POLL_MS);
|
|
99
|
+
refresh();
|
|
100
|
+
}
|
|
101
|
+
function stopPoll() {
|
|
102
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
103
|
+
}
|
|
104
|
+
async function refresh() {
|
|
105
|
+
try {
|
|
106
|
+
const snap = await api("GET", "/api/board");
|
|
107
|
+
emitAndMirror("board.snapshot", snap);
|
|
108
|
+
setConnected(true);
|
|
109
|
+
} catch {
|
|
110
|
+
setConnected(false);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function scheduleReconnect() {
|
|
115
|
+
if (reconnectTimer) return;
|
|
116
|
+
reconnectTimer = setTimeout(() => {
|
|
117
|
+
reconnectTimer = null;
|
|
118
|
+
connectSSE();
|
|
119
|
+
}, reconnectDelay);
|
|
120
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, … capped at 30s.
|
|
121
|
+
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_CAP_MS);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function refreshBoardAfterReconnect() {
|
|
125
|
+
try {
|
|
126
|
+
const snap = await api("GET", "/api/board");
|
|
127
|
+
emitAndMirror("board.snapshot", snap);
|
|
128
|
+
return true;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function connectSSE() {
|
|
135
|
+
if (es) {
|
|
136
|
+
try { es.close(); } catch {}
|
|
137
|
+
es = null;
|
|
138
|
+
}
|
|
139
|
+
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
es = new EventSource(SSE_PATH);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
// EventSource constructor can throw on bad URL etc. — fall back to poll.
|
|
145
|
+
startPoll();
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
es.onopen = () => {
|
|
150
|
+
setConnected(true);
|
|
151
|
+
reconnectDelay = RECONNECT_BASE_MS;
|
|
152
|
+
lastEventAt = Date.now();
|
|
153
|
+
stopPoll();
|
|
154
|
+
// SSE re-opened — pull a fresh board snapshot so the UI reflects
|
|
155
|
+
// anything that happened while we were disconnected, and let sibling
|
|
156
|
+
// tabs catch up too.
|
|
157
|
+
refreshBoardAfterReconnect();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
es.onerror = () => {
|
|
161
|
+
setConnected(false);
|
|
162
|
+
// The EventSource will try to reopen internally; we don't trust its
|
|
163
|
+
// timing so we tear down and schedule our own reconnect with
|
|
164
|
+
// exponential backoff. After RECONNECT_GIVE_UP_MS of no progress, fall
|
|
165
|
+
// back to polling instead of hammering the server.
|
|
166
|
+
try { es?.close(); } catch {}
|
|
167
|
+
es = null;
|
|
168
|
+
scheduleReconnect();
|
|
169
|
+
// If we haven't seen a single event in the give-up window, polling
|
|
170
|
+
// is more reliable than a tight SSE reconnect loop.
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
if (!connected && !reconnectTimer && Date.now() - lastEventAt > RECONNECT_GIVE_UP_MS) {
|
|
173
|
+
startPoll();
|
|
174
|
+
}
|
|
175
|
+
}, RECONNECT_GIVE_UP_MS + 100);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Generic event fan-out: any `onmessage` or named event fires `emit(event, data)`.
|
|
179
|
+
// The known list also gets mirrored to OpenKanCrossTab. Catch-all "message"
|
|
180
|
+
// events (no name) are emitted locally only — they're server-internal pings.
|
|
181
|
+
const KNOWN = [
|
|
182
|
+
"board.snapshot",
|
|
183
|
+
"task.created", "task.updated", "task.deleted",
|
|
184
|
+
"task.archived", "task.restored",
|
|
185
|
+
"task.comment.added", "task.comment.resolved", "task.comment.deleted",
|
|
186
|
+
"task.input.asked", "task.input.responded",
|
|
187
|
+
"task.changed",
|
|
188
|
+
"bulk.updated",
|
|
189
|
+
"theme.changed",
|
|
190
|
+
"filter.changed",
|
|
191
|
+
"session.ended",
|
|
192
|
+
"server.connected",
|
|
193
|
+
];
|
|
194
|
+
for (const name of KNOWN) {
|
|
195
|
+
es.addEventListener(name, (e) => {
|
|
196
|
+
let parsed = null;
|
|
197
|
+
try { parsed = JSON.parse(e.data); } catch { parsed = e.data; }
|
|
198
|
+
lastEventAt = Date.now();
|
|
199
|
+
emitAndMirror(name, parsed);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Unnamed `message` events.
|
|
203
|
+
es.addEventListener("message", (e) => {
|
|
204
|
+
lastEventAt = Date.now();
|
|
205
|
+
let parsed = null;
|
|
206
|
+
try { parsed = JSON.parse(e.data); } catch { parsed = e.data; }
|
|
207
|
+
emit("message", parsed);
|
|
208
|
+
});
|
|
209
|
+
return es;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function on(event, handler) {
|
|
213
|
+
let set = handlers.get(event);
|
|
214
|
+
if (!set) { set = new Set(); handlers.set(event, set); }
|
|
215
|
+
set.add(handler);
|
|
216
|
+
// Ensure SSE is running once a consumer attaches.
|
|
217
|
+
if (event !== "*") connectSSE();
|
|
218
|
+
return () => off(event, handler);
|
|
219
|
+
}
|
|
220
|
+
function off(event, handler) {
|
|
221
|
+
const set = handlers.get(event);
|
|
222
|
+
if (!set) return;
|
|
223
|
+
set.delete(handler);
|
|
224
|
+
if (set.size === 0) handlers.delete(event);
|
|
225
|
+
}
|
|
226
|
+
function onStatus(fn) {
|
|
227
|
+
statusListeners.add(fn);
|
|
228
|
+
fn(connected);
|
|
229
|
+
return () => statusListeners.delete(fn);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Auto-start on load.
|
|
233
|
+
connectSSE();
|
|
234
|
+
|
|
235
|
+
// Cross-tab subscriber → local emit. Same idempotency reasoning as above:
|
|
236
|
+
// a local tab may see its own SSE event mirrored back; both paths call the
|
|
237
|
+
// same handlers and Map/renderBoard are idempotent.
|
|
238
|
+
if (window.OpenKanCrossTab && window.OpenKanCrossTab.subscribe) {
|
|
239
|
+
const FORWARDED = [
|
|
240
|
+
"task.created", "task.updated", "task.deleted",
|
|
241
|
+
"task.archived", "task.restored",
|
|
242
|
+
"task.comment.added", "task.comment.resolved", "task.comment.deleted",
|
|
243
|
+
"task.input.asked", "task.input.responded",
|
|
244
|
+
"task.changed",
|
|
245
|
+
"bulk.updated",
|
|
246
|
+
"theme.changed",
|
|
247
|
+
"filter.changed",
|
|
248
|
+
"session.ended",
|
|
249
|
+
"board.snapshot",
|
|
250
|
+
];
|
|
251
|
+
for (const evt of FORWARDED) {
|
|
252
|
+
window.OpenKanCrossTab.subscribe(evt, (payload) => emit(evt, payload));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
window.OpenKanAPI = { api, connectSSE, on, off, onStatus, refresh, get connected() { return connected; } };
|
|
257
|
+
})();
|