archbyte 0.6.0 → 0.7.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.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { watch } from "chokidar";
3
3
  import { readFile, writeFile, mkdir } from "fs/promises";
4
- import { existsSync, readFileSync, statSync } from "fs";
4
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
5
5
  import path from "path";
6
6
  import { fileURLToPath } from "url";
7
+ import { homedir } from "os";
7
8
  import { createServer } from "http";
8
9
  import { execSync, spawn, spawnSync } from "child_process";
9
10
  // Get UI assets path
@@ -1223,18 +1224,6 @@ function createHttpServer() {
1223
1224
  res.end(JSON.stringify({ running: runningWorkflows.has("__generate__") }));
1224
1225
  return;
1225
1226
  }
1226
- // API: Reload — re-read architecture, reconcile pending changes
1227
- if (url === "/api/reload" && req.method === "POST") {
1228
- currentArchitecture = await loadArchitecture();
1229
- reconcilePendingWithGit();
1230
- broadcastUpdate();
1231
- if (pendingSourceChanges.size > 0) {
1232
- broadcastPendingChanges();
1233
- }
1234
- res.writeHead(200, { "Content-Type": "application/json" });
1235
- res.end(JSON.stringify({ ok: true }));
1236
- return;
1237
- }
1238
1227
  // API: Run analyze (static or LLM) + generate
1239
1228
  if (url === "/api/analyze" && req.method === "POST") {
1240
1229
  if (isAnalyzing) {
@@ -1696,6 +1685,38 @@ function createHttpServer() {
1696
1685
  });
1697
1686
  return;
1698
1687
  }
1688
+ // API: Sessions — list all sessions
1689
+ if (url === "/api/sessions" && req.method === "GET") {
1690
+ try {
1691
+ const sessionIndex = await getSessionsIndex();
1692
+ res.writeHead(200, { "Content-Type": "application/json" });
1693
+ res.end(JSON.stringify(sessionIndex));
1694
+ }
1695
+ catch (error) {
1696
+ res.writeHead(500, { "Content-Type": "application/json" });
1697
+ res.end(JSON.stringify({ error: String(error) }));
1698
+ }
1699
+ return;
1700
+ }
1701
+ // API: Sessions — get single session by ID
1702
+ if (url.startsWith("/api/sessions/") && req.method === "GET") {
1703
+ const sessionId = decodeURIComponent(url.replace("/api/sessions/", "").split("?")[0]);
1704
+ try {
1705
+ const session = await loadSingleSession(sessionId);
1706
+ if (!session) {
1707
+ res.writeHead(404, { "Content-Type": "application/json" });
1708
+ res.end(JSON.stringify({ error: "Session not found" }));
1709
+ return;
1710
+ }
1711
+ res.writeHead(200, { "Content-Type": "application/json" });
1712
+ res.end(JSON.stringify(session));
1713
+ }
1714
+ catch (error) {
1715
+ res.writeHead(500, { "Content-Type": "application/json" });
1716
+ res.end(JSON.stringify({ error: String(error) }));
1717
+ }
1718
+ return;
1719
+ }
1699
1720
  // Serve static UI files
1700
1721
  const MIME_TYPES = {
1701
1722
  ".html": "text/html",
@@ -2138,6 +2159,272 @@ function loadLicenseInfo() {
2138
2159
  return defaults;
2139
2160
  }
2140
2161
  }
2162
+ // Session helpers — load sessions from archbyte + claude-transcripts
2163
+ let sessionWatcher = null;
2164
+ // Cached session index + route map for fast single-session lookups
2165
+ let _cachedSessionIndex = null;
2166
+ const _sessionRouteMap = new Map();
2167
+ async function getSessionsIndex() {
2168
+ if (_cachedSessionIndex)
2169
+ return _cachedSessionIndex;
2170
+ _cachedSessionIndex = await loadSessionsIndex();
2171
+ return _cachedSessionIndex;
2172
+ }
2173
+ // Ignored directories when scanning for session dirs
2174
+ const SCAN_IGNORE = new Set(["node_modules", ".git", ".hg", "dist", "build", ".next", "__pycache__", "vendor", ".venv", "venv"]);
2175
+ const SESSION_SCAN_MAX_DEPTH = 4;
2176
+ /** Recursively find named hidden directories (e.g. ".archbyte") */
2177
+ function findSessionDirs(root, targetNames, depth = 0) {
2178
+ if (depth > SESSION_SCAN_MAX_DEPTH)
2179
+ return [];
2180
+ const results = [];
2181
+ try {
2182
+ const entries = readdirSync(root, { withFileTypes: true });
2183
+ for (const entry of entries) {
2184
+ if (!entry.isDirectory())
2185
+ continue;
2186
+ if (targetNames.includes(entry.name)) {
2187
+ results.push(path.join(root, entry.name));
2188
+ }
2189
+ else if (!SCAN_IGNORE.has(entry.name) && !entry.name.startsWith(".")) {
2190
+ results.push(...findSessionDirs(path.join(root, entry.name), targetNames, depth + 1));
2191
+ }
2192
+ }
2193
+ }
2194
+ catch {
2195
+ // Permission error or unreadable dir — skip
2196
+ }
2197
+ return results;
2198
+ }
2199
+ // Cache discovered dirs (refreshed on watcher events)
2200
+ let _archbyteDirs = null;
2201
+ let _claudeTranscriptDir = undefined; // undefined = not checked yet
2202
+ /** Load sessionsPaths from archbyte global config (~/.archbyte/config.json) */
2203
+ function getConfigSessionsPaths() {
2204
+ try {
2205
+ const configPath = path.join(homedir(), ".archbyte", "config.json");
2206
+ if (!existsSync(configPath))
2207
+ return [];
2208
+ const raw = readFileSync(configPath, "utf-8");
2209
+ const cfg = JSON.parse(raw);
2210
+ return Array.isArray(cfg.sessionsPaths) ? cfg.sessionsPaths : [];
2211
+ }
2212
+ catch {
2213
+ return [];
2214
+ }
2215
+ }
2216
+ /** Discover all archbyte session dirs from workspace + external paths */
2217
+ function discoverArchbyteDirs() {
2218
+ if (_archbyteDirs)
2219
+ return _archbyteDirs;
2220
+ const allArchbyte = [];
2221
+ // Scan within project tree
2222
+ const found = findSessionDirs(config.workspaceRoot, [".archbyte"]);
2223
+ for (const dir of found) {
2224
+ if (path.basename(dir) === ".archbyte")
2225
+ allArchbyte.push(dir);
2226
+ }
2227
+ // Also scan configured external paths
2228
+ for (const extPath of getConfigSessionsPaths()) {
2229
+ const resolved = path.resolve(extPath);
2230
+ if (!existsSync(resolved))
2231
+ continue;
2232
+ if (path.basename(resolved) === ".archbyte") {
2233
+ allArchbyte.push(resolved);
2234
+ }
2235
+ else {
2236
+ const extFound = findSessionDirs(resolved, [".archbyte"]);
2237
+ for (const dir of extFound) {
2238
+ if (path.basename(dir) === ".archbyte")
2239
+ allArchbyte.push(dir);
2240
+ }
2241
+ }
2242
+ }
2243
+ _archbyteDirs = [...new Set(allArchbyte)];
2244
+ return _archbyteDirs;
2245
+ }
2246
+ function getArchbyteDirs() {
2247
+ return discoverArchbyteDirs();
2248
+ }
2249
+ function getClaudeTranscriptDir() {
2250
+ if (_claudeTranscriptDir !== undefined)
2251
+ return _claudeTranscriptDir;
2252
+ const encoded = config.workspaceRoot.replace(/^\//, "").replace(/\//g, "-");
2253
+ const dir = path.join(homedir(), ".claude", "projects", `-${encoded}`);
2254
+ _claudeTranscriptDir = existsSync(dir) ? dir : null;
2255
+ return _claudeTranscriptDir;
2256
+ }
2257
+ async function loadSessionsIndex() {
2258
+ const index = { sessions: [] };
2259
+ // Clear route map — will be repopulated below
2260
+ _sessionRouteMap.clear();
2261
+ const archbyteDirs = discoverArchbyteDirs();
2262
+ // Import archbyte sessions
2263
+ if (archbyteDirs.length > 0) {
2264
+ try {
2265
+ const { importArchbyte } = await import("../../agents/observability/adapters/archbyte.js");
2266
+ for (const abDir of archbyteDirs) {
2267
+ const abSessions = await importArchbyte(abDir);
2268
+ const relativePath = path.relative(config.workspaceRoot, abDir);
2269
+ const sourceLabel = relativePath === ".archbyte" ? "archbyte" : `archbyte (${path.dirname(relativePath)})`;
2270
+ for (const abs of abSessions) {
2271
+ index.sessions.push({
2272
+ sessionId: abs.sessionId,
2273
+ startedAt: abs.startedAt,
2274
+ completedAt: abs.completedAt,
2275
+ status: abs.status,
2276
+ runCount: abs.summary.totalRuns,
2277
+ phases: abs.summary.phases,
2278
+ source: sourceLabel,
2279
+ });
2280
+ _sessionRouteMap.set(abs.sessionId, { source: "archbyte", dir: abDir });
2281
+ }
2282
+ }
2283
+ }
2284
+ catch (err) {
2285
+ console.error("[sessions] archbyte import error:", err);
2286
+ }
2287
+ }
2288
+ // Import Claude transcript sessions
2289
+ const ctDir = getClaudeTranscriptDir();
2290
+ if (ctDir) {
2291
+ try {
2292
+ const { scanTranscriptIndex } = await import("../../agents/observability/adapters/claude-transcripts.js");
2293
+ const ctEntries = await scanTranscriptIndex(ctDir);
2294
+ for (const e of ctEntries) {
2295
+ index.sessions.push({
2296
+ sessionId: e.sessionId,
2297
+ startedAt: e.startedAt,
2298
+ completedAt: e.completedAt,
2299
+ status: e.status,
2300
+ runCount: e.runCount,
2301
+ phases: e.phases,
2302
+ source: "claude-transcript",
2303
+ category: e.category,
2304
+ label: e.label,
2305
+ touchedDirs: e.touchedDirs,
2306
+ eventCount: e.eventCount,
2307
+ dirMetrics: e.dirMetrics,
2308
+ estimatedCost: e.estimatedCost,
2309
+ });
2310
+ _sessionRouteMap.set(e.sessionId, { source: "claude-transcript", dir: ctDir });
2311
+ }
2312
+ }
2313
+ catch (err) {
2314
+ console.error("[sessions] claude-transcripts index error:", err);
2315
+ }
2316
+ }
2317
+ // Sort newest first
2318
+ index.sessions.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
2319
+ return index;
2320
+ }
2321
+ async function loadSingleSession(sessionId) {
2322
+ // Fast path: use route map if available (populated after first index build)
2323
+ const route = _sessionRouteMap.get(sessionId);
2324
+ if (route) {
2325
+ try {
2326
+ if (route.source === "claude-transcript") {
2327
+ const { importClaudeTranscript } = await import("../../agents/observability/adapters/claude-transcripts.js");
2328
+ return await importClaudeTranscript(route.dir, sessionId);
2329
+ }
2330
+ else if (route.source === "archbyte") {
2331
+ const { importArchbyte } = await import("../../agents/observability/adapters/archbyte.js");
2332
+ const sessions = await importArchbyte(route.dir);
2333
+ const found = sessions.find((s) => s.sessionId === sessionId);
2334
+ if (found)
2335
+ return found;
2336
+ }
2337
+ }
2338
+ catch (err) {
2339
+ console.error(`[sessions] routed lookup failed for ${sessionId}:`, err);
2340
+ }
2341
+ }
2342
+ // Fallback: full scan (cold start or route miss)
2343
+ const archbyteDirs = discoverArchbyteDirs();
2344
+ if (archbyteDirs.length > 0) {
2345
+ try {
2346
+ const { importArchbyte } = await import("../../agents/observability/adapters/archbyte.js");
2347
+ for (const abDir of archbyteDirs) {
2348
+ const sessions = await importArchbyte(abDir);
2349
+ const found = sessions.find((s) => s.sessionId === sessionId);
2350
+ if (found)
2351
+ return found;
2352
+ }
2353
+ }
2354
+ catch (err) {
2355
+ console.error("[sessions] archbyte single session error:", err);
2356
+ }
2357
+ }
2358
+ const ctDir2 = getClaudeTranscriptDir();
2359
+ if (ctDir2) {
2360
+ try {
2361
+ const { importClaudeTranscript } = await import("../../agents/observability/adapters/claude-transcripts.js");
2362
+ return await importClaudeTranscript(ctDir2, sessionId);
2363
+ }
2364
+ catch (err) {
2365
+ console.error("[sessions] claude-transcripts session error:", err);
2366
+ }
2367
+ }
2368
+ return null;
2369
+ }
2370
+ /** Build a fingerprint of the session index for diffing */
2371
+ function sessionIndexFingerprint(index) {
2372
+ // Fast fingerprint: sorted sessionId + status + runCount + completedAt
2373
+ return index.sessions
2374
+ .map(s => `${s.sessionId}:${s.status}:${s.runCount}:${s.completedAt || ""}`)
2375
+ .sort()
2376
+ .join("|");
2377
+ }
2378
+ function setupSessionWatcher() {
2379
+ const watchPaths = [];
2380
+ // Watch all discovered session directories
2381
+ for (const abDir of getArchbyteDirs()) {
2382
+ watchPaths.push(abDir);
2383
+ }
2384
+ const ctDir3 = getClaudeTranscriptDir();
2385
+ if (ctDir3)
2386
+ watchPaths.push(ctDir3);
2387
+ if (watchPaths.length === 0)
2388
+ return;
2389
+ let debounce = null;
2390
+ sessionWatcher = watch(watchPaths, {
2391
+ ignoreInitial: true,
2392
+ depth: 4,
2393
+ ignored: /(^|[\/\\])\../,
2394
+ });
2395
+ sessionWatcher.on("all", (_event, changedPath) => {
2396
+ // Targeted cache invalidation for .jsonl transcript files
2397
+ if (changedPath && changedPath.endsWith(".jsonl")) {
2398
+ import("../../agents/observability/adapters/claude-transcripts.js")
2399
+ .then(mod => mod.invalidateIndexCache(changedPath))
2400
+ .catch(() => { });
2401
+ }
2402
+ if (debounce)
2403
+ clearTimeout(debounce);
2404
+ debounce = setTimeout(async () => {
2405
+ try {
2406
+ // Snapshot old fingerprint before rebuild
2407
+ const oldFingerprint = _cachedSessionIndex
2408
+ ? sessionIndexFingerprint(_cachedSessionIndex)
2409
+ : "";
2410
+ // Invalidate caches so new dirs are discovered
2411
+ _archbyteDirs = null;
2412
+ _claudeTranscriptDir = undefined;
2413
+ _cachedSessionIndex = null;
2414
+ const index = await loadSessionsIndex();
2415
+ _cachedSessionIndex = index;
2416
+ // Only broadcast if index actually changed
2417
+ const newFingerprint = sessionIndexFingerprint(index);
2418
+ if (newFingerprint !== oldFingerprint) {
2419
+ broadcastOpsEvent({ type: "session:update", sessions: index });
2420
+ }
2421
+ }
2422
+ catch {
2423
+ // Non-fatal
2424
+ }
2425
+ }, 1000);
2426
+ });
2427
+ }
2141
2428
  // Export start function
2142
2429
  export async function startServer(cfg) {
2143
2430
  config = cfg;
@@ -2156,6 +2443,7 @@ export async function startServer(cfg) {
2156
2443
  reconcilePendingWithGit();
2157
2444
  setupSourceWatcher();
2158
2445
  setupGitWatcher();
2446
+ setupSessionWatcher();
2159
2447
  console.error(`[archbyte] Serving ${config.name}`);
2160
2448
  console.error(`[archbyte] Diagram: ${config.diagramPath}`);
2161
2449
  // Listen for 'q' keypress to quit gracefully
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archbyte",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "ArchByte - See what agents build. As they build it.",
5
5
  "type": "module",
6
6
  "bin": {