upfynai-code 3.0.4 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -92
- package/bin/cli.js +191 -0
- package/dist/client/assets/AppContent-M14Au3SB.js +542 -0
- package/{client/dist/assets/BrowserPanel-0TLEl-IC.js → dist/client/assets/BrowserPanel-TFKm2NDJ.js} +2 -2
- package/dist/client/assets/DashboardPanel-C88HjsCh.js +1 -0
- package/dist/client/assets/FileTree-DvO1xnDE.js +1 -0
- package/{client/dist/assets/GitPanel-C_xFM-N2.js → dist/client/assets/GitPanel-D-slVlyy.js} +2 -2
- package/dist/client/assets/LoginModal-Chi4SYcr.js +21 -0
- package/{client/dist/assets/MarkdownPreview-CESjI261.js → dist/client/assets/MarkdownPreview-CuIix2u9.js} +1 -1
- package/dist/client/assets/MermaidBlock-Dq9uFv82.js +2 -0
- package/dist/client/assets/Onboarding-QYXx24dX.js +1 -0
- package/{client/dist/assets/PreviewPanel-CqCa92Tf.js → dist/client/assets/PreviewPanel-Dd8q-jo0.js} +1 -1
- package/dist/client/assets/SetupForm-CrspaUva.js +1 -0
- package/dist/client/assets/WorkflowsPanel-DIlYAdhB.js +1 -0
- package/dist/client/assets/index-CnNNzw9A.css +1 -0
- package/{client/dist/assets/index-HaY-3pK1.js → dist/client/assets/index-rUkK9FDP.js} +26 -26
- package/{client/dist/assets/vendor-codemirror-D2ALgpaX.js → dist/client/assets/vendor-codemirror-jc6nyJQg.js} +1 -1
- package/{client/dist/assets/vendor-diff-DNQpbhrT.js → dist/client/assets/vendor-diff-THJmAcEI.js} +1 -1
- package/{client/dist/assets/vendor-icons-GyYE35HP.js → dist/client/assets/vendor-icons-CfjIpdrD.js} +145 -155
- package/{client/dist/assets/vendor-markdown-CimbIo6Y.js → dist/client/assets/vendor-markdown-Cdm6NEGf.js} +1 -1
- package/dist/client/assets/vendor-mermaid-DTPaBx-U.js +2559 -0
- package/{client/dist/assets/vendor-react-96lCPsRK.js → dist/client/assets/vendor-react-wFkb6mSf.js} +1 -1
- package/{client/dist/assets/vendor-syntax-LS_Nt30I.js → dist/client/assets/vendor-syntax-C_UZR7tc.js} +1 -1
- package/dist/client/favicon.png +0 -0
- package/dist/client/icons/icon-128x128.png +0 -0
- package/dist/client/icons/icon-144x144.png +0 -0
- package/dist/client/icons/icon-152x152.png +0 -0
- package/dist/client/icons/icon-192x192.png +0 -0
- package/dist/client/icons/icon-384x384.png +0 -0
- package/dist/client/icons/icon-512x512.png +0 -0
- package/dist/client/icons/icon-72x72.png +0 -0
- package/dist/client/icons/icon-96x96.png +0 -0
- package/{client/dist → dist/client}/index.html +37 -36
- package/dist/client/logo-128.png +0 -0
- package/dist/client/logo-256.png +0 -0
- package/dist/client/logo-32.png +0 -0
- package/dist/client/logo-512.png +0 -0
- package/dist/client/logo-64.png +0 -0
- package/dist/client/logo.png +0 -0
- package/{client/dist → dist/client}/manifest.json +12 -12
- package/{client/dist → dist/client}/mcp-docs.html +1 -1
- package/{client/dist → dist/client}/sw.js +2 -2
- package/package.json +56 -105
- package/scripts/postinstall.js +9 -0
- package/scripts/prepublish.js +77 -0
- package/src/animation.js +228 -0
- package/src/auth.js +142 -0
- package/src/config.js +40 -0
- package/src/connect.js +416 -0
- package/src/launch.js +81 -0
- package/src/mcp.js +57 -0
- package/src/permissions.js +140 -0
- package/src/persistent-shell.js +261 -0
- package/src/server.js +54 -0
- package/client/dist/assets/AppContent-CwrTP6TW.js +0 -545
- package/client/dist/assets/CanvasFullScreen-D1GWQsGL.js +0 -1
- package/client/dist/assets/CanvasWorkspace-D7ORj358.js +0 -163
- package/client/dist/assets/DashboardPanel-BV7ybUDe.js +0 -1
- package/client/dist/assets/FileTree-5qfhBqdE.js +0 -1
- package/client/dist/assets/LoginModal-CImJHRjX.js +0 -13
- package/client/dist/assets/MermaidBlock-BFM21cwe.js +0 -2
- package/client/dist/assets/Onboarding-B3cteLu2.js +0 -1
- package/client/dist/assets/SetupForm-P6dsYgHO.js +0 -1
- package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +0 -1
- package/client/dist/assets/index-46kkVu2i.css +0 -1
- package/client/dist/assets/pdf-CE_K4jFx.js +0 -12
- package/client/dist/assets/vendor-canvas-BZV40eAE.css +0 -1
- package/client/dist/assets/vendor-canvas-DvHJ_Pn2.js +0 -49
- package/client/dist/assets/vendor-mermaid-DucWyDEe.js +0 -2556
- package/client/dist/favicon.png +0 -0
- package/client/dist/icons/icon-128x128.png +0 -0
- package/client/dist/icons/icon-144x144.png +0 -0
- package/client/dist/icons/icon-152x152.png +0 -0
- package/client/dist/icons/icon-192x192.png +0 -0
- package/client/dist/icons/icon-384x384.png +0 -0
- package/client/dist/icons/icon-512x512.png +0 -0
- package/client/dist/icons/icon-72x72.png +0 -0
- package/client/dist/icons/icon-96x96.png +0 -0
- package/client/dist/logo-128.png +0 -0
- package/client/dist/logo-256.png +0 -0
- package/client/dist/logo-32.png +0 -0
- package/client/dist/logo-512.png +0 -0
- package/client/dist/logo-64.png +0 -0
- package/commands/upfynai-connect.md +0 -59
- package/commands/upfynai-disconnect.md +0 -31
- package/commands/upfynai-doctor.md +0 -99
- package/commands/upfynai-export.md +0 -49
- package/commands/upfynai-local.md +0 -82
- package/commands/upfynai-status.md +0 -75
- package/commands/upfynai-stop.md +0 -49
- package/commands/upfynai-uninstall.md +0 -58
- package/commands/upfynai.md +0 -69
- package/scripts/build-client.js +0 -17
- package/scripts/fix-node-pty.js +0 -67
- package/scripts/install-commands.js +0 -78
- package/server/agent-loop.js +0 -242
- package/server/auto-compact.js +0 -99
- package/server/browser.js +0 -131
- package/server/claude-sdk.js +0 -797
- package/server/cli-ui.js +0 -798
- package/server/cli.js +0 -751
- package/server/constants/config.js +0 -31
- package/server/cursor-cli.js +0 -270
- package/server/database/auth.db +0 -0
- package/server/database/db.js +0 -1547
- package/server/database/init.sql +0 -70
- package/server/index.js +0 -3813
- package/server/load-env.js +0 -26
- package/server/mcp-server.js +0 -621
- package/server/middleware/auth.js +0 -184
- package/server/middleware/relayHelpers.js +0 -44
- package/server/middleware/sandboxRouter.js +0 -174
- package/server/openai-codex.js +0 -403
- package/server/openrouter.js +0 -137
- package/server/projects.js +0 -1807
- package/server/provider-factory.js +0 -174
- package/server/relay-client.js +0 -390
- package/server/routes/agent.js +0 -1234
- package/server/routes/auth.js +0 -559
- package/server/routes/browser.js +0 -419
- package/server/routes/canvas.js +0 -53
- package/server/routes/cli-auth.js +0 -263
- package/server/routes/codex.js +0 -396
- package/server/routes/commands.js +0 -707
- package/server/routes/composio.js +0 -176
- package/server/routes/cursor.js +0 -770
- package/server/routes/dashboard.js +0 -295
- package/server/routes/git.js +0 -1208
- package/server/routes/keys.js +0 -34
- package/server/routes/mcp-utils.js +0 -48
- package/server/routes/mcp.js +0 -661
- package/server/routes/payments.js +0 -227
- package/server/routes/projects.js +0 -754
- package/server/routes/sessions.js +0 -146
- package/server/routes/settings.js +0 -261
- package/server/routes/taskmaster.js +0 -1928
- package/server/routes/user.js +0 -106
- package/server/routes/vapi-chat.js +0 -624
- package/server/routes/voice.js +0 -235
- package/server/routes/webhooks.js +0 -166
- package/server/routes/workflows.js +0 -312
- package/server/sandbox.js +0 -120
- package/server/services/browser-ai.js +0 -154
- package/server/services/composio.js +0 -204
- package/server/services/sessionRegistry.js +0 -139
- package/server/services/whisperService.js +0 -84
- package/server/services/workflowScheduler.js +0 -211
- package/server/tests/relay-flow.test.js +0 -570
- package/server/tests/sessions.test.js +0 -259
- package/server/utils/commandParser.js +0 -303
- package/server/utils/email.js +0 -66
- package/server/utils/gitConfig.js +0 -24
- package/server/utils/mcp-detector.js +0 -198
- package/server/utils/taskmaster-websocket.js +0 -129
- package/shared/integrationCatalog.d.ts +0 -12
- package/shared/integrationCatalog.js +0 -172
- package/shared/modelConstants.js +0 -96
- /package/{shared → dist}/agents/claude.js +0 -0
- /package/{shared → dist}/agents/codex.js +0 -0
- /package/{shared → dist}/agents/cursor.js +0 -0
- /package/{shared → dist}/agents/detect.js +0 -0
- /package/{shared → dist}/agents/exec.js +0 -0
- /package/{shared → dist}/agents/files.js +0 -0
- /package/{shared → dist}/agents/git.js +0 -0
- /package/{shared → dist}/agents/gitagent.js +0 -0
- /package/{shared → dist}/agents/index.js +0 -0
- /package/{shared → dist}/agents/shell.js +0 -0
- /package/{shared → dist}/agents/utils.js +0 -0
- /package/{client/dist → dist/client}/api-docs.html +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
- /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
- /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
- /package/{client/dist → dist/client}/clear-cache.html +0 -0
- /package/{client/dist → dist/client}/convert-icons.md +0 -0
- /package/{client/dist → dist/client}/favicon.svg +0 -0
- /package/{client/dist → dist/client}/generate-icons.js +0 -0
- /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
- /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
- /package/{client/dist → dist/client}/icons/codex.svg +0 -0
- /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
- /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
- /package/{client/dist → dist/client}/logo.svg +0 -0
- /package/{client/dist → dist/client}/offline.html +0 -0
- /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
- /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
- /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
- /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
- /package/{shared → dist}/gitagent/index.js +0 -0
- /package/{shared → dist}/gitagent/parser.js +0 -0
- /package/{shared → dist}/gitagent/prompt-builder.js +0 -0
package/server/projects.js
DELETED
|
@@ -1,1807 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PROJECT DISCOVERY AND MANAGEMENT SYSTEM
|
|
3
|
-
* ========================================
|
|
4
|
-
*
|
|
5
|
-
* This module manages project discovery for both Claude CLI and Cursor CLI sessions.
|
|
6
|
-
*
|
|
7
|
-
* ## Architecture Overview
|
|
8
|
-
*
|
|
9
|
-
* 1. **Claude Projects** (stored in ~/.claude/projects/)
|
|
10
|
-
* - Each project is a directory named with the project path encoded (/ replaced with -)
|
|
11
|
-
* - Contains .jsonl files with conversation history including 'cwd' field
|
|
12
|
-
* - Project metadata stored in ~/.claude/project-config.json
|
|
13
|
-
*
|
|
14
|
-
* 2. **Cursor Projects** (stored in ~/.cursor/chats/)
|
|
15
|
-
* - Each project directory is named with MD5 hash of the absolute project path
|
|
16
|
-
* - Example: /Users/john/myproject -> MD5 -> a1b2c3d4e5f6...
|
|
17
|
-
* - Contains session directories with SQLite databases (store.db)
|
|
18
|
-
* - Project path is NOT stored in the database - only in the MD5 hash
|
|
19
|
-
*
|
|
20
|
-
* ## Project Discovery Strategy
|
|
21
|
-
*
|
|
22
|
-
* 1. **Claude Projects Discovery**:
|
|
23
|
-
* - Scan ~/.claude/projects/ directory for Claude project folders
|
|
24
|
-
* - Extract actual project path from .jsonl files (cwd field)
|
|
25
|
-
* - Fall back to decoded directory name if no sessions exist
|
|
26
|
-
*
|
|
27
|
-
* 2. **Cursor Sessions Discovery**:
|
|
28
|
-
* - For each KNOWN project (from Claude or manually added)
|
|
29
|
-
* - Compute MD5 hash of the project's absolute path
|
|
30
|
-
* - Check if ~/.cursor/chats/{md5_hash}/ directory exists
|
|
31
|
-
* - Read session metadata from SQLite store.db files
|
|
32
|
-
*
|
|
33
|
-
* 3. **Manual Project Addition**:
|
|
34
|
-
* - Users can manually add project paths via UI
|
|
35
|
-
* - Stored in ~/.claude/project-config.json with 'manuallyAdded' flag
|
|
36
|
-
* - Allows discovering Cursor sessions for projects without Claude sessions
|
|
37
|
-
*
|
|
38
|
-
* ## Critical Limitations
|
|
39
|
-
*
|
|
40
|
-
* - **CANNOT discover Cursor-only projects**: From a quick check, there was no mention of
|
|
41
|
-
* the cwd of each project. if someone has the time, you can try to reverse engineer it.
|
|
42
|
-
*
|
|
43
|
-
* - **Project relocation breaks history**: If a project directory is moved or renamed,
|
|
44
|
-
* the MD5 hash changes, making old Cursor sessions inaccessible unless the old
|
|
45
|
-
* path is known and manually added.
|
|
46
|
-
*
|
|
47
|
-
* ## Error Handling
|
|
48
|
-
*
|
|
49
|
-
* - Missing ~/.claude directory is handled gracefully with automatic creation
|
|
50
|
-
* - ENOENT errors are caught and handled without crashing
|
|
51
|
-
* - Empty arrays returned when no projects/sessions exist
|
|
52
|
-
*
|
|
53
|
-
* ## Caching Strategy
|
|
54
|
-
*
|
|
55
|
-
* - Project directory extraction is cached to minimize file I/O
|
|
56
|
-
* - Cache is cleared when project configuration changes
|
|
57
|
-
* - Session data is fetched on-demand, not cached
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
|
-
import { promises as fs } from 'fs';
|
|
61
|
-
import fsSync from 'fs';
|
|
62
|
-
import path from 'path';
|
|
63
|
-
import readline from 'readline';
|
|
64
|
-
import crypto from 'crypto';
|
|
65
|
-
// sqlite3 is a native module — conditionally imported (not available on Vercel)
|
|
66
|
-
let sqlite3 = null;
|
|
67
|
-
let sqliteOpen = null;
|
|
68
|
-
try {
|
|
69
|
-
sqlite3 = (await import('sqlite3')).default;
|
|
70
|
-
sqliteOpen = (await import('sqlite')).open;
|
|
71
|
-
} catch (e) {
|
|
72
|
-
console.warn('[WARN] sqlite3/sqlite not available. Project scanning from SQLite databases disabled.');
|
|
73
|
-
}
|
|
74
|
-
import os from 'os';
|
|
75
|
-
import yaml from 'js-yaml';
|
|
76
|
-
import { projectDb } from './database/db.js';
|
|
77
|
-
|
|
78
|
-
const IS_CLOUD_ENV = !!(process.env.RAILWAY_ENVIRONMENT || process.env.VERCEL || process.env.RENDER);
|
|
79
|
-
|
|
80
|
-
// Cloud mode: userId passed through function calls (no module-level state)
|
|
81
|
-
function setCloudUserId() { /* DEPRECATED — userId now passed directly */ }
|
|
82
|
-
|
|
83
|
-
// Import TaskMaster detection functions
|
|
84
|
-
async function detectTaskMasterFolder(projectPath) {
|
|
85
|
-
try {
|
|
86
|
-
const taskMasterPath = path.join(projectPath, '.taskmaster');
|
|
87
|
-
|
|
88
|
-
// Check if .taskmaster directory exists
|
|
89
|
-
try {
|
|
90
|
-
const stats = await fs.stat(taskMasterPath);
|
|
91
|
-
if (!stats.isDirectory()) {
|
|
92
|
-
return {
|
|
93
|
-
hasTaskmaster: false,
|
|
94
|
-
reason: '.taskmaster exists but is not a directory'
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
} catch (error) {
|
|
98
|
-
if (error.code === 'ENOENT') {
|
|
99
|
-
return {
|
|
100
|
-
hasTaskmaster: false,
|
|
101
|
-
reason: '.taskmaster directory not found'
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Check for key TaskMaster files
|
|
108
|
-
const keyFiles = [
|
|
109
|
-
'tasks/tasks.json',
|
|
110
|
-
'config.json'
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
const fileStatus = {};
|
|
114
|
-
let hasEssentialFiles = true;
|
|
115
|
-
|
|
116
|
-
for (const file of keyFiles) {
|
|
117
|
-
const filePath = path.join(taskMasterPath, file);
|
|
118
|
-
try {
|
|
119
|
-
await fs.access(filePath);
|
|
120
|
-
fileStatus[file] = true;
|
|
121
|
-
} catch (error) {
|
|
122
|
-
fileStatus[file] = false;
|
|
123
|
-
if (file === 'tasks/tasks.json') {
|
|
124
|
-
hasEssentialFiles = false;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Parse tasks.json if it exists for metadata
|
|
130
|
-
let taskMetadata = null;
|
|
131
|
-
if (fileStatus['tasks/tasks.json']) {
|
|
132
|
-
try {
|
|
133
|
-
const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
|
|
134
|
-
const tasksContent = await fs.readFile(tasksPath, 'utf8');
|
|
135
|
-
const tasksData = JSON.parse(tasksContent);
|
|
136
|
-
|
|
137
|
-
// Handle both tagged and legacy formats
|
|
138
|
-
let tasks = [];
|
|
139
|
-
if (tasksData.tasks) {
|
|
140
|
-
// Legacy format
|
|
141
|
-
tasks = tasksData.tasks;
|
|
142
|
-
} else {
|
|
143
|
-
// Tagged format - get tasks from all tags
|
|
144
|
-
Object.values(tasksData).forEach(tagData => {
|
|
145
|
-
if (tagData.tasks) {
|
|
146
|
-
tasks = tasks.concat(tagData.tasks);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Calculate task statistics
|
|
152
|
-
const stats = tasks.reduce((acc, task) => {
|
|
153
|
-
acc.total++;
|
|
154
|
-
acc[task.status] = (acc[task.status] || 0) + 1;
|
|
155
|
-
|
|
156
|
-
// Count subtasks
|
|
157
|
-
if (task.subtasks) {
|
|
158
|
-
task.subtasks.forEach(subtask => {
|
|
159
|
-
acc.subtotalTasks++;
|
|
160
|
-
acc.subtasks = acc.subtasks || {};
|
|
161
|
-
acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return acc;
|
|
166
|
-
}, {
|
|
167
|
-
total: 0,
|
|
168
|
-
subtotalTasks: 0,
|
|
169
|
-
pending: 0,
|
|
170
|
-
'in-progress': 0,
|
|
171
|
-
done: 0,
|
|
172
|
-
review: 0,
|
|
173
|
-
deferred: 0,
|
|
174
|
-
cancelled: 0,
|
|
175
|
-
subtasks: {}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
taskMetadata = {
|
|
179
|
-
taskCount: stats.total,
|
|
180
|
-
subtaskCount: stats.subtotalTasks,
|
|
181
|
-
completed: stats.done || 0,
|
|
182
|
-
pending: stats.pending || 0,
|
|
183
|
-
inProgress: stats['in-progress'] || 0,
|
|
184
|
-
review: stats.review || 0,
|
|
185
|
-
completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
|
|
186
|
-
lastModified: (await fs.stat(tasksPath)).mtime.toISOString()
|
|
187
|
-
};
|
|
188
|
-
} catch (parseError) {
|
|
189
|
-
console.warn('Failed to parse tasks.json:', parseError.message);
|
|
190
|
-
taskMetadata = { error: 'Failed to parse tasks.json' };
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
hasTaskmaster: true,
|
|
196
|
-
hasEssentialFiles,
|
|
197
|
-
files: fileStatus,
|
|
198
|
-
metadata: taskMetadata,
|
|
199
|
-
path: taskMasterPath
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
} catch (error) {
|
|
203
|
-
console.error('Error detecting TaskMaster folder:', error);
|
|
204
|
-
return {
|
|
205
|
-
hasTaskmaster: false,
|
|
206
|
-
reason: `Error checking directory: ${error.message}`
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Cache for extracted project directories
|
|
212
|
-
const projectDirectoryCache = new Map();
|
|
213
|
-
|
|
214
|
-
// Clear cache when needed (called when project files change)
|
|
215
|
-
function clearProjectDirectoryCache() {
|
|
216
|
-
projectDirectoryCache.clear();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Load project configuration file (or DB in cloud mode)
|
|
220
|
-
async function loadProjectConfig(userId = null) {
|
|
221
|
-
if (IS_CLOUD_ENV && userId) {
|
|
222
|
-
const rows = await projectDb.getAll(userId);
|
|
223
|
-
const config = {};
|
|
224
|
-
for (const row of rows) {
|
|
225
|
-
config[row.project_name] = {
|
|
226
|
-
manuallyAdded: true,
|
|
227
|
-
originalPath: row.original_path,
|
|
228
|
-
displayName: row.display_name || null
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
return config;
|
|
232
|
-
}
|
|
233
|
-
const configPath = path.join(os.homedir(), '.claude', 'project-config.json');
|
|
234
|
-
try {
|
|
235
|
-
const configData = await fs.readFile(configPath, 'utf8');
|
|
236
|
-
return JSON.parse(configData);
|
|
237
|
-
} catch (error) {
|
|
238
|
-
return {};
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Save project configuration file (no-op in cloud mode — DB writes happen directly)
|
|
243
|
-
async function saveProjectConfig(config) {
|
|
244
|
-
if (IS_CLOUD_ENV) return; // Cloud mode uses DB, writes happen in addProjectManually
|
|
245
|
-
const claudeDir = path.join(os.homedir(), '.claude');
|
|
246
|
-
const configPath = path.join(claudeDir, 'project-config.json');
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
await fs.mkdir(claudeDir, { recursive: true });
|
|
250
|
-
} catch (error) {
|
|
251
|
-
if (error.code !== 'EEXIST') {
|
|
252
|
-
throw error;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Generate better display name from path
|
|
260
|
-
async function generateDisplayName(projectName, actualProjectDir = null) {
|
|
261
|
-
// Use actual project directory if provided, otherwise decode from project name
|
|
262
|
-
let projectPath = actualProjectDir || projectName.replace(/-/g, '/');
|
|
263
|
-
|
|
264
|
-
// Try to read package.json from the project path
|
|
265
|
-
try {
|
|
266
|
-
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
267
|
-
const packageData = await fs.readFile(packageJsonPath, 'utf8');
|
|
268
|
-
const packageJson = JSON.parse(packageData);
|
|
269
|
-
|
|
270
|
-
// Return the name from package.json if it exists
|
|
271
|
-
if (packageJson.name) {
|
|
272
|
-
return packageJson.name;
|
|
273
|
-
}
|
|
274
|
-
} catch (error) {
|
|
275
|
-
// Fall back to path-based naming if package.json doesn't exist or can't be read
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// If it starts with /, it's an absolute path
|
|
279
|
-
if (projectPath.startsWith('/')) {
|
|
280
|
-
const parts = projectPath.split('/').filter(Boolean);
|
|
281
|
-
// Return only the last folder name
|
|
282
|
-
return parts[parts.length - 1] || projectPath;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return projectPath;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Extract the actual project directory from JSONL sessions (with caching)
|
|
289
|
-
async function extractProjectDirectory(projectName) {
|
|
290
|
-
// Check cache first
|
|
291
|
-
if (projectDirectoryCache.has(projectName)) {
|
|
292
|
-
return projectDirectoryCache.get(projectName);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Check project config for originalPath (manually added projects via UI or platform)
|
|
296
|
-
// This handles projects with dashes in their directory names correctly
|
|
297
|
-
const config = await loadProjectConfig();
|
|
298
|
-
if (config[projectName]?.originalPath) {
|
|
299
|
-
const originalPath = config[projectName].originalPath;
|
|
300
|
-
projectDirectoryCache.set(projectName, originalPath);
|
|
301
|
-
return originalPath;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
305
|
-
const cwdCounts = new Map();
|
|
306
|
-
let latestTimestamp = 0;
|
|
307
|
-
let latestCwd = null;
|
|
308
|
-
let extractedPath;
|
|
309
|
-
|
|
310
|
-
try {
|
|
311
|
-
// Check if the project directory exists
|
|
312
|
-
await fs.access(projectDir);
|
|
313
|
-
|
|
314
|
-
const files = await fs.readdir(projectDir);
|
|
315
|
-
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
|
|
316
|
-
|
|
317
|
-
if (jsonlFiles.length === 0) {
|
|
318
|
-
// Fall back to decoded project name if no sessions
|
|
319
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
320
|
-
} else {
|
|
321
|
-
// Process all JSONL files to collect cwd values
|
|
322
|
-
for (const file of jsonlFiles) {
|
|
323
|
-
const jsonlFile = path.join(projectDir, file);
|
|
324
|
-
const fileStream = fsSync.createReadStream(jsonlFile);
|
|
325
|
-
const rl = readline.createInterface({
|
|
326
|
-
input: fileStream,
|
|
327
|
-
crlfDelay: Infinity
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
for await (const line of rl) {
|
|
331
|
-
if (line.trim()) {
|
|
332
|
-
try {
|
|
333
|
-
const entry = JSON.parse(line);
|
|
334
|
-
|
|
335
|
-
if (entry.cwd) {
|
|
336
|
-
// Count occurrences of each cwd
|
|
337
|
-
cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
|
|
338
|
-
|
|
339
|
-
// Track the most recent cwd
|
|
340
|
-
const timestamp = new Date(entry.timestamp || 0).getTime();
|
|
341
|
-
if (timestamp > latestTimestamp) {
|
|
342
|
-
latestTimestamp = timestamp;
|
|
343
|
-
latestCwd = entry.cwd;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
} catch (parseError) {
|
|
347
|
-
// Skip malformed lines
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Determine the best cwd to use
|
|
354
|
-
if (cwdCounts.size === 0) {
|
|
355
|
-
// No cwd found, fall back to decoded project name
|
|
356
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
357
|
-
} else if (cwdCounts.size === 1) {
|
|
358
|
-
// Only one cwd, use it
|
|
359
|
-
extractedPath = Array.from(cwdCounts.keys())[0];
|
|
360
|
-
} else {
|
|
361
|
-
// Multiple cwd values - prefer the most recent one if it has reasonable usage
|
|
362
|
-
const mostRecentCount = cwdCounts.get(latestCwd) || 0;
|
|
363
|
-
const maxCount = Math.max(...cwdCounts.values());
|
|
364
|
-
|
|
365
|
-
// Use most recent if it has at least 25% of the max count
|
|
366
|
-
if (mostRecentCount >= maxCount * 0.25) {
|
|
367
|
-
extractedPath = latestCwd;
|
|
368
|
-
} else {
|
|
369
|
-
// Otherwise use the most frequently used cwd
|
|
370
|
-
for (const [cwd, count] of cwdCounts.entries()) {
|
|
371
|
-
if (count === maxCount) {
|
|
372
|
-
extractedPath = cwd;
|
|
373
|
-
break;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Fallback (shouldn't reach here)
|
|
379
|
-
if (!extractedPath) {
|
|
380
|
-
extractedPath = latestCwd || projectName.replace(/-/g, '/');
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Cache the result
|
|
386
|
-
projectDirectoryCache.set(projectName, extractedPath);
|
|
387
|
-
|
|
388
|
-
return extractedPath;
|
|
389
|
-
|
|
390
|
-
} catch (error) {
|
|
391
|
-
// If the directory doesn't exist, just use the decoded project name
|
|
392
|
-
if (error.code === 'ENOENT') {
|
|
393
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
394
|
-
} else {
|
|
395
|
-
console.error(`Error extracting project directory for ${projectName}:`, error);
|
|
396
|
-
// Fall back to decoded project name for other errors
|
|
397
|
-
extractedPath = projectName.replace(/-/g, '/');
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// Cache the fallback result too
|
|
401
|
-
projectDirectoryCache.set(projectName, extractedPath);
|
|
402
|
-
|
|
403
|
-
return extractedPath;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
async function getProjects(progressCallback = null, userId = null) {
|
|
408
|
-
// Wrap with a timeout to prevent hanging on slow filesystems
|
|
409
|
-
const timeoutMs = 15000;
|
|
410
|
-
const result = await Promise.race([
|
|
411
|
-
_getProjectsImpl(progressCallback, userId),
|
|
412
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Projects scan timed out')), timeoutMs))
|
|
413
|
-
]).catch(err => {
|
|
414
|
-
console.warn(`[WARN] getProjects failed: ${err.message}. Returning empty list.`);
|
|
415
|
-
return [];
|
|
416
|
-
});
|
|
417
|
-
return result;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
async function _getProjectsImpl(progressCallback = null, userId = null) {
|
|
421
|
-
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
422
|
-
const config = await loadProjectConfig(userId);
|
|
423
|
-
const projects = [];
|
|
424
|
-
const codexSessionsIndexRef = { sessionsByProject: null };
|
|
425
|
-
|
|
426
|
-
// Only load projects that were explicitly added by the user (manuallyAdded).
|
|
427
|
-
// No auto-scanning of ~/.claude/projects/ — the user adds projects via the UI.
|
|
428
|
-
const manualEntries = Object.entries(config).filter(([, cfg]) => cfg.manuallyAdded);
|
|
429
|
-
const totalProjects = manualEntries.length;
|
|
430
|
-
let processedProjects = 0;
|
|
431
|
-
|
|
432
|
-
for (const [projectName, projectConfig] of manualEntries) {
|
|
433
|
-
processedProjects++;
|
|
434
|
-
|
|
435
|
-
if (progressCallback) {
|
|
436
|
-
progressCallback({
|
|
437
|
-
phase: 'loading',
|
|
438
|
-
current: processedProjects,
|
|
439
|
-
total: totalProjects,
|
|
440
|
-
currentProject: projectName
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Use the original path if available, otherwise extract from potential sessions
|
|
445
|
-
let actualProjectDir = projectConfig.originalPath;
|
|
446
|
-
|
|
447
|
-
if (!actualProjectDir) {
|
|
448
|
-
try {
|
|
449
|
-
actualProjectDir = await extractProjectDirectory(projectName);
|
|
450
|
-
} catch (error) {
|
|
451
|
-
// Fall back to decoded project name
|
|
452
|
-
actualProjectDir = projectName.replace(/-/g, '/');
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const project = {
|
|
457
|
-
name: projectName,
|
|
458
|
-
path: actualProjectDir,
|
|
459
|
-
displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
|
|
460
|
-
fullPath: actualProjectDir,
|
|
461
|
-
isCustomName: !!projectConfig.displayName,
|
|
462
|
-
isManuallyAdded: true,
|
|
463
|
-
sessions: [],
|
|
464
|
-
sessionMeta: { hasMore: false, total: 0 },
|
|
465
|
-
cursorSessions: [],
|
|
466
|
-
codexSessions: []
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
// Check if a Claude project folder exists for this project (for session history)
|
|
470
|
-
const projectDir = path.join(claudeDir, projectName);
|
|
471
|
-
try {
|
|
472
|
-
await fs.access(projectDir);
|
|
473
|
-
const sessionResult = await getSessions(projectName, 5, 0);
|
|
474
|
-
project.sessions = sessionResult.sessions || [];
|
|
475
|
-
project.sessionMeta = {
|
|
476
|
-
hasMore: sessionResult.hasMore,
|
|
477
|
-
total: sessionResult.total
|
|
478
|
-
};
|
|
479
|
-
} catch (e) {
|
|
480
|
-
// No Claude sessions — that's fine
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Fetch Cursor sessions
|
|
484
|
-
try {
|
|
485
|
-
project.cursorSessions = await getCursorSessions(actualProjectDir);
|
|
486
|
-
} catch (e) {
|
|
487
|
-
// No Cursor sessions
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Fetch Codex sessions
|
|
491
|
-
try {
|
|
492
|
-
project.codexSessions = await getCodexSessions(actualProjectDir, {
|
|
493
|
-
indexRef: codexSessionsIndexRef,
|
|
494
|
-
});
|
|
495
|
-
} catch (e) {
|
|
496
|
-
// No Codex sessions
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// TaskMaster detection
|
|
500
|
-
try {
|
|
501
|
-
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
|
|
502
|
-
project.taskmaster = {
|
|
503
|
-
status: taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles
|
|
504
|
-
? 'configured' : 'not-configured',
|
|
505
|
-
hasTaskmaster: taskMasterResult.hasTaskmaster,
|
|
506
|
-
hasEssentialFiles: taskMasterResult.hasEssentialFiles,
|
|
507
|
-
metadata: taskMasterResult.metadata
|
|
508
|
-
};
|
|
509
|
-
} catch (error) {
|
|
510
|
-
project.taskmaster = {
|
|
511
|
-
status: 'error',
|
|
512
|
-
hasTaskmaster: false,
|
|
513
|
-
hasEssentialFiles: false,
|
|
514
|
-
error: error.message
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Gitagent detection (lightweight — only reads agent.yaml for metadata)
|
|
519
|
-
project.gitagent = { detected: false };
|
|
520
|
-
try {
|
|
521
|
-
const agentYamlPath = path.join(actualProjectDir, 'agent.yaml');
|
|
522
|
-
const agentYamlContent = await fs.readFile(agentYamlPath, 'utf8');
|
|
523
|
-
if (agentYamlContent) {
|
|
524
|
-
const manifest = yaml.load(agentYamlContent);
|
|
525
|
-
if (manifest && typeof manifest === 'object') {
|
|
526
|
-
project.gitagent = {
|
|
527
|
-
detected: true,
|
|
528
|
-
name: manifest.name || null,
|
|
529
|
-
version: manifest.version || null,
|
|
530
|
-
description: manifest.description || null,
|
|
531
|
-
model: manifest.model || null,
|
|
532
|
-
skillCount: Array.isArray(manifest.skills) ? manifest.skills.length : 0,
|
|
533
|
-
toolCount: Array.isArray(manifest.tools) ? manifest.tools.length : 0,
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
} catch {
|
|
538
|
-
// No agent.yaml or parse error — not a gitagent project
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
projects.push(project);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (progressCallback) {
|
|
545
|
-
progressCallback({
|
|
546
|
-
phase: 'complete',
|
|
547
|
-
current: totalProjects,
|
|
548
|
-
total: totalProjects
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
return projects;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async function getSessions(projectName, limit = 5, offset = 0) {
|
|
556
|
-
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
557
|
-
|
|
558
|
-
try {
|
|
559
|
-
const files = await fs.readdir(projectDir);
|
|
560
|
-
// agent-*.jsonl files contain session start data at this point. This needs to be revisited
|
|
561
|
-
// periodically to make sure only accurate data is there and no new functionality is added there
|
|
562
|
-
const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
|
|
563
|
-
|
|
564
|
-
if (jsonlFiles.length === 0) {
|
|
565
|
-
return { sessions: [], hasMore: false, total: 0 };
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Sort files by modification time (newest first)
|
|
569
|
-
const filesWithStats = await Promise.all(
|
|
570
|
-
jsonlFiles.map(async (file) => {
|
|
571
|
-
const filePath = path.join(projectDir, file);
|
|
572
|
-
const stats = await fs.stat(filePath);
|
|
573
|
-
return { file, mtime: stats.mtime };
|
|
574
|
-
})
|
|
575
|
-
);
|
|
576
|
-
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
577
|
-
|
|
578
|
-
const allSessions = new Map();
|
|
579
|
-
const allEntries = [];
|
|
580
|
-
const uuidToSessionMap = new Map();
|
|
581
|
-
|
|
582
|
-
// Collect all sessions and entries from all files
|
|
583
|
-
for (const { file } of filesWithStats) {
|
|
584
|
-
const jsonlFile = path.join(projectDir, file);
|
|
585
|
-
const result = await parseJsonlSessions(jsonlFile);
|
|
586
|
-
|
|
587
|
-
result.sessions.forEach(session => {
|
|
588
|
-
if (!allSessions.has(session.id)) {
|
|
589
|
-
allSessions.set(session.id, session);
|
|
590
|
-
}
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
allEntries.push(...result.entries);
|
|
594
|
-
|
|
595
|
-
// Early exit optimization for large projects
|
|
596
|
-
if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
|
|
597
|
-
break;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Build UUID-to-session mapping for timeline detection
|
|
602
|
-
allEntries.forEach(entry => {
|
|
603
|
-
if (entry.uuid && entry.sessionId) {
|
|
604
|
-
uuidToSessionMap.set(entry.uuid, entry.sessionId);
|
|
605
|
-
}
|
|
606
|
-
});
|
|
607
|
-
|
|
608
|
-
// Group sessions by first user message ID
|
|
609
|
-
const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
|
|
610
|
-
const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
|
|
611
|
-
|
|
612
|
-
// Find the first user message for each session
|
|
613
|
-
allEntries.forEach(entry => {
|
|
614
|
-
if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
|
|
615
|
-
// This is a first user message in a session (parentUuid is null)
|
|
616
|
-
const firstUserMsgId = entry.uuid;
|
|
617
|
-
|
|
618
|
-
if (!sessionToFirstUserMsgId.has(entry.sessionId)) {
|
|
619
|
-
sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId);
|
|
620
|
-
|
|
621
|
-
const session = allSessions.get(entry.sessionId);
|
|
622
|
-
if (session) {
|
|
623
|
-
if (!sessionGroups.has(firstUserMsgId)) {
|
|
624
|
-
sessionGroups.set(firstUserMsgId, {
|
|
625
|
-
latestSession: session,
|
|
626
|
-
allSessions: [session]
|
|
627
|
-
});
|
|
628
|
-
} else {
|
|
629
|
-
const group = sessionGroups.get(firstUserMsgId);
|
|
630
|
-
group.allSessions.push(session);
|
|
631
|
-
|
|
632
|
-
// Update latest session if this one is more recent
|
|
633
|
-
if (new Date(session.lastActivity) > new Date(group.latestSession.lastActivity)) {
|
|
634
|
-
group.latestSession = session;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
// Collect all sessions that don't belong to any group (standalone sessions)
|
|
643
|
-
const groupedSessionIds = new Set();
|
|
644
|
-
sessionGroups.forEach(group => {
|
|
645
|
-
group.allSessions.forEach(session => groupedSessionIds.add(session.id));
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
const standaloneSessionsArray = Array.from(allSessions.values())
|
|
649
|
-
.filter(session => !groupedSessionIds.has(session.id));
|
|
650
|
-
|
|
651
|
-
// Combine grouped sessions (only show latest from each group) + standalone sessions
|
|
652
|
-
const latestFromGroups = Array.from(sessionGroups.values()).map(group => {
|
|
653
|
-
const session = { ...group.latestSession };
|
|
654
|
-
// Add metadata about grouping
|
|
655
|
-
if (group.allSessions.length > 1) {
|
|
656
|
-
session.isGrouped = true;
|
|
657
|
-
session.groupSize = group.allSessions.length;
|
|
658
|
-
session.groupSessions = group.allSessions.map(s => s.id);
|
|
659
|
-
}
|
|
660
|
-
return session;
|
|
661
|
-
});
|
|
662
|
-
const visibleSessions = [...latestFromGroups, ...standaloneSessionsArray]
|
|
663
|
-
.filter(session => !session.summary.startsWith('{ "'))
|
|
664
|
-
.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
665
|
-
|
|
666
|
-
const total = visibleSessions.length;
|
|
667
|
-
const paginatedSessions = visibleSessions.slice(offset, offset + limit);
|
|
668
|
-
const hasMore = offset + limit < total;
|
|
669
|
-
|
|
670
|
-
return {
|
|
671
|
-
sessions: paginatedSessions,
|
|
672
|
-
hasMore,
|
|
673
|
-
total,
|
|
674
|
-
offset,
|
|
675
|
-
limit
|
|
676
|
-
};
|
|
677
|
-
} catch (error) {
|
|
678
|
-
console.error(`Error reading sessions for project ${projectName}:`, error);
|
|
679
|
-
return { sessions: [], hasMore: false, total: 0 };
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
async function parseJsonlSessions(filePath) {
|
|
684
|
-
const sessions = new Map();
|
|
685
|
-
const entries = [];
|
|
686
|
-
const pendingSummaries = new Map(); // leafUuid -> summary for entries without sessionId
|
|
687
|
-
|
|
688
|
-
try {
|
|
689
|
-
const fileStream = fsSync.createReadStream(filePath);
|
|
690
|
-
const rl = readline.createInterface({
|
|
691
|
-
input: fileStream,
|
|
692
|
-
crlfDelay: Infinity
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
for await (const line of rl) {
|
|
696
|
-
if (line.trim()) {
|
|
697
|
-
try {
|
|
698
|
-
const entry = JSON.parse(line);
|
|
699
|
-
entries.push(entry);
|
|
700
|
-
|
|
701
|
-
// Handle summary entries that don't have sessionId yet
|
|
702
|
-
if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
|
|
703
|
-
pendingSummaries.set(entry.leafUuid, entry.summary);
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
if (entry.sessionId) {
|
|
707
|
-
if (!sessions.has(entry.sessionId)) {
|
|
708
|
-
sessions.set(entry.sessionId, {
|
|
709
|
-
id: entry.sessionId,
|
|
710
|
-
summary: 'New Session',
|
|
711
|
-
messageCount: 0,
|
|
712
|
-
lastActivity: new Date(),
|
|
713
|
-
cwd: entry.cwd || '',
|
|
714
|
-
lastUserMessage: null,
|
|
715
|
-
lastAssistantMessage: null
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const session = sessions.get(entry.sessionId);
|
|
720
|
-
|
|
721
|
-
// Apply pending summary if this entry has a parentUuid that matches a pending summary
|
|
722
|
-
if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
|
|
723
|
-
session.summary = pendingSummaries.get(entry.parentUuid);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// Update summary from summary entries with sessionId
|
|
727
|
-
if (entry.type === 'summary' && entry.summary) {
|
|
728
|
-
session.summary = entry.summary;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// Track last user and assistant messages (skip system messages)
|
|
732
|
-
if (entry.message?.role === 'user' && entry.message?.content) {
|
|
733
|
-
const content = entry.message.content;
|
|
734
|
-
|
|
735
|
-
// Extract text from array format if needed
|
|
736
|
-
let textContent = content;
|
|
737
|
-
if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
|
|
738
|
-
textContent = content[0].text;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const isSystemMessage = typeof textContent === 'string' && (
|
|
742
|
-
textContent.startsWith('<command-name>') ||
|
|
743
|
-
textContent.startsWith('<command-message>') ||
|
|
744
|
-
textContent.startsWith('<command-args>') ||
|
|
745
|
-
textContent.startsWith('<local-command-stdout>') ||
|
|
746
|
-
textContent.startsWith('<system-reminder>') ||
|
|
747
|
-
textContent.startsWith('Caveat:') ||
|
|
748
|
-
textContent.startsWith('This session is being continued from a previous') ||
|
|
749
|
-
textContent.startsWith('Invalid API key') ||
|
|
750
|
-
textContent.includes('{"subtasks":') || // Filter Task Master prompts
|
|
751
|
-
textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
|
|
752
|
-
textContent === 'Warmup' // Explicitly filter out "Warmup"
|
|
753
|
-
);
|
|
754
|
-
|
|
755
|
-
if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
|
|
756
|
-
session.lastUserMessage = textContent;
|
|
757
|
-
}
|
|
758
|
-
} else if (entry.message?.role === 'assistant' && entry.message?.content) {
|
|
759
|
-
// Skip API error messages using the isApiErrorMessage flag
|
|
760
|
-
if (entry.isApiErrorMessage === true) {
|
|
761
|
-
// Skip this message entirely
|
|
762
|
-
} else {
|
|
763
|
-
// Track last assistant text message
|
|
764
|
-
let assistantText = null;
|
|
765
|
-
|
|
766
|
-
if (Array.isArray(entry.message.content)) {
|
|
767
|
-
for (const part of entry.message.content) {
|
|
768
|
-
if (part.type === 'text' && part.text) {
|
|
769
|
-
assistantText = part.text;
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
} else if (typeof entry.message.content === 'string') {
|
|
773
|
-
assistantText = entry.message.content;
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// Additional filter for assistant messages with system content
|
|
777
|
-
const isSystemAssistantMessage = typeof assistantText === 'string' && (
|
|
778
|
-
assistantText.startsWith('Invalid API key') ||
|
|
779
|
-
assistantText.includes('{"subtasks":') ||
|
|
780
|
-
assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
|
|
781
|
-
);
|
|
782
|
-
|
|
783
|
-
if (assistantText && !isSystemAssistantMessage) {
|
|
784
|
-
session.lastAssistantMessage = assistantText;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
session.messageCount++;
|
|
790
|
-
|
|
791
|
-
if (entry.timestamp) {
|
|
792
|
-
session.lastActivity = new Date(entry.timestamp);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
} catch (parseError) {
|
|
796
|
-
// Skip malformed lines silently
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
// After processing all entries, set final summary based on last message if no summary exists
|
|
802
|
-
for (const session of sessions.values()) {
|
|
803
|
-
if (session.summary === 'New Session') {
|
|
804
|
-
// Prefer last user message, fall back to last assistant message
|
|
805
|
-
const lastMessage = session.lastUserMessage || session.lastAssistantMessage;
|
|
806
|
-
if (lastMessage) {
|
|
807
|
-
session.summary = lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// Filter out sessions that contain JSON responses (Task Master errors)
|
|
813
|
-
const allSessions = Array.from(sessions.values());
|
|
814
|
-
const filteredSessions = allSessions.filter(session => {
|
|
815
|
-
const shouldFilter = session.summary.startsWith('{ "');
|
|
816
|
-
if (shouldFilter) {
|
|
817
|
-
}
|
|
818
|
-
// Log a sample of summaries to debug
|
|
819
|
-
if (Math.random() < 0.01) { // Log 1% of sessions
|
|
820
|
-
}
|
|
821
|
-
return !shouldFilter;
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
return {
|
|
826
|
-
sessions: filteredSessions,
|
|
827
|
-
entries: entries
|
|
828
|
-
};
|
|
829
|
-
|
|
830
|
-
} catch (error) {
|
|
831
|
-
console.error('Error reading JSONL file:', error);
|
|
832
|
-
return { sessions: [], entries: [] };
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
// Parse an agent JSONL file and extract tool uses
|
|
837
|
-
async function parseAgentTools(filePath) {
|
|
838
|
-
const tools = [];
|
|
839
|
-
|
|
840
|
-
try {
|
|
841
|
-
const fileStream = fsSync.createReadStream(filePath);
|
|
842
|
-
const rl = readline.createInterface({
|
|
843
|
-
input: fileStream,
|
|
844
|
-
crlfDelay: Infinity
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
for await (const line of rl) {
|
|
848
|
-
if (line.trim()) {
|
|
849
|
-
try {
|
|
850
|
-
const entry = JSON.parse(line);
|
|
851
|
-
// Look for assistant messages with tool_use
|
|
852
|
-
if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
|
|
853
|
-
for (const part of entry.message.content) {
|
|
854
|
-
if (part.type === 'tool_use') {
|
|
855
|
-
tools.push({
|
|
856
|
-
toolId: part.id,
|
|
857
|
-
toolName: part.name,
|
|
858
|
-
toolInput: part.input,
|
|
859
|
-
timestamp: entry.timestamp
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
// Look for tool results
|
|
865
|
-
if (entry.message?.role === 'user' && Array.isArray(entry.message?.content)) {
|
|
866
|
-
for (const part of entry.message.content) {
|
|
867
|
-
if (part.type === 'tool_result') {
|
|
868
|
-
// Find the matching tool and add result
|
|
869
|
-
const tool = tools.find(t => t.toolId === part.tool_use_id);
|
|
870
|
-
if (tool) {
|
|
871
|
-
tool.toolResult = {
|
|
872
|
-
content: typeof part.content === 'string' ? part.content :
|
|
873
|
-
Array.isArray(part.content) ? part.content.map(c => c.text || '').join('\n') :
|
|
874
|
-
JSON.stringify(part.content),
|
|
875
|
-
isError: Boolean(part.is_error)
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
} catch (parseError) {
|
|
882
|
-
// Skip malformed lines
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
} catch (error) {
|
|
887
|
-
console.warn(`Error parsing agent file ${filePath}:`, error.message);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
return tools;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// Get messages for a specific session with pagination support
|
|
894
|
-
async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) {
|
|
895
|
-
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
896
|
-
|
|
897
|
-
try {
|
|
898
|
-
const files = await fs.readdir(projectDir);
|
|
899
|
-
// agent-*.jsonl files contain subagent tool history - we'll process them separately
|
|
900
|
-
const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
|
|
901
|
-
const agentFiles = files.filter(file => file.endsWith('.jsonl') && file.startsWith('agent-'));
|
|
902
|
-
|
|
903
|
-
if (jsonlFiles.length === 0) {
|
|
904
|
-
return { messages: [], total: 0, hasMore: false };
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
const messages = [];
|
|
908
|
-
// Map of agentId -> tools for subagent tool grouping
|
|
909
|
-
const agentToolsCache = new Map();
|
|
910
|
-
|
|
911
|
-
// Process all JSONL files to find messages for this session
|
|
912
|
-
for (const file of jsonlFiles) {
|
|
913
|
-
const jsonlFile = path.join(projectDir, file);
|
|
914
|
-
const fileStream = fsSync.createReadStream(jsonlFile);
|
|
915
|
-
const rl = readline.createInterface({
|
|
916
|
-
input: fileStream,
|
|
917
|
-
crlfDelay: Infinity
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
for await (const line of rl) {
|
|
921
|
-
if (line.trim()) {
|
|
922
|
-
try {
|
|
923
|
-
const entry = JSON.parse(line);
|
|
924
|
-
if (entry.sessionId === sessionId) {
|
|
925
|
-
messages.push(entry);
|
|
926
|
-
}
|
|
927
|
-
} catch (parseError) {
|
|
928
|
-
console.warn('Error parsing line:', parseError.message);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// Collect agentIds from Task tool results
|
|
935
|
-
const agentIds = new Set();
|
|
936
|
-
for (const message of messages) {
|
|
937
|
-
if (message.toolUseResult?.agentId) {
|
|
938
|
-
agentIds.add(message.toolUseResult.agentId);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// Load agent tools for each agentId found
|
|
943
|
-
for (const agentId of agentIds) {
|
|
944
|
-
const agentFileName = `agent-${agentId}.jsonl`;
|
|
945
|
-
if (agentFiles.includes(agentFileName)) {
|
|
946
|
-
const agentFilePath = path.join(projectDir, agentFileName);
|
|
947
|
-
const tools = await parseAgentTools(agentFilePath);
|
|
948
|
-
agentToolsCache.set(agentId, tools);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Attach agent tools to their parent Task messages
|
|
953
|
-
for (const message of messages) {
|
|
954
|
-
if (message.toolUseResult?.agentId) {
|
|
955
|
-
const agentId = message.toolUseResult.agentId;
|
|
956
|
-
const agentTools = agentToolsCache.get(agentId);
|
|
957
|
-
if (agentTools && agentTools.length > 0) {
|
|
958
|
-
message.subagentTools = agentTools;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
// Sort messages by timestamp
|
|
964
|
-
const sortedMessages = messages.sort((a, b) =>
|
|
965
|
-
new Date(a.timestamp || 0) - new Date(b.timestamp || 0)
|
|
966
|
-
);
|
|
967
|
-
|
|
968
|
-
const total = sortedMessages.length;
|
|
969
|
-
|
|
970
|
-
// If no limit is specified, return all messages (backward compatibility)
|
|
971
|
-
if (limit === null) {
|
|
972
|
-
return sortedMessages;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Apply pagination - for recent messages, we need to slice from the end
|
|
976
|
-
// offset 0 should give us the most recent messages
|
|
977
|
-
const startIndex = Math.max(0, total - offset - limit);
|
|
978
|
-
const endIndex = total - offset;
|
|
979
|
-
const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
|
|
980
|
-
const hasMore = startIndex > 0;
|
|
981
|
-
|
|
982
|
-
return {
|
|
983
|
-
messages: paginatedMessages,
|
|
984
|
-
total,
|
|
985
|
-
hasMore,
|
|
986
|
-
offset,
|
|
987
|
-
limit
|
|
988
|
-
};
|
|
989
|
-
} catch (error) {
|
|
990
|
-
console.error(`Error reading messages for session ${sessionId}:`, error);
|
|
991
|
-
return limit === null ? [] : { messages: [], total: 0, hasMore: false };
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
// Rename a project's display name
|
|
996
|
-
async function renameProject(projectName, newDisplayName, userId = null) {
|
|
997
|
-
if (IS_CLOUD_ENV && userId) {
|
|
998
|
-
const rows = await projectDb.getAll(userId);
|
|
999
|
-
const match = rows.find(r => r.project_name === projectName);
|
|
1000
|
-
if (match) {
|
|
1001
|
-
await projectDb.rename(userId, match.original_path, newDisplayName?.trim() || null);
|
|
1002
|
-
}
|
|
1003
|
-
return true;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
const config = await loadProjectConfig();
|
|
1007
|
-
|
|
1008
|
-
if (!newDisplayName || newDisplayName.trim() === '') {
|
|
1009
|
-
delete config[projectName];
|
|
1010
|
-
} else {
|
|
1011
|
-
config[projectName] = {
|
|
1012
|
-
displayName: newDisplayName.trim()
|
|
1013
|
-
};
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
await saveProjectConfig(config);
|
|
1017
|
-
return true;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
// Delete a session from a project
|
|
1021
|
-
async function deleteSession(projectName, sessionId) {
|
|
1022
|
-
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
1023
|
-
|
|
1024
|
-
try {
|
|
1025
|
-
const files = await fs.readdir(projectDir);
|
|
1026
|
-
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
|
|
1027
|
-
|
|
1028
|
-
if (jsonlFiles.length === 0) {
|
|
1029
|
-
throw new Error('No session files found for this project');
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
// Check all JSONL files to find which one contains the session
|
|
1033
|
-
for (const file of jsonlFiles) {
|
|
1034
|
-
const jsonlFile = path.join(projectDir, file);
|
|
1035
|
-
const content = await fs.readFile(jsonlFile, 'utf8');
|
|
1036
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
1037
|
-
|
|
1038
|
-
// Check if this file contains the session
|
|
1039
|
-
const hasSession = lines.some(line => {
|
|
1040
|
-
try {
|
|
1041
|
-
const data = JSON.parse(line);
|
|
1042
|
-
return data.sessionId === sessionId;
|
|
1043
|
-
} catch {
|
|
1044
|
-
return false;
|
|
1045
|
-
}
|
|
1046
|
-
});
|
|
1047
|
-
|
|
1048
|
-
if (hasSession) {
|
|
1049
|
-
// Filter out all entries for this session
|
|
1050
|
-
const filteredLines = lines.filter(line => {
|
|
1051
|
-
try {
|
|
1052
|
-
const data = JSON.parse(line);
|
|
1053
|
-
return data.sessionId !== sessionId;
|
|
1054
|
-
} catch {
|
|
1055
|
-
return true; // Keep malformed lines
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
1058
|
-
|
|
1059
|
-
// Write back the filtered content
|
|
1060
|
-
await fs.writeFile(jsonlFile, filteredLines.join('\n') + (filteredLines.length > 0 ? '\n' : ''));
|
|
1061
|
-
return true;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
throw new Error(`Session ${sessionId} not found in any files`);
|
|
1066
|
-
} catch (error) {
|
|
1067
|
-
console.error(`Error deleting session ${sessionId} from project ${projectName}:`, error);
|
|
1068
|
-
throw error;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// Check if a project is empty (has no sessions)
|
|
1073
|
-
async function isProjectEmpty(projectName) {
|
|
1074
|
-
try {
|
|
1075
|
-
const sessionsResult = await getSessions(projectName, 1, 0);
|
|
1076
|
-
return sessionsResult.total === 0;
|
|
1077
|
-
} catch (error) {
|
|
1078
|
-
console.error(`Error checking if project ${projectName} is empty:`, error);
|
|
1079
|
-
return false;
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
// Delete a project (force=true to delete even with sessions)
|
|
1084
|
-
async function deleteProject(projectName, force = false, userId = null) {
|
|
1085
|
-
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
1086
|
-
|
|
1087
|
-
try {
|
|
1088
|
-
const isEmpty = await isProjectEmpty(projectName);
|
|
1089
|
-
if (!isEmpty && !force) {
|
|
1090
|
-
throw new Error('Cannot delete project with existing sessions');
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
const config = await loadProjectConfig(userId);
|
|
1094
|
-
let projectPath = config[projectName]?.path || config[projectName]?.originalPath;
|
|
1095
|
-
|
|
1096
|
-
// Fallback to extractProjectDirectory if projectPath is not in config
|
|
1097
|
-
if (!projectPath) {
|
|
1098
|
-
projectPath = await extractProjectDirectory(projectName);
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Remove the project directory (includes all Claude sessions)
|
|
1102
|
-
await fs.rm(projectDir, { recursive: true, force: true });
|
|
1103
|
-
|
|
1104
|
-
// Delete all Codex sessions associated with this project
|
|
1105
|
-
if (projectPath) {
|
|
1106
|
-
try {
|
|
1107
|
-
const codexSessions = await getCodexSessions(projectPath, { limit: 0 });
|
|
1108
|
-
for (const session of codexSessions) {
|
|
1109
|
-
try {
|
|
1110
|
-
await deleteCodexSession(session.id);
|
|
1111
|
-
} catch (err) {
|
|
1112
|
-
console.warn(`Failed to delete Codex session ${session.id}:`, err.message);
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
} catch (err) {
|
|
1116
|
-
console.warn('Failed to delete Codex sessions:', err.message);
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
// Delete Cursor sessions directory if it exists
|
|
1120
|
-
try {
|
|
1121
|
-
const hash = crypto.createHash('md5').update(projectPath).digest('hex');
|
|
1122
|
-
const cursorProjectDir = path.join(os.homedir(), '.cursor', 'chats', hash);
|
|
1123
|
-
await fs.rm(cursorProjectDir, { recursive: true, force: true });
|
|
1124
|
-
} catch (err) {
|
|
1125
|
-
// Cursor dir may not exist, ignore
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// Remove from project config
|
|
1130
|
-
if (IS_CLOUD_ENV && userId) {
|
|
1131
|
-
if (projectPath) {
|
|
1132
|
-
await projectDb.remove(userId, projectPath);
|
|
1133
|
-
}
|
|
1134
|
-
} else {
|
|
1135
|
-
delete config[projectName];
|
|
1136
|
-
await saveProjectConfig(config);
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
return true;
|
|
1140
|
-
} catch (error) {
|
|
1141
|
-
console.error(`Error deleting project ${projectName}:`, error);
|
|
1142
|
-
throw error;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// Add a project manually to the config (without creating folders)
|
|
1147
|
-
async function addProjectManually(projectPath, displayName = null, userId = null) {
|
|
1148
|
-
// In cloud mode, the path is on the user's local machine — don't resolve or validate on server
|
|
1149
|
-
const absolutePath = IS_CLOUD_ENV ? projectPath : path.resolve(projectPath);
|
|
1150
|
-
|
|
1151
|
-
if (!IS_CLOUD_ENV) {
|
|
1152
|
-
try {
|
|
1153
|
-
await fs.access(absolutePath);
|
|
1154
|
-
} catch (error) {
|
|
1155
|
-
throw new Error(`Path does not exist: ${absolutePath}`);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
const projectName = absolutePath.replace(/[\\/:\s~_]/g, '-');
|
|
1160
|
-
|
|
1161
|
-
// Cloud mode: use DB directly
|
|
1162
|
-
const effectiveUserId = userId || _cloudUserId;
|
|
1163
|
-
if (IS_CLOUD_ENV && effectiveUserId) {
|
|
1164
|
-
const result = await projectDb.upsert(effectiveUserId, absolutePath, displayName);
|
|
1165
|
-
return {
|
|
1166
|
-
name: projectName,
|
|
1167
|
-
path: absolutePath,
|
|
1168
|
-
fullPath: absolutePath,
|
|
1169
|
-
displayName: displayName || path.basename(absolutePath),
|
|
1170
|
-
isManuallyAdded: true,
|
|
1171
|
-
alreadyExists: !!result.alreadyExists,
|
|
1172
|
-
sessions: [],
|
|
1173
|
-
cursorSessions: []
|
|
1174
|
-
};
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// Local mode: use file config
|
|
1178
|
-
const config = await loadProjectConfig();
|
|
1179
|
-
|
|
1180
|
-
if (config[projectName]) {
|
|
1181
|
-
return {
|
|
1182
|
-
name: projectName,
|
|
1183
|
-
path: absolutePath,
|
|
1184
|
-
fullPath: absolutePath,
|
|
1185
|
-
displayName: config[projectName].displayName || displayName || path.basename(absolutePath),
|
|
1186
|
-
isManuallyAdded: true,
|
|
1187
|
-
alreadyExists: true,
|
|
1188
|
-
sessions: [],
|
|
1189
|
-
cursorSessions: []
|
|
1190
|
-
};
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
config[projectName] = {
|
|
1194
|
-
manuallyAdded: true,
|
|
1195
|
-
originalPath: absolutePath
|
|
1196
|
-
};
|
|
1197
|
-
|
|
1198
|
-
if (displayName) {
|
|
1199
|
-
config[projectName].displayName = displayName;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
await saveProjectConfig(config);
|
|
1203
|
-
|
|
1204
|
-
return {
|
|
1205
|
-
name: projectName,
|
|
1206
|
-
path: absolutePath,
|
|
1207
|
-
fullPath: absolutePath,
|
|
1208
|
-
displayName: displayName || path.basename(absolutePath),
|
|
1209
|
-
isManuallyAdded: true,
|
|
1210
|
-
sessions: [],
|
|
1211
|
-
cursorSessions: []
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// Fetch Cursor sessions for a given project path
|
|
1216
|
-
async function getCursorSessions(projectPath) {
|
|
1217
|
-
try {
|
|
1218
|
-
// Calculate cwdID hash for the project path (Cursor uses MD5 hash)
|
|
1219
|
-
const cwdId = crypto.createHash('md5').update(projectPath).digest('hex');
|
|
1220
|
-
const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
|
|
1221
|
-
|
|
1222
|
-
// Check if the directory exists
|
|
1223
|
-
try {
|
|
1224
|
-
await fs.access(cursorChatsPath);
|
|
1225
|
-
} catch (error) {
|
|
1226
|
-
// No sessions for this project
|
|
1227
|
-
return [];
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
// List all session directories
|
|
1231
|
-
const sessionDirs = await fs.readdir(cursorChatsPath);
|
|
1232
|
-
const sessions = [];
|
|
1233
|
-
|
|
1234
|
-
for (const sessionId of sessionDirs) {
|
|
1235
|
-
const sessionPath = path.join(cursorChatsPath, sessionId);
|
|
1236
|
-
const storeDbPath = path.join(sessionPath, 'store.db');
|
|
1237
|
-
|
|
1238
|
-
try {
|
|
1239
|
-
// Check if store.db exists
|
|
1240
|
-
await fs.access(storeDbPath);
|
|
1241
|
-
|
|
1242
|
-
// Capture store.db mtime as a reliable fallback timestamp
|
|
1243
|
-
let dbStatMtimeMs = null;
|
|
1244
|
-
try {
|
|
1245
|
-
const stat = await fs.stat(storeDbPath);
|
|
1246
|
-
dbStatMtimeMs = stat.mtimeMs;
|
|
1247
|
-
} catch (_) {}
|
|
1248
|
-
|
|
1249
|
-
// Open SQLite database (requires native sqlite3 module)
|
|
1250
|
-
if (!sqliteOpen || !sqlite3) {
|
|
1251
|
-
continue; // Skip on Vercel where native modules aren't available
|
|
1252
|
-
}
|
|
1253
|
-
const db = await sqliteOpen({
|
|
1254
|
-
filename: storeDbPath,
|
|
1255
|
-
driver: sqlite3.Database,
|
|
1256
|
-
mode: sqlite3.OPEN_READONLY
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
// Get metadata from meta table
|
|
1260
|
-
const metaRows = await db.all(`
|
|
1261
|
-
SELECT key, value FROM meta
|
|
1262
|
-
`);
|
|
1263
|
-
|
|
1264
|
-
// Parse metadata
|
|
1265
|
-
let metadata = {};
|
|
1266
|
-
for (const row of metaRows) {
|
|
1267
|
-
if (row.value) {
|
|
1268
|
-
try {
|
|
1269
|
-
// Try to decode as hex-encoded JSON
|
|
1270
|
-
const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
|
|
1271
|
-
if (hexMatch) {
|
|
1272
|
-
const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
|
|
1273
|
-
metadata[row.key] = JSON.parse(jsonStr);
|
|
1274
|
-
} else {
|
|
1275
|
-
metadata[row.key] = row.value.toString();
|
|
1276
|
-
}
|
|
1277
|
-
} catch (e) {
|
|
1278
|
-
metadata[row.key] = row.value.toString();
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
// Get message count
|
|
1284
|
-
const messageCountResult = await db.get(`
|
|
1285
|
-
SELECT COUNT(*) as count FROM blobs
|
|
1286
|
-
`);
|
|
1287
|
-
|
|
1288
|
-
await db.close();
|
|
1289
|
-
|
|
1290
|
-
// Extract session info
|
|
1291
|
-
const sessionName = metadata.title || metadata.sessionTitle || 'Untitled Session';
|
|
1292
|
-
|
|
1293
|
-
// Determine timestamp - prefer createdAt from metadata, fall back to db file mtime
|
|
1294
|
-
let createdAt = null;
|
|
1295
|
-
if (metadata.createdAt) {
|
|
1296
|
-
createdAt = new Date(metadata.createdAt).toISOString();
|
|
1297
|
-
} else if (dbStatMtimeMs) {
|
|
1298
|
-
createdAt = new Date(dbStatMtimeMs).toISOString();
|
|
1299
|
-
} else {
|
|
1300
|
-
createdAt = new Date().toISOString();
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
sessions.push({
|
|
1304
|
-
id: sessionId,
|
|
1305
|
-
name: sessionName,
|
|
1306
|
-
createdAt: createdAt,
|
|
1307
|
-
lastActivity: createdAt, // For compatibility with Claude sessions
|
|
1308
|
-
messageCount: messageCountResult.count || 0,
|
|
1309
|
-
projectPath: projectPath
|
|
1310
|
-
});
|
|
1311
|
-
|
|
1312
|
-
} catch (error) {
|
|
1313
|
-
console.warn(`Could not read Cursor session ${sessionId}:`, error.message);
|
|
1314
|
-
}
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
// Sort sessions by creation time (newest first)
|
|
1318
|
-
sessions.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
1319
|
-
|
|
1320
|
-
// Return only the first 5 sessions for performance
|
|
1321
|
-
return sessions.slice(0, 5);
|
|
1322
|
-
|
|
1323
|
-
} catch (error) {
|
|
1324
|
-
console.error('Error fetching Cursor sessions:', error);
|
|
1325
|
-
return [];
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
function normalizeComparablePath(inputPath) {
|
|
1331
|
-
if (!inputPath || typeof inputPath !== 'string') {
|
|
1332
|
-
return '';
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
const withoutLongPathPrefix = inputPath.startsWith('\\\\?\\')
|
|
1336
|
-
? inputPath.slice(4)
|
|
1337
|
-
: inputPath;
|
|
1338
|
-
const normalized = path.normalize(withoutLongPathPrefix.trim());
|
|
1339
|
-
|
|
1340
|
-
if (!normalized) {
|
|
1341
|
-
return '';
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
const resolved = path.resolve(normalized);
|
|
1345
|
-
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
async function findCodexJsonlFiles(dir) {
|
|
1349
|
-
const files = [];
|
|
1350
|
-
|
|
1351
|
-
try {
|
|
1352
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1353
|
-
for (const entry of entries) {
|
|
1354
|
-
const fullPath = path.join(dir, entry.name);
|
|
1355
|
-
if (entry.isDirectory()) {
|
|
1356
|
-
files.push(...await findCodexJsonlFiles(fullPath));
|
|
1357
|
-
} else if (entry.name.endsWith('.jsonl')) {
|
|
1358
|
-
files.push(fullPath);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
// Skip directories we can't read
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
return files;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
async function buildCodexSessionsIndex() {
|
|
1369
|
-
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
1370
|
-
const sessionsByProject = new Map();
|
|
1371
|
-
|
|
1372
|
-
try {
|
|
1373
|
-
await fs.access(codexSessionsDir);
|
|
1374
|
-
} catch (error) {
|
|
1375
|
-
return sessionsByProject;
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
const jsonlFiles = await findCodexJsonlFiles(codexSessionsDir);
|
|
1379
|
-
|
|
1380
|
-
for (const filePath of jsonlFiles) {
|
|
1381
|
-
try {
|
|
1382
|
-
const sessionData = await parseCodexSessionFile(filePath);
|
|
1383
|
-
if (!sessionData || !sessionData.id) {
|
|
1384
|
-
continue;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
const normalizedProjectPath = normalizeComparablePath(sessionData.cwd);
|
|
1388
|
-
if (!normalizedProjectPath) {
|
|
1389
|
-
continue;
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
const session = {
|
|
1393
|
-
id: sessionData.id,
|
|
1394
|
-
summary: sessionData.summary || 'Codex Session',
|
|
1395
|
-
messageCount: sessionData.messageCount || 0,
|
|
1396
|
-
lastActivity: sessionData.timestamp ? new Date(sessionData.timestamp) : new Date(),
|
|
1397
|
-
cwd: sessionData.cwd,
|
|
1398
|
-
model: sessionData.model,
|
|
1399
|
-
filePath,
|
|
1400
|
-
provider: 'codex',
|
|
1401
|
-
};
|
|
1402
|
-
|
|
1403
|
-
if (!sessionsByProject.has(normalizedProjectPath)) {
|
|
1404
|
-
sessionsByProject.set(normalizedProjectPath, []);
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
sessionsByProject.get(normalizedProjectPath).push(session);
|
|
1408
|
-
} catch (error) {
|
|
1409
|
-
console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
for (const sessions of sessionsByProject.values()) {
|
|
1414
|
-
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
return sessionsByProject;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
// Fetch Codex sessions for a given project path
|
|
1421
|
-
async function getCodexSessions(projectPath, options = {}) {
|
|
1422
|
-
const { limit = 5, indexRef = null } = options;
|
|
1423
|
-
try {
|
|
1424
|
-
const normalizedProjectPath = normalizeComparablePath(projectPath);
|
|
1425
|
-
if (!normalizedProjectPath) {
|
|
1426
|
-
return [];
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
if (indexRef && !indexRef.sessionsByProject) {
|
|
1430
|
-
indexRef.sessionsByProject = await buildCodexSessionsIndex();
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
const sessionsByProject = indexRef?.sessionsByProject || await buildCodexSessionsIndex();
|
|
1434
|
-
const sessions = sessionsByProject.get(normalizedProjectPath) || [];
|
|
1435
|
-
|
|
1436
|
-
// Return limited sessions for performance (0 = unlimited for deletion)
|
|
1437
|
-
return limit > 0 ? sessions.slice(0, limit) : [...sessions];
|
|
1438
|
-
|
|
1439
|
-
} catch (error) {
|
|
1440
|
-
console.error('Error fetching Codex sessions:', error);
|
|
1441
|
-
return [];
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// Parse a Codex session JSONL file to extract metadata
|
|
1446
|
-
async function parseCodexSessionFile(filePath) {
|
|
1447
|
-
try {
|
|
1448
|
-
const fileStream = fsSync.createReadStream(filePath);
|
|
1449
|
-
const rl = readline.createInterface({
|
|
1450
|
-
input: fileStream,
|
|
1451
|
-
crlfDelay: Infinity
|
|
1452
|
-
});
|
|
1453
|
-
|
|
1454
|
-
let sessionMeta = null;
|
|
1455
|
-
let lastTimestamp = null;
|
|
1456
|
-
let lastUserMessage = null;
|
|
1457
|
-
let messageCount = 0;
|
|
1458
|
-
|
|
1459
|
-
for await (const line of rl) {
|
|
1460
|
-
if (line.trim()) {
|
|
1461
|
-
try {
|
|
1462
|
-
const entry = JSON.parse(line);
|
|
1463
|
-
|
|
1464
|
-
// Track timestamp
|
|
1465
|
-
if (entry.timestamp) {
|
|
1466
|
-
lastTimestamp = entry.timestamp;
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
// Extract session metadata
|
|
1470
|
-
if (entry.type === 'session_meta' && entry.payload) {
|
|
1471
|
-
sessionMeta = {
|
|
1472
|
-
id: entry.payload.id,
|
|
1473
|
-
cwd: entry.payload.cwd,
|
|
1474
|
-
model: entry.payload.model || entry.payload.model_provider,
|
|
1475
|
-
timestamp: entry.timestamp,
|
|
1476
|
-
git: entry.payload.git
|
|
1477
|
-
};
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
// Count messages and extract user messages for summary
|
|
1481
|
-
if (entry.type === 'event_msg' && entry.payload?.type === 'user_message') {
|
|
1482
|
-
messageCount++;
|
|
1483
|
-
if (entry.payload.message) {
|
|
1484
|
-
lastUserMessage = entry.payload.message;
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
|
|
1489
|
-
messageCount++;
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
} catch (parseError) {
|
|
1493
|
-
// Skip malformed lines
|
|
1494
|
-
}
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
|
|
1498
|
-
if (sessionMeta) {
|
|
1499
|
-
return {
|
|
1500
|
-
...sessionMeta,
|
|
1501
|
-
timestamp: lastTimestamp || sessionMeta.timestamp,
|
|
1502
|
-
summary: lastUserMessage ?
|
|
1503
|
-
(lastUserMessage.length > 50 ? lastUserMessage.substring(0, 50) + '...' : lastUserMessage) :
|
|
1504
|
-
'Codex Session',
|
|
1505
|
-
messageCount
|
|
1506
|
-
};
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
return null;
|
|
1510
|
-
|
|
1511
|
-
} catch (error) {
|
|
1512
|
-
console.error('Error parsing Codex session file:', error);
|
|
1513
|
-
return null;
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
// Get messages for a specific Codex session
|
|
1518
|
-
async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
|
|
1519
|
-
try {
|
|
1520
|
-
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
1521
|
-
|
|
1522
|
-
// Find the session file by searching for the session ID
|
|
1523
|
-
const findSessionFile = async (dir) => {
|
|
1524
|
-
try {
|
|
1525
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1526
|
-
for (const entry of entries) {
|
|
1527
|
-
const fullPath = path.join(dir, entry.name);
|
|
1528
|
-
if (entry.isDirectory()) {
|
|
1529
|
-
const found = await findSessionFile(fullPath);
|
|
1530
|
-
if (found) return found;
|
|
1531
|
-
} else if (entry.name.includes(sessionId) && entry.name.endsWith('.jsonl')) {
|
|
1532
|
-
return fullPath;
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
} catch (error) {
|
|
1536
|
-
// Skip directories we can't read
|
|
1537
|
-
}
|
|
1538
|
-
return null;
|
|
1539
|
-
};
|
|
1540
|
-
|
|
1541
|
-
const sessionFilePath = await findSessionFile(codexSessionsDir);
|
|
1542
|
-
|
|
1543
|
-
if (!sessionFilePath) {
|
|
1544
|
-
console.warn(`Codex session file not found for session ${sessionId}`);
|
|
1545
|
-
return { messages: [], total: 0, hasMore: false };
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
const messages = [];
|
|
1549
|
-
let tokenUsage = null;
|
|
1550
|
-
const fileStream = fsSync.createReadStream(sessionFilePath);
|
|
1551
|
-
const rl = readline.createInterface({
|
|
1552
|
-
input: fileStream,
|
|
1553
|
-
crlfDelay: Infinity
|
|
1554
|
-
});
|
|
1555
|
-
|
|
1556
|
-
// Helper to extract text from Codex content array
|
|
1557
|
-
const extractText = (content) => {
|
|
1558
|
-
if (!Array.isArray(content)) return content;
|
|
1559
|
-
return content
|
|
1560
|
-
.map(item => {
|
|
1561
|
-
if (item.type === 'input_text' || item.type === 'output_text') {
|
|
1562
|
-
return item.text;
|
|
1563
|
-
}
|
|
1564
|
-
if (item.type === 'text') {
|
|
1565
|
-
return item.text;
|
|
1566
|
-
}
|
|
1567
|
-
return '';
|
|
1568
|
-
})
|
|
1569
|
-
.filter(Boolean)
|
|
1570
|
-
.join('\n');
|
|
1571
|
-
};
|
|
1572
|
-
|
|
1573
|
-
for await (const line of rl) {
|
|
1574
|
-
if (line.trim()) {
|
|
1575
|
-
try {
|
|
1576
|
-
const entry = JSON.parse(line);
|
|
1577
|
-
|
|
1578
|
-
// Extract token usage from token_count events (keep latest)
|
|
1579
|
-
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
1580
|
-
const info = entry.payload.info;
|
|
1581
|
-
if (info.total_token_usage) {
|
|
1582
|
-
tokenUsage = {
|
|
1583
|
-
used: info.total_token_usage.total_tokens || 0,
|
|
1584
|
-
total: info.model_context_window || 200000
|
|
1585
|
-
};
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
// Extract messages from response_item
|
|
1590
|
-
if (entry.type === 'response_item' && entry.payload?.type === 'message') {
|
|
1591
|
-
const content = entry.payload.content;
|
|
1592
|
-
const role = entry.payload.role || 'assistant';
|
|
1593
|
-
const textContent = extractText(content);
|
|
1594
|
-
|
|
1595
|
-
// Skip system context messages (environment_context)
|
|
1596
|
-
if (textContent?.includes('<environment_context>')) {
|
|
1597
|
-
continue;
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
// Only add if there's actual content
|
|
1601
|
-
if (textContent?.trim()) {
|
|
1602
|
-
messages.push({
|
|
1603
|
-
type: role === 'user' ? 'user' : 'assistant',
|
|
1604
|
-
timestamp: entry.timestamp,
|
|
1605
|
-
message: {
|
|
1606
|
-
role: role,
|
|
1607
|
-
content: textContent
|
|
1608
|
-
}
|
|
1609
|
-
});
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
if (entry.type === 'response_item' && entry.payload?.type === 'reasoning') {
|
|
1614
|
-
const summaryText = entry.payload.summary
|
|
1615
|
-
?.map(s => s.text)
|
|
1616
|
-
.filter(Boolean)
|
|
1617
|
-
.join('\n');
|
|
1618
|
-
if (summaryText?.trim()) {
|
|
1619
|
-
messages.push({
|
|
1620
|
-
type: 'thinking',
|
|
1621
|
-
timestamp: entry.timestamp,
|
|
1622
|
-
message: {
|
|
1623
|
-
role: 'assistant',
|
|
1624
|
-
content: summaryText
|
|
1625
|
-
}
|
|
1626
|
-
});
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
if (entry.type === 'response_item' && entry.payload?.type === 'function_call') {
|
|
1631
|
-
let toolName = entry.payload.name;
|
|
1632
|
-
let toolInput = entry.payload.arguments;
|
|
1633
|
-
|
|
1634
|
-
// Map Codex tool names to Claude equivalents
|
|
1635
|
-
if (toolName === 'shell_command') {
|
|
1636
|
-
toolName = 'Bash';
|
|
1637
|
-
try {
|
|
1638
|
-
const args = JSON.parse(entry.payload.arguments);
|
|
1639
|
-
toolInput = JSON.stringify({ command: args.command });
|
|
1640
|
-
} catch (e) {
|
|
1641
|
-
// Keep original if parsing fails
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
messages.push({
|
|
1646
|
-
type: 'tool_use',
|
|
1647
|
-
timestamp: entry.timestamp,
|
|
1648
|
-
toolName: toolName,
|
|
1649
|
-
toolInput: toolInput,
|
|
1650
|
-
toolCallId: entry.payload.call_id
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
if (entry.type === 'response_item' && entry.payload?.type === 'function_call_output') {
|
|
1655
|
-
messages.push({
|
|
1656
|
-
type: 'tool_result',
|
|
1657
|
-
timestamp: entry.timestamp,
|
|
1658
|
-
toolCallId: entry.payload.call_id,
|
|
1659
|
-
output: entry.payload.output
|
|
1660
|
-
});
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call') {
|
|
1664
|
-
const toolName = entry.payload.name || 'custom_tool';
|
|
1665
|
-
const input = entry.payload.input || '';
|
|
1666
|
-
|
|
1667
|
-
if (toolName === 'apply_patch') {
|
|
1668
|
-
// Parse Codex patch format and convert to Claude Edit format
|
|
1669
|
-
const fileMatch = input.match(/\*\*\* Update File: (.+)/);
|
|
1670
|
-
const filePath = fileMatch ? fileMatch[1].trim() : 'unknown';
|
|
1671
|
-
|
|
1672
|
-
// Extract old and new content from patch
|
|
1673
|
-
const lines = input.split('\n');
|
|
1674
|
-
const oldLines = [];
|
|
1675
|
-
const newLines = [];
|
|
1676
|
-
|
|
1677
|
-
for (const line of lines) {
|
|
1678
|
-
if (line.startsWith('-') && !line.startsWith('---')) {
|
|
1679
|
-
oldLines.push(line.substring(1));
|
|
1680
|
-
} else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
1681
|
-
newLines.push(line.substring(1));
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
messages.push({
|
|
1686
|
-
type: 'tool_use',
|
|
1687
|
-
timestamp: entry.timestamp,
|
|
1688
|
-
toolName: 'Edit',
|
|
1689
|
-
toolInput: JSON.stringify({
|
|
1690
|
-
file_path: filePath,
|
|
1691
|
-
old_string: oldLines.join('\n'),
|
|
1692
|
-
new_string: newLines.join('\n')
|
|
1693
|
-
}),
|
|
1694
|
-
toolCallId: entry.payload.call_id
|
|
1695
|
-
});
|
|
1696
|
-
} else {
|
|
1697
|
-
messages.push({
|
|
1698
|
-
type: 'tool_use',
|
|
1699
|
-
timestamp: entry.timestamp,
|
|
1700
|
-
toolName: toolName,
|
|
1701
|
-
toolInput: input,
|
|
1702
|
-
toolCallId: entry.payload.call_id
|
|
1703
|
-
});
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
if (entry.type === 'response_item' && entry.payload?.type === 'custom_tool_call_output') {
|
|
1708
|
-
messages.push({
|
|
1709
|
-
type: 'tool_result',
|
|
1710
|
-
timestamp: entry.timestamp,
|
|
1711
|
-
toolCallId: entry.payload.call_id,
|
|
1712
|
-
output: entry.payload.output || ''
|
|
1713
|
-
});
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
} catch (parseError) {
|
|
1717
|
-
// Skip malformed lines
|
|
1718
|
-
}
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
// Sort by timestamp
|
|
1723
|
-
messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
|
|
1724
|
-
|
|
1725
|
-
const total = messages.length;
|
|
1726
|
-
|
|
1727
|
-
// Apply pagination if limit is specified
|
|
1728
|
-
if (limit !== null) {
|
|
1729
|
-
const startIndex = Math.max(0, total - offset - limit);
|
|
1730
|
-
const endIndex = total - offset;
|
|
1731
|
-
const paginatedMessages = messages.slice(startIndex, endIndex);
|
|
1732
|
-
const hasMore = startIndex > 0;
|
|
1733
|
-
|
|
1734
|
-
return {
|
|
1735
|
-
messages: paginatedMessages,
|
|
1736
|
-
total,
|
|
1737
|
-
hasMore,
|
|
1738
|
-
offset,
|
|
1739
|
-
limit,
|
|
1740
|
-
tokenUsage
|
|
1741
|
-
};
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
return { messages, tokenUsage };
|
|
1745
|
-
|
|
1746
|
-
} catch (error) {
|
|
1747
|
-
console.error(`Error reading Codex session messages for ${sessionId}:`, error);
|
|
1748
|
-
return { messages: [], total: 0, hasMore: false };
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
async function deleteCodexSession(sessionId) {
|
|
1753
|
-
try {
|
|
1754
|
-
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
1755
|
-
|
|
1756
|
-
const findJsonlFiles = async (dir) => {
|
|
1757
|
-
const files = [];
|
|
1758
|
-
try {
|
|
1759
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1760
|
-
for (const entry of entries) {
|
|
1761
|
-
const fullPath = path.join(dir, entry.name);
|
|
1762
|
-
if (entry.isDirectory()) {
|
|
1763
|
-
files.push(...await findJsonlFiles(fullPath));
|
|
1764
|
-
} else if (entry.name.endsWith('.jsonl')) {
|
|
1765
|
-
files.push(fullPath);
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
} catch (error) {}
|
|
1769
|
-
return files;
|
|
1770
|
-
};
|
|
1771
|
-
|
|
1772
|
-
const jsonlFiles = await findJsonlFiles(codexSessionsDir);
|
|
1773
|
-
|
|
1774
|
-
for (const filePath of jsonlFiles) {
|
|
1775
|
-
const sessionData = await parseCodexSessionFile(filePath);
|
|
1776
|
-
if (sessionData && sessionData.id === sessionId) {
|
|
1777
|
-
await fs.unlink(filePath);
|
|
1778
|
-
return true;
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
throw new Error(`Codex session file not found for session ${sessionId}`);
|
|
1783
|
-
} catch (error) {
|
|
1784
|
-
console.error(`Error deleting Codex session ${sessionId}:`, error);
|
|
1785
|
-
throw error;
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
export {
|
|
1790
|
-
getProjects,
|
|
1791
|
-
getSessions,
|
|
1792
|
-
getSessionMessages,
|
|
1793
|
-
parseJsonlSessions,
|
|
1794
|
-
renameProject,
|
|
1795
|
-
deleteSession,
|
|
1796
|
-
isProjectEmpty,
|
|
1797
|
-
deleteProject,
|
|
1798
|
-
addProjectManually,
|
|
1799
|
-
loadProjectConfig,
|
|
1800
|
-
saveProjectConfig,
|
|
1801
|
-
extractProjectDirectory,
|
|
1802
|
-
clearProjectDirectoryCache,
|
|
1803
|
-
getCodexSessions,
|
|
1804
|
-
getCodexSessionMessages,
|
|
1805
|
-
deleteCodexSession,
|
|
1806
|
-
setCloudUserId
|
|
1807
|
-
};
|