@addai/node 0.24.0 → 0.25.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.
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ // Execute one claimed flow run on this machine.
3
+ //
4
+ // The engine does the walking. What this file owns is the part the engine
5
+ // deliberately knows nothing about: turning a database row into a graph call,
6
+ // keeping the server informed while it runs, and writing exactly one terminal
7
+ // answer at the end.
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.runFlow = runFlow;
43
+ const os = __importStar(require("os"));
44
+ const engine_loader_1 = require("./engine-loader");
45
+ const supabase_client_1 = require("../supabase-client");
46
+ const store_1 = require("../store");
47
+ const host_1 = require("./host");
48
+ /**
49
+ * What the engine's result means for the run row.
50
+ *
51
+ * An end condition outranks the walk's own verdict: a flow that hit a goal
52
+ * "completed" as far as the graph is concerned, but goal and exit are the
53
+ * answers the runs list and the stats are counted on, and a node run that
54
+ * reported plain 'completed' for a goal would quietly skew both.
55
+ */
56
+ function resolveStatus(result) {
57
+ const endType = result?.endConditionResult?.type;
58
+ if (endType === 'goal')
59
+ return 'goal';
60
+ if (endType === 'exit')
61
+ return 'exit';
62
+ switch (result?.status) {
63
+ case 'completed': return 'completed';
64
+ case 'cancelled': return 'cancelled';
65
+ case 'paused': return 'paused';
66
+ case 'failed': return 'failed';
67
+ default: return 'completed';
68
+ }
69
+ }
70
+ async function complete(run, status, fields = {}) {
71
+ const pairing = (0, store_1.readPairing)();
72
+ if (!pairing)
73
+ return;
74
+ try {
75
+ await (0, supabase_client_1.rpc)('flow_run_complete', {
76
+ p_token: pairing.daemonToken,
77
+ p_run_id: run.id,
78
+ p_status: status,
79
+ p_node_outputs: fields.nodeOutputs ?? null,
80
+ p_error: fields.error ?? null,
81
+ p_current_node_id: fields.currentNodeId ?? null,
82
+ p_metadata: fields.metadata ?? null,
83
+ });
84
+ }
85
+ catch (err) {
86
+ // The run is finished on this machine either way. Losing this write means
87
+ // the server's reclaim sweep eventually requeues it, which is the correct
88
+ // fallback — better a second attempt than a run stuck 'running' forever.
89
+ console.error(`[flows] could not report ${status} [run=${run.id}]:`, err.message);
90
+ }
91
+ }
92
+ /**
93
+ * Run it. Resolves when the run has reached a terminal or paused state and
94
+ * that state has been reported; never throws, because the pump's slot
95
+ * accounting is built on this settling exactly once.
96
+ */
97
+ async function runFlow(run) {
98
+ const label = `${run.flow_id?.slice(0, 8) ?? '?'} run=${run.id.slice(0, 8)}`;
99
+ const started = Date.now();
100
+ console.log(`[flows] starting ${label}`);
101
+ // Resuming: the row carries what already ran, so a run that paused on a form
102
+ // and came back does not re-execute the nodes before it. This is also what
103
+ // makes a run survive being handed to a different machine mid-flight.
104
+ const previousOutputs = run.node_outputs || {};
105
+ const previouslyExecuted = new Set(Object.keys(previousOutputs));
106
+ let lastNodeId = run.current_node_id;
107
+ try {
108
+ // Inside the try, deliberately. If the engine cannot load — a broken
109
+ // install, a native sandbox that never built — this run has to be marked
110
+ // failed with that message on it. Loading above the try would throw past
111
+ // every handler below and leave the run sitting 'running' until the
112
+ // server's reclaim sweep noticed, three minutes later, with no reason
113
+ // attached.
114
+ //
115
+ // The error classes must come from the loaded module too: `instanceof`
116
+ // against a separately imported copy would never match, and every paused
117
+ // flow would be recorded as a failure.
118
+ const { executeWorkflowGraph, FlowPausedError, FlowCancelledError } = await (0, engine_loader_1.loadEngine)();
119
+ const result = await executeWorkflowGraph({
120
+ nodes: run.nodes || [],
121
+ edges: run.edges || [],
122
+ inputData: run.trigger_data || {},
123
+ // Where to pick up. A fresh run has none and starts at its trigger; a
124
+ // run the cloud handed back after a form or a wait carries the node it
125
+ // stopped at. Without this a resumed flow would walk from the top —
126
+ // previouslyExecuted stops it re-running anything, but a flow whose
127
+ // trigger is a webhook has nothing to walk from.
128
+ startFromNode: run.resume_from_node_id || null,
129
+ previousOutputs,
130
+ previouslyExecuted,
131
+ endConditions: null,
132
+ hooks: {
133
+ onNodeStarted: (node) => {
134
+ lastNodeId = node?.id ?? lastNodeId;
135
+ },
136
+ onNodeCompleted: (node, output) => {
137
+ (0, host_1.reportNodeLog)(run, node, 'success', output);
138
+ },
139
+ onNodeError: (node, error) => {
140
+ (0, host_1.reportNodeLog)(run, node, 'error', {
141
+ error: error?.message || String(error),
142
+ success: false,
143
+ });
144
+ },
145
+ },
146
+ context: {
147
+ handlers: await (0, host_1.buildRegistry)(run),
148
+ workspaceId: run.workspace_id,
149
+ flowRunId: run.id,
150
+ flowId: run.flow_id,
151
+ versionId: run.aiflow_version_id,
152
+ isTest: run.is_test === true,
153
+ variables: run.resolved_variables || [],
154
+ // Cancellation. The engine asks between nodes, so a cancel from the
155
+ // runs list stops this machine at the next boundary rather than after
156
+ // the whole flow. Failures here answer "unknown" rather than throwing:
157
+ // a blip talking to Supabase must not kill a healthy run.
158
+ checkRunStatus: async (rid) => {
159
+ try {
160
+ const pairing = (0, store_1.readPairing)();
161
+ if (!pairing)
162
+ return undefined;
163
+ return await (0, supabase_client_1.rpc)('flow_run_status', {
164
+ p_token: pairing.daemonToken,
165
+ p_run_id: rid,
166
+ }) ?? undefined;
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ },
172
+ },
173
+ });
174
+ const status = resolveStatus(result);
175
+ await complete(run, status, {
176
+ nodeOutputs: result?.nodeOutputs ?? null,
177
+ // A flow can fail without throwing: the engine catches a handled node
178
+ // error and RETURNS status 'failed' with the reason on `error`. Missing
179
+ // it meant a node run showed "failed" in the runs list with nothing
180
+ // beside it, which is the least useful thing a failed run can say.
181
+ error: result?.error ?? null,
182
+ currentNodeId: lastNodeId,
183
+ metadata: {
184
+ // So the run view can say which machine ran it, and why it is not in
185
+ // the cloud's logs.
186
+ ran_on: 'node',
187
+ runtime_hostname: os.hostname(),
188
+ duration_ms: Date.now() - started,
189
+ },
190
+ });
191
+ console.log(`[flows] ${status} ${label} in ${((Date.now() - started) / 1000).toFixed(1)}s`);
192
+ }
193
+ catch (err) {
194
+ // A pause is a normal outcome, not a failure: the flow is sitting on a
195
+ // form, a webhook or a timer. It releases the slot and the claim; whichever
196
+ // machine is free when it resumes picks it up.
197
+ //
198
+ // Named rather than `instanceof` because the classes live inside the try's
199
+ // scope, and because a run that failed to load the engine at all has no
200
+ // classes to compare against.
201
+ const kind = err?.constructor?.name;
202
+ if (kind === 'FlowPausedError') {
203
+ await complete(run, 'waiting', {
204
+ nodeOutputs: err?.nodeOutputs ?? null,
205
+ currentNodeId: lastNodeId,
206
+ });
207
+ console.log(`[flows] waiting ${label}`);
208
+ return;
209
+ }
210
+ if (kind === 'FlowCancelledError') {
211
+ await complete(run, 'cancelled', { currentNodeId: lastNodeId });
212
+ console.log(`[flows] cancelled ${label}`);
213
+ return;
214
+ }
215
+ const message = err?.message || String(err);
216
+ await complete(run, 'failed', { error: message, currentNodeId: lastNodeId });
217
+ console.error(`[flows] failed ${label}: ${message}`);
218
+ }
219
+ }
@@ -104,8 +104,8 @@ function writeGeminiWorkspaceSettings(cwd, servers) {
104
104
  ...(s.env && Object.keys(s.env).length > 0 ? { env: s.env } : {}),
105
105
  };
106
106
  }
107
- // Start with the host's settings so auth method (security.auth.selectedType
108
- // = oauth-personal etc.) and theme carry through. Override just mcpServers.
107
+ // Start with the host's settings so auth method and theme carry through.
108
+ // Override just mcpServers and the auth type, below.
109
109
  const hostSettingsPath = path.join(os.homedir(), '.gemini', 'settings.json');
110
110
  let baseSettings = {};
111
111
  if (fs.existsSync(hostSettingsPath)) {
@@ -115,6 +115,24 @@ function writeGeminiWorkspaceSettings(cwd, servers) {
115
115
  catch { /* corrupt — start fresh */ }
116
116
  }
117
117
  const merged = { ...baseSettings, mcpServers };
118
+ // Google retired individual Gemini Code Assist OAuth on 18 June 2026: a CLI
119
+ // set to 'oauth-personal' now answers IneligibleTierError and points at
120
+ // Antigravity, whatever else is configured. Most hosts still carry that
121
+ // retired value in ~/.gemini/settings.json, so inheriting it verbatim means
122
+ // the run cannot start.
123
+ //
124
+ // An API key is the supported path that survives (as is vertex-ai for
125
+ // enterprise), so when one is present the auth type is forced to match it
126
+ // rather than trusting whatever the host was left set to. childEnv already
127
+ // passes GEMINI_API_KEY through from the daemon's own environment.
128
+ if (process.env.GEMINI_API_KEY) {
129
+ const security = { ...(baseSettings.security ?? {}) };
130
+ security.auth = {
131
+ ...(security.auth ?? {}),
132
+ selectedType: 'gemini-api-key',
133
+ };
134
+ merged.security = security;
135
+ }
118
136
  const settingsPath = path.join(dir, 'settings.json');
119
137
  fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2), { mode: 0o600 });
120
138
  return settingsPath;
@@ -274,6 +292,40 @@ function spawnGemini(input) {
274
292
  if (input.resumeSessionId && isGeminiSessionId(input.resumeSessionId)) {
275
293
  args.push('--resume', input.resumeSessionId);
276
294
  }
295
+ // Fail early and legibly when there is nothing to authenticate with. Without
296
+ // this the run dies inside gemini with "IneligibleTierError: This client is no
297
+ // longer supported for Gemini Code Assist for individuals", which reads like
298
+ // an account problem rather than a missing key on this machine.
299
+ if (!process.env.GEMINI_API_KEY) {
300
+ const host = path.join(os.homedir(), '.gemini', 'settings.json');
301
+ let selected = 'oauth-personal';
302
+ try {
303
+ const raw = JSON.parse(fs.readFileSync(host, 'utf8'));
304
+ selected = raw.security?.auth?.selectedType ?? selected;
305
+ }
306
+ catch { /* no host settings — the default is the retired one anyway */ }
307
+ if (selected === 'oauth-personal') {
308
+ const events = (0, events_1.bufferedEvents)([
309
+ {
310
+ type: 'error',
311
+ code: 'gemini_auth_retired',
312
+ message: 'gemini cannot start on this machine: Google retired individual ' +
313
+ 'Gemini Code Assist OAuth on 18 June 2026, and this host is still set ' +
314
+ "to 'oauth-personal'. Set GEMINI_API_KEY on the node (or switch the " +
315
+ 'host to vertex-ai for an enterprise licence), or point this entity at ' +
316
+ 'another agent.',
317
+ },
318
+ { type: 'turn_complete', stopReason: 'failed' },
319
+ ]);
320
+ return {
321
+ pid: undefined,
322
+ sessionId: null,
323
+ kill: () => { },
324
+ onEvent: events.onEvent,
325
+ done: Promise.resolve(1),
326
+ };
327
+ }
328
+ }
277
329
  // No HOME override — gemini's auth + shell-out tools all need the
278
330
  // real $HOME. Workspace-scope settings.json (written above) is enough
279
331
  // to override the host's mcpServers without poisoning everything else.
@@ -74,7 +74,6 @@ const grok_binary_1 = require("./grok-binary");
74
74
  const win_1 = require("./win");
75
75
  const events_1 = require("./events");
76
76
  const think_split_1 = require("./think-split");
77
- const mcp_headers_1 = require("./mcp-headers");
78
77
  /** Real config dir for the logged-in account. GROK_HOME points here so the
79
78
  * entity uses the same account and token refresh works. */
80
79
  function realGrokHome() {
@@ -161,30 +160,16 @@ function writeProjectMcpConfig(workingDirectory, servers) {
161
160
  }
162
161
  const lines = [];
163
162
  for (const s of usable) {
164
- // Remote MCP servers. xAI documents [mcp_servers.<name>] with `url` and a
165
- // `headers` inline table, so grok now gets them properly instead of the
166
- // skip this used to do a skip that was safe (better than emitting
167
- // `command = "http"`, which grok would try to exec) but meant an entity
168
- // running on grok silently had none of its remote servers.
169
- //
170
- // Folder trust still gates repo-local config.toml servers, so the --trust
171
- // launch flag remains load-bearing for these entries to start at all.
172
- if ((0, mcp_headers_1.isRemoteMcp)(s.command)) {
173
- const url = (0, mcp_headers_1.remoteMcpUrl)(s.args);
174
- if (!url) {
175
- console.error(`[grok] MCP ${s.slug}: ${s.command} server has no URL — skipped`);
176
- continue;
177
- }
178
- lines.push(`[mcp_servers.${tomlStr(s.slug)}]`);
179
- lines.push(`url = ${tomlStr(url)}`);
180
- const headers = Object.entries((0, mcp_headers_1.remoteMcpHeaders)(s.env));
181
- if (headers.length > 0) {
182
- const kv = headers.map(([k, v]) => `${tomlStr(k)} = ${tomlStr(v)}`).join(', ');
183
- lines.push(`headers = { ${kv} }`);
184
- }
185
- lines.push('enabled = true');
186
- lines.push('startup_timeout_sec = 30');
187
- lines.push('');
163
+ // Remote MCP servers (registry command='http'|'sse', args=[url]) have no
164
+ // verified grok config spelling, and writing the row verbatim is worse
165
+ // than skipping: grok would try to exec a binary called `http`, the
166
+ // server would never start, and its tools would vanish with no error
167
+ // the exact silent failure that cost an entity its GitHub access after a
168
+ // provider-ladder hop. Skip it, and say so loudly enough to be findable.
169
+ if (s.command === 'http' || s.command === 'sse') {
170
+ console.error(`[grok] MCP ${s.slug}: remote ${s.command} servers are not supported by the grok ` +
171
+ `adapter skipping. Its tools will be ABSENT from this run. Use claude (or codex, ` +
172
+ `which supports url + bearer_token_env_var) for entities that depend on it.`);
188
173
  continue;
189
174
  }
190
175
  // `cmd /c` wrapper for npm-shim commands on native Windows (no-op on POSIX).
package/dist/heartbeat.js CHANGED
@@ -13,6 +13,8 @@ const supabase_client_1 = require("./supabase-client");
13
13
  const store_1 = require("./store");
14
14
  const capabilities_1 = require("./capabilities");
15
15
  const request_pump_1 = require("./request-pump");
16
+ const pump_1 = require("./flows/pump");
17
+ const manager_1 = require("./desktop/manager");
16
18
  const config_1 = require("./config");
17
19
  let timer = null;
18
20
  let stopped = false;
@@ -59,6 +61,19 @@ async function tick() {
59
61
  // whatever it already had (its own default of 4).
60
62
  if (res && res.max_concurrent_runs != null)
61
63
  (0, request_pump_1.setMaxConcurrent)(res.max_concurrent_runs);
64
+ // Flows and desktops ride the same heartbeat for the same reason: they are
65
+ // fields on this node's own row and we are already talking every 30s.
66
+ //
67
+ // Each is applied only when the server actually said something. An older
68
+ // server that has not had the migration says nothing, and nothing must
69
+ // read as "leave it alone" — never as "turn flows off" (which would
70
+ // silently strand a queue) or "cap desktops at zero".
71
+ if (res && typeof res.flows_enabled === 'boolean')
72
+ (0, pump_1.setFlowsEnabled)(res.flows_enabled);
73
+ if (res && res.max_concurrent_flows != null)
74
+ (0, pump_1.setMaxConcurrentFlows)(res.max_concurrent_flows);
75
+ if (res && res.max_concurrent_desktops != null)
76
+ (0, manager_1.setMaxConcurrentDesktops)(res.max_concurrent_desktops);
62
77
  }
63
78
  catch (err) {
64
79
  if (err instanceof supabase_client_1.RpcError && err.status === 401) {
@@ -124,6 +139,15 @@ async function runTick() {
124
139
  p_token: pairing.daemonToken,
125
140
  p_request_ids: (0, request_pump_1.activeRequestIdList)(),
126
141
  });
142
+ // Flow runs are claimed rows too, and the server's reclaim sweep is just
143
+ // as capable of requeueing a healthy one that happens to be quiet. Sent in
144
+ // the same tick so the two never drift apart under load; the empty call
145
+ // matters, because it is what tells the server this build reports flow
146
+ // runs at all.
147
+ await (0, supabase_client_1.rpc)('flow_run_heartbeat', {
148
+ p_token: pairing.daemonToken,
149
+ p_run_ids: (0, pump_1.activeFlowRunIds)(),
150
+ });
127
151
  lastRunBeatError = '';
128
152
  }
129
153
  catch (err) {
package/dist/index.js CHANGED
@@ -56,6 +56,7 @@ const autostart_1 = require("./autostart");
56
56
  const sleep_detector_1 = require("./sleep-detector");
57
57
  const claude_config_1 = require("./claude-config");
58
58
  const request_pump_1 = require("./request-pump");
59
+ const pump_1 = require("./flows/pump");
59
60
  const projects_1 = require("./projects");
60
61
  const manager_1 = require("./desktop/manager");
61
62
  const relay_client_1 = require("./desktop/relay-client");
@@ -210,6 +211,12 @@ async function start(argv = []) {
210
211
  (0, projects_1.start)();
211
212
  // …and to keep desktops and the containers behind them in agreement.
212
213
  (0, manager_1.startDesktopManager)();
214
+ // The flows pump starts idle and stays that way until the heartbeat reports
215
+ // this machine has flows switched on. Starting it unconditionally is right:
216
+ // the alternative is a daemon that only notices the switch on restart, and
217
+ // "turn it on, then restart the node" is not a setup step anyone should have
218
+ // to be told about.
219
+ (0, pump_1.start)();
213
220
  // …and dial out to the relay. That socket is this node's live link: the
214
221
  // server pushes work down it the instant it exists, so a command no longer
215
222
  // waits up to 30s for the next heartbeat to notice it. The heartbeat and
@@ -231,6 +238,7 @@ async function start(argv = []) {
231
238
  (0, manager_1.stopDesktopManager)();
232
239
  (0, relay_client_1.stopRelayClient)();
233
240
  (0, request_pump_1.stop)();
241
+ (0, pump_1.stop)();
234
242
  (0, heartbeat_1.stop)();
235
243
  (0, auto_update_1.stop)();
236
244
  sleepMonitor.stop();
@@ -241,10 +249,20 @@ async function start(argv = []) {
241
249
  // leave paused flow runs stuck `waiting` until server-side reclaim
242
250
  // (5 min) requeued or failed them.
243
251
  try {
244
- const { drained, timedOut } = await (0, request_pump_1.drain)(drainTimeoutMs);
245
- lastDrain = { drained, timedOut };
246
- if (drained || timedOut) {
247
- console.log(`[shutdown] drained ${drained} request(s); ${timedOut} still in-flight (timed out)`);
252
+ // Both pools drain against the SAME deadline, in parallel. Draining them
253
+ // one after the other would give a machine holding both up to twice the
254
+ // timeout to shut down, and the timeout is there because something
255
+ // upstream is waiting for this process to be gone.
256
+ const [requests, flows] = await Promise.all([
257
+ (0, request_pump_1.drain)(drainTimeoutMs),
258
+ (0, pump_1.drain)(drainTimeoutMs),
259
+ ]);
260
+ lastDrain = { drained: requests.drained, timedOut: requests.timedOut };
261
+ if (requests.drained || requests.timedOut) {
262
+ console.log(`[shutdown] drained ${requests.drained} request(s); ${requests.timedOut} still in-flight (timed out)`);
263
+ }
264
+ if (flows.drained || flows.timedOut) {
265
+ console.log(`[shutdown] drained ${flows.drained} flow run(s); ${flows.timedOut} still in-flight (timed out)`);
248
266
  }
249
267
  }
250
268
  catch (err) {
@@ -13,3 +13,50 @@ export declare function remoteMcpUrl(args?: string[] | null): string | null;
13
13
  * and must never be sent as a header.
14
14
  */
15
15
  export declare function remoteMcpHeaders(env?: Record<string, string> | null): Record<string, string>;
16
+ /**
17
+ * Which agents can actually open an HTTP MCP connection, measured against the
18
+ * real binaries rather than their documentation.
19
+ *
20
+ * claude YES — proven in production; entities use these every day.
21
+ * codex YES — `codex mcp list` shows a `url` entry under Url, enabled.
22
+ * kimi YES — `kimi mcp list` prints "<slug> (http): <url>".
23
+ * gemini YES — its shipped bundle treats httpUrl as a network transport
24
+ * (hasNetworkTransport), carries StreamableHTTPClientTransport,
25
+ * and documents "httpUrl (for Streamable HTTP), url (for SSE),
26
+ * and command (for stdio)" with a headers map. Read out of the
27
+ * installed bundle rather than the docs, because docs are what
28
+ * lied about grok. It was never watched connecting, though:
29
+ * gemini-cli cannot authenticate at all on a personal account
30
+ * since Google retired individual Gemini Code Assist OAuth on
31
+ * 18 June 2026 (IneligibleTierError -> migrate to Antigravity).
32
+ * API-key and enterprise auth are unaffected. So gemini's
33
+ * blocker is AUTH, not MCP transport — bridging it would fix
34
+ * nothing. No entity runs gemini today.
35
+ * grok NO — grok 1.0.3 SILENTLY IGNORES url-based entries. In one run it
36
+ * loaded ten stdio servers and skipped every remote one, ours
37
+ * and a third party's, with no error, while its own bundled
38
+ * README documents `url` + `headers`. The docs describe a
39
+ * transport the binary does not have.
40
+ *
41
+ * An agent listed here gets its remote servers bridged instead — see
42
+ * remoteMcpBridge. Add to this set only on evidence from the binary; the whole
43
+ * point is that "the docs say it works" is not evidence.
44
+ */
45
+ export declare const AGENTS_WITHOUT_HTTP_MCP: Set<string>;
46
+ export declare function agentNeedsMcpBridge(agent: string): boolean;
47
+ /**
48
+ * Turn a remote MCP server into a STDIO command, for agents that cannot speak
49
+ * HTTP MCP themselves.
50
+ *
51
+ * mcp-remote is the standard stdio<->HTTP shim, and the one grok's own README
52
+ * reaches for in its Linear example. Verified end to end against grok 1.0.3:
53
+ * the bridged server's tools appear and are callable.
54
+ *
55
+ * The cost is an npx start per remote server — exactly the cost moving to HTTP
56
+ * removed — so this is a fallback for the agents that need it, never the
57
+ * default path.
58
+ */
59
+ export declare function remoteMcpBridge(url: string, env?: Record<string, string> | null): {
60
+ command: string;
61
+ args: string[];
62
+ };
@@ -8,9 +8,12 @@
8
8
  // inline in install.ts (the claude path) and the other four adapters each got
9
9
  // it wrong in their own way, so it lives here now and they all call it.
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.AGENTS_WITHOUT_HTTP_MCP = void 0;
11
12
  exports.isRemoteMcp = isRemoteMcp;
12
13
  exports.remoteMcpUrl = remoteMcpUrl;
13
14
  exports.remoteMcpHeaders = remoteMcpHeaders;
15
+ exports.agentNeedsMcpBridge = agentNeedsMcpBridge;
16
+ exports.remoteMcpBridge = remoteMcpBridge;
14
17
  /** True when a registry row describes a remote MCP server rather than a
15
18
  * local process to spawn. */
16
19
  function isRemoteMcp(command) {
@@ -47,3 +50,58 @@ function remoteMcpHeaders(env) {
47
50
  }
48
51
  return headers;
49
52
  }
53
+ /**
54
+ * Which agents can actually open an HTTP MCP connection, measured against the
55
+ * real binaries rather than their documentation.
56
+ *
57
+ * claude YES — proven in production; entities use these every day.
58
+ * codex YES — `codex mcp list` shows a `url` entry under Url, enabled.
59
+ * kimi YES — `kimi mcp list` prints "<slug> (http): <url>".
60
+ * gemini YES — its shipped bundle treats httpUrl as a network transport
61
+ * (hasNetworkTransport), carries StreamableHTTPClientTransport,
62
+ * and documents "httpUrl (for Streamable HTTP), url (for SSE),
63
+ * and command (for stdio)" with a headers map. Read out of the
64
+ * installed bundle rather than the docs, because docs are what
65
+ * lied about grok. It was never watched connecting, though:
66
+ * gemini-cli cannot authenticate at all on a personal account
67
+ * since Google retired individual Gemini Code Assist OAuth on
68
+ * 18 June 2026 (IneligibleTierError -> migrate to Antigravity).
69
+ * API-key and enterprise auth are unaffected. So gemini's
70
+ * blocker is AUTH, not MCP transport — bridging it would fix
71
+ * nothing. No entity runs gemini today.
72
+ * grok NO — grok 1.0.3 SILENTLY IGNORES url-based entries. In one run it
73
+ * loaded ten stdio servers and skipped every remote one, ours
74
+ * and a third party's, with no error, while its own bundled
75
+ * README documents `url` + `headers`. The docs describe a
76
+ * transport the binary does not have.
77
+ *
78
+ * An agent listed here gets its remote servers bridged instead — see
79
+ * remoteMcpBridge. Add to this set only on evidence from the binary; the whole
80
+ * point is that "the docs say it works" is not evidence.
81
+ */
82
+ exports.AGENTS_WITHOUT_HTTP_MCP = new Set(['grok']);
83
+ function agentNeedsMcpBridge(agent) {
84
+ return exports.AGENTS_WITHOUT_HTTP_MCP.has(agent);
85
+ }
86
+ /**
87
+ * Turn a remote MCP server into a STDIO command, for agents that cannot speak
88
+ * HTTP MCP themselves.
89
+ *
90
+ * mcp-remote is the standard stdio<->HTTP shim, and the one grok's own README
91
+ * reaches for in its Linear example. Verified end to end against grok 1.0.3:
92
+ * the bridged server's tools appear and are callable.
93
+ *
94
+ * The cost is an npx start per remote server — exactly the cost moving to HTTP
95
+ * removed — so this is a fallback for the agents that need it, never the
96
+ * default path.
97
+ */
98
+ function remoteMcpBridge(url, env) {
99
+ const args = ['-y', 'mcp-remote', url];
100
+ for (const [key, value] of Object.entries(remoteMcpHeaders(env))) {
101
+ // Spawned without a shell, so "K:V" needs no quoting of its own.
102
+ args.push('--header', `${key}:${value}`);
103
+ }
104
+ // Our servers are streamable HTTP; skip the SSE fallback probing.
105
+ args.push('--transport', 'http-only');
106
+ return { command: 'npx', args };
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
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": [
@@ -49,6 +49,7 @@
49
49
  "node": ">=18"
50
50
  },
51
51
  "dependencies": {
52
+ "@addai/node-flows": "^1.0.0",
52
53
  "node-pty": "^1.1.0",
53
54
  "ws": "^8.21.3"
54
55
  },