@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.
@@ -1,13 +1,13 @@
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');
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), 'tasks.json');
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, 'utf-8');
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), 'utf-8');
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, { withFileTypes: true });
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: 'claude',
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
- 'agents', 'commands', 'modes', 'plugins', 'skills', 'tools', 'themes', 'node_modules'
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
- 'bin', 'log', 'repos', 'snapshot', 'storage', 'tool-output'
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, 'opencode.db');
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, 'utf-8');
113
+ await fs.writeFile(tempQueryFile, query, "utf-8");
99
114
  let result;
100
115
  try {
101
116
  result = execSync(`sqlite3 "${dbPath}" < "${tempQueryFile}"`, {
102
- encoding: 'utf8',
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('\n')) {
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: 'opencode',
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, { withFileTypes: true });
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: 'opencode',
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, { withFileTypes: true });
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: 'opencode',
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 both Claude and opencode
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
- for (const session of opencodeSessions) {
206
- if (!sessionMap.has(session.name)) {
207
- sessionMap.set(session.name, session);
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 !== 'independent' &&
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: 'manual',
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: 'independent',
267
- path: getSessionDir('independent'),
327
+ name: "independent",
328
+ path: getSessionDir("independent"),
268
329
  detectedAt: new Date().toISOString(),
269
- source: 'independent',
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('independent');
287
- return 'independent';
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, 'utf-8');
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 + '.tmp';
308
- await fs.writeFile(tempFile, JSON.stringify(data, null, 2), 'utf-8');
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 ?? 'todo',
402
+ status: input.status ?? "todo",
342
403
  tags: input.tags ?? [],
343
- source: input.source ?? 'manual',
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
- 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'has', 'him', 'his', 'how', 'its', 'may', 'new', 'now', 'old', 'see', 'two', 'who', 'boy', 'did', 'she', 'use', 'her', 'way', 'many', 'oil', 'sit', 'set', 'run', 'eat', 'far', 'sea', 'eye', 'ago', 'off', 'too', 'any', 'say', 'man', 'try', 'ask', 'end', 'why', 'let', 'put', 'say', 'she', 'try', 'way', 'own', 'say', 'too', 'old', 'tell', 'very', 'when', 'much', 'would', 'there', 'their', 'what', 'said', 'each', 'which', 'will', 'about', 'could', 'other', 'after', 'first', 'never', 'these', 'think', 'where', 'being', 'every', 'great', 'might', 'shall', 'still', 'those', 'while', 'this', 'that', 'with', 'have', 'from', 'they', 'know', 'want', 'been', 'good', 'much', 'some', 'time', 'very', 'when', 'come', 'here', 'just', 'like', 'long', 'make', 'many', 'over', 'such', 'take', 'than', 'them', 'well', 'were'
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(' ').toLowerCase(),
424
- description: (task.description || '').toLowerCase(),
425
- notes: (task.notes || []).join(' ').toLowerCase(),
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: 'title', matched: word });
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: 'tag', matched: word });
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: 'description', matched: word });
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: 'notes', matched: word });
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 === 'in-progress') {
661
+ if (task.status === "in-progress") {
480
662
  score += 2;
481
663
  signals++;
482
664
  }
483
- else if (task.status === 'blocked') {
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: 'poor',
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 = 'excellent';
692
+ quality = "excellent";
511
693
  actualLimit = Math.min(limit, 2);
512
694
  }
513
695
  else if (maxScore >= 7) {
514
- quality = 'good';
696
+ quality = "good";
515
697
  actualLimit = Math.min(limit, 3);
516
698
  }
517
699
  else if (maxScore >= 5) {
518
- quality = 'fair';
700
+ quality = "fair";
519
701
  actualLimit = 1;
520
702
  }
521
703
  else {
522
- quality = 'poor';
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 === 'in-progress').length,
534
- done: data.tasks.filter((t) => t.status === 'done').length,
535
- blocked: data.tasks.filter((t) => t.status === 'blocked').length,
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 };