aisessions 1.0.0 → 1.1.2
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/bin/cli.js +1 -1
- package/package.json +5 -2
- package/src/router.js +4 -0
- package/src/session/detail.js +59 -0
- package/src/storage/backup.js +35 -9
- package/src/storage/trash.js +36 -11
- package/src/ui/css.js +104 -2
- package/src/ui/js.js +104 -11
- package/src/ui/render.js +15 -2
- package/src/usage/index.js +20 -17
package/bin/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aisessions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Browse, manage and analyze your AI coding agent sessions — Claude Code, Codex, and more",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aisessions": "bin/cli.js"
|
|
@@ -25,5 +25,8 @@
|
|
|
25
25
|
"aisessions",
|
|
26
26
|
"ccusage"
|
|
27
27
|
],
|
|
28
|
-
"license": "MIT"
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"ccusage": "^20.0.6"
|
|
31
|
+
}
|
|
29
32
|
}
|
package/src/router.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getAllSessions, getInstalledAgents } from './agents/index.js'
|
|
|
3
3
|
import { getUsageData } from './usage/index.js'
|
|
4
4
|
import { listTrash, moveToTrash, restoreFromTrash, purgeTrash } from './storage/trash.js'
|
|
5
5
|
import { listBackups, createBackup, restoreBackup, deleteBackup } from './storage/backup.js'
|
|
6
|
+
import { getSessionDetail } from './session/detail.js'
|
|
6
7
|
|
|
7
8
|
function sendJson(res, data, status = 200) {
|
|
8
9
|
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' })
|
|
@@ -60,6 +61,9 @@ export async function router(req, res) {
|
|
|
60
61
|
const sessions = await getAllSessions()
|
|
61
62
|
return sendJson(res, sessions.map(({ date, ...rest }) => rest))
|
|
62
63
|
}
|
|
64
|
+
if (method === 'GET' && pathname === '/api/session') {
|
|
65
|
+
return sendJson(res, await getSessionDetail(query.path || ''))
|
|
66
|
+
}
|
|
63
67
|
|
|
64
68
|
// ── Usage (supports ?agent=claude) ───────────────────────────────────────
|
|
65
69
|
if (method === 'GET' && pathname === '/api/usage') {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
2
|
+
import { parseJsonl, readLimited } from '../agents/utils.js'
|
|
3
|
+
|
|
4
|
+
const MAX_DETAIL_BYTES = 2_000_000
|
|
5
|
+
|
|
6
|
+
function cleanText(v) {
|
|
7
|
+
return String(v || '').replace(/\s+/g, ' ').trim()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function extractText(v) {
|
|
11
|
+
if (typeof v === 'string') return [v]
|
|
12
|
+
if (Array.isArray(v)) return v.flatMap(extractText)
|
|
13
|
+
if (v && typeof v === 'object') {
|
|
14
|
+
const out = []
|
|
15
|
+
if (typeof v.text === 'string') out.push(v.text)
|
|
16
|
+
if (v.content) out.push(...extractText(v.content))
|
|
17
|
+
if (v.message) out.push(...extractText(v.message))
|
|
18
|
+
if (v.parts) out.push(...extractText(v.parts))
|
|
19
|
+
return out
|
|
20
|
+
}
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function roleOf(obj) {
|
|
25
|
+
const role = cleanText(obj?.role || obj?.message?.role || obj?.payload?.role).toLowerCase()
|
|
26
|
+
const type = cleanText(obj?.type || obj?.item_type).toLowerCase()
|
|
27
|
+
if (role) return role
|
|
28
|
+
if (type.includes('user')) return 'user'
|
|
29
|
+
if (type.includes('assistant') || type.includes('model')) return 'assistant'
|
|
30
|
+
if (type.includes('system')) return 'system'
|
|
31
|
+
return type || 'entry'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function messagesFromObjs(objs) {
|
|
35
|
+
return objs.map((obj, idx) => {
|
|
36
|
+
const text = extractText(obj).map(cleanText).filter(Boolean).join('\n\n')
|
|
37
|
+
return {
|
|
38
|
+
index: idx + 1,
|
|
39
|
+
role: roleOf(obj),
|
|
40
|
+
text: text || JSON.stringify(obj).slice(0, 4000),
|
|
41
|
+
}
|
|
42
|
+
}).filter(m => m.text)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function getSessionDetail(path) {
|
|
46
|
+
if (!path || !existsSync(path)) return { ok: false, error: 'session file not found' }
|
|
47
|
+
const st = statSync(path)
|
|
48
|
+
if (!st.isFile()) return { ok: false, error: 'session path is not a file' }
|
|
49
|
+
const text = readLimited(path, MAX_DETAIL_BYTES)
|
|
50
|
+
const objs = parseJsonl(text)
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
path,
|
|
54
|
+
size: st.size,
|
|
55
|
+
truncated: st.size > MAX_DETAIL_BYTES,
|
|
56
|
+
messages: messagesFromObjs(objs),
|
|
57
|
+
raw: text,
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/storage/backup.js
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, statSync, rmSync, cpSync } from 'node:fs'
|
|
3
|
-
import { join, basename } from 'node:path'
|
|
3
|
+
import { dirname, join, basename } from 'node:path'
|
|
4
4
|
|
|
5
5
|
const HOME = homedir()
|
|
6
6
|
const BACKUP_DIR = join(HOME, '.aisessions', 'backups')
|
|
7
7
|
|
|
8
8
|
function ensureBackup() { mkdirSync(BACKUP_DIR, { recursive: true }) }
|
|
9
9
|
|
|
10
|
+
function safePart(v, fallback = 'item') {
|
|
11
|
+
return String(v || fallback).replace(/[^a-z0-9._-]+/gi, '_').replace(/^_+|_+$/g, '').slice(0, 80) || fallback
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function findMetaFiles(dir) {
|
|
15
|
+
const out = []
|
|
16
|
+
try {
|
|
17
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
18
|
+
const full = join(dir, e.name)
|
|
19
|
+
if (e.isDirectory()) out.push(...findMetaFiles(full))
|
|
20
|
+
else if (e.name.endsWith('.meta.json') || e.name === 'meta.json') out.push(full)
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function removeStoredItem(metaPath, payloadPath) {
|
|
27
|
+
if (basename(metaPath) === 'meta.json') {
|
|
28
|
+
rmSync(dirname(metaPath), { recursive: true, force: true })
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
if (payloadPath && existsSync(payloadPath)) rmSync(payloadPath, { recursive: true, force: true })
|
|
32
|
+
if (existsSync(metaPath)) rmSync(metaPath, { force: true })
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
function normaliseItems(raw) {
|
|
11
36
|
return (Array.isArray(raw) ? raw : [raw]).map(i =>
|
|
12
37
|
typeof i === 'string' ? { path: i } : i
|
|
@@ -17,9 +42,7 @@ export async function listBackups(agent) {
|
|
|
17
42
|
ensureBackup()
|
|
18
43
|
const items = []
|
|
19
44
|
try {
|
|
20
|
-
for (const
|
|
21
|
-
if (!f.endsWith('.meta.json')) continue
|
|
22
|
-
const mp = join(BACKUP_DIR, f)
|
|
45
|
+
for (const mp of findMetaFiles(BACKUP_DIR)) {
|
|
23
46
|
try {
|
|
24
47
|
const meta = JSON.parse(readFileSync(mp, 'utf8'))
|
|
25
48
|
if (agent && agent !== 'all' && meta.agent && meta.agent !== agent) continue
|
|
@@ -39,10 +62,13 @@ export async function createBackup(rawItems, note = '') {
|
|
|
39
62
|
if (!existsSync(p)) { results.push({ path: p, ok: false, error: 'not found' }); continue }
|
|
40
63
|
const ts = Date.now()
|
|
41
64
|
const name = basename(p)
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
65
|
+
const agentId = safePart(item.agent || 'unknown')
|
|
66
|
+
const backupName = ts + '_' + safePart(item.title || name)
|
|
67
|
+
const itemDir = join(BACKUP_DIR, agentId, backupName)
|
|
68
|
+
const backupPath = join(itemDir, 'payload')
|
|
69
|
+
const metaPath = join(itemDir, 'meta.json')
|
|
45
70
|
const st = statSync(p)
|
|
71
|
+
mkdirSync(itemDir, { recursive: true })
|
|
46
72
|
if (st.isDirectory()) cpSync(p, backupPath, { recursive: true })
|
|
47
73
|
else cpSync(p, backupPath)
|
|
48
74
|
const meta = {
|
|
@@ -72,6 +98,7 @@ export async function restoreBackup(metaPath) {
|
|
|
72
98
|
? meta.originalPath.replace(/(\.[^.]+)?$/, '_restored_' + Date.now() + '$1')
|
|
73
99
|
: meta.originalPath
|
|
74
100
|
const st = statSync(meta.backupPath)
|
|
101
|
+
mkdirSync(dirname(dest), { recursive: true })
|
|
75
102
|
if (st.isDirectory()) cpSync(meta.backupPath, dest, { recursive: true })
|
|
76
103
|
else cpSync(meta.backupPath, dest)
|
|
77
104
|
return { ok: true, restoredTo: dest }
|
|
@@ -84,8 +111,7 @@ export async function deleteBackup(metaPaths) {
|
|
|
84
111
|
try {
|
|
85
112
|
let backupPath = mp.replace(/\.meta\.json$/, '')
|
|
86
113
|
try { const m = JSON.parse(readFileSync(mp, 'utf8')); backupPath = m.backupPath || backupPath } catch {}
|
|
87
|
-
|
|
88
|
-
if (existsSync(mp)) rmSync(mp, { force: true })
|
|
114
|
+
removeStoredItem(mp, backupPath)
|
|
89
115
|
results.push({ metaPath: mp, ok: true })
|
|
90
116
|
} catch (e) { results.push({ metaPath: mp, ok: false, error: e.message }) }
|
|
91
117
|
}
|
package/src/storage/trash.js
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, statSync, rmSync, cpSync } from 'node:fs'
|
|
3
|
-
import { join } from 'node:path'
|
|
3
|
+
import { basename, dirname, join } from 'node:path'
|
|
4
4
|
|
|
5
5
|
const HOME = homedir()
|
|
6
6
|
const TRASH_DIR = join(HOME, '.aisessions', 'trash')
|
|
7
7
|
|
|
8
8
|
function ensureTrash() { mkdirSync(TRASH_DIR, { recursive: true }) }
|
|
9
9
|
|
|
10
|
+
function safePart(v, fallback = 'item') {
|
|
11
|
+
return String(v || fallback).replace(/[^a-z0-9._-]+/gi, '_').replace(/^_+|_+$/g, '').slice(0, 80) || fallback
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function findMetaFiles(dir) {
|
|
15
|
+
const out = []
|
|
16
|
+
try {
|
|
17
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
18
|
+
const full = join(dir, e.name)
|
|
19
|
+
if (e.isDirectory()) out.push(...findMetaFiles(full))
|
|
20
|
+
else if (e.name.endsWith('.meta.json') || e.name === 'meta.json') out.push(full)
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function removeStoredItem(metaPath, payloadPath) {
|
|
27
|
+
if (basename(metaPath) === 'meta.json') {
|
|
28
|
+
rmSync(dirname(metaPath), { recursive: true, force: true })
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
if (payloadPath && existsSync(payloadPath)) rmSync(payloadPath, { recursive: true, force: true })
|
|
32
|
+
if (existsSync(metaPath)) rmSync(metaPath, { force: true })
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
// items: array of { path, agent?, agentLabel?, project?, title? }
|
|
11
36
|
// OR plain strings (backwards compat)
|
|
12
37
|
function normaliseItems(raw) {
|
|
@@ -19,9 +44,7 @@ export async function listTrash(agent) {
|
|
|
19
44
|
ensureTrash()
|
|
20
45
|
const items = []
|
|
21
46
|
try {
|
|
22
|
-
for (const
|
|
23
|
-
if (!f.endsWith('.meta.json')) continue
|
|
24
|
-
const mp = join(TRASH_DIR, f)
|
|
47
|
+
for (const mp of findMetaFiles(TRASH_DIR)) {
|
|
25
48
|
try {
|
|
26
49
|
const meta = JSON.parse(readFileSync(mp, 'utf8'))
|
|
27
50
|
if (agent && agent !== 'all' && meta.agent && meta.agent !== agent) continue
|
|
@@ -40,10 +63,13 @@ export async function moveToTrash(rawItems) {
|
|
|
40
63
|
try {
|
|
41
64
|
if (!existsSync(p)) { results.push({ path: p, ok: false, error: 'not found' }); continue }
|
|
42
65
|
const ts = Date.now()
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
66
|
+
const agentId = safePart(item.agent || 'unknown')
|
|
67
|
+
const trashName = ts + '_' + safePart(item.title || p.split('/').pop())
|
|
68
|
+
const itemDir = join(TRASH_DIR, agentId, trashName)
|
|
69
|
+
const trashPath = join(itemDir, 'payload')
|
|
70
|
+
const metaPath = join(itemDir, 'meta.json')
|
|
46
71
|
const st = statSync(p)
|
|
72
|
+
mkdirSync(itemDir, { recursive: true })
|
|
47
73
|
if (st.isDirectory()) cpSync(p, trashPath, { recursive: true })
|
|
48
74
|
else cpSync(p, trashPath)
|
|
49
75
|
rmSync(p, { recursive: true, force: true })
|
|
@@ -70,10 +96,10 @@ export async function restoreFromTrash(metaPath) {
|
|
|
70
96
|
if (!existsSync(meta.trashPath)) return { ok: false, error: 'trash file missing' }
|
|
71
97
|
if (existsSync(meta.originalPath)) return { ok: false, error: 'destination already exists' }
|
|
72
98
|
const st = statSync(meta.trashPath)
|
|
99
|
+
mkdirSync(dirname(meta.originalPath), { recursive: true })
|
|
73
100
|
if (st.isDirectory()) cpSync(meta.trashPath, meta.originalPath, { recursive: true })
|
|
74
101
|
else cpSync(meta.trashPath, meta.originalPath)
|
|
75
|
-
|
|
76
|
-
rmSync(metaPath, { force: true })
|
|
102
|
+
removeStoredItem(metaPath, meta.trashPath)
|
|
77
103
|
return { ok: true, restoredTo: meta.originalPath }
|
|
78
104
|
} catch (e) { return { ok: false, error: e.message } }
|
|
79
105
|
}
|
|
@@ -84,8 +110,7 @@ export async function purgeTrash(metaPaths) {
|
|
|
84
110
|
try {
|
|
85
111
|
let trashPath = mp.replace(/\.meta\.json$/, '')
|
|
86
112
|
try { const m = JSON.parse(readFileSync(mp, 'utf8')); trashPath = m.trashPath || trashPath } catch {}
|
|
87
|
-
|
|
88
|
-
if (existsSync(mp)) rmSync(mp, { force: true })
|
|
113
|
+
removeStoredItem(mp, trashPath)
|
|
89
114
|
results.push({ metaPath: mp, ok: true })
|
|
90
115
|
} catch (e) { results.push({ metaPath: mp, ok: false, error: e.message }) }
|
|
91
116
|
}
|
package/src/ui/css.js
CHANGED
|
@@ -15,6 +15,7 @@ export const PAGE_CSS = `
|
|
|
15
15
|
--muted:#666;
|
|
16
16
|
--dim:#3a3a3a;
|
|
17
17
|
--accent:#ffffff;
|
|
18
|
+
--logo-shadow:0 0 10px rgba(255,255,255,.25);
|
|
18
19
|
--danger:#ff3333;
|
|
19
20
|
--success:#33cc33;
|
|
20
21
|
--warn:#ccaa00;
|
|
@@ -28,6 +29,24 @@ export const PAGE_CSS = `
|
|
|
28
29
|
--logo: 'Press Start 2P', monospace;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
html[data-theme="light"]{
|
|
33
|
+
--bg:#f4f1e8;
|
|
34
|
+
--sb:#ebe6d8;
|
|
35
|
+
--surf:#f8f5ec;
|
|
36
|
+
--surf2:#ede8dc;
|
|
37
|
+
--surf3:#e1dacb;
|
|
38
|
+
--border:rgba(24,24,20,0.12);
|
|
39
|
+
--border2:rgba(24,24,20,0.24);
|
|
40
|
+
--text:#181814;
|
|
41
|
+
--muted:#6f695d;
|
|
42
|
+
--dim:#a49c8d;
|
|
43
|
+
--accent:#11110f;
|
|
44
|
+
--logo-shadow:none;
|
|
45
|
+
--danger:#b42323;
|
|
46
|
+
--success:#197a33;
|
|
47
|
+
--warn:#8a6900;
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
html,body{height:100%;background:var(--bg);color:var(--text);overflow:hidden;
|
|
32
51
|
font-family:var(--pixel);font-size:20px;line-height:1.2;
|
|
33
52
|
-webkit-font-smoothing:none;font-smooth:never;image-rendering:pixelated}
|
|
@@ -66,7 +85,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);overflow:hidden;
|
|
|
66
85
|
#logo{
|
|
67
86
|
font-family:var(--logo);font-size:11px;
|
|
68
87
|
color:var(--accent);letter-spacing:1px;
|
|
69
|
-
text-shadow:
|
|
88
|
+
text-shadow:var(--logo-shadow);
|
|
70
89
|
white-space:nowrap;
|
|
71
90
|
}
|
|
72
91
|
#hdr-sub{font-size:16px;color:var(--muted);letter-spacing:.5px}
|
|
@@ -79,6 +98,23 @@ html,body{height:100%;background:var(--bg);color:var(--text);overflow:hidden;
|
|
|
79
98
|
}
|
|
80
99
|
#search:focus{border-color:var(--border2);background:var(--surf2)}
|
|
81
100
|
#search::placeholder{color:var(--dim)}
|
|
101
|
+
.theme-toggle{
|
|
102
|
+
width:74px;
|
|
103
|
+
height:39px;
|
|
104
|
+
border:2px solid var(--border);
|
|
105
|
+
background:var(--surf);
|
|
106
|
+
color:var(--muted);
|
|
107
|
+
font-family:var(--pixel);font-size:16px;
|
|
108
|
+
letter-spacing:.8px;
|
|
109
|
+
cursor:pointer;
|
|
110
|
+
-webkit-font-smoothing:none;
|
|
111
|
+
}
|
|
112
|
+
.theme-toggle:hover,.theme-toggle:focus{
|
|
113
|
+
color:var(--accent);
|
|
114
|
+
border-color:var(--border2);
|
|
115
|
+
background:var(--surf2);
|
|
116
|
+
outline:none;
|
|
117
|
+
}
|
|
82
118
|
|
|
83
119
|
/* ── SIDEBAR ─────────────────────────────────────────────────────────────────── */
|
|
84
120
|
.sb-section{padding:10px 0}
|
|
@@ -171,6 +207,26 @@ html,body{height:100%;background:var(--bg);color:var(--text);overflow:hidden;
|
|
|
171
207
|
font-size:12px;color:rgba(255,255,255,.26);letter-spacing:1px;
|
|
172
208
|
text-transform:uppercase;
|
|
173
209
|
}
|
|
210
|
+
html[data-theme="light"] .credit-kicker{color:rgba(24,24,20,.42)}
|
|
211
|
+
html[data-theme="light"] .credit-frame{
|
|
212
|
+
border-color:rgba(24,24,20,.18);
|
|
213
|
+
background:rgba(24,24,20,.025);
|
|
214
|
+
}
|
|
215
|
+
html[data-theme="light"] .credit-frame::before,
|
|
216
|
+
html[data-theme="light"] .credit-frame::after{background:var(--accent)}
|
|
217
|
+
html[data-theme="light"] .credit-link{
|
|
218
|
+
color:rgba(24,24,20,.58);
|
|
219
|
+
background:rgba(24,24,20,.02);
|
|
220
|
+
}
|
|
221
|
+
html[data-theme="light"] .credit-link:hover{
|
|
222
|
+
color:var(--accent);
|
|
223
|
+
background:rgba(24,24,20,.04);
|
|
224
|
+
}
|
|
225
|
+
html[data-theme="light"] .credit-link-icon{
|
|
226
|
+
border-color:rgba(24,24,20,.18);
|
|
227
|
+
color:rgba(24,24,20,.78);
|
|
228
|
+
}
|
|
229
|
+
html[data-theme="light"] .credit-pkg{color:rgba(24,24,20,.32)}
|
|
174
230
|
|
|
175
231
|
/* ── TAB BAR — hidden; sidebar drives navigation ────────────────────────────── */
|
|
176
232
|
#tab-bar{display:none}
|
|
@@ -222,7 +278,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);overflow:hidden;
|
|
|
222
278
|
|
|
223
279
|
.tbl-hdr,.tbl-row{
|
|
224
280
|
display:grid;
|
|
225
|
-
grid-template-columns:40px 80px 1fr 80px 80px 150px
|
|
281
|
+
grid-template-columns:40px 80px 1fr 80px 80px 150px 126px;
|
|
226
282
|
align-items:center;gap:0 10px;padding:0 16px;
|
|
227
283
|
}
|
|
228
284
|
.tbl-hdr{
|
|
@@ -384,6 +440,52 @@ html,body{height:100%;background:var(--bg);color:var(--text);overflow:hidden;
|
|
|
384
440
|
#toast.ok{border-color:rgba(51,204,51,.6);color:var(--success)}
|
|
385
441
|
#toast.err{border-color:rgba(255,51,51,.6);color:var(--danger)}
|
|
386
442
|
|
|
443
|
+
/* ── MODAL / SESSION VIEWER ────────────────────────────────────────────────── */
|
|
444
|
+
.modal{
|
|
445
|
+
position:fixed;inset:0;z-index:100;
|
|
446
|
+
display:flex;align-items:center;justify-content:center;
|
|
447
|
+
background:rgba(0,0,0,.72);
|
|
448
|
+
padding:28px;
|
|
449
|
+
}
|
|
450
|
+
.modal[hidden]{display:none}
|
|
451
|
+
.modal-box{
|
|
452
|
+
width:min(980px,calc(100vw - 56px));
|
|
453
|
+
height:min(760px,calc(100vh - 56px));
|
|
454
|
+
display:flex;flex-direction:column;
|
|
455
|
+
border:2px solid var(--border2);
|
|
456
|
+
background:var(--surf);
|
|
457
|
+
}
|
|
458
|
+
.modal-head{
|
|
459
|
+
display:flex;align-items:center;justify-content:space-between;gap:16px;
|
|
460
|
+
padding:14px 16px;border-bottom:1px solid var(--border);
|
|
461
|
+
flex-shrink:0;
|
|
462
|
+
}
|
|
463
|
+
.modal-title{font-family:var(--logo);font-size:10px;color:var(--accent);letter-spacing:1px}
|
|
464
|
+
.modal-sub{font-size:15px;color:var(--muted);margin-top:5px;word-break:break-all}
|
|
465
|
+
.modal-body{flex:1;overflow:auto;padding:14px;background:var(--bg)}
|
|
466
|
+
.chat-msg{
|
|
467
|
+
border:1px solid var(--border);
|
|
468
|
+
background:var(--surf);
|
|
469
|
+
margin-bottom:10px;
|
|
470
|
+
}
|
|
471
|
+
.chat-role{
|
|
472
|
+
padding:7px 10px;border-bottom:1px solid var(--border);
|
|
473
|
+
color:var(--muted);font-size:14px;letter-spacing:1px;text-transform:uppercase;
|
|
474
|
+
}
|
|
475
|
+
.chat-text{
|
|
476
|
+
padding:10px;
|
|
477
|
+
white-space:pre-wrap;
|
|
478
|
+
word-break:break-word;
|
|
479
|
+
font-size:17px;
|
|
480
|
+
line-height:1.35;
|
|
481
|
+
}
|
|
482
|
+
.chat-raw{
|
|
483
|
+
white-space:pre-wrap;
|
|
484
|
+
word-break:break-word;
|
|
485
|
+
font-size:15px;
|
|
486
|
+
line-height:1.35;
|
|
487
|
+
}
|
|
488
|
+
|
|
387
489
|
/* ── SCROLLBAR ───────────────────────────────────────────────────────────────── */
|
|
388
490
|
::-webkit-scrollbar{width:8px;height:8px}
|
|
389
491
|
::-webkit-scrollbar-track{background:var(--bg)}
|
package/src/ui/js.js
CHANGED
|
@@ -14,6 +14,7 @@ var collapsed = new Set();
|
|
|
14
14
|
var PAGE = 0;
|
|
15
15
|
var PER_PAGE = 100;
|
|
16
16
|
var usageCache = {}; // keyed by agentFilter
|
|
17
|
+
var theme = 'dark';
|
|
17
18
|
|
|
18
19
|
// ── utils ─────────────────────────────────────────────────────────────────────
|
|
19
20
|
function $(id) { return document.getElementById(id); }
|
|
@@ -64,14 +65,17 @@ document.addEventListener('click', function (e) {
|
|
|
64
65
|
var nav = btn.dataset.nav || '';
|
|
65
66
|
if (a === 'filter-agent') { setAgentFilter(ag); return; }
|
|
66
67
|
if (a === 'switch-tab') { switchTab(nav); return; }
|
|
67
|
-
if (a === 'agent-tab') {
|
|
68
|
+
if (a === 'agent-tab') { switchAgentTab(ag, nav); return; }
|
|
68
69
|
if (a === 'switch-tab-global') { setAgentFilter('all'); switchTab(nav); return; }
|
|
70
|
+
if (a === 'toggle-theme') { toggleTheme(); return; }
|
|
69
71
|
if (a === 'trash-one') { trashOne(p); return; }
|
|
70
72
|
if (a === 'backup-one') { backupOne(p); return; }
|
|
71
73
|
if (a === 'restore-trash') { restoreTrash(meta); return; }
|
|
72
74
|
if (a === 'purge-trash') { purgeTrash(meta); return; }
|
|
73
75
|
if (a === 'restore-backup') { restoreBackup(meta); return; }
|
|
74
76
|
if (a === 'delete-backup') { deleteBackup(meta); return; }
|
|
77
|
+
if (a === 'view-session') { viewSession(p); return; }
|
|
78
|
+
if (a === 'close-modal') { closeModal(); return; }
|
|
75
79
|
return;
|
|
76
80
|
}
|
|
77
81
|
var row = e.target.closest('.tbl-row');
|
|
@@ -95,8 +99,34 @@ document.addEventListener('click', function (e) {
|
|
|
95
99
|
if (gh) { var k = gh.dataset.gkey; collapsed.has(k) ? collapsed.delete(k) : collapsed.add(k); renderSessions(); }
|
|
96
100
|
});
|
|
97
101
|
|
|
102
|
+
// ── THEME ─────────────────────────────────────────────────────────────────────
|
|
103
|
+
function getSavedTheme() {
|
|
104
|
+
try { return localStorage.getItem('aisessions-theme') || 'dark'; }
|
|
105
|
+
catch { return 'dark'; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function saveTheme(next) {
|
|
109
|
+
try { localStorage.setItem('aisessions-theme', next); } catch {}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function applyTheme(next) {
|
|
113
|
+
theme = next === 'light' ? 'light' : 'dark';
|
|
114
|
+
document.documentElement.dataset.theme = theme;
|
|
115
|
+
var btn = $('theme-toggle');
|
|
116
|
+
if (btn) {
|
|
117
|
+
btn.textContent = theme === 'light' ? 'DARK' : 'LIGHT';
|
|
118
|
+
btn.title = 'Switch to ' + (theme === 'light' ? 'dark' : 'light') + ' mode';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function toggleTheme() {
|
|
123
|
+
applyTheme(theme === 'light' ? 'dark' : 'light');
|
|
124
|
+
saveTheme(theme);
|
|
125
|
+
}
|
|
126
|
+
|
|
98
127
|
// ── INIT ──────────────────────────────────────────────────────────────────────
|
|
99
128
|
async function init() {
|
|
129
|
+
applyTheme(getSavedTheme());
|
|
100
130
|
var tblBody = $('tbl-body');
|
|
101
131
|
if (!tblBody) return;
|
|
102
132
|
tblBody.innerHTML = '<div class="loading"><span class="loading-dots">LOADING SESSIONS</span></div>';
|
|
@@ -128,10 +158,10 @@ function buildSidebar(agents, sessions) {
|
|
|
128
158
|
'<span class="agent-dot" style="background:' + esc(a.color) + '"></span>' +
|
|
129
159
|
esc(a.label) + '<span class="sb-count">' + (counts[a.id] || 0) + '</span></button>' +
|
|
130
160
|
'<div class="sb-sub" id="sub-' + aid + '">' +
|
|
131
|
-
'<button class="sb-sub-item" data-action="agent-tab" data-nav="sessions">[=] SESSIONS</button>' +
|
|
132
|
-
'<button class="sb-sub-item" data-action="agent-tab" data-nav="usage">[$] USAGE</button>' +
|
|
133
|
-
'<button class="sb-sub-item" data-action="agent-tab" data-nav="trash">[T] TRASH</button>' +
|
|
134
|
-
'<button class="sb-sub-item" data-action="agent-tab" data-nav="backups">[B] BACKUPS</button>' +
|
|
161
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="sessions">[=] SESSIONS</button>' +
|
|
162
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="usage">[$] USAGE</button>' +
|
|
163
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="trash">[T] TRASH</button>' +
|
|
164
|
+
'<button class="sb-sub-item" data-action="agent-tab" data-agent="' + aid + '" data-nav="backups">[B] BACKUPS</button>' +
|
|
135
165
|
'</div>';
|
|
136
166
|
}).join('');
|
|
137
167
|
}
|
|
@@ -159,6 +189,18 @@ function setAgentFilter(id) {
|
|
|
159
189
|
switchTab('sessions');
|
|
160
190
|
}
|
|
161
191
|
|
|
192
|
+
function switchAgentTab(id, tab) {
|
|
193
|
+
if (id && id !== agentFilter) {
|
|
194
|
+
agentFilter = id; PAGE = 0;
|
|
195
|
+
setSidebarActive(id);
|
|
196
|
+
document.querySelectorAll('.sb-sub').forEach(function(el) {
|
|
197
|
+
el.classList.toggle('show', el.id === 'sub-' + id);
|
|
198
|
+
});
|
|
199
|
+
updateContextLabel();
|
|
200
|
+
}
|
|
201
|
+
switchTab(tab || 'sessions');
|
|
202
|
+
}
|
|
203
|
+
|
|
162
204
|
// ── CONTEXT LABEL (shows current filter scope in each panel) ──────────────────
|
|
163
205
|
function updateContextLabel() {
|
|
164
206
|
var label = agentFilter === 'all' ? 'ALL AGENTS' : agentLabel(agentFilter);
|
|
@@ -256,6 +298,7 @@ function rowHtml(s) {
|
|
|
256
298
|
'<span class="col-size">' + esc(s.sizeLabel || '') + '</span>' +
|
|
257
299
|
'<span class="col-date">' + esc(s.dateLabel || '') + '</span>' +
|
|
258
300
|
'<span class="row-actions">' +
|
|
301
|
+
'<button class="act-btn" data-action="view-session" data-path="' + p + '" title="View chat">VIEW</button>' +
|
|
259
302
|
'<button class="act-btn" data-action="backup-one" data-path="' + p + '" title="Backup">BAK</button>' +
|
|
260
303
|
'<button class="act-btn del" data-action="trash-one" data-path="' + p + '" title="Trash">DEL</button>' +
|
|
261
304
|
'</span></div>';
|
|
@@ -281,12 +324,15 @@ wireBtn('btn-trash', async function() {
|
|
|
281
324
|
var ok = res.filter(function(r) { return r.ok; }).map(function(r) { return r.path; });
|
|
282
325
|
ok.forEach(function(p) { ALL = ALL.filter(function(s) { return s.path !== p; }); selected.delete(p); });
|
|
283
326
|
applyFilter(); toast(ok.length + ' MOVED TO TRASH', 'ok');
|
|
327
|
+
if (ok.length) switchTab('trash');
|
|
284
328
|
});
|
|
285
329
|
wireBtn('btn-backup', async function() {
|
|
286
330
|
if (!selected.size) { toast('SELECT SESSIONS FIRST', 'err'); return; }
|
|
287
331
|
var items = buildItems(Array.from(selected));
|
|
288
332
|
var res = await post('/api/backup/create', { items: items, note: '' });
|
|
289
|
-
|
|
333
|
+
var ok = res.filter(function(r) { return r.ok; }).length;
|
|
334
|
+
toast(ok + ' BACKUP(S) CREATED', 'ok');
|
|
335
|
+
if (ok) switchTab('backups');
|
|
290
336
|
});
|
|
291
337
|
|
|
292
338
|
// Enrich paths with session metadata for storage
|
|
@@ -304,12 +350,13 @@ function switchTab(tab) {
|
|
|
304
350
|
document.querySelectorAll('.panel').forEach(function(p) { p.classList.toggle('active', p.id === tab + '-panel'); });
|
|
305
351
|
// Mark active sub-item (agent-specific nav)
|
|
306
352
|
document.querySelectorAll('.sb-sub-item').forEach(function(el) {
|
|
307
|
-
el.classList.toggle('active', el.dataset.nav === tab);
|
|
353
|
+
el.classList.toggle('active', el.dataset.nav === tab && el.dataset.agent === agentFilter);
|
|
308
354
|
});
|
|
309
355
|
// Mark global tools active only when agentFilter is all
|
|
310
356
|
document.querySelectorAll('.sb-item[data-action="switch-tab-global"]').forEach(function(el) {
|
|
311
357
|
el.classList.toggle('active', el.dataset.nav === tab && agentFilter === 'all');
|
|
312
358
|
});
|
|
359
|
+
if (tab === 'sessions') applyFilter();
|
|
313
360
|
if (tab === 'usage') loadUsage(false);
|
|
314
361
|
if (tab === 'trash') loadTrash(false);
|
|
315
362
|
if (tab === 'backups') loadBackups(false);
|
|
@@ -325,17 +372,55 @@ async function trashOne(path) {
|
|
|
325
372
|
var res = await post('/api/trash/move', { items: items });
|
|
326
373
|
if (res[0] && res[0].ok) {
|
|
327
374
|
ALL = ALL.filter(function(s) { return s.path !== path; }); selected.delete(path);
|
|
328
|
-
applyFilter(); toast('MOVED TO TRASH', 'ok');
|
|
375
|
+
applyFilter(); toast('MOVED TO TRASH', 'ok'); switchTab('trash');
|
|
329
376
|
} else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
|
|
330
377
|
}
|
|
331
378
|
async function backupOne(path) {
|
|
332
379
|
if (!path) return;
|
|
333
380
|
var items = buildItems([path]);
|
|
334
381
|
var res = await post('/api/backup/create', { items: items, note: '' });
|
|
335
|
-
if (res[0] && res[0].ok) toast('BACKUP CREATED', 'ok');
|
|
382
|
+
if (res[0] && res[0].ok) { toast('BACKUP CREATED', 'ok'); switchTab('backups'); }
|
|
336
383
|
else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
|
|
337
384
|
}
|
|
338
385
|
|
|
386
|
+
// ── SESSION VIEWER ───────────────────────────────────────────────────────────
|
|
387
|
+
function closeModal() {
|
|
388
|
+
var m = $('modal');
|
|
389
|
+
if (m) m.hidden = true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function viewSession(path) {
|
|
393
|
+
if (!path) return;
|
|
394
|
+
var modal = $('modal'), title = $('modal-title'), sub = $('modal-sub'), body = $('modal-body');
|
|
395
|
+
if (!modal || !body) return;
|
|
396
|
+
modal.hidden = false;
|
|
397
|
+
if (title) title.textContent = 'LOADING SESSION';
|
|
398
|
+
if (sub) sub.textContent = path;
|
|
399
|
+
body.innerHTML = '<div class="loading"><span class="loading-dots">LOADING CHAT</span></div>';
|
|
400
|
+
try {
|
|
401
|
+
var data = await fetch('/api/session?path=' + encodeURIComponent(path)).then(function(r) { return r.json(); });
|
|
402
|
+
if (!data.ok) {
|
|
403
|
+
body.innerHTML = '<div class="empty"><div class="e-icon">[!]</div><div>' + esc(data.error || 'Could not load session') + '</div></div>';
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (title) title.textContent = 'SESSION HISTORY';
|
|
407
|
+
if (sub) sub.textContent = data.path + (data.truncated ? ' / preview truncated' : '');
|
|
408
|
+
var messages = data.messages || [];
|
|
409
|
+
if (messages.length) {
|
|
410
|
+
body.innerHTML = messages.slice(0, 200).map(function(m) {
|
|
411
|
+
return '<div class="chat-msg">' +
|
|
412
|
+
'<div class="chat-role">' + esc(m.index + ' / ' + m.role) + '</div>' +
|
|
413
|
+
'<div class="chat-text">' + esc(m.text) + '</div>' +
|
|
414
|
+
'</div>';
|
|
415
|
+
}).join('') + (messages.length > 200 ? '<div class="empty"><div>Showing first 200 entries</div></div>' : '');
|
|
416
|
+
} else {
|
|
417
|
+
body.innerHTML = '<pre class="chat-raw">' + esc(data.raw || '') + '</pre>';
|
|
418
|
+
}
|
|
419
|
+
} catch (e) {
|
|
420
|
+
body.innerHTML = '<div class="empty"><div class="e-icon">[!]</div><div>' + esc(e.message) + '</div></div>';
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
339
424
|
// ── USAGE TAB ─────────────────────────────────────────────────────────────────
|
|
340
425
|
async function loadUsage(force) {
|
|
341
426
|
var key = agentFilter;
|
|
@@ -468,7 +553,7 @@ async function loadTrash(force) {
|
|
|
468
553
|
|
|
469
554
|
async function restoreTrash(mp) {
|
|
470
555
|
var res = await post('/api/trash/restore', { metaPath: mp });
|
|
471
|
-
if (res.ok) { toast('RESTORED TO ' + res.restoredTo, 'ok'); loadTrash(true); }
|
|
556
|
+
if (res.ok) { toast('RESTORED TO ' + res.restoredTo, 'ok'); loadTrash(true); reloadSessions(); }
|
|
472
557
|
else toast('ERROR: ' + (res.error || '?'), 'err');
|
|
473
558
|
}
|
|
474
559
|
async function purgeTrash(mp) {
|
|
@@ -511,7 +596,7 @@ async function loadBackups(force) {
|
|
|
511
596
|
|
|
512
597
|
async function restoreBackup(mp) {
|
|
513
598
|
var res = await post('/api/backup/restore', { metaPath: mp });
|
|
514
|
-
if (res.ok) toast('RESTORED TO ' + res.restoredTo, 'ok');
|
|
599
|
+
if (res.ok) { toast('RESTORED TO ' + res.restoredTo, 'ok'); reloadSessions(); }
|
|
515
600
|
else toast('ERROR: ' + (res.error || '?'), 'err');
|
|
516
601
|
}
|
|
517
602
|
async function deleteBackup(mp) {
|
|
@@ -521,6 +606,14 @@ async function deleteBackup(mp) {
|
|
|
521
606
|
else toast('ERROR: ' + ((res[0] && res[0].error) || '?'), 'err');
|
|
522
607
|
}
|
|
523
608
|
|
|
609
|
+
async function reloadSessions() {
|
|
610
|
+
try {
|
|
611
|
+
var sessions = await fetch('/api/sessions').then(function(r) { return r.json(); });
|
|
612
|
+
ALL = sessions;
|
|
613
|
+
applyFilter();
|
|
614
|
+
} catch {}
|
|
615
|
+
}
|
|
616
|
+
|
|
524
617
|
// ── BOOT ──────────────────────────────────────────────────────────────────────
|
|
525
618
|
init().catch(function(e) { console.error('[AM] fatal:', e); });
|
|
526
619
|
|
package/src/ui/render.js
CHANGED
|
@@ -22,6 +22,7 @@ export function renderPage() {
|
|
|
22
22
|
<span id="hdr-sub">// AI SESSION MANAGER</span>
|
|
23
23
|
<div id="hdr-right">
|
|
24
24
|
<input id="search" type="search" placeholder="SEARCH SESSIONS...">
|
|
25
|
+
<button id="theme-toggle" class="theme-toggle" data-action="toggle-theme" type="button" title="Toggle light/dark mode">LIGHT</button>
|
|
25
26
|
</div>
|
|
26
27
|
</header>
|
|
27
28
|
|
|
@@ -64,7 +65,7 @@ export function renderPage() {
|
|
|
64
65
|
<span>X.com</span>
|
|
65
66
|
</a>
|
|
66
67
|
</div>
|
|
67
|
-
<div class="credit-pkg">aisessions v1.
|
|
68
|
+
<div class="credit-pkg">aisessions v1.1.2</div>
|
|
68
69
|
</div>
|
|
69
70
|
</nav>
|
|
70
71
|
|
|
@@ -124,7 +125,7 @@ export function renderPage() {
|
|
|
124
125
|
|
|
125
126
|
<!-- FOOTER -->
|
|
126
127
|
<footer id="foot">
|
|
127
|
-
<span>AISESSIONS v1.
|
|
128
|
+
<span>AISESSIONS v1.1.2</span>
|
|
128
129
|
<span> | </span>
|
|
129
130
|
<span id="foot-info">LOADING...</span>
|
|
130
131
|
</footer>
|
|
@@ -132,6 +133,18 @@ export function renderPage() {
|
|
|
132
133
|
</div><!-- /shell -->
|
|
133
134
|
|
|
134
135
|
<div id="toast"></div>
|
|
136
|
+
<div id="modal" class="modal" hidden>
|
|
137
|
+
<div class="modal-box">
|
|
138
|
+
<div class="modal-head">
|
|
139
|
+
<div>
|
|
140
|
+
<div id="modal-title" class="modal-title">SESSION</div>
|
|
141
|
+
<div id="modal-sub" class="modal-sub"></div>
|
|
142
|
+
</div>
|
|
143
|
+
<button class="btn" data-action="close-modal">CLOSE</button>
|
|
144
|
+
</div>
|
|
145
|
+
<div id="modal-body" class="modal-body"></div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
135
148
|
<script>${PAGE_JS}</script>
|
|
136
149
|
</body>
|
|
137
150
|
</html>`
|
package/src/usage/index.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url)
|
|
5
|
+
|
|
6
|
+
function ccusageCliPath() {
|
|
7
|
+
try {
|
|
8
|
+
return require.resolve('ccusage/dist/cli.js')
|
|
9
|
+
} catch {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
}
|
|
2
13
|
|
|
3
14
|
function run(args) {
|
|
4
|
-
const
|
|
15
|
+
const cli = ccusageCliPath()
|
|
16
|
+
if (!cli) return null
|
|
17
|
+
|
|
18
|
+
const r = spawnSync(process.execPath, [cli, ...args, '--json'], {
|
|
5
19
|
timeout: 30_000, maxBuffer: 16 * 1024 * 1024, encoding: 'utf8',
|
|
6
20
|
env: { ...process.env, NO_COLOR: '1' },
|
|
7
21
|
})
|
|
@@ -9,17 +23,6 @@ function run(args) {
|
|
|
9
23
|
try { return JSON.parse(r.stdout) } catch { return null }
|
|
10
24
|
}
|
|
11
25
|
|
|
12
|
-
function runLocal(args) {
|
|
13
|
-
try {
|
|
14
|
-
const r = spawnSync('ccusage', [...args, '--json'], {
|
|
15
|
-
timeout: 30_000, maxBuffer: 16 * 1024 * 1024, encoding: 'utf8',
|
|
16
|
-
env: { ...process.env, NO_COLOR: '1' },
|
|
17
|
-
})
|
|
18
|
-
if (r.status !== 0 || !r.stdout) return null
|
|
19
|
-
return JSON.parse(r.stdout)
|
|
20
|
-
} catch { return null }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
26
|
// Normalise the two very different per-agent response shapes into one schema.
|
|
24
27
|
function normalizeEntries(entries, agentId) {
|
|
25
28
|
return (entries || []).map(d => {
|
|
@@ -87,22 +90,22 @@ export async function getUsageData({ since, until, agent } = {}) {
|
|
|
87
90
|
|
|
88
91
|
let raw
|
|
89
92
|
if (agent && agent !== 'all') {
|
|
90
|
-
// Per-agent command:
|
|
93
|
+
// Per-agent command shape: ccusage claude daily --json
|
|
91
94
|
const agentArgs = [agent, 'daily', ...dateArgs]
|
|
92
|
-
raw =
|
|
95
|
+
raw = run(agentArgs)
|
|
93
96
|
if (!raw) {
|
|
94
97
|
// Fallback: run global and filter by metadata.agents
|
|
95
98
|
const globalArgs = ['daily', ...dateArgs]
|
|
96
|
-
const g =
|
|
99
|
+
const g = run(globalArgs)
|
|
97
100
|
if (g && Array.isArray(g.daily)) {
|
|
98
101
|
raw = { daily: g.daily.filter(d => (d.metadata?.agents || []).includes(agent)) }
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
} else {
|
|
102
|
-
raw =
|
|
105
|
+
raw = run(['daily', ...dateArgs])
|
|
103
106
|
}
|
|
104
107
|
|
|
105
|
-
if (!raw) return { available: false, error: 'ccusage
|
|
108
|
+
if (!raw) return { available: false, error: 'Bundled ccusage is unavailable or could not read local usage data' }
|
|
106
109
|
|
|
107
110
|
const entries = normalizeEntries(raw.daily || (Array.isArray(raw) ? raw : []), agent || 'all')
|
|
108
111
|
entries.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0))
|