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
|
@@ -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.HERMES_DATA_DIR || join(HOME, '.hermes')
|
|
8
|
+
|
|
9
|
+
export const AGENT_ID = 'hermes'
|
|
10
|
+
export const AGENT_LABEL = 'Hermes'
|
|
11
|
+
export const AGENT_COLOR = '#999'
|
|
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,39 @@
|
|
|
1
|
+
import * as claude from './claude.js'
|
|
2
|
+
import * as codex from './codex.js'
|
|
3
|
+
import * as opencode from './opencode.js'
|
|
4
|
+
import * as amp from './amp.js'
|
|
5
|
+
import * as droid from './droid.js'
|
|
6
|
+
import * as codebuff from './codebuff.js'
|
|
7
|
+
import * as hermes from './hermes.js'
|
|
8
|
+
import * as pi from './pi.js'
|
|
9
|
+
import * as goose from './goose.js'
|
|
10
|
+
import * as kilo from './kilo.js'
|
|
11
|
+
import * as copilot from './copilot.js'
|
|
12
|
+
import * as gemini from './gemini.js'
|
|
13
|
+
import * as kimi from './kimi.js'
|
|
14
|
+
import * as qwen from './qwen.js'
|
|
15
|
+
import * as openclaw from './openclaw.js'
|
|
16
|
+
|
|
17
|
+
const ALL_AGENTS = [
|
|
18
|
+
claude, codex, opencode, amp, droid,
|
|
19
|
+
codebuff, hermes, pi, goose, kilo,
|
|
20
|
+
copilot, gemini, kimi, qwen, openclaw,
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
export function getInstalledAgents() {
|
|
24
|
+
return ALL_AGENTS
|
|
25
|
+
.filter(a => { try { return a.isInstalled() } catch { return false } })
|
|
26
|
+
.map(a => ({ id: a.AGENT_ID, label: a.AGENT_LABEL, color: a.AGENT_COLOR }))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function getAllSessions() {
|
|
30
|
+
const results = await Promise.allSettled(
|
|
31
|
+
ALL_AGENTS.map(a => {
|
|
32
|
+
try { return a.isInstalled() ? a.loadSessions() : Promise.resolve([]) }
|
|
33
|
+
catch { return Promise.resolve([]) }
|
|
34
|
+
})
|
|
35
|
+
)
|
|
36
|
+
return results
|
|
37
|
+
.flatMap(r => r.status === 'fulfilled' ? r.value : [])
|
|
38
|
+
.sort((a,b) => b.date - a.date)
|
|
39
|
+
}
|
|
@@ -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.KILO_DATA_DIR || join(HOME, '.kilo')
|
|
8
|
+
|
|
9
|
+
export const AGENT_ID = 'kilo'
|
|
10
|
+
export const AGENT_LABEL = 'Kilo'
|
|
11
|
+
export const AGENT_COLOR = '#bbb'
|
|
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,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.KIMI_DATA_DIR || join(HOME, '.kimi')
|
|
8
|
+
|
|
9
|
+
export const AGENT_ID = 'kimi'
|
|
10
|
+
export const AGENT_LABEL = 'Kimi'
|
|
11
|
+
export const AGENT_COLOR = '#ddd'
|
|
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,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.OPENCLAW_DATA_DIR || join(HOME, '.openclaw')
|
|
8
|
+
|
|
9
|
+
export const AGENT_ID = 'openclaw'
|
|
10
|
+
export const AGENT_LABEL = 'OpenClaw'
|
|
11
|
+
export const AGENT_COLOR = '#f0f0f0'
|
|
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.OPENCODE_DATA_DIR || join(HOME, '.opencode')
|
|
11
|
+
const SESS_DIRS = [join(DATA_DIR, 'sessions'), DATA_DIR]
|
|
12
|
+
|
|
13
|
+
export const AGENT_ID = 'opencode'
|
|
14
|
+
export const AGENT_LABEL = 'OpenCode'
|
|
15
|
+
export const AGENT_COLOR = '#b0b0b0'
|
|
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
|
+
SESS_DIRS.forEach(walk)
|
|
49
|
+
return items.sort((a,b) => b.date - a.date)
|
|
50
|
+
}
|
package/src/agents/pi.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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.PI_AGENT_DATA_DIR || join(HOME, '.pi-agent') || join(HOME, '.pi')
|
|
8
|
+
|
|
9
|
+
export const AGENT_ID = 'pi'
|
|
10
|
+
export const AGENT_LABEL = 'Pi Agent'
|
|
11
|
+
export const AGENT_COLOR = '#aaa'
|
|
12
|
+
|
|
13
|
+
export function isInstalled() {
|
|
14
|
+
return existsSync(join(HOME, '.pi-agent')) || existsSync(join(HOME, '.pi'))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadSessions() {
|
|
18
|
+
const dirs = [process.env.PI_AGENT_DATA_DIR, join(HOME, '.pi-agent'), join(HOME, '.pi')].filter(Boolean)
|
|
19
|
+
const items = []
|
|
20
|
+
const walk = dir => {
|
|
21
|
+
if (!existsSync(dir)) return
|
|
22
|
+
try {
|
|
23
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
24
|
+
const full = join(dir, e.name)
|
|
25
|
+
if (ignored(full)) continue
|
|
26
|
+
if (e.isDirectory()) walk(full)
|
|
27
|
+
else if (e.name.endsWith('.jsonl') || e.name.endsWith('.json')) {
|
|
28
|
+
const text = readLimited(full)
|
|
29
|
+
const objs = parseJsonl(text)
|
|
30
|
+
const [title, tsrc] = titleFromObjs(objs)
|
|
31
|
+
const pp = guessProj(text) || 'Unknown'
|
|
32
|
+
const date = dateFromPath(full)
|
|
33
|
+
const size = pathSize(full)
|
|
34
|
+
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 })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
dirs.forEach(walk)
|
|
40
|
+
return items.sort((a,b) => b.date - a.date)
|
|
41
|
+
}
|
|
@@ -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.QWEN_DATA_DIR || join(HOME, '.qwen')
|
|
8
|
+
|
|
9
|
+
export const AGENT_ID = 'qwen'
|
|
10
|
+
export const AGENT_LABEL = 'Qwen'
|
|
11
|
+
export const AGENT_COLOR = '#eee'
|
|
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,283 @@
|
|
|
1
|
+
import { statSync, readdirSync, openSync, readSync, closeSync } from 'node:fs'
|
|
2
|
+
import { join, basename, extname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export const SKIP_PARTS = new Set(['.tmp','plugins','.git','node_modules','memory'])
|
|
5
|
+
export const MAX_BYTES = 8_000_000
|
|
6
|
+
|
|
7
|
+
export const BAD_TITLES = new Set([
|
|
8
|
+
'none','null','undefined','untitled','no title','new chat','auto',
|
|
9
|
+
'exec_command','automation_update','response_item','reasoning',
|
|
10
|
+
'function_call','tool_call','shell_command','apply_patch',
|
|
11
|
+
'read_file','write_file','list_files','update_plan','plan_update',
|
|
12
|
+
'turn_context','message','event','delta','session',
|
|
13
|
+
'input_text','output_text','read_thread_terminal','read_thread',
|
|
14
|
+
'terminal','thread_terminal','codex_turn_context','conversation_item',
|
|
15
|
+
'assistant_message','user_message','system_message',
|
|
16
|
+
'local_shell_call','local_shell_output','mcp_tool_call','mcp_tool_output',
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
export const TITLE_KEYS = new Set(['title','conversation_title','chat_title','thread_title','session_title','summary'])
|
|
20
|
+
export const PROJECT_KEYS = new Set(['cwd','workdir','working_directory','current_working_directory','project','project_path','repository','repo','workspace','workspace_path'])
|
|
21
|
+
|
|
22
|
+
export const CLAUDE_SKIP_PREFIX = [
|
|
23
|
+
'<local-command','<command-name','<command-message','<command-args',
|
|
24
|
+
'<local-command-stdout','<system-reminder','<function_calls>','<user-prompt-submit-hook',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
// ── I/O ──────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
export function readLimited(p, limit = MAX_BYTES) {
|
|
30
|
+
try {
|
|
31
|
+
const st = statSync(p)
|
|
32
|
+
if (!st.isFile()) return ''
|
|
33
|
+
const size = Math.min(st.size, limit)
|
|
34
|
+
const buf = Buffer.allocUnsafe(size)
|
|
35
|
+
const fd = openSync(p, 'r')
|
|
36
|
+
readSync(fd, buf, 0, size, 0)
|
|
37
|
+
closeSync(fd)
|
|
38
|
+
return buf.toString('utf8')
|
|
39
|
+
} catch { return '' }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function pathSize(p) {
|
|
43
|
+
try {
|
|
44
|
+
const st = statSync(p)
|
|
45
|
+
if (st.isFile()) return st.size
|
|
46
|
+
let total = 0
|
|
47
|
+
const walk = dir => {
|
|
48
|
+
try {
|
|
49
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
if (SKIP_PARTS.has(e.name)) continue
|
|
51
|
+
const full = join(dir, e.name)
|
|
52
|
+
if (e.isDirectory()) walk(full)
|
|
53
|
+
else try { total += statSync(full).size } catch {}
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
walk(p)
|
|
58
|
+
return total
|
|
59
|
+
} catch { return 0 }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function humanSize(n) {
|
|
63
|
+
for (const u of ['B','KB','MB','GB']) {
|
|
64
|
+
if (n < 1024) return `${n.toFixed(1)} ${u}`
|
|
65
|
+
n /= 1024
|
|
66
|
+
}
|
|
67
|
+
return `${n.toFixed(1)} TB`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── JSONL ─────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
export function parseJsonl(text) {
|
|
73
|
+
const objs = []
|
|
74
|
+
text = (text || '').trim()
|
|
75
|
+
if (!text) return objs
|
|
76
|
+
try {
|
|
77
|
+
const d = JSON.parse(text)
|
|
78
|
+
if (d && typeof d === 'object' && !Array.isArray(d)) return [d]
|
|
79
|
+
if (Array.isArray(d)) return d.filter(x => x && typeof x === 'object')
|
|
80
|
+
} catch {}
|
|
81
|
+
for (const line of text.split('\n')) {
|
|
82
|
+
const l = line.trim()
|
|
83
|
+
if (!l) continue
|
|
84
|
+
try { const d = JSON.parse(l); if (d && typeof d === 'object') objs.push(d) } catch {}
|
|
85
|
+
}
|
|
86
|
+
return objs
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── String helpers ────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export function cleanStr(v) {
|
|
92
|
+
if (typeof v !== 'string') return ''
|
|
93
|
+
return v.replace(/\\n/g,' ').replace(/\\t/g,' ').replace(/\n|\t/g,' ').replace(/\s+/g,' ').trim()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function badTitle(v) {
|
|
97
|
+
v = cleanStr(v)
|
|
98
|
+
if (!v) return true
|
|
99
|
+
const lo = v.toLowerCase()
|
|
100
|
+
if (BAD_TITLES.has(lo)) return true
|
|
101
|
+
if (v.length < 4 || v.length > 220) return true
|
|
102
|
+
if (v[0] === '{' || v[0] === '[') return true
|
|
103
|
+
if (v.startsWith('/') && !v.includes(' ')) return true
|
|
104
|
+
if (/^[a-f0-9-]{20,}$/i.test(lo)) return true
|
|
105
|
+
if (/^rollout-\d{4}-/.test(lo)) return true
|
|
106
|
+
if (lo.endsWith('.json') || lo.endsWith('.jsonl')) return true
|
|
107
|
+
if (lo.startsWith('codex ') && lo.includes('event')) return true
|
|
108
|
+
if (/^[a-z]+(_[a-z0-9]+){1,4}$/.test(lo) && v.length<=40) return true
|
|
109
|
+
if (/^<[a-zA-Z_][a-zA-Z0-9_]*[\s>]/.test(v.trimStart())) return true
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function truncStr(v, n = 150) {
|
|
114
|
+
v = cleanStr(v)
|
|
115
|
+
return v.length <= n ? v : v.slice(0, n - 1).trimEnd() + '…'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function sidFromPath(p) {
|
|
119
|
+
const name = basename(p)
|
|
120
|
+
const m = name.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i)
|
|
121
|
+
return m ? m[1] : name.replace(/\.[^.]+$/, '')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function dateFromPath(p) {
|
|
125
|
+
const name = basename(p)
|
|
126
|
+
const pats = [
|
|
127
|
+
/(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})/,
|
|
128
|
+
/(\d{4})-(\d{2})-(\d{2})/,
|
|
129
|
+
]
|
|
130
|
+
for (const pat of pats) {
|
|
131
|
+
const m = name.match(pat)
|
|
132
|
+
if (m) {
|
|
133
|
+
const g = m.slice(1).map(Number)
|
|
134
|
+
try { return new Date(g[0], g[1]-1, g[2], g[3]||0, g[4]||0, g[5]||0) } catch {}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
try { return new Date(statSync(p).mtimeMs) } catch { return new Date(0) }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function fmtDate(d) {
|
|
141
|
+
try { return d.toISOString().slice(0,16).replace('T',' ') } catch { return '' }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function projName(p) {
|
|
145
|
+
if (!p || p === 'Unknown') return 'Unknown Project'
|
|
146
|
+
try { return basename(p) || p } catch { return p }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function ignored(p) {
|
|
150
|
+
return p.split('/').some(part => SKIP_PARTS.has(part))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function groupItems(items) {
|
|
154
|
+
const map = new Map()
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
const k = item.agent + '::' + item.project
|
|
157
|
+
if (!map.has(k)) map.set(k, { agent: item.agent, agentLabel: item.agentLabel, project: item.project, projectPath: item.projectPath, items: [], totalSize: 0, latest: item.date })
|
|
158
|
+
const g = map.get(k)
|
|
159
|
+
g.items.push(item)
|
|
160
|
+
g.totalSize += item.size
|
|
161
|
+
if (item.date > g.latest) g.latest = item.date
|
|
162
|
+
}
|
|
163
|
+
return [...map.values()].sort((a,b) => b.latest - a.latest)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Title extraction (port of Python logic) ───────────────────────────────────
|
|
167
|
+
|
|
168
|
+
export function findStrings(obj, keys) {
|
|
169
|
+
const out = []
|
|
170
|
+
if (!obj) return out
|
|
171
|
+
if (typeof obj === 'object' && !Array.isArray(obj)) {
|
|
172
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
173
|
+
if (keys.has(k.toLowerCase())) {
|
|
174
|
+
if (typeof v === 'string') out.push(v)
|
|
175
|
+
else if (Array.isArray(v)) v.forEach(i => { if (typeof i === 'string') out.push(i) })
|
|
176
|
+
}
|
|
177
|
+
if (v && typeof v === 'object') out.push(...findStrings(v, keys))
|
|
178
|
+
}
|
|
179
|
+
} else if (Array.isArray(obj)) {
|
|
180
|
+
obj.forEach(i => out.push(...findStrings(i, keys)))
|
|
181
|
+
}
|
|
182
|
+
return out
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function extractText(v) {
|
|
186
|
+
if (typeof v === 'string') return [v]
|
|
187
|
+
if (Array.isArray(v)) return v.flatMap(extractText)
|
|
188
|
+
if (v && typeof v === 'object') {
|
|
189
|
+
const out = []
|
|
190
|
+
const typ = cleanStr(v.type || '').toLowerCase()
|
|
191
|
+
if (typeof v.text === 'string') out.push(v.text)
|
|
192
|
+
if (v.content) out.push(...extractText(v.content))
|
|
193
|
+
if (v.message) out.push(...extractText(v.message))
|
|
194
|
+
if (v.input && !['local_shell_call','tool_call','function_call'].includes(typ)) out.push(...extractText(v.input))
|
|
195
|
+
if (v.prompt) out.push(...extractText(v.prompt))
|
|
196
|
+
return out
|
|
197
|
+
}
|
|
198
|
+
return []
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isUserObj(obj) {
|
|
202
|
+
if (!obj || typeof obj !== 'object') return false
|
|
203
|
+
const role = cleanStr(obj.role || '').toLowerCase()
|
|
204
|
+
const typ = cleanStr(obj.type || '').toLowerCase()
|
|
205
|
+
const it = cleanStr(obj.item_type || '').toLowerCase()
|
|
206
|
+
if (role === 'user') return true
|
|
207
|
+
if (['user','user_message'].includes(typ)) return true
|
|
208
|
+
if (['user','user_message'].includes(it)) return true
|
|
209
|
+
if (obj.item?.role && cleanStr(obj.item.role).toLowerCase() === 'user') return true
|
|
210
|
+
if (obj.payload?.role && cleanStr(obj.payload.role).toLowerCase() === 'user') return true
|
|
211
|
+
return false
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function userMessages(obj) {
|
|
215
|
+
const out = []
|
|
216
|
+
if (!obj) return out
|
|
217
|
+
if (typeof obj === 'object' && !Array.isArray(obj)) {
|
|
218
|
+
if (isUserObj(obj)) {
|
|
219
|
+
out.push(...extractText(obj))
|
|
220
|
+
for (const sub of [obj.item, obj.payload]) {
|
|
221
|
+
if (sub && typeof sub === 'object') out.push(...extractText(sub))
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
for (const v of Object.values(obj)) out.push(...userMessages(v))
|
|
225
|
+
} else if (Array.isArray(obj)) {
|
|
226
|
+
obj.forEach(i => out.push(...userMessages(i)))
|
|
227
|
+
}
|
|
228
|
+
return out
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function titleFromObjs(objs) {
|
|
232
|
+
for (const obj of objs) {
|
|
233
|
+
for (const v of findStrings(obj, TITLE_KEYS)) {
|
|
234
|
+
const t = truncStr(v)
|
|
235
|
+
if (!badTitle(t)) return [t, 'title field']
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const obj of objs) {
|
|
239
|
+
for (const v of userMessages(obj)) {
|
|
240
|
+
const t = truncStr(v)
|
|
241
|
+
if (!badTitle(t)) return [t, 'user message']
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return ['', '']
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function regexTitle(text) {
|
|
248
|
+
const pats = [
|
|
249
|
+
/"role"\s*:\s*"user".{0,2500}?"text"\s*:\s*"((?:\\.|[^"\\]){4,500})"/s,
|
|
250
|
+
/"type"\s*:\s*"user_message".{0,2500}?"text"\s*:\s*"((?:\\.|[^"\\]){4,500})"/s,
|
|
251
|
+
/"type"\s*:\s*"input_text"\s*,\s*"text"\s*:\s*"((?:\\.|[^"\\]){4,500})"/s,
|
|
252
|
+
]
|
|
253
|
+
for (const pat of pats) {
|
|
254
|
+
const m = text.match(pat)
|
|
255
|
+
if (m) {
|
|
256
|
+
let raw = m[1]
|
|
257
|
+
try { raw = JSON.parse(`"${raw}"`) } catch {}
|
|
258
|
+
const t = truncStr(raw)
|
|
259
|
+
if (!badTitle(t)) return [t, 'regex']
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return ['', '']
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function fileTitle(p) {
|
|
266
|
+
let t = basename(p, extname(p))
|
|
267
|
+
t = t.replace(/^rollout-/, '')
|
|
268
|
+
t = t.replace(/\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-?/g, '')
|
|
269
|
+
t = t.replace(/\d{4}-\d{2}-\d{2}-?/g, '')
|
|
270
|
+
t = t.replace(/[0-9a-f]{8}-[0-9a-f-]{27,}/gi, '')
|
|
271
|
+
t = t.replace(/[_-]/g, ' ').replace(/\s+/g, ' ').trim()
|
|
272
|
+
return t || 'Untitled Chat'
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function guessProj(text) {
|
|
276
|
+
const cands = []
|
|
277
|
+
for (const m of text.matchAll(/\/(?:Users|home|Volumes)\/[^\s"'\n\r\t]+/g)) {
|
|
278
|
+
const s = m[0].replace(/[.,;:)]+$/, '')
|
|
279
|
+
if (s.length > 8) cands.push(s)
|
|
280
|
+
}
|
|
281
|
+
const pref = ['documents','desktop','projects','workspace','work','upwork','repos','code','codes','dev']
|
|
282
|
+
return cands.find(c => pref.some(w => c.toLowerCase().includes(w))) || cands[0] || ''
|
|
283
|
+
}
|