kandown 0.11.2 → 0.13.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/README.md +13 -4
- package/bin/kandown.js +810 -14
- package/bin/tui.js +121 -11
- package/dist/android-chrome-192x192.png +0 -0
- package/dist/android-chrome-512x512.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/favicon-16x16.png +0 -0
- package/dist/favicon-32x32.png +0 -0
- package/dist/favicon-48x48.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +25 -7
- package/dist/index.html +134 -127
- package/dist/kandownlogo.png +0 -0
- package/dist/manifest.json +19 -1
- package/dist/mstile-150x150.png +0 -0
- package/dist/og-image.png +0 -0
- package/package.json +1 -1
- package/templates/AGENT.md +11 -8
- package/templates/AGENT_KANDOWN.md +16 -8
- package/templates/README.md +24 -20
package/bin/tui.js
CHANGED
|
@@ -54431,6 +54431,7 @@ function taskToBoardTask(task) {
|
|
|
54431
54431
|
priority: normalizePriority(frontmatter.priority),
|
|
54432
54432
|
ownerType: normalizeOwnerType(frontmatter.ownerType),
|
|
54433
54433
|
progress: total > 0 ? { done, total } : null,
|
|
54434
|
+
dependsOn: Array.isArray(frontmatter.depends_on) ? frontmatter.depends_on.filter((d) => typeof d === "string" && d.trim().length > 0) : [],
|
|
54434
54435
|
frontmatter: metadata
|
|
54435
54436
|
};
|
|
54436
54437
|
}
|
|
@@ -54536,8 +54537,11 @@ function serializeTaskFile(frontmatter, body) {
|
|
|
54536
54537
|
function getProjectRoot(kandownDir) {
|
|
54537
54538
|
return dirname(kandownDir);
|
|
54538
54539
|
}
|
|
54540
|
+
function getTasksDir(kandownDir) {
|
|
54541
|
+
return join2(getProjectRoot(kandownDir), "tasks");
|
|
54542
|
+
}
|
|
54539
54543
|
function listTaskIds(kandownDir) {
|
|
54540
|
-
const tasksDir =
|
|
54544
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
54541
54545
|
if (!existsSync3(tasksDir)) return [];
|
|
54542
54546
|
return readdirSync(tasksDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -3)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
54543
54547
|
}
|
|
@@ -54561,7 +54565,7 @@ function readBoard(kandownDir) {
|
|
|
54561
54565
|
};
|
|
54562
54566
|
}
|
|
54563
54567
|
function readTask(kandownDir, taskId) {
|
|
54564
|
-
const taskPath = join2(kandownDir,
|
|
54568
|
+
const taskPath = join2(getTasksDir(kandownDir), `${taskId}.md`);
|
|
54565
54569
|
if (!existsSync3(taskPath)) {
|
|
54566
54570
|
return {
|
|
54567
54571
|
frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
|
|
@@ -54593,7 +54597,7 @@ function readAgentDoc(kandownDir) {
|
|
|
54593
54597
|
return "";
|
|
54594
54598
|
}
|
|
54595
54599
|
function moveTaskToColumn(kandownDir, taskId, targetColumn) {
|
|
54596
|
-
const taskPath = join2(kandownDir,
|
|
54600
|
+
const taskPath = join2(getTasksDir(kandownDir), `${taskId}.md`);
|
|
54597
54601
|
if (!existsSync3(taskPath)) return false;
|
|
54598
54602
|
const parsed = readTask(kandownDir, taskId);
|
|
54599
54603
|
writeFileSync2(taskPath, serializeTaskFile({
|
|
@@ -56454,9 +56458,12 @@ var FileWatcher = class {
|
|
|
56454
56458
|
* 📖 Start watching task files and kandown.json.
|
|
56455
56459
|
* Uses chokidar for immediate FS event detection, plus a fallback 500ms
|
|
56456
56460
|
* poll to catch any races or network-mounted file changes.
|
|
56461
|
+
*
|
|
56462
|
+
* Tasks live at the project root (`./tasks/`, sibling of `.kandown/`);
|
|
56463
|
+
* kandown.json lives inside `.kandown/`.
|
|
56457
56464
|
*/
|
|
56458
56465
|
start(kandownDir) {
|
|
56459
|
-
const tasksDir =
|
|
56466
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
56460
56467
|
const configPath = join6(kandownDir, "kandown.json");
|
|
56461
56468
|
const existingIds = listTaskIds(kandownDir);
|
|
56462
56469
|
for (const id of existingIds) {
|
|
@@ -56515,7 +56522,7 @@ var FileWatcher = class {
|
|
|
56515
56522
|
// ─── Private ───────────────────────────────────────────────────────────────
|
|
56516
56523
|
handleFsEvent(event, filePath, kandownDir) {
|
|
56517
56524
|
if (this.stopped) return;
|
|
56518
|
-
const tasksDir =
|
|
56525
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
56519
56526
|
const configPath = join6(kandownDir, "kandown.json");
|
|
56520
56527
|
if (filePath === configPath) {
|
|
56521
56528
|
const key = `config:${event}`;
|
|
@@ -56568,7 +56575,7 @@ var FileWatcher = class {
|
|
|
56568
56575
|
/** 📖 Fallback poll — catches changes that chokidar missed (network mounts, exotic FS). */
|
|
56569
56576
|
async pollHashes(kandownDir) {
|
|
56570
56577
|
if (this.stopped) return;
|
|
56571
|
-
const tasksDir =
|
|
56578
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
56572
56579
|
const configPath = join6(kandownDir, "kandown.json");
|
|
56573
56580
|
try {
|
|
56574
56581
|
const newHash = hashFileSync(configPath);
|
|
@@ -56678,7 +56685,7 @@ ${taskPrompt}`;
|
|
|
56678
56685
|
---
|
|
56679
56686
|
|
|
56680
56687
|
${taskPrompt}`;
|
|
56681
|
-
return ["gemini", "-
|
|
56688
|
+
return ["gemini", "--prompt-interactive", combined];
|
|
56682
56689
|
}
|
|
56683
56690
|
},
|
|
56684
56691
|
{
|
|
@@ -56717,8 +56724,13 @@ ${taskPrompt}`;
|
|
|
56717
56724
|
bin: "opencode",
|
|
56718
56725
|
description: "SST AI coding TUI",
|
|
56719
56726
|
interactive: true,
|
|
56720
|
-
buildCommand: ({ taskPrompt }) => {
|
|
56721
|
-
|
|
56727
|
+
buildCommand: ({ systemPrompt, taskPrompt }) => {
|
|
56728
|
+
const combined = `${systemPrompt}
|
|
56729
|
+
|
|
56730
|
+
---
|
|
56731
|
+
|
|
56732
|
+
${taskPrompt}`;
|
|
56733
|
+
return ["opencode", "--prompt", combined];
|
|
56722
56734
|
}
|
|
56723
56735
|
}
|
|
56724
56736
|
];
|
|
@@ -56801,7 +56813,12 @@ ${taskPrompt}`, "utf8");
|
|
|
56801
56813
|
}
|
|
56802
56814
|
if (isInTmux()) {
|
|
56803
56815
|
const shellCmd = buildShellCmd(binary, args);
|
|
56804
|
-
|
|
56816
|
+
const envPrefix = [
|
|
56817
|
+
`KANDOWN_CONTEXT_FILE=${shellescape(contextFile)}`,
|
|
56818
|
+
`KANDOWN_TASK_ID=${shellescape(taskId)}`,
|
|
56819
|
+
`KANDOWN_DIR=${shellescape(kandownDir)}`
|
|
56820
|
+
].join(" ");
|
|
56821
|
+
execSync(`tmux split-window -h -p 50 ${shellescape(`env ${envPrefix} ${shellCmd}`)}`, {
|
|
56805
56822
|
stdio: "inherit"
|
|
56806
56823
|
});
|
|
56807
56824
|
} else {
|
|
@@ -57035,6 +57052,14 @@ function SingleTaskRow({ task, focused, dragging, colWidth }) {
|
|
|
57035
57052
|
" "
|
|
57036
57053
|
] }),
|
|
57037
57054
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: dragging ? "yellow" : focused ? "cyan" : "yellow", bold: focused || dragging, children: idStr }),
|
|
57055
|
+
task.dependsOn && task.dependsOn.length > 0 && // 📖 TUI equivalent of the web card's `↪N` chip. Surfaces blocked
|
|
57056
|
+
// work inline so the user doesn't try to move a task they can't
|
|
57057
|
+
// unblock without a different upstream task first.
|
|
57058
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: "yellow", children: [
|
|
57059
|
+
" ",
|
|
57060
|
+
"\u21AA",
|
|
57061
|
+
task.dependsOn.length
|
|
57062
|
+
] }),
|
|
57038
57063
|
tag && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: focused ? "white" : "magenta", bold: true, children: [
|
|
57039
57064
|
" ",
|
|
57040
57065
|
tag
|
|
@@ -57149,7 +57174,7 @@ function BoardHeader({ title, inTmux, modeHint, version, daemonStatus, daemonBus
|
|
|
57149
57174
|
const tmuxHint = inTmux ? " tmux" : "";
|
|
57150
57175
|
const versionTag = version ? ` v${version}` : "";
|
|
57151
57176
|
const daemonLabel = daemonBusy ? "\u25CC daemon\u2026" : daemonStatus.running ? `\u25CF web ${daemonStatus.metadata?.port ?? ""}` : "\u25CB web off";
|
|
57152
|
-
const hint = modeHint || "h/l cols \xB7 j/k tasks \xB7 drag tasks \xB7 d daemon \xB7 r reload \xB7 q quit";
|
|
57177
|
+
const hint = modeHint || "h/l cols \xB7 j/k tasks \xB7 drag tasks \xB7 a agent \xB7 g send-hook \xB7 d daemon \xB7 r reload \xB7 q quit";
|
|
57153
57178
|
const width = termWidth();
|
|
57154
57179
|
const leftWidth = Math.min(Math.max(34, Math.floor(width * 0.46)), width);
|
|
57155
57180
|
const daemonWidth = Math.min(16, Math.max(0, width - leftWidth));
|
|
@@ -57205,6 +57230,14 @@ function TaskDetail({ task, taskId, scrollOffset }) {
|
|
|
57205
57230
|
fm.assignee ? ` assignee: ${fm.assignee}` : "",
|
|
57206
57231
|
fm.due ? ` due: ${fm.due}` : ""
|
|
57207
57232
|
] }) }),
|
|
57233
|
+
Array.isArray(fm.depends_on) && fm.depends_on.length > 0 && // 📖 TUI equivalent of the web Drawer's dependency chips. Lists raw
|
|
57234
|
+
// ids — the user can `cat tasks/<id>.md` to inspect any one. We don't
|
|
57235
|
+
// resolve live status here (read-only context) but the move gate in
|
|
57236
|
+
// the TUI blocks the user from moving into Done until they're clear.
|
|
57237
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { marginBottom: 1, children: [
|
|
57238
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: "depends on: " }),
|
|
57239
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "yellow", children: fm.depends_on.join(", ") })
|
|
57240
|
+
] }),
|
|
57208
57241
|
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Text, { color: "gray", children: "\u2500".repeat(termWidth() - 4) }),
|
|
57209
57242
|
visibleLines.map((line, idx) => {
|
|
57210
57243
|
const isH = RE_HEADER.test(line);
|
|
@@ -57360,6 +57393,61 @@ function Board({ kandownDir, version }) {
|
|
|
57360
57393
|
setTimeout(() => setStatusMsg(""), 2500);
|
|
57361
57394
|
}
|
|
57362
57395
|
}, [daemonBusy, kandownDir, preferredDaemonPort]);
|
|
57396
|
+
const tryMoveWithGate = (0, import_react37.useCallback)((taskId, targetCol) => {
|
|
57397
|
+
if (!board) return false;
|
|
57398
|
+
const cfg = loadConfig(kandownDir);
|
|
57399
|
+
const cols = cfg.board.columns;
|
|
57400
|
+
const terminalLower = (cols[cols.length - 1] || "Done").toLowerCase();
|
|
57401
|
+
const isTerminal = targetCol.toLowerCase() === terminalLower;
|
|
57402
|
+
if (!isTerminal) return true;
|
|
57403
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
57404
|
+
for (const col of board.columns) {
|
|
57405
|
+
for (const t of col.tasks) {
|
|
57406
|
+
const isArch = t.frontmatter && (t.frontmatter.archived === true || t.frontmatter.archived === "true");
|
|
57407
|
+
resolved.set(t.id, isArch || col.name.toLowerCase() === terminalLower);
|
|
57408
|
+
}
|
|
57409
|
+
}
|
|
57410
|
+
const movingTask = board.columns.flatMap((c) => c.tasks).find((t) => t.id === taskId);
|
|
57411
|
+
if (!movingTask) return true;
|
|
57412
|
+
const deps = Array.isArray(movingTask.dependsOn) ? movingTask.dependsOn : [];
|
|
57413
|
+
const blocked = [];
|
|
57414
|
+
for (const dep of deps) {
|
|
57415
|
+
if (typeof dep !== "string" || !dep.trim() || dep === taskId) continue;
|
|
57416
|
+
const r = resolved.get(dep);
|
|
57417
|
+
if (!r) blocked.push(dep);
|
|
57418
|
+
}
|
|
57419
|
+
if (blocked.length > 0) {
|
|
57420
|
+
const list = blocked.length === 1 ? blocked[0] : `${blocked.slice(0, -1).join(", ")} and ${blocked[blocked.length - 1]}`;
|
|
57421
|
+
setStatusMsg(`Blocked: ${taskId} \u2190 ${list}`);
|
|
57422
|
+
setTimeout(() => setStatusMsg(""), 3500);
|
|
57423
|
+
return false;
|
|
57424
|
+
}
|
|
57425
|
+
return true;
|
|
57426
|
+
}, [board, kandownDir]);
|
|
57427
|
+
const sendTaskToAgentHook = (0, import_react37.useCallback)(async (taskId) => {
|
|
57428
|
+
const status = await getDaemonStatus(kandownDir);
|
|
57429
|
+
if (!status.running || !status.metadata) {
|
|
57430
|
+
setStatusMsg("Web daemon not running (press d to start)");
|
|
57431
|
+
setTimeout(() => setStatusMsg(""), 2500);
|
|
57432
|
+
return;
|
|
57433
|
+
}
|
|
57434
|
+
try {
|
|
57435
|
+
const res = await fetch(`http://127.0.0.1:${status.metadata.port}/api/tasks/${encodeURIComponent(taskId)}/agent`, {
|
|
57436
|
+
method: "POST",
|
|
57437
|
+
signal: AbortSignal.timeout(8e3)
|
|
57438
|
+
});
|
|
57439
|
+
if (res.ok) {
|
|
57440
|
+
setStatusMsg(`Sent ${taskId} to agent hook`);
|
|
57441
|
+
} else {
|
|
57442
|
+
const body = await res.text().catch(() => "");
|
|
57443
|
+
setStatusMsg(`Agent hook: ${res.status}${body ? " \u2014 " + body.slice(0, 60) : ""}`);
|
|
57444
|
+
}
|
|
57445
|
+
} catch (error) {
|
|
57446
|
+
setStatusMsg(`Agent hook failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
57447
|
+
} finally {
|
|
57448
|
+
setTimeout(() => setStatusMsg(""), 3e3);
|
|
57449
|
+
}
|
|
57450
|
+
}, [kandownDir]);
|
|
57363
57451
|
const getFocusedTask = (0, import_react37.useCallback)(() => {
|
|
57364
57452
|
if (!board) return null;
|
|
57365
57453
|
const col = board.columns[colIndex];
|
|
@@ -57500,6 +57588,11 @@ function Board({ kandownDir, version }) {
|
|
|
57500
57588
|
if (y === placeholderY) {
|
|
57501
57589
|
const targetColName = col.name;
|
|
57502
57590
|
if (moveTaskId) {
|
|
57591
|
+
if (!tryMoveWithGate(moveTaskId, targetColName)) {
|
|
57592
|
+
setMoveTaskId(null);
|
|
57593
|
+
setMode("browse");
|
|
57594
|
+
return;
|
|
57595
|
+
}
|
|
57503
57596
|
moveTaskToColumn(kandownDir, moveTaskId, targetColName);
|
|
57504
57597
|
const loaded = readBoard(kandownDir);
|
|
57505
57598
|
setBoard(loaded);
|
|
@@ -57570,6 +57663,12 @@ function Board({ kandownDir, version }) {
|
|
|
57570
57663
|
if (targetCol >= 0 && targetCol !== taskDrag.sourceCol) {
|
|
57571
57664
|
const targetColName = board.columns[targetCol]?.name;
|
|
57572
57665
|
if (targetColName) {
|
|
57666
|
+
if (!tryMoveWithGate(taskDrag.taskId, targetColName)) {
|
|
57667
|
+
setTaskDrag(null);
|
|
57668
|
+
setMousePress(null);
|
|
57669
|
+
setMode("browse");
|
|
57670
|
+
return;
|
|
57671
|
+
}
|
|
57573
57672
|
moveTaskToColumn(kandownDir, taskDrag.taskId, targetColName);
|
|
57574
57673
|
const loaded = readBoard(kandownDir);
|
|
57575
57674
|
setBoard(loaded);
|
|
@@ -57658,6 +57757,12 @@ function Board({ kandownDir, version }) {
|
|
|
57658
57757
|
setMode("agent-picker");
|
|
57659
57758
|
return;
|
|
57660
57759
|
}
|
|
57760
|
+
if (input === "g") {
|
|
57761
|
+
const task = getFocusedTask();
|
|
57762
|
+
if (!task) return;
|
|
57763
|
+
void sendTaskToAgentHook(task.id);
|
|
57764
|
+
return;
|
|
57765
|
+
}
|
|
57661
57766
|
}
|
|
57662
57767
|
if (mode === "context-menu") {
|
|
57663
57768
|
if (key.escape || input === "q") {
|
|
@@ -57725,6 +57830,11 @@ function Board({ kandownDir, version }) {
|
|
|
57725
57830
|
if (!board || !moveTaskId) return;
|
|
57726
57831
|
const name = board.columns[moveTargetCol]?.name;
|
|
57727
57832
|
if (name) {
|
|
57833
|
+
if (!tryMoveWithGate(moveTaskId, name)) {
|
|
57834
|
+
setMoveTaskId(null);
|
|
57835
|
+
setMode("browse");
|
|
57836
|
+
return;
|
|
57837
|
+
}
|
|
57728
57838
|
moveTaskToColumn(kandownDir, moveTaskId, name);
|
|
57729
57839
|
const loaded = readBoard(kandownDir);
|
|
57730
57840
|
setBoard(loaded);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/favicon.ico
ADDED
|
Binary file
|
package/dist/favicon.svg
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
<!--
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
@file public/favicon.svg
|
|
4
|
+
@description Kandown brand mark — vector favicon.
|
|
5
|
+
📖 Hand-traced from kandownlogo.png. Optimised for 16–256px rendering.
|
|
6
|
+
Colors: dark surface #0a0a0a, K strokes #ffffff, lime accent #cef867.
|
|
7
|
+
The lime slash is a parallelogram that slices from top-right to bottom-left,
|
|
8
|
+
crossing the K. Z-order is: surface → K strokes → lime slash.
|
|
9
|
+
-->
|
|
10
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Kandown">
|
|
11
|
+
<title>Kandown</title>
|
|
12
|
+
<rect width="256" height="256" rx="48" ry="48" fill="#0a0a0a"/>
|
|
13
|
+
|
|
14
|
+
<!-- K vertical bar (left stem) -->
|
|
15
|
+
<rect x="50" y="36" width="34" height="184" rx="3" ry="3" fill="#ffffff"/>
|
|
16
|
+
|
|
17
|
+
<!-- K upper diagonal: middle-left → top-right -->
|
|
18
|
+
<path d="M 84 128 L 200 36 L 222 36 L 84 152 Z" fill="#ffffff"/>
|
|
19
|
+
|
|
20
|
+
<!-- K lower diagonal: middle-left → bottom-right (with the small flag/notch on the right) -->
|
|
21
|
+
<path d="M 84 128 L 200 220 L 222 220 L 188 186 L 200 174 L 84 132 Z" fill="#ffffff"/>
|
|
22
|
+
|
|
23
|
+
<!-- Lime diagonal slash: thick parallelogram from top-right to bottom-left -->
|
|
24
|
+
<path d="M 232 22 L 174 22 L 24 172 L 24 230 Z" fill="#cef867"/>
|
|
25
|
+
</svg>
|