arcroom 0.0.1-beta → 0.0.3-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agents.js CHANGED
@@ -112,7 +112,7 @@ export function opencodePlugin(entry, project, node = process.execPath) {
112
112
  import { spawnSync } from 'node:child_process';
113
113
 
114
114
  const run = (payload) => {
115
- const r = spawnSync(${JSON.stringify(node)}, ${args}, { input: JSON.stringify(payload), encoding: 'utf8' });
115
+ const r = spawnSync(${JSON.stringify(node)}, ${args}, { input: JSON.stringify(payload), encoding: 'utf8', windowsHide: true });
116
116
  try { return JSON.parse(r.stdout || '{}'); } catch { return {}; }
117
117
  };
118
118
 
package/dist/git.js CHANGED
@@ -31,6 +31,7 @@ export function git(args, opts = {}) {
31
31
  const bin = gitPath();
32
32
  if (!bin)
33
33
  return { status: null, stdout: undefined };
34
- const r = spawnSync(bin, [...SAFE, ...args], { encoding: 'utf8', ...opts });
34
+ // hooks run with no console of their own: without windowsHide each git call flashes a window on Windows
35
+ const r = spawnSync(bin, [...SAFE, ...args], { encoding: 'utf8', windowsHide: true, ...opts });
35
36
  return { status: r.status, stdout: r.stdout };
36
37
  }
package/dist/home.js CHANGED
@@ -41,6 +41,15 @@ export function loadConfig() {
41
41
  return { url: DEFAULT_URL, apiKey: '', ...(existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : {}) };
42
42
  }
43
43
  const LOCAL = new Set(['localhost', '127.0.0.1', '[::1]']);
44
+ /** Whether `url` points at this machine (a developer's local API). */
45
+ export const isLocalUrl = (url) => {
46
+ try {
47
+ return LOCAL.has(new URL(url).hostname);
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ };
44
53
  /** The API URL, refused unless it is https:// or this machine: the key and the traces never cross a network in clear text. */
45
54
  export function apiUrl(url) {
46
55
  const u = new URL(url);
@@ -54,11 +63,17 @@ export function apiUrl(url) {
54
63
  * secret to the team.
55
64
  */
56
65
  export async function api(cfg, path, body, timeoutMs = 10_000) {
57
- const res = await fetch(apiUrl(cfg.url) + path, {
66
+ const base = apiUrl(cfg.url);
67
+ const res = await fetch(base + path, {
58
68
  method: body ? 'POST' : 'GET',
59
69
  headers: { Authorization: `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' },
60
70
  body: body ? JSON.stringify(redact(body)) : undefined,
61
71
  signal: AbortSignal.timeout(timeoutMs),
72
+ }).catch((e) => {
73
+ // a timeout stays as it is (lookup and the MCP tools check for TimeoutError); a refused or unknown host reads as one line
74
+ const cause = e.cause;
75
+ const why = cause?.code ?? cause?.errors?.[0]?.code ?? cause?.message ?? e.message;
76
+ throw e.name === 'TypeError' ? new Error(`could not reach ${base} (${why})`) : e;
62
77
  });
63
78
  const json = (await res.json());
64
79
  // the key was revoked, its person removed or its account deleted: ArcRoom stops on this machine until a new key
package/dist/index.js CHANGED
@@ -13,6 +13,12 @@ import { mcp } from './mcp.js';
13
13
  import { readStats, statusText } from './signs.js';
14
14
  import { sessionUsage, tokensOf } from './usage.js';
15
15
  const [cmd, ...args] = process.argv.slice(2);
16
+ // A command someone typed that fails prints one line, not a stack trace (the hook, daemon and MCP server keep their own handling).
17
+ if (!['hook', 'daemon', 'mcp', 'status'].includes(cmd))
18
+ process.on('uncaughtException', (e) => {
19
+ console.error(`arcroom${cmd ? ` ${cmd}` : ''}: ${e.message}`);
20
+ process.exit(1);
21
+ });
16
22
  if (cmd === 'init')
17
23
  await init(args);
18
24
  else if (cmd === 'hook')
package/dist/init.js CHANGED
@@ -6,7 +6,7 @@ import { createInterface } from 'node:readline/promises';
6
6
  import { AGENTS, CURSOR_HOOKS, opencodePlugin } from './agents.js';
7
7
  import { stopDaemon } from './daemon.js';
8
8
  import { onPath } from './git.js';
9
- import { apiUrl, ARCROOM_HOME, DEFAULT_URL } from './home.js';
9
+ import { apiUrl, ARCROOM_HOME, DEFAULT_URL, isLocalUrl, keyRefused } from './home.js';
10
10
  const EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop', 'SessionEnd'];
11
11
  const TOOL_EVENTS = new Set(['PreToolUse', 'PostToolUse']);
12
12
  const MARK = '.arcroom/runtime'; // identifies our hook entries in a settings file
@@ -43,6 +43,21 @@ const writeJson = (file, value) => {
43
43
  mkdirSync(dirname(file), { recursive: true });
44
44
  writeFileSync(file, JSON.stringify(value, null, 2) + '\n');
45
45
  };
46
+ /**
47
+ * The key and API URL init uses. Re-running init reuses what was saved, except a key the API refused (otherwise a new
48
+ * key could never get in) and a local URL left from development (it moves to the live API and says so).
49
+ */
50
+ export function initChoice(o) {
51
+ const refused = !!o.saved.apiKey && keyRefused({ apiKey: o.saved.apiKey, url: o.saved.url ?? DEFAULT_URL });
52
+ const key = o.argKey ?? o.envKey ?? (refused ? undefined : o.saved.apiKey);
53
+ const dropLocal = !o.argUrl && !!o.saved.url && isLocalUrl(o.saved.url);
54
+ const url = o.argUrl ?? (dropLocal ? DEFAULT_URL : o.saved.url ?? DEFAULT_URL);
55
+ const notes = [
56
+ ...(refused && !o.argKey && !o.envKey ? ['The saved key was refused by the API. Paste a new key (web app, Settings → API keys).'] : []),
57
+ ...(dropLocal ? [`The saved API was ${o.saved.url}; using ${DEFAULT_URL} now (arcroom init --url ${o.saved.url} keeps a local API).`] : []),
58
+ ];
59
+ return { key, url, notes };
60
+ }
46
61
  export async function init(args) {
47
62
  const arg = (name) => (args.includes(name) ? args[args.indexOf(name) + 1] : undefined);
48
63
  const agent = (arg('--agent') ?? 'claude');
@@ -52,10 +67,13 @@ export async function init(args) {
52
67
  }
53
68
  const configFile = join(ARCROOM_HOME, 'config.json');
54
69
  const saved = existsSync(configFile) ? JSON.parse(readFileSync(configFile, 'utf8')) : {}; // re-running init reuses saved values
55
- let key = arg('--key') ?? process.env.ARCROOM_API_KEY ?? saved.apiKey;
70
+ const choice = initChoice({ argKey: arg('--key'), envKey: process.env.ARCROOM_API_KEY, argUrl: arg('--url'), saved });
71
+ let key = choice.key;
72
+ const url = choice.url;
56
73
  if (arg('--key'))
57
74
  console.error('Note: a key typed on the command line stays in your shell history. Next time run arcroom init and paste it when asked, or set ARCROOM_API_KEY.');
58
- const url = arg('--url') ?? saved.url ?? DEFAULT_URL;
75
+ for (const note of choice.notes)
76
+ console.error(note);
59
77
  try {
60
78
  apiUrl(url);
61
79
  }
@@ -138,9 +156,10 @@ function addLocalMcp(entry) {
138
156
  const run = (args) => claude.endsWith('.cmd') // an npm install on Windows: a batch file runs through cmd, every argument quoted
139
157
  ? spawnSync(process.env.ComSpec ?? 'cmd.exe', ['/d', '/s', '/c', `"${[claude, ...args].map((a) => `"${a}"`).join(' ')}"`], {
140
158
  windowsVerbatimArguments: true,
159
+ windowsHide: true,
141
160
  encoding: 'utf8',
142
161
  })
143
- : spawnSync(claude, args, { encoding: 'utf8' });
162
+ : spawnSync(claude, args, { encoding: 'utf8', windowsHide: true });
144
163
  run(['mcp', 'remove', '--scope', 'local', 'arcroom']); // none yet is fine
145
164
  return run(['mcp', 'add', '--scope', 'local', 'arcroom', '--', NODE, entry, 'mcp']).status === 0;
146
165
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcroom",
3
- "version": "0.0.1-beta",
3
+ "version": "0.0.3-beta",
4
4
  "description": "Shared team memory and run control for coding agents: Claude Code, Codex, Cursor, OpenCode",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ArcRoom AI",