claude-remote-cli 3.5.3 → 3.6.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/dist/frontend/assets/index-BYXQcBQc.js +50 -0
- package/dist/frontend/assets/{index-CyOBs5Qv.css → index-CiwYPknn.css} +1 -1
- package/dist/frontend/index.html +2 -2
- package/dist/server/analytics.js +121 -0
- package/dist/server/index.js +11 -1
- package/dist/server/sessions.js +26 -0
- package/dist/server/workspaces.js +3 -0
- package/dist/server/ws.js +7 -0
- package/dist/test/analytics.test.js +152 -0
- package/package.json +3 -1
- package/dist/frontend/assets/index-Anv1KpSt.js +0 -50
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.pin-gate.svelte-1qp96yb{display:flex;align-items:center;justify-content:center;height:100vh;background:var(--bg);padding:1rem}.pin-container.svelte-1qp96yb{display:flex;flex-direction:column;align-items:center;gap:1rem;width:100%;max-width:320px;text-align:center}.pin-container.svelte-1qp96yb h1:where(.svelte-1qp96yb){font-size:1.5rem;color:var(--text)}.pin-container.svelte-1qp96yb p:where(.svelte-1qp96yb){color:var(--text-muted);font-size:.95rem}input.svelte-1qp96yb{width:100%;padding:14px 16px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:1.2rem;text-align:center;outline:none;-webkit-appearance:none}input.svelte-1qp96yb:focus{border-color:var(--accent)}button.svelte-1qp96yb{width:100%;padding:14px;background:var(--accent);color:#fff;border:none;border-radius:8px;font-size:1rem;font-weight:600;cursor:pointer;touch-action:manipulation}button.svelte-1qp96yb:active{opacity:.8}.error.svelte-1qp96yb{color:var(--accent);font-size:.9rem}.context-menu-trigger.svelte-1nmhce7{background:none;border:none;color:var(--text-muted);font-size:1rem;font-weight:700;cursor:pointer;padding:0 6px;border-radius:4px;touch-action:manipulation;flex-shrink:0;line-height:1;letter-spacing:1px;min-height:24px;display:inline-flex;align-items:center;transition:color .15s,background .15s}.context-menu-trigger.svelte-1nmhce7:hover{color:var(--text);background:var(--border)}.context-menu-backdrop.svelte-1nmhce7{position:fixed;top:0;right:0;bottom:0;left:0;z-index:999}.context-menu.svelte-1nmhce7{position:fixed;list-style:none;margin:0;padding:4px 0;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 16px #0006;z-index:1000;min-width:175px}.context-menu-item.svelte-1nmhce7{padding:9px 14px;font-size:.85rem;cursor:pointer;color:var(--text);white-space:nowrap}.context-menu-item.svelte-1nmhce7:hover{background:var(--border)}.context-menu-item.svelte-1nmhce7:focus{outline:2px solid var(--accent);outline-offset:-2px}.context-menu-item--danger.svelte-1nmhce7{color:#e74c3c}.context-menu-item--danger.svelte-1nmhce7:hover{background:#e74c3c1f}.context-menu-item--disabled.svelte-1nmhce7{opacity:.4;cursor:default}.context-menu-item--disabled.svelte-1nmhce7:hover{background:none}.workspace-item.svelte-168i8d5{display:flex;flex-direction:column}.workspace-header.svelte-168i8d5{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;cursor:pointer;min-height:44px;transition:background .12s}.workspace-header.svelte-168i8d5:hover{background:var(--surface-hover)}.workspace-item.active.svelte-168i8d5 .workspace-header:where(.svelte-168i8d5){background:var(--surface-hover)}.workspace-left.svelte-168i8d5{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.workspace-header.reorder-mode.svelte-168i8d5{cursor:grab}.workspace-header.reorder-mode.svelte-168i8d5:active{cursor:grabbing}.collapse-chevron.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;font-size:.7rem;color:var(--text-muted);cursor:pointer;flex-shrink:0;transition:color .12s}.collapse-chevron.svelte-168i8d5:hover{color:var(--text)}.collapse-count.svelte-168i8d5{font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);background:var(--border);border-radius:8px;padding:1px 6px;flex-shrink:0}.initial-block.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:4px;font-size:var(--font-size-xs);font-weight:700;color:#000;font-family:var(--font-mono);flex-shrink:0;line-height:1}.workspace-name.svelte-168i8d5{font-size:var(--font-size-sm);font-weight:700;color:var(--text);font-family:var(--font-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.workspace-actions.svelte-168i8d5{display:flex;align-items:center;gap:2px;opacity:0;transition:opacity .12s;flex-shrink:0}.workspace-header.svelte-168i8d5:hover .workspace-actions:where(.svelte-168i8d5){opacity:1}.action-btn.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:3px;font-size:var(--font-size-xs);color:var(--text-muted);cursor:pointer;font-family:var(--font-mono);transition:background .1s,color .1s}.action-btn.svelte-168i8d5:hover{background:var(--border);color:var(--text)}.session-list.svelte-168i8d5{list-style:none}.session-row.svelte-168i8d5{position:relative;display:flex;flex-direction:column;gap:2px;padding:6px 10px 6px 36px;cursor:pointer;min-height:44px;font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);transition:background .1s;border-left:3px solid transparent;justify-content:center}.session-row.svelte-168i8d5:hover{background:var(--surface-hover);color:var(--text)}.session-row.selected.svelte-168i8d5{border-left-color:var(--accent);background:var(--surface-hover);color:var(--text)}.session-row.attention.svelte-168i8d5 .session-name:where(.svelte-168i8d5){font-weight:700;color:var(--text)}.session-row-primary.svelte-168i8d5{display:flex;align-items:center;gap:8px;min-width:0}.session-row-secondary.svelte-168i8d5{display:flex;align-items:center;gap:6px;padding-left:15px;font-size:.65rem;color:var(--text-muted);min-width:0}.secondary-branch.svelte-168i8d5{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.row-menu-overlay.svelte-168i8d5{position:absolute;right:8px;top:0;bottom:0;display:flex;align-items:center;opacity:0;transition:opacity .12s;z-index:2}.session-row.svelte-168i8d5:hover .row-menu-overlay:where(.svelte-168i8d5){opacity:1}.secondary-pr.svelte-168i8d5{white-space:nowrap;flex-shrink:0;color:var(--accent)}.secondary-time.svelte-168i8d5{white-space:nowrap;flex-shrink:0;opacity:.7}.session-count-badge.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;border-radius:8px;background:var(--border);color:var(--text-muted);font-size:.55rem;font-family:var(--font-mono);font-weight:600;padding:0 4px;flex-shrink:0}.diff-badge.svelte-168i8d5{display:inline-flex;align-items:center;gap:4px;font-size:.6rem;flex-shrink:0;margin-left:auto}.diff-add.svelte-168i8d5{color:var(--status-success)}.diff-del.svelte-168i8d5{color:var(--status-error)}.status-dot.svelte-168i8d5{display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0}.status-dot--running.svelte-168i8d5{background:var(--status-success)}.status-dot--idle.svelte-168i8d5{background:var(--status-info)}.dot-inactive.svelte-168i8d5{width:7px;height:7px;border-radius:50%;background:#555;flex-shrink:0}.session-row.inactive.svelte-168i8d5 .session-name:where(.svelte-168i8d5){color:var(--text-muted)}.session-row.inactive.svelte-168i8d5:hover .session-name:where(.svelte-168i8d5){color:var(--text)}.session-row.loading.svelte-168i8d5{pointer-events:none;opacity:.7}.session-row.loading.svelte-168i8d5 .session-name:where(.svelte-168i8d5){color:var(--accent)}.status-dot--attention.svelte-168i8d5{background:var(--status-warning);box-shadow:0 0 5px 1px #fbbf2473;animation:svelte-168i8d5-attention-glow 2s ease-in-out infinite}.status-dot--permission-prompt.svelte-168i8d5{background:#eab308;box-shadow:0 0 5px 1px #eab30873;animation:svelte-168i8d5-attention-glow 1.5s ease-in-out infinite}@keyframes svelte-168i8d5-attention-glow{0%,to{box-shadow:0 0 3px 1px #fbbf244d}50%{box-shadow:0 0 7px 2px #fbbf2499}}.terminal-icon.svelte-168i8d5{font-size:.6rem;font-weight:700;color:var(--text-muted);flex-shrink:0;font-family:var(--font-mono);line-height:1}.session-name.svelte-168i8d5{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-width:0}.session-name.bold.svelte-168i8d5{font-weight:700}.add-worktree-row.svelte-168i8d5{padding:4px 10px 6px 36px}.add-worktree-btn.svelte-168i8d5{font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);opacity:.5;cursor:pointer;transition:opacity .1s,color .1s}.add-worktree-btn.svelte-168i8d5:hover{opacity:1;color:var(--text)}.add-worktree-row.disabled.svelte-168i8d5{pointer-events:none}.add-worktree-row.disabled.svelte-168i8d5 .add-worktree-btn:where(.svelte-168i8d5){opacity:.7;color:var(--accent)}.workspace-divider.svelte-168i8d5{height:1px;background:var(--border);margin:0}@media(max-width:600px){.workspace-header.svelte-168i8d5,.session-row.svelte-168i8d5{min-height:48px}.workspace-actions.svelte-168i8d5{opacity:1}.row-menu-overlay.svelte-168i8d5{display:none}}.smart-search.svelte-itm1qs{position:relative;flex-shrink:0}.input-row.svelte-itm1qs{display:flex;align-items:center;padding:6px 10px;border-bottom:1px solid var(--border);gap:6px}.prompt.svelte-itm1qs{font-family:var(--font-mono);font-size:var(--font-size-sm);color:var(--accent);flex-shrink:0;line-height:1;-webkit-user-select:none;user-select:none}.search-input.svelte-itm1qs{flex:1;background:transparent;border:none;outline:none;color:var(--text);font-family:var(--font-mono);font-size:var(--font-size-sm);caret-color:var(--accent)}.search-input.svelte-itm1qs::placeholder{color:var(--text-muted);opacity:.5}.dropdown.svelte-itm1qs{position:absolute;top:100%;left:0;right:0;background:var(--surface);border:1px solid var(--border);border-top:none;list-style:none;z-index:200;max-height:240px;overflow-y:auto}.dropdown-item.svelte-itm1qs{display:flex;align-items:baseline;gap:6px;padding:8px 10px;cursor:pointer;font-family:var(--font-mono);font-size:var(--font-size-sm);color:var(--text-muted);transition:background .1s;white-space:nowrap;overflow:hidden}.dropdown-item.svelte-itm1qs:hover,.dropdown-item.focused.svelte-itm1qs{background:var(--surface-hover);color:var(--text)}.dropdown-item.svelte-itm1qs strong:where(.svelte-itm1qs){color:var(--text);font-weight:700}.dropdown-path.svelte-itm1qs{font-size:var(--font-size-xs);color:var(--text-muted);opacity:.5;overflow:hidden;text-overflow:ellipsis;min-width:0;flex-shrink:1}@media(max-width:600px){.smart-search.svelte-itm1qs{width:100%}.input-row.svelte-itm1qs{width:100%;box-sizing:border-box}.search-input.svelte-itm1qs{width:100%;min-width:0}}.sidebar.svelte-owj5vn{position:relative;display:flex;flex-direction:column;background:var(--bg);border-right:1px solid var(--border);overflow:hidden;transition:transform .25s ease,width .2s ease,min-width .2s ease;z-index:100}.resize-handle.svelte-owj5vn{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;z-index:10;transition:background .15s}.resize-handle.svelte-owj5vn:hover{background:var(--accent)}.sidebar-header.svelte-owj5vn{display:flex;align-items:center;justify-content:space-between;padding:12px 10px;border-bottom:1px solid var(--border);flex-shrink:0}.sidebar-label.svelte-owj5vn{flex:1;font-size:var(--font-size-xs);font-weight:600;color:var(--text-muted);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em}.collapse-btn.svelte-owj5vn{background:none;border:none;color:var(--text-muted);font-size:1.1rem;cursor:pointer;padding:8px 10px;border-radius:4px;flex-shrink:0;line-height:1;font-family:var(--font-mono);min-width:36px;min-height:36px;display:flex;align-items:center;justify-content:center}.collapse-btn.svelte-owj5vn:hover{color:var(--text);background:var(--border)}.sidebar.collapsed.svelte-owj5vn .sidebar-header:where(.svelte-owj5vn){justify-content:center;padding:12px 4px}.icon-btn.svelte-owj5vn{background:none;border:none;color:var(--text);font-size:1.2rem;cursor:pointer;padding:4px 6px;border-radius:4px;touch-action:manipulation;display:none}.icon-btn.svelte-owj5vn:active{background:var(--border)}.workspace-list.svelte-owj5vn{flex:1;overflow-y:auto;overflow-x:hidden}.empty-state.svelte-owj5vn{padding:16px 10px;font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);opacity:.5;text-align:center}.sidebar-footer-row.svelte-owj5vn{display:flex;gap:8px;margin:8px;align-items:stretch;flex-shrink:0}.add-workspace-btn.svelte-owj5vn{flex:1;padding:10px 12px;min-height:40px;background:none;border:1px solid var(--accent);border-radius:0;color:var(--accent);font-size:var(--font-size-xs);font-family:var(--font-mono);cursor:pointer;touch-action:manipulation;text-align:center;transition:background .1s}.add-workspace-btn.svelte-owj5vn:hover{background:color-mix(in srgb,var(--accent) 12%,transparent)}.add-workspace-btn.svelte-owj5vn:active{background:var(--border)}.done-reorder-btn.svelte-owj5vn{margin:8px;padding:10px 12px;min-height:40px;background:none;border:1px solid var(--accent);border-radius:0;color:var(--accent);font-size:var(--font-size-xs);font-family:var(--font-mono);cursor:pointer;touch-action:manipulation;text-align:center;flex-shrink:0;transition:background .1s}.done-reorder-btn.svelte-owj5vn:hover{background:color-mix(in srgb,var(--accent) 12%,transparent)}.done-reorder-btn.svelte-owj5vn:active{background:var(--border)}.settings-icon-btn.svelte-owj5vn{width:40px;min-height:40px;background:none;border:1px solid var(--border);border-radius:0;color:var(--text-muted);font-size:1rem;cursor:pointer;touch-action:manipulation;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .1s,color .1s}.settings-icon-btn.svelte-owj5vn:hover{background:var(--surface-hover);color:var(--text)}.settings-icon-btn.svelte-owj5vn:active{background:var(--border)}@media(max-width:600px){.sidebar.svelte-owj5vn{position:fixed;top:0;left:0;height:100%;transform:translate(-100%);box-shadow:2px 0 12px #00000080;transition:transform .25s ease}.sidebar.open.svelte-owj5vn{transform:translate(0)}.collapse-btn.svelte-owj5vn{display:none}.icon-btn.svelte-owj5vn{display:block;font-size:1.4rem;padding:4px 8px}.resize-handle.svelte-owj5vn{display:none}}/**
|
|
1
|
+
.pin-gate.svelte-1qp96yb{display:flex;align-items:center;justify-content:center;height:100vh;background:var(--bg);padding:1rem}.pin-container.svelte-1qp96yb{display:flex;flex-direction:column;align-items:center;gap:1rem;width:100%;max-width:320px;text-align:center}.pin-container.svelte-1qp96yb h1:where(.svelte-1qp96yb){font-size:1.5rem;color:var(--text)}.pin-container.svelte-1qp96yb p:where(.svelte-1qp96yb){color:var(--text-muted);font-size:.95rem}input.svelte-1qp96yb{width:100%;padding:14px 16px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:1.2rem;text-align:center;outline:none;-webkit-appearance:none}input.svelte-1qp96yb:focus{border-color:var(--accent)}button.svelte-1qp96yb{width:100%;padding:14px;background:var(--accent);color:#fff;border:none;border-radius:8px;font-size:1rem;font-weight:600;cursor:pointer;touch-action:manipulation}button.svelte-1qp96yb:active{opacity:.8}.error.svelte-1qp96yb{color:var(--accent);font-size:.9rem}.context-menu-trigger.svelte-1nmhce7{background:none;border:none;color:var(--text-muted);font-size:1rem;font-weight:700;cursor:pointer;padding:0 6px;border-radius:4px;touch-action:manipulation;flex-shrink:0;line-height:1;letter-spacing:1px;min-height:24px;display:inline-flex;align-items:center;transition:color .15s,background .15s}.context-menu-trigger.svelte-1nmhce7:hover{color:var(--text);background:var(--border)}.context-menu-backdrop.svelte-1nmhce7{position:fixed;top:0;right:0;bottom:0;left:0;z-index:999}.context-menu.svelte-1nmhce7{position:fixed;list-style:none;margin:0;padding:4px 0;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 4px 16px #0006;z-index:1000;min-width:175px}.context-menu-item.svelte-1nmhce7{padding:9px 14px;font-size:.85rem;cursor:pointer;color:var(--text);white-space:nowrap}.context-menu-item.svelte-1nmhce7:hover{background:var(--border)}.context-menu-item.svelte-1nmhce7:focus{outline:2px solid var(--accent);outline-offset:-2px}.context-menu-item--danger.svelte-1nmhce7{color:#e74c3c}.context-menu-item--danger.svelte-1nmhce7:hover{background:#e74c3c1f}.context-menu-item--disabled.svelte-1nmhce7{opacity:.4;cursor:default}.context-menu-item--disabled.svelte-1nmhce7:hover{background:none}.workspace-item.svelte-168i8d5{display:flex;flex-direction:column}.workspace-header.svelte-168i8d5{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;cursor:pointer;min-height:44px;transition:background .12s}.workspace-header.svelte-168i8d5:hover{background:var(--surface-hover)}.workspace-item.active.svelte-168i8d5 .workspace-header:where(.svelte-168i8d5){background:var(--surface-hover)}.workspace-left.svelte-168i8d5{display:flex;align-items:center;gap:8px;min-width:0;flex:1}.workspace-header.reorder-mode.svelte-168i8d5{cursor:grab}.workspace-header.reorder-mode.svelte-168i8d5:active{cursor:grabbing}.collapse-chevron.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;font-size:.7rem;color:var(--text-muted);cursor:pointer;flex-shrink:0;transition:color .12s}.collapse-chevron.svelte-168i8d5:hover{color:var(--text)}.collapse-count.svelte-168i8d5{font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);background:var(--border);border-radius:8px;padding:1px 6px;flex-shrink:0}.initial-block.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:4px;font-size:var(--font-size-xs);font-weight:700;color:#000;font-family:var(--font-mono);flex-shrink:0;line-height:1}.workspace-name.svelte-168i8d5{font-size:var(--font-size-sm);font-weight:700;color:var(--text);font-family:var(--font-mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.workspace-actions.svelte-168i8d5{display:flex;align-items:center;gap:2px;opacity:0;transition:opacity .12s;flex-shrink:0}.workspace-header.svelte-168i8d5:hover .workspace-actions:where(.svelte-168i8d5){opacity:1}.action-btn.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:3px;font-size:var(--font-size-xs);color:var(--text-muted);cursor:pointer;font-family:var(--font-mono);transition:background .1s,color .1s}.action-btn.svelte-168i8d5:hover{background:var(--border);color:var(--text)}.session-list.svelte-168i8d5{list-style:none}.session-row.svelte-168i8d5{position:relative;display:flex;flex-direction:column;gap:2px;padding:6px 10px 6px 36px;cursor:pointer;min-height:44px;font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);transition:background .1s;border-left:3px solid transparent;justify-content:center}.session-row.svelte-168i8d5:hover{background:var(--surface-hover);color:var(--text)}.session-row.selected.svelte-168i8d5{border-left-color:var(--accent);background:var(--surface-hover);color:var(--text)}.session-row.attention.svelte-168i8d5 .session-name:where(.svelte-168i8d5){font-weight:700;color:var(--text)}.session-row-primary.svelte-168i8d5{display:flex;align-items:center;gap:8px;min-width:0}.session-row-secondary.svelte-168i8d5{display:flex;align-items:center;gap:6px;padding-left:15px;font-size:.65rem;color:var(--text-muted);min-width:0}.secondary-branch.svelte-168i8d5{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}.row-menu-overlay.svelte-168i8d5{position:absolute;right:8px;top:0;bottom:0;display:flex;align-items:center;opacity:0;transition:opacity .12s}.session-row.svelte-168i8d5:hover .row-menu-overlay:where(.svelte-168i8d5){opacity:1}.secondary-pr.svelte-168i8d5{white-space:nowrap;flex-shrink:0;color:var(--accent)}.secondary-time.svelte-168i8d5{white-space:nowrap;flex-shrink:0;opacity:.7}.session-count-badge.svelte-168i8d5{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;border-radius:8px;background:var(--border);color:var(--text-muted);font-size:.55rem;font-family:var(--font-mono);font-weight:600;padding:0 4px;flex-shrink:0}.diff-badge.svelte-168i8d5{display:inline-flex;align-items:center;gap:4px;font-size:.6rem;flex-shrink:0;margin-left:auto}.diff-add.svelte-168i8d5{color:var(--status-success)}.diff-del.svelte-168i8d5{color:var(--status-error)}.status-dot.svelte-168i8d5{display:inline-block;width:7px;height:7px;border-radius:50%;flex-shrink:0}.status-dot--running.svelte-168i8d5{background:var(--status-success)}.status-dot--idle.svelte-168i8d5{background:var(--status-info)}.dot-inactive.svelte-168i8d5{width:7px;height:7px;border-radius:50%;background:#555;flex-shrink:0}.session-row.inactive.svelte-168i8d5 .session-name:where(.svelte-168i8d5){color:var(--text-muted)}.session-row.inactive.svelte-168i8d5:hover .session-name:where(.svelte-168i8d5){color:var(--text)}.session-row.loading.svelte-168i8d5{pointer-events:none;opacity:.7}.session-row.loading.svelte-168i8d5 .session-name:where(.svelte-168i8d5){color:var(--accent)}.status-dot--attention.svelte-168i8d5{background:var(--status-warning);box-shadow:0 0 5px 1px #fbbf2473;animation:svelte-168i8d5-attention-glow 2s ease-in-out infinite}.status-dot--permission-prompt.svelte-168i8d5{background:#eab308;box-shadow:0 0 5px 1px #eab30873;animation:svelte-168i8d5-attention-glow 1.5s ease-in-out infinite}@keyframes svelte-168i8d5-attention-glow{0%,to{box-shadow:0 0 3px 1px #fbbf244d}50%{box-shadow:0 0 7px 2px #fbbf2499}}.terminal-icon.svelte-168i8d5{font-size:.6rem;font-weight:700;color:var(--text-muted);flex-shrink:0;font-family:var(--font-mono);line-height:1}.session-name.svelte-168i8d5{flex:1;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-width:0}.session-name.bold.svelte-168i8d5{font-weight:700}.add-worktree-row.svelte-168i8d5{padding:4px 10px 6px 36px}.add-worktree-btn.svelte-168i8d5{font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);opacity:.5;cursor:pointer;transition:opacity .1s,color .1s}.add-worktree-btn.svelte-168i8d5:hover{opacity:1;color:var(--text)}.add-worktree-row.disabled.svelte-168i8d5{pointer-events:none}.add-worktree-row.disabled.svelte-168i8d5 .add-worktree-btn:where(.svelte-168i8d5){opacity:.7;color:var(--accent)}.workspace-divider.svelte-168i8d5{height:1px;background:var(--border);margin:0}@media(max-width:600px){.workspace-header.svelte-168i8d5,.session-row.svelte-168i8d5{min-height:48px}.workspace-actions.svelte-168i8d5{opacity:1}.row-menu-overlay.svelte-168i8d5{display:none}}.smart-search.svelte-itm1qs{position:relative;flex-shrink:0}.input-row.svelte-itm1qs{display:flex;align-items:center;padding:6px 10px;border-bottom:1px solid var(--border);gap:6px}.prompt.svelte-itm1qs{font-family:var(--font-mono);font-size:var(--font-size-sm);color:var(--accent);flex-shrink:0;line-height:1;-webkit-user-select:none;user-select:none}.search-input.svelte-itm1qs{flex:1;background:transparent;border:none;outline:none;color:var(--text);font-family:var(--font-mono);font-size:var(--font-size-sm);caret-color:var(--accent)}.search-input.svelte-itm1qs::placeholder{color:var(--text-muted);opacity:.5}.dropdown.svelte-itm1qs{position:absolute;top:100%;left:0;right:0;background:var(--surface);border:1px solid var(--border);border-top:none;list-style:none;z-index:200;max-height:240px;overflow-y:auto}.dropdown-item.svelte-itm1qs{display:flex;align-items:baseline;gap:6px;padding:8px 10px;cursor:pointer;font-family:var(--font-mono);font-size:var(--font-size-sm);color:var(--text-muted);transition:background .1s;white-space:nowrap;overflow:hidden}.dropdown-item.svelte-itm1qs:hover,.dropdown-item.focused.svelte-itm1qs{background:var(--surface-hover);color:var(--text)}.dropdown-item.svelte-itm1qs strong:where(.svelte-itm1qs){color:var(--text);font-weight:700}.dropdown-path.svelte-itm1qs{font-size:var(--font-size-xs);color:var(--text-muted);opacity:.5;overflow:hidden;text-overflow:ellipsis;min-width:0;flex-shrink:1}@media(max-width:600px){.smart-search.svelte-itm1qs{width:100%}.input-row.svelte-itm1qs{width:100%;box-sizing:border-box}.search-input.svelte-itm1qs{width:100%;min-width:0}}.sidebar.svelte-owj5vn{position:relative;display:flex;flex-direction:column;background:var(--bg);border-right:1px solid var(--border);overflow:hidden;transition:transform .25s ease,width .2s ease,min-width .2s ease;z-index:100}.resize-handle.svelte-owj5vn{position:absolute;top:0;right:0;width:4px;height:100%;cursor:col-resize;z-index:10;transition:background .15s}.resize-handle.svelte-owj5vn:hover{background:var(--accent)}.sidebar-header.svelte-owj5vn{display:flex;align-items:center;justify-content:space-between;padding:12px 10px;border-bottom:1px solid var(--border);flex-shrink:0}.sidebar-label.svelte-owj5vn{flex:1;font-size:var(--font-size-xs);font-weight:600;color:var(--text-muted);font-family:var(--font-mono);text-transform:uppercase;letter-spacing:.08em}.collapse-btn.svelte-owj5vn{background:none;border:none;color:var(--text-muted);font-size:1.1rem;cursor:pointer;padding:8px 10px;border-radius:4px;flex-shrink:0;line-height:1;font-family:var(--font-mono);min-width:36px;min-height:36px;display:flex;align-items:center;justify-content:center}.collapse-btn.svelte-owj5vn:hover{color:var(--text);background:var(--border)}.sidebar.collapsed.svelte-owj5vn .sidebar-header:where(.svelte-owj5vn){justify-content:center;padding:12px 4px}.icon-btn.svelte-owj5vn{background:none;border:none;color:var(--text);font-size:1.2rem;cursor:pointer;padding:4px 6px;border-radius:4px;touch-action:manipulation;display:none}.icon-btn.svelte-owj5vn:active{background:var(--border)}.workspace-list.svelte-owj5vn{flex:1;overflow-y:auto;overflow-x:hidden}.empty-state.svelte-owj5vn{padding:16px 10px;font-size:var(--font-size-xs);font-family:var(--font-mono);color:var(--text-muted);opacity:.5;text-align:center}.sidebar-footer-row.svelte-owj5vn{display:flex;gap:8px;margin:8px;align-items:stretch;flex-shrink:0}.add-workspace-btn.svelte-owj5vn{flex:1;padding:10px 12px;min-height:40px;background:none;border:1px solid var(--accent);border-radius:0;color:var(--accent);font-size:var(--font-size-xs);font-family:var(--font-mono);cursor:pointer;touch-action:manipulation;text-align:center;transition:background .1s}.add-workspace-btn.svelte-owj5vn:hover{background:color-mix(in srgb,var(--accent) 12%,transparent)}.add-workspace-btn.svelte-owj5vn:active{background:var(--border)}.done-reorder-btn.svelte-owj5vn{margin:8px;padding:10px 12px;min-height:40px;background:none;border:1px solid var(--accent);border-radius:0;color:var(--accent);font-size:var(--font-size-xs);font-family:var(--font-mono);cursor:pointer;touch-action:manipulation;text-align:center;flex-shrink:0;transition:background .1s}.done-reorder-btn.svelte-owj5vn:hover{background:color-mix(in srgb,var(--accent) 12%,transparent)}.done-reorder-btn.svelte-owj5vn:active{background:var(--border)}.settings-icon-btn.svelte-owj5vn{width:40px;min-height:40px;background:none;border:1px solid var(--border);border-radius:0;color:var(--text-muted);font-size:1rem;cursor:pointer;touch-action:manipulation;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .1s,color .1s}.settings-icon-btn.svelte-owj5vn:hover{background:var(--surface-hover);color:var(--text)}.settings-icon-btn.svelte-owj5vn:active{background:var(--border)}@media(max-width:600px){.sidebar.svelte-owj5vn{position:fixed;top:0;left:0;height:100%;transform:translate(-100%);box-shadow:2px 0 12px #00000080;transition:transform .25s ease}.sidebar.open.svelte-owj5vn{transform:translate(0)}.collapse-btn.svelte-owj5vn{display:none}.icon-btn.svelte-owj5vn{display:block;font-size:1.4rem;padding:4px 8px}.resize-handle.svelte-owj5vn{display:none}}/**
|
|
2
2
|
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
3
3
|
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
4
4
|
* https://github.com/chjj/term.js
|
package/dist/frontend/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
12
12
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
13
13
|
<meta name="theme-color" content="#1a1a1a" />
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-BYXQcBQc.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CiwYPknn.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="app"></div>
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
let db = null;
|
|
6
|
+
let insertStmt = null;
|
|
7
|
+
const SCHEMA = `
|
|
8
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f', 'now')),
|
|
11
|
+
category TEXT NOT NULL, -- 'session', 'ui', 'agent', 'navigation', 'workspace'
|
|
12
|
+
action TEXT NOT NULL,
|
|
13
|
+
target TEXT,
|
|
14
|
+
properties TEXT,
|
|
15
|
+
session_id TEXT,
|
|
16
|
+
device TEXT
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_events_category_action ON events(category, action);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_events_target ON events(target);
|
|
21
|
+
`;
|
|
22
|
+
const INSERT_SQL = 'INSERT INTO events (category, action, target, properties, session_id, device) VALUES (?, ?, ?, ?, ?, ?)';
|
|
23
|
+
export function initAnalytics(configDir) {
|
|
24
|
+
if (db) {
|
|
25
|
+
db.close();
|
|
26
|
+
db = null;
|
|
27
|
+
insertStmt = null;
|
|
28
|
+
}
|
|
29
|
+
const dbPath = path.join(configDir, 'analytics.db');
|
|
30
|
+
db = new Database(dbPath);
|
|
31
|
+
db.pragma('journal_mode = WAL');
|
|
32
|
+
db.exec(SCHEMA);
|
|
33
|
+
insertStmt = db.prepare(INSERT_SQL);
|
|
34
|
+
}
|
|
35
|
+
export function closeAnalytics() {
|
|
36
|
+
if (db) {
|
|
37
|
+
db.close();
|
|
38
|
+
db = null;
|
|
39
|
+
insertStmt = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function runInsert(stmt, event) {
|
|
43
|
+
stmt.run(event.category, event.action, event.target ?? null, event.properties ? JSON.stringify(event.properties) : null, event.session_id ?? null, event.device ?? null);
|
|
44
|
+
}
|
|
45
|
+
export function trackEvent(event) {
|
|
46
|
+
if (!insertStmt)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
runInsert(insertStmt, event);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Analytics write failure is non-fatal
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function getDbPath(configDir) {
|
|
56
|
+
return path.join(configDir, 'analytics.db');
|
|
57
|
+
}
|
|
58
|
+
export function getDbSize(configDir) {
|
|
59
|
+
try {
|
|
60
|
+
return fs.statSync(getDbPath(configDir)).size;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function createAnalyticsRouter(configDir) {
|
|
67
|
+
const router = Router();
|
|
68
|
+
// POST /analytics/events — batch ingest from frontend
|
|
69
|
+
router.post('/events', (req, res) => {
|
|
70
|
+
const { events } = req.body;
|
|
71
|
+
if (!Array.isArray(events)) {
|
|
72
|
+
res.status(400).json({ error: 'events array required' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!db || !insertStmt) {
|
|
76
|
+
res.status(503).json({ error: 'Analytics not initialized' });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const stmt = insertStmt;
|
|
80
|
+
const insertMany = db.transaction((evts) => {
|
|
81
|
+
let inserted = 0;
|
|
82
|
+
for (const evt of evts) {
|
|
83
|
+
if (!evt.category || !evt.action)
|
|
84
|
+
continue;
|
|
85
|
+
runInsert(stmt, evt);
|
|
86
|
+
inserted++;
|
|
87
|
+
}
|
|
88
|
+
return inserted;
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
const inserted = insertMany(events);
|
|
92
|
+
res.json({ ok: true, count: inserted });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
res.status(500).json({ error: 'Failed to write events' });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// GET /analytics/size — DB file size in bytes
|
|
99
|
+
router.get('/size', (_req, res) => {
|
|
100
|
+
res.json({ bytes: getDbSize(configDir) });
|
|
101
|
+
});
|
|
102
|
+
// DELETE /analytics/events — truncate events table
|
|
103
|
+
router.delete('/events', (_req, res) => {
|
|
104
|
+
if (!db) {
|
|
105
|
+
res.status(503).json({ error: 'Analytics not initialized' });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
db.exec('DELETE FROM events');
|
|
110
|
+
try {
|
|
111
|
+
db.pragma('wal_checkpoint(TRUNCATE)');
|
|
112
|
+
}
|
|
113
|
+
catch { /* best-effort */ }
|
|
114
|
+
res.json({ ok: true });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
res.status(500).json({ error: 'Failed to clear analytics' });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return router;
|
|
121
|
+
}
|
package/dist/server/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { isInstalled as serviceIsInstalled } from './service.js';
|
|
|
18
18
|
import { extensionForMime, setClipboardImage } from './clipboard.js';
|
|
19
19
|
import { listBranches, isBranchStale } from './git.js';
|
|
20
20
|
import * as push from './push.js';
|
|
21
|
+
import { initAnalytics, closeAnalytics, createAnalyticsRouter } from './analytics.js';
|
|
21
22
|
import { createWorkspaceRouter } from './workspaces.js';
|
|
22
23
|
import { MOUNTAIN_NAMES } from './types.js';
|
|
23
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -167,6 +168,13 @@ async function main() {
|
|
|
167
168
|
if (process.env.CLAUDE_REMOTE_HOST)
|
|
168
169
|
config.host = process.env.CLAUDE_REMOTE_HOST;
|
|
169
170
|
push.ensureVapidKeys(config, CONFIG_PATH, saveConfig);
|
|
171
|
+
const configDir = path.dirname(CONFIG_PATH);
|
|
172
|
+
try {
|
|
173
|
+
initAnalytics(configDir);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
console.warn('Analytics disabled: failed to initialize:', err instanceof Error ? err.message : err);
|
|
177
|
+
}
|
|
170
178
|
if (!config.pinHash) {
|
|
171
179
|
const pin = await promptPin('Set up a PIN for claude-remote-cli:');
|
|
172
180
|
config.pinHash = await auth.hashPin(pin);
|
|
@@ -236,8 +244,9 @@ async function main() {
|
|
|
236
244
|
// Mount workspace router
|
|
237
245
|
const workspaceRouter = createWorkspaceRouter({ configPath: CONFIG_PATH });
|
|
238
246
|
app.use('/workspaces', requireAuth, workspaceRouter);
|
|
247
|
+
// Mount analytics router
|
|
248
|
+
app.use('/analytics', requireAuth, createAnalyticsRouter(configDir));
|
|
239
249
|
// Restore sessions from a previous update restart
|
|
240
|
-
const configDir = path.dirname(CONFIG_PATH);
|
|
241
250
|
const restoredCount = await restoreFromDisk(configDir);
|
|
242
251
|
if (restoredCount > 0) {
|
|
243
252
|
console.log(`Restored ${restoredCount} session(s) from previous update.`);
|
|
@@ -920,6 +929,7 @@ async function main() {
|
|
|
920
929
|
// tmux not installed or no sessions — ignore
|
|
921
930
|
}
|
|
922
931
|
function gracefulShutdown() {
|
|
932
|
+
closeAnalytics();
|
|
923
933
|
server.close();
|
|
924
934
|
// Serialize sessions to disk BEFORE killing them
|
|
925
935
|
const configDir = path.dirname(CONFIG_PATH);
|
package/dist/server/sessions.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promisify } from 'node:util';
|
|
|
6
6
|
import { AGENT_COMMANDS, AGENT_CONTINUE_ARGS, AGENT_YOLO_ARGS } from './types.js';
|
|
7
7
|
import { createPtySession } from './pty-handler.js';
|
|
8
8
|
import { getPrForBranch, getWorkingTreeDiff } from './git.js';
|
|
9
|
+
import { trackEvent } from './analytics.js';
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
const STALE_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
|
|
11
12
|
// In-memory registry: id -> Session
|
|
@@ -54,6 +55,18 @@ function create({ id: providedId, type, agent = 'claude', repoName, repoPath, cw
|
|
|
54
55
|
restored: paramRestored,
|
|
55
56
|
};
|
|
56
57
|
const { session: ptySession, result } = createPtySession(ptyParams, sessions, idleChangeCallbacks, stateChangeCallbacks);
|
|
58
|
+
trackEvent({
|
|
59
|
+
category: 'session',
|
|
60
|
+
action: 'created',
|
|
61
|
+
target: id,
|
|
62
|
+
properties: {
|
|
63
|
+
agent,
|
|
64
|
+
type: type ?? 'worktree',
|
|
65
|
+
workspace: root ?? repoPath,
|
|
66
|
+
mode: command ? 'terminal' : 'agent',
|
|
67
|
+
},
|
|
68
|
+
session_id: id,
|
|
69
|
+
});
|
|
57
70
|
if (paramNeedsBranchRename) {
|
|
58
71
|
ptySession.needsBranchRename = true;
|
|
59
72
|
}
|
|
@@ -112,6 +125,19 @@ function kill(id) {
|
|
|
112
125
|
if (session.tmuxSessionName) {
|
|
113
126
|
execFile('tmux', ['kill-session', '-t', session.tmuxSessionName], () => { });
|
|
114
127
|
}
|
|
128
|
+
const durationS = Math.round((Date.now() - new Date(session.createdAt).getTime()) / 1000);
|
|
129
|
+
trackEvent({
|
|
130
|
+
category: 'session',
|
|
131
|
+
action: 'ended',
|
|
132
|
+
target: id,
|
|
133
|
+
properties: {
|
|
134
|
+
agent: session.agent,
|
|
135
|
+
type: session.type,
|
|
136
|
+
workspace: session.root || session.repoPath,
|
|
137
|
+
duration_s: durationS,
|
|
138
|
+
},
|
|
139
|
+
session_id: id,
|
|
140
|
+
});
|
|
115
141
|
fireSessionEnd(id, session.repoPath, session.branchName);
|
|
116
142
|
sessions.delete(id);
|
|
117
143
|
}
|
|
@@ -5,6 +5,7 @@ import { execFile } from 'node:child_process';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { Router } from 'express';
|
|
7
7
|
import { loadConfig, saveConfig, getWorkspaceSettings, setWorkspaceSettings, deleteWorkspaceSettingKeys } from './config.js';
|
|
8
|
+
import { trackEvent } from './analytics.js';
|
|
8
9
|
import { listBranches, getActivityFeed, getCiStatus, getPrForBranch, getUnresolvedCommentCount, switchBranch, getCurrentBranch } from './git.js';
|
|
9
10
|
import { MOUNTAIN_NAMES } from './types.js';
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
@@ -135,6 +136,7 @@ export function createWorkspaceRouter(deps) {
|
|
|
135
136
|
};
|
|
136
137
|
}
|
|
137
138
|
saveConfig(configPath, config);
|
|
139
|
+
trackEvent({ category: 'workspace', action: 'added', target: resolved, properties: { name: path.basename(resolved) } });
|
|
138
140
|
const workspace = {
|
|
139
141
|
path: resolved,
|
|
140
142
|
name: path.basename(resolved),
|
|
@@ -163,6 +165,7 @@ export function createWorkspaceRouter(deps) {
|
|
|
163
165
|
}
|
|
164
166
|
config.workspaces = workspaces.filter((p) => p !== resolved);
|
|
165
167
|
saveConfig(configPath, config);
|
|
168
|
+
trackEvent({ category: 'workspace', action: 'removed', target: resolved });
|
|
166
169
|
res.json({ removed: resolved });
|
|
167
170
|
});
|
|
168
171
|
// -------------------------------------------------------------------------
|
package/dist/server/ws.js
CHANGED
|
@@ -4,6 +4,7 @@ import { promisify } from 'node:util';
|
|
|
4
4
|
import * as sessions from './sessions.js';
|
|
5
5
|
import { writeMeta } from './config.js';
|
|
6
6
|
import { branchToDisplayName } from './git.js';
|
|
7
|
+
import { trackEvent } from './analytics.js';
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
const BRANCH_POLL_INTERVAL_MS = 3000;
|
|
9
10
|
const BRANCH_POLL_MAX_ATTEMPTS = 10;
|
|
@@ -221,9 +222,15 @@ function setupWebSocket(server, authenticatedTokens, watcher, configPath) {
|
|
|
221
222
|
});
|
|
222
223
|
sessions.onIdleChange((sessionId, idle) => {
|
|
223
224
|
broadcastEvent('session-idle-changed', { sessionId, idle });
|
|
225
|
+
if (idle) {
|
|
226
|
+
trackEvent({ category: 'agent', action: 'idle', target: sessionId, session_id: sessionId });
|
|
227
|
+
}
|
|
224
228
|
});
|
|
225
229
|
sessions.onStateChange((sessionId, state) => {
|
|
226
230
|
broadcastEvent('session-state-changed', { sessionId, state });
|
|
231
|
+
if (state === 'waiting-for-input') {
|
|
232
|
+
trackEvent({ category: 'agent', action: 'waiting-for-input', target: sessionId, session_id: sessionId });
|
|
233
|
+
}
|
|
227
234
|
});
|
|
228
235
|
sessions.onSessionEnd((sessionId, repoPath, branchName) => {
|
|
229
236
|
broadcastEvent('session-ended', { sessionId, repoPath, branchName });
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { test, before, after, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
import { initAnalytics, closeAnalytics, trackEvent, getDbSize, getDbPath } from '../server/analytics.js';
|
|
8
|
+
let tmpDir;
|
|
9
|
+
before(() => {
|
|
10
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claude-remote-cli-analytics-test-'));
|
|
11
|
+
});
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
closeAnalytics();
|
|
14
|
+
// Clean up DB files between tests
|
|
15
|
+
for (const entry of fs.readdirSync(tmpDir)) {
|
|
16
|
+
fs.unlinkSync(path.join(tmpDir, entry));
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
after(() => {
|
|
20
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
21
|
+
});
|
|
22
|
+
test('initAnalytics creates database and schema', () => {
|
|
23
|
+
initAnalytics(tmpDir);
|
|
24
|
+
const dbPath = getDbPath(tmpDir);
|
|
25
|
+
assert.ok(fs.existsSync(dbPath), 'DB file should exist');
|
|
26
|
+
const db = new Database(dbPath, { readonly: true });
|
|
27
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
28
|
+
assert.ok(tables.some(t => t.name === 'events'), 'events table should exist');
|
|
29
|
+
db.close();
|
|
30
|
+
});
|
|
31
|
+
test('trackEvent inserts a row', () => {
|
|
32
|
+
initAnalytics(tmpDir);
|
|
33
|
+
trackEvent({
|
|
34
|
+
category: 'session',
|
|
35
|
+
action: 'created',
|
|
36
|
+
target: 'session-123',
|
|
37
|
+
properties: { workspace: '/proj', agent: 'claude' },
|
|
38
|
+
session_id: 'session-123',
|
|
39
|
+
device: 'desktop',
|
|
40
|
+
});
|
|
41
|
+
const db = new Database(getDbPath(tmpDir), { readonly: true });
|
|
42
|
+
const rows = db.prepare('SELECT * FROM events').all();
|
|
43
|
+
assert.equal(rows.length, 1);
|
|
44
|
+
assert.equal(rows[0].category, 'session');
|
|
45
|
+
assert.equal(rows[0].action, 'created');
|
|
46
|
+
assert.equal(rows[0].target, 'session-123');
|
|
47
|
+
assert.equal(rows[0].device, 'desktop');
|
|
48
|
+
const props = JSON.parse(rows[0].properties);
|
|
49
|
+
assert.equal(props.workspace, '/proj');
|
|
50
|
+
assert.equal(props.agent, 'claude');
|
|
51
|
+
db.close();
|
|
52
|
+
});
|
|
53
|
+
test('trackEvent handles optional fields as null', () => {
|
|
54
|
+
initAnalytics(tmpDir);
|
|
55
|
+
trackEvent({ category: 'ui', action: 'click' });
|
|
56
|
+
const db = new Database(getDbPath(tmpDir), { readonly: true });
|
|
57
|
+
const rows = db.prepare('SELECT * FROM events').all();
|
|
58
|
+
assert.equal(rows.length, 1);
|
|
59
|
+
assert.equal(rows[0].target, null);
|
|
60
|
+
assert.equal(rows[0].properties, null);
|
|
61
|
+
assert.equal(rows[0].session_id, null);
|
|
62
|
+
assert.equal(rows[0].device, null);
|
|
63
|
+
db.close();
|
|
64
|
+
});
|
|
65
|
+
test('trackEvent is no-op before initAnalytics', () => {
|
|
66
|
+
// Should not throw
|
|
67
|
+
trackEvent({ category: 'test', action: 'noop' });
|
|
68
|
+
});
|
|
69
|
+
test('getDbSize returns file size after writes', () => {
|
|
70
|
+
initAnalytics(tmpDir);
|
|
71
|
+
const sizeBefore = getDbSize(tmpDir);
|
|
72
|
+
assert.ok(sizeBefore > 0, 'DB file should have non-zero size after init');
|
|
73
|
+
for (let i = 0; i < 10; i++) {
|
|
74
|
+
trackEvent({ category: 'bulk', action: 'test', properties: { i } });
|
|
75
|
+
}
|
|
76
|
+
const sizeAfter = getDbSize(tmpDir);
|
|
77
|
+
assert.ok(sizeAfter >= sizeBefore, 'Size should grow after writes');
|
|
78
|
+
});
|
|
79
|
+
test('getDbSize returns 0 for non-existent path', () => {
|
|
80
|
+
assert.equal(getDbSize('/nonexistent/path'), 0);
|
|
81
|
+
});
|
|
82
|
+
test('initAnalytics is idempotent (schema already exists)', () => {
|
|
83
|
+
initAnalytics(tmpDir);
|
|
84
|
+
trackEvent({ category: 'test', action: 'first' });
|
|
85
|
+
closeAnalytics();
|
|
86
|
+
// Re-init should not throw or lose data
|
|
87
|
+
initAnalytics(tmpDir);
|
|
88
|
+
const db = new Database(getDbPath(tmpDir), { readonly: true });
|
|
89
|
+
const rows = db.prepare('SELECT * FROM events').all();
|
|
90
|
+
assert.equal(rows.length, 1);
|
|
91
|
+
db.close();
|
|
92
|
+
});
|
|
93
|
+
// ── Router endpoint tests ──────────────────────────────────────────────
|
|
94
|
+
// These test the Express Router in isolation (same pattern as fs-browse.test.ts)
|
|
95
|
+
import express from 'express';
|
|
96
|
+
import http from 'node:http';
|
|
97
|
+
import { createAnalyticsRouter } from '../server/analytics.js';
|
|
98
|
+
test('POST /analytics/events batch inserts events', async () => {
|
|
99
|
+
initAnalytics(tmpDir);
|
|
100
|
+
const app = express();
|
|
101
|
+
app.use(express.json());
|
|
102
|
+
app.use('/analytics', createAnalyticsRouter(tmpDir));
|
|
103
|
+
const server = http.createServer(app);
|
|
104
|
+
await new Promise(resolve => server.listen(0, resolve));
|
|
105
|
+
const port = server.address().port;
|
|
106
|
+
const res = await fetch(`http://localhost:${port}/analytics/events`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: { 'Content-Type': 'application/json' },
|
|
109
|
+
body: JSON.stringify({ events: [
|
|
110
|
+
{ category: 'ui', action: 'click', target: 'test-btn' },
|
|
111
|
+
{ category: 'session', action: 'created' },
|
|
112
|
+
] }),
|
|
113
|
+
});
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
assert.equal(data.ok, true);
|
|
116
|
+
assert.equal(data.count, 2);
|
|
117
|
+
const db = new Database(getDbPath(tmpDir), { readonly: true });
|
|
118
|
+
const rows = db.prepare('SELECT * FROM events').all();
|
|
119
|
+
assert.equal(rows.length, 2);
|
|
120
|
+
db.close();
|
|
121
|
+
server.close();
|
|
122
|
+
});
|
|
123
|
+
test('GET /analytics/size returns bytes', async () => {
|
|
124
|
+
initAnalytics(tmpDir);
|
|
125
|
+
const app = express();
|
|
126
|
+
app.use('/analytics', createAnalyticsRouter(tmpDir));
|
|
127
|
+
const server = http.createServer(app);
|
|
128
|
+
await new Promise(resolve => server.listen(0, resolve));
|
|
129
|
+
const port = server.address().port;
|
|
130
|
+
const res = await fetch(`http://localhost:${port}/analytics/size`);
|
|
131
|
+
const data = await res.json();
|
|
132
|
+
assert.ok(data.bytes > 0);
|
|
133
|
+
server.close();
|
|
134
|
+
});
|
|
135
|
+
test('DELETE /analytics/events clears all events', async () => {
|
|
136
|
+
initAnalytics(tmpDir);
|
|
137
|
+
trackEvent({ category: 'test', action: 'to-delete' });
|
|
138
|
+
const app = express();
|
|
139
|
+
app.use(express.json());
|
|
140
|
+
app.use('/analytics', createAnalyticsRouter(tmpDir));
|
|
141
|
+
const server = http.createServer(app);
|
|
142
|
+
await new Promise(resolve => server.listen(0, resolve));
|
|
143
|
+
const port = server.address().port;
|
|
144
|
+
const res = await fetch(`http://localhost:${port}/analytics/events`, { method: 'DELETE' });
|
|
145
|
+
const data = await res.json();
|
|
146
|
+
assert.equal(data.ok, true);
|
|
147
|
+
const db = new Database(getDbPath(tmpDir), { readonly: true });
|
|
148
|
+
const rows = db.prepare('SELECT * FROM events').all();
|
|
149
|
+
assert.equal(rows.length, 0);
|
|
150
|
+
db.close();
|
|
151
|
+
server.close();
|
|
152
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-remote-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"description": "Remote web interface for Claude Code CLI sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server/index.js",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"@xterm/addon-fit": "^0.11.0",
|
|
47
47
|
"@xterm/xterm": "^6.0.0",
|
|
48
48
|
"bcrypt": "^5.1.1",
|
|
49
|
+
"better-sqlite3": "^12.8.0",
|
|
49
50
|
"cookie-parser": "^1.4.7",
|
|
50
51
|
"express": "^4.21.0",
|
|
51
52
|
"node-pty": "^1.0.0",
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
"@playwright/test": "^1.58.2",
|
|
59
60
|
"@sveltejs/vite-plugin-svelte": "^5.1.1",
|
|
60
61
|
"@types/bcrypt": "^5.0.2",
|
|
62
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
61
63
|
"@types/cookie-parser": "^1.4.7",
|
|
62
64
|
"@types/express": "^4.17.21",
|
|
63
65
|
"@types/node": "^22.0.0",
|