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 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
+ }