@swifty.js/swifty 0.0.21 → 0.0.22
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.
- package/dist/{agent-ZCMBUWLZ.js → agent-5T7KNC7V.js} +1 -1
- package/dist/{anthropic-4ZVG7AMA.js → anthropic-P2R4GW6F.js} +1 -1
- package/dist/{chunk-Y5WGBAGP.js → chunk-HJIGA37J.js} +1 -1
- package/dist/{chunk-DDBH5AEN.js → chunk-L73IHJF4.js} +53 -53
- package/dist/{chunk-S6ZADC7C.js → chunk-NEAR6YPZ.js} +1 -1
- package/dist/{chunk-MUPVOATV.js → chunk-NLNH3IRT.js} +1 -1
- package/dist/{chunk-6E6UA7MT.js → chunk-YQQVB6VB.js} +9 -8
- package/dist/lib/agent-2AGRYN3R.js +9 -0
- package/dist/lib/anthropic-GFWP3ORB.js +22 -0
- package/dist/lib/bwrap-QRGPUP4J.js +6 -0
- package/dist/lib/checker-IIXJXQCB.js +19 -0
- package/dist/lib/chunk-6GOWRPYS.js +339 -0
- package/dist/lib/chunk-7URDLWQN.js +426 -0
- package/dist/lib/chunk-C7IHCJZ3.js +849 -0
- package/dist/lib/chunk-EJLGB2EJ.js +377 -0
- package/dist/lib/chunk-GHF2PSEW.js +8 -0
- package/dist/lib/chunk-GNBXECZN.js +243 -0
- package/dist/lib/chunk-HK2Z6WP4.js +223 -0
- package/dist/lib/chunk-LUEP4JMF.js +549 -0
- package/dist/lib/chunk-ORSYNBMM.js +38 -0
- package/dist/lib/chunk-RR2CZ6CY.js +598 -0
- package/dist/lib/chunk-UHVO63Y7.js +1246 -0
- package/dist/lib/chunk-UMFNTXKA.js +88 -0
- package/dist/lib/chunk-VUD72RTY.js +39 -0
- package/dist/lib/glob.wasm +0 -0
- package/dist/lib/index.d.ts +5350 -0
- package/dist/lib/index.js +12002 -0
- package/dist/lib/openai-HXRMPNB7.js +15 -0
- package/dist/lib/seatbelt-FT5IY73W.js +6 -0
- package/dist/lib/tool-filter-VF7TZRE5.js +19 -0
- package/dist/main.js +225 -225
- package/dist/{openai-HXD52MZB.js → openai-TVUUHRG7.js} +1 -1
- package/dist/{server-VMHWEO2Y.js → server-ZTLHJWEQ.js} +14 -14
- package/package.json +14 -2
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// src/logger/logger.ts
|
|
2
|
+
import { openSync, closeSync, mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { readdir, stat, unlink } from "fs/promises";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join, dirname, basename } from "path";
|
|
6
|
+
import pino from "pino";
|
|
7
|
+
var currentLogger = null;
|
|
8
|
+
var currentDest = null;
|
|
9
|
+
var currentFd = null;
|
|
10
|
+
function resolveLogPath(opts) {
|
|
11
|
+
const dir = opts.logDir ?? join(opts.workDir ?? process.cwd(), ".swifty", "logs");
|
|
12
|
+
return join(dir, `${opts.sessionId}.jsonl`);
|
|
13
|
+
}
|
|
14
|
+
function ensureSwiftyGitignore(logPath) {
|
|
15
|
+
let dir = dirname(logPath);
|
|
16
|
+
while (basename(dir) !== ".swifty") {
|
|
17
|
+
const parent = dirname(dir);
|
|
18
|
+
if (parent === dir) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
dir = parent;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
writeFileSync(join(dir, ".gitignore"), "*\n", { flag: "wx" });
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function sanitizeNameSegment(name) {
|
|
29
|
+
const cleaned = name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
30
|
+
return cleaned || "unnamed";
|
|
31
|
+
}
|
|
32
|
+
function flushDestination(dest) {
|
|
33
|
+
if (typeof dest !== "object" || dest === null) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const fn = Reflect.get(dest, "flushSync");
|
|
37
|
+
if (typeof fn === "function") {
|
|
38
|
+
fn.call(dest);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
var LOG_LEVEL = "warn";
|
|
42
|
+
function initLogger(opts) {
|
|
43
|
+
if (currentLogger) {
|
|
44
|
+
closeLogger();
|
|
45
|
+
}
|
|
46
|
+
const logPath = resolveLogPath(opts);
|
|
47
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
48
|
+
ensureSwiftyGitignore(logPath);
|
|
49
|
+
const fd = openSync(logPath, "a");
|
|
50
|
+
currentFd = fd;
|
|
51
|
+
currentDest = pino.destination(fd);
|
|
52
|
+
const pinoOpts = {
|
|
53
|
+
level: LOG_LEVEL,
|
|
54
|
+
base: { sessionId: opts.sessionId, mode: opts.mode },
|
|
55
|
+
serializers: { err: errSerializer }
|
|
56
|
+
};
|
|
57
|
+
if (opts.stdout) {
|
|
58
|
+
currentLogger = pino(
|
|
59
|
+
pinoOpts,
|
|
60
|
+
pino.multistream([
|
|
61
|
+
{ stream: currentDest, level: LOG_LEVEL },
|
|
62
|
+
{ stream: process.stdout, level: LOG_LEVEL }
|
|
63
|
+
])
|
|
64
|
+
);
|
|
65
|
+
} else {
|
|
66
|
+
currentLogger = pino(pinoOpts, currentDest);
|
|
67
|
+
}
|
|
68
|
+
if (!opts.skipCleanup) {
|
|
69
|
+
const workDir = opts.workDir ?? process.cwd();
|
|
70
|
+
void cleanExpiredLogs(workDir).catch(() => {
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return currentLogger;
|
|
74
|
+
}
|
|
75
|
+
function getLogger() {
|
|
76
|
+
return currentLogger;
|
|
77
|
+
}
|
|
78
|
+
function closeLogger() {
|
|
79
|
+
if (currentLogger) {
|
|
80
|
+
try {
|
|
81
|
+
currentLogger.flush();
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
currentLogger = null;
|
|
85
|
+
}
|
|
86
|
+
if (currentDest) {
|
|
87
|
+
flushDestination(currentDest);
|
|
88
|
+
currentDest = null;
|
|
89
|
+
}
|
|
90
|
+
if (currentFd !== null) {
|
|
91
|
+
try {
|
|
92
|
+
closeSync(currentFd);
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
currentFd = null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
var silentFallback = pino({ level: "silent" });
|
|
99
|
+
var logger = new Proxy(silentFallback, {
|
|
100
|
+
get(_target, prop, receiver) {
|
|
101
|
+
const current = getLogger();
|
|
102
|
+
const target = current ?? _target;
|
|
103
|
+
const value = Reflect.get(target, prop, receiver);
|
|
104
|
+
if (typeof value === "function") {
|
|
105
|
+
return value.bind(target);
|
|
106
|
+
}
|
|
107
|
+
return value;
|
|
108
|
+
},
|
|
109
|
+
set(_target, prop, value) {
|
|
110
|
+
return Reflect.set(getLogger() ?? _target, prop, value);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
function createChildLogger(bindings) {
|
|
114
|
+
let cachedChild = null;
|
|
115
|
+
let cachedLogger = null;
|
|
116
|
+
const resolveChild = () => {
|
|
117
|
+
const current = getLogger();
|
|
118
|
+
if (!current) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (cachedChild === null || cachedLogger !== current) {
|
|
122
|
+
cachedChild = current.child(bindings);
|
|
123
|
+
cachedLogger = current;
|
|
124
|
+
}
|
|
125
|
+
return cachedChild;
|
|
126
|
+
};
|
|
127
|
+
return new Proxy(silentFallback, {
|
|
128
|
+
get(_target, prop, receiver) {
|
|
129
|
+
const target = resolveChild() ?? _target;
|
|
130
|
+
const value = Reflect.get(target, prop, receiver);
|
|
131
|
+
if (typeof value === "function") {
|
|
132
|
+
return value.bind(target);
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
},
|
|
136
|
+
set(_target, prop, value) {
|
|
137
|
+
return Reflect.set(resolveChild() ?? _target, prop, value);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
async function cleanDir(dir) {
|
|
142
|
+
let files;
|
|
143
|
+
try {
|
|
144
|
+
files = (await readdir(dir)).filter((f) => f.endsWith(".jsonl"));
|
|
145
|
+
} catch {
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
let removed = 0;
|
|
150
|
+
for (const file of files) {
|
|
151
|
+
const filePath = join(dir, file);
|
|
152
|
+
try {
|
|
153
|
+
const s = await stat(filePath);
|
|
154
|
+
if (now - s.mtimeMs > 30 * 24 * 60 * 60 * 1e3) {
|
|
155
|
+
await unlink(filePath);
|
|
156
|
+
removed++;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return removed;
|
|
162
|
+
}
|
|
163
|
+
async function cleanExpiredLogs(workDir) {
|
|
164
|
+
let removed = 0;
|
|
165
|
+
removed += await cleanDir(join(workDir, ".swifty", "logs"));
|
|
166
|
+
const teamsDir = join(homedir(), ".swifty", "teams");
|
|
167
|
+
let teams;
|
|
168
|
+
try {
|
|
169
|
+
teams = await readdir(teamsDir);
|
|
170
|
+
} catch {
|
|
171
|
+
return removed;
|
|
172
|
+
}
|
|
173
|
+
for (const team of teams) {
|
|
174
|
+
removed += await cleanDir(join(teamsDir, team, "logs"));
|
|
175
|
+
}
|
|
176
|
+
return removed;
|
|
177
|
+
}
|
|
178
|
+
var CAUSE_MAX_DEPTH = 5;
|
|
179
|
+
var RESERVED_KEYS = /* @__PURE__ */ new Set(["name", "message", "stack", "cause"]);
|
|
180
|
+
function serializeErrorInstance(err, depth) {
|
|
181
|
+
const out = {
|
|
182
|
+
type: err.name,
|
|
183
|
+
message: err.message,
|
|
184
|
+
stack: err.stack
|
|
185
|
+
};
|
|
186
|
+
const causeDescriptor = Object.getOwnPropertyDescriptor(err, "cause");
|
|
187
|
+
if (causeDescriptor && depth < CAUSE_MAX_DEPTH) {
|
|
188
|
+
const cause = causeDescriptor.value;
|
|
189
|
+
if (cause instanceof Error) {
|
|
190
|
+
out.cause = serializeErrorInstance(cause, depth + 1);
|
|
191
|
+
} else if (cause !== void 0) {
|
|
192
|
+
out.cause = { message: safeStringify(cause) };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const key of Object.keys(err)) {
|
|
196
|
+
if (RESERVED_KEYS.has(key)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const descriptor = Object.getOwnPropertyDescriptor(err, key);
|
|
200
|
+
if (descriptor) {
|
|
201
|
+
const fieldValue = descriptor.value;
|
|
202
|
+
out[key] = fieldValue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function safeStringify(value) {
|
|
208
|
+
if (typeof value === "string") {
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const json = JSON.stringify(value);
|
|
213
|
+
if (json !== void 0) {
|
|
214
|
+
return json;
|
|
215
|
+
}
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
return String(value);
|
|
220
|
+
} catch {
|
|
221
|
+
return Object.prototype.toString.call(value);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function errSerializer(err) {
|
|
225
|
+
if (err instanceof Error) {
|
|
226
|
+
return serializeErrorInstance(err, 0);
|
|
227
|
+
}
|
|
228
|
+
return { message: safeStringify(err), value: err };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/utils/index.ts
|
|
232
|
+
var log = createChildLogger({ module: "utils" });
|
|
233
|
+
var DANGEROUSLY_JSON = "dangerouslyJson";
|
|
234
|
+
function contentToText(content) {
|
|
235
|
+
if (typeof content === "string") {
|
|
236
|
+
return content;
|
|
237
|
+
}
|
|
238
|
+
const parts = [];
|
|
239
|
+
for (const block of content) {
|
|
240
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
241
|
+
const b = block;
|
|
242
|
+
parts.push(b.text);
|
|
243
|
+
} else if (block.type === "image" && typeof block.source === "object") {
|
|
244
|
+
const b = block;
|
|
245
|
+
const mediaType = b.source.type === "base64" ? b.source.media_type : "image";
|
|
246
|
+
parts.push(`[Image: ${mediaType}]`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return parts.join("\n");
|
|
250
|
+
}
|
|
251
|
+
function isRecord(value) {
|
|
252
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
253
|
+
}
|
|
254
|
+
function asRecord(value) {
|
|
255
|
+
if (isRecord(value)) {
|
|
256
|
+
return value;
|
|
257
|
+
}
|
|
258
|
+
if (Array.isArray(value)) {
|
|
259
|
+
return Object.fromEntries(value.entries());
|
|
260
|
+
}
|
|
261
|
+
return {};
|
|
262
|
+
}
|
|
263
|
+
function asString(value) {
|
|
264
|
+
if (typeof value === "string") {
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
return String(value);
|
|
268
|
+
}
|
|
269
|
+
function asErrorString(value) {
|
|
270
|
+
if (value instanceof Error) {
|
|
271
|
+
return value.message;
|
|
272
|
+
}
|
|
273
|
+
return asString(value);
|
|
274
|
+
}
|
|
275
|
+
function isObject(value) {
|
|
276
|
+
return typeof value === "object" && value !== null;
|
|
277
|
+
}
|
|
278
|
+
function toTry(fn, ctx) {
|
|
279
|
+
if (typeof fn !== "function") {
|
|
280
|
+
return fn;
|
|
281
|
+
}
|
|
282
|
+
return function(...args) {
|
|
283
|
+
let ret;
|
|
284
|
+
try {
|
|
285
|
+
ret = ctx ? fn.call(ctx, ...args) : fn.call(this, ...args);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
log.error({ err }, "utils operation failed");
|
|
288
|
+
return void 0;
|
|
289
|
+
}
|
|
290
|
+
return ret;
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
var safeJSONParse = toTry(JSON.parse, JSON);
|
|
294
|
+
function asError(err) {
|
|
295
|
+
if (err instanceof Error) {
|
|
296
|
+
return err;
|
|
297
|
+
}
|
|
298
|
+
return new Error(String(err));
|
|
299
|
+
}
|
|
300
|
+
function intArg(args, key, fallback) {
|
|
301
|
+
const v = args[key];
|
|
302
|
+
if (typeof v === "number") {
|
|
303
|
+
return Math.floor(v);
|
|
304
|
+
}
|
|
305
|
+
if (typeof v === "string") {
|
|
306
|
+
const n = Number.parseInt(v, 10);
|
|
307
|
+
return Number.isNaN(n) ? fallback : n;
|
|
308
|
+
}
|
|
309
|
+
return fallback;
|
|
310
|
+
}
|
|
311
|
+
function strList(raw) {
|
|
312
|
+
if (Array.isArray(raw)) {
|
|
313
|
+
return raw.filter((v) => typeof v === "string");
|
|
314
|
+
}
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
function strArg(args, key, fallback) {
|
|
318
|
+
const v = args[key];
|
|
319
|
+
if (typeof v === "string") {
|
|
320
|
+
return v;
|
|
321
|
+
}
|
|
322
|
+
return fallback ?? "";
|
|
323
|
+
}
|
|
324
|
+
function boolArg(args, key, fallback) {
|
|
325
|
+
const v = args[key];
|
|
326
|
+
if (typeof v === "boolean") {
|
|
327
|
+
return v;
|
|
328
|
+
}
|
|
329
|
+
return fallback ?? Boolean(v);
|
|
330
|
+
}
|
|
331
|
+
function quickSort(arr, compare) {
|
|
332
|
+
if (arr.length <= 1) {
|
|
333
|
+
return [...arr];
|
|
334
|
+
}
|
|
335
|
+
const pivotIndex = Math.floor(arr.length / 2);
|
|
336
|
+
const pivot = arr[pivotIndex];
|
|
337
|
+
if (pivot === void 0) {
|
|
338
|
+
return [...arr];
|
|
339
|
+
}
|
|
340
|
+
const left = [];
|
|
341
|
+
const right = [];
|
|
342
|
+
const equal = [];
|
|
343
|
+
for (const item of arr) {
|
|
344
|
+
const result = compare(item, pivot);
|
|
345
|
+
if (result < 0) {
|
|
346
|
+
left.push(item);
|
|
347
|
+
} else if (result > 0) {
|
|
348
|
+
right.push(item);
|
|
349
|
+
} else {
|
|
350
|
+
equal.push(item);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return [...quickSort(left, compare), ...equal, ...quickSort(right, compare)];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export {
|
|
357
|
+
sanitizeNameSegment,
|
|
358
|
+
initLogger,
|
|
359
|
+
closeLogger,
|
|
360
|
+
logger,
|
|
361
|
+
createChildLogger,
|
|
362
|
+
DANGEROUSLY_JSON,
|
|
363
|
+
contentToText,
|
|
364
|
+
isRecord,
|
|
365
|
+
asRecord,
|
|
366
|
+
asString,
|
|
367
|
+
asErrorString,
|
|
368
|
+
isObject,
|
|
369
|
+
toTry,
|
|
370
|
+
safeJSONParse,
|
|
371
|
+
asError,
|
|
372
|
+
intArg,
|
|
373
|
+
strList,
|
|
374
|
+
strArg,
|
|
375
|
+
boolArg,
|
|
376
|
+
quickSort
|
|
377
|
+
};
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asErrorString,
|
|
3
|
+
asRecord,
|
|
4
|
+
createChildLogger,
|
|
5
|
+
strArg
|
|
6
|
+
} from "./chunk-EJLGB2EJ.js";
|
|
7
|
+
import {
|
|
8
|
+
MCP_CALL_TOOL_NAME
|
|
9
|
+
} from "./chunk-GHF2PSEW.js";
|
|
10
|
+
|
|
11
|
+
// src/mcp/tool-wrapper.ts
|
|
12
|
+
var log = createChildLogger({ module: "mcp" });
|
|
13
|
+
var MCP_TOOL_PREFIX = "mcp__";
|
|
14
|
+
var MCP_NAME_SEP = "__";
|
|
15
|
+
function sanitizeSegment(s) {
|
|
16
|
+
return s.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
17
|
+
}
|
|
18
|
+
function mcpToolNamePrefix(serverName) {
|
|
19
|
+
return MCP_TOOL_PREFIX + sanitizeSegment(serverName) + MCP_NAME_SEP;
|
|
20
|
+
}
|
|
21
|
+
function buildMcpToolName(serverName, toolName) {
|
|
22
|
+
return mcpToolNamePrefix(serverName) + sanitizeSegment(toolName);
|
|
23
|
+
}
|
|
24
|
+
var MCPToolWrapper = class {
|
|
25
|
+
name;
|
|
26
|
+
description;
|
|
27
|
+
category = "command";
|
|
28
|
+
// MCP tools are lazily loaded by default to avoid cramming all schemas into the prompt
|
|
29
|
+
deferred = true;
|
|
30
|
+
mcpServerName;
|
|
31
|
+
client;
|
|
32
|
+
originalName;
|
|
33
|
+
inputSchema;
|
|
34
|
+
constructor(client, serverName, tool) {
|
|
35
|
+
this.name = buildMcpToolName(serverName, tool.name);
|
|
36
|
+
this.description = tool.description;
|
|
37
|
+
this.originalName = tool.name;
|
|
38
|
+
this.client = client;
|
|
39
|
+
this.inputSchema = tool.inputSchema;
|
|
40
|
+
this.mcpServerName = serverName;
|
|
41
|
+
}
|
|
42
|
+
/** Original JSON schema. McpCall's argument coercion walks it layer by layer. */
|
|
43
|
+
mcpInputSchema() {
|
|
44
|
+
return this.inputSchema ?? {};
|
|
45
|
+
}
|
|
46
|
+
/** In eager mode the defer flag is cleared so MCP tools go straight into tools[]. */
|
|
47
|
+
setDeferLoading(on) {
|
|
48
|
+
this.deferred = on;
|
|
49
|
+
}
|
|
50
|
+
schema() {
|
|
51
|
+
return {
|
|
52
|
+
name: this.name,
|
|
53
|
+
description: this.description,
|
|
54
|
+
input_schema: this.inputSchema
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async execute(_ctx, args) {
|
|
58
|
+
try {
|
|
59
|
+
const { output, isError } = await this.client.callTool(this.originalName, args);
|
|
60
|
+
return { output, isError };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.error({ err }, "mcp operation failed");
|
|
63
|
+
return {
|
|
64
|
+
output: `MCP tool error: ${asErrorString(err)}`,
|
|
65
|
+
isError: true
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/tools/mcp-call.ts
|
|
72
|
+
function coerceScalar(value, want) {
|
|
73
|
+
if (want === "string" && typeof value === "number" && Number.isFinite(value)) {
|
|
74
|
+
return String(value);
|
|
75
|
+
}
|
|
76
|
+
if ((want === "integer" || want === "number") && typeof value === "string") {
|
|
77
|
+
const text = value.trim();
|
|
78
|
+
if (text === "") {
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
const shape = want === "integer" ? /^[+-]?\d+$/ : /^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/;
|
|
82
|
+
if (!shape.test(text)) {
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
const parsed = want === "integer" ? Number.parseInt(text, 10) : Number.parseFloat(text);
|
|
86
|
+
return Number.isNaN(parsed) ? value : parsed;
|
|
87
|
+
}
|
|
88
|
+
if (want === "boolean" && typeof value === "string") {
|
|
89
|
+
const low = value.trim().toLowerCase();
|
|
90
|
+
if (low === "true") {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
if (low === "false") {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function coerceBySchema(value, schema) {
|
|
100
|
+
if (typeof schema !== "object" || schema === null) {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
const schemaObj = asRecord(schema);
|
|
104
|
+
const want = strArg(schemaObj, "type", "");
|
|
105
|
+
if (want === "object" && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
106
|
+
const props = asRecord(schemaObj.properties ?? {});
|
|
107
|
+
const out = {};
|
|
108
|
+
for (const [key, item] of Object.entries(value)) {
|
|
109
|
+
out[key] = key in props ? coerceBySchema(item, props[key]) : item;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
if (want === "array") {
|
|
114
|
+
const itemSchema = schemaObj.items ?? {};
|
|
115
|
+
let working = value;
|
|
116
|
+
if (typeof working === "object" && working !== null && !Array.isArray(working)) {
|
|
117
|
+
const entries = Object.values(working);
|
|
118
|
+
if (entries.length === 1 && Array.isArray(entries[0])) {
|
|
119
|
+
working = entries[0];
|
|
120
|
+
}
|
|
121
|
+
} else if (typeof working === "string") {
|
|
122
|
+
working = working.split(",").map((p) => p.trim()).filter((p) => p !== "");
|
|
123
|
+
}
|
|
124
|
+
if (Array.isArray(working)) {
|
|
125
|
+
return working.map((item) => coerceBySchema(item, itemSchema));
|
|
126
|
+
}
|
|
127
|
+
return working;
|
|
128
|
+
}
|
|
129
|
+
if (want !== "") {
|
|
130
|
+
return coerceScalar(value, want);
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
function mcpCallPermissionContent(server, tool) {
|
|
135
|
+
if (tool.startsWith(MCP_TOOL_PREFIX)) {
|
|
136
|
+
const rest = tool.slice(MCP_TOOL_PREFIX.length);
|
|
137
|
+
const idx = rest.indexOf(MCP_NAME_SEP);
|
|
138
|
+
if (idx >= 0) {
|
|
139
|
+
return sanitizeSegment(rest.slice(0, idx)) + MCP_NAME_SEP + sanitizeSegment(rest.slice(idx + MCP_NAME_SEP.length));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return sanitizeSegment(server) + MCP_NAME_SEP + sanitizeSegment(tool);
|
|
143
|
+
}
|
|
144
|
+
function isMcpToolLike(tool) {
|
|
145
|
+
return "mcpInputSchema" in tool && typeof tool.mcpInputSchema === "function";
|
|
146
|
+
}
|
|
147
|
+
var McpCallTool = class {
|
|
148
|
+
constructor(registry) {
|
|
149
|
+
this.registry = registry;
|
|
150
|
+
}
|
|
151
|
+
registry;
|
|
152
|
+
name = MCP_CALL_TOOL_NAME;
|
|
153
|
+
description = "Invoke a tool on a connected MCP server. Call ToolSearch first to load the tool's schema, then pass its arguments here exactly as that schema requires, using the same JSON types.";
|
|
154
|
+
category = "command";
|
|
155
|
+
// This tool must stay in tools[] itself, otherwise the model has no entry point
|
|
156
|
+
deferred = false;
|
|
157
|
+
schema() {
|
|
158
|
+
return {
|
|
159
|
+
name: this.name,
|
|
160
|
+
description: this.description,
|
|
161
|
+
input_schema: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
server: {
|
|
165
|
+
type: "string",
|
|
166
|
+
description: "MCP server name, e.g. 'linear'."
|
|
167
|
+
},
|
|
168
|
+
tool: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "Full tool name as returned by ToolSearch, e.g. 'mcp__linear__create_issue'."
|
|
171
|
+
},
|
|
172
|
+
arguments: {
|
|
173
|
+
type: "object",
|
|
174
|
+
description: "The target tool's arguments. Must match that tool's input_schema exactly, including JSON types: bare numbers for integer fields, bare true/false for boolean fields, quoted strings for string fields, and plain JSON arrays for array fields."
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
required: ["server", "tool", "arguments"]
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Try in order: full name / server+short name / unique short-name suffix match.
|
|
183
|
+
*
|
|
184
|
+
* The model very often passes only the short name (roughly three in ten calls in
|
|
185
|
+
* practice), so this must be tolerant — otherwise it needlessly costs a retry
|
|
186
|
+
* round.
|
|
187
|
+
*/
|
|
188
|
+
resolve(server, tool) {
|
|
189
|
+
const direct = this.registry.get(tool) ?? this.registry.get(buildMcpToolName(server, tool));
|
|
190
|
+
if (direct) {
|
|
191
|
+
return direct;
|
|
192
|
+
}
|
|
193
|
+
const suffix = MCP_NAME_SEP + sanitizeSegment(tool);
|
|
194
|
+
const matches = this.registry.listTools().filter((t) => t.name.startsWith(MCP_TOOL_PREFIX) && t.name.endsWith(suffix));
|
|
195
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
196
|
+
}
|
|
197
|
+
availableNames() {
|
|
198
|
+
return this.registry.listTools().filter((t) => t.name.startsWith(MCP_TOOL_PREFIX)).map((t) => t.name).sort();
|
|
199
|
+
}
|
|
200
|
+
async execute(ctx, args) {
|
|
201
|
+
const server = strArg(args, "server", "");
|
|
202
|
+
const tool = strArg(args, "tool", "");
|
|
203
|
+
if (tool === "") {
|
|
204
|
+
return { output: "McpCall requires a 'tool' name", isError: true };
|
|
205
|
+
}
|
|
206
|
+
const target = this.resolve(server, tool);
|
|
207
|
+
if (!target) {
|
|
208
|
+
const names = this.availableNames();
|
|
209
|
+
const hint = names.length > 0 ? names.join(", ") : "(none connected)";
|
|
210
|
+
return {
|
|
211
|
+
output: `Unknown MCP tool '${tool}' on server '${server}'. Available tools: ${hint}`,
|
|
212
|
+
isError: true
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
let inner = {};
|
|
216
|
+
if (typeof args.arguments === "object" && args.arguments !== null && !Array.isArray(args.arguments)) {
|
|
217
|
+
inner = asRecord(args.arguments);
|
|
218
|
+
}
|
|
219
|
+
if (isMcpToolLike(target)) {
|
|
220
|
+
const schema = target.mcpInputSchema();
|
|
221
|
+
if (Object.keys(schema).length > 0) {
|
|
222
|
+
const fixed = coerceBySchema(inner, schema);
|
|
223
|
+
if (typeof fixed === "object" && fixed !== null && !Array.isArray(fixed)) {
|
|
224
|
+
inner = asRecord(fixed);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return target.execute(ctx, inner);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export {
|
|
233
|
+
MCP_TOOL_PREFIX,
|
|
234
|
+
MCP_NAME_SEP,
|
|
235
|
+
sanitizeSegment,
|
|
236
|
+
mcpToolNamePrefix,
|
|
237
|
+
buildMcpToolName,
|
|
238
|
+
MCPToolWrapper,
|
|
239
|
+
coerceBySchema,
|
|
240
|
+
mcpCallPermissionContent,
|
|
241
|
+
isMcpToolLike,
|
|
242
|
+
McpCallTool
|
|
243
|
+
};
|