@cwim/kanban 1.1.19 → 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 +317 -65
- 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,12 +1,13 @@
|
|
|
1
|
-
import { promises as fs, existsSync, mkdirSync } from
|
|
2
|
-
import * as path from
|
|
3
|
-
import * as os from
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
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");
|
|
10
11
|
function getDefaultData() {
|
|
11
12
|
return {
|
|
12
13
|
version: 1,
|
|
@@ -23,13 +24,13 @@ function getSessionDir(sessionName) {
|
|
|
23
24
|
return path.join(SESSIONS_DIR, sessionName);
|
|
24
25
|
}
|
|
25
26
|
function getSessionDataFile(sessionName) {
|
|
26
|
-
return path.join(getSessionDir(sessionName),
|
|
27
|
+
return path.join(getSessionDir(sessionName), "tasks.json");
|
|
27
28
|
}
|
|
28
29
|
export async function getActiveSession() {
|
|
29
30
|
try {
|
|
30
31
|
if (!existsSync(ACTIVE_SESSION_FILE))
|
|
31
32
|
return undefined;
|
|
32
|
-
const raw = await fs.readFile(ACTIVE_SESSION_FILE,
|
|
33
|
+
const raw = await fs.readFile(ACTIVE_SESSION_FILE, "utf-8");
|
|
33
34
|
const active = JSON.parse(raw);
|
|
34
35
|
return active.name;
|
|
35
36
|
}
|
|
@@ -43,21 +44,23 @@ export async function setActiveSession(sessionName) {
|
|
|
43
44
|
name: sessionName,
|
|
44
45
|
setAt: new Date().toISOString(),
|
|
45
46
|
};
|
|
46
|
-
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");
|
|
47
48
|
}
|
|
48
49
|
// Session detection
|
|
49
50
|
async function detectClaudeSessions() {
|
|
50
51
|
try {
|
|
51
52
|
if (!existsSync(CLAUDE_PROJECTS_DIR))
|
|
52
53
|
return [];
|
|
53
|
-
const entries = await fs.readdir(CLAUDE_PROJECTS_DIR, {
|
|
54
|
+
const entries = await fs.readdir(CLAUDE_PROJECTS_DIR, {
|
|
55
|
+
withFileTypes: true,
|
|
56
|
+
});
|
|
54
57
|
return entries
|
|
55
58
|
.filter((e) => e.isDirectory())
|
|
56
59
|
.map((e) => ({
|
|
57
60
|
name: e.name,
|
|
58
61
|
path: path.join(CLAUDE_PROJECTS_DIR, e.name),
|
|
59
62
|
detectedAt: new Date().toISOString(),
|
|
60
|
-
source:
|
|
63
|
+
source: "claude",
|
|
61
64
|
}));
|
|
62
65
|
}
|
|
63
66
|
catch {
|
|
@@ -65,28 +68,112 @@ async function detectClaudeSessions() {
|
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
const OPENCODE_CONFIG_EXCLUDES = new Set([
|
|
68
|
-
|
|
71
|
+
"agents",
|
|
72
|
+
"command",
|
|
73
|
+
"commands",
|
|
74
|
+
"modes",
|
|
75
|
+
"plugins",
|
|
76
|
+
"skills",
|
|
77
|
+
"tools",
|
|
78
|
+
"themes",
|
|
79
|
+
"node_modules",
|
|
69
80
|
]);
|
|
70
81
|
const OPENCODE_DATA_EXCLUDES = new Set([
|
|
71
|
-
|
|
82
|
+
"bin",
|
|
83
|
+
"log",
|
|
84
|
+
"repos",
|
|
85
|
+
"snapshot",
|
|
86
|
+
"storage",
|
|
87
|
+
"tool-output",
|
|
72
88
|
]);
|
|
73
89
|
const ALL_INTERNAL_NAMES = new Set([
|
|
74
90
|
...OPENCODE_CONFIG_EXCLUDES,
|
|
75
91
|
...OPENCODE_DATA_EXCLUDES,
|
|
76
92
|
]);
|
|
77
93
|
async function detectOpencodeSessions() {
|
|
94
|
+
// First, try to read from the OpenCode SQLite database (most accurate)
|
|
95
|
+
const dbSessions = await queryOpencodeDatabase();
|
|
96
|
+
if (dbSessions.length > 0) {
|
|
97
|
+
return dbSessions;
|
|
98
|
+
}
|
|
99
|
+
// Fallback: directory-based detection
|
|
100
|
+
return detectOpencodeSessionsFromDirectories();
|
|
101
|
+
}
|
|
102
|
+
async function queryOpencodeDatabase() {
|
|
103
|
+
const dbPath = path.join(OPENCODE_DATA_DIR, "opencode.db");
|
|
104
|
+
if (!existsSync(dbPath)) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
// Query projects from the SQLite database
|
|
109
|
+
// worktree is the actual project path, name may be empty
|
|
110
|
+
const query = `SELECT id, worktree, name, time_updated FROM project WHERE worktree IS NOT NULL AND worktree != '/' ORDER BY time_updated DESC;`;
|
|
111
|
+
// Write query to temp file to avoid shell escaping issues with paths
|
|
112
|
+
const tempQueryFile = path.join(os.tmpdir(), `opencode-query-${Date.now()}.sql`);
|
|
113
|
+
await fs.writeFile(tempQueryFile, query, "utf-8");
|
|
114
|
+
let result;
|
|
115
|
+
try {
|
|
116
|
+
result = execSync(`sqlite3 "${dbPath}" < "${tempQueryFile}"`, {
|
|
117
|
+
encoding: "utf8",
|
|
118
|
+
timeout: 10000,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
// Clean up temp file
|
|
123
|
+
try {
|
|
124
|
+
await fs.unlink(tempQueryFile);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Ignore cleanup errors
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const sessions = [];
|
|
131
|
+
const seenPaths = new Set();
|
|
132
|
+
for (const line of result.trim().split("\n")) {
|
|
133
|
+
if (!line.trim())
|
|
134
|
+
continue;
|
|
135
|
+
const parts = line.split("|");
|
|
136
|
+
if (parts.length < 4)
|
|
137
|
+
continue;
|
|
138
|
+
const [, worktree, name, timeUpdated] = parts;
|
|
139
|
+
if (!worktree || worktree === "/")
|
|
140
|
+
continue;
|
|
141
|
+
// Normalize path for deduplication
|
|
142
|
+
const normalizedPath = path.normalize(worktree);
|
|
143
|
+
if (seenPaths.has(normalizedPath))
|
|
144
|
+
continue;
|
|
145
|
+
seenPaths.add(normalizedPath);
|
|
146
|
+
// Use project name if available, otherwise basename of path
|
|
147
|
+
const sessionName = name?.trim() || path.basename(normalizedPath);
|
|
148
|
+
sessions.push({
|
|
149
|
+
name: sessionName,
|
|
150
|
+
path: normalizedPath,
|
|
151
|
+
detectedAt: new Date(parseInt(timeUpdated)).toISOString(),
|
|
152
|
+
source: "opencode",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return sessions;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// If SQLite query fails, return empty to trigger fallback
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function detectOpencodeSessionsFromDirectories() {
|
|
78
163
|
const sessions = [];
|
|
79
164
|
// Check ~/.config/opencode/ for project directories
|
|
80
165
|
try {
|
|
81
166
|
if (existsSync(OPENCODE_CONFIG_DIR)) {
|
|
82
|
-
const entries = await fs.readdir(OPENCODE_CONFIG_DIR, {
|
|
167
|
+
const entries = await fs.readdir(OPENCODE_CONFIG_DIR, {
|
|
168
|
+
withFileTypes: true,
|
|
169
|
+
});
|
|
83
170
|
for (const entry of entries) {
|
|
84
171
|
if (entry.isDirectory() && !OPENCODE_CONFIG_EXCLUDES.has(entry.name)) {
|
|
85
172
|
sessions.push({
|
|
86
173
|
name: entry.name,
|
|
87
174
|
path: path.join(OPENCODE_CONFIG_DIR, entry.name),
|
|
88
175
|
detectedAt: new Date().toISOString(),
|
|
89
|
-
source:
|
|
176
|
+
source: "opencode",
|
|
90
177
|
});
|
|
91
178
|
}
|
|
92
179
|
}
|
|
@@ -98,17 +185,19 @@ async function detectOpencodeSessions() {
|
|
|
98
185
|
// Check ~/.local/share/opencode/ for session data
|
|
99
186
|
try {
|
|
100
187
|
if (existsSync(OPENCODE_DATA_DIR)) {
|
|
101
|
-
const entries = await fs.readdir(OPENCODE_DATA_DIR, {
|
|
188
|
+
const entries = await fs.readdir(OPENCODE_DATA_DIR, {
|
|
189
|
+
withFileTypes: true,
|
|
190
|
+
});
|
|
102
191
|
for (const entry of entries) {
|
|
103
192
|
if (entry.isDirectory() && !OPENCODE_DATA_EXCLUDES.has(entry.name)) {
|
|
104
193
|
const sessionName = entry.name;
|
|
105
194
|
// Only add if not already found
|
|
106
|
-
if (!sessions.some(s => s.name === sessionName)) {
|
|
195
|
+
if (!sessions.some((s) => s.name === sessionName)) {
|
|
107
196
|
sessions.push({
|
|
108
197
|
name: sessionName,
|
|
109
198
|
path: path.join(OPENCODE_DATA_DIR, entry.name),
|
|
110
199
|
detectedAt: new Date().toISOString(),
|
|
111
|
-
source:
|
|
200
|
+
source: "opencode",
|
|
112
201
|
});
|
|
113
202
|
}
|
|
114
203
|
}
|
|
@@ -120,22 +209,58 @@ async function detectOpencodeSessions() {
|
|
|
120
209
|
}
|
|
121
210
|
return sessions;
|
|
122
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
|
+
}
|
|
123
244
|
export async function detectSessions() {
|
|
124
245
|
try {
|
|
125
|
-
// Detect sessions from
|
|
126
|
-
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(),
|
|
127
249
|
detectClaudeSessions(),
|
|
128
250
|
detectOpencodeSessions(),
|
|
129
251
|
]);
|
|
130
252
|
// Merge and deduplicate by name
|
|
253
|
+
// Priority: Git > Claude > OpenCode
|
|
131
254
|
const sessionMap = new Map();
|
|
255
|
+
for (const session of opencodeSessions) {
|
|
256
|
+
sessionMap.set(session.name, session);
|
|
257
|
+
}
|
|
132
258
|
for (const session of claudeSessions) {
|
|
133
259
|
sessionMap.set(session.name, session);
|
|
134
260
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
261
|
+
// Git sessions take highest priority - they reflect the actual working directory
|
|
262
|
+
for (const session of gitSessions) {
|
|
263
|
+
sessionMap.set(session.name, session);
|
|
139
264
|
}
|
|
140
265
|
const sessions = Array.from(sessionMap.values());
|
|
141
266
|
if (sessions.length === 0)
|
|
@@ -164,6 +289,12 @@ export async function detectSessions() {
|
|
|
164
289
|
}
|
|
165
290
|
export async function detectLatestSession() {
|
|
166
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
|
+
}
|
|
167
298
|
return sessions[0];
|
|
168
299
|
}
|
|
169
300
|
export async function listAllSessions() {
|
|
@@ -177,13 +308,13 @@ export async function listAllSessions() {
|
|
|
177
308
|
for (const entry of entries) {
|
|
178
309
|
if (entry.isDirectory() &&
|
|
179
310
|
!detectedNames.has(entry.name) &&
|
|
180
|
-
entry.name !==
|
|
311
|
+
entry.name !== "independent" &&
|
|
181
312
|
!ALL_INTERNAL_NAMES.has(entry.name)) {
|
|
182
313
|
storedSessions.push({
|
|
183
314
|
name: entry.name,
|
|
184
315
|
path: getSessionDir(entry.name),
|
|
185
316
|
detectedAt: new Date().toISOString(),
|
|
186
|
-
source:
|
|
317
|
+
source: "manual",
|
|
187
318
|
});
|
|
188
319
|
}
|
|
189
320
|
}
|
|
@@ -193,10 +324,10 @@ export async function listAllSessions() {
|
|
|
193
324
|
...detected,
|
|
194
325
|
...storedSessions,
|
|
195
326
|
{
|
|
196
|
-
name:
|
|
197
|
-
path: getSessionDir(
|
|
327
|
+
name: "independent",
|
|
328
|
+
path: getSessionDir("independent"),
|
|
198
329
|
detectedAt: new Date().toISOString(),
|
|
199
|
-
source:
|
|
330
|
+
source: "independent",
|
|
200
331
|
},
|
|
201
332
|
];
|
|
202
333
|
return allSessions;
|
|
@@ -213,15 +344,15 @@ export async function getCurrentSessionName() {
|
|
|
213
344
|
return latest.name;
|
|
214
345
|
}
|
|
215
346
|
// Fallback to independent
|
|
216
|
-
await setActiveSession(
|
|
217
|
-
return
|
|
347
|
+
await setActiveSession("independent");
|
|
348
|
+
return "independent";
|
|
218
349
|
}
|
|
219
350
|
// Per-session data operations
|
|
220
351
|
async function readSessionData(sessionName) {
|
|
221
352
|
const dataFile = getSessionDataFile(sessionName);
|
|
222
353
|
ensureDir(getSessionDir(sessionName));
|
|
223
354
|
try {
|
|
224
|
-
const raw = await fs.readFile(dataFile,
|
|
355
|
+
const raw = await fs.readFile(dataFile, "utf-8");
|
|
225
356
|
return JSON.parse(raw);
|
|
226
357
|
}
|
|
227
358
|
catch {
|
|
@@ -234,13 +365,13 @@ async function writeSessionData(sessionName, data) {
|
|
|
234
365
|
const dataFile = getSessionDataFile(sessionName);
|
|
235
366
|
ensureDir(getSessionDir(sessionName));
|
|
236
367
|
data.updatedAt = new Date().toISOString();
|
|
237
|
-
const tempFile = dataFile +
|
|
238
|
-
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");
|
|
239
370
|
await fs.rename(tempFile, dataFile);
|
|
240
371
|
}
|
|
241
372
|
// Task CRUD (session-aware)
|
|
242
373
|
export async function listTasks(sessionName, filter) {
|
|
243
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
374
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
244
375
|
const data = await readSessionData(targetSession);
|
|
245
376
|
let tasks = data.tasks;
|
|
246
377
|
if (filter?.status) {
|
|
@@ -257,20 +388,20 @@ export async function listTasks(sessionName, filter) {
|
|
|
257
388
|
return tasks;
|
|
258
389
|
}
|
|
259
390
|
export async function getTask(id, sessionName) {
|
|
260
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
391
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
261
392
|
const data = await readSessionData(targetSession);
|
|
262
393
|
return data.tasks.find((t) => t.id === id);
|
|
263
394
|
}
|
|
264
395
|
export async function createTask(input, sessionName) {
|
|
265
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
396
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
266
397
|
const data = await readSessionData(targetSession);
|
|
267
398
|
const task = {
|
|
268
399
|
id: generateId(),
|
|
269
400
|
title: input.title,
|
|
270
401
|
description: input.description,
|
|
271
|
-
status: input.status ??
|
|
402
|
+
status: input.status ?? "todo",
|
|
272
403
|
tags: input.tags ?? [],
|
|
273
|
-
source: input.source ??
|
|
404
|
+
source: input.source ?? "manual",
|
|
274
405
|
createdAt: new Date().toISOString(),
|
|
275
406
|
updatedAt: new Date().toISOString(),
|
|
276
407
|
};
|
|
@@ -279,7 +410,7 @@ export async function createTask(input, sessionName) {
|
|
|
279
410
|
return task;
|
|
280
411
|
}
|
|
281
412
|
export async function updateTask(id, input, sessionName) {
|
|
282
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
413
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
283
414
|
const data = await readSessionData(targetSession);
|
|
284
415
|
const idx = data.tasks.findIndex((t) => t.id === id);
|
|
285
416
|
if (idx === -1)
|
|
@@ -301,7 +432,7 @@ export async function moveTask(id, status, sessionName) {
|
|
|
301
432
|
return updateTask(id, { status }, sessionName);
|
|
302
433
|
}
|
|
303
434
|
export async function deleteTask(id, sessionName) {
|
|
304
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
435
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
305
436
|
const data = await readSessionData(targetSession);
|
|
306
437
|
const initialLen = data.tasks.length;
|
|
307
438
|
data.tasks = data.tasks.filter((t) => t.id !== id);
|
|
@@ -311,7 +442,7 @@ export async function deleteTask(id, sessionName) {
|
|
|
311
442
|
return true;
|
|
312
443
|
}
|
|
313
444
|
export async function appendNote(id, note, sessionName) {
|
|
314
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
445
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
315
446
|
const data = await readSessionData(targetSession);
|
|
316
447
|
const idx = data.tasks.findIndex((t) => t.id === id);
|
|
317
448
|
if (idx === -1)
|
|
@@ -327,7 +458,7 @@ export async function appendNote(id, note, sessionName) {
|
|
|
327
458
|
return task;
|
|
328
459
|
}
|
|
329
460
|
export async function getAllData(sessionName) {
|
|
330
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
461
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
331
462
|
const data = await readSessionData(targetSession);
|
|
332
463
|
// Attach session info
|
|
333
464
|
const sessions = await listAllSessions();
|
|
@@ -338,7 +469,128 @@ export async function getAllData(sessionName) {
|
|
|
338
469
|
return data;
|
|
339
470
|
}
|
|
340
471
|
const STOP_WORDS = new Set([
|
|
341
|
-
|
|
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",
|
|
342
594
|
]);
|
|
343
595
|
function extractKeywords(text) {
|
|
344
596
|
return text
|
|
@@ -350,9 +602,9 @@ function extractKeywords(text) {
|
|
|
350
602
|
function getTaskFields(task) {
|
|
351
603
|
return {
|
|
352
604
|
title: task.title.toLowerCase(),
|
|
353
|
-
tags: task.tags.join(
|
|
354
|
-
description: (task.description ||
|
|
355
|
-
notes: (task.notes || []).join(
|
|
605
|
+
tags: task.tags.join(" ").toLowerCase(),
|
|
606
|
+
description: (task.description || "").toLowerCase(),
|
|
607
|
+
notes: (task.notes || []).join(" ").toLowerCase(),
|
|
356
608
|
};
|
|
357
609
|
}
|
|
358
610
|
function scoreTask(task, queryWords) {
|
|
@@ -365,7 +617,7 @@ function scoreTask(task, queryWords) {
|
|
|
365
617
|
if (fields.title.includes(word)) {
|
|
366
618
|
score += 5;
|
|
367
619
|
signals++;
|
|
368
|
-
reasons.push({ field:
|
|
620
|
+
reasons.push({ field: "title", matched: word });
|
|
369
621
|
break; // Only count title once per word
|
|
370
622
|
}
|
|
371
623
|
}
|
|
@@ -374,7 +626,7 @@ function scoreTask(task, queryWords) {
|
|
|
374
626
|
if (fields.tags.includes(word)) {
|
|
375
627
|
score += 4;
|
|
376
628
|
signals++;
|
|
377
|
-
reasons.push({ field:
|
|
629
|
+
reasons.push({ field: "tag", matched: word });
|
|
378
630
|
break;
|
|
379
631
|
}
|
|
380
632
|
}
|
|
@@ -383,7 +635,7 @@ function scoreTask(task, queryWords) {
|
|
|
383
635
|
if (fields.description.includes(word)) {
|
|
384
636
|
score += 2;
|
|
385
637
|
signals++;
|
|
386
|
-
reasons.push({ field:
|
|
638
|
+
reasons.push({ field: "description", matched: word });
|
|
387
639
|
break;
|
|
388
640
|
}
|
|
389
641
|
}
|
|
@@ -392,7 +644,7 @@ function scoreTask(task, queryWords) {
|
|
|
392
644
|
if (fields.notes.includes(word)) {
|
|
393
645
|
score += 1;
|
|
394
646
|
signals++;
|
|
395
|
-
reasons.push({ field:
|
|
647
|
+
reasons.push({ field: "notes", matched: word });
|
|
396
648
|
break;
|
|
397
649
|
}
|
|
398
650
|
}
|
|
@@ -406,25 +658,25 @@ function scoreTask(task, queryWords) {
|
|
|
406
658
|
score += 1;
|
|
407
659
|
}
|
|
408
660
|
// Status boost
|
|
409
|
-
if (task.status ===
|
|
661
|
+
if (task.status === "in-progress") {
|
|
410
662
|
score += 2;
|
|
411
663
|
signals++;
|
|
412
664
|
}
|
|
413
|
-
else if (task.status ===
|
|
665
|
+
else if (task.status === "blocked") {
|
|
414
666
|
score += 1;
|
|
415
667
|
signals++;
|
|
416
668
|
}
|
|
417
669
|
return { task, score, signals, reasons };
|
|
418
670
|
}
|
|
419
671
|
export async function recallTasks(context, limit = 5, sessionName) {
|
|
420
|
-
const targetSession = sessionName ?? await getCurrentSessionName();
|
|
672
|
+
const targetSession = sessionName ?? (await getCurrentSessionName());
|
|
421
673
|
const data = await readSessionData(targetSession);
|
|
422
674
|
if (data.tasks.length === 0) {
|
|
423
675
|
return {
|
|
424
676
|
relevant: [],
|
|
425
677
|
reasons: new Map(),
|
|
426
678
|
summary: { active: 0, done: 0, blocked: 0, total: 0 },
|
|
427
|
-
quality:
|
|
679
|
+
quality: "poor",
|
|
428
680
|
};
|
|
429
681
|
}
|
|
430
682
|
const queryWords = extractKeywords(context);
|
|
@@ -437,19 +689,19 @@ export async function recallTasks(context, limit = 5, sessionName) {
|
|
|
437
689
|
let quality;
|
|
438
690
|
let actualLimit;
|
|
439
691
|
if (maxScore >= 10) {
|
|
440
|
-
quality =
|
|
692
|
+
quality = "excellent";
|
|
441
693
|
actualLimit = Math.min(limit, 2);
|
|
442
694
|
}
|
|
443
695
|
else if (maxScore >= 7) {
|
|
444
|
-
quality =
|
|
696
|
+
quality = "good";
|
|
445
697
|
actualLimit = Math.min(limit, 3);
|
|
446
698
|
}
|
|
447
699
|
else if (maxScore >= 5) {
|
|
448
|
-
quality =
|
|
700
|
+
quality = "fair";
|
|
449
701
|
actualLimit = 1;
|
|
450
702
|
}
|
|
451
703
|
else {
|
|
452
|
-
quality =
|
|
704
|
+
quality = "poor";
|
|
453
705
|
actualLimit = 0;
|
|
454
706
|
}
|
|
455
707
|
// Filter by minimum threshold and signal count
|
|
@@ -460,9 +712,9 @@ export async function recallTasks(context, limit = 5, sessionName) {
|
|
|
460
712
|
const reasons = new Map(filtered.map((s) => [s.task.id, s.reasons]));
|
|
461
713
|
// Calculate summary
|
|
462
714
|
const summary = {
|
|
463
|
-
active: data.tasks.filter((t) => t.status ===
|
|
464
|
-
done: data.tasks.filter((t) => t.status ===
|
|
465
|
-
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,
|
|
466
718
|
total: data.tasks.length,
|
|
467
719
|
};
|
|
468
720
|
return { relevant, reasons, summary, quality };
|