@cwim/kanban 1.1.20 → 1.2.1
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 +260 -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,14 +110,19 @@ 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,
|
|
119
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
104
120
|
});
|
|
105
121
|
}
|
|
122
|
+
catch {
|
|
123
|
+
// sqlite3 not installed or other error - return empty to trigger fallback
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
106
126
|
finally {
|
|
107
127
|
// Clean up temp file
|
|
108
128
|
try {
|
|
@@ -114,14 +134,14 @@ async function queryOpencodeDatabase() {
|
|
|
114
134
|
}
|
|
115
135
|
const sessions = [];
|
|
116
136
|
const seenPaths = new Set();
|
|
117
|
-
for (const line of result.trim().split(
|
|
137
|
+
for (const line of result.trim().split("\n")) {
|
|
118
138
|
if (!line.trim())
|
|
119
139
|
continue;
|
|
120
|
-
const parts = line.split(
|
|
140
|
+
const parts = line.split("|");
|
|
121
141
|
if (parts.length < 4)
|
|
122
142
|
continue;
|
|
123
143
|
const [, worktree, name, timeUpdated] = parts;
|
|
124
|
-
if (!worktree || worktree ===
|
|
144
|
+
if (!worktree || worktree === "/")
|
|
125
145
|
continue;
|
|
126
146
|
// Normalize path for deduplication
|
|
127
147
|
const normalizedPath = path.normalize(worktree);
|
|
@@ -134,7 +154,7 @@ async function queryOpencodeDatabase() {
|
|
|
134
154
|
name: sessionName,
|
|
135
155
|
path: normalizedPath,
|
|
136
156
|
detectedAt: new Date(parseInt(timeUpdated)).toISOString(),
|
|
137
|
-
source:
|
|
157
|
+
source: "opencode",
|
|
138
158
|
});
|
|
139
159
|
}
|
|
140
160
|
return sessions;
|
|
@@ -149,14 +169,16 @@ async function detectOpencodeSessionsFromDirectories() {
|
|
|
149
169
|
// Check ~/.config/opencode/ for project directories
|
|
150
170
|
try {
|
|
151
171
|
if (existsSync(OPENCODE_CONFIG_DIR)) {
|
|
152
|
-
const entries = await fs.readdir(OPENCODE_CONFIG_DIR, {
|
|
172
|
+
const entries = await fs.readdir(OPENCODE_CONFIG_DIR, {
|
|
173
|
+
withFileTypes: true,
|
|
174
|
+
});
|
|
153
175
|
for (const entry of entries) {
|
|
154
176
|
if (entry.isDirectory() && !OPENCODE_CONFIG_EXCLUDES.has(entry.name)) {
|
|
155
177
|
sessions.push({
|
|
156
178
|
name: entry.name,
|
|
157
179
|
path: path.join(OPENCODE_CONFIG_DIR, entry.name),
|
|
158
180
|
detectedAt: new Date().toISOString(),
|
|
159
|
-
source:
|
|
181
|
+
source: "opencode",
|
|
160
182
|
});
|
|
161
183
|
}
|
|
162
184
|
}
|
|
@@ -168,17 +190,19 @@ async function detectOpencodeSessionsFromDirectories() {
|
|
|
168
190
|
// Check ~/.local/share/opencode/ for session data
|
|
169
191
|
try {
|
|
170
192
|
if (existsSync(OPENCODE_DATA_DIR)) {
|
|
171
|
-
const entries = await fs.readdir(OPENCODE_DATA_DIR, {
|
|
193
|
+
const entries = await fs.readdir(OPENCODE_DATA_DIR, {
|
|
194
|
+
withFileTypes: true,
|
|
195
|
+
});
|
|
172
196
|
for (const entry of entries) {
|
|
173
197
|
if (entry.isDirectory() && !OPENCODE_DATA_EXCLUDES.has(entry.name)) {
|
|
174
198
|
const sessionName = entry.name;
|
|
175
199
|
// Only add if not already found
|
|
176
|
-
if (!sessions.some(s => s.name === sessionName)) {
|
|
200
|
+
if (!sessions.some((s) => s.name === sessionName)) {
|
|
177
201
|
sessions.push({
|
|
178
202
|
name: sessionName,
|
|
179
203
|
path: path.join(OPENCODE_DATA_DIR, entry.name),
|
|
180
204
|
detectedAt: new Date().toISOString(),
|
|
181
|
-
source:
|
|
205
|
+
source: "opencode",
|
|
182
206
|
});
|
|
183
207
|
}
|
|
184
208
|
}
|
|
@@ -190,22 +214,58 @@ async function detectOpencodeSessionsFromDirectories() {
|
|
|
190
214
|
}
|
|
191
215
|
return sessions;
|
|
192
216
|
}
|
|
217
|
+
function detectGitRepo() {
|
|
218
|
+
try {
|
|
219
|
+
const cwd = process.cwd();
|
|
220
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
221
|
+
cwd,
|
|
222
|
+
encoding: "utf8",
|
|
223
|
+
timeout: 5000,
|
|
224
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
225
|
+
}).trim();
|
|
226
|
+
if (!gitRoot)
|
|
227
|
+
return undefined;
|
|
228
|
+
const normalizedPath = path.normalize(gitRoot);
|
|
229
|
+
const name = path.basename(normalizedPath);
|
|
230
|
+
return { name, path: normalizedPath };
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function detectGitRepoSessions() {
|
|
237
|
+
const repo = detectGitRepo();
|
|
238
|
+
if (!repo)
|
|
239
|
+
return [];
|
|
240
|
+
return [
|
|
241
|
+
{
|
|
242
|
+
name: repo.name,
|
|
243
|
+
path: repo.path,
|
|
244
|
+
detectedAt: new Date().toISOString(),
|
|
245
|
+
source: "git",
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
}
|
|
193
249
|
export async function detectSessions() {
|
|
194
250
|
try {
|
|
195
|
-
// Detect sessions from
|
|
196
|
-
const [claudeSessions, opencodeSessions] = await Promise.all([
|
|
251
|
+
// Detect sessions from all sources: Git (CWD), Claude, and OpenCode
|
|
252
|
+
const [gitSessions, claudeSessions, opencodeSessions] = await Promise.all([
|
|
253
|
+
detectGitRepoSessions(),
|
|
197
254
|
detectClaudeSessions(),
|
|
198
255
|
detectOpencodeSessions(),
|
|
199
256
|
]);
|
|
200
257
|
// Merge and deduplicate by name
|
|
258
|
+
// Priority: Git > Claude > OpenCode
|
|
201
259
|
const sessionMap = new Map();
|
|
260
|
+
for (const session of opencodeSessions) {
|
|
261
|
+
sessionMap.set(session.name, session);
|
|
262
|
+
}
|
|
202
263
|
for (const session of claudeSessions) {
|
|
203
264
|
sessionMap.set(session.name, session);
|
|
204
265
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
266
|
+
// Git sessions take highest priority - they reflect the actual working directory
|
|
267
|
+
for (const session of gitSessions) {
|
|
268
|
+
sessionMap.set(session.name, session);
|
|
209
269
|
}
|
|
210
270
|
const sessions = Array.from(sessionMap.values());
|
|
211
271
|
if (sessions.length === 0)
|
|
@@ -234,6 +294,12 @@ export async function detectSessions() {
|
|
|
234
294
|
}
|
|
235
295
|
export async function detectLatestSession() {
|
|
236
296
|
const sessions = await detectSessions();
|
|
297
|
+
// If we're in a git repo, prefer the git-detected session
|
|
298
|
+
// even if it's not the most recently modified
|
|
299
|
+
const gitSession = sessions.find((s) => s.source === "git");
|
|
300
|
+
if (gitSession) {
|
|
301
|
+
return gitSession;
|
|
302
|
+
}
|
|
237
303
|
return sessions[0];
|
|
238
304
|
}
|
|
239
305
|
export async function listAllSessions() {
|
|
@@ -247,13 +313,13 @@ export async function listAllSessions() {
|
|
|
247
313
|
for (const entry of entries) {
|
|
248
314
|
if (entry.isDirectory() &&
|
|
249
315
|
!detectedNames.has(entry.name) &&
|
|
250
|
-
entry.name !==
|
|
316
|
+
entry.name !== "independent" &&
|
|
251
317
|
!ALL_INTERNAL_NAMES.has(entry.name)) {
|
|
252
318
|
storedSessions.push({
|
|
253
319
|
name: entry.name,
|
|
254
320
|
path: getSessionDir(entry.name),
|
|
255
321
|
detectedAt: new Date().toISOString(),
|
|
256
|
-
source:
|
|
322
|
+
source: "manual",
|
|
257
323
|
});
|
|
258
324
|
}
|
|
259
325
|
}
|
|
@@ -263,10 +329,10 @@ export async function listAllSessions() {
|
|
|
263
329
|
...detected,
|
|
264
330
|
...storedSessions,
|
|
265
331
|
{
|
|
266
|
-
name:
|
|
267
|
-
path: getSessionDir(
|
|
332
|
+
name: "independent",
|
|
333
|
+
path: getSessionDir("independent"),
|
|
268
334
|
detectedAt: new Date().toISOString(),
|
|
269
|
-
source:
|
|
335
|
+
source: "independent",
|
|
270
336
|
},
|
|
271
337
|
];
|
|
272
338
|
return allSessions;
|
|
@@ -283,15 +349,15 @@ export async function getCurrentSessionName() {
|
|
|
283
349
|
return latest.name;
|
|
284
350
|
}
|
|
285
351
|
// Fallback to independent
|
|
286
|
-
await setActiveSession(
|
|
287
|
-
return
|
|
352
|
+
await setActiveSession("independent");
|
|
353
|
+
return "independent";
|
|
288
354
|
}
|
|
289
355
|
// Per-session data operations
|
|
290
356
|
async function readSessionData(sessionName) {
|
|
291
357
|
const dataFile = getSessionDataFile(sessionName);
|
|
292
358
|
ensureDir(getSessionDir(sessionName));
|
|
293
359
|
try {
|
|
294
|
-
const raw = await fs.readFile(dataFile,
|
|
360
|
+
const raw = await fs.readFile(dataFile, "utf-8");
|
|
295
361
|
return JSON.parse(raw);
|
|
296
362
|
}
|
|
297
363
|
catch {
|
|
@@ -304,13 +370,13 @@ async function writeSessionData(sessionName, data) {
|
|
|
304
370
|
const dataFile = getSessionDataFile(sessionName);
|
|
305
371
|
ensureDir(getSessionDir(sessionName));
|
|
306
372
|
data.updatedAt = new Date().toISOString();
|
|
307
|
-
const tempFile = dataFile +
|
|
308
|
-
await fs.writeFile(tempFile, JSON.stringify(data, null, 2),
|
|
373
|
+
const tempFile = dataFile + ".tmp";
|
|
374
|
+
await fs.writeFile(tempFile, JSON.stringify(data, null, 2), "utf-8");
|
|
309
375
|
await fs.rename(tempFile, dataFile);
|
|
310
376
|
}
|
|
311
377
|
// Task CRUD (session-aware)
|
|
312
378
|
export async function listTasks(sessionName, filter) {
|
|
313
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
379
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
314
380
|
const data = await readSessionData(targetSession);
|
|
315
381
|
let tasks = data.tasks;
|
|
316
382
|
if (filter?.status) {
|
|
@@ -327,20 +393,20 @@ export async function listTasks(sessionName, filter) {
|
|
|
327
393
|
return tasks;
|
|
328
394
|
}
|
|
329
395
|
export async function getTask(id, sessionName) {
|
|
330
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
396
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
331
397
|
const data = await readSessionData(targetSession);
|
|
332
398
|
return data.tasks.find((t) => t.id === id);
|
|
333
399
|
}
|
|
334
400
|
export async function createTask(input, sessionName) {
|
|
335
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
401
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
336
402
|
const data = await readSessionData(targetSession);
|
|
337
403
|
const task = {
|
|
338
404
|
id: generateId(),
|
|
339
405
|
title: input.title,
|
|
340
406
|
description: input.description,
|
|
341
|
-
status: input.status ??
|
|
407
|
+
status: input.status ?? "todo",
|
|
342
408
|
tags: input.tags ?? [],
|
|
343
|
-
source: input.source ??
|
|
409
|
+
source: input.source ?? "manual",
|
|
344
410
|
createdAt: new Date().toISOString(),
|
|
345
411
|
updatedAt: new Date().toISOString(),
|
|
346
412
|
};
|
|
@@ -349,7 +415,7 @@ export async function createTask(input, sessionName) {
|
|
|
349
415
|
return task;
|
|
350
416
|
}
|
|
351
417
|
export async function updateTask(id, input, sessionName) {
|
|
352
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
418
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
353
419
|
const data = await readSessionData(targetSession);
|
|
354
420
|
const idx = data.tasks.findIndex((t) => t.id === id);
|
|
355
421
|
if (idx === -1)
|
|
@@ -371,7 +437,7 @@ export async function moveTask(id, status, sessionName) {
|
|
|
371
437
|
return updateTask(id, { status }, sessionName);
|
|
372
438
|
}
|
|
373
439
|
export async function deleteTask(id, sessionName) {
|
|
374
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
440
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
375
441
|
const data = await readSessionData(targetSession);
|
|
376
442
|
const initialLen = data.tasks.length;
|
|
377
443
|
data.tasks = data.tasks.filter((t) => t.id !== id);
|
|
@@ -381,7 +447,7 @@ export async function deleteTask(id, sessionName) {
|
|
|
381
447
|
return true;
|
|
382
448
|
}
|
|
383
449
|
export async function appendNote(id, note, sessionName) {
|
|
384
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
450
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
385
451
|
const data = await readSessionData(targetSession);
|
|
386
452
|
const idx = data.tasks.findIndex((t) => t.id === id);
|
|
387
453
|
if (idx === -1)
|
|
@@ -397,7 +463,7 @@ export async function appendNote(id, note, sessionName) {
|
|
|
397
463
|
return task;
|
|
398
464
|
}
|
|
399
465
|
export async function getAllData(sessionName) {
|
|
400
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
466
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
401
467
|
const data = await readSessionData(targetSession);
|
|
402
468
|
// Attach session info
|
|
403
469
|
const sessions = await listAllSessions();
|
|
@@ -408,7 +474,128 @@ export async function getAllData(sessionName) {
|
|
|
408
474
|
return data;
|
|
409
475
|
}
|
|
410
476
|
const STOP_WORDS = new Set([
|
|
411
|
-
|
|
477
|
+
"the",
|
|
478
|
+
"and",
|
|
479
|
+
"for",
|
|
480
|
+
"are",
|
|
481
|
+
"but",
|
|
482
|
+
"not",
|
|
483
|
+
"you",
|
|
484
|
+
"all",
|
|
485
|
+
"can",
|
|
486
|
+
"had",
|
|
487
|
+
"her",
|
|
488
|
+
"was",
|
|
489
|
+
"one",
|
|
490
|
+
"our",
|
|
491
|
+
"out",
|
|
492
|
+
"day",
|
|
493
|
+
"get",
|
|
494
|
+
"has",
|
|
495
|
+
"him",
|
|
496
|
+
"his",
|
|
497
|
+
"how",
|
|
498
|
+
"its",
|
|
499
|
+
"may",
|
|
500
|
+
"new",
|
|
501
|
+
"now",
|
|
502
|
+
"old",
|
|
503
|
+
"see",
|
|
504
|
+
"two",
|
|
505
|
+
"who",
|
|
506
|
+
"boy",
|
|
507
|
+
"did",
|
|
508
|
+
"she",
|
|
509
|
+
"use",
|
|
510
|
+
"her",
|
|
511
|
+
"way",
|
|
512
|
+
"many",
|
|
513
|
+
"oil",
|
|
514
|
+
"sit",
|
|
515
|
+
"set",
|
|
516
|
+
"run",
|
|
517
|
+
"eat",
|
|
518
|
+
"far",
|
|
519
|
+
"sea",
|
|
520
|
+
"eye",
|
|
521
|
+
"ago",
|
|
522
|
+
"off",
|
|
523
|
+
"too",
|
|
524
|
+
"any",
|
|
525
|
+
"say",
|
|
526
|
+
"man",
|
|
527
|
+
"try",
|
|
528
|
+
"ask",
|
|
529
|
+
"end",
|
|
530
|
+
"why",
|
|
531
|
+
"let",
|
|
532
|
+
"put",
|
|
533
|
+
"say",
|
|
534
|
+
"she",
|
|
535
|
+
"try",
|
|
536
|
+
"way",
|
|
537
|
+
"own",
|
|
538
|
+
"say",
|
|
539
|
+
"too",
|
|
540
|
+
"old",
|
|
541
|
+
"tell",
|
|
542
|
+
"very",
|
|
543
|
+
"when",
|
|
544
|
+
"much",
|
|
545
|
+
"would",
|
|
546
|
+
"there",
|
|
547
|
+
"their",
|
|
548
|
+
"what",
|
|
549
|
+
"said",
|
|
550
|
+
"each",
|
|
551
|
+
"which",
|
|
552
|
+
"will",
|
|
553
|
+
"about",
|
|
554
|
+
"could",
|
|
555
|
+
"other",
|
|
556
|
+
"after",
|
|
557
|
+
"first",
|
|
558
|
+
"never",
|
|
559
|
+
"these",
|
|
560
|
+
"think",
|
|
561
|
+
"where",
|
|
562
|
+
"being",
|
|
563
|
+
"every",
|
|
564
|
+
"great",
|
|
565
|
+
"might",
|
|
566
|
+
"shall",
|
|
567
|
+
"still",
|
|
568
|
+
"those",
|
|
569
|
+
"while",
|
|
570
|
+
"this",
|
|
571
|
+
"that",
|
|
572
|
+
"with",
|
|
573
|
+
"have",
|
|
574
|
+
"from",
|
|
575
|
+
"they",
|
|
576
|
+
"know",
|
|
577
|
+
"want",
|
|
578
|
+
"been",
|
|
579
|
+
"good",
|
|
580
|
+
"much",
|
|
581
|
+
"some",
|
|
582
|
+
"time",
|
|
583
|
+
"very",
|
|
584
|
+
"when",
|
|
585
|
+
"come",
|
|
586
|
+
"here",
|
|
587
|
+
"just",
|
|
588
|
+
"like",
|
|
589
|
+
"long",
|
|
590
|
+
"make",
|
|
591
|
+
"many",
|
|
592
|
+
"over",
|
|
593
|
+
"such",
|
|
594
|
+
"take",
|
|
595
|
+
"than",
|
|
596
|
+
"them",
|
|
597
|
+
"well",
|
|
598
|
+
"were",
|
|
412
599
|
]);
|
|
413
600
|
function extractKeywords(text) {
|
|
414
601
|
return text
|
|
@@ -420,9 +607,9 @@ function extractKeywords(text) {
|
|
|
420
607
|
function getTaskFields(task) {
|
|
421
608
|
return {
|
|
422
609
|
title: task.title.toLowerCase(),
|
|
423
|
-
tags: task.tags.join(
|
|
424
|
-
description: (task.description ||
|
|
425
|
-
notes: (task.notes || []).join(
|
|
610
|
+
tags: task.tags.join(" ").toLowerCase(),
|
|
611
|
+
description: (task.description || "").toLowerCase(),
|
|
612
|
+
notes: (task.notes || []).join(" ").toLowerCase(),
|
|
426
613
|
};
|
|
427
614
|
}
|
|
428
615
|
function scoreTask(task, queryWords) {
|
|
@@ -435,7 +622,7 @@ function scoreTask(task, queryWords) {
|
|
|
435
622
|
if (fields.title.includes(word)) {
|
|
436
623
|
score += 5;
|
|
437
624
|
signals++;
|
|
438
|
-
reasons.push({ field:
|
|
625
|
+
reasons.push({ field: "title", matched: word });
|
|
439
626
|
break; // Only count title once per word
|
|
440
627
|
}
|
|
441
628
|
}
|
|
@@ -444,7 +631,7 @@ function scoreTask(task, queryWords) {
|
|
|
444
631
|
if (fields.tags.includes(word)) {
|
|
445
632
|
score += 4;
|
|
446
633
|
signals++;
|
|
447
|
-
reasons.push({ field:
|
|
634
|
+
reasons.push({ field: "tag", matched: word });
|
|
448
635
|
break;
|
|
449
636
|
}
|
|
450
637
|
}
|
|
@@ -453,7 +640,7 @@ function scoreTask(task, queryWords) {
|
|
|
453
640
|
if (fields.description.includes(word)) {
|
|
454
641
|
score += 2;
|
|
455
642
|
signals++;
|
|
456
|
-
reasons.push({ field:
|
|
643
|
+
reasons.push({ field: "description", matched: word });
|
|
457
644
|
break;
|
|
458
645
|
}
|
|
459
646
|
}
|
|
@@ -462,7 +649,7 @@ function scoreTask(task, queryWords) {
|
|
|
462
649
|
if (fields.notes.includes(word)) {
|
|
463
650
|
score += 1;
|
|
464
651
|
signals++;
|
|
465
|
-
reasons.push({ field:
|
|
652
|
+
reasons.push({ field: "notes", matched: word });
|
|
466
653
|
break;
|
|
467
654
|
}
|
|
468
655
|
}
|
|
@@ -476,25 +663,25 @@ function scoreTask(task, queryWords) {
|
|
|
476
663
|
score += 1;
|
|
477
664
|
}
|
|
478
665
|
// Status boost
|
|
479
|
-
if (task.status ===
|
|
666
|
+
if (task.status === "in-progress") {
|
|
480
667
|
score += 2;
|
|
481
668
|
signals++;
|
|
482
669
|
}
|
|
483
|
-
else if (task.status ===
|
|
670
|
+
else if (task.status === "blocked") {
|
|
484
671
|
score += 1;
|
|
485
672
|
signals++;
|
|
486
673
|
}
|
|
487
674
|
return { task, score, signals, reasons };
|
|
488
675
|
}
|
|
489
676
|
export async function recallTasks(context, limit = 5, sessionName) {
|
|
490
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
677
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
491
678
|
const data = await readSessionData(targetSession);
|
|
492
679
|
if (data.tasks.length === 0) {
|
|
493
680
|
return {
|
|
494
681
|
relevant: [],
|
|
495
682
|
reasons: new Map(),
|
|
496
683
|
summary: { active: 0, done: 0, blocked: 0, total: 0 },
|
|
497
|
-
quality:
|
|
684
|
+
quality: "poor",
|
|
498
685
|
};
|
|
499
686
|
}
|
|
500
687
|
const queryWords = extractKeywords(context);
|
|
@@ -507,19 +694,19 @@ export async function recallTasks(context, limit = 5, sessionName) {
|
|
|
507
694
|
let quality;
|
|
508
695
|
let actualLimit;
|
|
509
696
|
if (maxScore >= 10) {
|
|
510
|
-
quality =
|
|
697
|
+
quality = "excellent";
|
|
511
698
|
actualLimit = Math.min(limit, 2);
|
|
512
699
|
}
|
|
513
700
|
else if (maxScore >= 7) {
|
|
514
|
-
quality =
|
|
701
|
+
quality = "good";
|
|
515
702
|
actualLimit = Math.min(limit, 3);
|
|
516
703
|
}
|
|
517
704
|
else if (maxScore >= 5) {
|
|
518
|
-
quality =
|
|
705
|
+
quality = "fair";
|
|
519
706
|
actualLimit = 1;
|
|
520
707
|
}
|
|
521
708
|
else {
|
|
522
|
-
quality =
|
|
709
|
+
quality = "poor";
|
|
523
710
|
actualLimit = 0;
|
|
524
711
|
}
|
|
525
712
|
// Filter by minimum threshold and signal count
|
|
@@ -530,9 +717,9 @@ export async function recallTasks(context, limit = 5, sessionName) {
|
|
|
530
717
|
const reasons = new Map(filtered.map((s) => [s.task.id, s.reasons]));
|
|
531
718
|
// Calculate summary
|
|
532
719
|
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 ===
|
|
720
|
+
active: data.tasks.filter((t) => t.status === "in-progress").length,
|
|
721
|
+
done: data.tasks.filter((t) => t.status === "done").length,
|
|
722
|
+
blocked: data.tasks.filter((t) => t.status === "blocked").length,
|
|
536
723
|
total: data.tasks.length,
|
|
537
724
|
};
|
|
538
725
|
return { relevant, reasons, summary, quality };
|