sidekick-shared 0.13.5 → 0.13.7
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/dist/index.d.ts +1 -1
- package/dist/providers/detect.js +5 -0
- package/dist/providers/openCode.d.ts +5 -1
- package/dist/providers/openCode.js +81 -43
- package/dist/providers/openCodeDatabase.d.ts +10 -1
- package/dist/providers/openCodeDatabase.js +114 -24
- package/dist/providers/types.d.ts +9 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { readHistory } from './readers/history';
|
|
|
25
25
|
export { readLatestHandoff } from './readers/handoff';
|
|
26
26
|
export { readPlans, getLatestPlan, writePlans, getPlanAnalytics, readClaudeCodePlanFiles } from './readers/plans';
|
|
27
27
|
export type { ReadPlansOptions, PlanAnalytics } from './readers/plans';
|
|
28
|
-
export type { ProviderId, SessionProvider, SessionProviderBase, SessionFileStats, SessionFileInfo, SearchHit, ProjectFolderInfo, SessionReader } from './providers/types';
|
|
28
|
+
export type { ProviderId, SessionProvider, SessionProviderBase, SessionFileStats, SessionFileInfo, SearchHit, ProjectFolderInfo, SessionReader, ProviderRuntimeStatus } from './providers/types';
|
|
29
29
|
export { detectProvider, getAllDetectedProviders } from './providers/detect';
|
|
30
30
|
export { ClaudeCodeProvider } from './providers/claudeCode';
|
|
31
31
|
export { OpenCodeProvider } from './providers/openCode';
|
package/dist/providers/detect.js
CHANGED
|
@@ -47,6 +47,11 @@ function getOpenCodeDataDir() {
|
|
|
47
47
|
const xdg = process.env.XDG_DATA_HOME;
|
|
48
48
|
if (xdg)
|
|
49
49
|
return path.join(xdg, 'opencode');
|
|
50
|
+
if (process.platform === 'darwin')
|
|
51
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'opencode');
|
|
52
|
+
if (process.platform === 'win32') {
|
|
53
|
+
return path.join(process.env.LOCALAPPDATA || process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'opencode');
|
|
54
|
+
}
|
|
50
55
|
return path.join(os.homedir(), '.local', 'share', 'opencode');
|
|
51
56
|
}
|
|
52
57
|
function getCodexHome() {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* @module providers/openCode
|
|
17
17
|
*/
|
|
18
|
-
import type { SessionProviderBase, SessionReader, ProjectFolderInfo, SearchHit, SessionFileStats, ProviderId } from './types';
|
|
18
|
+
import type { SessionProviderBase, SessionReader, ProjectFolderInfo, SearchHit, SessionFileStats, ProviderId, ProviderRuntimeStatus } from './types';
|
|
19
19
|
import type { TokenUsage, SubagentStats, ContextAttribution } from '../types/sessionEvent';
|
|
20
20
|
/**
|
|
21
21
|
* Session provider for OpenCode CLI.
|
|
@@ -28,10 +28,14 @@ export declare class OpenCodeProvider implements SessionProviderBase {
|
|
|
28
28
|
readonly displayName = "OpenCode";
|
|
29
29
|
private db;
|
|
30
30
|
private dbInitialized;
|
|
31
|
+
private dbStatus;
|
|
31
32
|
/** Cache of session metadata populated during listing */
|
|
32
33
|
private sessionMetaCache;
|
|
33
34
|
/** Lazy-initialize the database connection. */
|
|
34
35
|
private ensureDb;
|
|
36
|
+
getRuntimeStatus(): ProviderRuntimeStatus;
|
|
37
|
+
canMonitorDirectory(dir: string): boolean;
|
|
38
|
+
private mapDbSessions;
|
|
35
39
|
getSessionDirectory(workspacePath: string): string;
|
|
36
40
|
discoverSessionDirectory(workspacePath: string): string | null;
|
|
37
41
|
findActiveSession(workspacePath: string): string | null;
|
|
@@ -69,6 +69,12 @@ function getOpenCodeDataDir() {
|
|
|
69
69
|
if (xdg) {
|
|
70
70
|
return path.join(xdg, 'opencode');
|
|
71
71
|
}
|
|
72
|
+
if (process.platform === 'darwin') {
|
|
73
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'opencode');
|
|
74
|
+
}
|
|
75
|
+
if (process.platform === 'win32') {
|
|
76
|
+
return path.join(process.env.LOCALAPPDATA || process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'opencode');
|
|
77
|
+
}
|
|
72
78
|
return path.join(os.homedir(), '.local', 'share', 'opencode');
|
|
73
79
|
}
|
|
74
80
|
/**
|
|
@@ -170,12 +176,18 @@ function extractParentIdFromDbMessage(row) {
|
|
|
170
176
|
*
|
|
171
177
|
* Tries DB first, then file-based project metadata, then git root commit hash.
|
|
172
178
|
*/
|
|
173
|
-
function resolveProjectId(workspacePath, db) {
|
|
179
|
+
function resolveProjectId(workspacePath, db, runtimeStatus) {
|
|
174
180
|
// Strategy 1: DB lookup
|
|
175
181
|
if (db) {
|
|
176
182
|
const dbProject = db.findProjectByWorktree(workspacePath);
|
|
177
183
|
if (dbProject)
|
|
178
184
|
return dbProject.id;
|
|
185
|
+
const byDirectory = db.findProjectBySessionDirectory(workspacePath);
|
|
186
|
+
if (byDirectory)
|
|
187
|
+
return byDirectory.id;
|
|
188
|
+
}
|
|
189
|
+
if (runtimeStatus.kind !== 'db_missing') {
|
|
190
|
+
return null;
|
|
179
191
|
}
|
|
180
192
|
// Strategy 2: scan project files to find matching path
|
|
181
193
|
return resolveProjectIdFromFiles(workspacePath);
|
|
@@ -596,6 +608,7 @@ class OpenCodeProvider {
|
|
|
596
608
|
displayName = 'OpenCode';
|
|
597
609
|
db = null;
|
|
598
610
|
dbInitialized = false;
|
|
611
|
+
dbStatus = { available: false, kind: 'db_missing' };
|
|
599
612
|
/** Cache of session metadata populated during listing */
|
|
600
613
|
sessionMetaCache = new Map();
|
|
601
614
|
/** Lazy-initialize the database connection. */
|
|
@@ -605,15 +618,47 @@ class OpenCodeProvider {
|
|
|
605
618
|
this.dbInitialized = true;
|
|
606
619
|
const dataDir = getOpenCodeDataDir();
|
|
607
620
|
const db = new openCodeDatabase_1.OpenCodeDatabase(dataDir);
|
|
608
|
-
if (db.
|
|
621
|
+
if (db.open()) {
|
|
609
622
|
this.db = db;
|
|
610
623
|
}
|
|
624
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
611
625
|
return this.db;
|
|
612
626
|
}
|
|
627
|
+
getRuntimeStatus() {
|
|
628
|
+
const db = this.ensureDb();
|
|
629
|
+
if (db)
|
|
630
|
+
return db.getRuntimeStatus();
|
|
631
|
+
return this.dbStatus;
|
|
632
|
+
}
|
|
633
|
+
canMonitorDirectory(dir) {
|
|
634
|
+
if (fs.existsSync(dir))
|
|
635
|
+
return true;
|
|
636
|
+
const db = this.ensureDb();
|
|
637
|
+
if (!db)
|
|
638
|
+
return false;
|
|
639
|
+
const projectId = extractProjectIdFromDbPath(dir + path.sep + 'dummy.json') || path.basename(dir);
|
|
640
|
+
return projectId.length > 0 && db.hasProject(projectId);
|
|
641
|
+
}
|
|
642
|
+
mapDbSessions(projectId) {
|
|
643
|
+
const db = this.ensureDb();
|
|
644
|
+
if (!db)
|
|
645
|
+
return [];
|
|
646
|
+
const sessions = db.getSessionsForProject(projectId);
|
|
647
|
+
const dataDir = getOpenCodeDataDir();
|
|
648
|
+
return sessions.map(session => {
|
|
649
|
+
const syntheticPath = makeDbSessionPath(dataDir, projectId, session.id);
|
|
650
|
+
this.sessionMetaCache.set(syntheticPath, {
|
|
651
|
+
title: session.title,
|
|
652
|
+
timeUpdated: session.time_updated,
|
|
653
|
+
});
|
|
654
|
+
return syntheticPath;
|
|
655
|
+
});
|
|
656
|
+
}
|
|
613
657
|
// --- Path resolution ---
|
|
614
658
|
getSessionDirectory(workspacePath) {
|
|
615
659
|
const db = this.ensureDb();
|
|
616
|
-
const
|
|
660
|
+
const dbStatus = this.getRuntimeStatus();
|
|
661
|
+
const projectId = resolveProjectId(workspacePath, db, dbStatus);
|
|
617
662
|
if (projectId) {
|
|
618
663
|
// For DB sessions, return a synthetic directory path
|
|
619
664
|
if (db) {
|
|
@@ -621,20 +666,26 @@ class OpenCodeProvider {
|
|
|
621
666
|
}
|
|
622
667
|
return path.join(getStorageDir(), 'session', projectId);
|
|
623
668
|
}
|
|
669
|
+
if (dbStatus.kind !== 'db_missing') {
|
|
670
|
+
return path.join(getOpenCodeDataDir(), DB_SESSION_PREFIX);
|
|
671
|
+
}
|
|
624
672
|
return path.join(getStorageDir(), 'session');
|
|
625
673
|
}
|
|
626
674
|
discoverSessionDirectory(workspacePath) {
|
|
627
675
|
const db = this.ensureDb();
|
|
628
|
-
const
|
|
676
|
+
const dbStatus = this.getRuntimeStatus();
|
|
677
|
+
const projectId = resolveProjectId(workspacePath, db, dbStatus);
|
|
629
678
|
if (!projectId)
|
|
630
679
|
return null;
|
|
631
680
|
// DB: check if project has sessions
|
|
632
681
|
if (db) {
|
|
633
|
-
|
|
634
|
-
if (sessions.length > 0) {
|
|
682
|
+
if (db.hasProject(projectId)) {
|
|
635
683
|
return path.join(getOpenCodeDataDir(), DB_SESSION_PREFIX, projectId);
|
|
636
684
|
}
|
|
637
685
|
}
|
|
686
|
+
if (dbStatus.kind !== 'db_missing') {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
638
689
|
// File fallback
|
|
639
690
|
const dir = path.join(getStorageDir(), 'session', projectId);
|
|
640
691
|
return fs.existsSync(dir) ? dir : null;
|
|
@@ -642,7 +693,8 @@ class OpenCodeProvider {
|
|
|
642
693
|
// --- Session discovery ---
|
|
643
694
|
findActiveSession(workspacePath) {
|
|
644
695
|
const db = this.ensureDb();
|
|
645
|
-
const
|
|
696
|
+
const dbStatus = this.getRuntimeStatus();
|
|
697
|
+
const projectId = resolveProjectId(workspacePath, db, dbStatus);
|
|
646
698
|
if (!projectId)
|
|
647
699
|
return null;
|
|
648
700
|
// DB primary
|
|
@@ -657,6 +709,9 @@ class OpenCodeProvider {
|
|
|
657
709
|
return syntheticPath;
|
|
658
710
|
}
|
|
659
711
|
}
|
|
712
|
+
if (dbStatus.kind !== 'db_missing') {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
660
715
|
// File fallback
|
|
661
716
|
return this.findActiveSessionFromFiles(projectId);
|
|
662
717
|
}
|
|
@@ -717,23 +772,16 @@ class OpenCodeProvider {
|
|
|
717
772
|
}
|
|
718
773
|
findAllSessions(workspacePath) {
|
|
719
774
|
const db = this.ensureDb();
|
|
720
|
-
const
|
|
775
|
+
const dbStatus = this.getRuntimeStatus();
|
|
776
|
+
const projectId = resolveProjectId(workspacePath, db, dbStatus);
|
|
721
777
|
if (!projectId)
|
|
722
778
|
return [];
|
|
723
779
|
// DB primary
|
|
724
780
|
if (db) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const syntheticPath = makeDbSessionPath(dataDir, projectId, s.id);
|
|
730
|
-
this.sessionMetaCache.set(syntheticPath, {
|
|
731
|
-
title: s.title,
|
|
732
|
-
timeUpdated: s.time_updated,
|
|
733
|
-
});
|
|
734
|
-
return syntheticPath;
|
|
735
|
-
});
|
|
736
|
-
}
|
|
781
|
+
return this.mapDbSessions(projectId);
|
|
782
|
+
}
|
|
783
|
+
if (dbStatus.kind !== 'db_missing') {
|
|
784
|
+
return [];
|
|
737
785
|
}
|
|
738
786
|
// File fallback
|
|
739
787
|
const sessionDir = path.join(getStorageDir(), 'session', projectId);
|
|
@@ -741,39 +789,25 @@ class OpenCodeProvider {
|
|
|
741
789
|
}
|
|
742
790
|
findSessionsInDirectory(dir) {
|
|
743
791
|
const db = this.ensureDb();
|
|
792
|
+
const dbStatus = this.getRuntimeStatus();
|
|
744
793
|
// Check if this is a synthetic DB session directory
|
|
745
794
|
if (db && dir.includes(path.sep + DB_SESSION_PREFIX + path.sep)) {
|
|
746
795
|
const projectId = extractProjectIdFromDbPath(dir + path.sep + 'dummy.json');
|
|
747
796
|
if (projectId) {
|
|
748
|
-
|
|
749
|
-
const dataDir = getOpenCodeDataDir();
|
|
750
|
-
return sessions.map(s => {
|
|
751
|
-
const syntheticPath = makeDbSessionPath(dataDir, projectId, s.id);
|
|
752
|
-
this.sessionMetaCache.set(syntheticPath, {
|
|
753
|
-
title: s.title,
|
|
754
|
-
timeUpdated: s.time_updated,
|
|
755
|
-
});
|
|
756
|
-
return syntheticPath;
|
|
757
|
-
});
|
|
797
|
+
return this.mapDbSessions(projectId);
|
|
758
798
|
}
|
|
759
799
|
}
|
|
760
800
|
// For a DB-backed directory path ending with the project ID,
|
|
761
801
|
// try to extract project ID from dir name
|
|
762
802
|
if (db) {
|
|
763
803
|
const dirName = path.basename(dir);
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const dataDir = getOpenCodeDataDir();
|
|
767
|
-
return sessions.map(s => {
|
|
768
|
-
const syntheticPath = makeDbSessionPath(dataDir, dirName, s.id);
|
|
769
|
-
this.sessionMetaCache.set(syntheticPath, {
|
|
770
|
-
title: s.title,
|
|
771
|
-
timeUpdated: s.time_updated,
|
|
772
|
-
});
|
|
773
|
-
return syntheticPath;
|
|
774
|
-
});
|
|
804
|
+
if (db.hasProject(dirName)) {
|
|
805
|
+
return this.mapDbSessions(dirName);
|
|
775
806
|
}
|
|
776
807
|
}
|
|
808
|
+
if (dbStatus.kind !== 'db_missing') {
|
|
809
|
+
return [];
|
|
810
|
+
}
|
|
777
811
|
// File fallback
|
|
778
812
|
return this.findSessionsInDirectoryFromFiles(dir);
|
|
779
813
|
}
|
|
@@ -804,6 +838,7 @@ class OpenCodeProvider {
|
|
|
804
838
|
getAllProjectFolders(workspacePath) {
|
|
805
839
|
const db = this.ensureDb();
|
|
806
840
|
const folders = [];
|
|
841
|
+
const dbStatus = this.getRuntimeStatus();
|
|
807
842
|
// DB primary
|
|
808
843
|
if (db) {
|
|
809
844
|
const projects = db.getAllProjects();
|
|
@@ -811,7 +846,7 @@ class OpenCodeProvider {
|
|
|
811
846
|
const statsMap = new Map(stats.map(s => [s.projectId, s]));
|
|
812
847
|
let currentProjectId = null;
|
|
813
848
|
if (workspacePath) {
|
|
814
|
-
currentProjectId = resolveProjectId(workspacePath, db);
|
|
849
|
+
currentProjectId = resolveProjectId(workspacePath, db, dbStatus);
|
|
815
850
|
}
|
|
816
851
|
const dataDir = getOpenCodeDataDir();
|
|
817
852
|
for (const project of projects) {
|
|
@@ -841,6 +876,9 @@ class OpenCodeProvider {
|
|
|
841
876
|
if (folders.length > 0)
|
|
842
877
|
return folders;
|
|
843
878
|
}
|
|
879
|
+
if (dbStatus.kind !== 'db_missing') {
|
|
880
|
+
return [];
|
|
881
|
+
}
|
|
844
882
|
// File fallback
|
|
845
883
|
return this.getAllProjectFoldersFromFiles(workspacePath);
|
|
846
884
|
}
|
|
@@ -935,7 +973,7 @@ class OpenCodeProvider {
|
|
|
935
973
|
}
|
|
936
974
|
encodeWorkspacePath(workspacePath) {
|
|
937
975
|
const db = this.ensureDb();
|
|
938
|
-
return resolveProjectId(workspacePath, db) || workspacePath;
|
|
976
|
+
return resolveProjectId(workspacePath, db, this.getRuntimeStatus()) || workspacePath;
|
|
939
977
|
}
|
|
940
978
|
extractSessionLabel(sessionPath) {
|
|
941
979
|
const db = this.ensureDb();
|
|
@@ -6,6 +6,7 @@ export interface DbProject {
|
|
|
6
6
|
id: string;
|
|
7
7
|
worktree: string;
|
|
8
8
|
name: string | null;
|
|
9
|
+
sandboxes: string[];
|
|
9
10
|
time_created: number;
|
|
10
11
|
time_updated: number;
|
|
11
12
|
}
|
|
@@ -32,17 +33,25 @@ export interface DbPart {
|
|
|
32
33
|
time_updated: number;
|
|
33
34
|
data: string;
|
|
34
35
|
}
|
|
36
|
+
export interface OpenCodeDbRuntimeStatus {
|
|
37
|
+
available: boolean;
|
|
38
|
+
kind: 'available' | 'db_missing' | 'sqlite_missing' | 'sqlite_blocked' | 'query_failed';
|
|
39
|
+
message?: string;
|
|
40
|
+
}
|
|
35
41
|
export declare class OpenCodeDatabase {
|
|
36
42
|
private readonly dbPath;
|
|
37
|
-
private
|
|
43
|
+
private runtimeStatus;
|
|
38
44
|
constructor(dataDir: string);
|
|
39
45
|
isAvailable(): boolean;
|
|
46
|
+
getRuntimeStatus(): OpenCodeDbRuntimeStatus;
|
|
40
47
|
open(): boolean;
|
|
41
48
|
close(): void;
|
|
42
49
|
private query;
|
|
43
50
|
private queryOne;
|
|
44
51
|
findProjectByWorktree(workspacePath: string): DbProject | null;
|
|
52
|
+
findProjectBySessionDirectory(workspacePath: string): DbProject | null;
|
|
45
53
|
getAllProjects(): DbProject[];
|
|
54
|
+
hasProject(projectId: string): boolean;
|
|
46
55
|
getSessionsForProject(projectId: string): DbSession[];
|
|
47
56
|
/** Get the most recently updated session for a project (excludes subagent child sessions). */
|
|
48
57
|
getMostRecentSession(projectId: string): DbSession | null;
|
|
@@ -43,27 +43,40 @@ const path = __importStar(require("path"));
|
|
|
43
43
|
const child_process_1 = require("child_process");
|
|
44
44
|
class OpenCodeDatabase {
|
|
45
45
|
dbPath;
|
|
46
|
-
|
|
46
|
+
runtimeStatus = null;
|
|
47
47
|
constructor(dataDir) {
|
|
48
48
|
this.dbPath = path.join(dataDir, 'opencode.db');
|
|
49
49
|
}
|
|
50
50
|
isAvailable() { return fs.existsSync(this.dbPath); }
|
|
51
|
+
getRuntimeStatus() {
|
|
52
|
+
if (this.runtimeStatus)
|
|
53
|
+
return this.runtimeStatus;
|
|
54
|
+
if (!this.isAvailable()) {
|
|
55
|
+
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
56
|
+
return this.runtimeStatus;
|
|
57
|
+
}
|
|
58
|
+
return { available: false, kind: 'query_failed', message: 'OpenCode database has not been initialized yet.' };
|
|
59
|
+
}
|
|
51
60
|
open() {
|
|
52
|
-
if (this.
|
|
53
|
-
return this.
|
|
61
|
+
if (this.runtimeStatus)
|
|
62
|
+
return this.runtimeStatus.available;
|
|
63
|
+
if (!this.isAvailable()) {
|
|
64
|
+
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
54
67
|
try {
|
|
55
68
|
(0, child_process_1.execFileSync)('sqlite3', ['--version'], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
56
|
-
this.
|
|
69
|
+
this.runtimeStatus = { available: true, kind: 'available' };
|
|
57
70
|
return true;
|
|
58
71
|
}
|
|
59
|
-
catch {
|
|
60
|
-
this.
|
|
72
|
+
catch (error) {
|
|
73
|
+
this.runtimeStatus = toRuntimeStatus(error);
|
|
61
74
|
return false;
|
|
62
75
|
}
|
|
63
76
|
}
|
|
64
77
|
close() { }
|
|
65
78
|
query(sql, params = []) {
|
|
66
|
-
if (!this.
|
|
79
|
+
if (!this.open())
|
|
67
80
|
return [];
|
|
68
81
|
let query = sql;
|
|
69
82
|
for (const param of params) {
|
|
@@ -78,12 +91,14 @@ class OpenCodeDatabase {
|
|
|
78
91
|
const result = (0, child_process_1.execFileSync)('sqlite3', ['-json', '-readonly', this.dbPath, query], {
|
|
79
92
|
encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 50 * 1024 * 1024,
|
|
80
93
|
});
|
|
94
|
+
this.runtimeStatus = { available: true, kind: 'available' };
|
|
81
95
|
const trimmed = result.trim();
|
|
82
96
|
if (!trimmed)
|
|
83
97
|
return [];
|
|
84
98
|
return JSON.parse(trimmed);
|
|
85
99
|
}
|
|
86
|
-
catch {
|
|
100
|
+
catch (error) {
|
|
101
|
+
this.runtimeStatus = toRuntimeStatus(error, 'query_failed');
|
|
87
102
|
return [];
|
|
88
103
|
}
|
|
89
104
|
}
|
|
@@ -93,27 +108,50 @@ class OpenCodeDatabase {
|
|
|
93
108
|
}
|
|
94
109
|
findProjectByWorktree(workspacePath) {
|
|
95
110
|
const normalized = normalizePath(workspacePath);
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
const all = this.getAllProjects();
|
|
112
|
+
let best = null;
|
|
113
|
+
let bestScore = -1;
|
|
114
|
+
for (const project of all) {
|
|
115
|
+
const worktreeScore = pathMatchScore(project.worktree, normalized);
|
|
116
|
+
if (worktreeScore > bestScore) {
|
|
117
|
+
best = project;
|
|
118
|
+
bestScore = worktreeScore;
|
|
119
|
+
}
|
|
120
|
+
for (const sandbox of project.sandboxes) {
|
|
121
|
+
const sandboxScore = pathMatchScore(sandbox, normalized);
|
|
122
|
+
if (sandboxScore > bestScore) {
|
|
123
|
+
best = project;
|
|
124
|
+
bestScore = sandboxScore;
|
|
125
|
+
}
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
128
|
+
return bestScore >= 0 ? best : null;
|
|
129
|
+
}
|
|
130
|
+
findProjectBySessionDirectory(workspacePath) {
|
|
131
|
+
const normalized = normalizePath(workspacePath);
|
|
132
|
+
const sessions = this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
|
|
133
|
+
let bestProjectId = null;
|
|
134
|
+
let bestScore = -1;
|
|
135
|
+
for (const session of sessions) {
|
|
136
|
+
const score = pathMatchScore(session.directory, normalized);
|
|
137
|
+
if (score > bestScore) {
|
|
138
|
+
bestProjectId = session.project_id;
|
|
139
|
+
bestScore = score;
|
|
140
|
+
}
|
|
112
141
|
}
|
|
113
|
-
|
|
142
|
+
if (!bestProjectId)
|
|
143
|
+
return null;
|
|
144
|
+
return this.getAllProjects().find(project => project.id === bestProjectId) ?? null;
|
|
114
145
|
}
|
|
115
146
|
getAllProjects() {
|
|
116
|
-
return this.query('SELECT id, worktree, name, time_created, time_updated FROM project')
|
|
147
|
+
return this.query('SELECT id, worktree, name, sandboxes, time_created, time_updated FROM project')
|
|
148
|
+
.map(project => ({
|
|
149
|
+
...project,
|
|
150
|
+
sandboxes: parseStringArray(project.sandboxes),
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
hasProject(projectId) {
|
|
154
|
+
return this.queryOne('SELECT id FROM project WHERE id = ? LIMIT 1', [projectId]) !== null;
|
|
117
155
|
}
|
|
118
156
|
getSessionsForProject(projectId) {
|
|
119
157
|
return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC', [projectId]);
|
|
@@ -230,3 +268,55 @@ function normalizePath(input) {
|
|
|
230
268
|
return path.resolve(input);
|
|
231
269
|
}
|
|
232
270
|
}
|
|
271
|
+
function parseStringArray(value) {
|
|
272
|
+
if (!value)
|
|
273
|
+
return [];
|
|
274
|
+
try {
|
|
275
|
+
const parsed = JSON.parse(value);
|
|
276
|
+
return Array.isArray(parsed)
|
|
277
|
+
? parsed.filter((entry) => typeof entry === 'string' && entry.length > 0).map(normalizePath)
|
|
278
|
+
: [];
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function normalizeForCompare(input) {
|
|
285
|
+
const normalized = normalizePath(input);
|
|
286
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
287
|
+
}
|
|
288
|
+
function pathMatchScore(candidate, workspacePath) {
|
|
289
|
+
if (!candidate)
|
|
290
|
+
return -1;
|
|
291
|
+
const normalizedCandidate = normalizeForCompare(candidate);
|
|
292
|
+
const normalizedWorkspace = normalizeForCompare(workspacePath);
|
|
293
|
+
if (normalizedCandidate === normalizedWorkspace)
|
|
294
|
+
return 10_000 + normalizedCandidate.length;
|
|
295
|
+
const candidatePrefix = normalizedCandidate.endsWith(path.sep)
|
|
296
|
+
? normalizedCandidate
|
|
297
|
+
: normalizedCandidate + path.sep;
|
|
298
|
+
const workspacePrefix = normalizedWorkspace.endsWith(path.sep)
|
|
299
|
+
? normalizedWorkspace
|
|
300
|
+
: normalizedWorkspace + path.sep;
|
|
301
|
+
if (normalizedWorkspace.startsWith(candidatePrefix))
|
|
302
|
+
return 5_000 + normalizedCandidate.length;
|
|
303
|
+
if (normalizedCandidate.startsWith(workspacePrefix))
|
|
304
|
+
return 1_000 + normalizedCandidate.length;
|
|
305
|
+
return -1;
|
|
306
|
+
}
|
|
307
|
+
function toRuntimeStatus(error, fallback = 'sqlite_missing') {
|
|
308
|
+
if (isErrno(error, 'ENOENT')) {
|
|
309
|
+
return { available: false, kind: 'sqlite_missing', message: 'sqlite3 executable not found in PATH.' };
|
|
310
|
+
}
|
|
311
|
+
if (isErrno(error, 'EPERM') || isErrno(error, 'EACCES')) {
|
|
312
|
+
return { available: false, kind: 'sqlite_blocked', message: 'sqlite3 exists but could not be executed.' };
|
|
313
|
+
}
|
|
314
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
315
|
+
return { available: false, kind: fallback, message };
|
|
316
|
+
}
|
|
317
|
+
function isErrno(error, code) {
|
|
318
|
+
return typeof error === 'object'
|
|
319
|
+
&& error !== null
|
|
320
|
+
&& 'code' in error
|
|
321
|
+
&& error.code === code;
|
|
322
|
+
}
|
|
@@ -55,6 +55,11 @@ export interface ProjectFolderInfo {
|
|
|
55
55
|
sessionCount: number;
|
|
56
56
|
lastModified: Date;
|
|
57
57
|
}
|
|
58
|
+
export interface ProviderRuntimeStatus {
|
|
59
|
+
available: boolean;
|
|
60
|
+
kind: 'available' | 'db_missing' | 'sqlite_missing' | 'sqlite_blocked' | 'query_failed';
|
|
61
|
+
message?: string;
|
|
62
|
+
}
|
|
58
63
|
/**
|
|
59
64
|
* Incremental reader for session data.
|
|
60
65
|
*
|
|
@@ -133,6 +138,10 @@ export interface SessionProviderBase {
|
|
|
133
138
|
getCurrentUsageSnapshot?(sessionPath: string): TokenUsage | null;
|
|
134
139
|
/** Gets context attribution breakdown from provider data, if available. */
|
|
135
140
|
getContextAttribution?(sessionPath: string): ContextAttribution | null;
|
|
141
|
+
/** Reports provider runtime readiness for DB-backed providers. */
|
|
142
|
+
getRuntimeStatus?(): ProviderRuntimeStatus;
|
|
143
|
+
/** Tests whether a provider can monitor a directory path, including synthetic DB-backed paths. */
|
|
144
|
+
canMonitorDirectory?(dir: string): boolean;
|
|
136
145
|
/** Release resources. */
|
|
137
146
|
dispose(): void;
|
|
138
147
|
}
|
package/package.json
CHANGED