agentic-assistant 0.1.0 → 0.3.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,569 @@
1
+ import {
2
+ ScriptedProvider
3
+ } from "./chunk-HYT732TN.js";
4
+ import {
5
+ AnthropicProvider,
6
+ checkBaseUrl,
7
+ isLoopback
8
+ } from "./chunk-DXTIEMOT.js";
9
+ import {
10
+ PrincipalError,
11
+ ProviderError,
12
+ TOOL_RISKS,
13
+ ToolRegistry,
14
+ VERSION,
15
+ createPrincipal,
16
+ cutText,
17
+ digestCall,
18
+ printable,
19
+ runAssistant
20
+ } from "./chunk-3LKTJJ75.js";
21
+
22
+ // src/sources/mcp.ts
23
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
24
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
25
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
26
+ var MAX_PAGES = 20;
27
+ var MAX_SCHEMA_CHARS = 2e4;
28
+ function positive(name, value, fallback, max = Number.MAX_SAFE_INTEGER) {
29
+ const chosen = value ?? fallback;
30
+ if (!Number.isInteger(chosen) || chosen <= 0 || chosen > max) throw new RangeError(`${name} must be a positive integer of at most ${max}`);
31
+ return chosen;
32
+ }
33
+ var MAX_TIMER_MS = 2147483647;
34
+ function boundedFetch(limit) {
35
+ return async (input, init) => {
36
+ const response = await fetch(input, { ...init, redirect: "error" });
37
+ const declared = Number(response.headers.get("content-length"));
38
+ if (Number.isFinite(declared) && declared > limit) {
39
+ await response.body?.cancel();
40
+ throw new Error(`the tool server sent a response over the size limit of ${limit} bytes`);
41
+ }
42
+ if (response.body === null) return response;
43
+ let seen = 0;
44
+ const counted = response.body.pipeThrough(
45
+ new TransformStream({
46
+ transform(chunk, controller) {
47
+ seen += chunk.byteLength;
48
+ if (seen > limit) controller.error(new Error(`the tool server sent a response over the size limit of ${limit} bytes`));
49
+ else controller.enqueue(chunk);
50
+ }
51
+ })
52
+ );
53
+ return new Response(counted, { status: response.status, statusText: response.statusText, headers: response.headers });
54
+ };
55
+ }
56
+ var SCHEMA_NAME_MAPS = /* @__PURE__ */ new Set(["properties", "$defs", "definitions", "dependentSchemas"]);
57
+ var SCHEMA_TEXT_KEYS = /* @__PURE__ */ new Set(["description", "title", "$comment"]);
58
+ var SCHEMA_VALUE_KEYS = /* @__PURE__ */ new Set(["default", "const", "enum"]);
59
+ function cutStrings(value, max) {
60
+ if (typeof value === "string") return cut(value, max);
61
+ if (Array.isArray(value)) return value.map((item) => cutStrings(item, max));
62
+ if (typeof value === "object" && value !== null) {
63
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cutStrings(child, max)]));
64
+ }
65
+ return value;
66
+ }
67
+ function limitSchemaText(value, max, namesMap = false) {
68
+ if (Array.isArray(value)) return value.map((item) => limitSchemaText(item, max));
69
+ if (typeof value !== "object" || value === null) return value;
70
+ const out = {};
71
+ for (const [key, child] of Object.entries(value)) {
72
+ if (namesMap) out[key] = limitSchemaText(child, max);
73
+ else if (key === "examples") continue;
74
+ else if (SCHEMA_TEXT_KEYS.has(key) && typeof child === "string") out[key] = cut(child, max);
75
+ else if (SCHEMA_VALUE_KEYS.has(key)) out[key] = cutStrings(child, max);
76
+ else out[key] = limitSchemaText(child, max, SCHEMA_NAME_MAPS.has(key));
77
+ }
78
+ return out;
79
+ }
80
+ function makeTransport(config, maxResponseBytes) {
81
+ if (config.type === "stdio") {
82
+ return new StdioClientTransport({ command: config.command, args: config.args, env: config.env, cwd: config.cwd, stderr: "ignore" });
83
+ }
84
+ const { url } = checkBaseUrl(config.url);
85
+ return new StreamableHTTPClientTransport(url, {
86
+ fetch: boundedFetch(maxResponseBytes),
87
+ ...config.headers ? { requestInit: { headers: config.headers } } : {}
88
+ });
89
+ }
90
+ var cut = cutText;
91
+ function flatten(result, max) {
92
+ const parts = (result.content ?? []).map((block) => block.type === "text" && typeof block.text === "string" ? block.text : `[${block.type} content omitted]`);
93
+ let text = parts.join("\n");
94
+ if (text === "" && result.structuredContent !== void 0) text = JSON.stringify(result.structuredContent);
95
+ return cut(text, max);
96
+ }
97
+ async function connectMcpTools(options) {
98
+ const maxOutput = positive("maxOutputChars", options.maxOutputChars, 32768);
99
+ const maxTools = positive("maxTools", options.maxTools, 200);
100
+ const maxDescription = positive("maxDescriptionChars", options.maxDescriptionChars, 1e3);
101
+ const callTimeout = positive("callTimeoutMs", options.callTimeoutMs, 3e4, MAX_TIMER_MS);
102
+ const loadTimeout = positive("loadTimeoutMs", options.loadTimeoutMs, 3e4, MAX_TIMER_MS);
103
+ const maxResponseBytes = positive("maxResponseBytes", options.maxResponseBytes, 4e6);
104
+ const prefix = options.prefix ?? "";
105
+ const client = new Client({ name: "agentic-assistant", version: VERSION });
106
+ const loading = AbortSignal.timeout(loadTimeout);
107
+ try {
108
+ await client.connect(makeTransport(options.transport, maxResponseBytes), { signal: loading, timeout: loadTimeout });
109
+ const listed = [];
110
+ let cursor;
111
+ for (let page = 0; page < MAX_PAGES; page++) {
112
+ const response = await client.listTools(cursor === void 0 ? void 0 : { cursor }, { signal: loading, timeout: loadTimeout });
113
+ listed.push(...response.tools);
114
+ cursor = response.nextCursor;
115
+ if (cursor === void 0) break;
116
+ }
117
+ const tools = [];
118
+ const skipped = [];
119
+ const seen = /* @__PURE__ */ new Set();
120
+ for (const info of listed) {
121
+ if (seen.has(info.name)) {
122
+ skipped.push({ name: info.name, reason: "duplicate name" });
123
+ continue;
124
+ }
125
+ seen.add(info.name);
126
+ if (tools.length >= maxTools) {
127
+ skipped.push({ name: info.name, reason: "too many tools" });
128
+ continue;
129
+ }
130
+ try {
131
+ if (JSON.stringify(info.inputSchema).length > MAX_SCHEMA_CHARS) throw new Error("input schema is too large");
132
+ const scope = typeof options.scope === "function" ? options.scope(info) : options.scope;
133
+ const risk = typeof options.risk === "function" ? options.risk(info) : options.risk;
134
+ const definition = {
135
+ name: `${prefix}${info.name}`,
136
+ description: cut(info.description ?? "", maxDescription),
137
+ inputSchema: limitSchemaText(info.inputSchema, maxDescription),
138
+ scope,
139
+ risk,
140
+ handler: async (input, context) => {
141
+ const result = await client.callTool({ name: info.name, arguments: input }, void 0, {
142
+ signal: context.signal,
143
+ timeout: callTimeout
144
+ });
145
+ const text = flatten(result, maxOutput);
146
+ if (result.isError) throw new Error(text === "" ? "the tool reported an error" : text);
147
+ return text;
148
+ }
149
+ };
150
+ new ToolRegistry().register(definition);
151
+ tools.push(definition);
152
+ } catch (error) {
153
+ skipped.push({ name: info.name, reason: error instanceof Error ? error.message : String(error) });
154
+ }
155
+ }
156
+ return { tools, skipped, close: () => client.close() };
157
+ } catch (error) {
158
+ await client.close().catch(() => void 0);
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ // src/providers/openai-compatible.ts
164
+ import { randomUUID } from "crypto";
165
+ var FINISH_REASONS = {
166
+ stop: "end_turn",
167
+ tool_calls: "tool_use",
168
+ length: "max_tokens",
169
+ content_filter: "refusal"
170
+ };
171
+ function toChatMessages(system, messages) {
172
+ const out = system !== void 0 ? [{ role: "system", content: system }] : [];
173
+ for (const message of messages) {
174
+ const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
175
+ if (message.role === "user") {
176
+ for (const block of message.content) {
177
+ if (block.type === "tool_result") out.push({ role: "tool", tool_call_id: block.toolUseId, content: block.content });
178
+ }
179
+ if (text.length > 0) out.push({ role: "user", content: text });
180
+ continue;
181
+ }
182
+ const toolCalls = message.content.filter((block) => block.type === "tool_use").map((block) => ({ id: block.id, type: "function", function: { name: block.name, arguments: JSON.stringify(block.input ?? {}) } }));
183
+ out.push({
184
+ role: "assistant",
185
+ content: text.length > 0 ? text : null,
186
+ ...toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
187
+ });
188
+ }
189
+ return out;
190
+ }
191
+ var OpenAICompatibleProvider = class {
192
+ name;
193
+ model;
194
+ pricing;
195
+ baseUrl;
196
+ apiKey;
197
+ maxResponseBytes;
198
+ constructor(options) {
199
+ this.name = options.name ?? "openai-compatible";
200
+ this.model = options.model;
201
+ this.pricing = options.pricing;
202
+ this.apiKey = options.apiKey;
203
+ const limit = options.maxResponseBytes ?? 4e6;
204
+ if (!Number.isInteger(limit) || limit <= 0) throw new ProviderError("maxResponseBytes must be a positive integer");
205
+ this.maxResponseBytes = limit;
206
+ const { url, text } = checkBaseUrl(options.baseUrl);
207
+ this.baseUrl = text;
208
+ if (options.apiKey === "" || options.apiKey === void 0 && !isLoopback(url)) {
209
+ throw new ProviderError("apiKey must be a non-empty string for a remote host");
210
+ }
211
+ }
212
+ async *stream(request) {
213
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
214
+ method: "POST",
215
+ signal: request.signal,
216
+ headers: {
217
+ "content-type": "application/json",
218
+ ...this.apiKey !== void 0 ? { authorization: `Bearer ${this.apiKey}` } : {}
219
+ },
220
+ body: JSON.stringify({
221
+ model: this.model,
222
+ stream: true,
223
+ stream_options: { include_usage: true },
224
+ max_tokens: request.maxOutputTokens,
225
+ messages: toChatMessages(request.system, request.messages),
226
+ ...request.tools.length > 0 ? {
227
+ tools: request.tools.map((tool) => ({
228
+ type: "function",
229
+ function: { name: tool.name, description: tool.description, parameters: tool.inputSchema }
230
+ }))
231
+ } : {}
232
+ })
233
+ });
234
+ if (!response.ok || response.body === null) {
235
+ throw new ProviderError(`${this.name} request failed with status ${response.status}`);
236
+ }
237
+ let text = "";
238
+ let received = 0;
239
+ const limit = this.maxResponseBytes;
240
+ const calls = /* @__PURE__ */ new Map();
241
+ let finish;
242
+ let usage;
243
+ const decoder = new TextDecoder();
244
+ let buffer = "";
245
+ const handle = function* (line) {
246
+ if (!line.startsWith("data:")) return;
247
+ const data = line.slice(5).trim();
248
+ if (data === "" || data === "[DONE]") return;
249
+ let chunk;
250
+ try {
251
+ chunk = JSON.parse(data);
252
+ } catch {
253
+ throw new ProviderError("provider sent a stream chunk that is not JSON");
254
+ }
255
+ if (chunk.usage) usage = chunk.usage;
256
+ const choice = chunk.choices?.[0];
257
+ if (!choice) return;
258
+ if (choice.finish_reason) finish = choice.finish_reason;
259
+ const delta = choice.delta;
260
+ if (typeof delta?.content === "string" && delta.content.length > 0) {
261
+ text += delta.content;
262
+ yield { type: "text_delta", text: delta.content };
263
+ }
264
+ for (const part of delta?.tool_calls ?? []) {
265
+ const call = calls.get(part.index) ?? { id: "", name: "", args: "" };
266
+ if (part.id) call.id = part.id;
267
+ if (part.function?.name) call.name += part.function.name;
268
+ if (part.function?.arguments) call.args += part.function.arguments;
269
+ calls.set(part.index, call);
270
+ }
271
+ };
272
+ const reader = response.body.getReader();
273
+ try {
274
+ for (; ; ) {
275
+ const { done, value } = await reader.read();
276
+ if (done) break;
277
+ received += value.byteLength;
278
+ if (received > limit) throw new ProviderError(`${this.name} response passed the size limit of ${limit} bytes`);
279
+ buffer += decoder.decode(value, { stream: true });
280
+ const lines = buffer.split("\n");
281
+ buffer = lines.pop() ?? "";
282
+ for (const line of lines) yield* handle(line.trimEnd());
283
+ }
284
+ } finally {
285
+ await reader.cancel().catch(() => void 0);
286
+ }
287
+ if (buffer.length > 0) yield* handle(buffer.trimEnd());
288
+ if (!Number.isInteger(usage?.prompt_tokens) || !Number.isInteger(usage?.completion_tokens)) {
289
+ throw new ProviderError(`${this.name} reported no token usage, so token and cost caps cannot be enforced`);
290
+ }
291
+ const content = [];
292
+ if (text.length > 0) content.push({ type: "text", text });
293
+ for (const [, call] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {
294
+ let input = null;
295
+ try {
296
+ input = call.args.length > 0 ? JSON.parse(call.args) : {};
297
+ } catch {
298
+ input = null;
299
+ }
300
+ content.push({ type: "tool_use", id: call.id.length > 0 ? call.id : `call_${randomUUID()}`, name: call.name, input });
301
+ }
302
+ yield {
303
+ type: "message",
304
+ content,
305
+ stopReason: FINISH_REASONS[finish ?? ""] ?? "other",
306
+ usage: { inputTokens: usage?.prompt_tokens, outputTokens: usage?.completion_tokens }
307
+ };
308
+ }
309
+ };
310
+
311
+ // src/providers/fallback.ts
312
+ var CHARS_PER_TOKEN = 3;
313
+ function checkUsage(usage) {
314
+ for (const value of [usage.inputTokens, usage.outputTokens]) {
315
+ if (!Number.isInteger(value) || value < 0) throw new ProviderError("provider reported invalid token usage; caps cannot be enforced");
316
+ }
317
+ }
318
+ var FallbackProvider = class {
319
+ constructor(primary, fallback) {
320
+ this.primary = primary;
321
+ this.fallback = fallback;
322
+ this.name = primary.name;
323
+ this.model = primary.model;
324
+ this.maxAsks = (primary.maxAsks ?? 1) + (fallback.maxAsks ?? 1);
325
+ if (primary.pricing !== void 0 && fallback.pricing !== void 0) {
326
+ this.pricing = {
327
+ inputPerMTok: Math.max(primary.pricing.inputPerMTok, fallback.pricing.inputPerMTok),
328
+ outputPerMTok: Math.max(primary.pricing.outputPerMTok, fallback.pricing.outputPerMTok)
329
+ };
330
+ }
331
+ }
332
+ primary;
333
+ fallback;
334
+ name;
335
+ model;
336
+ pricing;
337
+ maxAsks;
338
+ async *stream(request) {
339
+ const held = [];
340
+ let heldChars = 0;
341
+ let started = false;
342
+ let final;
343
+ let failure;
344
+ try {
345
+ for await (const event of this.primary.stream(request)) {
346
+ started = true;
347
+ if (event.type === "message") final = event;
348
+ else {
349
+ held.push(event);
350
+ heldChars += event.text.length;
351
+ }
352
+ }
353
+ if (final === void 0) throw new ProviderError(`provider "${this.primary.name}" ended without a final message`);
354
+ } catch (error) {
355
+ failure = error;
356
+ }
357
+ const answered = (provider, fallbackFor) => ({
358
+ provider: provider.name,
359
+ model: provider.model,
360
+ fallback: fallbackFor !== void 0,
361
+ ...fallbackFor !== void 0 ? { fallbackFor } : {}
362
+ });
363
+ if (failure === void 0 && final !== void 0 && final.stopReason !== "refusal") {
364
+ for (const event of held) yield event;
365
+ yield { ...final, answeredBy: final.answeredBy ?? answered(this.primary) };
366
+ return;
367
+ }
368
+ if (request.signal?.aborted) {
369
+ if (failure !== void 0 || final === void 0) throw failure;
370
+ for (const event of held) yield event;
371
+ yield { ...final, answeredBy: final.answeredBy ?? answered(this.primary) };
372
+ return;
373
+ }
374
+ let spent = { inputTokens: 0, outputTokens: 0 };
375
+ if (final !== void 0) {
376
+ checkUsage(final.usage);
377
+ spent = final.usage;
378
+ } else if (started) {
379
+ const { system, messages, tools } = request;
380
+ spent = { inputTokens: Math.ceil(JSON.stringify({ system, messages, tools }).length / CHARS_PER_TOKEN), outputTokens: Math.ceil(heldChars / CHARS_PER_TOKEN) };
381
+ }
382
+ const left = request.maxOutputTokens - spent.outputTokens;
383
+ if (!(left > 0)) {
384
+ if (final !== void 0) {
385
+ for (const event of held) yield event;
386
+ yield { ...final, answeredBy: final.answeredBy ?? answered(this.primary) };
387
+ return;
388
+ }
389
+ throw failure;
390
+ }
391
+ let second;
392
+ for await (const event of this.fallback.stream({ ...request, maxOutputTokens: left })) {
393
+ if (event.type === "message") second = event;
394
+ else yield event;
395
+ }
396
+ if (second === void 0) throw new ProviderError(`provider "${this.fallback.name}" ended without a final message`);
397
+ checkUsage(second.usage);
398
+ yield {
399
+ ...second,
400
+ usage: {
401
+ inputTokens: spent.inputTokens + second.usage.inputTokens,
402
+ outputTokens: spent.outputTokens + second.usage.outputTokens
403
+ },
404
+ // A pair inside this one has already named the model that answered. Keep that name.
405
+ answeredBy: { ...second.answeredBy ?? answered(this.fallback), fallback: true, fallbackFor: failure === void 0 ? "refusal" : "error" }
406
+ };
407
+ }
408
+ };
409
+
410
+ // src/chat.ts
411
+ import { readFileSync } from "fs";
412
+ var ConfigError = class extends Error {
413
+ constructor(message) {
414
+ super(message);
415
+ this.name = "ConfigError";
416
+ }
417
+ };
418
+ function isSecureUrl(target) {
419
+ let url;
420
+ try {
421
+ url = new URL(target);
422
+ } catch {
423
+ return false;
424
+ }
425
+ if (url.protocol === "https:") return true;
426
+ return ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
427
+ }
428
+ function parseTarget(target, headerEnv, env) {
429
+ if (/^https?:\/\//i.test(target)) {
430
+ if (!isSecureUrl(target)) throw new ConfigError("--tools needs an https URL, or http on localhost only");
431
+ const headers = {};
432
+ for (const entry of headerEnv) {
433
+ const at = entry.indexOf("=");
434
+ const name = at < 0 ? "" : entry.slice(0, at);
435
+ const variable = at < 0 ? "" : entry.slice(at + 1);
436
+ if (!name || !variable || variable.includes("=")) throw new ConfigError(`--tools-header-env needs header=ENV_NAME, got "${entry}"`);
437
+ const value = env[variable];
438
+ if (!value) throw new ConfigError(`environment variable ${variable} is not set`);
439
+ headers[name] = value;
440
+ }
441
+ return { type: "http", url: target, ...Object.keys(headers).length > 0 ? { headers } : {} };
442
+ }
443
+ if (headerEnv.length > 0) throw new ConfigError("--tools-header-env works with an http(s) --tools URL only");
444
+ let words;
445
+ if (target.trimStart().startsWith("[")) {
446
+ try {
447
+ const parsed = JSON.parse(target);
448
+ if (!Array.isArray(parsed) || parsed.some((word) => typeof word !== "string")) throw new Error("not strings");
449
+ words = parsed;
450
+ } catch {
451
+ throw new ConfigError("--tools looks like a JSON list but is not a list of strings");
452
+ }
453
+ } else {
454
+ words = target.split(/\s+/).filter(Boolean);
455
+ }
456
+ const [command, ...args] = words;
457
+ if (!command) throw new ConfigError("--tools is empty");
458
+ return { type: "stdio", command, args };
459
+ }
460
+ function buildProvider(config, io, model = config.model) {
461
+ const key = config.apiKeyEnv === void 0 ? void 0 : io.env[config.apiKeyEnv];
462
+ if (config.apiKeyEnv !== void 0 && !key) throw new ConfigError(`environment variable ${config.apiKeyEnv} is not set`);
463
+ switch (config.provider) {
464
+ case "anthropic":
465
+ return new AnthropicProvider({
466
+ model,
467
+ baseUrl: config.baseUrl,
468
+ useDefaultEndpoint: config.useDefaultEndpoint,
469
+ pricing: config.pricing,
470
+ apiKey: key ?? io.env.ANTHROPIC_API_KEY
471
+ });
472
+ case "openai-compatible":
473
+ if (!config.baseUrl || !model) throw new ConfigError("--provider openai-compatible needs --base-url and --model");
474
+ return new OpenAICompatibleProvider({ baseUrl: config.baseUrl, model, apiKey: key, pricing: config.pricing });
475
+ case "scripted": {
476
+ if (!config.script) throw new ConfigError("--provider scripted needs --script <file>");
477
+ let script;
478
+ try {
479
+ script = JSON.parse(io.readFile(config.script));
480
+ } catch {
481
+ throw new ConfigError("--script must be a JSON file with a list of turns");
482
+ }
483
+ if (!Array.isArray(script)) throw new ConfigError("--script must be a JSON file with a list of turns");
484
+ return new ScriptedProvider(script, config.pricing);
485
+ }
486
+ }
487
+ }
488
+ var MAX_QUESTION_ARGUMENT_CHARS = 500;
489
+ var CommandLineGate = class {
490
+ constructor(preApproved, principal, mode, user, ask) {
491
+ this.mode = mode;
492
+ this.user = user;
493
+ this.ask = ask;
494
+ this.unused = preApproved.map((call) => digestCall({ tool: call.tool, input: call.input, principalId: principal.id, onBehalfOf: principal.parentId }));
495
+ }
496
+ mode;
497
+ user;
498
+ ask;
499
+ unused;
500
+ async request(request) {
501
+ const index = this.unused.indexOf(request.digest);
502
+ if (index >= 0) {
503
+ this.unused.splice(index, 1);
504
+ return { requestId: request.id, approved: true, digest: request.digest, decidedBy: "cli:pre-approved", approverKind: "host" };
505
+ }
506
+ if (this.mode === "ask" && this.ask !== void 0) {
507
+ const args = cutText(printable(JSON.stringify(request.input)), MAX_QUESTION_ARGUMENT_CHARS);
508
+ const answer = await this.ask(`[machine-made request] Approve ${printable(request.tool)} (${request.risk}) with ${args}? [y/N] `);
509
+ if (/^y(es)?$/i.test(answer.trim())) return { requestId: request.id, approved: true, digest: request.digest, decidedBy: `cli:${this.user}`, approverKind: "human" };
510
+ }
511
+ return { requestId: request.id, approved: false, reason: "not approved on the command line" };
512
+ }
513
+ };
514
+ async function buildSetup(config, io) {
515
+ if (config.scopes.length === 0) throw new ConfigError("give at least one --scope, or the assistant can do nothing");
516
+ if (!TOOL_RISKS.includes(config.toolRisk)) throw new ConfigError(`--tool-risk must be one of ${TOOL_RISKS.join(", ")}`);
517
+ if (config.approve === "ask" && io.ask === void 0) throw new ConfigError("--approve ask needs a terminal");
518
+ const principalOptions = { id: "cli-agent", parentId: config.user, scopes: config.scopes };
519
+ let provider;
520
+ try {
521
+ createPrincipal({ ...principalOptions, kind: "agent_of_human" });
522
+ provider = buildProvider(config, io);
523
+ if (config.fallbackModel !== void 0) {
524
+ if (config.provider === "scripted") throw new ConfigError("--fallback-model does not work with --provider scripted");
525
+ provider = new FallbackProvider(provider, buildProvider(config, io, config.fallbackModel));
526
+ }
527
+ } catch (error) {
528
+ if (error instanceof PrincipalError || error instanceof ProviderError) throw new ConfigError(error.message);
529
+ throw error;
530
+ }
531
+ const transport = parseTarget(config.tools, config.toolHeaderEnv, io.env);
532
+ const mcp = await connectMcpTools({ transport, scope: config.toolScope, risk: config.toolRisk });
533
+ return {
534
+ provider,
535
+ registry: new ToolRegistry().registerAll(mcp.tools),
536
+ gate: new CommandLineGate(config.preApproved, principalOptions, config.approve, config.user, io.ask),
537
+ principalOptions,
538
+ limits: config.limits,
539
+ system: config.system,
540
+ skipped: mcp.skipped,
541
+ close: () => mcp.close()
542
+ };
543
+ }
544
+ function runChat(setup, messages, signal) {
545
+ return runAssistant({
546
+ provider: setup.provider,
547
+ registry: setup.registry,
548
+ principal: createPrincipal({ ...setup.principalOptions, kind: "agent_of_human" }),
549
+ approvals: setup.gate,
550
+ messages,
551
+ system: setup.system,
552
+ limits: setup.limits,
553
+ signal
554
+ });
555
+ }
556
+ function readTextFile(path) {
557
+ return readFileSync(path, "utf8");
558
+ }
559
+
560
+ export {
561
+ connectMcpTools,
562
+ OpenAICompatibleProvider,
563
+ FallbackProvider,
564
+ ConfigError,
565
+ parseTarget,
566
+ buildSetup,
567
+ runChat,
568
+ readTextFile
569
+ };