@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.
@@ -1,12 +1,13 @@
1
- import { promises as fs, existsSync, mkdirSync } from 'fs';
2
- import * as path from 'path';
3
- import * as os from 'os';
4
- const KANBAN_DIR = path.join(os.homedir(), '.kanban');
5
- const SESSIONS_DIR = path.join(KANBAN_DIR, 'sessions');
6
- const ACTIVE_SESSION_FILE = path.join(KANBAN_DIR, 'active-session.json');
7
- const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
8
- const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
9
- 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");
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), 'tasks.json');
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, 'utf-8');
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), 'utf-8');
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, { withFileTypes: true });
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: 'claude',
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
- '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",
69
80
  ]);
70
81
  const OPENCODE_DATA_EXCLUDES = new Set([
71
- 'bin', 'log', 'repos', 'snapshot', 'storage', 'tool-output'
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, { withFileTypes: true });
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: 'opencode',
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, { withFileTypes: true });
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: 'opencode',
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 both Claude and opencode
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
- for (const session of opencodeSessions) {
136
- if (!sessionMap.has(session.name)) {
137
- sessionMap.set(session.name, session);
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 !== 'independent' &&
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: 'manual',
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: 'independent',
197
- path: getSessionDir('independent'),
327
+ name: "independent",
328
+ path: getSessionDir("independent"),
198
329
  detectedAt: new Date().toISOString(),
199
- source: 'independent',
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('independent');
217
- return 'independent';
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, 'utf-8');
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 + '.tmp';
238
- 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");
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 ?? 'todo',
402
+ status: input.status ?? "todo",
272
403
  tags: input.tags ?? [],
273
- source: input.source ?? 'manual',
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
- '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",
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(' ').toLowerCase(),
354
- description: (task.description || '').toLowerCase(),
355
- notes: (task.notes || []).join(' ').toLowerCase(),
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: 'title', matched: word });
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: 'tag', matched: word });
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: 'description', matched: word });
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: 'notes', matched: word });
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 === 'in-progress') {
661
+ if (task.status === "in-progress") {
410
662
  score += 2;
411
663
  signals++;
412
664
  }
413
- else if (task.status === 'blocked') {
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: 'poor',
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 = 'excellent';
692
+ quality = "excellent";
441
693
  actualLimit = Math.min(limit, 2);
442
694
  }
443
695
  else if (maxScore >= 7) {
444
- quality = 'good';
696
+ quality = "good";
445
697
  actualLimit = Math.min(limit, 3);
446
698
  }
447
699
  else if (maxScore >= 5) {
448
- quality = 'fair';
700
+ quality = "fair";
449
701
  actualLimit = 1;
450
702
  }
451
703
  else {
452
- quality = 'poor';
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 === 'in-progress').length,
464
- done: data.tasks.filter((t) => t.status === 'done').length,
465
- 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,
466
718
  total: data.tasks.length,
467
719
  };
468
720
  return { relevant, reasons, summary, quality };