@task-mcp/cli 1.0.16 → 1.0.18
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/package.json +1 -1
- package/src/__tests__/dashboard.test.ts +84 -86
- package/src/__tests__/list.test.ts +16 -21
- package/src/__tests__/storage.test.ts +1 -1
- package/src/commands/dashboard.ts +34 -32
- package/src/commands/inbox.ts +9 -34
- package/src/commands/list.ts +30 -45
- package/src/constants.ts +3 -0
- package/src/index.ts +10 -13
- package/src/interactive.ts +13 -23
- package/src/storage.ts +32 -18
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import type { Task,
|
|
2
|
+
import type { Task, InboxItem, WorkspaceInfo } from "../storage.js";
|
|
3
3
|
|
|
4
4
|
// Helper to create test tasks
|
|
5
5
|
function createTask(overrides: Partial<Task> = {}): Task {
|
|
@@ -8,21 +8,19 @@ function createTask(overrides: Partial<Task> = {}): Task {
|
|
|
8
8
|
title: "Test Task",
|
|
9
9
|
status: "pending",
|
|
10
10
|
priority: "medium",
|
|
11
|
-
|
|
11
|
+
workspace: "test-workspace",
|
|
12
12
|
createdAt: new Date().toISOString(),
|
|
13
13
|
updatedAt: new Date().toISOString(),
|
|
14
14
|
...overrides,
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// Helper to create test
|
|
19
|
-
function
|
|
18
|
+
// Helper to create test workspaces
|
|
19
|
+
function createWorkspace(overrides: Partial<WorkspaceInfo> = {}): WorkspaceInfo {
|
|
20
20
|
return {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
createdAt: new Date().toISOString(),
|
|
25
|
-
updatedAt: new Date().toISOString(),
|
|
21
|
+
name: "Test Workspace",
|
|
22
|
+
taskCount: 0,
|
|
23
|
+
completedCount: 0,
|
|
26
24
|
...overrides,
|
|
27
25
|
};
|
|
28
26
|
}
|
|
@@ -40,130 +38,130 @@ function createInboxItem(overrides: Partial<InboxItem> = {}): InboxItem {
|
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
describe("dashboard command - unit tests", () => {
|
|
43
|
-
describe("
|
|
44
|
-
test("finds
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
describe("workspace lookup logic", () => {
|
|
42
|
+
test("finds workspace by exact name", () => {
|
|
43
|
+
const workspaces = [
|
|
44
|
+
createWorkspace({ name: "workspace-a" }),
|
|
45
|
+
createWorkspace({ name: "workspace-b" }),
|
|
48
46
|
];
|
|
49
47
|
|
|
50
|
-
const
|
|
51
|
-
const
|
|
48
|
+
const workspaceName = "workspace-a";
|
|
49
|
+
const workspace = workspaces.find((w) => w.name === workspaceName || w.name.startsWith(workspaceName));
|
|
52
50
|
|
|
53
|
-
expect(
|
|
54
|
-
expect(
|
|
51
|
+
expect(workspace).toBeDefined();
|
|
52
|
+
expect(workspace?.name).toBe("workspace-a");
|
|
55
53
|
});
|
|
56
54
|
|
|
57
|
-
test("finds
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
test("finds workspace by partial name prefix", () => {
|
|
56
|
+
const workspaces = [
|
|
57
|
+
createWorkspace({ name: "workspace-abc123" }),
|
|
58
|
+
createWorkspace({ name: "workspace-xyz789" }),
|
|
61
59
|
];
|
|
62
60
|
|
|
63
|
-
const
|
|
64
|
-
const
|
|
61
|
+
const workspaceName = "workspace-abc";
|
|
62
|
+
const workspace = workspaces.find((w) => w.name === workspaceName || w.name.startsWith(workspaceName));
|
|
65
63
|
|
|
66
|
-
expect(
|
|
67
|
-
expect(
|
|
64
|
+
expect(workspace).toBeDefined();
|
|
65
|
+
expect(workspace?.name).toBe("workspace-abc123");
|
|
68
66
|
});
|
|
69
67
|
|
|
70
|
-
test("returns undefined for non-matching
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
test("returns undefined for non-matching name", () => {
|
|
69
|
+
const workspaces = [
|
|
70
|
+
createWorkspace({ name: "workspace-a" }),
|
|
71
|
+
createWorkspace({ name: "workspace-b" }),
|
|
74
72
|
];
|
|
75
73
|
|
|
76
|
-
const
|
|
77
|
-
const
|
|
74
|
+
const workspaceName = "workspace-nonexistent";
|
|
75
|
+
const workspace = workspaces.find((w) => w.name === workspaceName || w.name.startsWith(workspaceName));
|
|
78
76
|
|
|
79
|
-
expect(
|
|
77
|
+
expect(workspace).toBeUndefined();
|
|
80
78
|
});
|
|
81
79
|
|
|
82
|
-
test("returns first match when multiple
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
test("returns first match when multiple workspaces share prefix", () => {
|
|
81
|
+
const workspaces = [
|
|
82
|
+
createWorkspace({ name: "workspace-abc111" }),
|
|
83
|
+
createWorkspace({ name: "workspace-abc222" }),
|
|
86
84
|
];
|
|
87
85
|
|
|
88
|
-
const
|
|
89
|
-
const
|
|
86
|
+
const workspaceName = "workspace-abc";
|
|
87
|
+
const workspace = workspaces.find((w) => w.name === workspaceName || w.name.startsWith(workspaceName));
|
|
90
88
|
|
|
91
|
-
expect(
|
|
92
|
-
expect(
|
|
89
|
+
expect(workspace).toBeDefined();
|
|
90
|
+
expect(workspace?.name).toBe("workspace-abc111");
|
|
93
91
|
});
|
|
94
92
|
});
|
|
95
93
|
|
|
96
94
|
describe("dashboard mode selection", () => {
|
|
97
|
-
test("single
|
|
98
|
-
const
|
|
95
|
+
test("single workspace mode when only one workspace exists", () => {
|
|
96
|
+
const workspaces = [createWorkspace({ name: "only-workspace" })];
|
|
99
97
|
|
|
100
|
-
const
|
|
101
|
-
const shouldShowGlobal =
|
|
98
|
+
const hasSingleWorkspace = workspaces.length === 1;
|
|
99
|
+
const shouldShowGlobal = workspaces.length > 1;
|
|
102
100
|
|
|
103
|
-
expect(
|
|
101
|
+
expect(hasSingleWorkspace).toBe(true);
|
|
104
102
|
expect(shouldShowGlobal).toBe(false);
|
|
105
103
|
});
|
|
106
104
|
|
|
107
|
-
test("global mode when multiple
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
test("global mode when multiple workspaces exist", () => {
|
|
106
|
+
const workspaces = [
|
|
107
|
+
createWorkspace({ name: "workspace-1" }),
|
|
108
|
+
createWorkspace({ name: "workspace-2" }),
|
|
111
109
|
];
|
|
112
110
|
|
|
113
|
-
const
|
|
114
|
-
const shouldShowGlobal =
|
|
111
|
+
const hasSingleWorkspace = workspaces.length === 1;
|
|
112
|
+
const shouldShowGlobal = workspaces.length > 1;
|
|
115
113
|
|
|
116
|
-
expect(
|
|
114
|
+
expect(hasSingleWorkspace).toBe(false);
|
|
117
115
|
expect(shouldShowGlobal).toBe(true);
|
|
118
116
|
});
|
|
119
117
|
|
|
120
|
-
test("
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
test("workspace mode when specific workspace provided", () => {
|
|
119
|
+
const workspaces = [
|
|
120
|
+
createWorkspace({ name: "workspace-1" }),
|
|
121
|
+
createWorkspace({ name: "workspace-2" }),
|
|
124
122
|
];
|
|
125
123
|
|
|
126
|
-
const
|
|
127
|
-
const
|
|
124
|
+
const workspaceName = "workspace-1";
|
|
125
|
+
const targetWorkspace = workspaces.find((w) => w.name === workspaceName || w.name.startsWith(workspaceName));
|
|
128
126
|
|
|
129
|
-
expect(
|
|
130
|
-
expect(
|
|
127
|
+
expect(targetWorkspace).toBeDefined();
|
|
128
|
+
expect(targetWorkspace?.name).toBe("workspace-1");
|
|
131
129
|
});
|
|
132
130
|
});
|
|
133
131
|
|
|
134
|
-
describe("task grouping by
|
|
135
|
-
test("builds task map for each
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
132
|
+
describe("task grouping by workspace", () => {
|
|
133
|
+
test("builds task map for each workspace", () => {
|
|
134
|
+
const workspaces = [
|
|
135
|
+
createWorkspace({ name: "workspace-1" }),
|
|
136
|
+
createWorkspace({ name: "workspace-2" }),
|
|
139
137
|
];
|
|
140
138
|
|
|
141
139
|
const allTasks = [
|
|
142
|
-
createTask({ id: "task_1",
|
|
143
|
-
createTask({ id: "task_2",
|
|
144
|
-
createTask({ id: "task_3",
|
|
140
|
+
createTask({ id: "task_1", workspace: "workspace-1" }),
|
|
141
|
+
createTask({ id: "task_2", workspace: "workspace-1" }),
|
|
142
|
+
createTask({ id: "task_3", workspace: "workspace-2" }),
|
|
145
143
|
];
|
|
146
144
|
|
|
147
|
-
const
|
|
148
|
-
for (const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
allTasks.filter((t) => t.
|
|
145
|
+
const tasksByWorkspace = new Map<string, Task[]>();
|
|
146
|
+
for (const ws of workspaces) {
|
|
147
|
+
tasksByWorkspace.set(
|
|
148
|
+
ws.name,
|
|
149
|
+
allTasks.filter((t) => t.workspace === ws.name)
|
|
152
150
|
);
|
|
153
151
|
}
|
|
154
152
|
|
|
155
|
-
expect(
|
|
156
|
-
expect(
|
|
153
|
+
expect(tasksByWorkspace.get("workspace-1")?.length).toBe(2);
|
|
154
|
+
expect(tasksByWorkspace.get("workspace-2")?.length).toBe(1);
|
|
157
155
|
});
|
|
158
156
|
|
|
159
|
-
test("returns empty array for
|
|
160
|
-
const
|
|
161
|
-
|
|
157
|
+
test("returns empty array for workspace with no tasks", () => {
|
|
158
|
+
const tasksByWorkspace = new Map<string, Task[]>();
|
|
159
|
+
tasksByWorkspace.set("workspace-1", []);
|
|
162
160
|
|
|
163
|
-
const
|
|
161
|
+
const getWorkspaceTasks = (name: string) => tasksByWorkspace.get(name) ?? [];
|
|
164
162
|
|
|
165
|
-
expect(
|
|
166
|
-
expect(
|
|
163
|
+
expect(getWorkspaceTasks("workspace-1")).toEqual([]);
|
|
164
|
+
expect(getWorkspaceTasks("workspace-nonexistent")).toEqual([]);
|
|
167
165
|
});
|
|
168
166
|
});
|
|
169
167
|
|
|
@@ -184,12 +182,12 @@ describe("dashboard command - unit tests", () => {
|
|
|
184
182
|
});
|
|
185
183
|
|
|
186
184
|
describe("empty state handling", () => {
|
|
187
|
-
test("detects when no
|
|
188
|
-
const
|
|
185
|
+
test("detects when no workspaces exist", () => {
|
|
186
|
+
const workspaces: WorkspaceInfo[] = [];
|
|
189
187
|
|
|
190
|
-
const
|
|
188
|
+
const hasWorkspaces = workspaces.length > 0;
|
|
191
189
|
|
|
192
|
-
expect(
|
|
190
|
+
expect(hasWorkspaces).toBe(false);
|
|
193
191
|
});
|
|
194
192
|
|
|
195
193
|
test("detects when tasks are empty", () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test, mock, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
-
import type { Task,
|
|
2
|
+
import type { Task, WorkspaceInfo } from "../storage.js";
|
|
3
3
|
|
|
4
4
|
// Helper to create test tasks
|
|
5
5
|
function createTask(overrides: Partial<Task> = {}): Task {
|
|
@@ -8,21 +8,19 @@ function createTask(overrides: Partial<Task> = {}): Task {
|
|
|
8
8
|
title: "Test Task",
|
|
9
9
|
status: "pending",
|
|
10
10
|
priority: "medium",
|
|
11
|
-
|
|
11
|
+
workspace: "test-workspace",
|
|
12
12
|
createdAt: new Date().toISOString(),
|
|
13
13
|
updatedAt: new Date().toISOString(),
|
|
14
14
|
...overrides,
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
// Helper to create test
|
|
19
|
-
function
|
|
18
|
+
// Helper to create test workspaces
|
|
19
|
+
function createWorkspace(overrides: Partial<WorkspaceInfo> = {}): WorkspaceInfo {
|
|
20
20
|
return {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
createdAt: new Date().toISOString(),
|
|
25
|
-
updatedAt: new Date().toISOString(),
|
|
21
|
+
name: "Test Workspace",
|
|
22
|
+
taskCount: 0,
|
|
23
|
+
completedCount: 0,
|
|
26
24
|
...overrides,
|
|
27
25
|
};
|
|
28
26
|
}
|
|
@@ -317,9 +315,9 @@ describe("list commands - exports", () => {
|
|
|
317
315
|
expect(typeof listTasksCmd).toBe("function");
|
|
318
316
|
});
|
|
319
317
|
|
|
320
|
-
test("
|
|
321
|
-
const {
|
|
322
|
-
expect(typeof
|
|
318
|
+
test("listWorkspacesCmd is exported", async () => {
|
|
319
|
+
const { listWorkspacesCmd } = await import("../commands/list.js");
|
|
320
|
+
expect(typeof listWorkspacesCmd).toBe("function");
|
|
323
321
|
});
|
|
324
322
|
|
|
325
323
|
test("listTasksCmd accepts options object", async () => {
|
|
@@ -327,26 +325,23 @@ describe("list commands - exports", () => {
|
|
|
327
325
|
|
|
328
326
|
// Verify the function signature accepts the expected options
|
|
329
327
|
const options = {
|
|
330
|
-
|
|
328
|
+
workspace: "test-workspace",
|
|
331
329
|
status: "pending",
|
|
332
330
|
priority: "high",
|
|
333
331
|
all: true,
|
|
334
332
|
};
|
|
335
333
|
|
|
336
334
|
// Type check passes if this compiles
|
|
337
|
-
expect(options.
|
|
335
|
+
expect(options.workspace).toBeDefined();
|
|
338
336
|
expect(options.status).toBeDefined();
|
|
339
337
|
expect(options.priority).toBeDefined();
|
|
340
338
|
expect(options.all).toBeDefined();
|
|
341
339
|
});
|
|
342
340
|
|
|
343
|
-
test("
|
|
344
|
-
const {
|
|
341
|
+
test("listWorkspacesCmd accepts no options", async () => {
|
|
342
|
+
const { listWorkspacesCmd } = await import("../commands/list.js");
|
|
345
343
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
expect(options.all).toBeDefined();
|
|
344
|
+
// listWorkspacesCmd takes no options
|
|
345
|
+
expect(typeof listWorkspacesCmd).toBe("function");
|
|
351
346
|
});
|
|
352
347
|
});
|
|
@@ -13,7 +13,7 @@ function createTask(overrides: Partial<Task> = {}): Task {
|
|
|
13
13
|
title: "Test Task",
|
|
14
14
|
status: "pending",
|
|
15
15
|
priority: "medium",
|
|
16
|
-
|
|
16
|
+
workspace: "test-workspace",
|
|
17
17
|
createdAt: new Date().toISOString(),
|
|
18
18
|
updatedAt: new Date().toISOString(),
|
|
19
19
|
...overrides,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dashboard command - displays
|
|
2
|
+
* Dashboard command - displays workspace overview with unified widgets
|
|
3
3
|
* Uses shared dashboard renderer from @task-mcp/shared
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -7,81 +7,83 @@ import { VERSION } from "../index.js";
|
|
|
7
7
|
import { c } from "../ansi.js";
|
|
8
8
|
import {
|
|
9
9
|
renderGlobalDashboard,
|
|
10
|
-
|
|
10
|
+
renderWorkspaceDashboard,
|
|
11
11
|
type Task as SharedTask,
|
|
12
|
-
type
|
|
12
|
+
type WorkspaceInfo,
|
|
13
13
|
} from "@task-mcp/shared";
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
listWorkspaces,
|
|
16
16
|
listTasks,
|
|
17
17
|
listAllTasks,
|
|
18
18
|
listInboxItems,
|
|
19
19
|
getActiveTag,
|
|
20
|
+
getCurrentWorkspace,
|
|
20
21
|
type Task,
|
|
21
|
-
type Project,
|
|
22
22
|
} from "../storage.js";
|
|
23
23
|
|
|
24
24
|
// =============================================================================
|
|
25
25
|
// Main Dashboard
|
|
26
26
|
// =============================================================================
|
|
27
27
|
|
|
28
|
-
export async function dashboard(
|
|
29
|
-
// Get
|
|
30
|
-
const
|
|
28
|
+
export async function dashboard(workspaceName?: string): Promise<void> {
|
|
29
|
+
// Get workspaces, tasks, and active tag
|
|
30
|
+
const workspaces = await listWorkspaces();
|
|
31
31
|
const activeTag = await getActiveTag();
|
|
32
|
+
const currentWorkspace = getCurrentWorkspace();
|
|
32
33
|
|
|
33
|
-
if (
|
|
34
|
-
console.log(c.yellow("No
|
|
34
|
+
if (workspaces.length === 0) {
|
|
35
|
+
console.log(c.yellow("No tasks found. Create a task first using the MCP server."));
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
// If
|
|
39
|
-
let
|
|
39
|
+
// If workspace specified, show single workspace. Otherwise show all.
|
|
40
|
+
let selectedWorkspace: WorkspaceInfo | undefined;
|
|
40
41
|
let tasks: Task[];
|
|
41
42
|
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
if (!
|
|
45
|
-
console.log(c.error(`
|
|
43
|
+
if (workspaceName) {
|
|
44
|
+
selectedWorkspace = workspaces.find(w => w.name === workspaceName || w.name.startsWith(workspaceName));
|
|
45
|
+
if (!selectedWorkspace) {
|
|
46
|
+
console.log(c.error(`Workspace not found: ${workspaceName}`));
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
|
-
tasks = await
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
tasks = await listAllTasks();
|
|
50
|
+
tasks = tasks.filter(t => t.workspace === selectedWorkspace!.name);
|
|
51
|
+
} else if (workspaces.length === 1) {
|
|
52
|
+
selectedWorkspace = workspaces[0];
|
|
53
|
+
tasks = await listTasks();
|
|
52
54
|
} else {
|
|
53
|
-
// Show all tasks across all
|
|
55
|
+
// Show all tasks across all workspaces
|
|
54
56
|
tasks = await listAllTasks();
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
// Get inbox items
|
|
58
60
|
const inboxItems = await listInboxItems("pending");
|
|
59
61
|
|
|
60
|
-
// Create task lookup for
|
|
62
|
+
// Create task lookup for workspaces table (batch query to avoid N+1)
|
|
61
63
|
const allTasksForLookup = await listAllTasks();
|
|
62
|
-
const
|
|
64
|
+
const tasksByWorkspace = new Map<string, Task[]>();
|
|
63
65
|
for (const task of allTasksForLookup) {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
const workspaceTasks = tasksByWorkspace.get(task.workspace) ?? [];
|
|
67
|
+
workspaceTasks.push(task);
|
|
68
|
+
tasksByWorkspace.set(task.workspace, workspaceTasks);
|
|
67
69
|
}
|
|
68
|
-
const
|
|
70
|
+
const getWorkspaceTasks = (ws: string) => (tasksByWorkspace.get(ws) ?? []) as SharedTask[];
|
|
69
71
|
|
|
70
72
|
// Render dashboard using shared renderer
|
|
71
73
|
// Cast types - CLI types are compatible subset of shared types
|
|
72
74
|
let output: string;
|
|
73
|
-
if (
|
|
74
|
-
output =
|
|
75
|
-
|
|
75
|
+
if (selectedWorkspace) {
|
|
76
|
+
output = renderWorkspaceDashboard(
|
|
77
|
+
selectedWorkspace.name,
|
|
76
78
|
tasks as SharedTask[],
|
|
77
79
|
{ version: VERSION, activeTag }
|
|
78
80
|
);
|
|
79
81
|
} else {
|
|
80
82
|
output = renderGlobalDashboard(
|
|
81
|
-
|
|
83
|
+
workspaces,
|
|
82
84
|
tasks as SharedTask[],
|
|
83
85
|
inboxItems,
|
|
84
|
-
|
|
86
|
+
getWorkspaceTasks,
|
|
85
87
|
{ version: VERSION, activeTag }
|
|
86
88
|
);
|
|
87
89
|
}
|
package/src/commands/inbox.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { c, formatStatus, formatPriority } from "../ansi.js";
|
|
7
|
-
import { InboxStore, TaskStore
|
|
7
|
+
import { InboxStore, TaskStore } from "@task-mcp/mcp-server/storage";
|
|
8
8
|
import { parseInboxInput } from "@task-mcp/shared";
|
|
9
9
|
import {
|
|
10
10
|
LIST_SEPARATOR_WIDTH,
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
|
|
16
16
|
const inboxStore = new InboxStore();
|
|
17
17
|
const taskStore = new TaskStore();
|
|
18
|
-
const projectStore = new ProjectStore();
|
|
19
18
|
|
|
20
19
|
export interface InboxAddOptions {
|
|
21
20
|
content: string;
|
|
@@ -29,7 +28,6 @@ export interface InboxListOptions {
|
|
|
29
28
|
|
|
30
29
|
export interface InboxPromoteOptions {
|
|
31
30
|
itemId: string;
|
|
32
|
-
projectId?: string;
|
|
33
31
|
title?: string;
|
|
34
32
|
priority?: "critical" | "high" | "medium" | "low";
|
|
35
33
|
}
|
|
@@ -77,7 +75,7 @@ export async function inboxListCmd(options: InboxListOptions): Promise<void> {
|
|
|
77
75
|
// Header
|
|
78
76
|
const title = status ? `Inbox (${status})` : "Inbox (all)";
|
|
79
77
|
console.log(c.bold(c.cyan(title)) + c.dim(` - ${items.length} items`));
|
|
80
|
-
console.log(c.dim("─".repeat(
|
|
78
|
+
console.log(c.dim("─".repeat(LIST_SEPARATOR_WIDTH)));
|
|
81
79
|
console.log();
|
|
82
80
|
|
|
83
81
|
for (const item of items) {
|
|
@@ -116,7 +114,7 @@ export async function inboxGetCmd(itemId: string): Promise<void> {
|
|
|
116
114
|
|
|
117
115
|
console.log();
|
|
118
116
|
console.log(c.bold(c.cyan("Inbox Item")));
|
|
119
|
-
console.log(c.dim("─".repeat(
|
|
117
|
+
console.log(c.dim("─".repeat(MENU_SEPARATOR_WIDTH)));
|
|
120
118
|
console.log();
|
|
121
119
|
console.log(" " + c.dim("ID:") + " " + item.id);
|
|
122
120
|
console.log(" " + c.dim("Status:") + " " + formatStatus(item.status));
|
|
@@ -139,7 +137,7 @@ export async function inboxGetCmd(itemId: string): Promise<void> {
|
|
|
139
137
|
}
|
|
140
138
|
|
|
141
139
|
/**
|
|
142
|
-
* Promote inbox item to task
|
|
140
|
+
* Promote inbox item to task in current workspace
|
|
143
141
|
*/
|
|
144
142
|
export async function inboxPromoteCmd(options: InboxPromoteOptions): Promise<void> {
|
|
145
143
|
const item = await inboxStore.get(options.itemId);
|
|
@@ -158,31 +156,9 @@ export async function inboxPromoteCmd(options: InboxPromoteOptions): Promise<voi
|
|
|
158
156
|
return;
|
|
159
157
|
}
|
|
160
158
|
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const projects = await projectStore.list();
|
|
165
|
-
if (projects.length === 0) {
|
|
166
|
-
console.log();
|
|
167
|
-
console.log(c.error("No projects found. Create a project first."));
|
|
168
|
-
console.log();
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
// Use first project if not specified
|
|
172
|
-
projectId = projects[0]!.id;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const project = await projectStore.get(projectId);
|
|
176
|
-
if (!project) {
|
|
177
|
-
console.log();
|
|
178
|
-
console.log(c.error(`Project not found: ${projectId}`));
|
|
179
|
-
console.log();
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Create task
|
|
184
|
-
const task = await taskStore.create(projectId, {
|
|
185
|
-
title: options.title ?? item.content.slice(0, 100),
|
|
159
|
+
// Create task in current workspace (auto-detected)
|
|
160
|
+
const task = await taskStore.create({
|
|
161
|
+
title: options.title ?? item.content.slice(0, INBOX_PROMOTE_TITLE_MAX_LENGTH),
|
|
186
162
|
description: item.content,
|
|
187
163
|
priority: options.priority ?? "medium",
|
|
188
164
|
tags: item.tags,
|
|
@@ -196,7 +172,7 @@ export async function inboxPromoteCmd(options: InboxPromoteOptions): Promise<voi
|
|
|
196
172
|
console.log();
|
|
197
173
|
console.log(" " + c.dim("Task ID:") + " " + c.cyan(task.id));
|
|
198
174
|
console.log(" " + c.dim("Title:") + " " + task.title);
|
|
199
|
-
console.log(" " + c.dim("
|
|
175
|
+
console.log(" " + c.dim("Workspace:") + " " + task.workspace);
|
|
200
176
|
console.log(" " + c.dim("Priority:") + " " + formatPriority(task.priority));
|
|
201
177
|
console.log();
|
|
202
178
|
}
|
|
@@ -215,7 +191,7 @@ export async function inboxDiscardCmd(itemId: string): Promise<void> {
|
|
|
215
191
|
}
|
|
216
192
|
|
|
217
193
|
console.log();
|
|
218
|
-
console.log(c.dim("×") + " Discarded: " + item.content.slice(0,
|
|
194
|
+
console.log(c.dim("×") + " Discarded: " + item.content.slice(0, INBOX_DISCARD_PREVIEW_LENGTH) + "...");
|
|
219
195
|
console.log();
|
|
220
196
|
}
|
|
221
197
|
|
|
@@ -251,4 +227,3 @@ export async function inboxCountCmd(): Promise<void> {
|
|
|
251
227
|
}
|
|
252
228
|
console.log();
|
|
253
229
|
}
|
|
254
|
-
|
package/src/commands/list.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* List commands - list tasks and
|
|
2
|
+
* List commands - list tasks and workspaces
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { c, table, formatStatus, formatPriority, type TableColumn } from "../ansi.js";
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
listWorkspaces,
|
|
8
8
|
listTasks,
|
|
9
9
|
listAllTasks,
|
|
10
|
+
getCurrentWorkspace,
|
|
10
11
|
type Task,
|
|
11
|
-
type
|
|
12
|
+
type WorkspaceInfo,
|
|
12
13
|
} from "../storage.js";
|
|
13
14
|
import {
|
|
14
15
|
COLUMN_WIDTH_ID,
|
|
@@ -17,18 +18,9 @@ import {
|
|
|
17
18
|
COLUMN_WIDTH_STATUS,
|
|
18
19
|
COLUMN_WIDTH_PRIORITY,
|
|
19
20
|
COLUMN_WIDTH_DATE,
|
|
21
|
+
ID_DISPLAY_LENGTH,
|
|
20
22
|
} from "../constants.js";
|
|
21
23
|
|
|
22
|
-
function formatProjectStatus(status: Project["status"]): string {
|
|
23
|
-
const statusMap: Record<string, string> = {
|
|
24
|
-
active: c.green("● active"),
|
|
25
|
-
on_hold: c.yellow("◐ on_hold"),
|
|
26
|
-
completed: c.blue("✓ completed"),
|
|
27
|
-
archived: c.gray("⊘ archived"),
|
|
28
|
-
};
|
|
29
|
-
return statusMap[status] ?? status;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
24
|
function formatDate(dateStr?: string): string {
|
|
33
25
|
if (!dateStr) return c.gray("-");
|
|
34
26
|
const date = new Date(dateStr);
|
|
@@ -36,17 +28,20 @@ function formatDate(dateStr?: string): string {
|
|
|
36
28
|
}
|
|
37
29
|
|
|
38
30
|
export async function listTasksCmd(options: {
|
|
39
|
-
|
|
31
|
+
workspace?: string;
|
|
40
32
|
status?: string;
|
|
41
33
|
priority?: string;
|
|
42
34
|
all?: boolean;
|
|
43
35
|
}): Promise<void> {
|
|
44
36
|
let tasks: Task[];
|
|
45
37
|
|
|
46
|
-
if (options.
|
|
47
|
-
|
|
38
|
+
if (options.workspace) {
|
|
39
|
+
// Filter by specific workspace
|
|
40
|
+
const allTasks = await listAllTasks();
|
|
41
|
+
tasks = allTasks.filter(t => t.workspace === options.workspace || t.workspace.startsWith(options.workspace!));
|
|
48
42
|
} else {
|
|
49
|
-
|
|
43
|
+
// Current workspace
|
|
44
|
+
tasks = await listTasks();
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
// Apply filters
|
|
@@ -68,10 +63,11 @@ export async function listTasksCmd(options: {
|
|
|
68
63
|
return;
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
|
|
66
|
+
const currentWorkspace = getCurrentWorkspace();
|
|
67
|
+
console.log(c.bold(`\nTasks (${tasks.length}) - ${c.cyan(currentWorkspace)}\n`));
|
|
72
68
|
|
|
73
69
|
const columns: TableColumn[] = [
|
|
74
|
-
{ header: "ID", key: "id", width: COLUMN_WIDTH_ID, format: (v) => c.cyan(String(v).slice(0,
|
|
70
|
+
{ header: "ID", key: "id", width: COLUMN_WIDTH_ID, format: (v) => c.cyan(String(v).slice(0, ID_DISPLAY_LENGTH)) },
|
|
75
71
|
{ header: "Title", key: "title", width: COLUMN_WIDTH_TITLE },
|
|
76
72
|
{ header: "Status", key: "status", width: COLUMN_WIDTH_STATUS, format: (v) => formatStatus(v as Task["status"]) },
|
|
77
73
|
{ header: "Priority", key: "priority", width: COLUMN_WIDTH_PRIORITY, format: (v) => formatPriority(v as Task["priority"]) },
|
|
@@ -82,40 +78,29 @@ export async function listTasksCmd(options: {
|
|
|
82
78
|
console.log();
|
|
83
79
|
}
|
|
84
80
|
|
|
85
|
-
export async function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const projects = await listProjects(options.all);
|
|
81
|
+
export async function listWorkspacesCmd(): Promise<void> {
|
|
82
|
+
const workspaces = await listWorkspaces();
|
|
83
|
+
const currentWorkspace = getCurrentWorkspace();
|
|
89
84
|
|
|
90
|
-
if (
|
|
91
|
-
console.log(c.gray("No
|
|
85
|
+
if (workspaces.length === 0) {
|
|
86
|
+
console.log(c.gray("No workspaces found."));
|
|
92
87
|
return;
|
|
93
88
|
}
|
|
94
89
|
|
|
95
|
-
console.log(c.bold(`\
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const total = tasks.filter(t => t.status !== "cancelled").length;
|
|
103
|
-
return {
|
|
104
|
-
...p,
|
|
105
|
-
taskCount: tasks.length,
|
|
106
|
-
progress: total > 0 ? `${completed}/${total}` : "0/0",
|
|
107
|
-
};
|
|
108
|
-
})
|
|
109
|
-
);
|
|
90
|
+
console.log(c.bold(`\nWorkspaces (${workspaces.length})\n`));
|
|
91
|
+
|
|
92
|
+
const workspacesWithCurrent = workspaces.map(ws => ({
|
|
93
|
+
...ws,
|
|
94
|
+
current: ws.name === currentWorkspace ? c.green("●") : "",
|
|
95
|
+
progress: `${ws.completedCount}/${ws.taskCount}`,
|
|
96
|
+
}));
|
|
110
97
|
|
|
111
98
|
const columns: TableColumn[] = [
|
|
112
|
-
{ header: "
|
|
113
|
-
{ header: "Name", key: "name", width: COLUMN_WIDTH_PROJECT_NAME },
|
|
114
|
-
{ header: "Status", key: "status", width: COLUMN_WIDTH_STATUS, format: (v) => formatProjectStatus(v as Project["status"]) },
|
|
99
|
+
{ header: "", key: "current", width: 2 },
|
|
100
|
+
{ header: "Name", key: "name", width: COLUMN_WIDTH_PROJECT_NAME, format: (v) => c.cyan(String(v)) },
|
|
115
101
|
{ header: "Tasks", key: "progress", width: COLUMN_WIDTH_PRIORITY },
|
|
116
|
-
{ header: "Updated", key: "updatedAt", width: COLUMN_WIDTH_DATE, format: (v) => formatDate(v as string) },
|
|
117
102
|
];
|
|
118
103
|
|
|
119
|
-
console.log(table(
|
|
104
|
+
console.log(table(workspacesWithCurrent, columns));
|
|
120
105
|
console.log();
|
|
121
106
|
}
|
package/src/constants.ts
CHANGED
|
@@ -54,3 +54,6 @@ export const INBOX_PROMOTE_TITLE_MAX_LENGTH = 100;
|
|
|
54
54
|
|
|
55
55
|
/** Maximum length for content preview when discarding inbox item */
|
|
56
56
|
export const INBOX_DISCARD_PREVIEW_LENGTH = 50;
|
|
57
|
+
|
|
58
|
+
/** Number of characters to show for truncated task/project IDs */
|
|
59
|
+
export const ID_DISPLAY_LENGTH = 4;
|
package/src/index.ts
CHANGED
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* task # Show dashboard (default)
|
|
7
|
-
* task dashboard [
|
|
8
|
-
* task d [
|
|
7
|
+
* task dashboard [ws] # Show workspace dashboard
|
|
8
|
+
* task d [ws] # Alias for dashboard
|
|
9
9
|
* task list # List tasks
|
|
10
10
|
* task ls # Alias for list
|
|
11
|
-
* task
|
|
12
|
-
* task
|
|
11
|
+
* task workspaces # List workspaces
|
|
12
|
+
* task ws # Alias for workspaces
|
|
13
13
|
* task help # Show help
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { c, banner } from "./ansi.js";
|
|
17
17
|
import { dashboard } from "./commands/dashboard.js";
|
|
18
|
-
import { listTasksCmd,
|
|
18
|
+
import { listTasksCmd, listWorkspacesCmd } from "./commands/list.js";
|
|
19
19
|
import { startInteractive } from "./interactive.js";
|
|
20
20
|
import {
|
|
21
21
|
inboxAddCmd,
|
|
@@ -167,12 +167,11 @@ async function handleInboxCommand(
|
|
|
167
167
|
|
|
168
168
|
case "promote":
|
|
169
169
|
if (!args[1]) {
|
|
170
|
-
console.log(c.error("Usage: task inbox promote <item-id> [--
|
|
170
|
+
console.log(c.error("Usage: task inbox promote <item-id> [--priority <p>]"));
|
|
171
171
|
return;
|
|
172
172
|
}
|
|
173
173
|
await inboxPromoteCmd({
|
|
174
174
|
itemId: args[1],
|
|
175
|
-
projectId: flags.project as string | undefined,
|
|
176
175
|
title: flags.title as string | undefined,
|
|
177
176
|
priority: flags.priority as "critical" | "high" | "medium" | "low" | undefined,
|
|
178
177
|
});
|
|
@@ -224,18 +223,16 @@ async function main(): Promise<void> {
|
|
|
224
223
|
case "list":
|
|
225
224
|
case "ls":
|
|
226
225
|
await listTasksCmd({
|
|
227
|
-
|
|
226
|
+
workspace: args[0],
|
|
228
227
|
status: flags.status as string | undefined,
|
|
229
228
|
priority: flags.priority as string | undefined,
|
|
230
229
|
all: flags.all === true,
|
|
231
230
|
});
|
|
232
231
|
break;
|
|
233
232
|
|
|
234
|
-
case "
|
|
235
|
-
case "
|
|
236
|
-
await
|
|
237
|
-
all: flags.all === true,
|
|
238
|
-
});
|
|
233
|
+
case "workspaces":
|
|
234
|
+
case "ws":
|
|
235
|
+
await listWorkspacesCmd();
|
|
239
236
|
break;
|
|
240
237
|
|
|
241
238
|
case "interactive":
|
package/src/interactive.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { c, banner } from "./ansi.js";
|
|
7
7
|
import { dashboard } from "./commands/dashboard.js";
|
|
8
|
-
import { listTasksCmd,
|
|
8
|
+
import { listTasksCmd, listWorkspacesCmd } from "./commands/list.js";
|
|
9
9
|
import * as readline from "node:readline";
|
|
10
10
|
import {
|
|
11
11
|
MENU_SEPARATOR_WIDTH,
|
|
@@ -87,17 +87,17 @@ async function mainMenu(): Promise<boolean> {
|
|
|
87
87
|
{
|
|
88
88
|
key: "d",
|
|
89
89
|
label: "Dashboard",
|
|
90
|
-
description: "View
|
|
90
|
+
description: "View workspace dashboard",
|
|
91
91
|
action: async () => {
|
|
92
92
|
await dashboardMenu();
|
|
93
93
|
},
|
|
94
94
|
},
|
|
95
95
|
{
|
|
96
|
-
key: "
|
|
97
|
-
label: "
|
|
98
|
-
description: "List
|
|
96
|
+
key: "w",
|
|
97
|
+
label: "Workspaces",
|
|
98
|
+
description: "List workspaces",
|
|
99
99
|
action: async () => {
|
|
100
|
-
await
|
|
100
|
+
await workspacesMenu();
|
|
101
101
|
},
|
|
102
102
|
},
|
|
103
103
|
{
|
|
@@ -132,35 +132,25 @@ async function dashboardMenu(): Promise<void> {
|
|
|
132
132
|
console.log(c.dim("─".repeat(MENU_SEPARATOR_WIDTH)));
|
|
133
133
|
console.log();
|
|
134
134
|
|
|
135
|
-
const
|
|
135
|
+
const workspace = await prompt(c.cyan("Workspace name (Enter for current): "));
|
|
136
136
|
|
|
137
137
|
console.log();
|
|
138
|
-
await dashboard(
|
|
138
|
+
await dashboard(workspace || undefined);
|
|
139
139
|
console.log();
|
|
140
140
|
await waitForKey();
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
|
-
*
|
|
144
|
+
* Workspaces Menu
|
|
145
145
|
*/
|
|
146
|
-
async function
|
|
146
|
+
async function workspacesMenu(): Promise<void> {
|
|
147
147
|
const options: MenuOption[] = [
|
|
148
148
|
{
|
|
149
149
|
key: "l",
|
|
150
|
-
label: "List
|
|
151
|
-
action: async () => {
|
|
152
|
-
console.clear();
|
|
153
|
-
await listProjectsCmd({ all: false });
|
|
154
|
-
console.log();
|
|
155
|
-
await waitForKey();
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
key: "a",
|
|
160
|
-
label: "List All (include archived)",
|
|
150
|
+
label: "List Workspaces",
|
|
161
151
|
action: async () => {
|
|
162
152
|
console.clear();
|
|
163
|
-
await
|
|
153
|
+
await listWorkspacesCmd();
|
|
164
154
|
console.log();
|
|
165
155
|
await waitForKey();
|
|
166
156
|
},
|
|
@@ -172,7 +162,7 @@ async function projectsMenu(): Promise<void> {
|
|
|
172
162
|
},
|
|
173
163
|
];
|
|
174
164
|
|
|
175
|
-
await showMenu("
|
|
165
|
+
await showMenu("Workspaces", options);
|
|
176
166
|
}
|
|
177
167
|
|
|
178
168
|
/**
|
package/src/storage.ts
CHANGED
|
@@ -3,20 +3,19 @@
|
|
|
3
3
|
* Wrapper functions for store access and pure functions for task statistics
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Task,
|
|
6
|
+
import type { Task, InboxItem } from "@task-mcp/shared";
|
|
7
|
+
import type { WorkspaceInfo } from "@task-mcp/shared";
|
|
7
8
|
import {
|
|
8
9
|
TaskStore,
|
|
9
|
-
ProjectStore,
|
|
10
10
|
InboxStore,
|
|
11
11
|
StateStore,
|
|
12
12
|
} from "@task-mcp/mcp-server/storage";
|
|
13
13
|
|
|
14
14
|
// Re-export types from shared for consumers of this module
|
|
15
|
-
export type { Task,
|
|
15
|
+
export type { Task, InboxItem, WorkspaceInfo };
|
|
16
16
|
|
|
17
17
|
// Initialize stores
|
|
18
18
|
const taskStore = new TaskStore();
|
|
19
|
-
const projectStore = new ProjectStore();
|
|
20
19
|
const inboxStore = new InboxStore();
|
|
21
20
|
const stateStore = new StateStore();
|
|
22
21
|
|
|
@@ -25,25 +24,40 @@ const stateStore = new StateStore();
|
|
|
25
24
|
// =============================================================================
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
|
-
*
|
|
27
|
+
* Get current workspace name
|
|
29
28
|
*/
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
export function getCurrentWorkspace(): string {
|
|
30
|
+
return taskStore.currentWorkspace;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* List all workspaces
|
|
35
|
+
*/
|
|
36
|
+
export async function listWorkspaces(): Promise<WorkspaceInfo[]> {
|
|
37
|
+
const workspaceNames = await taskStore.listWorkspaces();
|
|
38
|
+
const workspaces: WorkspaceInfo[] = [];
|
|
39
|
+
|
|
40
|
+
for (const name of workspaceNames) {
|
|
41
|
+
const tasks = await taskStore.list({ workspaces: [name] });
|
|
42
|
+
workspaces.push({
|
|
43
|
+
name,
|
|
44
|
+
taskCount: tasks.length,
|
|
45
|
+
completedCount: tasks.filter(t => t.status === "completed").length,
|
|
46
|
+
});
|
|
34
47
|
}
|
|
35
|
-
|
|
48
|
+
|
|
49
|
+
return workspaces;
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
/**
|
|
39
|
-
* List tasks for
|
|
53
|
+
* List tasks for current workspace
|
|
40
54
|
*/
|
|
41
|
-
export async function listTasks(
|
|
42
|
-
return taskStore.list(
|
|
55
|
+
export async function listTasks(): Promise<Task[]> {
|
|
56
|
+
return taskStore.list();
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
/**
|
|
46
|
-
* List all tasks across all
|
|
60
|
+
* List all tasks across all workspaces
|
|
47
61
|
*/
|
|
48
62
|
export async function listAllTasks(): Promise<Task[]> {
|
|
49
63
|
return taskStore.listAll();
|
|
@@ -66,9 +80,9 @@ export async function getActiveTag(): Promise<string> {
|
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
/**
|
|
69
|
-
* Get
|
|
83
|
+
* Get workspace statistics
|
|
70
84
|
*/
|
|
71
|
-
export interface
|
|
85
|
+
export interface WorkspaceStats {
|
|
72
86
|
total: number;
|
|
73
87
|
completed: number;
|
|
74
88
|
inProgress: number;
|
|
@@ -79,8 +93,8 @@ export interface ProjectStats {
|
|
|
79
93
|
completionPercent: number;
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
export function calculateStats(tasks: Task[]):
|
|
83
|
-
const stats:
|
|
96
|
+
export function calculateStats(tasks: Task[]): WorkspaceStats {
|
|
97
|
+
const stats: WorkspaceStats = {
|
|
84
98
|
total: tasks.length,
|
|
85
99
|
completed: 0,
|
|
86
100
|
inProgress: 0,
|