@jacques-ai/core 0.0.7-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/archive/archive-store.d.ts +166 -0
- package/dist/archive/archive-store.d.ts.map +1 -0
- package/dist/archive/archive-store.js +612 -0
- package/dist/archive/archive-store.js.map +1 -0
- package/dist/archive/bulk-archive.d.ts +63 -0
- package/dist/archive/bulk-archive.d.ts.map +1 -0
- package/dist/archive/bulk-archive.js +315 -0
- package/dist/archive/bulk-archive.js.map +1 -0
- package/dist/archive/filename-utils.d.ts +39 -0
- package/dist/archive/filename-utils.d.ts.map +1 -0
- package/dist/archive/filename-utils.js +78 -0
- package/dist/archive/filename-utils.js.map +1 -0
- package/dist/archive/index.d.ts +21 -0
- package/dist/archive/index.d.ts.map +1 -0
- package/dist/archive/index.js +45 -0
- package/dist/archive/index.js.map +1 -0
- package/dist/archive/manifest-extractor.d.ts +40 -0
- package/dist/archive/manifest-extractor.d.ts.map +1 -0
- package/dist/archive/manifest-extractor.js +456 -0
- package/dist/archive/manifest-extractor.js.map +1 -0
- package/dist/archive/migration.d.ts +59 -0
- package/dist/archive/migration.d.ts.map +1 -0
- package/dist/archive/migration.js +172 -0
- package/dist/archive/migration.js.map +1 -0
- package/dist/archive/plan-cataloger.d.ts +24 -0
- package/dist/archive/plan-cataloger.d.ts.map +1 -0
- package/dist/archive/plan-cataloger.js +100 -0
- package/dist/archive/plan-cataloger.js.map +1 -0
- package/dist/archive/plan-extractor.d.ts +84 -0
- package/dist/archive/plan-extractor.d.ts.map +1 -0
- package/dist/archive/plan-extractor.js +371 -0
- package/dist/archive/plan-extractor.js.map +1 -0
- package/dist/archive/search-indexer.d.ts +50 -0
- package/dist/archive/search-indexer.d.ts.map +1 -0
- package/dist/archive/search-indexer.js +294 -0
- package/dist/archive/search-indexer.js.map +1 -0
- package/dist/archive/subagent-store.d.ts +113 -0
- package/dist/archive/subagent-store.d.ts.map +1 -0
- package/dist/archive/subagent-store.js +173 -0
- package/dist/archive/subagent-store.js.map +1 -0
- package/dist/archive/types.d.ts +236 -0
- package/dist/archive/types.d.ts.map +1 -0
- package/dist/archive/types.js +30 -0
- package/dist/archive/types.js.map +1 -0
- package/dist/branding.d.ts +9 -0
- package/dist/branding.d.ts.map +1 -0
- package/dist/branding.js +50 -0
- package/dist/branding.js.map +1 -0
- package/dist/cache/git-utils.d.ts +36 -0
- package/dist/cache/git-utils.d.ts.map +1 -0
- package/dist/cache/git-utils.js +160 -0
- package/dist/cache/git-utils.js.map +1 -0
- package/dist/cache/hidden-projects.d.ts +19 -0
- package/dist/cache/hidden-projects.d.ts.map +1 -0
- package/dist/cache/hidden-projects.js +48 -0
- package/dist/cache/hidden-projects.js.map +1 -0
- package/dist/cache/index.d.ts +15 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +20 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/metadata-extractor.d.ts +62 -0
- package/dist/cache/metadata-extractor.d.ts.map +1 -0
- package/dist/cache/metadata-extractor.js +574 -0
- package/dist/cache/metadata-extractor.js.map +1 -0
- package/dist/cache/mode-detector.d.ts +19 -0
- package/dist/cache/mode-detector.d.ts.map +1 -0
- package/dist/cache/mode-detector.js +161 -0
- package/dist/cache/mode-detector.js.map +1 -0
- package/dist/cache/persistence.d.ts +39 -0
- package/dist/cache/persistence.d.ts.map +1 -0
- package/dist/cache/persistence.js +98 -0
- package/dist/cache/persistence.js.map +1 -0
- package/dist/cache/project-discovery.d.ts +41 -0
- package/dist/cache/project-discovery.d.ts.map +1 -0
- package/dist/cache/project-discovery.js +212 -0
- package/dist/cache/project-discovery.js.map +1 -0
- package/dist/cache/session-index.d.ts +258 -0
- package/dist/cache/session-index.d.ts.map +1 -0
- package/dist/cache/session-index.js +1030 -0
- package/dist/cache/session-index.js.map +1 -0
- package/dist/cache/types.d.ts +159 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +29 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/catalog/bulk-extractor.d.ts +18 -0
- package/dist/catalog/bulk-extractor.d.ts.map +1 -0
- package/dist/catalog/bulk-extractor.js +150 -0
- package/dist/catalog/bulk-extractor.js.map +1 -0
- package/dist/catalog/extractor.d.ts +53 -0
- package/dist/catalog/extractor.d.ts.map +1 -0
- package/dist/catalog/extractor.js +522 -0
- package/dist/catalog/extractor.js.map +1 -0
- package/dist/catalog/index.d.ts +10 -0
- package/dist/catalog/index.d.ts.map +1 -0
- package/dist/catalog/index.js +11 -0
- package/dist/catalog/index.js.map +1 -0
- package/dist/catalog/types.d.ts +134 -0
- package/dist/catalog/types.d.ts.map +1 -0
- package/dist/catalog/types.js +8 -0
- package/dist/catalog/types.js.map +1 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/websocket-client.d.ts +96 -0
- package/dist/client/websocket-client.d.ts.map +1 -0
- package/dist/client/websocket-client.js +222 -0
- package/dist/client/websocket-client.js.map +1 -0
- package/dist/context/index.d.ts +13 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +26 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/indexer.d.ts +73 -0
- package/dist/context/indexer.d.ts.map +1 -0
- package/dist/context/indexer.js +233 -0
- package/dist/context/indexer.js.map +1 -0
- package/dist/context/manager.d.ts +66 -0
- package/dist/context/manager.d.ts.map +1 -0
- package/dist/context/manager.js +310 -0
- package/dist/context/manager.js.map +1 -0
- package/dist/context/types.d.ts +149 -0
- package/dist/context/types.d.ts.map +1 -0
- package/dist/context/types.js +36 -0
- package/dist/context/types.js.map +1 -0
- package/dist/handoff/catalog.d.ts +54 -0
- package/dist/handoff/catalog.d.ts.map +1 -0
- package/dist/handoff/catalog.js +121 -0
- package/dist/handoff/catalog.js.map +1 -0
- package/dist/handoff/generator.d.ts +107 -0
- package/dist/handoff/generator.d.ts.map +1 -0
- package/dist/handoff/generator.js +603 -0
- package/dist/handoff/generator.js.map +1 -0
- package/dist/handoff/index.d.ts +13 -0
- package/dist/handoff/index.d.ts.map +1 -0
- package/dist/handoff/index.js +12 -0
- package/dist/handoff/index.js.map +1 -0
- package/dist/handoff/llm-generator.d.ts +77 -0
- package/dist/handoff/llm-generator.d.ts.map +1 -0
- package/dist/handoff/llm-generator.js +513 -0
- package/dist/handoff/llm-generator.js.map +1 -0
- package/dist/handoff/prompts.d.ts +18 -0
- package/dist/handoff/prompts.d.ts.map +1 -0
- package/dist/handoff/prompts.js +22 -0
- package/dist/handoff/prompts.js.map +1 -0
- package/dist/handoff/types.d.ts +28 -0
- package/dist/handoff/types.d.ts.map +1 -0
- package/dist/handoff/types.js +7 -0
- package/dist/handoff/types.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +132 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/claude-operations.d.ts +111 -0
- package/dist/logging/claude-operations.d.ts.map +1 -0
- package/dist/logging/claude-operations.js +132 -0
- package/dist/logging/claude-operations.js.map +1 -0
- package/dist/logging/error-utils.d.ts +18 -0
- package/dist/logging/error-utils.d.ts.map +1 -0
- package/dist/logging/error-utils.js +37 -0
- package/dist/logging/error-utils.js.map +1 -0
- package/dist/logging/index.d.ts +11 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/index.js +10 -0
- package/dist/logging/index.js.map +1 -0
- package/dist/logging/logger.d.ts +25 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +39 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/notifications/constants.d.ts +16 -0
- package/dist/notifications/constants.d.ts.map +1 -0
- package/dist/notifications/constants.js +49 -0
- package/dist/notifications/constants.js.map +1 -0
- package/dist/notifications/index.d.ts +9 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +8 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/types.d.ts +28 -0
- package/dist/notifications/types.d.ts.map +1 -0
- package/dist/notifications/types.js +7 -0
- package/dist/notifications/types.js.map +1 -0
- package/dist/notifications/utils.d.ts +20 -0
- package/dist/notifications/utils.d.ts.map +1 -0
- package/dist/notifications/utils.js +37 -0
- package/dist/notifications/utils.js.map +1 -0
- package/dist/plan/index.d.ts +12 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/index.js +15 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/plan-parser.d.ts +33 -0
- package/dist/plan/plan-parser.d.ts.map +1 -0
- package/dist/plan/plan-parser.js +189 -0
- package/dist/plan/plan-parser.js.map +1 -0
- package/dist/plan/progress-computer.d.ts +34 -0
- package/dist/plan/progress-computer.d.ts.map +1 -0
- package/dist/plan/progress-computer.js +211 -0
- package/dist/plan/progress-computer.js.map +1 -0
- package/dist/plan/progress-matcher.d.ts +34 -0
- package/dist/plan/progress-matcher.d.ts.map +1 -0
- package/dist/plan/progress-matcher.js +297 -0
- package/dist/plan/progress-matcher.js.map +1 -0
- package/dist/plan/task-extractor.d.ts +30 -0
- package/dist/plan/task-extractor.d.ts.map +1 -0
- package/dist/plan/task-extractor.js +435 -0
- package/dist/plan/task-extractor.js.map +1 -0
- package/dist/plan/types.d.ts +131 -0
- package/dist/plan/types.d.ts.map +1 -0
- package/dist/plan/types.js +8 -0
- package/dist/plan/types.js.map +1 -0
- package/dist/project/aggregator.d.ts +43 -0
- package/dist/project/aggregator.d.ts.map +1 -0
- package/dist/project/aggregator.js +218 -0
- package/dist/project/aggregator.js.map +1 -0
- package/dist/project/index.d.ts +9 -0
- package/dist/project/index.d.ts.map +1 -0
- package/dist/project/index.js +9 -0
- package/dist/project/index.js.map +1 -0
- package/dist/project/types.d.ts +65 -0
- package/dist/project/types.d.ts.map +1 -0
- package/dist/project/types.js +27 -0
- package/dist/project/types.js.map +1 -0
- package/dist/session/detector.d.ts +113 -0
- package/dist/session/detector.d.ts.map +1 -0
- package/dist/session/detector.js +333 -0
- package/dist/session/detector.js.map +1 -0
- package/dist/session/filters.d.ts +32 -0
- package/dist/session/filters.d.ts.map +1 -0
- package/dist/session/filters.js +100 -0
- package/dist/session/filters.js.map +1 -0
- package/dist/session/format-title.d.ts +16 -0
- package/dist/session/format-title.d.ts.map +1 -0
- package/dist/session/format-title.js +54 -0
- package/dist/session/format-title.js.map +1 -0
- package/dist/session/index.d.ts +16 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +10 -0
- package/dist/session/index.js.map +1 -0
- package/dist/session/parser.d.ts +264 -0
- package/dist/session/parser.d.ts.map +1 -0
- package/dist/session/parser.js +588 -0
- package/dist/session/parser.js.map +1 -0
- package/dist/session/token-estimator.d.ts +32 -0
- package/dist/session/token-estimator.d.ts.map +1 -0
- package/dist/session/token-estimator.js +139 -0
- package/dist/session/token-estimator.js.map +1 -0
- package/dist/session/transformer.d.ts +126 -0
- package/dist/session/transformer.d.ts.map +1 -0
- package/dist/session/transformer.js +158 -0
- package/dist/session/transformer.js.map +1 -0
- package/dist/setup/hooks-config.d.ts +35 -0
- package/dist/setup/hooks-config.d.ts.map +1 -0
- package/dist/setup/hooks-config.js +107 -0
- package/dist/setup/hooks-config.js.map +1 -0
- package/dist/setup/hooks-symlink.d.ts +17 -0
- package/dist/setup/hooks-symlink.d.ts.map +1 -0
- package/dist/setup/hooks-symlink.js +89 -0
- package/dist/setup/hooks-symlink.js.map +1 -0
- package/dist/setup/index.d.ts +13 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/index.js +12 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/setup/prerequisites.d.ts +11 -0
- package/dist/setup/prerequisites.d.ts.map +1 -0
- package/dist/setup/prerequisites.js +72 -0
- package/dist/setup/prerequisites.js.map +1 -0
- package/dist/setup/settings-merge.d.ts +33 -0
- package/dist/setup/settings-merge.d.ts.map +1 -0
- package/dist/setup/settings-merge.js +131 -0
- package/dist/setup/settings-merge.js.map +1 -0
- package/dist/setup/skills-install.d.ts +17 -0
- package/dist/setup/skills-install.d.ts.map +1 -0
- package/dist/setup/skills-install.js +60 -0
- package/dist/setup/skills-install.js.map +1 -0
- package/dist/setup/types.d.ts +39 -0
- package/dist/setup/types.d.ts.map +1 -0
- package/dist/setup/types.js +7 -0
- package/dist/setup/types.js.map +1 -0
- package/dist/setup/verification.d.ts +9 -0
- package/dist/setup/verification.d.ts.map +1 -0
- package/dist/setup/verification.js +91 -0
- package/dist/setup/verification.js.map +1 -0
- package/dist/shortcuts/index.d.ts +8 -0
- package/dist/shortcuts/index.d.ts.map +1 -0
- package/dist/shortcuts/index.js +6 -0
- package/dist/shortcuts/index.js.map +1 -0
- package/dist/shortcuts/key-utils.d.ts +54 -0
- package/dist/shortcuts/key-utils.d.ts.map +1 -0
- package/dist/shortcuts/key-utils.js +129 -0
- package/dist/shortcuts/key-utils.js.map +1 -0
- package/dist/shortcuts/shortcut-registry.d.ts +37 -0
- package/dist/shortcuts/shortcut-registry.d.ts.map +1 -0
- package/dist/shortcuts/shortcut-registry.js +322 -0
- package/dist/shortcuts/shortcut-registry.js.map +1 -0
- package/dist/sources/config.d.ts +91 -0
- package/dist/sources/config.d.ts.map +1 -0
- package/dist/sources/config.js +229 -0
- package/dist/sources/config.js.map +1 -0
- package/dist/sources/googledocs.d.ts +43 -0
- package/dist/sources/googledocs.d.ts.map +1 -0
- package/dist/sources/googledocs.js +298 -0
- package/dist/sources/googledocs.js.map +1 -0
- package/dist/sources/index.d.ts +14 -0
- package/dist/sources/index.d.ts.map +1 -0
- package/dist/sources/index.js +19 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/sources/notion.d.ts +35 -0
- package/dist/sources/notion.d.ts.map +1 -0
- package/dist/sources/notion.js +352 -0
- package/dist/sources/notion.js.map +1 -0
- package/dist/sources/obsidian.d.ts +38 -0
- package/dist/sources/obsidian.d.ts.map +1 -0
- package/dist/sources/obsidian.js +228 -0
- package/dist/sources/obsidian.js.map +1 -0
- package/dist/sources/types.d.ts +133 -0
- package/dist/sources/types.d.ts.map +1 -0
- package/dist/sources/types.js +19 -0
- package/dist/sources/types.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/writer.d.ts +86 -0
- package/dist/storage/writer.d.ts.map +1 -0
- package/dist/storage/writer.js +137 -0
- package/dist/storage/writer.js.map +1 -0
- package/dist/types.d.ts +203 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/claude-token.d.ts +49 -0
- package/dist/utils/claude-token.d.ts.map +1 -0
- package/dist/utils/claude-token.js +169 -0
- package/dist/utils/claude-token.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +13 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/settings.d.ts +100 -0
- package/dist/utils/settings.d.ts.map +1 -0
- package/dist/utils/settings.js +206 -0
- package/dist/utils/settings.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Module
|
|
3
|
+
*
|
|
4
|
+
* Lightweight session indexing without content duplication.
|
|
5
|
+
* Reads directly from Claude Code JSONL files.
|
|
6
|
+
*/
|
|
7
|
+
export { getDefaultSessionIndex, decodeProjectPath, } from "./types.js";
|
|
8
|
+
// Persistence
|
|
9
|
+
export { getCacheDir, getIndexPath, ensureCacheDir, readSessionIndex, writeSessionIndex, getSessionIndex, invalidateIndex, } from "./persistence.js";
|
|
10
|
+
// Mode detection
|
|
11
|
+
export { detectModeAndPlans } from "./mode-detector.js";
|
|
12
|
+
// Git utilities
|
|
13
|
+
export { detectGitInfo, readGitBranchFromJsonl, readWorktreeRepoRoot, computeBranchDivergence, checkDirtyStatus } from "./git-utils.js";
|
|
14
|
+
// Metadata extraction & index building
|
|
15
|
+
export { extractTitle, extractTimestamps, extractSessionMetadata, extractContinueTitleFromHandoff, listAllProjects, buildSessionIndex, } from "./metadata-extractor.js";
|
|
16
|
+
// Project discovery & query
|
|
17
|
+
export { getProjectGroupKey, discoverProjects, getSessionEntry, getSessionsByProject, getIndexStats, } from "./project-discovery.js";
|
|
18
|
+
// Hidden projects
|
|
19
|
+
export { getHiddenProjects, hideProject, unhideProject, } from "./hidden-projects.js";
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAcH,OAAO,EACL,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAEpB,cAAc;AACd,OAAO,EACL,WAAW,EACX,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAE1B,iBAAiB;AACjB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,gBAAgB;AAChB,OAAO,EAAE,aAAa,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAExI,uCAAuC;AACvC,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,+BAA+B,EAC/B,eAAe,EACf,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AAEjC,4BAA4B;AAC5B,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,aAAa,GACd,MAAM,wBAAwB,CAAC;AAEhC,kBAAkB;AAClB,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,aAAa,GACd,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts session metadata from JSONL files and catalog data.
|
|
5
|
+
* Builds the session index using a catalog-first strategy.
|
|
6
|
+
*/
|
|
7
|
+
import type { SessionEntry, SessionIndex } from "./types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Extract "## Current Task" from the most recent handoff created before sessionStartedAt.
|
|
10
|
+
* Returns "Cont: <task>" or null if no matching handoff found.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractContinueTitleFromHandoff(projectPath: string, sessionStartedAt: string): Promise<string | null>;
|
|
13
|
+
/**
|
|
14
|
+
* Extract session title from parsed JSONL entries.
|
|
15
|
+
* Priority:
|
|
16
|
+
* 1. Summary entry (Claude's auto-generated title)
|
|
17
|
+
* 2. First real user message (skips internal command messages)
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractTitle(entries: Array<{
|
|
20
|
+
type: string;
|
|
21
|
+
content: {
|
|
22
|
+
summary?: string;
|
|
23
|
+
text?: string;
|
|
24
|
+
};
|
|
25
|
+
}>): string;
|
|
26
|
+
/**
|
|
27
|
+
* Extract timestamps from entries
|
|
28
|
+
*/
|
|
29
|
+
export declare function extractTimestamps(entries: Array<{
|
|
30
|
+
timestamp: string;
|
|
31
|
+
}>): {
|
|
32
|
+
startedAt: string;
|
|
33
|
+
endedAt: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Extract metadata from a single JSONL file
|
|
37
|
+
*/
|
|
38
|
+
export declare function extractSessionMetadata(jsonlPath: string, projectPath: string, projectSlug: string): Promise<SessionEntry | null>;
|
|
39
|
+
/**
|
|
40
|
+
* List all project directories in ~/.claude/projects/
|
|
41
|
+
*/
|
|
42
|
+
export declare function listAllProjects(): Promise<Array<{
|
|
43
|
+
encodedPath: string;
|
|
44
|
+
projectPath: string;
|
|
45
|
+
projectSlug: string;
|
|
46
|
+
}>>;
|
|
47
|
+
/**
|
|
48
|
+
* Scan all sessions and build the index.
|
|
49
|
+
*
|
|
50
|
+
* Uses catalog-first loading: reads pre-extracted metadata from .jacques/index.json
|
|
51
|
+
* for each project, only falling back to JSONL parsing for new/uncataloged sessions.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildSessionIndex(options?: {
|
|
54
|
+
/** Progress callback - called for each session scanned */
|
|
55
|
+
onProgress?: (progress: {
|
|
56
|
+
phase: "scanning" | "processing";
|
|
57
|
+
total: number;
|
|
58
|
+
completed: number;
|
|
59
|
+
current: string;
|
|
60
|
+
}) => void;
|
|
61
|
+
}): Promise<SessionIndex>;
|
|
62
|
+
//# sourceMappingURL=metadata-extractor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata-extractor.d.ts","sourceRoot":"","sources":["../../src/cache/metadata-extractor.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AASH,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAA0C,MAAM,YAAY,CAAC;AAWrG;;;GAGG;AACH,wBAAsB,+BAA+B,CACnD,WAAW,EAAE,MAAM,EACnB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiCxB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,GAC7E,MAAM,CA6BR;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,KAAK,CAAC;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,GACpC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAoBxC;AAkID;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAyG9B;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAC9C,KAAK,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CACzE,CAmCA;AA0MD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,CAAC,EAAE;IAChD,0DAA0D;IAC1D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE;QACtB,KAAK,EAAE,UAAU,GAAG,YAAY,CAAC;QACjC,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,KAAK,IAAI,CAAC;CACZ,GAAG,OAAO,CAAC,YAAY,CAAC,CAyExB"}
|
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts session metadata from JSONL files and catalog data.
|
|
5
|
+
* Builds the session index using a catalog-first strategy.
|
|
6
|
+
*/
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { parseJSONL, getEntryStatistics } from "../session/parser.js";
|
|
10
|
+
import { listSubagentFiles, decodeProjectPath } from "../session/detector.js";
|
|
11
|
+
import { readProjectIndex } from "../context/indexer.js";
|
|
12
|
+
import { CLAUDE_PROJECTS_PATH } from "./types.js";
|
|
13
|
+
import { detectGitInfo, readGitBranchFromJsonl } from "./git-utils.js";
|
|
14
|
+
import { detectModeAndPlans } from "./mode-detector.js";
|
|
15
|
+
import { writeSessionIndex } from "./persistence.js";
|
|
16
|
+
import { isContinueSession } from "../session/format-title.js";
|
|
17
|
+
import { isNotFoundError, getErrorMessage } from "../logging/error-utils.js";
|
|
18
|
+
import { createLogger } from "../logging/logger.js";
|
|
19
|
+
const logger = createLogger({ prefix: "[Metadata]" });
|
|
20
|
+
/**
|
|
21
|
+
* Extract "## Current Task" from the most recent handoff created before sessionStartedAt.
|
|
22
|
+
* Returns "Cont: <task>" or null if no matching handoff found.
|
|
23
|
+
*/
|
|
24
|
+
export async function extractContinueTitleFromHandoff(projectPath, sessionStartedAt) {
|
|
25
|
+
const handoffsDir = path.join(projectPath, ".jacques", "handoffs");
|
|
26
|
+
try {
|
|
27
|
+
const files = await fs.readdir(handoffsDir);
|
|
28
|
+
const handoffFiles = files
|
|
29
|
+
.filter((f) => f.endsWith("-handoff.md"))
|
|
30
|
+
.sort()
|
|
31
|
+
.reverse(); // newest first (filenames are ISO-ish timestamps)
|
|
32
|
+
for (const file of handoffFiles) {
|
|
33
|
+
// Filename: YYYY-MM-DDTHH-mm-ss-handoff.md → stat for reliable mtime comparison
|
|
34
|
+
const filePath = path.join(handoffsDir, file);
|
|
35
|
+
const stat = await fs.stat(filePath);
|
|
36
|
+
if (stat.mtime.getTime() > new Date(sessionStartedAt).getTime()) {
|
|
37
|
+
continue; // handoff created after session started — skip
|
|
38
|
+
}
|
|
39
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
40
|
+
const taskMatch = content.match(/## Current Task\n+(.+?)(?=\n## |\n*$)/s);
|
|
41
|
+
if (taskMatch) {
|
|
42
|
+
const task = taskMatch[1].trim();
|
|
43
|
+
const firstLine = task.split("\n")[0].trim();
|
|
44
|
+
if (firstLine) {
|
|
45
|
+
return `Cont: ${firstLine}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (!isNotFoundError(err)) {
|
|
52
|
+
logger.warn("Failed to read handoffs for continue title:", getErrorMessage(err));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract session title from parsed JSONL entries.
|
|
59
|
+
* Priority:
|
|
60
|
+
* 1. Summary entry (Claude's auto-generated title)
|
|
61
|
+
* 2. First real user message (skips internal command messages)
|
|
62
|
+
*/
|
|
63
|
+
export function extractTitle(entries) {
|
|
64
|
+
// Try summary first
|
|
65
|
+
const summaryEntry = entries.find((e) => e.type === "summary" && e.content.summary);
|
|
66
|
+
if (summaryEntry?.content.summary) {
|
|
67
|
+
return summaryEntry.content.summary;
|
|
68
|
+
}
|
|
69
|
+
// Fallback to first real user message (skip internal command messages)
|
|
70
|
+
const userMessage = entries.find((e) => {
|
|
71
|
+
if (e.type !== "user_message" || !e.content.text)
|
|
72
|
+
return false;
|
|
73
|
+
const text = e.content.text.trim();
|
|
74
|
+
// Skip internal Claude Code messages
|
|
75
|
+
if (text.startsWith("<local-command"))
|
|
76
|
+
return false;
|
|
77
|
+
if (text.startsWith("<command-"))
|
|
78
|
+
return false;
|
|
79
|
+
if (text.length === 0)
|
|
80
|
+
return false;
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
if (userMessage?.content.text) {
|
|
84
|
+
// Truncate long messages
|
|
85
|
+
const text = userMessage.content.text.trim();
|
|
86
|
+
if (text.length > 100) {
|
|
87
|
+
return text.slice(0, 97) + "...";
|
|
88
|
+
}
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
return "Untitled Session";
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Extract timestamps from entries
|
|
95
|
+
*/
|
|
96
|
+
export function extractTimestamps(entries) {
|
|
97
|
+
if (entries.length === 0) {
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
return { startedAt: now, endedAt: now };
|
|
100
|
+
}
|
|
101
|
+
// Find earliest and latest timestamps
|
|
102
|
+
let startedAt = entries[0].timestamp;
|
|
103
|
+
let endedAt = entries[0].timestamp;
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (entry.timestamp < startedAt) {
|
|
106
|
+
startedAt = entry.timestamp;
|
|
107
|
+
}
|
|
108
|
+
if (entry.timestamp > endedAt) {
|
|
109
|
+
endedAt = entry.timestamp;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { startedAt, endedAt };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Extract explore agents and web searches from entries.
|
|
116
|
+
* For explore agents, computes token cost from their subagent JSONL files.
|
|
117
|
+
*/
|
|
118
|
+
async function extractAgentsAndSearches(entries, subagentFiles) {
|
|
119
|
+
const exploreAgents = [];
|
|
120
|
+
const webSearches = [];
|
|
121
|
+
const seenAgentIds = new Set();
|
|
122
|
+
const seenQueries = new Set();
|
|
123
|
+
// Build a map of agentId -> subagent file for quick lookup
|
|
124
|
+
const subagentFileMap = new Map();
|
|
125
|
+
for (const f of subagentFiles) {
|
|
126
|
+
subagentFileMap.set(f.agentId, f);
|
|
127
|
+
}
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
// Extract explore agents from agent_progress entries
|
|
130
|
+
if (entry.type === 'agent_progress' && entry.content.agentType === 'Explore') {
|
|
131
|
+
const agentId = entry.content.agentId;
|
|
132
|
+
if (agentId && !seenAgentIds.has(agentId)) {
|
|
133
|
+
seenAgentIds.add(agentId);
|
|
134
|
+
exploreAgents.push({
|
|
135
|
+
id: agentId,
|
|
136
|
+
description: entry.content.agentDescription || 'Explore codebase',
|
|
137
|
+
timestamp: entry.timestamp,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Extract web searches from web_search entries with results
|
|
142
|
+
if (entry.type === 'web_search' && entry.content.searchType === 'results') {
|
|
143
|
+
const query = entry.content.searchQuery;
|
|
144
|
+
if (query && !seenQueries.has(query)) {
|
|
145
|
+
seenQueries.add(query);
|
|
146
|
+
webSearches.push({
|
|
147
|
+
query,
|
|
148
|
+
resultCount: entry.content.searchResultCount || 0,
|
|
149
|
+
timestamp: entry.timestamp,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Compute token costs for explore agents from their subagent JSONL files
|
|
155
|
+
for (const agent of exploreAgents) {
|
|
156
|
+
const subagentFile = subagentFileMap.get(agent.id);
|
|
157
|
+
if (subagentFile) {
|
|
158
|
+
try {
|
|
159
|
+
const subEntries = await parseJSONL(subagentFile.filePath);
|
|
160
|
+
if (subEntries.length > 0) {
|
|
161
|
+
const subStats = getEntryStatistics(subEntries);
|
|
162
|
+
// Total cost = last turn's context window size + estimated output
|
|
163
|
+
const inputCost = subStats.lastInputTokens + subStats.lastCacheRead;
|
|
164
|
+
const outputCost = subStats.totalOutputTokensEstimated;
|
|
165
|
+
agent.tokenCost = inputCost + outputCost;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
logger.warn(`Failed to parse subagent ${agent.id}:`, getErrorMessage(err));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { exploreAgents, webSearches };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Convert a catalog SubagentEntry (type=exploration) to an ExploreAgentRef.
|
|
177
|
+
*/
|
|
178
|
+
function catalogSubagentToExploreRef(entry) {
|
|
179
|
+
return {
|
|
180
|
+
id: entry.id,
|
|
181
|
+
description: entry.title,
|
|
182
|
+
timestamp: entry.timestamp,
|
|
183
|
+
tokenCost: entry.tokenCost,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Convert a catalog SubagentEntry (type=search) to a WebSearchRef.
|
|
188
|
+
*/
|
|
189
|
+
function catalogSubagentToSearchRef(entry) {
|
|
190
|
+
return {
|
|
191
|
+
query: entry.title,
|
|
192
|
+
resultCount: entry.resultCount || 0,
|
|
193
|
+
timestamp: entry.timestamp,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Convert a catalog PlanEntry to a partial PlanRef.
|
|
198
|
+
* messageIndex is set to 0 since catalog doesn't track this.
|
|
199
|
+
*/
|
|
200
|
+
function catalogPlanToPlanRef(plan) {
|
|
201
|
+
return {
|
|
202
|
+
title: plan.title,
|
|
203
|
+
source: "embedded",
|
|
204
|
+
messageIndex: 0,
|
|
205
|
+
catalogId: plan.id,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Read the session manifest JSON from .jacques/sessions/{id}.json.
|
|
210
|
+
* Returns null if file doesn't exist or is unreadable.
|
|
211
|
+
*/
|
|
212
|
+
async function readSessionManifest(projectPath, sessionId) {
|
|
213
|
+
try {
|
|
214
|
+
const manifestPath = path.join(projectPath, ".jacques", "sessions", `${sessionId}.json`);
|
|
215
|
+
const content = await fs.readFile(manifestPath, "utf-8");
|
|
216
|
+
return JSON.parse(content);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
if (!isNotFoundError(err)) {
|
|
220
|
+
logger.warn(`Failed to read session manifest ${sessionId}:`, getErrorMessage(err));
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Extract metadata from a single JSONL file
|
|
227
|
+
*/
|
|
228
|
+
export async function extractSessionMetadata(jsonlPath, projectPath, projectSlug) {
|
|
229
|
+
try {
|
|
230
|
+
// Get file stats
|
|
231
|
+
const stats = await fs.stat(jsonlPath);
|
|
232
|
+
const sessionId = path.basename(jsonlPath, ".jsonl");
|
|
233
|
+
// Parse JSONL to get metadata
|
|
234
|
+
const entries = await parseJSONL(jsonlPath);
|
|
235
|
+
if (entries.length === 0) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
// Get statistics
|
|
239
|
+
const entryStats = getEntryStatistics(entries);
|
|
240
|
+
// Get timestamps
|
|
241
|
+
const { startedAt, endedAt } = extractTimestamps(entries);
|
|
242
|
+
// Get title — resolve continue sessions from handoff data
|
|
243
|
+
let title = extractTitle(entries);
|
|
244
|
+
if (isContinueSession(title)) {
|
|
245
|
+
const continueTitle = await extractContinueTitleFromHandoff(projectPath, startedAt);
|
|
246
|
+
if (continueTitle)
|
|
247
|
+
title = continueTitle;
|
|
248
|
+
}
|
|
249
|
+
// Check for subagents
|
|
250
|
+
const subagentFiles = await listSubagentFiles(jsonlPath);
|
|
251
|
+
// Filter out internal agents (prompt_suggestion, acompact) from user-visible count
|
|
252
|
+
// These are system agents that shouldn't appear in the subagent count
|
|
253
|
+
const userVisibleSubagents = subagentFiles.filter((f) => !f.agentId.startsWith('aprompt_suggestion-') &&
|
|
254
|
+
!f.agentId.startsWith('acompact-'));
|
|
255
|
+
// Track if auto-compact occurred (for showing indicator in UI)
|
|
256
|
+
const autoCompactFile = subagentFiles.find((f) => f.agentId.startsWith('acompact-'));
|
|
257
|
+
const hadAutoCompact = !!autoCompactFile;
|
|
258
|
+
const autoCompactAt = autoCompactFile?.modifiedAt.toISOString();
|
|
259
|
+
const hasSubagents = userVisibleSubagents.length > 0;
|
|
260
|
+
// Detect mode and plans
|
|
261
|
+
const { mode, planRefs } = detectModeAndPlans(entries);
|
|
262
|
+
// Extract explore agents and web searches (with token costs from subagent files)
|
|
263
|
+
const { exploreAgents, webSearches } = await extractAgentsAndSearches(entries, subagentFiles);
|
|
264
|
+
// Detect git info from project path
|
|
265
|
+
const gitInfo = await detectGitInfo(projectPath);
|
|
266
|
+
// If detectGitInfo failed (e.g., deleted worktree), read gitBranch from raw JSONL
|
|
267
|
+
if (!gitInfo.branch) {
|
|
268
|
+
gitInfo.branch = await readGitBranchFromJsonl(jsonlPath) || undefined;
|
|
269
|
+
}
|
|
270
|
+
// Use LAST turn's input tokens for context window size
|
|
271
|
+
// Each turn reports the FULL context, so summing would overcount
|
|
272
|
+
// Total context = fresh input + cache read (cache_creation is subset of fresh, not additional)
|
|
273
|
+
const totalInput = entryStats.lastInputTokens + entryStats.lastCacheRead;
|
|
274
|
+
// Use tiktoken-estimated output tokens (cumulative - each turn generates NEW output)
|
|
275
|
+
const totalOutput = entryStats.totalOutputTokensEstimated;
|
|
276
|
+
const hasTokens = totalInput > 0 || totalOutput > 0;
|
|
277
|
+
return {
|
|
278
|
+
id: sessionId,
|
|
279
|
+
jsonlPath,
|
|
280
|
+
projectPath,
|
|
281
|
+
projectSlug,
|
|
282
|
+
title,
|
|
283
|
+
startedAt,
|
|
284
|
+
endedAt,
|
|
285
|
+
messageCount: entryStats.userMessages + entryStats.assistantMessages,
|
|
286
|
+
toolCallCount: entryStats.toolCalls,
|
|
287
|
+
hasSubagents,
|
|
288
|
+
subagentIds: hasSubagents
|
|
289
|
+
? userVisibleSubagents.map((f) => f.agentId)
|
|
290
|
+
: undefined,
|
|
291
|
+
hadAutoCompact: hadAutoCompact || undefined,
|
|
292
|
+
autoCompactAt: autoCompactAt || undefined,
|
|
293
|
+
tokens: hasTokens ? {
|
|
294
|
+
input: totalInput,
|
|
295
|
+
output: totalOutput,
|
|
296
|
+
cacheCreation: entryStats.lastCacheCreation,
|
|
297
|
+
cacheRead: entryStats.lastCacheRead,
|
|
298
|
+
} : undefined,
|
|
299
|
+
fileSizeBytes: stats.size,
|
|
300
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
301
|
+
mode: mode || undefined,
|
|
302
|
+
planCount: planRefs.length > 0 ? planRefs.length : undefined,
|
|
303
|
+
planRefs: planRefs.length > 0 ? planRefs : undefined,
|
|
304
|
+
gitRepoRoot: gitInfo.repoRoot || undefined,
|
|
305
|
+
gitBranch: gitInfo.branch || undefined,
|
|
306
|
+
gitWorktree: gitInfo.worktree || undefined,
|
|
307
|
+
exploreAgents: exploreAgents.length > 0 ? exploreAgents : undefined,
|
|
308
|
+
webSearches: webSearches.length > 0 ? webSearches : undefined,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
const sessionId = path.basename(jsonlPath, ".jsonl");
|
|
313
|
+
logger.error(`Failed to extract metadata for ${sessionId}:`, getErrorMessage(err));
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* List all project directories in ~/.claude/projects/
|
|
319
|
+
*/
|
|
320
|
+
export async function listAllProjects() {
|
|
321
|
+
const projects = [];
|
|
322
|
+
try {
|
|
323
|
+
const entries = await fs.readdir(CLAUDE_PROJECTS_PATH, {
|
|
324
|
+
withFileTypes: true,
|
|
325
|
+
});
|
|
326
|
+
const dirs = entries.filter((e) => e.isDirectory());
|
|
327
|
+
// Decode all project paths in parallel (each reads sessions-index.json or first JSONL)
|
|
328
|
+
const results = await Promise.all(dirs.map(async (entry) => {
|
|
329
|
+
const projectPath = await decodeProjectPath(entry.name);
|
|
330
|
+
const projectSlug = path.basename(projectPath);
|
|
331
|
+
return {
|
|
332
|
+
encodedPath: path.join(CLAUDE_PROJECTS_PATH, entry.name),
|
|
333
|
+
projectPath,
|
|
334
|
+
projectSlug,
|
|
335
|
+
};
|
|
336
|
+
}));
|
|
337
|
+
projects.push(...results);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
if (!isNotFoundError(err)) {
|
|
341
|
+
logger.warn("Failed to list projects:", getErrorMessage(err));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return projects;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Build session entries from catalog data (fast path).
|
|
348
|
+
*
|
|
349
|
+
* For each project:
|
|
350
|
+
* 1. Read .jacques/index.json for catalog metadata
|
|
351
|
+
* 2. List JSONL files in the encoded project dir
|
|
352
|
+
* 3. Stat JSONL files for size/mtime (in parallel)
|
|
353
|
+
* 4. Convert catalog entries to SessionEntry
|
|
354
|
+
* 5. Identify uncataloged JSONL files for fallback parsing
|
|
355
|
+
*/
|
|
356
|
+
async function buildFromCatalog(projects) {
|
|
357
|
+
const catalogSessions = [];
|
|
358
|
+
const uncatalogedFiles = [];
|
|
359
|
+
for (const project of projects) {
|
|
360
|
+
// Read catalog index (returns empty default if missing)
|
|
361
|
+
const index = await readProjectIndex(project.projectPath);
|
|
362
|
+
// List JSONL files in the encoded project directory
|
|
363
|
+
let jsonlFilenames = [];
|
|
364
|
+
try {
|
|
365
|
+
const dirEntries = await fs.readdir(project.encodedPath, { withFileTypes: true });
|
|
366
|
+
jsonlFilenames = dirEntries
|
|
367
|
+
.filter((e) => e.isFile() && e.name.endsWith(".jsonl"))
|
|
368
|
+
.map((e) => e.name);
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
if (!isNotFoundError(err)) {
|
|
372
|
+
logger.warn(`Skipping unreadable project dir ${project.projectSlug}:`, getErrorMessage(err));
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
// Build set of cataloged session IDs
|
|
377
|
+
const catalogedSessionIds = new Set(index.sessions.map((s) => s.id));
|
|
378
|
+
// Stat all JSONL files in parallel
|
|
379
|
+
const statResults = await Promise.all(jsonlFilenames.map(async (filename) => {
|
|
380
|
+
const jsonlPath = path.join(project.encodedPath, filename);
|
|
381
|
+
const sessionId = path.basename(filename, ".jsonl");
|
|
382
|
+
try {
|
|
383
|
+
const stats = await fs.stat(jsonlPath);
|
|
384
|
+
return { sessionId, jsonlPath, stats, filename };
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
return null; // File disappeared between readdir and stat (race condition)
|
|
388
|
+
}
|
|
389
|
+
}));
|
|
390
|
+
// Detect git info ONCE per project (not per session) — avoids spawning
|
|
391
|
+
// a git subprocess for every JSONL file in the same project directory.
|
|
392
|
+
const projectGitInfo = await detectGitInfo(project.projectPath);
|
|
393
|
+
if (!projectGitInfo.branch && jsonlFilenames.length > 0) {
|
|
394
|
+
projectGitInfo.branch = await readGitBranchFromJsonl(path.join(project.encodedPath, jsonlFilenames[0])) || undefined;
|
|
395
|
+
}
|
|
396
|
+
// If detectGitInfo failed (e.g., deleted worktree), try recovering
|
|
397
|
+
// gitRepoRoot from a previously-extracted session manifest.
|
|
398
|
+
if (!projectGitInfo.repoRoot) {
|
|
399
|
+
for (const result of statResults) {
|
|
400
|
+
if (!result)
|
|
401
|
+
continue;
|
|
402
|
+
const manifest = await readSessionManifest(project.projectPath, result.sessionId);
|
|
403
|
+
if (manifest?.gitRepoRoot) {
|
|
404
|
+
projectGitInfo.repoRoot = manifest.gitRepoRoot;
|
|
405
|
+
if (!projectGitInfo.branch && manifest.gitBranch) {
|
|
406
|
+
projectGitInfo.branch = manifest.gitBranch;
|
|
407
|
+
}
|
|
408
|
+
if (!projectGitInfo.worktree && manifest.gitWorktree) {
|
|
409
|
+
projectGitInfo.worktree = manifest.gitWorktree;
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const result of statResults) {
|
|
416
|
+
if (!result)
|
|
417
|
+
continue;
|
|
418
|
+
const { sessionId, jsonlPath, stats } = result;
|
|
419
|
+
if (!catalogedSessionIds.has(sessionId)) {
|
|
420
|
+
// Not in catalog - needs JSONL parsing
|
|
421
|
+
uncatalogedFiles.push({
|
|
422
|
+
filePath: jsonlPath,
|
|
423
|
+
projectPath: project.projectPath,
|
|
424
|
+
projectSlug: project.projectSlug,
|
|
425
|
+
});
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
// Find catalog session entry
|
|
429
|
+
const catalogSession = index.sessions.find((s) => s.id === sessionId);
|
|
430
|
+
if (!catalogSession)
|
|
431
|
+
continue;
|
|
432
|
+
// Staleness check: if JSONL is newer than catalog savedAt, re-parse
|
|
433
|
+
const jsonlMtime = stats.mtime.toISOString();
|
|
434
|
+
// Read the session manifest for planRefs and precise mtime check
|
|
435
|
+
const manifest = await readSessionManifest(project.projectPath, sessionId);
|
|
436
|
+
if (catalogSession.savedAt && jsonlMtime > catalogSession.savedAt) {
|
|
437
|
+
if (!manifest || jsonlMtime > manifest.jsonlModifiedAt) {
|
|
438
|
+
uncatalogedFiles.push({
|
|
439
|
+
filePath: jsonlPath,
|
|
440
|
+
projectPath: project.projectPath,
|
|
441
|
+
projectSlug: project.projectSlug,
|
|
442
|
+
});
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// Map subagents from index
|
|
447
|
+
const exploreSubagents = index.subagents.filter((s) => s.sessionId === sessionId && s.type === "exploration");
|
|
448
|
+
const searchSubagents = index.subagents.filter((s) => s.sessionId === sessionId && s.type === "search");
|
|
449
|
+
// Use planRefs from manifest (preserves source: embedded/write/agent)
|
|
450
|
+
// Fall back to reconstructing from PlanEntry if manifest lacks planRefs
|
|
451
|
+
let planRefs = [];
|
|
452
|
+
if (manifest?.planRefs && manifest.planRefs.length > 0) {
|
|
453
|
+
// Manifest has full planRefs with correct source types
|
|
454
|
+
planRefs = manifest.planRefs.map((ref) => {
|
|
455
|
+
// Find matching catalogId from planIds
|
|
456
|
+
const catalogId = catalogSession.planIds?.find((pid) => index.plans.some((p) => p.id === pid));
|
|
457
|
+
return {
|
|
458
|
+
title: ref.title,
|
|
459
|
+
source: ref.source,
|
|
460
|
+
messageIndex: ref.messageIndex,
|
|
461
|
+
filePath: ref.filePath,
|
|
462
|
+
agentId: ref.agentId,
|
|
463
|
+
catalogId: ref.catalogId || catalogId,
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
else if (catalogSession.planIds) {
|
|
468
|
+
// Fallback: reconstruct from PlanEntry (older manifests without planRefs)
|
|
469
|
+
for (const planId of catalogSession.planIds) {
|
|
470
|
+
const plan = index.plans.find((p) => p.id === planId);
|
|
471
|
+
if (plan) {
|
|
472
|
+
planRefs.push(catalogPlanToPlanRef(plan));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const exploreAgents = exploreSubagents.map(catalogSubagentToExploreRef);
|
|
477
|
+
const webSearches = searchSubagents.map(catalogSubagentToSearchRef);
|
|
478
|
+
// Resolve continue session titles from handoff data
|
|
479
|
+
let resolvedTitle = catalogSession.title;
|
|
480
|
+
if (resolvedTitle && isContinueSession(resolvedTitle)) {
|
|
481
|
+
const continueTitle = await extractContinueTitleFromHandoff(project.projectPath, catalogSession.startedAt);
|
|
482
|
+
if (continueTitle)
|
|
483
|
+
resolvedTitle = continueTitle;
|
|
484
|
+
}
|
|
485
|
+
// Build SessionEntry from catalog data + file stats
|
|
486
|
+
const entry = {
|
|
487
|
+
id: sessionId,
|
|
488
|
+
jsonlPath,
|
|
489
|
+
projectPath: project.projectPath,
|
|
490
|
+
projectSlug: project.projectSlug,
|
|
491
|
+
title: resolvedTitle,
|
|
492
|
+
startedAt: catalogSession.startedAt,
|
|
493
|
+
endedAt: catalogSession.endedAt,
|
|
494
|
+
messageCount: catalogSession.messageCount,
|
|
495
|
+
toolCallCount: catalogSession.toolCallCount,
|
|
496
|
+
hasSubagents: catalogSession.hasSubagents ?? false,
|
|
497
|
+
subagentIds: catalogSession.subagentIds,
|
|
498
|
+
hadAutoCompact: catalogSession.hadAutoCompact || undefined,
|
|
499
|
+
tokens: catalogSession.tokens,
|
|
500
|
+
fileSizeBytes: stats.size,
|
|
501
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
502
|
+
mode: catalogSession.mode || undefined,
|
|
503
|
+
planCount: planRefs.length > 0 ? planRefs.length : (catalogSession.planCount || undefined),
|
|
504
|
+
planRefs: planRefs.length > 0 ? planRefs : undefined,
|
|
505
|
+
gitRepoRoot: projectGitInfo.repoRoot || undefined,
|
|
506
|
+
gitBranch: projectGitInfo.branch || undefined,
|
|
507
|
+
gitWorktree: projectGitInfo.worktree || undefined,
|
|
508
|
+
exploreAgents: exploreAgents.length > 0 ? exploreAgents : undefined,
|
|
509
|
+
webSearches: webSearches.length > 0 ? webSearches : undefined,
|
|
510
|
+
};
|
|
511
|
+
catalogSessions.push(entry);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return { catalogSessions, uncatalogedFiles };
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Scan all sessions and build the index.
|
|
518
|
+
*
|
|
519
|
+
* Uses catalog-first loading: reads pre-extracted metadata from .jacques/index.json
|
|
520
|
+
* for each project, only falling back to JSONL parsing for new/uncataloged sessions.
|
|
521
|
+
*/
|
|
522
|
+
export async function buildSessionIndex(options) {
|
|
523
|
+
const { onProgress } = options || {};
|
|
524
|
+
onProgress?.({
|
|
525
|
+
phase: "scanning",
|
|
526
|
+
total: 0,
|
|
527
|
+
completed: 0,
|
|
528
|
+
current: "Scanning projects...",
|
|
529
|
+
});
|
|
530
|
+
// Get all projects
|
|
531
|
+
const projects = await listAllProjects();
|
|
532
|
+
// Phase 1: Read catalog data (fast - reads .jacques/index.json + stats JSONL files)
|
|
533
|
+
const { catalogSessions, uncatalogedFiles } = await buildFromCatalog(projects);
|
|
534
|
+
const totalFiles = catalogSessions.length + uncatalogedFiles.length;
|
|
535
|
+
onProgress?.({
|
|
536
|
+
phase: "processing",
|
|
537
|
+
total: totalFiles,
|
|
538
|
+
completed: catalogSessions.length,
|
|
539
|
+
current: `${catalogSessions.length} from catalog, ${uncatalogedFiles.length} to parse...`,
|
|
540
|
+
});
|
|
541
|
+
// Phase 2: Parse only uncataloged/stale sessions (slow path - only for new sessions)
|
|
542
|
+
const sessions = [...catalogSessions];
|
|
543
|
+
for (let i = 0; i < uncatalogedFiles.length; i++) {
|
|
544
|
+
const file = uncatalogedFiles[i];
|
|
545
|
+
const sessionId = path.basename(file.filePath, ".jsonl");
|
|
546
|
+
onProgress?.({
|
|
547
|
+
phase: "processing",
|
|
548
|
+
total: totalFiles,
|
|
549
|
+
completed: catalogSessions.length + i,
|
|
550
|
+
current: `${file.projectSlug}/${sessionId.substring(0, 8)}...`,
|
|
551
|
+
});
|
|
552
|
+
const metadata = await extractSessionMetadata(file.filePath, file.projectPath, file.projectSlug);
|
|
553
|
+
if (metadata) {
|
|
554
|
+
sessions.push(metadata);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// Sort by modification time (newest first)
|
|
558
|
+
sessions.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
|
|
559
|
+
const index = {
|
|
560
|
+
version: "2.0.0",
|
|
561
|
+
lastScanned: new Date().toISOString(),
|
|
562
|
+
sessions,
|
|
563
|
+
};
|
|
564
|
+
// Save to disk
|
|
565
|
+
await writeSessionIndex(index);
|
|
566
|
+
onProgress?.({
|
|
567
|
+
phase: "processing",
|
|
568
|
+
total: totalFiles,
|
|
569
|
+
completed: totalFiles,
|
|
570
|
+
current: "Complete",
|
|
571
|
+
});
|
|
572
|
+
return index;
|
|
573
|
+
}
|
|
574
|
+
//# sourceMappingURL=metadata-extractor.js.map
|