kandown 0.33.2 → 0.33.3

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,9 @@ if (typeof globalThis.require === 'undefined') {
5
5
  }
6
6
 
7
7
  // src/cli/cli.ts
8
- import { existsSync as existsSync6, readFileSync as readFileSync6, copyFileSync as copyFileSync2 } from "fs";
9
- import { join as join6, resolve as resolve3, basename } from "path";
10
- import { spawn as spawn4 } from "child_process";
8
+ import { existsSync as existsSync8, readFileSync as readFileSync8, copyFileSync as copyFileSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync3 } from "fs";
9
+ import { join as join8, resolve as resolve2, basename } from "path";
10
+ import { spawn as spawn4, spawnSync } from "child_process";
11
11
 
12
12
  // src/cli/lib/updater.ts
13
13
  import { existsSync, readFileSync, writeFileSync, unlinkSync, statSync, mkdirSync } from "fs";
@@ -16,7 +16,7 @@ import { spawn, execSync } from "child_process";
16
16
  import { homedir } from "os";
17
17
 
18
18
  // src/lib/version.ts
19
- var KANDOWN_VERSION = "0.33.2";
19
+ var KANDOWN_VERSION = "0.33.3";
20
20
 
21
21
  // src/cli/lib/updater.ts
22
22
  import { fileURLToPath } from "url";
@@ -139,7 +139,12 @@ async function performGlobalPackageUpdate(packageSpec) {
139
139
  });
140
140
  };
141
141
  const currentBin = resolveKandownBin() || "";
142
+ const currentBinDir = currentBin ? dirname(currentBin) : null;
143
+ const siblingNpm = currentBinDir ? join(currentBinDir, "npm") : null;
144
+ const siblingPnpm = currentBinDir ? join(currentBinDir, "pnpm") : null;
142
145
  const isPnpmInstall = currentBin.includes("pnpm");
146
+ if (siblingPnpm && existsSync(siblingPnpm) && await tryPkgCmd(siblingPnpm, ["add", "-g", packageSpec])) return true;
147
+ if (siblingNpm && existsSync(siblingNpm) && await tryPkgCmd(siblingNpm, ["install", "-g", packageSpec, "--force"])) return true;
143
148
  if (isPnpmInstall) {
144
149
  if (await tryPkgCmd("pnpm", ["add", "-g", packageSpec])) return true;
145
150
  if (await tryPkgCmd("npm", ["install", "-g", packageSpec, "--force"])) return true;
@@ -165,7 +170,7 @@ async function checkForUpdate(argv = process.argv) {
165
170
  }
166
171
  } catch {
167
172
  }
168
- const latest = await new Promise((resolve4) => {
173
+ const latest = await new Promise((resolve3) => {
169
174
  const child2 = spawn("npm", ["view", "kandown", "version"], {
170
175
  timeout: 6e3,
171
176
  stdio: ["pipe", "pipe", "pipe"],
@@ -178,11 +183,11 @@ async function checkForUpdate(argv = process.argv) {
178
183
  });
179
184
  child2.stderr.on("data", () => {
180
185
  });
181
- child2.on("error", () => resolve4(null));
186
+ child2.on("error", () => resolve3(null));
182
187
  child2.on("close", (code) => {
183
- if (code !== 0) return resolve4(null);
188
+ if (code !== 0) return resolve3(null);
184
189
  const v = stdout.trim().replace(/^"|"$/g, "");
185
- resolve4(v || null);
190
+ resolve3(v || null);
186
191
  });
187
192
  });
188
193
  if (!latest) return;
@@ -547,14 +552,14 @@ function readBoard(kandownDir) {
547
552
  };
548
553
  }
549
554
  function readTask(kandownDir, taskId) {
550
- const taskPath = join3(getTasksDir(kandownDir), `${taskId}.md`);
551
- if (!existsSync3(taskPath)) {
555
+ const taskPath2 = join3(getTasksDir(kandownDir), `${taskId}.md`);
556
+ if (!existsSync3(taskPath2)) {
552
557
  return {
553
558
  frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
554
559
  body: ""
555
560
  };
556
561
  }
557
- const content = readFileSync3(taskPath, "utf8");
562
+ const content = readFileSync3(taskPath2, "utf8");
558
563
  const parsed = parseTaskFile(content);
559
564
  return {
560
565
  ...parsed,
@@ -608,21 +613,21 @@ ${gitLog}
608
613
  return sections.filter(Boolean).join("\n\n---\n\n");
609
614
  }
610
615
  function moveTaskToColumn(kandownDir, taskId, targetColumn) {
611
- const taskPath = join3(getTasksDir(kandownDir), `${taskId}.md`);
612
- if (!existsSync3(taskPath)) return false;
616
+ const taskPath2 = join3(getTasksDir(kandownDir), `${taskId}.md`);
617
+ if (!existsSync3(taskPath2)) return false;
613
618
  try {
614
- const prevContent = readFileSync3(taskPath, "utf8");
619
+ const prevContent = readFileSync3(taskPath2, "utf8");
615
620
  const parsed = readTask(kandownDir, taskId);
616
621
  const newContent = serializeTaskFile({
617
622
  ...parsed.frontmatter,
618
623
  id: taskId,
619
624
  status: targetColumn
620
625
  }, parsed.body);
621
- atomicWriteFileSync(taskPath, newContent);
626
+ atomicWriteFileSync(taskPath2, newContent);
622
627
  pushUndo(kandownDir, {
623
628
  type: "move",
624
629
  taskId,
625
- path: taskPath,
630
+ path: taskPath2,
626
631
  previousContent: prevContent,
627
632
  newContent,
628
633
  timestamp: Date.now()
@@ -652,6 +657,102 @@ function pushUndo(kandownDir, record) {
652
657
  } catch {
653
658
  }
654
659
  }
660
+ function createTaskInBoard(kandownDir, rawInput, status) {
661
+ const tasksDir = getTasksDir(kandownDir);
662
+ if (!existsSync3(tasksDir)) mkdirSync2(tasksDir, { recursive: true });
663
+ const ids = listTaskIds(kandownDir);
664
+ let maxN = 0;
665
+ for (const id of ids) {
666
+ const m = id.match(/^t(\d+)$/i);
667
+ if (m) {
668
+ const n = parseInt(m[1], 10);
669
+ if (Number.isFinite(n) && n > maxN) maxN = n;
670
+ }
671
+ }
672
+ const newId = `t${maxN + 1}`;
673
+ const config = loadConfig(kandownDir);
674
+ const targetStatus = status || (config.board.columns[0] ?? "Backlog");
675
+ let text = rawInput.trim();
676
+ let priority;
677
+ const tags = [];
678
+ let assignee;
679
+ let due;
680
+ const depends_on = [];
681
+ text = text.replace(/(?:^|\s)p([1-4])(?:\s|$)/i, (_, level) => {
682
+ priority = `P${level}`;
683
+ return " ";
684
+ });
685
+ text = text.replace(/(?:^|\s)#([a-zA-Z0-9_-]+)/g, (_, tag) => {
686
+ tags.push(tag.toLowerCase());
687
+ return " ";
688
+ });
689
+ text = text.replace(/(?:^|\s)@([a-zA-Z0-9_-]+)/g, (_, user) => {
690
+ assignee = user;
691
+ return " ";
692
+ });
693
+ text = text.replace(/(?:^|\s)due:([^\s]+)/i, (_, d) => {
694
+ due = d;
695
+ return " ";
696
+ });
697
+ text = text.replace(/(?:^|\s)\+([a-zA-Z0-9_-]+)/g, (_, depId) => {
698
+ depends_on.push(depId);
699
+ return " ";
700
+ });
701
+ const title = text.replace(/\s+/g, " ").trim() || rawInput;
702
+ const fm = {
703
+ id: newId,
704
+ title,
705
+ status: targetStatus,
706
+ created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
707
+ };
708
+ if (priority) fm.priority = priority;
709
+ if (assignee) fm.assignee = assignee;
710
+ if (tags.length > 0) fm.tags = tags;
711
+ if (due) fm.due = due;
712
+ if (depends_on.length > 0) fm.depends_on = depends_on;
713
+ const content = serializeTaskFile(fm, "");
714
+ const taskPath2 = join3(tasksDir, `${newId}.md`);
715
+ atomicWriteFileSync(taskPath2, content);
716
+ pushUndo(kandownDir, {
717
+ type: "create",
718
+ taskId: newId,
719
+ path: taskPath2,
720
+ previousContent: null,
721
+ newContent: content,
722
+ timestamp: Date.now()
723
+ });
724
+ return newId;
725
+ }
726
+ function archiveTaskInBoard(kandownDir, taskId) {
727
+ const tasksDir = getTasksDir(kandownDir);
728
+ const taskPath2 = join3(tasksDir, `${taskId}.md`);
729
+ if (!existsSync3(taskPath2)) return false;
730
+ try {
731
+ const prevContent = readFileSync3(taskPath2, "utf8");
732
+ const archiveDir = join3(tasksDir, "archive");
733
+ if (!existsSync3(archiveDir)) mkdirSync2(archiveDir, { recursive: true });
734
+ const parsed = readTask(kandownDir, taskId);
735
+ const newContent = serializeTaskFile({
736
+ ...parsed.frontmatter,
737
+ id: taskId,
738
+ archived: true
739
+ }, parsed.body);
740
+ const destPath = join3(archiveDir, `${taskId}.md`);
741
+ atomicWriteFileSync(destPath, newContent);
742
+ unlinkSync3(taskPath2);
743
+ pushUndo(kandownDir, {
744
+ type: "archive",
745
+ taskId,
746
+ path: destPath,
747
+ previousContent: prevContent,
748
+ newContent,
749
+ timestamp: Date.now()
750
+ });
751
+ return true;
752
+ } catch {
753
+ return false;
754
+ }
755
+ }
655
756
 
656
757
  // src/cli/lib/daemon.ts
657
758
  import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync as unlinkSync4 } from "fs";
@@ -717,16 +818,16 @@ async function fetchDaemonInfo(port) {
717
818
  }
718
819
  }
719
820
  function isPortListening(port, timeoutMs = 400) {
720
- return new Promise((resolve4) => {
821
+ return new Promise((resolve3) => {
721
822
  const socket = createConnection({ port, host: "127.0.0.1" }, () => {
722
823
  socket.destroy();
723
- resolve4(true);
824
+ resolve3(true);
724
825
  });
725
- socket.on("error", () => resolve4(false));
826
+ socket.on("error", () => resolve3(false));
726
827
  socket.setTimeout(timeoutMs);
727
828
  socket.on("timeout", () => {
728
829
  socket.destroy();
729
- resolve4(false);
830
+ resolve3(false);
730
831
  });
731
832
  });
732
833
  }
@@ -754,7 +855,7 @@ async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
754
855
  if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
755
856
  return { running: true, metadata };
756
857
  }
757
- await new Promise((resolve4) => setTimeout(resolve4, 120));
858
+ await new Promise((resolve3) => setTimeout(resolve3, 120));
758
859
  }
759
860
  return { running: false, metadata: null };
760
861
  }
@@ -783,13 +884,54 @@ async function startProjectDaemon(kandownDir, preferredPort) {
783
884
  child.unref();
784
885
  return waitForDaemon(kandownDir);
785
886
  }
887
+ async function isOwnedKandownDaemon(pid, port, kandownDir) {
888
+ const remote = await fetchDaemonInfo(port);
889
+ if (remote) return remote.pid === pid && remote.kandownDir === kandownDir;
890
+ try {
891
+ const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], {
892
+ encoding: "utf8",
893
+ timeout: 2e3
894
+ }).trim();
895
+ return /kandown/.test(cmd) && cmd.includes(kandownDir);
896
+ } catch {
897
+ return false;
898
+ }
899
+ }
900
+ async function stopProjectDaemon(kandownDir) {
901
+ const metadata = readDaemonMetadata(kandownDir);
902
+ if (!metadata) return false;
903
+ const pid = metadata.pid;
904
+ if (!isProcessAlive(pid)) {
905
+ removeDaemonMetadata(kandownDir);
906
+ return false;
907
+ }
908
+ if (!await isOwnedKandownDaemon(pid, metadata.port, kandownDir)) {
909
+ removeDaemonMetadata(kandownDir);
910
+ return false;
911
+ }
912
+ try {
913
+ process.kill(pid, "SIGTERM");
914
+ } catch {
915
+ }
916
+ const started = Date.now();
917
+ while (Date.now() - started < 2500 && isProcessAlive(pid)) {
918
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
919
+ }
920
+ if (isProcessAlive(pid)) {
921
+ try {
922
+ process.kill(pid, "SIGKILL");
923
+ } catch {
924
+ }
925
+ }
926
+ removeDaemonMetadata(kandownDir);
927
+ return true;
928
+ }
786
929
 
787
930
  // src/cli/lib/server.ts
788
931
  import { createServer } from "http";
789
932
  import { existsSync as existsSync5, readFileSync as readFileSync5, copyFileSync, unlinkSync as unlinkSync5, mkdirSync as mkdirSync3 } from "fs";
790
- import { join as join5, resolve as resolve2, dirname as dirname4 } from "path";
791
- import { execSync as execSync2, spawn as spawn3 } from "child_process";
792
- import { homedir as homedir3 } from "os";
933
+ import { join as join5 } from "path";
934
+ import { spawn as spawn3 } from "child_process";
793
935
  var START_PORT_RANGE = 2050;
794
936
  var END_PORT_RANGE = 2099;
795
937
  var UNSAFE_PORTS = /* @__PURE__ */ new Set([2049, 4045, 6e3, 6665, 6666, 6667, 6668, 6669, 6697]);
@@ -826,8 +968,7 @@ function writeText(res, status, text) {
826
968
  function syncProjectKandownHtml(kandownDir) {
827
969
  try {
828
970
  const projectHtml = join5(kandownDir, "kandown.html");
829
- const cliRoot = resolve2(import.meta.url ? new URL("../..", import.meta.url).pathname : process.cwd());
830
- const distHtml = join5(cliRoot, "dist", "index.html");
971
+ const distHtml = join5(PKG_ROOT, "dist", "index.html");
831
972
  if (!existsSync5(distHtml)) return false;
832
973
  if (!existsSync5(projectHtml)) {
833
974
  copyFileSync(distHtml, projectHtml);
@@ -843,27 +984,6 @@ function syncProjectKandownHtml(kandownDir) {
843
984
  }
844
985
  return false;
845
986
  }
846
- function syncGlobalSymlinks() {
847
- try {
848
- const home = homedir3();
849
- const candidatePaths = [
850
- join5(home, ".local", "bin", "kandown"),
851
- join5(home, "Library", "pnpm", "bin", "kandown"),
852
- join5(home, ".nvm", "versions", "node", "v25.2.1", "bin", "kandown")
853
- ];
854
- const currentBin = resolve2(import.meta.url ? new URL("../../bin/kandown.js", import.meta.url).pathname : process.cwd());
855
- for (const targetPath of candidatePaths) {
856
- if (existsSync5(dirname4(targetPath))) {
857
- try {
858
- if (existsSync5(targetPath)) unlinkSync5(targetPath);
859
- execSync2(`ln -sf "${currentBin}" "${targetPath}" 2>/dev/null || true`);
860
- } catch {
861
- }
862
- }
863
- }
864
- } catch {
865
- }
866
- }
867
987
  async function handleApi(req, res, url, kandownDir) {
868
988
  const path = url.pathname;
869
989
  const method = req.method || "GET";
@@ -884,7 +1004,7 @@ async function handleApi(req, res, url, kandownDir) {
884
1004
  }
885
1005
  if (path === "/api/update/check" && method === "GET") {
886
1006
  const current = getCurrentVersion();
887
- const latest = await new Promise((resolve4) => {
1007
+ const latest = await new Promise((resolve3) => {
888
1008
  const child = spawn3("npm", ["view", "kandown", "version"], {
889
1009
  timeout: 4e3,
890
1010
  stdio: ["pipe", "pipe", "pipe"],
@@ -897,10 +1017,10 @@ async function handleApi(req, res, url, kandownDir) {
897
1017
  });
898
1018
  child.stderr.on("data", () => {
899
1019
  });
900
- child.on("error", () => resolve4(null));
1020
+ child.on("error", () => resolve3(null));
901
1021
  child.on("close", (code) => {
902
- if (code !== 0) return resolve4(null);
903
- resolve4(stdout.trim().replace(/^"|"$/g, "") || null);
1022
+ if (code !== 0) return resolve3(null);
1023
+ resolve3(stdout.trim().replace(/^"|"$/g, "") || null);
904
1024
  });
905
1025
  });
906
1026
  const updateAvailable = latest ? semverGt(latest, current) > 0 : false;
@@ -912,7 +1032,7 @@ async function handleApi(req, res, url, kandownDir) {
912
1032
  }
913
1033
  if (path === "/api/update/apply" && method === "POST") {
914
1034
  const current = getCurrentVersion();
915
- const latest = await new Promise((resolve4) => {
1035
+ const latest = await new Promise((resolve3) => {
916
1036
  const child = spawn3("npm", ["view", "kandown", "version"], {
917
1037
  timeout: 4e3,
918
1038
  stdio: ["pipe", "pipe", "pipe"],
@@ -923,12 +1043,11 @@ async function handleApi(req, res, url, kandownDir) {
923
1043
  child.stdout.on("data", (d) => {
924
1044
  stdout += d;
925
1045
  });
926
- child.on("error", () => resolve4(null));
927
- child.on("close", (code) => resolve4(code === 0 ? stdout.trim() : null));
1046
+ child.on("error", () => resolve3(null));
1047
+ child.on("close", (code) => resolve3(code === 0 ? stdout.trim() : null));
928
1048
  });
929
1049
  const targetVersion = latest || current;
930
1050
  const ok = await performGlobalPackageUpdate(`kandown@${targetVersion}`);
931
- syncGlobalSymlinks();
932
1051
  syncProjectKandownHtml(kandownDir);
933
1052
  if (ok) {
934
1053
  writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully" });
@@ -1003,10 +1122,10 @@ async function handleApi(req, res, url, kandownDir) {
1003
1122
  if (path.startsWith("/api/tasks/")) {
1004
1123
  const taskId = decodeURIComponent(path.slice("/api/tasks/".length));
1005
1124
  const tasksDir = getTasksDir(kandownDir);
1006
- const taskPath = join5(tasksDir, `${taskId}.md`);
1125
+ const taskPath2 = join5(tasksDir, `${taskId}.md`);
1007
1126
  if (method === "GET") {
1008
- if (!existsSync5(taskPath)) return writeText(res, 404, "Task not found");
1009
- return writeText(res, 200, readFileSync5(taskPath, "utf8"));
1127
+ if (!existsSync5(taskPath2)) return writeText(res, 404, "Task not found");
1128
+ return writeText(res, 200, readFileSync5(taskPath2, "utf8"));
1010
1129
  }
1011
1130
  if (method === "PUT") {
1012
1131
  let body = "";
@@ -1015,14 +1134,14 @@ async function handleApi(req, res, url, kandownDir) {
1015
1134
  });
1016
1135
  req.on("end", () => {
1017
1136
  if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
1018
- atomicWriteFileSync(taskPath, body);
1137
+ atomicWriteFileSync(taskPath2, body);
1019
1138
  broadcastSseEvent({ type: "task", id: taskId });
1020
1139
  writeJson(res, 200, { ok: true });
1021
1140
  });
1022
1141
  return;
1023
1142
  }
1024
1143
  if (method === "DELETE") {
1025
- if (existsSync5(taskPath)) unlinkSync5(taskPath);
1144
+ if (existsSync5(taskPath2)) unlinkSync5(taskPath2);
1026
1145
  broadcastSseEvent({ type: "task_delete", id: taskId });
1027
1146
  return writeJson(res, 200, { ok: true });
1028
1147
  }
@@ -1041,7 +1160,6 @@ function serveApp(res, kandownDir) {
1041
1160
  }
1042
1161
  function createServeServer(kandownDir) {
1043
1162
  syncProjectKandownHtml(kandownDir);
1044
- syncGlobalSymlinks();
1045
1163
  return createServer((req, res) => {
1046
1164
  const url = new URL(req.url || "/", "http://localhost");
1047
1165
  if (req.method === "OPTIONS") return handleCors(res);
@@ -1084,6 +1202,281 @@ async function listenOnAvailablePort(kandownDir, preferredPort) {
1084
1202
  throw new Error(`No free port in range ${START_PORT_RANGE}-${END_PORT_RANGE}`);
1085
1203
  }
1086
1204
 
1205
+ // src/cli/lib/init.ts
1206
+ import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1207
+ import { join as join6 } from "path";
1208
+ function copyRecursive(src, dest) {
1209
+ const errors = [];
1210
+ try {
1211
+ if (!existsSync6(dest)) mkdirSync4(dest, { recursive: true });
1212
+ const entries = readdirSync2(src);
1213
+ for (const entry of entries) {
1214
+ const srcPath = join6(src, entry);
1215
+ const destPath = join6(dest, entry);
1216
+ try {
1217
+ if (statSync2(srcPath).isDirectory()) {
1218
+ errors.push(...copyRecursive(srcPath, destPath));
1219
+ } else if (!existsSync6(destPath)) {
1220
+ copyFileSync2(srcPath, destPath);
1221
+ }
1222
+ } catch (error) {
1223
+ errors.push(`${entry}: ${error instanceof Error ? error.message : String(error)}`);
1224
+ }
1225
+ }
1226
+ } catch (error) {
1227
+ errors.push(`${src}: ${error instanceof Error ? error.message : String(error)}`);
1228
+ }
1229
+ return errors;
1230
+ }
1231
+ function syncKandownAgentDoc(kandownDir) {
1232
+ const source = join6(PKG_ROOT, "templates", "AGENT_KANDOWN.md");
1233
+ const target = join6(kandownDir, "AGENT_KANDOWN.md");
1234
+ if (!existsSync6(source)) return false;
1235
+ try {
1236
+ const expected = readFileSync6(source, "utf8");
1237
+ const existing = existsSync6(target) ? readFileSync6(target, "utf8") : null;
1238
+ if (existing === null || !existing.includes("# Kandown")) {
1239
+ atomicWriteFileSync(target, expected.endsWith("\n") ? expected : `${expected}
1240
+ `);
1241
+ return true;
1242
+ }
1243
+ } catch {
1244
+ }
1245
+ return false;
1246
+ }
1247
+ function doInit(kandownDir) {
1248
+ try {
1249
+ mkdirSync4(kandownDir, { recursive: true });
1250
+ const htmlSrc = join6(PKG_ROOT, "dist", "index.html");
1251
+ const htmlDest = join6(kandownDir, "kandown.html");
1252
+ if (existsSync6(htmlSrc)) {
1253
+ copyFileSync2(htmlSrc, htmlDest);
1254
+ }
1255
+ syncKandownAgentDoc(kandownDir);
1256
+ const templatesDir = join6(PKG_ROOT, "templates");
1257
+ if (existsSync6(templatesDir)) {
1258
+ if (!existsSync6(join6(kandownDir, "README.md")) && existsSync6(join6(templatesDir, "README.md"))) {
1259
+ copyFileSync2(join6(templatesDir, "README.md"), join6(kandownDir, "README.md"));
1260
+ }
1261
+ if (!existsSync6(join6(kandownDir, "AGENT.md")) && existsSync6(join6(templatesDir, "AGENT.md"))) {
1262
+ copyFileSync2(join6(templatesDir, "AGENT.md"), join6(kandownDir, "AGENT.md"));
1263
+ }
1264
+ const tasksSrc = join6(templatesDir, "tasks");
1265
+ const tasksDest = getTasksDir(kandownDir);
1266
+ if (!existsSync6(tasksDest) && existsSync6(tasksSrc)) {
1267
+ copyRecursive(tasksSrc, tasksDest);
1268
+ }
1269
+ if (!existsSync6(join6(kandownDir, "kandown.json")) && existsSync6(join6(templatesDir, "kandown.json"))) {
1270
+ copyFileSync2(join6(templatesDir, "kandown.json"), join6(kandownDir, "kandown.json"));
1271
+ }
1272
+ }
1273
+ return true;
1274
+ } catch (error) {
1275
+ console.error(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
1276
+ return false;
1277
+ }
1278
+ }
1279
+
1280
+ // src/cli/lib/mcp.ts
1281
+ import { existsSync as existsSync7 } from "fs";
1282
+ import { join as join7 } from "path";
1283
+ function startMcpServer(kandownDir) {
1284
+ process.stdin.setEncoding("utf8");
1285
+ let buffer = "";
1286
+ process.stdin.on("data", (chunk) => {
1287
+ buffer += chunk;
1288
+ const lines = buffer.split("\n");
1289
+ buffer = lines.pop() ?? "";
1290
+ for (const line of lines) {
1291
+ const trimmed = line.trim();
1292
+ if (!trimmed) continue;
1293
+ try {
1294
+ const req = JSON.parse(trimmed);
1295
+ handleJsonRpc(kandownDir, req);
1296
+ } catch (e) {
1297
+ sendResponse(null, { error: { code: -32700, message: "Parse error" } });
1298
+ }
1299
+ }
1300
+ });
1301
+ }
1302
+ function sendResponse(id, resultOrError) {
1303
+ if (id === void 0) return;
1304
+ const resp = {
1305
+ jsonrpc: "2.0",
1306
+ id,
1307
+ ...resultOrError
1308
+ };
1309
+ process.stdout.write(JSON.stringify(resp) + "\n");
1310
+ }
1311
+ function handleJsonRpc(kandownDir, req) {
1312
+ const { id, method, params } = req;
1313
+ if (method === "initialize") {
1314
+ sendResponse(id, {
1315
+ result: {
1316
+ protocolVersion: "2024-11-05",
1317
+ capabilities: { tools: {} },
1318
+ serverInfo: { name: "kandown", version: "0.20.0" }
1319
+ }
1320
+ });
1321
+ return;
1322
+ }
1323
+ if (method === "notifications/initialized") {
1324
+ return;
1325
+ }
1326
+ if (method === "tools/list") {
1327
+ sendResponse(id, {
1328
+ result: {
1329
+ tools: [
1330
+ {
1331
+ name: "list_tasks",
1332
+ description: "List all tasks on the Kandown board with optional filtering",
1333
+ inputSchema: {
1334
+ type: "object",
1335
+ properties: {
1336
+ status: { type: "string", description: "Filter by column/status name" },
1337
+ assignee: { type: "string", description: "Filter by assignee" },
1338
+ tag: { type: "string", description: "Filter by tag" },
1339
+ priority: { type: "string", description: "Filter by priority (P1, P2, P3, P4)" }
1340
+ }
1341
+ }
1342
+ },
1343
+ {
1344
+ name: "get_task",
1345
+ description: "Get details and full content of a specific task by ID",
1346
+ inputSchema: {
1347
+ type: "object",
1348
+ properties: {
1349
+ id: { type: "string", description: "Task ID (e.g. t1, t42)" }
1350
+ },
1351
+ required: ["id"]
1352
+ }
1353
+ },
1354
+ {
1355
+ name: "create_task",
1356
+ description: "Create a new task on the Kandown board",
1357
+ inputSchema: {
1358
+ type: "object",
1359
+ properties: {
1360
+ title: { type: "string", description: "Task title (supports inline syntax #tag @user p1 due:date)" },
1361
+ status: { type: "string", description: "Target column name (default: Backlog)" },
1362
+ priority: { type: "string", description: "Priority level (P1, P2, P3, P4)" },
1363
+ assignee: { type: "string", description: "Assignee username" },
1364
+ tags: { type: "array", items: { type: "string" }, description: "Tags" },
1365
+ body: { type: "string", description: "Markdown body content" }
1366
+ },
1367
+ required: ["title"]
1368
+ }
1369
+ },
1370
+ {
1371
+ name: "move_task",
1372
+ description: "Move a task to a different column or to archived",
1373
+ inputSchema: {
1374
+ type: "object",
1375
+ properties: {
1376
+ id: { type: "string", description: "Task ID" },
1377
+ status: { type: "string", description: 'Target column name or "archived"' }
1378
+ },
1379
+ required: ["id", "status"]
1380
+ }
1381
+ },
1382
+ {
1383
+ name: "add_report",
1384
+ description: "Append an agent execution report to a task body",
1385
+ inputSchema: {
1386
+ type: "object",
1387
+ properties: {
1388
+ id: { type: "string", description: "Task ID" },
1389
+ report: { type: "string", description: "Markdown report content to append under ## Report" }
1390
+ },
1391
+ required: ["id", "report"]
1392
+ }
1393
+ },
1394
+ {
1395
+ name: "list_columns",
1396
+ description: "List configured board columns and task counts",
1397
+ inputSchema: { type: "object", properties: {} }
1398
+ }
1399
+ ]
1400
+ }
1401
+ });
1402
+ return;
1403
+ }
1404
+ if (method === "tools/call") {
1405
+ const { name, arguments: args = {} } = params || {};
1406
+ if (name === "list_tasks") {
1407
+ const board = readBoard(kandownDir);
1408
+ let tasks = [];
1409
+ for (const col of board.columns) {
1410
+ if (args.status && col.name.toLowerCase() !== String(args.status).toLowerCase()) continue;
1411
+ for (const t of col.tasks) {
1412
+ if (args.assignee && t.assignee !== args.assignee) continue;
1413
+ if (args.priority && t.priority !== args.priority) continue;
1414
+ if (args.tag && !t.tags.includes(args.tag)) continue;
1415
+ tasks.push({ ...t, status: col.name });
1416
+ }
1417
+ }
1418
+ sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] } });
1419
+ return;
1420
+ }
1421
+ if (name === "get_task") {
1422
+ const task = readTask(kandownDir, args.id);
1423
+ sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] } });
1424
+ return;
1425
+ }
1426
+ if (name === "create_task") {
1427
+ const newId = createTaskInBoard(kandownDir, args.title, args.status);
1428
+ if (args.body || args.priority || args.assignee || args.tags) {
1429
+ const task = readTask(kandownDir, newId);
1430
+ const fm = {
1431
+ ...task.frontmatter,
1432
+ ...args.priority ? { priority: args.priority } : {},
1433
+ ...args.assignee ? { assignee: args.assignee } : {},
1434
+ ...args.tags ? { tags: args.tags } : {}
1435
+ };
1436
+ const body = args.body ? (task.body + "\n\n" + args.body).trim() : task.body;
1437
+ const taskPath2 = join7(getTasksDir(kandownDir), `${newId}.md`);
1438
+ atomicWriteFileSync(taskPath2, serializeTaskFile(fm, body));
1439
+ }
1440
+ sendResponse(id, { result: { content: [{ type: "text", text: `Created task ${newId}` }] } });
1441
+ return;
1442
+ }
1443
+ if (name === "move_task") {
1444
+ const ok = moveTaskToColumn(kandownDir, args.id, args.status);
1445
+ sendResponse(id, { result: { content: [{ type: "text", text: ok ? `Moved ${args.id} to ${args.status}` : `Failed to move ${args.id}` }] } });
1446
+ return;
1447
+ }
1448
+ if (name === "add_report") {
1449
+ const taskPath2 = join7(getTasksDir(kandownDir), `${args.id}.md`);
1450
+ if (!existsSync7(taskPath2)) {
1451
+ sendResponse(id, { error: { code: -32602, message: `Task ${args.id} not found` } });
1452
+ return;
1453
+ }
1454
+ const task = readTask(kandownDir, args.id);
1455
+ const reportSection = `
1456
+
1457
+ ## Report
1458
+
1459
+ ${args.report.trim()}`;
1460
+ const newBody = task.body.includes("## Report") ? task.body.replace(/## Report[\s\S]*/, `## Report
1461
+
1462
+ ${args.report.trim()}`) : task.body.trim() + reportSection;
1463
+ atomicWriteFileSync(taskPath2, serializeTaskFile(task.frontmatter, newBody));
1464
+ sendResponse(id, { result: { content: [{ type: "text", text: `Appended report to ${args.id}` }] } });
1465
+ return;
1466
+ }
1467
+ if (name === "list_columns") {
1468
+ const config = loadConfig(kandownDir);
1469
+ const board = readBoard(kandownDir);
1470
+ const cols = board.columns.map((c2) => ({ name: c2.name, count: c2.tasks.length }));
1471
+ sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(cols, null, 2) }] } });
1472
+ return;
1473
+ }
1474
+ sendResponse(id, { error: { code: -32601, message: `Unknown tool: ${name}` } });
1475
+ return;
1476
+ }
1477
+ sendResponse(id, { error: { code: -32601, message: `Method not found: ${method}` } });
1478
+ }
1479
+
1087
1480
  // src/cli/cli.ts
1088
1481
  var c = {
1089
1482
  reset: "\x1B[0m",
@@ -1099,10 +1492,10 @@ function log(msg) {
1099
1492
  console.log(msg);
1100
1493
  }
1101
1494
  function info(msg) {
1102
- console.log(`${c.blue}\u2139${c.reset} ${msg}`);
1495
+ console.error(`${c.blue}\u2139${c.reset} ${msg}`);
1103
1496
  }
1104
1497
  function success(msg) {
1105
- console.log(`${c.green}\u2713${c.reset} ${msg}`);
1498
+ console.error(`${c.green}\u2713${c.reset} ${msg}`);
1106
1499
  }
1107
1500
  function err(msg) {
1108
1501
  console.error(`${c.red}\u2717${c.reset} ${msg}`);
@@ -1136,20 +1529,74 @@ function parseArgs(args) {
1136
1529
  }
1137
1530
  return { flags, positional, path: typeof flags.path === "string" ? flags.path : ".kandown" };
1138
1531
  }
1532
+ var COMMANDS = /* @__PURE__ */ new Set([
1533
+ "init",
1534
+ "update",
1535
+ "upgrade",
1536
+ "doctor",
1537
+ "work",
1538
+ "list",
1539
+ "ls",
1540
+ "show",
1541
+ "move",
1542
+ "help",
1543
+ "daemon",
1544
+ "board",
1545
+ "settings",
1546
+ "tasks",
1547
+ "create",
1548
+ "new",
1549
+ "assign",
1550
+ "commit",
1551
+ "projects",
1552
+ "export",
1553
+ "import",
1554
+ "mcp",
1555
+ "version"
1556
+ ]);
1557
+ function splitCommand(args) {
1558
+ const withoutGlobalFlags = args.filter((arg) => arg !== "--no-update-check");
1559
+ const commandIndex = withoutGlobalFlags.findIndex((arg, index) => {
1560
+ if (arg.startsWith("-")) return false;
1561
+ if (COMMANDS.has(arg)) return true;
1562
+ return index === 0;
1563
+ });
1564
+ if (commandIndex === -1) {
1565
+ return { cmd: void 0, rest: withoutGlobalFlags };
1566
+ }
1567
+ return {
1568
+ cmd: withoutGlobalFlags[commandIndex],
1569
+ rest: [...withoutGlobalFlags.slice(0, commandIndex), ...withoutGlobalFlags.slice(commandIndex + 1)]
1570
+ };
1571
+ }
1572
+ function stripFirstPositional(args, value) {
1573
+ const result = [];
1574
+ let stripped = false;
1575
+ for (const arg of args) {
1576
+ if (!stripped && arg === value) {
1577
+ stripped = true;
1578
+ continue;
1579
+ }
1580
+ result.push(arg);
1581
+ }
1582
+ return result;
1583
+ }
1139
1584
  function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
1140
- if (basename(cwd) === ".kandown" || existsSync6(join6(cwd, "kandown.json"))) {
1585
+ if (basename(cwd) === ".kandown" || existsSync8(join8(cwd, "kandown.json"))) {
1141
1586
  return cwd;
1142
1587
  }
1143
- return resolve3(cwd, pathArg);
1588
+ return resolve2(cwd, pathArg);
1144
1589
  }
1145
1590
  function ensureKandownDir(rawArgs) {
1146
1591
  const args = parseArgs(rawArgs);
1147
1592
  const cwd = process.cwd();
1148
1593
  const kandownDir = resolveKandownDir(args.path, cwd);
1149
- if (!existsSync6(kandownDir)) {
1150
- err(`No Kandown installation found at ${c.bold}${kandownDir}${c.reset}`);
1151
- log(` Run ${c.cyan}npx kandown init${c.reset} to create one.`);
1152
- process.exit(1);
1594
+ if (!existsSync8(kandownDir)) {
1595
+ info(`No .kandown/ found \u2014 auto-initializing ${c.bold}${kandownDir}${c.reset}`);
1596
+ if (!doInit(kandownDir)) {
1597
+ err(`Could not auto-initialize Kandown at ${c.bold}${kandownDir}${c.reset}`);
1598
+ process.exit(1);
1599
+ }
1153
1600
  }
1154
1601
  return { kandownDir, cwd };
1155
1602
  }
@@ -1163,6 +1610,7 @@ ${c.bold}USAGE:${c.reset}
1163
1610
 
1164
1611
  ${c.bold}COMMANDS:${c.reset}
1165
1612
  (none) Start web server, open browser & launch TUI
1613
+ init Initialize Kandown in this project
1166
1614
  work Output agent rules + live board digest
1167
1615
  list List tasks (alias: ls)
1168
1616
  show <id> Display task details
@@ -1183,13 +1631,25 @@ ${c.bold}OPTIONS:${c.reset}
1183
1631
  --path <dir> Path to .kandown folder (default: .kandown)
1184
1632
  --port <number> Server port (default: 2050)
1185
1633
  --no-open Don't open browser automatically
1634
+ --no-update-check Skip the registry update check for this run
1635
+ --version Print CLI version
1186
1636
  --help, -h Show help screen
1187
1637
  `);
1188
1638
  }
1639
+ function cmdInit(rawArgs) {
1640
+ const args = parseArgs(rawArgs);
1641
+ const kandownDir = resolve2(process.cwd(), args.path);
1642
+ const created = doInit(kandownDir);
1643
+ if (!created) {
1644
+ err("Failed to initialize Kandown.");
1645
+ process.exit(1);
1646
+ }
1647
+ success(`Kandown initialized at ${kandownDir}`);
1648
+ }
1189
1649
  async function cmdUpdate(rawArgs) {
1190
1650
  const current = getCurrentVersion();
1191
1651
  log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
1192
- const latest = await new Promise((resolve4) => {
1652
+ const latest = await new Promise((resolve3) => {
1193
1653
  const child = spawn4("npm", ["view", "kandown", "version"], {
1194
1654
  timeout: 6e3,
1195
1655
  stdio: ["pipe", "pipe", "pipe"],
@@ -1202,11 +1662,11 @@ async function cmdUpdate(rawArgs) {
1202
1662
  });
1203
1663
  child.stderr.on("data", () => {
1204
1664
  });
1205
- child.on("error", () => resolve4(null));
1665
+ child.on("error", () => resolve3(null));
1206
1666
  child.on("close", (code) => {
1207
- if (code !== 0) return resolve4(null);
1667
+ if (code !== 0) return resolve3(null);
1208
1668
  const v = stdout.trim().replace(/^"|"$/g, "");
1209
- resolve4(v || null);
1669
+ resolve3(v || null);
1210
1670
  });
1211
1671
  });
1212
1672
  if (latest && semverGt(latest, current) > 0) {
@@ -1222,12 +1682,12 @@ async function cmdUpdate(rawArgs) {
1222
1682
  }
1223
1683
  const args = parseArgs(rawArgs);
1224
1684
  const cwd = process.cwd();
1225
- const kandownDir = resolve3(cwd, args.path);
1226
- const htmlDest = join6(kandownDir, "kandown.html");
1227
- if (existsSync6(htmlDest)) {
1228
- const htmlSrc = resolve3(PKG_ROOT, "dist", "index.html");
1229
- if (existsSync6(htmlSrc)) {
1230
- copyFileSync2(htmlSrc, htmlDest);
1685
+ const kandownDir = resolve2(cwd, args.path);
1686
+ const htmlDest = join8(kandownDir, "kandown.html");
1687
+ if (existsSync8(htmlDest)) {
1688
+ const htmlSrc = resolve2(PKG_ROOT, "dist", "index.html");
1689
+ if (existsSync8(htmlSrc)) {
1690
+ copyFileSync3(htmlSrc, htmlDest);
1231
1691
  success(`Refreshed ${args.path}/kandown.html`);
1232
1692
  }
1233
1693
  }
@@ -1238,10 +1698,10 @@ async function cmdDoctor(rawArgs) {
1238
1698
  log(`${c.bold}kandown doctor${c.reset} ${c.dim}\u2014 environment & board diagnostic${c.reset}
1239
1699
  `);
1240
1700
  log(` CLI Version: ${currentVersion}`);
1241
- const configPath = join6(kandownDir, "kandown.json");
1242
- if (existsSync6(configPath)) {
1701
+ const configPath = join8(kandownDir, "kandown.json");
1702
+ if (existsSync8(configPath)) {
1243
1703
  try {
1244
- JSON.parse(readFileSync6(configPath, "utf8"));
1704
+ JSON.parse(readFileSync8(configPath, "utf8"));
1245
1705
  success("kandown.json valid");
1246
1706
  } catch (e) {
1247
1707
  err(`kandown.json invalid: ${e.message}`);
@@ -1275,63 +1735,400 @@ async function cmdWork(rawArgs) {
1275
1735
  log(`- **${col.name}** (${col.tasks.length}): ${col.tasks.map((t) => `${t.id} ${t.title}`).join(", ") || "empty"}`);
1276
1736
  }
1277
1737
  }
1738
+ function addMultiFlag(flags, key, value) {
1739
+ const current = flags[key];
1740
+ if (Array.isArray(current)) current.push(value);
1741
+ else if (typeof current === "string") flags[key] = [current, value];
1742
+ else flags[key] = value;
1743
+ }
1744
+ function taskParseArgs(argv) {
1745
+ const flags = {};
1746
+ const positional = [];
1747
+ const aliases = { s: "status", p: "priority", a: "assignee", t: "tag", m: "message" };
1748
+ for (let i = 0; i < argv.length; i++) {
1749
+ const arg = argv[i];
1750
+ if (arg.startsWith("--")) {
1751
+ const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
1752
+ const key = rawKey === "to" ? "to" : rawKey;
1753
+ const next = inlineValue ?? argv[i + 1];
1754
+ if (inlineValue === void 0 && next && !next.startsWith("-")) i++;
1755
+ const value = inlineValue ?? (next && !next.startsWith("-") ? next : true);
1756
+ if (key === "tag") addMultiFlag(flags, key, String(value));
1757
+ else flags[key] = value;
1758
+ continue;
1759
+ }
1760
+ if (/^-[spatm]$/.test(arg)) {
1761
+ const key = aliases[arg.slice(1)];
1762
+ const value = argv[i + 1];
1763
+ if (!value || value.startsWith("-")) {
1764
+ flags[key] = true;
1765
+ } else {
1766
+ i++;
1767
+ if (key === "tag") addMultiFlag(flags, key, value);
1768
+ else flags[key] = value;
1769
+ }
1770
+ continue;
1771
+ }
1772
+ positional.push(arg);
1773
+ }
1774
+ return { flags, positional };
1775
+ }
1776
+ function stringFlag(flags, ...keys) {
1777
+ for (const key of keys) {
1778
+ const value = flags[key];
1779
+ if (typeof value === "string" && value.trim()) return value.trim();
1780
+ }
1781
+ return null;
1782
+ }
1783
+ function listFlag(flags, key) {
1784
+ const value = flags[key];
1785
+ if (Array.isArray(value)) return value.map(String).filter(Boolean);
1786
+ if (typeof value === "string" && value) return [value];
1787
+ return [];
1788
+ }
1789
+ function resolveStatusArg(kandownDir, status) {
1790
+ const config = loadConfig(kandownDir);
1791
+ return config.board.columns.find((col) => col.toLowerCase() === status.toLowerCase()) ?? null;
1792
+ }
1793
+ function taskPath(kandownDir, id, archived = false) {
1794
+ return archived ? join8(getTasksDir(kandownDir), "archive", `${id}.md`) : join8(getTasksDir(kandownDir), `${id}.md`);
1795
+ }
1796
+ function findTaskPath(kandownDir, id) {
1797
+ const active = taskPath(kandownDir, id);
1798
+ if (existsSync8(active)) return active;
1799
+ const archived = taskPath(kandownDir, id, true);
1800
+ if (existsSync8(archived)) return archived;
1801
+ return null;
1802
+ }
1803
+ function nextTaskId(kandownDir) {
1804
+ const ids = new Set(listTaskIds(kandownDir));
1805
+ const archiveDir = join8(getTasksDir(kandownDir), "archive");
1806
+ if (existsSync8(archiveDir)) {
1807
+ for (const file of readdirSync3(archiveDir)) {
1808
+ if (file.endsWith(".md")) ids.add(file.slice(0, -3));
1809
+ }
1810
+ }
1811
+ let max = 0;
1812
+ for (const id of ids) {
1813
+ const match = id.match(/^t(\d+)$/i);
1814
+ if (match) max = Math.max(max, Number(match[1]));
1815
+ }
1816
+ return `t${max + 1}`;
1817
+ }
1818
+ function readTaskFile(kandownDir, id) {
1819
+ const path = findTaskPath(kandownDir, id);
1820
+ if (!path) return null;
1821
+ const parsed = parseTaskFile(readFileSync8(path, "utf8"));
1822
+ return {
1823
+ path,
1824
+ frontmatter: { ...parsed.frontmatter, id: parsed.frontmatter.id || id },
1825
+ body: parsed.body,
1826
+ archived: path.includes("/archive/")
1827
+ };
1828
+ }
1278
1829
  function cmdList(rawArgs) {
1279
1830
  const { kandownDir } = ensureKandownDir(rawArgs);
1280
- const board = readBoard(kandownDir);
1281
- const { flags } = parseArgs(rawArgs);
1282
- const statusFilter = typeof flags.status === "string" ? flags.status.toLowerCase() : null;
1283
- const priorityFilter = typeof flags.priority === "string" ? flags.priority.toUpperCase() : null;
1284
- for (const col of board.columns) {
1285
- if (statusFilter && col.name.toLowerCase() !== statusFilter) continue;
1831
+ const args = taskParseArgs(rawArgs);
1832
+ const includeArchived = args.flags.archived === true;
1833
+ const statusFilter = stringFlag(args.flags, "status")?.toLowerCase() ?? null;
1834
+ const priorityFilter = stringFlag(args.flags, "priority")?.toUpperCase() ?? null;
1835
+ const assigneeFilter = stringFlag(args.flags, "assignee");
1836
+ const tagFilters = listFlag(args.flags, "tag").map((tag) => tag.toLowerCase());
1837
+ const rows = [];
1838
+ for (const id of listTaskIds(kandownDir)) {
1839
+ const task = readTask(kandownDir, id);
1840
+ rows.push({
1841
+ id,
1842
+ title: task.frontmatter.title || id,
1843
+ status: task.frontmatter.status || "Backlog",
1844
+ priority: task.frontmatter.priority || "",
1845
+ assignee: task.frontmatter.assignee || "",
1846
+ tags: Array.isArray(task.frontmatter.tags) ? task.frontmatter.tags : [],
1847
+ archived: false
1848
+ });
1849
+ }
1850
+ if (includeArchived) {
1851
+ const archiveDir = join8(getTasksDir(kandownDir), "archive");
1852
+ if (existsSync8(archiveDir)) {
1853
+ for (const file of readdirSync3(archiveDir).filter((name) => name.endsWith(".md"))) {
1854
+ const id = file.slice(0, -3);
1855
+ const parsed = parseTaskFile(readFileSync8(join8(archiveDir, file), "utf8"));
1856
+ rows.push({
1857
+ id,
1858
+ title: parsed.frontmatter.title || id,
1859
+ status: `${parsed.frontmatter.status || "Backlog"} (archived)`,
1860
+ priority: parsed.frontmatter.priority || "",
1861
+ assignee: parsed.frontmatter.assignee || "",
1862
+ tags: Array.isArray(parsed.frontmatter.tags) ? parsed.frontmatter.tags : [],
1863
+ archived: true
1864
+ });
1865
+ }
1866
+ }
1867
+ }
1868
+ const filtered = rows.filter((row) => {
1869
+ if (statusFilter && row.status.toLowerCase() !== statusFilter) return false;
1870
+ if (priorityFilter && row.priority.toUpperCase() !== priorityFilter) return false;
1871
+ if (assigneeFilter && row.assignee !== assigneeFilter) return false;
1872
+ if (tagFilters.length > 0 && !tagFilters.every((tag) => row.tags.map((t) => t.toLowerCase()).includes(tag))) return false;
1873
+ return true;
1874
+ });
1875
+ if (args.flags.json === true) {
1876
+ process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
1877
+ return;
1878
+ }
1879
+ const byStatus = /* @__PURE__ */ new Map();
1880
+ for (const row of filtered) {
1881
+ const list = byStatus.get(row.status) ?? [];
1882
+ list.push(row);
1883
+ byStatus.set(row.status, list);
1884
+ }
1885
+ for (const [status, tasks] of byStatus) {
1286
1886
  log(`
1287
- ${c.bold}${col.name}${c.reset} (${col.tasks.length})`);
1288
- for (const t of col.tasks) {
1289
- if (priorityFilter && t.priority !== priorityFilter) continue;
1290
- log(` ${c.cyan}${t.id}${c.reset} [${t.priority || "P2"}] ${t.title}`);
1887
+ ${c.bold}${status}${c.reset} (${tasks.length})`);
1888
+ for (const task of tasks) {
1889
+ const pri = task.priority || "P2";
1890
+ const assignee = task.assignee ? ` @${task.assignee}` : "";
1891
+ log(` ${c.cyan}${task.id}${c.reset} [${pri}] ${task.title}${assignee}`);
1291
1892
  }
1292
1893
  }
1293
1894
  log("");
1294
1895
  }
1295
1896
  function cmdShow(rawArgs) {
1296
1897
  const { kandownDir } = ensureKandownDir(rawArgs);
1297
- const id = rawArgs.find((a) => !a.startsWith("-"));
1898
+ const args = taskParseArgs(rawArgs);
1899
+ const id = args.positional[0];
1298
1900
  if (!id) {
1299
1901
  err("Usage: kandown show <task-id>");
1300
1902
  process.exit(1);
1301
1903
  }
1302
- try {
1303
- const task = readTask(kandownDir, id);
1304
- log(`${c.bold}${task.frontmatter.id}: ${task.frontmatter.title}${c.reset}`);
1305
- log(`Status: ${task.frontmatter.status} | Priority: ${task.frontmatter.priority || "P2"} | Assignee: ${task.frontmatter.assignee || "none"}
1306
- `);
1307
- log(task.body);
1308
- } catch (e) {
1309
- err(`Could not read task ${id}: ${e.message}`);
1904
+ const path = findTaskPath(kandownDir, id);
1905
+ if (!path) {
1906
+ err(`Task not found: ${id}`);
1907
+ process.exit(1);
1310
1908
  }
1909
+ process.stdout.write(readFileSync8(path, "utf8"));
1910
+ }
1911
+ function cmdCreate(rawArgs) {
1912
+ const { kandownDir } = ensureKandownDir(rawArgs);
1913
+ const args = taskParseArgs(rawArgs);
1914
+ const title = args.positional.join(" ").trim();
1915
+ if (!title) {
1916
+ err('Usage: kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id custom-id] [--json]');
1917
+ process.exit(1);
1918
+ }
1919
+ const id = stringFlag(args.flags, "id") ?? nextTaskId(kandownDir);
1920
+ if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
1921
+ err(`Invalid task id: ${id}`);
1922
+ process.exit(1);
1923
+ }
1924
+ if (findTaskPath(kandownDir, id)) {
1925
+ err(`Task already exists: ${id}`);
1926
+ process.exit(1);
1927
+ }
1928
+ const config = loadConfig(kandownDir);
1929
+ const rawStatus = stringFlag(args.flags, "to", "status");
1930
+ const status = rawStatus ? resolveStatusArg(kandownDir, rawStatus) : config.board.columns[0] || "Backlog";
1931
+ if (!status) {
1932
+ err(`Unknown status: ${rawStatus}`);
1933
+ process.exit(1);
1934
+ }
1935
+ const fm = {
1936
+ id,
1937
+ title,
1938
+ status,
1939
+ created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
1940
+ };
1941
+ const priority = stringFlag(args.flags, "priority")?.toUpperCase();
1942
+ const assignee = stringFlag(args.flags, "assignee");
1943
+ const tags = listFlag(args.flags, "tag");
1944
+ if (priority) fm.priority = priority;
1945
+ if (assignee) fm.assignee = assignee;
1946
+ if (tags.length > 0) fm.tags = tags;
1947
+ const tasksDir = getTasksDir(kandownDir);
1948
+ if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
1949
+ const path = taskPath(kandownDir, id);
1950
+ atomicWriteFileSync(path, serializeTaskFile(fm, ""));
1951
+ process.stderr.write(`${c.green}\u2713${c.reset} Created ${c.bold}${id}${c.reset} \u2192 ${status}
1952
+ `);
1953
+ process.stdout.write(args.flags.json === true ? JSON.stringify(fm, null, 2) + "\n" : `${id}
1954
+ `);
1311
1955
  }
1312
1956
  function cmdMove(rawArgs) {
1313
1957
  const { kandownDir } = ensureKandownDir(rawArgs);
1314
- const [id, newStatus] = rawArgs.filter((a) => !a.startsWith("-"));
1315
- if (!id || !newStatus) {
1958
+ const args = taskParseArgs(rawArgs);
1959
+ const id = args.positional[0];
1960
+ const rawStatus = args.positional.slice(1).join(" ") || stringFlag(args.flags, "to", "status");
1961
+ if (!id || !rawStatus) {
1316
1962
  err("Usage: kandown move <task-id> <status>");
1317
1963
  process.exit(1);
1318
1964
  }
1965
+ if (rawStatus.toLowerCase() === "archived") {
1966
+ if (!archiveTaskInBoard(kandownDir, id)) {
1967
+ err(`Archive failed: ${id}`);
1968
+ process.exit(1);
1969
+ }
1970
+ success(`Archived ${id}`);
1971
+ return;
1972
+ }
1973
+ const status = resolveStatusArg(kandownDir, rawStatus);
1974
+ if (!status) {
1975
+ err(`Unknown status: ${rawStatus}`);
1976
+ process.exit(1);
1977
+ }
1978
+ if (!moveTaskToColumn(kandownDir, id, status)) {
1979
+ err(`Move failed: ${id}`);
1980
+ process.exit(1);
1981
+ }
1982
+ success(`Moved ${id} \u2192 "${status}"`);
1983
+ }
1984
+ function cmdAssign(rawArgs) {
1985
+ const { kandownDir } = ensureKandownDir(rawArgs);
1986
+ const args = taskParseArgs(rawArgs);
1987
+ const [id, assignee] = args.positional;
1988
+ if (!id) {
1989
+ err("Usage: kandown assign <task-id> [assignee]");
1990
+ process.exit(1);
1991
+ }
1992
+ const task = readTaskFile(kandownDir, id);
1993
+ if (!task) {
1994
+ err(`Task not found: ${id}`);
1995
+ process.exit(1);
1996
+ }
1997
+ const frontmatter = { ...task.frontmatter, id };
1998
+ if (assignee) frontmatter.assignee = assignee;
1999
+ else delete frontmatter.assignee;
2000
+ atomicWriteFileSync(task.path, serializeTaskFile(frontmatter, task.body));
2001
+ success(assignee ? `Assigned ${id} \u2192 ${assignee}` : `Unassigned ${id}`);
2002
+ }
2003
+ function cmdCommit(rawArgs) {
2004
+ ensureKandownDir(rawArgs);
2005
+ const args = taskParseArgs(rawArgs);
2006
+ const message = stringFlag(args.flags, "message") || "tasks: update kandown board";
2007
+ const add = spawnSync("git", ["add", "tasks", ".kandown/kandown.json"], { stdio: "inherit" });
2008
+ if (add.status !== 0) process.exit(add.status ?? 1);
2009
+ const commit = spawnSync("git", ["commit", "-m", message], { stdio: "inherit" });
2010
+ process.exit(commit.status ?? 1);
2011
+ }
2012
+ function printTaskCommandsHelp() {
2013
+ log(`
2014
+ ${c.bold}Kandown task commands${c.reset}
2015
+
2016
+ kandown list [-s status] [-p P1] [-a user] [-t tag] [--archived] [--json]
2017
+ kandown show <id>
2018
+ kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id id] [--json]
2019
+ kandown move <id> <status|archived>
2020
+ kandown assign <id> [user]
2021
+ kandown commit [-m "message"]
2022
+ `);
2023
+ }
2024
+ async function launchTui(screen, kandownDir) {
2025
+ if (!process.stdin.isTTY) {
2026
+ info(`TUI skipped because stdin is not interactive. Use ${c.cyan}kandown daemon status${c.reset} to inspect the web daemon.`);
2027
+ return;
2028
+ }
2029
+ const tuiPath = join8(PKG_ROOT, "bin", "tui.js");
2030
+ if (!existsSync8(tuiPath)) {
2031
+ err(`TUI binary not found at ${tuiPath}`);
2032
+ process.exit(1);
2033
+ }
2034
+ await new Promise((resolveTui) => {
2035
+ const child = spawn4(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
2036
+ child.on("close", (code) => {
2037
+ if (typeof code === "number" && code !== 0) process.exit(code);
2038
+ resolveTui();
2039
+ });
2040
+ });
2041
+ }
2042
+ async function cmdTui(screen, rawArgs) {
2043
+ const { kandownDir } = ensureKandownDir(rawArgs);
2044
+ await launchTui(screen, kandownDir);
2045
+ }
2046
+ function cmdExport(rawArgs) {
2047
+ const { kandownDir } = ensureKandownDir(rawArgs);
2048
+ const board = readBoard(kandownDir);
2049
+ process.stdout.write(JSON.stringify(board, null, 2) + "\n");
2050
+ }
2051
+ function cmdProjects(rawArgs) {
2052
+ const { kandownDir } = ensureKandownDir(rawArgs);
2053
+ const metadataPath2 = join8(kandownDir, "daemon.json");
2054
+ if (!existsSync8(metadataPath2)) {
2055
+ info("No daemon metadata for this project.");
2056
+ return;
2057
+ }
2058
+ process.stdout.write(readFileSync8(metadataPath2, "utf8").trim() + "\n");
2059
+ }
2060
+ function cmdImport(rawArgs) {
2061
+ const { kandownDir } = ensureKandownDir(rawArgs);
2062
+ const args = taskParseArgs(rawArgs);
2063
+ const file = args.positional[0];
2064
+ if (!file) {
2065
+ err("Usage: kandown import <file.json> [--overwrite]");
2066
+ process.exit(1);
2067
+ }
2068
+ const importPath = resolve2(process.cwd(), file);
2069
+ if (!existsSync8(importPath)) {
2070
+ err(`Import file not found: ${file}`);
2071
+ process.exit(1);
2072
+ }
2073
+ let raw;
1319
2074
  try {
1320
- moveTaskToColumn(kandownDir, id, newStatus);
1321
- success(`Moved ${id} \u2192 "${newStatus}"`);
1322
- } catch (e) {
1323
- err(`Move failed: ${e.message}`);
2075
+ raw = JSON.parse(readFileSync8(importPath, "utf8"));
2076
+ } catch (error) {
2077
+ err(`Import file must be JSON: ${error instanceof Error ? error.message : String(error)}`);
2078
+ process.exit(1);
2079
+ }
2080
+ const rows = [];
2081
+ if (Array.isArray(raw)) {
2082
+ rows.push(...raw.filter((value) => typeof value === "object" && value !== null));
2083
+ } else if (typeof raw === "object" && raw !== null && Array.isArray(raw.columns)) {
2084
+ for (const column of raw.columns) {
2085
+ if (typeof column !== "object" || column === null) continue;
2086
+ const col = column;
2087
+ if (!Array.isArray(col.tasks)) continue;
2088
+ for (const task of col.tasks) {
2089
+ if (typeof task === "object" && task !== null) rows.push({ ...task, status: String(col.name || "Backlog") });
2090
+ }
2091
+ }
2092
+ }
2093
+ if (rows.length === 0) {
2094
+ err("No tasks found to import. Expected a list JSON array or kandown export object.");
2095
+ process.exit(1);
2096
+ }
2097
+ const tasksDir = getTasksDir(kandownDir);
2098
+ if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
2099
+ let imported = 0;
2100
+ for (const row of rows) {
2101
+ const id = typeof row.id === "string" && /^[a-zA-Z0-9_-]+$/.test(row.id) ? row.id : nextTaskId(kandownDir);
2102
+ const path = taskPath(kandownDir, id);
2103
+ if (existsSync8(path) && args.flags.overwrite !== true) continue;
2104
+ const fm = {
2105
+ id,
2106
+ title: typeof row.title === "string" && row.title ? row.title : id,
2107
+ status: typeof row.status === "string" && row.status ? row.status.replace(/ \(archived\)$/i, "") : "Backlog"
2108
+ };
2109
+ if (typeof row.priority === "string") fm.priority = row.priority;
2110
+ if (typeof row.assignee === "string") fm.assignee = row.assignee;
2111
+ if (Array.isArray(row.tags)) fm.tags = row.tags.map(String);
2112
+ atomicWriteFileSync(path, serializeTaskFile(fm, typeof row.body === "string" ? row.body : ""));
2113
+ imported++;
1324
2114
  }
2115
+ success(`Imported ${imported} task${imported === 1 ? "" : "s"}`);
1325
2116
  }
1326
2117
  async function main() {
1327
- const args = process.argv.slice(2);
1328
- const cmd = args[0];
1329
- const rest = args.slice(1);
1330
- const skipUpdate = args.includes("--no-update-check") || process.env.KANDOWN_NO_UPDATE === "1";
2118
+ const rawArgs = process.argv.slice(2);
2119
+ if (rawArgs.length === 1 && (rawArgs[0] === "--version" || rawArgs[0] === "version")) {
2120
+ log(getCurrentVersion());
2121
+ return;
2122
+ }
2123
+ const { cmd, rest } = splitCommand(rawArgs);
2124
+ const skipUpdate = rawArgs.includes("--no-update-check") || process.env.KANDOWN_NO_UPDATE === "1";
1331
2125
  if (!skipUpdate) {
1332
2126
  await checkForUpdate(process.argv);
1333
2127
  }
1334
2128
  switch (cmd) {
2129
+ case "init":
2130
+ cmdInit(rest);
2131
+ break;
1335
2132
  case "update":
1336
2133
  case "upgrade":
1337
2134
  await cmdUpdate(rest);
@@ -1342,6 +2139,12 @@ async function main() {
1342
2139
  case "work":
1343
2140
  await cmdWork(rest);
1344
2141
  break;
2142
+ case "board":
2143
+ await cmdTui("board", rest);
2144
+ break;
2145
+ case "settings":
2146
+ await cmdTui("settings", rest);
2147
+ break;
1345
2148
  case "list":
1346
2149
  case "ls":
1347
2150
  cmdList(rest);
@@ -1349,21 +2152,52 @@ async function main() {
1349
2152
  case "show":
1350
2153
  cmdShow(rest);
1351
2154
  break;
2155
+ case "create":
2156
+ case "new":
2157
+ cmdCreate(rest);
2158
+ break;
1352
2159
  case "move":
1353
2160
  cmdMove(rest);
1354
2161
  break;
2162
+ case "assign":
2163
+ cmdAssign(rest);
2164
+ break;
2165
+ case "commit":
2166
+ cmdCommit(rest);
2167
+ break;
2168
+ case "tasks":
2169
+ printTaskCommandsHelp();
2170
+ break;
2171
+ case "projects":
2172
+ cmdProjects(rest);
2173
+ break;
2174
+ case "export":
2175
+ cmdExport(rest);
2176
+ break;
2177
+ case "import":
2178
+ cmdImport(rest);
2179
+ break;
2180
+ case "mcp": {
2181
+ const { kandownDir } = ensureKandownDir(rest);
2182
+ startMcpServer(kandownDir);
2183
+ break;
2184
+ }
1355
2185
  case "help":
1356
2186
  case "--help":
1357
2187
  case "-h":
1358
2188
  help();
1359
2189
  break;
1360
2190
  case "daemon": {
1361
- const subcommand = rest[0] || "status";
1362
- const { kandownDir } = ensureKandownDir(rest.slice(1));
2191
+ const parsedDaemonArgs = parseArgs(rest);
2192
+ const subcommand = parsedDaemonArgs.positional[0] || "status";
2193
+ const daemonArgs = subcommand ? stripFirstPositional(rest, subcommand) : rest;
2194
+ const { kandownDir } = ensureKandownDir(daemonArgs);
1363
2195
  if (subcommand === "run") {
1364
- const { port } = await listenOnAvailablePort(kandownDir);
2196
+ const daemonOptions = parseArgs(daemonArgs);
2197
+ const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
2198
+ const { port } = await listenOnAvailablePort(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
1365
2199
  const url = `http://localhost:${port}`;
1366
- const metadataPath2 = join6(kandownDir, "daemon.json");
2200
+ const metadataPath2 = join8(kandownDir, "daemon.json");
1367
2201
  atomicWriteFileSync(metadataPath2, JSON.stringify({
1368
2202
  pid: process.pid,
1369
2203
  port,
@@ -1376,24 +2210,44 @@ async function main() {
1376
2210
  info(`Kandown daemon running on port ${port} (PID ${process.pid})`);
1377
2211
  await new Promise(() => {
1378
2212
  });
1379
- } else if (subcommand === "stop") {
1380
- const status = await getDaemonStatus(kandownDir);
1381
- if (status.running && status.metadata) {
1382
- try {
1383
- process.kill(status.metadata.pid, "SIGTERM");
1384
- } catch {
1385
- }
1386
- success(`Stopped daemon PID ${status.metadata.pid}`);
1387
- } else {
1388
- info("Daemon not running");
2213
+ } else if (subcommand === "start") {
2214
+ const daemonOptions = parseArgs(daemonArgs);
2215
+ const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
2216
+ const status = await startProjectDaemon(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
2217
+ if (status.running && status.metadata) success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
2218
+ else {
2219
+ err("Daemon failed to start");
2220
+ process.exit(1);
1389
2221
  }
1390
- } else {
2222
+ } else if (subcommand === "restart") {
2223
+ await stopProjectDaemon(kandownDir);
2224
+ const status = await startProjectDaemon(kandownDir);
2225
+ if (status.running && status.metadata) success(`Daemon restarted on port ${status.metadata.port} (PID ${status.metadata.pid})`);
2226
+ else {
2227
+ err("Daemon failed to restart");
2228
+ process.exit(1);
2229
+ }
2230
+ } else if (subcommand === "stop") {
2231
+ const stopped = await stopProjectDaemon(kandownDir);
2232
+ if (stopped) success("Daemon stopped");
2233
+ else info("Daemon not running");
2234
+ } else if (subcommand === "status") {
1391
2235
  const status = await getDaemonStatus(kandownDir);
1392
2236
  if (status.running && status.metadata) {
1393
2237
  success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
1394
2238
  } else {
1395
2239
  info("Daemon not running");
1396
2240
  }
2241
+ } else if (subcommand === "refresh-all") {
2242
+ const status = await getDaemonStatus(kandownDir);
2243
+ if (status.running) await stopProjectDaemon(kandownDir);
2244
+ const restarted = await startProjectDaemon(kandownDir);
2245
+ if (restarted.running && restarted.metadata) success(`Refreshed current project daemon on port ${restarted.metadata.port}`);
2246
+ else info("No running daemon refreshed");
2247
+ } else {
2248
+ err(`Unknown daemon command: ${subcommand}`);
2249
+ log(` Use ${c.cyan}kandown daemon start|stop|restart|status|refresh-all${c.reset}`);
2250
+ process.exit(1);
1397
2251
  }
1398
2252
  break;
1399
2253
  }
@@ -1405,21 +2259,14 @@ async function main() {
1405
2259
  status = await startProjectDaemon(kandownDir);
1406
2260
  }
1407
2261
  if (!parsed.flags["no-open"]) {
1408
- const urlToOpen = status.metadata?.url || join6(kandownDir, "kandown.html");
2262
+ const urlToOpen = status.metadata?.url || join8(kandownDir, "kandown.html");
1409
2263
  openBrowser(urlToOpen);
1410
2264
  }
1411
- const tuiPath = join6(PKG_ROOT, "bin", "tui.js");
1412
- if (existsSync6(tuiPath)) {
1413
- const child = spawn4(process.execPath, [tuiPath, ...rest], { stdio: "inherit" });
1414
- child.on("close", (code) => process.exit(code || 0));
1415
- } else {
1416
- err(`TUI binary not found at ${tuiPath}`);
1417
- process.exit(1);
1418
- }
2265
+ await launchTui("board", kandownDir);
1419
2266
  break;
1420
2267
  }
1421
2268
  default:
1422
- if (cmd.startsWith("-")) {
2269
+ if (!cmd || cmd.startsWith("-")) {
1423
2270
  help();
1424
2271
  break;
1425
2272
  }