aisessions 1.0.0 → 1.1.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/bin/cli.js CHANGED
@@ -3,7 +3,7 @@ import { createServer } from '../src/server.js'
3
3
  import { parseArgs } from 'node:util'
4
4
  import { exec } from 'node:child_process'
5
5
 
6
- const VERSION = '1.0.0'
6
+ const VERSION = '1.1.0'
7
7
 
8
8
  const { values } = parseArgs({
9
9
  options: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aisessions",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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/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:0 0 10px rgba(255,255,255,.25);
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}
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); }
@@ -66,6 +67,7 @@ document.addEventListener('click', function (e) {
66
67
  if (a === 'switch-tab') { switchTab(nav); return; }
67
68
  if (a === 'agent-tab') { switchTab(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; }
@@ -95,8 +97,34 @@ document.addEventListener('click', function (e) {
95
97
  if (gh) { var k = gh.dataset.gkey; collapsed.has(k) ? collapsed.delete(k) : collapsed.add(k); renderSessions(); }
96
98
  });
97
99
 
100
+ // ── THEME ─────────────────────────────────────────────────────────────────────
101
+ function getSavedTheme() {
102
+ try { return localStorage.getItem('aisessions-theme') || 'dark'; }
103
+ catch { return 'dark'; }
104
+ }
105
+
106
+ function saveTheme(next) {
107
+ try { localStorage.setItem('aisessions-theme', next); } catch {}
108
+ }
109
+
110
+ function applyTheme(next) {
111
+ theme = next === 'light' ? 'light' : 'dark';
112
+ document.documentElement.dataset.theme = theme;
113
+ var btn = $('theme-toggle');
114
+ if (btn) {
115
+ btn.textContent = theme === 'light' ? 'DARK' : 'LIGHT';
116
+ btn.title = 'Switch to ' + (theme === 'light' ? 'dark' : 'light') + ' mode';
117
+ }
118
+ }
119
+
120
+ function toggleTheme() {
121
+ applyTheme(theme === 'light' ? 'dark' : 'light');
122
+ saveTheme(theme);
123
+ }
124
+
98
125
  // ── INIT ──────────────────────────────────────────────────────────────────────
99
126
  async function init() {
127
+ applyTheme(getSavedTheme());
100
128
  var tblBody = $('tbl-body');
101
129
  if (!tblBody) return;
102
130
  tblBody.innerHTML = '<div class="loading"><span class="loading-dots">LOADING SESSIONS</span></div>';
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.0</div>
68
+ <div class="credit-pkg">aisessions v1.1</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.0</span>
128
+ <span>AISESSIONS v1.1</span>
128
129
  <span>&nbsp;|&nbsp;</span>
129
130
  <span id="foot-info">LOADING...</span>
130
131
  </footer>
@@ -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 r = spawnSync('npx', ['--yes', 'ccusage@latest', ...args, '--json'], {
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: npx ccusage claude daily --json
93
+ // Per-agent command shape: ccusage claude daily --json
91
94
  const agentArgs = [agent, 'daily', ...dateArgs]
92
- raw = runLocal(agentArgs) ?? run(agentArgs)
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 = runLocal(globalArgs) ?? run(globalArgs)
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 = runLocal(['daily', ...dateArgs]) ?? run(['daily', ...dateArgs])
105
+ raw = run(['daily', ...dateArgs])
103
106
  }
104
107
 
105
- if (!raw) return { available: false, error: 'ccusage not found run: npm i -g 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))