@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,311 @@
1
+ // OpenKan — image upload, paste, drop, list, delete, lightbox.
2
+ //
3
+ // Public API (window.OpenKanImages):
4
+ // list(taskId) -> Promise<ImageMeta[]>
5
+ // upload(taskId, file, opts?) -> Promise<ImageMeta>
6
+ // remove(taskId, name) -> Promise<void>
7
+ // srcFor(taskId, name) -> string (URL for the image)
8
+ // markdownFor(taskId, name) -> string (markdown reference)
9
+ // copyMarkdown(taskId, name) -> Promise<void>
10
+ //
11
+ // File handling (read as data URL via FileReader, POST JSON):
12
+ // POST /api/tasks/<id>/images
13
+ // { data: "<base64>", contentType: "image/png", filename: "shot.png" }
14
+ // GET /api/tasks/<id>/images -> { images: ImageMeta[] }
15
+ // DEL /api/tasks/<id>/images/<name> -> ok
16
+ // GET /api/tasks/<id>/images/<name> -> image file (used as <img src>)
17
+
18
+ (() => {
19
+ "use strict";
20
+
21
+ const { api } = window.OpenKanAPI;
22
+
23
+ const ALLOWED_TYPES = new Set([
24
+ "image/png", "image/jpeg", "image/jpg", "image/gif",
25
+ "image/webp", "image/svg+xml",
26
+ ]);
27
+ const MAX_BYTES = 10 * 1024 * 1024; // 10 MB
28
+
29
+ // ─── Helpers ───────────────────────────────────────────────────────────────
30
+
31
+ function readAsDataURL(file) {
32
+ return new Promise((resolve, reject) => {
33
+ const fr = new FileReader();
34
+ fr.onload = () => resolve(String(fr.result || ""));
35
+ fr.onerror = () => reject(fr.error || new Error("FileReader failed"));
36
+ fr.readAsDataURL(file);
37
+ });
38
+ }
39
+
40
+ function dataUrlToBase64(dataUrl) {
41
+ // "data:image/png;base64,iVBORw0..." -> "iVBORw0..."
42
+ const idx = dataUrl.indexOf(",");
43
+ return idx >= 0 ? dataUrl.slice(idx + 1) : dataUrl;
44
+ }
45
+
46
+ function inferContentType(file) {
47
+ if (file?.type && ALLOWED_TYPES.has(file.type)) return file.type;
48
+ // Fallback by extension.
49
+ const name = String(file?.name || "").toLowerCase();
50
+ if (name.endsWith(".png")) return "image/png";
51
+ if (name.endsWith(".jpg") || name.endsWith(".jpeg")) return "image/jpeg";
52
+ if (name.endsWith(".gif")) return "image/gif";
53
+ if (name.endsWith(".webp")) return "image/webp";
54
+ if (name.endsWith(".svg")) return "image/svg+xml";
55
+ return "application/octet-stream";
56
+ }
57
+
58
+ function inferExtension(file, contentType) {
59
+ const name = String(file?.name || "");
60
+ const dot = name.lastIndexOf(".");
61
+ if (dot > 0) return name.slice(dot + 1);
62
+ const ct = contentType || "";
63
+ if (ct.includes("png")) return "png";
64
+ if (ct.includes("jpeg") || ct.includes("jpg")) return "jpg";
65
+ if (ct.includes("gif")) return "gif";
66
+ if (ct.includes("webp")) return "webp";
67
+ if (ct.includes("svg")) return "svg";
68
+ return "png";
69
+ }
70
+
71
+ function ensureImageFile(file) {
72
+ const ct = inferContentType(file);
73
+ if (!ALLOWED_TYPES.has(ct)) {
74
+ throw new Error(`Unsupported image type: ${ct}`);
75
+ }
76
+ if (file.size > MAX_BYTES) {
77
+ throw new Error(`Image too large (${(file.size / 1024 / 1024).toFixed(1)} MB; max 10 MB)`);
78
+ }
79
+ return { contentType: ct, extension: inferExtension(file, ct) };
80
+ }
81
+
82
+ // ─── Public API ────────────────────────────────────────────────────────────
83
+
84
+ async function list(taskId) {
85
+ if (!taskId) return [];
86
+ try {
87
+ const data = await api("GET", `/api/tasks/${encodeURIComponent(taskId)}/images`);
88
+ if (Array.isArray(data?.images)) return data.images;
89
+ if (Array.isArray(data)) return data;
90
+ return [];
91
+ } catch (err) {
92
+ // 404 means the endpoint isn't mounted yet — treat as empty.
93
+ if (/->\s*404/.test(String(err?.message || ""))) return [];
94
+ throw err;
95
+ }
96
+ }
97
+
98
+ async function upload(taskId, file, opts = {}) {
99
+ if (!taskId) throw new Error("taskId is required");
100
+ if (!file) throw new Error("file is required");
101
+ const { contentType, extension } = ensureImageFile(file);
102
+ const dataUrl = await readAsDataURL(file);
103
+ const base64 = dataUrlToBase64(dataUrl);
104
+ const body = {
105
+ data: base64,
106
+ contentType,
107
+ filename: String(opts.filename || file.name || `image.${extension}`),
108
+ };
109
+ return api("POST", `/api/tasks/${encodeURIComponent(taskId)}/images`, body);
110
+ }
111
+
112
+ async function remove(taskId, name) {
113
+ if (!taskId || !name) return;
114
+ return api("DELETE", `/api/tasks/${encodeURIComponent(taskId)}/images/${encodeURIComponent(name)}`);
115
+ }
116
+
117
+ function srcFor(taskId, name) {
118
+ return `/api/tasks/${encodeURIComponent(taskId)}/images/${encodeURIComponent(name)}`;
119
+ }
120
+
121
+ function markdownFor(taskId, name) {
122
+ const alt = String(name || "image").replace(/[\[\]]/g, "_");
123
+ return `![${alt}](${srcFor(taskId, name)})`;
124
+ }
125
+
126
+ async function copyMarkdown(taskId, name) {
127
+ const text = markdownFor(taskId, name);
128
+ if (navigator.clipboard?.writeText) {
129
+ try {
130
+ await navigator.clipboard.writeText(text);
131
+ return;
132
+ } catch { /* fall through to legacy path */ }
133
+ }
134
+ // Legacy fallback (older browsers / non-secure contexts).
135
+ const ta = document.createElement("textarea");
136
+ ta.value = text;
137
+ ta.style.position = "fixed";
138
+ ta.style.opacity = "0";
139
+ document.body.append(ta);
140
+ ta.select();
141
+ try { document.execCommand("copy"); } catch { /* ignore */ }
142
+ ta.remove();
143
+ }
144
+
145
+ // ─── Lightbox ──────────────────────────────────────────────────────────────
146
+
147
+ function ensureLightbox() {
148
+ let lb = document.getElementById("image-lightbox");
149
+ if (lb) return lb;
150
+ lb = document.createElement("div");
151
+ lb.id = "image-lightbox";
152
+ lb.className = "image-lightbox";
153
+ lb.hidden = true;
154
+ lb.setAttribute("role", "dialog");
155
+ lb.setAttribute("aria-modal", "true");
156
+ lb.setAttribute("aria-label", "Image preview");
157
+ lb.tabIndex = -1;
158
+ lb.append(Object.assign(document.createElement("img"), { className: "image-lightbox-img", alt: "" }));
159
+ const closeBtn = document.createElement("button");
160
+ closeBtn.type = "button";
161
+ closeBtn.className = "image-lightbox-close";
162
+ closeBtn.setAttribute("aria-label", "Close");
163
+ closeBtn.textContent = "×";
164
+ closeBtn.addEventListener("click", closeLightbox);
165
+ lb.append(closeBtn);
166
+ document.body.append(lb);
167
+
168
+ // Click on backdrop (not the image or close button) closes.
169
+ lb.addEventListener("click", (e) => {
170
+ if (e.target === lb) closeLightbox();
171
+ });
172
+ return lb;
173
+ }
174
+
175
+ function openLightbox(src, alt) {
176
+ const lb = ensureLightbox();
177
+ const img = lb.querySelector("img.image-lightbox-img");
178
+ img.src = src;
179
+ img.alt = alt || "";
180
+ lb.hidden = false;
181
+ document.body.classList.add("image-lightbox-open");
182
+ // Focus the lightbox so Esc works without a click first.
183
+ try { lb.focus({ preventScroll: true }); } catch { lb.focus(); }
184
+ }
185
+
186
+ function closeLightbox() {
187
+ const lb = document.getElementById("image-lightbox");
188
+ if (!lb) return;
189
+ lb.hidden = true;
190
+ const img = lb.querySelector("img.image-lightbox-img");
191
+ if (img) img.removeAttribute("src");
192
+ document.body.classList.remove("image-lightbox-open");
193
+ }
194
+
195
+ // Esc closes the lightbox (capture phase so it fires before any local handlers).
196
+ document.addEventListener("keydown", (e) => {
197
+ if (e.key === "Escape") {
198
+ const lb = document.getElementById("image-lightbox");
199
+ if (lb && !lb.hidden) {
200
+ e.stopPropagation();
201
+ closeLightbox();
202
+ }
203
+ }
204
+ }, true);
205
+
206
+ // ─── Drop-zone wiring (reusable for the task view panel) ───────────────────
207
+
208
+ /**
209
+ * Attach drop / dragover / dragleave to a zone element. The callback
210
+ * receives an array of File objects.
211
+ */
212
+ function attachDropZone(zone, onFiles) {
213
+ if (!zone) return () => {};
214
+ const onDragOver = (e) => {
215
+ if (!hasFiles(e)) return;
216
+ e.preventDefault();
217
+ e.stopPropagation();
218
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
219
+ zone.classList.add("dragging");
220
+ };
221
+ const onDragLeave = (e) => {
222
+ // Only clear when we leave the zone, not when crossing child elements.
223
+ if (e.target === zone) zone.classList.remove("dragging");
224
+ };
225
+ const onDrop = (e) => {
226
+ e.preventDefault();
227
+ e.stopPropagation();
228
+ zone.classList.remove("dragging");
229
+ const files = collectImageFiles(e.dataTransfer);
230
+ if (files.length > 0) onFiles(files, e);
231
+ };
232
+ zone.addEventListener("dragover", onDragOver);
233
+ zone.addEventListener("dragleave", onDragLeave);
234
+ zone.addEventListener("drop", onDrop);
235
+ return () => {
236
+ zone.removeEventListener("dragover", onDragOver);
237
+ zone.removeEventListener("dragleave", onDragLeave);
238
+ zone.removeEventListener("drop", onDrop);
239
+ };
240
+ }
241
+
242
+ function hasFiles(e) {
243
+ if (!e.dataTransfer) return false;
244
+ const types = e.dataTransfer.types;
245
+ if (!types) return false;
246
+ for (let i = 0; i < types.length; i++) {
247
+ if (types[i] === "Files") return true;
248
+ }
249
+ return false;
250
+ }
251
+
252
+ function collectImageFiles(dt) {
253
+ if (!dt?.files) return [];
254
+ const out = [];
255
+ for (const f of dt.files) {
256
+ const ct = inferContentType(f);
257
+ if (ALLOWED_TYPES.has(ct)) out.push(f);
258
+ }
259
+ return out;
260
+ }
261
+
262
+ function collectClipboardImages(dt) {
263
+ if (!dt) return [];
264
+ const out = [];
265
+ for (const item of dt.items || []) {
266
+ if (item.kind !== "file") continue;
267
+ if (!item.type || !item.type.startsWith("image/")) continue;
268
+ const f = item.getAsFile();
269
+ if (f) out.push(f);
270
+ }
271
+ return out;
272
+ }
273
+
274
+ /**
275
+ * Attach a document-level paste handler that calls onFiles when image
276
+ * files are on the clipboard. Returns a detacher.
277
+ *
278
+ * The handler is conservative: it always preventDefault()s pastes that
279
+ * contain images (so the browser doesn't try to render them in editable
280
+ * regions), but ignores pastes that have no image content.
281
+ */
282
+ function attachPasteHandler(onFiles) {
283
+ const handler = (e) => {
284
+ const files = collectClipboardImages(e.clipboardData);
285
+ if (files.length === 0) return;
286
+ e.preventDefault();
287
+ onFiles(files, e);
288
+ };
289
+ document.addEventListener("paste", handler, true);
290
+ return () => document.removeEventListener("paste", handler, true);
291
+ }
292
+
293
+ // ─── Expose ────────────────────────────────────────────────────────────────
294
+
295
+ window.OpenKanImages = {
296
+ list,
297
+ upload,
298
+ remove,
299
+ srcFor,
300
+ markdownFor,
301
+ copyMarkdown,
302
+ openLightbox,
303
+ closeLightbox,
304
+ attachDropZone,
305
+ attachPasteHandler,
306
+ collectImageFiles,
307
+ collectClipboardImages,
308
+ ALLOWED_TYPES,
309
+ MAX_BYTES,
310
+ };
311
+ })();