kandown 0.2.3 → 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 +13 -7
- package/bin/kandown.js +172 -11
- package/bin/tui.js +476 -267
- package/dist/index.html +407 -394
- package/package.json +7 -2
- package/templates/README.md +8 -7
- package/templates/AGENT_KANDOWN_COMPACT.md +0 -65
package/bin/tui.js
CHANGED
|
@@ -26207,7 +26207,7 @@ var require_websocket = __commonJS({
|
|
|
26207
26207
|
var http = __require("http");
|
|
26208
26208
|
var net = __require("net");
|
|
26209
26209
|
var tls = __require("tls");
|
|
26210
|
-
var { randomBytes, createHash } = __require("crypto");
|
|
26210
|
+
var { randomBytes, createHash: createHash2 } = __require("crypto");
|
|
26211
26211
|
var { Duplex, Readable: Readable2 } = __require("stream");
|
|
26212
26212
|
var { URL: URL2 } = __require("url");
|
|
26213
26213
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -26867,7 +26867,7 @@ var require_websocket = __commonJS({
|
|
|
26867
26867
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
26868
26868
|
return;
|
|
26869
26869
|
}
|
|
26870
|
-
const digest =
|
|
26870
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
26871
26871
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
26872
26872
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
26873
26873
|
return;
|
|
@@ -27234,7 +27234,7 @@ var require_websocket_server = __commonJS({
|
|
|
27234
27234
|
var EventEmitter4 = __require("events");
|
|
27235
27235
|
var http = __require("http");
|
|
27236
27236
|
var { Duplex } = __require("stream");
|
|
27237
|
-
var { createHash } = __require("crypto");
|
|
27237
|
+
var { createHash: createHash2 } = __require("crypto");
|
|
27238
27238
|
var extension2 = require_extension();
|
|
27239
27239
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
27240
27240
|
var subprotocol2 = require_subprotocol();
|
|
@@ -27535,7 +27535,7 @@ var require_websocket_server = __commonJS({
|
|
|
27535
27535
|
);
|
|
27536
27536
|
}
|
|
27537
27537
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
27538
|
-
const digest =
|
|
27538
|
+
const digest = createHash2("sha1").update(key + GUID).digest("base64");
|
|
27539
27539
|
const headers = [
|
|
27540
27540
|
"HTTP/1.1 101 Switching Protocols",
|
|
27541
27541
|
"Upgrade: websocket",
|
|
@@ -54328,6 +54328,267 @@ function ValueDisplay({ setting, value, focused }) {
|
|
|
54328
54328
|
// src/cli/screens/board.tsx
|
|
54329
54329
|
var import_react36 = __toESM(require_react(), 1);
|
|
54330
54330
|
|
|
54331
|
+
// src/cli/lib/board-reader.ts
|
|
54332
|
+
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
54333
|
+
import { dirname, join as join2 } from "path";
|
|
54334
|
+
|
|
54335
|
+
// src/lib/types.ts
|
|
54336
|
+
var DEFAULT_COLUMNS = ["Backlog", "Todo", "In Progress", "Review", "Done"];
|
|
54337
|
+
|
|
54338
|
+
// src/lib/parser.ts
|
|
54339
|
+
function parseSimpleYaml(yaml) {
|
|
54340
|
+
const obj = {};
|
|
54341
|
+
if (!yaml || typeof yaml !== "string") return obj;
|
|
54342
|
+
const lines = yaml.split("\n");
|
|
54343
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54344
|
+
const line = lines[i] ?? "";
|
|
54345
|
+
const m = line.match(/^([a-zA-Z_][\w-]*)\s*:\s*(.*)$/);
|
|
54346
|
+
if (!m) continue;
|
|
54347
|
+
const key = m[1];
|
|
54348
|
+
if (!key) continue;
|
|
54349
|
+
let val = m[2]?.trim() ?? "";
|
|
54350
|
+
if (val === "|") {
|
|
54351
|
+
const block = [];
|
|
54352
|
+
i++;
|
|
54353
|
+
while (i < lines.length && (/^\s+/.test(lines[i] ?? "") || (lines[i] ?? "") === "")) {
|
|
54354
|
+
block.push((lines[i] ?? "").replace(/^ /, ""));
|
|
54355
|
+
i++;
|
|
54356
|
+
}
|
|
54357
|
+
i--;
|
|
54358
|
+
obj[key] = block.join("\n").trimEnd();
|
|
54359
|
+
continue;
|
|
54360
|
+
}
|
|
54361
|
+
if (typeof val !== "string") val = "";
|
|
54362
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
54363
|
+
const arr = val.slice(1, -1).split(",").map((s) => s && typeof s === "string" ? s.trim().replace(/^["']|["']$/g, "") : "").filter(Boolean);
|
|
54364
|
+
obj[key] = arr;
|
|
54365
|
+
} else {
|
|
54366
|
+
obj[key] = typeof val === "string" ? val.replace(/^["']|["']$/g, "") : val;
|
|
54367
|
+
}
|
|
54368
|
+
}
|
|
54369
|
+
return obj;
|
|
54370
|
+
}
|
|
54371
|
+
function parseTaskFile(md) {
|
|
54372
|
+
if (!md || typeof md !== "string") {
|
|
54373
|
+
return { frontmatter: { id: "", title: "" }, body: "" };
|
|
54374
|
+
}
|
|
54375
|
+
const lines = md.split("\n");
|
|
54376
|
+
if (lines[0] && lines[0].trim() === "---") {
|
|
54377
|
+
const fmLines = [];
|
|
54378
|
+
let i = 1;
|
|
54379
|
+
while (i < lines.length && lines[i].trim() !== "---") {
|
|
54380
|
+
fmLines.push(lines[i]);
|
|
54381
|
+
i++;
|
|
54382
|
+
}
|
|
54383
|
+
const body = lines.slice(i + 1).join("\n").trimStart();
|
|
54384
|
+
const fm = parseSimpleYaml(fmLines.join("\n"));
|
|
54385
|
+
return { frontmatter: fm, body };
|
|
54386
|
+
}
|
|
54387
|
+
return { frontmatter: { id: "", title: "" }, body: md };
|
|
54388
|
+
}
|
|
54389
|
+
function normalizeStatus(status) {
|
|
54390
|
+
const value = typeof status === "string" ? status.trim() : "";
|
|
54391
|
+
return value || "Backlog";
|
|
54392
|
+
}
|
|
54393
|
+
function normalizePriority(priority) {
|
|
54394
|
+
if (typeof priority !== "string") return null;
|
|
54395
|
+
const value = priority.toUpperCase();
|
|
54396
|
+
return /^(P1|P2|P3|P4)$/.test(value) ? value : null;
|
|
54397
|
+
}
|
|
54398
|
+
function normalizeOwnerType(ownerType) {
|
|
54399
|
+
if (typeof ownerType !== "string") return "";
|
|
54400
|
+
const value = ownerType.toLowerCase();
|
|
54401
|
+
return value === "human" || value === "ai" ? value : "";
|
|
54402
|
+
}
|
|
54403
|
+
function taskOrder(task) {
|
|
54404
|
+
const value = task.frontmatter.order;
|
|
54405
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
54406
|
+
if (typeof value === "string" && value.trim()) {
|
|
54407
|
+
const parsed = Number(value);
|
|
54408
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
54409
|
+
}
|
|
54410
|
+
return Number.MAX_SAFE_INTEGER;
|
|
54411
|
+
}
|
|
54412
|
+
function taskToBoardTask(task) {
|
|
54413
|
+
const { frontmatter, body } = task;
|
|
54414
|
+
const { subtasks } = extractSubtasks(body);
|
|
54415
|
+
const done = subtasks.filter((s) => s.done).length;
|
|
54416
|
+
const total = subtasks.length;
|
|
54417
|
+
const status = normalizeStatus(frontmatter.status);
|
|
54418
|
+
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : [];
|
|
54419
|
+
return {
|
|
54420
|
+
id: frontmatter.id || "",
|
|
54421
|
+
title: frontmatter.title || frontmatter.id || "Untitled task",
|
|
54422
|
+
checked: /done|termin|closed|complet/i.test(status),
|
|
54423
|
+
tags,
|
|
54424
|
+
assignee: typeof frontmatter.assignee === "string" && frontmatter.assignee ? frontmatter.assignee : null,
|
|
54425
|
+
priority: normalizePriority(frontmatter.priority),
|
|
54426
|
+
ownerType: normalizeOwnerType(frontmatter.ownerType),
|
|
54427
|
+
progress: total > 0 ? { done, total } : null
|
|
54428
|
+
};
|
|
54429
|
+
}
|
|
54430
|
+
function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
|
|
54431
|
+
const columnNames = configuredColumns.length > 0 ? configuredColumns : DEFAULT_COLUMNS;
|
|
54432
|
+
const columnsByName = /* @__PURE__ */ new Map();
|
|
54433
|
+
const configured = columnNames.map((name) => ({ name, tasks: [] }));
|
|
54434
|
+
for (const column of configured) columnsByName.set(column.name.toLowerCase(), column);
|
|
54435
|
+
const unknownColumns = [];
|
|
54436
|
+
const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).sort((a, b) => {
|
|
54437
|
+
const byOrder = taskOrder(a) - taskOrder(b);
|
|
54438
|
+
if (byOrder !== 0) return byOrder;
|
|
54439
|
+
return a.frontmatter.id.localeCompare(b.frontmatter.id, void 0, { numeric: true });
|
|
54440
|
+
});
|
|
54441
|
+
for (const task of sortedTasks) {
|
|
54442
|
+
const status = normalizeStatus(task.frontmatter.status);
|
|
54443
|
+
let column = columnsByName.get(status.toLowerCase());
|
|
54444
|
+
if (!column) {
|
|
54445
|
+
column = { name: status, tasks: [] };
|
|
54446
|
+
columnsByName.set(status.toLowerCase(), column);
|
|
54447
|
+
unknownColumns.push(column);
|
|
54448
|
+
}
|
|
54449
|
+
column.tasks.push(taskToBoardTask(task));
|
|
54450
|
+
}
|
|
54451
|
+
return [...unknownColumns, ...configured];
|
|
54452
|
+
}
|
|
54453
|
+
function extractSubtasks(body) {
|
|
54454
|
+
const subtasks = [];
|
|
54455
|
+
if (!body || typeof body !== "string") return { subtasks, bodyWithoutSubtasks: body ?? "" };
|
|
54456
|
+
const lines = body.split("\n");
|
|
54457
|
+
const kept = [];
|
|
54458
|
+
let inSubtaskSection = false;
|
|
54459
|
+
for (const line of lines) {
|
|
54460
|
+
if (/^#{1,6}\s+(subtasks?|sous[- ]t[âa]ches?|crit[èe]res?)/i.test(line)) {
|
|
54461
|
+
inSubtaskSection = true;
|
|
54462
|
+
kept.push(line);
|
|
54463
|
+
continue;
|
|
54464
|
+
}
|
|
54465
|
+
if (/^#{1,6}\s+/.test(line) && inSubtaskSection) {
|
|
54466
|
+
inSubtaskSection = false;
|
|
54467
|
+
kept.push(line);
|
|
54468
|
+
continue;
|
|
54469
|
+
}
|
|
54470
|
+
const m = line.match(/^\s*-\s+\[([ xX])\]\s+(.+)$/);
|
|
54471
|
+
if (m && inSubtaskSection) {
|
|
54472
|
+
const text = m[2]?.trim() ?? "";
|
|
54473
|
+
subtasks.push({ done: (m[1]?.toLowerCase() ?? "") === "x", text });
|
|
54474
|
+
continue;
|
|
54475
|
+
}
|
|
54476
|
+
const descMatch = line.match(/^\s*\[DESC\]\s*(.*)$/);
|
|
54477
|
+
if (descMatch && subtasks.length > 0) {
|
|
54478
|
+
subtasks[subtasks.length - 1].description = descMatch[1];
|
|
54479
|
+
continue;
|
|
54480
|
+
}
|
|
54481
|
+
const reportMatch = line.match(/^\s*\[REPORT\]\s*(.*)$/);
|
|
54482
|
+
if (reportMatch && subtasks.length > 0) {
|
|
54483
|
+
subtasks[subtasks.length - 1].report = reportMatch[1];
|
|
54484
|
+
continue;
|
|
54485
|
+
}
|
|
54486
|
+
kept.push(line);
|
|
54487
|
+
}
|
|
54488
|
+
return { subtasks, bodyWithoutSubtasks: kept.join("\n") };
|
|
54489
|
+
}
|
|
54490
|
+
|
|
54491
|
+
// src/lib/serializer.ts
|
|
54492
|
+
function serializeTaskFile(frontmatter, body) {
|
|
54493
|
+
const lines = ["---"];
|
|
54494
|
+
if (frontmatter && typeof frontmatter === "object") {
|
|
54495
|
+
for (const [k, v] of Object.entries(frontmatter)) {
|
|
54496
|
+
if (v === null || v === void 0 || v === "") continue;
|
|
54497
|
+
if (Array.isArray(v)) {
|
|
54498
|
+
if (v.length === 0) continue;
|
|
54499
|
+
lines.push(`${k}: [${v.join(", ")}]`);
|
|
54500
|
+
} else if (typeof v === "string" && v.includes("\n")) {
|
|
54501
|
+
lines.push(`${k}: |`);
|
|
54502
|
+
lines.push(...v.split("\n").map((line) => ` ${line}`));
|
|
54503
|
+
} else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
54504
|
+
lines.push(`${k}: ${v}`);
|
|
54505
|
+
}
|
|
54506
|
+
}
|
|
54507
|
+
}
|
|
54508
|
+
lines.push("---");
|
|
54509
|
+
lines.push("");
|
|
54510
|
+
lines.push((body ?? "").trim());
|
|
54511
|
+
lines.push("");
|
|
54512
|
+
return lines.join("\n");
|
|
54513
|
+
}
|
|
54514
|
+
|
|
54515
|
+
// src/cli/lib/board-reader.ts
|
|
54516
|
+
function getProjectRoot(kandownDir) {
|
|
54517
|
+
return dirname(kandownDir);
|
|
54518
|
+
}
|
|
54519
|
+
function listTaskIds(kandownDir) {
|
|
54520
|
+
const tasksDir = join2(kandownDir, "tasks");
|
|
54521
|
+
if (!existsSync3(tasksDir)) return [];
|
|
54522
|
+
return readdirSync(tasksDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -3)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
54523
|
+
}
|
|
54524
|
+
function readBoard(kandownDir) {
|
|
54525
|
+
const config = loadConfig(kandownDir);
|
|
54526
|
+
const tasks = listTaskIds(kandownDir).map((id) => {
|
|
54527
|
+
const task = readTask(kandownDir, id);
|
|
54528
|
+
return {
|
|
54529
|
+
...task,
|
|
54530
|
+
frontmatter: {
|
|
54531
|
+
...task.frontmatter,
|
|
54532
|
+
id: task.frontmatter.id || id,
|
|
54533
|
+
status: task.frontmatter.status || "Backlog"
|
|
54534
|
+
}
|
|
54535
|
+
};
|
|
54536
|
+
});
|
|
54537
|
+
return {
|
|
54538
|
+
frontmatter: null,
|
|
54539
|
+
title: "Project Kanban",
|
|
54540
|
+
columns: buildColumnsFromTasks(tasks, config.board.columns)
|
|
54541
|
+
};
|
|
54542
|
+
}
|
|
54543
|
+
function readTask(kandownDir, taskId) {
|
|
54544
|
+
const taskPath = join2(kandownDir, "tasks", `${taskId}.md`);
|
|
54545
|
+
if (!existsSync3(taskPath)) {
|
|
54546
|
+
return {
|
|
54547
|
+
frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
|
|
54548
|
+
body: ""
|
|
54549
|
+
};
|
|
54550
|
+
}
|
|
54551
|
+
const content = readFileSync3(taskPath, "utf8");
|
|
54552
|
+
const parsed = parseTaskFile(content);
|
|
54553
|
+
return {
|
|
54554
|
+
...parsed,
|
|
54555
|
+
frontmatter: {
|
|
54556
|
+
...parsed.frontmatter,
|
|
54557
|
+
id: parsed.frontmatter.id || taskId,
|
|
54558
|
+
status: parsed.frontmatter.status || "Backlog"
|
|
54559
|
+
}
|
|
54560
|
+
};
|
|
54561
|
+
}
|
|
54562
|
+
function readAgentDoc(kandownDir) {
|
|
54563
|
+
const root = getProjectRoot(kandownDir);
|
|
54564
|
+
const candidates = [
|
|
54565
|
+
join2(root, "AGENT_KANDOWN.md"),
|
|
54566
|
+
join2(kandownDir, "AGENT.md")
|
|
54567
|
+
];
|
|
54568
|
+
for (const candidate of candidates) {
|
|
54569
|
+
if (existsSync3(candidate)) {
|
|
54570
|
+
return readFileSync3(candidate, "utf8");
|
|
54571
|
+
}
|
|
54572
|
+
}
|
|
54573
|
+
return "";
|
|
54574
|
+
}
|
|
54575
|
+
function moveTaskToColumn(kandownDir, taskId, targetColumn) {
|
|
54576
|
+
const taskPath = join2(kandownDir, "tasks", `${taskId}.md`);
|
|
54577
|
+
if (!existsSync3(taskPath)) return false;
|
|
54578
|
+
const parsed = readTask(kandownDir, taskId);
|
|
54579
|
+
writeFileSync2(taskPath, serializeTaskFile({
|
|
54580
|
+
...parsed.frontmatter,
|
|
54581
|
+
id: taskId,
|
|
54582
|
+
status: targetColumn
|
|
54583
|
+
}, parsed.body), "utf8");
|
|
54584
|
+
return true;
|
|
54585
|
+
}
|
|
54586
|
+
|
|
54587
|
+
// src/cli/lib/file-watcher.ts
|
|
54588
|
+
import { createReadStream, statSync } from "fs";
|
|
54589
|
+
import { createHash } from "crypto";
|
|
54590
|
+
import { join as join5 } from "path";
|
|
54591
|
+
|
|
54331
54592
|
// node_modules/.pnpm/chokidar@4.0.3/node_modules/chokidar/esm/index.js
|
|
54332
54593
|
import { stat as statcb } from "fs";
|
|
54333
54594
|
import { stat as stat3, readdir as readdir2 } from "fs/promises";
|
|
@@ -56018,263 +56279,211 @@ function watch(paths, options = {}) {
|
|
|
56018
56279
|
return watcher;
|
|
56019
56280
|
}
|
|
56020
56281
|
|
|
56021
|
-
// src/cli/
|
|
56022
|
-
|
|
56023
|
-
|
|
56024
|
-
|
|
56025
|
-
|
|
56026
|
-
|
|
56027
|
-
|
|
56028
|
-
|
|
56029
|
-
|
|
56030
|
-
|
|
56031
|
-
|
|
56032
|
-
|
|
56033
|
-
|
|
56034
|
-
|
|
56035
|
-
|
|
56036
|
-
|
|
56037
|
-
|
|
56038
|
-
|
|
56039
|
-
|
|
56040
|
-
|
|
56041
|
-
|
|
56042
|
-
|
|
56043
|
-
|
|
56044
|
-
|
|
56045
|
-
|
|
56046
|
-
|
|
56047
|
-
|
|
56048
|
-
|
|
56282
|
+
// src/cli/lib/file-watcher.ts
|
|
56283
|
+
function hashFile(filePath) {
|
|
56284
|
+
return new Promise((resolve3, reject) => {
|
|
56285
|
+
const hash = createHash("sha256");
|
|
56286
|
+
const stream = createReadStream(filePath);
|
|
56287
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
56288
|
+
stream.on("end", () => resolve3(hash.digest("hex")));
|
|
56289
|
+
stream.on("error", reject);
|
|
56290
|
+
});
|
|
56291
|
+
}
|
|
56292
|
+
function hashFileSync(filePath) {
|
|
56293
|
+
const content = __require("fs").readFileSync(filePath, "utf8");
|
|
56294
|
+
return createHash("sha256").update(content).digest("hex");
|
|
56295
|
+
}
|
|
56296
|
+
var FileWatcher = class {
|
|
56297
|
+
watcher = null;
|
|
56298
|
+
taskHashes = /* @__PURE__ */ new Map();
|
|
56299
|
+
knownTaskIds = /* @__PURE__ */ new Set();
|
|
56300
|
+
listeners = /* @__PURE__ */ new Map();
|
|
56301
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
56302
|
+
debounceDelay = 50;
|
|
56303
|
+
watchDebounceDelay = 100;
|
|
56304
|
+
pollInterval = null;
|
|
56305
|
+
stopped = false;
|
|
56306
|
+
/**
|
|
56307
|
+
* 📖 Start watching task files and kandown.json.
|
|
56308
|
+
* Uses chokidar for immediate FS event detection, plus a fallback 500ms
|
|
56309
|
+
* poll to catch any races or network-mounted file changes.
|
|
56310
|
+
*/
|
|
56311
|
+
start(kandownDir) {
|
|
56312
|
+
const tasksDir = join5(kandownDir, "tasks");
|
|
56313
|
+
const configPath = join5(kandownDir, "kandown.json");
|
|
56314
|
+
const existingIds = listTaskIds(kandownDir);
|
|
56315
|
+
for (const id of existingIds) {
|
|
56316
|
+
this.knownTaskIds.add(id);
|
|
56317
|
+
try {
|
|
56318
|
+
const filePath = join5(tasksDir, `${id}.md`);
|
|
56319
|
+
this.taskHashes.set(id, hashFileSync(filePath));
|
|
56320
|
+
} catch {
|
|
56049
56321
|
}
|
|
56050
|
-
i--;
|
|
56051
|
-
obj[key] = block.join("\n").trimEnd();
|
|
56052
|
-
continue;
|
|
56053
|
-
}
|
|
56054
|
-
if (typeof val !== "string") val = "";
|
|
56055
|
-
if (val.startsWith("[") && val.endsWith("]")) {
|
|
56056
|
-
const arr = val.slice(1, -1).split(",").map((s) => s && typeof s === "string" ? s.trim().replace(/^["']|["']$/g, "") : "").filter(Boolean);
|
|
56057
|
-
obj[key] = arr;
|
|
56058
|
-
} else {
|
|
56059
|
-
obj[key] = typeof val === "string" ? val.replace(/^["']|["']$/g, "") : val;
|
|
56060
56322
|
}
|
|
56323
|
+
this.watcher = watch([join5(tasksDir, "*.md"), configPath], {
|
|
56324
|
+
persistent: true,
|
|
56325
|
+
ignoreInitial: true,
|
|
56326
|
+
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 }
|
|
56327
|
+
});
|
|
56328
|
+
this.watcher.on("all", (event, path) => {
|
|
56329
|
+
this.handleFsEvent(event, path, kandownDir);
|
|
56330
|
+
});
|
|
56331
|
+
this.pollInterval = setInterval(() => {
|
|
56332
|
+
this.pollHashes(kandownDir);
|
|
56333
|
+
}, 500);
|
|
56061
56334
|
}
|
|
56062
|
-
|
|
56063
|
-
|
|
56064
|
-
|
|
56065
|
-
|
|
56066
|
-
|
|
56335
|
+
/**
|
|
56336
|
+
* 📖 Stop watching and clean up all resources.
|
|
56337
|
+
*/
|
|
56338
|
+
stop() {
|
|
56339
|
+
this.stopped = true;
|
|
56340
|
+
if (this.pollInterval) {
|
|
56341
|
+
clearInterval(this.pollInterval);
|
|
56342
|
+
this.pollInterval = null;
|
|
56343
|
+
}
|
|
56344
|
+
if (this.watcher) {
|
|
56345
|
+
this.watcher.close();
|
|
56346
|
+
this.watcher = null;
|
|
56347
|
+
}
|
|
56348
|
+
this.debounceTimers.forEach((t) => clearTimeout(t));
|
|
56349
|
+
this.debounceTimers.clear();
|
|
56350
|
+
this.taskHashes.clear();
|
|
56351
|
+
this.knownTaskIds.clear();
|
|
56352
|
+
this.emit("stopped");
|
|
56353
|
+
}
|
|
56354
|
+
/** 📖 Register an event handler. Returns an unsubscribe function. */
|
|
56355
|
+
on(event, handler) {
|
|
56356
|
+
if (!this.listeners.has(event)) {
|
|
56357
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
56358
|
+
}
|
|
56359
|
+
this.listeners.get(event).add(handler);
|
|
56360
|
+
return () => {
|
|
56361
|
+
this.listeners.get(event)?.delete(handler);
|
|
56362
|
+
};
|
|
56067
56363
|
}
|
|
56068
|
-
|
|
56069
|
-
|
|
56070
|
-
|
|
56071
|
-
|
|
56072
|
-
|
|
56073
|
-
|
|
56074
|
-
|
|
56364
|
+
/** 📖 Current set of known task IDs. */
|
|
56365
|
+
getKnownTaskIds() {
|
|
56366
|
+
return Array.from(this.knownTaskIds);
|
|
56367
|
+
}
|
|
56368
|
+
// ─── Private ───────────────────────────────────────────────────────────────
|
|
56369
|
+
handleFsEvent(event, filePath, kandownDir) {
|
|
56370
|
+
if (this.stopped) return;
|
|
56371
|
+
const tasksDir = join5(kandownDir, "tasks");
|
|
56372
|
+
const configPath = join5(kandownDir, "kandown.json");
|
|
56373
|
+
if (filePath === configPath) {
|
|
56374
|
+
const key = `config:${event}`;
|
|
56375
|
+
const existing = this.debounceTimers.get(key);
|
|
56376
|
+
if (existing) clearTimeout(existing);
|
|
56377
|
+
this.debounceTimers.set(key, setTimeout(() => {
|
|
56378
|
+
this.debounceTimers.delete(key);
|
|
56379
|
+
this.emit("configChanged");
|
|
56380
|
+
}, this.watchDebounceDelay));
|
|
56381
|
+
return;
|
|
56075
56382
|
}
|
|
56076
|
-
const
|
|
56077
|
-
|
|
56078
|
-
|
|
56079
|
-
|
|
56080
|
-
|
|
56081
|
-
|
|
56082
|
-
|
|
56083
|
-
|
|
56084
|
-
|
|
56085
|
-
}
|
|
56086
|
-
|
|
56087
|
-
|
|
56088
|
-
|
|
56089
|
-
|
|
56090
|
-
}
|
|
56091
|
-
|
|
56092
|
-
|
|
56093
|
-
|
|
56094
|
-
|
|
56095
|
-
|
|
56096
|
-
|
|
56097
|
-
|
|
56098
|
-
|
|
56099
|
-
|
|
56100
|
-
|
|
56101
|
-
|
|
56102
|
-
|
|
56103
|
-
|
|
56104
|
-
}
|
|
56105
|
-
|
|
56106
|
-
|
|
56107
|
-
|
|
56108
|
-
|
|
56109
|
-
|
|
56110
|
-
|
|
56111
|
-
|
|
56112
|
-
return {
|
|
56113
|
-
id: frontmatter.id || "",
|
|
56114
|
-
title: frontmatter.title || frontmatter.id || "Untitled task",
|
|
56115
|
-
checked: /done|termin|closed|complet/i.test(status),
|
|
56116
|
-
tags,
|
|
56117
|
-
assignee: typeof frontmatter.assignee === "string" && frontmatter.assignee ? frontmatter.assignee : null,
|
|
56118
|
-
priority: normalizePriority(frontmatter.priority),
|
|
56119
|
-
ownerType: normalizeOwnerType(frontmatter.ownerType),
|
|
56120
|
-
progress: total > 0 ? { done, total } : null
|
|
56121
|
-
};
|
|
56122
|
-
}
|
|
56123
|
-
function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
|
|
56124
|
-
const columnNames = configuredColumns.length > 0 ? configuredColumns : DEFAULT_COLUMNS;
|
|
56125
|
-
const columnsByName = /* @__PURE__ */ new Map();
|
|
56126
|
-
const configured = columnNames.map((name) => ({ name, tasks: [] }));
|
|
56127
|
-
for (const column of configured) columnsByName.set(column.name.toLowerCase(), column);
|
|
56128
|
-
const unknownColumns = [];
|
|
56129
|
-
const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).sort((a, b) => {
|
|
56130
|
-
const byOrder = taskOrder(a) - taskOrder(b);
|
|
56131
|
-
if (byOrder !== 0) return byOrder;
|
|
56132
|
-
return a.frontmatter.id.localeCompare(b.frontmatter.id, void 0, { numeric: true });
|
|
56133
|
-
});
|
|
56134
|
-
for (const task of sortedTasks) {
|
|
56135
|
-
const status = normalizeStatus(task.frontmatter.status);
|
|
56136
|
-
let column = columnsByName.get(status.toLowerCase());
|
|
56137
|
-
if (!column) {
|
|
56138
|
-
column = { name: status, tasks: [] };
|
|
56139
|
-
columnsByName.set(status.toLowerCase(), column);
|
|
56140
|
-
unknownColumns.push(column);
|
|
56383
|
+
const taskId = filePath.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/, "") ?? "";
|
|
56384
|
+
if (!taskId) return;
|
|
56385
|
+
if (event === "add" || event === "change") {
|
|
56386
|
+
const key = `task:${taskId}:${event}`;
|
|
56387
|
+
const existing = this.debounceTimers.get(key);
|
|
56388
|
+
if (existing) clearTimeout(existing);
|
|
56389
|
+
this.debounceTimers.set(key, setTimeout(() => {
|
|
56390
|
+
this.debounceTimers.delete(key);
|
|
56391
|
+
void this.checkTaskContentChange(taskId, filePath, true);
|
|
56392
|
+
}, this.watchDebounceDelay));
|
|
56393
|
+
} else if (event === "unlink") {
|
|
56394
|
+
this.taskHashes.delete(taskId);
|
|
56395
|
+
this.knownTaskIds.delete(taskId);
|
|
56396
|
+
}
|
|
56397
|
+
}
|
|
56398
|
+
async checkTaskContentChange(taskId, filePath, isNew) {
|
|
56399
|
+
try {
|
|
56400
|
+
const newHash = await hashFile(filePath);
|
|
56401
|
+
const oldHash = this.taskHashes.get(taskId);
|
|
56402
|
+
if (isNew && !this.knownTaskIds.has(taskId)) {
|
|
56403
|
+
this.knownTaskIds.add(taskId);
|
|
56404
|
+
this.taskHashes.set(taskId, newHash);
|
|
56405
|
+
this.emit("newTaskDetected", taskId);
|
|
56406
|
+
return;
|
|
56407
|
+
}
|
|
56408
|
+
if (oldHash !== void 0 && newHash !== oldHash) {
|
|
56409
|
+
this.taskHashes.set(taskId, newHash);
|
|
56410
|
+
this.emit("taskChanged", taskId);
|
|
56411
|
+
} else if (oldHash === void 0) {
|
|
56412
|
+
this.knownTaskIds.add(taskId);
|
|
56413
|
+
this.taskHashes.set(taskId, newHash);
|
|
56414
|
+
if (isNew) {
|
|
56415
|
+
this.emit("newTaskDetected", taskId);
|
|
56416
|
+
}
|
|
56417
|
+
}
|
|
56418
|
+
} catch {
|
|
56141
56419
|
}
|
|
56142
|
-
column.tasks.push(taskToBoardTask(task));
|
|
56143
56420
|
}
|
|
56144
|
-
|
|
56145
|
-
|
|
56146
|
-
|
|
56147
|
-
|
|
56148
|
-
|
|
56149
|
-
|
|
56150
|
-
|
|
56151
|
-
|
|
56152
|
-
for (const line of lines) {
|
|
56153
|
-
if (/^#{1,6}\s+(subtasks?|sous[- ]t[âa]ches?|crit[èe]res?)/i.test(line)) {
|
|
56154
|
-
inSubtaskSection = true;
|
|
56155
|
-
kept.push(line);
|
|
56156
|
-
continue;
|
|
56157
|
-
}
|
|
56158
|
-
if (/^#{1,6}\s+/.test(line) && inSubtaskSection) {
|
|
56159
|
-
inSubtaskSection = false;
|
|
56160
|
-
kept.push(line);
|
|
56161
|
-
continue;
|
|
56162
|
-
}
|
|
56163
|
-
const m = line.match(/^\s*-\s+\[([ xX])\]\s+(.+)$/);
|
|
56164
|
-
if (m && inSubtaskSection) {
|
|
56165
|
-
const text = m[2]?.trim() ?? "";
|
|
56166
|
-
subtasks.push({ done: (m[1]?.toLowerCase() ?? "") === "x", text });
|
|
56167
|
-
continue;
|
|
56168
|
-
}
|
|
56169
|
-
const descMatch = line.match(/^\s*\[DESC\]\s*(.*)$/);
|
|
56170
|
-
if (descMatch && subtasks.length > 0) {
|
|
56171
|
-
subtasks[subtasks.length - 1].description = descMatch[1];
|
|
56172
|
-
continue;
|
|
56173
|
-
}
|
|
56174
|
-
const reportMatch = line.match(/^\s*\[REPORT\]\s*(.*)$/);
|
|
56175
|
-
if (reportMatch && subtasks.length > 0) {
|
|
56176
|
-
subtasks[subtasks.length - 1].report = reportMatch[1];
|
|
56177
|
-
continue;
|
|
56421
|
+
/** 📖 Fallback poll — catches changes that chokidar missed (network mounts, exotic FS). */
|
|
56422
|
+
async pollHashes(kandownDir) {
|
|
56423
|
+
if (this.stopped) return;
|
|
56424
|
+
const tasksDir = join5(kandownDir, "tasks");
|
|
56425
|
+
const configPath = join5(kandownDir, "kandown.json");
|
|
56426
|
+
try {
|
|
56427
|
+
const newHash = hashFileSync(configPath);
|
|
56428
|
+
} catch {
|
|
56178
56429
|
}
|
|
56179
|
-
|
|
56180
|
-
|
|
56181
|
-
|
|
56182
|
-
|
|
56183
|
-
|
|
56184
|
-
|
|
56185
|
-
|
|
56186
|
-
|
|
56187
|
-
|
|
56188
|
-
|
|
56189
|
-
|
|
56190
|
-
|
|
56191
|
-
|
|
56192
|
-
|
|
56193
|
-
|
|
56194
|
-
|
|
56195
|
-
|
|
56196
|
-
|
|
56197
|
-
|
|
56430
|
+
for (const taskId of this.knownTaskIds) {
|
|
56431
|
+
const filePath = join5(tasksDir, `${taskId}.md`);
|
|
56432
|
+
try {
|
|
56433
|
+
statSync(filePath);
|
|
56434
|
+
const newHash = await hashFile(filePath);
|
|
56435
|
+
const oldHash = this.taskHashes.get(taskId);
|
|
56436
|
+
if (oldHash !== void 0 && newHash !== oldHash) {
|
|
56437
|
+
this.taskHashes.set(taskId, newHash);
|
|
56438
|
+
this.emit("taskChanged", taskId);
|
|
56439
|
+
}
|
|
56440
|
+
} catch {
|
|
56441
|
+
this.taskHashes.delete(taskId);
|
|
56442
|
+
this.knownTaskIds.delete(taskId);
|
|
56443
|
+
}
|
|
56444
|
+
}
|
|
56445
|
+
const currentIds = listTaskIds(kandownDir);
|
|
56446
|
+
for (const id of currentIds) {
|
|
56447
|
+
if (!this.knownTaskIds.has(id)) {
|
|
56448
|
+
const filePath = join5(tasksDir, `${id}.md`);
|
|
56449
|
+
try {
|
|
56450
|
+
const newHash = await hashFile(filePath);
|
|
56451
|
+
this.knownTaskIds.add(id);
|
|
56452
|
+
this.taskHashes.set(id, newHash);
|
|
56453
|
+
this.emit("newTaskDetected", id);
|
|
56454
|
+
} catch {
|
|
56455
|
+
}
|
|
56198
56456
|
}
|
|
56199
56457
|
}
|
|
56200
56458
|
}
|
|
56201
|
-
|
|
56202
|
-
|
|
56203
|
-
|
|
56204
|
-
|
|
56205
|
-
|
|
56206
|
-
|
|
56207
|
-
|
|
56208
|
-
|
|
56209
|
-
|
|
56210
|
-
|
|
56211
|
-
|
|
56212
|
-
|
|
56213
|
-
|
|
56214
|
-
|
|
56215
|
-
|
|
56216
|
-
|
|
56217
|
-
|
|
56218
|
-
|
|
56219
|
-
|
|
56220
|
-
|
|
56221
|
-
|
|
56222
|
-
|
|
56223
|
-
frontmatter: {
|
|
56224
|
-
...task.frontmatter,
|
|
56225
|
-
id: task.frontmatter.id || id,
|
|
56226
|
-
status: task.frontmatter.status || "Backlog"
|
|
56459
|
+
debouncedEmit(event, ...args) {
|
|
56460
|
+
const key = event + JSON.stringify(args);
|
|
56461
|
+
const existing = this.debounceTimers.get(key);
|
|
56462
|
+
if (existing) clearTimeout(existing);
|
|
56463
|
+
const timer = setTimeout(() => {
|
|
56464
|
+
this.debounceTimers.delete(key);
|
|
56465
|
+
this.emit(event, ...args);
|
|
56466
|
+
}, this.debounceDelay);
|
|
56467
|
+
this.debounceTimers.set(key, timer);
|
|
56468
|
+
}
|
|
56469
|
+
emit(event, ...args) {
|
|
56470
|
+
if (this.stopped) return;
|
|
56471
|
+
const handlers = this.listeners.get(event);
|
|
56472
|
+
handlers?.forEach((handler) => {
|
|
56473
|
+
if (event === "configChanged") {
|
|
56474
|
+
handler();
|
|
56475
|
+
} else if (event === "taskChanged") {
|
|
56476
|
+
handler(...args);
|
|
56477
|
+
} else if (event === "newTaskDetected") {
|
|
56478
|
+
handler(...args);
|
|
56479
|
+
} else {
|
|
56480
|
+
handler();
|
|
56227
56481
|
}
|
|
56228
|
-
};
|
|
56229
|
-
});
|
|
56230
|
-
return {
|
|
56231
|
-
frontmatter: null,
|
|
56232
|
-
title: "Project Kanban",
|
|
56233
|
-
columns: buildColumnsFromTasks(tasks, config.board.columns)
|
|
56234
|
-
};
|
|
56235
|
-
}
|
|
56236
|
-
function readTask(kandownDir, taskId) {
|
|
56237
|
-
const taskPath = join4(kandownDir, "tasks", `${taskId}.md`);
|
|
56238
|
-
if (!existsSync3(taskPath)) {
|
|
56239
|
-
return {
|
|
56240
|
-
frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
|
|
56241
|
-
body: ""
|
|
56242
|
-
};
|
|
56243
|
-
}
|
|
56244
|
-
const content = readFileSync3(taskPath, "utf8");
|
|
56245
|
-
const parsed = parseTaskFile(content);
|
|
56246
|
-
return {
|
|
56247
|
-
...parsed,
|
|
56248
|
-
frontmatter: {
|
|
56249
|
-
...parsed.frontmatter,
|
|
56250
|
-
id: parsed.frontmatter.id || taskId,
|
|
56251
|
-
status: parsed.frontmatter.status || "Backlog"
|
|
56252
|
-
}
|
|
56253
|
-
};
|
|
56254
|
-
}
|
|
56255
|
-
function readAgentDoc(kandownDir) {
|
|
56256
|
-
const root = getProjectRoot(kandownDir);
|
|
56257
|
-
const candidates = [
|
|
56258
|
-
join4(root, "AGENT_KANDOWN.md"),
|
|
56259
|
-
join4(kandownDir, "AGENT.md")
|
|
56260
|
-
];
|
|
56261
|
-
for (const candidate of candidates) {
|
|
56262
|
-
if (existsSync3(candidate)) {
|
|
56263
|
-
return readFileSync3(candidate, "utf8");
|
|
56264
|
-
}
|
|
56482
|
+
});
|
|
56265
56483
|
}
|
|
56266
|
-
|
|
56267
|
-
|
|
56268
|
-
|
|
56269
|
-
const taskPath = join4(kandownDir, "tasks", `${taskId}.md`);
|
|
56270
|
-
if (!existsSync3(taskPath)) return false;
|
|
56271
|
-
const parsed = readTask(kandownDir, taskId);
|
|
56272
|
-
writeFileSync2(taskPath, serializeTaskFile({
|
|
56273
|
-
...parsed.frontmatter,
|
|
56274
|
-
id: taskId,
|
|
56275
|
-
status: targetColumn
|
|
56276
|
-
}, parsed.body), "utf8");
|
|
56277
|
-
return true;
|
|
56484
|
+
};
|
|
56485
|
+
function createWatcher() {
|
|
56486
|
+
return new FileWatcher();
|
|
56278
56487
|
}
|
|
56279
56488
|
|
|
56280
56489
|
// src/cli/lib/agents.ts
|
|
@@ -56408,7 +56617,7 @@ function buildPrompt(agentDoc, taskContent, taskId, kandownDir) {
|
|
|
56408
56617
|
// src/cli/lib/launcher.ts
|
|
56409
56618
|
import { execSync, spawn } from "child_process";
|
|
56410
56619
|
import { writeFileSync as writeFileSync3 } from "fs";
|
|
56411
|
-
import { join as
|
|
56620
|
+
import { join as join6 } from "path";
|
|
56412
56621
|
import { tmpdir } from "os";
|
|
56413
56622
|
function isInTmux() {
|
|
56414
56623
|
return !!process.env.TMUX;
|
|
@@ -56432,7 +56641,7 @@ function launchAgent(opts) {
|
|
|
56432
56641
|
].join("\n");
|
|
56433
56642
|
const { systemPrompt, taskPrompt } = buildPrompt(agentDoc, taskFileContent, taskId, kandownDir);
|
|
56434
56643
|
moveTaskToColumn(kandownDir, taskId, "In Progress");
|
|
56435
|
-
const contextFile =
|
|
56644
|
+
const contextFile = join6(tmpdir(), `kandown-${taskId}-context.md`);
|
|
56436
56645
|
writeFileSync3(contextFile, `${systemPrompt}
|
|
56437
56646
|
|
|
56438
56647
|
---
|
|
@@ -56723,27 +56932,27 @@ function Board({ kandownDir }) {
|
|
|
56723
56932
|
setBoard(loaded);
|
|
56724
56933
|
setInstalledAgents(detectInstalledAgents());
|
|
56725
56934
|
}, [kandownDir]);
|
|
56935
|
+
const colIndexRef = (0, import_react36.useRef)(0);
|
|
56936
|
+
const rowIndexRef = (0, import_react36.useRef)(0);
|
|
56726
56937
|
(0, import_react36.useEffect)(() => {
|
|
56727
|
-
const
|
|
56728
|
-
|
|
56729
|
-
|
|
56730
|
-
|
|
56731
|
-
|
|
56732
|
-
|
|
56733
|
-
|
|
56938
|
+
const watcher = createWatcher();
|
|
56939
|
+
watcher.on("taskChanged", () => {
|
|
56940
|
+
const loaded = readBoard(kandownDir);
|
|
56941
|
+
setBoard(loaded);
|
|
56942
|
+
});
|
|
56943
|
+
watcher.on("newTaskDetected", (taskId) => {
|
|
56944
|
+
const loaded = readBoard(kandownDir);
|
|
56945
|
+
setBoard(loaded);
|
|
56946
|
+
setStatusMsg(`New task: ${taskId}`);
|
|
56947
|
+
setTimeout(() => setStatusMsg(""), 2e3);
|
|
56734
56948
|
});
|
|
56735
|
-
watcher.on("
|
|
56736
|
-
|
|
56737
|
-
|
|
56738
|
-
const loaded = readBoard(kandownDir);
|
|
56739
|
-
setBoard(loaded);
|
|
56740
|
-
setStatusMsg(`Reloaded (${event})`);
|
|
56741
|
-
setTimeout(() => setStatusMsg(""), 1500);
|
|
56742
|
-
}, 100);
|
|
56949
|
+
watcher.on("configChanged", () => {
|
|
56950
|
+
const loaded = readBoard(kandownDir);
|
|
56951
|
+
setBoard(loaded);
|
|
56743
56952
|
});
|
|
56953
|
+
watcher.start(kandownDir);
|
|
56744
56954
|
return () => {
|
|
56745
|
-
watcher.
|
|
56746
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
56955
|
+
watcher.stop();
|
|
56747
56956
|
};
|
|
56748
56957
|
}, [kandownDir]);
|
|
56749
56958
|
const reloadBoard = (0, import_react36.useCallback)(() => {
|