@addai/node 0.30.2 → 0.30.4

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.
@@ -76,7 +76,14 @@ function runCmd(cmd, args, timeoutMs = 5000) {
76
76
  // a single failing probe must not take down the whole heartbeat.
77
77
  try {
78
78
  const inv = (0, win_1.resolveCliInvocation)(cmd, args);
79
- (0, child_process_1.execFile)(inv.file, inv.args, { timeout: timeoutMs, maxBuffer: 256 * 1024 }, (err, stdout, stderr) => {
79
+ // windowsHide: this probe runs for every harness on every heartbeat, so
80
+ // without it a Windows node flashes a console window several times a
81
+ // minute, for ever. stderr is captured here rather than discarded — a
82
+ // probe's error text is the diagnosis when a harness is present but
83
+ // broken, unlike the "not found" that whereLookup drops.
84
+ (0, child_process_1.execFile)(inv.file, inv.args, {
85
+ timeout: timeoutMs, maxBuffer: 256 * 1024, windowsHide: true,
86
+ }, (err, stdout, stderr) => {
80
87
  resolve({ ok: !err, stdout: (stdout ?? '').trim(), stderr: (stderr ?? '').trim() });
81
88
  });
82
89
  }
@@ -86,7 +86,13 @@ function findClaudeBinary() {
86
86
  // (extensionless bash script, .cmd, .ps1). Prefer a native .exe
87
87
  // (installer builds), then .cmd — never the extensionless one,
88
88
  // which CreateProcess can't run.
89
- const lines = (0, child_process_1.execSync)('where claude', { encoding: 'utf-8', timeout: 5000 })
89
+ // windowsHide and a discarded stderr see whereLookup in win.ts for
90
+ // why. execSync goes through cmd.exe, so a miss here costs a console
91
+ // window AND a line of noise in daemon.log every time it runs.
92
+ const lines = (0, child_process_1.execSync)('where claude', {
93
+ encoding: 'utf-8', timeout: 5000, windowsHide: true,
94
+ stdio: ['ignore', 'pipe', 'ignore'],
95
+ })
90
96
  .split(/\r?\n/)
91
97
  .map(l => l.trim())
92
98
  .filter(Boolean);
@@ -181,6 +181,9 @@ function spawnClaudePrint(input) {
181
181
  cwd: input.workingDirectory,
182
182
  env: { ...process.env, TERM: 'dumb' },
183
183
  stdio: ['ignore', 'pipe', 'pipe'],
184
+ // No console window. This is headless work on somebody's machine —
185
+ // on Windows a spawn without this flashes a terminal every run.
186
+ windowsHide: true,
184
187
  });
185
188
  }
186
189
  catch (err) {
@@ -301,6 +301,9 @@ function spawnCodex(input) {
301
301
  cwd: input.workingDirectory,
302
302
  env: childEnv,
303
303
  stdio: ['ignore', 'pipe', 'pipe'],
304
+ // No console window. This is headless work on somebody's machine —
305
+ // on Windows a spawn without this flashes a terminal every run.
306
+ windowsHide: true,
304
307
  });
305
308
  }
306
309
  catch (err) {
@@ -87,9 +87,66 @@ exports.WRAPPER_PATH = '/usr/local/bin/browser';
87
87
  * so the loop cannot be rebuilt even if something else recreates the shape.
88
88
  */
89
89
  exports.WRAPPER_MARKER = 'Written by @addai/node';
90
+ /**
91
+ * The first-run flow, off.
92
+ *
93
+ * ⚠️ Without these you do not get a browser. You get Chrome's first-run
94
+ * DIALOGS — the terms of service, then "make Chrome your default" — each one a
95
+ * separate top-level X window. Over xpra, which forwards windows one at a
96
+ * time, that is what lands on somebody's desktop: a 610x564 dialog, then a
97
+ * 490x162 one, and never the browser behind them.
98
+ *
99
+ * Worse, they are modal and they are traps. Escape on the terms dialog means
100
+ * DECLINE, and Chrome exits — so "close that popup" closed the whole thing,
101
+ * and an entity typing into what it assumed was a search box was typing into
102
+ * a consent prompt.
103
+ *
104
+ * Every container starts with a fresh profile, so this happened on every
105
+ * desktop, every time, and only ever looked like the browser being broken.
106
+ *
107
+ * The crash bubble is the same class of problem: any unclean exit — a rebuild,
108
+ * a killed container — and the next launch opens with "Restore pages?" over
109
+ * the top of whatever was asked for.
110
+ *
111
+ * Verified on a live container: with these flags the first window is
112
+ * 1918x1078 with a tab strip and an address bar, instead of a dialog.
113
+ */
114
+ const FIRST_RUN = ' --no-first-run --no-default-browser-check --hide-crash-restore-bubble';
90
115
  /** The in-container script that signs the browser in. Beside the extension,
91
116
  * under the same bind mount, so it arrives by the same route. */
92
117
  exports.SEED_SCRIPT = '/conf/vault/vault-seed.mjs';
118
+ /**
119
+ * Chrome's profile, on the mount, and named.
120
+ *
121
+ * ⚠️ This one flag fixes three separate things, which is why it is worth the
122
+ * one-off cost below.
123
+ *
124
+ * THE DEBUG PORT. Chrome has refused --remote-debugging-port on the DEFAULT
125
+ * user data directory since M136. We have always passed the port and never
126
+ * passed a directory, so the port has never listened — verified on a live
127
+ * container, where Chrome's own command line carried
128
+ * --remote-debugging-port=9222 while nothing was bound to 9222. Every reason
129
+ * the wrapper exists — +Ai Learn watching which control was clicked, the vault
130
+ * seeder attaching — has therefore been quietly dead on modern Chrome.
131
+ *
132
+ * THE PROFILE. Chrome's default lives in the container's writable layer, which
133
+ * a rebuild destroys. So every login an entity has ever made was thrown away
134
+ * the next time its desktop was rebuilt. /conf is bind-mounted from the host
135
+ * (provider.ts publishes it), so a profile here SURVIVES — which is the first
136
+ * time that has been true.
137
+ *
138
+ * THE BOT WALL. A profile with no history and no cookies is a brand-new
139
+ * stranger, and Google treats it like one. That is a large part of why an
140
+ * entity meets a CAPTCHA on an ordinary search. A profile that persists earns
141
+ * reputation instead of resetting it daily.
142
+ *
143
+ * ⚠️ THE ONE-OFF COST, stated plainly: this abandons whatever is in the
144
+ * current default profile, so an entity signed into something appears signed
145
+ * out, once. That profile was already destroyed on every rebuild, so nothing
146
+ * durable is being lost — the loss has simply been happening quietly, and this
147
+ * is the last time.
148
+ */
149
+ const PROFILE_DIR = '/conf/browser-profile';
93
150
  /**
94
151
  * The wrapper, exactly.
95
152
  *
@@ -117,7 +174,7 @@ function wrapperScript(realBrowser, extensionDir) {
117
174
  return `#!/bin/sh
118
175
  # Written by @addai/node so +Ai Learn can watch this browser${extensionDir ? ' and the +Ai Vault can fill logins in it' : ''}.
119
176
  # Removing it costs you the page detail in a lesson${extensionDir ? ' and vault autofill' : ''} and nothing else.
120
- ${seed}exec ${realBrowser} --remote-debugging-port=${exports.DEBUG_PORT} --remote-debugging-address=127.0.0.1${ext} "$@"
177
+ ${seed}exec ${realBrowser} --user-data-dir=${PROFILE_DIR} --remote-debugging-port=${exports.DEBUG_PORT} --remote-debugging-address=127.0.0.1${FIRST_RUN}${ext} "$@"
121
178
  `;
122
179
  }
123
180
  /** Is what is on disk already what we would write? Exact match, so a change to
@@ -357,6 +357,9 @@ function spawnGemini(input) {
357
357
  cwd: input.workingDirectory,
358
358
  env: childEnv,
359
359
  stdio: ['ignore', 'pipe', 'pipe'],
360
+ // No console window. This is headless work on somebody's machine —
361
+ // on Windows a spawn without this flashes a terminal every run.
362
+ windowsHide: true,
360
363
  });
361
364
  }
362
365
  catch (err) {
@@ -66,7 +66,9 @@ async function applyGitIdentity(dir, requestId, token) {
66
66
  if (!ident?.configured || !ident.author_name || !ident.author_email)
67
67
  return;
68
68
  const git = (args) => new Promise((resolve) => {
69
- (0, child_process_1.execFile)('git', ['-C', dir, ...args], { timeout: 10_000 }, () => resolve());
69
+ (0, child_process_1.execFile)('git', ['-C', dir, ...args],
70
+ // Headless: no console window on Windows.
71
+ { timeout: 10_000, windowsHide: true }, () => resolve());
70
72
  });
71
73
  // Repo-local only — never touch the machine's global config, which other
72
74
  // entities and the human owner share.
@@ -349,6 +349,9 @@ function spawnGrok(input) {
349
349
  cwd: input.workingDirectory,
350
350
  env: childEnv,
351
351
  stdio: ['ignore', 'pipe', 'pipe'],
352
+ // No console window. This is headless work on somebody's machine —
353
+ // on Windows a spawn without this flashes a terminal every run.
354
+ windowsHide: true,
352
355
  });
353
356
  }
354
357
  catch (err) {
@@ -390,6 +390,9 @@ function spawnKimi(input) {
390
390
  cwd: input.workingDirectory,
391
391
  env: childEnv,
392
392
  stdio: ['ignore', 'pipe', 'pipe'],
393
+ // No console window. This is headless work on somebody's machine —
394
+ // on Windows a spawn without this flashes a terminal every run.
395
+ windowsHide: true,
393
396
  });
394
397
  }
395
398
  catch (err) {
package/dist/projects.js CHANGED
@@ -105,7 +105,12 @@ function projectDir(p) {
105
105
  }
106
106
  function run(cmd, args, opts = {}) {
107
107
  return new Promise(resolve => {
108
- (0, child_process_1.execFile)(cmd, args, { cwd: opts.cwd, timeout: opts.timeoutMs ?? 5 * 60_000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
108
+ (0, child_process_1.execFile)(cmd, args, {
109
+ cwd: opts.cwd, timeout: opts.timeoutMs ?? 5 * 60_000,
110
+ maxBuffer: 4 * 1024 * 1024,
111
+ // Headless: no console window on Windows.
112
+ windowsHide: true,
113
+ }, (err, stdout, stderr) => {
109
114
  resolve({ ok: !err, stdout: stdout.toString(), stderr: stderr.toString() });
110
115
  });
111
116
  });
package/dist/win.js CHANGED
@@ -91,7 +91,19 @@ function whereLookup(name) {
91
91
  if (!exports.IS_WINDOWS)
92
92
  return null;
93
93
  try {
94
- const out = (0, child_process_1.execFileSync)('where', [name], { encoding: 'utf8', timeout: 5000 });
94
+ // ⚠️ windowsHide, and stderr thrown away. Both for the same reason: this
95
+ // runs for every CLI we look for, on a timer, and most of them are not
96
+ // installed. Without windowsHide each miss flashes a console window on
97
+ // the user's screen; without the stdio the miss writes
98
+ //
99
+ // INFO: Could not find files for the given pattern(s).
100
+ //
101
+ // to our stderr, and daemon.log fills with nothing else. "Not found" is
102
+ // the return value here, not an event worth reporting.
103
+ const out = (0, child_process_1.execFileSync)('where', [name], {
104
+ encoding: 'utf8', timeout: 5000, windowsHide: true,
105
+ stdio: ['ignore', 'pipe', 'ignore'],
106
+ });
95
107
  const lines = out.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
96
108
  const pick = lines.find(l => /\.exe$/i.test(l)) ??
97
109
  lines.find(l => /\.cmd$/i.test(l)) ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.30.2",
3
+ "version": "0.30.4",
4
4
  "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
5
  "license": "MIT",
6
6
  "keywords": [