Merge pull request #332 from PR0M3TH3AN/claude/review-project-improvements-nTZRz

Refactor dashboard script to external module and improve code quality
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-19 16:06:00 -05:00
commit d5e0dd26ef
40 files changed
+1167 -949

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
# TORCH Environment Variables
# Copy this file to .env and fill in the values for your deployment.
# ─── Dashboard ────────────────────────────────────────────────────────
# Basic Auth credentials for the dashboard (user:password format)
# TORCH_DASHBOARD_AUTH=admin:changeme
# ─── Configuration ────────────────────────────────────────────────────
# Path to torch-config.json (optional — overrides default lookup)
# TORCH_CONFIG_PATH=./torch-config.json
# ─── Agent Platform Detection ─────────────────────────────────────────
# Override automatic platform detection (jules | claude | codex | goose)
# AGENT_PLATFORM=
# AGENT_MODEL=
# Platform-specific API keys (used for automatic platform detection)
# CLAUDE_API_KEY=
# JULES_API_KEY=
# CODEX_API_KEY=
# GOOSE_API_KEY=
# ─── Nostr Lock Configuration ────────────────────────────────────────
# Comma-separated relay URLs (overrides config file)
# NOSTR_LOCK_RELAYS=wss://relay.damus.io,wss://nos.lol,wss://relay.primal.net
# Lock TTL in seconds (default: 7200)
# NOSTR_LOCK_TTL=7200
# Relay connection timeout in milliseconds (default: 8000)
# NOSTR_LOCK_TIMEOUT=8000
# Minimum relay quorum for successful publish (default: 1)
# NOSTR_LOCK_QUORUM=1
# ─── Memory Subsystem ────────────────────────────────────────────────
# Master toggle for the memory subsystem
# TORCH_MEMORY_ENABLED=false
# Individual memory feature flags
# TORCH_MEMORY_INGEST_ENABLED=false
# TORCH_MEMORY_RETRIEVAL_ENABLED=false
# TORCH_MEMORY_PRUNE_ENABLED=false
# TORCH_MEMORY_PRUNE_MODE=off
# ─── Telemetry (opt-in) ──────────────────────────────────────────────
# TELEMETRY=0
+17
View File
@@ -0,0 +1,17 @@
## Summary
<!-- 1-3 bullet points describing what this PR does and why -->
-
## Test plan
<!-- How was this tested? -->
- [ ] `npm run lint` passes
- [ ] `npm test` passes
- [ ] Manual verification (if applicable)
## Related issues
<!-- Link related issues: Fixes #123, Closes #456 -->
+12
View File
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 3
+9 -2
View File
@@ -9,14 +9,21 @@ on:
jobs:
build-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22, 23]
steps:
- uses: actions/checkout@v4
- name: Use Node.js 22
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: 22
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm run lint
- name: Check innerHTML usage
run: node scripts/check-innerhtml.mjs --report
- name: Security audit
run: npm audit --omit=dev --audit-level=high || true
- run: npm test
+61 -8
View File
@@ -16,11 +16,17 @@ Thank you for your interest in contributing to TORCH!
npm install
```
3. Copy the environment template (optional):
```bash
cp .env.example .env
```
## Development Workflow
### Building
This project requires a build step for certain artifacts (e.g., distribution files, dashboard assets).
This project requires a build step for distribution artifacts (dashboard, landing page, tarball).
```bash
npm run build
@@ -28,32 +34,79 @@ npm run build
### Testing
Run the test suite to ensure your changes don't break existing functionality.
Run the full test suite:
```bash
npm test
```
For faster feedback loop on unit tests (excluding integration):
For faster feedback on lock-backend unit tests:
```bash
npm run test:unit:lock-backend
```
For integration/resilience tests:
```bash
npm run test:extended-main
```
### Linting
Ensure your code follows the project's style guidelines.
Ensure your code follows the project's style guidelines:
```bash
npm run lint
```
ESLint is configured with `no-unused-vars` as an error. Prefix intentionally unused parameters with `_` (e.g., `_err`, `_unused`).
## Commit Conventions
This project uses [Conventional Commits](https://www.conventionalcommits.org/):
```
feat(scope): add new feature
fix(scope): fix a bug
chore(scope): maintenance task
docs(scope): documentation update
test(scope): add or fix tests
```
Common scopes: `dashboard`, `scheduler`, `lock-ops`, `memory`, `ci`, `tests`, `security`.
## Adding a New Agent Prompt
Agent prompts live in `src/prompts/daily/` and `src/prompts/weekly/`. To add a new agent:
1. Create a markdown file following the naming convention: `<agent-name>.md`
2. Add the agent to the roster in `src/prompts/roster.json`
3. Run `npm run validate:scheduler` to verify roster/prompt consistency
See existing prompts for the expected format.
## Architecture Overview
- **Core library:** `src/lib.mjs` — CLI command dispatch
- **Lock operations:** `src/lock-ops.mjs` — Nostr relay publish/query with health tracking
- **Dashboard:** `dashboard/` — Static web UI served by `src/dashboard.mjs`
- **Memory subsystem:** `src/services/memory/` — Event ingestion, retrieval, pruning
- **Scheduler:** `scripts/agent/run-scheduler-cycle.mjs` — Orchestrates agent runs
For detailed architecture docs, see `docs/lib-overview.md` and `docs/lock-ops-overview.md`.
## Test Integrity
When modifying tests, follow the test integrity protocol in `CLAUDE.md`. Tests are behavioral specifications — never weaken assertions to make them pass.
## Pull Requests
Please follow the repository conventions for Pull Requests.
> **Note:** Detailed agent policies and conventions are normally documented in `AGENTS.md`. If this file is missing, please refer to existing patterns in the codebase or open an issue for clarification.
- Keep PR titles short (under 70 characters)
- Include a summary of changes and test plan in the description
- Ensure `npm run lint` and `npm test` pass before submitting
- Reference related issues where applicable
## Reporting Issues
If you encounter any bugs or have feature requests, please open an issue on GitHub.
If you encounter bugs or have feature requests, please open an issue on GitHub.
+797
View File
@@ -0,0 +1,797 @@
/* global marked */
import { KIND_APP_DATA } from '../src/constants.mjs';
import { createElement } from './domUtils.js';
// -----------------------------------------------------------------------
// Modal Logic
// -----------------------------------------------------------------------
const docsModal = document.getElementById("docsModal");
const settingsModal = document.getElementById("settingsModal");
const openDocsBtn = document.getElementById("openDocs");
const openSettingsBtn = document.getElementById("settingsBtn");
const closeDocsBtn = document.getElementsByClassName("close-modal")[0];
const closeSettingsBtn = document.getElementById("closeSettings");
const contentDiv = document.getElementById("markdown-content");
let docsLoaded = false;
// Docs Modal
if (openDocsBtn) {
openDocsBtn.onclick = function(e) {
e.preventDefault();
docsModal.classList.add("show");
if (!docsLoaded) {
fetchDocs();
}
}
}
if (closeDocsBtn) {
closeDocsBtn.onclick = function() {
docsModal.classList.remove("show");
}
}
// Settings Modal
if (openSettingsBtn) {
openSettingsBtn.onclick = function(e) {
e.preventDefault();
// Pre-fill values
document.getElementById('settingHashtag').value = HASHTAG;
document.getElementById('settingNamespace').value = DASHBOARD_CONFIG.namespace;
document.getElementById('settingRelays').value = RELAYS.join(', ');
settingsModal.classList.add("show");
}
}
if (closeSettingsBtn) {
closeSettingsBtn.onclick = function() {
settingsModal.classList.remove("show");
}
}
document.getElementById('cancelSettings').onclick = function() {
settingsModal.classList.remove("show");
}
// Handle Settings Save
document.getElementById('settingsForm').onsubmit = function(e) {
e.preventDefault();
const newHashtag = document.getElementById('settingHashtag').value.trim();
const newNamespace = document.getElementById('settingNamespace').value.trim();
const newRelays = document.getElementById('settingRelays').value.split(',').map(r => r.trim()).filter(Boolean);
if (!newHashtag) {
alert('Hashtag is required.');
return;
}
const prefs = {
hashtag: newHashtag,
namespace: newNamespace,
relays: newRelays
};
// Save to LocalStorage
localStorage.setItem('torch_dashboard_prefs', JSON.stringify(prefs));
// Update URL to reflect new settings (so link is shareable)
const url = new URL(window.location);
url.searchParams.set('hashtag', newHashtag);
url.searchParams.set('namespace', newNamespace);
url.searchParams.set('relays', newRelays.join(','));
window.history.pushState({}, '', url);
// Reload to apply
window.location.reload();
}
window.onclick = function(event) {
if (event.target == docsModal) {
docsModal.classList.remove("show");
}
if (event.target == settingsModal) {
settingsModal.classList.remove("show");
}
}
async function fetchDocs() {
try {
const response = await fetch('../src/docs/TORCH.md');
if (!response.ok) {
throw new Error('Failed to load documentation');
}
const text = await response.text();
contentDiv.innerHTML = marked.parse(text);
docsLoaded = true;
} catch (error) {
console.error(error);
const p = document.createElement('p');
p.className = 'text-danger';
p.textContent = `Error loading documentation: ${error.message}`;
contentDiv.replaceChildren(p);
}
}
// -----------------------------------------------------------------------
// Configuration — matches bin/torch-lock.mjs defaults
// Optional URL params:
// ?namespace=torch&hashtag=torch-agent-lock&relays=wss://relay.damus.io,wss://nos.lol
// -----------------------------------------------------------------------
const DASHBOARD_DEFAULTS = {
namespace: 'torch',
hashtag: '',
defaultCadenceView: 'daily',
defaultStatusView: 'active',
relays: [
'wss://relay.damus.io',
'wss://nos.lol',
'wss://relay.primal.net',
],
};
function normalizeCadence(value) {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'daily' || normalized === 'weekly' || normalized === 'all'
? normalized
: DASHBOARD_DEFAULTS.defaultCadenceView;
}
function normalizeStatus(value) {
const normalized = String(value || '').trim().toLowerCase();
return ['active', 'completed', 'all'].includes(normalized)
? normalized
: DASHBOARD_DEFAULTS.defaultStatusView;
}
async function loadTorchConfigFile() {
const configCandidates = ['../torch-config.json', './torch-config.json'];
for (const path of configCandidates) {
try {
const response = await fetch(path, { cache: 'no-store' });
if (!response.ok) continue;
return await response.json();
} catch {
// optional config, ignore
}
}
return {};
}
function parseDashboardConfig(torchConfig = {}) {
const params = new URLSearchParams(window.location.search);
// 1. LocalStorage Preferences
let localPrefs = {};
try {
localPrefs = JSON.parse(localStorage.getItem('torch_dashboard_prefs')) || {};
} catch { /* ignore malformed localStorage */ }
const dashboardConfig = torchConfig.dashboard || {};
const lockConfig = torchConfig.nostrLock || {};
// Hierarchy: URL Param > LocalStorage > Config File > Default
// Namespace
const configNamespace = dashboardConfig.namespace || lockConfig.namespace || DASHBOARD_DEFAULTS.namespace;
const namespace = (
params.get('namespace') ||
localPrefs.namespace ||
configNamespace
).trim() || DASHBOARD_DEFAULTS.namespace;
// Hashtag
const configHashtag = dashboardConfig.hashtag || DASHBOARD_DEFAULTS.hashtag;
const hashtag = (
params.get('hashtag') ||
localPrefs.hashtag ||
configHashtag ||
`${namespace}-agent-lock`
).trim();
// Relays
const paramsRelays = params.get('relays') ? params.get('relays').split(',') : null;
const localRelays = localPrefs.relays;
const configRelays = dashboardConfig.relays || lockConfig.relays || DASHBOARD_DEFAULTS.relays;
let relays = (paramsRelays || localRelays || configRelays)
.map(r => r.trim())
.filter(Boolean);
if (relays.length === 0) relays = DASHBOARD_DEFAULTS.relays;
const defaultCadenceView = normalizeCadence(dashboardConfig.defaultCadenceView);
const defaultStatusView = normalizeStatus(dashboardConfig.defaultStatusView);
return {
namespace,
hashtag,
relays,
defaultCadenceView,
defaultStatusView,
};
}
let DASHBOARD_CONFIG = parseDashboardConfig({});
let RELAYS = DASHBOARD_CONFIG.relays;
const LOCK_EVENT_KIND = KIND_APP_DATA;
let HASHTAG = DASHBOARD_CONFIG.hashtag;
function applyConfigToHelpText() {
const byId = (id) => document.getElementById(id);
const hashtagDisplay = `#${HASHTAG}`;
const introHashtag = byId('introHashtag');
if (introHashtag) introHashtag.textContent = hashtagDisplay;
}
// -----------------------------------------------------------------------
// State
// -----------------------------------------------------------------------
/** @type {Map<string, object>} dTag -> latest parsed lock */
const lockStore = new Map();
/** @type {WebSocket[]} */
const sockets = [];
let connectedCount = 0;
let refreshIntervalId = null;
let isTeardown = false;
// DOM refs
const connectionStatus = document.getElementById('connectionStatus');
const lockGrid = document.getElementById('lockGrid');
const emptyState = document.getElementById('emptyState');
const rawLog = document.getElementById('rawLog');
const eventCount = document.getElementById('eventCount');
const summaryBar = document.getElementById('summaryBar');
const cadenceFilter = document.getElementById('cadenceFilter');
const statusFilter = document.getElementById('statusFilter');
const refreshBtn = document.getElementById('refreshBtn');
// Pagination DOM refs
const paginationControls = document.getElementById('paginationControls');
const mobilePrevBtn = document.getElementById('mobilePrevBtn');
const mobileNextBtn = document.getElementById('mobileNextBtn');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const pageStartEl = document.getElementById('pageStart');
const pageEndEl = document.getElementById('pageEnd');
const totalItemsEl = document.getElementById('totalItems');
// Pagination State
let currentPage = 1;
const itemsPerPage = 20;
// -----------------------------------------------------------------------
// Cleanup check
// -----------------------------------------------------------------------
function isAlive() {
if (isTeardown) return false;
// Check if our root element is still in the DOM
const stillInDom = document.body.contains(lockGrid);
if (!stillInDom) {
teardown();
return false;
}
return true;
}
function teardown() {
if (isTeardown) return;
isTeardown = true;
if (refreshIntervalId) {
clearInterval(refreshIntervalId);
refreshIntervalId = null;
}
for (const ws of sockets) {
try {
ws.close();
} catch {
// ignore
}
}
sockets.length = 0;
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
function nowUnix() {
return Math.floor(Date.now() / 1000);
}
function todayStr() {
return new Date().toISOString().slice(0, 10);
}
function fmtTime(unixSec) {
return new Date(unixSec * 1000).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
}
function fmtDuration(seconds) {
if (seconds < 0) return 'expired';
const m = Math.floor(seconds / 60);
if (m < 60) return `${m}m`;
const h = Math.floor(m / 60);
return `${h}h ${m % 60}m`;
}
function parseLockEvent(event) {
const dTag = (event.tags || []).find((t) => t[0] === 'd')?.[1] ?? '';
const expTag = (event.tags || []).find(
(t) => t[0] === 'expiration',
)?.[1];
const expiresAt = expTag ? parseInt(expTag, 10) : null;
let content = {};
try {
content = JSON.parse(event.content);
} catch {
// non-JSON content
}
return {
eventId: event.id,
pubkey: event.pubkey,
createdAt: event.created_at,
expiresAt,
dTag,
agent: content.agent ?? dTag.split('/')[2] ?? 'unknown',
cadence: content.cadence ?? dTag.split('/')[1] ?? 'unknown',
status: content.status ?? 'unknown',
date: content.date ?? dTag.split('/')[3] ?? todayStr(),
platform: content.platform ?? 'unknown',
model: content.model ?? null,
};
}
function isExpired(lock) {
return lock.expiresAt != null && lock.expiresAt <= nowUnix();
}
function shouldReplaceLock(existingLock, incomingLock) {
if (!existingLock) return true;
if (incomingLock.createdAt !== existingLock.createdAt) {
return incomingLock.createdAt > existingLock.createdAt;
}
// Deterministic tie-break: prefer lexicographically higher event id.
return (incomingLock.eventId || '') > (existingLock.eventId || '');
}
// -----------------------------------------------------------------------
// Platform styling
// -----------------------------------------------------------------------
const PLATFORM_COLORS = {
jules: { bg: 'bg-info/10', text: 'text-info', label: 'Jules' },
'claude-code': {
bg: 'bg-warning/10',
text: 'text-warning',
label: 'Claude Code',
},
codex: {
bg: 'bg-success/10',
text: 'text-success',
label: 'Codex',
},
unknown: { bg: 'bg-muted/10', text: 'text-muted', label: 'Unknown' },
};
function platformStyle(platform) {
const raw = String(platform || '').toLowerCase().trim();
if (raw === 'claude' || raw.includes('claude')) return PLATFORM_COLORS['claude-code'];
return PLATFORM_COLORS[raw] || PLATFORM_COLORS.unknown;
}
// -----------------------------------------------------------------------
// Rendering
// -----------------------------------------------------------------------
function getFilteredLocks() {
const cadence = cadenceFilter.value;
const statusMode = statusFilter.value;
return [...lockStore.values()]
.filter((l) => {
if (cadence !== 'all' && l.cadence !== cadence) return false;
if (statusMode === 'completed') {
return l.status === 'completed';
}
if (statusMode === 'active') {
// Active means not expired AND not completed
if (isExpired(l)) return false;
if (l.status === 'completed') return false;
return true;
}
// If 'all', show everything (expired, completed, started)
return true;
})
.sort((a, b) => b.createdAt - a.createdAt);
}
function renderSummary(locks) {
const active = locks.filter((l) => !isExpired(l));
const platforms = {};
for (const l of active) {
const label = platformStyle(l.platform).label;
platforms[label] = (platforms[label] || 0) + 1;
}
const cards = [
{
label: 'Active locks',
value: active.length,
color: 'text-accent-strong',
},
{
label: 'Total events',
value: locks.length,
color: 'text-text',
},
{
label: 'Platforms',
value: Object.keys(platforms).join(', ') || 'none',
color: 'text-info',
},
{
label: 'Date',
value: todayStr(),
color: 'text-muted',
},
];
summaryBar.replaceChildren();
for (const c of cards) {
const card = createElement('div', 'rounded-lg bg-surface-alt p-4');
const label = createElement('div', 'text-xs text-muted mb-1', c.label);
const value = createElement('div', `text-lg font-semibold ${c.color} mono`, String(c.value));
card.appendChild(label);
card.appendChild(value);
summaryBar.appendChild(card);
}
}
function createLockCard(lock, now) {
const expired = isExpired(lock);
const isCompleted = lock.status === 'completed';
const ps = platformStyle(lock.platform);
const age = now - lock.createdAt;
const remaining = lock.expiresAt ? lock.expiresAt - now : 0;
const ttlTotal = lock.expiresAt
? lock.expiresAt - lock.createdAt
: 7200;
const ttlPct = expired
? 0
: Math.max(0, Math.min(100, (remaining / ttlTotal) * 100));
let cardBg = 'bg-surface-alt';
let cardBorder = expired ? 'border-border/50 opacity-60' : 'border-border';
if (isCompleted) {
cardBg = 'bg-success/5';
cardBorder = 'border-success/30';
}
const card = createElement('div', `rounded-lg border ${cardBorder} ${cardBg} p-4 flex flex-col gap-3`);
// Header
const header = createElement('div', 'flex items-start justify-between');
const left = createElement('div', 'flex items-center gap-2');
if (!expired && !isCompleted) {
left.appendChild(createElement('span', 'pulse-dot bg-success mt-1 shrink-0'));
}
const meta = createElement('div');
meta.appendChild(createElement('div', 'font-medium text-text-strong text-sm', lock.agent));
meta.appendChild(createElement('div', 'text-xs text-muted', `${lock.cadence} \u00B7 ${lock.date}`));
left.appendChild(meta);
header.appendChild(left);
const right = createElement('div', 'flex flex-col items-end gap-1');
const badge = createElement('span', `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${ps.bg} ${ps.text}`, ps.label);
right.appendChild(badge);
if (lock.model && lock.model !== 'unknown') {
right.appendChild(createElement('span', 'text-xs text-muted mono opacity-75', lock.model));
}
header.appendChild(right);
card.appendChild(header);
// Grid
const grid = createElement('div', 'grid grid-cols-2 gap-x-4 gap-y-1 text-xs');
grid.appendChild(createElement('div', 'text-muted', 'Started'));
grid.appendChild(createElement('div', 'mono text-text', fmtTime(lock.createdAt)));
grid.appendChild(createElement('div', 'text-muted', 'Age'));
grid.appendChild(createElement('div', 'mono text-text', fmtDuration(age)));
grid.appendChild(createElement('div', 'text-muted', 'TTL remaining'));
grid.appendChild(createElement('div', `mono ${expired ? 'text-danger' : 'text-text'}`, fmtDuration(remaining)));
grid.appendChild(createElement('div', 'text-muted', 'Status'));
grid.appendChild(createElement('div', `mono ${expired ? 'text-danger' : 'text-success'}`, expired ? 'expired' : lock.status));
card.appendChild(grid);
// Progress Bar
const barContainer = createElement('div', 'w-full bg-border/30 rounded-full overflow-hidden');
const bar = createElement('div', `ttl-bar ${expired ? 'bg-danger' : 'bg-success'}`);
bar.style.width = `${ttlPct}%`;
barContainer.appendChild(bar);
card.appendChild(barContainer);
// Footer (Event ID)
const footer = createElement('div', 'text-xs text-muted mono truncate', lock.eventId ? lock.eventId.slice(0, 16) + '\u2026' : 'no id');
footer.title = lock.eventId || '';
card.appendChild(footer);
return card;
}
function renderLocks() {
if (!isAlive()) return;
const allLocks = getFilteredLocks();
const now = nowUnix();
eventCount.textContent = `${lockStore.size} events`;
renderSummary(allLocks);
const totalItems = allLocks.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
// Adjust currentPage if out of bounds
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
}
if (currentPage < 1) currentPage = 1;
if (totalItems === 0) {
lockGrid.replaceChildren(emptyState);
emptyState.textContent =
lockStore.size === 0
? 'Waiting for lock events from relays\u2026'
: 'No locks match the current filters.';
paginationControls.classList.add('hidden');
return;
}
// Slice for pagination
const startIdx = (currentPage - 1) * itemsPerPage;
const endIdx = Math.min(startIdx + itemsPerPage, totalItems);
const pageLocks = allLocks.slice(startIdx, endIdx);
const cards = pageLocks.map((lock) => createLockCard(lock, now));
lockGrid.replaceChildren(...cards);
// Update Pagination Controls
if (totalItems > itemsPerPage) {
paginationControls.classList.remove('hidden');
// Update text
pageStartEl.textContent = startIdx + 1;
pageEndEl.textContent = endIdx;
totalItemsEl.textContent = totalItems;
// Update buttons state
const isFirst = currentPage === 1;
const isLast = currentPage === totalPages;
prevBtn.disabled = isFirst;
nextBtn.disabled = isLast;
mobilePrevBtn.disabled = isFirst;
mobileNextBtn.disabled = isLast;
} else {
paginationControls.classList.add('hidden');
}
}
// -----------------------------------------------------------------------
// Raw log
// -----------------------------------------------------------------------
let rawLogInitialized = false;
function appendRawLog(event) {
if (!isAlive()) return;
if (!rawLogInitialized) {
rawLog.replaceChildren();
rawLogInitialized = true;
}
const ts = new Date(event.created_at * 1000).toISOString();
const text = `[${ts}] ${JSON.stringify(event).slice(0, 300)}`;
const line = createElement('div', 'mb-2 pb-2 border-b border-border/30', text);
rawLog.prepend(line);
// Keep log bounded
while (rawLog.children.length > 200) {
rawLog.removeChild(rawLog.lastChild);
}
}
// -----------------------------------------------------------------------
// WebSocket relay connections
// -----------------------------------------------------------------------
function updateConnectionBadge() {
if (!connectionStatus) return;
const dot = connectionStatus.querySelector('.pulse-dot');
if (connectedCount === RELAYS.length) {
connectionStatus.className =
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium bg-success/10 text-success';
dot.className = 'pulse-dot bg-success';
connectionStatus.lastChild.textContent = ` ${connectedCount}/${RELAYS.length} relays`;
} else if (connectedCount > 0) {
connectionStatus.className =
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium bg-warning/10 text-warning';
dot.className = 'pulse-dot bg-warning';
connectionStatus.lastChild.textContent = ` ${connectedCount}/${RELAYS.length} relays`;
} else {
connectionStatus.className =
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium bg-danger/10 text-danger';
dot.className = 'pulse-dot bg-danger';
connectionStatus.lastChild.textContent = ' Disconnected';
}
}
function connectToRelay(url) {
if (!isAlive()) return;
const ws = new WebSocket(url);
const subId = 'torch-dash-' + Math.random().toString(36).slice(2, 8);
ws.addEventListener('open', () => {
if (!isAlive()) return;
connectedCount++;
updateConnectionBadge();
// NIP-01 REQ: subscribe to TORCH lock events
// Request from 7 days ago to cover active locks created before midnight
// and recent history for weekly cadence
const since = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60;
const filter = {
kinds: [LOCK_EVENT_KIND],
'#t': [HASHTAG],
since: since,
};
ws.send(JSON.stringify(['REQ', subId, filter]));
});
ws.addEventListener('message', (msg) => {
if (!isAlive()) return;
let data;
try {
data = JSON.parse(msg.data);
} catch {
return;
}
// NIP-01: ["EVENT", subId, event]
if (data[0] === 'EVENT' && data[2]) {
const event = data[2];
const lock = parseLockEvent(event);
// De-duplicate by dTag: keep one lock per d-tag, preferring the
// most recent created_at; if equal, replace deterministically by event id.
const existing = lockStore.get(lock.dTag);
if (shouldReplaceLock(existing, lock)) {
lockStore.set(lock.dTag, lock);
}
appendRawLog(event);
renderLocks();
}
// NIP-01: ["EOSE", subId] — end of stored events
if (data[0] === 'EOSE') {
renderLocks();
}
});
ws.addEventListener('close', () => {
// Clean up socket from tracking array
const idx = sockets.indexOf(ws);
if (idx !== -1) {
sockets.splice(idx, 1);
}
if (isTeardown) return;
if (ws.isManualClose) return;
connectedCount = Math.max(0, connectedCount - 1);
updateConnectionBadge();
// Auto-reconnect after 5 seconds if still alive
setTimeout(() => {
if (isAlive()) {
connectToRelay(url);
}
}, 5000);
});
ws.addEventListener('error', () => {
// Error fires before close, so close handler will decrement
});
sockets.push(ws);
}
// -----------------------------------------------------------------------
// Periodic TTL refresh
// -----------------------------------------------------------------------
refreshIntervalId = setInterval(() => {
if (isAlive() && !document.hidden) {
renderLocks();
}
}, 30_000);
document.addEventListener('visibilitychange', () => {
if (!document.hidden && isAlive()) {
renderLocks();
}
});
// -----------------------------------------------------------------------
// Event handlers
// -----------------------------------------------------------------------
function setPage(p) {
currentPage = p;
renderLocks();
}
if (cadenceFilter) cadenceFilter.addEventListener('change', () => { currentPage = 1; renderLocks(); });
if (statusFilter) statusFilter.addEventListener('change', () => { currentPage = 1; renderLocks(); });
if (prevBtn) prevBtn.addEventListener('click', () => setPage(currentPage - 1));
if (nextBtn) nextBtn.addEventListener('click', () => setPage(currentPage + 1));
if (mobilePrevBtn) mobilePrevBtn.addEventListener('click', () => setPage(currentPage - 1));
if (mobileNextBtn) mobileNextBtn.addEventListener('click', () => setPage(currentPage + 1));
if (refreshBtn) refreshBtn.addEventListener('click', () => {
// Close existing connections and reconnect
for (const ws of sockets) {
try {
ws.isManualClose = true;
ws.close();
} catch {
// ignore
}
}
sockets.length = 0;
connectedCount = 0;
lockStore.clear();
updateConnectionBadge();
init();
});
// -----------------------------------------------------------------------
// Init
// -----------------------------------------------------------------------
function init() {
if (!isAlive()) return;
applyConfigToHelpText();
for (const url of RELAYS) {
connectToRelay(url);
}
}
async function bootstrap() {
const torchConfig = await loadTorchConfigFile();
DASHBOARD_CONFIG = parseDashboardConfig(torchConfig);
RELAYS = DASHBOARD_CONFIG.relays;
HASHTAG = DASHBOARD_CONFIG.hashtag;
cadenceFilter.value = DASHBOARD_CONFIG.defaultCadenceView;
statusFilter.value = DASHBOARD_CONFIG.defaultStatusView;
init();
}
bootstrap();
+1 -809
View File
@@ -230,814 +230,6 @@
</div>
</div>
<script type="module">
import { KIND_APP_DATA } from '../src/constants.mjs';
import { createElement, escapeHtml } from './domUtils.js';
// -----------------------------------------------------------------------
// Modal Logic
// -----------------------------------------------------------------------
const docsModal = document.getElementById("docsModal");
const settingsModal = document.getElementById("settingsModal");
const openDocsBtn = document.getElementById("openDocs");
const openSettingsBtn = document.getElementById("settingsBtn");
const closeDocsBtn = document.getElementsByClassName("close-modal")[0];
const closeSettingsBtn = document.getElementById("closeSettings");
const contentDiv = document.getElementById("markdown-content");
let docsLoaded = false;
// Docs Modal
if (openDocsBtn) {
openDocsBtn.onclick = function(e) {
e.preventDefault();
docsModal.classList.add("show");
if (!docsLoaded) {
fetchDocs();
}
}
}
if (closeDocsBtn) {
closeDocsBtn.onclick = function() {
docsModal.classList.remove("show");
}
}
// Settings Modal
if (openSettingsBtn) {
openSettingsBtn.onclick = function(e) {
e.preventDefault();
// Pre-fill values
document.getElementById('settingHashtag').value = HASHTAG;
document.getElementById('settingNamespace').value = DASHBOARD_CONFIG.namespace;
document.getElementById('settingRelays').value = RELAYS.join(', ');
settingsModal.classList.add("show");
}
}
if (closeSettingsBtn) {
closeSettingsBtn.onclick = function() {
settingsModal.classList.remove("show");
}
}
document.getElementById('cancelSettings').onclick = function() {
settingsModal.classList.remove("show");
}
// Handle Settings Save
document.getElementById('settingsForm').onsubmit = function(e) {
e.preventDefault();
const newHashtag = document.getElementById('settingHashtag').value.trim();
const newNamespace = document.getElementById('settingNamespace').value.trim();
const newRelays = document.getElementById('settingRelays').value.split(',').map(r => r.trim()).filter(Boolean);
if (!newHashtag) {
alert('Hashtag is required.');
return;
}
const prefs = {
hashtag: newHashtag,
namespace: newNamespace,
relays: newRelays
};
// Save to LocalStorage
localStorage.setItem('torch_dashboard_prefs', JSON.stringify(prefs));
// Update URL to reflect new settings (so link is shareable)
const url = new URL(window.location);
url.searchParams.set('hashtag', newHashtag);
url.searchParams.set('namespace', newNamespace);
url.searchParams.set('relays', newRelays.join(','));
window.history.pushState({}, '', url);
// Reload to apply
window.location.reload();
}
window.onclick = function(event) {
if (event.target == docsModal) {
docsModal.classList.remove("show");
}
if (event.target == settingsModal) {
settingsModal.classList.remove("show");
}
}
async function fetchDocs() {
try {
const response = await fetch('../src/docs/TORCH.md');
if (!response.ok) {
throw new Error('Failed to load documentation');
}
const text = await response.text();
contentDiv.innerHTML = marked.parse(text);
docsLoaded = true;
} catch (error) {
console.error(error);
contentDiv.innerHTML = `<p class="text-danger">Error loading documentation: ${error.message}</p>`;
}
}
// -----------------------------------------------------------------------
// Configuration — matches bin/torch-lock.mjs defaults
// Optional URL params:
// ?namespace=torch&hashtag=torch-agent-lock&relays=wss://relay.damus.io,wss://nos.lol
// -----------------------------------------------------------------------
const DASHBOARD_DEFAULTS = {
namespace: 'torch',
hashtag: '',
defaultCadenceView: 'daily',
defaultStatusView: 'active',
relays: [
'wss://relay.damus.io',
'wss://nos.lol',
'wss://relay.primal.net',
],
};
function normalizeCadence(value) {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'daily' || normalized === 'weekly' || normalized === 'all'
? normalized
: DASHBOARD_DEFAULTS.defaultCadenceView;
}
function normalizeStatus(value) {
const normalized = String(value || '').trim().toLowerCase();
return ['active', 'completed', 'all'].includes(normalized)
? normalized
: DASHBOARD_DEFAULTS.defaultStatusView;
}
async function loadTorchConfigFile() {
const configCandidates = ['../torch-config.json', './torch-config.json'];
for (const path of configCandidates) {
try {
const response = await fetch(path, { cache: 'no-store' });
if (!response.ok) continue;
return await response.json();
} catch {
// optional config, ignore
}
}
return {};
}
function parseDashboardConfig(torchConfig = {}) {
const params = new URLSearchParams(window.location.search);
// 1. LocalStorage Preferences
let localPrefs = {};
try {
localPrefs = JSON.parse(localStorage.getItem('torch_dashboard_prefs')) || {};
} catch {}
const dashboardConfig = torchConfig.dashboard || {};
const lockConfig = torchConfig.nostrLock || {};
// Hierarchy: URL Param > LocalStorage > Config File > Default
// Namespace
const configNamespace = dashboardConfig.namespace || lockConfig.namespace || DASHBOARD_DEFAULTS.namespace;
const namespace = (
params.get('namespace') ||
localPrefs.namespace ||
configNamespace
).trim() || DASHBOARD_DEFAULTS.namespace;
// Hashtag
const configHashtag = dashboardConfig.hashtag || DASHBOARD_DEFAULTS.hashtag;
const hashtag = (
params.get('hashtag') ||
localPrefs.hashtag ||
configHashtag ||
`${namespace}-agent-lock`
).trim();
// Relays
const paramsRelays = params.get('relays') ? params.get('relays').split(',') : null;
const localRelays = localPrefs.relays;
const configRelays = dashboardConfig.relays || lockConfig.relays || DASHBOARD_DEFAULTS.relays;
let relays = (paramsRelays || localRelays || configRelays)
.map(r => r.trim())
.filter(Boolean);
if (relays.length === 0) relays = DASHBOARD_DEFAULTS.relays;
const defaultCadenceView = normalizeCadence(dashboardConfig.defaultCadenceView);
const defaultStatusView = normalizeStatus(dashboardConfig.defaultStatusView);
return {
namespace,
hashtag,
relays,
defaultCadenceView,
defaultStatusView,
};
}
let DASHBOARD_CONFIG = parseDashboardConfig({});
let RELAYS = DASHBOARD_CONFIG.relays;
let D_TAG_PREFIX = `${DASHBOARD_CONFIG.namespace}-lock/`;
const LOCK_EVENT_KIND = KIND_APP_DATA;
let HASHTAG = DASHBOARD_CONFIG.hashtag;
function applyConfigToHelpText() {
const byId = (id) => document.getElementById(id);
const hashtagDisplay = `#${HASHTAG}`;
const filterExample = JSON.stringify({ kinds: [LOCK_EVENT_KIND], '#t': [HASHTAG] });
const introHashtag = byId('introHashtag');
if (introHashtag) introHashtag.textContent = hashtagDisplay;
// These elements were in the footer which is now removed
// byId('infoHashtag').textContent = hashtagDisplay;
// byId('infoHashtagRepeat').textContent = hashtagDisplay;
// byId('followHashtag').textContent = `#t = ${HASHTAG}`;
// byId('dTagPrefix').textContent = D_TAG_PREFIX;
// byId('filterExample').textContent = filterExample;
// byId('relayList').textContent = RELAYS.join(', ');
}
// -----------------------------------------------------------------------
// State
// -----------------------------------------------------------------------
/** @type {Map<string, object>} dTag -> latest parsed lock */
const lockStore = new Map();
/** @type {WebSocket[]} */
const sockets = [];
let connectedCount = 0;
let refreshIntervalId = null;
let isTeardown = false;
// DOM refs
const connectionStatus = document.getElementById('connectionStatus');
const lockGrid = document.getElementById('lockGrid');
const emptyState = document.getElementById('emptyState');
const rawLog = document.getElementById('rawLog');
const eventCount = document.getElementById('eventCount');
const summaryBar = document.getElementById('summaryBar');
const cadenceFilter = document.getElementById('cadenceFilter');
const statusFilter = document.getElementById('statusFilter');
const refreshBtn = document.getElementById('refreshBtn');
// Pagination DOM refs
const paginationControls = document.getElementById('paginationControls');
const mobilePrevBtn = document.getElementById('mobilePrevBtn');
const mobileNextBtn = document.getElementById('mobileNextBtn');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const pageStartEl = document.getElementById('pageStart');
const pageEndEl = document.getElementById('pageEnd');
const totalItemsEl = document.getElementById('totalItems');
// Pagination State
let currentPage = 1;
const itemsPerPage = 20;
// -----------------------------------------------------------------------
// Cleanup check
// -----------------------------------------------------------------------
function isAlive() {
if (isTeardown) return false;
// Check if our root element is still in the DOM
const stillInDom = document.body.contains(lockGrid);
if (!stillInDom) {
teardown();
return false;
}
return true;
}
function teardown() {
if (isTeardown) return;
isTeardown = true;
if (refreshIntervalId) {
clearInterval(refreshIntervalId);
refreshIntervalId = null;
}
for (const ws of sockets) {
try {
ws.close();
} catch {
// ignore
}
}
sockets.length = 0;
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
function nowUnix() {
return Math.floor(Date.now() / 1000);
}
function todayStr() {
return new Date().toISOString().slice(0, 10);
}
function fmtTime(unixSec) {
return new Date(unixSec * 1000).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
}
function fmtDuration(seconds) {
if (seconds < 0) return 'expired';
const m = Math.floor(seconds / 60);
if (m < 60) return `${m}m`;
const h = Math.floor(m / 60);
return `${h}h ${m % 60}m`;
}
function parseLockEvent(event) {
const dTag = (event.tags || []).find((t) => t[0] === 'd')?.[1] ?? '';
const expTag = (event.tags || []).find(
(t) => t[0] === 'expiration',
)?.[1];
const expiresAt = expTag ? parseInt(expTag, 10) : null;
let content = {};
try {
content = JSON.parse(event.content);
} catch {
// non-JSON content
}
return {
eventId: event.id,
pubkey: event.pubkey,
createdAt: event.created_at,
expiresAt,
dTag,
agent: content.agent ?? dTag.split('/')[2] ?? 'unknown',
cadence: content.cadence ?? dTag.split('/')[1] ?? 'unknown',
status: content.status ?? 'unknown',
date: content.date ?? dTag.split('/')[3] ?? todayStr(),
platform: content.platform ?? 'unknown',
model: content.model ?? null,
};
}
function isExpired(lock) {
return lock.expiresAt != null && lock.expiresAt <= nowUnix();
}
function shouldReplaceLock(existingLock, incomingLock) {
if (!existingLock) return true;
if (incomingLock.createdAt !== existingLock.createdAt) {
return incomingLock.createdAt > existingLock.createdAt;
}
// Deterministic tie-break: prefer lexicographically higher event id.
return (incomingLock.eventId || '') > (existingLock.eventId || '');
}
// -----------------------------------------------------------------------
// Platform styling
// -----------------------------------------------------------------------
const PLATFORM_COLORS = {
jules: { bg: 'bg-info/10', text: 'text-info', label: 'Jules' },
'claude-code': {
bg: 'bg-warning/10',
text: 'text-warning',
label: 'Claude Code',
},
codex: {
bg: 'bg-success/10',
text: 'text-success',
label: 'Codex',
},
unknown: { bg: 'bg-muted/10', text: 'text-muted', label: 'Unknown' },
};
function platformStyle(platform) {
const raw = String(platform || '').toLowerCase().trim();
if (raw === 'claude' || raw.includes('claude')) return PLATFORM_COLORS['claude-code'];
return PLATFORM_COLORS[raw] || PLATFORM_COLORS.unknown;
}
// -----------------------------------------------------------------------
// Rendering
// -----------------------------------------------------------------------
function getFilteredLocks() {
const cadence = cadenceFilter.value;
const statusMode = statusFilter.value;
const now = nowUnix();
return [...lockStore.values()]
.filter((l) => {
if (cadence !== 'all' && l.cadence !== cadence) return false;
if (statusMode === 'completed') {
return l.status === 'completed';
}
if (statusMode === 'active') {
// Active means not expired AND not completed
if (isExpired(l)) return false;
if (l.status === 'completed') return false;
return true;
}
// If 'all', show everything (expired, completed, started)
return true;
})
.sort((a, b) => b.createdAt - a.createdAt);
}
function renderSummary(locks) {
const now = nowUnix();
const active = locks.filter((l) => !isExpired(l));
const platforms = {};
for (const l of active) {
const label = platformStyle(l.platform).label;
platforms[label] = (platforms[label] || 0) + 1;
}
const cards = [
{
label: 'Active locks',
value: active.length,
color: 'text-accent-strong',
},
{
label: 'Total events',
value: locks.length,
color: 'text-text',
},
{
label: 'Platforms',
value: Object.keys(platforms).join(', ') || 'none',
color: 'text-info',
},
{
label: 'Date',
value: todayStr(),
color: 'text-muted',
},
];
summaryBar.replaceChildren();
for (const c of cards) {
const card = createElement('div', 'rounded-lg bg-surface-alt p-4');
const label = createElement('div', 'text-xs text-muted mb-1', c.label);
const value = createElement('div', `text-lg font-semibold ${c.color} mono`, String(c.value));
card.appendChild(label);
card.appendChild(value);
summaryBar.appendChild(card);
}
}
function createLockCard(lock, now) {
const expired = isExpired(lock);
const isCompleted = lock.status === 'completed';
const ps = platformStyle(lock.platform);
const age = now - lock.createdAt;
const remaining = lock.expiresAt ? lock.expiresAt - now : 0;
const ttlTotal = lock.expiresAt
? lock.expiresAt - lock.createdAt
: 7200;
const ttlPct = expired
? 0
: Math.max(0, Math.min(100, (remaining / ttlTotal) * 100));
let cardBg = 'bg-surface-alt';
let cardBorder = expired ? 'border-border/50 opacity-60' : 'border-border';
if (isCompleted) {
cardBg = 'bg-success/5';
cardBorder = 'border-success/30';
}
const card = createElement('div', `rounded-lg border ${cardBorder} ${cardBg} p-4 flex flex-col gap-3`);
// Header
const header = createElement('div', 'flex items-start justify-between');
const left = createElement('div', 'flex items-center gap-2');
if (!expired && !isCompleted) {
left.appendChild(createElement('span', 'pulse-dot bg-success mt-1 shrink-0'));
}
const meta = createElement('div');
meta.appendChild(createElement('div', 'font-medium text-text-strong text-sm', lock.agent));
meta.appendChild(createElement('div', 'text-xs text-muted', `${lock.cadence} \u00B7 ${lock.date}`));
left.appendChild(meta);
header.appendChild(left);
const right = createElement('div', 'flex flex-col items-end gap-1');
const badge = createElement('span', `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${ps.bg} ${ps.text}`, ps.label);
right.appendChild(badge);
if (lock.model && lock.model !== 'unknown') {
right.appendChild(createElement('span', 'text-xs text-muted mono opacity-75', lock.model));
}
header.appendChild(right);
card.appendChild(header);
// Grid
const grid = createElement('div', 'grid grid-cols-2 gap-x-4 gap-y-1 text-xs');
grid.appendChild(createElement('div', 'text-muted', 'Started'));
grid.appendChild(createElement('div', 'mono text-text', fmtTime(lock.createdAt)));
grid.appendChild(createElement('div', 'text-muted', 'Age'));
grid.appendChild(createElement('div', 'mono text-text', fmtDuration(age)));
grid.appendChild(createElement('div', 'text-muted', 'TTL remaining'));
grid.appendChild(createElement('div', `mono ${expired ? 'text-danger' : 'text-text'}`, fmtDuration(remaining)));
grid.appendChild(createElement('div', 'text-muted', 'Status'));
grid.appendChild(createElement('div', `mono ${expired ? 'text-danger' : 'text-success'}`, expired ? 'expired' : lock.status));
card.appendChild(grid);
// Progress Bar
const barContainer = createElement('div', 'w-full bg-border/30 rounded-full overflow-hidden');
const bar = createElement('div', `ttl-bar ${expired ? 'bg-danger' : 'bg-success'}`);
bar.style.width = `${ttlPct}%`;
barContainer.appendChild(bar);
card.appendChild(barContainer);
// Footer (Event ID)
const footer = createElement('div', 'text-xs text-muted mono truncate', lock.eventId ? lock.eventId.slice(0, 16) + '\u2026' : 'no id');
footer.title = lock.eventId || '';
card.appendChild(footer);
return card;
}
function renderLocks() {
if (!isAlive()) return;
const allLocks = getFilteredLocks();
const now = nowUnix();
eventCount.textContent = `${lockStore.size} events`;
renderSummary(allLocks);
const totalItems = allLocks.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
// Adjust currentPage if out of bounds
if (currentPage > totalPages && totalPages > 0) {
currentPage = totalPages;
}
if (currentPage < 1) currentPage = 1;
if (totalItems === 0) {
lockGrid.replaceChildren(emptyState);
emptyState.textContent =
lockStore.size === 0
? 'Waiting for lock events from relays\u2026'
: 'No locks match the current filters.';
paginationControls.classList.add('hidden');
return;
}
// Slice for pagination
const startIdx = (currentPage - 1) * itemsPerPage;
const endIdx = Math.min(startIdx + itemsPerPage, totalItems);
const pageLocks = allLocks.slice(startIdx, endIdx);
const cards = pageLocks.map((lock) => createLockCard(lock, now));
lockGrid.replaceChildren(...cards);
// Update Pagination Controls
if (totalItems > itemsPerPage) {
paginationControls.classList.remove('hidden');
// Update text
pageStartEl.textContent = startIdx + 1;
pageEndEl.textContent = endIdx;
totalItemsEl.textContent = totalItems;
// Update buttons state
const isFirst = currentPage === 1;
const isLast = currentPage === totalPages;
prevBtn.disabled = isFirst;
nextBtn.disabled = isLast;
mobilePrevBtn.disabled = isFirst;
mobileNextBtn.disabled = isLast;
} else {
paginationControls.classList.add('hidden');
}
}
// -----------------------------------------------------------------------
// Raw log
// -----------------------------------------------------------------------
let rawLogInitialized = false;
function appendRawLog(event) {
if (!isAlive()) return;
if (!rawLogInitialized) {
rawLog.replaceChildren();
rawLogInitialized = true;
}
const ts = new Date(event.created_at * 1000).toISOString();
const text = `[${ts}] ${JSON.stringify(event).slice(0, 300)}`;
const line = createElement('div', 'mb-2 pb-2 border-b border-border/30', text);
rawLog.prepend(line);
// Keep log bounded
while (rawLog.children.length > 200) {
rawLog.removeChild(rawLog.lastChild);
}
}
// -----------------------------------------------------------------------
// WebSocket relay connections
// -----------------------------------------------------------------------
function updateConnectionBadge() {
if (!connectionStatus) return;
const dot = connectionStatus.querySelector('.pulse-dot');
if (connectedCount === RELAYS.length) {
connectionStatus.className =
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium bg-success/10 text-success';
dot.className = 'pulse-dot bg-success';
connectionStatus.lastChild.textContent = ` ${connectedCount}/${RELAYS.length} relays`;
} else if (connectedCount > 0) {
connectionStatus.className =
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium bg-warning/10 text-warning';
dot.className = 'pulse-dot bg-warning';
connectionStatus.lastChild.textContent = ` ${connectedCount}/${RELAYS.length} relays`;
} else {
connectionStatus.className =
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium bg-danger/10 text-danger';
dot.className = 'pulse-dot bg-danger';
connectionStatus.lastChild.textContent = ' Disconnected';
}
}
function connectToRelay(url) {
if (!isAlive()) return;
const ws = new WebSocket(url);
const subId = 'torch-dash-' + Math.random().toString(36).slice(2, 8);
ws.addEventListener('open', () => {
if (!isAlive()) return;
connectedCount++;
updateConnectionBadge();
// NIP-01 REQ: subscribe to TORCH lock events
// Request from 7 days ago to cover active locks created before midnight
// and recent history for weekly cadence
const since = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60;
const filter = {
kinds: [LOCK_EVENT_KIND],
'#t': [HASHTAG],
since: since,
};
ws.send(JSON.stringify(['REQ', subId, filter]));
});
ws.addEventListener('message', (msg) => {
if (!isAlive()) return;
let data;
try {
data = JSON.parse(msg.data);
} catch {
return;
}
// NIP-01: ["EVENT", subId, event]
if (data[0] === 'EVENT' && data[2]) {
const event = data[2];
const lock = parseLockEvent(event);
// De-duplicate by dTag: keep one lock per d-tag, preferring the
// most recent created_at; if equal, replace deterministically by event id.
const existing = lockStore.get(lock.dTag);
if (shouldReplaceLock(existing, lock)) {
lockStore.set(lock.dTag, lock);
}
appendRawLog(event);
renderLocks();
}
// NIP-01: ["EOSE", subId] — end of stored events
if (data[0] === 'EOSE') {
renderLocks();
}
});
ws.addEventListener('close', () => {
// Clean up socket from tracking array
const idx = sockets.indexOf(ws);
if (idx !== -1) {
sockets.splice(idx, 1);
}
if (isTeardown) return;
if (ws.isManualClose) return;
connectedCount = Math.max(0, connectedCount - 1);
updateConnectionBadge();
// Auto-reconnect after 5 seconds if still alive
setTimeout(() => {
if (isAlive()) {
connectToRelay(url);
}
}, 5000);
});
ws.addEventListener('error', () => {
// Error fires before close, so close handler will decrement
});
sockets.push(ws);
}
// -----------------------------------------------------------------------
// Periodic TTL refresh
// -----------------------------------------------------------------------
refreshIntervalId = setInterval(() => {
if (isAlive() && !document.hidden) {
renderLocks();
}
}, 30_000);
document.addEventListener('visibilitychange', () => {
if (!document.hidden && isAlive()) {
renderLocks();
}
});
// -----------------------------------------------------------------------
// Event handlers
// -----------------------------------------------------------------------
function setPage(p) {
currentPage = p;
renderLocks();
}
if (cadenceFilter) cadenceFilter.addEventListener('change', () => { currentPage = 1; renderLocks(); });
if (statusFilter) statusFilter.addEventListener('change', () => { currentPage = 1; renderLocks(); });
if (prevBtn) prevBtn.addEventListener('click', () => setPage(currentPage - 1));
if (nextBtn) nextBtn.addEventListener('click', () => setPage(currentPage + 1));
if (mobilePrevBtn) mobilePrevBtn.addEventListener('click', () => setPage(currentPage - 1));
if (mobileNextBtn) mobileNextBtn.addEventListener('click', () => setPage(currentPage + 1));
if (refreshBtn) refreshBtn.addEventListener('click', () => {
// Close existing connections and reconnect
for (const ws of sockets) {
try {
ws.isManualClose = true;
ws.close();
} catch {
// ignore
}
}
sockets.length = 0;
connectedCount = 0;
lockStore.clear();
updateConnectionBadge();
init();
});
// -----------------------------------------------------------------------
// Init
// -----------------------------------------------------------------------
function init() {
if (!isAlive()) return;
applyConfigToHelpText();
for (const url of RELAYS) {
connectToRelay(url);
}
}
async function bootstrap() {
const torchConfig = await loadTorchConfigFile();
DASHBOARD_CONFIG = parseDashboardConfig(torchConfig);
RELAYS = DASHBOARD_CONFIG.relays;
D_TAG_PREFIX = `${DASHBOARD_CONFIG.namespace}-lock/`;
HASHTAG = DASHBOARD_CONFIG.hashtag;
cadenceFilter.value = DASHBOARD_CONFIG.defaultCadenceView;
statusFilter.value = DASHBOARD_CONFIG.defaultStatusView;
init();
}
bootstrap();
</script>
<script type="module" src="./app.js"></script>
</body>
</html>
+157
View File
@@ -0,0 +1,157 @@
# TORCH System Architecture
## High-Level Overview
```mermaid
graph TB
subgraph Agents["AI Agent Platforms"]
Jules["Jules"]
Claude["Claude Code"]
Codex["Codex"]
Goose["Goose"]
end
subgraph TORCH["TORCH CLI / Scheduler"]
CLI["bin/torch-lock.mjs"]
Lib["src/lib.mjs<br/>Command Dispatch"]
LockOps["src/lock-ops.mjs<br/>Lock Publisher / Query"]
Health["src/relay-health.mjs<br/>Relay Health Monitor"]
Scheduler["scripts/agent/<br/>run-scheduler-cycle.mjs"]
end
subgraph Nostr["Nostr Network"]
R1["Relay 1<br/>relay.damus.io"]
R2["Relay 2<br/>nos.lol"]
R3["Relay 3<br/>relay.primal.net"]
end
subgraph Dashboard["Dashboard"]
Server["src/dashboard.mjs<br/>HTTP Server"]
UI["dashboard/index.html<br/>+ app.js"]
end
subgraph Memory["Memory Subsystem"]
Ingestor["Ingestor"]
Retriever["Retriever"]
Pruner["Pruner"]
Store["memory-store.json"]
end
Agents -->|"torch-lock lock/check/complete"| CLI
CLI --> Lib
Lib --> LockOps
LockOps -->|"NIP-01 Events<br/>(kind 30078)"| R1 & R2 & R3
Health -->|"Probe & Track"| R1 & R2 & R3
Scheduler -->|"Orchestrate"| CLI
Server -->|"Serve Static"| UI
UI -->|"WebSocket SUB"| R1 & R2 & R3
Lib --> Ingestor
Lib --> Retriever
Ingestor --> Store
Retriever --> Store
Pruner --> Store
```
## Lock Lifecycle
```mermaid
sequenceDiagram
participant Agent
participant TORCH as TORCH CLI
participant Relays as Nostr Relays
Agent->>TORCH: torch-lock check --cadence daily
TORCH->>Relays: Query active locks (REQ filter)
Relays-->>TORCH: Existing lock events
TORCH-->>Agent: Status: available / locked
Agent->>TORCH: torch-lock lock --agent my-agent
TORCH->>TORCH: Generate NIP-01 event (kind 30078)
TORCH->>Relays: Publish lock event (fanout)
Relays-->>TORCH: ACK from N relays
TORCH->>Relays: Race-check query (verify no conflicts)
Relays-->>TORCH: Confirm lock acquired
TORCH-->>Agent: Lock acquired (exit 0)
Note over Agent: Agent performs work...
Agent->>TORCH: torch-lock complete --agent my-agent
TORCH->>Relays: Publish completion event
TORCH-->>Agent: Completed (exit 0)
```
## Scheduler Flow
```mermaid
flowchart TD
Start["Scheduler Cycle Start"] --> LoadRoster["Load Roster<br/>(roster.json)"]
LoadRoster --> Filter["Filter by cadence<br/>(daily/weekly)"]
Filter --> CheckPaused{"Agent<br/>paused?"}
CheckPaused -->|Yes| Skip["Skip agent"]
CheckPaused -->|No| CheckCompleted{"Already<br/>completed<br/>today?"}
CheckCompleted -->|Yes| Skip
CheckCompleted -->|No| CheckLock{"Lock<br/>available?"}
CheckLock -->|No| Skip
CheckLock -->|Yes| Acquire["Acquire Lock"]
Acquire --> RunAgent["Execute Agent Prompt"]
RunAgent --> Complete["Publish Completion"]
Complete --> LogResult["Write Task Log"]
Skip --> LogResult
LogResult --> Next{"More<br/>agents?"}
Next -->|Yes| CheckPaused
Next -->|No| End["Cycle Complete"]
```
## Memory Subsystem
```mermaid
flowchart LR
Events["Runtime Events"] --> Ingestor
Ingestor --> |"Chunk + Summarize"| Store["Memory Store<br/>(JSON file)"]
Store --> Retriever
Retriever --> |"Rank: semantic +<br/>importance + recency"| Results["Ranked Memories"]
Scheduler["Maintenance<br/>Scheduler"] --> Pruner
Pruner --> |"TTL / merge /<br/>archive / delete"| Store
subgraph Jobs["Scheduled Jobs"]
J1["Ingest (1-5 min)"]
J2["Consolidate (1 hr)"]
J3["Prune (daily)"]
J4["Deep Merge (weekly)"]
end
Scheduler --> Jobs
```
## Key Data Flows
| Flow | Protocol | Event Kind | Key Fields |
|------|----------|-----------|------------|
| Lock acquire | NIP-01 publish | 30078 | d-tag, expiration, agent, cadence, status |
| Lock query | NIP-01 REQ | 30078 | #t filter on hashtag |
| Lock complete | NIP-01 publish | 30078 | status: "completed" |
| Dashboard feed | NIP-01 WebSocket | 30078 | Real-time SUB with since filter |
## Directory Structure
```
TORCH/
├── bin/ # CLI entry point
├── src/
│ ├── lib.mjs # Command dispatch
│ ├── lock-ops.mjs # Nostr relay operations
│ ├── relay-health.mjs # Relay health monitoring
│ ├── dashboard.mjs # Dashboard HTTP server
│ ├── ops.mjs # Install/update operations
│ ├── services/
│ │ ├── memory/ # Memory subsystem
│ │ └── governance/ # Governance service
│ └── prompts/ # Agent prompt definitions
├── dashboard/ # Dashboard frontend
├── scripts/agent/ # Scheduler and agent scripts
├── test/ # Test suite
├── docs/ # Architecture documentation
└── migrations/ # Database schemas (future)
```
+1 -1
View File
@@ -16,7 +16,7 @@ export default [
sourceType: "module",
},
rules: {
"no-unused-vars": "warn",
"no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }],
"no-console": "off",
},
},
-64
View File
@@ -17,7 +17,6 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.58.2",
"eslint": "^10.0.0",
"globals": "^17.3.0"
},
@@ -260,22 +259,6 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@playwright/test": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
@@ -691,21 +674,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -988,38 +956,6 @@
"node": ">=8"
}
},
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+1 -2
View File
@@ -48,7 +48,7 @@
"LICENSE"
],
"scripts": {
"test": "npm run validate:scheduler && npm run test:integration:e2e && node --test test/*.test.mjs test/*.test.js",
"test": "npm run validate:scheduler && npm run test:integration:e2e && node --test --test-timeout=30000 test/*.test.mjs test/*.test.js",
"test:unit:lock-backend": "node --test test/lock-ops.test.mjs test/lock-ops-publish-retry.test.mjs",
"lock:check:daily": "node bin/torch-lock.mjs check --cadence daily",
"lock:check:weekly": "node bin/torch-lock.mjs check --cadence weekly",
@@ -76,7 +76,6 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.58.2",
"eslint": "^10.0.0",
"globals": "^17.3.0"
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { createLifecyclePlan } from '../src/services/memory/pruner.js';
import crypto from 'node:crypto';
const COUNT = 30000;
const DUPLICATE_WINDOW_MS = 1000 * 60 * 60; // 1 hour
+1 -4
View File
@@ -16,11 +16,8 @@ const getArg = (key) => {
if (flagIdx !== -1 && ARGS[flagIdx + 1] && !ARGS[flagIdx + 1].startsWith('--')) return ARGS[flagIdx + 1];
return null;
};
const hasArg = (key) => ARGS.includes(`--${key}`);
const RELAYS = (getArg('relays') || process.env.RELAY_URLS || '').split(',').filter(Boolean);
const SERVE_CMD = getArg('serve') || 'npx';
const CONFIRM_PUBLIC = hasArg('confirm-public');
const OUT_DIR = getArg('out') || 'artifacts';
// --- Logging ---
@@ -45,7 +42,7 @@ async function waitForServer(url, timeoutMs = 10000) {
try {
const res = await fetch(url);
if (res.ok) return true;
} catch (e) {
} catch (_e) {
// ignore
}
await sleep(500);
+3 -3
View File
@@ -22,7 +22,7 @@ async function waitForServer() {
req.end();
});
return;
} catch (e) {
} catch (_e) {
await sleep(100);
}
}
@@ -59,8 +59,8 @@ async function benchmark() {
if (current >= REQUESTS) break;
try {
await makeRequest();
} catch (e) {
// console.error(e);
} catch (_e) {
// console.error(_e);
}
}
};
+1 -1
View File
@@ -96,7 +96,7 @@ async function runBenchmark() {
duplicateWindowMs: 1000 * 60 * 60, // 1 hour
similarityThreshold: 0.99,
getEmbedding: getEmbeddingWrapper,
generateSummary: async (prompt) => {
generateSummary: async (_prompt) => {
await delay(LATENCY_MS);
return JSON.stringify({
summary: 'Condensed summary based on prompt.',
+1 -1
View File
@@ -31,7 +31,7 @@ async function runTests(i) {
child.stdout.on('data', (d) => stdout += d.toString());
child.on('close', (code) => {
child.on('close', (_code) => {
const lines = stdout.split('\n');
for (const line of lines) {
if (line.startsWith('not ok ') && !line.includes('# skip') && !line.includes('# todo')) {
+1 -1
View File
@@ -12,7 +12,7 @@ function getFiles(dir, files = []) {
files.push(name);
}
}
} catch (e) {
} catch (_e) {
// ignore if dir doesn't exist
}
return files;
+1 -2
View File
@@ -1,7 +1,6 @@
import { createProposal, listProposals, applyProposal, rejectProposal, getProposal } from './services/governance/index.js';
import { ExitError } from './errors.mjs';
import fs from 'node:fs/promises';
import path from 'node:path';
export async function cmdProposal(subcommand, args = {}) {
switch (subcommand) {
@@ -30,7 +29,7 @@ async function handleCreate({ agent, target, contentFile, reason }) {
let newContent;
try {
newContent = await fs.readFile(contentFile, 'utf8');
} catch (e) {
} catch (_e) {
console.error(`Failed to read content file: ${contentFile}`);
throw new ExitError(1, 'File read error');
}
+10 -3
View File
@@ -8,7 +8,7 @@ import { DEFAULT_DASHBOARD_PORT } from './constants.mjs';
import { getDashboardAuth, parseTorchConfig } from './torch-config.mjs';
const SECURITY_HEADERS = {
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss:; object-src 'none'; base-uri 'self';",
'Content-Security-Policy': "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss:; object-src 'none'; base-uri 'self';",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin'
@@ -18,8 +18,15 @@ function timingSafeCompare(a, b) {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
if (bufA.length !== bufB.length) {
// Still do a comparison to avoid some timing leaks, though length leak is hard to avoid entirely
crypto.timingSafeEqual(bufA, bufA);
// Pad both buffers to equal length so the comparison takes constant time
// regardless of input lengths. The length difference is still observable,
// but the content timing channel is closed.
const maxLen = Math.max(bufA.length, bufB.length);
const paddedA = Buffer.alloc(maxLen);
const paddedB = Buffer.alloc(maxLen);
bufA.copy(paddedA);
bufB.copy(paddedB);
crypto.timingSafeEqual(paddedA, paddedB);
return false;
}
return crypto.timingSafeEqual(bufA, bufB);
+1 -11
View File
@@ -9,19 +9,13 @@ import {
} from './torch-config.mjs';
import {
KIND_APP_DATA,
DEFAULT_RETRY_ATTEMPTS,
DEFAULT_RETRY_BASE_DELAY_MS,
DEFAULT_RETRY_CAP_DELAY_MS,
DEFAULT_ROLLING_WINDOW_SIZE,
DEFAULT_FAILURE_THRESHOLD,
DEFAULT_QUARANTINE_COOLDOWN_MS,
DEFAULT_MAX_QUARANTINE_COOLDOWN_MS,
DEFAULT_SNAPSHOT_INTERVAL_MS,
} from './constants.mjs';
function nowUnix() {
return Math.floor(Date.now() / 1000);
}
import { nowUnix } from './utils.mjs';
/**
* Parses a raw Nostr event into a structured lock object.
@@ -630,10 +624,6 @@ export class LockPublisher {
async publish() {
const {
poolFactory = () => new SimplePool(),
getPublishTimeoutMsFn = getPublishTimeoutMs,
getMinSuccessfulRelayPublishesFn = getMinSuccessfulRelayPublishes,
getRelayFallbacksFn = getRelayFallbacks,
getMinActiveRelayPoolFn = getMinActiveRelayPool,
retryAttempts = 4,
retryBaseDelayMs = 500,
retryCapDelayMs = 8_000,
+1 -1
View File
@@ -26,7 +26,7 @@ export async function getCompletedAgents(cadence, logDir, deps) {
const match = file.match(/^(\d{4}-\d{2}-\d{2})T.*__([a-zA-Z0-9-_]+)__(.*)\.md$/);
if (!match) continue;
const [_, datePart, agent, status] = match;
const [, datePart, agent, status] = match;
if (status === 'completed') {
if (cadence === 'daily' && datePart === today) {
+1 -1
View File
@@ -381,7 +381,7 @@ export function cmdUpdate(force = false, cwd = process.cwd()) {
if (pkg.name === 'torch-lock') {
installDirName = '.';
}
} catch (e) {
} catch (_e) {
// Ignore error if package.json is missing or invalid
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ async function run() {
const res = await loadTorchConfig();
// Use res to prevent dead code elimination if JIT is smart (though side effects exist)
if (!res) throw new Error("Failed");
} catch (e) {
} catch (_e) {
// ignore
}
}
+4 -4
View File
@@ -30,7 +30,7 @@ export async function listProposals() {
id: entry.name,
...meta
});
} catch (e) {
} catch (_e) {
// Ignore invalid proposals
}
}
@@ -107,7 +107,7 @@ export async function createProposal({ agent, target, newContent, reason }) {
console.error('Diff generation failed:', e.message);
}
}
} catch (err) {
} catch (_err) {
diff = '(New File)';
}
@@ -187,7 +187,7 @@ export async function applyProposal(id) {
// User requested <path>/<hash>.md.
const archivePath = path.join(archiveDir, `${path.basename(meta.target, '.md')}_${hash}.md`);
await fs.writeFile(archivePath, oldContent);
} catch (e) {
} catch (_e) {
// If file didn't exist, nothing to archive
}
@@ -265,7 +265,7 @@ export async function rollbackPrompt(target, hashOrStrategy = 'latest') {
sourceContent = await fs.readFile(path.join(archiveDir, match), 'utf8');
}
}
} catch (e) {
} catch (_e) {
// Archive lookup failed
}
+1 -1
View File
@@ -14,7 +14,7 @@ const VECTOR_BACKEND_ENV = 'MEMORY_VECTOR_BACKEND';
* @param {string} text
* @returns {number[]}
*/
function fallbackEmbedding(text) {
function fallbackEmbedding(_text) {
// Disabled as per configuration: Jules uses text-based memory, not vector embeddings.
return [];
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { getMemoryPruneMode, isMemoryIngestEnabled } from './feature-flags.js';
import fs from 'node:fs';
import path from 'node:path';
import util from 'node:util';
import { ensureDir } from '../../utils.mjs';
const MEMORY_FILE_PATH = path.join(process.cwd(), '.scheduler-memory', 'memory-store.json');
const debug = util.debuglog('torch-memory');
+1 -1
View File
@@ -5,7 +5,7 @@ import assert from 'node:assert';
const DIST_DIR = path.resolve('dist');
test('Build artifacts verification', async (t) => {
test('Build artifacts verification', { skip: !fs.existsSync(DIST_DIR) && 'dist/ not found — run npm run build first' }, async (t) => {
await t.test('dist directory exists', () => {
assert.strictEqual(fs.existsSync(DIST_DIR), true, 'dist directory should exist');
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { test, describe, it } from 'node:test';
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { cmdList } from '../src/cmd-list.mjs';
+1 -3
View File
@@ -40,8 +40,6 @@ test('Dashboard Configuration Exposure Security', async (t) => {
// Override env to use this config
const originalEnvPath = process.env.TORCH_CONFIG_PATH;
const originalAuth = process.env.TORCH_DASHBOARD_AUTH;
process.env.TORCH_CONFIG_PATH = tempConfigPath;
// Disable auth via env so we can access the endpoint without credentials
// The vulnerability is about exposure of *other* fields.
@@ -93,7 +91,7 @@ test('Dashboard Configuration Exposure Security', async (t) => {
let exposedConfig;
try {
exposedConfig = JSON.parse(responseBody);
} catch (e) {
} catch (_e) {
assert.fail(`Failed to parse JSON response: ${responseBody}`);
}
+1 -1
View File
@@ -80,7 +80,7 @@ test('Dashboard File Access Security', async (t) => {
assert.ok(headers['content-security-policy'], 'CSP header missing');
const csp = headers['content-security-policy'];
assert.match(csp, /default-src 'self'/, 'CSP: missing default-src self');
assert.match(csp, /script-src 'self' 'unsafe-inline' https:\/\/cdn\.jsdelivr\.net/, 'CSP: missing script-src');
assert.match(csp, /script-src 'self' https:\/\/cdn\.jsdelivr\.net/, 'CSP: missing script-src');
assert.match(csp, /object-src 'none'/, 'CSP: missing object-src none');
// X-Content-Type-Options
-2
View File
@@ -99,7 +99,6 @@ describe('src/lib.mjs', () => {
});
describe('cmdLock', () => {
const mockPublishLock = async () => {};
const mockGenerateSecretKey = () => new Uint8Array(32);
const mockGetPublicKey = () => 'mock-pubkey';
const mockFinalizeEvent = (t) => ({ ...t, id: 'mock-id', sig: 'mock-sig' });
@@ -353,7 +352,6 @@ describe('src/lib.mjs', () => {
});
describe('cmdComplete', () => {
const mockPublishLock = async () => {};
const mockGenerateSecretKey = () => new Uint8Array(32);
const mockGetPublicKey = () => 'mock-pubkey';
const mockFinalizeEvent = (t) => ({ ...t, id: 'mock-id-complete' });
+1 -1
View File
@@ -138,7 +138,7 @@ test('runPruneCycle respects prune feature-flag modes', async () => {
test('markMemoryMerged delegates to repository and clears cache', async () => {
let callCount = 0;
const repository = {
async markMerged(id, mergedInto) {
async markMerged(id, _mergedInto) {
callCount++;
return id === 'exists';
}
+16 -7
View File
@@ -23,15 +23,24 @@ test('stores runtime events in signer/session namespace and clears scope', () =>
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's2' }).length, 1);
});
test('expires runtime events and records expiration metrics', async () => {
const cache = createMemoryCache();
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 1 }, 1);
test('expires runtime events and records expiration metrics', () => {
const originalNow = Date.now;
try {
const start = originalNow.call(Date);
Date.now = () => start;
await new Promise((resolve) => setTimeout(resolve, 10));
const cache = createMemoryCache();
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 1 }, 1);
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).length, 0);
const metrics = cache.getMetrics();
assert.equal(metrics.runtime_events_expired, 1);
// Advance mocked clock past the 1ms TTL
Date.now = () => start + 10;
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).length, 0);
const metrics = cache.getMetrics();
assert.equal(metrics.runtime_events_expired, 1);
} finally {
Date.now = originalNow;
}
});
test('enforces LRU bounds and tracks eviction metrics', () => {
-1
View File
@@ -6,7 +6,6 @@ import {
formatMemoryBlock,
formatMemoriesForPrompt,
renderPromptWithMemoryContext,
isMemoryRetrievalEnabled
} from '../src/services/memory/formatter.js';
function createMemory(overrides = {}) {
+1 -1
View File
@@ -110,7 +110,7 @@ test('getRelevantMemories passes correct parameters to ranker', async (t) => {
await getRelevantMemories(params);
assert.equal(ranker.mock.callCount(), 1);
const [_, queryParams] = ranker.mock.calls[0].arguments;
const [, queryParams] = ranker.mock.calls[0].arguments;
assert.equal(queryParams.agent_id, 'agent-1');
assert.equal(queryParams.query, 'my query');
+2 -2
View File
@@ -1,4 +1,4 @@
import { test, describe, beforeEach, afterEach } from 'node:test';
import { test, describe, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { ingestEvents, getRelevantMemories } from '../src/services/memory/index.js';
@@ -11,7 +11,7 @@ describe('ingestEvents', () => {
let linkedEmbeddings = new Map();
const mockOptions = {
embedText: async (text) => Array(128).fill(0.1), // Deterministic embedding
embedText: async (_text) => Array(128).fill(0.1), // Deterministic embedding
emitTelemetry: (event, payload) => telemetryEvents.push({ event, payload }),
metrics: {
emit: (metric, payload) => metricsEvents.push({ metric, payload }),
+2 -2
View File
@@ -52,7 +52,7 @@ test('pinMemory returns null for non-existent ID but still clears cache', async
listMemoriesCallCount++;
return records;
},
async setPinned(id, pinned) {
async setPinned(_id, _pinned) {
return null;
},
async updateMemoryUsage() { return null; }
@@ -121,7 +121,7 @@ test('unpinMemory returns null for non-existent ID but still clears cache', asyn
listMemoriesCallCount++;
return records;
},
async setPinned(id, pinned) {
async setPinned(_id, _pinned) {
return null;
},
async updateMemoryUsage() { return null; }
+1 -2
View File
@@ -42,8 +42,7 @@ test.before(async () => {
await fs.mkdir(memDir, { recursive: true });
// Create initial state
const now = 200_000;
// retentionMs will be 100,000
// now = 200_000, retentionMs will be 100,000
// cutoff = 100,000
const memories = [
+7 -2
View File
@@ -15,7 +15,12 @@ import {
SYNTHETIC_EVENTS,
} from './fixtures/memory-fixtures.js';
const wait = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
/** Drain the async job queue without wall-clock dependency. */
const flushAsync = async () => {
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setImmediate(resolve));
}
};
const withMockedNow = async (now, run) => {
const originalNow = Date.now;
@@ -265,7 +270,7 @@ test('scheduler smoke test handles mocked clock and lock contention', async () =
},
});
await wait(30);
await flushAsync();
scheduler.stop();
});
+1 -1
View File
@@ -1,4 +1,4 @@
import { test, describe, it } from 'node:test';
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';