repoburg 1.3.255 → 1.3.256
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/daemon/dist/index.js +61 -3
- package/daemon/dist/staticServing.js +2 -2
- package/daemon/dist/tabtape/api/follow.js +174 -0
- package/daemon/dist/tabtape/api/http.js +221 -0
- package/daemon/dist/tabtape/api/record.js +149 -0
- package/daemon/dist/tabtape/api/router.js +519 -0
- package/daemon/dist/tabtape/api/router.spec.js +604 -0
- package/daemon/dist/tabtape/api/static.js +70 -0
- package/daemon/dist/tabtape/api/ws.js +166 -0
- package/daemon/dist/tabtape/buffer/buffer.js +347 -0
- package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
- package/daemon/dist/tabtape/config.js +108 -0
- package/daemon/dist/tabtape/e2e-server.js +96 -0
- package/daemon/dist/tabtape/errors.js +77 -0
- package/daemon/dist/tabtape/export/export.js +331 -0
- package/daemon/dist/tabtape/export/export.spec.js +262 -0
- package/daemon/dist/tabtape/har/gostd.js +395 -0
- package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
- package/daemon/dist/tabtape/har/har.js +582 -0
- package/daemon/dist/tabtape/har/har.spec.js +325 -0
- package/daemon/dist/tabtape/index.js +134 -0
- package/daemon/dist/tabtape/index.spec.js +104 -0
- package/daemon/dist/tabtape/lock.js +67 -0
- package/daemon/dist/tabtape/log.js +32 -0
- package/daemon/dist/tabtape/mcp/definitions.js +362 -0
- package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
- package/daemon/dist/tabtape/mcp/params.js +345 -0
- package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
- package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
- package/daemon/dist/tabtape/mcp/server.js +411 -0
- package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
- package/daemon/dist/tabtape/mcp/tools.js +1085 -0
- package/daemon/dist/tabtape/query/format.js +195 -0
- package/daemon/dist/tabtape/query/network.js +133 -0
- package/daemon/dist/tabtape/query/perf.js +93 -0
- package/daemon/dist/tabtape/query/query.spec.js +233 -0
- package/daemon/dist/tabtape/query/reader.js +602 -0
- package/daemon/dist/tabtape/query/types.js +274 -0
- package/daemon/dist/tabtape/redact.js +37 -0
- package/daemon/dist/tabtape/repro/lang.js +95 -0
- package/daemon/dist/tabtape/repro/repro.js +422 -0
- package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
- package/daemon/dist/tabtape/sink.js +24 -0
- package/daemon/dist/tabtape/store/record.js +99 -0
- package/daemon/dist/tabtape/store/segment.js +267 -0
- package/daemon/dist/tabtape/store/store.js +846 -0
- package/daemon/dist/tabtape/store/store.spec.js +485 -0
- package/daemon/dist/tabtape/testing/querytest.js +119 -0
- package/daemon/dist/tabtape/testing/tmp.js +33 -0
- package/package.json +3 -2
- package/tabtape/extension/dist/background.js +8 -0
- package/tabtape/extension/dist/background.js.map +7 -0
- package/tabtape/extension/dist/bridge.js +3 -0
- package/tabtape/extension/dist/bridge.js.map +7 -0
- package/tabtape/extension/dist/icons/128.png +0 -0
- package/tabtape/extension/dist/icons/16.png +0 -0
- package/tabtape/extension/dist/icons/32.png +0 -0
- package/tabtape/extension/dist/icons/48.png +0 -0
- package/tabtape/extension/dist/manifest.json +28 -0
- package/tabtape/extension/dist/popup.css +62 -0
- package/tabtape/extension/dist/popup.html +34 -0
- package/tabtape/extension/dist/popup.js +4 -0
- package/tabtape/extension/dist/popup.js.map +7 -0
- package/tabtape/extension/dist/recorder.js +133 -0
- package/tabtape/extension/dist/recorder.js.map +1 -0
- package/tabtape/extension/dist/state.js +2 -0
- package/tabtape/extension/dist/state.js.map +1 -0
- package/tabtape/playwright/mcp.mjs +58 -0
- package/tabtape/playwright/tabtape.d.mts +37 -0
- package/tabtape/playwright/tabtape.mjs +146 -0
- package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
- package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
- package/tabtape/ui/dist/app/index.html +14 -0
- package/tabtape/ui/dist/export/viewer.css +2 -0
- package/tabtape/ui/dist/export/viewer.js +143 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ArgumentsError = exports.TOOL_LIST = void 0;
|
|
4
|
+
exports.decodeArguments = decodeArguments;
|
|
5
|
+
exports.hasTool = hasTool;
|
|
6
|
+
exports.callTool = callTool;
|
|
7
|
+
const errors_1 = require("../errors");
|
|
8
|
+
const types_1 = require("../query/types");
|
|
9
|
+
const gofmt_1 = require("./gofmt");
|
|
10
|
+
// ---------- schema building ----------
|
|
11
|
+
const STR = { type: 'string' };
|
|
12
|
+
const INT = { type: 'integer' };
|
|
13
|
+
const NUM = { type: 'number' };
|
|
14
|
+
const BOOL = { type: 'boolean' };
|
|
15
|
+
const NULL_NUM = { type: ['null', 'number'] };
|
|
16
|
+
/** A Go map[string]string: never null (jsonschema-go does not make maps nullable). */
|
|
17
|
+
const STR_MAP = { type: 'object', additionalProperties: STR };
|
|
18
|
+
/** A Go struct: no other properties; fields without omitempty are required ("required" is left out when none are). */
|
|
19
|
+
function obj(properties, required, nullable = false) {
|
|
20
|
+
return {
|
|
21
|
+
type: nullable ? ['null', 'object'] : 'object',
|
|
22
|
+
properties,
|
|
23
|
+
...(required.length > 0 ? { required } : {}),
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** A Go slice: null when nil. */
|
|
28
|
+
const list = (items) => ({ type: ['null', 'array'], items });
|
|
29
|
+
const sessionOut = obj({
|
|
30
|
+
id: STR,
|
|
31
|
+
startedAt: STR,
|
|
32
|
+
durationMs: INT,
|
|
33
|
+
title: STR,
|
|
34
|
+
url: STR,
|
|
35
|
+
browser: STR,
|
|
36
|
+
pages: INT,
|
|
37
|
+
clicks: INT,
|
|
38
|
+
errors: INT,
|
|
39
|
+
failedRequests: INT,
|
|
40
|
+
trimmed: BOOL,
|
|
41
|
+
live: BOOL,
|
|
42
|
+
source: STR,
|
|
43
|
+
user: STR,
|
|
44
|
+
perf: STR,
|
|
45
|
+
replay: STR,
|
|
46
|
+
}, ['id', 'startedAt', 'durationMs', 'pages', 'clicks', 'errors', 'failedRequests', 'replay']);
|
|
47
|
+
const itemOut = obj({ id: STR, at: STR, offsetMs: INT, kind: STR, text: STR, replay: STR }, ['id', 'at', 'offsetMs', 'kind', 'text', 'replay']);
|
|
48
|
+
const frameCounts = obj({ sent: INT, received: INT, sentBytes: NUM, receivedBytes: NUM, dropped: INT, dataOmitted: INT }, ['sent', 'received', 'sentBytes', 'receivedBytes'], true);
|
|
49
|
+
const requestSummaryProps = {
|
|
50
|
+
id: STR,
|
|
51
|
+
at: STR,
|
|
52
|
+
offsetMs: INT,
|
|
53
|
+
kind: STR,
|
|
54
|
+
method: STR,
|
|
55
|
+
url: STR,
|
|
56
|
+
status: INT,
|
|
57
|
+
error: STR,
|
|
58
|
+
durationMs: NULL_NUM,
|
|
59
|
+
size: NUM,
|
|
60
|
+
frames: frameCounts,
|
|
61
|
+
reqBody: STR,
|
|
62
|
+
resBody: STR,
|
|
63
|
+
replay: STR,
|
|
64
|
+
};
|
|
65
|
+
const requestSummaryRequired = ['id', 'at', 'offsetMs', 'kind', 'method', 'url', 'replay'];
|
|
66
|
+
const blockedCookie = obj({ name: STR, reasons: list(STR), domain: STR, path: STR }, ['name', 'reasons']);
|
|
67
|
+
const frameOut = obj({
|
|
68
|
+
id: STR,
|
|
69
|
+
at: STR,
|
|
70
|
+
offsetMs: INT,
|
|
71
|
+
dir: STR,
|
|
72
|
+
opcode: INT,
|
|
73
|
+
size: NUM,
|
|
74
|
+
data: STR,
|
|
75
|
+
truncated: BOOL,
|
|
76
|
+
dataOmitted: BOOL,
|
|
77
|
+
dropped: INT,
|
|
78
|
+
error: STR,
|
|
79
|
+
}, ['id', 'at', 'offsetMs', 'dir', 'opcode', 'size']);
|
|
80
|
+
const timing = obj({ blocked: NULL_NUM, dns: NULL_NUM, connect: NULL_NUM, ssl: NULL_NUM, send: NULL_NUM, wait: NULL_NUM, receive: NULL_NUM }, [], true);
|
|
81
|
+
const consoleItem = obj({ id: STR, at: STR, offsetMs: INT, level: STR, source: STR, text: STR, stack: STR, location: STR, replay: STR }, ['id', 'at', 'offsetMs', 'level', 'source', 'text', 'replay']);
|
|
82
|
+
const stateActionOut = obj({ id: STR, at: STR, offsetMs: INT, store: STR, action: STR, changes: list(STR), replay: STR }, ['id', 'at', 'offsetMs', 'store', 'action', 'replay']);
|
|
83
|
+
const OUTPUT = {
|
|
84
|
+
list: obj({ sessions: list(sessionOut), matched: INT }, ['sessions', 'matched']),
|
|
85
|
+
summary: obj({ session: sessionOut, items: list(itemOut), total: INT, next_cursor: STR }, ['session', 'items', 'total']),
|
|
86
|
+
network: obj({ requests: list(obj(requestSummaryProps, requestSummaryRequired)), matched: INT, next_cursor: STR }, ['requests', 'matched']),
|
|
87
|
+
request: obj({
|
|
88
|
+
...requestSummaryProps,
|
|
89
|
+
statusText: STR,
|
|
90
|
+
ttfbMs: NULL_NUM,
|
|
91
|
+
mime: STR,
|
|
92
|
+
initiator: STR,
|
|
93
|
+
reqHeaders: STR_MAP,
|
|
94
|
+
resHeaders: STR_MAP,
|
|
95
|
+
reqBodyNote: STR,
|
|
96
|
+
resBodyNote: STR,
|
|
97
|
+
reqBodyLength: INT,
|
|
98
|
+
resBodyLength: INT,
|
|
99
|
+
next_body_offset: INT,
|
|
100
|
+
recorded: STR,
|
|
101
|
+
protocol: STR,
|
|
102
|
+
remoteAddress: STR,
|
|
103
|
+
cache: STR,
|
|
104
|
+
worker: obj({ type: STR, url: STR }, ['type', 'url'], true),
|
|
105
|
+
timing,
|
|
106
|
+
failureReason: STR,
|
|
107
|
+
blockedCookies: list(blockedCookie),
|
|
108
|
+
blockedSetCookies: list(blockedCookie),
|
|
109
|
+
messages: list(frameOut),
|
|
110
|
+
messagesTotal: INT,
|
|
111
|
+
next_frames_offset: INT,
|
|
112
|
+
}, requestSummaryRequired),
|
|
113
|
+
console: obj({ messages: list(consoleItem), matched: INT, next_cursor: STR }, ['messages', 'matched']),
|
|
114
|
+
context: obj({ at: STR, offsetMs: INT, items: list(itemOut), stateActions: list(stateActionOut) }, ['at', 'offsetMs', 'items']),
|
|
115
|
+
repro: obj({ markdown: STR, steps: INT, replay: STR }, ['markdown', 'steps', 'replay']),
|
|
116
|
+
recordLink: obj({ start: STR, stop: STR }, ['start', 'stop']),
|
|
117
|
+
};
|
|
118
|
+
// ---------- the tools ----------
|
|
119
|
+
const SESSION_LATEST = 'session id, or latest';
|
|
120
|
+
const CURSOR = 'continue a cut result: pass next_cursor from the previous call';
|
|
121
|
+
const FROM = 'start of the time window: mm:ss(.mmm), a duration like 90s, or milliseconds';
|
|
122
|
+
const TO = 'end of the time window, same format as from';
|
|
123
|
+
function def(name, description, params, output, run) {
|
|
124
|
+
return { name, description, params, output, run: run };
|
|
125
|
+
}
|
|
126
|
+
const TOOLS = [
|
|
127
|
+
def('list_sessions', 'List tabtape recordings, newest first, with page, click, error and failed-request counts, the identified user and a replay link. Filter by text, user, time and errors.', [
|
|
128
|
+
{ name: 'query', type: 'string', description: 'only sessions whose title, URL, id or user id contains this text (case-insensitive)' },
|
|
129
|
+
{
|
|
130
|
+
name: 'user',
|
|
131
|
+
type: 'string',
|
|
132
|
+
description: 'only sessions of this user: the id the page passed to tabtape.identify() contains this text (case-insensitive)',
|
|
133
|
+
},
|
|
134
|
+
{ name: 'since', type: 'string', description: 'only sessions started within this long (e.g. 2h, 30m, 3d) or after this RFC 3339 time' },
|
|
135
|
+
{ name: 'has_errors', type: 'boolean', description: 'only sessions with console errors or failed requests' },
|
|
136
|
+
{ name: 'limit', type: 'integer', description: 'maximum number of sessions (default 20)' },
|
|
137
|
+
], OUTPUT.list, (t, input) => t.listSessions(input)),
|
|
138
|
+
def('get_session_summary', "What happened in a recording, in order: page views, clicks, form changes, submits, the app's own events (tabtape.track), console errors, failed requests, poor Web Vitals and long freezes, each with its time and replay link. Also the identified user (tabtape.identify).", [
|
|
139
|
+
{ name: 'session_id', type: 'string', description: 'session id, or latest for the most recent recording', required: true },
|
|
140
|
+
{ name: 'cursor', type: 'string', description: CURSOR },
|
|
141
|
+
], OUTPUT.summary, (t, input) => t.getSessionSummary(input)),
|
|
142
|
+
def('get_network', 'Network requests of a recording: time, method, URL, status, duration, size and request id. Filter by status, URL, method, kind (websocket for WebSocket connections) and time window.', [
|
|
143
|
+
{ name: 'session_id', type: 'string', description: SESSION_LATEST, required: true },
|
|
144
|
+
{ name: 'status', type: 'string', description: 'status filter: 500, 5xx, >=400, failed (>=400 or network error), error (network errors only)' },
|
|
145
|
+
{ name: 'url_contains', type: 'string', description: 'only URLs containing this text (case-insensitive)' },
|
|
146
|
+
{ name: 'method', type: 'string', description: 'HTTP method, e.g. POST' },
|
|
147
|
+
{
|
|
148
|
+
name: 'kind',
|
|
149
|
+
type: 'string',
|
|
150
|
+
description: 'fetch, xhr, beacon, api (fetch+xhr+beacon), document, resource (scripts, images, CSS), or websocket (deep mode recordings)',
|
|
151
|
+
},
|
|
152
|
+
{ name: 'from', type: 'string', description: FROM },
|
|
153
|
+
{ name: 'to', type: 'string', description: TO },
|
|
154
|
+
{
|
|
155
|
+
name: 'include_bodies',
|
|
156
|
+
type: 'boolean',
|
|
157
|
+
description: 'also return request and response bodies, cut to 2000 characters (use get_request for the full body)',
|
|
158
|
+
},
|
|
159
|
+
{ name: 'limit', type: 'integer', description: 'maximum number of requests (default 50)' },
|
|
160
|
+
{ name: 'cursor', type: 'string', description: CURSOR },
|
|
161
|
+
], OUTPUT.network, (t, input) => t.getNetwork(input)),
|
|
162
|
+
def('get_request', 'One request in full: headers (sensitive ones redacted), request and response bodies in pages of 20000 characters, and timing. Deep mode recordings add the timing split, cookies Chrome blocked and why a request failed (CORS, ...). For a WebSocket connection it returns its messages in order, 100 per call.', [
|
|
163
|
+
{ name: 'session_id', type: 'string', description: SESSION_LATEST, required: true },
|
|
164
|
+
{ name: 'request_id', type: 'string', description: 'request id from get_network or get_session_summary, e.g. n12-345', required: true },
|
|
165
|
+
{ name: 'body_offset', type: 'integer', description: 'character offset into the bodies, for bodies longer than 20000 characters' },
|
|
166
|
+
{ name: 'frames_offset', type: 'integer', description: 'WebSocket connections: index of the first message to return (100 per call)' },
|
|
167
|
+
], OUTPUT.request, (t, input) => t.getRequest(input)),
|
|
168
|
+
def('get_console', 'Console output of a recording: level, source, message and a short stack. Uncaught errors and unhandled rejections are level error.', [
|
|
169
|
+
{ name: 'session_id', type: 'string', description: SESSION_LATEST, required: true },
|
|
170
|
+
{ name: 'level', type: 'string', description: 'error, warn, info, log, debug or trace; warn+ means warnings and errors' },
|
|
171
|
+
{ name: 'text_contains', type: 'string', description: 'only messages containing this text (case-insensitive)' },
|
|
172
|
+
{ name: 'from', type: 'string', description: FROM },
|
|
173
|
+
{ name: 'to', type: 'string', description: TO },
|
|
174
|
+
{ name: 'limit', type: 'integer', description: 'maximum number of messages (default 100)' },
|
|
175
|
+
{ name: 'cursor', type: 'string', description: CURSOR },
|
|
176
|
+
], OUTPUT.console, (t, input) => t.getConsole(input)),
|
|
177
|
+
def('get_context_at', 'Everything around one moment, in order: user actions, requests and console messages from 10 seconds before to 2 seconds after (adjustable), then the last 10 Redux / Zustand store actions before the moment with the state paths they changed. Use it to see what led to an error.', [
|
|
178
|
+
{ name: 'session_id', type: 'string', description: SESSION_LATEST, required: true },
|
|
179
|
+
{
|
|
180
|
+
name: 'at',
|
|
181
|
+
type: 'string',
|
|
182
|
+
description: 'the moment: mm:ss(.mmm) like 01:23.4, or the id of a request, console message or event',
|
|
183
|
+
required: true,
|
|
184
|
+
},
|
|
185
|
+
{ name: 'before', type: 'string', description: 'how far to look back: seconds or a duration like 1500ms (default 10s)' },
|
|
186
|
+
{ name: 'after', type: 'string', description: 'how far to look ahead (default 2s)' },
|
|
187
|
+
], OUTPUT.context, (t, input) => t.getContextAt(input)),
|
|
188
|
+
def('get_repro_steps', 'Markdown reproduction steps for a bug report: environment, numbered user actions with failed requests and console errors under the step that caused them, and a replay link.', [
|
|
189
|
+
{ name: 'session_id', type: 'string', description: SESSION_LATEST, required: true },
|
|
190
|
+
{ name: 'from', type: 'string', description: 'start of the range: mm:ss(.mmm), a duration like 90s, or milliseconds' },
|
|
191
|
+
{ name: 'to', type: 'string', description: 'end of the range, same format as from' },
|
|
192
|
+
{ name: 'lang', type: 'string', description: 'en (default) or tr' },
|
|
193
|
+
], OUTPUT.repro, (t, input) => t.getReproSteps(input)),
|
|
194
|
+
def('get_record_link', 'Links that start and stop a tabtape recording in the browser that opens them, e.g. one driven by Playwright MCP: open the start link before checking a page and the stop link after it. The stop page names the session to look at with the other tools. The browser needs the tabtape extension.', [
|
|
195
|
+
{
|
|
196
|
+
name: 'name',
|
|
197
|
+
type: 'string',
|
|
198
|
+
description: "what is about to be recorded, e.g. the check you are going to run; it becomes the recording's title",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'url',
|
|
202
|
+
type: 'string',
|
|
203
|
+
description: 'page the tab opens once the recording runs (http, https or file URL); recorded from its first request',
|
|
204
|
+
},
|
|
205
|
+
], OUTPUT.recordLink, (t, input) => t.getRecordLink(input)),
|
|
206
|
+
].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); // the Go SDK lists tools by name
|
|
207
|
+
const BY_NAME = new Map(TOOLS.map((t) => [t.name, t]));
|
|
208
|
+
function inputSchema(params) {
|
|
209
|
+
const properties = {};
|
|
210
|
+
for (const p of params)
|
|
211
|
+
properties[p.name] = { type: p.type, description: p.description };
|
|
212
|
+
const required = params.filter((p) => p.required).map((p) => p.name);
|
|
213
|
+
return { type: 'object', properties, ...(required.length > 0 ? { required } : {}), additionalProperties: false };
|
|
214
|
+
}
|
|
215
|
+
/** The tools as tools/list returns them. */
|
|
216
|
+
exports.TOOL_LIST = TOOLS.map((t) => ({
|
|
217
|
+
name: t.name,
|
|
218
|
+
description: t.description,
|
|
219
|
+
inputSchema: inputSchema(t.params),
|
|
220
|
+
outputSchema: t.output,
|
|
221
|
+
// The Go SDK always sends idempotentHint and readOnlyHint.
|
|
222
|
+
annotations: { idempotentHint: false, openWorldHint: false, readOnlyHint: true },
|
|
223
|
+
}));
|
|
224
|
+
// ---------- arguments ----------
|
|
225
|
+
/** Arguments that do not match a tool's input schema. */
|
|
226
|
+
class ArgumentsError extends Error {
|
|
227
|
+
constructor(detail) {
|
|
228
|
+
super(`validating "arguments": ${detail}`);
|
|
229
|
+
this.name = 'ArgumentsError';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
exports.ArgumentsError = ArgumentsError;
|
|
233
|
+
/** jsonschema-go's JSON type of a decoded value; whole numbers are integers. */
|
|
234
|
+
function jsonType(v) {
|
|
235
|
+
if (v === null)
|
|
236
|
+
return 'null';
|
|
237
|
+
if (Array.isArray(v))
|
|
238
|
+
return 'array';
|
|
239
|
+
if (typeof v === 'number')
|
|
240
|
+
return Number.isInteger(v) ? 'integer' : 'number';
|
|
241
|
+
return typeof v;
|
|
242
|
+
}
|
|
243
|
+
/** fmt's %v of a float64: shortest digits, exponent form below 1e-4 and from 1e6. */
|
|
244
|
+
function goFloat(n) {
|
|
245
|
+
if (Number.isNaN(n))
|
|
246
|
+
return 'NaN';
|
|
247
|
+
if (!Number.isFinite(n))
|
|
248
|
+
return n > 0 ? '+Inf' : '-Inf';
|
|
249
|
+
if (n === 0)
|
|
250
|
+
return Object.is(n, -0) ? '-0' : '0';
|
|
251
|
+
const [mant, e] = n.toExponential().split('e');
|
|
252
|
+
const exp = Number(e);
|
|
253
|
+
if (exp < -4 || exp >= 6)
|
|
254
|
+
return `${mant}e${exp < 0 ? '-' : '+'}${String(Math.abs(exp)).padStart(2, '0')}`;
|
|
255
|
+
return String(n);
|
|
256
|
+
}
|
|
257
|
+
/** fmt's %v of a value decoded by encoding/json (a JSON null is an invalid reflect.Value there). */
|
|
258
|
+
function goValue(v) {
|
|
259
|
+
if (v === null || v === undefined)
|
|
260
|
+
return '<invalid reflect.Value>';
|
|
261
|
+
if (typeof v === 'number')
|
|
262
|
+
return goFloat(v);
|
|
263
|
+
if (Array.isArray(v))
|
|
264
|
+
return '[' + v.map(goValue).join(' ') + ']';
|
|
265
|
+
if ((0, types_1.isObj)(v)) {
|
|
266
|
+
return ('map[' +
|
|
267
|
+
Object.keys(v)
|
|
268
|
+
.sort()
|
|
269
|
+
.map((k) => k + ':' + goValue(v[k]))
|
|
270
|
+
.join(' ') +
|
|
271
|
+
']');
|
|
272
|
+
}
|
|
273
|
+
return String(v);
|
|
274
|
+
}
|
|
275
|
+
/** fmt's %q of a []string. */
|
|
276
|
+
const goQuoteList = (list) => '[' + list.map(gofmt_1.goQuote).join(' ') + ']';
|
|
277
|
+
const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
278
|
+
function fits(v, type) {
|
|
279
|
+
switch (type) {
|
|
280
|
+
case 'string':
|
|
281
|
+
return typeof v === 'string';
|
|
282
|
+
case 'integer':
|
|
283
|
+
return typeof v === 'number' && Number.isInteger(v);
|
|
284
|
+
case 'boolean':
|
|
285
|
+
return typeof v === 'boolean';
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Validates arguments against a tool's parameters like the Go SDK does
|
|
290
|
+
* (property types, then unknown properties, then required ones) and fills in
|
|
291
|
+
* Go's zero values for the missing ones. Strings get Go's JSON decoding of
|
|
292
|
+
* lone surrogates (U+FFFD).
|
|
293
|
+
*/
|
|
294
|
+
function decodeArguments(params, raw) {
|
|
295
|
+
const args = raw === undefined || raw === null ? {} : raw;
|
|
296
|
+
if (!(0, types_1.isObj)(args)) {
|
|
297
|
+
throw new ArgumentsError(`validating root: type: ${goValue(args)} has type ${(0, gofmt_1.goQuote)(jsonType(args))}, want "object"`);
|
|
298
|
+
}
|
|
299
|
+
for (const p of params) {
|
|
300
|
+
if (!has(args, p.name))
|
|
301
|
+
continue;
|
|
302
|
+
const v = args[p.name];
|
|
303
|
+
if (!fits(v, p.type)) {
|
|
304
|
+
throw new ArgumentsError(`validating root: validating /properties/${p.name}: type: ${goValue(v)} has type ${(0, gofmt_1.goQuote)(jsonType(v))}, want ${(0, gofmt_1.goQuote)(p.type)}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const known = new Set(params.map((p) => p.name));
|
|
308
|
+
const extra = Object.keys(args).filter((k) => !known.has(k));
|
|
309
|
+
if (extra.length > 0)
|
|
310
|
+
throw new ArgumentsError(`validating root: unexpected additional properties ${goQuoteList(extra.sort())}`);
|
|
311
|
+
const missing = params.filter((p) => p.required && !has(args, p.name)).map((p) => p.name);
|
|
312
|
+
if (missing.length > 0)
|
|
313
|
+
throw new ArgumentsError(`validating root: required: missing properties: ${goQuoteList(missing)}`);
|
|
314
|
+
const out = {};
|
|
315
|
+
for (const p of params) {
|
|
316
|
+
const v = args[p.name];
|
|
317
|
+
if (v === undefined)
|
|
318
|
+
out[p.name] = p.type === 'string' ? '' : p.type === 'integer' ? 0 : false;
|
|
319
|
+
else
|
|
320
|
+
out[p.name] = typeof v === 'string' ? (0, gofmt_1.wellFormed)(v) : v;
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
/** Whether a tool exists. */
|
|
325
|
+
function hasTool(name) {
|
|
326
|
+
return BY_NAME.has(name);
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* A copy of a JSON value with object keys sorted and undefined fields left
|
|
330
|
+
* out. The Go SDK validated structured output as a map[string]any and sent
|
|
331
|
+
* that re-encoded, which sorts keys at every level.
|
|
332
|
+
*/
|
|
333
|
+
function sortKeys(v) {
|
|
334
|
+
if (Array.isArray(v))
|
|
335
|
+
return v.map(sortKeys);
|
|
336
|
+
if (!(0, types_1.isObj)(v))
|
|
337
|
+
return v;
|
|
338
|
+
const out = {};
|
|
339
|
+
for (const k of Object.keys(v).sort()) {
|
|
340
|
+
if (v[k] !== undefined)
|
|
341
|
+
out[k] = sortKeys(v[k]);
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Runs a tool. Invalid arguments and tool failures come back as error
|
|
347
|
+
* results (isError), which the model sees and can correct; an unknown tool
|
|
348
|
+
* name throws.
|
|
349
|
+
*/
|
|
350
|
+
async function callTool(tools, name, args) {
|
|
351
|
+
const def = BY_NAME.get(name);
|
|
352
|
+
if (!def)
|
|
353
|
+
throw new Error(`unknown tool ${(0, gofmt_1.goQuote)(name)}`);
|
|
354
|
+
try {
|
|
355
|
+
const input = decodeArguments(def.params, args);
|
|
356
|
+
const { text, out } = await def.run(tools, input);
|
|
357
|
+
return { content: [{ type: 'text', text }], structuredContent: sortKeys(out) };
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
return { content: [{ type: 'text', text: (0, errors_1.errorMessage)(err) }], isError: true };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Text helpers that reproduce what the Go server printed: strconv.Quote
|
|
4
|
+
* (fmt's %q), %-Ns padding, UTF-8 byte lengths (Go strings are bytes), Go's
|
|
5
|
+
* strings.TrimSpace and strings.ToLower, and time formatting in a zone.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.TextBuf = void 0;
|
|
9
|
+
exports.goQuote = goQuote;
|
|
10
|
+
exports.runeCount = runeCount;
|
|
11
|
+
exports.padRight = padRight;
|
|
12
|
+
exports.byteLen = byteLen;
|
|
13
|
+
exports.goTrimSpace = goTrimSpace;
|
|
14
|
+
exports.goToLower = goToLower;
|
|
15
|
+
exports.wellFormed = wellFormed;
|
|
16
|
+
exports.formatRFC3339 = formatRFC3339;
|
|
17
|
+
exports.formatStamp = formatStamp;
|
|
18
|
+
exports.rfc3339DateTime = rfc3339DateTime;
|
|
19
|
+
const PRINTABLE = /^[\p{L}\p{M}\p{N}\p{P}\p{S}]$/u;
|
|
20
|
+
const HEX = '0123456789abcdef';
|
|
21
|
+
function hex(cp, digits) {
|
|
22
|
+
let s = '';
|
|
23
|
+
for (let shift = (digits - 1) * 4; shift >= 0; shift -= 4)
|
|
24
|
+
s += HEX[(cp >> shift) & 0xf];
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
/** Go's unicode.IsPrint: letters, marks, numbers, punctuation, symbols and the ASCII space. */
|
|
28
|
+
function isPrint(cp) {
|
|
29
|
+
return cp === 0x20 || PRINTABLE.test(String.fromCodePoint(cp));
|
|
30
|
+
}
|
|
31
|
+
/** Go's strconv.Quote, which fmt's %q uses for strings. */
|
|
32
|
+
function goQuote(s) {
|
|
33
|
+
let out = '"';
|
|
34
|
+
for (const ch of s) {
|
|
35
|
+
let cp = ch.codePointAt(0);
|
|
36
|
+
if (cp >= 0xd800 && cp <= 0xdfff)
|
|
37
|
+
cp = 0xfffd; // a lone surrogate: Go decoded it as U+FFFD
|
|
38
|
+
if (cp === 0x22 || cp === 0x5c) {
|
|
39
|
+
out += '\\' + String.fromCodePoint(cp);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (isPrint(cp)) {
|
|
43
|
+
out += String.fromCodePoint(cp);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
switch (cp) {
|
|
47
|
+
case 0x07:
|
|
48
|
+
out += '\\a';
|
|
49
|
+
continue;
|
|
50
|
+
case 0x08:
|
|
51
|
+
out += '\\b';
|
|
52
|
+
continue;
|
|
53
|
+
case 0x0c:
|
|
54
|
+
out += '\\f';
|
|
55
|
+
continue;
|
|
56
|
+
case 0x0a:
|
|
57
|
+
out += '\\n';
|
|
58
|
+
continue;
|
|
59
|
+
case 0x0d:
|
|
60
|
+
out += '\\r';
|
|
61
|
+
continue;
|
|
62
|
+
case 0x09:
|
|
63
|
+
out += '\\t';
|
|
64
|
+
continue;
|
|
65
|
+
case 0x0b:
|
|
66
|
+
out += '\\v';
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (cp < 0x20 || cp === 0x7f)
|
|
70
|
+
out += '\\x' + hex(cp, 2);
|
|
71
|
+
else if (cp < 0x10000)
|
|
72
|
+
out += '\\u' + hex(cp, 4);
|
|
73
|
+
else
|
|
74
|
+
out += '\\U' + hex(cp, 8);
|
|
75
|
+
}
|
|
76
|
+
return out + '"';
|
|
77
|
+
}
|
|
78
|
+
/** Number of code points (Go's runes). */
|
|
79
|
+
function runeCount(s) {
|
|
80
|
+
let n = 0;
|
|
81
|
+
for (let i = 0; i < s.length; i++) {
|
|
82
|
+
const c = s.charCodeAt(i);
|
|
83
|
+
if (c >= 0xd800 && c <= 0xdbff) {
|
|
84
|
+
const d = s.charCodeAt(i + 1);
|
|
85
|
+
if (d >= 0xdc00 && d <= 0xdfff)
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
n++;
|
|
89
|
+
}
|
|
90
|
+
return n;
|
|
91
|
+
}
|
|
92
|
+
/** fmt's %-Ns: pads s with spaces to width runes. */
|
|
93
|
+
function padRight(s, width) {
|
|
94
|
+
const n = runeCount(s);
|
|
95
|
+
return n < width ? s + ' '.repeat(width - n) : s;
|
|
96
|
+
}
|
|
97
|
+
/** UTF-8 length, which is what len() of a Go string counts. */
|
|
98
|
+
function byteLen(s) {
|
|
99
|
+
return Buffer.byteLength(s, 'utf8');
|
|
100
|
+
}
|
|
101
|
+
/** A strings.Builder that knows its length in UTF-8 bytes. */
|
|
102
|
+
class TextBuf {
|
|
103
|
+
constructor() {
|
|
104
|
+
this.parts = [];
|
|
105
|
+
/** Bytes written so far. */
|
|
106
|
+
this.len = 0;
|
|
107
|
+
}
|
|
108
|
+
write(s) {
|
|
109
|
+
this.parts.push(s);
|
|
110
|
+
this.len += byteLen(s);
|
|
111
|
+
}
|
|
112
|
+
toString() {
|
|
113
|
+
return this.parts.join('');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.TextBuf = TextBuf;
|
|
117
|
+
/** Go's unicode.IsSpace. */
|
|
118
|
+
function isGoSpace(c) {
|
|
119
|
+
return ((c >= 0x09 && c <= 0x0d) ||
|
|
120
|
+
c === 0x20 ||
|
|
121
|
+
c === 0x85 ||
|
|
122
|
+
c === 0xa0 ||
|
|
123
|
+
c === 0x1680 ||
|
|
124
|
+
(c >= 0x2000 && c <= 0x200a) ||
|
|
125
|
+
c === 0x2028 ||
|
|
126
|
+
c === 0x2029 ||
|
|
127
|
+
c === 0x202f ||
|
|
128
|
+
c === 0x205f ||
|
|
129
|
+
c === 0x3000);
|
|
130
|
+
}
|
|
131
|
+
/** Go's strings.TrimSpace: Unicode white space at both ends. */
|
|
132
|
+
function goTrimSpace(s) {
|
|
133
|
+
let i = 0;
|
|
134
|
+
let j = s.length;
|
|
135
|
+
while (i < j && isGoSpace(s.charCodeAt(i)))
|
|
136
|
+
i++;
|
|
137
|
+
while (j > i && isGoSpace(s.charCodeAt(j - 1)))
|
|
138
|
+
j--;
|
|
139
|
+
return s.slice(i, j);
|
|
140
|
+
}
|
|
141
|
+
const CAPITAL_I_WITH_DOT = String.fromCharCode(0x130);
|
|
142
|
+
const CAPITAL_SIGMA = String.fromCharCode(0x3a3);
|
|
143
|
+
const SMALL_SIGMA = String.fromCharCode(0x3c3);
|
|
144
|
+
/**
|
|
145
|
+
* Go's strings.ToLower, which maps rune by rune. JavaScript's toLowerCase
|
|
146
|
+
* differs in two places: it turns "İ" into "i" plus a combining dot and a
|
|
147
|
+
* word-final "Σ" into "ς".
|
|
148
|
+
*/
|
|
149
|
+
function goToLower(s) {
|
|
150
|
+
let ascii = true;
|
|
151
|
+
for (let i = 0; i < s.length && ascii; i++)
|
|
152
|
+
ascii = s.charCodeAt(i) < 0x80;
|
|
153
|
+
if (ascii)
|
|
154
|
+
return s.toLowerCase();
|
|
155
|
+
return s.split(CAPITAL_I_WITH_DOT).join('i').split(CAPITAL_SIGMA).join(SMALL_SIGMA).toLowerCase();
|
|
156
|
+
}
|
|
157
|
+
/** Replaces lone surrogates with U+FFFD, as Go's JSON decoder does. */
|
|
158
|
+
function wellFormed(s) {
|
|
159
|
+
let out = '';
|
|
160
|
+
let last = 0;
|
|
161
|
+
for (let i = 0; i < s.length; i++) {
|
|
162
|
+
const c = s.charCodeAt(i);
|
|
163
|
+
if (c < 0xd800 || c > 0xdfff)
|
|
164
|
+
continue;
|
|
165
|
+
if (c <= 0xdbff) {
|
|
166
|
+
const d = s.charCodeAt(i + 1);
|
|
167
|
+
if (d >= 0xdc00 && d <= 0xdfff) {
|
|
168
|
+
i++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
out += s.slice(last, i) + String.fromCharCode(0xfffd);
|
|
173
|
+
last = i + 1;
|
|
174
|
+
}
|
|
175
|
+
return last === 0 ? s : out + s.slice(last);
|
|
176
|
+
}
|
|
177
|
+
const formatters = new Map();
|
|
178
|
+
function formatter(timeZone) {
|
|
179
|
+
const key = timeZone ?? '';
|
|
180
|
+
let f = formatters.get(key);
|
|
181
|
+
if (!f) {
|
|
182
|
+
f = new Intl.DateTimeFormat('en-US', {
|
|
183
|
+
timeZone,
|
|
184
|
+
year: 'numeric',
|
|
185
|
+
month: '2-digit',
|
|
186
|
+
day: '2-digit',
|
|
187
|
+
hour: '2-digit',
|
|
188
|
+
minute: '2-digit',
|
|
189
|
+
second: '2-digit',
|
|
190
|
+
hourCycle: 'h23',
|
|
191
|
+
});
|
|
192
|
+
formatters.set(key, f);
|
|
193
|
+
}
|
|
194
|
+
return f;
|
|
195
|
+
}
|
|
196
|
+
/** The date and time of epoch ms in a time zone (undefined: the machine's). */
|
|
197
|
+
function wallClock(ms, timeZone) {
|
|
198
|
+
const parts = formatter(timeZone).formatToParts(new Date(ms));
|
|
199
|
+
const get = (type) => Number(parts.find((p) => p.type === type)?.value ?? '0');
|
|
200
|
+
const w = { year: get('year'), month: get('month'), day: get('day'), hour: get('hour'), minute: get('minute'), second: get('second'), offset: 0 };
|
|
201
|
+
const asUTC = new Date(0);
|
|
202
|
+
asUTC.setUTCFullYear(w.year, w.month - 1, w.day);
|
|
203
|
+
asUTC.setUTCHours(w.hour, w.minute, w.second, 0);
|
|
204
|
+
w.offset = Math.round((asUTC.getTime() - Math.floor(ms / 1000) * 1000) / 1000);
|
|
205
|
+
return w;
|
|
206
|
+
}
|
|
207
|
+
const pad = (n, width) => String(n).padStart(width, '0');
|
|
208
|
+
/** Go's time.RFC3339 layout, e.g. "2025-10-05T07:47:12Z" or "2025-10-05T09:47:12+02:00". */
|
|
209
|
+
function formatRFC3339(ms, timeZone) {
|
|
210
|
+
const w = wallClock(ms, timeZone);
|
|
211
|
+
let zone = 'Z';
|
|
212
|
+
if (w.offset !== 0) {
|
|
213
|
+
const minutes = Math.trunc(Math.abs(w.offset) / 60);
|
|
214
|
+
zone = (w.offset < 0 ? '-' : '+') + pad(Math.floor(minutes / 60), 2) + ':' + pad(minutes % 60, 2);
|
|
215
|
+
}
|
|
216
|
+
return `${pad(w.year, 4)}-${pad(w.month, 2)}-${pad(w.day, 2)}T${pad(w.hour, 2)}:${pad(w.minute, 2)}:${pad(w.second, 2)}${zone}`;
|
|
217
|
+
}
|
|
218
|
+
const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
219
|
+
/** Go's "Jan 2 15:04" layout, e.g. "Oct 5 07:47". */
|
|
220
|
+
function formatStamp(ms, timeZone) {
|
|
221
|
+
const w = wallClock(ms, timeZone);
|
|
222
|
+
return `${MONTHS[w.month - 1]} ${w.day} ${pad(w.hour, 2)}:${pad(w.minute, 2)}`;
|
|
223
|
+
}
|
|
224
|
+
/** An RFC 3339 time of formatRFC3339 in Go's "2006-01-02 15:04:05" layout, in its own zone. */
|
|
225
|
+
function rfc3339DateTime(t) {
|
|
226
|
+
return t.slice(0, 10) + ' ' + t.slice(11, 19);
|
|
227
|
+
}
|