@sidecar-ai/cli 0.1.0-alpha.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.
- package/dist/index.d.ts +19 -0
- package/dist/index.js +4922 -0
- package/dist/index.js.map +1 -0
- package/package.json +28 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4922 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/cli/src/index.ts
|
|
4
|
+
import { existsSync as existsSync5, realpathSync } from "fs";
|
|
5
|
+
import { mkdir as mkdir9, readFile as readFile8, writeFile as writeFile9 } from "fs/promises";
|
|
6
|
+
import { createServer as createServer2 } from "http";
|
|
7
|
+
import path18 from "path";
|
|
8
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
10
|
+
import { cwd, exit, stdin as stdin2, stdout as stdout2 } from "process";
|
|
11
|
+
import { tsImport } from "tsx/esm/api";
|
|
12
|
+
|
|
13
|
+
// packages/auth/src/core.ts
|
|
14
|
+
var authBrand = /* @__PURE__ */ Symbol.for("sidecar.auth");
|
|
15
|
+
function isSidecarAuth(value) {
|
|
16
|
+
return Boolean(
|
|
17
|
+
value && typeof value === "object" && value[authBrand]
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// packages/core/src/index.ts
|
|
22
|
+
var MCP_APP_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
23
|
+
var toolBrand = /* @__PURE__ */ Symbol.for("sidecar.tool");
|
|
24
|
+
var toolResultBrand = /* @__PURE__ */ Symbol.for("sidecar.toolResult");
|
|
25
|
+
var resourceBrand = /* @__PURE__ */ Symbol.for("sidecar.resource");
|
|
26
|
+
var resourceResultBrand = /* @__PURE__ */ Symbol.for("sidecar.resourceResult");
|
|
27
|
+
var promptBrand = /* @__PURE__ */ Symbol.for("sidecar.prompt");
|
|
28
|
+
function isSidecarTool(value) {
|
|
29
|
+
return Boolean(value && typeof value === "object" && value[toolBrand]);
|
|
30
|
+
}
|
|
31
|
+
function isSidecarResource(value) {
|
|
32
|
+
return Boolean(value && typeof value === "object" && value[resourceBrand]);
|
|
33
|
+
}
|
|
34
|
+
function isSidecarPrompt(value) {
|
|
35
|
+
return Boolean(value && typeof value === "object" && value[promptBrand]);
|
|
36
|
+
}
|
|
37
|
+
var toolResult = Object.assign(
|
|
38
|
+
createToolResult,
|
|
39
|
+
{
|
|
40
|
+
text(text) {
|
|
41
|
+
return { type: "text", text };
|
|
42
|
+
},
|
|
43
|
+
error(message, options = {}) {
|
|
44
|
+
return createToolResult({
|
|
45
|
+
...options,
|
|
46
|
+
structuredContent: options.structuredContent,
|
|
47
|
+
content: message,
|
|
48
|
+
isError: true
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
var resourceResult = Object.assign(
|
|
54
|
+
createResourceResult,
|
|
55
|
+
{
|
|
56
|
+
many(input) {
|
|
57
|
+
return createResourceResult(input);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
function createToolResult(input) {
|
|
62
|
+
const resultEnvelope = stripUndefined({
|
|
63
|
+
structuredContent: input.structuredContent,
|
|
64
|
+
content: normalizeRequiredContent(input.content),
|
|
65
|
+
_meta: input.meta,
|
|
66
|
+
isError: input.isError
|
|
67
|
+
});
|
|
68
|
+
Object.defineProperty(resultEnvelope, toolResultBrand, {
|
|
69
|
+
enumerable: false,
|
|
70
|
+
value: true
|
|
71
|
+
});
|
|
72
|
+
return resultEnvelope;
|
|
73
|
+
}
|
|
74
|
+
function createResourceResult(input) {
|
|
75
|
+
const contents = Array.isArray(input) ? [...input] : [input];
|
|
76
|
+
if (!contents.length) {
|
|
77
|
+
throw new SidecarRuntimeError(
|
|
78
|
+
"resourceResult(...) must include at least one content item.",
|
|
79
|
+
"invalid_resource_result"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const resultEnvelope = {
|
|
83
|
+
contents
|
|
84
|
+
};
|
|
85
|
+
Object.defineProperty(resultEnvelope, resourceResultBrand, {
|
|
86
|
+
enumerable: false,
|
|
87
|
+
value: true
|
|
88
|
+
});
|
|
89
|
+
return resultEnvelope;
|
|
90
|
+
}
|
|
91
|
+
function isToolResult(value) {
|
|
92
|
+
return Boolean(
|
|
93
|
+
value && typeof value === "object" && value[toolResultBrand] === true
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function isResourceResult(value) {
|
|
97
|
+
return Boolean(
|
|
98
|
+
value && typeof value === "object" && value[resourceResultBrand] === true
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
function normalizeToolResult(value) {
|
|
102
|
+
if (!isToolResult(value)) {
|
|
103
|
+
throw new SidecarRuntimeError(
|
|
104
|
+
"Tool execute() must return toolResult({ structuredContent, content, meta }).",
|
|
105
|
+
"invalid_tool_result"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return stripUndefined({
|
|
109
|
+
structuredContent: value.structuredContent,
|
|
110
|
+
content: value.content ?? [],
|
|
111
|
+
_meta: value._meta,
|
|
112
|
+
isError: value.isError
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function normalizeResourceResult(value, uri, defaultMimeType) {
|
|
116
|
+
if (!isResourceResult(value)) {
|
|
117
|
+
throw new SidecarRuntimeError(
|
|
118
|
+
"Resource read() must return resourceResult(...).",
|
|
119
|
+
"invalid_resource_result"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
contents: value.contents.map(
|
|
124
|
+
(content) => normalizeResourceContent(content, uri, defaultMimeType)
|
|
125
|
+
)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function executeTool(sidecarTool, params, ctx) {
|
|
129
|
+
const parsedParams = validateParams(sidecarTool, params);
|
|
130
|
+
const value = await sidecarTool.execute(parsedParams, ctx);
|
|
131
|
+
return normalizeToolResult(value);
|
|
132
|
+
}
|
|
133
|
+
async function executeResource(sidecarResource, ctx, options) {
|
|
134
|
+
const value = await sidecarResource.read(ctx);
|
|
135
|
+
return normalizeResourceResult(value, options.uri, sidecarResource.mimeType ?? options.mimeType);
|
|
136
|
+
}
|
|
137
|
+
async function executePrompt(sidecarPrompt, args, ctx) {
|
|
138
|
+
const parsedArgs = validatePromptArgs(sidecarPrompt, args);
|
|
139
|
+
const value = await sidecarPrompt.run(parsedArgs, ctx);
|
|
140
|
+
return normalizePromptResult(value, sidecarPrompt.description);
|
|
141
|
+
}
|
|
142
|
+
function createToolDescriptor(definition) {
|
|
143
|
+
const machineName = definition.id ?? toMachineName(definition.name);
|
|
144
|
+
validateToolId(machineName);
|
|
145
|
+
const target = definition.target ?? "mcp";
|
|
146
|
+
return stripUndefined({
|
|
147
|
+
name: machineName,
|
|
148
|
+
title: definition.name,
|
|
149
|
+
description: definition.description,
|
|
150
|
+
inputSchema: definition.inputSchema ?? emptyObjectSchema(),
|
|
151
|
+
outputSchema: definition.outputSchema,
|
|
152
|
+
securitySchemes: securitySchemes(definition.auth),
|
|
153
|
+
annotations: withAnnotationDefaults(definition.annotations),
|
|
154
|
+
_meta: mergeMeta(
|
|
155
|
+
securitySchemesMeta(definition.auth),
|
|
156
|
+
visibilityMeta(definition.visibility),
|
|
157
|
+
target === "chatgpt" ? chatGptToolMeta(definition.hosts?.chatgpt) : void 0,
|
|
158
|
+
definition.meta
|
|
159
|
+
)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function createResourceDescriptor(definition) {
|
|
163
|
+
validateResourceUri(definition.uri);
|
|
164
|
+
return stripUndefined({
|
|
165
|
+
uri: definition.uri,
|
|
166
|
+
name: definition.name,
|
|
167
|
+
title: definition.title,
|
|
168
|
+
description: definition.description,
|
|
169
|
+
mimeType: definition.mimeType,
|
|
170
|
+
size: definition.size,
|
|
171
|
+
icons: definition.icons ? [...definition.icons] : void 0,
|
|
172
|
+
annotations: normalizeAnnotations(definition.annotations),
|
|
173
|
+
_meta: definition.meta
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function createPromptDescriptor(definition) {
|
|
177
|
+
validatePromptName(definition.name);
|
|
178
|
+
return stripUndefined({
|
|
179
|
+
name: definition.name,
|
|
180
|
+
title: definition.title,
|
|
181
|
+
description: definition.description,
|
|
182
|
+
arguments: promptArguments(definition.args),
|
|
183
|
+
icons: definition.icons ? [...definition.icons] : void 0
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function securitySchemes(policy) {
|
|
187
|
+
if (!policy || policy.public === true) {
|
|
188
|
+
return [{ type: "noauth" }];
|
|
189
|
+
}
|
|
190
|
+
if ("scopes" in policy && policy.scopes?.length) {
|
|
191
|
+
return [{ type: "oauth2", scopes: policy.scopes.map((entry) => entry.id) }];
|
|
192
|
+
}
|
|
193
|
+
return [{ type: "oauth2", scopes: [] }];
|
|
194
|
+
}
|
|
195
|
+
function securitySchemesMeta(policy) {
|
|
196
|
+
return {
|
|
197
|
+
securitySchemes: securitySchemes(policy)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function visibilityMeta(visibility) {
|
|
201
|
+
if (!visibility) {
|
|
202
|
+
return void 0;
|
|
203
|
+
}
|
|
204
|
+
const uiVisibility = [
|
|
205
|
+
visibility.model !== false ? "model" : void 0,
|
|
206
|
+
visibility.widgets !== false ? "app" : void 0
|
|
207
|
+
].filter(Boolean);
|
|
208
|
+
return {
|
|
209
|
+
ui: {
|
|
210
|
+
visibility: uiVisibility
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function chatGptToolMeta(options) {
|
|
215
|
+
if (!options) {
|
|
216
|
+
return void 0;
|
|
217
|
+
}
|
|
218
|
+
return stripUndefined({
|
|
219
|
+
"openai/widgetAccessible": options.widgetAccessible,
|
|
220
|
+
"openai/visibility": options.visibility,
|
|
221
|
+
"openai/toolInvocation/invoking": options.invoking,
|
|
222
|
+
"openai/toolInvocation/invoked": options.invoked,
|
|
223
|
+
"openai/fileParams": options.fileParams ? [...options.fileParams] : void 0
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function mergeMeta(...entries) {
|
|
227
|
+
const merged = {};
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
if (!entry) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
233
|
+
if (key === "ui" && isRecord(value) && isRecord(merged.ui)) {
|
|
234
|
+
merged.ui = { ...merged.ui, ...value };
|
|
235
|
+
} else {
|
|
236
|
+
merged[key] = value;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return Object.keys(merged).length ? stripUndefined(merged) : void 0;
|
|
241
|
+
}
|
|
242
|
+
function isRecord(value) {
|
|
243
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
244
|
+
}
|
|
245
|
+
function emptyObjectSchema() {
|
|
246
|
+
return {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {},
|
|
249
|
+
required: [],
|
|
250
|
+
additionalProperties: false
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function toMachineName(name) {
|
|
254
|
+
return name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "").replace(/_{2,}/g, "_").toLowerCase();
|
|
255
|
+
}
|
|
256
|
+
function validateToolId(id) {
|
|
257
|
+
if (!/^[A-Za-z0-9._-]{1,128}$/.test(id) || id === "." || id === ".." || id.includes("/..") || id.includes("../")) {
|
|
258
|
+
throw new SidecarDefinitionError(
|
|
259
|
+
`Invalid tool id "${id}". Tool ids must be 1-128 ASCII letters, digits, dots, hyphens, or underscores.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function validateResourceUri(uri) {
|
|
264
|
+
try {
|
|
265
|
+
const parsed = new URL(uri);
|
|
266
|
+
if (!parsed.protocol || parsed.hash) {
|
|
267
|
+
throw new Error("invalid uri");
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
throw new SidecarDefinitionError(
|
|
271
|
+
`Invalid resource uri "${uri}". Resource URIs must be absolute and must not include fragments.`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function validatePromptName(name) {
|
|
276
|
+
if (!/^[A-Za-z0-9._-]{1,128}$/.test(name) || name === "." || name === "..") {
|
|
277
|
+
throw new SidecarDefinitionError(
|
|
278
|
+
`Invalid prompt name "${name}". Prompt names must be 1-128 ASCII letters, digits, dots, hyphens, or underscores.`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function withAnnotationDefaults(annotations) {
|
|
283
|
+
const readOnlyHint = annotations?.readOnlyHint ?? false;
|
|
284
|
+
return {
|
|
285
|
+
title: annotations?.title,
|
|
286
|
+
readOnlyHint,
|
|
287
|
+
destructiveHint: annotations?.destructiveHint ?? !readOnlyHint,
|
|
288
|
+
idempotentHint: annotations?.idempotentHint ?? false,
|
|
289
|
+
openWorldHint: annotations?.openWorldHint ?? true
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
var SidecarDefinitionError = class extends Error {
|
|
293
|
+
constructor(message) {
|
|
294
|
+
super(message);
|
|
295
|
+
this.name = "SidecarDefinitionError";
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
var SidecarRuntimeError = class extends Error {
|
|
299
|
+
constructor(message, code) {
|
|
300
|
+
super(message);
|
|
301
|
+
this.code = code;
|
|
302
|
+
this.name = "SidecarRuntimeError";
|
|
303
|
+
}
|
|
304
|
+
code;
|
|
305
|
+
};
|
|
306
|
+
function validateParams(sidecarTool, params) {
|
|
307
|
+
if (!sidecarTool.params) {
|
|
308
|
+
return params ?? {};
|
|
309
|
+
}
|
|
310
|
+
const parsed = sidecarTool.params.safeParse(params ?? {});
|
|
311
|
+
if (parsed.success) {
|
|
312
|
+
return parsed.data;
|
|
313
|
+
}
|
|
314
|
+
throw new SidecarRuntimeError(`Invalid parameters for tool "${sidecarTool.name}".`, "invalid_tool_params");
|
|
315
|
+
}
|
|
316
|
+
function normalizeRequiredContent(content) {
|
|
317
|
+
if (typeof content === "string") {
|
|
318
|
+
return [toolResult.text(content)];
|
|
319
|
+
}
|
|
320
|
+
const blocks = Array.isArray(content) ? content : [content];
|
|
321
|
+
if (blocks.length > 0) {
|
|
322
|
+
return blocks;
|
|
323
|
+
}
|
|
324
|
+
throw new SidecarRuntimeError(
|
|
325
|
+
"toolResult({ content }) must include at least one MCP content block.",
|
|
326
|
+
"invalid_tool_result"
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
function normalizeResourceContent(content, uri, defaultMimeType) {
|
|
330
|
+
const annotations = normalizeAnnotations(content.annotations);
|
|
331
|
+
const meta = content.meta;
|
|
332
|
+
if (isBinaryLike(content.content)) {
|
|
333
|
+
return stripUndefined({
|
|
334
|
+
uri,
|
|
335
|
+
mimeType: content.mimeType ?? defaultMimeType ?? "application/octet-stream",
|
|
336
|
+
blob: bytesToBase64(content.content),
|
|
337
|
+
annotations,
|
|
338
|
+
_meta: meta
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (typeof content.content === "string") {
|
|
342
|
+
return stripUndefined({
|
|
343
|
+
uri,
|
|
344
|
+
mimeType: content.mimeType ?? defaultMimeType ?? "text/plain",
|
|
345
|
+
text: content.content,
|
|
346
|
+
annotations,
|
|
347
|
+
_meta: meta
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return stripUndefined({
|
|
351
|
+
uri,
|
|
352
|
+
mimeType: content.mimeType ?? defaultMimeType ?? "application/json",
|
|
353
|
+
text: JSON.stringify(content.content),
|
|
354
|
+
annotations,
|
|
355
|
+
_meta: meta
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
function isBinaryLike(value) {
|
|
359
|
+
return value instanceof ArrayBuffer || value instanceof Uint8Array;
|
|
360
|
+
}
|
|
361
|
+
function bytesToBase64(value) {
|
|
362
|
+
const bytes = value instanceof ArrayBuffer ? new Uint8Array(value) : value;
|
|
363
|
+
if (typeof Buffer !== "undefined") {
|
|
364
|
+
return Buffer.from(bytes).toString("base64");
|
|
365
|
+
}
|
|
366
|
+
let binary = "";
|
|
367
|
+
for (const byte of bytes) {
|
|
368
|
+
binary += String.fromCharCode(byte);
|
|
369
|
+
}
|
|
370
|
+
return btoa(binary);
|
|
371
|
+
}
|
|
372
|
+
function normalizeAnnotations(annotations) {
|
|
373
|
+
if (!annotations) {
|
|
374
|
+
return void 0;
|
|
375
|
+
}
|
|
376
|
+
return stripUndefined({
|
|
377
|
+
audience: annotations.audience ? [...annotations.audience] : void 0,
|
|
378
|
+
priority: annotations.priority,
|
|
379
|
+
lastModified: annotations.lastModified instanceof Date ? annotations.lastModified.toISOString() : annotations.lastModified
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function promptArguments(args) {
|
|
383
|
+
if (!args) {
|
|
384
|
+
return void 0;
|
|
385
|
+
}
|
|
386
|
+
const entries = Object.entries(args).map(([name, value]) => {
|
|
387
|
+
if (typeof value === "string") {
|
|
388
|
+
return { name, description: value, required: true };
|
|
389
|
+
}
|
|
390
|
+
if (isReadonlyArray(value)) {
|
|
391
|
+
return {
|
|
392
|
+
name,
|
|
393
|
+
description: value.length ? `One of: ${value.map(String).join(", ")}.` : void 0,
|
|
394
|
+
required: true
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
name,
|
|
399
|
+
description: value.description,
|
|
400
|
+
required: value.required ?? true
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
return entries.length ? entries : void 0;
|
|
404
|
+
}
|
|
405
|
+
function isReadonlyArray(value) {
|
|
406
|
+
return Array.isArray(value);
|
|
407
|
+
}
|
|
408
|
+
function validatePromptArgs(promptDefinition, args) {
|
|
409
|
+
const input = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
410
|
+
for (const argument of promptArguments(promptDefinition.args) ?? []) {
|
|
411
|
+
if (argument.required !== false && !(argument.name in input)) {
|
|
412
|
+
throw new SidecarRuntimeError(
|
|
413
|
+
`Prompt "${promptDefinition.name ?? promptDefinition.title}" is missing required argument "${argument.name}".`,
|
|
414
|
+
"invalid_prompt_args"
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return input;
|
|
419
|
+
}
|
|
420
|
+
function normalizePromptResult(value, defaultDescription) {
|
|
421
|
+
if (typeof value === "string") {
|
|
422
|
+
return {
|
|
423
|
+
description: defaultDescription,
|
|
424
|
+
messages: [{
|
|
425
|
+
role: "user",
|
|
426
|
+
content: toolResult.text(value)
|
|
427
|
+
}]
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (Array.isArray(value)) {
|
|
431
|
+
return {
|
|
432
|
+
description: defaultDescription,
|
|
433
|
+
messages: value
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
if ("messages" in value) {
|
|
437
|
+
return {
|
|
438
|
+
description: value.description ?? defaultDescription,
|
|
439
|
+
messages: value.messages
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
description: defaultDescription,
|
|
444
|
+
messages: [value]
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function stripUndefined(value) {
|
|
448
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// packages/compiler/src/analyze.ts
|
|
452
|
+
import { readdir } from "fs/promises";
|
|
453
|
+
import path4 from "path";
|
|
454
|
+
import {
|
|
455
|
+
Node as Node4,
|
|
456
|
+
SyntaxKind as SyntaxKind2
|
|
457
|
+
} from "ts-morph";
|
|
458
|
+
|
|
459
|
+
// packages/compiler/src/ast.ts
|
|
460
|
+
import {
|
|
461
|
+
Node
|
|
462
|
+
} from "ts-morph";
|
|
463
|
+
function resolveDefaultExportCall(sourceFile, helperName) {
|
|
464
|
+
const exportAssignment = sourceFile.getExportAssignment(
|
|
465
|
+
(assignment) => !assignment.isExportEquals()
|
|
466
|
+
);
|
|
467
|
+
const expression = unwrapExpression(exportAssignment?.getExpression());
|
|
468
|
+
const direct = asHelperCall(expression, helperName);
|
|
469
|
+
if (direct) {
|
|
470
|
+
return direct;
|
|
471
|
+
}
|
|
472
|
+
if (!expression || !Node.isIdentifier(expression)) {
|
|
473
|
+
return void 0;
|
|
474
|
+
}
|
|
475
|
+
const declaration = findVariableDeclaration(sourceFile, expression.getText());
|
|
476
|
+
const initializer = unwrapExpression(declaration?.getInitializer());
|
|
477
|
+
return asHelperCall(initializer, helperName);
|
|
478
|
+
}
|
|
479
|
+
function unwrapExpression(expression) {
|
|
480
|
+
if (!expression) {
|
|
481
|
+
return void 0;
|
|
482
|
+
}
|
|
483
|
+
if (Node.isSatisfiesExpression(expression) || Node.isAsExpression(expression)) {
|
|
484
|
+
return expression.getExpression();
|
|
485
|
+
}
|
|
486
|
+
return expression;
|
|
487
|
+
}
|
|
488
|
+
function asHelperCall(expression, helperName) {
|
|
489
|
+
if (!expression || !Node.isCallExpression(expression)) {
|
|
490
|
+
return void 0;
|
|
491
|
+
}
|
|
492
|
+
const callee = expression.getExpression().getText();
|
|
493
|
+
return matchesHelperName(callee, helperName) ? expression : void 0;
|
|
494
|
+
}
|
|
495
|
+
function findVariableDeclaration(sourceFile, name) {
|
|
496
|
+
return sourceFile.getVariableDeclarations().find((declaration) => declaration.getName() === name);
|
|
497
|
+
}
|
|
498
|
+
function matchesHelperName(callee, helperName) {
|
|
499
|
+
return callee === helperName || callee.endsWith(`.${helperName}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// packages/compiler/src/errors.ts
|
|
503
|
+
var CompilerError = class extends Error {
|
|
504
|
+
constructor(sourceFile, message) {
|
|
505
|
+
super(`${sourceFile.getFilePath()}: ${message}`);
|
|
506
|
+
this.name = "CompilerError";
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// packages/compiler/src/project.ts
|
|
511
|
+
import path2 from "path";
|
|
512
|
+
import {
|
|
513
|
+
ModuleKind,
|
|
514
|
+
ModuleResolutionKind,
|
|
515
|
+
Project,
|
|
516
|
+
ScriptTarget
|
|
517
|
+
} from "ts-morph";
|
|
518
|
+
|
|
519
|
+
// packages/compiler/src/utils.ts
|
|
520
|
+
import { existsSync } from "fs";
|
|
521
|
+
import path from "path";
|
|
522
|
+
function existsSyncSafe(filePath) {
|
|
523
|
+
return existsSync(filePath);
|
|
524
|
+
}
|
|
525
|
+
function toImportSpecifier(fromDir, toFile, options = {}) {
|
|
526
|
+
const relative = path.relative(fromDir, toFile).replaceAll(path.sep, "/");
|
|
527
|
+
const normalized = relative.startsWith(".") ? relative : `./${relative}`;
|
|
528
|
+
switch (options.extension ?? "none") {
|
|
529
|
+
case "js":
|
|
530
|
+
return normalized.replace(/\.(tsx|ts)$/, ".js");
|
|
531
|
+
case "preserve":
|
|
532
|
+
return normalized;
|
|
533
|
+
case "none":
|
|
534
|
+
return normalized.replace(/\.(tsx|ts)$/, "");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function toIdentifier(value) {
|
|
538
|
+
const identifier = value.replace(/[^A-Za-z0-9]+(.)/g, (_match, char) => char.toUpperCase()).replace(/^[^A-Za-z_$]+/, "").replace(/[^A-Za-z0-9_$]/g, "");
|
|
539
|
+
return identifier || "tool";
|
|
540
|
+
}
|
|
541
|
+
function safePathSegment(value) {
|
|
542
|
+
const segment = value.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
543
|
+
if (!segment || segment === "." || segment === "..") {
|
|
544
|
+
return "_";
|
|
545
|
+
}
|
|
546
|
+
return segment;
|
|
547
|
+
}
|
|
548
|
+
function safeFileStem(value) {
|
|
549
|
+
return safePathSegment(value).replace(/^\.+/, "_").replace(/^_+/, "_");
|
|
550
|
+
}
|
|
551
|
+
function yamlScalar(value) {
|
|
552
|
+
return JSON.stringify(String(value ?? ""));
|
|
553
|
+
}
|
|
554
|
+
function escapeHtml(value) {
|
|
555
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
556
|
+
}
|
|
557
|
+
function stripUndefined2(value) {
|
|
558
|
+
return Object.fromEntries(
|
|
559
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0)
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
function readObjectString(source, key) {
|
|
563
|
+
const match = source.match(
|
|
564
|
+
new RegExp(`${key}\\s*:\\s*(["'\`])([\\s\\S]*?)\\1`)
|
|
565
|
+
);
|
|
566
|
+
return match?.[2]?.trim();
|
|
567
|
+
}
|
|
568
|
+
function readObjectStringArray(source, key) {
|
|
569
|
+
const match = source.match(new RegExp(`${key}\\s*:\\s*\\[([\\s\\S]*?)\\]`));
|
|
570
|
+
if (!match?.[1]) {
|
|
571
|
+
return void 0;
|
|
572
|
+
}
|
|
573
|
+
return [...match[1].matchAll(/["']([^"']+)["']/g)].map((item) => item[1]).filter(Boolean);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// packages/compiler/src/project.ts
|
|
577
|
+
function createProject(rootDir) {
|
|
578
|
+
const tsconfig = path2.join(rootDir, "tsconfig.json");
|
|
579
|
+
return new Project({
|
|
580
|
+
tsConfigFilePath: existsSyncSafe(tsconfig) ? tsconfig : void 0,
|
|
581
|
+
compilerOptions: existsSyncSafe(tsconfig) ? void 0 : {
|
|
582
|
+
allowJs: false,
|
|
583
|
+
baseUrl: process.cwd(),
|
|
584
|
+
esModuleInterop: true,
|
|
585
|
+
module: ModuleKind.NodeNext,
|
|
586
|
+
moduleResolution: ModuleResolutionKind.NodeNext,
|
|
587
|
+
paths: devSidecarTypePaths(),
|
|
588
|
+
strict: true,
|
|
589
|
+
target: ScriptTarget.ES2022
|
|
590
|
+
},
|
|
591
|
+
skipAddingFilesFromTsConfig: true
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function devSidecarTypePaths() {
|
|
595
|
+
const repoRoot = process.cwd();
|
|
596
|
+
const corePath = path2.join(repoRoot, "packages", "core", "src", "index.ts");
|
|
597
|
+
if (!existsSyncSafe(corePath)) {
|
|
598
|
+
return void 0;
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
"sidecar-ai": ["packages/sidecar-ai/src/index.ts"],
|
|
602
|
+
"@sidecar-ai/core": ["packages/core/src/index.ts"],
|
|
603
|
+
"@sidecar-ai/client": ["packages/client/src/index.ts"],
|
|
604
|
+
"@sidecar-ai/react": ["packages/react/src/index.ts"],
|
|
605
|
+
"@sidecar-ai/native": ["packages/native/src/index.ts"],
|
|
606
|
+
"@sidecar-ai/native/components": ["packages/native/src/components/index.tsx"],
|
|
607
|
+
"@sidecar-ai/openai": ["packages/openai/src/index.ts"],
|
|
608
|
+
"@sidecar-ai/openai/components": ["packages/openai/src/components.tsx"],
|
|
609
|
+
"@sidecar-ai/anthropic": ["packages/anthropic/src/index.ts"],
|
|
610
|
+
"@sidecar-ai/anthropic/components": ["packages/anthropic/src/components.tsx"],
|
|
611
|
+
"@sidecar-ai/anthropic/plugin": ["packages/anthropic/src/plugin.ts"],
|
|
612
|
+
"@sidecar-ai/anthropic/hooks": ["packages/anthropic/src/hooks.ts"]
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// packages/compiler/src/schema.ts
|
|
617
|
+
import {
|
|
618
|
+
Node as Node2
|
|
619
|
+
} from "ts-morph";
|
|
620
|
+
function getParamsSchema(definition, execute) {
|
|
621
|
+
const explicitParams = definition.getProperty("params");
|
|
622
|
+
if (explicitParams && Node2.isPropertyAssignment(explicitParams)) {
|
|
623
|
+
const inferred = getSchemaFromZodProperty(explicitParams);
|
|
624
|
+
if (inferred) {
|
|
625
|
+
return inferred;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const [params] = execute.getParameters();
|
|
629
|
+
if (!params) {
|
|
630
|
+
return emptyObjectSchema();
|
|
631
|
+
}
|
|
632
|
+
return typeToJsonSchema(
|
|
633
|
+
params.getType(),
|
|
634
|
+
schemaDescription(params.getSymbol())
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
function getOutputSchema(definition, execute) {
|
|
638
|
+
const explicitOutput = definition.getProperty("output");
|
|
639
|
+
if (explicitOutput && Node2.isPropertyAssignment(explicitOutput)) {
|
|
640
|
+
return void 0;
|
|
641
|
+
}
|
|
642
|
+
const returnType = unwrapToolResultType(unwrapPromiseType(execute.getReturnType()));
|
|
643
|
+
if (returnType.isVoid() || returnType.isUndefined()) {
|
|
644
|
+
return void 0;
|
|
645
|
+
}
|
|
646
|
+
return typeToJsonSchema(returnType);
|
|
647
|
+
}
|
|
648
|
+
function getSchemaFromZodProperty(property) {
|
|
649
|
+
const initializer = property.getInitializer();
|
|
650
|
+
if (!initializer) {
|
|
651
|
+
return void 0;
|
|
652
|
+
}
|
|
653
|
+
if (Node2.isCallExpression(initializer) && initializer.getExpression().getText().endsWith(".object")) {
|
|
654
|
+
const [shape] = initializer.getArguments();
|
|
655
|
+
if (shape && Node2.isObjectLiteralExpression(shape)) {
|
|
656
|
+
return zodObjectLiteralToSchema(shape);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return void 0;
|
|
660
|
+
}
|
|
661
|
+
function zodObjectLiteralToSchema(shape) {
|
|
662
|
+
const properties = {};
|
|
663
|
+
const required = [];
|
|
664
|
+
for (const property of shape.getProperties()) {
|
|
665
|
+
if (!Node2.isPropertyAssignment(property)) {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const name = property.getName().replace(/^["']|["']$/g, "");
|
|
669
|
+
const initializer = property.getInitializer();
|
|
670
|
+
if (!initializer) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const propertySchema = zodExpressionToSchema(initializer);
|
|
674
|
+
properties[name] = propertySchema.schema;
|
|
675
|
+
if (!propertySchema.optional) {
|
|
676
|
+
required.push(name);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
type: "object",
|
|
681
|
+
properties,
|
|
682
|
+
required,
|
|
683
|
+
additionalProperties: false
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function zodExpressionToSchema(expression) {
|
|
687
|
+
const text = expression.getText();
|
|
688
|
+
const optional = /\.optional\(\)/.test(text) || /\.default\(/.test(text);
|
|
689
|
+
const schema = {};
|
|
690
|
+
if (/z\.string\(\)/.test(text)) {
|
|
691
|
+
schema.type = "string";
|
|
692
|
+
} else if (/z\.number\(\)/.test(text)) {
|
|
693
|
+
schema.type = "number";
|
|
694
|
+
} else if (/z\.boolean\(\)/.test(text)) {
|
|
695
|
+
schema.type = "boolean";
|
|
696
|
+
} else if (/z\.array\(/.test(text)) {
|
|
697
|
+
schema.type = "array";
|
|
698
|
+
schema.items = {};
|
|
699
|
+
} else {
|
|
700
|
+
schema.type = "object";
|
|
701
|
+
}
|
|
702
|
+
const min = text.match(/\.min\((\d+)/);
|
|
703
|
+
const max = text.match(/\.max\((\d+)/);
|
|
704
|
+
if (schema.type === "string") {
|
|
705
|
+
if (min) schema.minLength = Number(min[1]);
|
|
706
|
+
if (max) schema.maxLength = Number(max[1]);
|
|
707
|
+
} else {
|
|
708
|
+
if (min) schema.minimum = Number(min[1]);
|
|
709
|
+
if (max) schema.maximum = Number(max[1]);
|
|
710
|
+
}
|
|
711
|
+
if (/\.int\(\)/.test(text)) {
|
|
712
|
+
schema.type = "integer";
|
|
713
|
+
}
|
|
714
|
+
return { schema, optional };
|
|
715
|
+
}
|
|
716
|
+
function typeToJsonSchema(type, description) {
|
|
717
|
+
const withoutUndefined = removeUndefined(type);
|
|
718
|
+
const schema = typeToJsonSchemaInner(withoutUndefined);
|
|
719
|
+
if (description) {
|
|
720
|
+
schema.description = description;
|
|
721
|
+
}
|
|
722
|
+
return schema;
|
|
723
|
+
}
|
|
724
|
+
function typeToJsonSchemaInner(type) {
|
|
725
|
+
if (type.isString() || type.isStringLiteral()) {
|
|
726
|
+
return literalOrPrimitive(type, "string");
|
|
727
|
+
}
|
|
728
|
+
if (type.isNumber() || type.isNumberLiteral()) {
|
|
729
|
+
return literalOrPrimitive(type, "number");
|
|
730
|
+
}
|
|
731
|
+
if (type.isBoolean() || type.isBooleanLiteral()) {
|
|
732
|
+
return literalOrPrimitive(type, "boolean");
|
|
733
|
+
}
|
|
734
|
+
if (type.isNull()) {
|
|
735
|
+
return { type: "null" };
|
|
736
|
+
}
|
|
737
|
+
if (type.isArray()) {
|
|
738
|
+
return {
|
|
739
|
+
type: "array",
|
|
740
|
+
items: typeToJsonSchema(type.getArrayElementTypeOrThrow())
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
if (type.isTuple()) {
|
|
744
|
+
const elements = type.getTupleElements();
|
|
745
|
+
return {
|
|
746
|
+
type: "array",
|
|
747
|
+
items: elements.length === 1 ? typeToJsonSchema(elements[0]) : { anyOf: elements.map((item) => typeToJsonSchema(item)) }
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
if (type.isUnion()) {
|
|
751
|
+
const parts = type.getUnionTypes().filter((part) => !part.isUndefined());
|
|
752
|
+
if (parts.every(isLiteralType)) {
|
|
753
|
+
return { enum: parts.map((part) => literalValue(part)) };
|
|
754
|
+
}
|
|
755
|
+
return { anyOf: parts.map((part) => typeToJsonSchema(part)) };
|
|
756
|
+
}
|
|
757
|
+
const properties = type.getProperties();
|
|
758
|
+
if (properties.length > 0) {
|
|
759
|
+
return objectTypeToSchema(properties);
|
|
760
|
+
}
|
|
761
|
+
return {};
|
|
762
|
+
}
|
|
763
|
+
function objectTypeToSchema(properties) {
|
|
764
|
+
const schemaProperties = {};
|
|
765
|
+
const required = [];
|
|
766
|
+
for (const property of properties) {
|
|
767
|
+
const declaration = property.getValueDeclaration() ?? property.getDeclarations()[0];
|
|
768
|
+
if (!declaration) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const name = property.getName();
|
|
772
|
+
const rawType = property.getTypeAtLocation(declaration);
|
|
773
|
+
const propertyType = removeUndefined(rawType);
|
|
774
|
+
const propertySchema = typeToJsonSchema(
|
|
775
|
+
propertyType,
|
|
776
|
+
schemaDescription(property)
|
|
777
|
+
);
|
|
778
|
+
schemaProperties[name] = propertySchema;
|
|
779
|
+
if (!property.isOptional() && !containsUndefined(rawType)) {
|
|
780
|
+
required.push(name);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return {
|
|
784
|
+
type: "object",
|
|
785
|
+
properties: schemaProperties,
|
|
786
|
+
required,
|
|
787
|
+
additionalProperties: false
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function literalOrPrimitive(type, primitive) {
|
|
791
|
+
if (isLiteralType(type)) {
|
|
792
|
+
return { const: literalValue(type) };
|
|
793
|
+
}
|
|
794
|
+
return { type: primitive };
|
|
795
|
+
}
|
|
796
|
+
function isLiteralType(type) {
|
|
797
|
+
return type.isStringLiteral() || type.isNumberLiteral() || type.isBooleanLiteral();
|
|
798
|
+
}
|
|
799
|
+
function literalValue(type) {
|
|
800
|
+
if (type.isStringLiteral()) return String(type.getLiteralValue());
|
|
801
|
+
if (type.isNumberLiteral()) return Number(type.getLiteralValue());
|
|
802
|
+
const text = type.getText();
|
|
803
|
+
return text === "true";
|
|
804
|
+
}
|
|
805
|
+
function unwrapPromiseType(type) {
|
|
806
|
+
if (!type.getText().startsWith("Promise<")) {
|
|
807
|
+
return type;
|
|
808
|
+
}
|
|
809
|
+
const args = type.getTypeArguments();
|
|
810
|
+
return args[0] ?? type;
|
|
811
|
+
}
|
|
812
|
+
function unwrapToolResultType(type) {
|
|
813
|
+
const aliasName = type.getAliasSymbol()?.getName();
|
|
814
|
+
if (aliasName === "ToolResult") {
|
|
815
|
+
const [structured2] = type.getAliasTypeArguments();
|
|
816
|
+
return structured2 ?? type;
|
|
817
|
+
}
|
|
818
|
+
const structured = type.getProperty("structuredContent");
|
|
819
|
+
const declaration = structured?.getValueDeclaration() ?? structured?.getDeclarations()[0];
|
|
820
|
+
if (!structured || !declaration) {
|
|
821
|
+
return type;
|
|
822
|
+
}
|
|
823
|
+
return removeUndefined(structured.getTypeAtLocation(declaration));
|
|
824
|
+
}
|
|
825
|
+
function removeUndefined(type) {
|
|
826
|
+
if (!type.isUnion()) {
|
|
827
|
+
return type;
|
|
828
|
+
}
|
|
829
|
+
const nonUndefined = type.getUnionTypes().filter((part) => !part.isUndefined());
|
|
830
|
+
return nonUndefined.length === 1 ? nonUndefined[0] : type;
|
|
831
|
+
}
|
|
832
|
+
function containsUndefined(type) {
|
|
833
|
+
return type.isUnion() && type.getUnionTypes().some((part) => part.isUndefined());
|
|
834
|
+
}
|
|
835
|
+
function schemaDescription(symbol) {
|
|
836
|
+
if (!symbol) {
|
|
837
|
+
return void 0;
|
|
838
|
+
}
|
|
839
|
+
return symbol.getDeclarations().flatMap((declaration) => {
|
|
840
|
+
if (Node2.isJSDocable(declaration)) {
|
|
841
|
+
return declaration.getJsDocs().map((doc) => doc.getCommentText() ?? "");
|
|
842
|
+
}
|
|
843
|
+
return [];
|
|
844
|
+
}).find((comment) => comment.trim().length > 0);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// packages/compiler/src/widgets.ts
|
|
848
|
+
import { createHash } from "crypto";
|
|
849
|
+
import { existsSync as existsSync2 } from "fs";
|
|
850
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
851
|
+
import path3 from "path";
|
|
852
|
+
import { build as esbuild } from "esbuild";
|
|
853
|
+
import postcss from "postcss";
|
|
854
|
+
import {
|
|
855
|
+
Node as Node3,
|
|
856
|
+
SyntaxKind
|
|
857
|
+
} from "ts-morph";
|
|
858
|
+
async function buildWidgets(rootDir, outDir, tools) {
|
|
859
|
+
const widgets = tools.filter(
|
|
860
|
+
(entry) => Boolean(entry.widget)
|
|
861
|
+
);
|
|
862
|
+
if (!widgets.length) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const cacheDir = path3.join(rootDir, ".sidecar", "cache", "widgets");
|
|
866
|
+
await mkdir(cacheDir, { recursive: true });
|
|
867
|
+
const appStyle = await prepareAppStyle(rootDir, cacheDir);
|
|
868
|
+
for (const entry of widgets) {
|
|
869
|
+
const sourceFile = path3.join(rootDir, entry.widget.sourceFile);
|
|
870
|
+
const safeId = safePathSegment(entry.id);
|
|
871
|
+
const entryFile = path3.join(cacheDir, `${safeId}.entry.tsx`);
|
|
872
|
+
const importPath = toImportSpecifier(path3.dirname(entryFile), sourceFile);
|
|
873
|
+
await writeFile(
|
|
874
|
+
entryFile,
|
|
875
|
+
`import React from "react";
|
|
876
|
+
import { createRoot } from "react-dom/client";
|
|
877
|
+
import { SidecarWidgetRoot } from "@sidecar-ai/react";
|
|
878
|
+
import "@sidecar-ai/native/styles.css";
|
|
879
|
+
${appStyle ? `import ${JSON.stringify(toImportSpecifier(path3.dirname(entryFile), appStyle))};` : ""}
|
|
880
|
+
import Component from ${JSON.stringify(importPath)};
|
|
881
|
+
|
|
882
|
+
createRoot(document.getElementById("root")!).render(
|
|
883
|
+
React.createElement(SidecarWidgetRoot, null, React.createElement(Component))
|
|
884
|
+
);
|
|
885
|
+
`
|
|
886
|
+
);
|
|
887
|
+
const bundled = await esbuild({
|
|
888
|
+
absWorkingDir: rootDir,
|
|
889
|
+
alias: devSidecarBundleAliases(rootDir),
|
|
890
|
+
bundle: true,
|
|
891
|
+
entryPoints: [entryFile],
|
|
892
|
+
format: "iife",
|
|
893
|
+
jsx: "automatic",
|
|
894
|
+
minify: false,
|
|
895
|
+
nodePaths: [
|
|
896
|
+
path3.join(rootDir, "node_modules"),
|
|
897
|
+
path3.join(process.cwd(), "node_modules")
|
|
898
|
+
],
|
|
899
|
+
outfile: "widget.js",
|
|
900
|
+
platform: "browser",
|
|
901
|
+
sourcemap: false,
|
|
902
|
+
write: false
|
|
903
|
+
});
|
|
904
|
+
const javascript = bundled.outputFiles.find((file) => file.path.endsWith(".js"))?.text ?? "";
|
|
905
|
+
const css = bundled.outputFiles.filter((file) => file.path.endsWith(".css")).map((file) => file.text).join("\n");
|
|
906
|
+
const html = renderWidgetHtml(entry.name, javascript, css);
|
|
907
|
+
const hash = createHash("sha256").update(html).digest("hex").slice(0, 12);
|
|
908
|
+
const outputDir = path3.join(outDir, "public", "widgets", safeId);
|
|
909
|
+
const outputFile = path3.join(outputDir, `widget.${hash}.html`);
|
|
910
|
+
const resourceUri = `ui://${safeId}/widget.${hash}.html`;
|
|
911
|
+
await mkdir(outputDir, { recursive: true });
|
|
912
|
+
await writeFile(outputFile, html);
|
|
913
|
+
entry.widget.resourceUri = resourceUri;
|
|
914
|
+
entry.widget.resourceMeta = widgetResourceMeta(entry.widget.options, entry.target);
|
|
915
|
+
entry.widget.outputFile = path3.relative(outDir, outputFile);
|
|
916
|
+
entry.descriptor._meta = mergeWidgetMeta(entry.descriptor._meta, widgetMeta(resourceUri, entry.widget.options, entry.target));
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
function devSidecarBundleAliases(rootDir) {
|
|
920
|
+
const repoRoot = findSidecarRepoRoot(rootDir) ?? findSidecarRepoRoot(process.cwd());
|
|
921
|
+
if (!repoRoot) {
|
|
922
|
+
return void 0;
|
|
923
|
+
}
|
|
924
|
+
const reactEntry = path3.join(repoRoot, "packages", "react", "src", "index.ts");
|
|
925
|
+
if (!existsSync2(reactEntry)) {
|
|
926
|
+
return void 0;
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
"sidecar-ai": path3.join(repoRoot, "packages", "sidecar-ai", "src", "index.ts"),
|
|
930
|
+
"@sidecar-ai/client": path3.join(repoRoot, "packages", "client", "src", "index.ts"),
|
|
931
|
+
"@sidecar-ai/core": path3.join(repoRoot, "packages", "core", "src", "index.ts"),
|
|
932
|
+
"@sidecar-ai/react": reactEntry,
|
|
933
|
+
"@sidecar-ai/native": path3.join(repoRoot, "packages", "native", "src", "index.ts"),
|
|
934
|
+
"@sidecar-ai/native/components": path3.join(repoRoot, "packages", "native", "src", "components", "index.tsx"),
|
|
935
|
+
"@sidecar-ai/native/styles.css": path3.join(repoRoot, "packages", "native", "src", "styles.css")
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function findSidecarRepoRoot(startDir) {
|
|
939
|
+
let current = path3.resolve(startDir);
|
|
940
|
+
while (true) {
|
|
941
|
+
if (existsSync2(path3.join(current, "packages", "core", "src", "index.ts"))) {
|
|
942
|
+
return current;
|
|
943
|
+
}
|
|
944
|
+
const parent = path3.dirname(current);
|
|
945
|
+
if (parent === current) {
|
|
946
|
+
return void 0;
|
|
947
|
+
}
|
|
948
|
+
current = parent;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
async function prepareAppStyle(rootDir, cacheDir) {
|
|
952
|
+
const sourceFile = path3.join(rootDir, "style.css");
|
|
953
|
+
if (!existsSync2(sourceFile)) {
|
|
954
|
+
return void 0;
|
|
955
|
+
}
|
|
956
|
+
const source = await readFile(sourceFile, "utf8");
|
|
957
|
+
const css = await processAppStyle(source, sourceFile);
|
|
958
|
+
const outputFile = path3.join(cacheDir, "style.css");
|
|
959
|
+
await writeFile(outputFile, css);
|
|
960
|
+
return outputFile;
|
|
961
|
+
}
|
|
962
|
+
async function processAppStyle(source, from) {
|
|
963
|
+
if (!needsPostcss(source)) {
|
|
964
|
+
return source;
|
|
965
|
+
}
|
|
966
|
+
const cssSource = source.replace(/@import\s+["']tailwindcss["'];?/g, "@tailwind utilities;").replace(/@import\s+["']@openai\/apps-sdk-ui\/css["'];?/g, "");
|
|
967
|
+
const plugins = [];
|
|
968
|
+
if (cssSource.includes("@tailwind")) {
|
|
969
|
+
const tailwind = await import("@tailwindcss/postcss");
|
|
970
|
+
plugins.push(tailwind.default({ base: path3.dirname(from) }));
|
|
971
|
+
}
|
|
972
|
+
const autoprefixer = await import("autoprefixer");
|
|
973
|
+
plugins.push(autoprefixer.default());
|
|
974
|
+
const result = await postcss(plugins).process(cssSource, {
|
|
975
|
+
from,
|
|
976
|
+
map: false
|
|
977
|
+
});
|
|
978
|
+
return result.css;
|
|
979
|
+
}
|
|
980
|
+
function needsPostcss(source) {
|
|
981
|
+
return /@import\s+["']tailwindcss["']|@tailwind|@source|@theme|@plugin/.test(source);
|
|
982
|
+
}
|
|
983
|
+
function findWidget(rootDir, toolFile, id, target = "mcp", toolVariant = "shared") {
|
|
984
|
+
const selected = selectWidgetFile(path3.dirname(toolFile), target, toolVariant);
|
|
985
|
+
if (!selected) {
|
|
986
|
+
return void 0;
|
|
987
|
+
}
|
|
988
|
+
const safeId = safePathSegment(id);
|
|
989
|
+
return {
|
|
990
|
+
sourceFile: path3.relative(rootDir, selected.filePath),
|
|
991
|
+
variant: selected.variant,
|
|
992
|
+
resourceUri: `ui://${safeId}/widget.html`
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
function readWidgetOptions(sourceFile) {
|
|
996
|
+
const definition = findWidgetDefinition(sourceFile);
|
|
997
|
+
if (!definition) {
|
|
998
|
+
return void 0;
|
|
999
|
+
}
|
|
1000
|
+
const options = {};
|
|
1001
|
+
const description = readStringProperty(definition, "description");
|
|
1002
|
+
const prefersBorder = readBooleanProperty(definition, "prefersBorder");
|
|
1003
|
+
const csp = readWidgetCsp(definition);
|
|
1004
|
+
const permissions = readWidgetPermissions(definition);
|
|
1005
|
+
const chatgpt = readChatGptWidgetOptions(definition);
|
|
1006
|
+
const domain = readStringProperty(definition, "domain");
|
|
1007
|
+
if (description) options.description = description;
|
|
1008
|
+
if (prefersBorder !== void 0) options.prefersBorder = prefersBorder;
|
|
1009
|
+
if (domain) options.domain = domain;
|
|
1010
|
+
if (csp) options.csp = csp;
|
|
1011
|
+
if (permissions) options.permissions = permissions;
|
|
1012
|
+
if (chatgpt) options.hosts = { chatgpt };
|
|
1013
|
+
return Object.keys(options).length ? options : void 0;
|
|
1014
|
+
}
|
|
1015
|
+
function widgetMeta(resourceUri, options = {}, target = "mcp") {
|
|
1016
|
+
const chatgptCsp = {
|
|
1017
|
+
connect_domains: options.csp?.connectDomains ? [...options.csp.connectDomains] : [],
|
|
1018
|
+
resource_domains: options.csp?.resourceDomains ? [...options.csp.resourceDomains] : [],
|
|
1019
|
+
frame_domains: options.csp?.frameDomains ? [...options.csp.frameDomains] : void 0,
|
|
1020
|
+
redirect_domains: options.hosts?.chatgpt?.redirectDomains ? [...options.hosts.chatgpt.redirectDomains] : void 0
|
|
1021
|
+
};
|
|
1022
|
+
const standard = {
|
|
1023
|
+
ui: {
|
|
1024
|
+
resourceUri
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
if (target !== "chatgpt") {
|
|
1028
|
+
return standard;
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
...standard,
|
|
1032
|
+
"openai/outputTemplate": resourceUri,
|
|
1033
|
+
"openai/widgetDescription": options.description,
|
|
1034
|
+
"openai/widgetDomain": options.hosts?.chatgpt?.domain,
|
|
1035
|
+
"openai/widgetCSP": stripUndefined3(chatgptCsp)
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function widgetResourceMeta(options = {}, target = "mcp") {
|
|
1039
|
+
const csp = stripUndefined3({
|
|
1040
|
+
connectDomains: options.csp?.connectDomains ? [...options.csp.connectDomains] : [],
|
|
1041
|
+
resourceDomains: options.csp?.resourceDomains ? [...options.csp.resourceDomains] : [],
|
|
1042
|
+
frameDomains: options.csp?.frameDomains ? [...options.csp.frameDomains] : void 0,
|
|
1043
|
+
baseUriDomains: options.csp?.baseUriDomains ? [...options.csp.baseUriDomains] : void 0
|
|
1044
|
+
});
|
|
1045
|
+
const permissions = widgetPermissionsMeta(options);
|
|
1046
|
+
const ui = stripUndefined3({
|
|
1047
|
+
csp,
|
|
1048
|
+
permissions,
|
|
1049
|
+
domain: options.domain ?? (target === "chatgpt" ? options.hosts?.chatgpt?.domain : void 0),
|
|
1050
|
+
prefersBorder: options.prefersBorder
|
|
1051
|
+
});
|
|
1052
|
+
return Object.keys(ui).length ? { ui } : void 0;
|
|
1053
|
+
}
|
|
1054
|
+
function findWidgetDefinition(sourceFile) {
|
|
1055
|
+
const call = resolveDefaultExportCall(sourceFile, "widget");
|
|
1056
|
+
if (!call) {
|
|
1057
|
+
return void 0;
|
|
1058
|
+
}
|
|
1059
|
+
const definition = unwrapExpression(call.getArguments()[0]);
|
|
1060
|
+
if (!definition || !Node3.isObjectLiteralExpression(definition)) {
|
|
1061
|
+
throw new CompilerError(
|
|
1062
|
+
sourceFile,
|
|
1063
|
+
"widget(...) must receive its metadata object as the first argument."
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
return definition;
|
|
1067
|
+
}
|
|
1068
|
+
function readWidgetCsp(definition) {
|
|
1069
|
+
const initializer = readObjectProperty(definition, "csp");
|
|
1070
|
+
if (!initializer) {
|
|
1071
|
+
return void 0;
|
|
1072
|
+
}
|
|
1073
|
+
const csp = {
|
|
1074
|
+
connectDomains: readStringArrayProperty(initializer, "connectDomains"),
|
|
1075
|
+
resourceDomains: readStringArrayProperty(initializer, "resourceDomains"),
|
|
1076
|
+
frameDomains: readStringArrayProperty(initializer, "frameDomains"),
|
|
1077
|
+
baseUriDomains: readStringArrayProperty(initializer, "baseUriDomains")
|
|
1078
|
+
};
|
|
1079
|
+
return stripUndefined3(csp);
|
|
1080
|
+
}
|
|
1081
|
+
function readWidgetPermissions(definition) {
|
|
1082
|
+
const initializer = readObjectProperty(definition, "permissions");
|
|
1083
|
+
if (!initializer) {
|
|
1084
|
+
return void 0;
|
|
1085
|
+
}
|
|
1086
|
+
const permissions = {
|
|
1087
|
+
camera: readBooleanProperty(initializer, "camera"),
|
|
1088
|
+
microphone: readBooleanProperty(initializer, "microphone"),
|
|
1089
|
+
geolocation: readBooleanProperty(initializer, "geolocation"),
|
|
1090
|
+
clipboardWrite: readBooleanProperty(initializer, "clipboardWrite")
|
|
1091
|
+
};
|
|
1092
|
+
return stripUndefined3(permissions);
|
|
1093
|
+
}
|
|
1094
|
+
function widgetPermissionsMeta(options) {
|
|
1095
|
+
const permissions = options.permissions;
|
|
1096
|
+
if (!permissions) {
|
|
1097
|
+
return void 0;
|
|
1098
|
+
}
|
|
1099
|
+
const meta = stripUndefined3({
|
|
1100
|
+
camera: permissions.camera ? {} : void 0,
|
|
1101
|
+
microphone: permissions.microphone ? {} : void 0,
|
|
1102
|
+
geolocation: permissions.geolocation ? {} : void 0,
|
|
1103
|
+
clipboardWrite: permissions.clipboardWrite ? {} : void 0
|
|
1104
|
+
});
|
|
1105
|
+
return Object.keys(meta).length ? meta : void 0;
|
|
1106
|
+
}
|
|
1107
|
+
function readChatGptWidgetOptions(definition) {
|
|
1108
|
+
const hosts = readObjectProperty(definition, "hosts");
|
|
1109
|
+
const chatgpt = hosts ? readObjectProperty(hosts, "chatgpt") : void 0;
|
|
1110
|
+
if (!chatgpt) {
|
|
1111
|
+
return void 0;
|
|
1112
|
+
}
|
|
1113
|
+
const options = {
|
|
1114
|
+
domain: readStringProperty(chatgpt, "domain"),
|
|
1115
|
+
redirectDomains: readStringArrayProperty(chatgpt, "redirectDomains")
|
|
1116
|
+
};
|
|
1117
|
+
return stripUndefined3(options);
|
|
1118
|
+
}
|
|
1119
|
+
function readObjectProperty(definition, propertyName) {
|
|
1120
|
+
const property = definition.getProperty(propertyName);
|
|
1121
|
+
if (!property || !Node3.isPropertyAssignment(property)) {
|
|
1122
|
+
return void 0;
|
|
1123
|
+
}
|
|
1124
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1125
|
+
return initializer && Node3.isObjectLiteralExpression(initializer) ? initializer : void 0;
|
|
1126
|
+
}
|
|
1127
|
+
function readStringProperty(definition, propertyName) {
|
|
1128
|
+
const property = definition.getProperty(propertyName);
|
|
1129
|
+
if (!property || !Node3.isPropertyAssignment(property)) {
|
|
1130
|
+
return void 0;
|
|
1131
|
+
}
|
|
1132
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1133
|
+
return initializer && Node3.isStringLiteral(initializer) ? initializer.getLiteralText() : void 0;
|
|
1134
|
+
}
|
|
1135
|
+
function readBooleanProperty(definition, propertyName) {
|
|
1136
|
+
const property = definition.getProperty(propertyName);
|
|
1137
|
+
if (!property || !Node3.isPropertyAssignment(property)) {
|
|
1138
|
+
return void 0;
|
|
1139
|
+
}
|
|
1140
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1141
|
+
if (!initializer) {
|
|
1142
|
+
return void 0;
|
|
1143
|
+
}
|
|
1144
|
+
if (initializer.getKind() === SyntaxKind.TrueKeyword) {
|
|
1145
|
+
return true;
|
|
1146
|
+
}
|
|
1147
|
+
if (initializer.getKind() === SyntaxKind.FalseKeyword) {
|
|
1148
|
+
return false;
|
|
1149
|
+
}
|
|
1150
|
+
return void 0;
|
|
1151
|
+
}
|
|
1152
|
+
function readStringArrayProperty(definition, propertyName) {
|
|
1153
|
+
const property = definition.getProperty(propertyName);
|
|
1154
|
+
if (!property || !Node3.isPropertyAssignment(property)) {
|
|
1155
|
+
return void 0;
|
|
1156
|
+
}
|
|
1157
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1158
|
+
if (!initializer || !Node3.isArrayLiteralExpression(initializer)) {
|
|
1159
|
+
return void 0;
|
|
1160
|
+
}
|
|
1161
|
+
return initializer.getElements().filter(Node3.isStringLiteral).map((element) => element.getLiteralText());
|
|
1162
|
+
}
|
|
1163
|
+
function selectWidgetFile(directory, target, toolVariant) {
|
|
1164
|
+
if (toolVariant === "openai") {
|
|
1165
|
+
const filePath = path3.join(directory, "widget.openai.tsx");
|
|
1166
|
+
return existsSync2(filePath) ? { filePath, variant: "openai" } : void 0;
|
|
1167
|
+
}
|
|
1168
|
+
if (toolVariant === "anthropic") {
|
|
1169
|
+
const filePath = path3.join(directory, "widget.anthropic.tsx");
|
|
1170
|
+
return existsSync2(filePath) ? { filePath, variant: "anthropic" } : void 0;
|
|
1171
|
+
}
|
|
1172
|
+
const candidates = target === "chatgpt" || target === "claude" || target === "mcp" ? [{ name: "widget.tsx", variant: "shared" }] : [];
|
|
1173
|
+
for (const candidate of candidates) {
|
|
1174
|
+
const filePath = path3.join(directory, candidate.name);
|
|
1175
|
+
if (existsSync2(filePath)) {
|
|
1176
|
+
return { filePath, variant: candidate.variant };
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return void 0;
|
|
1180
|
+
}
|
|
1181
|
+
function mergeWidgetMeta(existing, widget) {
|
|
1182
|
+
const merged = { ...existing ?? {} };
|
|
1183
|
+
for (const [key, value] of Object.entries(widget)) {
|
|
1184
|
+
if (key === "ui" && isRecord2(value) && isRecord2(merged.ui)) {
|
|
1185
|
+
merged.ui = { ...merged.ui, ...value };
|
|
1186
|
+
} else {
|
|
1187
|
+
merged[key] = value;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return merged;
|
|
1191
|
+
}
|
|
1192
|
+
function renderWidgetHtml(title, javascript, css = "") {
|
|
1193
|
+
return `<!doctype html>
|
|
1194
|
+
<html lang="en">
|
|
1195
|
+
<head>
|
|
1196
|
+
<meta charset="utf-8" />
|
|
1197
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1198
|
+
<title>${escapeHtml(title)}</title>
|
|
1199
|
+
<style>
|
|
1200
|
+
:root { color-scheme: light dark; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
1201
|
+
html, body, #root { min-height: 100%; margin: 0; background: transparent; }
|
|
1202
|
+
body { color: CanvasText; }
|
|
1203
|
+
* { box-sizing: border-box; }
|
|
1204
|
+
${css}
|
|
1205
|
+
</style>
|
|
1206
|
+
</head>
|
|
1207
|
+
<body>
|
|
1208
|
+
<div id="root"></div>
|
|
1209
|
+
<script>${javascript}</script>
|
|
1210
|
+
</body>
|
|
1211
|
+
</html>
|
|
1212
|
+
`;
|
|
1213
|
+
}
|
|
1214
|
+
function isRecord2(value) {
|
|
1215
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1216
|
+
}
|
|
1217
|
+
function stripUndefined3(value) {
|
|
1218
|
+
return Object.fromEntries(
|
|
1219
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0)
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// packages/compiler/src/analyze.ts
|
|
1224
|
+
async function analyzeProjectTools(rootDir, options = {}) {
|
|
1225
|
+
const target = options.target ?? "mcp";
|
|
1226
|
+
const toolFiles = await findToolFiles(path4.join(rootDir, "server"), target);
|
|
1227
|
+
if (toolFiles.length === 0) {
|
|
1228
|
+
return [];
|
|
1229
|
+
}
|
|
1230
|
+
const project = createProject(rootDir);
|
|
1231
|
+
const authScopes = readAuthScopeCatalog(project, rootDir);
|
|
1232
|
+
return toolFiles.map(
|
|
1233
|
+
(candidate) => analyzeToolFile(project.addSourceFileAtPath(candidate.filePath), rootDir, {
|
|
1234
|
+
target,
|
|
1235
|
+
variant: candidate.variant,
|
|
1236
|
+
authScopes
|
|
1237
|
+
})
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
function analyzeToolFile(sourceFile, rootDir, options = {}) {
|
|
1241
|
+
const target = options.target ?? "mcp";
|
|
1242
|
+
const variant = options.variant ?? "shared";
|
|
1243
|
+
const definition = findToolDefinition(sourceFile);
|
|
1244
|
+
const absoluteFile = sourceFile.getFilePath();
|
|
1245
|
+
const directory = path4.basename(path4.dirname(absoluteFile));
|
|
1246
|
+
const name = getRequiredStringProperty(definition, "name", sourceFile);
|
|
1247
|
+
const id = getOptionalStringProperty(definition, "id") ?? toMachineName(directory);
|
|
1248
|
+
const description = getRequiredStringProperty(
|
|
1249
|
+
definition,
|
|
1250
|
+
"description",
|
|
1251
|
+
sourceFile
|
|
1252
|
+
);
|
|
1253
|
+
const annotations = readAnnotations(definition);
|
|
1254
|
+
const visibility = readVisibility(definition);
|
|
1255
|
+
const hosts = readHosts(definition);
|
|
1256
|
+
const auth = readAuthPolicy(definition, options.authScopes ?? {});
|
|
1257
|
+
const execute = getExecuteFunction(definition, sourceFile);
|
|
1258
|
+
const inputSchema = getParamsSchema(definition, execute);
|
|
1259
|
+
const outputSchema = getOutputSchema(definition, execute);
|
|
1260
|
+
const descriptor = createToolDescriptor({
|
|
1261
|
+
name,
|
|
1262
|
+
id,
|
|
1263
|
+
description,
|
|
1264
|
+
target,
|
|
1265
|
+
inputSchema,
|
|
1266
|
+
outputSchema,
|
|
1267
|
+
annotations,
|
|
1268
|
+
visibility,
|
|
1269
|
+
hosts,
|
|
1270
|
+
auth,
|
|
1271
|
+
meta: void 0
|
|
1272
|
+
});
|
|
1273
|
+
validateWidgetHierarchy(sourceFile, absoluteFile, target, variant);
|
|
1274
|
+
const widget = findWidget(rootDir, absoluteFile, id, target, variant);
|
|
1275
|
+
if (widget) {
|
|
1276
|
+
const widgetFile = path4.join(rootDir, widget.sourceFile);
|
|
1277
|
+
const project = sourceFile.getProject();
|
|
1278
|
+
const widgetSourceFile = project.getSourceFile(widgetFile) ?? project.addSourceFileAtPath(widgetFile);
|
|
1279
|
+
widget.options = {
|
|
1280
|
+
description,
|
|
1281
|
+
...readWidgetOptions(widgetSourceFile)
|
|
1282
|
+
};
|
|
1283
|
+
widget.resourceMeta = widgetResourceMeta(widget.options, target);
|
|
1284
|
+
descriptor._meta = mergeWidgetMeta(descriptor._meta, widgetMeta(widget.resourceUri, widget.options, target));
|
|
1285
|
+
}
|
|
1286
|
+
return {
|
|
1287
|
+
sourceFile: path4.relative(rootDir, absoluteFile),
|
|
1288
|
+
variant,
|
|
1289
|
+
target,
|
|
1290
|
+
directory,
|
|
1291
|
+
id,
|
|
1292
|
+
name,
|
|
1293
|
+
description,
|
|
1294
|
+
inputSchema,
|
|
1295
|
+
outputSchema,
|
|
1296
|
+
annotations,
|
|
1297
|
+
visibility,
|
|
1298
|
+
widget,
|
|
1299
|
+
descriptor
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function readAuthPolicy(definition, authScopes) {
|
|
1303
|
+
const initializer = readObjectProperty2(definition, "auth");
|
|
1304
|
+
if (!initializer) {
|
|
1305
|
+
return void 0;
|
|
1306
|
+
}
|
|
1307
|
+
const publicValue = readBooleanProperty2(initializer, "public");
|
|
1308
|
+
if (publicValue === true) {
|
|
1309
|
+
return { public: true };
|
|
1310
|
+
}
|
|
1311
|
+
const scopesProperty = initializer.getProperty("scopes");
|
|
1312
|
+
if (scopesProperty && Node4.isPropertyAssignment(scopesProperty)) {
|
|
1313
|
+
return { scopes: readScopeReferences(scopesProperty.getInitializer(), authScopes) };
|
|
1314
|
+
}
|
|
1315
|
+
return { authenticated: true };
|
|
1316
|
+
}
|
|
1317
|
+
function readAuthScopeCatalog(project, rootDir) {
|
|
1318
|
+
const authPath = path4.join(rootDir, "auth.ts");
|
|
1319
|
+
if (!existsSyncSafe(authPath)) {
|
|
1320
|
+
return {};
|
|
1321
|
+
}
|
|
1322
|
+
const sourceFile = project.addSourceFileAtPath(authPath);
|
|
1323
|
+
const catalog = {};
|
|
1324
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind2.CallExpression)) {
|
|
1325
|
+
if (!isNamedCall(call, "auth")) {
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const definition = unwrapExpression(call.getArguments()[0]);
|
|
1329
|
+
if (!definition || !Node4.isObjectLiteralExpression(definition)) {
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
const scopes = readObjectProperty2(definition, "scopes");
|
|
1333
|
+
if (!scopes) {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
for (const property of scopes.getProperties()) {
|
|
1337
|
+
if (!Node4.isPropertyAssignment(property)) {
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
const scope = readScopeCall(property.getInitializer());
|
|
1341
|
+
if (scope) {
|
|
1342
|
+
catalog[property.getName().replace(/^["']|["']$/g, "")] = scope;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return catalog;
|
|
1347
|
+
}
|
|
1348
|
+
function readScopeReferences(initializer, authScopes) {
|
|
1349
|
+
const expression = unwrapExpression(initializer);
|
|
1350
|
+
if (!expression || !Node4.isArrayLiteralExpression(expression)) {
|
|
1351
|
+
return [];
|
|
1352
|
+
}
|
|
1353
|
+
return expression.getElements().flatMap((element) => {
|
|
1354
|
+
const direct = readScopeCall(element);
|
|
1355
|
+
if (direct) {
|
|
1356
|
+
return [{ kind: "sidecar.scope", ...direct }];
|
|
1357
|
+
}
|
|
1358
|
+
const name = scopeReferenceName(element);
|
|
1359
|
+
const declared = name ? authScopes[name] : void 0;
|
|
1360
|
+
if (declared) {
|
|
1361
|
+
return [{ kind: "sidecar.scope", ...declared }];
|
|
1362
|
+
}
|
|
1363
|
+
const literal = unwrapExpression(element);
|
|
1364
|
+
if (literal && Node4.isStringLiteral(literal)) {
|
|
1365
|
+
return [{
|
|
1366
|
+
kind: "sidecar.scope",
|
|
1367
|
+
id: literal.getLiteralText(),
|
|
1368
|
+
description: ""
|
|
1369
|
+
}];
|
|
1370
|
+
}
|
|
1371
|
+
return [];
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
function readScopeCall(node) {
|
|
1375
|
+
const expression = unwrapExpression(node);
|
|
1376
|
+
if (!expression || !Node4.isCallExpression(expression) || !isNamedCall(expression, "scope")) {
|
|
1377
|
+
return void 0;
|
|
1378
|
+
}
|
|
1379
|
+
const [idArg, descriptionArg] = expression.getArguments();
|
|
1380
|
+
if (!idArg || !Node4.isStringLiteral(idArg)) {
|
|
1381
|
+
return void 0;
|
|
1382
|
+
}
|
|
1383
|
+
return {
|
|
1384
|
+
id: idArg.getLiteralText(),
|
|
1385
|
+
description: Node4.isStringLiteral(descriptionArg) ? descriptionArg.getLiteralText() : ""
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
function scopeReferenceName(node) {
|
|
1389
|
+
const expression = unwrapExpression(node);
|
|
1390
|
+
if (!expression || !Node4.isPropertyAccessExpression(expression)) {
|
|
1391
|
+
return void 0;
|
|
1392
|
+
}
|
|
1393
|
+
return expression.getName();
|
|
1394
|
+
}
|
|
1395
|
+
function isNamedCall(call, name) {
|
|
1396
|
+
const callee = call.getExpression().getText();
|
|
1397
|
+
return callee === name || callee.endsWith(`.${name}`);
|
|
1398
|
+
}
|
|
1399
|
+
function validateWidgetHierarchy(sourceFile, toolFile, target, variant) {
|
|
1400
|
+
const directory = path4.dirname(toolFile);
|
|
1401
|
+
const platformWidget = target === "chatgpt" ? "widget.openai.tsx" : target === "claude" ? "widget.anthropic.tsx" : void 0;
|
|
1402
|
+
if (!platformWidget || variant !== "shared") {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
if (existsSyncSafe(path4.join(directory, platformWidget))) {
|
|
1406
|
+
const expectedTool = target === "chatgpt" ? "tool.openai.ts" : "tool.anthropic.ts";
|
|
1407
|
+
throw new CompilerError(
|
|
1408
|
+
sourceFile,
|
|
1409
|
+
`${platformWidget} requires a sibling ${expectedTool}; platform-specific widgets cannot attach to a shared tool.ts.`
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
async function findToolFiles(serverDir, target) {
|
|
1414
|
+
if (!existsSyncSafe(serverDir)) {
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
const entries = await readdir(serverDir, { withFileTypes: true });
|
|
1418
|
+
const files = [];
|
|
1419
|
+
for (const entry of entries) {
|
|
1420
|
+
const entryPath = path4.join(serverDir, entry.name);
|
|
1421
|
+
if (entry.isDirectory()) {
|
|
1422
|
+
const candidate = selectToolFile(entryPath, target);
|
|
1423
|
+
if (candidate) {
|
|
1424
|
+
files.push(candidate);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
return files.sort((left, right) => left.filePath.localeCompare(right.filePath));
|
|
1429
|
+
}
|
|
1430
|
+
function selectToolFile(directory, target) {
|
|
1431
|
+
const candidates = target === "chatgpt" ? [
|
|
1432
|
+
{ name: "tool.openai.ts", variant: "openai" },
|
|
1433
|
+
{ name: "tool.ts", variant: "shared" }
|
|
1434
|
+
] : target === "claude" ? [
|
|
1435
|
+
{ name: "tool.anthropic.ts", variant: "anthropic" },
|
|
1436
|
+
{ name: "tool.ts", variant: "shared" }
|
|
1437
|
+
] : [{ name: "tool.ts", variant: "shared" }];
|
|
1438
|
+
for (const candidate of candidates) {
|
|
1439
|
+
const filePath = path4.join(directory, candidate.name);
|
|
1440
|
+
if (existsSyncSafe(filePath)) {
|
|
1441
|
+
return { filePath, variant: candidate.variant };
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return void 0;
|
|
1445
|
+
}
|
|
1446
|
+
function findToolDefinition(sourceFile) {
|
|
1447
|
+
const call = resolveDefaultExportCall(sourceFile, "tool");
|
|
1448
|
+
if (!call) {
|
|
1449
|
+
throw new CompilerError(
|
|
1450
|
+
sourceFile,
|
|
1451
|
+
"tool.ts must default-export tool({ ... }) or an identifier initialized with tool({ ... })."
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
const definition = unwrapExpression(call.getArguments()[0]);
|
|
1455
|
+
if (!definition || !Node4.isObjectLiteralExpression(definition)) {
|
|
1456
|
+
throw new CompilerError(
|
|
1457
|
+
sourceFile,
|
|
1458
|
+
"tool(...) must receive one object literal."
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
return definition;
|
|
1462
|
+
}
|
|
1463
|
+
function getExecuteFunction(definition, sourceFile) {
|
|
1464
|
+
const property = definition.getProperty("execute");
|
|
1465
|
+
if (!property) {
|
|
1466
|
+
throw new CompilerError(
|
|
1467
|
+
sourceFile,
|
|
1468
|
+
"tool({ ... }) must include an execute method."
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
if (Node4.isMethodDeclaration(property)) {
|
|
1472
|
+
return property;
|
|
1473
|
+
}
|
|
1474
|
+
if (Node4.isPropertyAssignment(property)) {
|
|
1475
|
+
const initializer = property.getInitializer();
|
|
1476
|
+
if (initializer && (Node4.isArrowFunction(initializer) || Node4.isFunctionExpression(initializer))) {
|
|
1477
|
+
return initializer;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
throw new CompilerError(
|
|
1481
|
+
sourceFile,
|
|
1482
|
+
"execute must be a method, function expression, or arrow function."
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
function getRequiredStringProperty(definition, propertyName, sourceFile) {
|
|
1486
|
+
const value = getOptionalStringProperty(definition, propertyName);
|
|
1487
|
+
if (value === void 0 || !value.trim()) {
|
|
1488
|
+
throw new CompilerError(
|
|
1489
|
+
sourceFile,
|
|
1490
|
+
`tool({ ... }) must include a non-empty ${propertyName} string.`
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
return value;
|
|
1494
|
+
}
|
|
1495
|
+
function getOptionalStringProperty(definition, propertyName) {
|
|
1496
|
+
const property = definition.getProperty(propertyName);
|
|
1497
|
+
if (!property || !Node4.isPropertyAssignment(property)) {
|
|
1498
|
+
return void 0;
|
|
1499
|
+
}
|
|
1500
|
+
const initializer = property.getInitializer();
|
|
1501
|
+
if (!initializer || !Node4.isStringLiteral(initializer)) {
|
|
1502
|
+
return void 0;
|
|
1503
|
+
}
|
|
1504
|
+
return initializer.getLiteralText();
|
|
1505
|
+
}
|
|
1506
|
+
function readAnnotations(definition) {
|
|
1507
|
+
const property = definition.getProperty("annotations");
|
|
1508
|
+
if (!property || !Node4.isPropertyAssignment(property)) {
|
|
1509
|
+
return void 0;
|
|
1510
|
+
}
|
|
1511
|
+
const initializer = property.getInitializer();
|
|
1512
|
+
if (!initializer || !Node4.isObjectLiteralExpression(initializer)) {
|
|
1513
|
+
return void 0;
|
|
1514
|
+
}
|
|
1515
|
+
const annotations = {};
|
|
1516
|
+
for (const key of [
|
|
1517
|
+
"title",
|
|
1518
|
+
"readOnlyHint",
|
|
1519
|
+
"destructiveHint",
|
|
1520
|
+
"idempotentHint",
|
|
1521
|
+
"openWorldHint"
|
|
1522
|
+
]) {
|
|
1523
|
+
const value = initializer.getProperty(key);
|
|
1524
|
+
if (!value || !Node4.isPropertyAssignment(value)) {
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
const expression = value.getInitializer();
|
|
1528
|
+
if (!expression) {
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
if (key === "title" && Node4.isStringLiteral(expression)) {
|
|
1532
|
+
annotations.title = expression.getLiteralText();
|
|
1533
|
+
} else if (key !== "title" && (expression.getKind() === SyntaxKind2.TrueKeyword || expression.getKind() === SyntaxKind2.FalseKeyword)) {
|
|
1534
|
+
annotations[key] = expression.getKind() === SyntaxKind2.TrueKeyword;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return Object.keys(annotations).length ? annotations : void 0;
|
|
1538
|
+
}
|
|
1539
|
+
function readVisibility(definition) {
|
|
1540
|
+
const initializer = readObjectProperty2(definition, "visibility");
|
|
1541
|
+
if (!initializer) {
|
|
1542
|
+
return void 0;
|
|
1543
|
+
}
|
|
1544
|
+
const visibility = {};
|
|
1545
|
+
for (const key of ["model", "widgets", "tools"]) {
|
|
1546
|
+
const property = initializer.getProperty(key);
|
|
1547
|
+
if (!property || !Node4.isPropertyAssignment(property)) {
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
const value = property.getInitializer();
|
|
1551
|
+
if (!value) {
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
if (value.getKind() === SyntaxKind2.TrueKeyword || value.getKind() === SyntaxKind2.FalseKeyword) {
|
|
1555
|
+
assignVisibility(visibility, key, value.getKind() === SyntaxKind2.TrueKeyword);
|
|
1556
|
+
} else if (Node4.isArrayLiteralExpression(value)) {
|
|
1557
|
+
assignVisibility(visibility, key, value.getElements().filter(Node4.isStringLiteral).map((element) => element.getLiteralText()));
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
return Object.keys(visibility).length ? visibility : void 0;
|
|
1561
|
+
}
|
|
1562
|
+
function assignVisibility(visibility, key, value) {
|
|
1563
|
+
visibility[key] = value;
|
|
1564
|
+
}
|
|
1565
|
+
function readHosts(definition) {
|
|
1566
|
+
const initializer = readObjectProperty2(definition, "hosts");
|
|
1567
|
+
if (!initializer) {
|
|
1568
|
+
return void 0;
|
|
1569
|
+
}
|
|
1570
|
+
const chatgpt = readObjectProperty2(initializer, "chatgpt");
|
|
1571
|
+
if (!chatgpt) {
|
|
1572
|
+
return void 0;
|
|
1573
|
+
}
|
|
1574
|
+
const options = {};
|
|
1575
|
+
const invoking = readStringProperty2(chatgpt, "invoking");
|
|
1576
|
+
const invoked = readStringProperty2(chatgpt, "invoked");
|
|
1577
|
+
const visibility = readStringProperty2(chatgpt, "visibility");
|
|
1578
|
+
const widgetAccessible = readBooleanProperty2(chatgpt, "widgetAccessible");
|
|
1579
|
+
const fileParams = readStringArrayProperty2(chatgpt, "fileParams");
|
|
1580
|
+
if (invoking) options.invoking = invoking;
|
|
1581
|
+
if (invoked) options.invoked = invoked;
|
|
1582
|
+
if (visibility === "public" || visibility === "private") {
|
|
1583
|
+
options.visibility = visibility;
|
|
1584
|
+
}
|
|
1585
|
+
if (widgetAccessible !== void 0) {
|
|
1586
|
+
options.widgetAccessible = widgetAccessible;
|
|
1587
|
+
}
|
|
1588
|
+
if (fileParams) {
|
|
1589
|
+
options.fileParams = fileParams;
|
|
1590
|
+
}
|
|
1591
|
+
return Object.keys(options).length ? { chatgpt: options } : void 0;
|
|
1592
|
+
}
|
|
1593
|
+
function readObjectProperty2(definition, propertyName) {
|
|
1594
|
+
const property = definition.getProperty(propertyName);
|
|
1595
|
+
if (!property || !Node4.isPropertyAssignment(property)) {
|
|
1596
|
+
return void 0;
|
|
1597
|
+
}
|
|
1598
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1599
|
+
return initializer && Node4.isObjectLiteralExpression(initializer) ? initializer : void 0;
|
|
1600
|
+
}
|
|
1601
|
+
function readStringProperty2(definition, propertyName) {
|
|
1602
|
+
const property = definition.getProperty(propertyName);
|
|
1603
|
+
if (!property || !Node4.isPropertyAssignment(property)) {
|
|
1604
|
+
return void 0;
|
|
1605
|
+
}
|
|
1606
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1607
|
+
return initializer && Node4.isStringLiteral(initializer) ? initializer.getLiteralText() : void 0;
|
|
1608
|
+
}
|
|
1609
|
+
function readBooleanProperty2(definition, propertyName) {
|
|
1610
|
+
const property = definition.getProperty(propertyName);
|
|
1611
|
+
if (!property || !Node4.isPropertyAssignment(property)) {
|
|
1612
|
+
return void 0;
|
|
1613
|
+
}
|
|
1614
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1615
|
+
if (!initializer) {
|
|
1616
|
+
return void 0;
|
|
1617
|
+
}
|
|
1618
|
+
if (initializer.getKind() === SyntaxKind2.TrueKeyword) {
|
|
1619
|
+
return true;
|
|
1620
|
+
}
|
|
1621
|
+
if (initializer.getKind() === SyntaxKind2.FalseKeyword) {
|
|
1622
|
+
return false;
|
|
1623
|
+
}
|
|
1624
|
+
return void 0;
|
|
1625
|
+
}
|
|
1626
|
+
function readStringArrayProperty2(definition, propertyName) {
|
|
1627
|
+
const property = definition.getProperty(propertyName);
|
|
1628
|
+
if (!property || !Node4.isPropertyAssignment(property)) {
|
|
1629
|
+
return void 0;
|
|
1630
|
+
}
|
|
1631
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1632
|
+
if (!initializer || !Node4.isArrayLiteralExpression(initializer)) {
|
|
1633
|
+
return void 0;
|
|
1634
|
+
}
|
|
1635
|
+
return initializer.getElements().filter(Node4.isStringLiteral).map((element) => element.getLiteralText());
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// packages/compiler/src/build.ts
|
|
1639
|
+
import { mkdir as mkdir8, writeFile as writeFile8 } from "fs/promises";
|
|
1640
|
+
import path17 from "path";
|
|
1641
|
+
|
|
1642
|
+
// packages/compiler/src/config.ts
|
|
1643
|
+
import path5 from "path";
|
|
1644
|
+
import {
|
|
1645
|
+
Node as Node5,
|
|
1646
|
+
SyntaxKind as SyntaxKind3
|
|
1647
|
+
} from "ts-morph";
|
|
1648
|
+
function analyzeProjectConfig(rootDir) {
|
|
1649
|
+
const configPath = path5.join(rootDir, "sidecar.config.ts");
|
|
1650
|
+
if (!existsSyncSafe(configPath)) {
|
|
1651
|
+
return defaultCompilerConfig();
|
|
1652
|
+
}
|
|
1653
|
+
const project = createProject(rootDir);
|
|
1654
|
+
const sourceFile = project.addSourceFileAtPath(configPath);
|
|
1655
|
+
const call = resolveDefaultExportCall(sourceFile, "defineConfig");
|
|
1656
|
+
const definition = unwrapExpression(call?.getArguments()[0]);
|
|
1657
|
+
if (!definition || !Node5.isObjectLiteralExpression(definition)) {
|
|
1658
|
+
return defaultCompilerConfig();
|
|
1659
|
+
}
|
|
1660
|
+
return {
|
|
1661
|
+
resources: {
|
|
1662
|
+
subscribe: readBooleanNested(definition, "resources", "subscribe") ?? false,
|
|
1663
|
+
listChanged: readBooleanNested(definition, "resources", "listChanged") ?? false
|
|
1664
|
+
},
|
|
1665
|
+
prompts: {
|
|
1666
|
+
listChanged: readBooleanNested(definition, "prompts", "listChanged") ?? false
|
|
1667
|
+
},
|
|
1668
|
+
tools: {
|
|
1669
|
+
listChanged: readBooleanNested(definition, "tools", "listChanged") ?? false
|
|
1670
|
+
},
|
|
1671
|
+
pagination: {
|
|
1672
|
+
pageSize: readNumberNested(definition, "pagination", "pageSize") ?? 10,
|
|
1673
|
+
hasOverride: hasProperty(readObjectProperty3(definition, "pagination"), "override")
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
function defaultCompilerConfig() {
|
|
1678
|
+
return {
|
|
1679
|
+
resources: {
|
|
1680
|
+
subscribe: false,
|
|
1681
|
+
listChanged: false
|
|
1682
|
+
},
|
|
1683
|
+
prompts: {
|
|
1684
|
+
listChanged: false
|
|
1685
|
+
},
|
|
1686
|
+
tools: {
|
|
1687
|
+
listChanged: false
|
|
1688
|
+
},
|
|
1689
|
+
pagination: {
|
|
1690
|
+
pageSize: 10,
|
|
1691
|
+
hasOverride: false
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
function readBooleanNested(definition, section, propertyName) {
|
|
1696
|
+
const object = readObjectProperty3(definition, section);
|
|
1697
|
+
if (!object) {
|
|
1698
|
+
return void 0;
|
|
1699
|
+
}
|
|
1700
|
+
const property = object.getProperty(propertyName);
|
|
1701
|
+
if (!property || !Node5.isPropertyAssignment(property)) {
|
|
1702
|
+
return void 0;
|
|
1703
|
+
}
|
|
1704
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1705
|
+
if (!initializer) {
|
|
1706
|
+
return void 0;
|
|
1707
|
+
}
|
|
1708
|
+
if (initializer.getKind() === SyntaxKind3.TrueKeyword) return true;
|
|
1709
|
+
if (initializer.getKind() === SyntaxKind3.FalseKeyword) return false;
|
|
1710
|
+
return void 0;
|
|
1711
|
+
}
|
|
1712
|
+
function readNumberNested(definition, section, propertyName) {
|
|
1713
|
+
const object = readObjectProperty3(definition, section);
|
|
1714
|
+
if (!object) {
|
|
1715
|
+
return void 0;
|
|
1716
|
+
}
|
|
1717
|
+
const property = object.getProperty(propertyName);
|
|
1718
|
+
if (!property || !Node5.isPropertyAssignment(property)) {
|
|
1719
|
+
return void 0;
|
|
1720
|
+
}
|
|
1721
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1722
|
+
return initializer && Node5.isNumericLiteral(initializer) ? Number(initializer.getLiteralText()) : void 0;
|
|
1723
|
+
}
|
|
1724
|
+
function readObjectProperty3(definition, propertyName) {
|
|
1725
|
+
const property = definition.getProperty(propertyName);
|
|
1726
|
+
if (!property || !Node5.isPropertyAssignment(property)) {
|
|
1727
|
+
return void 0;
|
|
1728
|
+
}
|
|
1729
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
1730
|
+
return initializer && Node5.isObjectLiteralExpression(initializer) ? initializer : void 0;
|
|
1731
|
+
}
|
|
1732
|
+
function hasProperty(definition, propertyName) {
|
|
1733
|
+
return Boolean(definition?.getProperty(propertyName));
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// packages/compiler/src/diagnostics.ts
|
|
1737
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1738
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1739
|
+
import { createRequire } from "module";
|
|
1740
|
+
import path6 from "path";
|
|
1741
|
+
async function collectProjectDiagnostics(rootDir, input) {
|
|
1742
|
+
const diagnostics = [];
|
|
1743
|
+
const tools = Array.isArray(input) ? input : input.tools;
|
|
1744
|
+
const resources = Array.isArray(input) ? [] : input.resources ?? [];
|
|
1745
|
+
const prompts = Array.isArray(input) ? [] : input.prompts ?? [];
|
|
1746
|
+
const config = Array.isArray(input) ? void 0 : input.config;
|
|
1747
|
+
const hasAuthConfig = existsSync3(path6.join(rootDir, "auth.ts"));
|
|
1748
|
+
const hasOpenAiAppsSdkUi = canResolveOpenAiAppsSdkUi(rootDir);
|
|
1749
|
+
for (const entry of tools) {
|
|
1750
|
+
const toolPath = path6.join(rootDir, entry.sourceFile);
|
|
1751
|
+
const toolSource = await readFile2(toolPath, "utf8");
|
|
1752
|
+
diagnostics.push(...diagnoseToolSource(rootDir, entry, toolSource, hasAuthConfig));
|
|
1753
|
+
if (entry.widget) {
|
|
1754
|
+
const widgetPath = path6.join(rootDir, entry.widget.sourceFile);
|
|
1755
|
+
const widgetSource = await readFile2(widgetPath, "utf8");
|
|
1756
|
+
diagnostics.push(...diagnoseWidgetSource(rootDir, entry, widgetSource, hasOpenAiAppsSdkUi));
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
for (const entry of resources) {
|
|
1760
|
+
const resourcePath = path6.join(rootDir, entry.sourceFile);
|
|
1761
|
+
const resourceSource = await readFile2(resourcePath, "utf8");
|
|
1762
|
+
diagnostics.push(...diagnoseResourceSource(entry, resourceSource));
|
|
1763
|
+
}
|
|
1764
|
+
for (const entry of prompts) {
|
|
1765
|
+
const promptPath = path6.join(rootDir, entry.sourceFile);
|
|
1766
|
+
const promptSource = await readFile2(promptPath, "utf8");
|
|
1767
|
+
diagnostics.push(...diagnosePromptSource(entry, promptSource));
|
|
1768
|
+
}
|
|
1769
|
+
if (config) {
|
|
1770
|
+
diagnostics.push(...diagnoseCapabilities(config, resources));
|
|
1771
|
+
}
|
|
1772
|
+
return diagnostics;
|
|
1773
|
+
}
|
|
1774
|
+
function formatDiagnostic(diagnostic) {
|
|
1775
|
+
const location = `${diagnostic.filePath}:${diagnostic.line}:${diagnostic.column}`;
|
|
1776
|
+
const hint = diagnostic.hint ? `
|
|
1777
|
+
hint: ${diagnostic.hint}` : "";
|
|
1778
|
+
return `${location} - ${diagnostic.severity} ${diagnostic.code}: ${diagnostic.message}${hint}`;
|
|
1779
|
+
}
|
|
1780
|
+
function diagnoseToolSource(rootDir, entry, source, hasAuthConfig) {
|
|
1781
|
+
const diagnostics = [];
|
|
1782
|
+
const toolLocation = locate(source, "tool({");
|
|
1783
|
+
if (!entry.description.trim().startsWith("Use this when")) {
|
|
1784
|
+
diagnostics.push({
|
|
1785
|
+
severity: "warning",
|
|
1786
|
+
code: "SIDECAR_METADATA_DESCRIPTION",
|
|
1787
|
+
message: `Tool "${entry.id}" description should start with "Use this when..." for better model routing.`,
|
|
1788
|
+
filePath: entry.sourceFile,
|
|
1789
|
+
...toolLocation,
|
|
1790
|
+
hint: "Keep the first sentence specific and include explicit disallowed cases when useful."
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
for (const key of ["readOnlyHint", "destructiveHint", "openWorldHint"]) {
|
|
1794
|
+
if (!(key in (entry.annotations ?? {}))) {
|
|
1795
|
+
diagnostics.push({
|
|
1796
|
+
severity: "warning",
|
|
1797
|
+
code: "SIDECAR_TOOL_ANNOTATION",
|
|
1798
|
+
message: `Tool "${entry.id}" should explicitly declare annotations.${key}.`,
|
|
1799
|
+
filePath: entry.sourceFile,
|
|
1800
|
+
...toolLocation,
|
|
1801
|
+
hint: "Hosts use these hints to frame approval and safety UX; Sidecar applies conservative defaults only as a fallback."
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
if (/auth\s*:/.test(source) && !hasAuthConfig && !isIgnored(source, "SIDECAR_AUTH_MISSING_CONFIG")) {
|
|
1806
|
+
diagnostics.push({
|
|
1807
|
+
severity: "warning",
|
|
1808
|
+
code: "SIDECAR_AUTH_MISSING_CONFIG",
|
|
1809
|
+
message: `Tool "${entry.id}" declares auth but the project has no reserved auth.ts file.`,
|
|
1810
|
+
filePath: entry.sourceFile,
|
|
1811
|
+
...locate(source, "auth"),
|
|
1812
|
+
hint: "Add auth.ts at the project root or mark the tool auth policy as public."
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
if (!returnsToolResult(source) && !isIgnored(source, "SIDECAR_TOOL_RESULT_REQUIRED")) {
|
|
1816
|
+
diagnostics.push({
|
|
1817
|
+
severity: "warning",
|
|
1818
|
+
code: "SIDECAR_TOOL_RESULT_REQUIRED",
|
|
1819
|
+
message: `Tool "${entry.id}" should return toolResult(...) from execute.`,
|
|
1820
|
+
filePath: entry.sourceFile,
|
|
1821
|
+
...locate(source, "execute"),
|
|
1822
|
+
hint: "The runtime rejects plain objects so content, structuredContent, and meta always map cleanly to MCP result channels."
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
if (/["']openai\//.test(source) && !/@sidecar-ai\/openai/.test(source) && !isIgnored(source, "SIDECAR_OPENAI_MAGIC_META")) {
|
|
1826
|
+
diagnostics.push({
|
|
1827
|
+
severity: "warning",
|
|
1828
|
+
code: "SIDECAR_OPENAI_MAGIC_META",
|
|
1829
|
+
message: `Tool "${entry.id}" uses raw ChatGPT metadata strings.`,
|
|
1830
|
+
filePath: entry.sourceFile,
|
|
1831
|
+
...locate(source, "openai/"),
|
|
1832
|
+
hint: "Use @sidecar-ai/openai helpers or the typed hosts.chatgpt field so platform metadata stays typed."
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
for (const parameterName of missingParameterDescriptions(entry.inputSchema)) {
|
|
1836
|
+
diagnostics.push({
|
|
1837
|
+
severity: "warning",
|
|
1838
|
+
code: "SIDECAR_PARAM_DESCRIPTION",
|
|
1839
|
+
message: `Parameter "${parameterName}" on tool "${entry.id}" is missing a JSDoc/schema description.`,
|
|
1840
|
+
filePath: entry.sourceFile,
|
|
1841
|
+
...locate(source, parameterName),
|
|
1842
|
+
hint: "Add a JSDoc comment to the TypeScript field or provide a schema description."
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
if (looksLikeKnowledgeTool(entry) && !hasCompanyKnowledgeShape(entry.inputSchema) && !isIgnored(source, "SIDECAR_COMPANY_KNOWLEDGE_SHAPE")) {
|
|
1846
|
+
diagnostics.push({
|
|
1847
|
+
severity: "warning",
|
|
1848
|
+
code: "SIDECAR_COMPANY_KNOWLEDGE_SHAPE",
|
|
1849
|
+
message: `Tool "${entry.id}" looks like a search/fetch knowledge tool but does not use the expected simple input shape.`,
|
|
1850
|
+
filePath: entry.sourceFile,
|
|
1851
|
+
...toolLocation,
|
|
1852
|
+
hint: "Use a string query for search tools or a string id/url for fetch tools, or add // sidecar-ignore SIDECAR_COMPANY_KNOWLEDGE_SHAPE."
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
return diagnostics.filter((diagnostic) => !isIgnored(source, diagnostic.code));
|
|
1856
|
+
}
|
|
1857
|
+
function diagnoseWidgetSource(_rootDir, entry, source, hasOpenAiAppsSdkUi) {
|
|
1858
|
+
const diagnostics = [];
|
|
1859
|
+
const widgetFile = entry.widget?.sourceFile;
|
|
1860
|
+
if (!widgetFile) {
|
|
1861
|
+
return diagnostics;
|
|
1862
|
+
}
|
|
1863
|
+
if (/window\s*\.\s*openai|\(\s*window\s+as\s+[^)]*\)\s*\.\s*openai/.test(source) && !isIgnored(source, "SIDECAR_OPENAI_RAW_BRIDGE")) {
|
|
1864
|
+
diagnostics.push({
|
|
1865
|
+
severity: "warning",
|
|
1866
|
+
code: "SIDECAR_OPENAI_RAW_BRIDGE",
|
|
1867
|
+
message: `Widget for "${entry.id}" reads window.openai directly.`,
|
|
1868
|
+
filePath: widgetFile,
|
|
1869
|
+
...locate(source, "openai"),
|
|
1870
|
+
hint: "Use @sidecar-ai/openai runtime helpers in ChatGPT-only widgets, or @sidecar-ai/native/@sidecar-ai/client for portable behavior."
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
if (entry.widget?.variant !== "openai" && /@sidecar-ai\/openai/.test(source) && !/@sidecar-ai\/native/.test(source) && !isIgnored(source, "SIDECAR_OPENAI_FALLBACK")) {
|
|
1874
|
+
diagnostics.push({
|
|
1875
|
+
severity: "warning",
|
|
1876
|
+
code: "SIDECAR_OPENAI_FALLBACK",
|
|
1877
|
+
message: `Widget for "${entry.id}" imports @sidecar-ai/openai without a portable fallback.`,
|
|
1878
|
+
filePath: widgetFile,
|
|
1879
|
+
...locate(source, "@sidecar-ai/openai"),
|
|
1880
|
+
hint: "Prefer @sidecar-ai/native for user-facing behavior and reserve @sidecar-ai/openai for typed metadata or explicit ChatGPT-only code paths."
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
if (entry.widget?.variant !== "openai" && /@sidecar-ai\/openai\/components/.test(source) && !isIgnored(source, "SIDECAR_OPENAI_COMPONENT_CROSS_HOST")) {
|
|
1884
|
+
diagnostics.push({
|
|
1885
|
+
severity: "warning",
|
|
1886
|
+
code: "SIDECAR_OPENAI_COMPONENT_CROSS_HOST",
|
|
1887
|
+
message: `Widget for "${entry.id}" imports ChatGPT-only components.`,
|
|
1888
|
+
filePath: widgetFile,
|
|
1889
|
+
...locate(source, "@sidecar-ai/openai/components"),
|
|
1890
|
+
hint: "Use @sidecar-ai/native for portable primitives. Keep @sidecar-ai/openai/components for widgets intentionally targeted to ChatGPT."
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
if (/@sidecar-ai\/openai\/components/.test(source) && !hasOpenAiAppsSdkUi && !isIgnored(source, "SIDECAR_OPENAI_UI_SDK_MISSING")) {
|
|
1894
|
+
diagnostics.push({
|
|
1895
|
+
severity: "error",
|
|
1896
|
+
code: "SIDECAR_OPENAI_UI_SDK_MISSING",
|
|
1897
|
+
message: `Widget for "${entry.id}" imports @sidecar-ai/openai/components but @openai/apps-sdk-ui is not installed.`,
|
|
1898
|
+
filePath: widgetFile,
|
|
1899
|
+
...locate(source, "@sidecar-ai/openai/components"),
|
|
1900
|
+
hint: "Install it with: npm install @openai/apps-sdk-ui"
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
if (entry.widget?.variant !== "anthropic" && /@sidecar-ai\/anthropic\/components/.test(source) && !isIgnored(source, "SIDECAR_ANTHROPIC_COMPONENT_CROSS_HOST")) {
|
|
1904
|
+
diagnostics.push({
|
|
1905
|
+
severity: "warning",
|
|
1906
|
+
code: "SIDECAR_ANTHROPIC_COMPONENT_CROSS_HOST",
|
|
1907
|
+
message: `Widget for "${entry.id}" imports Claude-only components.`,
|
|
1908
|
+
filePath: widgetFile,
|
|
1909
|
+
...locate(source, "@sidecar-ai/anthropic/components"),
|
|
1910
|
+
hint: "Use @sidecar-ai/native for portable primitives. Keep @sidecar-ai/anthropic/components for widgets intentionally targeted to Claude."
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
if (/\bPopover\b/.test(source) && /@sidecar-ai\/native/.test(source) && !isIgnored(source, "SIDECAR_NATIVE_NON_PORTABLE_COMPONENT")) {
|
|
1914
|
+
diagnostics.push({
|
|
1915
|
+
severity: "warning",
|
|
1916
|
+
code: "SIDECAR_NATIVE_NON_PORTABLE_COMPONENT",
|
|
1917
|
+
message: `Widget for "${entry.id}" appears to expect a native Popover.`,
|
|
1918
|
+
filePath: widgetFile,
|
|
1919
|
+
...locate(source, "Popover"),
|
|
1920
|
+
hint: "Popover is host-specific because Claude inline apps discourage clipped overlay UI. Use @sidecar-ai/openai/components for ChatGPT-only popovers."
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
return diagnostics.filter((diagnostic) => !isIgnored(source, diagnostic.code));
|
|
1924
|
+
}
|
|
1925
|
+
function diagnoseResourceSource(entry, source) {
|
|
1926
|
+
const diagnostics = [];
|
|
1927
|
+
if (!returnsResourceResult(source) && !isIgnored(source, "SIDECAR_RESOURCE_RESULT_REQUIRED")) {
|
|
1928
|
+
diagnostics.push({
|
|
1929
|
+
severity: "warning",
|
|
1930
|
+
code: "SIDECAR_RESOURCE_RESULT_REQUIRED",
|
|
1931
|
+
message: `Resource "${entry.uri}" should return resourceResult(...) from read.`,
|
|
1932
|
+
filePath: entry.sourceFile,
|
|
1933
|
+
...locate(source, "read"),
|
|
1934
|
+
hint: "The runtime rejects plain objects so text/blob content maps cleanly to MCP resource contents."
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
return diagnostics.filter((diagnostic) => !isIgnored(source, diagnostic.code));
|
|
1938
|
+
}
|
|
1939
|
+
function diagnosePromptSource(entry, source) {
|
|
1940
|
+
const diagnostics = [];
|
|
1941
|
+
if (!source.includes("run") && !isIgnored(source, "SIDECAR_PROMPT_RUN_REQUIRED")) {
|
|
1942
|
+
diagnostics.push({
|
|
1943
|
+
severity: "warning",
|
|
1944
|
+
code: "SIDECAR_PROMPT_RUN_REQUIRED",
|
|
1945
|
+
message: `Prompt "${entry.name}" should include a run method.`,
|
|
1946
|
+
filePath: entry.sourceFile,
|
|
1947
|
+
...locate(source, "prompt({"),
|
|
1948
|
+
hint: "Prompt run methods return a string for the common case or MCP prompt messages for advanced cases."
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
return diagnostics.filter((diagnostic) => !isIgnored(source, diagnostic.code));
|
|
1952
|
+
}
|
|
1953
|
+
function diagnoseCapabilities(config, resources) {
|
|
1954
|
+
const diagnostics = [];
|
|
1955
|
+
if (!config.resources.subscribe && resources.some((entry) => entry.subscribe)) {
|
|
1956
|
+
const entry = resources.find((resource) => resource.subscribe);
|
|
1957
|
+
if (entry) {
|
|
1958
|
+
diagnostics.push({
|
|
1959
|
+
severity: "error",
|
|
1960
|
+
code: "SIDECAR_RESOURCE_SUBSCRIBE_DISABLED",
|
|
1961
|
+
message: `Resource "${entry.uri}" sets subscribe: true but sidecar.config.ts does not enable resources.subscribe.`,
|
|
1962
|
+
filePath: entry.sourceFile,
|
|
1963
|
+
line: 1,
|
|
1964
|
+
column: 1,
|
|
1965
|
+
hint: "Add resources: { subscribe: true } to sidecar.config.ts or remove subscribe: true from this resource."
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return diagnostics;
|
|
1970
|
+
}
|
|
1971
|
+
function returnsToolResult(source) {
|
|
1972
|
+
return /\breturn\s+toolResult(?:\.\w+)?\s*\(/.test(source);
|
|
1973
|
+
}
|
|
1974
|
+
function returnsResourceResult(source) {
|
|
1975
|
+
return /\breturn\s+resourceResult(?:\.\w+)?\s*\(/.test(source);
|
|
1976
|
+
}
|
|
1977
|
+
function missingParameterDescriptions(schema) {
|
|
1978
|
+
const properties = schema.properties ?? {};
|
|
1979
|
+
return Object.entries(properties).filter(([, value]) => !value.description?.trim()).map(([key]) => key);
|
|
1980
|
+
}
|
|
1981
|
+
function looksLikeKnowledgeTool(entry) {
|
|
1982
|
+
const text = `${entry.id} ${entry.name}`.toLowerCase();
|
|
1983
|
+
return /\b(search|fetch)\b/.test(text) || /(^|[._-])(search|fetch)([._-]|$)/.test(text);
|
|
1984
|
+
}
|
|
1985
|
+
function hasCompanyKnowledgeShape(schema) {
|
|
1986
|
+
const properties = schema.properties ?? {};
|
|
1987
|
+
const query = properties.query;
|
|
1988
|
+
const url = properties.url;
|
|
1989
|
+
const id = properties.id;
|
|
1990
|
+
return query?.type === "string" || url?.type === "string" || id?.type === "string";
|
|
1991
|
+
}
|
|
1992
|
+
function canResolveOpenAiAppsSdkUi(rootDir) {
|
|
1993
|
+
try {
|
|
1994
|
+
createRequire(path6.join(rootDir, "package.json")).resolve("@openai/apps-sdk-ui/css");
|
|
1995
|
+
return true;
|
|
1996
|
+
} catch {
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
function locate(source, needle) {
|
|
2001
|
+
const index = Math.max(0, source.indexOf(needle));
|
|
2002
|
+
const prefix = source.slice(0, index);
|
|
2003
|
+
const lines = prefix.split("\n");
|
|
2004
|
+
return {
|
|
2005
|
+
line: lines.length,
|
|
2006
|
+
column: (lines.at(-1)?.length ?? 0) + 1
|
|
2007
|
+
};
|
|
2008
|
+
}
|
|
2009
|
+
function isIgnored(source, code) {
|
|
2010
|
+
return source.includes(`sidecar-ignore ${code}`) || source.includes(`sidecar-ignore all`);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// packages/compiler/src/generated.ts
|
|
2014
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
2015
|
+
import path7 from "path";
|
|
2016
|
+
async function writeGeneratedTypes(rootDir, tools) {
|
|
2017
|
+
const generatedDir = path7.join(rootDir, ".sidecar", "generated");
|
|
2018
|
+
await mkdir2(generatedDir, { recursive: true });
|
|
2019
|
+
const appCallableTools = tools.filter(isAppCallable);
|
|
2020
|
+
const methodNames = uniqueMethodNames(appCallableTools.map((entry) => toIdentifier(entry.id)));
|
|
2021
|
+
const imports = appCallableTools.map(
|
|
2022
|
+
(entry, index) => `import type tool${index} from ${JSON.stringify(toImportSpecifier(generatedDir, path7.join(rootDir, entry.sourceFile), { extension: "js" }))};`
|
|
2023
|
+
).join("\n");
|
|
2024
|
+
const ids = appCallableTools.map((entry, index) => ` ${methodNames[index]}: ${JSON.stringify(entry.id)}`).join(",\n");
|
|
2025
|
+
const toolTypes = appCallableTools.map(
|
|
2026
|
+
(entry, index) => ` ${methodNames[index]}(params: ToolParams<typeof tool${index}>): Promise<ToolOutput<typeof tool${index}>>;`
|
|
2027
|
+
).join("\n");
|
|
2028
|
+
await writeFile2(
|
|
2029
|
+
path7.join(generatedDir, "tools.ts"),
|
|
2030
|
+
`/**
|
|
2031
|
+
* Generated Sidecar widget tool client.
|
|
2032
|
+
*
|
|
2033
|
+
* Do not edit this file directly; it is regenerated by sidecar build and
|
|
2034
|
+
* sidecar dev.
|
|
2035
|
+
*/
|
|
2036
|
+
${imports}
|
|
2037
|
+
import { createToolClient } from "@sidecar-ai/client";
|
|
2038
|
+
import type { ToolResult } from "@sidecar-ai/core";
|
|
2039
|
+
|
|
2040
|
+
type ExecuteOf<T> = T extends { execute: infer Execute } ? Execute : never;
|
|
2041
|
+
type ToolParams<T> = ExecuteOf<T> extends (params: infer Params, ...args: any[]) => unknown ? Params : never;
|
|
2042
|
+
type RawToolOutput<T> = Awaited<ReturnType<Extract<ExecuteOf<T>, (...args: any[]) => unknown>>>;
|
|
2043
|
+
type ToolOutput<T> = RawToolOutput<T> extends ToolResult<infer Structured, any> ? Structured : never;
|
|
2044
|
+
|
|
2045
|
+
/** Machine names keyed by widget-friendly method names. */
|
|
2046
|
+
export const toolIds = {
|
|
2047
|
+
${ids}
|
|
2048
|
+
} as const;
|
|
2049
|
+
|
|
2050
|
+
/** Typed callable tools available to widgets. */
|
|
2051
|
+
export type WidgetTools = {
|
|
2052
|
+
${toolTypes}
|
|
2053
|
+
};
|
|
2054
|
+
|
|
2055
|
+
/** Default widget tool client backed by the host bridge. */
|
|
2056
|
+
export const tools = createToolClient<WidgetTools>(toolIds);
|
|
2057
|
+
`
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
function isAppCallable(entry) {
|
|
2061
|
+
const ui = entry.descriptor._meta?.ui;
|
|
2062
|
+
if (!ui || typeof ui !== "object" || Array.isArray(ui)) {
|
|
2063
|
+
return true;
|
|
2064
|
+
}
|
|
2065
|
+
const visibility = ui.visibility;
|
|
2066
|
+
return !Array.isArray(visibility) || visibility.includes("app");
|
|
2067
|
+
}
|
|
2068
|
+
function uniqueMethodNames(names) {
|
|
2069
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2070
|
+
return names.map((name) => {
|
|
2071
|
+
const count = seen.get(name) ?? 0;
|
|
2072
|
+
seen.set(name, count + 1);
|
|
2073
|
+
return count === 0 ? name : `${name}${count + 1}`;
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// packages/compiler/src/plugins.ts
|
|
2078
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "fs/promises";
|
|
2079
|
+
import path14 from "path";
|
|
2080
|
+
|
|
2081
|
+
// packages/compiler/src/identity.ts
|
|
2082
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2083
|
+
import path8 from "path";
|
|
2084
|
+
async function loadProjectIdentity(rootDir) {
|
|
2085
|
+
const packageJson = await readOptionalJson(path8.join(rootDir, "package.json"));
|
|
2086
|
+
const configText = await readOptionalText(
|
|
2087
|
+
path8.join(rootDir, "sidecar.config.ts")
|
|
2088
|
+
);
|
|
2089
|
+
const name = readConfigString(configText, "name") ?? packageJson?.name ?? path8.basename(rootDir);
|
|
2090
|
+
const version = readConfigString(configText, "version") ?? packageJson?.version ?? "0.0.0-dev";
|
|
2091
|
+
const description = readConfigString(configText, "description") ?? packageJson?.description ?? `${name} Sidecar app.`;
|
|
2092
|
+
return {
|
|
2093
|
+
name,
|
|
2094
|
+
slug: toMachineName(name).replaceAll("_", "-"),
|
|
2095
|
+
version,
|
|
2096
|
+
description
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
async function readOptionalJson(filePath) {
|
|
2100
|
+
if (!existsSyncSafe(filePath)) {
|
|
2101
|
+
return void 0;
|
|
2102
|
+
}
|
|
2103
|
+
return JSON.parse(await readFile3(filePath, "utf8"));
|
|
2104
|
+
}
|
|
2105
|
+
async function readOptionalText(filePath) {
|
|
2106
|
+
if (!existsSyncSafe(filePath)) {
|
|
2107
|
+
return void 0;
|
|
2108
|
+
}
|
|
2109
|
+
return readFile3(filePath, "utf8");
|
|
2110
|
+
}
|
|
2111
|
+
function readConfigString(configText, key) {
|
|
2112
|
+
if (!configText) {
|
|
2113
|
+
return void 0;
|
|
2114
|
+
}
|
|
2115
|
+
const match = configText.match(
|
|
2116
|
+
new RegExp(`${key}\\s*:\\s*["'\`]([^"'\`]+)["'\`]`)
|
|
2117
|
+
);
|
|
2118
|
+
return match?.[1];
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// packages/compiler/src/plugin-components/agents.ts
|
|
2122
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
2123
|
+
import path9 from "path";
|
|
2124
|
+
async function emitClaudeAgents(rootDir, destination) {
|
|
2125
|
+
const source = path9.join(rootDir, "agents");
|
|
2126
|
+
if (!existsSyncSafe(source)) {
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
const entries = await readdir2(source, { withFileTypes: true });
|
|
2130
|
+
const agentDirs = entries.filter(
|
|
2131
|
+
(entry) => entry.isDirectory() && existsSyncSafe(path9.join(source, entry.name, "agent.ts"))
|
|
2132
|
+
);
|
|
2133
|
+
if (!agentDirs.length) {
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
await mkdir3(destination, { recursive: true });
|
|
2137
|
+
for (const entry of agentDirs) {
|
|
2138
|
+
const sourceText = await readFile4(path9.join(source, entry.name, "agent.ts"), "utf8");
|
|
2139
|
+
const markdown = parseClaudeAgent(
|
|
2140
|
+
sourceText,
|
|
2141
|
+
entry.name
|
|
2142
|
+
);
|
|
2143
|
+
const agentName = readObjectString(sourceText, "name") ?? entry.name;
|
|
2144
|
+
await writeFile3(path9.join(destination, `${safeFileStem(agentName)}.md`), markdown);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
function parseClaudeAgent(source, fallbackName) {
|
|
2148
|
+
const name = readObjectString(source, "name") ?? fallbackName;
|
|
2149
|
+
const description = readObjectString(source, "description") ?? `${name} agent.`;
|
|
2150
|
+
const prompt = readObjectString(source, "prompt") ?? "";
|
|
2151
|
+
const tools = readObjectStringArray(source, "tools");
|
|
2152
|
+
const disallowedTools = readObjectStringArray(source, "disallowedTools");
|
|
2153
|
+
const model = readObjectString(source, "model");
|
|
2154
|
+
const color = readObjectString(source, "color");
|
|
2155
|
+
const frontmatter = stripUndefined2({
|
|
2156
|
+
name,
|
|
2157
|
+
description,
|
|
2158
|
+
model,
|
|
2159
|
+
color,
|
|
2160
|
+
tools: tools?.join(", "),
|
|
2161
|
+
"disallowed-tools": disallowedTools?.join(", ")
|
|
2162
|
+
});
|
|
2163
|
+
return `---
|
|
2164
|
+
${Object.entries(frontmatter).map(([key, value]) => `${key}: ${yamlScalar(value)}`).join("\n")}
|
|
2165
|
+
---
|
|
2166
|
+
|
|
2167
|
+
${prompt.trim()}
|
|
2168
|
+
`;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// packages/compiler/src/plugin-components/commands.ts
|
|
2172
|
+
import { cp, lstat, mkdir as mkdir4, readdir as readdir3, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
|
|
2173
|
+
import path10 from "path";
|
|
2174
|
+
async function copyCommands(rootDir, destination) {
|
|
2175
|
+
const source = path10.join(rootDir, "commands");
|
|
2176
|
+
if (!existsSyncSafe(source)) {
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
await mkdir4(destination, { recursive: true });
|
|
2180
|
+
const entries = await readdir3(source, { withFileTypes: true });
|
|
2181
|
+
for (const entry of entries) {
|
|
2182
|
+
const sourcePath = path10.join(source, entry.name);
|
|
2183
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2184
|
+
if (await safeCommandCopyFilter(sourcePath)) {
|
|
2185
|
+
await cp(sourcePath, path10.join(destination, safeFileStem(entry.name.replace(/\.md$/, "")) + ".md"));
|
|
2186
|
+
}
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
if (!entry.isDirectory()) {
|
|
2190
|
+
continue;
|
|
2191
|
+
}
|
|
2192
|
+
const dynamicCommand = path10.join(sourcePath, "command.ts");
|
|
2193
|
+
if (existsSyncSafe(dynamicCommand)) {
|
|
2194
|
+
const sourceText = await readFile5(dynamicCommand, "utf8");
|
|
2195
|
+
const markdown = parseDynamicCommand(sourceText, entry.name);
|
|
2196
|
+
const commandName = readObjectString(sourceText, "name") ?? entry.name;
|
|
2197
|
+
await writeFile4(path10.join(destination, `${safeFileStem(commandName)}.md`), markdown);
|
|
2198
|
+
continue;
|
|
2199
|
+
}
|
|
2200
|
+
await cp(sourcePath, path10.join(destination, entry.name), {
|
|
2201
|
+
recursive: true,
|
|
2202
|
+
filter: safeCommandCopyFilter
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
async function safeCommandCopyFilter(sourcePath) {
|
|
2207
|
+
const basename = path10.basename(sourcePath);
|
|
2208
|
+
if (basename === ".env" || basename.startsWith(".env.") || basename === "node_modules" || basename === ".git") {
|
|
2209
|
+
return false;
|
|
2210
|
+
}
|
|
2211
|
+
const stat = await lstat(sourcePath);
|
|
2212
|
+
return !stat.isSymbolicLink();
|
|
2213
|
+
}
|
|
2214
|
+
function parseDynamicCommand(source, fallbackName) {
|
|
2215
|
+
const name = readObjectString(source, "name") ?? fallbackName;
|
|
2216
|
+
const description = readObjectString(source, "description");
|
|
2217
|
+
const prompt = readObjectString(source, "prompt") ?? "";
|
|
2218
|
+
const argumentHint = readObjectString(source, "argumentHint");
|
|
2219
|
+
const allowedTools = readObjectStringArray(source, "allowedTools");
|
|
2220
|
+
const frontmatter = stripUndefined2({
|
|
2221
|
+
description,
|
|
2222
|
+
"argument-hint": argumentHint,
|
|
2223
|
+
"allowed-tools": allowedTools?.join(", ")
|
|
2224
|
+
});
|
|
2225
|
+
const header = Object.keys(frontmatter).length ? `---
|
|
2226
|
+
${Object.entries(frontmatter).map(([key, value]) => `${key}: ${yamlScalar(value)}`).join("\n")}
|
|
2227
|
+
---
|
|
2228
|
+
|
|
2229
|
+
` : "";
|
|
2230
|
+
return `${header}${prompt.trim() || `# ${name}`}
|
|
2231
|
+
`;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
// packages/compiler/src/plugin-components/hooks.ts
|
|
2235
|
+
import { mkdir as mkdir5, readdir as readdir4, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
|
|
2236
|
+
import path11 from "path";
|
|
2237
|
+
import {
|
|
2238
|
+
Node as Node6,
|
|
2239
|
+
Project as Project2,
|
|
2240
|
+
SyntaxKind as SyntaxKind4
|
|
2241
|
+
} from "ts-morph";
|
|
2242
|
+
async function copyHooks(rootDir, destination) {
|
|
2243
|
+
const source = path11.join(rootDir, "hooks");
|
|
2244
|
+
if (!existsSyncSafe(source)) {
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
const entries = await readdir4(source, { withFileTypes: true });
|
|
2248
|
+
const hookDirs = entries.filter(
|
|
2249
|
+
(entry) => entry.isDirectory() && existsSyncSafe(path11.join(source, entry.name, "hook.ts"))
|
|
2250
|
+
);
|
|
2251
|
+
if (!hookDirs.length) {
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
const config = {};
|
|
2255
|
+
for (const entry of hookDirs) {
|
|
2256
|
+
const filePath = path11.join(source, entry.name, "hook.ts");
|
|
2257
|
+
mergeHooks(config, parseHookFile(await readFile6(filePath, "utf8"), entry.name));
|
|
2258
|
+
}
|
|
2259
|
+
await mkdir5(destination, { recursive: true });
|
|
2260
|
+
await writeFile5(
|
|
2261
|
+
path11.join(destination, "hooks.json"),
|
|
2262
|
+
`${JSON.stringify(config, null, 2)}
|
|
2263
|
+
`
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
function parseHookFile(source, fallbackName) {
|
|
2267
|
+
const project = new Project2({ useInMemoryFileSystem: true });
|
|
2268
|
+
const sourceFile = project.createSourceFile(`${fallbackName}.ts`, source);
|
|
2269
|
+
const call = resolveDefaultExportCall(sourceFile, "hook") ?? resolveDefaultExportCall(sourceFile, "hooks");
|
|
2270
|
+
if (!call) {
|
|
2271
|
+
throw new Error(`hooks/${fallbackName}/hook.ts must default-export hook({ ... }) or hooks({ ... }).`);
|
|
2272
|
+
}
|
|
2273
|
+
const definition = unwrapExpression(call.getArguments()[0]);
|
|
2274
|
+
if (!definition || !Node6.isObjectLiteralExpression(definition)) {
|
|
2275
|
+
throw new Error(`hooks/${fallbackName}/hook.ts must default-export hook({ ... }) or hooks({ ... }).`);
|
|
2276
|
+
}
|
|
2277
|
+
const callee = call.getExpression().getText();
|
|
2278
|
+
if (callee === "hook" || callee.endsWith(".hook")) {
|
|
2279
|
+
const entry = parseHookDefinition(definition, fallbackName);
|
|
2280
|
+
return { [entry.event]: [stripUndefined2({ matcher: entry.matcher, hooks: entry.run })] };
|
|
2281
|
+
}
|
|
2282
|
+
if (callee === "hooks" || callee.endsWith(".hooks")) {
|
|
2283
|
+
return parseHooksDefinition(definition, fallbackName);
|
|
2284
|
+
}
|
|
2285
|
+
throw new Error(`hooks/${fallbackName}/hook.ts must default-export hook({ ... }) or hooks({ ... }).`);
|
|
2286
|
+
}
|
|
2287
|
+
function parseHookDefinition(definition, fallbackName) {
|
|
2288
|
+
const event = readStringProperty3(definition, "event");
|
|
2289
|
+
if (!event) {
|
|
2290
|
+
throw new Error(`hooks/${fallbackName}/hook.ts must declare an event string.`);
|
|
2291
|
+
}
|
|
2292
|
+
const hooks = readHookArray(definition, "run", fallbackName);
|
|
2293
|
+
if (!hooks.length) {
|
|
2294
|
+
throw new Error(`hooks/${fallbackName}/hook.ts must declare at least one run handler.`);
|
|
2295
|
+
}
|
|
2296
|
+
return {
|
|
2297
|
+
event,
|
|
2298
|
+
matcher: readStringProperty3(definition, "matcher"),
|
|
2299
|
+
run: hooks
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
function parseHooksDefinition(definition, fallbackName) {
|
|
2303
|
+
const config = {};
|
|
2304
|
+
for (const property of definition.getProperties()) {
|
|
2305
|
+
if (!Node6.isPropertyAssignment(property)) {
|
|
2306
|
+
continue;
|
|
2307
|
+
}
|
|
2308
|
+
const event = property.getName().replace(/^["']|["']$/g, "");
|
|
2309
|
+
const initializer = property.getInitializer();
|
|
2310
|
+
if (!initializer || !Node6.isArrayLiteralExpression(initializer)) {
|
|
2311
|
+
throw new Error(`hooks/${fallbackName}/hook.ts event "${event}" must be an array.`);
|
|
2312
|
+
}
|
|
2313
|
+
config[event] = initializer.getElements().map((entry) => {
|
|
2314
|
+
if (!Node6.isObjectLiteralExpression(entry)) {
|
|
2315
|
+
throw new Error(`hooks/${fallbackName}/hook.ts event "${event}" entries must be objects.`);
|
|
2316
|
+
}
|
|
2317
|
+
return stripUndefined2({
|
|
2318
|
+
matcher: readStringProperty3(entry, "matcher"),
|
|
2319
|
+
hooks: readHookArray(entry, "run", fallbackName)
|
|
2320
|
+
});
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
return config;
|
|
2324
|
+
}
|
|
2325
|
+
function readHookArray(definition, propertyName, fallbackName) {
|
|
2326
|
+
const property = definition.getProperty(propertyName);
|
|
2327
|
+
if (!property || !Node6.isPropertyAssignment(property)) {
|
|
2328
|
+
return [];
|
|
2329
|
+
}
|
|
2330
|
+
const initializer = property.getInitializer();
|
|
2331
|
+
if (!initializer || !Node6.isArrayLiteralExpression(initializer)) {
|
|
2332
|
+
throw new Error(`hooks/${fallbackName}/hook.ts ${propertyName} must be an array.`);
|
|
2333
|
+
}
|
|
2334
|
+
return parseHookHandlers(initializer, fallbackName);
|
|
2335
|
+
}
|
|
2336
|
+
function parseHookHandlers(handlers, fallbackName) {
|
|
2337
|
+
return handlers.getElements().map((handler) => parseHookHandler(handler, fallbackName));
|
|
2338
|
+
}
|
|
2339
|
+
function parseHookHandler(handler, fallbackName) {
|
|
2340
|
+
if (Node6.isObjectLiteralExpression(handler)) {
|
|
2341
|
+
const type = readStringProperty3(handler, "type");
|
|
2342
|
+
if (type !== "command" && type !== "http") {
|
|
2343
|
+
throw new Error(`hooks/${fallbackName}/hook.ts hook handlers need type "command" or "http".`);
|
|
2344
|
+
}
|
|
2345
|
+
return objectLiteralToRecord(handler);
|
|
2346
|
+
}
|
|
2347
|
+
if (Node6.isCallExpression(handler)) {
|
|
2348
|
+
return parseHookHandlerCall(handler, fallbackName);
|
|
2349
|
+
}
|
|
2350
|
+
throw new Error(`hooks/${fallbackName}/hook.ts hook handlers must be objects or helper calls.`);
|
|
2351
|
+
}
|
|
2352
|
+
function parseHookHandlerCall(handler, fallbackName) {
|
|
2353
|
+
const callee = handler.getExpression().getText();
|
|
2354
|
+
const [firstArg, optionsArg] = handler.getArguments();
|
|
2355
|
+
const first = firstArg ? stringFromExpression(firstArg) : void 0;
|
|
2356
|
+
if (!first) {
|
|
2357
|
+
throw new Error(`hooks/${fallbackName}/hook.ts hook helper calls require a string first argument.`);
|
|
2358
|
+
}
|
|
2359
|
+
const options = optionsArg && Node6.isObjectLiteralExpression(optionsArg) ? objectLiteralToRecord(optionsArg) : {};
|
|
2360
|
+
if (callee.endsWith("commandHook")) {
|
|
2361
|
+
return stripUndefined2({
|
|
2362
|
+
type: "command",
|
|
2363
|
+
command: first,
|
|
2364
|
+
...options
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
if (callee.endsWith("httpHook")) {
|
|
2368
|
+
return stripUndefined2({
|
|
2369
|
+
type: "http",
|
|
2370
|
+
url: first,
|
|
2371
|
+
...options
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
throw new Error(`hooks/${fallbackName}/hook.ts uses an unsupported hook helper "${callee}".`);
|
|
2375
|
+
}
|
|
2376
|
+
function objectLiteralToRecord(object) {
|
|
2377
|
+
const record = {};
|
|
2378
|
+
for (const property of object.getProperties()) {
|
|
2379
|
+
if (!Node6.isPropertyAssignment(property)) {
|
|
2380
|
+
continue;
|
|
2381
|
+
}
|
|
2382
|
+
const key = property.getName().replace(/^["']|["']$/g, "");
|
|
2383
|
+
const initializer = property.getInitializer();
|
|
2384
|
+
if (!initializer) {
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2387
|
+
record[key] = valueFromExpression(initializer);
|
|
2388
|
+
}
|
|
2389
|
+
return stripUndefined2(record);
|
|
2390
|
+
}
|
|
2391
|
+
function readStringProperty3(object, propertyName) {
|
|
2392
|
+
const property = object.getProperty(propertyName);
|
|
2393
|
+
if (!property || !Node6.isPropertyAssignment(property)) {
|
|
2394
|
+
return void 0;
|
|
2395
|
+
}
|
|
2396
|
+
const initializer = property.getInitializer();
|
|
2397
|
+
return initializer ? stringFromExpression(initializer) : void 0;
|
|
2398
|
+
}
|
|
2399
|
+
function valueFromExpression(expression) {
|
|
2400
|
+
const string = stringFromExpression(expression);
|
|
2401
|
+
if (string !== void 0) {
|
|
2402
|
+
return string;
|
|
2403
|
+
}
|
|
2404
|
+
if (expression.getKind() === SyntaxKind4.TrueKeyword) {
|
|
2405
|
+
return true;
|
|
2406
|
+
}
|
|
2407
|
+
if (expression.getKind() === SyntaxKind4.FalseKeyword) {
|
|
2408
|
+
return false;
|
|
2409
|
+
}
|
|
2410
|
+
if (Node6.isNumericLiteral(expression)) {
|
|
2411
|
+
return Number(expression.getLiteralText());
|
|
2412
|
+
}
|
|
2413
|
+
if (Node6.isArrayLiteralExpression(expression)) {
|
|
2414
|
+
return expression.getElements().map(valueFromExpression);
|
|
2415
|
+
}
|
|
2416
|
+
if (Node6.isObjectLiteralExpression(expression)) {
|
|
2417
|
+
return objectLiteralToRecord(expression);
|
|
2418
|
+
}
|
|
2419
|
+
return void 0;
|
|
2420
|
+
}
|
|
2421
|
+
function stringFromExpression(expression) {
|
|
2422
|
+
if (Node6.isStringLiteral(expression) || Node6.isNoSubstitutionTemplateLiteral(expression)) {
|
|
2423
|
+
return expression.getLiteralText();
|
|
2424
|
+
}
|
|
2425
|
+
return void 0;
|
|
2426
|
+
}
|
|
2427
|
+
function mergeHooks(target, source) {
|
|
2428
|
+
for (const [event, matchers] of Object.entries(source)) {
|
|
2429
|
+
target[event] = [...target[event] ?? [], ...matchers];
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
// packages/compiler/src/plugin-components/passthrough.ts
|
|
2434
|
+
import { cp as cp2, lstat as lstat2 } from "fs/promises";
|
|
2435
|
+
import path12 from "path";
|
|
2436
|
+
async function copyClaudePassthroughDirectories(rootDir, pluginDir) {
|
|
2437
|
+
for (const directory of [
|
|
2438
|
+
"assets",
|
|
2439
|
+
"bin",
|
|
2440
|
+
"monitors",
|
|
2441
|
+
"output-styles",
|
|
2442
|
+
"themes"
|
|
2443
|
+
]) {
|
|
2444
|
+
const source = path12.join(rootDir, directory);
|
|
2445
|
+
if (!existsSyncSafe(source)) {
|
|
2446
|
+
continue;
|
|
2447
|
+
}
|
|
2448
|
+
await cp2(source, path12.join(pluginDir, directory), {
|
|
2449
|
+
recursive: true,
|
|
2450
|
+
filter: safePluginCopyFilter
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
async function safePluginCopyFilter(sourcePath) {
|
|
2455
|
+
const basename = path12.basename(sourcePath);
|
|
2456
|
+
if (basename === "node_modules" || basename === ".git" || basename === ".env" || basename.startsWith(".env.")) {
|
|
2457
|
+
return false;
|
|
2458
|
+
}
|
|
2459
|
+
const stat = await lstat2(sourcePath);
|
|
2460
|
+
return !stat.isSymbolicLink();
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// packages/compiler/src/plugin-components/skills.ts
|
|
2464
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2465
|
+
import { cp as cp3, lstat as lstat3, mkdir as mkdir6, readdir as readdir5, readFile as readFile7, writeFile as writeFile6 } from "fs/promises";
|
|
2466
|
+
import path13 from "path";
|
|
2467
|
+
async function copySkills(rootDir, destination) {
|
|
2468
|
+
const source = path13.join(rootDir, "skills");
|
|
2469
|
+
if (!existsSyncSafe(source)) {
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
await mkdir6(destination, { recursive: true });
|
|
2473
|
+
const entries = await readdir5(source, { withFileTypes: true });
|
|
2474
|
+
for (const entry of entries) {
|
|
2475
|
+
if (!entry.isDirectory()) {
|
|
2476
|
+
continue;
|
|
2477
|
+
}
|
|
2478
|
+
const sourceDir = path13.join(source, entry.name);
|
|
2479
|
+
const destinationDir = path13.join(destination, entry.name);
|
|
2480
|
+
const staticSkill = path13.join(sourceDir, "SKILL.md");
|
|
2481
|
+
const dynamicSkill = path13.join(sourceDir, "skill.ts");
|
|
2482
|
+
await mkdir6(destinationDir, { recursive: true });
|
|
2483
|
+
if (existsSync4(staticSkill)) {
|
|
2484
|
+
await cp3(sourceDir, destinationDir, {
|
|
2485
|
+
recursive: true,
|
|
2486
|
+
filter: async (sourcePath) => !sourcePath.endsWith(`${path13.sep}skill.ts`) && await safeSkillCopyFilter(sourcePath)
|
|
2487
|
+
});
|
|
2488
|
+
} else if (existsSync4(dynamicSkill)) {
|
|
2489
|
+
const generated = parseDynamicSkill(
|
|
2490
|
+
await readFile7(dynamicSkill, "utf8"),
|
|
2491
|
+
entry.name
|
|
2492
|
+
);
|
|
2493
|
+
await writeFile6(path13.join(destinationDir, "SKILL.md"), generated);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
async function safeSkillCopyFilter(sourcePath) {
|
|
2498
|
+
const basename = path13.basename(sourcePath);
|
|
2499
|
+
if (basename === ".env" || basename.startsWith(".env.") || basename === "node_modules" || basename === ".git") {
|
|
2500
|
+
return false;
|
|
2501
|
+
}
|
|
2502
|
+
const stat = await lstat3(sourcePath);
|
|
2503
|
+
return !stat.isSymbolicLink();
|
|
2504
|
+
}
|
|
2505
|
+
function parseDynamicSkill(source, fallbackName) {
|
|
2506
|
+
const name = readObjectString(source, "name") ?? fallbackName;
|
|
2507
|
+
const description = readObjectString(source, "description") ?? `${name} skill.`;
|
|
2508
|
+
const body = readObjectString(source, "body") ?? "";
|
|
2509
|
+
return `---
|
|
2510
|
+
name: ${yamlScalar(name)}
|
|
2511
|
+
description: ${yamlScalar(description)}
|
|
2512
|
+
---
|
|
2513
|
+
|
|
2514
|
+
${body.trim()}
|
|
2515
|
+
`;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// packages/compiler/src/plugins.ts
|
|
2519
|
+
async function buildPluginPackages(rootDir, outRoot, manifest) {
|
|
2520
|
+
const identity = await loadProjectIdentity(rootDir);
|
|
2521
|
+
await buildClaudePlugin(rootDir, outRoot, identity, manifest);
|
|
2522
|
+
}
|
|
2523
|
+
async function buildClaudePlugin(rootDir, outRoot, identity, manifest) {
|
|
2524
|
+
const pluginDir = path14.join(outRoot, "claude-plugin");
|
|
2525
|
+
await mkdir7(path14.join(pluginDir, ".claude-plugin"), { recursive: true });
|
|
2526
|
+
await copySkills(rootDir, path14.join(pluginDir, "skills"));
|
|
2527
|
+
await copyCommands(rootDir, path14.join(pluginDir, "commands"));
|
|
2528
|
+
await copyHooks(rootDir, path14.join(pluginDir, "hooks"));
|
|
2529
|
+
await emitClaudeAgents(rootDir, path14.join(pluginDir, "agents"));
|
|
2530
|
+
await copyClaudePassthroughDirectories(rootDir, pluginDir);
|
|
2531
|
+
await writeJson(path14.join(pluginDir, ".claude-plugin", "plugin.json"), {
|
|
2532
|
+
name: identity.slug,
|
|
2533
|
+
version: identity.version,
|
|
2534
|
+
description: identity.description,
|
|
2535
|
+
displayName: identity.name,
|
|
2536
|
+
installationPreference: "available"
|
|
2537
|
+
});
|
|
2538
|
+
await writeJson(path14.join(pluginDir, "version.json"), {
|
|
2539
|
+
version: identity.version,
|
|
2540
|
+
generatedBy: "sidecar"
|
|
2541
|
+
});
|
|
2542
|
+
await writeJson(path14.join(pluginDir, ".mcp.json"), {
|
|
2543
|
+
mcpServers: {
|
|
2544
|
+
[identity.slug]: {
|
|
2545
|
+
type: "http",
|
|
2546
|
+
url: "${SIDECAR_MCP_URL}"
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
});
|
|
2550
|
+
await writeJson(path14.join(pluginDir, "settings.json"), {
|
|
2551
|
+
sidecar: {
|
|
2552
|
+
tools: manifest.tools.map((entry) => entry.id),
|
|
2553
|
+
resources: manifest.resources.map((entry) => entry.uri),
|
|
2554
|
+
prompts: manifest.prompts.map((entry) => entry.name)
|
|
2555
|
+
}
|
|
2556
|
+
});
|
|
2557
|
+
await writeFile7(
|
|
2558
|
+
path14.join(pluginDir, "README.md"),
|
|
2559
|
+
`# ${identity.name} Claude Plugin
|
|
2560
|
+
|
|
2561
|
+
This package was generated by Sidecar.
|
|
2562
|
+
|
|
2563
|
+
Set \`SIDECAR_MCP_URL\` to the hosted MCP endpoint for this app.
|
|
2564
|
+
|
|
2565
|
+
For local testing, run \`sidecar dev --tunnel\` and use the printed HTTPS MCP URL.
|
|
2566
|
+
|
|
2567
|
+
Claude/Cowork plugin-specific components such as skills, slash commands, agents, hooks, output styles, themes, monitors, assets, and bin scripts will be emitted here when the source project defines them.
|
|
2568
|
+
`
|
|
2569
|
+
);
|
|
2570
|
+
}
|
|
2571
|
+
async function writeJson(filePath, value) {
|
|
2572
|
+
await mkdir7(path14.dirname(filePath), { recursive: true });
|
|
2573
|
+
await writeFile7(
|
|
2574
|
+
filePath,
|
|
2575
|
+
`${JSON.stringify(stripUndefined2(value), null, 2)}
|
|
2576
|
+
`
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// packages/compiler/src/prompts.ts
|
|
2581
|
+
import { readdir as readdir6 } from "fs/promises";
|
|
2582
|
+
import path15 from "path";
|
|
2583
|
+
import {
|
|
2584
|
+
Node as Node7,
|
|
2585
|
+
SyntaxKind as SyntaxKind5
|
|
2586
|
+
} from "ts-morph";
|
|
2587
|
+
async function analyzeProjectPrompts(rootDir) {
|
|
2588
|
+
const promptFiles = await findPromptFiles(path15.join(rootDir, "prompts"));
|
|
2589
|
+
if (promptFiles.length === 0) {
|
|
2590
|
+
return [];
|
|
2591
|
+
}
|
|
2592
|
+
const project = createProject(rootDir);
|
|
2593
|
+
return promptFiles.map(
|
|
2594
|
+
(filePath) => analyzePromptFile(project.addSourceFileAtPath(filePath), rootDir)
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
function analyzePromptFile(sourceFile, rootDir) {
|
|
2598
|
+
const definition = findPromptDefinition(sourceFile);
|
|
2599
|
+
const absoluteFile = sourceFile.getFilePath();
|
|
2600
|
+
const directory = path15.basename(path15.dirname(absoluteFile));
|
|
2601
|
+
const name = getOptionalStringProperty2(definition, "name") ?? safePathSegment(directory);
|
|
2602
|
+
const title = getRequiredStringProperty2(definition, "title", sourceFile);
|
|
2603
|
+
const description = getOptionalStringProperty2(definition, "description");
|
|
2604
|
+
const args = readArgs(definition);
|
|
2605
|
+
const descriptor = createPromptDescriptor({
|
|
2606
|
+
name,
|
|
2607
|
+
title,
|
|
2608
|
+
description,
|
|
2609
|
+
args,
|
|
2610
|
+
icons: readIcons(definition)
|
|
2611
|
+
});
|
|
2612
|
+
if (!getMethodOrFunctionProperty(definition, "run")) {
|
|
2613
|
+
throw new CompilerError(sourceFile, "prompt({ ... }) must include a run method.");
|
|
2614
|
+
}
|
|
2615
|
+
return {
|
|
2616
|
+
sourceFile: path15.relative(rootDir, absoluteFile),
|
|
2617
|
+
directory,
|
|
2618
|
+
name,
|
|
2619
|
+
title,
|
|
2620
|
+
description,
|
|
2621
|
+
args,
|
|
2622
|
+
descriptor
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
async function findPromptFiles(promptsDir) {
|
|
2626
|
+
if (!existsSyncSafe(promptsDir)) {
|
|
2627
|
+
return [];
|
|
2628
|
+
}
|
|
2629
|
+
const entries = await readdir6(promptsDir, { withFileTypes: true });
|
|
2630
|
+
const files = [];
|
|
2631
|
+
for (const entry of entries) {
|
|
2632
|
+
if (!entry.isDirectory()) {
|
|
2633
|
+
continue;
|
|
2634
|
+
}
|
|
2635
|
+
const filePath = path15.join(promptsDir, entry.name, "prompt.ts");
|
|
2636
|
+
if (existsSyncSafe(filePath)) {
|
|
2637
|
+
files.push(filePath);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
2641
|
+
}
|
|
2642
|
+
function findPromptDefinition(sourceFile) {
|
|
2643
|
+
const call = resolveDefaultExportCall(sourceFile, "prompt");
|
|
2644
|
+
if (!call) {
|
|
2645
|
+
throw new CompilerError(
|
|
2646
|
+
sourceFile,
|
|
2647
|
+
"prompt.ts must default-export prompt({ ... }) or an identifier initialized with prompt({ ... })."
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
const definition = unwrapExpression(call.getArguments()[0]);
|
|
2651
|
+
if (!definition || !Node7.isObjectLiteralExpression(definition)) {
|
|
2652
|
+
throw new CompilerError(sourceFile, "prompt(...) must receive one object literal.");
|
|
2653
|
+
}
|
|
2654
|
+
return definition;
|
|
2655
|
+
}
|
|
2656
|
+
function getRequiredStringProperty2(definition, propertyName, sourceFile) {
|
|
2657
|
+
const value = getOptionalStringProperty2(definition, propertyName);
|
|
2658
|
+
if (value === void 0 || !value.trim()) {
|
|
2659
|
+
throw new CompilerError(
|
|
2660
|
+
sourceFile,
|
|
2661
|
+
`prompt({ ... }) must include a non-empty ${propertyName} string.`
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
return value;
|
|
2665
|
+
}
|
|
2666
|
+
function getOptionalStringProperty2(definition, propertyName) {
|
|
2667
|
+
const property = definition.getProperty(propertyName);
|
|
2668
|
+
if (!property || !Node7.isPropertyAssignment(property)) {
|
|
2669
|
+
return void 0;
|
|
2670
|
+
}
|
|
2671
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2672
|
+
return initializer && Node7.isStringLiteral(initializer) ? initializer.getLiteralText() : void 0;
|
|
2673
|
+
}
|
|
2674
|
+
function readArgs(definition) {
|
|
2675
|
+
const initializer = readObjectProperty4(definition, "args");
|
|
2676
|
+
if (!initializer) {
|
|
2677
|
+
return void 0;
|
|
2678
|
+
}
|
|
2679
|
+
const args = {};
|
|
2680
|
+
for (const property of initializer.getProperties()) {
|
|
2681
|
+
if (!Node7.isPropertyAssignment(property)) {
|
|
2682
|
+
continue;
|
|
2683
|
+
}
|
|
2684
|
+
const value = readArgInput(property.getInitializer());
|
|
2685
|
+
if (value !== void 0) {
|
|
2686
|
+
args[property.getName().replace(/^["']|["']$/g, "")] = value;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
return Object.keys(args).length ? args : void 0;
|
|
2690
|
+
}
|
|
2691
|
+
function readArgInput(node) {
|
|
2692
|
+
const initializer = unwrapExpression(node);
|
|
2693
|
+
if (!initializer) {
|
|
2694
|
+
return void 0;
|
|
2695
|
+
}
|
|
2696
|
+
if (Node7.isStringLiteral(initializer)) {
|
|
2697
|
+
return initializer.getLiteralText();
|
|
2698
|
+
}
|
|
2699
|
+
if (Node7.isArrayLiteralExpression(initializer)) {
|
|
2700
|
+
return initializer.getElements().filter(Node7.isLiteralExpression).map((element) => element.getLiteralText());
|
|
2701
|
+
}
|
|
2702
|
+
if (Node7.isObjectLiteralExpression(initializer)) {
|
|
2703
|
+
return {
|
|
2704
|
+
description: getOptionalStringProperty2(initializer, "description"),
|
|
2705
|
+
required: readBooleanProperty3(initializer, "required")
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
return void 0;
|
|
2709
|
+
}
|
|
2710
|
+
function readBooleanProperty3(definition, propertyName) {
|
|
2711
|
+
const property = definition.getProperty(propertyName);
|
|
2712
|
+
if (!property || !Node7.isPropertyAssignment(property)) {
|
|
2713
|
+
return void 0;
|
|
2714
|
+
}
|
|
2715
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2716
|
+
if (!initializer) {
|
|
2717
|
+
return void 0;
|
|
2718
|
+
}
|
|
2719
|
+
if (initializer.getKind() === SyntaxKind5.TrueKeyword) return true;
|
|
2720
|
+
if (initializer.getKind() === SyntaxKind5.FalseKeyword) return false;
|
|
2721
|
+
return void 0;
|
|
2722
|
+
}
|
|
2723
|
+
function getMethodOrFunctionProperty(definition, propertyName) {
|
|
2724
|
+
const property = definition.getProperty(propertyName);
|
|
2725
|
+
if (Node7.isMethodDeclaration(property)) {
|
|
2726
|
+
return property;
|
|
2727
|
+
}
|
|
2728
|
+
if (property && Node7.isPropertyAssignment(property)) {
|
|
2729
|
+
const initializer = property.getInitializer();
|
|
2730
|
+
if (initializer && (Node7.isArrowFunction(initializer) || Node7.isFunctionExpression(initializer))) {
|
|
2731
|
+
return initializer;
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
return void 0;
|
|
2735
|
+
}
|
|
2736
|
+
function readIcons(definition) {
|
|
2737
|
+
const property = definition.getProperty("icons");
|
|
2738
|
+
if (!property || !Node7.isPropertyAssignment(property)) {
|
|
2739
|
+
return void 0;
|
|
2740
|
+
}
|
|
2741
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2742
|
+
if (!initializer || !Node7.isArrayLiteralExpression(initializer)) {
|
|
2743
|
+
return void 0;
|
|
2744
|
+
}
|
|
2745
|
+
const icons = initializer.getElements().flatMap((element) => {
|
|
2746
|
+
const object = unwrapExpression(element);
|
|
2747
|
+
if (!object || !Node7.isObjectLiteralExpression(object)) {
|
|
2748
|
+
return [];
|
|
2749
|
+
}
|
|
2750
|
+
const src = getOptionalStringProperty2(object, "src");
|
|
2751
|
+
if (!src) {
|
|
2752
|
+
return [];
|
|
2753
|
+
}
|
|
2754
|
+
return [{
|
|
2755
|
+
src,
|
|
2756
|
+
mimeType: getOptionalStringProperty2(object, "mimeType"),
|
|
2757
|
+
sizes: readStringArrayProperty3(object, "sizes")
|
|
2758
|
+
}];
|
|
2759
|
+
});
|
|
2760
|
+
return icons.length ? icons : void 0;
|
|
2761
|
+
}
|
|
2762
|
+
function readObjectProperty4(definition, propertyName) {
|
|
2763
|
+
const property = definition.getProperty(propertyName);
|
|
2764
|
+
if (!property || !Node7.isPropertyAssignment(property)) {
|
|
2765
|
+
return void 0;
|
|
2766
|
+
}
|
|
2767
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2768
|
+
return initializer && Node7.isObjectLiteralExpression(initializer) ? initializer : void 0;
|
|
2769
|
+
}
|
|
2770
|
+
function readStringArrayProperty3(definition, propertyName) {
|
|
2771
|
+
const property = definition.getProperty(propertyName);
|
|
2772
|
+
if (!property || !Node7.isPropertyAssignment(property)) {
|
|
2773
|
+
return void 0;
|
|
2774
|
+
}
|
|
2775
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2776
|
+
if (!initializer || !Node7.isArrayLiteralExpression(initializer)) {
|
|
2777
|
+
return void 0;
|
|
2778
|
+
}
|
|
2779
|
+
return initializer.getElements().filter(Node7.isStringLiteral).map((element) => element.getLiteralText());
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// packages/compiler/src/resources.ts
|
|
2783
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
2784
|
+
import path16 from "path";
|
|
2785
|
+
import {
|
|
2786
|
+
Node as Node8,
|
|
2787
|
+
SyntaxKind as SyntaxKind6
|
|
2788
|
+
} from "ts-morph";
|
|
2789
|
+
async function analyzeProjectResources(rootDir) {
|
|
2790
|
+
const resourceFiles = await findResourceFiles(path16.join(rootDir, "resources"));
|
|
2791
|
+
if (resourceFiles.length === 0) {
|
|
2792
|
+
return [];
|
|
2793
|
+
}
|
|
2794
|
+
const project = createProject(rootDir);
|
|
2795
|
+
return resourceFiles.map(
|
|
2796
|
+
(filePath) => analyzeResourceFile(project.addSourceFileAtPath(filePath), rootDir)
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
function analyzeResourceFile(sourceFile, rootDir) {
|
|
2800
|
+
const definition = findResourceDefinition(sourceFile);
|
|
2801
|
+
const absoluteFile = sourceFile.getFilePath();
|
|
2802
|
+
const directory = path16.basename(path16.dirname(absoluteFile));
|
|
2803
|
+
const defaultUri = `sidecar://resources/${safePathSegment(directory)}`;
|
|
2804
|
+
const name = getRequiredStringProperty3(definition, "name", sourceFile);
|
|
2805
|
+
const uri = getOptionalStringProperty3(definition, "uri") ?? defaultUri;
|
|
2806
|
+
const descriptor = createResourceDescriptor({
|
|
2807
|
+
uri,
|
|
2808
|
+
name,
|
|
2809
|
+
title: getOptionalStringProperty3(definition, "title"),
|
|
2810
|
+
description: getOptionalStringProperty3(definition, "description"),
|
|
2811
|
+
mimeType: getOptionalStringProperty3(definition, "mimeType"),
|
|
2812
|
+
size: getOptionalNumberProperty(definition, "size"),
|
|
2813
|
+
icons: readIcons2(definition),
|
|
2814
|
+
annotations: readAnnotations2(definition)
|
|
2815
|
+
});
|
|
2816
|
+
if (!getMethodOrFunctionProperty2(definition, "read")) {
|
|
2817
|
+
throw new CompilerError(sourceFile, "resource({ ... }) must include a read method.");
|
|
2818
|
+
}
|
|
2819
|
+
return {
|
|
2820
|
+
sourceFile: path16.relative(rootDir, absoluteFile),
|
|
2821
|
+
directory,
|
|
2822
|
+
uri,
|
|
2823
|
+
name,
|
|
2824
|
+
title: descriptor.title,
|
|
2825
|
+
description: descriptor.description,
|
|
2826
|
+
mimeType: descriptor.mimeType,
|
|
2827
|
+
size: descriptor.size,
|
|
2828
|
+
annotations: descriptor.annotations,
|
|
2829
|
+
subscribe: readBooleanProperty4(definition, "subscribe"),
|
|
2830
|
+
descriptor
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
async function findResourceFiles(resourcesDir) {
|
|
2834
|
+
if (!existsSyncSafe(resourcesDir)) {
|
|
2835
|
+
return [];
|
|
2836
|
+
}
|
|
2837
|
+
const entries = await readdir7(resourcesDir, { withFileTypes: true });
|
|
2838
|
+
const files = [];
|
|
2839
|
+
for (const entry of entries) {
|
|
2840
|
+
if (!entry.isDirectory()) {
|
|
2841
|
+
continue;
|
|
2842
|
+
}
|
|
2843
|
+
const filePath = path16.join(resourcesDir, entry.name, "resource.ts");
|
|
2844
|
+
if (existsSyncSafe(filePath)) {
|
|
2845
|
+
files.push(filePath);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
2849
|
+
}
|
|
2850
|
+
function findResourceDefinition(sourceFile) {
|
|
2851
|
+
const call = resolveDefaultExportCall(sourceFile, "resource");
|
|
2852
|
+
if (!call) {
|
|
2853
|
+
throw new CompilerError(
|
|
2854
|
+
sourceFile,
|
|
2855
|
+
"resource.ts must default-export resource({ ... }) or an identifier initialized with resource({ ... })."
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
const definition = unwrapExpression(call.getArguments()[0]);
|
|
2859
|
+
if (!definition || !Node8.isObjectLiteralExpression(definition)) {
|
|
2860
|
+
throw new CompilerError(sourceFile, "resource(...) must receive one object literal.");
|
|
2861
|
+
}
|
|
2862
|
+
return definition;
|
|
2863
|
+
}
|
|
2864
|
+
function getRequiredStringProperty3(definition, propertyName, sourceFile) {
|
|
2865
|
+
const value = getOptionalStringProperty3(definition, propertyName);
|
|
2866
|
+
if (value === void 0 || !value.trim()) {
|
|
2867
|
+
throw new CompilerError(
|
|
2868
|
+
sourceFile,
|
|
2869
|
+
`resource({ ... }) must include a non-empty ${propertyName} string.`
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2872
|
+
return value;
|
|
2873
|
+
}
|
|
2874
|
+
function getOptionalStringProperty3(definition, propertyName) {
|
|
2875
|
+
const property = definition.getProperty(propertyName);
|
|
2876
|
+
if (!property || !Node8.isPropertyAssignment(property)) {
|
|
2877
|
+
return void 0;
|
|
2878
|
+
}
|
|
2879
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2880
|
+
return initializer && Node8.isStringLiteral(initializer) ? initializer.getLiteralText() : void 0;
|
|
2881
|
+
}
|
|
2882
|
+
function getOptionalNumberProperty(definition, propertyName) {
|
|
2883
|
+
const property = definition.getProperty(propertyName);
|
|
2884
|
+
if (!property || !Node8.isPropertyAssignment(property)) {
|
|
2885
|
+
return void 0;
|
|
2886
|
+
}
|
|
2887
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2888
|
+
return initializer && Node8.isNumericLiteral(initializer) ? Number(initializer.getLiteralText()) : void 0;
|
|
2889
|
+
}
|
|
2890
|
+
function readBooleanProperty4(definition, propertyName) {
|
|
2891
|
+
const property = definition.getProperty(propertyName);
|
|
2892
|
+
if (!property || !Node8.isPropertyAssignment(property)) {
|
|
2893
|
+
return void 0;
|
|
2894
|
+
}
|
|
2895
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2896
|
+
if (!initializer) {
|
|
2897
|
+
return void 0;
|
|
2898
|
+
}
|
|
2899
|
+
if (initializer.getKind() === SyntaxKind6.TrueKeyword) return true;
|
|
2900
|
+
if (initializer.getKind() === SyntaxKind6.FalseKeyword) return false;
|
|
2901
|
+
return void 0;
|
|
2902
|
+
}
|
|
2903
|
+
function getMethodOrFunctionProperty2(definition, propertyName) {
|
|
2904
|
+
const property = definition.getProperty(propertyName);
|
|
2905
|
+
if (Node8.isMethodDeclaration(property)) {
|
|
2906
|
+
return property;
|
|
2907
|
+
}
|
|
2908
|
+
if (property && Node8.isPropertyAssignment(property)) {
|
|
2909
|
+
const initializer = property.getInitializer();
|
|
2910
|
+
if (initializer && (Node8.isArrowFunction(initializer) || Node8.isFunctionExpression(initializer))) {
|
|
2911
|
+
return initializer;
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
return void 0;
|
|
2915
|
+
}
|
|
2916
|
+
function readAnnotations2(definition) {
|
|
2917
|
+
const initializer = readObjectProperty5(definition, "annotations");
|
|
2918
|
+
if (!initializer) {
|
|
2919
|
+
return void 0;
|
|
2920
|
+
}
|
|
2921
|
+
const annotations = {};
|
|
2922
|
+
const audience = readStringArrayProperty4(initializer, "audience")?.filter((value) => value === "user" || value === "assistant");
|
|
2923
|
+
const priority = getOptionalNumberProperty(initializer, "priority");
|
|
2924
|
+
const lastModified = getOptionalStringProperty3(initializer, "lastModified");
|
|
2925
|
+
if (audience?.length) annotations.audience = audience;
|
|
2926
|
+
if (priority !== void 0) annotations.priority = priority;
|
|
2927
|
+
if (lastModified) annotations.lastModified = lastModified;
|
|
2928
|
+
return Object.keys(annotations).length ? annotations : void 0;
|
|
2929
|
+
}
|
|
2930
|
+
function readIcons2(definition) {
|
|
2931
|
+
const property = definition.getProperty("icons");
|
|
2932
|
+
if (!property || !Node8.isPropertyAssignment(property)) {
|
|
2933
|
+
return void 0;
|
|
2934
|
+
}
|
|
2935
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2936
|
+
if (!initializer || !Node8.isArrayLiteralExpression(initializer)) {
|
|
2937
|
+
return void 0;
|
|
2938
|
+
}
|
|
2939
|
+
const icons = initializer.getElements().flatMap((element) => {
|
|
2940
|
+
const object = unwrapExpression(element);
|
|
2941
|
+
if (!object || !Node8.isObjectLiteralExpression(object)) {
|
|
2942
|
+
return [];
|
|
2943
|
+
}
|
|
2944
|
+
const src = getOptionalStringProperty3(object, "src");
|
|
2945
|
+
if (!src) {
|
|
2946
|
+
return [];
|
|
2947
|
+
}
|
|
2948
|
+
return [{
|
|
2949
|
+
src,
|
|
2950
|
+
mimeType: getOptionalStringProperty3(object, "mimeType"),
|
|
2951
|
+
sizes: readStringArrayProperty4(object, "sizes")
|
|
2952
|
+
}];
|
|
2953
|
+
});
|
|
2954
|
+
return icons.length ? icons : void 0;
|
|
2955
|
+
}
|
|
2956
|
+
function readObjectProperty5(definition, propertyName) {
|
|
2957
|
+
const property = definition.getProperty(propertyName);
|
|
2958
|
+
if (!property || !Node8.isPropertyAssignment(property)) {
|
|
2959
|
+
return void 0;
|
|
2960
|
+
}
|
|
2961
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2962
|
+
return initializer && Node8.isObjectLiteralExpression(initializer) ? initializer : void 0;
|
|
2963
|
+
}
|
|
2964
|
+
function readStringArrayProperty4(definition, propertyName) {
|
|
2965
|
+
const property = definition.getProperty(propertyName);
|
|
2966
|
+
if (!property || !Node8.isPropertyAssignment(property)) {
|
|
2967
|
+
return void 0;
|
|
2968
|
+
}
|
|
2969
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2970
|
+
if (!initializer || !Node8.isArrayLiteralExpression(initializer)) {
|
|
2971
|
+
return void 0;
|
|
2972
|
+
}
|
|
2973
|
+
return initializer.getElements().filter(Node8.isStringLiteral).map((element) => element.getLiteralText());
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// packages/compiler/src/build.ts
|
|
2977
|
+
async function buildProject(options) {
|
|
2978
|
+
const rootDir = path17.resolve(options.rootDir);
|
|
2979
|
+
const target = options.target ?? "mcp";
|
|
2980
|
+
const config = analyzeProjectConfig(rootDir);
|
|
2981
|
+
const tools = await analyzeProjectTools(rootDir, { target });
|
|
2982
|
+
const resources = await analyzeProjectResources(rootDir);
|
|
2983
|
+
const resourceTemplates = [];
|
|
2984
|
+
const prompts = await analyzeProjectPrompts(rootDir);
|
|
2985
|
+
const diagnostics = await collectProjectDiagnostics(rootDir, {
|
|
2986
|
+
tools,
|
|
2987
|
+
resources,
|
|
2988
|
+
prompts,
|
|
2989
|
+
config
|
|
2990
|
+
});
|
|
2991
|
+
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
2992
|
+
if (errors.length) {
|
|
2993
|
+
throw new Error(errors.map(formatDiagnostic).join("\n"));
|
|
2994
|
+
}
|
|
2995
|
+
const outDir = resolveInsideRoot(rootDir, options.outDir ?? "out/mcp");
|
|
2996
|
+
await buildWidgets(rootDir, outDir, tools);
|
|
2997
|
+
const manifest = {
|
|
2998
|
+
version: 1,
|
|
2999
|
+
target,
|
|
3000
|
+
rootDir: ".",
|
|
3001
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3002
|
+
config,
|
|
3003
|
+
tools,
|
|
3004
|
+
resources,
|
|
3005
|
+
resourceTemplates,
|
|
3006
|
+
prompts,
|
|
3007
|
+
diagnostics
|
|
3008
|
+
};
|
|
3009
|
+
await mkdir8(outDir, { recursive: true });
|
|
3010
|
+
await writeFile8(
|
|
3011
|
+
path17.join(outDir, "manifest.sidecar.json"),
|
|
3012
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
3013
|
+
`
|
|
3014
|
+
);
|
|
3015
|
+
await writeFile8(path17.join(outDir, "README.md"), renderMcpReadme(manifest));
|
|
3016
|
+
await writeGeneratedTypes(rootDir, tools);
|
|
3017
|
+
if (options.plugins) {
|
|
3018
|
+
await buildPluginPackages(rootDir, path17.dirname(outDir), manifest);
|
|
3019
|
+
}
|
|
3020
|
+
return manifest;
|
|
3021
|
+
}
|
|
3022
|
+
function resolveInsideRoot(rootDir, output) {
|
|
3023
|
+
const resolved = path17.resolve(rootDir, output);
|
|
3024
|
+
const relative = path17.relative(rootDir, resolved);
|
|
3025
|
+
if (relative.startsWith("..") || path17.isAbsolute(relative)) {
|
|
3026
|
+
throw new Error(`Build output must stay inside the project root: ${output}`);
|
|
3027
|
+
}
|
|
3028
|
+
return resolved;
|
|
3029
|
+
}
|
|
3030
|
+
function renderMcpReadme(manifest) {
|
|
3031
|
+
const tools = manifest.tools.map((toolEntry) => `- \`${toolEntry.id}\`: ${toolEntry.description}`).join("\n");
|
|
3032
|
+
const resources = manifest.resources.map((resourceEntry) => `- \`${resourceEntry.uri}\`: ${resourceEntry.description ?? resourceEntry.name}`).join("\n");
|
|
3033
|
+
const prompts = manifest.prompts.map((promptEntry) => `- \`${promptEntry.name}\`: ${promptEntry.description ?? promptEntry.title}`).join("\n");
|
|
3034
|
+
return `# Sidecar MCP Build
|
|
3035
|
+
|
|
3036
|
+
Generated by Sidecar.
|
|
3037
|
+
|
|
3038
|
+
## Tools
|
|
3039
|
+
|
|
3040
|
+
${tools || "No tools detected."}
|
|
3041
|
+
|
|
3042
|
+
## Resources
|
|
3043
|
+
|
|
3044
|
+
${resources || "No resources detected."}
|
|
3045
|
+
|
|
3046
|
+
## Prompts
|
|
3047
|
+
|
|
3048
|
+
${prompts || "No prompts detected."}
|
|
3049
|
+
|
|
3050
|
+
## Local HTTPS Testing
|
|
3051
|
+
|
|
3052
|
+
Run \`sidecar dev --tunnel\` from the project root to start the local MCP server on Streamable HTTP and print an HTTPS MCP URL that can be added to ChatGPT, Claude, or a Claude plugin install.
|
|
3053
|
+
`;
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
// packages/server/src/index.ts
|
|
3057
|
+
import { createServer } from "http";
|
|
3058
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3059
|
+
|
|
3060
|
+
// packages/server/src/proxy.ts
|
|
3061
|
+
import { randomUUID } from "crypto";
|
|
3062
|
+
var proxyBrand = /* @__PURE__ */ Symbol.for("sidecar.proxy");
|
|
3063
|
+
function isSidecarProxy(value) {
|
|
3064
|
+
return Boolean(
|
|
3065
|
+
value && typeof value === "object" && value[proxyBrand] === true
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
async function runProxy(proxyConfig, request) {
|
|
3069
|
+
for (const middleware of proxyConfig?.before ?? []) {
|
|
3070
|
+
const result = await middleware(request);
|
|
3071
|
+
if (result) {
|
|
3072
|
+
return result;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
return void 0;
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
// packages/server/src/index.ts
|
|
3079
|
+
import { JSONRPC_VERSION } from "@modelcontextprotocol/sdk/types.js";
|
|
3080
|
+
var SIDECAR_MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
3081
|
+
var SidecarMcpServer = class {
|
|
3082
|
+
constructor(options) {
|
|
3083
|
+
this.options = options;
|
|
3084
|
+
for (const loaded of options.tools) {
|
|
3085
|
+
const descriptorProvided = Boolean(loaded.descriptor);
|
|
3086
|
+
const descriptor = loaded.descriptor ?? createToolDescriptor(loaded.tool);
|
|
3087
|
+
if (this.tools.has(descriptor.name)) {
|
|
3088
|
+
throw new Error(`Duplicate Sidecar tool id "${descriptor.name}".`);
|
|
3089
|
+
}
|
|
3090
|
+
this.tools.set(descriptor.name, { ...loaded, descriptor, descriptorProvided });
|
|
3091
|
+
}
|
|
3092
|
+
for (const resource of options.resources ?? []) {
|
|
3093
|
+
if (this.resources.has(resource.uri)) {
|
|
3094
|
+
throw new Error(`Duplicate Sidecar resource uri "${resource.uri}".`);
|
|
3095
|
+
}
|
|
3096
|
+
const descriptor = resource.descriptor ?? {
|
|
3097
|
+
uri: resource.uri,
|
|
3098
|
+
name: resource.name ?? resource.uri,
|
|
3099
|
+
description: resource.description,
|
|
3100
|
+
mimeType: resource.mimeType,
|
|
3101
|
+
_meta: resource._meta
|
|
3102
|
+
};
|
|
3103
|
+
this.resources.set(resource.uri, { ...resource, descriptor });
|
|
3104
|
+
}
|
|
3105
|
+
for (const loaded of options.prompts ?? []) {
|
|
3106
|
+
const descriptor = loaded.descriptor ?? {
|
|
3107
|
+
name: loaded.prompt.name ?? loaded.prompt.title,
|
|
3108
|
+
title: loaded.prompt.title,
|
|
3109
|
+
description: loaded.prompt.description
|
|
3110
|
+
};
|
|
3111
|
+
if (this.prompts.has(descriptor.name)) {
|
|
3112
|
+
throw new Error(`Duplicate Sidecar prompt name "${descriptor.name}".`);
|
|
3113
|
+
}
|
|
3114
|
+
this.prompts.set(descriptor.name, { ...loaded, descriptor });
|
|
3115
|
+
}
|
|
3116
|
+
this.resourceTemplates = options.resourceTemplates ?? [];
|
|
3117
|
+
}
|
|
3118
|
+
options;
|
|
3119
|
+
tools = /* @__PURE__ */ new Map();
|
|
3120
|
+
resources = /* @__PURE__ */ new Map();
|
|
3121
|
+
prompts = /* @__PURE__ */ new Map();
|
|
3122
|
+
resourceTemplates;
|
|
3123
|
+
/** Returns all tool descriptors exposed through `tools/list`. */
|
|
3124
|
+
descriptors() {
|
|
3125
|
+
return [...this.tools.values()].map((loaded) => loaded.descriptor);
|
|
3126
|
+
}
|
|
3127
|
+
/** Returns all resource descriptors exposed through `resources/list`. */
|
|
3128
|
+
resourceDescriptors() {
|
|
3129
|
+
return [...this.resources.values()].map((loaded) => loaded.descriptor);
|
|
3130
|
+
}
|
|
3131
|
+
/** Returns all prompt descriptors exposed through `prompts/list`. */
|
|
3132
|
+
promptDescriptors() {
|
|
3133
|
+
return [...this.prompts.values()].map((loaded) => loaded.descriptor);
|
|
3134
|
+
}
|
|
3135
|
+
/** Handles a JSON-RPC request or notification. */
|
|
3136
|
+
async handle(request, context = {}) {
|
|
3137
|
+
if (request.id === void 0) {
|
|
3138
|
+
await this.authorizeEndpoint(context);
|
|
3139
|
+
await this.handleNotification(request);
|
|
3140
|
+
return void 0;
|
|
3141
|
+
}
|
|
3142
|
+
try {
|
|
3143
|
+
const authSession = await this.authorizeEndpoint(context);
|
|
3144
|
+
const authorizedContext = { ...context, authSession };
|
|
3145
|
+
const result = await this.dispatch(request, authorizedContext);
|
|
3146
|
+
return {
|
|
3147
|
+
jsonrpc: JSONRPC_VERSION,
|
|
3148
|
+
id: request.id,
|
|
3149
|
+
result
|
|
3150
|
+
};
|
|
3151
|
+
} catch (error) {
|
|
3152
|
+
return {
|
|
3153
|
+
jsonrpc: JSONRPC_VERSION,
|
|
3154
|
+
id: request.id,
|
|
3155
|
+
error: normalizeError(error)
|
|
3156
|
+
};
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
/** Routes an MCP method to its implementation. */
|
|
3160
|
+
async dispatch(request, context) {
|
|
3161
|
+
switch (request.method) {
|
|
3162
|
+
case "initialize":
|
|
3163
|
+
return {
|
|
3164
|
+
protocolVersion: SIDECAR_MCP_PROTOCOL_VERSION,
|
|
3165
|
+
capabilities: this.capabilities(),
|
|
3166
|
+
serverInfo: {
|
|
3167
|
+
name: this.options.name ?? "sidecar",
|
|
3168
|
+
version: this.options.version ?? "0.0.0-dev"
|
|
3169
|
+
}
|
|
3170
|
+
};
|
|
3171
|
+
case "tools/list":
|
|
3172
|
+
return this.listTools(request, context);
|
|
3173
|
+
case "tools/call":
|
|
3174
|
+
return this.callTool(request, context);
|
|
3175
|
+
case "resources/list":
|
|
3176
|
+
return this.listResources(request, context);
|
|
3177
|
+
case "resources/read":
|
|
3178
|
+
return this.readResource(request, context);
|
|
3179
|
+
case "resources/templates/list":
|
|
3180
|
+
return this.listResourceTemplates(request, context);
|
|
3181
|
+
case "resources/subscribe":
|
|
3182
|
+
return this.subscribeResource(request);
|
|
3183
|
+
case "resources/unsubscribe":
|
|
3184
|
+
return this.unsubscribeResource(request);
|
|
3185
|
+
case "prompts/list":
|
|
3186
|
+
return this.listPrompts(request, context);
|
|
3187
|
+
case "prompts/get":
|
|
3188
|
+
return this.getPrompt(request, context);
|
|
3189
|
+
default:
|
|
3190
|
+
throw new JsonRpcError(-32601, `Unsupported method "${request.method}".`);
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
/** Builds the MCP server capability object from implemented runtime features. */
|
|
3194
|
+
capabilities() {
|
|
3195
|
+
const configured = this.options.capabilities ?? {};
|
|
3196
|
+
return stripUndefined4({
|
|
3197
|
+
tools: stripUndefined4({
|
|
3198
|
+
listChanged: configured.tools?.listChanged || void 0
|
|
3199
|
+
}),
|
|
3200
|
+
resources: stripUndefined4({
|
|
3201
|
+
subscribe: configured.resources?.subscribe || void 0,
|
|
3202
|
+
listChanged: configured.resources?.listChanged || void 0
|
|
3203
|
+
}),
|
|
3204
|
+
prompts: this.prompts.size || configured.prompts?.listChanged ? stripUndefined4({
|
|
3205
|
+
listChanged: configured.prompts?.listChanged || void 0
|
|
3206
|
+
}) : void 0
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
/** Lists tools with MCP cursor pagination. */
|
|
3210
|
+
async listTools(request, context) {
|
|
3211
|
+
const page = await this.paginate("tools/list", this.descriptors(), request, context);
|
|
3212
|
+
return stripUndefined4({
|
|
3213
|
+
tools: [...page.items],
|
|
3214
|
+
nextCursor: page.nextCursor
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
/** Lists resources with MCP cursor pagination. */
|
|
3218
|
+
async listResources(request, context) {
|
|
3219
|
+
const page = await this.paginate("resources/list", this.resourceDescriptors(), request, context);
|
|
3220
|
+
return stripUndefined4({
|
|
3221
|
+
resources: [...page.items],
|
|
3222
|
+
nextCursor: page.nextCursor
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
/** Lists resource templates with MCP cursor pagination. */
|
|
3226
|
+
async listResourceTemplates(request, context) {
|
|
3227
|
+
const page = await this.paginate(
|
|
3228
|
+
"resources/templates/list",
|
|
3229
|
+
this.resourceTemplates.map((entry) => entry.descriptor),
|
|
3230
|
+
request,
|
|
3231
|
+
context
|
|
3232
|
+
);
|
|
3233
|
+
return stripUndefined4({
|
|
3234
|
+
resourceTemplates: [...page.items],
|
|
3235
|
+
nextCursor: page.nextCursor
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
/** Lists prompts with MCP cursor pagination. */
|
|
3239
|
+
async listPrompts(request, context) {
|
|
3240
|
+
const page = await this.paginate("prompts/list", this.promptDescriptors(), request, context);
|
|
3241
|
+
return stripUndefined4({
|
|
3242
|
+
prompts: [...page.items],
|
|
3243
|
+
nextCursor: page.nextCursor
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
/** Applies configured or default cursor pagination to one supported list operation. */
|
|
3247
|
+
async paginate(operation, items, request, context) {
|
|
3248
|
+
const cursor = readCursorParam(request);
|
|
3249
|
+
const pageSize = this.pageSize();
|
|
3250
|
+
const override = selectPaginationOverride(this.options.pagination?.override, operation);
|
|
3251
|
+
if (override) {
|
|
3252
|
+
return override({
|
|
3253
|
+
operation,
|
|
3254
|
+
items,
|
|
3255
|
+
cursor,
|
|
3256
|
+
pageSize,
|
|
3257
|
+
auth: context.authSession
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
return defaultPagination(items, cursor, pageSize);
|
|
3261
|
+
}
|
|
3262
|
+
/** Returns the server-chosen page size for all built-in list pagination. */
|
|
3263
|
+
pageSize() {
|
|
3264
|
+
const configured = this.options.pagination?.pageSize;
|
|
3265
|
+
return configured && configured > 0 ? Math.floor(configured) : 10;
|
|
3266
|
+
}
|
|
3267
|
+
/** Executes a tool after request-level and tool-level auth checks. */
|
|
3268
|
+
async callTool(request, context) {
|
|
3269
|
+
const params = request.params;
|
|
3270
|
+
const name = typeof params?.name === "string" ? params.name : void 0;
|
|
3271
|
+
if (!name) {
|
|
3272
|
+
throw new JsonRpcError(-32602, "tools/call requires params.name.");
|
|
3273
|
+
}
|
|
3274
|
+
const loaded = this.tools.get(name);
|
|
3275
|
+
if (!loaded) {
|
|
3276
|
+
throw new JsonRpcError(-32602, `Unknown tool "${name}".`);
|
|
3277
|
+
}
|
|
3278
|
+
const authSession = await this.authorizeTool(loaded, context);
|
|
3279
|
+
const ctx = this.options.createContext ? await this.options.createContext(request) : createDefaultContext(request);
|
|
3280
|
+
if (authSession !== void 0) {
|
|
3281
|
+
ctx.auth = authSession;
|
|
3282
|
+
}
|
|
3283
|
+
const controller = new AbortController();
|
|
3284
|
+
ctx.request = {
|
|
3285
|
+
...ctx.request,
|
|
3286
|
+
signal: controller.signal
|
|
3287
|
+
};
|
|
3288
|
+
const args = loaded.descriptorProvided ? validateAgainstSchema(
|
|
3289
|
+
loaded.descriptor.inputSchema,
|
|
3290
|
+
params?.arguments ?? {},
|
|
3291
|
+
`Invalid parameters for tool "${name}".`
|
|
3292
|
+
) : params?.arguments ?? {};
|
|
3293
|
+
const result = await withTimeout(
|
|
3294
|
+
executeTool(loaded.tool, args, ctx),
|
|
3295
|
+
this.options.toolTimeoutMs,
|
|
3296
|
+
controller
|
|
3297
|
+
);
|
|
3298
|
+
if (loaded.descriptorProvided) {
|
|
3299
|
+
validateAgainstSchema(
|
|
3300
|
+
loaded.descriptor.outputSchema,
|
|
3301
|
+
result.structuredContent,
|
|
3302
|
+
`Invalid structuredContent returned by tool "${name}".`
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
return result;
|
|
3306
|
+
}
|
|
3307
|
+
/** Authenticates the whole HTTP MCP request when auth.ts is configured. */
|
|
3308
|
+
async authorizeEndpoint(context) {
|
|
3309
|
+
if (context.authSession !== void 0) {
|
|
3310
|
+
return context.authSession;
|
|
3311
|
+
}
|
|
3312
|
+
if (!this.options.auth) {
|
|
3313
|
+
return void 0;
|
|
3314
|
+
}
|
|
3315
|
+
const request = context.request ?? new Request(this.options.auth.resource);
|
|
3316
|
+
const requestAuth = await this.options.auth.authorizeRequest(request);
|
|
3317
|
+
if (!requestAuth.ok) {
|
|
3318
|
+
throw authJsonRpcError(requestAuth);
|
|
3319
|
+
}
|
|
3320
|
+
return requestAuth.auth;
|
|
3321
|
+
}
|
|
3322
|
+
/** Applies Sidecar auth policy for a tool call. */
|
|
3323
|
+
async authorizeTool(loaded, context) {
|
|
3324
|
+
const policy = loaded.tool.auth;
|
|
3325
|
+
if (!this.options.auth) {
|
|
3326
|
+
if (policy && policy.public !== true) {
|
|
3327
|
+
throw new JsonRpcError(
|
|
3328
|
+
-32001,
|
|
3329
|
+
`Tool "${loaded.descriptor?.name ?? loaded.tool.name}" requires auth, but no auth.ts configuration is loaded.`
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
return void 0;
|
|
3333
|
+
}
|
|
3334
|
+
const authSession = context.authSession ?? await this.authorizeEndpoint(context);
|
|
3335
|
+
const toolAuth = this.options.auth.authorizeTool(policy, authSession);
|
|
3336
|
+
if (!toolAuth.ok) {
|
|
3337
|
+
throw authJsonRpcError(toolAuth);
|
|
3338
|
+
}
|
|
3339
|
+
return toolAuth.auth;
|
|
3340
|
+
}
|
|
3341
|
+
/** Reads a generated widget or authored resource by URI. */
|
|
3342
|
+
async readResource(request, context) {
|
|
3343
|
+
const params = request.params;
|
|
3344
|
+
const uri = typeof params?.uri === "string" ? params.uri : void 0;
|
|
3345
|
+
if (!uri) {
|
|
3346
|
+
throw new JsonRpcError(-32602, "resources/read requires params.uri.");
|
|
3347
|
+
}
|
|
3348
|
+
const resource = this.resources.get(uri);
|
|
3349
|
+
if (!resource) {
|
|
3350
|
+
throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
|
|
3351
|
+
}
|
|
3352
|
+
if (resource.resource) {
|
|
3353
|
+
const toolContext = this.options.createContext ? await this.options.createContext(request) : createDefaultContext(request);
|
|
3354
|
+
if (context.authSession !== void 0) {
|
|
3355
|
+
toolContext.auth = context.authSession;
|
|
3356
|
+
}
|
|
3357
|
+
return executeResource(resource.resource, toResourceContext(toolContext), {
|
|
3358
|
+
uri,
|
|
3359
|
+
mimeType: resource.descriptor.mimeType
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
return {
|
|
3363
|
+
contents: [
|
|
3364
|
+
{
|
|
3365
|
+
uri,
|
|
3366
|
+
mimeType: resource.mimeType ?? "text/plain",
|
|
3367
|
+
text: resource.text ?? "",
|
|
3368
|
+
_meta: resource._meta
|
|
3369
|
+
}
|
|
3370
|
+
]
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
/** Accepts a resource subscription when server-level support is enabled. */
|
|
3374
|
+
subscribeResource(request) {
|
|
3375
|
+
if (!this.options.capabilities?.resources?.subscribe) {
|
|
3376
|
+
throw new JsonRpcError(-32601, "Resource subscriptions are not enabled.");
|
|
3377
|
+
}
|
|
3378
|
+
const uri = readUriParam(request, "resources/subscribe");
|
|
3379
|
+
if (!this.resources.has(uri)) {
|
|
3380
|
+
throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
|
|
3381
|
+
}
|
|
3382
|
+
return {};
|
|
3383
|
+
}
|
|
3384
|
+
/** Accepts a resource unsubscription when server-level support is enabled. */
|
|
3385
|
+
unsubscribeResource(request) {
|
|
3386
|
+
if (!this.options.capabilities?.resources?.subscribe) {
|
|
3387
|
+
throw new JsonRpcError(-32601, "Resource subscriptions are not enabled.");
|
|
3388
|
+
}
|
|
3389
|
+
const uri = readUriParam(request, "resources/unsubscribe");
|
|
3390
|
+
if (!this.resources.has(uri)) {
|
|
3391
|
+
throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
|
|
3392
|
+
}
|
|
3393
|
+
return {};
|
|
3394
|
+
}
|
|
3395
|
+
/** Renders one named prompt with validated arguments. */
|
|
3396
|
+
async getPrompt(request, context) {
|
|
3397
|
+
const params = request.params;
|
|
3398
|
+
const name = typeof params?.name === "string" ? params.name : void 0;
|
|
3399
|
+
if (!name) {
|
|
3400
|
+
throw new JsonRpcError(-32602, "prompts/get requires params.name.");
|
|
3401
|
+
}
|
|
3402
|
+
const loaded = this.prompts.get(name);
|
|
3403
|
+
if (!loaded) {
|
|
3404
|
+
throw new JsonRpcError(-32602, `Unknown prompt "${name}".`);
|
|
3405
|
+
}
|
|
3406
|
+
const toolContext = this.options.createContext ? await this.options.createContext(request) : createDefaultContext(request);
|
|
3407
|
+
if (context.authSession !== void 0) {
|
|
3408
|
+
toolContext.auth = context.authSession;
|
|
3409
|
+
}
|
|
3410
|
+
return executePrompt(loaded.prompt, params?.arguments ?? {}, toPromptContext(toolContext));
|
|
3411
|
+
}
|
|
3412
|
+
/** Accepts notifications without side effects for client compatibility. */
|
|
3413
|
+
async handleNotification(_request) {
|
|
3414
|
+
}
|
|
3415
|
+
};
|
|
3416
|
+
function createSidecarMcpServer(options) {
|
|
3417
|
+
return new SidecarMcpServer(options);
|
|
3418
|
+
}
|
|
3419
|
+
function createSidecarHttpServer(options) {
|
|
3420
|
+
const mcp = createSidecarMcpServer(options);
|
|
3421
|
+
const endpoint = options.path ?? "/mcp";
|
|
3422
|
+
const maxBodyBytes = options.maxBodyBytes ?? 1e6;
|
|
3423
|
+
return createServer(async (request, response) => {
|
|
3424
|
+
if (isRejectedOrigin(request, options.allowedOrigins)) {
|
|
3425
|
+
response.writeHead(403, { "content-type": "application/json" });
|
|
3426
|
+
response.end(JSON.stringify({ error: "forbidden_origin" }));
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3429
|
+
const proxyResult = await runProxy(options.proxy, request);
|
|
3430
|
+
if (proxyResult) {
|
|
3431
|
+
response.writeHead(proxyResult.status, proxyResult.headers);
|
|
3432
|
+
response.end(proxyResult.body ?? "");
|
|
3433
|
+
return;
|
|
3434
|
+
}
|
|
3435
|
+
const pathname = request.url?.split("?")[0];
|
|
3436
|
+
if (options.auth && request.method === "GET" && isProtectedResourceMetadataPath(pathname, endpoint)) {
|
|
3437
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
3438
|
+
response.end(JSON.stringify(options.auth.metadata()));
|
|
3439
|
+
return;
|
|
3440
|
+
}
|
|
3441
|
+
if (request.method === "GET" && pathname === endpoint) {
|
|
3442
|
+
response.writeHead(405, { "content-type": "application/json" });
|
|
3443
|
+
response.end(JSON.stringify({ error: "sse_not_supported" }));
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
if (request.method !== "POST" || pathname !== endpoint) {
|
|
3447
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
3448
|
+
response.end(JSON.stringify({ error: "not_found" }));
|
|
3449
|
+
return;
|
|
3450
|
+
}
|
|
3451
|
+
try {
|
|
3452
|
+
validateProtocolVersion(request);
|
|
3453
|
+
validatePostHeaders(request);
|
|
3454
|
+
const body = await readJson(request, maxBodyBytes);
|
|
3455
|
+
if (Array.isArray(body)) {
|
|
3456
|
+
throw new JsonRpcHttpError(400, -32600, "JSON-RPC batches are not supported by MCP Streamable HTTP.");
|
|
3457
|
+
}
|
|
3458
|
+
if (isJsonRpcResponseMessage(body)) {
|
|
3459
|
+
response.writeHead(202);
|
|
3460
|
+
response.end();
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
const rpcRequest = assertJsonRpcRequest(body);
|
|
3464
|
+
const fetchRequest = toFetchRequest(request, options.publicUrl ?? options.auth?.resource);
|
|
3465
|
+
const authSession = options.auth ? await authorizeHttpRequest(options.auth, fetchRequest, response) : void 0;
|
|
3466
|
+
if (options.auth && authSession === AUTH_RESPONSE_SENT) {
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
const payload = await mcp.handle(rpcRequest, {
|
|
3470
|
+
request: fetchRequest,
|
|
3471
|
+
authSession
|
|
3472
|
+
}) ?? null;
|
|
3473
|
+
const responses = payload === null ? [] : [payload];
|
|
3474
|
+
if (!responses.length) {
|
|
3475
|
+
response.writeHead(202);
|
|
3476
|
+
response.end();
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
const authError = httpAuthError(payload);
|
|
3480
|
+
response.writeHead(authError?.status ?? 200, {
|
|
3481
|
+
"content-type": "application/json",
|
|
3482
|
+
...authError?.headers ?? {}
|
|
3483
|
+
});
|
|
3484
|
+
response.end(JSON.stringify(payload));
|
|
3485
|
+
} catch (error) {
|
|
3486
|
+
const status = error instanceof JsonRpcHttpError ? error.status : 400;
|
|
3487
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
3488
|
+
response.end(
|
|
3489
|
+
JSON.stringify({
|
|
3490
|
+
jsonrpc: JSONRPC_VERSION,
|
|
3491
|
+
id: null,
|
|
3492
|
+
error: normalizeHttpError(error)
|
|
3493
|
+
})
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
var DEFAULT_ALLOWED_ORIGINS = [
|
|
3499
|
+
"https://chatgpt.com",
|
|
3500
|
+
"https://chat.openai.com",
|
|
3501
|
+
"https://claude.ai",
|
|
3502
|
+
"https://*.claude.ai"
|
|
3503
|
+
];
|
|
3504
|
+
function isRejectedOrigin(request, allowedOrigins = []) {
|
|
3505
|
+
const origin = request.headers.origin;
|
|
3506
|
+
if (!origin) {
|
|
3507
|
+
return false;
|
|
3508
|
+
}
|
|
3509
|
+
const host = request.headers.host ?? "";
|
|
3510
|
+
let originUrl;
|
|
3511
|
+
try {
|
|
3512
|
+
originUrl = new URL(origin);
|
|
3513
|
+
} catch {
|
|
3514
|
+
return true;
|
|
3515
|
+
}
|
|
3516
|
+
if (isLocalHost(host)) {
|
|
3517
|
+
return !isLocalHost(originUrl.host);
|
|
3518
|
+
}
|
|
3519
|
+
const requestOrigin = `https://${host}`;
|
|
3520
|
+
const allowed = [requestOrigin, ...DEFAULT_ALLOWED_ORIGINS, ...allowedOrigins];
|
|
3521
|
+
return !allowed.some((pattern) => matchesOrigin(pattern, originUrl.origin));
|
|
3522
|
+
}
|
|
3523
|
+
function isLocalHost(host) {
|
|
3524
|
+
if (host.startsWith("[::1]")) {
|
|
3525
|
+
return true;
|
|
3526
|
+
}
|
|
3527
|
+
const hostname = host.split(":")[0]?.toLowerCase();
|
|
3528
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
3529
|
+
}
|
|
3530
|
+
function matchesOrigin(pattern, origin) {
|
|
3531
|
+
if (pattern === origin) {
|
|
3532
|
+
return true;
|
|
3533
|
+
}
|
|
3534
|
+
if (!pattern.includes("*")) {
|
|
3535
|
+
return false;
|
|
3536
|
+
}
|
|
3537
|
+
const escaped = pattern.split("*").map(escapeRegExp).join("[^.]+");
|
|
3538
|
+
return new RegExp(`^${escaped}$`).test(origin);
|
|
3539
|
+
}
|
|
3540
|
+
function escapeRegExp(value) {
|
|
3541
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3542
|
+
}
|
|
3543
|
+
function isProtectedResourceMetadataPath(pathname, endpoint) {
|
|
3544
|
+
return pathname === "/.well-known/oauth-protected-resource" || pathname === `/.well-known/oauth-protected-resource${endpoint}`;
|
|
3545
|
+
}
|
|
3546
|
+
var JsonRpcError = class extends Error {
|
|
3547
|
+
constructor(code, message, data) {
|
|
3548
|
+
super(message);
|
|
3549
|
+
this.code = code;
|
|
3550
|
+
this.data = data;
|
|
3551
|
+
this.name = "JsonRpcError";
|
|
3552
|
+
}
|
|
3553
|
+
code;
|
|
3554
|
+
data;
|
|
3555
|
+
};
|
|
3556
|
+
var JsonRpcHttpError = class extends JsonRpcError {
|
|
3557
|
+
constructor(status, code, message, data) {
|
|
3558
|
+
super(code, message, data);
|
|
3559
|
+
this.status = status;
|
|
3560
|
+
this.name = "JsonRpcHttpError";
|
|
3561
|
+
}
|
|
3562
|
+
status;
|
|
3563
|
+
};
|
|
3564
|
+
var AUTH_RESPONSE_SENT = /* @__PURE__ */ Symbol("sidecar.auth.response-sent");
|
|
3565
|
+
async function authorizeHttpRequest(auth, request, response) {
|
|
3566
|
+
const result = await auth.authorizeRequest(request);
|
|
3567
|
+
if (result.ok) {
|
|
3568
|
+
return result.auth;
|
|
3569
|
+
}
|
|
3570
|
+
response.writeHead(result.status, {
|
|
3571
|
+
"content-type": "application/json",
|
|
3572
|
+
...headersToRecord(result.headers)
|
|
3573
|
+
});
|
|
3574
|
+
response.end(
|
|
3575
|
+
JSON.stringify({
|
|
3576
|
+
jsonrpc: JSONRPC_VERSION,
|
|
3577
|
+
id: null,
|
|
3578
|
+
error: {
|
|
3579
|
+
code: result.status === 401 ? -32001 : -32003,
|
|
3580
|
+
message: result.body.error_description ?? result.body.error,
|
|
3581
|
+
data: {
|
|
3582
|
+
status: result.status,
|
|
3583
|
+
body: result.body
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
})
|
|
3587
|
+
);
|
|
3588
|
+
return AUTH_RESPONSE_SENT;
|
|
3589
|
+
}
|
|
3590
|
+
function createDefaultContext(request) {
|
|
3591
|
+
const memory = /* @__PURE__ */ new Map();
|
|
3592
|
+
return {
|
|
3593
|
+
auth: void 0,
|
|
3594
|
+
request: {
|
|
3595
|
+
id: String(request.id ?? randomUUID2()),
|
|
3596
|
+
signal: new AbortController().signal,
|
|
3597
|
+
host: "unknown",
|
|
3598
|
+
transport: "streamable-http"
|
|
3599
|
+
},
|
|
3600
|
+
services: {},
|
|
3601
|
+
tools: {},
|
|
3602
|
+
log: consoleLogger,
|
|
3603
|
+
trace: {
|
|
3604
|
+
async span(_name, run) {
|
|
3605
|
+
return run();
|
|
3606
|
+
}
|
|
3607
|
+
},
|
|
3608
|
+
storage: {
|
|
3609
|
+
async get(key) {
|
|
3610
|
+
return memory.get(key);
|
|
3611
|
+
},
|
|
3612
|
+
async set(key, value) {
|
|
3613
|
+
memory.set(key, value);
|
|
3614
|
+
},
|
|
3615
|
+
async delete(key) {
|
|
3616
|
+
memory.delete(key);
|
|
3617
|
+
}
|
|
3618
|
+
},
|
|
3619
|
+
env: process.env
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
function toResourceContext(ctx) {
|
|
3623
|
+
return {
|
|
3624
|
+
auth: ctx.auth,
|
|
3625
|
+
request: ctx.request,
|
|
3626
|
+
services: ctx.services,
|
|
3627
|
+
log: ctx.log,
|
|
3628
|
+
storage: ctx.storage,
|
|
3629
|
+
env: ctx.env
|
|
3630
|
+
};
|
|
3631
|
+
}
|
|
3632
|
+
function toPromptContext(ctx) {
|
|
3633
|
+
return {
|
|
3634
|
+
auth: ctx.auth,
|
|
3635
|
+
request: ctx.request,
|
|
3636
|
+
services: ctx.services,
|
|
3637
|
+
log: ctx.log,
|
|
3638
|
+
storage: ctx.storage,
|
|
3639
|
+
env: ctx.env
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
function readCursorParam(request) {
|
|
3643
|
+
const params = request.params;
|
|
3644
|
+
if (params?.cursor === void 0) {
|
|
3645
|
+
return void 0;
|
|
3646
|
+
}
|
|
3647
|
+
if (typeof params.cursor !== "string" || !params.cursor) {
|
|
3648
|
+
throw new JsonRpcError(-32602, "Pagination cursor must be a non-empty string.");
|
|
3649
|
+
}
|
|
3650
|
+
return params.cursor;
|
|
3651
|
+
}
|
|
3652
|
+
function readUriParam(request, method) {
|
|
3653
|
+
const params = request.params;
|
|
3654
|
+
const uri = typeof params?.uri === "string" ? params.uri : void 0;
|
|
3655
|
+
if (!uri) {
|
|
3656
|
+
throw new JsonRpcError(-32602, `${method} requires params.uri.`);
|
|
3657
|
+
}
|
|
3658
|
+
return uri;
|
|
3659
|
+
}
|
|
3660
|
+
function selectPaginationOverride(override, operation) {
|
|
3661
|
+
if (!override) {
|
|
3662
|
+
return void 0;
|
|
3663
|
+
}
|
|
3664
|
+
if (typeof override === "function") {
|
|
3665
|
+
return override;
|
|
3666
|
+
}
|
|
3667
|
+
const key = operationToPaginationKey(operation);
|
|
3668
|
+
return override[key] ?? override.default;
|
|
3669
|
+
}
|
|
3670
|
+
function operationToPaginationKey(operation) {
|
|
3671
|
+
switch (operation) {
|
|
3672
|
+
case "tools/list":
|
|
3673
|
+
return "toolsList";
|
|
3674
|
+
case "resources/list":
|
|
3675
|
+
return "resourcesList";
|
|
3676
|
+
case "resources/templates/list":
|
|
3677
|
+
return "resourceTemplatesList";
|
|
3678
|
+
case "prompts/list":
|
|
3679
|
+
return "promptsList";
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
function defaultPagination(items, cursor, pageSize) {
|
|
3683
|
+
const offset = cursor ? decodeCursor(cursor) : 0;
|
|
3684
|
+
const page = items.slice(offset, offset + pageSize);
|
|
3685
|
+
const nextOffset = offset + page.length;
|
|
3686
|
+
return {
|
|
3687
|
+
items: page,
|
|
3688
|
+
nextCursor: nextOffset < items.length ? encodeCursor(nextOffset) : void 0
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
function encodeCursor(offset) {
|
|
3692
|
+
return Buffer.from(JSON.stringify({ offset }), "utf8").toString("base64url");
|
|
3693
|
+
}
|
|
3694
|
+
function decodeCursor(cursor) {
|
|
3695
|
+
try {
|
|
3696
|
+
const decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
3697
|
+
const offset = decoded.offset;
|
|
3698
|
+
if (!Number.isInteger(offset) || typeof offset !== "number" || offset < 0) {
|
|
3699
|
+
throw new Error("invalid offset");
|
|
3700
|
+
}
|
|
3701
|
+
return offset;
|
|
3702
|
+
} catch {
|
|
3703
|
+
throw new JsonRpcError(-32602, "Invalid pagination cursor.");
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
function authJsonRpcError(result) {
|
|
3707
|
+
return new JsonRpcError(
|
|
3708
|
+
result.status === 401 ? -32001 : -32003,
|
|
3709
|
+
result.body.error_description ?? result.body.error,
|
|
3710
|
+
{
|
|
3711
|
+
status: result.status,
|
|
3712
|
+
headers: headersToRecord(result.headers),
|
|
3713
|
+
body: result.body
|
|
3714
|
+
}
|
|
3715
|
+
);
|
|
3716
|
+
}
|
|
3717
|
+
function validateProtocolVersion(request) {
|
|
3718
|
+
const value = request.headers["mcp-protocol-version"];
|
|
3719
|
+
const version = Array.isArray(value) ? value[0] : value;
|
|
3720
|
+
if (!version) {
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
const supported = /* @__PURE__ */ new Set([SIDECAR_MCP_PROTOCOL_VERSION]);
|
|
3724
|
+
if (!supported.has(version)) {
|
|
3725
|
+
throw new JsonRpcHttpError(
|
|
3726
|
+
400,
|
|
3727
|
+
-32600,
|
|
3728
|
+
`Unsupported MCP-Protocol-Version "${version}".`
|
|
3729
|
+
);
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
function validatePostHeaders(request) {
|
|
3733
|
+
const contentType = request.headers["content-type"];
|
|
3734
|
+
const contentTypeValue = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
3735
|
+
if (!contentTypeValue?.toLowerCase().includes("application/json")) {
|
|
3736
|
+
throw new JsonRpcHttpError(415, -32600, "POST Content-Type must be application/json.");
|
|
3737
|
+
}
|
|
3738
|
+
const accept = request.headers.accept;
|
|
3739
|
+
const acceptValue = Array.isArray(accept) ? accept.join(",") : accept;
|
|
3740
|
+
if (!acceptValue || !acceptValue.toLowerCase().includes("application/json") || !acceptValue.toLowerCase().includes("text/event-stream")) {
|
|
3741
|
+
throw new JsonRpcHttpError(
|
|
3742
|
+
406,
|
|
3743
|
+
-32600,
|
|
3744
|
+
"POST Accept must include application/json and text/event-stream."
|
|
3745
|
+
);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
async function withTimeout(promise, timeoutMs, controller) {
|
|
3749
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
3750
|
+
return promise;
|
|
3751
|
+
}
|
|
3752
|
+
let timeout;
|
|
3753
|
+
try {
|
|
3754
|
+
return await Promise.race([
|
|
3755
|
+
promise,
|
|
3756
|
+
new Promise((_resolve, reject) => {
|
|
3757
|
+
timeout = setTimeout(() => {
|
|
3758
|
+
controller.abort();
|
|
3759
|
+
reject(new JsonRpcError(-32e3, "Tool execution timed out."));
|
|
3760
|
+
}, timeoutMs);
|
|
3761
|
+
})
|
|
3762
|
+
]);
|
|
3763
|
+
} finally {
|
|
3764
|
+
if (timeout) {
|
|
3765
|
+
clearTimeout(timeout);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
function validateAgainstSchema(schema, value, message, options = {}) {
|
|
3770
|
+
if (!schema || value === void 0 && options.optional) {
|
|
3771
|
+
return value;
|
|
3772
|
+
}
|
|
3773
|
+
const failure = schemaFailure(schema, value, "$");
|
|
3774
|
+
if (failure) {
|
|
3775
|
+
throw new JsonRpcError(-32602, message, { validation: failure });
|
|
3776
|
+
}
|
|
3777
|
+
return value;
|
|
3778
|
+
}
|
|
3779
|
+
function schemaFailure(schema, value, path19) {
|
|
3780
|
+
if (schema.anyOf?.length && !schema.anyOf.some((entry) => !schemaFailure(entry, value, path19))) {
|
|
3781
|
+
return `${path19} must match one anyOf schema.`;
|
|
3782
|
+
}
|
|
3783
|
+
if (schema.oneOf?.length && schema.oneOf.filter((entry) => !schemaFailure(entry, value, path19)).length !== 1) {
|
|
3784
|
+
return `${path19} must match exactly one oneOf schema.`;
|
|
3785
|
+
}
|
|
3786
|
+
if (schema.allOf?.length) {
|
|
3787
|
+
for (const entry of schema.allOf) {
|
|
3788
|
+
const failure = schemaFailure(entry, value, path19);
|
|
3789
|
+
if (failure) return failure;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
if (schema.const !== void 0 && value !== schema.const) {
|
|
3793
|
+
return `${path19} must equal ${JSON.stringify(schema.const)}.`;
|
|
3794
|
+
}
|
|
3795
|
+
if (schema.enum && !schema.enum.some((entry) => JSON.stringify(entry) === JSON.stringify(value))) {
|
|
3796
|
+
return `${path19} must be one of the declared enum values.`;
|
|
3797
|
+
}
|
|
3798
|
+
if (schema.type) {
|
|
3799
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
3800
|
+
if (!types.some((type) => matchesJsonSchemaType(type, value))) {
|
|
3801
|
+
return `${path19} must be ${types.join(" or ")}.`;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
if (schema.type === "object" || schema.properties || schema.required) {
|
|
3805
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3806
|
+
return `${path19} must be an object.`;
|
|
3807
|
+
}
|
|
3808
|
+
const record = value;
|
|
3809
|
+
for (const required of schema.required ?? []) {
|
|
3810
|
+
if (!(required in record)) {
|
|
3811
|
+
return `${path19}.${required} is required.`;
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
for (const [key, propertySchema] of Object.entries(schema.properties ?? {})) {
|
|
3815
|
+
if (key in record) {
|
|
3816
|
+
const failure = schemaFailure(propertySchema, record[key], `${path19}.${key}`);
|
|
3817
|
+
if (failure) return failure;
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
if (schema.additionalProperties === false) {
|
|
3821
|
+
const allowed = new Set(Object.keys(schema.properties ?? {}));
|
|
3822
|
+
const extra = Object.keys(record).find((key) => !allowed.has(key));
|
|
3823
|
+
if (extra) {
|
|
3824
|
+
return `${path19}.${extra} is not allowed.`;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
if (schema.type === "array" || schema.items) {
|
|
3829
|
+
if (!Array.isArray(value)) {
|
|
3830
|
+
return `${path19} must be an array.`;
|
|
3831
|
+
}
|
|
3832
|
+
if (schema.items) {
|
|
3833
|
+
for (const [index, entry] of value.entries()) {
|
|
3834
|
+
const failure = schemaFailure(schema.items, entry, `${path19}[${index}]`);
|
|
3835
|
+
if (failure) return failure;
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
if (typeof value === "string") {
|
|
3840
|
+
if (schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
3841
|
+
return `${path19} is shorter than ${schema.minLength}.`;
|
|
3842
|
+
}
|
|
3843
|
+
if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
|
|
3844
|
+
return `${path19} is longer than ${schema.maxLength}.`;
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
if (typeof value === "number") {
|
|
3848
|
+
if (schema.minimum !== void 0 && value < schema.minimum) {
|
|
3849
|
+
return `${path19} is less than ${schema.minimum}.`;
|
|
3850
|
+
}
|
|
3851
|
+
if (schema.maximum !== void 0 && value > schema.maximum) {
|
|
3852
|
+
return `${path19} is greater than ${schema.maximum}.`;
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
return void 0;
|
|
3856
|
+
}
|
|
3857
|
+
function matchesJsonSchemaType(type, value) {
|
|
3858
|
+
switch (type) {
|
|
3859
|
+
case "object":
|
|
3860
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
3861
|
+
case "array":
|
|
3862
|
+
return Array.isArray(value);
|
|
3863
|
+
case "string":
|
|
3864
|
+
return typeof value === "string";
|
|
3865
|
+
case "number":
|
|
3866
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
3867
|
+
case "integer":
|
|
3868
|
+
return Number.isInteger(value);
|
|
3869
|
+
case "boolean":
|
|
3870
|
+
return typeof value === "boolean";
|
|
3871
|
+
case "null":
|
|
3872
|
+
return value === null;
|
|
3873
|
+
default:
|
|
3874
|
+
return true;
|
|
3875
|
+
}
|
|
3876
|
+
}
|
|
3877
|
+
function headersToRecord(headers) {
|
|
3878
|
+
const record = {};
|
|
3879
|
+
headers.forEach((value, key) => {
|
|
3880
|
+
record[key] = value;
|
|
3881
|
+
});
|
|
3882
|
+
return record;
|
|
3883
|
+
}
|
|
3884
|
+
function stripUndefined4(value) {
|
|
3885
|
+
return Object.fromEntries(
|
|
3886
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0)
|
|
3887
|
+
);
|
|
3888
|
+
}
|
|
3889
|
+
function httpAuthError(value) {
|
|
3890
|
+
if (!value || typeof value !== "object" || !("error" in value)) {
|
|
3891
|
+
return void 0;
|
|
3892
|
+
}
|
|
3893
|
+
const error = value.error;
|
|
3894
|
+
const data = error?.data;
|
|
3895
|
+
if (!data || typeof data !== "object") {
|
|
3896
|
+
return void 0;
|
|
3897
|
+
}
|
|
3898
|
+
const status = data.status;
|
|
3899
|
+
const headers = data.headers;
|
|
3900
|
+
if (status !== 401 && status !== 403 || !headers || typeof headers !== "object") {
|
|
3901
|
+
return void 0;
|
|
3902
|
+
}
|
|
3903
|
+
return {
|
|
3904
|
+
status,
|
|
3905
|
+
headers
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
var consoleLogger = {
|
|
3909
|
+
debug(message, data) {
|
|
3910
|
+
console.debug(message, data ?? "");
|
|
3911
|
+
},
|
|
3912
|
+
info(message, data) {
|
|
3913
|
+
console.info(message, data ?? "");
|
|
3914
|
+
},
|
|
3915
|
+
warn(message, data) {
|
|
3916
|
+
console.warn(message, data ?? "");
|
|
3917
|
+
},
|
|
3918
|
+
error(message, data) {
|
|
3919
|
+
console.error(message, data ?? "");
|
|
3920
|
+
}
|
|
3921
|
+
};
|
|
3922
|
+
async function readJson(request, maxBodyBytes) {
|
|
3923
|
+
const chunks = [];
|
|
3924
|
+
let size = 0;
|
|
3925
|
+
for await (const chunk of request) {
|
|
3926
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3927
|
+
size += buffer.byteLength;
|
|
3928
|
+
if (size > maxBodyBytes) {
|
|
3929
|
+
throw new JsonRpcHttpError(413, -32600, "Request body is too large.");
|
|
3930
|
+
}
|
|
3931
|
+
chunks.push(buffer);
|
|
3932
|
+
}
|
|
3933
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
3934
|
+
}
|
|
3935
|
+
function toFetchRequest(request, baseUrl) {
|
|
3936
|
+
const headers = new Headers();
|
|
3937
|
+
for (const [key, value] of Object.entries(request.headers)) {
|
|
3938
|
+
if (Array.isArray(value)) {
|
|
3939
|
+
for (const entry of value) {
|
|
3940
|
+
headers.append(key, entry);
|
|
3941
|
+
}
|
|
3942
|
+
} else if (value !== void 0) {
|
|
3943
|
+
headers.set(key, value);
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
const url = baseUrl ? new URL(request.url ?? "/", baseUrl) : new URL(
|
|
3947
|
+
request.url ?? "/",
|
|
3948
|
+
`${headers.get("x-forwarded-proto") ?? "http"}://${headers.get("host") ?? "127.0.0.1"}`
|
|
3949
|
+
);
|
|
3950
|
+
return new Request(url, {
|
|
3951
|
+
headers,
|
|
3952
|
+
method: request.method
|
|
3953
|
+
});
|
|
3954
|
+
}
|
|
3955
|
+
function assertJsonRpcRequest(value) {
|
|
3956
|
+
if (!value || typeof value !== "object") {
|
|
3957
|
+
throw new JsonRpcError(-32600, "Request must be a JSON object.");
|
|
3958
|
+
}
|
|
3959
|
+
const record = value;
|
|
3960
|
+
if (record.jsonrpc !== JSONRPC_VERSION || typeof record.method !== "string") {
|
|
3961
|
+
throw new JsonRpcError(-32600, "Invalid JSON-RPC request.");
|
|
3962
|
+
}
|
|
3963
|
+
if ("id" in record && record.id !== void 0 && record.id !== null && typeof record.id !== "string" && typeof record.id !== "number") {
|
|
3964
|
+
throw new JsonRpcError(-32600, "JSON-RPC id must be a string, number, or null.");
|
|
3965
|
+
}
|
|
3966
|
+
return {
|
|
3967
|
+
jsonrpc: JSONRPC_VERSION,
|
|
3968
|
+
id: typeof record.id === "string" || typeof record.id === "number" || record.id === null ? record.id : void 0,
|
|
3969
|
+
method: record.method,
|
|
3970
|
+
params: record.params
|
|
3971
|
+
};
|
|
3972
|
+
}
|
|
3973
|
+
function isJsonRpcResponseMessage(value) {
|
|
3974
|
+
if (!value || typeof value !== "object") {
|
|
3975
|
+
return false;
|
|
3976
|
+
}
|
|
3977
|
+
const record = value;
|
|
3978
|
+
if (record.jsonrpc !== JSONRPC_VERSION || !("id" in record) || !("result" in record || "error" in record)) {
|
|
3979
|
+
return false;
|
|
3980
|
+
}
|
|
3981
|
+
return record.id === null || typeof record.id === "string" || typeof record.id === "number";
|
|
3982
|
+
}
|
|
3983
|
+
function normalizeError(error) {
|
|
3984
|
+
if (error instanceof JsonRpcError) {
|
|
3985
|
+
return {
|
|
3986
|
+
code: error.code,
|
|
3987
|
+
message: error.message,
|
|
3988
|
+
data: error.data
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
if (error instanceof SidecarRuntimeError && error.code === "invalid_pagination_cursor") {
|
|
3992
|
+
return {
|
|
3993
|
+
code: -32602,
|
|
3994
|
+
message: error.message
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
if (error instanceof Error) {
|
|
3998
|
+
return {
|
|
3999
|
+
code: -32e3,
|
|
4000
|
+
message: "Internal server error."
|
|
4001
|
+
};
|
|
4002
|
+
}
|
|
4003
|
+
return {
|
|
4004
|
+
code: -32e3,
|
|
4005
|
+
message: "Unknown server error.",
|
|
4006
|
+
data: error
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
function normalizeHttpError(error) {
|
|
4010
|
+
if (error instanceof JsonRpcError) {
|
|
4011
|
+
return {
|
|
4012
|
+
code: error.code,
|
|
4013
|
+
message: error.message,
|
|
4014
|
+
data: error.data
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
return {
|
|
4018
|
+
code: -32700,
|
|
4019
|
+
message: "Invalid JSON request body."
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
// packages/cli/src/tunnel.ts
|
|
4024
|
+
import { spawn, spawnSync } from "child_process";
|
|
4025
|
+
import { createInterface } from "readline/promises";
|
|
4026
|
+
import { stdin, stdout } from "process";
|
|
4027
|
+
async function startTunnel(options) {
|
|
4028
|
+
const provider = await resolveProvider(options.provider);
|
|
4029
|
+
const localUrl = `http://127.0.0.1:${options.port}`;
|
|
4030
|
+
const path19 = options.path ?? "/mcp";
|
|
4031
|
+
if (provider === "cloudflared") {
|
|
4032
|
+
return startCloudflared(localUrl, path19, options.timeoutMs);
|
|
4033
|
+
}
|
|
4034
|
+
return startWrangler(localUrl, path19, options.timeoutMs);
|
|
4035
|
+
}
|
|
4036
|
+
function tunnelInstallMessage(provider = "auto") {
|
|
4037
|
+
if (provider === "wrangler") {
|
|
4038
|
+
return [
|
|
4039
|
+
"npx was not found on PATH, so Sidecar cannot run Wrangler quick-start.",
|
|
4040
|
+
"Install Node.js/npm, or install cloudflared directly:",
|
|
4041
|
+
" brew install cloudflared"
|
|
4042
|
+
].join("\n");
|
|
4043
|
+
}
|
|
4044
|
+
return [
|
|
4045
|
+
"cloudflared was not found on PATH.",
|
|
4046
|
+
"Install cloudflared for the fastest Sidecar tunnel path:",
|
|
4047
|
+
" brew install cloudflared",
|
|
4048
|
+
"Or run with Wrangler explicitly:",
|
|
4049
|
+
" sidecar dev --tunnel wrangler"
|
|
4050
|
+
].join("\n");
|
|
4051
|
+
}
|
|
4052
|
+
async function resolveProvider(provider) {
|
|
4053
|
+
if (provider === "cloudflared") {
|
|
4054
|
+
if (hasCommand("cloudflared")) {
|
|
4055
|
+
return "cloudflared";
|
|
4056
|
+
}
|
|
4057
|
+
if (await promptForCloudflaredInstall()) {
|
|
4058
|
+
await installCloudflared();
|
|
4059
|
+
return "cloudflared";
|
|
4060
|
+
}
|
|
4061
|
+
assertNpxAvailable();
|
|
4062
|
+
return "wrangler";
|
|
4063
|
+
}
|
|
4064
|
+
if (provider === "wrangler") {
|
|
4065
|
+
assertNpxAvailable();
|
|
4066
|
+
return "wrangler";
|
|
4067
|
+
}
|
|
4068
|
+
if (hasCommand("cloudflared")) {
|
|
4069
|
+
return "cloudflared";
|
|
4070
|
+
}
|
|
4071
|
+
const choice = await promptForMissingCloudflared();
|
|
4072
|
+
if (choice === "install") {
|
|
4073
|
+
await installCloudflared();
|
|
4074
|
+
return "cloudflared";
|
|
4075
|
+
}
|
|
4076
|
+
if (choice === "wrangler") {
|
|
4077
|
+
assertNpxAvailable();
|
|
4078
|
+
return "wrangler";
|
|
4079
|
+
}
|
|
4080
|
+
throw new Error("Cancelled HTTPS tunnel startup.");
|
|
4081
|
+
}
|
|
4082
|
+
async function promptForCloudflaredInstall() {
|
|
4083
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
4084
|
+
throw new Error(tunnelInstallMessage("cloudflared"));
|
|
4085
|
+
}
|
|
4086
|
+
const answer = await question([
|
|
4087
|
+
"cloudflared is not installed.",
|
|
4088
|
+
"Install cloudflared with Homebrew now? Answer no to continue with npx wrangler. [y/N] "
|
|
4089
|
+
].join("\n"));
|
|
4090
|
+
return answer.trim().toLowerCase().startsWith("y");
|
|
4091
|
+
}
|
|
4092
|
+
async function promptForMissingCloudflared() {
|
|
4093
|
+
if (!stdin.isTTY || !stdout.isTTY) {
|
|
4094
|
+
throw new Error(tunnelInstallMessage("auto"));
|
|
4095
|
+
}
|
|
4096
|
+
const answer = await question([
|
|
4097
|
+
"cloudflared is not installed.",
|
|
4098
|
+
"Choose how Sidecar should create an HTTPS MCP URL:",
|
|
4099
|
+
" [i] install cloudflared with Homebrew",
|
|
4100
|
+
" [w] continue with npx wrangler tunnel quick-start",
|
|
4101
|
+
" [c] cancel",
|
|
4102
|
+
"Selection [i/w/c]: "
|
|
4103
|
+
].join("\n"));
|
|
4104
|
+
const normalized = answer.trim().toLowerCase();
|
|
4105
|
+
if (normalized === "i" || normalized === "install") {
|
|
4106
|
+
return "install";
|
|
4107
|
+
}
|
|
4108
|
+
if (normalized === "w" || normalized === "wrangler") {
|
|
4109
|
+
return "wrangler";
|
|
4110
|
+
}
|
|
4111
|
+
return "cancel";
|
|
4112
|
+
}
|
|
4113
|
+
async function question(prompt) {
|
|
4114
|
+
const input = createInterface({ input: stdin, output: stdout });
|
|
4115
|
+
try {
|
|
4116
|
+
return await input.question(prompt);
|
|
4117
|
+
} finally {
|
|
4118
|
+
input.close();
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
async function installCloudflared() {
|
|
4122
|
+
if (!hasCommand("brew")) {
|
|
4123
|
+
throw new Error([
|
|
4124
|
+
"Sidecar can only install cloudflared automatically when Homebrew is available.",
|
|
4125
|
+
"Install cloudflared manually, then rerun:",
|
|
4126
|
+
" sidecar dev --tunnel",
|
|
4127
|
+
"Or continue without installing it:",
|
|
4128
|
+
" sidecar dev --tunnel wrangler"
|
|
4129
|
+
].join("\n"));
|
|
4130
|
+
}
|
|
4131
|
+
await runInherited("brew", ["install", "cloudflared"]);
|
|
4132
|
+
}
|
|
4133
|
+
function assertNpxAvailable() {
|
|
4134
|
+
if (!hasCommand("npx")) {
|
|
4135
|
+
throw new Error(tunnelInstallMessage("wrangler"));
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
async function startCloudflared(localUrl, path19, timeoutMs = 2e4) {
|
|
4139
|
+
const child = spawn("cloudflared", ["tunnel", "--url", localUrl, "--no-autoupdate"], {
|
|
4140
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4141
|
+
});
|
|
4142
|
+
const publicUrl = await waitForTunnelUrl(child, /https:\/\/[a-zA-Z0-9.-]+\.trycloudflare\.com/, timeoutMs);
|
|
4143
|
+
return {
|
|
4144
|
+
provider: "cloudflared",
|
|
4145
|
+
publicUrl,
|
|
4146
|
+
mcpUrl: appendPath(publicUrl, path19),
|
|
4147
|
+
close() {
|
|
4148
|
+
child.kill("SIGTERM");
|
|
4149
|
+
}
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
4152
|
+
async function startWrangler(localUrl, path19, timeoutMs = 2e4) {
|
|
4153
|
+
const child = spawn("npx", ["--yes", "wrangler", "tunnel", "quick-start", localUrl], {
|
|
4154
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4155
|
+
});
|
|
4156
|
+
const publicUrl = await waitForTunnelUrl(child, /https:\/\/[a-zA-Z0-9.-]+\.trycloudflare\.com/, timeoutMs);
|
|
4157
|
+
return {
|
|
4158
|
+
provider: "wrangler",
|
|
4159
|
+
publicUrl,
|
|
4160
|
+
mcpUrl: appendPath(publicUrl, path19),
|
|
4161
|
+
close() {
|
|
4162
|
+
child.kill("SIGTERM");
|
|
4163
|
+
}
|
|
4164
|
+
};
|
|
4165
|
+
}
|
|
4166
|
+
function waitForTunnelUrl(child, pattern, timeoutMs) {
|
|
4167
|
+
return new Promise((resolve, reject) => {
|
|
4168
|
+
let settled = false;
|
|
4169
|
+
const timeout = setTimeout(() => {
|
|
4170
|
+
if (!settled) {
|
|
4171
|
+
settled = true;
|
|
4172
|
+
child.kill("SIGTERM");
|
|
4173
|
+
reject(new Error("Timed out waiting for HTTPS tunnel URL."));
|
|
4174
|
+
}
|
|
4175
|
+
}, timeoutMs);
|
|
4176
|
+
const inspect = (chunk) => {
|
|
4177
|
+
const match = chunk.toString("utf8").match(pattern);
|
|
4178
|
+
if (match?.[0] && !settled) {
|
|
4179
|
+
settled = true;
|
|
4180
|
+
clearTimeout(timeout);
|
|
4181
|
+
resolve(match[0]);
|
|
4182
|
+
}
|
|
4183
|
+
};
|
|
4184
|
+
child.stdout?.on("data", inspect);
|
|
4185
|
+
child.stderr?.on("data", inspect);
|
|
4186
|
+
child.once("error", (error) => {
|
|
4187
|
+
if (!settled) {
|
|
4188
|
+
settled = true;
|
|
4189
|
+
clearTimeout(timeout);
|
|
4190
|
+
reject(error);
|
|
4191
|
+
}
|
|
4192
|
+
});
|
|
4193
|
+
child.once("exit", (code) => {
|
|
4194
|
+
if (!settled) {
|
|
4195
|
+
settled = true;
|
|
4196
|
+
clearTimeout(timeout);
|
|
4197
|
+
reject(new Error(`Tunnel process exited before producing a URL${code === null ? "" : ` with code ${code}`}.`));
|
|
4198
|
+
}
|
|
4199
|
+
});
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
function hasCommand(command) {
|
|
4203
|
+
return spawnSync("which", [command], { stdio: "ignore" }).status === 0;
|
|
4204
|
+
}
|
|
4205
|
+
function runInherited(command, args) {
|
|
4206
|
+
return new Promise((resolve, reject) => {
|
|
4207
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
4208
|
+
child.once("error", reject);
|
|
4209
|
+
child.once("exit", (code) => {
|
|
4210
|
+
if (code === 0) {
|
|
4211
|
+
resolve();
|
|
4212
|
+
} else {
|
|
4213
|
+
reject(new Error(`${command} ${args.join(" ")} exited with code ${code}.`));
|
|
4214
|
+
}
|
|
4215
|
+
});
|
|
4216
|
+
});
|
|
4217
|
+
}
|
|
4218
|
+
function appendPath(origin, path19) {
|
|
4219
|
+
return `${origin.replace(/\/+$/, "")}/${path19.replace(/^\/+/, "")}`;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
// packages/cli/src/index.ts
|
|
4223
|
+
async function main(argv) {
|
|
4224
|
+
const command = argv[2] ?? "help";
|
|
4225
|
+
const rootDir = readOption(argv, "--cwd") ?? cwd();
|
|
4226
|
+
switch (command) {
|
|
4227
|
+
case "build": {
|
|
4228
|
+
const target = readTarget(argv);
|
|
4229
|
+
const outDir = readOption(argv, "--out") ?? `out/${target}`;
|
|
4230
|
+
const plugins = argv.includes("--plugins");
|
|
4231
|
+
const strict = argv.includes("--strict");
|
|
4232
|
+
const manifest = await buildProject({ rootDir, outDir, plugins, strict, target });
|
|
4233
|
+
printDiagnostics(manifest.diagnostics ?? []);
|
|
4234
|
+
if (strict && manifest.diagnostics?.length) {
|
|
4235
|
+
exit(1);
|
|
4236
|
+
}
|
|
4237
|
+
console.log(
|
|
4238
|
+
`Built ${manifest.tools.length} ${target} tool${manifest.tools.length === 1 ? "" : "s"}, ${manifest.resources.length} resource${manifest.resources.length === 1 ? "" : "s"}, and ${manifest.prompts.length} prompt${manifest.prompts.length === 1 ? "" : "s"} to ${outDir}.`
|
|
4239
|
+
);
|
|
4240
|
+
if (plugins) {
|
|
4241
|
+
console.log("Built claude-plugin package.");
|
|
4242
|
+
}
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
case "check": {
|
|
4246
|
+
const target = readTarget(argv);
|
|
4247
|
+
const config = analyzeProjectConfig(rootDir);
|
|
4248
|
+
const tools = await analyzeProjectTools(rootDir, { target });
|
|
4249
|
+
const resources = await analyzeProjectResources(rootDir);
|
|
4250
|
+
const prompts = await analyzeProjectPrompts(rootDir);
|
|
4251
|
+
const diagnostics = await collectProjectDiagnostics(rootDir, {
|
|
4252
|
+
tools,
|
|
4253
|
+
resources,
|
|
4254
|
+
prompts,
|
|
4255
|
+
config
|
|
4256
|
+
});
|
|
4257
|
+
printDiagnostics(diagnostics);
|
|
4258
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error") || argv.includes("--strict") && diagnostics.length > 0) {
|
|
4259
|
+
exit(1);
|
|
4260
|
+
}
|
|
4261
|
+
if (!diagnostics.length) {
|
|
4262
|
+
console.log("No Sidecar diagnostics.");
|
|
4263
|
+
}
|
|
4264
|
+
return;
|
|
4265
|
+
}
|
|
4266
|
+
case "dev": {
|
|
4267
|
+
const port = Number(readOption(argv, "--port") ?? "3001");
|
|
4268
|
+
const target = readTarget(argv);
|
|
4269
|
+
const tunnelProvider = readTunnelProvider(argv);
|
|
4270
|
+
const outDir = `.sidecar/dev/${target}`;
|
|
4271
|
+
const manifest = await buildProject({ rootDir, outDir, target });
|
|
4272
|
+
printDiagnostics(manifest.diagnostics ?? []);
|
|
4273
|
+
const tools = await loadRuntimeTools(rootDir, manifest);
|
|
4274
|
+
let tunnel;
|
|
4275
|
+
if (tunnelProvider) {
|
|
4276
|
+
tunnel = await startTunnel({ provider: tunnelProvider, port, path: "/mcp" });
|
|
4277
|
+
process.env.SIDECAR_MCP_URL = tunnel.mcpUrl;
|
|
4278
|
+
}
|
|
4279
|
+
const loadedAuth = await loadRuntimeAuth(rootDir);
|
|
4280
|
+
const runtimeConfig = await loadRuntimeConfig(rootDir);
|
|
4281
|
+
const auth = loadedAuth && tunnel ? loadedAuth.withResource(tunnel.mcpUrl) : loadedAuth;
|
|
4282
|
+
const proxy = await loadRuntimeProxy(rootDir);
|
|
4283
|
+
const resources = await loadResources(rootDir, outDir, manifest);
|
|
4284
|
+
const prompts = await loadRuntimePrompts(rootDir, manifest);
|
|
4285
|
+
const server = createSidecarHttpServer({
|
|
4286
|
+
name: "sidecar-dev",
|
|
4287
|
+
version: "0.0.0-dev",
|
|
4288
|
+
path: "/mcp",
|
|
4289
|
+
auth,
|
|
4290
|
+
proxy,
|
|
4291
|
+
tools,
|
|
4292
|
+
resources,
|
|
4293
|
+
prompts,
|
|
4294
|
+
capabilities: {
|
|
4295
|
+
tools: runtimeConfig?.tools ?? manifest.config.tools,
|
|
4296
|
+
resources: runtimeConfig?.resources ?? manifest.config.resources,
|
|
4297
|
+
prompts: runtimeConfig?.prompts ?? manifest.config.prompts
|
|
4298
|
+
},
|
|
4299
|
+
pagination: runtimeConfig?.pagination ?? {
|
|
4300
|
+
pageSize: manifest.config.pagination.pageSize
|
|
4301
|
+
}
|
|
4302
|
+
});
|
|
4303
|
+
server.listen(port, "127.0.0.1", () => {
|
|
4304
|
+
console.log(`MCP running on Streamable HTTP (${target}) at http://127.0.0.1:${port}/mcp`);
|
|
4305
|
+
console.log(`Loaded ${tools.length} tool${tools.length === 1 ? "" : "s"}, ${resources.length} resource${resources.length === 1 ? "" : "s"}, and ${prompts.length} prompt${prompts.length === 1 ? "" : "s"}.`);
|
|
4306
|
+
if (tunnel) {
|
|
4307
|
+
console.log(`HTTPS tunnel (${tunnel.provider}) ready: ${tunnel.mcpUrl}`);
|
|
4308
|
+
console.log("Use this HTTPS MCP URL in ChatGPT, Claude, or a Claude plugin install.");
|
|
4309
|
+
}
|
|
4310
|
+
});
|
|
4311
|
+
const shutdown = () => {
|
|
4312
|
+
tunnel?.close();
|
|
4313
|
+
server.close(() => exit(0));
|
|
4314
|
+
};
|
|
4315
|
+
process.once("SIGINT", shutdown);
|
|
4316
|
+
process.once("SIGTERM", shutdown);
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
case "inspect": {
|
|
4320
|
+
const target = readTarget(argv);
|
|
4321
|
+
const tools = await analyzeProjectTools(rootDir, { target });
|
|
4322
|
+
if (!tools.length) {
|
|
4323
|
+
console.log("No Sidecar tools found.");
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
4326
|
+
for (const tool of tools) {
|
|
4327
|
+
console.log(`${tool.id} \u2014 ${tool.name} (${tool.variant}, ${tool.target})`);
|
|
4328
|
+
console.log(` ${tool.description}`);
|
|
4329
|
+
console.log(` source: ${tool.sourceFile}`);
|
|
4330
|
+
}
|
|
4331
|
+
return;
|
|
4332
|
+
}
|
|
4333
|
+
case "preview": {
|
|
4334
|
+
if (argv[3] !== "components") {
|
|
4335
|
+
throw new Error("Only `sidecar preview components` is supported right now.");
|
|
4336
|
+
}
|
|
4337
|
+
await previewComponents({
|
|
4338
|
+
rootDir,
|
|
4339
|
+
host: readOption(argv, "--host") ?? "chatgpt",
|
|
4340
|
+
port: Number(readOption(argv, "--port") ?? "3102"),
|
|
4341
|
+
compare: readOption(argv, "--compare") ?? "native,openai",
|
|
4342
|
+
componentSet: readPreviewComponentSet(readOption(argv, "--components")),
|
|
4343
|
+
themes: readPreviewThemes(readOption(argv, "--theme")),
|
|
4344
|
+
approve: !argv.includes("--no-approve")
|
|
4345
|
+
});
|
|
4346
|
+
return;
|
|
4347
|
+
}
|
|
4348
|
+
case "help":
|
|
4349
|
+
default:
|
|
4350
|
+
console.log(`Sidecar
|
|
4351
|
+
|
|
4352
|
+
Usage:
|
|
4353
|
+
sidecar build [--cwd <dir>] [--target mcp|chatgpt|claude] [--out <dir>] [--plugins] [--strict]
|
|
4354
|
+
sidecar check [--cwd <dir>] [--target mcp|chatgpt|claude] [--strict]
|
|
4355
|
+
sidecar dev [--cwd <dir>] [--target mcp|chatgpt|claude] [--port <port>] [--tunnel [cloudflared|wrangler]]
|
|
4356
|
+
sidecar inspect [--cwd <dir>] [--target mcp|chatgpt|claude]
|
|
4357
|
+
sidecar preview components [--cwd <dir>] [--host chatgpt|claude|generic] [--compare native,openai] [--components representative|all] [--theme light|dark|both] [--port <port>] [--no-approve]
|
|
4358
|
+
`);
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
function readTarget(argv) {
|
|
4362
|
+
const target = readOption(argv, "--target") ?? "mcp";
|
|
4363
|
+
if (target === "mcp" || target === "chatgpt" || target === "claude") {
|
|
4364
|
+
return target;
|
|
4365
|
+
}
|
|
4366
|
+
throw new Error(`Unsupported Sidecar target "${target}". Expected mcp, chatgpt, or claude.`);
|
|
4367
|
+
}
|
|
4368
|
+
async function previewComponents(options) {
|
|
4369
|
+
const cliDir = path18.dirname(fileURLToPath(import.meta.url));
|
|
4370
|
+
const css = await readFile8(path18.join(cliDir, "../../native/src/styles.css"), "utf8").catch(() => readFile8(path18.join(process.cwd(), "packages/native/src/styles.css"), "utf8")).catch(() => "");
|
|
4371
|
+
const html = renderComponentPreviewHtml(
|
|
4372
|
+
options.host,
|
|
4373
|
+
options.compare,
|
|
4374
|
+
css,
|
|
4375
|
+
options.themes,
|
|
4376
|
+
options.componentSet
|
|
4377
|
+
);
|
|
4378
|
+
const server = createServer2((_request, response) => {
|
|
4379
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
4380
|
+
response.end(html);
|
|
4381
|
+
});
|
|
4382
|
+
await new Promise((resolve) => {
|
|
4383
|
+
server.listen(options.port, "127.0.0.1", resolve);
|
|
4384
|
+
});
|
|
4385
|
+
const url = `http://127.0.0.1:${options.port}`;
|
|
4386
|
+
console.log(`Sidecar component preview running at ${url}`);
|
|
4387
|
+
console.log(`Compare set: ${options.compare}`);
|
|
4388
|
+
console.log(`Component set: ${options.componentSet}`);
|
|
4389
|
+
console.log(`Themes: ${options.themes.join(", ")}`);
|
|
4390
|
+
if (!options.approve || !stdin2.isTTY) {
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
const answer = await askYesNo(
|
|
4394
|
+
`Do the shared primitives look equivalent for ${options.host}? [y/N] `
|
|
4395
|
+
);
|
|
4396
|
+
if (answer) {
|
|
4397
|
+
await writeComponentApproval(options.rootDir, {
|
|
4398
|
+
host: options.host,
|
|
4399
|
+
compare: options.compare.split(",").map((entry) => entry.trim()).filter(Boolean),
|
|
4400
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4401
|
+
components: previewComponentNames(options.componentSet)
|
|
4402
|
+
});
|
|
4403
|
+
console.log("Recorded component parity approval.");
|
|
4404
|
+
} else {
|
|
4405
|
+
console.log("No approval recorded.");
|
|
4406
|
+
}
|
|
4407
|
+
server.close();
|
|
4408
|
+
}
|
|
4409
|
+
async function writeComponentApproval(rootDir, approval) {
|
|
4410
|
+
const dir = path18.join(rootDir, ".sidecar", "approvals");
|
|
4411
|
+
await mkdir9(dir, { recursive: true });
|
|
4412
|
+
await writeFile9(
|
|
4413
|
+
path18.join(dir, `components.${approval.host}.json`),
|
|
4414
|
+
`${JSON.stringify(approval, null, 2)}
|
|
4415
|
+
`
|
|
4416
|
+
);
|
|
4417
|
+
}
|
|
4418
|
+
async function askYesNo(question2) {
|
|
4419
|
+
const readline = createInterface2({ input: stdin2, output: stdout2 });
|
|
4420
|
+
try {
|
|
4421
|
+
const answer = await readline.question(question2);
|
|
4422
|
+
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
4423
|
+
} finally {
|
|
4424
|
+
readline.close();
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
function renderComponentPreviewHtml(host, compare, css, themes, componentSet) {
|
|
4428
|
+
const themeFrames = themes.map(
|
|
4429
|
+
(theme) => `<section class="theme-panel">
|
|
4430
|
+
<div class="theme-label">${escapeHtml2(theme)}</div>
|
|
4431
|
+
<iframe title="${escapeHtml2(theme)} component preview" srcdoc="${escapeHtml2(renderComponentPreviewFrame(host, compare, css, theme, componentSet))}"></iframe>
|
|
4432
|
+
</section>`
|
|
4433
|
+
).join("");
|
|
4434
|
+
return `<!doctype html>
|
|
4435
|
+
<html lang="en">
|
|
4436
|
+
<head>
|
|
4437
|
+
<meta charset="utf-8" />
|
|
4438
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
4439
|
+
<title>Sidecar component preview</title>
|
|
4440
|
+
<style>
|
|
4441
|
+
body { background: #f4f4f5; color: #18181b; font: 14px/1.4 ui-sans-serif, -apple-system, system-ui, "Segoe UI", sans-serif; margin: 0; padding: 18px; }
|
|
4442
|
+
.preview-shell { display: grid; gap: 18px; }
|
|
4443
|
+
.theme-panel { display: grid; gap: 8px; }
|
|
4444
|
+
.theme-label { font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; }
|
|
4445
|
+
iframe { background: transparent; border: 1px solid rgb(0 0 0 / 12%); border-radius: 10px; height: 82vh; min-height: 680px; width: 100%; }
|
|
4446
|
+
</style>
|
|
4447
|
+
</head>
|
|
4448
|
+
<body>
|
|
4449
|
+
<main class="preview-shell">${themeFrames}</main>
|
|
4450
|
+
</body>
|
|
4451
|
+
</html>`;
|
|
4452
|
+
}
|
|
4453
|
+
function renderComponentPreviewFrame(host, compare, css, theme, componentSet) {
|
|
4454
|
+
const columns = compare.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
4455
|
+
const cells = columns.map((column) => renderPreviewColumn(column, host, componentSet)).join("");
|
|
4456
|
+
return `<!doctype html>
|
|
4457
|
+
<html lang="en" data-sidecar-host="${escapeHtml2(host)}" data-sidecar-theme="${theme}">
|
|
4458
|
+
<head>
|
|
4459
|
+
<meta charset="utf-8" />
|
|
4460
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
4461
|
+
<title>Sidecar component preview</title>
|
|
4462
|
+
<style>
|
|
4463
|
+
${css}
|
|
4464
|
+
html, body { background: var(--sc-surface); }
|
|
4465
|
+
body { padding: 24px; }
|
|
4466
|
+
.preview-grid { display: grid; gap: 20px; grid-template-columns: repeat(${Math.max(columns.length, 1)}, minmax(240px, 1fr)); }
|
|
4467
|
+
.preview-column { display: flex; flex-direction: column; gap: 14px; }
|
|
4468
|
+
.preview-group { border-bottom: 1px solid var(--sc-border); display: grid; gap: 10px; padding-bottom: 14px; }
|
|
4469
|
+
.preview-group:last-child { border-bottom: 0; }
|
|
4470
|
+
.component-label { color: var(--sc-text-muted); font: 600 11px/1.2 var(--sc-font-sans); text-transform: uppercase; }
|
|
4471
|
+
.preview-row { align-items: start; display: grid; gap: 8px; }
|
|
4472
|
+
.preview-label { color: var(--sc-text-muted); font: 600 12px/1.2 var(--sc-font-sans); text-transform: uppercase; }
|
|
4473
|
+
</style>
|
|
4474
|
+
</head>
|
|
4475
|
+
<body>
|
|
4476
|
+
<main class="preview-grid">${cells}</main>
|
|
4477
|
+
</body>
|
|
4478
|
+
</html>`;
|
|
4479
|
+
}
|
|
4480
|
+
function renderPreviewColumn(column, host, componentSet) {
|
|
4481
|
+
const recipe = previewRecipe(column, host);
|
|
4482
|
+
const groups = componentSet === "all" ? renderAllPreviewGroups(recipe) : renderRepresentativePreviewGroups(recipe);
|
|
4483
|
+
return `<section class="preview-column">
|
|
4484
|
+
<div class="preview-label">${escapeHtml2(column)}</div>
|
|
4485
|
+
${groups}
|
|
4486
|
+
</section>`;
|
|
4487
|
+
}
|
|
4488
|
+
function previewRecipe(column, host) {
|
|
4489
|
+
switch (column) {
|
|
4490
|
+
case "native":
|
|
4491
|
+
return "auto";
|
|
4492
|
+
case "native-chatgpt":
|
|
4493
|
+
case "openai":
|
|
4494
|
+
return "chatgpt";
|
|
4495
|
+
case "native-claude":
|
|
4496
|
+
case "anthropic":
|
|
4497
|
+
return "claude";
|
|
4498
|
+
default:
|
|
4499
|
+
return host;
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
function renderRepresentativePreviewGroups(recipe) {
|
|
4503
|
+
return [
|
|
4504
|
+
previewGroup("Buttons", buttonsPreview(recipe)),
|
|
4505
|
+
previewGroup("Fields", fieldsPreview(recipe)),
|
|
4506
|
+
previewGroup("Choices", choicesPreview(recipe)),
|
|
4507
|
+
previewGroup("Feedback", feedbackPreview(recipe)),
|
|
4508
|
+
previewGroup("Loading", loadingPreview(recipe))
|
|
4509
|
+
].join("");
|
|
4510
|
+
}
|
|
4511
|
+
function renderAllPreviewGroups(recipe) {
|
|
4512
|
+
return [
|
|
4513
|
+
previewGroup("Text", textPreview(recipe)),
|
|
4514
|
+
previewGroup("Buttons", buttonsPreview(recipe)),
|
|
4515
|
+
previewGroup("Links", linksPreview(recipe)),
|
|
4516
|
+
previewGroup("Fields", fieldsPreview(recipe)),
|
|
4517
|
+
previewGroup("Choice Controls", choicesPreview(recipe)),
|
|
4518
|
+
previewGroup("Select", selectPreview(recipe)),
|
|
4519
|
+
previewGroup("Feedback", feedbackPreview(recipe)),
|
|
4520
|
+
previewGroup("Identity", identityPreview(recipe)),
|
|
4521
|
+
previewGroup("Empty State", emptyPreview(recipe)),
|
|
4522
|
+
previewGroup("Loading", loadingPreview(recipe)),
|
|
4523
|
+
previewGroup("Layout", layoutPreview(recipe)),
|
|
4524
|
+
previewGroup("Data", dataPreview(recipe)),
|
|
4525
|
+
previewGroup("Media", mediaPreview(recipe))
|
|
4526
|
+
].join("");
|
|
4527
|
+
}
|
|
4528
|
+
function previewGroup(label, body) {
|
|
4529
|
+
return `<div class="preview-group"><div class="component-label">${escapeHtml2(label)}</div>${body}</div>`;
|
|
4530
|
+
}
|
|
4531
|
+
function textPreview(recipe) {
|
|
4532
|
+
return `<div class="preview-row">
|
|
4533
|
+
<h2 data-sc-component="heading" data-sc-level="2" data-sc-recipe="${recipe}">Heading</h2>
|
|
4534
|
+
<p data-sc-component="text" data-sc-recipe="${recipe}">Body text follows the active host typography.</p>
|
|
4535
|
+
<p data-sc-component="text" data-sc-tone="muted" data-sc-recipe="${recipe}">Muted supporting text</p>
|
|
4536
|
+
<code data-sc-component="code" data-sc-recipe="${recipe}">tool_result.id</code>
|
|
4537
|
+
<span data-sc-component="shimmer-text" data-sc-recipe="${recipe}">Shimmer text</span>
|
|
4538
|
+
</div>`;
|
|
4539
|
+
}
|
|
4540
|
+
function buttonsPreview(recipe) {
|
|
4541
|
+
return `<div class="preview-row">
|
|
4542
|
+
<button data-sc-component="button" data-sc-color="primary" data-sc-variant="solid" data-sc-size="md" data-sc-pill data-sc-recipe="${recipe}" type="button"><span data-sc-component="button-label">Primary</span></button>
|
|
4543
|
+
<button data-sc-component="button" data-sc-color="secondary" data-sc-variant="soft" data-sc-size="md" data-sc-pill data-sc-recipe="${recipe}" type="button"><span data-sc-component="button-label">Secondary</span></button>
|
|
4544
|
+
<button data-sc-component="button" data-sc-color="danger" data-sc-variant="outline" data-sc-size="md" data-sc-pill data-sc-recipe="${recipe}" type="button"><span data-sc-component="button-label">Danger</span></button>
|
|
4545
|
+
<button data-sc-component="button" data-sc-color="secondary" data-sc-variant="ghost" data-sc-size="md" data-sc-pill data-sc-recipe="${recipe}" type="button"><span data-sc-component="button-label">Ghost</span></button>
|
|
4546
|
+
<button data-sc-component="button" data-sc-color="primary" data-sc-variant="solid" data-sc-size="md" data-sc-pill data-sc-loading data-sc-recipe="${recipe}" type="button"><span data-sc-component="loading-indicator" data-sc-recipe="${recipe}" style="--sc-indicator-size: 1em;"></span><span data-sc-component="button-label">Loading</span></button>
|
|
4547
|
+
</div>`;
|
|
4548
|
+
}
|
|
4549
|
+
function linksPreview(recipe) {
|
|
4550
|
+
return `<div class="preview-row">
|
|
4551
|
+
<a data-sc-component="button" data-sc-color="primary" data-sc-variant="solid" data-sc-size="md" data-sc-pill data-sc-recipe="${recipe}" href="#"><span data-sc-component="button-label">ButtonLink</span></a>
|
|
4552
|
+
<a data-sc-component="text-link" data-sc-primary data-sc-underline data-sc-recipe="${recipe}" href="#">TextLink</a>
|
|
4553
|
+
</div>`;
|
|
4554
|
+
}
|
|
4555
|
+
function fieldsPreview(recipe) {
|
|
4556
|
+
return `<div class="preview-row">
|
|
4557
|
+
<div data-sc-component="form-field" data-sc-recipe="${recipe}">
|
|
4558
|
+
<label data-sc-component="field-label" data-sc-recipe="${recipe}">FormField label</label>
|
|
4559
|
+
<span data-sc-component="input-shell" data-sc-variant="outline" data-sc-size="md" data-sc-recipe="${recipe}"><input data-sc-component="input" value="Input" /></span>
|
|
4560
|
+
<p data-sc-component="field-description" data-sc-recipe="${recipe}">Field description</p>
|
|
4561
|
+
</div>
|
|
4562
|
+
<span data-sc-component="input-shell" data-sc-variant="soft" data-sc-size="md" data-sc-recipe="${recipe}"><span data-sc-component="input-adornment">$</span><input data-sc-component="input" value="Soft input" /></span>
|
|
4563
|
+
<textarea data-sc-component="textarea" data-sc-variant="outline" data-sc-size="md" data-sc-recipe="${recipe}">Textarea</textarea>
|
|
4564
|
+
<div data-sc-component="form-field" data-sc-invalid data-sc-recipe="${recipe}">
|
|
4565
|
+
<span data-sc-component="input-shell" data-sc-invalid data-sc-variant="outline" data-sc-size="md" data-sc-recipe="${recipe}"><input data-sc-component="input" value="Invalid" aria-invalid="true" /></span>
|
|
4566
|
+
<p data-sc-component="field-error" data-sc-recipe="${recipe}">Field error</p>
|
|
4567
|
+
</div>
|
|
4568
|
+
</div>`;
|
|
4569
|
+
}
|
|
4570
|
+
function choicesPreview(recipe) {
|
|
4571
|
+
return `<div class="preview-row">
|
|
4572
|
+
<label data-sc-component="checkbox-label" data-sc-recipe="${recipe}"><button data-sc-component="checkbox" data-sc-checked data-sc-recipe="${recipe}" role="checkbox" aria-checked="true" type="button"></button><span>Checkbox</span></label>
|
|
4573
|
+
<label data-sc-component="switch-label" data-sc-recipe="${recipe}"><button data-sc-component="switch" data-sc-checked data-sc-recipe="${recipe}" role="switch" aria-checked="true" type="button"><span data-sc-component="switch-thumb"></span></button><span>Switch</span></label>
|
|
4574
|
+
<div data-sc-component="radio-group" data-sc-direction="row" data-sc-recipe="${recipe}" role="radiogroup">
|
|
4575
|
+
<button data-sc-component="radio-item" data-sc-checked data-sc-recipe="${recipe}" role="radio" aria-checked="true" type="button"><span data-sc-component="radio-indicator"></span><span data-sc-component="radio-label">List</span></button>
|
|
4576
|
+
<button data-sc-component="radio-item" data-sc-recipe="${recipe}" role="radio" aria-checked="false" type="button"><span data-sc-component="radio-indicator"></span><span data-sc-component="radio-label">Grid</span></button>
|
|
4577
|
+
</div>
|
|
4578
|
+
<div data-sc-component="segmented-control" data-sc-size="md" data-sc-recipe="${recipe}"><button data-sc-component="segmented-option" data-sc-selected type="button">List</button><button data-sc-component="segmented-option" type="button">Grid</button></div>
|
|
4579
|
+
<input data-sc-component="slider" data-sc-recipe="${recipe}" type="range" value="60" />
|
|
4580
|
+
</div>`;
|
|
4581
|
+
}
|
|
4582
|
+
function selectPreview(recipe) {
|
|
4583
|
+
return `<div class="preview-row">
|
|
4584
|
+
<span data-sc-component="select-control" data-sc-variant="outline" data-sc-size="md" data-sc-selected data-sc-recipe="${recipe}"><span data-sc-component="select-control-value">Selected option</span><span data-sc-component="select-dropdown-icon">\u25BE</span></span>
|
|
4585
|
+
<div data-sc-component="select" data-sc-open data-sc-recipe="${recipe}">
|
|
4586
|
+
<span data-sc-component="select-control" data-sc-variant="outline" data-sc-size="md" data-sc-selected data-sc-recipe="${recipe}"><span data-sc-component="select-control-value">CSV</span><span data-sc-component="select-dropdown-icon">\u25BE</span></span>
|
|
4587
|
+
<div data-sc-component="select-list" data-sc-recipe="${recipe}" role="listbox">
|
|
4588
|
+
<button data-sc-component="select-option" data-sc-selected role="option" aria-selected="true" type="button"><span data-sc-component="select-option-label">CSV</span><span data-sc-component="select-option-description">Comma-separated values</span></button>
|
|
4589
|
+
<button data-sc-component="select-option" role="option" aria-selected="false" type="button"><span data-sc-component="select-option-label">PDF</span><span data-sc-component="select-option-description">Portable document</span></button>
|
|
4590
|
+
</div>
|
|
4591
|
+
</div>
|
|
4592
|
+
</div>`;
|
|
4593
|
+
}
|
|
4594
|
+
function feedbackPreview(recipe) {
|
|
4595
|
+
return `<div class="preview-row">
|
|
4596
|
+
<div data-sc-component="alert" data-sc-color="info" data-sc-variant="soft" data-sc-recipe="${recipe}"><span data-sc-component="alert-indicator">\u2022</span><div data-sc-component="alert-content"><div data-sc-component="alert-title">Alert</div><div data-sc-component="alert-description">Native recipe check</div></div></div>
|
|
4597
|
+
<span data-sc-component="badge" data-sc-color="success" data-sc-variant="soft" data-sc-size="sm" data-sc-recipe="${recipe}">Badge</span>
|
|
4598
|
+
<span data-sc-component="badge" data-sc-color="danger" data-sc-variant="outline" data-sc-size="md" data-sc-pill data-sc-recipe="${recipe}">Pill badge</span>
|
|
4599
|
+
</div>`;
|
|
4600
|
+
}
|
|
4601
|
+
function identityPreview(recipe) {
|
|
4602
|
+
return `<div data-sc-component="avatar-group" data-sc-recipe="${recipe}">
|
|
4603
|
+
<span data-sc-component="avatar" data-sc-color="primary" data-sc-variant="solid" data-sc-recipe="${recipe}" style="--sc-avatar-size: 32px;">SD</span>
|
|
4604
|
+
<span data-sc-component="avatar" data-sc-color="secondary" data-sc-variant="soft" data-sc-recipe="${recipe}" style="--sc-avatar-size: 32px;">+3</span>
|
|
4605
|
+
</div>`;
|
|
4606
|
+
}
|
|
4607
|
+
function emptyPreview(recipe) {
|
|
4608
|
+
return `<div data-sc-component="empty-message" data-sc-fill="static" data-sc-recipe="${recipe}">
|
|
4609
|
+
<div data-sc-component="empty-message-icon" data-sc-color="secondary" data-sc-size="md" data-sc-recipe="${recipe}">\u25A1</div>
|
|
4610
|
+
<div data-sc-component="empty-message-title" data-sc-color="secondary" data-sc-recipe="${recipe}">No results</div>
|
|
4611
|
+
<div data-sc-component="empty-message-description">Try another filter.</div>
|
|
4612
|
+
<div data-sc-component="empty-message-actions"><button data-sc-component="button" data-sc-color="secondary" data-sc-variant="soft" data-sc-size="sm" data-sc-pill data-sc-recipe="${recipe}" type="button"><span data-sc-component="button-label">Reset</span></button></div>
|
|
4613
|
+
</div>`;
|
|
4614
|
+
}
|
|
4615
|
+
function loadingPreview(recipe) {
|
|
4616
|
+
return `<div class="preview-row">
|
|
4617
|
+
<div data-sc-component="loading-dots" data-sc-recipe="${recipe}"><span></span><span></span><span></span></div>
|
|
4618
|
+
<span data-sc-component="loading-indicator" data-sc-recipe="${recipe}" style="--sc-indicator-size: 24px;"></span>
|
|
4619
|
+
<span data-sc-component="circular-progress" data-sc-recipe="${recipe}" style="--sc-progress-size: 28px;"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="10" fill="none" stroke="color-mix(in srgb, currentColor 18%, transparent)" stroke-width="2"></circle><circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="62.8" stroke-dashoffset="18" stroke-linecap="round" transform="rotate(-90 12 12)"></circle></svg></span>
|
|
4620
|
+
<div data-sc-component="skeleton" data-sc-recipe="${recipe}" style="width: 100%; height: 24px;"></div>
|
|
4621
|
+
</div>`;
|
|
4622
|
+
}
|
|
4623
|
+
function layoutPreview(recipe) {
|
|
4624
|
+
return `<div data-sc-component="stack" data-sc-gap="sm" data-sc-recipe="${recipe}">
|
|
4625
|
+
<div data-sc-component="surface" data-sc-variant="card" data-sc-recipe="${recipe}"><p data-sc-component="text" data-sc-recipe="${recipe}">Card surface</p></div>
|
|
4626
|
+
<div data-sc-component="surface" data-sc-variant="inset" data-sc-recipe="${recipe}"><p data-sc-component="text" data-sc-recipe="${recipe}">Inset surface</p></div>
|
|
4627
|
+
<div data-sc-component="inline" data-sc-gap="sm" data-sc-recipe="${recipe}"><span data-sc-component="badge" data-sc-recipe="${recipe}">Inline</span><span data-sc-component="badge" data-sc-recipe="${recipe}">Row</span></div>
|
|
4628
|
+
<hr data-sc-component="divider" data-sc-recipe="${recipe}" />
|
|
4629
|
+
<div data-sc-component="tabs" data-sc-recipe="${recipe}"><div data-sc-component="segmented-control" data-sc-size="sm" data-sc-recipe="${recipe}"><button data-sc-component="segmented-option" data-sc-selected type="button">One</button><button data-sc-component="segmented-option" type="button">Two</button></div></div>
|
|
4630
|
+
</div>`;
|
|
4631
|
+
}
|
|
4632
|
+
function dataPreview(recipe) {
|
|
4633
|
+
return `<div class="preview-row">
|
|
4634
|
+
<dl data-sc-component="key-value" data-sc-recipe="${recipe}">
|
|
4635
|
+
<div data-sc-component="key-value-row"><dt>Status</dt><dd>Ready</dd></div>
|
|
4636
|
+
<div data-sc-component="key-value-row"><dt>Target</dt><dd>ChatGPT</dd></div>
|
|
4637
|
+
</dl>
|
|
4638
|
+
<table data-sc-component="table" data-sc-recipe="${recipe}"><tbody><tr><th>Name</th><td>Sidecar</td></tr><tr><th>Version</th><td>alpha</td></tr></tbody></table>
|
|
4639
|
+
<progress data-sc-component="progress" data-sc-recipe="${recipe}" value="70" max="100"></progress>
|
|
4640
|
+
</div>`;
|
|
4641
|
+
}
|
|
4642
|
+
function mediaPreview(recipe) {
|
|
4643
|
+
return `<svg data-sc-component="image" data-sc-recipe="${recipe}" viewBox="0 0 320 120" role="img" aria-label="Preview image">
|
|
4644
|
+
<rect width="320" height="120" rx="12" fill="currentColor" opacity="0.08"></rect>
|
|
4645
|
+
<circle cx="64" cy="60" r="28" fill="currentColor" opacity="0.18"></circle>
|
|
4646
|
+
<rect x="112" y="42" width="150" height="12" rx="6" fill="currentColor" opacity="0.2"></rect>
|
|
4647
|
+
<rect x="112" y="66" width="104" height="10" rx="5" fill="currentColor" opacity="0.14"></rect>
|
|
4648
|
+
</svg>`;
|
|
4649
|
+
}
|
|
4650
|
+
function previewComponentNames(componentSet) {
|
|
4651
|
+
if (componentSet === "representative") {
|
|
4652
|
+
return [
|
|
4653
|
+
"Button",
|
|
4654
|
+
"Input",
|
|
4655
|
+
"SelectControl",
|
|
4656
|
+
"Checkbox",
|
|
4657
|
+
"Switch",
|
|
4658
|
+
"RadioGroup",
|
|
4659
|
+
"SegmentedControl",
|
|
4660
|
+
"Alert",
|
|
4661
|
+
"Badge",
|
|
4662
|
+
"Skeleton"
|
|
4663
|
+
];
|
|
4664
|
+
}
|
|
4665
|
+
return [
|
|
4666
|
+
"Alert",
|
|
4667
|
+
"Avatar",
|
|
4668
|
+
"AvatarGroup",
|
|
4669
|
+
"Badge",
|
|
4670
|
+
"Button",
|
|
4671
|
+
"ButtonLink",
|
|
4672
|
+
"Checkbox",
|
|
4673
|
+
"CircularProgress",
|
|
4674
|
+
"Code",
|
|
4675
|
+
"CopyButton",
|
|
4676
|
+
"Divider",
|
|
4677
|
+
"EmptyMessage",
|
|
4678
|
+
"FieldDescription",
|
|
4679
|
+
"FieldError",
|
|
4680
|
+
"FieldLabel",
|
|
4681
|
+
"FormField",
|
|
4682
|
+
"Heading",
|
|
4683
|
+
"Image",
|
|
4684
|
+
"Inline",
|
|
4685
|
+
"Input",
|
|
4686
|
+
"KeyValue",
|
|
4687
|
+
"LoadingDots",
|
|
4688
|
+
"LoadingIndicator",
|
|
4689
|
+
"Progress",
|
|
4690
|
+
"RadioGroup",
|
|
4691
|
+
"SegmentedControl",
|
|
4692
|
+
"Select",
|
|
4693
|
+
"SelectControl",
|
|
4694
|
+
"ShimmerText",
|
|
4695
|
+
"Skeleton",
|
|
4696
|
+
"Slider",
|
|
4697
|
+
"Stack",
|
|
4698
|
+
"Surface",
|
|
4699
|
+
"Switch",
|
|
4700
|
+
"Table",
|
|
4701
|
+
"Tabs",
|
|
4702
|
+
"Text",
|
|
4703
|
+
"Textarea",
|
|
4704
|
+
"TextLink"
|
|
4705
|
+
];
|
|
4706
|
+
}
|
|
4707
|
+
function readPreviewComponentSet(value) {
|
|
4708
|
+
if (!value || value === "representative") {
|
|
4709
|
+
return "representative";
|
|
4710
|
+
}
|
|
4711
|
+
if (value === "all") {
|
|
4712
|
+
return value;
|
|
4713
|
+
}
|
|
4714
|
+
throw new Error(`Unsupported component preview set "${value}". Expected representative or all.`);
|
|
4715
|
+
}
|
|
4716
|
+
function readPreviewThemes(value) {
|
|
4717
|
+
if (!value || value === "light") {
|
|
4718
|
+
return ["light"];
|
|
4719
|
+
}
|
|
4720
|
+
if (value === "dark") {
|
|
4721
|
+
return ["dark"];
|
|
4722
|
+
}
|
|
4723
|
+
if (value === "both") {
|
|
4724
|
+
return ["light", "dark"];
|
|
4725
|
+
}
|
|
4726
|
+
const themes = value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
4727
|
+
if (themes.every((theme) => theme === "light" || theme === "dark")) {
|
|
4728
|
+
return themes;
|
|
4729
|
+
}
|
|
4730
|
+
throw new Error(`Unsupported component preview theme "${value}". Expected light, dark, both, or a comma-separated light,dark list.`);
|
|
4731
|
+
}
|
|
4732
|
+
function escapeHtml2(value) {
|
|
4733
|
+
return value.replace(/[&<>"']/g, (character) => {
|
|
4734
|
+
switch (character) {
|
|
4735
|
+
case "&":
|
|
4736
|
+
return "&";
|
|
4737
|
+
case "<":
|
|
4738
|
+
return "<";
|
|
4739
|
+
case ">":
|
|
4740
|
+
return ">";
|
|
4741
|
+
case '"':
|
|
4742
|
+
return """;
|
|
4743
|
+
default:
|
|
4744
|
+
return "'";
|
|
4745
|
+
}
|
|
4746
|
+
});
|
|
4747
|
+
}
|
|
4748
|
+
function readTunnelProvider(argv) {
|
|
4749
|
+
const index = argv.indexOf("--tunnel");
|
|
4750
|
+
if (index === -1) {
|
|
4751
|
+
return void 0;
|
|
4752
|
+
}
|
|
4753
|
+
const value = argv[index + 1];
|
|
4754
|
+
if (value === "cloudflared" || value === "wrangler") {
|
|
4755
|
+
return value;
|
|
4756
|
+
}
|
|
4757
|
+
return "auto";
|
|
4758
|
+
}
|
|
4759
|
+
function printDiagnostics(diagnostics) {
|
|
4760
|
+
for (const diagnostic of diagnostics) {
|
|
4761
|
+
console.warn(formatDiagnostic(diagnostic));
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
async function loadRuntimeAuth(rootDir) {
|
|
4765
|
+
const authPath = path18.join(rootDir, "auth.ts");
|
|
4766
|
+
if (!existsSync5(authPath)) {
|
|
4767
|
+
return void 0;
|
|
4768
|
+
}
|
|
4769
|
+
const parentURL = pathToFileURL(path18.join(rootDir, "sidecar.config.ts")).href;
|
|
4770
|
+
const tsconfigPath = path18.join(rootDir, "tsconfig.json");
|
|
4771
|
+
const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
|
|
4772
|
+
const module = await tsImport(pathToFileURL(authPath).href, {
|
|
4773
|
+
parentURL,
|
|
4774
|
+
tsconfig
|
|
4775
|
+
});
|
|
4776
|
+
if (!isSidecarAuth(module.default)) {
|
|
4777
|
+
throw new Error("auth.ts must default-export auth({ ... }) from sidecar-ai.");
|
|
4778
|
+
}
|
|
4779
|
+
return module.default;
|
|
4780
|
+
}
|
|
4781
|
+
async function loadRuntimeProxy(rootDir) {
|
|
4782
|
+
const proxyPath = path18.join(rootDir, "proxy.ts");
|
|
4783
|
+
if (!existsSync5(proxyPath)) {
|
|
4784
|
+
return void 0;
|
|
4785
|
+
}
|
|
4786
|
+
const parentURL = pathToFileURL(path18.join(rootDir, "sidecar.config.ts")).href;
|
|
4787
|
+
const tsconfigPath = path18.join(rootDir, "tsconfig.json");
|
|
4788
|
+
const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
|
|
4789
|
+
const module = await tsImport(pathToFileURL(proxyPath).href, {
|
|
4790
|
+
parentURL,
|
|
4791
|
+
tsconfig
|
|
4792
|
+
});
|
|
4793
|
+
if (!isSidecarProxy(module.default)) {
|
|
4794
|
+
throw new Error("proxy.ts must default-export proxy({ ... }) from @sidecar-ai/server/proxy.");
|
|
4795
|
+
}
|
|
4796
|
+
return module.default;
|
|
4797
|
+
}
|
|
4798
|
+
async function loadRuntimeConfig(rootDir) {
|
|
4799
|
+
const configPath = path18.join(rootDir, "sidecar.config.ts");
|
|
4800
|
+
if (!existsSync5(configPath)) {
|
|
4801
|
+
return void 0;
|
|
4802
|
+
}
|
|
4803
|
+
const parentURL = pathToFileURL(configPath).href;
|
|
4804
|
+
const tsconfigPath = path18.join(rootDir, "tsconfig.json");
|
|
4805
|
+
const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
|
|
4806
|
+
const module = await tsImport(pathToFileURL(configPath).href, {
|
|
4807
|
+
parentURL,
|
|
4808
|
+
tsconfig
|
|
4809
|
+
});
|
|
4810
|
+
if (!module.default || typeof module.default !== "object") {
|
|
4811
|
+
throw new Error("sidecar.config.ts must default-export defineConfig({ ... }) from sidecar-ai.");
|
|
4812
|
+
}
|
|
4813
|
+
return module.default;
|
|
4814
|
+
}
|
|
4815
|
+
async function loadRuntimeTools(rootDir, manifest) {
|
|
4816
|
+
const parentURL = pathToFileURL(path18.join(rootDir, "sidecar.config.ts")).href;
|
|
4817
|
+
const tsconfigPath = path18.join(rootDir, "tsconfig.json");
|
|
4818
|
+
const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
|
|
4819
|
+
const loaded = [];
|
|
4820
|
+
for (const entry of manifest.tools) {
|
|
4821
|
+
const sourcePath = path18.join(rootDir, entry.sourceFile);
|
|
4822
|
+
const module = await tsImport(pathToFileURL(sourcePath).href, {
|
|
4823
|
+
parentURL,
|
|
4824
|
+
tsconfig
|
|
4825
|
+
});
|
|
4826
|
+
if (!isSidecarTool(module.default)) {
|
|
4827
|
+
throw new Error(`${entry.sourceFile} did not default-export a Sidecar tool.`);
|
|
4828
|
+
}
|
|
4829
|
+
loaded.push({
|
|
4830
|
+
tool: module.default,
|
|
4831
|
+
descriptor: entry.descriptor
|
|
4832
|
+
});
|
|
4833
|
+
}
|
|
4834
|
+
return loaded;
|
|
4835
|
+
}
|
|
4836
|
+
async function loadResources(rootDir, outDir, manifest) {
|
|
4837
|
+
const resources = [];
|
|
4838
|
+
for (const entry of manifest.tools) {
|
|
4839
|
+
if (!entry.widget?.outputFile) {
|
|
4840
|
+
continue;
|
|
4841
|
+
}
|
|
4842
|
+
const text = await readFile8(path18.join(rootDir, outDir, entry.widget.outputFile), "utf8");
|
|
4843
|
+
resources.push({
|
|
4844
|
+
uri: entry.widget.resourceUri,
|
|
4845
|
+
name: entry.name,
|
|
4846
|
+
description: entry.widget.options?.description,
|
|
4847
|
+
mimeType: MCP_APP_RESOURCE_MIME_TYPE,
|
|
4848
|
+
text,
|
|
4849
|
+
_meta: entry.widget.resourceMeta
|
|
4850
|
+
});
|
|
4851
|
+
}
|
|
4852
|
+
const parentURL = pathToFileURL(path18.join(rootDir, "sidecar.config.ts")).href;
|
|
4853
|
+
const tsconfigPath = path18.join(rootDir, "tsconfig.json");
|
|
4854
|
+
const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
|
|
4855
|
+
for (const entry of manifest.resources) {
|
|
4856
|
+
const sourcePath = path18.join(rootDir, entry.sourceFile);
|
|
4857
|
+
const module = await tsImport(pathToFileURL(sourcePath).href, {
|
|
4858
|
+
parentURL,
|
|
4859
|
+
tsconfig
|
|
4860
|
+
});
|
|
4861
|
+
if (!isSidecarResource(module.default)) {
|
|
4862
|
+
throw new Error(`${entry.sourceFile} did not default-export a Sidecar resource.`);
|
|
4863
|
+
}
|
|
4864
|
+
resources.push({
|
|
4865
|
+
uri: entry.uri,
|
|
4866
|
+
descriptor: entry.descriptor,
|
|
4867
|
+
resource: module.default
|
|
4868
|
+
});
|
|
4869
|
+
}
|
|
4870
|
+
return resources;
|
|
4871
|
+
}
|
|
4872
|
+
async function loadRuntimePrompts(rootDir, manifest) {
|
|
4873
|
+
const parentURL = pathToFileURL(path18.join(rootDir, "sidecar.config.ts")).href;
|
|
4874
|
+
const tsconfigPath = path18.join(rootDir, "tsconfig.json");
|
|
4875
|
+
const tsconfig = existsSync5(tsconfigPath) ? tsconfigPath : false;
|
|
4876
|
+
const loaded = [];
|
|
4877
|
+
for (const entry of manifest.prompts) {
|
|
4878
|
+
const sourcePath = path18.join(rootDir, entry.sourceFile);
|
|
4879
|
+
const module = await tsImport(pathToFileURL(sourcePath).href, {
|
|
4880
|
+
parentURL,
|
|
4881
|
+
tsconfig
|
|
4882
|
+
});
|
|
4883
|
+
if (!isSidecarPrompt(module.default)) {
|
|
4884
|
+
throw new Error(`${entry.sourceFile} did not default-export a Sidecar prompt.`);
|
|
4885
|
+
}
|
|
4886
|
+
loaded.push({
|
|
4887
|
+
prompt: module.default,
|
|
4888
|
+
descriptor: entry.descriptor
|
|
4889
|
+
});
|
|
4890
|
+
}
|
|
4891
|
+
return loaded;
|
|
4892
|
+
}
|
|
4893
|
+
function readOption(argv, name) {
|
|
4894
|
+
const index = argv.indexOf(name);
|
|
4895
|
+
if (index === -1) {
|
|
4896
|
+
return void 0;
|
|
4897
|
+
}
|
|
4898
|
+
return argv[index + 1];
|
|
4899
|
+
}
|
|
4900
|
+
function isDirectRun() {
|
|
4901
|
+
const entry = process.argv[1];
|
|
4902
|
+
if (!entry) {
|
|
4903
|
+
return false;
|
|
4904
|
+
}
|
|
4905
|
+
const entryPath = realpathSync.native(entry);
|
|
4906
|
+
return import.meta.url === pathToFileURL(entryPath).href;
|
|
4907
|
+
}
|
|
4908
|
+
if (isDirectRun()) {
|
|
4909
|
+
main(process.argv).catch((error) => {
|
|
4910
|
+
console.error(error instanceof Error ? error.message : error);
|
|
4911
|
+
exit(1);
|
|
4912
|
+
});
|
|
4913
|
+
}
|
|
4914
|
+
export {
|
|
4915
|
+
main,
|
|
4916
|
+
previewComponentNames,
|
|
4917
|
+
readPreviewComponentSet,
|
|
4918
|
+
readPreviewThemes,
|
|
4919
|
+
renderComponentPreviewFrame,
|
|
4920
|
+
renderComponentPreviewHtml
|
|
4921
|
+
};
|
|
4922
|
+
//# sourceMappingURL=index.js.map
|