clawvault 2.4.5 → 2.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/bin/clawvault.js +10 -0
  2. package/bin/command-registration.test.js +1 -1
  3. package/bin/help-contract.test.js +1 -0
  4. package/bin/register-config-route-commands.test.js +8 -1
  5. package/bin/register-core-commands.js +3 -3
  6. package/bin/register-kanban-commands.js +56 -0
  7. package/bin/register-kanban-commands.test.js +83 -0
  8. package/bin/register-project-commands.js +209 -0
  9. package/bin/register-project-commands.test.js +201 -0
  10. package/bin/register-query-commands.js +40 -0
  11. package/bin/register-task-commands.js +60 -25
  12. package/bin/register-task-commands.test.js +49 -4
  13. package/bin/test-helpers/cli-command-fixtures.js +15 -0
  14. package/dist/{chunk-3PJIGGWV.js → chunk-2CDEETQN.js} +1 -0
  15. package/dist/{chunk-B3SMJZIZ.js → chunk-2RK2AG32.js} +5 -5
  16. package/dist/chunk-5GZFTAL7.js +340 -0
  17. package/dist/{chunk-YIRWDQKA.js → chunk-6RQPD7X6.js} +3 -4
  18. package/dist/{chunk-HNMFXFYP.js → chunk-7OHQFMJK.js} +2 -1
  19. package/dist/{chunk-4JJL47IJ.js → chunk-C3PF7WBA.js} +2 -2
  20. package/dist/{chunk-JXY6T5R7.js → chunk-FW465EEA.js} +1 -1
  21. package/dist/{chunk-BI6SGGZP.js → chunk-G3OQJ2NQ.js} +1 -1
  22. package/dist/chunk-GSD4ALSI.js +724 -0
  23. package/dist/chunk-IOALNTAN.js +757 -0
  24. package/dist/chunk-ITPEXLHA.js +528 -0
  25. package/dist/chunk-J5EMBUPK.js +399 -0
  26. package/dist/chunk-K3CDT7IH.js +122 -0
  27. package/dist/{chunk-AHGUJG76.js → chunk-KCCHROBR.js} +13 -69
  28. package/dist/{chunk-U2ONVV7N.js → chunk-LMCC5OC7.js} +2 -2
  29. package/dist/{chunk-QALB2V3E.js → chunk-MQUJNOHK.js} +1 -1
  30. package/dist/{chunk-RXEIQ3KQ.js → chunk-TMZMN7OS.js} +334 -457
  31. package/dist/{chunk-HVTTYDCJ.js → chunk-VR5NE7PZ.js} +1 -1
  32. package/dist/{chunk-22WE3J4F.js → chunk-WIICLBNF.js} +35 -4
  33. package/dist/chunk-YCVDVI5B.js +273 -0
  34. package/dist/{chunk-NAMFB7ZA.js → chunk-Z2XBWN7A.js} +0 -2
  35. package/dist/commands/archive.js +3 -3
  36. package/dist/commands/backlog.js +1 -1
  37. package/dist/commands/blocked.js +1 -1
  38. package/dist/commands/canvas.d.ts +1 -14
  39. package/dist/commands/canvas.js +123 -1543
  40. package/dist/commands/context.js +5 -6
  41. package/dist/commands/doctor.js +5 -5
  42. package/dist/commands/inject.d.ts +2 -0
  43. package/dist/commands/inject.js +14 -0
  44. package/dist/commands/kanban.d.ts +63 -0
  45. package/dist/commands/kanban.js +21 -0
  46. package/dist/commands/migrate-observations.js +2 -2
  47. package/dist/commands/observe.js +8 -6
  48. package/dist/commands/project.d.ts +85 -0
  49. package/dist/commands/project.js +411 -0
  50. package/dist/commands/rebuild.js +7 -5
  51. package/dist/commands/reflect.js +5 -4
  52. package/dist/commands/replay.js +10 -7
  53. package/dist/commands/setup.d.ts +1 -1
  54. package/dist/commands/setup.js +2 -2
  55. package/dist/commands/sleep.d.ts +1 -1
  56. package/dist/commands/sleep.js +11 -8
  57. package/dist/commands/status.js +5 -5
  58. package/dist/commands/task.d.ts +20 -8
  59. package/dist/commands/task.js +11 -244
  60. package/dist/commands/wake.d.ts +1 -1
  61. package/dist/commands/wake.js +4 -4
  62. package/dist/index.d.ts +76 -106
  63. package/dist/index.js +99 -34
  64. package/dist/inject-x65KXWPk.d.ts +137 -0
  65. package/dist/lib/project-utils.d.ts +97 -0
  66. package/dist/lib/project-utils.js +19 -0
  67. package/dist/lib/task-utils.d.ts +48 -12
  68. package/dist/lib/task-utils.js +5 -1
  69. package/dist/{types-DMU3SuAV.d.ts → types-jjuYN2Xn.d.ts} +1 -1
  70. package/package.json +2 -2
  71. package/dist/chunk-DEFBIVQ3.js +0 -373
  72. package/dist/chunk-L3DJ36BZ.js +0 -40
  73. package/dist/chunk-UMMCYTJV.js +0 -105
@@ -3,14 +3,13 @@ import {
3
3
  contextCommand,
4
4
  formatContextMarkdown,
5
5
  registerContextCommand
6
- } from "../chunk-22WE3J4F.js";
7
- import "../chunk-HNMFXFYP.js";
6
+ } from "../chunk-WIICLBNF.js";
7
+ import "../chunk-7OHQFMJK.js";
8
8
  import "../chunk-6B3JWM7J.js";
9
- import "../chunk-3PJIGGWV.js";
10
- import "../chunk-L3DJ36BZ.js";
11
- import "../chunk-ZZA73MFY.js";
9
+ import "../chunk-2CDEETQN.js";
12
10
  import "../chunk-FHFUXL6G.js";
13
- import "../chunk-NAMFB7ZA.js";
11
+ import "../chunk-ZZA73MFY.js";
12
+ import "../chunk-Z2XBWN7A.js";
14
13
  export {
15
14
  buildContext,
16
15
  contextCommand,
@@ -1,21 +1,21 @@
1
+ import {
2
+ formatAge
3
+ } from "../chunk-7ZRP733D.js";
1
4
  import {
2
5
  scanVaultLinks
3
6
  } from "../chunk-4VQTUVH7.js";
4
7
  import "../chunk-J7ZWCI2C.js";
5
- import {
6
- formatAge
7
- } from "../chunk-7ZRP733D.js";
8
8
  import {
9
9
  checkOpenClawCompatibility
10
10
  } from "../chunk-PAYUH64O.js";
11
11
  import {
12
12
  ClawVault,
13
13
  findVault
14
- } from "../chunk-HNMFXFYP.js";
14
+ } from "../chunk-7OHQFMJK.js";
15
15
  import {
16
16
  hasQmd
17
17
  } from "../chunk-6B3JWM7J.js";
18
- import "../chunk-3PJIGGWV.js";
18
+ import "../chunk-2CDEETQN.js";
19
19
  import {
20
20
  loadMemoryGraphIndex
21
21
  } from "../chunk-ZZA73MFY.js";
@@ -0,0 +1,2 @@
1
+ import 'commander';
2
+ export { I as InjectCommandOptions, a as InjectFormat, q as buildInjectionResult, v as injectCommand, x as registerInjectCommand } from '../inject-x65KXWPk.js';
@@ -0,0 +1,14 @@
1
+ import {
2
+ buildInjectionResult,
3
+ injectCommand,
4
+ registerInjectCommand
5
+ } from "../chunk-GSD4ALSI.js";
6
+ import "../chunk-K3CDT7IH.js";
7
+ import "../chunk-ITPEXLHA.js";
8
+ import "../chunk-2CDEETQN.js";
9
+ import "../chunk-ZZA73MFY.js";
10
+ export {
11
+ buildInjectionResult,
12
+ injectCommand,
13
+ registerInjectCommand
14
+ };
@@ -0,0 +1,63 @@
1
+ import { Task } from '../lib/task-utils.js';
2
+
3
+ /**
4
+ * Kanban command for ClawVault.
5
+ * Syncs task frontmatter to/from Obsidian Kanban markdown boards.
6
+ */
7
+
8
+ type KanbanGroupBy = 'status' | 'priority' | 'project' | 'owner';
9
+ interface KanbanSyncOptions {
10
+ output?: string;
11
+ groupBy?: KanbanGroupBy | string;
12
+ filterProject?: string;
13
+ filterOwner?: string;
14
+ includeDone?: boolean;
15
+ now?: Date;
16
+ }
17
+ interface KanbanImportOptions {
18
+ output?: string;
19
+ }
20
+ interface KanbanLane {
21
+ name: string;
22
+ cards: string[];
23
+ }
24
+ interface KanbanSyncResult {
25
+ outputPath: string;
26
+ groupBy: KanbanGroupBy;
27
+ markdown: string;
28
+ lanes: KanbanLane[];
29
+ taskCount: number;
30
+ }
31
+ interface KanbanImportChange {
32
+ slug: string;
33
+ field: KanbanGroupBy;
34
+ from: string | null;
35
+ to: string | null;
36
+ }
37
+ interface KanbanImportResult {
38
+ outputPath: string;
39
+ groupBy: KanbanGroupBy;
40
+ changes: KanbanImportChange[];
41
+ missingSlugs: string[];
42
+ }
43
+ interface ParsedKanbanLane {
44
+ name: string;
45
+ slugs: string[];
46
+ }
47
+ interface ParsedKanbanBoard {
48
+ groupBy: KanbanGroupBy;
49
+ lanes: ParsedKanbanLane[];
50
+ }
51
+ declare function formatKanbanCard(task: Task): string;
52
+ declare function buildKanbanLanes(tasks: Task[], groupBy: KanbanGroupBy): KanbanLane[];
53
+ declare function generateKanbanMarkdown(tasks: Task[], options?: {
54
+ groupBy?: KanbanGroupBy | string;
55
+ now?: Date;
56
+ }): string;
57
+ declare function syncKanbanBoard(vaultPath: string, options?: KanbanSyncOptions): KanbanSyncResult;
58
+ declare function extractCardSlug(line: string): string | null;
59
+ declare function parseKanbanMarkdown(markdown: string): ParsedKanbanBoard;
60
+ declare function importKanbanBoard(vaultPath: string, options?: KanbanImportOptions): KanbanImportResult;
61
+ declare function kanbanCommand(vaultPath: string, action: 'sync' | 'import', options?: KanbanSyncOptions & KanbanImportOptions): Promise<void>;
62
+
63
+ export { type KanbanGroupBy, type KanbanImportChange, type KanbanImportOptions, type KanbanImportResult, type KanbanLane, type KanbanSyncOptions, type KanbanSyncResult, type ParsedKanbanBoard, type ParsedKanbanLane, buildKanbanLanes, extractCardSlug, formatKanbanCard, generateKanbanMarkdown, importKanbanBoard, kanbanCommand, parseKanbanMarkdown, syncKanbanBoard };
@@ -0,0 +1,21 @@
1
+ import {
2
+ buildKanbanLanes,
3
+ extractCardSlug,
4
+ formatKanbanCard,
5
+ generateKanbanMarkdown,
6
+ importKanbanBoard,
7
+ kanbanCommand,
8
+ parseKanbanMarkdown,
9
+ syncKanbanBoard
10
+ } from "../chunk-J5EMBUPK.js";
11
+ import "../chunk-IOALNTAN.js";
12
+ export {
13
+ buildKanbanLanes,
14
+ extractCardSlug,
15
+ formatKanbanCard,
16
+ generateKanbanMarkdown,
17
+ importKanbanBoard,
18
+ kanbanCommand,
19
+ parseKanbanMarkdown,
20
+ syncKanbanBoard
21
+ };
@@ -2,10 +2,10 @@ import {
2
2
  migrateObservations,
3
3
  migrateObservationsCommand,
4
4
  registerMigrateObservationsCommand
5
- } from "../chunk-JXY6T5R7.js";
5
+ } from "../chunk-FW465EEA.js";
6
6
  import "../chunk-MXSSG3QU.js";
7
7
  import "../chunk-FHFUXL6G.js";
8
- import "../chunk-NAMFB7ZA.js";
8
+ import "../chunk-Z2XBWN7A.js";
9
9
  export {
10
10
  migrateObservations,
11
11
  migrateObservationsCommand,
@@ -1,15 +1,17 @@
1
1
  import {
2
2
  observeCommand,
3
3
  registerObserveCommand
4
- } from "../chunk-B3SMJZIZ.js";
5
- import "../chunk-HRLWZGMA.js";
4
+ } from "../chunk-2RK2AG32.js";
6
5
  import "../chunk-P5EPF6MB.js";
7
- import "../chunk-RXEIQ3KQ.js";
6
+ import "../chunk-TMZMN7OS.js";
7
+ import "../chunk-5GZFTAL7.js";
8
+ import "../chunk-HRLWZGMA.js";
9
+ import "../chunk-ITPEXLHA.js";
8
10
  import "../chunk-MXSSG3QU.js";
9
- import "../chunk-DEFBIVQ3.js";
10
- import "../chunk-3PJIGGWV.js";
11
+ import "../chunk-IOALNTAN.js";
12
+ import "../chunk-2CDEETQN.js";
11
13
  import "../chunk-FHFUXL6G.js";
12
- import "../chunk-NAMFB7ZA.js";
14
+ import "../chunk-Z2XBWN7A.js";
13
15
  export {
14
16
  observeCommand,
15
17
  registerObserveCommand
@@ -0,0 +1,85 @@
1
+ import { ProjectStatus, Project } from '../lib/project-utils.js';
2
+ import '../lib/task-utils.js';
3
+
4
+ /**
5
+ * Project command for ClawVault
6
+ * Manages project add/update/archive/list/show/tasks/board operations
7
+ */
8
+
9
+ interface ProjectAddOptions {
10
+ status?: ProjectStatus;
11
+ owner?: string;
12
+ team?: string[];
13
+ client?: string;
14
+ tags?: string[];
15
+ description?: string;
16
+ deadline?: string;
17
+ repo?: string;
18
+ url?: string;
19
+ content?: string;
20
+ }
21
+ interface ProjectUpdateOptions {
22
+ status?: ProjectStatus;
23
+ owner?: string | null;
24
+ team?: string[] | null;
25
+ client?: string | null;
26
+ tags?: string[] | null;
27
+ description?: string | null;
28
+ deadline?: string | null;
29
+ repo?: string | null;
30
+ url?: string | null;
31
+ }
32
+ interface ProjectArchiveOptions {
33
+ reason?: string;
34
+ }
35
+ interface ProjectListOptions {
36
+ status?: ProjectStatus;
37
+ owner?: string;
38
+ client?: string;
39
+ tag?: string;
40
+ json?: boolean;
41
+ }
42
+ interface ProjectShowOptions {
43
+ json?: boolean;
44
+ }
45
+ interface ProjectTasksOptions {
46
+ json?: boolean;
47
+ }
48
+ type ProjectBoardGroupBy = 'status' | 'owner' | 'client';
49
+ interface ProjectBoardOptions {
50
+ output?: string;
51
+ groupBy?: ProjectBoardGroupBy | string;
52
+ now?: Date;
53
+ }
54
+ interface ProjectBoardLane {
55
+ name: string;
56
+ cards: string[];
57
+ }
58
+ interface ProjectBoardResult {
59
+ outputPath: string;
60
+ groupBy: ProjectBoardGroupBy;
61
+ markdown: string;
62
+ lanes: ProjectBoardLane[];
63
+ projectCount: number;
64
+ }
65
+ declare function buildProjectBoardLanes(projects: Project[], groupBy: ProjectBoardGroupBy): ProjectBoardLane[];
66
+ declare function generateProjectBoardMarkdown(projects: Project[], options?: {
67
+ groupBy?: ProjectBoardGroupBy | string;
68
+ now?: Date;
69
+ }): string;
70
+ declare function syncProjectBoard(vaultPath: string, options?: ProjectBoardOptions): ProjectBoardResult;
71
+ declare function projectAdd(vaultPath: string, title: string, options?: ProjectAddOptions): Project;
72
+ declare function projectUpdate(vaultPath: string, slug: string, options: ProjectUpdateOptions): Project;
73
+ declare function projectArchive(vaultPath: string, slug: string, options?: ProjectArchiveOptions): Project;
74
+ declare function projectList(vaultPath: string, options?: ProjectListOptions): Project[];
75
+ declare function formatProjectList(projects: Project[]): string;
76
+ declare function formatProjectDetails(vaultPath: string, project: Project, options?: {
77
+ activityLimit?: number;
78
+ }): string;
79
+ declare function projectCommand(vaultPath: string, action: 'add' | 'update' | 'archive' | 'list' | 'show' | 'tasks' | 'board', args: {
80
+ title?: string;
81
+ slug?: string;
82
+ options?: ProjectAddOptions & ProjectUpdateOptions & ProjectArchiveOptions & ProjectListOptions & ProjectShowOptions & ProjectTasksOptions & ProjectBoardOptions;
83
+ }): Promise<void>;
84
+
85
+ export { type ProjectAddOptions, type ProjectArchiveOptions, type ProjectBoardGroupBy, type ProjectBoardLane, type ProjectBoardOptions, type ProjectBoardResult, type ProjectListOptions, type ProjectShowOptions, type ProjectTasksOptions, type ProjectUpdateOptions, buildProjectBoardLanes, formatProjectDetails, formatProjectList, generateProjectBoardMarkdown, projectAdd, projectArchive, projectCommand, projectList, projectUpdate, syncProjectBoard };
@@ -0,0 +1,411 @@
1
+ import {
2
+ formatTaskList,
3
+ taskList
4
+ } from "../chunk-YCVDVI5B.js";
5
+ import {
6
+ archiveProject,
7
+ createProject,
8
+ getProjectActivity,
9
+ getProjectTasks,
10
+ listProjects,
11
+ readProject,
12
+ updateProject
13
+ } from "../chunk-5GZFTAL7.js";
14
+ import "../chunk-IOALNTAN.js";
15
+
16
+ // src/commands/project.ts
17
+ import * as fs from "fs";
18
+ import * as path from "path";
19
+ function toDateStr(value) {
20
+ if (!value) return "-";
21
+ return value.includes("T") ? value.split("T")[0] : value;
22
+ }
23
+ function toHashTag(value) {
24
+ return value.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-9/_-]/g, "");
25
+ }
26
+ function toMention(value) {
27
+ return value.trim().replace(/\s+/g, "-").replace(/[^A-Za-z0-9._-]/g, "");
28
+ }
29
+ function normalizeBoardGroupBy(value) {
30
+ const normalized = String(value || "status").trim().toLowerCase();
31
+ if (normalized === "status" || normalized === "owner" || normalized === "client") {
32
+ return normalized;
33
+ }
34
+ throw new Error(`Unsupported project board group field: ${normalized}`);
35
+ }
36
+ function resolveBoardPath(vaultPath, output) {
37
+ const resolvedVaultPath = path.resolve(vaultPath);
38
+ if (!output) {
39
+ return path.join(resolvedVaultPath, "Projects-Board.md");
40
+ }
41
+ if (path.isAbsolute(output)) {
42
+ return output;
43
+ }
44
+ return path.join(resolvedVaultPath, output);
45
+ }
46
+ function parseDeadlineTimestamp(project) {
47
+ if (!project.frontmatter.deadline) return Number.POSITIVE_INFINITY;
48
+ const timestamp = Date.parse(project.frontmatter.deadline);
49
+ return Number.isNaN(timestamp) ? Number.POSITIVE_INFINITY : timestamp;
50
+ }
51
+ function sortProjectsForCards(projects) {
52
+ return [...projects].sort((left, right) => {
53
+ const deadlineDiff = parseDeadlineTimestamp(left) - parseDeadlineTimestamp(right);
54
+ if (deadlineDiff !== 0) return deadlineDiff;
55
+ return new Date(right.frontmatter.updated).getTime() - new Date(left.frontmatter.updated).getTime();
56
+ });
57
+ }
58
+ function laneNameForStatus(status) {
59
+ switch (status) {
60
+ case "active":
61
+ return "Active";
62
+ case "paused":
63
+ return "Paused";
64
+ case "completed":
65
+ return "Completed";
66
+ case "archived":
67
+ return "Archived";
68
+ default:
69
+ return "Active";
70
+ }
71
+ }
72
+ function laneNameForProject(project, groupBy) {
73
+ switch (groupBy) {
74
+ case "status":
75
+ return laneNameForStatus(project.frontmatter.status);
76
+ case "owner":
77
+ return project.frontmatter.owner?.trim() || "Unassigned";
78
+ case "client":
79
+ return project.frontmatter.client?.trim() || "No Client";
80
+ default:
81
+ return laneNameForStatus(project.frontmatter.status);
82
+ }
83
+ }
84
+ function defaultLaneOrder(groupBy, projects) {
85
+ if (groupBy === "status") {
86
+ return ["Active", "Paused", "Completed", "Archived"];
87
+ }
88
+ const fallback = groupBy === "owner" ? "Unassigned" : "No Client";
89
+ const names = /* @__PURE__ */ new Set();
90
+ for (const project of projects) {
91
+ names.add(laneNameForProject(project, groupBy));
92
+ }
93
+ if (names.size === 0) {
94
+ return [fallback];
95
+ }
96
+ const sorted = Array.from(names).sort((left, right) => left.localeCompare(right));
97
+ if (sorted.includes(fallback)) {
98
+ return [...sorted.filter((name) => name !== fallback), fallback];
99
+ }
100
+ return sorted;
101
+ }
102
+ function formatProjectCard(project) {
103
+ const checkbox = project.frontmatter.status === "completed" || project.frontmatter.status === "archived" ? "x" : " ";
104
+ const parts = [`[[projects/${project.slug}|${project.title}]]`];
105
+ if (project.frontmatter.owner) {
106
+ const mention = toMention(project.frontmatter.owner);
107
+ if (mention) parts.push(`@${mention}`);
108
+ }
109
+ if (project.frontmatter.client) {
110
+ const clientTag = toHashTag(project.frontmatter.client);
111
+ if (clientTag) parts.push(`#client/${clientTag}`);
112
+ }
113
+ if (project.frontmatter.tags && project.frontmatter.tags.length > 0) {
114
+ for (const tag of project.frontmatter.tags) {
115
+ const normalizedTag = toHashTag(tag);
116
+ if (normalizedTag) parts.push(`#${normalizedTag}`);
117
+ }
118
+ }
119
+ if (project.frontmatter.deadline) {
120
+ parts.push(`\u{1F4C5} ${toDateStr(project.frontmatter.deadline)}`);
121
+ }
122
+ if (project.frontmatter.description) {
123
+ parts.push(`\u2014 ${project.frontmatter.description}`);
124
+ }
125
+ return `- [${checkbox}] ${parts.join(" ")}`;
126
+ }
127
+ function buildProjectBoardLanes(projects, groupBy) {
128
+ const laneOrder = defaultLaneOrder(groupBy, projects);
129
+ const lanes = /* @__PURE__ */ new Map();
130
+ for (const laneName of laneOrder) {
131
+ lanes.set(laneName, []);
132
+ }
133
+ for (const project of sortProjectsForCards(projects)) {
134
+ const laneName = laneNameForProject(project, groupBy);
135
+ if (!lanes.has(laneName)) {
136
+ lanes.set(laneName, []);
137
+ }
138
+ lanes.get(laneName)?.push(formatProjectCard(project));
139
+ }
140
+ return Array.from(lanes.entries()).map(([name, cards]) => ({ name, cards }));
141
+ }
142
+ function generateProjectBoardMarkdown(projects, options = {}) {
143
+ const groupBy = normalizeBoardGroupBy(options.groupBy);
144
+ const syncedAt = (options.now || /* @__PURE__ */ new Date()).toISOString();
145
+ const lanes = buildProjectBoardLanes(projects, groupBy);
146
+ const sections = lanes.map((lane) => {
147
+ const cardsBlock = lane.cards.length > 0 ? lane.cards.join("\n") : "";
148
+ return `## ${lane.name}
149
+
150
+ ${cardsBlock}`.trimEnd();
151
+ }).join("\n\n");
152
+ return [
153
+ "---",
154
+ "kanban-plugin: basic",
155
+ `clawvault-group-by: ${groupBy}`,
156
+ `clawvault-last-sync: '${syncedAt}'`,
157
+ "---",
158
+ "",
159
+ sections,
160
+ "",
161
+ "%% kanban:settings",
162
+ '{"kanban-plugin":"basic","list-collapse":["Completed","Archived"],"show-checkboxes":true}',
163
+ "%%",
164
+ ""
165
+ ].join("\n");
166
+ }
167
+ function syncProjectBoard(vaultPath, options = {}) {
168
+ const groupBy = normalizeBoardGroupBy(options.groupBy);
169
+ const outputPath = resolveBoardPath(vaultPath, options.output);
170
+ const projects = listProjects(vaultPath);
171
+ const markdown = generateProjectBoardMarkdown(projects, { groupBy, now: options.now });
172
+ fs.writeFileSync(outputPath, markdown);
173
+ return {
174
+ outputPath,
175
+ groupBy,
176
+ markdown,
177
+ lanes: buildProjectBoardLanes(projects, groupBy),
178
+ projectCount: projects.length
179
+ };
180
+ }
181
+ function projectAdd(vaultPath, title, options = {}) {
182
+ return createProject(vaultPath, title, {
183
+ status: options.status,
184
+ owner: options.owner,
185
+ team: options.team,
186
+ client: options.client,
187
+ tags: options.tags,
188
+ description: options.description,
189
+ deadline: options.deadline,
190
+ repo: options.repo,
191
+ url: options.url,
192
+ content: options.content
193
+ });
194
+ }
195
+ function projectUpdate(vaultPath, slug, options) {
196
+ return updateProject(vaultPath, slug, {
197
+ status: options.status,
198
+ owner: options.owner,
199
+ team: options.team,
200
+ client: options.client,
201
+ tags: options.tags,
202
+ description: options.description,
203
+ deadline: options.deadline,
204
+ repo: options.repo,
205
+ url: options.url
206
+ });
207
+ }
208
+ function projectArchive(vaultPath, slug, options = {}) {
209
+ return archiveProject(vaultPath, slug, options.reason);
210
+ }
211
+ function projectList(vaultPath, options = {}) {
212
+ const projects = listProjects(vaultPath, {
213
+ status: options.status,
214
+ owner: options.owner,
215
+ client: options.client,
216
+ tag: options.tag
217
+ });
218
+ if (!options.status) {
219
+ return projects.filter((project) => project.frontmatter.status !== "archived");
220
+ }
221
+ return projects;
222
+ }
223
+ function formatProjectList(projects) {
224
+ if (projects.length === 0) {
225
+ return "No projects found.\n";
226
+ }
227
+ const headers = ["STATUS", "OWNER", "DEADLINE", "TITLE"];
228
+ const widths = [10, 12, 12, 40];
229
+ const truncate = (value, width) => {
230
+ if (value.length <= width) return value;
231
+ return value.slice(0, width - 3) + "...";
232
+ };
233
+ let output = headers.map((header, index) => header.padEnd(widths[index])).join(" ") + "\n";
234
+ for (const project of projects) {
235
+ const row = [
236
+ project.frontmatter.status.padEnd(widths[0]),
237
+ (project.frontmatter.owner || "-").padEnd(widths[1]),
238
+ toDateStr(project.frontmatter.deadline).padEnd(widths[2]),
239
+ truncate(project.title, widths[3])
240
+ ];
241
+ output += row.join(" ") + "\n";
242
+ }
243
+ return output;
244
+ }
245
+ function formatFieldValue(value) {
246
+ if (Array.isArray(value)) {
247
+ return value.join(", ");
248
+ }
249
+ if (typeof value === "string") {
250
+ return value;
251
+ }
252
+ if (value === null || value === void 0) {
253
+ return "-";
254
+ }
255
+ return String(value);
256
+ }
257
+ function countTasksByStatus(projectSlug, vaultPath) {
258
+ const tasks = getProjectTasks(vaultPath, projectSlug);
259
+ return {
260
+ open: tasks.filter((task) => task.frontmatter.status === "open").length,
261
+ inProgress: tasks.filter((task) => task.frontmatter.status === "in-progress").length,
262
+ done: tasks.filter((task) => task.frontmatter.status === "done").length
263
+ };
264
+ }
265
+ function formatProjectDetails(vaultPath, project, options = {}) {
266
+ const lines = [];
267
+ const taskSummary = countTasksByStatus(project.slug, vaultPath);
268
+ const activity = getProjectActivity(vaultPath, project.slug).slice(0, options.activityLimit ?? 5);
269
+ lines.push(`# ${project.title}`);
270
+ lines.push("-".repeat(40));
271
+ const orderedFields = [
272
+ "type",
273
+ "status",
274
+ "owner",
275
+ "team",
276
+ "client",
277
+ "tags",
278
+ "description",
279
+ "started",
280
+ "deadline",
281
+ "repo",
282
+ "url",
283
+ "created",
284
+ "updated",
285
+ "completed",
286
+ "reason"
287
+ ];
288
+ for (const field of orderedFields) {
289
+ const value = project.frontmatter[field];
290
+ if (value === void 0) continue;
291
+ lines.push(`${field}: ${formatFieldValue(value)}`);
292
+ }
293
+ lines.push("");
294
+ lines.push(`Linked tasks: ${taskSummary.open} open, ${taskSummary.inProgress} in-progress, ${taskSummary.done} done`);
295
+ if (project.frontmatter.team && project.frontmatter.team.length > 0) {
296
+ lines.push("Team members:");
297
+ for (const member of project.frontmatter.team) {
298
+ lines.push(`- ${member}`);
299
+ }
300
+ }
301
+ lines.push("");
302
+ lines.push("Recent activity:");
303
+ if (activity.length === 0) {
304
+ lines.push("- none");
305
+ } else {
306
+ for (const filePath of activity) {
307
+ lines.push(`- ${path.basename(filePath)}`);
308
+ }
309
+ }
310
+ return lines.join("\n");
311
+ }
312
+ async function projectCommand(vaultPath, action, args) {
313
+ const options = args.options || {};
314
+ switch (action) {
315
+ case "add": {
316
+ if (!args.title) {
317
+ throw new Error("Title is required for project add");
318
+ }
319
+ const project = projectAdd(vaultPath, args.title, options);
320
+ console.log(`\u2713 Created project: ${project.slug}`);
321
+ console.log(` Path: ${path.join(path.resolve(vaultPath), "projects", `${project.slug}.md`)}`);
322
+ break;
323
+ }
324
+ case "update": {
325
+ if (!args.slug) {
326
+ throw new Error("Project slug is required for update");
327
+ }
328
+ const project = projectUpdate(vaultPath, args.slug, options);
329
+ console.log(`\u2713 Updated project: ${project.slug}`);
330
+ break;
331
+ }
332
+ case "archive": {
333
+ if (!args.slug) {
334
+ throw new Error("Project slug is required for archive");
335
+ }
336
+ const project = projectArchive(vaultPath, args.slug, options);
337
+ console.log(`\u2713 Archived project: ${project.slug}`);
338
+ break;
339
+ }
340
+ case "list": {
341
+ const projects = projectList(vaultPath, options);
342
+ if (options.json) {
343
+ console.log(JSON.stringify(projects, null, 2));
344
+ } else {
345
+ console.log(formatProjectList(projects));
346
+ }
347
+ break;
348
+ }
349
+ case "show": {
350
+ if (!args.slug) {
351
+ throw new Error("Project slug is required for show");
352
+ }
353
+ const project = readProject(vaultPath, args.slug);
354
+ if (!project) {
355
+ throw new Error(`Project not found: ${args.slug}`);
356
+ }
357
+ const taskSummary = countTasksByStatus(project.slug, vaultPath);
358
+ const recentActivity = getProjectActivity(vaultPath, project.slug).slice(0, 5);
359
+ if (options.json) {
360
+ console.log(
361
+ JSON.stringify(
362
+ {
363
+ project,
364
+ taskSummary,
365
+ team: project.frontmatter.team || [],
366
+ recentActivity
367
+ },
368
+ null,
369
+ 2
370
+ )
371
+ );
372
+ } else {
373
+ console.log(formatProjectDetails(vaultPath, project, { activityLimit: 5 }));
374
+ }
375
+ break;
376
+ }
377
+ case "tasks": {
378
+ if (!args.slug) {
379
+ throw new Error("Project slug is required for tasks");
380
+ }
381
+ const tasks = taskList(vaultPath, { project: args.slug });
382
+ if (options.json) {
383
+ console.log(JSON.stringify(tasks, null, 2));
384
+ } else {
385
+ console.log(formatTaskList(tasks));
386
+ }
387
+ break;
388
+ }
389
+ case "board": {
390
+ const result = syncProjectBoard(vaultPath, options);
391
+ console.log(`\u2713 Synced project board: ${result.outputPath}`);
392
+ console.log(` Grouped by: ${result.groupBy}`);
393
+ console.log(` Projects included: ${result.projectCount}`);
394
+ break;
395
+ }
396
+ default:
397
+ throw new Error(`Unknown project action: ${action}`);
398
+ }
399
+ }
400
+ export {
401
+ buildProjectBoardLanes,
402
+ formatProjectDetails,
403
+ formatProjectList,
404
+ generateProjectBoardMarkdown,
405
+ projectAdd,
406
+ projectArchive,
407
+ projectCommand,
408
+ projectList,
409
+ projectUpdate,
410
+ syncProjectBoard
411
+ };