@cwim/kanban 1.1.20 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +284 -270
- package/dashboard/dist/assets/{index-p_oYIoTL.js → index-C9-ldHtP.js} +5 -5
- package/dashboard/dist/assets/{index-WVxfVg2r.css → index-DrSkXUSP.css} +1 -1
- package/dashboard/dist/index.html +13 -13
- package/dist/cli/commands.js +79 -77
- package/dist/cli/commands.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +168 -134
- package/dist/mcp/server.js.map +1 -1
- package/dist/storage/store.d.ts +4 -4
- package/dist/storage/store.d.ts.map +1 -1
- package/dist/storage/store.js +255 -73
- package/dist/storage/store.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +14 -9
- package/dist/types.js.map +1 -1
- package/package.json +68 -68
package/dist/storage/store.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { promises as fs, existsSync, mkdirSync } from
|
|
2
|
-
import * as path from
|
|
3
|
-
import * as os from
|
|
4
|
-
import { execSync } from
|
|
5
|
-
const KANBAN_DIR = path.join(os.homedir(),
|
|
6
|
-
const SESSIONS_DIR = path.join(KANBAN_DIR,
|
|
7
|
-
const ACTIVE_SESSION_FILE = path.join(KANBAN_DIR,
|
|
8
|
-
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(),
|
|
9
|
-
const OPENCODE_CONFIG_DIR = path.join(os.homedir(),
|
|
10
|
-
const OPENCODE_DATA_DIR = path.join(os.homedir(),
|
|
1
|
+
import { promises as fs, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
const KANBAN_DIR = path.join(os.homedir(), ".kanban");
|
|
6
|
+
const SESSIONS_DIR = path.join(KANBAN_DIR, "sessions");
|
|
7
|
+
const ACTIVE_SESSION_FILE = path.join(KANBAN_DIR, "active-session.json");
|
|
8
|
+
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
|
|
9
|
+
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), ".config", "opencode");
|
|
10
|
+
const OPENCODE_DATA_DIR = path.join(os.homedir(), ".local", "share", "opencode");
|
|
11
11
|
function getDefaultData() {
|
|
12
12
|
return {
|
|
13
13
|
version: 1,
|
|
@@ -24,13 +24,13 @@ function getSessionDir(sessionName) {
|
|
|
24
24
|
return path.join(SESSIONS_DIR, sessionName);
|
|
25
25
|
}
|
|
26
26
|
function getSessionDataFile(sessionName) {
|
|
27
|
-
return path.join(getSessionDir(sessionName),
|
|
27
|
+
return path.join(getSessionDir(sessionName), "tasks.json");
|
|
28
28
|
}
|
|
29
29
|
export async function getActiveSession() {
|
|
30
30
|
try {
|
|
31
31
|
if (!existsSync(ACTIVE_SESSION_FILE))
|
|
32
32
|
return undefined;
|
|
33
|
-
const raw = await fs.readFile(ACTIVE_SESSION_FILE,
|
|
33
|
+
const raw = await fs.readFile(ACTIVE_SESSION_FILE, "utf-8");
|
|
34
34
|
const active = JSON.parse(raw);
|
|
35
35
|
return active.name;
|
|
36
36
|
}
|
|
@@ -44,21 +44,23 @@ export async function setActiveSession(sessionName) {
|
|
|
44
44
|
name: sessionName,
|
|
45
45
|
setAt: new Date().toISOString(),
|
|
46
46
|
};
|
|
47
|
-
await fs.writeFile(ACTIVE_SESSION_FILE, JSON.stringify(active, null, 2),
|
|
47
|
+
await fs.writeFile(ACTIVE_SESSION_FILE, JSON.stringify(active, null, 2), "utf-8");
|
|
48
48
|
}
|
|
49
49
|
// Session detection
|
|
50
50
|
async function detectClaudeSessions() {
|
|
51
51
|
try {
|
|
52
52
|
if (!existsSync(CLAUDE_PROJECTS_DIR))
|
|
53
53
|
return [];
|
|
54
|
-
const entries = await fs.readdir(CLAUDE_PROJECTS_DIR, {
|
|
54
|
+
const entries = await fs.readdir(CLAUDE_PROJECTS_DIR, {
|
|
55
|
+
withFileTypes: true,
|
|
56
|
+
});
|
|
55
57
|
return entries
|
|
56
58
|
.filter((e) => e.isDirectory())
|
|
57
59
|
.map((e) => ({
|
|
58
60
|
name: e.name,
|
|
59
61
|
path: path.join(CLAUDE_PROJECTS_DIR, e.name),
|
|
60
62
|
detectedAt: new Date().toISOString(),
|
|
61
|
-
source:
|
|
63
|
+
source: "claude",
|
|
62
64
|
}));
|
|
63
65
|
}
|
|
64
66
|
catch {
|
|
@@ -66,10 +68,23 @@ async function detectClaudeSessions() {
|
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
const OPENCODE_CONFIG_EXCLUDES = new Set([
|
|
69
|
-
|
|
71
|
+
"agents",
|
|
72
|
+
"command",
|
|
73
|
+
"commands",
|
|
74
|
+
"modes",
|
|
75
|
+
"plugins",
|
|
76
|
+
"skills",
|
|
77
|
+
"tools",
|
|
78
|
+
"themes",
|
|
79
|
+
"node_modules",
|
|
70
80
|
]);
|
|
71
81
|
const OPENCODE_DATA_EXCLUDES = new Set([
|
|
72
|
-
|
|
82
|
+
"bin",
|
|
83
|
+
"log",
|
|
84
|
+
"repos",
|
|
85
|
+
"snapshot",
|
|
86
|
+
"storage",
|
|
87
|
+
"tool-output",
|
|
73
88
|
]);
|
|
74
89
|
const ALL_INTERNAL_NAMES = new Set([
|
|
75
90
|
...OPENCODE_CONFIG_EXCLUDES,
|
|
@@ -85,7 +100,7 @@ async function detectOpencodeSessions() {
|
|
|
85
100
|
return detectOpencodeSessionsFromDirectories();
|
|
86
101
|
}
|
|
87
102
|
async function queryOpencodeDatabase() {
|
|
88
|
-
const dbPath = path.join(OPENCODE_DATA_DIR,
|
|
103
|
+
const dbPath = path.join(OPENCODE_DATA_DIR, "opencode.db");
|
|
89
104
|
if (!existsSync(dbPath)) {
|
|
90
105
|
return [];
|
|
91
106
|
}
|
|
@@ -95,11 +110,11 @@ async function queryOpencodeDatabase() {
|
|
|
95
110
|
const query = `SELECT id, worktree, name, time_updated FROM project WHERE worktree IS NOT NULL AND worktree != '/' ORDER BY time_updated DESC;`;
|
|
96
111
|
// Write query to temp file to avoid shell escaping issues with paths
|
|
97
112
|
const tempQueryFile = path.join(os.tmpdir(), `opencode-query-${Date.now()}.sql`);
|
|
98
|
-
await fs.writeFile(tempQueryFile, query,
|
|
113
|
+
await fs.writeFile(tempQueryFile, query, "utf-8");
|
|
99
114
|
let result;
|
|
100
115
|
try {
|
|
101
116
|
result = execSync(`sqlite3 "${dbPath}" < "${tempQueryFile}"`, {
|
|
102
|
-
encoding:
|
|
117
|
+
encoding: "utf8",
|
|
103
118
|
timeout: 10000,
|
|
104
119
|
});
|
|
105
120
|
}
|
|
@@ -114,14 +129,14 @@ async function queryOpencodeDatabase() {
|
|
|
114
129
|
}
|
|
115
130
|
const sessions = [];
|
|
116
131
|
const seenPaths = new Set();
|
|
117
|
-
for (const line of result.trim().split(
|
|
132
|
+
for (const line of result.trim().split("\n")) {
|
|
118
133
|
if (!line.trim())
|
|
119
134
|
continue;
|
|
120
|
-
const parts = line.split(
|
|
135
|
+
const parts = line.split("|");
|
|
121
136
|
if (parts.length < 4)
|
|
122
137
|
continue;
|
|
123
138
|
const [, worktree, name, timeUpdated] = parts;
|
|
124
|
-
if (!worktree || worktree ===
|
|
139
|
+
if (!worktree || worktree === "/")
|
|
125
140
|
continue;
|
|
126
141
|
// Normalize path for deduplication
|
|
127
142
|
const normalizedPath = path.normalize(worktree);
|
|
@@ -134,7 +149,7 @@ async function queryOpencodeDatabase() {
|
|
|
134
149
|
name: sessionName,
|
|
135
150
|
path: normalizedPath,
|
|
136
151
|
detectedAt: new Date(parseInt(timeUpdated)).toISOString(),
|
|
137
|
-
source:
|
|
152
|
+
source: "opencode",
|
|
138
153
|
});
|
|
139
154
|
}
|
|
140
155
|
return sessions;
|
|
@@ -149,14 +164,16 @@ async function detectOpencodeSessionsFromDirectories() {
|
|
|
149
164
|
// Check ~/.config/opencode/ for project directories
|
|
150
165
|
try {
|
|
151
166
|
if (existsSync(OPENCODE_CONFIG_DIR)) {
|
|
152
|
-
const entries = await fs.readdir(OPENCODE_CONFIG_DIR, {
|
|
167
|
+
const entries = await fs.readdir(OPENCODE_CONFIG_DIR, {
|
|
168
|
+
withFileTypes: true,
|
|
169
|
+
});
|
|
153
170
|
for (const entry of entries) {
|
|
154
171
|
if (entry.isDirectory() && !OPENCODE_CONFIG_EXCLUDES.has(entry.name)) {
|
|
155
172
|
sessions.push({
|
|
156
173
|
name: entry.name,
|
|
157
174
|
path: path.join(OPENCODE_CONFIG_DIR, entry.name),
|
|
158
175
|
detectedAt: new Date().toISOString(),
|
|
159
|
-
source:
|
|
176
|
+
source: "opencode",
|
|
160
177
|
});
|
|
161
178
|
}
|
|
162
179
|
}
|
|
@@ -168,17 +185,19 @@ async function detectOpencodeSessionsFromDirectories() {
|
|
|
168
185
|
// Check ~/.local/share/opencode/ for session data
|
|
169
186
|
try {
|
|
170
187
|
if (existsSync(OPENCODE_DATA_DIR)) {
|
|
171
|
-
const entries = await fs.readdir(OPENCODE_DATA_DIR, {
|
|
188
|
+
const entries = await fs.readdir(OPENCODE_DATA_DIR, {
|
|
189
|
+
withFileTypes: true,
|
|
190
|
+
});
|
|
172
191
|
for (const entry of entries) {
|
|
173
192
|
if (entry.isDirectory() && !OPENCODE_DATA_EXCLUDES.has(entry.name)) {
|
|
174
193
|
const sessionName = entry.name;
|
|
175
194
|
// Only add if not already found
|
|
176
|
-
if (!sessions.some(s => s.name === sessionName)) {
|
|
195
|
+
if (!sessions.some((s) => s.name === sessionName)) {
|
|
177
196
|
sessions.push({
|
|
178
197
|
name: sessionName,
|
|
179
198
|
path: path.join(OPENCODE_DATA_DIR, entry.name),
|
|
180
199
|
detectedAt: new Date().toISOString(),
|
|
181
|
-
source:
|
|
200
|
+
source: "opencode",
|
|
182
201
|
});
|
|
183
202
|
}
|
|
184
203
|
}
|
|
@@ -190,22 +209,58 @@ async function detectOpencodeSessionsFromDirectories() {
|
|
|
190
209
|
}
|
|
191
210
|
return sessions;
|
|
192
211
|
}
|
|
212
|
+
function detectGitRepo() {
|
|
213
|
+
try {
|
|
214
|
+
const cwd = process.cwd();
|
|
215
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
216
|
+
cwd,
|
|
217
|
+
encoding: "utf8",
|
|
218
|
+
timeout: 5000,
|
|
219
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
220
|
+
}).trim();
|
|
221
|
+
if (!gitRoot)
|
|
222
|
+
return undefined;
|
|
223
|
+
const normalizedPath = path.normalize(gitRoot);
|
|
224
|
+
const name = path.basename(normalizedPath);
|
|
225
|
+
return { name, path: normalizedPath };
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function detectGitRepoSessions() {
|
|
232
|
+
const repo = detectGitRepo();
|
|
233
|
+
if (!repo)
|
|
234
|
+
return [];
|
|
235
|
+
return [
|
|
236
|
+
{
|
|
237
|
+
name: repo.name,
|
|
238
|
+
path: repo.path,
|
|
239
|
+
detectedAt: new Date().toISOString(),
|
|
240
|
+
source: "git",
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
}
|
|
193
244
|
export async function detectSessions() {
|
|
194
245
|
try {
|
|
195
|
-
// Detect sessions from
|
|
196
|
-
const [claudeSessions, opencodeSessions] = await Promise.all([
|
|
246
|
+
// Detect sessions from all sources: Git (CWD), Claude, and OpenCode
|
|
247
|
+
const [gitSessions, claudeSessions, opencodeSessions] = await Promise.all([
|
|
248
|
+
detectGitRepoSessions(),
|
|
197
249
|
detectClaudeSessions(),
|
|
198
250
|
detectOpencodeSessions(),
|
|
199
251
|
]);
|
|
200
252
|
// Merge and deduplicate by name
|
|
253
|
+
// Priority: Git > Claude > OpenCode
|
|
201
254
|
const sessionMap = new Map();
|
|
255
|
+
for (const session of opencodeSessions) {
|
|
256
|
+
sessionMap.set(session.name, session);
|
|
257
|
+
}
|
|
202
258
|
for (const session of claudeSessions) {
|
|
203
259
|
sessionMap.set(session.name, session);
|
|
204
260
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
261
|
+
// Git sessions take highest priority - they reflect the actual working directory
|
|
262
|
+
for (const session of gitSessions) {
|
|
263
|
+
sessionMap.set(session.name, session);
|
|
209
264
|
}
|
|
210
265
|
const sessions = Array.from(sessionMap.values());
|
|
211
266
|
if (sessions.length === 0)
|
|
@@ -234,6 +289,12 @@ export async function detectSessions() {
|
|
|
234
289
|
}
|
|
235
290
|
export async function detectLatestSession() {
|
|
236
291
|
const sessions = await detectSessions();
|
|
292
|
+
// If we're in a git repo, prefer the git-detected session
|
|
293
|
+
// even if it's not the most recently modified
|
|
294
|
+
const gitSession = sessions.find((s) => s.source === "git");
|
|
295
|
+
if (gitSession) {
|
|
296
|
+
return gitSession;
|
|
297
|
+
}
|
|
237
298
|
return sessions[0];
|
|
238
299
|
}
|
|
239
300
|
export async function listAllSessions() {
|
|
@@ -247,13 +308,13 @@ export async function listAllSessions() {
|
|
|
247
308
|
for (const entry of entries) {
|
|
248
309
|
if (entry.isDirectory() &&
|
|
249
310
|
!detectedNames.has(entry.name) &&
|
|
250
|
-
entry.name !==
|
|
311
|
+
entry.name !== "independent" &&
|
|
251
312
|
!ALL_INTERNAL_NAMES.has(entry.name)) {
|
|
252
313
|
storedSessions.push({
|
|
253
314
|
name: entry.name,
|
|
254
315
|
path: getSessionDir(entry.name),
|
|
255
316
|
detectedAt: new Date().toISOString(),
|
|
256
|
-
source:
|
|
317
|
+
source: "manual",
|
|
257
318
|
});
|
|
258
319
|
}
|
|
259
320
|
}
|
|
@@ -263,10 +324,10 @@ export async function listAllSessions() {
|
|
|
263
324
|
...detected,
|
|
264
325
|
...storedSessions,
|
|
265
326
|
{
|
|
266
|
-
name:
|
|
267
|
-
path: getSessionDir(
|
|
327
|
+
name: "independent",
|
|
328
|
+
path: getSessionDir("independent"),
|
|
268
329
|
detectedAt: new Date().toISOString(),
|
|
269
|
-
source:
|
|
330
|
+
source: "independent",
|
|
270
331
|
},
|
|
271
332
|
];
|
|
272
333
|
return allSessions;
|
|
@@ -283,15 +344,15 @@ export async function getCurrentSessionName() {
|
|
|
283
344
|
return latest.name;
|
|
284
345
|
}
|
|
285
346
|
// Fallback to independent
|
|
286
|
-
await setActiveSession(
|
|
287
|
-
return
|
|
347
|
+
await setActiveSession("independent");
|
|
348
|
+
return "independent";
|
|
288
349
|
}
|
|
289
350
|
// Per-session data operations
|
|
290
351
|
async function readSessionData(sessionName) {
|
|
291
352
|
const dataFile = getSessionDataFile(sessionName);
|
|
292
353
|
ensureDir(getSessionDir(sessionName));
|
|
293
354
|
try {
|
|
294
|
-
const raw = await fs.readFile(dataFile,
|
|
355
|
+
const raw = await fs.readFile(dataFile, "utf-8");
|
|
295
356
|
return JSON.parse(raw);
|
|
296
357
|
}
|
|
297
358
|
catch {
|
|
@@ -304,13 +365,13 @@ async function writeSessionData(sessionName, data) {
|
|
|
304
365
|
const dataFile = getSessionDataFile(sessionName);
|
|
305
366
|
ensureDir(getSessionDir(sessionName));
|
|
306
367
|
data.updatedAt = new Date().toISOString();
|
|
307
|
-
const tempFile = dataFile +
|
|
308
|
-
await fs.writeFile(tempFile, JSON.stringify(data, null, 2),
|
|
368
|
+
const tempFile = dataFile + ".tmp";
|
|
369
|
+
await fs.writeFile(tempFile, JSON.stringify(data, null, 2), "utf-8");
|
|
309
370
|
await fs.rename(tempFile, dataFile);
|
|
310
371
|
}
|
|
311
372
|
// Task CRUD (session-aware)
|
|
312
373
|
export async function listTasks(sessionName, filter) {
|
|
313
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
374
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
314
375
|
const data = await readSessionData(targetSession);
|
|
315
376
|
let tasks = data.tasks;
|
|
316
377
|
if (filter?.status) {
|
|
@@ -327,20 +388,20 @@ export async function listTasks(sessionName, filter) {
|
|
|
327
388
|
return tasks;
|
|
328
389
|
}
|
|
329
390
|
export async function getTask(id, sessionName) {
|
|
330
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
391
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
331
392
|
const data = await readSessionData(targetSession);
|
|
332
393
|
return data.tasks.find((t) => t.id === id);
|
|
333
394
|
}
|
|
334
395
|
export async function createTask(input, sessionName) {
|
|
335
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
396
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
336
397
|
const data = await readSessionData(targetSession);
|
|
337
398
|
const task = {
|
|
338
399
|
id: generateId(),
|
|
339
400
|
title: input.title,
|
|
340
401
|
description: input.description,
|
|
341
|
-
status: input.status ??
|
|
402
|
+
status: input.status ?? "todo",
|
|
342
403
|
tags: input.tags ?? [],
|
|
343
|
-
source: input.source ??
|
|
404
|
+
source: input.source ?? "manual",
|
|
344
405
|
createdAt: new Date().toISOString(),
|
|
345
406
|
updatedAt: new Date().toISOString(),
|
|
346
407
|
};
|
|
@@ -349,7 +410,7 @@ export async function createTask(input, sessionName) {
|
|
|
349
410
|
return task;
|
|
350
411
|
}
|
|
351
412
|
export async function updateTask(id, input, sessionName) {
|
|
352
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
413
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
353
414
|
const data = await readSessionData(targetSession);
|
|
354
415
|
const idx = data.tasks.findIndex((t) => t.id === id);
|
|
355
416
|
if (idx === -1)
|
|
@@ -371,7 +432,7 @@ export async function moveTask(id, status, sessionName) {
|
|
|
371
432
|
return updateTask(id, { status }, sessionName);
|
|
372
433
|
}
|
|
373
434
|
export async function deleteTask(id, sessionName) {
|
|
374
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
435
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
375
436
|
const data = await readSessionData(targetSession);
|
|
376
437
|
const initialLen = data.tasks.length;
|
|
377
438
|
data.tasks = data.tasks.filter((t) => t.id !== id);
|
|
@@ -381,7 +442,7 @@ export async function deleteTask(id, sessionName) {
|
|
|
381
442
|
return true;
|
|
382
443
|
}
|
|
383
444
|
export async function appendNote(id, note, sessionName) {
|
|
384
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
445
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
385
446
|
const data = await readSessionData(targetSession);
|
|
386
447
|
const idx = data.tasks.findIndex((t) => t.id === id);
|
|
387
448
|
if (idx === -1)
|
|
@@ -397,7 +458,7 @@ export async function appendNote(id, note, sessionName) {
|
|
|
397
458
|
return task;
|
|
398
459
|
}
|
|
399
460
|
export async function getAllData(sessionName) {
|
|
400
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
461
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
401
462
|
const data = await readSessionData(targetSession);
|
|
402
463
|
// Attach session info
|
|
403
464
|
const sessions = await listAllSessions();
|
|
@@ -408,7 +469,128 @@ export async function getAllData(sessionName) {
|
|
|
408
469
|
return data;
|
|
409
470
|
}
|
|
410
471
|
const STOP_WORDS = new Set([
|
|
411
|
-
|
|
472
|
+
"the",
|
|
473
|
+
"and",
|
|
474
|
+
"for",
|
|
475
|
+
"are",
|
|
476
|
+
"but",
|
|
477
|
+
"not",
|
|
478
|
+
"you",
|
|
479
|
+
"all",
|
|
480
|
+
"can",
|
|
481
|
+
"had",
|
|
482
|
+
"her",
|
|
483
|
+
"was",
|
|
484
|
+
"one",
|
|
485
|
+
"our",
|
|
486
|
+
"out",
|
|
487
|
+
"day",
|
|
488
|
+
"get",
|
|
489
|
+
"has",
|
|
490
|
+
"him",
|
|
491
|
+
"his",
|
|
492
|
+
"how",
|
|
493
|
+
"its",
|
|
494
|
+
"may",
|
|
495
|
+
"new",
|
|
496
|
+
"now",
|
|
497
|
+
"old",
|
|
498
|
+
"see",
|
|
499
|
+
"two",
|
|
500
|
+
"who",
|
|
501
|
+
"boy",
|
|
502
|
+
"did",
|
|
503
|
+
"she",
|
|
504
|
+
"use",
|
|
505
|
+
"her",
|
|
506
|
+
"way",
|
|
507
|
+
"many",
|
|
508
|
+
"oil",
|
|
509
|
+
"sit",
|
|
510
|
+
"set",
|
|
511
|
+
"run",
|
|
512
|
+
"eat",
|
|
513
|
+
"far",
|
|
514
|
+
"sea",
|
|
515
|
+
"eye",
|
|
516
|
+
"ago",
|
|
517
|
+
"off",
|
|
518
|
+
"too",
|
|
519
|
+
"any",
|
|
520
|
+
"say",
|
|
521
|
+
"man",
|
|
522
|
+
"try",
|
|
523
|
+
"ask",
|
|
524
|
+
"end",
|
|
525
|
+
"why",
|
|
526
|
+
"let",
|
|
527
|
+
"put",
|
|
528
|
+
"say",
|
|
529
|
+
"she",
|
|
530
|
+
"try",
|
|
531
|
+
"way",
|
|
532
|
+
"own",
|
|
533
|
+
"say",
|
|
534
|
+
"too",
|
|
535
|
+
"old",
|
|
536
|
+
"tell",
|
|
537
|
+
"very",
|
|
538
|
+
"when",
|
|
539
|
+
"much",
|
|
540
|
+
"would",
|
|
541
|
+
"there",
|
|
542
|
+
"their",
|
|
543
|
+
"what",
|
|
544
|
+
"said",
|
|
545
|
+
"each",
|
|
546
|
+
"which",
|
|
547
|
+
"will",
|
|
548
|
+
"about",
|
|
549
|
+
"could",
|
|
550
|
+
"other",
|
|
551
|
+
"after",
|
|
552
|
+
"first",
|
|
553
|
+
"never",
|
|
554
|
+
"these",
|
|
555
|
+
"think",
|
|
556
|
+
"where",
|
|
557
|
+
"being",
|
|
558
|
+
"every",
|
|
559
|
+
"great",
|
|
560
|
+
"might",
|
|
561
|
+
"shall",
|
|
562
|
+
"still",
|
|
563
|
+
"those",
|
|
564
|
+
"while",
|
|
565
|
+
"this",
|
|
566
|
+
"that",
|
|
567
|
+
"with",
|
|
568
|
+
"have",
|
|
569
|
+
"from",
|
|
570
|
+
"they",
|
|
571
|
+
"know",
|
|
572
|
+
"want",
|
|
573
|
+
"been",
|
|
574
|
+
"good",
|
|
575
|
+
"much",
|
|
576
|
+
"some",
|
|
577
|
+
"time",
|
|
578
|
+
"very",
|
|
579
|
+
"when",
|
|
580
|
+
"come",
|
|
581
|
+
"here",
|
|
582
|
+
"just",
|
|
583
|
+
"like",
|
|
584
|
+
"long",
|
|
585
|
+
"make",
|
|
586
|
+
"many",
|
|
587
|
+
"over",
|
|
588
|
+
"such",
|
|
589
|
+
"take",
|
|
590
|
+
"than",
|
|
591
|
+
"them",
|
|
592
|
+
"well",
|
|
593
|
+
"were",
|
|
412
594
|
]);
|
|
413
595
|
function extractKeywords(text) {
|
|
414
596
|
return text
|
|
@@ -420,9 +602,9 @@ function extractKeywords(text) {
|
|
|
420
602
|
function getTaskFields(task) {
|
|
421
603
|
return {
|
|
422
604
|
title: task.title.toLowerCase(),
|
|
423
|
-
tags: task.tags.join(
|
|
424
|
-
description: (task.description ||
|
|
425
|
-
notes: (task.notes || []).join(
|
|
605
|
+
tags: task.tags.join(" ").toLowerCase(),
|
|
606
|
+
description: (task.description || "").toLowerCase(),
|
|
607
|
+
notes: (task.notes || []).join(" ").toLowerCase(),
|
|
426
608
|
};
|
|
427
609
|
}
|
|
428
610
|
function scoreTask(task, queryWords) {
|
|
@@ -435,7 +617,7 @@ function scoreTask(task, queryWords) {
|
|
|
435
617
|
if (fields.title.includes(word)) {
|
|
436
618
|
score += 5;
|
|
437
619
|
signals++;
|
|
438
|
-
reasons.push({ field:
|
|
620
|
+
reasons.push({ field: "title", matched: word });
|
|
439
621
|
break; // Only count title once per word
|
|
440
622
|
}
|
|
441
623
|
}
|
|
@@ -444,7 +626,7 @@ function scoreTask(task, queryWords) {
|
|
|
444
626
|
if (fields.tags.includes(word)) {
|
|
445
627
|
score += 4;
|
|
446
628
|
signals++;
|
|
447
|
-
reasons.push({ field:
|
|
629
|
+
reasons.push({ field: "tag", matched: word });
|
|
448
630
|
break;
|
|
449
631
|
}
|
|
450
632
|
}
|
|
@@ -453,7 +635,7 @@ function scoreTask(task, queryWords) {
|
|
|
453
635
|
if (fields.description.includes(word)) {
|
|
454
636
|
score += 2;
|
|
455
637
|
signals++;
|
|
456
|
-
reasons.push({ field:
|
|
638
|
+
reasons.push({ field: "description", matched: word });
|
|
457
639
|
break;
|
|
458
640
|
}
|
|
459
641
|
}
|
|
@@ -462,7 +644,7 @@ function scoreTask(task, queryWords) {
|
|
|
462
644
|
if (fields.notes.includes(word)) {
|
|
463
645
|
score += 1;
|
|
464
646
|
signals++;
|
|
465
|
-
reasons.push({ field:
|
|
647
|
+
reasons.push({ field: "notes", matched: word });
|
|
466
648
|
break;
|
|
467
649
|
}
|
|
468
650
|
}
|
|
@@ -476,25 +658,25 @@ function scoreTask(task, queryWords) {
|
|
|
476
658
|
score += 1;
|
|
477
659
|
}
|
|
478
660
|
// Status boost
|
|
479
|
-
if (task.status ===
|
|
661
|
+
if (task.status === "in-progress") {
|
|
480
662
|
score += 2;
|
|
481
663
|
signals++;
|
|
482
664
|
}
|
|
483
|
-
else if (task.status ===
|
|
665
|
+
else if (task.status === "blocked") {
|
|
484
666
|
score += 1;
|
|
485
667
|
signals++;
|
|
486
668
|
}
|
|
487
669
|
return { task, score, signals, reasons };
|
|
488
670
|
}
|
|
489
671
|
export async function recallTasks(context, limit = 5, sessionName) {
|
|
490
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
672
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
491
673
|
const data = await readSessionData(targetSession);
|
|
492
674
|
if (data.tasks.length === 0) {
|
|
493
675
|
return {
|
|
494
676
|
relevant: [],
|
|
495
677
|
reasons: new Map(),
|
|
496
678
|
summary: { active: 0, done: 0, blocked: 0, total: 0 },
|
|
497
|
-
quality:
|
|
679
|
+
quality: "poor",
|
|
498
680
|
};
|
|
499
681
|
}
|
|
500
682
|
const queryWords = extractKeywords(context);
|
|
@@ -507,19 +689,19 @@ export async function recallTasks(context, limit = 5, sessionName) {
|
|
|
507
689
|
let quality;
|
|
508
690
|
let actualLimit;
|
|
509
691
|
if (maxScore >= 10) {
|
|
510
|
-
quality =
|
|
692
|
+
quality = "excellent";
|
|
511
693
|
actualLimit = Math.min(limit, 2);
|
|
512
694
|
}
|
|
513
695
|
else if (maxScore >= 7) {
|
|
514
|
-
quality =
|
|
696
|
+
quality = "good";
|
|
515
697
|
actualLimit = Math.min(limit, 3);
|
|
516
698
|
}
|
|
517
699
|
else if (maxScore >= 5) {
|
|
518
|
-
quality =
|
|
700
|
+
quality = "fair";
|
|
519
701
|
actualLimit = 1;
|
|
520
702
|
}
|
|
521
703
|
else {
|
|
522
|
-
quality =
|
|
704
|
+
quality = "poor";
|
|
523
705
|
actualLimit = 0;
|
|
524
706
|
}
|
|
525
707
|
// Filter by minimum threshold and signal count
|
|
@@ -530,9 +712,9 @@ export async function recallTasks(context, limit = 5, sessionName) {
|
|
|
530
712
|
const reasons = new Map(filtered.map((s) => [s.task.id, s.reasons]));
|
|
531
713
|
// Calculate summary
|
|
532
714
|
const summary = {
|
|
533
|
-
active: data.tasks.filter((t) => t.status ===
|
|
534
|
-
done: data.tasks.filter((t) => t.status ===
|
|
535
|
-
blocked: data.tasks.filter((t) => t.status ===
|
|
715
|
+
active: data.tasks.filter((t) => t.status === "in-progress").length,
|
|
716
|
+
done: data.tasks.filter((t) => t.status === "done").length,
|
|
717
|
+
blocked: data.tasks.filter((t) => t.status === "blocked").length,
|
|
536
718
|
total: data.tasks.length,
|
|
537
719
|
};
|
|
538
720
|
return { relevant, reasons, summary, quality };
|