aisessions 1.0.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 +24 -0
- package/bin/cli.js +65 -0
- package/package.json +29 -0
- package/src/agents/amp.js +51 -0
- package/src/agents/claude.js +102 -0
- package/src/agents/codebuff.js +38 -0
- package/src/agents/codex.js +118 -0
- package/src/agents/copilot.js +43 -0
- package/src/agents/cursor.js +53 -0
- package/src/agents/droid.js +38 -0
- package/src/agents/gemini.js +50 -0
- package/src/agents/goose.js +52 -0
- package/src/agents/hermes.js +38 -0
- package/src/agents/index.js +39 -0
- package/src/agents/kilo.js +38 -0
- package/src/agents/kimi.js +38 -0
- package/src/agents/openclaw.js +38 -0
- package/src/agents/opencode.js +50 -0
- package/src/agents/pi.js +41 -0
- package/src/agents/qwen.js +38 -0
- package/src/agents/utils.js +283 -0
- package/src/router.js +83 -0
- package/src/server.js +12 -0
- package/src/storage/backup.js +93 -0
- package/src/storage/trash.js +93 -0
- package/src/ui/css.js +392 -0
- package/src/ui/js.js +528 -0
- package/src/ui/render.js +138 -0
- package/src/usage/index.js +111 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# aisessions
|
|
2
|
+
|
|
3
|
+
Browse, manage, and analyze local AI coding agent sessions from one browser UI.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx aisessions
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npx aisessions --port 7879
|
|
15
|
+
npx aisessions --no-open
|
|
16
|
+
npx aisessions --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The server binds to `127.0.0.1` and opens the UI in your browser by default.
|
|
20
|
+
|
|
21
|
+
## Data
|
|
22
|
+
|
|
23
|
+
The tool reads local agent session folders such as `~/.claude/projects` and
|
|
24
|
+
`~/.codex`. Trash and backup data is stored under `~/.aisessions`.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer } from '../src/server.js'
|
|
3
|
+
import { parseArgs } from 'node:util'
|
|
4
|
+
import { exec } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
const VERSION = '1.0.0'
|
|
7
|
+
|
|
8
|
+
const { values } = parseArgs({
|
|
9
|
+
options: {
|
|
10
|
+
port: { type: 'string', short: 'p', default: '7878' },
|
|
11
|
+
'no-open': { type: 'boolean', default: false },
|
|
12
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
13
|
+
version: { type: 'boolean', short: 'v', default: false },
|
|
14
|
+
},
|
|
15
|
+
strict: false,
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
if (values.version) { console.log(`aisessions v${VERSION}`); process.exit(0) }
|
|
19
|
+
|
|
20
|
+
if (values.help) {
|
|
21
|
+
console.log(`
|
|
22
|
+
aisessions v${VERSION} — AI coding agent session manager
|
|
23
|
+
|
|
24
|
+
Usage: npx aisessions [options]
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
-p, --port <port> Port to listen on (default: 7878)
|
|
28
|
+
--no-open Don't auto-open browser
|
|
29
|
+
-h, --help Show this help
|
|
30
|
+
-v, --version Show version
|
|
31
|
+
|
|
32
|
+
Manages sessions for:
|
|
33
|
+
Claude Code · Codex · OpenCode · Amp · Goose · Gemini CLI
|
|
34
|
+
|
|
35
|
+
Data folders (read-only unless you delete/trash/backup):
|
|
36
|
+
~/.claude/projects/ ~/.codex/
|
|
37
|
+
|
|
38
|
+
Trash & backup folder:
|
|
39
|
+
~/.aisessions/
|
|
40
|
+
`)
|
|
41
|
+
process.exit(0)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const PORT = parseInt(values.port, 10) || 7878
|
|
45
|
+
const server = createServer()
|
|
46
|
+
|
|
47
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
48
|
+
const url = `http://127.0.0.1:${PORT}`
|
|
49
|
+
console.log(`\n aisessions ready -> ${url}\n`)
|
|
50
|
+
|
|
51
|
+
if (!values['no-open']) {
|
|
52
|
+
const cmd = process.platform === 'darwin' ? `open "${url}"` :
|
|
53
|
+
process.platform === 'win32' ? `start "${url}"` :
|
|
54
|
+
`xdg-open "${url}"`
|
|
55
|
+
exec(cmd, () => {})
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
server.on('error', err => {
|
|
60
|
+
if (err.code === 'EADDRINUSE')
|
|
61
|
+
console.error(` Port ${PORT} is in use. Try: npx aisessions --port ${PORT + 1}`)
|
|
62
|
+
else
|
|
63
|
+
console.error(' Server error:', err.message)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aisessions",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Browse, manage and analyze your AI coding agent sessions — Claude Code, Codex, and more",
|
|
5
|
+
"bin": {
|
|
6
|
+
"aisessions": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node bin/cli.js",
|
|
18
|
+
"dev": "node bin/cli.js --no-open"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"claude",
|
|
22
|
+
"codex",
|
|
23
|
+
"ai",
|
|
24
|
+
"sessions",
|
|
25
|
+
"aisessions",
|
|
26
|
+
"ccusage"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { join, basename } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
readLimited, parseJsonl, pathSize, humanSize, ignored,
|
|
6
|
+
titleFromObjs, fileTitle, dateFromPath, projName, guessProj, fmtDate,
|
|
7
|
+
} from './utils.js'
|
|
8
|
+
|
|
9
|
+
const HOME = homedir()
|
|
10
|
+
const DATA_DIR = process.env.AMP_DATA_DIR || join(HOME, '.amp')
|
|
11
|
+
const THREAD_DIR = join(DATA_DIR, 'threads')
|
|
12
|
+
|
|
13
|
+
export const AGENT_ID = 'amp'
|
|
14
|
+
export const AGENT_LABEL = 'Amp CLI'
|
|
15
|
+
export const AGENT_COLOR = '#a0a0a0'
|
|
16
|
+
|
|
17
|
+
export function isInstalled() { return existsSync(DATA_DIR) }
|
|
18
|
+
|
|
19
|
+
export async function loadSessions() {
|
|
20
|
+
const items = []
|
|
21
|
+
if (!existsSync(THREAD_DIR)) return items
|
|
22
|
+
|
|
23
|
+
const walk = dir => {
|
|
24
|
+
try {
|
|
25
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
26
|
+
const full = join(dir, e.name)
|
|
27
|
+
if (ignored(full)) continue
|
|
28
|
+
if (e.isDirectory()) walk(full)
|
|
29
|
+
else if (e.name.endsWith('.jsonl') || e.name.endsWith('.json')) {
|
|
30
|
+
const text = readLimited(full)
|
|
31
|
+
const objs = parseJsonl(text)
|
|
32
|
+
const [title, tsrc] = titleFromObjs(objs)
|
|
33
|
+
const pp = guessProj(text) || 'Unknown'
|
|
34
|
+
const date = dateFromPath(full)
|
|
35
|
+
const size = pathSize(full)
|
|
36
|
+
items.push({
|
|
37
|
+
agent: AGENT_ID, agentLabel: AGENT_LABEL,
|
|
38
|
+
path: full, sessionId: basename(full, e.name.endsWith('.jsonl') ? '.jsonl' : '.json'),
|
|
39
|
+
title: title || fileTitle(full), titleSource: tsrc || 'filename',
|
|
40
|
+
project: projName(pp), projectPath: pp,
|
|
41
|
+
date, dateLabel: fmtDate(date),
|
|
42
|
+
size, sizeLabel: humanSize(size),
|
|
43
|
+
filename: e.name, msgCount: objs.length,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
walk(THREAD_DIR)
|
|
50
|
+
return items.sort((a,b) => b.date - a.date)
|
|
51
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { join, basename } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
readLimited, parseJsonl, cleanStr, badTitle, truncStr,
|
|
6
|
+
dateFromPath, projName, pathSize, humanSize, ignored,
|
|
7
|
+
CLAUDE_SKIP_PREFIX, fileTitle, fmtDate,
|
|
8
|
+
} from './utils.js'
|
|
9
|
+
|
|
10
|
+
const HOME = homedir()
|
|
11
|
+
const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(HOME, '.claude')
|
|
12
|
+
const PROJ_DIR = join(CLAUDE_DIR, 'projects')
|
|
13
|
+
|
|
14
|
+
export const AGENT_ID = 'claude'
|
|
15
|
+
export const AGENT_LABEL = 'Claude Code'
|
|
16
|
+
export const AGENT_COLOR = '#e0e0e0'
|
|
17
|
+
|
|
18
|
+
export function isInstalled() { return existsSync(PROJ_DIR) }
|
|
19
|
+
|
|
20
|
+
function claudeTexts(content) {
|
|
21
|
+
if (typeof content === 'string') return [content]
|
|
22
|
+
if (Array.isArray(content)) {
|
|
23
|
+
return content.flatMap(b => {
|
|
24
|
+
if (typeof b === 'string') return [b]
|
|
25
|
+
if (b && typeof b.text === 'string') return [b.text]
|
|
26
|
+
return []
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
return []
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isMeta(s) {
|
|
33
|
+
s = s.trimStart()
|
|
34
|
+
return s.startsWith('<') && CLAUDE_SKIP_PREFIX.some(p => s.startsWith(p))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function loadSessions() {
|
|
38
|
+
const items = []
|
|
39
|
+
if (!existsSync(PROJ_DIR)) return items
|
|
40
|
+
|
|
41
|
+
let projDirs
|
|
42
|
+
try { projDirs = readdirSync(PROJ_DIR, { withFileTypes: true }).filter(e => e.isDirectory()) }
|
|
43
|
+
catch { return items }
|
|
44
|
+
|
|
45
|
+
for (const pd of projDirs) {
|
|
46
|
+
const projPath = join(PROJ_DIR, pd.name)
|
|
47
|
+
let sessFiles
|
|
48
|
+
try { sessFiles = readdirSync(projPath).filter(f => f.endsWith('.jsonl')).sort() }
|
|
49
|
+
catch { continue }
|
|
50
|
+
|
|
51
|
+
for (const sf of sessFiles) {
|
|
52
|
+
const sessPath = join(projPath, sf)
|
|
53
|
+
if (ignored(sessPath)) continue
|
|
54
|
+
|
|
55
|
+
const sid = basename(sf, '.jsonl')
|
|
56
|
+
let title = '', pp = '', date = null, msgCount = 0
|
|
57
|
+
|
|
58
|
+
for (const line of readLimited(sessPath).split('\n')) {
|
|
59
|
+
const l = line.trim()
|
|
60
|
+
if (!l) continue
|
|
61
|
+
let obj
|
|
62
|
+
try { obj = JSON.parse(l) } catch { continue }
|
|
63
|
+
msgCount++
|
|
64
|
+
|
|
65
|
+
if (!pp && typeof obj.cwd === 'string' && obj.cwd.startsWith('/')) pp = obj.cwd
|
|
66
|
+
|
|
67
|
+
if (date === null && obj.timestamp) {
|
|
68
|
+
try { date = new Date(obj.timestamp) } catch {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!title && !obj.isMeta) {
|
|
72
|
+
const msg = obj.message
|
|
73
|
+
if (msg && msg.role === 'user') {
|
|
74
|
+
for (const t of claudeTexts(msg.content || '')) {
|
|
75
|
+
const tc = cleanStr(t)
|
|
76
|
+
if (tc && !isMeta(tc)) {
|
|
77
|
+
const tr = truncStr(tc)
|
|
78
|
+
if (!badTitle(tr)) { title = tr; break }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!title) title = fileTitle(sessPath)
|
|
86
|
+
if (!pp) pp = pd.name.startsWith('-') ? '/' + pd.name.slice(1).replace(/-/g, '/') : pd.name
|
|
87
|
+
if (!date) date = dateFromPath(sessPath)
|
|
88
|
+
const size = pathSize(sessPath)
|
|
89
|
+
|
|
90
|
+
items.push({
|
|
91
|
+
agent: AGENT_ID, agentLabel: AGENT_LABEL,
|
|
92
|
+
path: sessPath, sessionId: sid,
|
|
93
|
+
title, titleSource: title ? 'user message' : 'filename',
|
|
94
|
+
project: projName(pp), projectPath: pp,
|
|
95
|
+
date, dateLabel: fmtDate(date),
|
|
96
|
+
size, sizeLabel: humanSize(size),
|
|
97
|
+
filename: sf, msgCount,
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return items.sort((a,b) => b.date - a.date)
|
|
102
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { join, basename } from 'node:path'
|
|
4
|
+
import { readLimited, parseJsonl, pathSize, humanSize, ignored, titleFromObjs, fileTitle, dateFromPath, projName, guessProj, fmtDate } from './utils.js'
|
|
5
|
+
|
|
6
|
+
const HOME = homedir()
|
|
7
|
+
const DATA_DIR = process.env.CODEBUFF_DATA_DIR || join(HOME, '.codebuff')
|
|
8
|
+
|
|
9
|
+
export const AGENT_ID = 'codebuff'
|
|
10
|
+
export const AGENT_LABEL = 'Codebuff'
|
|
11
|
+
export const AGENT_COLOR = '#777'
|
|
12
|
+
|
|
13
|
+
export function isInstalled() { return existsSync(DATA_DIR) }
|
|
14
|
+
|
|
15
|
+
export async function loadSessions() {
|
|
16
|
+
const items = []
|
|
17
|
+
const walk = dir => {
|
|
18
|
+
if (!existsSync(dir)) return
|
|
19
|
+
try {
|
|
20
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
21
|
+
const full = join(dir, e.name)
|
|
22
|
+
if (ignored(full)) continue
|
|
23
|
+
if (e.isDirectory()) walk(full)
|
|
24
|
+
else if (e.name.endsWith('.jsonl') || e.name.endsWith('.json')) {
|
|
25
|
+
const text = readLimited(full)
|
|
26
|
+
const objs = parseJsonl(text)
|
|
27
|
+
const [title, tsrc] = titleFromObjs(objs)
|
|
28
|
+
const pp = guessProj(text) || 'Unknown'
|
|
29
|
+
const date = dateFromPath(full)
|
|
30
|
+
const size = pathSize(full)
|
|
31
|
+
items.push({ agent: AGENT_ID, agentLabel: AGENT_LABEL, path: full, sessionId: basename(full, e.name.endsWith('.jsonl') ? '.jsonl' : '.json'), title: title || fileTitle(full), titleSource: tsrc || 'filename', project: projName(pp), projectPath: pp, date, dateLabel: fmtDate(date), size, sizeLabel: humanSize(size), filename: e.name, msgCount: objs.length })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
walk(DATA_DIR)
|
|
37
|
+
return items.sort((a,b) => b.date - a.date)
|
|
38
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { join, basename } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
readLimited, parseJsonl, cleanStr, badTitle, truncStr,
|
|
6
|
+
sidFromPath, dateFromPath, projName, pathSize, humanSize,
|
|
7
|
+
ignored, PROJECT_KEYS, findStrings, titleFromObjs, regexTitle,
|
|
8
|
+
fileTitle, guessProj, fmtDate,
|
|
9
|
+
} from './utils.js'
|
|
10
|
+
|
|
11
|
+
const HOME = homedir()
|
|
12
|
+
const CODEX_DIR = process.env.CODEX_HOME || join(HOME, '.codex')
|
|
13
|
+
const SESS_DIRS = [join(CODEX_DIR, 'sessions'), join(CODEX_DIR, 'browser', 'sessions')]
|
|
14
|
+
const IDX_FILES = [
|
|
15
|
+
join(CODEX_DIR, 'session_index.jsonl'),
|
|
16
|
+
join(CODEX_DIR, 'history.jsonl'),
|
|
17
|
+
join(CODEX_DIR, 'history.json'),
|
|
18
|
+
join(CODEX_DIR, 'transcription-history.jsonl'),
|
|
19
|
+
]
|
|
20
|
+
const PROTECTED = new Set(['config.toml','auth.json','credentials.json','settings.json'])
|
|
21
|
+
|
|
22
|
+
export const AGENT_ID = 'codex'
|
|
23
|
+
export const AGENT_LABEL = 'Codex'
|
|
24
|
+
export const AGENT_COLOR = '#c0c0c0'
|
|
25
|
+
|
|
26
|
+
export function isInstalled() { return existsSync(CODEX_DIR) }
|
|
27
|
+
|
|
28
|
+
function buildIndex() {
|
|
29
|
+
const idx = {}
|
|
30
|
+
for (const f of IDX_FILES) {
|
|
31
|
+
if (!existsSync(f)) continue
|
|
32
|
+
const objs = parseJsonl(readLimited(f, 12_000_000))
|
|
33
|
+
for (const obj of objs) {
|
|
34
|
+
const strs = findStrings(obj, new Set(['id','session_id','conversation_id','path','file','filename','session_path']))
|
|
35
|
+
const ids = new Set()
|
|
36
|
+
for (const s of strs) {
|
|
37
|
+
const sc = cleanStr(s)
|
|
38
|
+
const m = sc.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i)
|
|
39
|
+
if (m) ids.add(m[1])
|
|
40
|
+
if (sc.endsWith('.json') || sc.endsWith('.jsonl')) {
|
|
41
|
+
ids.add(basename(sc).replace(/\.(jsonl|json)$/, ''))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const [t, ts] = titleFromObjs([obj])
|
|
45
|
+
const pp = findStrings(obj, PROJECT_KEYS).find(v => v.includes('/') || v.includes('\\')) || ''
|
|
46
|
+
for (const sid of ids) {
|
|
47
|
+
if (!idx[sid] || (t && !idx[sid].title)) idx[sid] = { title: t, titleSource: ts, project: pp }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return idx
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isSubagent(text) {
|
|
55
|
+
for (const line of text.split('\n').slice(0, 20)) {
|
|
56
|
+
const l = line.trim()
|
|
57
|
+
if (!l) continue
|
|
58
|
+
try {
|
|
59
|
+
const obj = JSON.parse(l)
|
|
60
|
+
if (obj.type === 'session_meta') {
|
|
61
|
+
return cleanStr(String(obj.payload?.thread_source || '')).toLowerCase() === 'subagent'
|
|
62
|
+
}
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function allSessFiles() {
|
|
69
|
+
const files = []
|
|
70
|
+
const walk = dir => {
|
|
71
|
+
if (!existsSync(dir)) return
|
|
72
|
+
try {
|
|
73
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
74
|
+
const full = join(dir, e.name)
|
|
75
|
+
if (ignored(full) || PROTECTED.has(e.name)) continue
|
|
76
|
+
if (e.isDirectory()) walk(full)
|
|
77
|
+
else if (e.name.endsWith('.json') || e.name.endsWith('.jsonl')) files.push(full)
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
SESS_DIRS.forEach(walk)
|
|
82
|
+
return files
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function loadSessions() {
|
|
86
|
+
const idx = buildIndex()
|
|
87
|
+
const items = []
|
|
88
|
+
|
|
89
|
+
for (const p of allSessFiles()) {
|
|
90
|
+
const sid = sidFromPath(p)
|
|
91
|
+
const text = readLimited(p)
|
|
92
|
+
if (isSubagent(text)) continue
|
|
93
|
+
|
|
94
|
+
const objs = parseJsonl(text)
|
|
95
|
+
const info = idx[sid] || {}
|
|
96
|
+
|
|
97
|
+
let [title, tsrc] = ['', '']
|
|
98
|
+
if (info.title && !badTitle(info.title)) { title = truncStr(info.title); tsrc = 'index' }
|
|
99
|
+
if (!title) [title, tsrc] = titleFromObjs(objs)
|
|
100
|
+
if (!title) [title, tsrc] = regexTitle(text)
|
|
101
|
+
if (!title) { title = fileTitle(p); tsrc = 'filename' }
|
|
102
|
+
|
|
103
|
+
const pp = info.project || guessProj(text) || 'Unknown'
|
|
104
|
+
const date = dateFromPath(p)
|
|
105
|
+
const size = pathSize(p)
|
|
106
|
+
|
|
107
|
+
items.push({
|
|
108
|
+
agent: AGENT_ID, agentLabel: AGENT_LABEL,
|
|
109
|
+
path: p, sessionId: sid,
|
|
110
|
+
title, titleSource: tsrc,
|
|
111
|
+
project: projName(pp), projectPath: pp,
|
|
112
|
+
date, dateLabel: fmtDate(date),
|
|
113
|
+
size, sizeLabel: humanSize(size),
|
|
114
|
+
filename: basename(p), msgCount: objs.length,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
return items.sort((a,b) => b.date - a.date)
|
|
118
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { join, basename } from 'node:path'
|
|
4
|
+
import { readLimited, parseJsonl, pathSize, humanSize, ignored, titleFromObjs, fileTitle, dateFromPath, projName, guessProj, fmtDate } from './utils.js'
|
|
5
|
+
|
|
6
|
+
const HOME = homedir()
|
|
7
|
+
const CANDIDATE_DIRS = [
|
|
8
|
+
process.env.COPILOT_DATA_DIR,
|
|
9
|
+
join(HOME, '.config', 'github-copilot'),
|
|
10
|
+
join(HOME, 'Library', 'Application Support', 'GitHub Copilot'),
|
|
11
|
+
join(HOME, '.github-copilot'),
|
|
12
|
+
].filter(Boolean)
|
|
13
|
+
|
|
14
|
+
export const AGENT_ID = 'copilot'
|
|
15
|
+
export const AGENT_LABEL = 'Copilot'
|
|
16
|
+
export const AGENT_COLOR = '#ccc'
|
|
17
|
+
|
|
18
|
+
export function isInstalled() { return CANDIDATE_DIRS.some(d => existsSync(d)) }
|
|
19
|
+
|
|
20
|
+
export async function loadSessions() {
|
|
21
|
+
const items = []
|
|
22
|
+
const walk = dir => {
|
|
23
|
+
if (!existsSync(dir)) return
|
|
24
|
+
try {
|
|
25
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
26
|
+
const full = join(dir, e.name)
|
|
27
|
+
if (ignored(full)) continue
|
|
28
|
+
if (e.isDirectory()) walk(full)
|
|
29
|
+
else if (e.name.endsWith('.jsonl') || e.name.endsWith('.json')) {
|
|
30
|
+
const text = readLimited(full)
|
|
31
|
+
const objs = parseJsonl(text)
|
|
32
|
+
const [title, tsrc] = titleFromObjs(objs)
|
|
33
|
+
const pp = guessProj(text) || 'Unknown'
|
|
34
|
+
const date = dateFromPath(full)
|
|
35
|
+
const size = pathSize(full)
|
|
36
|
+
items.push({ agent: AGENT_ID, agentLabel: AGENT_LABEL, path: full, sessionId: basename(full, e.name.endsWith('.jsonl') ? '.jsonl' : '.json'), title: title || fileTitle(full), titleSource: tsrc || 'filename', project: projName(pp), projectPath: pp, date, dateLabel: fmtDate(date), size, sizeLabel: humanSize(size), filename: e.name, msgCount: objs.length })
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
CANDIDATE_DIRS.forEach(walk)
|
|
42
|
+
return items.sort((a,b) => b.date - a.date)
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { join, basename } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
readLimited, parseJsonl, pathSize, humanSize, ignored,
|
|
6
|
+
titleFromObjs, fileTitle, dateFromPath, projName, guessProj, fmtDate,
|
|
7
|
+
} from './utils.js'
|
|
8
|
+
|
|
9
|
+
const HOME = homedir()
|
|
10
|
+
const DATA_DIRS = [
|
|
11
|
+
process.env.CURSOR_DATA_DIR,
|
|
12
|
+
join(HOME, 'Library', 'Application Support', 'Cursor', 'User', 'workspaceStorage'),
|
|
13
|
+
join(HOME, '.config', 'Cursor', 'User', 'workspaceStorage'),
|
|
14
|
+
].filter(Boolean)
|
|
15
|
+
|
|
16
|
+
export const AGENT_ID = 'cursor'
|
|
17
|
+
export const AGENT_LABEL = 'Cursor'
|
|
18
|
+
export const AGENT_COLOR = '#707070'
|
|
19
|
+
|
|
20
|
+
export function isInstalled() { return DATA_DIRS.some(d => existsSync(d)) }
|
|
21
|
+
|
|
22
|
+
export async function loadSessions() {
|
|
23
|
+
const items = []
|
|
24
|
+
const walk = dir => {
|
|
25
|
+
if (!existsSync(dir)) return
|
|
26
|
+
try {
|
|
27
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
28
|
+
const full = join(dir, e.name)
|
|
29
|
+
if (ignored(full)) continue
|
|
30
|
+
if (e.isDirectory()) walk(full)
|
|
31
|
+
else if (e.name.endsWith('.jsonl') || e.name === 'chat.json') {
|
|
32
|
+
const text = readLimited(full)
|
|
33
|
+
const objs = parseJsonl(text)
|
|
34
|
+
const [title, tsrc] = titleFromObjs(objs)
|
|
35
|
+
const pp = guessProj(text) || 'Unknown'
|
|
36
|
+
const date = dateFromPath(full)
|
|
37
|
+
const size = pathSize(full)
|
|
38
|
+
items.push({
|
|
39
|
+
agent: AGENT_ID, agentLabel: AGENT_LABEL,
|
|
40
|
+
path: full, sessionId: basename(full, '.jsonl'),
|
|
41
|
+
title: title || fileTitle(full), titleSource: tsrc || 'filename',
|
|
42
|
+
project: projName(pp), projectPath: pp,
|
|
43
|
+
date, dateLabel: fmtDate(date),
|
|
44
|
+
size, sizeLabel: humanSize(size),
|
|
45
|
+
filename: e.name, msgCount: objs.length,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
DATA_DIRS.forEach(walk)
|
|
52
|
+
return items.sort((a,b) => b.date - a.date)
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { join, basename } from 'node:path'
|
|
4
|
+
import { readLimited, parseJsonl, pathSize, humanSize, ignored, titleFromObjs, fileTitle, dateFromPath, projName, guessProj, fmtDate } from './utils.js'
|
|
5
|
+
|
|
6
|
+
const HOME = homedir()
|
|
7
|
+
const DATA_DIR = process.env.DROID_DATA_DIR || join(HOME, '.droid')
|
|
8
|
+
|
|
9
|
+
export const AGENT_ID = 'droid'
|
|
10
|
+
export const AGENT_LABEL = 'Droid'
|
|
11
|
+
export const AGENT_COLOR = '#888'
|
|
12
|
+
|
|
13
|
+
export function isInstalled() { return existsSync(DATA_DIR) }
|
|
14
|
+
|
|
15
|
+
export async function loadSessions() {
|
|
16
|
+
const items = []
|
|
17
|
+
const walk = dir => {
|
|
18
|
+
if (!existsSync(dir)) return
|
|
19
|
+
try {
|
|
20
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
21
|
+
const full = join(dir, e.name)
|
|
22
|
+
if (ignored(full)) continue
|
|
23
|
+
if (e.isDirectory()) walk(full)
|
|
24
|
+
else if (e.name.endsWith('.jsonl') || e.name.endsWith('.json')) {
|
|
25
|
+
const text = readLimited(full)
|
|
26
|
+
const objs = parseJsonl(text)
|
|
27
|
+
const [title, tsrc] = titleFromObjs(objs)
|
|
28
|
+
const pp = guessProj(text) || 'Unknown'
|
|
29
|
+
const date = dateFromPath(full)
|
|
30
|
+
const size = pathSize(full)
|
|
31
|
+
items.push({ agent: AGENT_ID, agentLabel: AGENT_LABEL, path: full, sessionId: basename(full, e.name.endsWith('.jsonl') ? '.jsonl' : '.json'), title: title || fileTitle(full), titleSource: tsrc || 'filename', project: projName(pp), projectPath: pp, date, dateLabel: fmtDate(date), size, sizeLabel: humanSize(size), filename: e.name, msgCount: objs.length })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
walk(DATA_DIR)
|
|
37
|
+
return items.sort((a,b) => b.date - a.date)
|
|
38
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { join, basename } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
readLimited, parseJsonl, pathSize, humanSize, ignored,
|
|
6
|
+
titleFromObjs, fileTitle, dateFromPath, projName, guessProj, fmtDate,
|
|
7
|
+
} from './utils.js'
|
|
8
|
+
|
|
9
|
+
const HOME = homedir()
|
|
10
|
+
const DATA_DIR = process.env.GEMINI_DATA_DIR ||
|
|
11
|
+
join(HOME, '.gemini')
|
|
12
|
+
|
|
13
|
+
export const AGENT_ID = 'gemini'
|
|
14
|
+
export const AGENT_LABEL = 'Gemini CLI'
|
|
15
|
+
export const AGENT_COLOR = '#808080'
|
|
16
|
+
|
|
17
|
+
export function isInstalled() { return existsSync(DATA_DIR) }
|
|
18
|
+
|
|
19
|
+
export async function loadSessions() {
|
|
20
|
+
const items = []
|
|
21
|
+
const walk = dir => {
|
|
22
|
+
if (!existsSync(dir)) return
|
|
23
|
+
try {
|
|
24
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
25
|
+
const full = join(dir, e.name)
|
|
26
|
+
if (ignored(full)) continue
|
|
27
|
+
if (e.isDirectory()) walk(full)
|
|
28
|
+
else if (e.name.endsWith('.jsonl') || e.name.endsWith('.json')) {
|
|
29
|
+
const text = readLimited(full)
|
|
30
|
+
const objs = parseJsonl(text)
|
|
31
|
+
const [title, tsrc] = titleFromObjs(objs)
|
|
32
|
+
const pp = guessProj(text) || 'Unknown'
|
|
33
|
+
const date = dateFromPath(full)
|
|
34
|
+
const size = pathSize(full)
|
|
35
|
+
items.push({
|
|
36
|
+
agent: AGENT_ID, agentLabel: AGENT_LABEL,
|
|
37
|
+
path: full, sessionId: basename(full, e.name.endsWith('.jsonl') ? '.jsonl' : '.json'),
|
|
38
|
+
title: title || fileTitle(full), titleSource: tsrc || 'filename',
|
|
39
|
+
project: projName(pp), projectPath: pp,
|
|
40
|
+
date, dateLabel: fmtDate(date),
|
|
41
|
+
size, sizeLabel: humanSize(size),
|
|
42
|
+
filename: e.name, msgCount: objs.length,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {}
|
|
47
|
+
}
|
|
48
|
+
walk(DATA_DIR)
|
|
49
|
+
return items.sort((a,b) => b.date - a.date)
|
|
50
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
3
|
+
import { join, basename } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
readLimited, parseJsonl, pathSize, humanSize, ignored,
|
|
6
|
+
titleFromObjs, fileTitle, dateFromPath, projName, guessProj, fmtDate,
|
|
7
|
+
} from './utils.js'
|
|
8
|
+
|
|
9
|
+
const HOME = homedir()
|
|
10
|
+
const DATA_DIR = process.env.GOOSE_PATH_ROOT ||
|
|
11
|
+
process.env.GOOSE_DATA_DIR ||
|
|
12
|
+
join(HOME, '.local', 'share', 'goose')
|
|
13
|
+
const SESS_DIRS = [join(DATA_DIR, 'sessions'), DATA_DIR]
|
|
14
|
+
|
|
15
|
+
export const AGENT_ID = 'goose'
|
|
16
|
+
export const AGENT_LABEL = 'Goose'
|
|
17
|
+
export const AGENT_COLOR = '#909090'
|
|
18
|
+
|
|
19
|
+
export function isInstalled() { return existsSync(DATA_DIR) }
|
|
20
|
+
|
|
21
|
+
export async function loadSessions() {
|
|
22
|
+
const items = []
|
|
23
|
+
const walk = dir => {
|
|
24
|
+
if (!existsSync(dir)) return
|
|
25
|
+
try {
|
|
26
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
27
|
+
const full = join(dir, e.name)
|
|
28
|
+
if (ignored(full)) continue
|
|
29
|
+
if (e.isDirectory()) walk(full)
|
|
30
|
+
else if (e.name.endsWith('.jsonl') || e.name.endsWith('.json')) {
|
|
31
|
+
const text = readLimited(full)
|
|
32
|
+
const objs = parseJsonl(text)
|
|
33
|
+
const [title, tsrc] = titleFromObjs(objs)
|
|
34
|
+
const pp = guessProj(text) || 'Unknown'
|
|
35
|
+
const date = dateFromPath(full)
|
|
36
|
+
const size = pathSize(full)
|
|
37
|
+
items.push({
|
|
38
|
+
agent: AGENT_ID, agentLabel: AGENT_LABEL,
|
|
39
|
+
path: full, sessionId: basename(full, e.name.endsWith('.jsonl') ? '.jsonl' : '.json'),
|
|
40
|
+
title: title || fileTitle(full), titleSource: tsrc || 'filename',
|
|
41
|
+
project: projName(pp), projectPath: pp,
|
|
42
|
+
date, dateLabel: fmtDate(date),
|
|
43
|
+
size, sizeLabel: humanSize(size),
|
|
44
|
+
filename: e.name, msgCount: objs.length,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
SESS_DIRS.forEach(walk)
|
|
51
|
+
return items.sort((a,b) => b.date - a.date)
|
|
52
|
+
}
|