@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.
Files changed (114) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/LICENSE +21 -0
  3. package/README.md +318 -0
  4. package/agents/openkan.md +254 -0
  5. package/bin/install-agent.mjs +63 -0
  6. package/bin/ok.mjs +17 -0
  7. package/bin/openkan.mjs +10 -0
  8. package/dist/.claude/skills/ok-planning/SKILL.md +285 -0
  9. package/dist/.claude/skills/ok-planning/references/integration.md +153 -0
  10. package/dist/.claude/skills/ok-planning/references/schemas.md +270 -0
  11. package/dist/.claude/skills/ok-planning/references/workflows.md +185 -0
  12. package/dist/.claude/skills/ok-planning/scripts/ok-init.sh +14 -0
  13. package/dist/.claude/skills/ok-planning/scripts/ok-resume.sh +38 -0
  14. package/dist/.claude/skills/ok-planning/scripts/ok-status.sh +24 -0
  15. package/dist/agents/openkan.md +254 -0
  16. package/dist/bin/install-agent.mjs +76 -0
  17. package/dist/bin/ok-install.js +58 -0
  18. package/dist/bin/ok.js +138 -0
  19. package/dist/bin/openkan.js +804 -0
  20. package/dist/commands/organize.md +15 -0
  21. package/dist/kanban/agent-profile.js +8 -0
  22. package/dist/kanban/archive.js +49 -0
  23. package/dist/kanban/bizar.js +242 -0
  24. package/dist/kanban/board.js +367 -0
  25. package/dist/kanban/bulk.js +139 -0
  26. package/dist/kanban/changelog.js +186 -0
  27. package/dist/kanban/chat.js +1280 -0
  28. package/dist/kanban/claude-state.js +974 -0
  29. package/dist/kanban/comments.js +80 -0
  30. package/dist/kanban/docs.js +144 -0
  31. package/dist/kanban/fs.js +163 -0
  32. package/dist/kanban/git.js +196 -0
  33. package/dist/kanban/images.js +140 -0
  34. package/dist/kanban/import.js +295 -0
  35. package/dist/kanban/inputs.js +94 -0
  36. package/dist/kanban/insights.js +140 -0
  37. package/dist/kanban/io.js +75 -0
  38. package/dist/kanban/mdx-render.js +348 -0
  39. package/dist/kanban/mdx.js +231 -0
  40. package/dist/kanban/projects.js +545 -0
  41. package/dist/kanban/search.js +121 -0
  42. package/dist/kanban/server.js +3296 -0
  43. package/dist/kanban/tags.js +124 -0
  44. package/dist/kanban/template.js +145 -0
  45. package/dist/kanban/tsx-sandbox.js +187 -0
  46. package/dist/kanban/watcher.js +270 -0
  47. package/dist/ok/commands/goal.js +65 -0
  48. package/dist/ok/commands/index.js +87 -0
  49. package/dist/ok/commands/init.js +15 -0
  50. package/dist/ok/commands/plan.js +155 -0
  51. package/dist/ok/commands/prd.js +202 -0
  52. package/dist/ok/commands/progress.js +31 -0
  53. package/dist/ok/commands/task.js +377 -0
  54. package/dist/ok/ids.js +98 -0
  55. package/dist/ok/lock.js +156 -0
  56. package/dist/ok/migrate.js +197 -0
  57. package/dist/ok/schemas.js +402 -0
  58. package/dist/ok/storage.js +222 -0
  59. package/dist/skills/openkan/SKILL.md +111 -0
  60. package/dist/skills/openkan/agents/openai.yaml +4 -0
  61. package/dist/skills/openkan/examples/simple-task.mdx +34 -0
  62. package/dist/skills/openkan/examples/with-ask.mdx +32 -0
  63. package/dist/skills/openkan/examples/with-choice.mdx +51 -0
  64. package/dist/skills/openkan/examples/with-preview.mdx +54 -0
  65. package/dist/skills/openkan/references/api.md +169 -0
  66. package/dist/skills/openkan/templates/task.mdx +46 -0
  67. package/dist/web/api.js +257 -0
  68. package/dist/web/app.js +4251 -0
  69. package/dist/web/bizar.js +39 -0
  70. package/dist/web/brand/agent-activity-sprite.svg +1 -0
  71. package/dist/web/brand/banner-docs.svg +24 -0
  72. package/dist/web/brand/banner.svg +32 -0
  73. package/dist/web/brand/empty-sessions.svg +17 -0
  74. package/dist/web/brand/empty-tasks.svg +17 -0
  75. package/dist/web/brand/favicon.svg +9 -0
  76. package/dist/web/brand/infinity-loader-animated.svg +220 -0
  77. package/dist/web/brand/infinity-loader-spritesheet.svg +230 -0
  78. package/dist/web/brand/logo-wordmark.svg +10 -0
  79. package/dist/web/brand/logo.svg +9 -0
  80. package/dist/web/brand/pixel-infinity-track.svg +1 -0
  81. package/dist/web/brand/social-card.svg +26 -0
  82. package/dist/web/changelog-view.js +456 -0
  83. package/dist/web/charts.js +269 -0
  84. package/dist/web/chat-sidebar.js +2397 -0
  85. package/dist/web/chat-status-motion.js +154 -0
  86. package/dist/web/claude-pane.js +820 -0
  87. package/dist/web/command-palette.js +381 -0
  88. package/dist/web/contributors-view.js +317 -0
  89. package/dist/web/cross-tab.js +102 -0
  90. package/dist/web/docs-view.js +168 -0
  91. package/dist/web/experience.css +165 -0
  92. package/dist/web/goals-view.js +45 -0
  93. package/dist/web/home-view.js +113 -0
  94. package/dist/web/images.js +311 -0
  95. package/dist/web/index.html +485 -0
  96. package/dist/web/insights.js +217 -0
  97. package/dist/web/keyboard.js +446 -0
  98. package/dist/web/mdx-viewer.js +600 -0
  99. package/dist/web/path-picker.js +787 -0
  100. package/dist/web/preview-frame.html +187 -0
  101. package/dist/web/settings.js +582 -0
  102. package/dist/web/style.css +8545 -0
  103. package/dist/web/task-view.js +1759 -0
  104. package/dist/web/vendor/gsap.min.js +11 -0
  105. package/dist/web/workspace.css +1513 -0
  106. package/package.json +71 -0
  107. package/skills/openkan/SKILL.md +111 -0
  108. package/skills/openkan/agents/openai.yaml +4 -0
  109. package/skills/openkan/examples/simple-task.mdx +34 -0
  110. package/skills/openkan/examples/with-ask.mdx +32 -0
  111. package/skills/openkan/examples/with-choice.mdx +51 -0
  112. package/skills/openkan/examples/with-preview.mdx +54 -0
  113. package/skills/openkan/references/api.md +169 -0
  114. package/skills/openkan/templates/task.mdx +46 -0
@@ -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
+ })();