negotium 0.2.8 → 0.2.10

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.
Files changed (35) hide show
  1. package/dist/agent-helpers.js +21 -20
  2. package/dist/agent-helpers.js.map +6 -6
  3. package/dist/background-bash.js +2 -1
  4. package/dist/background-bash.js.map +3 -3
  5. package/dist/browser-runtime.js +2 -1
  6. package/dist/browser-runtime.js.map +3 -3
  7. package/dist/{chunk-knv045mn.js → chunk-dcfmsrnb.js} +2 -1
  8. package/dist/{chunk-knv045mn.js.map → chunk-dcfmsrnb.js.map} +3 -3
  9. package/dist/hosted-agent.js +3 -2
  10. package/dist/hosted-agent.js.map +4 -4
  11. package/dist/main.js +522 -269
  12. package/dist/main.js.map +11 -9
  13. package/dist/mcp-factories.js +21 -20
  14. package/dist/mcp-factories.js.map +6 -6
  15. package/dist/prompts.js +2 -1
  16. package/dist/prompts.js.map +3 -3
  17. package/dist/query-runtime.js +2 -1
  18. package/dist/query-runtime.js.map +3 -3
  19. package/dist/registry.js +9 -5
  20. package/dist/registry.js.map +4 -4
  21. package/dist/rollout.js +1 -1
  22. package/dist/runtime/src/agents/maestro-registry.ts +32 -2
  23. package/dist/runtime/src/agents/mcp-tools/inline-assets.ts +193 -0
  24. package/dist/runtime/src/agents/mcp-tools/publish-html.ts +183 -0
  25. package/dist/runtime/src/mcp-runtime-host.ts +1 -0
  26. package/dist/runtime/src/platform/config.ts +12 -0
  27. package/dist/runtime/src/runtime/turn-runner.ts +34 -11
  28. package/dist/runtime/src/version.ts +1 -1
  29. package/dist/runtime-helpers.js +2 -1
  30. package/dist/runtime-helpers.js.map +3 -3
  31. package/dist/types/packages/core/src/platform/config.d.ts +7 -0
  32. package/dist/types/packages/core/src/version.d.ts +1 -1
  33. package/dist/vault.js +2 -1
  34. package/dist/vault.js.map +3 -3
  35. package/package.json +2 -2
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Inline local file references in an HTML document as data URIs.
3
+ *
4
+ * A published snippet is a single file with no asset directory, so a
5
+ * generated document that points at `./chart.png` would render a broken
6
+ * image. Rewriting those references into `data:` URIs keeps the snippet
7
+ * self-contained without giving the store a second lifecycle to manage.
8
+ *
9
+ * Only references that resolve to an existing local file are touched.
10
+ * Remote URLs, `data:`/`blob:` URIs, protocol-relative URLs and fragments
11
+ * are left exactly as written.
12
+ *
13
+ * The scanner is regex-based rather than a full HTML parse. That is a
14
+ * deliberate tradeoff for machine-generated documents: it handles the
15
+ * attribute shapes models actually emit, and anything it fails to match is
16
+ * simply left alone rather than corrupted.
17
+ */
18
+ import { existsSync, readFileSync, statSync } from "node:fs";
19
+ import { extname, isAbsolute, resolve } from "node:path";
20
+
21
+ const MIME_BY_EXT: Record<string, string> = {
22
+ ".apng": "image/apng",
23
+ ".avif": "image/avif",
24
+ ".bmp": "image/bmp",
25
+ ".css": "text/css",
26
+ ".gif": "image/gif",
27
+ ".ico": "image/x-icon",
28
+ ".jpeg": "image/jpeg",
29
+ ".jpg": "image/jpeg",
30
+ ".mp3": "audio/mpeg",
31
+ ".mp4": "video/mp4",
32
+ ".otf": "font/otf",
33
+ ".png": "image/png",
34
+ ".svg": "image/svg+xml",
35
+ ".ttf": "font/ttf",
36
+ ".wav": "audio/wav",
37
+ ".webm": "video/webm",
38
+ ".webp": "image/webp",
39
+ ".woff": "font/woff",
40
+ ".woff2": "font/woff2",
41
+ };
42
+
43
+ export interface InlineOptions {
44
+ /** Directory that relative references resolve against. */
45
+ baseDir: string;
46
+ /** Skip any single asset larger than this. */
47
+ maxAssetBytes: number;
48
+ /** Stop inlining once the document would exceed this. */
49
+ maxTotalBytes: number;
50
+ }
51
+
52
+ export interface InlineReport {
53
+ html: string;
54
+ inlined: { ref: string; bytes: number }[];
55
+ /** Looked like a local file but nothing was there — likely a broken image. */
56
+ unresolved: string[];
57
+ /** Existed but was left as-is; the document would have grown too large. */
58
+ skipped: { ref: string; bytes: number; reason: "asset-too-large" | "document-too-large" }[];
59
+ }
60
+
61
+ /** Remote URL, data/blob URI, protocol-relative URL, or bare fragment. */
62
+ function isNonLocal(ref: string): boolean {
63
+ return /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(ref.trim());
64
+ }
65
+
66
+ /** `./chart.png?v=2#frag` → `./chart.png` */
67
+ function stripSuffix(ref: string): string {
68
+ return ref.replace(/[?#].*$/, "");
69
+ }
70
+
71
+ export function inlineLocalAssets(html: string, options: InlineOptions): InlineReport {
72
+ const { baseDir, maxAssetBytes, maxTotalBytes } = options;
73
+
74
+ const inlined: InlineReport["inlined"] = [];
75
+ const unresolved: string[] = [];
76
+ const skipped: InlineReport["skipped"] = [];
77
+
78
+ // Same file referenced twice should only be read (and counted) once.
79
+ const cache = new Map<string, string | null>();
80
+ let budget = maxTotalBytes - Buffer.byteLength(html, "utf-8");
81
+
82
+ /** Returns the data URI for a reference, or null to leave it untouched. */
83
+ function toDataUri(rawRef: string): string | null {
84
+ const ref = rawRef.trim();
85
+ if (!ref || isNonLocal(ref)) return null;
86
+
87
+ const cached = cache.get(ref);
88
+ if (cached !== undefined) return cached;
89
+
90
+ const relPath = stripSuffix(ref);
91
+ if (!relPath) return null;
92
+
93
+ const abs = isAbsolute(relPath) ? relPath : resolve(baseDir, relPath);
94
+
95
+ let bytes: number;
96
+ try {
97
+ if (!existsSync(abs) || !statSync(abs).isFile()) {
98
+ unresolved.push(ref);
99
+ cache.set(ref, null);
100
+ return null;
101
+ }
102
+ bytes = statSync(abs).size;
103
+ } catch {
104
+ unresolved.push(ref);
105
+ cache.set(ref, null);
106
+ return null;
107
+ }
108
+
109
+ if (bytes > maxAssetBytes) {
110
+ skipped.push({ ref, bytes, reason: "asset-too-large" });
111
+ cache.set(ref, null);
112
+ return null;
113
+ }
114
+
115
+ // base64 costs ~4/3 of the raw bytes; check against the remaining budget
116
+ // before reading so an oversized asset never lands in memory twice.
117
+ const encodedCost = Math.ceil(bytes / 3) * 4;
118
+ if (encodedCost > budget) {
119
+ skipped.push({ ref, bytes, reason: "document-too-large" });
120
+ cache.set(ref, null);
121
+ return null;
122
+ }
123
+
124
+ let uri: string;
125
+ try {
126
+ const mime = MIME_BY_EXT[extname(abs).toLowerCase()] ?? "application/octet-stream";
127
+ uri = `data:${mime};base64,${readFileSync(abs).toString("base64")}`;
128
+ } catch {
129
+ unresolved.push(ref);
130
+ cache.set(ref, null);
131
+ return null;
132
+ }
133
+
134
+ budget -= uri.length;
135
+ inlined.push({ ref, bytes });
136
+ cache.set(ref, uri);
137
+ return uri;
138
+ }
139
+
140
+ /** `a.png 1x, b.png 2x` — rewrite each candidate, keep its descriptor. */
141
+ function rewriteSrcset(value: string): string {
142
+ return value
143
+ .split(",")
144
+ .map((candidate) => {
145
+ const trimmed = candidate.trim();
146
+ if (!trimmed) return candidate;
147
+ const [url, ...descriptor] = trimmed.split(/\s+/);
148
+ const uri = url ? toDataUri(url) : null;
149
+ if (!uri) return candidate;
150
+ return [uri, ...descriptor].join(" ");
151
+ })
152
+ .join(", ");
153
+ }
154
+
155
+ let out = html;
156
+
157
+ // src="..." on img/script/source/video/audio/iframe, and poster="..."
158
+ out = out.replace(
159
+ /(\b(?:src|poster)\s*=\s*)(["'])([^"']*)\2/gi,
160
+ (match, prefix: string, quote: string, ref: string) => {
161
+ const uri = toDataUri(ref);
162
+ return uri ? `${prefix}${quote}${uri}${quote}` : match;
163
+ },
164
+ );
165
+
166
+ // srcset="a.png 1x, b.png 2x"
167
+ out = out.replace(
168
+ /(\bsrcset\s*=\s*)(["'])([^"']*)\2/gi,
169
+ (match, prefix: string, quote: string, value: string) => {
170
+ const rewritten = rewriteSrcset(value);
171
+ return rewritten === value ? match : `${prefix}${quote}${rewritten}${quote}`;
172
+ },
173
+ );
174
+
175
+ // <link href="style.css"> / icons — but never <a href>.
176
+ out = out.replace(/<link\b[^>]*>/gi, (tag) =>
177
+ tag.replace(
178
+ /(\bhref\s*=\s*)(["'])([^"']*)\2/i,
179
+ (match, prefix: string, quote: string, ref: string) => {
180
+ const uri = toDataUri(ref);
181
+ return uri ? `${prefix}${quote}${uri}${quote}` : match;
182
+ },
183
+ ),
184
+ );
185
+
186
+ // CSS url(...) in <style> blocks and style="" attributes.
187
+ out = out.replace(/url\(\s*(["']?)([^"')]+)\1\s*\)/gi, (match, quote: string, ref: string) => {
188
+ const uri = toDataUri(ref);
189
+ return uri ? `url(${quote}${uri}${quote})` : match;
190
+ });
191
+
192
+ return { html: out, inlined, unresolved, skipped };
193
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * `publish_html` — hand the user a public link to a rendered HTML document.
3
+ *
4
+ * This sits beside `show_html`: both exist to show rendered HTML rather than
5
+ * raw markup, so both are gated by the same `visualTools` capability. They
6
+ * differ only in delivery. `show_html` paints a sandboxed in-app panel, which
7
+ * blocks network requests and cannot be opened outside the app; publishing
8
+ * returns an ordinary URL that survives the turn and can be shared.
9
+ *
10
+ * Requires a snippet backend (`NEGOTIUM_SNIPPETS_API_URL`). The tool is left
11
+ * out of the catalog entirely when none is configured, so a deployment
12
+ * without one never advertises a link it cannot mint.
13
+ */
14
+ import { z } from "zod";
15
+ import { errorResult, type SharedMcpTool, textResult } from "#agents/mcp-tools/common";
16
+ import { inlineLocalAssets } from "#agents/mcp-tools/inline-assets";
17
+ import { SNIPPETS_API_URL } from "#platform/config";
18
+
19
+ const REQUEST_TIMEOUT_MS = 30_000;
20
+ /** Keep in step with the snippet service's own body limit. */
21
+ export const MAX_PUBLISHED_BYTES = 10 * 1024 * 1024;
22
+ export const MAX_PUBLISHED_ASSET_BYTES = 5 * 1024 * 1024;
23
+
24
+ export interface PublishHtmlContext {
25
+ /** Directory that relative asset paths resolve against (the agent's cwd). */
26
+ cwd?: string;
27
+ }
28
+
29
+ type SnippetResponse = {
30
+ ok: boolean;
31
+ url?: string;
32
+ snippetId?: string;
33
+ expiresAt?: string;
34
+ error?: string;
35
+ };
36
+
37
+ export function formatPublishBytes(bytes: number): string {
38
+ if (bytes < 1024) return `${bytes}B`;
39
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}KB`;
40
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
41
+ }
42
+
43
+ /** Accept a full snippet URL or a bare id, return the id. */
44
+ export function parseSnippetId(input: string): string | null {
45
+ const trimmed = input.trim();
46
+ const fromUrl = trimmed.match(/\/snippets\/([A-Za-z0-9-]+)/)?.[1];
47
+ const id = fromUrl ?? trimmed;
48
+ return /^[A-Za-z0-9-]+$/.test(id) ? id : null;
49
+ }
50
+
51
+ async function snippetRequest(
52
+ baseUrl: string,
53
+ path: string,
54
+ init: RequestInit = {},
55
+ ): Promise<SnippetResponse> {
56
+ try {
57
+ const response = await fetch(`${baseUrl}${path}`, {
58
+ ...init,
59
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
60
+ });
61
+ const body = (await response.json().catch(() => null)) as SnippetResponse | null;
62
+ if (body) return body;
63
+ return { ok: false, error: `snippet service returned ${response.status}` };
64
+ } catch (error) {
65
+ const message = error instanceof Error ? error.message : "snippet service unreachable";
66
+ return { ok: false, error: message };
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Build the publish tools, or `[]` when no snippet backend is configured.
72
+ * `ctx` may be a getter so a long-lived server picks up the current turn's cwd.
73
+ */
74
+ export function createPublishHtmlToolDefinitions(
75
+ ctx: PublishHtmlContext | (() => PublishHtmlContext),
76
+ options: { apiUrl?: string } = {},
77
+ ): SharedMcpTool[] {
78
+ const apiUrl = (options.apiUrl ?? SNIPPETS_API_URL)?.replace(/\/+$/, "");
79
+ if (!apiUrl) return [];
80
+ const getCtx = typeof ctx === "function" ? ctx : () => ctx;
81
+
82
+ return [
83
+ {
84
+ name: "publish_html",
85
+ description: [
86
+ "Publish an HTML document and return a public, shareable https link that",
87
+ "stays live for 72 hours. Use this when the user should be able to open,",
88
+ "keep, or send the result — a report, dashboard, or styled table — rather",
89
+ "than only glance at it in the side panel.",
90
+ "Pass a complete standalone document and inline the CSS and JS.",
91
+ "For charts and diagrams prefer inline <svg>: it stays sharp at any zoom",
92
+ "and is far smaller than an image file.",
93
+ "Images may be remote URLs, data: URIs, or paths to local files you",
94
+ "created — local files are embedded automatically, so the page keeps",
95
+ "working after your workspace is gone.",
96
+ ].join(" "),
97
+ schema: {
98
+ html: z
99
+ .string()
100
+ .describe("Complete standalone HTML document to publish. Inline the CSS and JS."),
101
+ base_dir: z
102
+ .string()
103
+ .optional()
104
+ .describe(
105
+ "Directory that relative image paths resolve against. Defaults to the working directory.",
106
+ ),
107
+ },
108
+ async handler(input: { html?: string; base_dir?: string }) {
109
+ const html = input?.html ?? "";
110
+ if (!html.trim()) return errorResult("html is empty — provide an HTML document.");
111
+
112
+ const baseDir = input.base_dir?.trim() || getCtx().cwd || process.cwd();
113
+ const report = inlineLocalAssets(html, {
114
+ baseDir,
115
+ maxAssetBytes: MAX_PUBLISHED_ASSET_BYTES,
116
+ maxTotalBytes: MAX_PUBLISHED_BYTES,
117
+ });
118
+
119
+ const bytes = Buffer.byteLength(report.html, "utf-8");
120
+ if (bytes > MAX_PUBLISHED_BYTES) {
121
+ return errorResult(
122
+ `Document is ${formatPublishBytes(bytes)} after embedding images, over the ${formatPublishBytes(MAX_PUBLISHED_BYTES)} limit. Use fewer or smaller images, or switch charts to inline <svg>.`,
123
+ );
124
+ }
125
+
126
+ const result = await snippetRequest(apiUrl, "/snippets", {
127
+ method: "POST",
128
+ headers: { "content-type": "text/html; charset=utf-8" },
129
+ body: report.html,
130
+ });
131
+ if (!result.ok || !result.url) {
132
+ return errorResult(result.error ?? "Failed to publish.");
133
+ }
134
+
135
+ // Surface asset problems here so a broken image is visible now rather
136
+ // than only in the user's browser.
137
+ const notes: string[] = [];
138
+ if (report.inlined.length > 0) {
139
+ const total = report.inlined.reduce((sum, a) => sum + a.bytes, 0);
140
+ notes.push(
141
+ `Embedded ${report.inlined.length} local file(s), ${formatPublishBytes(total)}.`,
142
+ );
143
+ }
144
+ if (report.unresolved.length > 0) {
145
+ notes.push(
146
+ `WARNING — these look like local files but were not found, so they will render broken: ${report.unresolved.join(", ")}. Check the paths (relative to ${baseDir}) and republish.`,
147
+ );
148
+ }
149
+ for (const s of report.skipped) {
150
+ notes.push(
151
+ s.reason === "asset-too-large"
152
+ ? `WARNING — ${s.ref} is ${formatPublishBytes(s.bytes)}, over the ${formatPublishBytes(MAX_PUBLISHED_ASSET_BYTES)} per-file limit, and was left as a broken link.`
153
+ : `WARNING — ${s.ref} was left out because the document hit the ${formatPublishBytes(MAX_PUBLISHED_BYTES)} limit.`,
154
+ );
155
+ }
156
+
157
+ const payload = {
158
+ url: result.url,
159
+ snippetId: result.snippetId,
160
+ expiresAt: result.expiresAt,
161
+ sizeBytes: bytes,
162
+ ...(report.inlined.length > 0 ? { embeddedFiles: report.inlined.length } : {}),
163
+ };
164
+ return textResult([JSON.stringify(payload, null, 2), ...notes].join("\n\n"));
165
+ },
166
+ },
167
+ {
168
+ name: "unpublish_html",
169
+ description:
170
+ "Delete a previously published HTML document before its 72-hour expiry. Accepts the URL returned by publish_html, or its id.",
171
+ schema: {
172
+ snippet: z.string().describe("URL returned by publish_html, or the bare id."),
173
+ },
174
+ async handler(input: { snippet?: string }) {
175
+ const id = parseSnippetId(input?.snippet ?? "");
176
+ if (!id) return errorResult("Could not read a published document id from that value.");
177
+ const result = await snippetRequest(apiUrl, `/snippets/${id}`, { method: "DELETE" });
178
+ if (!result.ok) return errorResult(result.error ?? "Failed to delete.");
179
+ return textResult(`Published document ${id} deleted.`);
180
+ },
181
+ },
182
+ ];
183
+ }
@@ -1,6 +1,7 @@
1
1
  /** Stable core surface used by the embedded runtime MCP endpoint. */
2
2
  export { createAskUserToolDefinition } from "#agents/mcp-tools/ask-user";
3
3
  export { errorResult, textResult } from "#agents/mcp-tools/common";
4
+ export { createPublishHtmlToolDefinitions } from "#agents/mcp-tools/publish-html";
4
5
  export type { SelfConfigContext } from "#agents/mcp-tools/self-config";
5
6
  export { createSelfConfigToolDefinitions } from "#agents/mcp-tools/self-config";
6
7
  export {
@@ -153,6 +153,18 @@ export function resolveBrowserRsBin(envValue?: string): string | undefined {
153
153
  }
154
154
  }
155
155
 
156
+ /**
157
+ * Backend that stores documents published by `publish_html` and serves them
158
+ * at `<base>/snippets/<id>`. Unset means the deployment has nowhere to
159
+ * publish, and the publish tools are omitted from the catalog rather than
160
+ * offering a link that cannot be minted.
161
+ */
162
+ export const SNIPPETS_API_URL = (
163
+ envText("NEGOTIUM_SNIPPETS_API_URL") ??
164
+ envText("SNIPPETS_API_URL") ??
165
+ ""
166
+ ).replace(/\/+$/, "");
167
+
156
168
  export const BROWSER_RS_BIN = resolveBrowserRsBin(envText("NEGOTIUM_BROWSER_RS_BIN"));
157
169
 
158
170
  // Managed Browser.rs terminates MCP transports and security policy itself.
@@ -1423,9 +1423,29 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
1423
1423
  ].join("\n");
1424
1424
  }
1425
1425
 
1426
+ /**
1427
+ * Per-turn `<system-reminder>` blocks, appended to the *user* prompt rather
1428
+ * than the system prompt.
1429
+ *
1430
+ * These facts flip between turns on the same model — whether the browser MCP
1431
+ * came up, whether this turn is an auto-resume. A prompt cache invalidates
1432
+ * from the first diverging byte, so putting them in the system prompt means
1433
+ * every flip re-reads the entire prompt: measured against the live DeepSeek
1434
+ * API, one flip at turn 15 of a 20-turn conversation cost 17,852 uncached
1435
+ * tokens (hit=0) versus 1,215 with the reminder on the user turn — 40% more
1436
+ * uncached tokens across the run. On the user turn the divergence lands at
1437
+ * the tail, where the text is new anyway.
1438
+ *
1439
+ * Applied after the conversation log is written, so a reminder never becomes
1440
+ * part of the recorded user message (it would then persist into synthesized
1441
+ * rollouts and defeat the point).
1442
+ */
1443
+ const turnReminders: string[] = [];
1444
+
1426
1445
  if (params.from === FROM_AUTO_CONTINUE) {
1427
- systemPrompt +=
1428
- "\n\n<system-reminder>이 턴은 설정 자동 조정(effort/model/agent) 후 자동 재개된 턴이다. effort/model/agent 난이도 재평가 및 설정 변경 없이 즉시 작업을 시작할 것.</system-reminder>";
1446
+ turnReminders.push(
1447
+ "<system-reminder>이 턴은 설정 자동 조정(effort/model/agent) 후 자동 재개된 턴이다. effort/model/agent 난이도 재평가 및 설정 변경 없이 즉시 작업을 시작할 것.</system-reminder>",
1448
+ );
1429
1449
  }
1430
1450
 
1431
1451
  if (!silent && !sessionRetried) {
@@ -1538,25 +1558,28 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
1538
1558
  );
1539
1559
  }
1540
1560
  }
1541
- let effectiveSystemPrompt = systemPrompt;
1542
1561
  if (playwrightRequested && !browserProfileOwner) {
1543
- const ownerOnlyNote =
1544
- "<system-reminder>Browser tools are unavailable in this turn because browser profiles are private to the topic owner. Do not attempt browser calls or ask to reuse the owner's login state.</system-reminder>";
1545
- effectiveSystemPrompt = `${effectiveSystemPrompt}\n\n${ownerOnlyNote}`;
1562
+ turnReminders.push(
1563
+ "<system-reminder>Browser tools are unavailable in this turn because browser profiles are private to the topic owner. Do not attempt browser calls or ask to reuse the owner's login state.</system-reminder>",
1564
+ );
1546
1565
  }
1547
1566
  if (consumePlaywrightUnavailable(userId, topic.title)) {
1548
- const playwrightNote =
1549
- "<system-reminder>Playwright browser tools are UNAVAILABLE this turn. The `mcp__playwright__*` tools have been removed from this turn's catalog because the long-lived browser MCP could not be prepared. Do not attempt to call browser tools. If browser interaction is required, ask the user to retry shortly or use a non-browser alternative.</system-reminder>";
1550
- effectiveSystemPrompt = `${effectiveSystemPrompt}\n\n${playwrightNote}`;
1567
+ turnReminders.push(
1568
+ "<system-reminder>Playwright browser tools are UNAVAILABLE this turn. The `mcp__playwright__*` tools have been removed from this turn's catalog because the long-lived browser MCP could not be prepared. Do not attempt to call browser tools. If browser interaction is required, ask the user to retry shortly or use a non-browser alternative.</system-reminder>",
1569
+ );
1551
1570
  }
1571
+ // The system prompt is now byte-identical across turns of a topic, so the
1572
+ // provider's prefix cache survives; per-turn facts ride the prompt tail.
1573
+ const effectivePrompt =
1574
+ turnReminders.length > 0 ? `${agentPrompt}\n\n${turnReminders.join("\n\n")}` : agentPrompt;
1552
1575
 
1553
1576
  try {
1554
1577
  yield* runAgent({
1555
1578
  agent: agentKind,
1556
- prompt: agentPrompt,
1579
+ prompt: effectivePrompt,
1557
1580
  attachments: promptAttachments,
1558
1581
  cwd: workspaceCwd,
1559
- systemPrompt: effectiveSystemPrompt,
1582
+ systemPrompt,
1560
1583
  sessionId,
1561
1584
  userId,
1562
1585
  vaultUserId,
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.2.8";
1
+ export const NEGOTIUM_VERSION = "0.2.10";
@@ -494,6 +494,7 @@ function resolveBrowserRsBin(envValue) {
494
494
  return;
495
495
  }
496
496
  }
497
+ var SNIPPETS_API_URL = (envText("NEGOTIUM_SNIPPETS_API_URL") ?? envText("SNIPPETS_API_URL") ?? "").replace(/\/+$/, "");
497
498
  var BROWSER_RS_BIN = resolveBrowserRsBin(envText("NEGOTIUM_BROWSER_RS_BIN"));
498
499
  function resolveBrowserMcpBin(envValue) {
499
500
  const override = envValue?.trim();
@@ -1132,4 +1133,4 @@ export {
1132
1133
  CLAUDE_EFFORT_VALUES
1133
1134
  };
1134
1135
 
1135
- //# debugId=078B7BFBF513B96264756E2164756E21
1136
+ //# debugId=842C00B02967857064756E2164756E21