arceus-s 1.6.4 → 1.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -248,6 +248,28 @@ Arceus ships with skill files that teach AI agents how to use the tools effectiv
248
248
 
249
249
  Installed automatically by both `arc analyze` (per-repo) and `arc setup` (global).
250
250
 
251
+ ---
252
+
253
+ ## Context Efficiency & Token Optimization (Benchmark)
254
+
255
+ Arceus's semantic graph model drastically reduces token wastage and context pollution for downstream LLMs compared to traditional lexical exploration (e.g., recursive grep and file reading).
256
+
257
+ ### Quantitative Efficiency Comparison
258
+
259
+ | Inquiry Scenario | Lexical Method (Grep + Full File Read) | Semantics-Guided (MCP / Cypher Query) | Token Conservation Ratio | Impact & Efficiency Gain |
260
+ | :--- | :--- | :--- | :--- | :--- |
261
+ | **1. Call-Site Tracing** <br> Retrieve all calling methods and files invoking `withLbugDb`. | **~21,000 tokens** <br>(Requires scanning grep results, opening and parsing `api.ts` [69.6KB] and `lbug-adapter.ts` [14.4KB] to locate calling signatures). | **~28 tokens** <br>(Cypher execution: returns a targeted JSON array referencing the exact caller `handler` in `api.ts`). | **750x Reduction** <br>(99.87% Saved) | **Critical Path Tracing**: Eliminates ingestion of unrelated implementation details, preserving LLM context window. |
262
+ | **2. API Route Mapping** <br> Discover all registered endpoints and handler files. | **~20,162 tokens** <br>(Requires reading multiple route-registration files, middleware modules, and unit test suites). | **~65 tokens** <br>(Cypher execution: fetches all `Route` nodes containing route paths and source locations). | **310x Reduction** <br>(99.68% Saved) | **Interface Discovery**: Obtains complete routing topography without feeding entire source files to the LLM. |
263
+ | **3. Monorepo Class Indexing** <br> Index all classes and paths in the workspace. | **~87,500 tokens** <br>(Requires reading over 20 files containing class structures to capture inheritance and signatures). | **~1,250 tokens** <br>(Cypher execution: returns a complete node list of all `Class` names and file paths). | **70x Reduction** <br>(98.57% Saved) | **Architecture Mapping**: Instant monorepo-wide indexing with minimal network and computational overhead. |
264
+
265
+ ### Architectural Advantages
266
+
267
+ 1. **Deterministic Precision (Zero Noise)**: Lexical tools like grep require models to ingest noise (boilerplates, imports, formatting, unrelated logic) to resolve relationships. Arceus returns only the exact requested graph nodes and edges.
268
+ 2. **Multi-Hop Traversal**: Tracing transitive chains (e.g., `Class A extends Class B implements Interface C`) normally requires iterative lexical searches. A single Cypher query (e.g., `MATCH (a:Class)-[:EXTENDS]->(b)-[:IMPLEMENTS]->(c) RETURN a, c`) evaluates this instantly on the graph.
269
+ 3. **Optimized Concurrency**: Read-only graph locking ensures concurrent MCP context retrieval does not block editor processes, runtime tasks, or local file systems.
270
+
271
+ ---
272
+
251
273
  ## Requirements
252
274
 
253
275
  - Node.js >= 18
@@ -384,7 +406,7 @@ For repositories with very large source files, `ARC_WORKER_SUB_BATCH_MAX_BYTES`
384
406
 
385
407
  ## Web UI
386
408
 
387
- Arceus also has a browser-based UI at [arc.vercel.app](https://arc.vercel.app) — 100% client-side, your code never leaves the browser.
409
+ Arceus also has a browser-based UI at [arceus-arc.vercel.app](https://arceus-arc.vercel.app) — 100% client-side, your code never leaves the browser.
388
410
 
389
411
  **Local Backend Mode:** Run `arc serve` and open the web UI locally — it auto-detects the server and shows all your indexed repos, with full AI chat support. No need to re-upload or re-index. The agent's tools (Cypher queries, search, code navigation) route through the backend HTTP API automatically.
390
412
 
package/dist/cli/index.js CHANGED
@@ -99,6 +99,7 @@ Commands:
99
99
  analyze [path] Index a repository (full analysis)
100
100
  index [path...] Register an existing .arc/ folder into the global registry
101
101
  serve Start local HTTP server for web UI connection
102
+ stop Stop the local HTTP server on a port (default: 4747)
102
103
  mcp Start MCP server (stdio) — serves all indexed repos
103
104
  list List all indexed repositories
104
105
  status Show index status for current repo
@@ -175,6 +176,14 @@ Options:
175
176
  -p, --port <port> Port number (default: 4747)
176
177
  --host <host> Bind address (default: 127.0.0.1)`);
177
178
  break;
179
+ case 'stop':
180
+ console.log(`Usage: ${binName} stop [options]
181
+
182
+ Stop the local HTTP server on a port
183
+
184
+ Options:
185
+ -p, --port <port> Port number (default: 4747)`);
186
+ break;
178
187
  case 'clean':
179
188
  console.log(`Usage: ${binName} clean [options]
180
189
 
@@ -369,6 +378,13 @@ async function runCLI() {
369
378
  await serveCommand(options);
370
379
  break;
371
380
  }
381
+ case 'stop': {
382
+ if (options.port === undefined)
383
+ options.port = '4747';
384
+ const { stopCommand } = await import('./stop.js');
385
+ await stopCommand(options);
386
+ break;
387
+ }
372
388
  case 'mcp': {
373
389
  const { mcpCommand } = await import('./mcp.js');
374
390
  await mcpCommand();
package/dist/cli/serve.js CHANGED
@@ -26,7 +26,7 @@ export const serveCommand = async (options) => {
26
26
  const port = Number(options?.port ?? 4747);
27
27
  // Default to 'localhost' so the OS decides whether to bind to 127.0.0.1 or
28
28
  // ::1 based on system configuration, avoiding spurious CORS errors when the
29
- // hosted frontend at arc.vercel.app connects to localhost.
29
+ // hosted frontend at arceus-arc.vercel.app connects to localhost.
30
30
  const host = options?.host ?? 'localhost';
31
31
  try {
32
32
  await createServer(port, host);
@@ -0,0 +1,3 @@
1
+ export declare const stopCommand: (options?: {
2
+ port?: string;
3
+ }) => Promise<void>;
@@ -0,0 +1,122 @@
1
+ import { exec } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { logger } from '../core/logger.js';
4
+ const execAsync = promisify(exec);
5
+ export const stopCommand = async (options) => {
6
+ const port = Number(options?.port ?? 4747);
7
+ if (isNaN(port) || port <= 0 || port > 65535) {
8
+ console.error(`Error: Invalid port number: ${options?.port}`);
9
+ process.exitCode = 1;
10
+ return;
11
+ }
12
+ console.log(`Stopping process on port ${port}...`);
13
+ try {
14
+ const pids = await findPidsOnPort(port);
15
+ if (pids.size === 0) {
16
+ console.log(`No active process found using port ${port}.`);
17
+ return;
18
+ }
19
+ const killedPids = [];
20
+ const failedPids = [];
21
+ for (const pid of pids) {
22
+ if (pid === process.pid) {
23
+ // Don't kill ourselves
24
+ continue;
25
+ }
26
+ try {
27
+ await killPid(pid);
28
+ killedPids.push(pid);
29
+ }
30
+ catch (err) {
31
+ logger.error({ err, pid }, 'Failed to kill process');
32
+ failedPids.push(pid);
33
+ }
34
+ }
35
+ if (killedPids.length > 0) {
36
+ console.log(`Successfully stopped process(es) on port ${port} (PID: ${killedPids.join(', ')}).`);
37
+ }
38
+ if (failedPids.length > 0) {
39
+ console.error(`Failed to stop process(es) on port ${port} (PID: ${failedPids.join(', ')}).`);
40
+ process.exitCode = 1;
41
+ }
42
+ }
43
+ catch (err) {
44
+ console.error(`Error while scanning port ${port}:`, err.message || err);
45
+ process.exitCode = 1;
46
+ }
47
+ };
48
+ async function findPidsOnPort(port) {
49
+ const pids = new Set();
50
+ if (process.platform === 'win32') {
51
+ try {
52
+ const { stdout } = await execAsync('netstat -ano');
53
+ const lines = stdout.split('\n');
54
+ for (const line of lines) {
55
+ const trimmed = line.trim();
56
+ if (!trimmed)
57
+ continue;
58
+ const parts = trimmed.split(/\s+/);
59
+ if (parts.length < 4)
60
+ continue;
61
+ // Local address is the second column (index 1)
62
+ const localAddress = parts[1];
63
+ if (localAddress &&
64
+ (localAddress.endsWith(`:${port}`) || localAddress.endsWith(`]:${port}`))) {
65
+ const pidStr = parts[parts.length - 1];
66
+ const pid = parseInt(pidStr, 10);
67
+ if (!isNaN(pid) && pid > 0) {
68
+ pids.add(pid);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ catch (err) {
74
+ logger.debug({ err }, 'netstat execution failed');
75
+ throw new Error(`Failed to list connections via netstat: ${err.message}`);
76
+ }
77
+ }
78
+ else {
79
+ // macOS / Linux
80
+ try {
81
+ const { stdout } = await execAsync(`lsof -t -i :${port}`);
82
+ const lines = stdout.trim().split('\n');
83
+ for (const line of lines) {
84
+ const pid = parseInt(line.trim(), 10);
85
+ if (!isNaN(pid) && pid > 0) {
86
+ pids.add(pid);
87
+ }
88
+ }
89
+ }
90
+ catch (err) {
91
+ // lsof returns exit code 1 if no matches are found, which is not a real failure
92
+ if (err.code !== 1) {
93
+ // Try fallback to fuser
94
+ try {
95
+ const { stdout } = await execAsync(`fuser ${port}/tcp`);
96
+ const lines = stdout.trim().split(/\s+/);
97
+ for (const line of lines) {
98
+ const pid = parseInt(line.trim(), 10);
99
+ if (!isNaN(pid) && pid > 0) {
100
+ pids.add(pid);
101
+ }
102
+ }
103
+ }
104
+ catch (fuserErr) {
105
+ logger.debug({ fuserErr }, 'fuser execution failed');
106
+ if (fuserErr.code !== 1) {
107
+ throw new Error(`Failed to list connections via lsof/fuser: ${err.message}`);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ return pids;
114
+ }
115
+ async function killPid(pid) {
116
+ if (process.platform === 'win32') {
117
+ await execAsync(`taskkill /F /PID ${pid}`);
118
+ }
119
+ else {
120
+ await execAsync(`kill -9 ${pid}`);
121
+ }
122
+ }
@@ -2,6 +2,7 @@ import lbug from '@ladybugdb/core';
2
2
  import { KnowledgeGraph } from '../graph/types.js';
3
3
  import type { CachedEmbedding } from '../embeddings/types.js';
4
4
  import { type ExtensionEnsureOptions } from './extension-loader.js';
5
+ import { type LbugDatabaseOptions } from './lbug-config.js';
5
6
  /** Factory for creating WriteStreams — injectable for testing. */
6
7
  export type WriteStreamFactory = (filePath: string) => import('fs').WriteStream;
7
8
  /** Result of splitting the relationship CSV into per-label-pair files. */
@@ -53,7 +54,7 @@ export declare const initLbug: (dbPath: string) => Promise<{
53
54
  * database is busy (e.g. `arc analyze` holds the write lock).
54
55
  * Each retry waits DB_LOCK_RETRY_DELAY_MS * attempt milliseconds.
55
56
  */
56
- export declare const withLbugDb: <T>(dbPath: string, operation: () => Promise<T>) => Promise<T>;
57
+ export declare const withLbugDb: <T>(dbPath: string, operation: () => Promise<T>, options?: LbugDatabaseOptions) => Promise<T>;
57
58
  export type LbugProgressCallback = (message: string) => void;
58
59
  export declare const loadGraphToLbug: (graph: KnowledgeGraph, repoPath: string, storagePath: string, onProgress?: LbugProgressCallback) => Promise<{
59
60
  success: boolean;
@@ -237,12 +237,12 @@ export const initLbug = async (dbPath) => {
237
237
  * database is busy (e.g. `arc analyze` holds the write lock).
238
238
  * Each retry waits DB_LOCK_RETRY_DELAY_MS * attempt milliseconds.
239
239
  */
240
- export const withLbugDb = async (dbPath, operation) => {
240
+ export const withLbugDb = async (dbPath, operation, options) => {
241
241
  let lastError;
242
242
  for (let attempt = 1; attempt <= DB_LOCK_RETRY_ATTEMPTS; attempt++) {
243
243
  try {
244
244
  return await runWithSessionLock(async () => {
245
- await ensureLbugInitialized(dbPath);
245
+ await ensureLbugInitialized(dbPath, options);
246
246
  return operation();
247
247
  });
248
248
  }
@@ -272,14 +272,14 @@ export const withLbugDb = async (dbPath, operation) => {
272
272
  // but TypeScript needs an explicit throw to satisfy the return type.
273
273
  throw lastError;
274
274
  };
275
- const ensureLbugInitialized = async (dbPath) => {
275
+ const ensureLbugInitialized = async (dbPath, options) => {
276
276
  if (conn && currentDbPath === dbPath) {
277
277
  return { db, conn };
278
278
  }
279
- await doInitLbug(dbPath);
279
+ await doInitLbug(dbPath, options);
280
280
  return { db, conn };
281
281
  };
282
- const doInitLbug = async (dbPath) => {
282
+ const doInitLbug = async (dbPath, options) => {
283
283
  // Different database requested — close the old one first
284
284
  if (conn || db) {
285
285
  await safeClose();
@@ -316,7 +316,7 @@ const doInitLbug = async (dbPath) => {
316
316
  // Ensure parent directory exists
317
317
  const parentDir = path.dirname(dbPath);
318
318
  await fs.mkdir(parentDir, { recursive: true });
319
- const opened = await openLbugConnection(lbug, dbPath);
319
+ const opened = await openLbugConnection(lbug, dbPath, options);
320
320
  db = opened.db;
321
321
  conn = opened.conn;
322
322
  for (const schemaQuery of SCHEMA_QUERIES) {
@@ -56,7 +56,7 @@ export const isAllowedOrigin = (origin) => {
56
56
  origin === 'http://127.0.0.1' ||
57
57
  origin.startsWith('http://[::1]:') ||
58
58
  origin === 'http://[::1]' ||
59
- origin === 'https://arc.vercel.app') {
59
+ origin === 'https://arceus-arc.vercel.app') {
60
60
  return true;
61
61
  }
62
62
  // RFC 1918 private network ranges — allow any port on these hosts.
@@ -163,7 +163,7 @@ a.ext:hover{text-decoration:underline}
163
163
  <div class="terminal"><span class="prompt">$ </span><span class="cmd">cd arceus-web &amp;&amp; npm run build</span></div>
164
164
  <div class="link-row">
165
165
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
166
- <a class="ext" href="https://arc.vercel.app" target="_blank" rel="noopener noreferrer">arc.vercel.app</a>
166
+ <a class="ext" href="https://arceus-arc.vercel.app" target="_blank" rel="noopener noreferrer">arceus-arc.vercel.app</a>
167
167
  <span style="color:#5a5a70">— connects to this server</span>
168
168
  </div>
169
169
  </div>
@@ -852,7 +852,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
852
852
  res.once('finish', markFinished);
853
853
  res.once('close', abortStreaming);
854
854
  try {
855
- await withLbugDb(lbugPath, async () => streamGraphNdjson(res, includeContent, abortController.signal));
855
+ await withLbugDb(lbugPath, async () => streamGraphNdjson(res, includeContent, abortController.signal), { readOnly: true });
856
856
  if (!abortController.signal.aborted && !res.writableEnded) {
857
857
  res.end();
858
858
  }
@@ -864,7 +864,9 @@ export const createServer = async (port, host = '127.0.0.1') => {
864
864
  }
865
865
  return;
866
866
  }
867
- const graph = await withLbugDb(lbugPath, async () => buildGraph(includeContent));
867
+ const graph = await withLbugDb(lbugPath, async () => buildGraph(includeContent), {
868
+ readOnly: true,
869
+ });
868
870
  res.json(graph);
869
871
  }
870
872
  catch (err) {
@@ -904,7 +906,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
904
906
  return;
905
907
  }
906
908
  const lbugPath = path.join(entry.storagePath, 'lbug');
907
- const result = await withLbugDb(lbugPath, () => executeQuery(cypher));
909
+ const result = await withLbugDb(lbugPath, () => executeQuery(cypher), { readOnly: true });
908
910
  res.json({ result });
909
911
  }
910
912
  catch (err) {
@@ -1031,7 +1033,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
1031
1033
  return { ...r, ...enrichment };
1032
1034
  }));
1033
1035
  return { searchResults: enriched, ftsAvailable };
1034
- });
1036
+ }, { readOnly: true });
1035
1037
  const response = { results: results.searchResults ?? results };
1036
1038
  if (results.ftsAvailable === false) {
1037
1039
  response.warning =
@@ -1098,7 +1100,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
1098
1100
  const results = [];
1099
1101
  const repoRoot = path.resolve(entry.path);
1100
1102
  const lbugPath = path.join(entry.storagePath, 'lbug');
1101
- const fileRows = await withLbugDb(lbugPath, () => executeQuery(`MATCH (n:File) WHERE n.content IS NOT NULL RETURN n.filePath AS filePath`));
1103
+ const fileRows = await withLbugDb(lbugPath, () => executeQuery(`MATCH (n:File) WHERE n.content IS NOT NULL RETURN n.filePath AS filePath`), { readOnly: true });
1102
1104
  for (const row of fileRows) {
1103
1105
  if (results.length >= limit)
1104
1106
  break;
@@ -1139,7 +1141,9 @@ export const createServer = async (port, host = '127.0.0.1') => {
1139
1141
  res.json(result);
1140
1142
  }
1141
1143
  catch (err) {
1142
- res.status(statusFromError(err)).json({ error: err.message || 'Failed to query processes' });
1144
+ res
1145
+ .status(statusFromError(err))
1146
+ .json({ error: err.message || 'Failed to query processes' });
1143
1147
  }
1144
1148
  },
1145
1149
  },
@@ -1176,7 +1180,9 @@ export const createServer = async (port, host = '127.0.0.1') => {
1176
1180
  res.json(result);
1177
1181
  }
1178
1182
  catch (err) {
1179
- res.status(statusFromError(err)).json({ error: err.message || 'Failed to query clusters' });
1183
+ res
1184
+ .status(statusFromError(err))
1185
+ .json({ error: err.message || 'Failed to query clusters' });
1180
1186
  }
1181
1187
  },
1182
1188
  },
@@ -1276,7 +1282,9 @@ export const createServer = async (port, host = '127.0.0.1') => {
1276
1282
  : [];
1277
1283
  const forkWorker = () => {
1278
1284
  const currentJob = jobManager.getJob(job.id);
1279
- if (!currentJob || currentJob.status === 'complete' || currentJob.status === 'failed')
1285
+ if (!currentJob ||
1286
+ currentJob.status === 'complete' ||
1287
+ currentJob.status === 'failed')
1280
1288
  return;
1281
1289
  const child = fork(workerPath, [], {
1282
1290
  execArgv: [...tsxHookArgs, '--max-old-space-size=8192'],
@@ -1451,7 +1459,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
1451
1459
  embedJobManager.updateJob(job.id, {
1452
1460
  repoName: entry.name,
1453
1461
  status: 'analyzing',
1454
- progress: { phase: 'analyzing', percent: 0, message: 'Starting embedding generation...' },
1462
+ progress: {
1463
+ phase: 'analyzing',
1464
+ percent: 0,
1465
+ message: 'Starting embedding generation...',
1466
+ },
1455
1467
  });
1456
1468
  const EMBED_TIMEOUT_MS = 30 * 60 * 1000;
1457
1469
  const embedTimeout = setTimeout(() => {
@@ -1477,7 +1489,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
1477
1489
  await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (p) => {
1478
1490
  embedJobManager.updateJob(job.id, {
1479
1491
  progress: {
1480
- phase: p.phase === 'ready' ? 'complete' : p.phase === 'error' ? 'failed' : p.phase,
1492
+ phase: p.phase === 'ready'
1493
+ ? 'complete'
1494
+ : p.phase === 'error'
1495
+ ? 'failed'
1496
+ : p.phase,
1481
1497
  percent: p.percent,
1482
1498
  message: p.phase === 'loading-model'
1483
1499
  ? 'Loading embedding model...'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arceus-s",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Chandan Kumar Behera",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -119,4 +119,4 @@
119
119
  "engines": {
120
120
  "node": ">=22.0.0"
121
121
  }
122
- }
122
+ }