claude-live 2.0.7 → 3.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 +30 -39
- package/bin/claude-live +0 -0
- package/client/dist/assets/AgentDemoScene-DmPViizX.js +147 -0
- package/client/dist/assets/EffectDemoScene-DiyNw7OL.js +138 -0
- package/client/dist/assets/SakuraPage-BuSWb222.js +518 -0
- package/client/dist/assets/index-D3vltDuF.js +4446 -0
- package/client/dist/assets/index-TkH4paIm.css +1 -0
- package/client/dist/index.html +2 -2
- package/package.json +14 -8
- package/server/history-reader.js +68 -0
- package/server/index.js +101 -7
- package/server/project-tree.js +178 -0
- package/server/session-scanner.js +150 -0
- package/server/transcript-parser.js +177 -0
- package/.claude-plugin/hooks/hooks.json +0 -161
- package/client/dist/assets/BufferResource-B3YcFk1L.js +0 -185
- package/client/dist/assets/CanvasRenderer-B7cP3KcG.js +0 -1
- package/client/dist/assets/Filter-BXkJkOCD.js +0 -1
- package/client/dist/assets/RenderTargetSystem-DkV5EZ2H.js +0 -172
- package/client/dist/assets/WebGLRenderer-Cgmusykq.js +0 -156
- package/client/dist/assets/WebGPURenderer-B_Gw9-ml.js +0 -41
- package/client/dist/assets/browserAll-wXmCMyRg.js +0 -14
- package/client/dist/assets/index-BGs_09Jl.js +0 -318
- package/client/dist/assets/index-DjcKbX6b.css +0 -1
- package/client/dist/assets/webworkerAll-Hyzs6HuJ.js +0 -83
- package/hooks/hooks.json +0 -161
- package/hooks/run-hook.cmd +0 -38
- package/hooks/send-event +0 -5
- package/hooks/session-start +0 -40
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&family=Oxanium:wght@400;600;700&display=swap";*{margin:0;padding:0;box-sizing:border-box}html,body,#root{width:100%;height:100%;overflow:hidden;background:#050507}#root{position:relative}#root:before{content:"";position:fixed;top:0;right:0;bottom:0;left:0;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E");background-size:150px;opacity:.035;pointer-events:none;z-index:10}.hud-bar{position:fixed;top:14px;right:14px;z-index:50;display:flex;gap:8px;align-items:center;padding:5px 10px;border-radius:20px;background:#06070e8c;border:1px solid rgba(255,255,255,.06);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);box-shadow:0 4px 20px #0006;pointer-events:auto;font-family:IBM Plex Mono,monospace;animation:hud-fadein .5s ease-out}@keyframes hud-fadein{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}.hud-chip{font-size:11px;color:#b0b4c4;font-variant-numeric:tabular-nums;display:flex;align-items:center;gap:3px;letter-spacing:.02em}.hud-chip-label{font-size:8px;color:#555;text-transform:uppercase;letter-spacing:.08em}.hud-panel{min-width:210px;padding:14px 14px 12px;border-radius:12px;background:linear-gradient(140deg,#10121cb8,#08090ed1),radial-gradient(120% 140% at 0% 0%,#5aa0ff24,#0000);border:1px solid rgba(255,255,255,.08);box-shadow:0 24px 60px #0000008c,inset 0 0 0 1px #ffffff05;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);font-family:IBM Plex Mono,monospace;animation:hud-fadein .6s ease-out}@keyframes hud-fadein{0%{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}.hud-title{display:flex;align-items:center;gap:6px;font-family:Oxanium,sans-serif;font-size:16px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:#e6e8f2;line-height:1}.hud-title-mark{font-size:12px;color:#7ee7d8;animation:mark-spin 12s linear infinite}@keyframes mark-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.hud-title-accent{color:#7ee7d8}.hud-divider{height:1px;margin:10px 0 8px;background:linear-gradient(90deg,#7ee7d899,#fff0);opacity:.5;position:relative;overflow:hidden}.hud-divider:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(90deg,transparent,rgba(126,231,216,.8),transparent);animation:divider-sweep 4s ease-in-out infinite}@keyframes divider-sweep{0%,to{transform:translate(-100%);opacity:0}50%{transform:translate(100%);opacity:1}}.hud-stats{display:grid;gap:6px}.hud-stat{display:grid;grid-template-columns:66px 1fr;align-items:center;column-gap:8px}.hud-label{font-size:9px;color:#7b7f8f;text-transform:uppercase;letter-spacing:1.4px}.hud-value{font-size:12px;color:#c8cbd7;display:flex;align-items:center;gap:6px;letter-spacing:.2px}.hud-value--mono{font-variant-numeric:tabular-nums}.hud-dot{width:7px;height:7px;border-radius:50%;transition:background .3s,box-shadow .3s}.hud-dot--on{background:#7ee7d8;box-shadow:0 0 8px #7ee7d899;animation:dot-breathe 2s ease-in-out infinite}.hud-dot--err{background:#f87171;box-shadow:0 0 8px #f8717180;animation:dot-breathe 1s ease-in-out infinite}.hud-dot--warn{background:#fbbf24;box-shadow:0 0 8px #fbbf2466;animation:dot-breathe 1.5s ease-in-out infinite}@keyframes dot-breathe{0%,to{opacity:1;transform:scale(1)}50%{opacity:.6;transform:scale(.85)}}.hud-tool{font-weight:600;transition:color .2s ease}.tooltip{position:fixed;z-index:30;background:#08080ef0;border:1px solid rgba(255,255,255,.12);border-radius:6px;padding:10px 14px;pointer-events:none;transition:opacity .1s ease;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);min-width:160px;box-shadow:0 8px 32px #000000b3}.tooltip-label{font-family:IBM Plex Mono,monospace;font-size:12px;color:#fff;font-weight:500;margin-bottom:2px;white-space:nowrap;max-width:260px;overflow:hidden;text-overflow:ellipsis}.tooltip-type{font-family:IBM Plex Mono,monospace;font-size:9px;color:#888;text-transform:uppercase;letter-spacing:1px;margin-bottom:7px}.tooltip-meta{font-family:IBM Plex Mono,monospace;font-size:10px;color:#aaa;margin-bottom:3px}.tooltip-count{font-family:IBM Plex Mono,monospace;font-size:9px;color:#666}.sidebar{position:fixed;right:0;top:0;height:100%;width:280px;background:#06060bf5;border-left:1px solid rgba(255,255,255,.08);z-index:25;transform:translate(100%);transition:transform .25s cubic-bezier(.16,1,.3,1);-webkit-backdrop-filter:blur(24px);backdrop-filter:blur(24px);display:flex;flex-direction:column;overflow:hidden}.sidebar--open{transform:translate(0)}.sidebar-header{padding:24px 20px 16px;border-bottom:1px solid rgba(255,255,255,.06);position:relative}.sidebar-close{position:absolute;top:20px;right:16px;width:26px;height:26px;display:flex;align-items:center;justify-content:center;color:#666;cursor:pointer;font-size:18px;border-radius:4px;transition:color .15s,background .15s;pointer-events:all;line-height:1}.sidebar-close:hover{color:#fff;background:#ffffff14}.sidebar-title{font-family:Fira Code,monospace;font-size:13px;color:#fff;font-weight:500;margin-bottom:4px;padding-right:32px;word-break:break-all}.sidebar-type{font-family:Space Mono,monospace;font-size:9px;color:#666;text-transform:uppercase;letter-spacing:1px}.sidebar-section{padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.05)}.sidebar-section-label{font-family:Space Mono,monospace;font-size:9px;color:#555;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}.sidebar-action{font-family:Fira Code,monospace;font-size:15px;font-weight:500;margin-bottom:3px}.sidebar-time{font-family:Space Mono,monospace;font-size:10px;color:#777}.sidebar-count{font-family:Space Mono,monospace;font-size:24px;color:#ccc;font-weight:700}.sidebar-session{font-family:Fira Code,monospace;font-size:11px;color:#4ade80}.perm-badge{display:inline-flex;align-items:center;gap:5px;font-family:Space Mono,monospace;font-size:9px;color:#fbbf24;background:#fbbf241a;border:1px solid rgba(251,191,36,.3);border-radius:3px;padding:2px 6px;margin-top:6px}.perm-dot{width:5px;height:5px;border-radius:50%;background:#fbbf24;animation:perm-pulse 1s ease-in-out infinite}@keyframes perm-pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}}.elog{position:fixed;bottom:16px;left:16px;z-index:20;display:flex;flex-direction:column;gap:6px;pointer-events:none;max-width:320px;font-family:IBM Plex Mono,monospace}.elog>*{pointer-events:all}.elog-live{display:flex;flex-direction:column;gap:3px}.elog-row{display:flex;align-items:center;gap:5px;padding:2px 8px 2px 5px;border-radius:4px;background:#06070e40;border:1px solid rgba(255,255,255,.03);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);position:relative;overflow:hidden;animation:elog-slide .35s cubic-bezier(.16,1,.3,1);font-size:7px}.elog-row:after{content:"";position:absolute;top:0;bottom:0;left:-70%;width:70%;background:linear-gradient(90deg,transparent,var(--entry-color, #888) 60%,transparent);opacity:.15;animation:elog-sweep .5s ease-out forwards;pointer-events:none}@keyframes elog-slide{0%{opacity:0;transform:translate(-12px) scale(.97)}60%{opacity:1;transform:translate(2px) scale(1)}to{opacity:1;transform:translate(0) scale(1)}}@keyframes elog-sweep{0%{left:-70%;opacity:.15}to{left:140%;opacity:0}}.elog-row--static{animation:none;background:#06070e4d;-webkit-backdrop-filter:none;backdrop-filter:none}.elog-row--static:after{display:none}.elog-row--static .elog-dot{animation:none}.elog-dot{width:4px;height:4px;border-radius:50%;flex-shrink:0;animation:elog-dot-pop .4s cubic-bezier(.34,1.56,.64,1)}@keyframes elog-dot-pop{0%{transform:scale(0)}70%{transform:scale(1.6)}to{transform:scale(1)}}.elog-tool{font-size:7px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;min-width:28px;flex-shrink:0}.elog-file{font-size:7px;color:#666;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100px}.elog-id{font-size:7px;color:#3a3a4a;margin-left:auto;flex-shrink:0;letter-spacing:.02em;opacity:.6}.elog-hist-btn{font-family:IBM Plex Mono,monospace;font-size:8px;color:#444;background:none;border:none;cursor:pointer;text-align:left;padding:2px 0 0 6px;letter-spacing:.06em;transition:color .15s}.elog-hist-btn:hover{color:#888}.elog-hist{position:fixed;left:16px;top:16px;bottom:16px;width:320px;border-radius:10px;background:#06070ee0;border:1px solid rgba(255,255,255,.07);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);box-shadow:0 8px 32px #00000080;overflow:hidden;display:flex;flex-direction:column;animation:hud-fadein .3s ease-out;z-index:30}.elog-hist-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.06)}.elog-hist-title{font-size:9px;color:#666;text-transform:uppercase;letter-spacing:.1em}.elog-hist-count{font-size:9px;color:#7ee7d8;background:#7ee7d81a;padding:1px 6px;border-radius:8px;letter-spacing:.04em}.elog-hist-scroll{overflow-y:auto;padding:6px;display:flex;flex-direction:column;gap:2px;flex:1}.elog-hist-scroll::-webkit-scrollbar{width:4px}.elog-hist-scroll::-webkit-scrollbar-track{background:transparent}.elog-hist-scroll::-webkit-scrollbar-thumb{background:#ffffff14;border-radius:2px}.elog-hist-scroll::-webkit-scrollbar-thumb:hover{background:#ffffff26}.help-btn{background:none;border:1px solid rgba(255,255,255,.1);border-radius:4px;color:#444;font-family:Space Mono,monospace;font-size:9px;padding:3px 8px;cursor:pointer;width:fit-content;transition:color .15s,border-color .15s;letter-spacing:.05em;text-transform:uppercase}.help-btn:hover{color:#999;border-color:#ffffff40}.hud-ctrl-sep{width:1px;height:16px;background:#ffffff14;margin:0 2px}.hud-ctrl-btn{width:30px;height:30px;background:transparent;border:1px solid transparent;border-radius:6px;color:#666;cursor:pointer;font-size:13px;display:flex;align-items:center;justify-content:center;transition:color .2s,background .2s,border-color .2s;padding:0}.hud-ctrl-btn svg{width:15px;height:15px;stroke:currentColor}.hud-ctrl-btn:hover{color:#c8cbd7;background:#ffffff0f;border-color:#ffffff1a}.hud-ctrl-btn:active{background:#ffffff1a;transform:scale(.95)}.hud-button{width:32px;height:32px;background:#ffffff14;border:1px solid rgba(255,255,255,.12);border-radius:4px;color:#666;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:color .2s}.hud-button:hover{color:#aaa}.help-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:60;background:#0000008c;display:flex;align-items:center;justify-content:center;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.help-panel{background:#08080efa;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:20px 24px;min-width:200px;box-shadow:0 16px 48px #000c}.help-panel-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px}.help-panel-title{font-family:Space Mono,monospace;font-size:9px;color:#555;text-transform:uppercase;letter-spacing:1.5px}.legend-title{font-size:8px;color:#444;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:8px}.legend-items{display:flex;flex-direction:column;gap:5px}.legend-item{display:flex;align-items:center;gap:8px}.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.legend-badge{width:14px;height:14px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:7px;font-weight:700;color:#000;flex-shrink:0}.legend-name{font-size:9px;color:#888}.legend-key{font-size:8px;color:#555;margin-left:auto;padding-left:8px}.legend-perm{display:flex;align-items:center;gap:8px;margin-top:4px;padding-top:6px;border-top:1px solid rgba(255,255,255,.04)}.legend-perm-ring{width:14px;height:14px;border-radius:50%;border:2px dashed #fbbf24;flex-shrink:0}.legend-perm-name{font-size:9px;color:#fbbf24}.debug-toggle{position:fixed;bottom:24px;right:24px;z-index:40;background:#fbbf241f;border:1px solid rgba(251,191,36,.4);border-radius:5px;color:#fbbf24e6;font-family:Space Mono,monospace;font-size:10px;font-weight:700;letter-spacing:.15em;padding:6px 12px;cursor:pointer;transition:color .15s,background .15s,border-color .15s}.debug-toggle:hover{background:#fbbf2438;border-color:#fbbf24b3;color:#fbbf24}.debug-panel{position:fixed;bottom:56px;right:24px;width:260px;background:#06060cf7;border:1px solid rgba(255,255,255,.1);border-radius:8px;z-index:40;-webkit-backdrop-filter:blur(20px);backdrop-filter:blur(20px);box-shadow:0 16px 48px #000c;transform:translateY(8px) scale(.97);opacity:0;pointer-events:none;transition:opacity .18s ease,transform .18s ease}.debug-panel--open{opacity:1;transform:translateY(0) scale(1);pointer-events:all}.debug-panel-header{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 10px;border-bottom:1px solid rgba(255,255,255,.07)}.debug-panel-title{font-family:Space Mono,monospace;font-size:9px;color:#555;text-transform:uppercase;letter-spacing:1.5px}.debug-close{background:none;border:none;color:#555;font-size:16px;cursor:pointer;padding:0 2px;line-height:1;transition:color .15s}.debug-close:hover{color:#ccc}.debug-section{padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.05)}.debug-section:last-child{border-bottom:none}.debug-section-label{font-family:Space Mono,monospace;font-size:8px;color:#444;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:8px}.debug-session-row{display:flex;gap:6px}.debug-select{flex:1;background:#ffffff0d;border:1px solid rgba(255,255,255,.1);border-radius:4px;color:#aaa;font-family:Fira Code,monospace;font-size:10px;padding:4px 7px;outline:none;cursor:pointer}.debug-new-btn{background:#ffffff0f;border:1px solid rgba(255,255,255,.12);border-radius:4px;color:#888;font-family:Space Mono,monospace;font-size:9px;padding:4px 8px;cursor:pointer;white-space:nowrap;transition:color .15s,background .15s}.debug-new-btn:hover{color:#ccc;background:#ffffff1a}.debug-tool-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:5px}.debug-tool-btn{background:#ffffff0a;border:1px solid rgba(255,255,255,.08);border-radius:4px;color:var(--tool-color, #888);font-family:Space Mono,monospace;font-size:9px;padding:6px 4px;cursor:pointer;transition:background .15s,border-color .15s,opacity .15s;text-align:center}.debug-tool-btn:hover{background:color-mix(in srgb,var(--tool-color, #888) 15%,transparent);border-color:color-mix(in srgb,var(--tool-color, #888) 40%,transparent)}.debug-tool-btn:active{opacity:.6}.perm-notifications{position:fixed;left:24px;top:130px;display:flex;flex-direction:column;gap:6px;z-index:100;pointer-events:none;max-width:280px}.perm-notifications-title{font-size:9px;letter-spacing:.2em;text-transform:uppercase;color:#fbbf24;text-align:left;margin-bottom:2px;opacity:.7}.perm-notification-item{display:flex;align-items:flex-start;gap:10px;background:#fbbf240f;border:1px solid rgba(251,191,36,.25);border-radius:3px;padding:8px 14px;animation:perm-fadein .3s ease}@keyframes perm-fadein{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.perm-notification-dot{width:6px;height:6px;border-radius:50%;background:#fbbf24;margin-top:3px;flex-shrink:0;animation:perm-pulse 1s ease-in-out infinite}.perm-notification-body{display:flex;flex-direction:column;gap:2px}.perm-notification-session{font-size:9px;letter-spacing:.15em;color:#fbbf2499;text-transform:uppercase}.perm-notification-msg{font-size:11px;color:#fbbf24e6;max-width:320px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-overlay{position:fixed;top:24px;right:24px;max-width:400px;max-height:calc(100vh - 48px);background:#020209f2;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:16px;overflow-y:auto;z-index:100;font-size:12px;line-height:1.6;color:#ccc}.panel-close,.panel-close-btn{position:absolute;top:8px;right:8px;background:none;border:none;color:#666;cursor:pointer;font-size:16px;padding:0;width:24px;height:24px}.panel-close:hover,.panel-close-btn:hover{color:#aaa}.panel-overlay h3{margin:0 0 12px;font-size:14px;font-weight:600;color:#fff}.panel-overlay h4{margin:8px 0 6px;font-size:12px;font-weight:600;color:#aaa;text-transform:uppercase;letter-spacing:.05em}.panel-section{margin-bottom:16px}.panel-section:last-child{margin-bottom:0}.node-types-grid{display:flex;flex-direction:column;gap:8px}.node-type-item{display:flex;align-items:center;gap:8px}.node-type-badge{width:28px;height:28px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;color:#fff;flex-shrink:0}.node-type-info{display:flex;flex-direction:column;gap:2px}.node-type-name{font-size:12px;font-weight:500;color:#fff}.node-type-description{font-size:10px;color:#888}.animations-list{display:flex;flex-direction:column;gap:10px}.animation-item{font-size:11px}.animation-name{font-weight:500;color:#fff;margin-bottom:2px}.animation-description{font-size:10px;color:#888;padding-left:8px;border-left:2px solid rgba(255,255,255,.1)}
|
package/client/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
body { background: #080808; overflow: hidden; }
|
|
12
12
|
#root { width: 100vw; height: 100vh; }
|
|
13
13
|
</style>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-D3vltDuF.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-TkH4paIm.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-live",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Realtime Claude Code activity visualizer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "https://github.com/marisancans/claude-live",
|
|
@@ -12,30 +12,36 @@
|
|
|
12
12
|
"start": "node server/index.js",
|
|
13
13
|
"dev": "cd client && vite dev",
|
|
14
14
|
"build": "cd client && vite build",
|
|
15
|
-
"test": "cd client && npx tsc --noEmit"
|
|
15
|
+
"test": "cd client && npx tsc --noEmit",
|
|
16
|
+
"test:server": "vitest run --config vitest.config.server.js"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"server/",
|
|
19
20
|
"bin/",
|
|
20
21
|
"client/dist/",
|
|
21
22
|
".claude-plugin/",
|
|
22
|
-
"commands/"
|
|
23
|
-
"hooks/"
|
|
23
|
+
"commands/"
|
|
24
24
|
],
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=18"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"howler": "^2.2.4",
|
|
30
29
|
"@types/howler": "^2.2.12",
|
|
31
30
|
"@types/react": "^18.2.0",
|
|
32
31
|
"@types/react-dom": "^18.2.0",
|
|
32
|
+
"@types/three": "^0.183.1",
|
|
33
33
|
"@vitejs/plugin-react": "^4.2.0",
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
"react": "^18.2.0",
|
|
34
|
+
"howler": "^2.2.4",
|
|
35
|
+
"react": "^18.2.0",
|
|
37
36
|
"react-dom": "^18.2.0",
|
|
37
|
+
"three": "^0.183.2",
|
|
38
38
|
"typescript": "^5.3.0",
|
|
39
39
|
"vite": "^5.1.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@dgreenheck/ez-tree": "^1.1.0",
|
|
43
|
+
"@types/three": "^0.183.1",
|
|
44
|
+
"d3-force-3d": "^3.0.6",
|
|
45
|
+
"three": "^0.183.2"
|
|
40
46
|
}
|
|
41
47
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { TranscriptParser } from './transcript-parser.js';
|
|
4
|
+
|
|
5
|
+
function listSessionFiles(projectsDir) {
|
|
6
|
+
const files = [];
|
|
7
|
+
|
|
8
|
+
let entries;
|
|
9
|
+
try {
|
|
10
|
+
entries = readdirSync(projectsDir, { withFileTypes: true });
|
|
11
|
+
} catch {
|
|
12
|
+
return files;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (!entry.isDirectory()) continue;
|
|
17
|
+
const dirPath = join(projectsDir, entry.name);
|
|
18
|
+
let sessionFiles;
|
|
19
|
+
try {
|
|
20
|
+
sessionFiles = readdirSync(dirPath).filter(name => name.endsWith('.jsonl'));
|
|
21
|
+
} catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (const file of sessionFiles) {
|
|
26
|
+
const filePath = join(dirPath, file);
|
|
27
|
+
try {
|
|
28
|
+
files.push({ filePath, mtime: statSync(filePath).mtimeMs });
|
|
29
|
+
} catch {
|
|
30
|
+
// ignore unreadable files
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return files.sort((a, b) => a.mtime - b.mtime);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readProjectHistoryFromDisk(projectsDir, projectId, options = {}) {
|
|
39
|
+
const normalizedProjectId = resolve(projectId);
|
|
40
|
+
const maxSessions = options.maxSessions ?? 40;
|
|
41
|
+
const maxEvents = options.maxEvents ?? 4000;
|
|
42
|
+
const files = listSessionFiles(projectsDir);
|
|
43
|
+
const selectedFiles = files.slice(-maxSessions);
|
|
44
|
+
const events = [];
|
|
45
|
+
|
|
46
|
+
for (const { filePath } of selectedFiles) {
|
|
47
|
+
const parser = new TranscriptParser(event => {
|
|
48
|
+
if (typeof event.cwd !== 'string') return;
|
|
49
|
+
if (resolve(event.cwd) !== normalizedProjectId) return;
|
|
50
|
+
events.push(event);
|
|
51
|
+
if (events.length > maxEvents) events.splice(0, events.length - maxEvents);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let text;
|
|
55
|
+
try {
|
|
56
|
+
text = readFileSync(filePath, 'utf8');
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const line of text.split('\n')) {
|
|
62
|
+
if (!line.trim()) continue;
|
|
63
|
+
parser.processLine(line);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return events.sort((a, b) => a.timestamp - b.timestamp);
|
|
68
|
+
}
|
package/server/index.js
CHANGED
|
@@ -3,6 +3,10 @@ import { readFileSync, existsSync, statSync } from 'fs'
|
|
|
3
3
|
import { join, extname, resolve, sep } from 'path'
|
|
4
4
|
import { fileURLToPath } from 'url'
|
|
5
5
|
import { dirname } from 'path'
|
|
6
|
+
import { homedir } from 'os'
|
|
7
|
+
import { SessionScanner } from './session-scanner.js'
|
|
8
|
+
import { buildProjectTree, listActiveProjects } from './project-tree.js'
|
|
9
|
+
import { readProjectHistoryFromDisk } from './history-reader.js'
|
|
6
10
|
|
|
7
11
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
8
12
|
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version
|
|
@@ -26,22 +30,41 @@ const MIME = {
|
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
const clients = new Set()
|
|
33
|
+
const eventHistory = [] // all events seen since server start
|
|
34
|
+
const MAX_HISTORY = 5000
|
|
35
|
+
|
|
36
|
+
function filterHistory(events, { sessionId, projectId }) {
|
|
37
|
+
return events.filter(event => {
|
|
38
|
+
if (sessionId && event.session_id !== sessionId) return false
|
|
39
|
+
if (projectId && resolve(event.cwd || '') !== projectId) return false
|
|
40
|
+
return true
|
|
41
|
+
})
|
|
42
|
+
}
|
|
29
43
|
|
|
30
44
|
function broadcast(data) {
|
|
31
45
|
const msg = `data: ${JSON.stringify(data)}\n\n`
|
|
32
46
|
for (const res of clients) {
|
|
33
47
|
try { res.write(msg) } catch { clients.delete(res) }
|
|
34
48
|
}
|
|
49
|
+
// Buffer for history API
|
|
50
|
+
if (data.type === 'event') {
|
|
51
|
+
eventHistory.push(data.data)
|
|
52
|
+
if (eventHistory.length > MAX_HISTORY) eventHistory.splice(0, eventHistory.length - MAX_HISTORY)
|
|
53
|
+
}
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
const server = createServer((req, res) => {
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
const requestUrl = new URL(req.url || '/', 'http://localhost')
|
|
58
|
+
const pathname = requestUrl.pathname
|
|
59
|
+
|
|
60
|
+
// POST /hook — used by debug panel to inject test events
|
|
61
|
+
if (req.method === 'POST' && pathname === '/hook') {
|
|
40
62
|
let body = ''
|
|
41
63
|
req.on('data', c => body += c)
|
|
42
64
|
req.on('end', () => {
|
|
43
65
|
try {
|
|
44
66
|
const event = JSON.parse(body)
|
|
67
|
+
if (!event.id) event.id = `hook-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
45
68
|
broadcast({ type: 'event', data: event })
|
|
46
69
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
47
70
|
res.end('{"ok":true}')
|
|
@@ -54,14 +77,70 @@ const server = createServer((req, res) => {
|
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
// GET /health — health check
|
|
57
|
-
if (req.method === 'GET' &&
|
|
80
|
+
if (req.method === 'GET' && pathname === '/health') {
|
|
58
81
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
59
82
|
res.end(JSON.stringify({ ok: true, version: VERSION, clients: clients.size, port: PORT }))
|
|
60
83
|
return
|
|
61
84
|
}
|
|
62
85
|
|
|
86
|
+
if (req.method === 'GET' && pathname === '/api/projects') {
|
|
87
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
88
|
+
res.end(JSON.stringify({ projects: listActiveProjects(eventHistory) }))
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (req.method === 'GET' && pathname === '/api/project-tree') {
|
|
93
|
+
const projects = listActiveProjects(eventHistory)
|
|
94
|
+
const projectId = requestUrl.searchParams.get('project')
|
|
95
|
+
if (!projectId) {
|
|
96
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
97
|
+
res.end(JSON.stringify({ error: 'missing project query parameter' }))
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const normalizedId = resolve(projectId)
|
|
102
|
+
const project = projects.find(item => resolve(item.root) === normalizedId)
|
|
103
|
+
if (!project) {
|
|
104
|
+
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
105
|
+
res.end(JSON.stringify({ error: 'project not active or unavailable' }))
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const tree = buildProjectTree(project.root)
|
|
111
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
112
|
+
res.end(JSON.stringify(tree))
|
|
113
|
+
} catch (error) {
|
|
114
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
115
|
+
res.end(JSON.stringify({ error: error instanceof Error ? error.message : 'failed to build tree' }))
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// GET /api/history?session=ID — events for a specific session since last compact
|
|
121
|
+
if (req.method === 'GET' && pathname === '/api/history') {
|
|
122
|
+
const sessionFilter = requestUrl.searchParams.get('session')
|
|
123
|
+
const projectFilter = requestUrl.searchParams.get('project')
|
|
124
|
+
const persisted = requestUrl.searchParams.get('persisted') === '1'
|
|
125
|
+
const normalizedProject = projectFilter ? resolve(projectFilter) : null
|
|
126
|
+
let events = persisted && normalizedProject
|
|
127
|
+
? readProjectHistoryFromDisk(PROJECTS_DIR, normalizedProject)
|
|
128
|
+
: eventHistory
|
|
129
|
+
|
|
130
|
+
events = filterHistory(events, { sessionId: sessionFilter, projectId: normalizedProject })
|
|
131
|
+
// Only return events since the last PostCompact — history before that is irrelevant
|
|
132
|
+
let lastCompact = -1
|
|
133
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
134
|
+
if (events[i].hook_event_name === 'PostCompact') { lastCompact = i; break }
|
|
135
|
+
}
|
|
136
|
+
if (lastCompact >= 0) events = events.slice(lastCompact + 1)
|
|
137
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
138
|
+
res.end(JSON.stringify(events))
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
63
142
|
// GET /events — SSE stream
|
|
64
|
-
if (req.method === 'GET' &&
|
|
143
|
+
if (req.method === 'GET' && pathname === '/events') {
|
|
65
144
|
res.writeHead(200, {
|
|
66
145
|
'Content-Type': 'text/event-stream',
|
|
67
146
|
'Cache-Control': 'no-cache',
|
|
@@ -77,7 +156,7 @@ const server = createServer((req, res) => {
|
|
|
77
156
|
}
|
|
78
157
|
|
|
79
158
|
// Static files
|
|
80
|
-
const urlPath =
|
|
159
|
+
const urlPath = pathname
|
|
81
160
|
let filePath = join(DIST, urlPath === '/' ? 'index.html' : urlPath)
|
|
82
161
|
if (!resolve(filePath).startsWith(resolve(DIST) + sep) && resolve(filePath) !== resolve(DIST)) {
|
|
83
162
|
filePath = join(DIST, 'index.html')
|
|
@@ -96,6 +175,21 @@ const server = createServer((req, res) => {
|
|
|
96
175
|
res.end(readFileSync(filePath))
|
|
97
176
|
})
|
|
98
177
|
|
|
99
|
-
|
|
100
|
-
|
|
178
|
+
const PROJECTS_DIR = process.env.CLAUDE_PROJECTS_DIR
|
|
179
|
+
|| join(homedir(), '.claude', 'projects')
|
|
180
|
+
|
|
181
|
+
const scanner = new SessionScanner(PROJECTS_DIR, event => {
|
|
182
|
+
broadcast({ type: 'event', data: event })
|
|
101
183
|
})
|
|
184
|
+
|
|
185
|
+
// Only auto-start when run directly (not imported for testing)
|
|
186
|
+
const isMainModule = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))
|
|
187
|
+
if (isMainModule) {
|
|
188
|
+
server.listen(PORT, () => {
|
|
189
|
+
console.log(`claude-live running at http://localhost:${PORT}`)
|
|
190
|
+
scanner.start()
|
|
191
|
+
console.log(`watching ${PROJECTS_DIR} for sessions`)
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { server, scanner, broadcast }
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, join, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_IGNORES = new Set([
|
|
5
|
+
'.git',
|
|
6
|
+
'.cache',
|
|
7
|
+
'.next',
|
|
8
|
+
'.nuxt',
|
|
9
|
+
'.output',
|
|
10
|
+
'.parcel-cache',
|
|
11
|
+
'.pnpm-store',
|
|
12
|
+
'.superpowers',
|
|
13
|
+
'.svelte-kit',
|
|
14
|
+
'.turbo',
|
|
15
|
+
'.worktrees',
|
|
16
|
+
'.yarn',
|
|
17
|
+
'build',
|
|
18
|
+
'coverage',
|
|
19
|
+
'dist',
|
|
20
|
+
'node_modules',
|
|
21
|
+
'out',
|
|
22
|
+
'target',
|
|
23
|
+
'tmp',
|
|
24
|
+
'temp',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const DEFAULT_LIMITS = {
|
|
28
|
+
maxDepth: 7,
|
|
29
|
+
maxChildren: 48,
|
|
30
|
+
maxNodes: 1200,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function toPosixPath(value) {
|
|
34
|
+
return value.split(sep).join('/');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeProjectLabel(rootPath) {
|
|
38
|
+
return basename(rootPath) || rootPath;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function shouldIgnore(entry) {
|
|
42
|
+
if (DEFAULT_IGNORES.has(entry.name)) return true;
|
|
43
|
+
if (entry.isSymbolicLink()) return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function compareEntries(a, b) {
|
|
48
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
49
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
50
|
+
return a.name.localeCompare(b.name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function listActiveProjects(events) {
|
|
54
|
+
const projects = new Map();
|
|
55
|
+
|
|
56
|
+
for (const event of events) {
|
|
57
|
+
if (!event || typeof event.cwd !== 'string' || !event.cwd) continue;
|
|
58
|
+
const projectId = resolve(event.cwd);
|
|
59
|
+
let project = projects.get(projectId);
|
|
60
|
+
if (!project) {
|
|
61
|
+
project = {
|
|
62
|
+
id: projectId,
|
|
63
|
+
root: projectId,
|
|
64
|
+
label: makeProjectLabel(projectId),
|
|
65
|
+
eventCount: 0,
|
|
66
|
+
lastEventTime: 0,
|
|
67
|
+
sessionIds: new Set(),
|
|
68
|
+
};
|
|
69
|
+
projects.set(projectId, project);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
project.eventCount += 1;
|
|
73
|
+
project.lastEventTime = Math.max(project.lastEventTime, Number(event.timestamp) || 0);
|
|
74
|
+
if (event.session_id) project.sessionIds.add(event.session_id);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [...projects.values()]
|
|
78
|
+
.map(project => ({
|
|
79
|
+
id: project.id,
|
|
80
|
+
root: project.root,
|
|
81
|
+
label: project.label,
|
|
82
|
+
eventCount: project.eventCount,
|
|
83
|
+
lastEventTime: project.lastEventTime,
|
|
84
|
+
sessionCount: project.sessionIds.size,
|
|
85
|
+
}))
|
|
86
|
+
.sort((a, b) => b.lastEventTime - a.lastEventTime || a.label.localeCompare(b.label));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildProjectTree(rootPath, options = {}) {
|
|
90
|
+
const root = resolve(rootPath);
|
|
91
|
+
const limits = { ...DEFAULT_LIMITS, ...options };
|
|
92
|
+
const stats = {
|
|
93
|
+
directories: 0,
|
|
94
|
+
files: 0,
|
|
95
|
+
totalNodes: 0,
|
|
96
|
+
maxDepthReached: 0,
|
|
97
|
+
truncated: false,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function visit(absPath, relPath, depth) {
|
|
101
|
+
if (stats.totalNodes >= limits.maxNodes) {
|
|
102
|
+
stats.truncated = true;
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let st;
|
|
107
|
+
try {
|
|
108
|
+
st = statSync(absPath);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pathId = relPath || '.';
|
|
114
|
+
const name = relPath ? basename(absPath) : makeProjectLabel(root);
|
|
115
|
+
stats.totalNodes += 1;
|
|
116
|
+
stats.maxDepthReached = Math.max(stats.maxDepthReached, depth);
|
|
117
|
+
|
|
118
|
+
if (!st.isDirectory()) {
|
|
119
|
+
stats.files += 1;
|
|
120
|
+
return {
|
|
121
|
+
id: pathId,
|
|
122
|
+
name,
|
|
123
|
+
path: pathId,
|
|
124
|
+
type: 'file',
|
|
125
|
+
depth,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
stats.directories += 1;
|
|
130
|
+
const node = {
|
|
131
|
+
id: pathId,
|
|
132
|
+
name,
|
|
133
|
+
path: pathId,
|
|
134
|
+
type: 'folder',
|
|
135
|
+
depth,
|
|
136
|
+
children: [],
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (depth >= limits.maxDepth) {
|
|
140
|
+
try {
|
|
141
|
+
if (readdirSync(absPath).length > 0) stats.truncated = true;
|
|
142
|
+
} catch {
|
|
143
|
+
// ignore unreadable directories
|
|
144
|
+
}
|
|
145
|
+
return node;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let entries;
|
|
149
|
+
try {
|
|
150
|
+
entries = readdirSync(absPath, { withFileTypes: true })
|
|
151
|
+
.filter(entry => !shouldIgnore(entry))
|
|
152
|
+
.sort(compareEntries);
|
|
153
|
+
} catch {
|
|
154
|
+
return node;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (entries.length > limits.maxChildren) {
|
|
158
|
+
entries = entries.slice(0, limits.maxChildren);
|
|
159
|
+
stats.truncated = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
const childRelPath = relPath ? toPosixPath(join(relPath, entry.name)) : entry.name;
|
|
164
|
+
const childNode = visit(join(absPath, entry.name), childRelPath, depth + 1);
|
|
165
|
+
if (childNode) node.children.push(childNode);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return node;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
projectId: root,
|
|
173
|
+
rootPath: root,
|
|
174
|
+
label: makeProjectLabel(root),
|
|
175
|
+
tree: visit(root, '', 0),
|
|
176
|
+
stats,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readdirSync, statSync, openSync, readSync, closeSync, watch } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { TranscriptParser } from './transcript-parser.js';
|
|
4
|
+
|
|
5
|
+
const MAX_SESSIONS = 50;
|
|
6
|
+
const SCAN_INTERVAL = 5000;
|
|
7
|
+
const POLL_INTERVAL = 3000;
|
|
8
|
+
const ACTIVE_AGE_MS = 10 * 60 * 1000; // Only watch sessions modified in last 10 minutes
|
|
9
|
+
|
|
10
|
+
export class SessionScanner {
|
|
11
|
+
constructor(projectsDir, onEvent) {
|
|
12
|
+
this.projectsDir = projectsDir;
|
|
13
|
+
this.onEvent = onEvent;
|
|
14
|
+
this.sessions = new Map();
|
|
15
|
+
this._scanTimer = null;
|
|
16
|
+
this._pollTimer = null;
|
|
17
|
+
this._dirWatcher = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
start() {
|
|
21
|
+
this.scan();
|
|
22
|
+
this._scanTimer = setInterval(() => this.scan(), SCAN_INTERVAL);
|
|
23
|
+
this._pollTimer = setInterval(() => this.pollAll(), POLL_INTERVAL);
|
|
24
|
+
try {
|
|
25
|
+
this._dirWatcher = watch(this.projectsDir, { recursive: false }, () => {
|
|
26
|
+
this.scan();
|
|
27
|
+
});
|
|
28
|
+
} catch {
|
|
29
|
+
// fs.watch may not be supported
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
stop() {
|
|
34
|
+
if (this._scanTimer) { clearInterval(this._scanTimer); this._scanTimer = null; }
|
|
35
|
+
if (this._pollTimer) { clearInterval(this._pollTimer); this._pollTimer = null; }
|
|
36
|
+
if (this._dirWatcher) { this._dirWatcher.close(); this._dirWatcher = null; }
|
|
37
|
+
for (const session of this.sessions.values()) {
|
|
38
|
+
if (session.watcher) { session.watcher.close(); }
|
|
39
|
+
}
|
|
40
|
+
this.sessions.clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
scan() {
|
|
44
|
+
let subdirs;
|
|
45
|
+
try {
|
|
46
|
+
subdirs = readdirSync(this.projectsDir, { withFileTypes: true })
|
|
47
|
+
.filter(d => d.isDirectory())
|
|
48
|
+
.map(d => d.name);
|
|
49
|
+
} catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Prune stale sessions (no longer recently modified)
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
for (const [path, session] of this.sessions) {
|
|
56
|
+
try {
|
|
57
|
+
const mtime = statSync(path).mtimeMs;
|
|
58
|
+
if (now - mtime >= ACTIVE_AGE_MS) {
|
|
59
|
+
if (session.watcher) session.watcher.close();
|
|
60
|
+
this.sessions.delete(path);
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
if (session.watcher) session.watcher.close();
|
|
64
|
+
this.sessions.delete(path);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Collect all JSONL files with their mtimes
|
|
69
|
+
const candidates = [];
|
|
70
|
+
for (const subdir of subdirs) {
|
|
71
|
+
const dirPath = join(this.projectsDir, subdir);
|
|
72
|
+
let files;
|
|
73
|
+
try {
|
|
74
|
+
files = readdirSync(dirPath).filter(f => f.endsWith('.jsonl'));
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
for (const file of files) {
|
|
79
|
+
const filePath = join(dirPath, file);
|
|
80
|
+
if (this.sessions.has(filePath)) continue;
|
|
81
|
+
try {
|
|
82
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
83
|
+
// Only consider recently active sessions
|
|
84
|
+
if (now - mtime < ACTIVE_AGE_MS) {
|
|
85
|
+
candidates.push({ filePath, mtime });
|
|
86
|
+
}
|
|
87
|
+
} catch { continue; }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Sort by most recent first so active sessions get priority
|
|
92
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
93
|
+
|
|
94
|
+
for (const { filePath } of candidates) {
|
|
95
|
+
if (this.sessions.size >= MAX_SESSIONS) break;
|
|
96
|
+
|
|
97
|
+
const parser = new TranscriptParser(this.onEvent);
|
|
98
|
+
let watcher = null;
|
|
99
|
+
try {
|
|
100
|
+
watcher = watch(filePath, () => this._readNewLines(this.sessions.get(filePath)));
|
|
101
|
+
} catch { /* ignore */ }
|
|
102
|
+
|
|
103
|
+
const session = { filePath, offset: 0, parser, watcher };
|
|
104
|
+
this.sessions.set(filePath, session);
|
|
105
|
+
this._readNewLines(session);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pollAll() {
|
|
110
|
+
for (const session of this.sessions.values()) {
|
|
111
|
+
this._readNewLines(session);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_readNewLines(session) {
|
|
116
|
+
if (!session) return;
|
|
117
|
+
let size;
|
|
118
|
+
try {
|
|
119
|
+
size = statSync(session.filePath).size;
|
|
120
|
+
} catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (size === session.offset) return;
|
|
125
|
+
|
|
126
|
+
if (size < session.offset) {
|
|
127
|
+
session.offset = 0;
|
|
128
|
+
session.parser = new TranscriptParser(this.onEvent);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const bytesToRead = size - session.offset;
|
|
132
|
+
const buf = Buffer.alloc(bytesToRead);
|
|
133
|
+
let fd;
|
|
134
|
+
try {
|
|
135
|
+
fd = openSync(session.filePath, 'r');
|
|
136
|
+
readSync(fd, buf, 0, bytesToRead, session.offset);
|
|
137
|
+
} finally {
|
|
138
|
+
if (fd !== undefined) closeSync(fd);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const text = buf.toString('utf8');
|
|
142
|
+
const lines = text.split('\n');
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
if (line.trim()) {
|
|
145
|
+
session.parser.processLine(line);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
session.offset = size;
|
|
149
|
+
}
|
|
150
|
+
}
|