kandown 0.34.0 → 0.34.2

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/bin/kandown.js CHANGED
@@ -5,9 +5,8 @@ if (typeof globalThis.require === 'undefined') {
5
5
  }
6
6
 
7
7
  // src/cli/cli.ts
8
- import { existsSync as existsSync8, readFileSync as readFileSync8, copyFileSync as copyFileSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
9
- import { join as join8, resolve as resolve2, basename } from "path";
10
- import { spawn as spawn5, spawnSync } from "child_process";
8
+ import { existsSync as existsSync11 } from "fs";
9
+ import { join as join12 } from "path";
11
10
 
12
11
  // src/cli/lib/updater.ts
13
12
  import { existsSync, readFileSync, writeFileSync, unlinkSync, statSync, mkdirSync } from "fs";
@@ -16,7 +15,7 @@ import { spawn, execSync } from "child_process";
16
15
  import { homedir } from "os";
17
16
 
18
17
  // src/lib/version.ts
19
- var KANDOWN_VERSION = "0.34.0";
18
+ var KANDOWN_VERSION = "0.34.2";
20
19
 
21
20
  // src/cli/lib/updater.ts
22
21
  import { fileURLToPath } from "url";
@@ -209,7 +208,7 @@ async function checkForUpdate(argv = process.argv) {
209
208
  }
210
209
  } catch {
211
210
  }
212
- const latest = await new Promise((resolve3) => {
211
+ const latest = await new Promise((resolve5) => {
213
212
  const child2 = spawn("npm", ["view", "kandown", "version"], {
214
213
  timeout: 6e3,
215
214
  stdio: ["pipe", "pipe", "pipe"],
@@ -222,11 +221,11 @@ async function checkForUpdate(argv = process.argv) {
222
221
  });
223
222
  child2.stderr.on("data", () => {
224
223
  });
225
- child2.on("error", () => resolve3(null));
224
+ child2.on("error", () => resolve5(null));
226
225
  child2.on("close", (code) => {
227
- if (code !== 0) return resolve3(null);
226
+ if (code !== 0) return resolve5(null);
228
227
  const v = stdout.trim().replace(/^"|"$/g, "");
229
- resolve3(v || null);
228
+ resolve5(v || null);
230
229
  });
231
230
  });
232
231
  if (!latest) return;
@@ -264,15 +263,192 @@ ${now}`, "utf8");
264
263
  process.exit(0);
265
264
  }
266
265
 
266
+ // src/cli/lib/daemon.ts
267
+ import { existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2 } from "fs";
268
+ import { dirname as dirname2, join as join2 } from "path";
269
+ import { execFileSync, spawn as spawn2 } from "child_process";
270
+ import { createConnection } from "net";
271
+ function metadataPath(kandownDir) {
272
+ return join2(kandownDir, "daemon.json");
273
+ }
274
+ function isRecord(value) {
275
+ return typeof value === "object" && value !== null;
276
+ }
277
+ function parseMetadata(value) {
278
+ if (!isRecord(value)) return null;
279
+ const { pid, port, url, kandownDir, startedAt, version, token } = value;
280
+ if (typeof pid !== "number" || !Number.isInteger(pid)) return null;
281
+ if (typeof port !== "number" || !Number.isInteger(port)) return null;
282
+ if (typeof url !== "string" || typeof kandownDir !== "string") return null;
283
+ if (typeof startedAt !== "string") return null;
284
+ if (version !== null && typeof version !== "string" && version !== void 0) return null;
285
+ if (token !== null && typeof token !== "string" && token !== void 0) return null;
286
+ return { pid, port, url, kandownDir, startedAt, version: version ?? null, token: typeof token === "string" ? token : null };
287
+ }
288
+ function readDaemonMetadata(kandownDir) {
289
+ const path = metadataPath(kandownDir);
290
+ if (!existsSync2(path)) return null;
291
+ try {
292
+ return parseMetadata(JSON.parse(readFileSync2(path, "utf8")));
293
+ } catch {
294
+ return null;
295
+ }
296
+ }
297
+ function removeDaemonMetadata(kandownDir) {
298
+ try {
299
+ const path = metadataPath(kandownDir);
300
+ if (existsSync2(path)) unlinkSync2(path);
301
+ } catch {
302
+ }
303
+ }
304
+ function isProcessAlive(pid) {
305
+ try {
306
+ process.kill(pid, 0);
307
+ return true;
308
+ } catch {
309
+ return false;
310
+ }
311
+ }
312
+ function parseRemoteDaemonInfo(value) {
313
+ if (!isRecord(value)) return null;
314
+ const { ok, pid, kandownDir, version } = value;
315
+ if (ok !== true || typeof pid !== "number" || !Number.isInteger(pid) || typeof kandownDir !== "string") return null;
316
+ if (version !== null && typeof version !== "string" && version !== void 0) return null;
317
+ return { ok, pid, kandownDir, version: typeof version === "string" ? version : null };
318
+ }
319
+ async function fetchDaemonInfo(port) {
320
+ try {
321
+ const response = await fetch(`http://127.0.0.1:${port}/api/daemon`, {
322
+ signal: AbortSignal.timeout(700)
323
+ });
324
+ if (!response.ok) return null;
325
+ return parseRemoteDaemonInfo(await response.json());
326
+ } catch {
327
+ return null;
328
+ }
329
+ }
330
+ function isPortListening(port, timeoutMs = 400) {
331
+ return new Promise((resolve5) => {
332
+ const socket = createConnection({ port, host: "127.0.0.1" }, () => {
333
+ socket.destroy();
334
+ resolve5(true);
335
+ });
336
+ socket.on("error", () => resolve5(false));
337
+ socket.setTimeout(timeoutMs);
338
+ socket.on("timeout", () => {
339
+ socket.destroy();
340
+ resolve5(false);
341
+ });
342
+ });
343
+ }
344
+ async function getDaemonStatus(kandownDir) {
345
+ const metadata = readDaemonMetadata(kandownDir);
346
+ if (!metadata) return { running: false, metadata: null };
347
+ if (!isProcessAlive(metadata.pid)) {
348
+ removeDaemonMetadata(kandownDir);
349
+ return { running: false, metadata: null };
350
+ }
351
+ const remote = await fetchDaemonInfo(metadata.port);
352
+ if (!remote) {
353
+ return { running: false, metadata: null };
354
+ }
355
+ if (remote.pid !== metadata.pid || remote.kandownDir !== kandownDir) {
356
+ removeDaemonMetadata(kandownDir);
357
+ return { running: false, metadata: null };
358
+ }
359
+ return { running: true, metadata: { ...metadata, version: remote.version ?? metadata.version } };
360
+ }
361
+ async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
362
+ const started = Date.now();
363
+ while (Date.now() - started < timeoutMs) {
364
+ const metadata = readDaemonMetadata(kandownDir);
365
+ if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
366
+ return { running: true, metadata };
367
+ }
368
+ await new Promise((resolve5) => setTimeout(resolve5, 120));
369
+ }
370
+ return { running: false, metadata: null };
371
+ }
372
+ async function startProjectDaemon(kandownDir, preferredPort) {
373
+ const current = await getDaemonStatus(kandownDir);
374
+ if (current.running) {
375
+ if (current.metadata?.version === getCurrentVersion()) return current;
376
+ await stopProjectDaemon(kandownDir);
377
+ }
378
+ const cliPath = join2(PKG_ROOT, "bin", "kandown.js");
379
+ if (!existsSync2(cliPath)) throw new Error(`Cannot locate kandown CLI entrypoint at ${cliPath}`);
380
+ const args = [
381
+ cliPath,
382
+ "--no-update-check",
383
+ "daemon",
384
+ "run",
385
+ "--path",
386
+ kandownDir
387
+ ];
388
+ if (typeof preferredPort === "number" && Number.isInteger(preferredPort)) {
389
+ args.push("--port", String(preferredPort));
390
+ }
391
+ const child = spawn2(process.execPath, args, {
392
+ cwd: dirname2(kandownDir),
393
+ detached: true,
394
+ stdio: "ignore",
395
+ env: { ...process.env, KANDOWN_DAEMON: "1" }
396
+ });
397
+ child.unref();
398
+ return waitForDaemon(kandownDir);
399
+ }
400
+ async function isOwnedKandownDaemon(pid, port, kandownDir) {
401
+ const remote = await fetchDaemonInfo(port);
402
+ if (remote) return remote.pid === pid && remote.kandownDir === kandownDir;
403
+ try {
404
+ const cmd = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
405
+ encoding: "utf8",
406
+ timeout: 2e3
407
+ }).trim();
408
+ return /kandown/.test(cmd) && cmd.includes(kandownDir);
409
+ } catch {
410
+ return false;
411
+ }
412
+ }
413
+ async function stopProjectDaemon(kandownDir) {
414
+ const metadata = readDaemonMetadata(kandownDir);
415
+ if (!metadata) return false;
416
+ const pid = metadata.pid;
417
+ if (!isProcessAlive(pid)) {
418
+ removeDaemonMetadata(kandownDir);
419
+ return false;
420
+ }
421
+ if (!await isOwnedKandownDaemon(pid, metadata.port, kandownDir)) {
422
+ removeDaemonMetadata(kandownDir);
423
+ return false;
424
+ }
425
+ try {
426
+ process.kill(pid, "SIGTERM");
427
+ } catch {
428
+ }
429
+ const started = Date.now();
430
+ while (Date.now() - started < 2500 && isProcessAlive(pid)) {
431
+ await new Promise((resolve5) => setTimeout(resolve5, 100));
432
+ }
433
+ if (isProcessAlive(pid)) {
434
+ try {
435
+ process.kill(pid, "SIGKILL");
436
+ } catch {
437
+ }
438
+ }
439
+ removeDaemonMetadata(kandownDir);
440
+ return true;
441
+ }
442
+
267
443
  // src/cli/lib/board-reader.ts
268
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, unlinkSync as unlinkSync3 } from "fs";
269
- import { dirname as dirname2, join as join3 } from "path";
444
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, mkdirSync as mkdirSync2, unlinkSync as unlinkSync4 } from "fs";
445
+ import { dirname as dirname3, join as join4 } from "path";
270
446
  import { fileURLToPath as fileURLToPath2 } from "url";
271
447
  import { homedir as homedir2 } from "os";
272
- import { execFileSync } from "child_process";
448
+ import { execFileSync as execFileSync2 } from "child_process";
273
449
 
274
450
  // src/cli/lib/atomic-write.ts
275
- import { renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
451
+ import { renameSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "fs";
276
452
  function atomicWriteFileSync(path, content) {
277
453
  const tmp = `${path}.${process.pid}.tmp`;
278
454
  try {
@@ -280,7 +456,7 @@ function atomicWriteFileSync(path, content) {
280
456
  renameSync(tmp, path);
281
457
  } catch (e) {
282
458
  try {
283
- unlinkSync2(tmp);
459
+ unlinkSync3(tmp);
284
460
  } catch {
285
461
  }
286
462
  throw e;
@@ -496,8 +672,8 @@ function serializeTaskFile(frontmatter, body) {
496
672
  }
497
673
 
498
674
  // src/cli/lib/config.ts
499
- import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
500
- import { join as join2 } from "path";
675
+ import { readFileSync as readFileSync3, existsSync as existsSync3, readdirSync, statSync as statSync2 } from "fs";
676
+ import { join as join3 } from "path";
501
677
  var DEFAULT_CONFIG = {
502
678
  ui: { language: "en", theme: "auto", skin: "kandown", font: "inter" },
503
679
  agent: { suggestFollowUp: false, maxSuggestions: 3 },
@@ -526,11 +702,11 @@ var DEFAULT_CONFIG = {
526
702
  }
527
703
  };
528
704
  function loadConfig(kandownDir) {
529
- const configPath = join2(kandownDir, "kandown.json");
530
- if (!existsSync2(configPath)) return structuredClone(DEFAULT_CONFIG);
705
+ const configPath = join3(kandownDir, "kandown.json");
706
+ if (!existsSync3(configPath)) return structuredClone(DEFAULT_CONFIG);
531
707
  let raw;
532
708
  try {
533
- raw = JSON.parse(readFileSync2(configPath, "utf8"));
709
+ raw = JSON.parse(readFileSync3(configPath, "utf8"));
534
710
  } catch (e) {
535
711
  const err2 = e;
536
712
  if (err2.code === "ENOENT") return structuredClone(DEFAULT_CONFIG);
@@ -561,22 +737,22 @@ function loadConfig(kandownDir) {
561
737
  return merged;
562
738
  }
563
739
  function saveConfig(kandownDir, config) {
564
- const configPath = join2(kandownDir, "kandown.json");
740
+ const configPath = join3(kandownDir, "kandown.json");
565
741
  atomicWriteFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
566
742
  }
567
743
 
568
744
  // src/cli/lib/board-reader.ts
569
745
  function getProjectRoot(kandownDir) {
570
- return dirname2(kandownDir);
746
+ return dirname3(kandownDir);
571
747
  }
572
748
  function getTasksDir(kandownDir) {
573
- return join3(getProjectRoot(kandownDir), "tasks");
749
+ return join4(getProjectRoot(kandownDir), "tasks");
574
750
  }
575
751
  function listTaskIds(kandownDir) {
576
752
  const tasksDir = getTasksDir(kandownDir);
577
753
  const ids = /* @__PURE__ */ new Set();
578
- for (const directory of [tasksDir, join3(tasksDir, "archive")]) {
579
- if (!existsSync3(directory)) continue;
754
+ for (const directory of [tasksDir, join4(tasksDir, "archive")]) {
755
+ if (!existsSync4(directory)) continue;
580
756
  for (const name of readdirSync2(directory).filter((entry) => entry.endsWith(".md"))) {
581
757
  ids.add(name.slice(0, -3));
582
758
  }
@@ -586,10 +762,10 @@ function listTaskIds(kandownDir) {
586
762
  function findTaskPath(kandownDir, taskId) {
587
763
  if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) return null;
588
764
  const tasksDir = getTasksDir(kandownDir);
589
- const activePath = join3(tasksDir, `${taskId}.md`);
590
- if (existsSync3(activePath)) return activePath;
591
- const archivedPath = join3(tasksDir, "archive", `${taskId}.md`);
592
- return existsSync3(archivedPath) ? archivedPath : null;
765
+ const activePath = join4(tasksDir, `${taskId}.md`);
766
+ if (existsSync4(activePath)) return activePath;
767
+ const archivedPath = join4(tasksDir, "archive", `${taskId}.md`);
768
+ return existsSync4(archivedPath) ? archivedPath : null;
593
769
  }
594
770
  function readBoard(kandownDir) {
595
771
  const config = loadConfig(kandownDir);
@@ -624,7 +800,7 @@ function readTask(kandownDir, taskId) {
624
800
  body: ""
625
801
  };
626
802
  }
627
- const content = readFileSync3(taskPath2, "utf8");
803
+ const content = readFileSync4(taskPath2, "utf8");
628
804
  const parsed = parseTaskFile(content);
629
805
  return {
630
806
  ...parsed,
@@ -635,37 +811,37 @@ function readTask(kandownDir, taskId) {
635
811
  }
636
812
  };
637
813
  }
638
- var PKG_ROOT2 = dirname2(dirname2(fileURLToPath2(import.meta.url)));
814
+ var PKG_ROOT2 = dirname3(dirname3(fileURLToPath2(import.meta.url)));
639
815
  function readAgentDoc(kandownDir) {
640
816
  const sections = [];
641
817
  try {
642
- sections.push(readFileSync3(join3(PKG_ROOT2, "templates", "AGENT_KANDOWN.md"), "utf8").trim());
818
+ sections.push(readFileSync4(join4(PKG_ROOT2, "templates", "AGENT_KANDOWN.md"), "utf8").trim());
643
819
  } catch (e) {
644
820
  console.warn("[kandown] Could not read base agent rules:", e.message);
645
821
  }
646
- const globalPath = join3(homedir2(), ".kandown", "instructions.md");
647
- if (existsSync3(globalPath)) {
822
+ const globalPath = join4(homedir2(), ".kandown", "instructions.md");
823
+ if (existsSync4(globalPath)) {
648
824
  try {
649
825
  sections.push(`## Global instructions
650
826
 
651
- ${readFileSync3(globalPath, "utf8").trim()}`);
827
+ ${readFileSync4(globalPath, "utf8").trim()}`);
652
828
  } catch (e) {
653
829
  console.warn(`[kandown] Could not read ${globalPath}:`, e.message);
654
830
  }
655
831
  }
656
- const projectPath = join3(kandownDir, "instructions.md");
657
- if (existsSync3(projectPath)) {
832
+ const projectPath = join4(kandownDir, "instructions.md");
833
+ if (existsSync4(projectPath)) {
658
834
  try {
659
835
  sections.push(`## Project-specific instructions
660
836
 
661
- ${readFileSync3(projectPath, "utf8").trim()}`);
837
+ ${readFileSync4(projectPath, "utf8").trim()}`);
662
838
  } catch (e) {
663
839
  console.warn(`[kandown] Could not read ${projectPath}:`, e.message);
664
840
  }
665
841
  }
666
842
  try {
667
843
  const root = getProjectRoot(kandownDir);
668
- const gitLog = execFileSync("git", ["log", "-n", "5", "--oneline", "--", "tasks/"], { cwd: root, encoding: "utf8" }).trim();
844
+ const gitLog = execFileSync2("git", ["log", "-n", "5", "--oneline", "--", "tasks/"], { cwd: root, encoding: "utf8" }).trim();
669
845
  if (gitLog) {
670
846
  sections.push(`## Recent Task Activity (Git History)
671
847
 
@@ -681,7 +857,7 @@ function moveTaskToColumn(kandownDir, taskId, targetColumn) {
681
857
  const taskPath2 = findTaskPath(kandownDir, taskId);
682
858
  if (!taskPath2) return false;
683
859
  try {
684
- const prevContent = readFileSync3(taskPath2, "utf8");
860
+ const prevContent = readFileSync4(taskPath2, "utf8");
685
861
  const parsed = readTask(kandownDir, taskId);
686
862
  const newContent = serializeTaskFile({
687
863
  ...parsed.frontmatter,
@@ -705,13 +881,13 @@ function moveTaskToColumn(kandownDir, taskId, targetColumn) {
705
881
  }
706
882
  function pushUndo(kandownDir, record) {
707
883
  try {
708
- const undoDir = join3(kandownDir, ".undo");
709
- if (!existsSync3(undoDir)) mkdirSync2(undoDir, { recursive: true });
710
- const logPath = join3(undoDir, "log.json");
884
+ const undoDir = join4(kandownDir, ".undo");
885
+ if (!existsSync4(undoDir)) mkdirSync2(undoDir, { recursive: true });
886
+ const logPath = join4(undoDir, "log.json");
711
887
  let list = [];
712
- if (existsSync3(logPath)) {
888
+ if (existsSync4(logPath)) {
713
889
  try {
714
- list = JSON.parse(readFileSync3(logPath, "utf8"));
890
+ list = JSON.parse(readFileSync4(logPath, "utf8"));
715
891
  } catch {
716
892
  list = [];
717
893
  }
@@ -724,7 +900,7 @@ function pushUndo(kandownDir, record) {
724
900
  }
725
901
  function createTaskInBoard(kandownDir, rawInput, status) {
726
902
  const tasksDir = getTasksDir(kandownDir);
727
- if (!existsSync3(tasksDir)) mkdirSync2(tasksDir, { recursive: true });
903
+ if (!existsSync4(tasksDir)) mkdirSync2(tasksDir, { recursive: true });
728
904
  const ids = listTaskIds(kandownDir);
729
905
  let maxN = 0;
730
906
  for (const id of ids) {
@@ -776,7 +952,7 @@ function createTaskInBoard(kandownDir, rawInput, status) {
776
952
  if (due) fm.due = due;
777
953
  if (depends_on.length > 0) fm.depends_on = depends_on;
778
954
  const content = serializeTaskFile(fm, "");
779
- const taskPath2 = join3(tasksDir, `${newId}.md`);
955
+ const taskPath2 = join4(tasksDir, `${newId}.md`);
780
956
  atomicWriteFileSync(taskPath2, content);
781
957
  pushUndo(kandownDir, {
782
958
  type: "create",
@@ -790,21 +966,21 @@ function createTaskInBoard(kandownDir, rawInput, status) {
790
966
  }
791
967
  function archiveTaskInBoard(kandownDir, taskId) {
792
968
  const tasksDir = getTasksDir(kandownDir);
793
- const taskPath2 = join3(tasksDir, `${taskId}.md`);
794
- if (!existsSync3(taskPath2)) return false;
969
+ const taskPath2 = join4(tasksDir, `${taskId}.md`);
970
+ if (!existsSync4(taskPath2)) return false;
795
971
  try {
796
- const prevContent = readFileSync3(taskPath2, "utf8");
797
- const archiveDir = join3(tasksDir, "archive");
798
- if (!existsSync3(archiveDir)) mkdirSync2(archiveDir, { recursive: true });
972
+ const prevContent = readFileSync4(taskPath2, "utf8");
973
+ const archiveDir = join4(tasksDir, "archive");
974
+ if (!existsSync4(archiveDir)) mkdirSync2(archiveDir, { recursive: true });
799
975
  const parsed = readTask(kandownDir, taskId);
800
976
  const newContent = serializeTaskFile({
801
977
  ...parsed.frontmatter,
802
978
  id: taskId,
803
979
  archived: true
804
980
  }, parsed.body);
805
- const destPath = join3(archiveDir, `${taskId}.md`);
981
+ const destPath = join4(archiveDir, `${taskId}.md`);
806
982
  atomicWriteFileSync(destPath, newContent);
807
- unlinkSync3(taskPath2);
983
+ unlinkSync4(taskPath2);
808
984
  pushUndo(kandownDir, {
809
985
  type: "archive",
810
986
  taskId,
@@ -819,675 +995,48 @@ function archiveTaskInBoard(kandownDir, taskId) {
819
995
  }
820
996
  }
821
997
 
822
- // src/cli/lib/daemon.ts
823
- import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync as unlinkSync4 } from "fs";
824
- import { dirname as dirname3, join as join4 } from "path";
825
- import { execFileSync as execFileSync2, spawn as spawn2 } from "child_process";
826
- import { createConnection } from "net";
827
- function metadataPath(kandownDir) {
828
- return join4(kandownDir, "daemon.json");
829
- }
830
- function isRecord(value) {
831
- return typeof value === "object" && value !== null;
832
- }
833
- function parseMetadata(value) {
834
- if (!isRecord(value)) return null;
835
- const { pid, port, url, kandownDir, startedAt, version, token } = value;
836
- if (typeof pid !== "number" || !Number.isInteger(pid)) return null;
837
- if (typeof port !== "number" || !Number.isInteger(port)) return null;
838
- if (typeof url !== "string" || typeof kandownDir !== "string") return null;
839
- if (typeof startedAt !== "string") return null;
840
- if (version !== null && typeof version !== "string" && version !== void 0) return null;
841
- if (token !== null && typeof token !== "string" && token !== void 0) return null;
842
- return { pid, port, url, kandownDir, startedAt, version: version ?? null, token: typeof token === "string" ? token : null };
843
- }
844
- function readDaemonMetadata(kandownDir) {
845
- const path = metadataPath(kandownDir);
846
- if (!existsSync4(path)) return null;
847
- try {
848
- return parseMetadata(JSON.parse(readFileSync4(path, "utf8")));
849
- } catch {
850
- return null;
851
- }
852
- }
853
- function removeDaemonMetadata(kandownDir) {
854
- try {
855
- const path = metadataPath(kandownDir);
856
- if (existsSync4(path)) unlinkSync4(path);
857
- } catch {
858
- }
859
- }
860
- function isProcessAlive(pid) {
861
- try {
862
- process.kill(pid, 0);
863
- return true;
864
- } catch {
865
- return false;
866
- }
998
+ // src/cli/lib/mcp.ts
999
+ import { existsSync as existsSync5 } from "fs";
1000
+ import { join as join5 } from "path";
1001
+ function startMcpServer(kandownDir) {
1002
+ process.stdin.setEncoding("utf8");
1003
+ let buffer = "";
1004
+ process.stdin.on("data", (chunk) => {
1005
+ buffer += chunk;
1006
+ const lines = buffer.split("\n");
1007
+ buffer = lines.pop() ?? "";
1008
+ for (const line of lines) {
1009
+ const trimmed = line.trim();
1010
+ if (!trimmed) continue;
1011
+ try {
1012
+ const req = JSON.parse(trimmed);
1013
+ handleJsonRpc(kandownDir, req);
1014
+ } catch (e) {
1015
+ sendResponse(null, { error: { code: -32700, message: "Parse error" } });
1016
+ }
1017
+ }
1018
+ });
867
1019
  }
868
- function parseRemoteDaemonInfo(value) {
869
- if (!isRecord(value)) return null;
870
- const { ok, pid, kandownDir, version } = value;
871
- if (ok !== true || typeof pid !== "number" || !Number.isInteger(pid) || typeof kandownDir !== "string") return null;
872
- if (version !== null && typeof version !== "string" && version !== void 0) return null;
873
- return { ok, pid, kandownDir, version: typeof version === "string" ? version : null };
1020
+ function sendResponse(id, resultOrError) {
1021
+ if (id === void 0) return;
1022
+ const resp = {
1023
+ jsonrpc: "2.0",
1024
+ id,
1025
+ ...resultOrError
1026
+ };
1027
+ process.stdout.write(JSON.stringify(resp) + "\n");
874
1028
  }
875
- async function fetchDaemonInfo(port) {
876
- try {
877
- const response = await fetch(`http://127.0.0.1:${port}/api/daemon`, {
878
- signal: AbortSignal.timeout(700)
1029
+ function handleJsonRpc(kandownDir, req) {
1030
+ const { id, method, params } = req;
1031
+ if (method === "initialize") {
1032
+ sendResponse(id, {
1033
+ result: {
1034
+ protocolVersion: "2024-11-05",
1035
+ capabilities: { tools: {} },
1036
+ serverInfo: { name: "kandown", version: "0.20.0" }
1037
+ }
879
1038
  });
880
- if (!response.ok) return null;
881
- return parseRemoteDaemonInfo(await response.json());
882
- } catch {
883
- return null;
884
- }
885
- }
886
- function isPortListening(port, timeoutMs = 400) {
887
- return new Promise((resolve3) => {
888
- const socket = createConnection({ port, host: "127.0.0.1" }, () => {
889
- socket.destroy();
890
- resolve3(true);
891
- });
892
- socket.on("error", () => resolve3(false));
893
- socket.setTimeout(timeoutMs);
894
- socket.on("timeout", () => {
895
- socket.destroy();
896
- resolve3(false);
897
- });
898
- });
899
- }
900
- async function getDaemonStatus(kandownDir) {
901
- const metadata = readDaemonMetadata(kandownDir);
902
- if (!metadata) return { running: false, metadata: null };
903
- if (!isProcessAlive(metadata.pid)) {
904
- removeDaemonMetadata(kandownDir);
905
- return { running: false, metadata: null };
906
- }
907
- const remote = await fetchDaemonInfo(metadata.port);
908
- if (!remote) {
909
- return { running: false, metadata: null };
910
- }
911
- if (remote.pid !== metadata.pid || remote.kandownDir !== kandownDir) {
912
- removeDaemonMetadata(kandownDir);
913
- return { running: false, metadata: null };
914
- }
915
- return { running: true, metadata: { ...metadata, version: remote.version ?? metadata.version } };
916
- }
917
- async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
918
- const started = Date.now();
919
- while (Date.now() - started < timeoutMs) {
920
- const metadata = readDaemonMetadata(kandownDir);
921
- if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
922
- return { running: true, metadata };
923
- }
924
- await new Promise((resolve3) => setTimeout(resolve3, 120));
925
- }
926
- return { running: false, metadata: null };
927
- }
928
- async function startProjectDaemon(kandownDir, preferredPort) {
929
- const current = await getDaemonStatus(kandownDir);
930
- if (current.running) {
931
- if (current.metadata?.version === getCurrentVersion()) return current;
932
- await stopProjectDaemon(kandownDir);
933
- }
934
- const cliPath = join4(PKG_ROOT, "bin", "kandown.js");
935
- if (!existsSync4(cliPath)) throw new Error(`Cannot locate kandown CLI entrypoint at ${cliPath}`);
936
- const args = [
937
- cliPath,
938
- "--no-update-check",
939
- "daemon",
940
- "run",
941
- "--path",
942
- kandownDir
943
- ];
944
- if (typeof preferredPort === "number" && Number.isInteger(preferredPort)) {
945
- args.push("--port", String(preferredPort));
946
- }
947
- const child = spawn2(process.execPath, args, {
948
- cwd: dirname3(kandownDir),
949
- detached: true,
950
- stdio: "ignore",
951
- env: { ...process.env, KANDOWN_DAEMON: "1" }
952
- });
953
- child.unref();
954
- return waitForDaemon(kandownDir);
955
- }
956
- async function isOwnedKandownDaemon(pid, port, kandownDir) {
957
- const remote = await fetchDaemonInfo(port);
958
- if (remote) return remote.pid === pid && remote.kandownDir === kandownDir;
959
- try {
960
- const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], {
961
- encoding: "utf8",
962
- timeout: 2e3
963
- }).trim();
964
- return /kandown/.test(cmd) && cmd.includes(kandownDir);
965
- } catch {
966
- return false;
967
- }
968
- }
969
- async function stopProjectDaemon(kandownDir) {
970
- const metadata = readDaemonMetadata(kandownDir);
971
- if (!metadata) return false;
972
- const pid = metadata.pid;
973
- if (!isProcessAlive(pid)) {
974
- removeDaemonMetadata(kandownDir);
975
- return false;
976
- }
977
- if (!await isOwnedKandownDaemon(pid, metadata.port, kandownDir)) {
978
- removeDaemonMetadata(kandownDir);
979
- return false;
980
- }
981
- try {
982
- process.kill(pid, "SIGTERM");
983
- } catch {
984
- }
985
- const started = Date.now();
986
- while (Date.now() - started < 2500 && isProcessAlive(pid)) {
987
- await new Promise((resolve3) => setTimeout(resolve3, 100));
988
- }
989
- if (isProcessAlive(pid)) {
990
- try {
991
- process.kill(pid, "SIGKILL");
992
- } catch {
993
- }
994
- }
995
- removeDaemonMetadata(kandownDir);
996
- return true;
997
- }
998
-
999
- // src/cli/lib/server.ts
1000
- import { createServer } from "http";
1001
- import { existsSync as existsSync5, readFileSync as readFileSync5, copyFileSync, unlinkSync as unlinkSync5, mkdirSync as mkdirSync3 } from "fs";
1002
- import { join as join5 } from "path";
1003
- import { spawn as spawn3 } from "child_process";
1004
- var START_PORT_RANGE = 2050;
1005
- var END_PORT_RANGE = 2099;
1006
- var UNSAFE_PORTS = /* @__PURE__ */ new Set([2049, 4045, 6e3, 6665, 6666, 6667, 6668, 6669, 6697]);
1007
- var sseClients = [];
1008
- var nextClientId = 1;
1009
- function broadcastSseEvent(data) {
1010
- const payload = `data: ${JSON.stringify(data)}
1011
-
1012
- `;
1013
- sseClients.forEach((c2) => c2.res.write(payload));
1014
- }
1015
- function handleCors(res) {
1016
- res.writeHead(204, {
1017
- "Access-Control-Allow-Origin": "*",
1018
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1019
- "Access-Control-Allow-Headers": "Content-Type, X-Kandown-Token"
1020
- });
1021
- res.end();
1022
- }
1023
- function writeJson(res, status, data) {
1024
- res.writeHead(status, {
1025
- "Content-Type": "application/json",
1026
- "Access-Control-Allow-Origin": "*"
1027
- });
1028
- res.end(JSON.stringify(data));
1029
- }
1030
- function writeText(res, status, text) {
1031
- res.writeHead(status, {
1032
- "Content-Type": "text/plain; charset=utf-8",
1033
- "Access-Control-Allow-Origin": "*"
1034
- });
1035
- res.end(text);
1036
- }
1037
- function readRequestBody(req) {
1038
- return new Promise((resolveBody, rejectBody) => {
1039
- let body = "";
1040
- req.setEncoding("utf8");
1041
- req.on("data", (chunk) => {
1042
- body += chunk;
1043
- });
1044
- req.on("end", () => resolveBody(body));
1045
- req.on("error", rejectBody);
1046
- });
1047
- }
1048
- function syncProjectKandownHtml(kandownDir) {
1049
- try {
1050
- const projectHtml = join5(kandownDir, "kandown.html");
1051
- const distHtml = join5(PKG_ROOT, "dist", "index.html");
1052
- if (!existsSync5(distHtml)) return false;
1053
- if (!existsSync5(projectHtml)) {
1054
- copyFileSync(distHtml, projectHtml);
1055
- return true;
1056
- }
1057
- const currentContent = readFileSync5(projectHtml, "utf8");
1058
- const newContent = readFileSync5(distHtml, "utf8");
1059
- if (currentContent !== newContent) {
1060
- atomicWriteFileSync(projectHtml, newContent);
1061
- return true;
1062
- }
1063
- } catch {
1064
- }
1065
- return false;
1066
- }
1067
- function readDaemonPort(kandownDir) {
1068
- try {
1069
- const raw = JSON.parse(readFileSync5(join5(kandownDir, "daemon.json"), "utf8"));
1070
- return typeof raw.port === "number" && Number.isInteger(raw.port) ? raw.port : null;
1071
- } catch {
1072
- return null;
1073
- }
1074
- }
1075
- function restartDaemonAfterUpdateResponse(res, kandownDir) {
1076
- const cliPath = process.argv[1];
1077
- if (!cliPath) return;
1078
- const args = ["--no-update-check", "daemon", "run", "--path", kandownDir];
1079
- const port = readDaemonPort(kandownDir);
1080
- if (port !== null) args.push("--port", String(port));
1081
- const launcher = `
1082
- const { spawn } = require('node:child_process');
1083
- const [nodeBin, cliPath, ...cliArgs] = process.argv.slice(1);
1084
- setTimeout(() => {
1085
- const child = spawn(nodeBin, [cliPath, ...cliArgs], {
1086
- detached: true,
1087
- stdio: 'ignore',
1088
- env: { ...process.env, KANDOWN_DAEMON: '1' },
1089
- });
1090
- child.unref();
1091
- }, 350);
1092
- `;
1093
- res.on("finish", () => {
1094
- const child = spawn3(process.execPath, ["-e", launcher, process.execPath, cliPath, ...args], {
1095
- detached: true,
1096
- stdio: "ignore",
1097
- env: { ...process.env, KANDOWN_DAEMON: "1" }
1098
- });
1099
- child.unref();
1100
- setTimeout(() => process.exit(0), 50).unref();
1101
- });
1102
- }
1103
- async function handleApi(req, res, url, kandownDir) {
1104
- const path = url.pathname;
1105
- const method = req.method || "GET";
1106
- if (path === "/api/daemon" && method === "GET") {
1107
- return writeJson(res, 200, {
1108
- ok: true,
1109
- pid: process.pid,
1110
- kandownDir,
1111
- version: getCurrentVersion(),
1112
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1113
- agentHook: process.env.KANDOWN_AGENT_HOOK_URL ? { enabled: true, label: process.env.KANDOWN_AGENT_HOOK_LABEL || "Send to Agent" } : null
1114
- });
1115
- }
1116
- if (path === "/api/version" && method === "GET") {
1117
- return writeJson(res, 200, {
1118
- version: getCurrentVersion()
1119
- });
1120
- }
1121
- if (path === "/api/update/check" && method === "GET") {
1122
- const current = getCurrentVersion();
1123
- const latest = await new Promise((resolve3) => {
1124
- const child = spawn3("npm", ["view", "kandown", "version"], {
1125
- timeout: 4e3,
1126
- stdio: ["pipe", "pipe", "pipe"],
1127
- env: { ...process.env },
1128
- detached: false
1129
- });
1130
- let stdout = "";
1131
- child.stdout.on("data", (d) => {
1132
- stdout += d;
1133
- });
1134
- child.stderr.on("data", () => {
1135
- });
1136
- child.on("error", () => resolve3(null));
1137
- child.on("close", (code) => {
1138
- if (code !== 0) return resolve3(null);
1139
- resolve3(stdout.trim().replace(/^"|"$/g, "") || null);
1140
- });
1141
- });
1142
- const updateAvailable = latest ? semverGt(latest, current) > 0 : false;
1143
- return writeJson(res, 200, {
1144
- current,
1145
- latest: latest || current,
1146
- updateAvailable
1147
- });
1148
- }
1149
- if (path === "/api/update/apply" && method === "POST") {
1150
- const current = getCurrentVersion();
1151
- const latest = await new Promise((resolve3) => {
1152
- const child = spawn3("npm", ["view", "kandown", "version"], {
1153
- timeout: 4e3,
1154
- stdio: ["pipe", "pipe", "pipe"],
1155
- env: { ...process.env },
1156
- detached: false
1157
- });
1158
- let stdout = "";
1159
- child.stdout.on("data", (d) => {
1160
- stdout += d;
1161
- });
1162
- child.on("error", () => resolve3(null));
1163
- child.on("close", (code) => resolve3(code === 0 ? stdout.trim() : null));
1164
- });
1165
- const targetVersion = latest || current;
1166
- const ok = await performGlobalPackageUpdate(`kandown@${targetVersion}`);
1167
- syncProjectKandownHtml(kandownDir);
1168
- if (ok) {
1169
- restartDaemonAfterUpdateResponse(res, kandownDir);
1170
- writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully; daemon is restarting" });
1171
- broadcastSseEvent({ type: "update", version: targetVersion });
1172
- } else {
1173
- writeJson(res, 500, { ok: false, message: "Global package installation failed" });
1174
- }
1175
- return;
1176
- }
1177
- if (path === "/api/events" && method === "GET") {
1178
- res.writeHead(200, {
1179
- "Content-Type": "text/event-stream",
1180
- "Cache-Control": "no-cache",
1181
- "Connection": "keep-alive",
1182
- "Access-Control-Allow-Origin": "*"
1183
- });
1184
- res.write("retry: 2000\n\n");
1185
- const id = nextClientId++;
1186
- sseClients.push({ id, res });
1187
- req.on("close", () => {
1188
- sseClients = sseClients.filter((c2) => c2.id !== id);
1189
- });
1190
- return;
1191
- }
1192
- if (path === "/api/board") {
1193
- if (method === "GET") {
1194
- const tasksDir = getTasksDir(kandownDir);
1195
- const boardPath = join5(tasksDir, "board.md");
1196
- const text = existsSync5(boardPath) ? readFileSync5(boardPath, "utf8") : "";
1197
- return writeText(res, 200, text);
1198
- }
1199
- if (method === "PUT") {
1200
- let body = "";
1201
- req.on("data", (chunk) => {
1202
- body += chunk;
1203
- });
1204
- req.on("end", () => {
1205
- const tasksDir = getTasksDir(kandownDir);
1206
- if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
1207
- atomicWriteFileSync(join5(tasksDir, "board.md"), body);
1208
- broadcastSseEvent({ type: "board" });
1209
- writeJson(res, 200, { ok: true });
1210
- });
1211
- return;
1212
- }
1213
- }
1214
- if (path === "/api/config") {
1215
- if (method === "GET") {
1216
- return writeJson(res, 200, loadConfig(kandownDir));
1217
- }
1218
- if (method === "PUT") {
1219
- let body = "";
1220
- req.on("data", (chunk) => {
1221
- body += chunk;
1222
- });
1223
- req.on("end", () => {
1224
- try {
1225
- const parsed = JSON.parse(body);
1226
- saveConfig(kandownDir, parsed);
1227
- broadcastSseEvent({ type: "config" });
1228
- writeJson(res, 200, { ok: true });
1229
- } catch (e) {
1230
- writeJson(res, 400, { error: e.message });
1231
- }
1232
- });
1233
- return;
1234
- }
1235
- }
1236
- if (path === "/api/tasks" && method === "GET") {
1237
- return writeJson(res, 200, listTaskIds(kandownDir));
1238
- }
1239
- if (path.startsWith("/api/tasks/")) {
1240
- const routeParts = path.slice("/api/tasks/".length).split("/").filter(Boolean);
1241
- let taskId;
1242
- try {
1243
- taskId = decodeURIComponent(routeParts[0] ?? "");
1244
- } catch {
1245
- return writeText(res, 400, "Invalid task id");
1246
- }
1247
- if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) return writeText(res, 400, "Invalid task id");
1248
- const tasksDir = getTasksDir(kandownDir);
1249
- const archiveDir = join5(tasksDir, "archive");
1250
- const activePath = join5(tasksDir, `${taskId}.md`);
1251
- const archivedPath = join5(archiveDir, `${taskId}.md`);
1252
- const action = routeParts[1];
1253
- if (method === "POST" && (action === "archive" || action === "unarchive")) {
1254
- if (routeParts.length !== 2) return writeText(res, 400, "Invalid task route");
1255
- const archiving = action === "archive";
1256
- const source = archiving ? activePath : archivedPath;
1257
- const destination = archiving ? archivedPath : activePath;
1258
- if (!existsSync5(source) && !existsSync5(destination)) {
1259
- return writeText(res, 404, "Task not found");
1260
- }
1261
- try {
1262
- if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
1263
- if (!existsSync5(archiveDir)) mkdirSync3(archiveDir, { recursive: true });
1264
- const body = await readRequestBody(req);
1265
- atomicWriteFileSync(destination, body);
1266
- if (source !== destination && existsSync5(source)) unlinkSync5(source);
1267
- broadcastSseEvent({ type: "task", id: taskId });
1268
- return writeJson(res, 200, { ok: true });
1269
- } catch (error) {
1270
- return writeJson(res, 500, {
1271
- error: `Failed to ${action} task: ${error instanceof Error ? error.message : String(error)}`
1272
- });
1273
- }
1274
- }
1275
- if (routeParts.length !== 1) return writeText(res, 404, "Route not found");
1276
- if (method === "GET") {
1277
- const taskPath2 = findTaskPath(kandownDir, taskId);
1278
- if (!taskPath2) return writeText(res, 404, "Task not found");
1279
- return writeText(res, 200, readFileSync5(taskPath2, "utf8"));
1280
- }
1281
- if (method === "PUT") {
1282
- try {
1283
- if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
1284
- const taskPath2 = findTaskPath(kandownDir, taskId) ?? activePath;
1285
- const body = await readRequestBody(req);
1286
- atomicWriteFileSync(taskPath2, body);
1287
- broadcastSseEvent({ type: "task", id: taskId });
1288
- return writeJson(res, 200, { ok: true });
1289
- } catch (error) {
1290
- return writeJson(res, 500, {
1291
- error: `Failed to write task: ${error instanceof Error ? error.message : String(error)}`
1292
- });
1293
- }
1294
- }
1295
- if (method === "DELETE") {
1296
- try {
1297
- if (existsSync5(activePath)) unlinkSync5(activePath);
1298
- if (existsSync5(archivedPath)) unlinkSync5(archivedPath);
1299
- broadcastSseEvent({ type: "task_delete", id: taskId });
1300
- return writeJson(res, 200, { ok: true });
1301
- } catch (error) {
1302
- return writeJson(res, 500, {
1303
- error: `Failed to delete task: ${error instanceof Error ? error.message : String(error)}`
1304
- });
1305
- }
1306
- }
1307
- }
1308
- writeJson(res, 404, { error: "Route not found" });
1309
- }
1310
- function injectServerRoot(html, kandownDir) {
1311
- const marker = "</head>";
1312
- const markerIndex = html.toLowerCase().lastIndexOf(marker);
1313
- const safeRoot = JSON.stringify(kandownDir).replace(/</g, "\\u003c");
1314
- const script = `<script>window.__KANDOWN_ROOT__ = ${safeRoot};</script>
1315
- `;
1316
- if (markerIndex === -1) return script + html;
1317
- return html.slice(0, markerIndex) + script + html.slice(markerIndex);
1318
- }
1319
- function serveApp(res, kandownDir) {
1320
- syncProjectKandownHtml(kandownDir);
1321
- const htmlPath = join5(kandownDir, "kandown.html");
1322
- if (existsSync5(htmlPath)) {
1323
- const html = readFileSync5(htmlPath, "utf8");
1324
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1325
- res.end(injectServerRoot(html, kandownDir));
1326
- } else {
1327
- writeText(res, 404, "kandown.html not found");
1328
- }
1329
- }
1330
- function createServeServer(kandownDir) {
1331
- syncProjectKandownHtml(kandownDir);
1332
- return createServer((req, res) => {
1333
- const url = new URL(req.url || "/", "http://localhost");
1334
- if (req.method === "OPTIONS") return handleCors(res);
1335
- if (url.pathname === "/" || url.pathname === "/kandown.html" || !url.pathname.startsWith("/api/")) {
1336
- return serveApp(res, kandownDir);
1337
- }
1338
- if (url.pathname.startsWith("/api/")) {
1339
- return handleApi(req, res, url, kandownDir);
1340
- }
1341
- writeText(res, 404, "Not found");
1342
- });
1343
- }
1344
- function listen(server, port) {
1345
- return new Promise((resolveListen, rejectListen) => {
1346
- const onError = (e) => {
1347
- server.off("listening", onListening);
1348
- rejectListen(e);
1349
- };
1350
- const onListening = () => {
1351
- server.off("error", onError);
1352
- resolveListen();
1353
- };
1354
- server.once("error", onError);
1355
- server.once("listening", onListening);
1356
- server.listen(port, "127.0.0.1");
1357
- });
1358
- }
1359
- async function listenOnAvailablePort(kandownDir, preferredPort) {
1360
- const startPort = preferredPort ?? START_PORT_RANGE;
1361
- for (let p = startPort; p <= END_PORT_RANGE; p++) {
1362
- if (UNSAFE_PORTS.has(p)) continue;
1363
- const server = createServeServer(kandownDir);
1364
- try {
1365
- await listen(server, p);
1366
- return { server, port: p };
1367
- } catch (e) {
1368
- if (e.code !== "EADDRINUSE" && e.code !== "EACCES") throw e;
1369
- }
1370
- }
1371
- throw new Error(`No free port in range ${START_PORT_RANGE}-${END_PORT_RANGE}`);
1372
- }
1373
-
1374
- // src/cli/lib/init.ts
1375
- import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
1376
- import { join as join6 } from "path";
1377
- function copyRecursive(src, dest) {
1378
- const errors = [];
1379
- try {
1380
- if (!existsSync6(dest)) mkdirSync4(dest, { recursive: true });
1381
- const entries = readdirSync3(src);
1382
- for (const entry of entries) {
1383
- const srcPath = join6(src, entry);
1384
- const destPath = join6(dest, entry);
1385
- try {
1386
- if (statSync3(srcPath).isDirectory()) {
1387
- errors.push(...copyRecursive(srcPath, destPath));
1388
- } else if (!existsSync6(destPath)) {
1389
- copyFileSync2(srcPath, destPath);
1390
- }
1391
- } catch (error) {
1392
- errors.push(`${entry}: ${error instanceof Error ? error.message : String(error)}`);
1393
- }
1394
- }
1395
- } catch (error) {
1396
- errors.push(`${src}: ${error instanceof Error ? error.message : String(error)}`);
1397
- }
1398
- return errors;
1399
- }
1400
- function syncKandownAgentDoc(kandownDir) {
1401
- const source = join6(PKG_ROOT, "templates", "AGENT_KANDOWN.md");
1402
- const target = join6(kandownDir, "AGENT_KANDOWN.md");
1403
- if (!existsSync6(source)) return false;
1404
- try {
1405
- const expected = readFileSync6(source, "utf8");
1406
- const existing = existsSync6(target) ? readFileSync6(target, "utf8") : null;
1407
- if (existing === null || !existing.includes("# Kandown")) {
1408
- atomicWriteFileSync(target, expected.endsWith("\n") ? expected : `${expected}
1409
- `);
1410
- return true;
1411
- }
1412
- } catch {
1413
- }
1414
- return false;
1415
- }
1416
- function doInit(kandownDir) {
1417
- try {
1418
- mkdirSync4(kandownDir, { recursive: true });
1419
- const htmlSrc = join6(PKG_ROOT, "dist", "index.html");
1420
- const htmlDest = join6(kandownDir, "kandown.html");
1421
- if (existsSync6(htmlSrc)) {
1422
- copyFileSync2(htmlSrc, htmlDest);
1423
- }
1424
- syncKandownAgentDoc(kandownDir);
1425
- const templatesDir = join6(PKG_ROOT, "templates");
1426
- if (existsSync6(templatesDir)) {
1427
- if (!existsSync6(join6(kandownDir, "README.md")) && existsSync6(join6(templatesDir, "README.md"))) {
1428
- copyFileSync2(join6(templatesDir, "README.md"), join6(kandownDir, "README.md"));
1429
- }
1430
- if (!existsSync6(join6(kandownDir, "AGENT.md")) && existsSync6(join6(templatesDir, "AGENT.md"))) {
1431
- copyFileSync2(join6(templatesDir, "AGENT.md"), join6(kandownDir, "AGENT.md"));
1432
- }
1433
- const tasksSrc = join6(templatesDir, "tasks");
1434
- const tasksDest = getTasksDir(kandownDir);
1435
- if (!existsSync6(tasksDest) && existsSync6(tasksSrc)) {
1436
- copyRecursive(tasksSrc, tasksDest);
1437
- }
1438
- if (!existsSync6(join6(kandownDir, "kandown.json")) && existsSync6(join6(templatesDir, "kandown.json"))) {
1439
- copyFileSync2(join6(templatesDir, "kandown.json"), join6(kandownDir, "kandown.json"));
1440
- }
1441
- }
1442
- return true;
1443
- } catch (error) {
1444
- console.error(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
1445
- return false;
1446
- }
1447
- }
1448
-
1449
- // src/cli/lib/mcp.ts
1450
- import { existsSync as existsSync7 } from "fs";
1451
- import { join as join7 } from "path";
1452
- function startMcpServer(kandownDir) {
1453
- process.stdin.setEncoding("utf8");
1454
- let buffer = "";
1455
- process.stdin.on("data", (chunk) => {
1456
- buffer += chunk;
1457
- const lines = buffer.split("\n");
1458
- buffer = lines.pop() ?? "";
1459
- for (const line of lines) {
1460
- const trimmed = line.trim();
1461
- if (!trimmed) continue;
1462
- try {
1463
- const req = JSON.parse(trimmed);
1464
- handleJsonRpc(kandownDir, req);
1465
- } catch (e) {
1466
- sendResponse(null, { error: { code: -32700, message: "Parse error" } });
1467
- }
1468
- }
1469
- });
1470
- }
1471
- function sendResponse(id, resultOrError) {
1472
- if (id === void 0) return;
1473
- const resp = {
1474
- jsonrpc: "2.0",
1475
- id,
1476
- ...resultOrError
1477
- };
1478
- process.stdout.write(JSON.stringify(resp) + "\n");
1479
- }
1480
- function handleJsonRpc(kandownDir, req) {
1481
- const { id, method, params } = req;
1482
- if (method === "initialize") {
1483
- sendResponse(id, {
1484
- result: {
1485
- protocolVersion: "2024-11-05",
1486
- capabilities: { tools: {} },
1487
- serverInfo: { name: "kandown", version: "0.20.0" }
1488
- }
1489
- });
1490
- return;
1039
+ return;
1491
1040
  }
1492
1041
  if (method === "notifications/initialized") {
1493
1042
  return;
@@ -1603,7 +1152,7 @@ function handleJsonRpc(kandownDir, req) {
1603
1152
  ...args.tags ? { tags: args.tags } : {}
1604
1153
  };
1605
1154
  const body = args.body ? (task.body + "\n\n" + args.body).trim() : task.body;
1606
- const taskPath2 = join7(getTasksDir(kandownDir), `${newId}.md`);
1155
+ const taskPath2 = join5(getTasksDir(kandownDir), `${newId}.md`);
1607
1156
  atomicWriteFileSync(taskPath2, serializeTaskFile(fm, body));
1608
1157
  }
1609
1158
  sendResponse(id, { result: { content: [{ type: "text", text: `Created task ${newId}` }] } });
@@ -1615,8 +1164,8 @@ function handleJsonRpc(kandownDir, req) {
1615
1164
  return;
1616
1165
  }
1617
1166
  if (name === "add_report") {
1618
- const taskPath2 = join7(getTasksDir(kandownDir), `${args.id}.md`);
1619
- if (!existsSync7(taskPath2)) {
1167
+ const taskPath2 = join5(getTasksDir(kandownDir), `${args.id}.md`);
1168
+ if (!existsSync5(taskPath2)) {
1620
1169
  sendResponse(id, { error: { code: -32602, message: `Task ${args.id} not found` } });
1621
1170
  return;
1622
1171
  }
@@ -1640,23 +1189,103 @@ ${args.report.trim()}`) : task.body.trim() + reportSection;
1640
1189
  sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(cols, null, 2) }] } });
1641
1190
  return;
1642
1191
  }
1643
- sendResponse(id, { error: { code: -32601, message: `Unknown tool: ${name}` } });
1644
- return;
1192
+ sendResponse(id, { error: { code: -32601, message: `Unknown tool: ${name}` } });
1193
+ return;
1194
+ }
1195
+ sendResponse(id, { error: { code: -32601, message: `Method not found: ${method}` } });
1196
+ }
1197
+
1198
+ // src/cli/lib/browser.ts
1199
+ import { spawn as spawn3 } from "child_process";
1200
+ function openBrowser(target) {
1201
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1202
+ try {
1203
+ spawn3(cmd, [target], { detached: true, stdio: "ignore" }).unref();
1204
+ } catch {
1205
+ }
1206
+ }
1207
+
1208
+ // src/cli/lib/cli-shared.ts
1209
+ import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
1210
+ import { join as join7, resolve as resolve2, basename } from "path";
1211
+ import { spawn as spawn4 } from "child_process";
1212
+
1213
+ // src/cli/lib/init.ts
1214
+ import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync3, copyFileSync, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
1215
+ import { join as join6 } from "path";
1216
+ function copyRecursive(src, dest) {
1217
+ const errors = [];
1218
+ try {
1219
+ if (!existsSync6(dest)) mkdirSync3(dest, { recursive: true });
1220
+ const entries = readdirSync3(src);
1221
+ for (const entry of entries) {
1222
+ const srcPath = join6(src, entry);
1223
+ const destPath = join6(dest, entry);
1224
+ try {
1225
+ if (statSync3(srcPath).isDirectory()) {
1226
+ errors.push(...copyRecursive(srcPath, destPath));
1227
+ } else if (!existsSync6(destPath)) {
1228
+ copyFileSync(srcPath, destPath);
1229
+ }
1230
+ } catch (error) {
1231
+ errors.push(`${entry}: ${error instanceof Error ? error.message : String(error)}`);
1232
+ }
1233
+ }
1234
+ } catch (error) {
1235
+ errors.push(`${src}: ${error instanceof Error ? error.message : String(error)}`);
1645
1236
  }
1646
- sendResponse(id, { error: { code: -32601, message: `Method not found: ${method}` } });
1237
+ return errors;
1647
1238
  }
1648
-
1649
- // src/cli/lib/browser.ts
1650
- import { spawn as spawn4 } from "child_process";
1651
- function openBrowser(target) {
1652
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1239
+ function syncKandownAgentDoc(kandownDir) {
1240
+ const source = join6(PKG_ROOT, "templates", "AGENT_KANDOWN.md");
1241
+ const target = join6(kandownDir, "AGENT_KANDOWN.md");
1242
+ if (!existsSync6(source)) return false;
1653
1243
  try {
1654
- spawn4(cmd, [target], { detached: true, stdio: "ignore" }).unref();
1244
+ const expected = readFileSync6(source, "utf8");
1245
+ const existing = existsSync6(target) ? readFileSync6(target, "utf8") : null;
1246
+ if (existing === null || !existing.includes("# Kandown")) {
1247
+ atomicWriteFileSync(target, expected.endsWith("\n") ? expected : `${expected}
1248
+ `);
1249
+ return true;
1250
+ }
1655
1251
  } catch {
1656
1252
  }
1253
+ return false;
1254
+ }
1255
+ function doInit(kandownDir) {
1256
+ try {
1257
+ mkdirSync3(kandownDir, { recursive: true });
1258
+ const htmlSrc = join6(PKG_ROOT, "dist", "index.html");
1259
+ const htmlDest = join6(kandownDir, "kandown.html");
1260
+ if (existsSync6(htmlSrc)) {
1261
+ copyFileSync(htmlSrc, htmlDest);
1262
+ }
1263
+ syncKandownAgentDoc(kandownDir);
1264
+ const templatesDir = join6(PKG_ROOT, "templates");
1265
+ if (existsSync6(templatesDir)) {
1266
+ if (!existsSync6(join6(kandownDir, "README.md")) && existsSync6(join6(templatesDir, "README.md"))) {
1267
+ copyFileSync(join6(templatesDir, "README.md"), join6(kandownDir, "README.md"));
1268
+ }
1269
+ if (!existsSync6(join6(kandownDir, "AGENT.md")) && existsSync6(join6(templatesDir, "AGENT.md"))) {
1270
+ copyFileSync(join6(templatesDir, "AGENT.md"), join6(kandownDir, "AGENT.md"));
1271
+ }
1272
+ const tasksSrc = join6(templatesDir, "tasks");
1273
+ const tasksDest = getTasksDir(kandownDir);
1274
+ if (!existsSync6(tasksDest) && existsSync6(tasksSrc)) {
1275
+ copyRecursive(tasksSrc, tasksDest);
1276
+ }
1277
+ if (!existsSync6(join6(kandownDir, "kandown.json")) && existsSync6(join6(templatesDir, "kandown.json"))) {
1278
+ copyFileSync(join6(templatesDir, "kandown.json"), join6(kandownDir, "kandown.json"));
1279
+ }
1280
+ }
1281
+ return true;
1282
+ } catch (error) {
1283
+ console.error(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
1284
+ return false;
1285
+ }
1657
1286
  }
1658
1287
 
1659
- // src/cli/cli.ts
1288
+ // src/cli/lib/cli-shared.ts
1660
1289
  var c = {
1661
1290
  reset: "\x1B[0m",
1662
1291
  bold: "\x1B[1m",
@@ -1754,7 +1383,7 @@ function stripFirstPositional(args, value) {
1754
1383
  return result;
1755
1384
  }
1756
1385
  function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
1757
- if (basename(cwd) === ".kandown" || existsSync8(join8(cwd, "kandown.json"))) {
1386
+ if (basename(cwd) === ".kandown" || existsSync7(join7(cwd, "kandown.json"))) {
1758
1387
  return cwd;
1759
1388
  }
1760
1389
  if (pathArg !== ".kandown") {
@@ -1768,7 +1397,7 @@ function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
1768
1397
  }
1769
1398
  for (const name of entries) {
1770
1399
  if (name.startsWith(".") || name === "node_modules" || name === "tasks") continue;
1771
- const subPath = join8(cwd, name);
1400
+ const subPath = join7(cwd, name);
1772
1401
  let stat;
1773
1402
  try {
1774
1403
  stat = statSync4(subPath);
@@ -1777,7 +1406,7 @@ function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
1777
1406
  }
1778
1407
  if (!stat.isDirectory()) continue;
1779
1408
  const found = resolveKandownDir(pathArg, subPath);
1780
- if (existsSync8(found)) return found;
1409
+ if (existsSync7(found)) return found;
1781
1410
  }
1782
1411
  return resolve2(cwd, pathArg);
1783
1412
  }
@@ -1785,7 +1414,7 @@ function ensureKandownDir(rawArgs) {
1785
1414
  const args = parseArgs(rawArgs);
1786
1415
  const cwd = process.cwd();
1787
1416
  const kandownDir = resolveKandownDir(args.path, cwd);
1788
- if (!existsSync8(kandownDir)) {
1417
+ if (!existsSync7(kandownDir)) {
1789
1418
  info(`No .kandown/ found \u2014 auto-initializing ${c.bold}${kandownDir}${c.reset}`);
1790
1419
  if (!doInit(kandownDir)) {
1791
1420
  err(`Could not auto-initialize Kandown at ${c.bold}${kandownDir}${c.reset}`);
@@ -1830,105 +1459,6 @@ ${c.bold}OPTIONS:${c.reset}
1830
1459
  --help, -h Show help screen
1831
1460
  `);
1832
1461
  }
1833
- function cmdInit(rawArgs) {
1834
- const args = parseArgs(rawArgs);
1835
- const kandownDir = resolve2(process.cwd(), args.path);
1836
- const created = doInit(kandownDir);
1837
- if (!created) {
1838
- err("Failed to initialize Kandown.");
1839
- process.exit(1);
1840
- }
1841
- success(`Kandown initialized at ${kandownDir}`);
1842
- }
1843
- async function cmdUpdate(rawArgs) {
1844
- const current = getCurrentVersion();
1845
- log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
1846
- const latest = await new Promise((resolve3) => {
1847
- const child = spawn5("npm", ["view", "kandown", "version"], {
1848
- timeout: 6e3,
1849
- stdio: ["pipe", "pipe", "pipe"],
1850
- env: { ...process.env },
1851
- detached: false
1852
- });
1853
- let stdout = "";
1854
- child.stdout.on("data", (d) => {
1855
- stdout += d;
1856
- });
1857
- child.stderr.on("data", () => {
1858
- });
1859
- child.on("error", () => resolve3(null));
1860
- child.on("close", (code) => {
1861
- if (code !== 0) return resolve3(null);
1862
- const v = stdout.trim().replace(/^"|"$/g, "");
1863
- resolve3(v || null);
1864
- });
1865
- });
1866
- if (latest && semverGt(latest, current) > 0) {
1867
- info(`Updating kandown package v${current} \u2192 v${latest}\u2026`);
1868
- const updateOk = await performGlobalPackageUpdate(`kandown@${latest}`);
1869
- if (updateOk) {
1870
- success(`Successfully upgraded kandown to v${latest}`);
1871
- } else {
1872
- err(`Global CLI update failed \u2014 try: pnpm add -g kandown@latest or npm install -g kandown@latest`);
1873
- }
1874
- } else {
1875
- info(`kandown CLI is already up to date (v${current}).`);
1876
- }
1877
- const args = parseArgs(rawArgs);
1878
- const cwd = process.cwd();
1879
- const kandownDir = resolve2(cwd, args.path);
1880
- const htmlDest = join8(kandownDir, "kandown.html");
1881
- if (existsSync8(htmlDest)) {
1882
- const htmlSrc = resolve2(PKG_ROOT, "dist", "index.html");
1883
- if (existsSync8(htmlSrc)) {
1884
- copyFileSync3(htmlSrc, htmlDest);
1885
- success(`Refreshed ${args.path}/kandown.html`);
1886
- }
1887
- }
1888
- }
1889
- async function cmdDoctor(rawArgs) {
1890
- const { kandownDir } = ensureKandownDir(rawArgs);
1891
- const currentVersion = getCurrentVersion();
1892
- log(`${c.bold}kandown doctor${c.reset} ${c.dim}\u2014 environment & board diagnostic${c.reset}
1893
- `);
1894
- log(` CLI Version: ${currentVersion}`);
1895
- const configPath = join8(kandownDir, "kandown.json");
1896
- if (existsSync8(configPath)) {
1897
- try {
1898
- JSON.parse(readFileSync8(configPath, "utf8"));
1899
- success("kandown.json valid");
1900
- } catch (e) {
1901
- err(`kandown.json invalid: ${e.message}`);
1902
- }
1903
- } else {
1904
- err("Missing kandown.json");
1905
- }
1906
- const daemonStatus = await getDaemonStatus(kandownDir);
1907
- if (daemonStatus.running && daemonStatus.metadata) {
1908
- success(`Daemon running on port ${daemonStatus.metadata.port} (PID ${daemonStatus.metadata.pid})`);
1909
- } else {
1910
- info("Daemon not running");
1911
- }
1912
- const taskIds = listTaskIds(kandownDir);
1913
- success(`Tasks: ${taskIds.length} active task files`);
1914
- log(`
1915
- ${c.green}\u2713 Everything looks good!${c.reset}
1916
- `);
1917
- }
1918
- async function cmdWork(rawArgs) {
1919
- const { kandownDir } = ensureKandownDir(rawArgs);
1920
- const doc = readAgentDoc(kandownDir);
1921
- const board = readBoard(kandownDir);
1922
- log(doc);
1923
- log("\n---\n");
1924
- log(`## Current Board Digest
1925
- `);
1926
- const allTasksCount = board.columns.reduce((sum, col) => sum + col.tasks.length, 0);
1927
- log(`Tasks total: ${allTasksCount}`);
1928
- for (const col of board.columns) {
1929
- log(`- **${col.name}** (${col.tasks.length}): ${col.tasks.map((t) => `${t.id} ${t.title}`).join(", ") || "empty"}`);
1930
- }
1931
- }
1932
1462
  function addMultiFlag(flags, key, value) {
1933
1463
  const current = flags[key];
1934
1464
  if (Array.isArray(current)) current.push(value);
@@ -1985,19 +1515,19 @@ function resolveStatusArg(kandownDir, status) {
1985
1515
  return config.board.columns.find((col) => col.toLowerCase() === status.toLowerCase()) ?? null;
1986
1516
  }
1987
1517
  function taskPath(kandownDir, id, archived = false) {
1988
- return archived ? join8(getTasksDir(kandownDir), "archive", `${id}.md`) : join8(getTasksDir(kandownDir), `${id}.md`);
1518
+ return archived ? join7(getTasksDir(kandownDir), "archive", `${id}.md`) : join7(getTasksDir(kandownDir), `${id}.md`);
1989
1519
  }
1990
1520
  function findTaskPath2(kandownDir, id) {
1991
1521
  const active = taskPath(kandownDir, id);
1992
- if (existsSync8(active)) return active;
1522
+ if (existsSync7(active)) return active;
1993
1523
  const archived = taskPath(kandownDir, id, true);
1994
- if (existsSync8(archived)) return archived;
1524
+ if (existsSync7(archived)) return archived;
1995
1525
  return null;
1996
1526
  }
1997
1527
  function nextTaskId(kandownDir) {
1998
1528
  const ids = new Set(listTaskIds(kandownDir));
1999
- const archiveDir = join8(getTasksDir(kandownDir), "archive");
2000
- if (existsSync8(archiveDir)) {
1529
+ const archiveDir = join7(getTasksDir(kandownDir), "archive");
1530
+ if (existsSync7(archiveDir)) {
2001
1531
  for (const file of readdirSync4(archiveDir)) {
2002
1532
  if (file.endsWith(".md")) ids.add(file.slice(0, -3));
2003
1533
  }
@@ -2009,17 +1539,156 @@ function nextTaskId(kandownDir) {
2009
1539
  }
2010
1540
  return `t${max + 1}`;
2011
1541
  }
2012
- function readTaskFile(kandownDir, id) {
2013
- const path = findTaskPath2(kandownDir, id);
2014
- if (!path) return null;
2015
- const parsed = parseTaskFile(readFileSync8(path, "utf8"));
2016
- return {
2017
- path,
2018
- frontmatter: { ...parsed.frontmatter, id: parsed.frontmatter.id || id },
2019
- body: parsed.body,
2020
- archived: path.includes("/archive/")
2021
- };
1542
+ function readTaskFile(kandownDir, id) {
1543
+ const path = findTaskPath2(kandownDir, id);
1544
+ if (!path) return null;
1545
+ const parsed = parseTaskFile(readFileSync7(path, "utf8"));
1546
+ return {
1547
+ path,
1548
+ frontmatter: { ...parsed.frontmatter, id: parsed.frontmatter.id || id },
1549
+ body: parsed.body,
1550
+ archived: path.includes("/archive/")
1551
+ };
1552
+ }
1553
+ function printTaskCommandsHelp() {
1554
+ log(`
1555
+ ${c.bold}Kandown task commands${c.reset}
1556
+
1557
+ kandown list [-s status] [-p P1] [-a user] [-t tag] [--archived] [--json]
1558
+ kandown show <id>
1559
+ kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id id] [--json]
1560
+ kandown move <id> <status|archived>
1561
+ kandown assign <id> [user]
1562
+ kandown commit [-m "message"]
1563
+ `);
1564
+ }
1565
+ async function launchTui(screen, kandownDir) {
1566
+ if (!process.stdin.isTTY) {
1567
+ info(`TUI skipped because stdin is not interactive. Use ${c.cyan}kandown daemon status${c.reset} to inspect the web daemon.`);
1568
+ return;
1569
+ }
1570
+ const tuiPath = join7(PKG_ROOT, "bin", "tui.js");
1571
+ if (!existsSync7(tuiPath)) {
1572
+ err(`TUI binary not found at ${tuiPath}`);
1573
+ process.exit(1);
1574
+ }
1575
+ await new Promise((resolveTui) => {
1576
+ const child = spawn4(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
1577
+ child.on("close", (code) => {
1578
+ if (typeof code === "number" && code !== 0) process.exit(code);
1579
+ resolveTui();
1580
+ });
1581
+ });
1582
+ }
1583
+
1584
+ // src/cli/commands/project.ts
1585
+ import { existsSync as existsSync8, readFileSync as readFileSync8, copyFileSync as copyFileSync2 } from "fs";
1586
+ import { join as join8, resolve as resolve3 } from "path";
1587
+ import { spawn as spawn5 } from "child_process";
1588
+ function cmdInit(rawArgs) {
1589
+ const args = parseArgs(rawArgs);
1590
+ const kandownDir = resolve3(process.cwd(), args.path);
1591
+ const created = doInit(kandownDir);
1592
+ if (!created) {
1593
+ err("Failed to initialize Kandown.");
1594
+ process.exit(1);
1595
+ }
1596
+ success(`Kandown initialized at ${kandownDir}`);
1597
+ }
1598
+ async function cmdUpdate(rawArgs) {
1599
+ const current = getCurrentVersion();
1600
+ log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
1601
+ const latest = await new Promise((resolve5) => {
1602
+ const child = spawn5("npm", ["view", "kandown", "version"], {
1603
+ timeout: 6e3,
1604
+ stdio: ["pipe", "pipe", "pipe"],
1605
+ env: { ...process.env },
1606
+ detached: false
1607
+ });
1608
+ let stdout = "";
1609
+ child.stdout.on("data", (d) => {
1610
+ stdout += d;
1611
+ });
1612
+ child.stderr.on("data", () => {
1613
+ });
1614
+ child.on("error", () => resolve5(null));
1615
+ child.on("close", (code) => {
1616
+ if (code !== 0) return resolve5(null);
1617
+ const v = stdout.trim().replace(/^"|"$/g, "");
1618
+ resolve5(v || null);
1619
+ });
1620
+ });
1621
+ if (latest && semverGt(latest, current) > 0) {
1622
+ info(`Updating kandown package v${current} \u2192 v${latest}\u2026`);
1623
+ const updateOk = await performGlobalPackageUpdate(`kandown@${latest}`);
1624
+ if (updateOk) {
1625
+ success(`Successfully upgraded kandown to v${latest}`);
1626
+ } else {
1627
+ err(`Global CLI update failed \u2014 try: pnpm add -g kandown@latest or npm install -g kandown@latest`);
1628
+ }
1629
+ } else {
1630
+ info(`kandown CLI is already up to date (v${current}).`);
1631
+ }
1632
+ const args = parseArgs(rawArgs);
1633
+ const cwd = process.cwd();
1634
+ const kandownDir = resolve3(cwd, args.path);
1635
+ const htmlDest = join8(kandownDir, "kandown.html");
1636
+ if (existsSync8(htmlDest)) {
1637
+ const htmlSrc = resolve3(PKG_ROOT, "dist", "index.html");
1638
+ if (existsSync8(htmlSrc)) {
1639
+ copyFileSync2(htmlSrc, htmlDest);
1640
+ success(`Refreshed ${args.path}/kandown.html`);
1641
+ }
1642
+ }
1643
+ }
1644
+ async function cmdDoctor(rawArgs) {
1645
+ const { kandownDir } = ensureKandownDir(rawArgs);
1646
+ const currentVersion = getCurrentVersion();
1647
+ log(`${c.bold}kandown doctor${c.reset} ${c.dim}\u2014 environment & board diagnostic${c.reset}
1648
+ `);
1649
+ log(` CLI Version: ${currentVersion}`);
1650
+ const configPath = join8(kandownDir, "kandown.json");
1651
+ if (existsSync8(configPath)) {
1652
+ try {
1653
+ JSON.parse(readFileSync8(configPath, "utf8"));
1654
+ success("kandown.json valid");
1655
+ } catch (e) {
1656
+ err(`kandown.json invalid: ${e.message}`);
1657
+ }
1658
+ } else {
1659
+ err("Missing kandown.json");
1660
+ }
1661
+ const daemonStatus = await getDaemonStatus(kandownDir);
1662
+ if (daemonStatus.running && daemonStatus.metadata) {
1663
+ success(`Daemon running on port ${daemonStatus.metadata.port} (PID ${daemonStatus.metadata.pid})`);
1664
+ } else {
1665
+ info("Daemon not running");
1666
+ }
1667
+ const taskIds = listTaskIds(kandownDir);
1668
+ success(`Tasks: ${taskIds.length} active task files`);
1669
+ log(`
1670
+ ${c.green}\u2713 Everything looks good!${c.reset}
1671
+ `);
1672
+ }
1673
+ async function cmdWork(rawArgs) {
1674
+ const { kandownDir } = ensureKandownDir(rawArgs);
1675
+ const doc = readAgentDoc(kandownDir);
1676
+ const board = readBoard(kandownDir);
1677
+ log(doc);
1678
+ log("\n---\n");
1679
+ log(`## Current Board Digest
1680
+ `);
1681
+ const allTasksCount = board.columns.reduce((sum, col) => sum + col.tasks.length, 0);
1682
+ log(`Tasks total: ${allTasksCount}`);
1683
+ for (const col of board.columns) {
1684
+ log(`- **${col.name}** (${col.tasks.length}): ${col.tasks.map((t) => `${t.id} ${t.title}`).join(", ") || "empty"}`);
1685
+ }
2022
1686
  }
1687
+
1688
+ // src/cli/commands/tasks.ts
1689
+ import { existsSync as existsSync9, readFileSync as readFileSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync5 } from "fs";
1690
+ import { join as join9, resolve as resolve4 } from "path";
1691
+ import { spawnSync } from "child_process";
2023
1692
  function cmdList(rawArgs) {
2024
1693
  const { kandownDir } = ensureKandownDir(rawArgs);
2025
1694
  const args = taskParseArgs(rawArgs);
@@ -2042,11 +1711,11 @@ function cmdList(rawArgs) {
2042
1711
  });
2043
1712
  }
2044
1713
  if (includeArchived) {
2045
- const archiveDir = join8(getTasksDir(kandownDir), "archive");
2046
- if (existsSync8(archiveDir)) {
2047
- for (const file of readdirSync4(archiveDir).filter((name) => name.endsWith(".md"))) {
1714
+ const archiveDir = join9(getTasksDir(kandownDir), "archive");
1715
+ if (existsSync9(archiveDir)) {
1716
+ for (const file of readdirSync5(archiveDir).filter((name) => name.endsWith(".md"))) {
2048
1717
  const id = file.slice(0, -3);
2049
- const parsed = parseTaskFile(readFileSync8(join8(archiveDir, file), "utf8"));
1718
+ const parsed = parseTaskFile(readFileSync9(join9(archiveDir, file), "utf8"));
2050
1719
  rows.push({
2051
1720
  id,
2052
1721
  title: parsed.frontmatter.title || id,
@@ -2066,249 +1735,664 @@ function cmdList(rawArgs) {
2066
1735
  if (tagFilters.length > 0 && !tagFilters.every((tag) => row.tags.map((t) => t.toLowerCase()).includes(tag))) return false;
2067
1736
  return true;
2068
1737
  });
2069
- if (args.flags.json === true) {
2070
- process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
2071
- return;
2072
- }
2073
- const byStatus = /* @__PURE__ */ new Map();
2074
- for (const row of filtered) {
2075
- const list = byStatus.get(row.status) ?? [];
2076
- list.push(row);
2077
- byStatus.set(row.status, list);
2078
- }
2079
- for (const [status, tasks] of byStatus) {
2080
- log(`
2081
- ${c.bold}${status}${c.reset} (${tasks.length})`);
2082
- for (const task of tasks) {
2083
- const pri = task.priority || "P2";
2084
- const assignee = task.assignee ? ` @${task.assignee}` : "";
2085
- log(` ${c.cyan}${task.id}${c.reset} [${pri}] ${task.title}${assignee}`);
1738
+ if (args.flags.json === true) {
1739
+ process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
1740
+ return;
1741
+ }
1742
+ const byStatus = /* @__PURE__ */ new Map();
1743
+ for (const row of filtered) {
1744
+ const list = byStatus.get(row.status) ?? [];
1745
+ list.push(row);
1746
+ byStatus.set(row.status, list);
1747
+ }
1748
+ for (const [status, tasks] of byStatus) {
1749
+ log(`
1750
+ ${c.bold}${status}${c.reset} (${tasks.length})`);
1751
+ for (const task of tasks) {
1752
+ const pri = task.priority || "P2";
1753
+ const assignee = task.assignee ? ` @${task.assignee}` : "";
1754
+ log(` ${c.cyan}${task.id}${c.reset} [${pri}] ${task.title}${assignee}`);
1755
+ }
1756
+ }
1757
+ log("");
1758
+ }
1759
+ function cmdShow(rawArgs) {
1760
+ const { kandownDir } = ensureKandownDir(rawArgs);
1761
+ const args = taskParseArgs(rawArgs);
1762
+ const id = args.positional[0];
1763
+ if (!id) {
1764
+ err("Usage: kandown show <task-id>");
1765
+ process.exit(1);
1766
+ }
1767
+ const path = findTaskPath2(kandownDir, id);
1768
+ if (!path) {
1769
+ err(`Task not found: ${id}`);
1770
+ process.exit(1);
1771
+ }
1772
+ process.stdout.write(readFileSync9(path, "utf8"));
1773
+ }
1774
+ function cmdCreate(rawArgs) {
1775
+ const { kandownDir } = ensureKandownDir(rawArgs);
1776
+ const args = taskParseArgs(rawArgs);
1777
+ const title = args.positional.join(" ").trim();
1778
+ if (!title) {
1779
+ err('Usage: kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id custom-id] [--json]');
1780
+ process.exit(1);
1781
+ }
1782
+ const id = stringFlag(args.flags, "id") ?? nextTaskId(kandownDir);
1783
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
1784
+ err(`Invalid task id: ${id}`);
1785
+ process.exit(1);
1786
+ }
1787
+ if (findTaskPath2(kandownDir, id)) {
1788
+ err(`Task already exists: ${id}`);
1789
+ process.exit(1);
1790
+ }
1791
+ const config = loadConfig(kandownDir);
1792
+ const rawStatus = stringFlag(args.flags, "to", "status");
1793
+ const status = rawStatus ? resolveStatusArg(kandownDir, rawStatus) : config.board.columns[0] || "Backlog";
1794
+ if (!status) {
1795
+ err(`Unknown status: ${rawStatus}`);
1796
+ process.exit(1);
1797
+ }
1798
+ const fm = {
1799
+ id,
1800
+ title,
1801
+ status,
1802
+ created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
1803
+ };
1804
+ const priority = stringFlag(args.flags, "priority")?.toUpperCase();
1805
+ const assignee = stringFlag(args.flags, "assignee");
1806
+ const tags = listFlag(args.flags, "tag");
1807
+ if (priority) fm.priority = priority;
1808
+ if (assignee) fm.assignee = assignee;
1809
+ if (tags.length > 0) fm.tags = tags;
1810
+ const tasksDir = getTasksDir(kandownDir);
1811
+ if (!existsSync9(tasksDir)) mkdirSync4(tasksDir, { recursive: true });
1812
+ const path = taskPath(kandownDir, id);
1813
+ atomicWriteFileSync(path, serializeTaskFile(fm, ""));
1814
+ process.stderr.write(`${c.green}\u2713${c.reset} Created ${c.bold}${id}${c.reset} \u2192 ${status}
1815
+ `);
1816
+ process.stdout.write(args.flags.json === true ? JSON.stringify(fm, null, 2) + "\n" : `${id}
1817
+ `);
1818
+ }
1819
+ function cmdMove(rawArgs) {
1820
+ const { kandownDir } = ensureKandownDir(rawArgs);
1821
+ const args = taskParseArgs(rawArgs);
1822
+ const id = args.positional[0];
1823
+ const rawStatus = args.positional.slice(1).join(" ") || stringFlag(args.flags, "to", "status");
1824
+ if (!id || !rawStatus) {
1825
+ err("Usage: kandown move <task-id> <status>");
1826
+ process.exit(1);
1827
+ }
1828
+ if (rawStatus.toLowerCase() === "archived") {
1829
+ if (!archiveTaskInBoard(kandownDir, id)) {
1830
+ err(`Archive failed: ${id}`);
1831
+ process.exit(1);
1832
+ }
1833
+ success(`Archived ${id}`);
1834
+ return;
1835
+ }
1836
+ const status = resolveStatusArg(kandownDir, rawStatus);
1837
+ if (!status) {
1838
+ err(`Unknown status: ${rawStatus}`);
1839
+ process.exit(1);
1840
+ }
1841
+ if (!moveTaskToColumn(kandownDir, id, status)) {
1842
+ err(`Move failed: ${id}`);
1843
+ process.exit(1);
1844
+ }
1845
+ success(`Moved ${id} \u2192 "${status}"`);
1846
+ }
1847
+ function cmdAssign(rawArgs) {
1848
+ const { kandownDir } = ensureKandownDir(rawArgs);
1849
+ const args = taskParseArgs(rawArgs);
1850
+ const [id, assignee] = args.positional;
1851
+ if (!id) {
1852
+ err("Usage: kandown assign <task-id> [assignee]");
1853
+ process.exit(1);
1854
+ }
1855
+ const task = readTaskFile(kandownDir, id);
1856
+ if (!task) {
1857
+ err(`Task not found: ${id}`);
1858
+ process.exit(1);
1859
+ }
1860
+ const frontmatter = { ...task.frontmatter, id };
1861
+ if (assignee) frontmatter.assignee = assignee;
1862
+ else delete frontmatter.assignee;
1863
+ atomicWriteFileSync(task.path, serializeTaskFile(frontmatter, task.body));
1864
+ success(assignee ? `Assigned ${id} \u2192 ${assignee}` : `Unassigned ${id}`);
1865
+ }
1866
+ function cmdCommit(rawArgs) {
1867
+ ensureKandownDir(rawArgs);
1868
+ const args = taskParseArgs(rawArgs);
1869
+ const message = stringFlag(args.flags, "message") || "tasks: update kandown board";
1870
+ const add = spawnSync("git", ["add", "tasks", ".kandown/kandown.json"], { stdio: "inherit" });
1871
+ if (add.status !== 0) process.exit(add.status ?? 1);
1872
+ const commit = spawnSync("git", ["commit", "-m", message], { stdio: "inherit" });
1873
+ process.exit(commit.status ?? 1);
1874
+ }
1875
+ function cmdExport(rawArgs) {
1876
+ const { kandownDir } = ensureKandownDir(rawArgs);
1877
+ const board = readBoard(kandownDir);
1878
+ process.stdout.write(JSON.stringify(board, null, 2) + "\n");
1879
+ }
1880
+ function cmdProjects(rawArgs) {
1881
+ const { kandownDir } = ensureKandownDir(rawArgs);
1882
+ const metadataPath2 = join9(kandownDir, "daemon.json");
1883
+ if (!existsSync9(metadataPath2)) {
1884
+ info("No daemon metadata for this project.");
1885
+ return;
1886
+ }
1887
+ process.stdout.write(readFileSync9(metadataPath2, "utf8").trim() + "\n");
1888
+ }
1889
+ function cmdImport(rawArgs) {
1890
+ const { kandownDir } = ensureKandownDir(rawArgs);
1891
+ const args = taskParseArgs(rawArgs);
1892
+ const file = args.positional[0];
1893
+ if (!file) {
1894
+ err("Usage: kandown import <file.json> [--overwrite]");
1895
+ process.exit(1);
1896
+ }
1897
+ const importPath = resolve4(process.cwd(), file);
1898
+ if (!existsSync9(importPath)) {
1899
+ err(`Import file not found: ${file}`);
1900
+ process.exit(1);
1901
+ }
1902
+ let raw;
1903
+ try {
1904
+ raw = JSON.parse(readFileSync9(importPath, "utf8"));
1905
+ } catch (error) {
1906
+ err(`Import file must be JSON: ${error instanceof Error ? error.message : String(error)}`);
1907
+ process.exit(1);
1908
+ }
1909
+ const rows = [];
1910
+ if (Array.isArray(raw)) {
1911
+ rows.push(...raw.filter((value) => typeof value === "object" && value !== null));
1912
+ } else if (typeof raw === "object" && raw !== null && Array.isArray(raw.columns)) {
1913
+ for (const column of raw.columns) {
1914
+ if (typeof column !== "object" || column === null) continue;
1915
+ const col = column;
1916
+ if (!Array.isArray(col.tasks)) continue;
1917
+ for (const task of col.tasks) {
1918
+ if (typeof task === "object" && task !== null) rows.push({ ...task, status: String(col.name || "Backlog") });
1919
+ }
1920
+ }
1921
+ }
1922
+ if (rows.length === 0) {
1923
+ err("No tasks found to import. Expected a list JSON array or kandown export object.");
1924
+ process.exit(1);
1925
+ }
1926
+ const tasksDir = getTasksDir(kandownDir);
1927
+ if (!existsSync9(tasksDir)) mkdirSync4(tasksDir, { recursive: true });
1928
+ let imported = 0;
1929
+ for (const row of rows) {
1930
+ const id = typeof row.id === "string" && /^[a-zA-Z0-9_-]+$/.test(row.id) ? row.id : nextTaskId(kandownDir);
1931
+ const path = taskPath(kandownDir, id);
1932
+ if (existsSync9(path) && args.flags.overwrite !== true) continue;
1933
+ const fm = {
1934
+ id,
1935
+ title: typeof row.title === "string" && row.title ? row.title : id,
1936
+ status: typeof row.status === "string" && row.status ? row.status.replace(/ \(archived\)$/i, "") : "Backlog"
1937
+ };
1938
+ if (typeof row.priority === "string") fm.priority = row.priority;
1939
+ if (typeof row.assignee === "string") fm.assignee = row.assignee;
1940
+ if (Array.isArray(row.tags)) fm.tags = row.tags.map(String);
1941
+ atomicWriteFileSync(path, serializeTaskFile(fm, typeof row.body === "string" ? row.body : ""));
1942
+ imported++;
1943
+ }
1944
+ success(`Imported ${imported} task${imported === 1 ? "" : "s"}`);
1945
+ }
1946
+
1947
+ // src/cli/commands/daemon.ts
1948
+ import { join as join11 } from "path";
1949
+
1950
+ // src/cli/lib/server.ts
1951
+ import { createServer } from "http";
1952
+ import { existsSync as existsSync10, readFileSync as readFileSync10, copyFileSync as copyFileSync3, unlinkSync as unlinkSync5, mkdirSync as mkdirSync5 } from "fs";
1953
+ import { join as join10 } from "path";
1954
+ import { spawn as spawn6 } from "child_process";
1955
+ var START_PORT_RANGE = 2050;
1956
+ var END_PORT_RANGE = 2099;
1957
+ var UNSAFE_PORTS = /* @__PURE__ */ new Set([2049, 4045, 6e3, 6665, 6666, 6667, 6668, 6669, 6697]);
1958
+ var sseClients = [];
1959
+ var nextClientId = 1;
1960
+ function broadcastSseEvent(data) {
1961
+ const payload = `data: ${JSON.stringify(data)}
1962
+
1963
+ `;
1964
+ sseClients.forEach((c2) => c2.res.write(payload));
1965
+ }
1966
+ function handleCors(res) {
1967
+ res.writeHead(204, {
1968
+ "Access-Control-Allow-Origin": "*",
1969
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
1970
+ "Access-Control-Allow-Headers": "Content-Type, X-Kandown-Token"
1971
+ });
1972
+ res.end();
1973
+ }
1974
+ function writeJson(res, status, data) {
1975
+ res.writeHead(status, {
1976
+ "Content-Type": "application/json",
1977
+ "Access-Control-Allow-Origin": "*"
1978
+ });
1979
+ res.end(JSON.stringify(data));
1980
+ }
1981
+ function writeText(res, status, text) {
1982
+ res.writeHead(status, {
1983
+ "Content-Type": "text/plain; charset=utf-8",
1984
+ "Access-Control-Allow-Origin": "*"
1985
+ });
1986
+ res.end(text);
1987
+ }
1988
+ function readRequestBody(req) {
1989
+ return new Promise((resolveBody, rejectBody) => {
1990
+ let body = "";
1991
+ req.setEncoding("utf8");
1992
+ req.on("data", (chunk) => {
1993
+ body += chunk;
1994
+ });
1995
+ req.on("end", () => resolveBody(body));
1996
+ req.on("error", rejectBody);
1997
+ });
1998
+ }
1999
+ function syncProjectKandownHtml(kandownDir) {
2000
+ try {
2001
+ const projectHtml = join10(kandownDir, "kandown.html");
2002
+ const distHtml = join10(PKG_ROOT, "dist", "index.html");
2003
+ if (!existsSync10(distHtml)) return false;
2004
+ if (!existsSync10(projectHtml)) {
2005
+ copyFileSync3(distHtml, projectHtml);
2006
+ return true;
2007
+ }
2008
+ const currentContent = readFileSync10(projectHtml, "utf8");
2009
+ const newContent = readFileSync10(distHtml, "utf8");
2010
+ if (currentContent !== newContent) {
2011
+ atomicWriteFileSync(projectHtml, newContent);
2012
+ return true;
2086
2013
  }
2014
+ } catch {
2087
2015
  }
2088
- log("");
2016
+ return false;
2089
2017
  }
2090
- function cmdShow(rawArgs) {
2091
- const { kandownDir } = ensureKandownDir(rawArgs);
2092
- const args = taskParseArgs(rawArgs);
2093
- const id = args.positional[0];
2094
- if (!id) {
2095
- err("Usage: kandown show <task-id>");
2096
- process.exit(1);
2097
- }
2098
- const path = findTaskPath2(kandownDir, id);
2099
- if (!path) {
2100
- err(`Task not found: ${id}`);
2101
- process.exit(1);
2018
+ function readDaemonPort(kandownDir) {
2019
+ try {
2020
+ const raw = JSON.parse(readFileSync10(join10(kandownDir, "daemon.json"), "utf8"));
2021
+ return typeof raw.port === "number" && Number.isInteger(raw.port) ? raw.port : null;
2022
+ } catch {
2023
+ return null;
2102
2024
  }
2103
- process.stdout.write(readFileSync8(path, "utf8"));
2104
2025
  }
2105
- function cmdCreate(rawArgs) {
2106
- const { kandownDir } = ensureKandownDir(rawArgs);
2107
- const args = taskParseArgs(rawArgs);
2108
- const title = args.positional.join(" ").trim();
2109
- if (!title) {
2110
- err('Usage: kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id custom-id] [--json]');
2111
- process.exit(1);
2026
+ function restartDaemonAfterUpdateResponse(res, kandownDir) {
2027
+ const cliPath = process.argv[1];
2028
+ if (!cliPath) return;
2029
+ const args = ["--no-update-check", "daemon", "run", "--path", kandownDir];
2030
+ const port = readDaemonPort(kandownDir);
2031
+ if (port !== null) args.push("--port", String(port));
2032
+ const launcher = `
2033
+ const { spawn } = require('node:child_process');
2034
+ const [nodeBin, cliPath, ...cliArgs] = process.argv.slice(1);
2035
+ setTimeout(() => {
2036
+ const child = spawn(nodeBin, [cliPath, ...cliArgs], {
2037
+ detached: true,
2038
+ stdio: 'ignore',
2039
+ env: { ...process.env, KANDOWN_DAEMON: '1' },
2040
+ });
2041
+ child.unref();
2042
+ }, 350);
2043
+ `;
2044
+ res.on("finish", () => {
2045
+ const child = spawn6(process.execPath, ["-e", launcher, process.execPath, cliPath, ...args], {
2046
+ detached: true,
2047
+ stdio: "ignore",
2048
+ env: { ...process.env, KANDOWN_DAEMON: "1" }
2049
+ });
2050
+ child.unref();
2051
+ setTimeout(() => process.exit(0), 50).unref();
2052
+ });
2053
+ }
2054
+ async function handleApi(req, res, url, kandownDir) {
2055
+ const path = url.pathname;
2056
+ const method = req.method || "GET";
2057
+ if (path === "/api/daemon" && method === "GET") {
2058
+ return writeJson(res, 200, {
2059
+ ok: true,
2060
+ pid: process.pid,
2061
+ kandownDir,
2062
+ version: getCurrentVersion(),
2063
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2064
+ agentHook: process.env.KANDOWN_AGENT_HOOK_URL ? { enabled: true, label: process.env.KANDOWN_AGENT_HOOK_LABEL || "Send to Agent" } : null
2065
+ });
2112
2066
  }
2113
- const id = stringFlag(args.flags, "id") ?? nextTaskId(kandownDir);
2114
- if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
2115
- err(`Invalid task id: ${id}`);
2116
- process.exit(1);
2067
+ if (path === "/api/version" && method === "GET") {
2068
+ return writeJson(res, 200, {
2069
+ version: getCurrentVersion()
2070
+ });
2117
2071
  }
2118
- if (findTaskPath2(kandownDir, id)) {
2119
- err(`Task already exists: ${id}`);
2120
- process.exit(1);
2072
+ if (path === "/api/update/check" && method === "GET") {
2073
+ const current = getCurrentVersion();
2074
+ const latest = await new Promise((resolve5) => {
2075
+ const child = spawn6("npm", ["view", "kandown", "version"], {
2076
+ timeout: 4e3,
2077
+ stdio: ["pipe", "pipe", "pipe"],
2078
+ env: { ...process.env },
2079
+ detached: false
2080
+ });
2081
+ let stdout = "";
2082
+ child.stdout.on("data", (d) => {
2083
+ stdout += d;
2084
+ });
2085
+ child.stderr.on("data", () => {
2086
+ });
2087
+ child.on("error", () => resolve5(null));
2088
+ child.on("close", (code) => {
2089
+ if (code !== 0) return resolve5(null);
2090
+ resolve5(stdout.trim().replace(/^"|"$/g, "") || null);
2091
+ });
2092
+ });
2093
+ const updateAvailable = latest ? semverGt(latest, current) > 0 : false;
2094
+ return writeJson(res, 200, {
2095
+ current,
2096
+ latest: latest || current,
2097
+ updateAvailable
2098
+ });
2121
2099
  }
2122
- const config = loadConfig(kandownDir);
2123
- const rawStatus = stringFlag(args.flags, "to", "status");
2124
- const status = rawStatus ? resolveStatusArg(kandownDir, rawStatus) : config.board.columns[0] || "Backlog";
2125
- if (!status) {
2126
- err(`Unknown status: ${rawStatus}`);
2127
- process.exit(1);
2100
+ if (path === "/api/update/apply" && method === "POST") {
2101
+ const current = getCurrentVersion();
2102
+ const latest = await new Promise((resolve5) => {
2103
+ const child = spawn6("npm", ["view", "kandown", "version"], {
2104
+ timeout: 4e3,
2105
+ stdio: ["pipe", "pipe", "pipe"],
2106
+ env: { ...process.env },
2107
+ detached: false
2108
+ });
2109
+ let stdout = "";
2110
+ child.stdout.on("data", (d) => {
2111
+ stdout += d;
2112
+ });
2113
+ child.on("error", () => resolve5(null));
2114
+ child.on("close", (code) => resolve5(code === 0 ? stdout.trim() : null));
2115
+ });
2116
+ const targetVersion = latest || current;
2117
+ const ok = await performGlobalPackageUpdate(`kandown@${targetVersion}`);
2118
+ syncProjectKandownHtml(kandownDir);
2119
+ if (ok) {
2120
+ restartDaemonAfterUpdateResponse(res, kandownDir);
2121
+ writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully; daemon is restarting" });
2122
+ broadcastSseEvent({ type: "update", version: targetVersion });
2123
+ } else {
2124
+ writeJson(res, 500, { ok: false, message: "Global package installation failed" });
2125
+ }
2126
+ return;
2128
2127
  }
2129
- const fm = {
2130
- id,
2131
- title,
2132
- status,
2133
- created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
2134
- };
2135
- const priority = stringFlag(args.flags, "priority")?.toUpperCase();
2136
- const assignee = stringFlag(args.flags, "assignee");
2137
- const tags = listFlag(args.flags, "tag");
2138
- if (priority) fm.priority = priority;
2139
- if (assignee) fm.assignee = assignee;
2140
- if (tags.length > 0) fm.tags = tags;
2141
- const tasksDir = getTasksDir(kandownDir);
2142
- if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
2143
- const path = taskPath(kandownDir, id);
2144
- atomicWriteFileSync(path, serializeTaskFile(fm, ""));
2145
- process.stderr.write(`${c.green}\u2713${c.reset} Created ${c.bold}${id}${c.reset} \u2192 ${status}
2146
- `);
2147
- process.stdout.write(args.flags.json === true ? JSON.stringify(fm, null, 2) + "\n" : `${id}
2148
- `);
2149
- }
2150
- function cmdMove(rawArgs) {
2151
- const { kandownDir } = ensureKandownDir(rawArgs);
2152
- const args = taskParseArgs(rawArgs);
2153
- const id = args.positional[0];
2154
- const rawStatus = args.positional.slice(1).join(" ") || stringFlag(args.flags, "to", "status");
2155
- if (!id || !rawStatus) {
2156
- err("Usage: kandown move <task-id> <status>");
2157
- process.exit(1);
2128
+ if (path === "/api/events" && method === "GET") {
2129
+ res.writeHead(200, {
2130
+ "Content-Type": "text/event-stream",
2131
+ "Cache-Control": "no-cache",
2132
+ "Connection": "keep-alive",
2133
+ "Access-Control-Allow-Origin": "*"
2134
+ });
2135
+ res.write("retry: 2000\n\n");
2136
+ const id = nextClientId++;
2137
+ sseClients.push({ id, res });
2138
+ req.on("close", () => {
2139
+ sseClients = sseClients.filter((c2) => c2.id !== id);
2140
+ });
2141
+ return;
2158
2142
  }
2159
- if (rawStatus.toLowerCase() === "archived") {
2160
- if (!archiveTaskInBoard(kandownDir, id)) {
2161
- err(`Archive failed: ${id}`);
2162
- process.exit(1);
2143
+ if (path === "/api/board") {
2144
+ if (method === "GET") {
2145
+ const tasksDir = getTasksDir(kandownDir);
2146
+ const boardPath = join10(tasksDir, "board.md");
2147
+ const text = existsSync10(boardPath) ? readFileSync10(boardPath, "utf8") : "";
2148
+ return writeText(res, 200, text);
2149
+ }
2150
+ if (method === "PUT") {
2151
+ let body = "";
2152
+ req.on("data", (chunk) => {
2153
+ body += chunk;
2154
+ });
2155
+ req.on("end", () => {
2156
+ const tasksDir = getTasksDir(kandownDir);
2157
+ if (!existsSync10(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
2158
+ atomicWriteFileSync(join10(tasksDir, "board.md"), body);
2159
+ broadcastSseEvent({ type: "board" });
2160
+ writeJson(res, 200, { ok: true });
2161
+ });
2162
+ return;
2163
+ }
2164
+ }
2165
+ if (path === "/api/config") {
2166
+ if (method === "GET") {
2167
+ return writeJson(res, 200, loadConfig(kandownDir));
2168
+ }
2169
+ if (method === "PUT") {
2170
+ let body = "";
2171
+ req.on("data", (chunk) => {
2172
+ body += chunk;
2173
+ });
2174
+ req.on("end", () => {
2175
+ try {
2176
+ const parsed = JSON.parse(body);
2177
+ saveConfig(kandownDir, parsed);
2178
+ broadcastSseEvent({ type: "config" });
2179
+ writeJson(res, 200, { ok: true });
2180
+ } catch (e) {
2181
+ writeJson(res, 400, { error: e.message });
2182
+ }
2183
+ });
2184
+ return;
2163
2185
  }
2164
- success(`Archived ${id}`);
2165
- return;
2166
2186
  }
2167
- const status = resolveStatusArg(kandownDir, rawStatus);
2168
- if (!status) {
2169
- err(`Unknown status: ${rawStatus}`);
2170
- process.exit(1);
2187
+ if (path === "/api/tasks" && method === "GET") {
2188
+ return writeJson(res, 200, listTaskIds(kandownDir));
2171
2189
  }
2172
- if (!moveTaskToColumn(kandownDir, id, status)) {
2173
- err(`Move failed: ${id}`);
2174
- process.exit(1);
2190
+ if (path.startsWith("/api/tasks/")) {
2191
+ const routeParts = path.slice("/api/tasks/".length).split("/").filter(Boolean);
2192
+ let taskId;
2193
+ try {
2194
+ taskId = decodeURIComponent(routeParts[0] ?? "");
2195
+ } catch {
2196
+ return writeText(res, 400, "Invalid task id");
2197
+ }
2198
+ if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) return writeText(res, 400, "Invalid task id");
2199
+ const tasksDir = getTasksDir(kandownDir);
2200
+ const archiveDir = join10(tasksDir, "archive");
2201
+ const activePath = join10(tasksDir, `${taskId}.md`);
2202
+ const archivedPath = join10(archiveDir, `${taskId}.md`);
2203
+ const action = routeParts[1];
2204
+ if (method === "POST" && (action === "archive" || action === "unarchive")) {
2205
+ if (routeParts.length !== 2) return writeText(res, 400, "Invalid task route");
2206
+ const archiving = action === "archive";
2207
+ const source = archiving ? activePath : archivedPath;
2208
+ const destination = archiving ? archivedPath : activePath;
2209
+ if (!existsSync10(source) && !existsSync10(destination)) {
2210
+ return writeText(res, 404, "Task not found");
2211
+ }
2212
+ try {
2213
+ if (!existsSync10(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
2214
+ if (!existsSync10(archiveDir)) mkdirSync5(archiveDir, { recursive: true });
2215
+ const body = await readRequestBody(req);
2216
+ atomicWriteFileSync(destination, body);
2217
+ if (source !== destination && existsSync10(source)) unlinkSync5(source);
2218
+ broadcastSseEvent({ type: "task", id: taskId });
2219
+ return writeJson(res, 200, { ok: true });
2220
+ } catch (error) {
2221
+ return writeJson(res, 500, {
2222
+ error: `Failed to ${action} task: ${error instanceof Error ? error.message : String(error)}`
2223
+ });
2224
+ }
2225
+ }
2226
+ if (routeParts.length !== 1) return writeText(res, 404, "Route not found");
2227
+ if (method === "GET") {
2228
+ const taskPath2 = findTaskPath(kandownDir, taskId);
2229
+ if (!taskPath2) return writeText(res, 404, "Task not found");
2230
+ return writeText(res, 200, readFileSync10(taskPath2, "utf8"));
2231
+ }
2232
+ if (method === "PUT") {
2233
+ try {
2234
+ if (!existsSync10(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
2235
+ const taskPath2 = findTaskPath(kandownDir, taskId) ?? activePath;
2236
+ const body = await readRequestBody(req);
2237
+ atomicWriteFileSync(taskPath2, body);
2238
+ broadcastSseEvent({ type: "task", id: taskId });
2239
+ return writeJson(res, 200, { ok: true });
2240
+ } catch (error) {
2241
+ return writeJson(res, 500, {
2242
+ error: `Failed to write task: ${error instanceof Error ? error.message : String(error)}`
2243
+ });
2244
+ }
2245
+ }
2246
+ if (method === "DELETE") {
2247
+ try {
2248
+ if (existsSync10(activePath)) unlinkSync5(activePath);
2249
+ if (existsSync10(archivedPath)) unlinkSync5(archivedPath);
2250
+ broadcastSseEvent({ type: "task_delete", id: taskId });
2251
+ return writeJson(res, 200, { ok: true });
2252
+ } catch (error) {
2253
+ return writeJson(res, 500, {
2254
+ error: `Failed to delete task: ${error instanceof Error ? error.message : String(error)}`
2255
+ });
2256
+ }
2257
+ }
2175
2258
  }
2176
- success(`Moved ${id} \u2192 "${status}"`);
2259
+ writeJson(res, 404, { error: "Route not found" });
2177
2260
  }
2178
- function cmdAssign(rawArgs) {
2179
- const { kandownDir } = ensureKandownDir(rawArgs);
2180
- const args = taskParseArgs(rawArgs);
2181
- const [id, assignee] = args.positional;
2182
- if (!id) {
2183
- err("Usage: kandown assign <task-id> [assignee]");
2184
- process.exit(1);
2185
- }
2186
- const task = readTaskFile(kandownDir, id);
2187
- if (!task) {
2188
- err(`Task not found: ${id}`);
2189
- process.exit(1);
2261
+ function injectServerRoot(html, kandownDir) {
2262
+ const marker = "</head>";
2263
+ const markerIndex = html.toLowerCase().lastIndexOf(marker);
2264
+ const safeRoot = JSON.stringify(kandownDir).replace(/</g, "\\u003c");
2265
+ const script = `<script>window.__KANDOWN_ROOT__ = ${safeRoot};</script>
2266
+ `;
2267
+ if (markerIndex === -1) return script + html;
2268
+ return html.slice(0, markerIndex) + script + html.slice(markerIndex);
2269
+ }
2270
+ function serveApp(res, kandownDir) {
2271
+ syncProjectKandownHtml(kandownDir);
2272
+ const htmlPath = join10(kandownDir, "kandown.html");
2273
+ if (existsSync10(htmlPath)) {
2274
+ const html = readFileSync10(htmlPath, "utf8");
2275
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2276
+ res.end(injectServerRoot(html, kandownDir));
2277
+ } else {
2278
+ writeText(res, 404, "kandown.html not found");
2190
2279
  }
2191
- const frontmatter = { ...task.frontmatter, id };
2192
- if (assignee) frontmatter.assignee = assignee;
2193
- else delete frontmatter.assignee;
2194
- atomicWriteFileSync(task.path, serializeTaskFile(frontmatter, task.body));
2195
- success(assignee ? `Assigned ${id} \u2192 ${assignee}` : `Unassigned ${id}`);
2196
2280
  }
2197
- function cmdCommit(rawArgs) {
2198
- ensureKandownDir(rawArgs);
2199
- const args = taskParseArgs(rawArgs);
2200
- const message = stringFlag(args.flags, "message") || "tasks: update kandown board";
2201
- const add = spawnSync("git", ["add", "tasks", ".kandown/kandown.json"], { stdio: "inherit" });
2202
- if (add.status !== 0) process.exit(add.status ?? 1);
2203
- const commit = spawnSync("git", ["commit", "-m", message], { stdio: "inherit" });
2204
- process.exit(commit.status ?? 1);
2281
+ function createServeServer(kandownDir) {
2282
+ syncProjectKandownHtml(kandownDir);
2283
+ return createServer((req, res) => {
2284
+ const url = new URL(req.url || "/", "http://localhost");
2285
+ if (req.method === "OPTIONS") return handleCors(res);
2286
+ if (url.pathname === "/" || url.pathname === "/kandown.html" || !url.pathname.startsWith("/api/")) {
2287
+ return serveApp(res, kandownDir);
2288
+ }
2289
+ if (url.pathname.startsWith("/api/")) {
2290
+ return handleApi(req, res, url, kandownDir);
2291
+ }
2292
+ writeText(res, 404, "Not found");
2293
+ });
2205
2294
  }
2206
- function printTaskCommandsHelp() {
2207
- log(`
2208
- ${c.bold}Kandown task commands${c.reset}
2209
-
2210
- kandown list [-s status] [-p P1] [-a user] [-t tag] [--archived] [--json]
2211
- kandown show <id>
2212
- kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id id] [--json]
2213
- kandown move <id> <status|archived>
2214
- kandown assign <id> [user]
2215
- kandown commit [-m "message"]
2216
- `);
2295
+ function listen(server, port) {
2296
+ return new Promise((resolveListen, rejectListen) => {
2297
+ const onError = (e) => {
2298
+ server.off("listening", onListening);
2299
+ rejectListen(e);
2300
+ };
2301
+ const onListening = () => {
2302
+ server.off("error", onError);
2303
+ resolveListen();
2304
+ };
2305
+ server.once("error", onError);
2306
+ server.once("listening", onListening);
2307
+ server.listen(port, "127.0.0.1");
2308
+ });
2217
2309
  }
2218
- async function launchTui(screen, kandownDir) {
2219
- if (!process.stdin.isTTY) {
2220
- info(`TUI skipped because stdin is not interactive. Use ${c.cyan}kandown daemon status${c.reset} to inspect the web daemon.`);
2221
- return;
2310
+ async function listenOnAvailablePort(kandownDir, preferredPort) {
2311
+ const startPort = preferredPort ?? START_PORT_RANGE;
2312
+ for (let p = startPort; p <= END_PORT_RANGE; p++) {
2313
+ if (UNSAFE_PORTS.has(p)) continue;
2314
+ const server = createServeServer(kandownDir);
2315
+ try {
2316
+ await listen(server, p);
2317
+ return { server, port: p };
2318
+ } catch (e) {
2319
+ if (e.code !== "EADDRINUSE" && e.code !== "EACCES") throw e;
2320
+ }
2222
2321
  }
2223
- const tuiPath = join8(PKG_ROOT, "bin", "tui.js");
2224
- if (!existsSync8(tuiPath)) {
2225
- err(`TUI binary not found at ${tuiPath}`);
2322
+ throw new Error(`No free port in range ${START_PORT_RANGE}-${END_PORT_RANGE}`);
2323
+ }
2324
+
2325
+ // src/cli/commands/daemon.ts
2326
+ async function cmdDaemon(rest) {
2327
+ const parsedDaemonArgs = parseArgs(rest);
2328
+ const subcommand = parsedDaemonArgs.positional[0] || "status";
2329
+ const daemonArgs = subcommand ? stripFirstPositional(rest, subcommand) : rest;
2330
+ const { kandownDir } = ensureKandownDir(daemonArgs);
2331
+ if (subcommand === "run") {
2332
+ const daemonOptions = parseArgs(daemonArgs);
2333
+ const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
2334
+ const { port } = await listenOnAvailablePort(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
2335
+ const url = `http://localhost:${port}`;
2336
+ const metadataPath2 = join11(kandownDir, "daemon.json");
2337
+ atomicWriteFileSync(metadataPath2, JSON.stringify({
2338
+ pid: process.pid,
2339
+ port,
2340
+ url,
2341
+ kandownDir,
2342
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2343
+ version: getCurrentVersion(),
2344
+ token: null
2345
+ }, null, 2));
2346
+ info(`Kandown daemon running on port ${port} (PID ${process.pid})`);
2347
+ await new Promise(() => {
2348
+ });
2349
+ } else if (subcommand === "start") {
2350
+ const daemonOptions = parseArgs(daemonArgs);
2351
+ const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
2352
+ const status = await startProjectDaemon(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
2353
+ if (status.running && status.metadata) success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
2354
+ else {
2355
+ err("Daemon failed to start");
2356
+ process.exit(1);
2357
+ }
2358
+ } else if (subcommand === "restart") {
2359
+ await stopProjectDaemon(kandownDir);
2360
+ const status = await startProjectDaemon(kandownDir);
2361
+ if (status.running && status.metadata) success(`Daemon restarted on port ${status.metadata.port} (PID ${status.metadata.pid})`);
2362
+ else {
2363
+ err("Daemon failed to restart");
2364
+ process.exit(1);
2365
+ }
2366
+ } else if (subcommand === "stop") {
2367
+ const stopped = await stopProjectDaemon(kandownDir);
2368
+ if (stopped) success("Daemon stopped");
2369
+ else info("Daemon not running");
2370
+ } else if (subcommand === "status") {
2371
+ const status = await getDaemonStatus(kandownDir);
2372
+ if (status.running && status.metadata) {
2373
+ success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
2374
+ } else {
2375
+ info("Daemon not running");
2376
+ }
2377
+ } else if (subcommand === "refresh-all") {
2378
+ const status = await getDaemonStatus(kandownDir);
2379
+ if (status.running) await stopProjectDaemon(kandownDir);
2380
+ const restarted = await startProjectDaemon(kandownDir);
2381
+ if (restarted.running && restarted.metadata) success(`Refreshed current project daemon on port ${restarted.metadata.port}`);
2382
+ else info("No running daemon refreshed");
2383
+ } else {
2384
+ err(`Unknown daemon command: ${subcommand}`);
2385
+ log(` Use ${c.cyan}kandown daemon start|stop|restart|status|refresh-all${c.reset}`);
2226
2386
  process.exit(1);
2227
2387
  }
2228
- await new Promise((resolveTui) => {
2229
- const child = spawn5(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
2230
- child.on("close", (code) => {
2231
- if (typeof code === "number" && code !== 0) process.exit(code);
2232
- resolveTui();
2233
- });
2234
- });
2235
2388
  }
2389
+
2390
+ // src/cli/cli.ts
2236
2391
  async function cmdTui(screen, rawArgs) {
2237
2392
  const args = parseArgs(rawArgs);
2238
2393
  const kandownDir = resolveKandownDir(args.path, process.cwd());
2239
2394
  await launchTui(screen, kandownDir);
2240
2395
  }
2241
- function cmdExport(rawArgs) {
2242
- const { kandownDir } = ensureKandownDir(rawArgs);
2243
- const board = readBoard(kandownDir);
2244
- process.stdout.write(JSON.stringify(board, null, 2) + "\n");
2245
- }
2246
- function cmdProjects(rawArgs) {
2247
- const { kandownDir } = ensureKandownDir(rawArgs);
2248
- const metadataPath2 = join8(kandownDir, "daemon.json");
2249
- if (!existsSync8(metadataPath2)) {
2250
- info("No daemon metadata for this project.");
2251
- return;
2252
- }
2253
- process.stdout.write(readFileSync8(metadataPath2, "utf8").trim() + "\n");
2254
- }
2255
- function cmdImport(rawArgs) {
2256
- const { kandownDir } = ensureKandownDir(rawArgs);
2257
- const args = taskParseArgs(rawArgs);
2258
- const file = args.positional[0];
2259
- if (!file) {
2260
- err("Usage: kandown import <file.json> [--overwrite]");
2261
- process.exit(1);
2262
- }
2263
- const importPath = resolve2(process.cwd(), file);
2264
- if (!existsSync8(importPath)) {
2265
- err(`Import file not found: ${file}`);
2266
- process.exit(1);
2267
- }
2268
- let raw;
2269
- try {
2270
- raw = JSON.parse(readFileSync8(importPath, "utf8"));
2271
- } catch (error) {
2272
- err(`Import file must be JSON: ${error instanceof Error ? error.message : String(error)}`);
2273
- process.exit(1);
2274
- }
2275
- const rows = [];
2276
- if (Array.isArray(raw)) {
2277
- rows.push(...raw.filter((value) => typeof value === "object" && value !== null));
2278
- } else if (typeof raw === "object" && raw !== null && Array.isArray(raw.columns)) {
2279
- for (const column of raw.columns) {
2280
- if (typeof column !== "object" || column === null) continue;
2281
- const col = column;
2282
- if (!Array.isArray(col.tasks)) continue;
2283
- for (const task of col.tasks) {
2284
- if (typeof task === "object" && task !== null) rows.push({ ...task, status: String(col.name || "Backlog") });
2285
- }
2286
- }
2287
- }
2288
- if (rows.length === 0) {
2289
- err("No tasks found to import. Expected a list JSON array or kandown export object.");
2290
- process.exit(1);
2291
- }
2292
- const tasksDir = getTasksDir(kandownDir);
2293
- if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
2294
- let imported = 0;
2295
- for (const row of rows) {
2296
- const id = typeof row.id === "string" && /^[a-zA-Z0-9_-]+$/.test(row.id) ? row.id : nextTaskId(kandownDir);
2297
- const path = taskPath(kandownDir, id);
2298
- if (existsSync8(path) && args.flags.overwrite !== true) continue;
2299
- const fm = {
2300
- id,
2301
- title: typeof row.title === "string" && row.title ? row.title : id,
2302
- status: typeof row.status === "string" && row.status ? row.status.replace(/ \(archived\)$/i, "") : "Backlog"
2303
- };
2304
- if (typeof row.priority === "string") fm.priority = row.priority;
2305
- if (typeof row.assignee === "string") fm.assignee = row.assignee;
2306
- if (Array.isArray(row.tags)) fm.tags = row.tags.map(String);
2307
- atomicWriteFileSync(path, serializeTaskFile(fm, typeof row.body === "string" ? row.body : ""));
2308
- imported++;
2309
- }
2310
- success(`Imported ${imported} task${imported === 1 ? "" : "s"}`);
2311
- }
2312
2396
  async function main() {
2313
2397
  const rawArgs = process.argv.slice(2);
2314
2398
  if (rawArgs.length === 1 && (rawArgs[0] === "--version" || rawArgs[0] === "version")) {
@@ -2382,80 +2466,19 @@ async function main() {
2382
2466
  case "-h":
2383
2467
  help();
2384
2468
  break;
2385
- case "daemon": {
2386
- const parsedDaemonArgs = parseArgs(rest);
2387
- const subcommand = parsedDaemonArgs.positional[0] || "status";
2388
- const daemonArgs = subcommand ? stripFirstPositional(rest, subcommand) : rest;
2389
- const { kandownDir } = ensureKandownDir(daemonArgs);
2390
- if (subcommand === "run") {
2391
- const daemonOptions = parseArgs(daemonArgs);
2392
- const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
2393
- const { port } = await listenOnAvailablePort(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
2394
- const url = `http://localhost:${port}`;
2395
- const metadataPath2 = join8(kandownDir, "daemon.json");
2396
- atomicWriteFileSync(metadataPath2, JSON.stringify({
2397
- pid: process.pid,
2398
- port,
2399
- url,
2400
- kandownDir,
2401
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2402
- version: getCurrentVersion(),
2403
- token: null
2404
- }, null, 2));
2405
- info(`Kandown daemon running on port ${port} (PID ${process.pid})`);
2406
- await new Promise(() => {
2407
- });
2408
- } else if (subcommand === "start") {
2409
- const daemonOptions = parseArgs(daemonArgs);
2410
- const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
2411
- const status = await startProjectDaemon(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
2412
- if (status.running && status.metadata) success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
2413
- else {
2414
- err("Daemon failed to start");
2415
- process.exit(1);
2416
- }
2417
- } else if (subcommand === "restart") {
2418
- await stopProjectDaemon(kandownDir);
2419
- const status = await startProjectDaemon(kandownDir);
2420
- if (status.running && status.metadata) success(`Daemon restarted on port ${status.metadata.port} (PID ${status.metadata.pid})`);
2421
- else {
2422
- err("Daemon failed to restart");
2423
- process.exit(1);
2424
- }
2425
- } else if (subcommand === "stop") {
2426
- const stopped = await stopProjectDaemon(kandownDir);
2427
- if (stopped) success("Daemon stopped");
2428
- else info("Daemon not running");
2429
- } else if (subcommand === "status") {
2430
- const status = await getDaemonStatus(kandownDir);
2431
- if (status.running && status.metadata) {
2432
- success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
2433
- } else {
2434
- info("Daemon not running");
2435
- }
2436
- } else if (subcommand === "refresh-all") {
2437
- const status = await getDaemonStatus(kandownDir);
2438
- if (status.running) await stopProjectDaemon(kandownDir);
2439
- const restarted = await startProjectDaemon(kandownDir);
2440
- if (restarted.running && restarted.metadata) success(`Refreshed current project daemon on port ${restarted.metadata.port}`);
2441
- else info("No running daemon refreshed");
2442
- } else {
2443
- err(`Unknown daemon command: ${subcommand}`);
2444
- log(` Use ${c.cyan}kandown daemon start|stop|restart|status|refresh-all${c.reset}`);
2445
- process.exit(1);
2446
- }
2469
+ case "daemon":
2470
+ await cmdDaemon(rest);
2447
2471
  break;
2448
- }
2449
2472
  case void 0: {
2450
2473
  const parsed = parseArgs(rest);
2451
2474
  const kandownDir = resolveKandownDir(parsed.path, process.cwd());
2452
- if (existsSync8(kandownDir)) {
2475
+ if (existsSync11(kandownDir)) {
2453
2476
  let status = await getDaemonStatus(kandownDir);
2454
2477
  if (!status.running) {
2455
2478
  status = await startProjectDaemon(kandownDir);
2456
2479
  }
2457
2480
  if (!parsed.flags["no-open"]) {
2458
- const urlToOpen = status.metadata?.url || join8(kandownDir, "kandown.html");
2481
+ const urlToOpen = status.metadata?.url || join12(kandownDir, "kandown.html");
2459
2482
  openBrowser(urlToOpen);
2460
2483
  }
2461
2484
  } else if (!process.stdin.isTTY) {