@addai/node 0.24.0 → 0.24.1

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.
@@ -13,6 +13,20 @@ export interface ClaudePrintInput {
13
13
  /** Restrict to a specific set of tools (claude only). null/undefined = inherit. */
14
14
  allowedTools?: string[];
15
15
  }
16
+ /**
17
+ * The text to emit for one flushed block, given what came before.
18
+ *
19
+ * The final reply is rebuilt with string_agg(delta, '') — an empty join,
20
+ * because deltas are partial TOKENS and any separator between them would
21
+ * corrupt words mid-word. Correct within a block, wrong between them: the
22
+ * entity narrates, calls a tool, narrates again, and the two blocks own no
23
+ * whitespace of their own, so they arrive glued as "the chat tool.Let me".
24
+ * The boundary is known here and nowhere else, so it is marked here.
25
+ */
26
+ export declare function blockDelta(text: string, opts: {
27
+ afterTool: boolean;
28
+ hadTextBefore: boolean;
29
+ }): string;
16
30
  export interface ClaudePrintHandle {
17
31
  pid: number | undefined;
18
32
  sessionId: string | null;
@@ -5,11 +5,29 @@
5
5
  // our normalised RuntimeEvent shape, and resolves when the process exits.
6
6
  // No PTY needed — Claude detects non-TTY and runs headless.
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.blockDelta = blockDelta;
8
9
  exports.lineToEvents = lineToEvents;
9
10
  exports.spawnClaudePrint = spawnClaudePrint;
10
11
  const child_process_1 = require("child_process");
11
12
  const claude_binary_1 = require("./claude-binary");
12
13
  const win_1 = require("./win");
14
+ /**
15
+ * The text to emit for one flushed block, given what came before.
16
+ *
17
+ * The final reply is rebuilt with string_agg(delta, '') — an empty join,
18
+ * because deltas are partial TOKENS and any separator between them would
19
+ * corrupt words mid-word. Correct within a block, wrong between them: the
20
+ * entity narrates, calls a tool, narrates again, and the two blocks own no
21
+ * whitespace of their own, so they arrive glued as "the chat tool.Let me".
22
+ * The boundary is known here and nowhere else, so it is marked here.
23
+ */
24
+ function blockDelta(text, opts) {
25
+ if (!opts.afterTool || !opts.hadTextBefore || !text.trim())
26
+ return text;
27
+ // Drop leading whitespace the model may already have supplied so a break
28
+ // is exactly one blank line, never two.
29
+ return `\n\n${text.replace(/^\s+/, '')}`;
30
+ }
13
31
  /**
14
32
  * Parse one stream-json line into 0+ RuntimeEvents.
15
33
  *
@@ -190,6 +208,16 @@ function spawnClaudePrint(input) {
190
208
  let textBuf = '';
191
209
  let flushTimer = null;
192
210
  let sawDeltaThisMsg = false;
211
+ // Has any answer text been emitted for this run yet? The final reply is
212
+ // rebuilt by string_agg(delta, '') — an empty join, because deltas are
213
+ // partial TOKENS and any separator between them would corrupt words. That
214
+ // is right within a block and wrong between them: the model narrates, calls
215
+ // a tool, narrates again, and the two blocks arrive as separate messages
216
+ // with no whitespace of their own. Glued together they read
217
+ // "load the chat tool.Let me confirm". So the boundary is marked here,
218
+ // where it is actually known, rather than guessed at in SQL.
219
+ let emittedAnyText = false;
220
+ let blockBreakPending = false;
193
221
  // Thinking deltas (extended thinking) — coalesced lazily into 'thinking'
194
222
  // events; the chat relay renders them as a collapsed section.
195
223
  const THINK_FLUSH_MS = 700;
@@ -212,8 +240,11 @@ function spawnClaudePrint(input) {
212
240
  flushTimer = null;
213
241
  }
214
242
  if (textBuf) {
215
- const t = textBuf;
243
+ let t = textBuf;
216
244
  textBuf = '';
245
+ t = blockDelta(t, { afterTool: blockBreakPending, hadTextBefore: emittedAnyText });
246
+ blockBreakPending = false;
247
+ emittedAnyText = true;
217
248
  emit({ type: 'assistant_text', delta: t });
218
249
  }
219
250
  };
@@ -258,6 +289,11 @@ function spawnClaudePrint(input) {
258
289
  // was seen for this message, in which case we keep the block (never lose text).
259
290
  if (partialStream && (type === 'assistant' || type === 'user' || type === 'result'))
260
291
  flushText();
292
+ // A tool result means the entity stopped talking to go and do something.
293
+ // Whatever it says next is a new paragraph, not a continuation of the
294
+ // sentence it was halfway through.
295
+ if (type === 'user')
296
+ blockBreakPending = true;
261
297
  const suppressText = partialStream && sawDeltaThisMsg && type === 'assistant';
262
298
  for (const ev of lineToEvents(parsed, suppressText))
263
299
  emit(ev);
@@ -161,11 +161,24 @@ function writeProjectMcpConfig(workingDirectory, servers) {
161
161
  }
162
162
  const lines = [];
163
163
  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.
164
+ // Remote MCP servers are BRIDGED through mcp-remote, because grok cannot
165
+ // speak HTTP MCP itself.
166
+ //
167
+ // Its bundled README documents `url` + a `headers` inline table, and this
168
+ // code used to emit exactly that. Measured against grok 1.0.3, that entry
169
+ // is SILENTLY IGNORED: in one run grok loaded ten stdio servers and skipped
170
+ // both url-based ones — ours and a pre-existing third-party entry — with no
171
+ // error for either. The docs describe a transport the binary does not have.
172
+ //
173
+ // That is why entities on grok lost every Connection the moment the
174
+ // registry moved from `npx` to `http`: the servers were simply not there,
175
+ // and grok reported it to the model as "(auth required)".
176
+ //
177
+ // mcp-remote is the standard stdio<->HTTP shim (grok's own README reaches
178
+ // for it in its Linear example) and it is verified working here. The cost
179
+ // is an npx start per remote server on grok runs only — the very cost the
180
+ // HTTP move removed for every other agent. Revisit when grok ships real
181
+ // remote-MCP support.
169
182
  //
170
183
  // Folder trust still gates repo-local config.toml servers, so the --trust
171
184
  // launch flag remains load-bearing for these entries to start at all.
@@ -175,15 +188,16 @@ function writeProjectMcpConfig(workingDirectory, servers) {
175
188
  console.error(`[grok] MCP ${s.slug}: ${s.command} server has no URL — skipped`);
176
189
  continue;
177
190
  }
191
+ // The bridge itself is shared — see AGENTS_WITHOUT_HTTP_MCP for which
192
+ // agents need it and the evidence for each.
193
+ const bridge = (0, mcp_headers_1.remoteMcpBridge)(url, s.env);
194
+ const bridged = (0, win_1.wrapMcpCommandForPlatform)(bridge.command, bridge.args);
178
195
  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
- }
196
+ lines.push(`command = ${tomlStr(bridged.command)}`);
197
+ lines.push(`args = [${bridged.args.map(tomlStr).join(', ')}]`);
185
198
  lines.push('enabled = true');
186
- lines.push('startup_timeout_sec = 30');
199
+ // The first run on a machine downloads mcp-remote, which 30s does not cover.
200
+ lines.push('startup_timeout_sec = 90');
187
201
  lines.push('');
188
202
  continue;
189
203
  }
@@ -13,3 +13,44 @@ 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 ? — UNVERIFIED. The CLI on hand cannot authenticate at all
24
+ * ("no longer supported for Gemini Code Assist for
25
+ * individuals"), so its httpUrl handling was never observed
26
+ * connecting. No entity runs gemini today. If one ever does,
27
+ * verify before trusting it — this exact assumption is what
28
+ * cost us grok.
29
+ * grok NO — grok 1.0.3 SILENTLY IGNORES url-based entries. In one run it
30
+ * loaded ten stdio servers and skipped every remote one, ours
31
+ * and a third party's, with no error, while its own bundled
32
+ * README documents `url` + `headers`. The docs describe a
33
+ * transport the binary does not have.
34
+ *
35
+ * An agent listed here gets its remote servers bridged instead — see
36
+ * remoteMcpBridge. Add to this set only on evidence from the binary; the whole
37
+ * point is that "the docs say it works" is not evidence.
38
+ */
39
+ export declare const AGENTS_WITHOUT_HTTP_MCP: Set<string>;
40
+ export declare function agentNeedsMcpBridge(agent: string): boolean;
41
+ /**
42
+ * Turn a remote MCP server into a STDIO command, for agents that cannot speak
43
+ * HTTP MCP themselves.
44
+ *
45
+ * mcp-remote is the standard stdio<->HTTP shim, and the one grok's own README
46
+ * reaches for in its Linear example. Verified end to end against grok 1.0.3:
47
+ * the bridged server's tools appear and are callable.
48
+ *
49
+ * The cost is an npx start per remote server — exactly the cost moving to HTTP
50
+ * removed — so this is a fallback for the agents that need it, never the
51
+ * default path.
52
+ */
53
+ export declare function remoteMcpBridge(url: string, env?: Record<string, string> | null): {
54
+ command: string;
55
+ args: string[];
56
+ };
@@ -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,52 @@ 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 ? — UNVERIFIED. The CLI on hand cannot authenticate at all
61
+ * ("no longer supported for Gemini Code Assist for
62
+ * individuals"), so its httpUrl handling was never observed
63
+ * connecting. No entity runs gemini today. If one ever does,
64
+ * verify before trusting it — this exact assumption is what
65
+ * cost us grok.
66
+ * grok NO — grok 1.0.3 SILENTLY IGNORES url-based entries. In one run it
67
+ * loaded ten stdio servers and skipped every remote one, ours
68
+ * and a third party's, with no error, while its own bundled
69
+ * README documents `url` + `headers`. The docs describe a
70
+ * transport the binary does not have.
71
+ *
72
+ * An agent listed here gets its remote servers bridged instead — see
73
+ * remoteMcpBridge. Add to this set only on evidence from the binary; the whole
74
+ * point is that "the docs say it works" is not evidence.
75
+ */
76
+ exports.AGENTS_WITHOUT_HTTP_MCP = new Set(['grok']);
77
+ function agentNeedsMcpBridge(agent) {
78
+ return exports.AGENTS_WITHOUT_HTTP_MCP.has(agent);
79
+ }
80
+ /**
81
+ * Turn a remote MCP server into a STDIO command, for agents that cannot speak
82
+ * HTTP MCP themselves.
83
+ *
84
+ * mcp-remote is the standard stdio<->HTTP shim, and the one grok's own README
85
+ * reaches for in its Linear example. Verified end to end against grok 1.0.3:
86
+ * the bridged server's tools appear and are callable.
87
+ *
88
+ * The cost is an npx start per remote server — exactly the cost moving to HTTP
89
+ * removed — so this is a fallback for the agents that need it, never the
90
+ * default path.
91
+ */
92
+ function remoteMcpBridge(url, env) {
93
+ const args = ['-y', 'mcp-remote', url];
94
+ for (const [key, value] of Object.entries(remoteMcpHeaders(env))) {
95
+ // Spawned without a shell, so "K:V" needs no quoting of its own.
96
+ args.push('--header', `${key}:${value}`);
97
+ }
98
+ // Our servers are streamable HTTP; skip the SSE fallback probing.
99
+ args.push('--transport', 'http-only');
100
+ return { command: 'npx', args };
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
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": [