arcane-agents 1.0.2 → 1.2.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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Arcane Agents</title>
7
- <script type="module" crossorigin src="/assets/index-CyA5FKrE.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-TxsheMVB.css">
7
+ <script type="module" crossorigin src="/assets/index-DNXJVqF0.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-CWU29xaz.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -13,12 +13,16 @@ const statusMonitor_1 = require("../status/statusMonitor");
13
13
  const tmuxAdapter_1 = require("../tmux/tmuxAdapter");
14
14
  const realtimeHub_1 = require("../ws/realtimeHub");
15
15
  const terminalBridge_1 = require("../ws/terminalBridge");
16
- async function createServerContext() {
17
- const paths = (0, loadConfig_1.getArcaneAgentsPaths)();
16
+ async function createServerContext(sessionName) {
17
+ const paths = (0, loadConfig_1.getArcaneAgentsPaths)(sessionName);
18
18
  node_fs_1.default.mkdirSync(paths.configDir, { recursive: true });
19
19
  node_fs_1.default.mkdirSync(paths.stateDir, { recursive: true });
20
20
  node_fs_1.default.mkdirSync(paths.cacheDir, { recursive: true });
21
21
  const baseConfig = (0, loadConfig_1.loadResolvedConfig)(paths);
22
+ if ((0, loadConfig_1.isNonDefaultSession)(sessionName)) {
23
+ const baseTmuxName = baseConfig.backend.tmux.sessionName;
24
+ baseConfig.backend.tmux.sessionName = `${baseTmuxName}-${sessionName}`;
25
+ }
22
26
  const discoveryService = new discovery_1.DiscoveryService();
23
27
  const initialDiscovery = await discoveryService.discover(baseConfig);
24
28
  for (const warning of initialDiscovery.warnings) {
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.bootstrap = bootstrap;
7
7
  const node_http_1 = __importDefault(require("node:http"));
8
8
  const httpApp_1 = require("./bootstrap/httpApp");
9
+ const loadConfig_1 = require("./config/loadConfig");
9
10
  const serverContext_1 = require("./bootstrap/serverContext");
10
11
  const shutdown_1 = require("./bootstrap/shutdown");
11
12
  const websocketUpgrade_1 = require("./bootstrap/websocketUpgrade");
@@ -61,10 +62,15 @@ function renderStartupBanner() {
61
62
  })
62
63
  .join("\n");
63
64
  }
64
- async function bootstrap() {
65
+ async function bootstrap(sessionName) {
65
66
  console.log(renderStartupBanner());
66
- console.log("[arcane-agents] launching Arcane Agents...");
67
- const context = await (0, serverContext_1.createServerContext)();
67
+ if ((0, loadConfig_1.isNonDefaultSession)(sessionName)) {
68
+ console.log(`[arcane-agents] launching Arcane Agents (session: ${sessionName})...`);
69
+ }
70
+ else {
71
+ console.log("[arcane-agents] launching Arcane Agents...");
72
+ }
73
+ const context = await (0, serverContext_1.createServerContext)(sessionName);
68
74
  context.statusMonitor.start();
69
75
  const app = (0, httpApp_1.createHttpApp)(context);
70
76
  const server = node_http_1.default.createServer(app);
@@ -7,13 +7,41 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const node_child_process_1 = require("node:child_process");
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
+ const node_readline_1 = __importDefault(require("node:readline"));
10
11
  const bootstrapApp_1 = require("./bootstrapApp");
11
12
  const loadConfig_1 = require("./config/loadConfig");
12
13
  const appRoot_1 = require("./utils/appRoot");
13
14
  const path_1 = require("./utils/path");
15
+ function extractSessionFlag(args) {
16
+ const remaining = [...args];
17
+ let sessionName;
18
+ for (let i = 0; i < remaining.length; i++) {
19
+ if (remaining[i] === "--session" || remaining[i] === "-s") {
20
+ const value = remaining[i + 1];
21
+ if (!value || value.startsWith("-")) {
22
+ console.error("[arcane-agents] --session requires a name argument.");
23
+ process.exit(1);
24
+ }
25
+ sessionName = value;
26
+ remaining.splice(i, 2);
27
+ break;
28
+ }
29
+ const eqMatch = remaining[i].match(/^(?:--session|-s)=(.+)$/);
30
+ if (eqMatch) {
31
+ sessionName = eqMatch[1];
32
+ remaining.splice(i, 1);
33
+ break;
34
+ }
35
+ }
36
+ if (sessionName !== undefined && !/^[a-zA-Z0-9_-]+$/.test(sessionName)) {
37
+ console.error("[arcane-agents] session name must only contain letters, digits, hyphens, and underscores.");
38
+ process.exit(1);
39
+ }
40
+ return { sessionName, remainingArgs: remaining };
41
+ }
14
42
  async function runCli() {
15
43
  (0, appRoot_1.setAppRoot)((0, appRoot_1.resolveAppRoot)());
16
- const args = process.argv.slice(2);
44
+ const { sessionName, remainingArgs: args } = extractSessionFlag(process.argv.slice(2));
17
45
  const firstArg = args[0];
18
46
  if (firstArg === "--help" || firstArg === "-h") {
19
47
  printHelp();
@@ -26,13 +54,15 @@ async function runCli() {
26
54
  const [command = "start", ...commandArgs] = args;
27
55
  switch (command) {
28
56
  case "start":
29
- return runStart();
57
+ return runStart(sessionName);
30
58
  case "init":
31
59
  return runInit(commandArgs);
32
60
  case "config":
33
61
  return runConfig(commandArgs);
34
62
  case "doctor":
35
63
  return runDoctor();
64
+ case "sessions":
65
+ return runSessions(commandArgs);
36
66
  case "help":
37
67
  printHelp();
38
68
  return 0;
@@ -45,11 +75,11 @@ async function runCli() {
45
75
  return 1;
46
76
  }
47
77
  }
48
- async function runStart() {
78
+ async function runStart(sessionName) {
49
79
  if (!process.env.NODE_ENV) {
50
80
  process.env.NODE_ENV = "production";
51
81
  }
52
- const paths = (0, loadConfig_1.getArcaneAgentsPaths)();
82
+ const paths = (0, loadConfig_1.getArcaneAgentsPaths)(sessionName);
53
83
  let configResult;
54
84
  try {
55
85
  configResult = ensureStarterConfig(paths);
@@ -63,7 +93,7 @@ async function runStart() {
63
93
  console.log(`[arcane-agents] no config found; wrote starter config to ${paths.configPath}`);
64
94
  console.log("[arcane-agents] next: edit it with 'arcane-agents config edit'.");
65
95
  }
66
- await (0, bootstrapApp_1.bootstrap)();
96
+ await (0, bootstrapApp_1.bootstrap)(sessionName);
67
97
  return 0;
68
98
  }
69
99
  function runInit(args) {
@@ -235,6 +265,85 @@ Commands:
235
265
  help Show this config help message
236
266
  `);
237
267
  }
268
+ async function runSessions(args) {
269
+ const [subcommand = "list", ...subcommandArgs] = args;
270
+ switch (subcommand) {
271
+ case "list":
272
+ break;
273
+ case "delete":
274
+ return runSessionsDelete(subcommandArgs);
275
+ default:
276
+ console.error(`[arcane-agents] unknown sessions command '${subcommand}'.`);
277
+ console.log("Usage: arcane-agents sessions [list|delete <name>]");
278
+ return 1;
279
+ }
280
+ const defaultPaths = (0, loadConfig_1.getArcaneAgentsPaths)();
281
+ const sessionsDir = node_path_1.default.join(defaultPaths.stateDir, "sessions");
282
+ const defaultDbPath = defaultPaths.dbPath;
283
+ const sessions = [];
284
+ if (node_fs_1.default.existsSync(defaultDbPath)) {
285
+ sessions.push("default");
286
+ }
287
+ if (node_fs_1.default.existsSync(sessionsDir)) {
288
+ try {
289
+ const entries = node_fs_1.default.readdirSync(sessionsDir, { withFileTypes: true });
290
+ for (const entry of entries) {
291
+ if (entry.isDirectory()) {
292
+ const dbPath = node_path_1.default.join(sessionsDir, entry.name, "arcane-agents.db");
293
+ if (node_fs_1.default.existsSync(dbPath)) {
294
+ sessions.push(entry.name);
295
+ }
296
+ }
297
+ }
298
+ }
299
+ catch {
300
+ // no-op
301
+ }
302
+ }
303
+ if (sessions.length === 0) {
304
+ console.log("[arcane-agents] no sessions found.");
305
+ }
306
+ else {
307
+ console.log("[arcane-agents] sessions:");
308
+ for (const session of sessions) {
309
+ console.log(` ${session}`);
310
+ }
311
+ }
312
+ return 0;
313
+ }
314
+ async function runSessionsDelete(args) {
315
+ const name = args[0];
316
+ if (!name) {
317
+ console.error("[arcane-agents] usage: arcane-agents sessions delete <name>");
318
+ return 1;
319
+ }
320
+ if (name === "default") {
321
+ console.error("[arcane-agents] cannot delete the default session.");
322
+ return 1;
323
+ }
324
+ const sessionDir = (0, loadConfig_1.getArcaneAgentsPaths)(name).stateDir;
325
+ if (!node_fs_1.default.existsSync(sessionDir)) {
326
+ console.error(`[arcane-agents] session '${name}' not found.`);
327
+ return 1;
328
+ }
329
+ const answer = await promptConfirm(`Delete session '${name}' and all its data (${sessionDir})? [y/N] `);
330
+ if (!answer) {
331
+ console.log("[arcane-agents] aborted.");
332
+ return 0;
333
+ }
334
+ node_fs_1.default.rmSync(sessionDir, { recursive: true, force: true });
335
+ console.log(`[arcane-agents] deleted session '${name}'.`);
336
+ return 0;
337
+ }
338
+ function promptConfirm(question) {
339
+ const rl = node_readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
340
+ return new Promise((resolve) => {
341
+ rl.question(question, (answer) => {
342
+ rl.close();
343
+ resolve(answer.trim().toLowerCase() === "y");
344
+ });
345
+ });
346
+ }
238
347
  function runDoctor() {
239
348
  const checks = [];
240
349
  const nodeVersion = process.versions.node;
@@ -357,9 +466,10 @@ function printHelp() {
357
466
  console.log(`Arcane Agents CLI
358
467
 
359
468
  Usage:
360
- arcane-agents [start]
469
+ arcane-agents [start] [--session <name>]
361
470
  arcane-agents init [--force]
362
471
  arcane-agents config [path|show|edit]
472
+ arcane-agents sessions [list|delete <name>]
363
473
  arcane-agents doctor
364
474
  arcane-agents --help
365
475
  arcane-agents --version
@@ -368,10 +478,16 @@ Commands:
368
478
  start Start the Arcane Agents server
369
479
  init Write ~/.config/arcane-agents/config.yaml from config.example.yaml
370
480
  config Print, show, or edit config files
481
+ sessions List or delete named sessions
371
482
  doctor Check dependencies and runtime command availability
372
483
  help Show this help message
373
484
  version Print CLI version
374
485
 
486
+ Options:
487
+ --session <name>, -s <name>
488
+ Run with a named session (separate DB and tmux session).
489
+ Default session uses the standard paths for backwards compatibility.
490
+
375
491
  Config paths:
376
492
  primary: ${paths.configPath}
377
493
  local override: ${paths.localOverridePath}
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isNonDefaultSession = isNonDefaultSession;
6
7
  exports.getArcaneAgentsPaths = getArcaneAgentsPaths;
7
8
  exports.loadResolvedConfig = loadResolvedConfig;
8
9
  const node_fs_1 = __importDefault(require("node:fs"));
@@ -10,9 +11,16 @@ const node_path_1 = __importDefault(require("node:path"));
10
11
  const yaml_1 = __importDefault(require("yaml"));
11
12
  const schema_1 = require("./schema");
12
13
  const path_1 = require("../utils/path");
13
- function getArcaneAgentsPaths() {
14
+ function isNonDefaultSession(sessionName) {
15
+ return sessionName !== undefined && sessionName !== "default";
16
+ }
17
+ function getArcaneAgentsPaths(sessionName) {
14
18
  const configDir = (0, path_1.resolveUserPath)("~/.config/arcane-agents");
15
- const stateDir = (0, path_1.resolveUserPath)("~/.local/state/arcane-agents");
19
+ const baseStateDir = (0, path_1.resolveUserPath)("~/.local/state/arcane-agents");
20
+ const isNamedSession = sessionName !== undefined && sessionName !== "default";
21
+ const stateDir = isNamedSession
22
+ ? node_path_1.default.join(baseStateDir, "sessions", sessionName)
23
+ : baseStateDir;
16
24
  return {
17
25
  configDir,
18
26
  configPath: node_path_1.default.join(configDir, "config.yaml"),
@@ -9,12 +9,14 @@ function parseSpawnInput(body) {
9
9
  }
10
10
  const record = body;
11
11
  const spawnNearWorkerIds = parseSpawnNearWorkerIds(record);
12
+ const displayName = parseDisplayName(record);
12
13
  if (typeof record.shortcutIndex !== "undefined") {
13
14
  if (typeof record.shortcutIndex !== "number" || !Number.isInteger(record.shortcutIndex) || record.shortcutIndex < 0) {
14
15
  throw (0, appError_1.validationError)("shortcutIndex must be a non-negative integer.", "spawn_invalid_shortcut_index");
15
16
  }
16
17
  return {
17
18
  shortcutIndex: record.shortcutIndex,
19
+ displayName,
18
20
  spawnNearWorkerIds
19
21
  };
20
22
  }
@@ -24,11 +26,22 @@ function parseSpawnInput(body) {
24
26
  projectId: record.projectId,
25
27
  runtimeId: record.runtimeId,
26
28
  command,
29
+ displayName,
27
30
  spawnNearWorkerIds
28
31
  };
29
32
  }
30
33
  throw (0, appError_1.validationError)("Invalid spawn request: expected shortcutIndex or projectId+runtimeId.", "spawn_invalid_payload");
31
34
  }
35
+ function parseDisplayName(record) {
36
+ if (typeof record.displayName === "undefined") {
37
+ return undefined;
38
+ }
39
+ if (typeof record.displayName !== "string") {
40
+ throw (0, appError_1.validationError)("displayName must be a string when provided.", "spawn_invalid_display_name");
41
+ }
42
+ const trimmed = record.displayName.trim();
43
+ return trimmed.length > 0 ? trimmed : undefined;
44
+ }
32
45
  function parseSpawnNearWorkerIds(record) {
33
46
  if (typeof record.spawnNearWorkerIds === "undefined") {
34
47
  return undefined;
@@ -22,7 +22,7 @@ function resolveSpawnPlan(config, input) {
22
22
  runtimeId: shortcut.runtime,
23
23
  runtime,
24
24
  command: shortcut.command ?? runtime.command,
25
- displayName: shortcut.label,
25
+ displayName: input.displayName ?? shortcut.label,
26
26
  avatar: shortcut.avatar
27
27
  };
28
28
  }
@@ -39,6 +39,7 @@ function resolveSpawnPlan(config, input) {
39
39
  project,
40
40
  runtimeId: input.runtimeId,
41
41
  runtime,
42
- command: input.command && input.command.length > 0 ? input.command : runtime.command
42
+ command: input.command && input.command.length > 0 ? input.command : runtime.command,
43
+ displayName: input.displayName
43
44
  };
44
45
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.recoverableToolErrorMatchers = exports.fatalRuntimeErrorMatchers = exports.openCodeWorkingFreshWindowMs = exports.claudeWorkingFreshWindowMs = exports.genericWorkingFreshWindowMs = exports.claudeSpawnGraceMs = exports.cachedActivityWindowMs = exports.stickyWorkingWindowMs = exports.commandWarmupWindowMs = exports.recentErrorSignalWindowMs = exports.parsedStrongEvidenceWindowMs = void 0;
3
+ exports.recoverableToolErrorMatchers = exports.fatalRuntimeErrorMatchers = exports.openCodeWorkingFreshWindowMs = exports.claudeWorkingFreshWindowMs = exports.genericWorkingFreshWindowMs = exports.openCodeSpawnGraceMs = exports.claudeSpawnGraceMs = exports.cachedActivityWindowMs = exports.stickyWorkingWindowMs = exports.commandWarmupWindowMs = exports.recentErrorSignalWindowMs = exports.parsedStrongEvidenceWindowMs = void 0;
4
4
  const parsedStrongEvidenceWindowMs = 8_000;
5
5
  exports.parsedStrongEvidenceWindowMs = parsedStrongEvidenceWindowMs;
6
6
  const recentErrorSignalWindowMs = 15_000;
@@ -13,6 +13,8 @@ const cachedActivityWindowMs = 12_000;
13
13
  exports.cachedActivityWindowMs = cachedActivityWindowMs;
14
14
  const claudeSpawnGraceMs = 5_000;
15
15
  exports.claudeSpawnGraceMs = claudeSpawnGraceMs;
16
+ const openCodeSpawnGraceMs = 5_000;
17
+ exports.openCodeSpawnGraceMs = openCodeSpawnGraceMs;
16
18
  const genericWorkingFreshWindowMs = 12_000;
17
19
  exports.genericWorkingFreshWindowMs = genericWorkingFreshWindowMs;
18
20
  const claudeWorkingFreshWindowMs = 10_000;
@@ -137,4 +137,17 @@ function createContext(overrides = {}) {
137
137
  (0, vitest_1.expect)(decision.activityTool).toBeUndefined();
138
138
  (0, vitest_1.expect)(decision.reasons[0]?.code).toBe("opencode-prompt-idle");
139
139
  });
140
+ (0, vitest_1.it)("returns idle for freshly spawned OpenCode sessions within grace window", () => {
141
+ const decision = (0, decision_1.deriveWorkerStatusDecision)(createContext({
142
+ isOpenCodeSession: true,
143
+ currentCommand: "opencode",
144
+ commandLower: "opencode",
145
+ hasOpenCodePromptSignal: false,
146
+ hasOpenCodeActiveSignal: false,
147
+ commandQuietForMs: 500,
148
+ workerAgeMs: 2_000
149
+ }));
150
+ (0, vitest_1.expect)(decision.status).toBe("idle");
151
+ (0, vitest_1.expect)(decision.reasons.some((r) => r.code === "opencode-spawn-grace-idle")).toBe(true);
152
+ });
140
153
  });
@@ -41,6 +41,19 @@ function detectIdleBlocker(context, evidence) {
41
41
  }
42
42
  };
43
43
  }
44
+ if (context.isOpenCodeSession &&
45
+ context.workerAgeMs <= constants_1.openCodeSpawnGraceMs &&
46
+ context.transcriptSnapshot?.status !== "working" &&
47
+ !context.hasOpenCodeActiveSignal &&
48
+ !evidence.parsedStrongSignal) {
49
+ return {
50
+ reason: {
51
+ code: "opencode-spawn-grace-idle",
52
+ message: "During early OpenCode spawn grace window without active signals.",
53
+ detail: `${Math.round(context.workerAgeMs)}ms since worker creation`
54
+ }
55
+ };
56
+ }
44
57
  const activeWindowMs = (0, helpers_1.statusFreshnessWindowMs)(context);
45
58
  if (context.outputQuietForMs > activeWindowMs && context.transcriptSnapshot?.status !== "working") {
46
59
  return {
@@ -183,15 +183,14 @@ class TmuxAdapter {
183
183
  }
184
184
  const normalizedText = text.replace(/\r\n?/g, "\n");
185
185
  if (normalizedText.length > 0) {
186
- const lines = normalizedText.split("\n");
187
- for (let index = 0; index < lines.length; index += 1) {
188
- const line = lines[index] ?? "";
189
- if (line.length > 0) {
190
- await this.runTmux(["send-keys", "-t", target, "-l", line]);
191
- }
192
- if (index < lines.length - 1) {
193
- await this.runTmux(["send-keys", "-t", target, "Enter"]);
194
- }
186
+ const isMultiline = normalizedText.includes("\n");
187
+ if (isMultiline) {
188
+ await this.runTmux(["send-keys", "-t", target, "-l", "\x1b[200~"]);
189
+ await this.runTmux(["send-keys", "-t", target, "-l", normalizedText]);
190
+ await this.runTmux(["send-keys", "-t", target, "-l", "\x1b[201~"]);
191
+ }
192
+ else {
193
+ await this.runTmux(["send-keys", "-t", target, "-l", normalizedText]);
195
194
  }
196
195
  }
197
196
  if (options?.submit ?? true) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-agents",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Local-first visual control room for tmux-backed coding agents",
5
5
  "bin": {
6
6
  "arcane-agents": "dist/server/server/cli.js"