@sidecar-ai/cli 0.1.0-alpha.10 → 0.1.0-alpha.12
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 +38 -1
- package/dist/index.js +2570 -1608
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
package/dist/index.js
CHANGED
|
@@ -1,45 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// packages/cli/src/index.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync8, realpathSync } from "fs";
|
|
5
5
|
import { mkdir as mkdir10, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
6
|
+
import { spawn as spawn2 } from "child_process";
|
|
7
|
+
import { createServer as createServer3 } from "http";
|
|
8
|
+
import path21 from "path";
|
|
8
9
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
9
|
-
import { fileURLToPath, pathToFileURL as pathToFileURL3 } from "url";
|
|
10
|
-
import { cwd, exit, stdin as stdin2, stdout as stdout2 } from "process";
|
|
10
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL3 } from "url";
|
|
11
|
+
import { cwd, execPath, exit, stdin as stdin2, stdout as stdout2 } from "process";
|
|
11
12
|
import { tsImport as tsImport2 } from "tsx/esm/api";
|
|
12
13
|
|
|
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] || value.kind === "sidecar.auth")
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
14
|
// packages/core/src/index.ts
|
|
22
|
-
var MCP_APP_RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
23
15
|
var toolBrand = /* @__PURE__ */ Symbol.for("sidecar.tool");
|
|
24
16
|
var toolResultBrand = /* @__PURE__ */ Symbol.for("sidecar.toolResult");
|
|
25
|
-
var resourceBrand = /* @__PURE__ */ Symbol.for("sidecar.resource");
|
|
26
17
|
var resourceResultBrand = /* @__PURE__ */ Symbol.for("sidecar.resourceResult");
|
|
27
|
-
var promptBrand = /* @__PURE__ */ Symbol.for("sidecar.prompt");
|
|
28
18
|
function isSidecarTool(value) {
|
|
29
19
|
return Boolean(
|
|
30
20
|
value && typeof value === "object" && (value[toolBrand] || value.kind === "sidecar.tool")
|
|
31
21
|
);
|
|
32
22
|
}
|
|
33
|
-
function isSidecarResource(value) {
|
|
34
|
-
return Boolean(
|
|
35
|
-
value && typeof value === "object" && (value[resourceBrand] || value.kind === "sidecar.resource")
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
function isSidecarPrompt(value) {
|
|
39
|
-
return Boolean(
|
|
40
|
-
value && typeof value === "object" && (value[promptBrand] || value.kind === "sidecar.prompt")
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
23
|
var toolResult = Object.assign(
|
|
44
24
|
createToolResult,
|
|
45
25
|
{
|
|
@@ -94,57 +74,6 @@ function createResourceResult(input) {
|
|
|
94
74
|
});
|
|
95
75
|
return resultEnvelope;
|
|
96
76
|
}
|
|
97
|
-
function isToolResult(value) {
|
|
98
|
-
return Boolean(
|
|
99
|
-
value && typeof value === "object" && value[toolResultBrand] === true
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
function isResourceResult(value) {
|
|
103
|
-
return Boolean(
|
|
104
|
-
value && typeof value === "object" && value[resourceResultBrand] === true
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
function normalizeToolResult(value) {
|
|
108
|
-
if (!isToolResult(value)) {
|
|
109
|
-
throw new SidecarRuntimeError(
|
|
110
|
-
"Tool execute() must return toolResult({ structuredContent, content, meta }).",
|
|
111
|
-
"invalid_tool_result"
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
return stripUndefined({
|
|
115
|
-
structuredContent: stripJsonUndefined(value.structuredContent),
|
|
116
|
-
content: value.content ?? [],
|
|
117
|
-
_meta: stripJsonUndefined(value._meta),
|
|
118
|
-
isError: value.isError
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
function normalizeResourceResult(value, uri, defaultMimeType) {
|
|
122
|
-
if (!isResourceResult(value)) {
|
|
123
|
-
throw new SidecarRuntimeError(
|
|
124
|
-
"Resource read() must return resourceResult(...).",
|
|
125
|
-
"invalid_resource_result"
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
return {
|
|
129
|
-
contents: value.contents.map(
|
|
130
|
-
(content) => normalizeResourceContent(content, uri, defaultMimeType)
|
|
131
|
-
)
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
async function executeTool(sidecarTool, params, ctx) {
|
|
135
|
-
const parsedParams = validateParams(sidecarTool, params);
|
|
136
|
-
const value = await sidecarTool.execute(parsedParams, ctx);
|
|
137
|
-
return normalizeToolResult(value);
|
|
138
|
-
}
|
|
139
|
-
async function executeResource(sidecarResource, ctx, options) {
|
|
140
|
-
const value = await sidecarResource.read(ctx);
|
|
141
|
-
return normalizeResourceResult(value, options.uri, sidecarResource.mimeType ?? options.mimeType);
|
|
142
|
-
}
|
|
143
|
-
async function executePrompt(sidecarPrompt, args, ctx) {
|
|
144
|
-
const parsedArgs = validatePromptArgs(sidecarPrompt, args);
|
|
145
|
-
const value = await sidecarPrompt.run(parsedArgs, ctx);
|
|
146
|
-
return normalizePromptResult(value, sidecarPrompt.description);
|
|
147
|
-
}
|
|
148
77
|
function createToolDescriptor(definition) {
|
|
149
78
|
const machineName = definition.id ?? toMachineName(definition.name);
|
|
150
79
|
validateToolId(machineName);
|
|
@@ -309,16 +238,6 @@ var SidecarRuntimeError = class extends Error {
|
|
|
309
238
|
}
|
|
310
239
|
code;
|
|
311
240
|
};
|
|
312
|
-
function validateParams(sidecarTool, params) {
|
|
313
|
-
if (!sidecarTool.params) {
|
|
314
|
-
return params ?? {};
|
|
315
|
-
}
|
|
316
|
-
const parsed = sidecarTool.params.safeParse(params ?? {});
|
|
317
|
-
if (parsed.success) {
|
|
318
|
-
return parsed.data;
|
|
319
|
-
}
|
|
320
|
-
throw new SidecarRuntimeError(`Invalid parameters for tool "${sidecarTool.name}".`, "invalid_tool_params");
|
|
321
|
-
}
|
|
322
241
|
function normalizeRequiredContent(content) {
|
|
323
242
|
if (typeof content === "string") {
|
|
324
243
|
return [toolResult.text(content)];
|
|
@@ -332,49 +251,6 @@ function normalizeRequiredContent(content) {
|
|
|
332
251
|
"invalid_tool_result"
|
|
333
252
|
);
|
|
334
253
|
}
|
|
335
|
-
function normalizeResourceContent(content, uri, defaultMimeType) {
|
|
336
|
-
const annotations = normalizeAnnotations(content.annotations);
|
|
337
|
-
const meta = content.meta;
|
|
338
|
-
if (isBinaryLike(content.content)) {
|
|
339
|
-
return stripUndefined({
|
|
340
|
-
uri,
|
|
341
|
-
mimeType: content.mimeType ?? defaultMimeType ?? "application/octet-stream",
|
|
342
|
-
blob: bytesToBase64(content.content),
|
|
343
|
-
annotations,
|
|
344
|
-
_meta: meta
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
if (typeof content.content === "string") {
|
|
348
|
-
return stripUndefined({
|
|
349
|
-
uri,
|
|
350
|
-
mimeType: content.mimeType ?? defaultMimeType ?? "text/plain",
|
|
351
|
-
text: content.content,
|
|
352
|
-
annotations,
|
|
353
|
-
_meta: meta
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
return stripUndefined({
|
|
357
|
-
uri,
|
|
358
|
-
mimeType: content.mimeType ?? defaultMimeType ?? "application/json",
|
|
359
|
-
text: JSON.stringify(content.content),
|
|
360
|
-
annotations,
|
|
361
|
-
_meta: meta
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
function isBinaryLike(value) {
|
|
365
|
-
return value instanceof ArrayBuffer || value instanceof Uint8Array;
|
|
366
|
-
}
|
|
367
|
-
function bytesToBase64(value) {
|
|
368
|
-
const bytes = value instanceof ArrayBuffer ? new Uint8Array(value) : value;
|
|
369
|
-
if (typeof Buffer !== "undefined") {
|
|
370
|
-
return Buffer.from(bytes).toString("base64");
|
|
371
|
-
}
|
|
372
|
-
let binary = "";
|
|
373
|
-
for (const byte of bytes) {
|
|
374
|
-
binary += String.fromCharCode(byte);
|
|
375
|
-
}
|
|
376
|
-
return btoa(binary);
|
|
377
|
-
}
|
|
378
254
|
function normalizeAnnotations(annotations) {
|
|
379
255
|
if (!annotations) {
|
|
380
256
|
return void 0;
|
|
@@ -411,45 +287,6 @@ function promptArguments(args) {
|
|
|
411
287
|
function isReadonlyArray(value) {
|
|
412
288
|
return Array.isArray(value);
|
|
413
289
|
}
|
|
414
|
-
function validatePromptArgs(promptDefinition, args) {
|
|
415
|
-
const input = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
416
|
-
for (const argument of promptArguments(promptDefinition.args) ?? []) {
|
|
417
|
-
if (argument.required !== false && !(argument.name in input)) {
|
|
418
|
-
throw new SidecarRuntimeError(
|
|
419
|
-
`Prompt "${promptDefinition.name ?? promptDefinition.title}" is missing required argument "${argument.name}".`,
|
|
420
|
-
"invalid_prompt_args"
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return input;
|
|
425
|
-
}
|
|
426
|
-
function normalizePromptResult(value, defaultDescription) {
|
|
427
|
-
if (typeof value === "string") {
|
|
428
|
-
return {
|
|
429
|
-
description: defaultDescription,
|
|
430
|
-
messages: [{
|
|
431
|
-
role: "user",
|
|
432
|
-
content: toolResult.text(value)
|
|
433
|
-
}]
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
if (Array.isArray(value)) {
|
|
437
|
-
return {
|
|
438
|
-
description: defaultDescription,
|
|
439
|
-
messages: value
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
if ("messages" in value) {
|
|
443
|
-
return {
|
|
444
|
-
description: value.description ?? defaultDescription,
|
|
445
|
-
messages: value.messages
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
return {
|
|
449
|
-
description: defaultDescription,
|
|
450
|
-
messages: [value]
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
290
|
function stripUndefined(value) {
|
|
454
291
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
455
292
|
}
|
|
@@ -991,6 +828,83 @@ createRoot(document.getElementById("root")!).render(
|
|
|
991
828
|
entry.descriptor._meta = mergeWidgetMeta(entry.descriptor._meta, widgetMeta(resourceUri, entry.widget.options, entry.target));
|
|
992
829
|
}
|
|
993
830
|
}
|
|
831
|
+
async function buildCodeModeWidget(rootDir, outDir, tools, target, config = void 0) {
|
|
832
|
+
const renderable = tools.filter(
|
|
833
|
+
(entry) => Boolean(entry.widget)
|
|
834
|
+
);
|
|
835
|
+
if (!renderable.length) {
|
|
836
|
+
return void 0;
|
|
837
|
+
}
|
|
838
|
+
const cacheDir = path4.join(rootDir, ".sidecar", "cache", "widgets");
|
|
839
|
+
await mkdir(cacheDir, { recursive: true });
|
|
840
|
+
const appStyle = await prepareAppStyle(rootDir, cacheDir);
|
|
841
|
+
const configure = await loadWidgetBundlerHook(rootDir, cacheDir, config?.configure);
|
|
842
|
+
const safeId = "code-mode";
|
|
843
|
+
const entryFile = path4.join(cacheDir, `${safeId}.entry.tsx`);
|
|
844
|
+
await writeFile(entryFile, renderCodeModeWidgetEntry(rootDir, entryFile, renderable, appStyle));
|
|
845
|
+
const baseOptions = enforceWidgetBuildInvariants({
|
|
846
|
+
absWorkingDir: rootDir,
|
|
847
|
+
alias: sidecarWidgetAliases(rootDir),
|
|
848
|
+
bundle: true,
|
|
849
|
+
define: {
|
|
850
|
+
"process.env.NODE_ENV": JSON.stringify("production")
|
|
851
|
+
},
|
|
852
|
+
entryPoints: [entryFile],
|
|
853
|
+
format: "iife",
|
|
854
|
+
jsx: "automatic",
|
|
855
|
+
minify: true,
|
|
856
|
+
nodePaths: [
|
|
857
|
+
path4.join(rootDir, "node_modules"),
|
|
858
|
+
path4.join(process.cwd(), "node_modules")
|
|
859
|
+
],
|
|
860
|
+
outfile: "widget.js",
|
|
861
|
+
platform: "browser",
|
|
862
|
+
sourcemap: false,
|
|
863
|
+
write: false
|
|
864
|
+
});
|
|
865
|
+
const staticOptions = widgetBuildOptionsFromConfig(rootDir, config);
|
|
866
|
+
const configuredOptions = configure ? await configureWidgetBuild(configure, {
|
|
867
|
+
rootDir,
|
|
868
|
+
outDir,
|
|
869
|
+
entryFile,
|
|
870
|
+
widget: {
|
|
871
|
+
toolId: "execute_code",
|
|
872
|
+
toolName: "Execute Code",
|
|
873
|
+
target,
|
|
874
|
+
sourceFile: path4.relative(rootDir, entryFile)
|
|
875
|
+
},
|
|
876
|
+
esbuildOptions: mergeWidgetBuildOptions(baseOptions, staticOptions)
|
|
877
|
+
}) : void 0;
|
|
878
|
+
const bundled = await esbuild(enforceWidgetBuildInvariants(
|
|
879
|
+
mergeWidgetBuildOptions(baseOptions, staticOptions, configuredOptions),
|
|
880
|
+
{ rootDir, entryFile }
|
|
881
|
+
));
|
|
882
|
+
const outputFiles = bundled.outputFiles ?? [];
|
|
883
|
+
const javascript = outputFiles.find((file) => file.path.endsWith(".js"))?.text ?? "";
|
|
884
|
+
const css = outputFiles.filter((file) => file.path.endsWith(".css")).map((file) => file.text).join("\n");
|
|
885
|
+
const html = renderWidgetHtml("Execute Code", javascript, css);
|
|
886
|
+
const hash = createHash("sha256").update(html).digest("hex").slice(0, 12);
|
|
887
|
+
const outputDir = path4.join(outDir, "public", "widgets", safeId);
|
|
888
|
+
const outputFile = path4.join(outputDir, `widget.${hash}.html`);
|
|
889
|
+
const resourceUri = `ui://${safeId}/widget.${hash}.html`;
|
|
890
|
+
const options = {
|
|
891
|
+
description: "Renders the selected result from a Sidecar code-mode execution.",
|
|
892
|
+
csp: {
|
|
893
|
+
connectDomains: [],
|
|
894
|
+
resourceDomains: []
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
await mkdir(outputDir, { recursive: true });
|
|
898
|
+
await writeFile(outputFile, html);
|
|
899
|
+
return {
|
|
900
|
+
sourceFile: path4.relative(rootDir, entryFile),
|
|
901
|
+
variant: "shared",
|
|
902
|
+
resourceUri,
|
|
903
|
+
resourceMeta: widgetResourceMeta(options, target),
|
|
904
|
+
outputFile: path4.relative(outDir, outputFile),
|
|
905
|
+
options
|
|
906
|
+
};
|
|
907
|
+
}
|
|
994
908
|
function widgetBuildOptionsFromConfig(rootDir, config) {
|
|
995
909
|
const esbuildConfig = config?.esbuild;
|
|
996
910
|
if (!esbuildConfig) {
|
|
@@ -1087,6 +1001,105 @@ function enforceWidgetBuildInvariants(options, required) {
|
|
|
1087
1001
|
write: false
|
|
1088
1002
|
};
|
|
1089
1003
|
}
|
|
1004
|
+
function renderCodeModeWidgetEntry(rootDir, entryFile, entries, appStyle) {
|
|
1005
|
+
const entryDir = path4.dirname(entryFile);
|
|
1006
|
+
const imports = entries.map((entry, index) => {
|
|
1007
|
+
const sourceFile = path4.join(rootDir, entry.widget.sourceFile);
|
|
1008
|
+
return `import Widget${index} from ${JSON.stringify(toImportSpecifier(entryDir, sourceFile))};`;
|
|
1009
|
+
}).join("\n");
|
|
1010
|
+
const registry = entries.map((entry, index) => `${JSON.stringify(entry.id)}: Widget${index}`).join(",\n ");
|
|
1011
|
+
return `import React, { useMemo } from "react";
|
|
1012
|
+
import { createRoot } from "react-dom/client";
|
|
1013
|
+
import {
|
|
1014
|
+
SidecarWidgetProvider,
|
|
1015
|
+
SidecarWidgetRoot,
|
|
1016
|
+
browserBridge,
|
|
1017
|
+
useToolResult
|
|
1018
|
+
} from "@sidecar-ai/react";
|
|
1019
|
+
import { Heading, Stack, Surface, Text } from "@sidecar-ai/native/components";
|
|
1020
|
+
import "@sidecar-ai/native/styles.css";
|
|
1021
|
+
${appStyle ? `import ${JSON.stringify(toImportSpecifier(entryDir, appStyle))};` : ""}
|
|
1022
|
+
${imports}
|
|
1023
|
+
|
|
1024
|
+
const registry = {
|
|
1025
|
+
${registry}
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
function nestedResult(result, payload) {
|
|
1029
|
+
return {
|
|
1030
|
+
...result,
|
|
1031
|
+
structuredContent: payload?.result ?? payload?.value,
|
|
1032
|
+
structured: payload?.result ?? payload?.value,
|
|
1033
|
+
content: payload?.content ?? result.content ?? [],
|
|
1034
|
+
meta: payload?.meta ?? result.meta ?? {},
|
|
1035
|
+
_meta: payload?.meta ?? result._meta ?? {},
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function createNestedBridge(result, payload) {
|
|
1040
|
+
return {
|
|
1041
|
+
...browserBridge,
|
|
1042
|
+
getToolResult() {
|
|
1043
|
+
return nestedResult(result, payload);
|
|
1044
|
+
},
|
|
1045
|
+
subscribeToolResult(listener) {
|
|
1046
|
+
return browserBridge.subscribeToolResult((next) => {
|
|
1047
|
+
const nextPayload = next?.structuredContent?.codeMode;
|
|
1048
|
+
listener(nestedResult(next, nextPayload));
|
|
1049
|
+
});
|
|
1050
|
+
},
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function GenericCodeModeResult({ payload }) {
|
|
1055
|
+
return React.createElement("main", { className: "sidecar-code-mode" },
|
|
1056
|
+
React.createElement(Stack, { gap: "md" },
|
|
1057
|
+
React.createElement(Heading, { level: 1 }, "Code result"),
|
|
1058
|
+
React.createElement(Surface, { variant: "card" },
|
|
1059
|
+
React.createElement("pre", {
|
|
1060
|
+
style: {
|
|
1061
|
+
margin: 0,
|
|
1062
|
+
overflowWrap: "anywhere",
|
|
1063
|
+
whiteSpace: "pre-wrap"
|
|
1064
|
+
}
|
|
1065
|
+
}, JSON.stringify(payload?.value ?? payload ?? {}, null, 2))
|
|
1066
|
+
)
|
|
1067
|
+
)
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function CodeModeWidget() {
|
|
1072
|
+
const result = useToolResult();
|
|
1073
|
+
const payload = result.structuredContent?.codeMode;
|
|
1074
|
+
const renderer = payload?.renderer;
|
|
1075
|
+
const Component = renderer ? registry[renderer] : undefined;
|
|
1076
|
+
const bridge = useMemo(() => createNestedBridge(result, payload), [result, payload]);
|
|
1077
|
+
|
|
1078
|
+
if (Component) {
|
|
1079
|
+
return React.createElement(
|
|
1080
|
+
SidecarWidgetProvider,
|
|
1081
|
+
{ bridge },
|
|
1082
|
+
React.createElement(Component)
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (payload?.ok === false) {
|
|
1087
|
+
return React.createElement("main", { className: "sidecar-code-mode" },
|
|
1088
|
+
React.createElement(Stack, { gap: "sm" },
|
|
1089
|
+
React.createElement(Heading, { level: 1 }, "Code failed"),
|
|
1090
|
+
React.createElement(Text, { tone: "muted" }, "The generated code did not complete successfully.")
|
|
1091
|
+
)
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return React.createElement(GenericCodeModeResult, { payload });
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
createRoot(document.getElementById("root")).render(
|
|
1099
|
+
React.createElement(SidecarWidgetRoot, null, React.createElement(CodeModeWidget))
|
|
1100
|
+
);
|
|
1101
|
+
`;
|
|
1102
|
+
}
|
|
1090
1103
|
function sidecarWidgetAliases(rootDir) {
|
|
1091
1104
|
return {
|
|
1092
1105
|
...devSidecarBundleAliases(rootDir) ?? {},
|
|
@@ -1135,6 +1148,7 @@ function devSidecarBundleAliases(rootDir) {
|
|
|
1135
1148
|
return void 0;
|
|
1136
1149
|
}
|
|
1137
1150
|
return {
|
|
1151
|
+
"sidecar-ai/remote": path4.join(repoRoot, "packages", "sidecar-ai", "src", "remote.ts"),
|
|
1138
1152
|
"sidecar-ai": path4.join(repoRoot, "packages", "sidecar-ai", "src", "index.ts"),
|
|
1139
1153
|
"@sidecar-ai/client": path4.join(repoRoot, "packages", "client", "src", "index.ts"),
|
|
1140
1154
|
"@sidecar-ai/core": path4.join(repoRoot, "packages", "core", "src", "index.ts"),
|
|
@@ -1896,6 +1910,7 @@ function readStringArrayProperty2(definition, propertyName) {
|
|
|
1896
1910
|
|
|
1897
1911
|
// packages/compiler/src/build.ts
|
|
1898
1912
|
import { mkdir as mkdir9, writeFile as writeFile9 } from "fs/promises";
|
|
1913
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1899
1914
|
import path19 from "path";
|
|
1900
1915
|
|
|
1901
1916
|
// packages/compiler/src/config.ts
|
|
@@ -1937,6 +1952,10 @@ function analyzeProjectConfig(rootDir) {
|
|
|
1937
1952
|
pagination: {
|
|
1938
1953
|
pageSize: readNumberNested(definition, "pagination", "pageSize") ?? 50,
|
|
1939
1954
|
hasOverride: hasProperty(readObjectProperty3(definition, "pagination"), "override")
|
|
1955
|
+
},
|
|
1956
|
+
codeMode: readCodeModeConfig(definition),
|
|
1957
|
+
remoteExecution: {
|
|
1958
|
+
enabled: readBooleanProperty3(definition, "remoteExecution") ?? false
|
|
1940
1959
|
}
|
|
1941
1960
|
};
|
|
1942
1961
|
}
|
|
@@ -1994,9 +2013,75 @@ function defaultCompilerConfig() {
|
|
|
1994
2013
|
pagination: {
|
|
1995
2014
|
pageSize: 50,
|
|
1996
2015
|
hasOverride: false
|
|
2016
|
+
},
|
|
2017
|
+
codeMode: {
|
|
2018
|
+
enabled: false,
|
|
2019
|
+
unsafe: false,
|
|
2020
|
+
render: {
|
|
2021
|
+
enabled: true,
|
|
2022
|
+
strategy: "last-renderable"
|
|
2023
|
+
}
|
|
2024
|
+
},
|
|
2025
|
+
remoteExecution: {
|
|
2026
|
+
enabled: false
|
|
1997
2027
|
}
|
|
1998
2028
|
};
|
|
1999
2029
|
}
|
|
2030
|
+
function readCodeModeConfig(definition) {
|
|
2031
|
+
const defaults = defaultCompilerConfig().codeMode;
|
|
2032
|
+
const property = definition.getProperty("codeMode");
|
|
2033
|
+
if (!property || !Node5.isPropertyAssignment(property)) {
|
|
2034
|
+
return defaults;
|
|
2035
|
+
}
|
|
2036
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2037
|
+
if (!initializer) {
|
|
2038
|
+
return defaults;
|
|
2039
|
+
}
|
|
2040
|
+
if (initializer.getKind() === SyntaxKind3.TrueKeyword) {
|
|
2041
|
+
return { ...defaults, enabled: true };
|
|
2042
|
+
}
|
|
2043
|
+
if (initializer.getKind() === SyntaxKind3.FalseKeyword) {
|
|
2044
|
+
return { ...defaults, enabled: false };
|
|
2045
|
+
}
|
|
2046
|
+
if (!Node5.isObjectLiteralExpression(initializer)) {
|
|
2047
|
+
return defaults;
|
|
2048
|
+
}
|
|
2049
|
+
return {
|
|
2050
|
+
enabled: true,
|
|
2051
|
+
unsafe: readBooleanProperty3(initializer, "unsafe") ?? false,
|
|
2052
|
+
render: readCodeModeRenderConfig(initializer) ?? defaults.render
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
function readCodeModeRenderConfig(definition) {
|
|
2056
|
+
const property = definition.getProperty("render");
|
|
2057
|
+
if (!property || !Node5.isPropertyAssignment(property)) {
|
|
2058
|
+
return void 0;
|
|
2059
|
+
}
|
|
2060
|
+
const initializer = unwrapExpression(property.getInitializer());
|
|
2061
|
+
if (!initializer) {
|
|
2062
|
+
return void 0;
|
|
2063
|
+
}
|
|
2064
|
+
if (initializer.getKind() === SyntaxKind3.TrueKeyword) {
|
|
2065
|
+
return { enabled: true, strategy: "last-renderable" };
|
|
2066
|
+
}
|
|
2067
|
+
if (initializer.getKind() === SyntaxKind3.FalseKeyword) {
|
|
2068
|
+
return { enabled: false, strategy: "last-renderable" };
|
|
2069
|
+
}
|
|
2070
|
+
if (!Node5.isObjectLiteralExpression(initializer)) {
|
|
2071
|
+
return void 0;
|
|
2072
|
+
}
|
|
2073
|
+
return {
|
|
2074
|
+
enabled: readBooleanProperty3(initializer, "enabled") ?? true,
|
|
2075
|
+
strategy: readRenderStrategy(initializer) ?? "last-renderable"
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
function readRenderStrategy(definition) {
|
|
2079
|
+
const value = readStringProperty3(definition, "strategy");
|
|
2080
|
+
if (value === "last-renderable" || value === "first-renderable" || value === "explicit") {
|
|
2081
|
+
return value;
|
|
2082
|
+
}
|
|
2083
|
+
return void 0;
|
|
2084
|
+
}
|
|
2000
2085
|
function readTargetNested(definition, section, propertyName) {
|
|
2001
2086
|
const value = readStringNested(definition, section, propertyName);
|
|
2002
2087
|
return value === "mcp" || value === "chatgpt" || value === "claude" ? value : void 0;
|
|
@@ -2017,7 +2102,10 @@ function readBooleanNested(definition, section, propertyName) {
|
|
|
2017
2102
|
if (!object) {
|
|
2018
2103
|
return void 0;
|
|
2019
2104
|
}
|
|
2020
|
-
|
|
2105
|
+
return readBooleanProperty3(object, propertyName);
|
|
2106
|
+
}
|
|
2107
|
+
function readBooleanProperty3(definition, propertyName) {
|
|
2108
|
+
const property = definition.getProperty(propertyName);
|
|
2021
2109
|
if (!property || !Node5.isPropertyAssignment(property)) {
|
|
2022
2110
|
return void 0;
|
|
2023
2111
|
}
|
|
@@ -3046,12 +3134,12 @@ function readArgInput(node) {
|
|
|
3046
3134
|
if (Node7.isObjectLiteralExpression(initializer)) {
|
|
3047
3135
|
return {
|
|
3048
3136
|
description: getOptionalStringProperty2(initializer, "description"),
|
|
3049
|
-
required:
|
|
3137
|
+
required: readBooleanProperty4(initializer, "required")
|
|
3050
3138
|
};
|
|
3051
3139
|
}
|
|
3052
3140
|
return void 0;
|
|
3053
3141
|
}
|
|
3054
|
-
function
|
|
3142
|
+
function readBooleanProperty4(definition, propertyName) {
|
|
3055
3143
|
const property = definition.getProperty(propertyName);
|
|
3056
3144
|
if (!property || !Node7.isPropertyAssignment(property)) {
|
|
3057
3145
|
return void 0;
|
|
@@ -3170,7 +3258,7 @@ function analyzeResourceFile(sourceFile, rootDir) {
|
|
|
3170
3258
|
mimeType: descriptor.mimeType,
|
|
3171
3259
|
size: descriptor.size,
|
|
3172
3260
|
annotations: descriptor.annotations,
|
|
3173
|
-
subscribe:
|
|
3261
|
+
subscribe: readBooleanProperty5(definition, "subscribe"),
|
|
3174
3262
|
descriptor
|
|
3175
3263
|
};
|
|
3176
3264
|
}
|
|
@@ -3231,7 +3319,7 @@ function getOptionalNumberProperty(definition, propertyName) {
|
|
|
3231
3319
|
const initializer = unwrapExpression(property.getInitializer());
|
|
3232
3320
|
return initializer && Node8.isNumericLiteral(initializer) ? Number(initializer.getLiteralText()) : void 0;
|
|
3233
3321
|
}
|
|
3234
|
-
function
|
|
3322
|
+
function readBooleanProperty5(definition, propertyName) {
|
|
3235
3323
|
const property = definition.getProperty(propertyName);
|
|
3236
3324
|
if (!property || !Node8.isPropertyAssignment(property)) {
|
|
3237
3325
|
return void 0;
|
|
@@ -3379,7 +3467,7 @@ function renderServerEntry(rootDir, entryFile, manifest, identity) {
|
|
|
3379
3467
|
`import { pathToFileURL } from "node:url";`,
|
|
3380
3468
|
`import { createSidecarHttpHandler } from "@sidecar-ai/server";`,
|
|
3381
3469
|
`import { isSidecarAuth } from "@sidecar-ai/auth";`,
|
|
3382
|
-
`import { isSidecarPrompt, isSidecarResource, isSidecarTool } from "@sidecar-ai/core";`,
|
|
3470
|
+
`import { isSidecarPrompt, isSidecarRemote, isSidecarResource, isSidecarTool } from "@sidecar-ai/core";`,
|
|
3383
3471
|
`import { isSidecarProxy } from "@sidecar-ai/server/proxy";`,
|
|
3384
3472
|
...tools.map(
|
|
3385
3473
|
(entry, index) => `import tool${index} from ${JSON.stringify(toImportSpecifier(entryDir, path18.join(rootDir, entry.sourceFile)))};`
|
|
@@ -3392,6 +3480,7 @@ function renderServerEntry(rootDir, entryFile, manifest, identity) {
|
|
|
3392
3480
|
),
|
|
3393
3481
|
existsSync6(path18.join(rootDir, "auth.ts")) ? `import authExport from ${JSON.stringify(toImportSpecifier(entryDir, path18.join(rootDir, "auth.ts")))};` : `const authExport = undefined;`,
|
|
3394
3482
|
existsSync6(path18.join(rootDir, "proxy.ts")) ? `import proxyExport from ${JSON.stringify(toImportSpecifier(entryDir, path18.join(rootDir, "proxy.ts")))};` : `const proxyExport = undefined;`,
|
|
3483
|
+
existsSync6(path18.join(rootDir, "remote.ts")) ? `import remoteExport from ${JSON.stringify(toImportSpecifier(entryDir, path18.join(rootDir, "remote.ts")))};` : `const remoteExport = undefined;`,
|
|
3395
3484
|
existsSync6(path18.join(rootDir, "sidecar.config.ts")) ? `import configExport from ${JSON.stringify(toImportSpecifier(entryDir, path18.join(rootDir, "sidecar.config.ts")))};` : `const configExport = undefined;`
|
|
3396
3485
|
].join("\n");
|
|
3397
3486
|
return `${imports}
|
|
@@ -3404,6 +3493,9 @@ const auth = loadedAuth && process.env.SIDECAR_MCP_URL
|
|
|
3404
3493
|
? loadedAuth.withResource(process.env.SIDECAR_MCP_URL)
|
|
3405
3494
|
: loadedAuth;
|
|
3406
3495
|
const proxy = proxyExport === undefined ? undefined : assertProxy(proxyExport);
|
|
3496
|
+
const remoteExecution = manifest.codeMode?.remoteExecution
|
|
3497
|
+
? assertRemote(remoteExport)
|
|
3498
|
+
: undefined;
|
|
3407
3499
|
const runtimeConfig = configExport && typeof configExport === "object" ? configExport : undefined;
|
|
3408
3500
|
|
|
3409
3501
|
if (auth && !process.env.SIDECAR_MCP_URL) {
|
|
@@ -3417,6 +3509,21 @@ export const handler = createSidecarHttpHandler({
|
|
|
3417
3509
|
publicUrl: process.env.SIDECAR_PUBLIC_URL ?? process.env.SIDECAR_MCP_URL,
|
|
3418
3510
|
auth,
|
|
3419
3511
|
proxy,
|
|
3512
|
+
codeMode: manifest.codeMode
|
|
3513
|
+
? {
|
|
3514
|
+
enabled: true,
|
|
3515
|
+
unsafe: manifest.codeMode.unsafe,
|
|
3516
|
+
render: manifest.codeMode.render,
|
|
3517
|
+
widgetMeta: manifest.codeMode.widget?.resourceUri
|
|
3518
|
+
? {
|
|
3519
|
+
ui: { resourceUri: manifest.codeMode.widget.resourceUri },
|
|
3520
|
+
"ui/resourceUri": manifest.codeMode.widget.resourceUri,
|
|
3521
|
+
...(manifest.target === "chatgpt" ? { "openai/outputTemplate": manifest.codeMode.widget.resourceUri } : {}),
|
|
3522
|
+
}
|
|
3523
|
+
: undefined,
|
|
3524
|
+
}
|
|
3525
|
+
: undefined,
|
|
3526
|
+
remoteExecution,
|
|
3420
3527
|
tools: [
|
|
3421
3528
|
${tools.map((entry, index) => ` loadTool(${JSON.stringify(entry.sourceFile)}, tool${index}, manifest.tools[${index}].descriptor),`).join("\n")}
|
|
3422
3529
|
],
|
|
@@ -3448,12 +3555,23 @@ if (isDirectRun()) {
|
|
|
3448
3555
|
server.listen(port, host, () => {
|
|
3449
3556
|
const localHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
3450
3557
|
console.log(\`MCP running on Streamable HTTP at http://\${localHost}:\${port}\${process.env.SIDECAR_MCP_PATH ?? "/mcp"}\`);
|
|
3558
|
+
printRuntimeUrls(localHost, port);
|
|
3451
3559
|
});
|
|
3452
3560
|
|
|
3453
3561
|
process.on("SIGTERM", () => shutdown());
|
|
3454
3562
|
process.on("SIGINT", () => shutdown());
|
|
3455
3563
|
}
|
|
3456
3564
|
|
|
3565
|
+
function printRuntimeUrls(localHost, port) {
|
|
3566
|
+
const mcpPath = process.env.SIDECAR_MCP_PATH ?? "/mcp";
|
|
3567
|
+
console.log("Sidecar URLs:");
|
|
3568
|
+
console.log(\` Local MCP: http://\${localHost}:\${port}\${mcpPath}\`);
|
|
3569
|
+
console.log(\` Public MCP: \${process.env.SIDECAR_MCP_URL ?? "set SIDECAR_MCP_URL=https://your-host.example.com" + mcpPath}\`);
|
|
3570
|
+
if (process.env.SIDECAR_PUBLIC_URL) {
|
|
3571
|
+
console.log(\` Public base: \${process.env.SIDECAR_PUBLIC_URL}\`);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3457
3575
|
function loadTool(sourceFile, value, descriptor) {
|
|
3458
3576
|
const tool = unwrapRuntimeDefault(value);
|
|
3459
3577
|
if (!isSidecarTool(tool)) {
|
|
@@ -3498,6 +3616,14 @@ function assertProxy(value) {
|
|
|
3498
3616
|
return proxyValue;
|
|
3499
3617
|
}
|
|
3500
3618
|
|
|
3619
|
+
function assertRemote(value) {
|
|
3620
|
+
const remoteValue = unwrapRuntimeDefault(value);
|
|
3621
|
+
if (!isSidecarRemote(remoteValue)) {
|
|
3622
|
+
throw new Error("remote.ts must default-export remote({ execute }) from sidecar-ai/remote.");
|
|
3623
|
+
}
|
|
3624
|
+
return remoteValue;
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3501
3627
|
function unwrapRuntimeDefault(value) {
|
|
3502
3628
|
if (
|
|
3503
3629
|
value &&
|
|
@@ -3538,7 +3664,7 @@ function shutdown() {
|
|
|
3538
3664
|
`;
|
|
3539
3665
|
}
|
|
3540
3666
|
function renderWidgetResources(manifest) {
|
|
3541
|
-
|
|
3667
|
+
const toolWidgets = manifest.tools.filter((entry) => entry.widget?.outputFile).map((entry) => ` {
|
|
3542
3668
|
uri: ${JSON.stringify(entry.widget?.resourceUri)},
|
|
3543
3669
|
name: ${JSON.stringify(entry.name)},
|
|
3544
3670
|
description: ${JSON.stringify(entry.widget?.options?.description)},
|
|
@@ -3546,6 +3672,15 @@ function renderWidgetResources(manifest) {
|
|
|
3546
3672
|
text: readWidget(${JSON.stringify(entry.widget?.outputFile)}),
|
|
3547
3673
|
_meta: ${JSON.stringify(entry.widget?.resourceMeta ?? void 0)},
|
|
3548
3674
|
},`).join("\n");
|
|
3675
|
+
const codeModeWidget = manifest.codeMode?.widget?.outputFile ? ` {
|
|
3676
|
+
uri: ${JSON.stringify(manifest.codeMode.widget.resourceUri)},
|
|
3677
|
+
name: "Execute Code",
|
|
3678
|
+
description: ${JSON.stringify(manifest.codeMode.widget.options?.description)},
|
|
3679
|
+
mimeType: "text/html;profile=mcp-app",
|
|
3680
|
+
text: readWidget(${JSON.stringify(manifest.codeMode.widget.outputFile)}),
|
|
3681
|
+
_meta: ${JSON.stringify(manifest.codeMode.widget.resourceMeta ?? void 0)},
|
|
3682
|
+
},` : "";
|
|
3683
|
+
return [codeModeWidget, toolWidgets].filter(Boolean).join("\n");
|
|
3549
3684
|
}
|
|
3550
3685
|
function renderServerPackage(identity) {
|
|
3551
3686
|
return `${JSON.stringify({
|
|
@@ -3595,6 +3730,7 @@ function sidecarBundleAliases(rootDir) {
|
|
|
3595
3730
|
return void 0;
|
|
3596
3731
|
}
|
|
3597
3732
|
return {
|
|
3733
|
+
"sidecar-ai/remote": path18.join(repoRoot, "packages", "sidecar-ai", "src", "remote.ts"),
|
|
3598
3734
|
"sidecar-ai": path18.join(repoRoot, "packages", "sidecar-ai", "src", "index.ts"),
|
|
3599
3735
|
"@sidecar-ai/auth": path18.join(repoRoot, "packages", "auth", "src", "index.ts"),
|
|
3600
3736
|
"@sidecar-ai/core": path18.join(repoRoot, "packages", "core", "src", "index.ts"),
|
|
@@ -3638,6 +3774,12 @@ async function buildProject(options) {
|
|
|
3638
3774
|
const target = options.target ?? config.build.target ?? "mcp";
|
|
3639
3775
|
const host = options.host ?? config.build.host ?? "node";
|
|
3640
3776
|
const plugins = options.plugins ?? config.build.plugins ?? false;
|
|
3777
|
+
if (config.codeMode.enabled && !config.codeMode.unsafe && !config.remoteExecution.enabled) {
|
|
3778
|
+
throw new Error("Code mode requires remoteExecution: true, or codeMode: { unsafe: true } for trusted local use.");
|
|
3779
|
+
}
|
|
3780
|
+
if (config.codeMode.enabled && config.remoteExecution.enabled && !existsSync7(path19.join(rootDir, "remote.ts"))) {
|
|
3781
|
+
throw new Error("remoteExecution: true requires a reserved remote.ts file at the project root.");
|
|
3782
|
+
}
|
|
3641
3783
|
const tools = await analyzeProjectTools(rootDir, { target });
|
|
3642
3784
|
const resources = await analyzeProjectResources(rootDir);
|
|
3643
3785
|
const resourceTemplates = [];
|
|
@@ -3656,6 +3798,7 @@ async function buildProject(options) {
|
|
|
3656
3798
|
const outDir = resolveInsideRoot(rootDir, options.outDir ?? config.build.outDir ?? defaultBuildOutDir(host, target));
|
|
3657
3799
|
const runtimeOutDir = resolveRuntimeOutputDir(outDir, host);
|
|
3658
3800
|
await buildWidgets(rootDir, runtimeOutDir, tools, config.build.widgets);
|
|
3801
|
+
const codeModeWidget = config.codeMode.enabled && config.codeMode.render.enabled ? await buildCodeModeWidget(rootDir, runtimeOutDir, tools, target, config.build.widgets) : void 0;
|
|
3659
3802
|
const manifest = {
|
|
3660
3803
|
version: 1,
|
|
3661
3804
|
target,
|
|
@@ -3664,6 +3807,13 @@ async function buildProject(options) {
|
|
|
3664
3807
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3665
3808
|
config,
|
|
3666
3809
|
tools,
|
|
3810
|
+
codeMode: config.codeMode.enabled ? {
|
|
3811
|
+
enabled: true,
|
|
3812
|
+
unsafe: config.codeMode.unsafe,
|
|
3813
|
+
remoteExecution: config.remoteExecution.enabled,
|
|
3814
|
+
render: config.codeMode.render,
|
|
3815
|
+
widget: codeModeWidget
|
|
3816
|
+
} : void 0,
|
|
3667
3817
|
resources,
|
|
3668
3818
|
resourceTemplates,
|
|
3669
3819
|
prompts,
|
|
@@ -3768,1359 +3918,1879 @@ This output uses Vercel's Build Output API. The MCP function is emitted at \`${V
|
|
|
3768
3918
|
|
|
3769
3919
|
// packages/server/src/index.ts
|
|
3770
3920
|
import { createServer } from "http";
|
|
3771
|
-
import {
|
|
3921
|
+
import {
|
|
3922
|
+
createCipheriv,
|
|
3923
|
+
createDecipheriv,
|
|
3924
|
+
createHash as createHash2,
|
|
3925
|
+
randomBytes,
|
|
3926
|
+
randomUUID as randomUUID2
|
|
3927
|
+
} from "crypto";
|
|
3772
3928
|
|
|
3773
3929
|
// packages/server/src/proxy.ts
|
|
3774
3930
|
import { randomUUID } from "crypto";
|
|
3775
|
-
var proxyBrand = /* @__PURE__ */ Symbol.for("sidecar.proxy");
|
|
3776
|
-
function isSidecarProxy(value) {
|
|
3777
|
-
return Boolean(
|
|
3778
|
-
value && typeof value === "object" && (value[proxyBrand] === true || value.kind === "sidecar.proxy")
|
|
3779
|
-
);
|
|
3780
|
-
}
|
|
3781
|
-
async function runProxy(proxyConfig, request) {
|
|
3782
|
-
for (const middleware of proxyConfig?.before ?? []) {
|
|
3783
|
-
const result = await middleware(request);
|
|
3784
|
-
if (result) {
|
|
3785
|
-
return result;
|
|
3786
|
-
}
|
|
3787
|
-
}
|
|
3788
|
-
return void 0;
|
|
3789
|
-
}
|
|
3790
3931
|
|
|
3791
3932
|
// packages/server/src/sse.ts
|
|
3792
3933
|
import { JSONRPC_VERSION } from "@modelcontextprotocol/sdk/types.js";
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3934
|
+
|
|
3935
|
+
// packages/server/src/index.ts
|
|
3936
|
+
import { JSONRPC_VERSION as JSONRPC_VERSION2 } from "@modelcontextprotocol/sdk/types.js";
|
|
3937
|
+
var CODE_MODE_CALLBACK_TOKEN_TTL_MS = 5 * 60 * 1e3;
|
|
3938
|
+
|
|
3939
|
+
// packages/cli/src/dev-harness.ts
|
|
3940
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
3941
|
+
import { readFileSync } from "fs";
|
|
3942
|
+
import { createServer as createServer2 } from "http";
|
|
3943
|
+
import { createRequire as createRequire2 } from "module";
|
|
3944
|
+
import path20 from "path";
|
|
3945
|
+
import { fileURLToPath } from "url";
|
|
3946
|
+
var require2 = createRequire2(import.meta.url);
|
|
3947
|
+
var OPENAI_CHAT_COMPLETIONS_URL = "https://api.openai.com/v1/chat/completions";
|
|
3948
|
+
var MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
3949
|
+
var MCP_APPS_PROTOCOL_VERSION = "2026-01-26";
|
|
3950
|
+
var MAX_CHAT_TOOL_ITERATIONS = 4;
|
|
3951
|
+
var streamdownClientScriptPromise;
|
|
3952
|
+
async function startDevHarness(options) {
|
|
3953
|
+
const state = {
|
|
3954
|
+
host: options.host,
|
|
3955
|
+
theme: options.theme,
|
|
3956
|
+
device: options.device,
|
|
3957
|
+
target: options.target,
|
|
3958
|
+
model: options.model
|
|
3809
3959
|
};
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3960
|
+
const stateClients = /* @__PURE__ */ new Set();
|
|
3961
|
+
const server = createServer2(async (request, response) => {
|
|
3962
|
+
try {
|
|
3963
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
3964
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
3965
|
+
sendHtml(response, renderDevHarnessHtml(state, { initialBearerToken: options.initialBearerToken }));
|
|
3966
|
+
return;
|
|
3967
|
+
}
|
|
3968
|
+
if (request.method === "GET" && url.pathname === "/__sidecar/dev/streamdown-client.js") {
|
|
3969
|
+
response.writeHead(200, {
|
|
3970
|
+
"cache-control": "no-cache",
|
|
3971
|
+
"content-type": "text/javascript; charset=utf-8"
|
|
3972
|
+
});
|
|
3973
|
+
response.end(await streamdownClientScript());
|
|
3974
|
+
return;
|
|
3975
|
+
}
|
|
3976
|
+
if (request.method === "GET" && url.pathname === "/__sidecar/dev/state") {
|
|
3977
|
+
sendJson(response, devStatePayload(state, options.mcpUrl));
|
|
3978
|
+
return;
|
|
3979
|
+
}
|
|
3980
|
+
if (request.method === "POST" && url.pathname === "/__sidecar/dev/state") {
|
|
3981
|
+
const body = await readJson(request);
|
|
3982
|
+
updateDevState(state, body);
|
|
3983
|
+
sendJson(response, devStatePayload(state, options.mcpUrl));
|
|
3984
|
+
broadcastState(stateClients, state, options.mcpUrl);
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
3987
|
+
if (request.method === "GET" && url.pathname === "/__sidecar/dev/events") {
|
|
3988
|
+
openStateEvents(response, stateClients, state, options.mcpUrl);
|
|
3989
|
+
return;
|
|
3990
|
+
}
|
|
3991
|
+
if (request.method === "GET" && url.pathname === "/__sidecar/dev/resource") {
|
|
3992
|
+
const uri = url.searchParams.get("uri");
|
|
3993
|
+
const authToken = readBearerFromCookie(request);
|
|
3994
|
+
if (!uri) {
|
|
3995
|
+
sendJson(response, { error: "missing_resource_uri" }, 400);
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
const resource = await readMcpResource(options.mcpUrl, uri, authToken);
|
|
3999
|
+
response.writeHead(200, { "content-type": resource.mimeType ?? "text/html; charset=utf-8" });
|
|
4000
|
+
response.end(resource.text ?? "");
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
4003
|
+
if (request.method === "POST" && url.pathname === "/__sidecar/dev/rpc") {
|
|
4004
|
+
const body = await readJson(request);
|
|
4005
|
+
const method = typeof body.method === "string" ? body.method : void 0;
|
|
4006
|
+
if (!method) {
|
|
4007
|
+
sendJson(response, { error: "missing_method" }, 400);
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
const result = await callMcp(options.mcpUrl, method, body.params, readAuthToken(body.authToken));
|
|
4011
|
+
sendJson(response, { result });
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
if (request.method === "POST" && url.pathname === "/__sidecar/dev/chat") {
|
|
4015
|
+
await handleChatRequest(request, response, options);
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
sendJson(response, { error: "not_found" }, 404);
|
|
4019
|
+
} catch (error) {
|
|
4020
|
+
sendJson(response, normalizeHttpError(error), error instanceof HttpError ? error.status : 500);
|
|
4021
|
+
}
|
|
3827
4022
|
});
|
|
3828
|
-
|
|
3829
|
-
data:
|
|
3830
|
-
|
|
3831
|
-
`);
|
|
3832
|
-
function writeRaw(frame) {
|
|
3833
|
-
if (!closed && !response.destroyed) {
|
|
3834
|
-
response.write(frame);
|
|
3835
|
-
}
|
|
3836
|
-
}
|
|
3837
|
-
function sendJson(value) {
|
|
3838
|
-
writeRaw([
|
|
3839
|
-
`id: ${nextSseEventId()}`,
|
|
3840
|
-
"event: message",
|
|
3841
|
-
`data: ${JSON.stringify(value)}`,
|
|
3842
|
-
"",
|
|
3843
|
-
""
|
|
3844
|
-
].join("\n"));
|
|
3845
|
-
}
|
|
4023
|
+
const port = await listenOnLocalhost(server, options.port);
|
|
3846
4024
|
return {
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
jsonrpc: JSONRPC_VERSION,
|
|
3851
|
-
method,
|
|
3852
|
-
params
|
|
3853
|
-
}));
|
|
3854
|
-
},
|
|
3855
|
-
sendJson,
|
|
3856
|
-
end() {
|
|
3857
|
-
closed = true;
|
|
3858
|
-
clearInterval(keepAlive);
|
|
3859
|
-
response.end();
|
|
3860
|
-
}
|
|
4025
|
+
port,
|
|
4026
|
+
url: `http://127.0.0.1:${port}`,
|
|
4027
|
+
close: () => closeServer(server)
|
|
3861
4028
|
};
|
|
3862
4029
|
}
|
|
3863
|
-
function
|
|
3864
|
-
|
|
3865
|
-
|
|
4030
|
+
function mcpToolsToOpenAiTools(tools) {
|
|
4031
|
+
const used = /* @__PURE__ */ new Set();
|
|
4032
|
+
const byOpenAiName = /* @__PURE__ */ new Map();
|
|
4033
|
+
const openAiTools = tools.map((tool) => {
|
|
4034
|
+
const name = uniqueOpenAiToolName(tool.name, used);
|
|
4035
|
+
byOpenAiName.set(name, tool);
|
|
4036
|
+
return {
|
|
4037
|
+
type: "function",
|
|
4038
|
+
function: {
|
|
4039
|
+
name,
|
|
4040
|
+
description: tool.description ?? tool.title ?? tool.name,
|
|
4041
|
+
parameters: normalizeToolParameters(tool.inputSchema)
|
|
4042
|
+
}
|
|
4043
|
+
};
|
|
4044
|
+
});
|
|
4045
|
+
return { byOpenAiName, tools: openAiTools };
|
|
3866
4046
|
}
|
|
3867
|
-
function
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
4047
|
+
function toolResourceUri(tool) {
|
|
4048
|
+
const meta = tool._meta;
|
|
4049
|
+
const nested = meta?.ui && typeof meta.ui === "object" ? meta.ui.resourceUri : void 0;
|
|
4050
|
+
const uri = nested ?? meta?.["ui/resourceUri"] ?? meta?.["openai/outputTemplate"];
|
|
4051
|
+
return typeof uri === "string" && uri ? uri : void 0;
|
|
3871
4052
|
}
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
4053
|
+
function renderDevHarnessHtml(state, options = {}) {
|
|
4054
|
+
return `<!doctype html>
|
|
4055
|
+
<html lang="en" data-sidecar-host="${state.host}" data-sidecar-theme="${state.theme}" data-sidecar-device="${state.device}">
|
|
4056
|
+
<head>
|
|
4057
|
+
<meta charset="utf-8" />
|
|
4058
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
4059
|
+
<title>Sidecar dev</title>
|
|
4060
|
+
<style>
|
|
4061
|
+
:root {
|
|
4062
|
+
color-scheme: light dark;
|
|
4063
|
+
--radius: 12px;
|
|
4064
|
+
--bg: #f8f9fa;
|
|
4065
|
+
--text: #18181b;
|
|
4066
|
+
--panel: #ffffff;
|
|
4067
|
+
--panel-soft: #f4f4f5;
|
|
4068
|
+
--muted: #71717a;
|
|
4069
|
+
--border: #e4e4e7;
|
|
4070
|
+
--accent: #2563eb;
|
|
4071
|
+
--accent-text: #ffffff;
|
|
4072
|
+
--control-bg: #18181b;
|
|
4073
|
+
--control-text: #ffffff;
|
|
4074
|
+
--send-bg: var(--accent);
|
|
4075
|
+
--send-text: var(--accent-text);
|
|
4076
|
+
--ring: rgba(37, 99, 235, 0.16);
|
|
4077
|
+
--widget-bg: #ffffff;
|
|
4078
|
+
--user-bubble-bg: #f4f4f5;
|
|
4079
|
+
--code-bg: #f4f4f5;
|
|
4080
|
+
--code-inline-bg: rgba(0, 0, 0, 0.04);
|
|
4081
|
+
--sidebar-width: 80px;
|
|
4082
|
+
--chat-width: 420px;
|
|
4083
|
+
--mobile-width: min(430px, calc(100vw - 28px));
|
|
4084
|
+
--shadow: 0 16px 48px rgba(0, 0, 0, 0.06);
|
|
3884
4085
|
}
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
4086
|
+
:root[data-sidecar-theme="dark"] {
|
|
4087
|
+
--bg: #0b0b0f;
|
|
4088
|
+
--text: #f4f4f5;
|
|
4089
|
+
--panel: #16161a;
|
|
4090
|
+
--panel-soft: #1e1e24;
|
|
4091
|
+
--muted: #8a8a93;
|
|
4092
|
+
--border: #232329;
|
|
4093
|
+
--accent: #3b82f6;
|
|
4094
|
+
--accent-text: #ffffff;
|
|
4095
|
+
--control-bg: #f4f4f5;
|
|
4096
|
+
--control-text: #0b0b0f;
|
|
4097
|
+
--send-bg: var(--accent);
|
|
4098
|
+
--send-text: var(--accent-text);
|
|
4099
|
+
--ring: rgba(59, 130, 246, 0.2);
|
|
4100
|
+
--widget-bg: #16161a;
|
|
4101
|
+
--user-bubble-bg: #1c1c21;
|
|
4102
|
+
--code-bg: #121216;
|
|
4103
|
+
--code-inline-bg: rgba(255, 255, 255, 0.06);
|
|
4104
|
+
--shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
|
|
3890
4105
|
}
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
mimeType: resource.mimeType,
|
|
3896
|
-
_meta: resource._meta
|
|
3897
|
-
};
|
|
3898
|
-
this.resources.set(resource.uri, { ...resource, descriptor });
|
|
3899
|
-
}
|
|
3900
|
-
for (const loaded of options.prompts ?? []) {
|
|
3901
|
-
const descriptor = loaded.descriptor ?? {
|
|
3902
|
-
name: loaded.prompt.name ?? loaded.prompt.title,
|
|
3903
|
-
title: loaded.prompt.title,
|
|
3904
|
-
description: loaded.prompt.description
|
|
3905
|
-
};
|
|
3906
|
-
if (this.prompts.has(descriptor.name)) {
|
|
3907
|
-
throw new Error(`Duplicate Sidecar prompt name "${descriptor.name}".`);
|
|
4106
|
+
:root[data-sidecar-host="claude"] {
|
|
4107
|
+
--accent: #c96442;
|
|
4108
|
+
--accent-text: #fffaf4;
|
|
4109
|
+
--ring: rgba(201, 100, 66, 0.2);
|
|
3908
4110
|
}
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
options;
|
|
3914
|
-
tools = /* @__PURE__ */ new Map();
|
|
3915
|
-
resources = /* @__PURE__ */ new Map();
|
|
3916
|
-
prompts = /* @__PURE__ */ new Map();
|
|
3917
|
-
resourceTemplates;
|
|
3918
|
-
activeRequests = /* @__PURE__ */ new Map();
|
|
3919
|
-
subscribedResourceUris = /* @__PURE__ */ new Set();
|
|
3920
|
-
/** Returns all tool descriptors exposed through `tools/list`. */
|
|
3921
|
-
descriptors() {
|
|
3922
|
-
return [...this.tools.values()].map((loaded) => loaded.descriptor);
|
|
3923
|
-
}
|
|
3924
|
-
/** Returns all resource descriptors exposed through `resources/list`. */
|
|
3925
|
-
resourceDescriptors() {
|
|
3926
|
-
return [...this.resources.values()].map((loaded) => loaded.descriptor);
|
|
3927
|
-
}
|
|
3928
|
-
/** Returns all prompt descriptors exposed through `prompts/list`. */
|
|
3929
|
-
promptDescriptors() {
|
|
3930
|
-
return [...this.prompts.values()].map((loaded) => loaded.descriptor);
|
|
3931
|
-
}
|
|
3932
|
-
/** Handles a JSON-RPC request or notification. */
|
|
3933
|
-
async handle(request, context = {}) {
|
|
3934
|
-
if (request.id === void 0) {
|
|
3935
|
-
await this.authorizeEndpoint(context);
|
|
3936
|
-
await this.handleNotification(request);
|
|
3937
|
-
return void 0;
|
|
3938
|
-
}
|
|
3939
|
-
try {
|
|
3940
|
-
const authSession = await this.authorizeEndpoint(context);
|
|
3941
|
-
const authorizedContext = { ...context, authSession };
|
|
3942
|
-
const result = await this.dispatch(request, authorizedContext);
|
|
3943
|
-
return {
|
|
3944
|
-
jsonrpc: JSONRPC_VERSION2,
|
|
3945
|
-
id: request.id,
|
|
3946
|
-
result
|
|
3947
|
-
};
|
|
3948
|
-
} catch (error) {
|
|
3949
|
-
if (error instanceof RequestCancelledError) {
|
|
3950
|
-
return void 0;
|
|
4111
|
+
:root[data-sidecar-host="claude"][data-sidecar-theme="dark"] {
|
|
4112
|
+
--accent: #d97757;
|
|
4113
|
+
--accent-text: #ffffff;
|
|
4114
|
+
--ring: rgba(217, 119, 87, 0.24);
|
|
3951
4115
|
}
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
};
|
|
3957
|
-
}
|
|
3958
|
-
}
|
|
3959
|
-
/** Routes an MCP method to its implementation. */
|
|
3960
|
-
async dispatch(request, context) {
|
|
3961
|
-
switch (request.method) {
|
|
3962
|
-
case "initialize":
|
|
3963
|
-
return {
|
|
3964
|
-
protocolVersion: SIDECAR_MCP_PROTOCOL_VERSION,
|
|
3965
|
-
capabilities: this.capabilities(),
|
|
3966
|
-
serverInfo: {
|
|
3967
|
-
name: this.options.name ?? "sidecar",
|
|
3968
|
-
version: this.options.version ?? "0.0.0-dev"
|
|
3969
|
-
}
|
|
3970
|
-
};
|
|
3971
|
-
case "ping":
|
|
3972
|
-
return {};
|
|
3973
|
-
case "tools/list":
|
|
3974
|
-
return this.listTools(request, context);
|
|
3975
|
-
case "tools/call":
|
|
3976
|
-
return this.callTool(request, context);
|
|
3977
|
-
case "resources/list":
|
|
3978
|
-
return this.listResources(request, context);
|
|
3979
|
-
case "resources/read":
|
|
3980
|
-
return this.readResource(request, context);
|
|
3981
|
-
case "resources/templates/list":
|
|
3982
|
-
return this.listResourceTemplates(request, context);
|
|
3983
|
-
case "resources/subscribe":
|
|
3984
|
-
return this.subscribeResource(request);
|
|
3985
|
-
case "resources/unsubscribe":
|
|
3986
|
-
return this.unsubscribeResource(request);
|
|
3987
|
-
case "prompts/list":
|
|
3988
|
-
return this.listPrompts(request, context);
|
|
3989
|
-
case "prompts/get":
|
|
3990
|
-
return this.getPrompt(request, context);
|
|
3991
|
-
default:
|
|
3992
|
-
throw new JsonRpcError(-32601, `Unsupported method "${request.method}".`);
|
|
3993
|
-
}
|
|
3994
|
-
}
|
|
3995
|
-
/** Builds the MCP server capability object from implemented runtime features. */
|
|
3996
|
-
capabilities() {
|
|
3997
|
-
const configured = this.options.capabilities ?? {};
|
|
3998
|
-
return stripUndefined4({
|
|
3999
|
-
tools: stripUndefined4({
|
|
4000
|
-
listChanged: configured.tools?.listChanged || void 0
|
|
4001
|
-
}),
|
|
4002
|
-
resources: stripUndefined4({
|
|
4003
|
-
subscribe: configured.resources?.subscribe || void 0,
|
|
4004
|
-
listChanged: configured.resources?.listChanged || void 0
|
|
4005
|
-
}),
|
|
4006
|
-
prompts: this.prompts.size || configured.prompts?.listChanged ? stripUndefined4({
|
|
4007
|
-
listChanged: configured.prompts?.listChanged || void 0
|
|
4008
|
-
}) : void 0
|
|
4009
|
-
});
|
|
4010
|
-
}
|
|
4011
|
-
/** Lists tools with MCP cursor pagination. */
|
|
4012
|
-
async listTools(request, context) {
|
|
4013
|
-
const page = await this.paginate("tools/list", this.descriptors(), request, context);
|
|
4014
|
-
return stripUndefined4({
|
|
4015
|
-
tools: [...page.items],
|
|
4016
|
-
nextCursor: page.nextCursor
|
|
4017
|
-
});
|
|
4018
|
-
}
|
|
4019
|
-
/** Lists resources with MCP cursor pagination. */
|
|
4020
|
-
async listResources(request, context) {
|
|
4021
|
-
const page = await this.paginate("resources/list", this.resourceDescriptors(), request, context);
|
|
4022
|
-
return stripUndefined4({
|
|
4023
|
-
resources: [...page.items],
|
|
4024
|
-
nextCursor: page.nextCursor
|
|
4025
|
-
});
|
|
4026
|
-
}
|
|
4027
|
-
/** Lists resource templates with MCP cursor pagination. */
|
|
4028
|
-
async listResourceTemplates(request, context) {
|
|
4029
|
-
const page = await this.paginate(
|
|
4030
|
-
"resources/templates/list",
|
|
4031
|
-
this.resourceTemplates.map((entry) => entry.descriptor),
|
|
4032
|
-
request,
|
|
4033
|
-
context
|
|
4034
|
-
);
|
|
4035
|
-
return stripUndefined4({
|
|
4036
|
-
resourceTemplates: [...page.items],
|
|
4037
|
-
nextCursor: page.nextCursor
|
|
4038
|
-
});
|
|
4039
|
-
}
|
|
4040
|
-
/** Lists prompts with MCP cursor pagination. */
|
|
4041
|
-
async listPrompts(request, context) {
|
|
4042
|
-
const page = await this.paginate("prompts/list", this.promptDescriptors(), request, context);
|
|
4043
|
-
return stripUndefined4({
|
|
4044
|
-
prompts: [...page.items],
|
|
4045
|
-
nextCursor: page.nextCursor
|
|
4046
|
-
});
|
|
4047
|
-
}
|
|
4048
|
-
/** Applies configured or default cursor pagination to one supported list operation. */
|
|
4049
|
-
async paginate(operation, items, request, context) {
|
|
4050
|
-
const cursor = readCursorParam(request);
|
|
4051
|
-
const pageSize = this.pageSize();
|
|
4052
|
-
const override = selectPaginationOverride(this.options.pagination?.override, operation);
|
|
4053
|
-
if (override) {
|
|
4054
|
-
return override({
|
|
4055
|
-
operation,
|
|
4056
|
-
items,
|
|
4057
|
-
cursor,
|
|
4058
|
-
pageSize,
|
|
4059
|
-
auth: context.authSession
|
|
4060
|
-
});
|
|
4061
|
-
}
|
|
4062
|
-
return defaultPagination(items, cursor, pageSize);
|
|
4063
|
-
}
|
|
4064
|
-
/** Returns the server-chosen page size for all built-in list pagination. */
|
|
4065
|
-
pageSize() {
|
|
4066
|
-
const configured = this.options.pagination?.pageSize;
|
|
4067
|
-
return configured && configured > 0 ? Math.floor(configured) : 50;
|
|
4068
|
-
}
|
|
4069
|
-
/** Executes a tool after request-level and tool-level auth checks. */
|
|
4070
|
-
async callTool(request, context) {
|
|
4071
|
-
const params = request.params;
|
|
4072
|
-
const name = typeof params?.name === "string" ? params.name : void 0;
|
|
4073
|
-
if (!name) {
|
|
4074
|
-
throw new JsonRpcError(-32602, "tools/call requires params.name.");
|
|
4075
|
-
}
|
|
4076
|
-
const loaded = this.tools.get(name);
|
|
4077
|
-
if (!loaded) {
|
|
4078
|
-
throw new JsonRpcError(-32602, `Unknown tool "${name}".`);
|
|
4079
|
-
}
|
|
4080
|
-
const authSession = await this.authorizeTool(loaded, context);
|
|
4081
|
-
const ctx = this.options.createContext ? await this.options.createContext(request) : createDefaultContext(request);
|
|
4082
|
-
if (authSession !== void 0) {
|
|
4083
|
-
ctx.auth = authSession;
|
|
4084
|
-
}
|
|
4085
|
-
ctx.notify = this.createNotifications(context, request);
|
|
4086
|
-
const controller = new AbortController();
|
|
4087
|
-
if (request.id !== null && request.id !== void 0) {
|
|
4088
|
-
this.activeRequests.set(request.id, controller);
|
|
4089
|
-
}
|
|
4090
|
-
ctx.request = {
|
|
4091
|
-
...ctx.request,
|
|
4092
|
-
signal: controller.signal
|
|
4093
|
-
};
|
|
4094
|
-
try {
|
|
4095
|
-
const args = loaded.descriptorProvided ? validateAgainstSchema(
|
|
4096
|
-
loaded.descriptor.inputSchema,
|
|
4097
|
-
params?.arguments ?? {},
|
|
4098
|
-
`Invalid parameters for tool "${name}".`
|
|
4099
|
-
) : params?.arguments ?? {};
|
|
4100
|
-
const result = await withTimeout(
|
|
4101
|
-
executeTool(loaded.tool, args, ctx),
|
|
4102
|
-
this.options.toolTimeoutMs,
|
|
4103
|
-
controller
|
|
4104
|
-
);
|
|
4105
|
-
if (loaded.descriptorProvided) {
|
|
4106
|
-
validateAgainstSchema(
|
|
4107
|
-
loaded.descriptor.outputSchema,
|
|
4108
|
-
result.structuredContent,
|
|
4109
|
-
`Invalid structuredContent returned by tool "${name}".`
|
|
4110
|
-
);
|
|
4116
|
+
:root[data-sidecar-host="chatgpt"] {
|
|
4117
|
+
--accent: #10a37f;
|
|
4118
|
+
--accent-text: #ffffff;
|
|
4119
|
+
--ring: rgba(16, 163, 127, 0.2);
|
|
4111
4120
|
}
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4121
|
+
* { box-sizing: border-box; }
|
|
4122
|
+
*::-webkit-scrollbar { height: 6px; width: 6px; }
|
|
4123
|
+
*::-webkit-scrollbar-track { background: transparent; }
|
|
4124
|
+
*::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }
|
|
4125
|
+
|
|
4126
|
+
body {
|
|
4127
|
+
background: var(--bg);
|
|
4128
|
+
color: var(--text);
|
|
4129
|
+
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
4130
|
+
font-size: 14px;
|
|
4131
|
+
line-height: 1.5;
|
|
4132
|
+
margin: 0;
|
|
4133
|
+
overflow: hidden;
|
|
4116
4134
|
}
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
if (context.authSession !== void 0) {
|
|
4122
|
-
return context.authSession;
|
|
4123
|
-
}
|
|
4124
|
-
if (!this.options.auth) {
|
|
4125
|
-
return void 0;
|
|
4126
|
-
}
|
|
4127
|
-
const request = context.request ?? new Request(this.options.auth.resource);
|
|
4128
|
-
const requestAuth = await this.options.auth.authorizeRequest(request);
|
|
4129
|
-
if (!requestAuth.ok) {
|
|
4130
|
-
throw authJsonRpcError(requestAuth);
|
|
4131
|
-
}
|
|
4132
|
-
return requestAuth.auth;
|
|
4133
|
-
}
|
|
4134
|
-
/** Applies Sidecar auth policy for a tool call. */
|
|
4135
|
-
async authorizeTool(loaded, context) {
|
|
4136
|
-
const policy = loaded.tool.auth;
|
|
4137
|
-
if (!this.options.auth) {
|
|
4138
|
-
if (policy && policy.public !== true) {
|
|
4139
|
-
throw new JsonRpcError(
|
|
4140
|
-
-32001,
|
|
4141
|
-
`Tool "${loaded.descriptor?.name ?? loaded.tool.name}" requires auth, but no auth.ts configuration is loaded.`
|
|
4142
|
-
);
|
|
4135
|
+
.shell {
|
|
4136
|
+
display: flex;
|
|
4137
|
+
height: 100vh;
|
|
4138
|
+
min-height: 0;
|
|
4143
4139
|
}
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
}
|
|
4151
|
-
return toolAuth.auth;
|
|
4152
|
-
}
|
|
4153
|
-
/** Reads a generated widget or authored resource by URI. */
|
|
4154
|
-
async readResource(request, context) {
|
|
4155
|
-
const params = request.params;
|
|
4156
|
-
const uri = typeof params?.uri === "string" ? params.uri : void 0;
|
|
4157
|
-
if (!uri) {
|
|
4158
|
-
throw new JsonRpcError(-32602, "resources/read requires params.uri.");
|
|
4159
|
-
}
|
|
4160
|
-
const resource = this.resources.get(uri);
|
|
4161
|
-
if (!resource) {
|
|
4162
|
-
throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
|
|
4163
|
-
}
|
|
4164
|
-
if (resource.resource) {
|
|
4165
|
-
const toolContext = this.options.createContext ? await this.options.createContext(request) : createDefaultContext(request);
|
|
4166
|
-
if (context.authSession !== void 0) {
|
|
4167
|
-
toolContext.auth = context.authSession;
|
|
4140
|
+
.content-shell {
|
|
4141
|
+
display: flex;
|
|
4142
|
+
flex-direction: column;
|
|
4143
|
+
flex: 1 1 auto;
|
|
4144
|
+
min-height: 0;
|
|
4145
|
+
min-width: 0;
|
|
4168
4146
|
}
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
{
|
|
4179
|
-
uri,
|
|
4180
|
-
mimeType: resource.mimeType ?? "text/plain",
|
|
4181
|
-
text: resource.text ?? "",
|
|
4182
|
-
_meta: resource._meta
|
|
4183
|
-
}
|
|
4184
|
-
]
|
|
4185
|
-
};
|
|
4186
|
-
}
|
|
4187
|
-
/** Accepts a resource subscription when server-level support is enabled. */
|
|
4188
|
-
subscribeResource(request) {
|
|
4189
|
-
if (!this.options.capabilities?.resources?.subscribe) {
|
|
4190
|
-
throw new JsonRpcError(-32601, "Resource subscriptions are not enabled.");
|
|
4191
|
-
}
|
|
4192
|
-
const uri = readUriParam(request, "resources/subscribe");
|
|
4193
|
-
if (!this.resources.has(uri)) {
|
|
4194
|
-
throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
|
|
4195
|
-
}
|
|
4196
|
-
this.subscribedResourceUris.add(uri);
|
|
4197
|
-
return {};
|
|
4198
|
-
}
|
|
4199
|
-
/** Accepts a resource unsubscription when server-level support is enabled. */
|
|
4200
|
-
unsubscribeResource(request) {
|
|
4201
|
-
if (!this.options.capabilities?.resources?.subscribe) {
|
|
4202
|
-
throw new JsonRpcError(-32601, "Resource subscriptions are not enabled.");
|
|
4203
|
-
}
|
|
4204
|
-
const uri = readUriParam(request, "resources/unsubscribe");
|
|
4205
|
-
if (!this.resources.has(uri)) {
|
|
4206
|
-
throw new JsonRpcError(-32002, `Resource not found: "${uri}".`, { uri });
|
|
4207
|
-
}
|
|
4208
|
-
this.subscribedResourceUris.delete(uri);
|
|
4209
|
-
return {};
|
|
4210
|
-
}
|
|
4211
|
-
/** Renders one named prompt with validated arguments. */
|
|
4212
|
-
async getPrompt(request, context) {
|
|
4213
|
-
const params = request.params;
|
|
4214
|
-
const name = typeof params?.name === "string" ? params.name : void 0;
|
|
4215
|
-
if (!name) {
|
|
4216
|
-
throw new JsonRpcError(-32602, "prompts/get requires params.name.");
|
|
4217
|
-
}
|
|
4218
|
-
const loaded = this.prompts.get(name);
|
|
4219
|
-
if (!loaded) {
|
|
4220
|
-
throw new JsonRpcError(-32602, `Unknown prompt "${name}".`);
|
|
4221
|
-
}
|
|
4222
|
-
const toolContext = this.options.createContext ? await this.options.createContext(request) : createDefaultContext(request);
|
|
4223
|
-
if (context.authSession !== void 0) {
|
|
4224
|
-
toolContext.auth = context.authSession;
|
|
4225
|
-
}
|
|
4226
|
-
toolContext.notify = this.createNotifications(context, request);
|
|
4227
|
-
return executePrompt(loaded.prompt, params?.arguments ?? {}, toPromptContext(toolContext));
|
|
4228
|
-
}
|
|
4229
|
-
/** Creates notification helpers scoped by advertised server capabilities. */
|
|
4230
|
-
createNotifications(context, request) {
|
|
4231
|
-
const configured = this.options.capabilities ?? {};
|
|
4232
|
-
return createRuntimeNotifications(
|
|
4233
|
-
context.notifications,
|
|
4234
|
-
readProgressToken(request),
|
|
4235
|
-
{
|
|
4236
|
-
toolsListChanged: Boolean(configured.tools?.listChanged),
|
|
4237
|
-
resourcesListChanged: Boolean(configured.resources?.listChanged),
|
|
4238
|
-
promptsListChanged: Boolean(configured.prompts?.listChanged),
|
|
4239
|
-
isResourceSubscribed: (uri) => this.subscribedResourceUris.has(uri)
|
|
4147
|
+
.topbar {
|
|
4148
|
+
align-items: center;
|
|
4149
|
+
background: var(--bg);
|
|
4150
|
+
border-bottom: 1px solid var(--border);
|
|
4151
|
+
display: flex;
|
|
4152
|
+
gap: 16px;
|
|
4153
|
+
justify-content: space-between;
|
|
4154
|
+
min-height: 56px;
|
|
4155
|
+
padding: 8px 24px;
|
|
4240
4156
|
}
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
const controller = this.activeRequests.get(requestId);
|
|
4257
|
-
if (!controller || controller.signal.aborted) {
|
|
4258
|
-
return;
|
|
4259
|
-
}
|
|
4260
|
-
const reason = typeof params?.reason === "string" ? params.reason : "Request cancelled.";
|
|
4261
|
-
controller.abort(new RequestCancelledError(reason));
|
|
4262
|
-
}
|
|
4263
|
-
};
|
|
4264
|
-
function createSidecarMcpServer(options) {
|
|
4265
|
-
return new SidecarMcpServer(options);
|
|
4266
|
-
}
|
|
4267
|
-
function createSidecarHttpServer(options) {
|
|
4268
|
-
return createServer(createSidecarHttpHandler(options));
|
|
4269
|
-
}
|
|
4270
|
-
function createSidecarHttpHandler(options) {
|
|
4271
|
-
const endpoint = options.path ?? "/mcp";
|
|
4272
|
-
const maxBodyBytes = options.maxBodyBytes ?? 1e6;
|
|
4273
|
-
const streamHub = createSseHub();
|
|
4274
|
-
const mcp = createSidecarMcpServer(options);
|
|
4275
|
-
return async (request, response) => {
|
|
4276
|
-
const pathname = request.url?.split("?")[0];
|
|
4277
|
-
const requestLog = createHttpRequestLog(request, pathname);
|
|
4278
|
-
response.once("finish", () => {
|
|
4279
|
-
logHttpRequest(requestLog, response.statusCode);
|
|
4280
|
-
});
|
|
4281
|
-
if (isRejectedOrigin(request, options.allowedOrigins)) {
|
|
4282
|
-
response.writeHead(403, { "content-type": "application/json" });
|
|
4283
|
-
response.end(JSON.stringify({ error: "forbidden_origin" }));
|
|
4284
|
-
return;
|
|
4285
|
-
}
|
|
4286
|
-
const proxyResult = await runProxy(options.proxy, request);
|
|
4287
|
-
if (proxyResult) {
|
|
4288
|
-
response.writeHead(proxyResult.status, proxyResult.headers);
|
|
4289
|
-
response.end(proxyResult.body ?? "");
|
|
4290
|
-
return;
|
|
4291
|
-
}
|
|
4292
|
-
if (options.auth && request.method === "GET" && isProtectedResourceMetadataPath(pathname, endpoint)) {
|
|
4293
|
-
response.writeHead(200, { "content-type": "application/json" });
|
|
4294
|
-
response.end(JSON.stringify(options.auth.metadata()));
|
|
4295
|
-
return;
|
|
4296
|
-
}
|
|
4297
|
-
if (options.auth && request.method === "GET" && pathname === "/.well-known/oauth-authorization-server") {
|
|
4298
|
-
await proxyAuthorizationServerMetadata(options.auth, response);
|
|
4299
|
-
return;
|
|
4300
|
-
}
|
|
4301
|
-
if (request.method === "GET" && pathname === endpoint) {
|
|
4302
|
-
try {
|
|
4303
|
-
const fetchRequest = toFetchRequest(request, options.publicUrl ?? options.auth?.resource);
|
|
4304
|
-
if (options.auth) {
|
|
4305
|
-
const authSession = await authorizeHttpRequest(options.auth, fetchRequest, response);
|
|
4306
|
-
if (authSession === AUTH_RESPONSE_SENT) {
|
|
4307
|
-
return;
|
|
4308
|
-
}
|
|
4309
|
-
}
|
|
4310
|
-
validateProtocolVersion(request);
|
|
4311
|
-
validateGetHeaders(request);
|
|
4312
|
-
streamHub.open(response);
|
|
4313
|
-
} catch (error) {
|
|
4314
|
-
const status = error instanceof JsonRpcHttpError ? error.status : 400;
|
|
4315
|
-
response.writeHead(status, { "content-type": "application/json" });
|
|
4316
|
-
response.end(
|
|
4317
|
-
JSON.stringify({
|
|
4318
|
-
jsonrpc: JSONRPC_VERSION2,
|
|
4319
|
-
id: null,
|
|
4320
|
-
error: normalizeHttpError(error)
|
|
4321
|
-
})
|
|
4322
|
-
);
|
|
4157
|
+
.brand { display: flex; align-items: center; }
|
|
4158
|
+
h1 { font-size: 14px; font-weight: 600; letter-spacing: 0; margin: 0; color: var(--text); }
|
|
4159
|
+
|
|
4160
|
+
.controls { align-items: center; display: flex; flex-wrap: wrap; gap: 12px; justify-content: flex-end; }
|
|
4161
|
+
.control-group { align-items: center; display: flex; }
|
|
4162
|
+
|
|
4163
|
+
.segmented {
|
|
4164
|
+
background: var(--panel-soft);
|
|
4165
|
+
border: 1px solid var(--border);
|
|
4166
|
+
border-radius: 8px;
|
|
4167
|
+
display: grid;
|
|
4168
|
+
gap: 0;
|
|
4169
|
+
grid-template-columns: repeat(var(--segments, 2), 32px);
|
|
4170
|
+
padding: 2px;
|
|
4171
|
+
position: relative;
|
|
4323
4172
|
}
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
response.writeHead(404, { "content-type": "application/json" });
|
|
4336
|
-
response.end(JSON.stringify({ error: "not_found" }));
|
|
4337
|
-
return;
|
|
4338
|
-
}
|
|
4339
|
-
try {
|
|
4340
|
-
const fetchRequest = toFetchRequest(request, options.publicUrl ?? options.auth?.resource);
|
|
4341
|
-
const authSession = options.auth ? await authorizeHttpRequest(options.auth, fetchRequest, response) : void 0;
|
|
4342
|
-
if (options.auth && authSession === AUTH_RESPONSE_SENT) {
|
|
4343
|
-
return;
|
|
4173
|
+
.segmented::before {
|
|
4174
|
+
background: var(--panel);
|
|
4175
|
+
border: 1px solid var(--border);
|
|
4176
|
+
border-radius: 6px;
|
|
4177
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
|
4178
|
+
content: "";
|
|
4179
|
+
inset: 2px auto 2px 2px;
|
|
4180
|
+
position: absolute;
|
|
4181
|
+
transform: translateX(calc(var(--active-index, 0) * 32px));
|
|
4182
|
+
transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
4183
|
+
width: 32px;
|
|
4344
4184
|
}
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4185
|
+
.segmented button, .send, .icon-button, .auth-trigger, .modal-button {
|
|
4186
|
+
appearance: none;
|
|
4187
|
+
cursor: pointer;
|
|
4188
|
+
font: inherit;
|
|
4189
|
+
outline: none;
|
|
4350
4190
|
}
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4191
|
+
.segmented button {
|
|
4192
|
+
align-items: center;
|
|
4193
|
+
background: transparent;
|
|
4194
|
+
border: 0;
|
|
4195
|
+
border-radius: 6px;
|
|
4196
|
+
color: var(--muted);
|
|
4197
|
+
display: inline-flex;
|
|
4198
|
+
font-weight: 500;
|
|
4199
|
+
justify-content: center;
|
|
4200
|
+
min-height: 32px;
|
|
4201
|
+
min-width: 32px;
|
|
4202
|
+
padding: 0;
|
|
4203
|
+
position: relative;
|
|
4204
|
+
transition: color 140ms ease;
|
|
4205
|
+
z-index: 1;
|
|
4355
4206
|
}
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
const stream = createSseStream(response, { supportsRequestProgress: true });
|
|
4359
|
-
const payload2 = await mcp.handle(rpcRequest, {
|
|
4360
|
-
request: fetchRequest,
|
|
4361
|
-
authSession,
|
|
4362
|
-
notifications: stream
|
|
4363
|
-
});
|
|
4364
|
-
if (payload2) {
|
|
4365
|
-
stream.sendJson(payload2);
|
|
4366
|
-
}
|
|
4367
|
-
stream.end();
|
|
4368
|
-
return;
|
|
4207
|
+
.segmented button[aria-pressed="true"] {
|
|
4208
|
+
color: var(--text);
|
|
4369
4209
|
}
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4210
|
+
.auth-trigger {
|
|
4211
|
+
align-items: center;
|
|
4212
|
+
background: var(--panel);
|
|
4213
|
+
border: 1px solid var(--border);
|
|
4214
|
+
border-radius: 8px;
|
|
4215
|
+
color: var(--text);
|
|
4216
|
+
display: inline-flex;
|
|
4217
|
+
font-size: 12px;
|
|
4218
|
+
font-weight: 500;
|
|
4219
|
+
min-height: 36px;
|
|
4220
|
+
padding: 0 14px;
|
|
4221
|
+
transition: background 120ms ease, border-color 120ms ease;
|
|
4380
4222
|
}
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4223
|
+
.auth-trigger:hover {
|
|
4224
|
+
background: var(--panel-soft);
|
|
4225
|
+
border-color: var(--muted);
|
|
4226
|
+
}
|
|
4227
|
+
.auth-trigger:focus-visible, .modal-button:focus-visible, .modal-close:focus-visible {
|
|
4228
|
+
box-shadow: 0 0 0 3px var(--ring);
|
|
4229
|
+
}
|
|
4230
|
+
.icon {
|
|
4231
|
+
align-items: center;
|
|
4232
|
+
display: inline-flex;
|
|
4233
|
+
height: 16px;
|
|
4234
|
+
justify-content: center;
|
|
4235
|
+
line-height: 1;
|
|
4236
|
+
width: 16px;
|
|
4237
|
+
}
|
|
4238
|
+
.icon svg {
|
|
4239
|
+
display: block;
|
|
4240
|
+
height: 16px;
|
|
4241
|
+
stroke: currentColor;
|
|
4242
|
+
width: 16px;
|
|
4243
|
+
}
|
|
4244
|
+
.button-text { display: none; }
|
|
4245
|
+
|
|
4246
|
+
.modal-input, textarea {
|
|
4247
|
+
background: var(--panel);
|
|
4248
|
+
border: 1px solid var(--border);
|
|
4249
|
+
border-radius: 8px;
|
|
4250
|
+
color: var(--text);
|
|
4251
|
+
font: inherit;
|
|
4252
|
+
outline: 0;
|
|
4253
|
+
}
|
|
4254
|
+
.modal-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--ring); }
|
|
4255
|
+
.modal-input { min-height: 38px; padding: 0 12px; width: 100%; }
|
|
4256
|
+
|
|
4257
|
+
.modal-overlay {
|
|
4258
|
+
align-items: center;
|
|
4259
|
+
background: rgba(0, 0, 0, 0.5);
|
|
4260
|
+
backdrop-filter: blur(8px);
|
|
4261
|
+
-webkit-backdrop-filter: blur(8px);
|
|
4262
|
+
display: flex;
|
|
4263
|
+
inset: 0;
|
|
4264
|
+
justify-content: center;
|
|
4265
|
+
opacity: 0;
|
|
4266
|
+
padding: 24px;
|
|
4267
|
+
pointer-events: none;
|
|
4268
|
+
position: fixed;
|
|
4269
|
+
transition: opacity 250ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
4270
|
+
z-index: 100;
|
|
4271
|
+
}
|
|
4272
|
+
.modal-overlay[data-open="true"] {
|
|
4273
|
+
opacity: 1;
|
|
4274
|
+
pointer-events: auto;
|
|
4275
|
+
}
|
|
4276
|
+
.modal-panel {
|
|
4277
|
+
background: var(--panel);
|
|
4278
|
+
border: 1px solid var(--border);
|
|
4279
|
+
border-radius: 16px;
|
|
4280
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.25);
|
|
4281
|
+
display: grid;
|
|
4282
|
+
gap: 20px;
|
|
4283
|
+
max-width: 400px;
|
|
4284
|
+
opacity: 0;
|
|
4285
|
+
padding: 24px;
|
|
4286
|
+
transform: translateY(12px) scale(0.97);
|
|
4287
|
+
transition: opacity 250ms cubic-bezier(0.16, 1, 0.3, 1), transform 250ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
4288
|
+
width: 100%;
|
|
4289
|
+
}
|
|
4290
|
+
.modal-overlay[data-open="true"] .modal-panel {
|
|
4291
|
+
opacity: 1;
|
|
4292
|
+
transform: translateY(0) scale(1);
|
|
4293
|
+
}
|
|
4294
|
+
.modal-head {
|
|
4295
|
+
align-items: start;
|
|
4296
|
+
display: flex;
|
|
4297
|
+
gap: 16px;
|
|
4298
|
+
justify-content: space-between;
|
|
4299
|
+
}
|
|
4300
|
+
.modal-title { font-size: 16px; font-weight: 600; letter-spacing: 0; margin: 0; }
|
|
4301
|
+
.modal-description { color: var(--muted); font-size: 13px; margin: 4px 0 0; line-height: 1.4; }
|
|
4302
|
+
.modal-close {
|
|
4303
|
+
align-items: center;
|
|
4304
|
+
appearance: none;
|
|
4305
|
+
background: transparent;
|
|
4306
|
+
border: 0;
|
|
4307
|
+
border-radius: 999px;
|
|
4308
|
+
color: var(--muted);
|
|
4309
|
+
cursor: pointer;
|
|
4310
|
+
display: inline-flex;
|
|
4311
|
+
height: 24px;
|
|
4312
|
+
justify-content: center;
|
|
4313
|
+
padding: 0;
|
|
4314
|
+
width: 24px;
|
|
4315
|
+
transition: background 120ms ease, color 120ms ease;
|
|
4316
|
+
}
|
|
4317
|
+
.modal-close:hover { background: var(--panel-soft); color: var(--text); }
|
|
4318
|
+
.modal-close svg {
|
|
4319
|
+
display: block;
|
|
4320
|
+
height: 14px;
|
|
4321
|
+
width: 14px;
|
|
4322
|
+
}
|
|
4323
|
+
.modal-field { display: grid; gap: 6px; }
|
|
4324
|
+
.modal-label { font-size: 12px; font-weight: 600; color: var(--muted); }
|
|
4325
|
+
.modal-error { color: #ef4444; display: none; font-size: 13px; font-weight: 500; }
|
|
4326
|
+
.modal-error[data-visible="true"] { display: block; }
|
|
4327
|
+
.modal-actions {
|
|
4328
|
+
display: flex;
|
|
4329
|
+
gap: 10px;
|
|
4330
|
+
justify-content: flex-end;
|
|
4331
|
+
}
|
|
4332
|
+
.modal-button {
|
|
4333
|
+
border-radius: 8px;
|
|
4334
|
+
font-size: 13px;
|
|
4335
|
+
font-weight: 600;
|
|
4336
|
+
min-height: 36px;
|
|
4337
|
+
padding: 0 14px;
|
|
4338
|
+
transition: background 120ms ease, transform 100ms ease;
|
|
4339
|
+
}
|
|
4340
|
+
.modal-button:active { transform: scale(0.98); }
|
|
4341
|
+
.modal-button[data-variant="secondary"] {
|
|
4342
|
+
background: transparent;
|
|
4343
|
+
border: 1px solid var(--border);
|
|
4344
|
+
color: var(--text);
|
|
4345
|
+
}
|
|
4346
|
+
.modal-button[data-variant="secondary"]:hover {
|
|
4347
|
+
background: var(--panel-soft);
|
|
4348
|
+
}
|
|
4349
|
+
.modal-button[data-variant="primary"] {
|
|
4350
|
+
background: var(--control-bg);
|
|
4351
|
+
border: 1px solid var(--control-bg);
|
|
4352
|
+
color: var(--control-text);
|
|
4353
|
+
}
|
|
4354
|
+
.modal-button[data-variant="primary"]:hover {
|
|
4355
|
+
opacity: 0.9;
|
|
4356
|
+
}
|
|
4357
|
+
.modal-button:disabled {
|
|
4358
|
+
cursor: wait;
|
|
4359
|
+
opacity: .68;
|
|
4360
|
+
}
|
|
4361
|
+
|
|
4362
|
+
.stage {
|
|
4363
|
+
display: flex;
|
|
4364
|
+
flex: 1 1 auto;
|
|
4365
|
+
min-height: 0;
|
|
4366
|
+
overflow: hidden;
|
|
4367
|
+
}
|
|
4368
|
+
.surface {
|
|
4369
|
+
background: var(--bg);
|
|
4370
|
+
display: flex;
|
|
4371
|
+
flex-direction: column;
|
|
4372
|
+
height: 100%;
|
|
4373
|
+
min-height: 0;
|
|
4374
|
+
overflow: hidden;
|
|
4375
|
+
margin: 0 auto;
|
|
4376
|
+
max-width: 800px;
|
|
4377
|
+
width: 100%;
|
|
4378
|
+
transition: width 160ms ease, height 160ms ease, border-radius 160ms ease, box-shadow 160ms ease;
|
|
4379
|
+
}
|
|
4380
|
+
:root[data-sidecar-device="mobile"] .surface {
|
|
4381
|
+
border: 1px solid var(--border);
|
|
4382
|
+
border-radius: 24px;
|
|
4383
|
+
box-shadow: var(--shadow);
|
|
4384
|
+
height: min(800px, calc(100dvh - 112px));
|
|
4385
|
+
width: var(--mobile-width);
|
|
4386
|
+
max-width: var(--mobile-width);
|
|
4387
|
+
}
|
|
4388
|
+
:root[data-sidecar-device="mobile"] .stage {
|
|
4389
|
+
align-items: center;
|
|
4390
|
+
display: flex;
|
|
4391
|
+
justify-content: center;
|
|
4392
|
+
padding: 18px;
|
|
4393
|
+
}
|
|
4394
|
+
.surface-bar {
|
|
4395
|
+
align-items: center;
|
|
4396
|
+
background: var(--bg);
|
|
4397
|
+
border-bottom: 1px solid var(--border);
|
|
4398
|
+
display: flex;
|
|
4399
|
+
gap: 10px;
|
|
4400
|
+
justify-content: space-between;
|
|
4401
|
+
min-height: 48px;
|
|
4402
|
+
padding: 0 20px;
|
|
4403
|
+
}
|
|
4404
|
+
.surface-title { align-items: center; display: flex; gap: 8px; font-weight: 600; font-size: 13px; color: var(--text); }
|
|
4405
|
+
.surface-dot {
|
|
4406
|
+
background: var(--accent);
|
|
4407
|
+
border-radius: 999px;
|
|
4408
|
+
box-shadow: 0 0 0 3px var(--ring);
|
|
4409
|
+
height: 6px;
|
|
4410
|
+
width: 6px;
|
|
4411
|
+
transition: background 150ms ease, box-shadow 150ms ease;
|
|
4412
|
+
}
|
|
4413
|
+
.surface-meta { color: var(--muted); font-size: 11px; font-weight: 500; }
|
|
4414
|
+
|
|
4415
|
+
.messages {
|
|
4416
|
+
display: flex;
|
|
4417
|
+
flex-direction: column;
|
|
4418
|
+
flex: 1 1 auto;
|
|
4419
|
+
gap: 24px;
|
|
4420
|
+
min-height: 0;
|
|
4421
|
+
overflow: auto;
|
|
4422
|
+
padding: 24px 20px;
|
|
4423
|
+
width: 100%;
|
|
4424
|
+
}
|
|
4425
|
+
.message {
|
|
4426
|
+
display: grid;
|
|
4427
|
+
gap: 8px;
|
|
4428
|
+
max-width: 100%;
|
|
4429
|
+
}
|
|
4430
|
+
.message[data-role="user"] {
|
|
4431
|
+
background: var(--user-bubble-bg);
|
|
4432
|
+
border: 1px solid var(--border);
|
|
4433
|
+
border-radius: 16px;
|
|
4434
|
+
color: var(--text);
|
|
4435
|
+
justify-self: end;
|
|
4436
|
+
margin-left: auto;
|
|
4437
|
+
max-width: 80%;
|
|
4438
|
+
padding: 12px 16px;
|
|
4439
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
4440
|
+
}
|
|
4441
|
+
:root[data-sidecar-device="mobile"] .message[data-role="user"] { max-width: 90%; }
|
|
4442
|
+
|
|
4443
|
+
.message[data-role="assistant"] {
|
|
4444
|
+
justify-self: start;
|
|
4445
|
+
margin-right: auto;
|
|
4446
|
+
width: 100%;
|
|
4447
|
+
padding: 0 4px;
|
|
4448
|
+
}
|
|
4449
|
+
.role { display: none; }
|
|
4450
|
+
|
|
4451
|
+
.content {
|
|
4452
|
+
font-size: 14px;
|
|
4453
|
+
line-height: 1.55;
|
|
4454
|
+
overflow-wrap: anywhere;
|
|
4455
|
+
white-space: pre-wrap;
|
|
4456
|
+
}
|
|
4457
|
+
.content.error {
|
|
4458
|
+
color: #ef4444;
|
|
4459
|
+
}
|
|
4460
|
+
.content.markdown {
|
|
4461
|
+
display: grid;
|
|
4462
|
+
gap: 12px;
|
|
4463
|
+
white-space: normal;
|
|
4464
|
+
}
|
|
4465
|
+
.content.markdown p {
|
|
4466
|
+
margin: 0;
|
|
4467
|
+
line-height: 1.55;
|
|
4468
|
+
}
|
|
4469
|
+
.content.markdown ul, .content.markdown ol {
|
|
4470
|
+
margin: 0;
|
|
4471
|
+
padding-left: 20px;
|
|
4472
|
+
}
|
|
4473
|
+
.content.markdown li {
|
|
4474
|
+
margin-bottom: 4px;
|
|
4475
|
+
}
|
|
4476
|
+
.content.markdown pre {
|
|
4477
|
+
background: var(--code-bg);
|
|
4478
|
+
border: 1px solid var(--border);
|
|
4479
|
+
border-radius: 10px;
|
|
4480
|
+
overflow: auto;
|
|
4481
|
+
padding: 14px;
|
|
4482
|
+
margin: 8px 0;
|
|
4483
|
+
}
|
|
4484
|
+
.content.markdown code {
|
|
4485
|
+
background: var(--code-inline-bg);
|
|
4486
|
+
border-radius: 4px;
|
|
4487
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
4488
|
+
font-size: 0.88em;
|
|
4489
|
+
padding: 2px 5px;
|
|
4490
|
+
}
|
|
4491
|
+
.content.markdown pre code {
|
|
4492
|
+
background: transparent;
|
|
4493
|
+
padding: 0;
|
|
4494
|
+
font-size: 0.88em;
|
|
4495
|
+
line-height: 1.5;
|
|
4496
|
+
}
|
|
4497
|
+
.content.markdown table {
|
|
4498
|
+
border-collapse: collapse;
|
|
4499
|
+
display: block;
|
|
4500
|
+
max-width: 100%;
|
|
4501
|
+
overflow-x: auto;
|
|
4502
|
+
margin: 12px 0;
|
|
4503
|
+
}
|
|
4504
|
+
.content.markdown th, .content.markdown td {
|
|
4505
|
+
border: 1px solid var(--border);
|
|
4506
|
+
padding: 8px 12px;
|
|
4507
|
+
text-align: left;
|
|
4508
|
+
}
|
|
4509
|
+
.content.markdown th {
|
|
4510
|
+
background: var(--panel-soft);
|
|
4511
|
+
font-weight: 600;
|
|
4512
|
+
}
|
|
4513
|
+
|
|
4514
|
+
.tool-card {
|
|
4515
|
+
background: var(--panel);
|
|
4516
|
+
border: 1px solid var(--border);
|
|
4517
|
+
border-radius: 14px;
|
|
4518
|
+
overflow: hidden;
|
|
4519
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
|
|
4520
|
+
margin: 8px 0;
|
|
4521
|
+
width: 100%;
|
|
4522
|
+
}
|
|
4523
|
+
.tool-head {
|
|
4524
|
+
align-items: center;
|
|
4525
|
+
background: var(--panel-soft);
|
|
4526
|
+
border-bottom: 1px solid var(--border);
|
|
4527
|
+
display: flex;
|
|
4528
|
+
justify-content: space-between;
|
|
4529
|
+
padding: 10px 14px;
|
|
4530
|
+
}
|
|
4531
|
+
.tool-title {
|
|
4532
|
+
color: var(--text);
|
|
4533
|
+
font-size: 12px;
|
|
4534
|
+
font-weight: 600;
|
|
4535
|
+
letter-spacing: 0;
|
|
4536
|
+
}
|
|
4537
|
+
.tool-head .status {
|
|
4538
|
+
font-size: 11px;
|
|
4539
|
+
font-weight: 500;
|
|
4540
|
+
background: rgba(0, 0, 0, 0.04);
|
|
4541
|
+
padding: 2px 8px;
|
|
4542
|
+
border-radius: 6px;
|
|
4543
|
+
color: var(--muted);
|
|
4544
|
+
}
|
|
4545
|
+
:root[data-sidecar-theme="dark"] .tool-head .status {
|
|
4546
|
+
background: rgba(255, 255, 255, 0.05);
|
|
4547
|
+
}
|
|
4548
|
+
.tool-body { display: grid; gap: 10px; padding: 14px; background: var(--panel); }
|
|
4549
|
+
|
|
4550
|
+
iframe {
|
|
4551
|
+
background: var(--widget-bg);
|
|
4552
|
+
border: 1px solid var(--border);
|
|
4553
|
+
border-radius: 10px;
|
|
4554
|
+
min-height: 380px;
|
|
4555
|
+
width: 100%;
|
|
4556
|
+
}
|
|
4557
|
+
:root[data-sidecar-device="mobile"] iframe { min-height: 520px; }
|
|
4558
|
+
|
|
4559
|
+
.composer {
|
|
4560
|
+
background: var(--bg);
|
|
4561
|
+
padding: 14px 20px 24px;
|
|
4562
|
+
flex: 0 0 auto;
|
|
4563
|
+
}
|
|
4564
|
+
form {
|
|
4565
|
+
display: flex;
|
|
4566
|
+
width: 100%;
|
|
4567
|
+
}
|
|
4568
|
+
.composer-container {
|
|
4569
|
+
background: var(--panel);
|
|
4570
|
+
border: 1px solid var(--border);
|
|
4571
|
+
border-radius: 16px;
|
|
4572
|
+
display: flex;
|
|
4573
|
+
flex-direction: column;
|
|
4574
|
+
width: 100%;
|
|
4575
|
+
transition: border-color 150ms ease, box-shadow 150ms ease;
|
|
4576
|
+
}
|
|
4577
|
+
.composer-container:focus-within {
|
|
4578
|
+
border-color: var(--accent);
|
|
4579
|
+
box-shadow: 0 0 0 1px var(--accent), 0 0 0 4px var(--ring);
|
|
4580
|
+
}
|
|
4581
|
+
textarea {
|
|
4582
|
+
background: transparent;
|
|
4583
|
+
border: 0;
|
|
4584
|
+
color: var(--text);
|
|
4585
|
+
font: inherit;
|
|
4586
|
+
font-size: 14px;
|
|
4587
|
+
min-height: 56px;
|
|
4588
|
+
max-height: 160px;
|
|
4589
|
+
padding: 14px 16px 8px;
|
|
4590
|
+
resize: none;
|
|
4591
|
+
width: 100%;
|
|
4592
|
+
outline: none;
|
|
4593
|
+
}
|
|
4594
|
+
.composer-toolbar {
|
|
4595
|
+
align-items: center;
|
|
4596
|
+
display: flex;
|
|
4597
|
+
justify-content: space-between;
|
|
4598
|
+
min-height: 44px;
|
|
4599
|
+
padding: 4px 8px 8px 16px;
|
|
4600
|
+
}
|
|
4601
|
+
.send {
|
|
4602
|
+
align-items: center;
|
|
4603
|
+
aspect-ratio: 1;
|
|
4604
|
+
background: var(--send-bg);
|
|
4605
|
+
border: 0;
|
|
4606
|
+
border-radius: 999px;
|
|
4607
|
+
color: var(--send-text);
|
|
4608
|
+
display: inline-flex;
|
|
4609
|
+
height: 32px;
|
|
4610
|
+
justify-content: center;
|
|
4611
|
+
padding: 0;
|
|
4612
|
+
width: 32px;
|
|
4613
|
+
transition: transform 120ms ease, opacity 120ms ease;
|
|
4614
|
+
}
|
|
4615
|
+
.send:hover {
|
|
4616
|
+
transform: scale(1.04);
|
|
4617
|
+
filter: brightness(1.1);
|
|
4618
|
+
}
|
|
4619
|
+
.send:active {
|
|
4620
|
+
transform: scale(0.96);
|
|
4621
|
+
}
|
|
4622
|
+
.send svg {
|
|
4623
|
+
display: block;
|
|
4624
|
+
height: 16px;
|
|
4625
|
+
stroke-width: 2.5;
|
|
4626
|
+
width: 16px;
|
|
4627
|
+
}
|
|
4628
|
+
.status {
|
|
4629
|
+
color: var(--muted);
|
|
4630
|
+
font-size: 12px;
|
|
4631
|
+
font-weight: 500;
|
|
4632
|
+
display: flex;
|
|
4633
|
+
align-items: center;
|
|
4634
|
+
gap: 6px;
|
|
4635
|
+
}
|
|
4636
|
+
.status.error {
|
|
4637
|
+
color: #ef4444;
|
|
4638
|
+
}
|
|
4639
|
+
.empty {
|
|
4640
|
+
align-self: center;
|
|
4641
|
+
color: var(--muted);
|
|
4642
|
+
display: flex;
|
|
4643
|
+
flex-direction: column;
|
|
4644
|
+
gap: 8px;
|
|
4645
|
+
margin: auto 0;
|
|
4646
|
+
max-width: 320px;
|
|
4647
|
+
padding: 40px 16px;
|
|
4648
|
+
text-align: center;
|
|
4649
|
+
}
|
|
4650
|
+
.empty strong {
|
|
4651
|
+
color: var(--text);
|
|
4652
|
+
font-size: 16px;
|
|
4653
|
+
font-weight: 500;
|
|
4654
|
+
letter-spacing: 0;
|
|
4655
|
+
}
|
|
4656
|
+
.empty span {
|
|
4657
|
+
font-size: 13px;
|
|
4658
|
+
line-height: 1.5;
|
|
4659
|
+
}
|
|
4660
|
+
pre {
|
|
4661
|
+
background: var(--panel-soft);
|
|
4662
|
+
border-radius: 10px;
|
|
4663
|
+
margin: 0;
|
|
4664
|
+
overflow: auto;
|
|
4665
|
+
padding: 12px;
|
|
4666
|
+
}
|
|
4667
|
+
@media (max-width: 760px) {
|
|
4668
|
+
.shell { display: flex; flex-direction: column; }
|
|
4669
|
+
.topbar { align-items: stretch; flex-direction: column; min-height: 0; padding: 12px; }
|
|
4670
|
+
.brand { min-width: 0; }
|
|
4671
|
+
.controls { justify-content: flex-start; }
|
|
4672
|
+
.auth-trigger { min-height: 36px; }
|
|
4673
|
+
.stage { display: flex; justify-content: center; padding: 12px; }
|
|
4674
|
+
.surface { border: 1px solid var(--border); border-radius: 20px; box-shadow: var(--shadow); width: calc(100vw - 24px); }
|
|
4675
|
+
}
|
|
4676
|
+
</style>
|
|
4677
|
+
</head>
|
|
4678
|
+
<body>
|
|
4679
|
+
<main class="shell">
|
|
4680
|
+
<section class="content-shell">
|
|
4681
|
+
<div class="topbar">
|
|
4682
|
+
<div class="brand">
|
|
4683
|
+
<h1>Sidecar dev</h1>
|
|
4684
|
+
</div>
|
|
4685
|
+
<div class="controls">
|
|
4686
|
+
<div class="control-group">
|
|
4687
|
+
<div class="segmented" aria-label="Host">
|
|
4688
|
+
<button type="button" data-host="chatgpt" aria-label="ChatGPT preview" title="ChatGPT"><span class="icon">${iconSvg("chatgpt")}</span><span class="button-text">ChatGPT</span></button>
|
|
4689
|
+
<button type="button" data-host="claude" aria-label="Claude preview" title="Claude"><span class="icon">${iconSvg("claude")}</span><span class="button-text">Claude</span></button>
|
|
4690
|
+
<button type="button" data-host="generic" aria-label="Generic MCP preview" title="MCP"><span class="icon">${iconSvg("braces")}</span><span class="button-text">MCP</span></button>
|
|
4691
|
+
</div>
|
|
4692
|
+
</div>
|
|
4693
|
+
<div class="control-group">
|
|
4694
|
+
<div class="segmented" aria-label="Theme">
|
|
4695
|
+
<button type="button" data-theme="light" aria-label="Light theme" title="Light"><span class="icon">${iconSvg("sun")}</span><span class="button-text">Light</span></button>
|
|
4696
|
+
<button type="button" data-theme="dark" aria-label="Dark theme" title="Dark"><span class="icon">${iconSvg("moon")}</span><span class="button-text">Dark</span></button>
|
|
4697
|
+
</div>
|
|
4698
|
+
</div>
|
|
4699
|
+
<div class="control-group">
|
|
4700
|
+
<div class="segmented" aria-label="Device">
|
|
4701
|
+
<button type="button" data-device="desktop" aria-label="Desktop preview" title="Desktop"><span class="icon">${iconSvg("monitor")}</span><span class="button-text">Desktop</span></button>
|
|
4702
|
+
<button type="button" data-device="mobile" aria-label="Mobile preview" title="Mobile"><span class="icon">${iconSvg("phone")}</span><span class="button-text">Mobile</span></button>
|
|
4703
|
+
</div>
|
|
4704
|
+
</div>
|
|
4705
|
+
<button id="authTrigger" class="auth-trigger" type="button" data-token-set="false">
|
|
4706
|
+
<span>Set Bearer Token</span>
|
|
4707
|
+
</button>
|
|
4708
|
+
</div>
|
|
4709
|
+
</div>
|
|
4710
|
+
<section class="stage">
|
|
4711
|
+
<aside class="surface" aria-label="Sidecar chat preview">
|
|
4712
|
+
<div class="surface-bar">
|
|
4713
|
+
<div class="surface-title"><span class="surface-dot"></span><span id="surfaceTitle">Claude preview</span></div>
|
|
4714
|
+
<div id="surfaceMeta" class="surface-meta">Desktop</div>
|
|
4715
|
+
</div>
|
|
4716
|
+
<section id="messages" class="messages">
|
|
4717
|
+
<div class="empty"><strong>Start a local MCP run.</strong><span>Ask for a tool call, then inspect model text and widget output here.</span></div>
|
|
4718
|
+
</section>
|
|
4719
|
+
<section class="composer">
|
|
4720
|
+
<form id="chatForm">
|
|
4721
|
+
<div class="composer-container">
|
|
4722
|
+
<textarea id="prompt" placeholder="Ask a question or request a tool call..."></textarea>
|
|
4723
|
+
<div class="composer-toolbar">
|
|
4724
|
+
<div id="status" class="status"></div>
|
|
4725
|
+
<button class="send" type="submit" aria-label="Send">
|
|
4726
|
+
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
|
4727
|
+
<path d="M12 19V5" />
|
|
4728
|
+
<path d="m5 12 7-7 7 7" />
|
|
4729
|
+
</svg>
|
|
4730
|
+
</button>
|
|
4731
|
+
</div>
|
|
4732
|
+
</div>
|
|
4733
|
+
</form>
|
|
4734
|
+
</section>
|
|
4735
|
+
</aside>
|
|
4736
|
+
</section>
|
|
4737
|
+
</section>
|
|
4738
|
+
</main>
|
|
4739
|
+
<div id="authModal" class="modal-overlay" data-open="false" aria-hidden="true">
|
|
4740
|
+
<section class="modal-panel" role="dialog" aria-modal="true" aria-labelledby="authModalTitle" aria-describedby="authModalDescription">
|
|
4741
|
+
<div class="modal-head">
|
|
4742
|
+
<div>
|
|
4743
|
+
<h2 id="authModalTitle" class="modal-title">Set bearer token</h2>
|
|
4744
|
+
<p id="authModalDescription" class="modal-description">Sidecar will test this token against the local MCP server before saving it.</p>
|
|
4745
|
+
</div>
|
|
4746
|
+
<button id="authClose" class="modal-close" type="button" aria-label="Close bearer token dialog">${iconSvg("x")}</button>
|
|
4747
|
+
</div>
|
|
4748
|
+
<label class="modal-field" for="authToken">
|
|
4749
|
+
<span class="modal-label">Bearer token</span>
|
|
4750
|
+
<input id="authToken" class="modal-input" type="password" autocomplete="off" placeholder="Paste a bearer token" />
|
|
4751
|
+
</label>
|
|
4752
|
+
<div id="authError" class="modal-error" role="status"></div>
|
|
4753
|
+
<div class="modal-actions">
|
|
4754
|
+
<button id="authCancel" class="modal-button" data-variant="secondary" type="button">Cancel</button>
|
|
4755
|
+
<button id="authSave" class="modal-button" data-variant="primary" type="button">Save</button>
|
|
4756
|
+
</div>
|
|
4757
|
+
</section>
|
|
4758
|
+
</div>
|
|
4759
|
+
<script>
|
|
4760
|
+
${devHarnessBrowserScript(options.initialBearerToken)}
|
|
4761
|
+
</script>
|
|
4762
|
+
</body>
|
|
4763
|
+
</html>`;
|
|
4399
4764
|
}
|
|
4400
|
-
var
|
|
4401
|
-
|
|
4402
|
-
"
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4765
|
+
var lobeIconCache = /* @__PURE__ */ new Map();
|
|
4766
|
+
function iconSvg(name) {
|
|
4767
|
+
const common = `viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"`;
|
|
4768
|
+
switch (name) {
|
|
4769
|
+
case "chatgpt":
|
|
4770
|
+
return lobeIconSvg("openai.svg") ?? `<svg ${common}><path d="M7.5 8.5a5 5 0 0 1 8.6-3.5 4.2 4.2 0 0 1 5.1 5.1 5 5 0 0 1-3.6 8.5 4.2 4.2 0 0 1-6.8 2.2 5 5 0 0 1-8-4.3 4.2 4.2 0 0 1-1-6.9 5 5 0 0 1 5.7-1.1Z"/></svg>`;
|
|
4771
|
+
case "claude":
|
|
4772
|
+
return lobeIconSvg("claude.svg") ?? `<svg ${common}><path d="M12 3v18"/><path d="m5.6 5.6 12.8 12.8"/><path d="M3 12h18"/><path d="M5.6 18.4 18.4 5.6"/></svg>`;
|
|
4773
|
+
case "braces":
|
|
4774
|
+
return `<svg ${common}><path d="M8 4c-2 1.3-3 2.8-3 4.7v1.1c0 1-.7 1.8-1.7 2.2C4.3 12.4 5 13.2 5 14.2v1.1c0 1.9 1 3.4 3 4.7"/><path d="M16 4c2 1.3 3 2.8 3 4.7v1.1c0 1 .7 1.8 1.7 2.2-1 .4-1.7 1.2-1.7 2.2v1.1c0 1.9-1 3.4-3 4.7"/></svg>`;
|
|
4775
|
+
case "sun":
|
|
4776
|
+
return `<svg ${common}><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>`;
|
|
4777
|
+
case "moon":
|
|
4778
|
+
return `<svg ${common}><path d="M20.5 14.5A8.5 8.5 0 0 1 9.5 3.5 7 7 0 1 0 20.5 14.5Z"/></svg>`;
|
|
4779
|
+
case "monitor":
|
|
4780
|
+
return `<svg ${common}><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8"/><path d="M12 16v4"/></svg>`;
|
|
4781
|
+
case "phone":
|
|
4782
|
+
return `<svg ${common}><rect x="7" y="2.5" width="10" height="19" rx="2"/><path d="M11 18.5h2"/></svg>`;
|
|
4783
|
+
case "x":
|
|
4784
|
+
return `<svg ${common}><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`;
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
function lobeIconSvg(fileName) {
|
|
4788
|
+
const cached = lobeIconCache.get(fileName);
|
|
4789
|
+
if (cached) {
|
|
4790
|
+
return cached;
|
|
4410
4791
|
}
|
|
4411
|
-
const host = request.headers.host ?? "";
|
|
4412
|
-
let originUrl;
|
|
4413
4792
|
try {
|
|
4414
|
-
|
|
4793
|
+
const file = require2.resolve(`@lobehub/icons-static-svg/icons/${fileName}`);
|
|
4794
|
+
const svg = readFileSync(file, "utf8").replace(/\s+xmlns="[^"]*"/, "").replace("<svg ", '<svg aria-hidden="true" ');
|
|
4795
|
+
lobeIconCache.set(fileName, svg);
|
|
4796
|
+
return svg;
|
|
4415
4797
|
} catch {
|
|
4416
|
-
return true;
|
|
4417
|
-
}
|
|
4418
|
-
if (isLocalHost(host)) {
|
|
4419
|
-
return !isLocalHost(originUrl.host);
|
|
4420
|
-
}
|
|
4421
|
-
const requestOrigin = `https://${host}`;
|
|
4422
|
-
const allowed = [requestOrigin, ...DEFAULT_ALLOWED_ORIGINS, ...allowedOrigins];
|
|
4423
|
-
return !allowed.some((pattern) => matchesOrigin(pattern, originUrl.origin));
|
|
4424
|
-
}
|
|
4425
|
-
function isLocalHost(host) {
|
|
4426
|
-
if (host.startsWith("[::1]")) {
|
|
4427
|
-
return true;
|
|
4428
|
-
}
|
|
4429
|
-
const hostname = host.split(":")[0]?.toLowerCase();
|
|
4430
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
4431
|
-
}
|
|
4432
|
-
function matchesOrigin(pattern, origin) {
|
|
4433
|
-
if (pattern === origin) {
|
|
4434
|
-
return true;
|
|
4435
|
-
}
|
|
4436
|
-
if (!pattern.includes("*")) {
|
|
4437
|
-
return false;
|
|
4438
|
-
}
|
|
4439
|
-
const escaped = pattern.split("*").map(escapeRegExp).join("[^.]+");
|
|
4440
|
-
return new RegExp(`^${escaped}$`).test(origin);
|
|
4441
|
-
}
|
|
4442
|
-
function escapeRegExp(value) {
|
|
4443
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4444
|
-
}
|
|
4445
|
-
function isProtectedResourceMetadataPath(pathname, endpoint) {
|
|
4446
|
-
return pathname === "/.well-known/oauth-protected-resource" || pathname === `/.well-known/oauth-protected-resource${endpoint}`;
|
|
4447
|
-
}
|
|
4448
|
-
function createHttpRequestLog(request, pathname) {
|
|
4449
|
-
return {
|
|
4450
|
-
method: request.method,
|
|
4451
|
-
path: pathname,
|
|
4452
|
-
host: truncateHeader(singleHeader(request.headers.host)),
|
|
4453
|
-
accept: truncateHeader(singleHeader(request.headers.accept)),
|
|
4454
|
-
contentType: truncateHeader(singleHeader(request.headers["content-type"])),
|
|
4455
|
-
contentLength: truncateHeader(singleHeader(request.headers["content-length"])),
|
|
4456
|
-
mcpProtocolVersion: truncateHeader(singleHeader(request.headers["mcp-protocol-version"])),
|
|
4457
|
-
origin: truncateHeader(singleHeader(request.headers.origin)),
|
|
4458
|
-
userAgent: truncateHeader(singleHeader(request.headers["user-agent"])),
|
|
4459
|
-
authorization: request.headers.authorization ? "present" : "absent",
|
|
4460
|
-
cookie: request.headers.cookie ? "present" : "absent"
|
|
4461
|
-
};
|
|
4462
|
-
}
|
|
4463
|
-
function logHttpRequest(metadata, status) {
|
|
4464
|
-
const debug = process.env.SIDECAR_DEBUG === "1" || process.env.SIDECAR_LOG_LEVEL === "debug";
|
|
4465
|
-
if (!debug && status < 400) {
|
|
4466
|
-
return;
|
|
4467
|
-
}
|
|
4468
|
-
const message = JSON.stringify({
|
|
4469
|
-
event: "sidecar.mcp.http",
|
|
4470
|
-
status,
|
|
4471
|
-
...stripUndefined4(metadata)
|
|
4472
|
-
});
|
|
4473
|
-
if (status >= 500) {
|
|
4474
|
-
console.error(message);
|
|
4475
|
-
} else if (status >= 400) {
|
|
4476
|
-
console.warn(message);
|
|
4477
|
-
} else {
|
|
4478
|
-
console.info(message);
|
|
4479
|
-
}
|
|
4480
|
-
}
|
|
4481
|
-
function singleHeader(value) {
|
|
4482
|
-
return Array.isArray(value) ? value.join(", ") : value;
|
|
4483
|
-
}
|
|
4484
|
-
function truncateHeader(value) {
|
|
4485
|
-
if (!value) {
|
|
4486
4798
|
return void 0;
|
|
4487
4799
|
}
|
|
4488
|
-
return value.length > 240 ? `${value.slice(0, 237)}...` : value;
|
|
4489
4800
|
}
|
|
4490
|
-
async function
|
|
4491
|
-
const
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4801
|
+
async function handleChatRequest(request, response, options) {
|
|
4802
|
+
const body = await readJson(request);
|
|
4803
|
+
const messages = readChatMessages(body.messages);
|
|
4804
|
+
const authToken = readAuthToken(body.authToken);
|
|
4805
|
+
const apiKey = options.openAiApiKey ?? process.env.OPENAI_API_KEY;
|
|
4806
|
+
response.writeHead(200, {
|
|
4807
|
+
"cache-control": "no-cache, no-transform",
|
|
4808
|
+
"connection": "keep-alive",
|
|
4809
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
4810
|
+
"x-accel-buffering": "no"
|
|
4811
|
+
});
|
|
4812
|
+
const sink = createSseSink(response);
|
|
4813
|
+
if (!apiKey) {
|
|
4814
|
+
sink.send("error", {
|
|
4815
|
+
message: "OPENAI_API_KEY is required for sidecar dev chat."
|
|
4816
|
+
});
|
|
4817
|
+
response.end();
|
|
4495
4818
|
return;
|
|
4496
4819
|
}
|
|
4497
4820
|
try {
|
|
4498
|
-
const
|
|
4499
|
-
const
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4821
|
+
const mcpTools = await listAllMcpTools(options.mcpUrl, authToken);
|
|
4822
|
+
const toolMap = mcpToolsToOpenAiTools(mcpTools);
|
|
4823
|
+
await streamChatWithTools({
|
|
4824
|
+
apiKey,
|
|
4825
|
+
baseUrl: options.openAiBaseUrl ?? process.env.OPENAI_BASE_URL ?? OPENAI_CHAT_COMPLETIONS_URL,
|
|
4826
|
+
model: options.model,
|
|
4827
|
+
messages: toOpenAiMessages(messages),
|
|
4828
|
+
tools: toolMap,
|
|
4829
|
+
mcpUrl: options.mcpUrl,
|
|
4830
|
+
authToken,
|
|
4831
|
+
sink
|
|
4503
4832
|
});
|
|
4504
|
-
|
|
4833
|
+
sink.send("done", {});
|
|
4505
4834
|
} catch (error) {
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4835
|
+
sink.send("error", normalizeHttpError(error));
|
|
4836
|
+
} finally {
|
|
4837
|
+
response.end();
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
async function streamChatWithTools(options) {
|
|
4841
|
+
const messages = [...options.messages];
|
|
4842
|
+
for (let iteration = 0; iteration < MAX_CHAT_TOOL_ITERATIONS; iteration += 1) {
|
|
4843
|
+
const assistant = await streamOpenAiChat({
|
|
4844
|
+
apiKey: options.apiKey,
|
|
4845
|
+
baseUrl: options.baseUrl,
|
|
4846
|
+
model: options.model,
|
|
4847
|
+
messages,
|
|
4848
|
+
tools: options.tools.tools,
|
|
4849
|
+
sink: options.sink
|
|
4850
|
+
});
|
|
4851
|
+
messages.push(assistant.message);
|
|
4852
|
+
if (!assistant.toolCalls.length) {
|
|
4853
|
+
return;
|
|
4854
|
+
}
|
|
4855
|
+
for (const toolCall of assistant.toolCalls) {
|
|
4856
|
+
const descriptor = options.tools.byOpenAiName.get(toolCall.function.name);
|
|
4857
|
+
if (!descriptor) {
|
|
4858
|
+
throw new HttpError(500, `Unknown model tool call "${toolCall.function.name}".`);
|
|
4859
|
+
}
|
|
4860
|
+
const args = parseToolArguments(toolCall.function.arguments);
|
|
4861
|
+
options.sink.send("tool_start", {
|
|
4862
|
+
id: toolCall.id,
|
|
4863
|
+
name: descriptor.name,
|
|
4864
|
+
title: descriptor.title ?? descriptor.name,
|
|
4865
|
+
arguments: args
|
|
4866
|
+
});
|
|
4867
|
+
const result = await callMcp(
|
|
4868
|
+
options.mcpUrl,
|
|
4869
|
+
"tools/call",
|
|
4870
|
+
{ name: descriptor.name, arguments: args },
|
|
4871
|
+
options.authToken
|
|
4872
|
+
);
|
|
4873
|
+
options.sink.send("tool_result", {
|
|
4874
|
+
id: toolCall.id,
|
|
4875
|
+
tool: {
|
|
4876
|
+
name: descriptor.name,
|
|
4877
|
+
title: descriptor.title ?? descriptor.name,
|
|
4878
|
+
resourceUri: toolResourceUri(descriptor)
|
|
4879
|
+
},
|
|
4880
|
+
result
|
|
4881
|
+
});
|
|
4882
|
+
messages.push({
|
|
4883
|
+
role: "tool",
|
|
4884
|
+
tool_call_id: toolCall.id,
|
|
4885
|
+
content: toolResultForModel(result)
|
|
4886
|
+
});
|
|
4887
|
+
}
|
|
4513
4888
|
}
|
|
4889
|
+
throw new HttpError(500, "The model kept requesting tools and hit the dev harness iteration limit.");
|
|
4514
4890
|
}
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
this.name = "JsonRpcHttpError";
|
|
4530
|
-
}
|
|
4531
|
-
status;
|
|
4532
|
-
};
|
|
4533
|
-
var RequestCancelledError = class extends Error {
|
|
4534
|
-
constructor(message = "Request cancelled.") {
|
|
4535
|
-
super(message);
|
|
4536
|
-
this.name = "RequestCancelledError";
|
|
4537
|
-
}
|
|
4538
|
-
};
|
|
4539
|
-
var AUTH_RESPONSE_SENT = /* @__PURE__ */ Symbol("sidecar.auth.response-sent");
|
|
4540
|
-
async function authorizeHttpRequest(auth, request, response) {
|
|
4541
|
-
const result = await auth.authorizeRequest(request);
|
|
4542
|
-
if (result.ok) {
|
|
4543
|
-
return result.auth;
|
|
4544
|
-
}
|
|
4545
|
-
response.writeHead(result.status, {
|
|
4546
|
-
"content-type": "application/json",
|
|
4547
|
-
...headersToRecord(result.headers)
|
|
4891
|
+
async function streamOpenAiChat(options) {
|
|
4892
|
+
const response = await fetch(options.baseUrl, {
|
|
4893
|
+
method: "POST",
|
|
4894
|
+
headers: {
|
|
4895
|
+
"authorization": `Bearer ${options.apiKey}`,
|
|
4896
|
+
"content-type": "application/json"
|
|
4897
|
+
},
|
|
4898
|
+
body: JSON.stringify({
|
|
4899
|
+
model: options.model,
|
|
4900
|
+
messages: options.messages,
|
|
4901
|
+
stream: true,
|
|
4902
|
+
tools: options.tools,
|
|
4903
|
+
tool_choice: "auto"
|
|
4904
|
+
})
|
|
4548
4905
|
});
|
|
4549
|
-
response.
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4906
|
+
if (!response.ok || !response.body) {
|
|
4907
|
+
throw new HttpError(
|
|
4908
|
+
response.status,
|
|
4909
|
+
`OpenAI chat request failed with HTTP ${response.status}: ${await response.text()}`
|
|
4910
|
+
);
|
|
4911
|
+
}
|
|
4912
|
+
let content = "";
|
|
4913
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
4914
|
+
for await (const event of readOpenAiSse(response.body)) {
|
|
4915
|
+
if (event === "[DONE]") {
|
|
4916
|
+
break;
|
|
4917
|
+
}
|
|
4918
|
+
const parsed = JSON.parse(event);
|
|
4919
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
4920
|
+
if (!delta) {
|
|
4921
|
+
continue;
|
|
4922
|
+
}
|
|
4923
|
+
if (delta.content) {
|
|
4924
|
+
content += delta.content;
|
|
4925
|
+
options.sink.send("delta", { text: delta.content });
|
|
4926
|
+
}
|
|
4927
|
+
for (const chunk of delta.tool_calls ?? []) {
|
|
4928
|
+
const current = toolCalls.get(chunk.index) ?? {
|
|
4929
|
+
id: chunk.id ?? `call_${chunk.index}`,
|
|
4930
|
+
type: "function",
|
|
4931
|
+
function: {
|
|
4932
|
+
name: "",
|
|
4933
|
+
arguments: ""
|
|
4559
4934
|
}
|
|
4560
|
-
}
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4935
|
+
};
|
|
4936
|
+
current.id = chunk.id ?? current.id;
|
|
4937
|
+
current.type = chunk.type ?? current.type;
|
|
4938
|
+
current.function.name += chunk.function?.name ?? "";
|
|
4939
|
+
current.function.arguments += chunk.function?.arguments ?? "";
|
|
4940
|
+
toolCalls.set(chunk.index, current);
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
const calls = [...toolCalls.entries()].sort(([left], [right]) => left - right).map(([, call]) => call);
|
|
4567
4944
|
return {
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
host: "unknown",
|
|
4573
|
-
transport: "streamable-http"
|
|
4945
|
+
message: {
|
|
4946
|
+
role: "assistant",
|
|
4947
|
+
content: content || null,
|
|
4948
|
+
...calls.length ? { tool_calls: calls } : {}
|
|
4574
4949
|
},
|
|
4575
|
-
|
|
4576
|
-
tools: {},
|
|
4577
|
-
log: consoleLogger,
|
|
4578
|
-
trace: {
|
|
4579
|
-
async span(_name, run) {
|
|
4580
|
-
return run();
|
|
4581
|
-
}
|
|
4582
|
-
},
|
|
4583
|
-
storage: {
|
|
4584
|
-
async get(key) {
|
|
4585
|
-
return memory.get(key);
|
|
4586
|
-
},
|
|
4587
|
-
async set(key, value) {
|
|
4588
|
-
memory.set(key, value);
|
|
4589
|
-
},
|
|
4590
|
-
async delete(key) {
|
|
4591
|
-
memory.delete(key);
|
|
4592
|
-
}
|
|
4593
|
-
},
|
|
4594
|
-
notify: noopNotifications,
|
|
4595
|
-
env: process.env
|
|
4950
|
+
toolCalls: calls
|
|
4596
4951
|
};
|
|
4597
4952
|
}
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
progressToken,
|
|
4618
|
-
progress: update.progress,
|
|
4619
|
-
total: update.total,
|
|
4620
|
-
message: update.message
|
|
4621
|
-
}));
|
|
4622
|
-
},
|
|
4623
|
-
async toolsChanged() {
|
|
4624
|
-
if (options.toolsListChanged) {
|
|
4625
|
-
sink?.send("notifications/tools/list_changed");
|
|
4626
|
-
}
|
|
4627
|
-
},
|
|
4628
|
-
async resourcesChanged() {
|
|
4629
|
-
if (options.resourcesListChanged) {
|
|
4630
|
-
sink?.send("notifications/resources/list_changed");
|
|
4631
|
-
}
|
|
4632
|
-
},
|
|
4633
|
-
async promptsChanged() {
|
|
4634
|
-
if (options.promptsListChanged) {
|
|
4635
|
-
sink?.send("notifications/prompts/list_changed");
|
|
4953
|
+
async function* readOpenAiSse(body) {
|
|
4954
|
+
const decoder = new TextDecoder();
|
|
4955
|
+
let buffer = "";
|
|
4956
|
+
const reader = body.getReader();
|
|
4957
|
+
while (true) {
|
|
4958
|
+
const { done, value } = await reader.read();
|
|
4959
|
+
if (done) {
|
|
4960
|
+
break;
|
|
4961
|
+
}
|
|
4962
|
+
const chunk = value;
|
|
4963
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
4964
|
+
let boundary = buffer.indexOf("\n\n");
|
|
4965
|
+
while (boundary !== -1) {
|
|
4966
|
+
const raw = buffer.slice(0, boundary);
|
|
4967
|
+
buffer = buffer.slice(boundary + 2);
|
|
4968
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
4969
|
+
if (line.startsWith("data:")) {
|
|
4970
|
+
yield line.slice(5).trim();
|
|
4971
|
+
}
|
|
4636
4972
|
}
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4973
|
+
boundary = buffer.indexOf("\n\n");
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
buffer += decoder.decode();
|
|
4977
|
+
if (buffer.trim()) {
|
|
4978
|
+
for (const line of buffer.split(/\r?\n/)) {
|
|
4979
|
+
if (line.startsWith("data:")) {
|
|
4980
|
+
yield line.slice(5).trim();
|
|
4641
4981
|
}
|
|
4642
4982
|
}
|
|
4643
|
-
}
|
|
4644
|
-
}
|
|
4645
|
-
function toResourceContext(ctx) {
|
|
4646
|
-
return {
|
|
4647
|
-
auth: ctx.auth,
|
|
4648
|
-
request: ctx.request,
|
|
4649
|
-
services: ctx.services,
|
|
4650
|
-
log: ctx.log,
|
|
4651
|
-
storage: ctx.storage,
|
|
4652
|
-
notify: ctx.notify,
|
|
4653
|
-
env: ctx.env
|
|
4654
|
-
};
|
|
4983
|
+
}
|
|
4655
4984
|
}
|
|
4656
|
-
function
|
|
4985
|
+
async function listAllMcpTools(mcpUrl, authToken) {
|
|
4986
|
+
const tools = [];
|
|
4987
|
+
let cursor;
|
|
4988
|
+
do {
|
|
4989
|
+
const result = await callMcp(
|
|
4990
|
+
mcpUrl,
|
|
4991
|
+
"tools/list",
|
|
4992
|
+
cursor ? { cursor } : {},
|
|
4993
|
+
authToken
|
|
4994
|
+
);
|
|
4995
|
+
tools.push(...result.tools ?? []);
|
|
4996
|
+
cursor = result.nextCursor;
|
|
4997
|
+
} while (cursor);
|
|
4998
|
+
return tools;
|
|
4999
|
+
}
|
|
5000
|
+
async function readMcpResource(mcpUrl, uri, authToken) {
|
|
5001
|
+
const result = await callMcp(mcpUrl, "resources/read", { uri }, authToken);
|
|
5002
|
+
const content = result.contents?.find((entry) => typeof entry.text === "string") ?? result.contents?.[0];
|
|
4657
5003
|
return {
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
services: ctx.services,
|
|
4661
|
-
log: ctx.log,
|
|
4662
|
-
storage: ctx.storage,
|
|
4663
|
-
notify: ctx.notify,
|
|
4664
|
-
env: ctx.env
|
|
5004
|
+
text: content?.text ?? "",
|
|
5005
|
+
mimeType: content?.mimeType
|
|
4665
5006
|
};
|
|
4666
5007
|
}
|
|
4667
|
-
function
|
|
4668
|
-
const
|
|
4669
|
-
|
|
4670
|
-
|
|
5008
|
+
async function callMcp(mcpUrl, method, params, authToken) {
|
|
5009
|
+
const response = await fetch(mcpUrl, {
|
|
5010
|
+
method: "POST",
|
|
5011
|
+
headers: {
|
|
5012
|
+
"accept": "application/json, text/event-stream",
|
|
5013
|
+
"content-type": "application/json",
|
|
5014
|
+
"mcp-protocol-version": MCP_PROTOCOL_VERSION,
|
|
5015
|
+
...authToken ? { authorization: `Bearer ${authToken}` } : {}
|
|
5016
|
+
},
|
|
5017
|
+
body: JSON.stringify({
|
|
5018
|
+
jsonrpc: "2.0",
|
|
5019
|
+
id: randomUUID3(),
|
|
5020
|
+
method,
|
|
5021
|
+
params
|
|
5022
|
+
})
|
|
5023
|
+
});
|
|
5024
|
+
const text = await response.text();
|
|
5025
|
+
if (!response.ok) {
|
|
5026
|
+
throw new HttpError(response.status, `MCP ${method} failed with HTTP ${response.status}: ${text}`);
|
|
4671
5027
|
}
|
|
4672
|
-
|
|
4673
|
-
|
|
5028
|
+
const json = JSON.parse(text);
|
|
5029
|
+
if (json.error) {
|
|
5030
|
+
throw new HttpError(502, json.error.message ?? `MCP ${method} returned an error.`);
|
|
4674
5031
|
}
|
|
4675
|
-
return
|
|
5032
|
+
return json.result;
|
|
5033
|
+
}
|
|
5034
|
+
function toOpenAiMessages(messages) {
|
|
5035
|
+
return [
|
|
5036
|
+
{
|
|
5037
|
+
role: "system",
|
|
5038
|
+
content: [
|
|
5039
|
+
"You are Sidecar dev, a local test harness for an MCP server.",
|
|
5040
|
+
"Use the available MCP tools whenever they are relevant.",
|
|
5041
|
+
"When a tool renders UI, keep your text concise because the harness displays the widget separately.",
|
|
5042
|
+
"Do not claim to be ChatGPT or Claude; the host selector only changes widget styling."
|
|
5043
|
+
].join(" ")
|
|
5044
|
+
},
|
|
5045
|
+
...messages.map((message) => ({
|
|
5046
|
+
role: message.role,
|
|
5047
|
+
content: message.content
|
|
5048
|
+
}))
|
|
5049
|
+
];
|
|
4676
5050
|
}
|
|
4677
|
-
function
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
if (!uri) {
|
|
4681
|
-
throw new JsonRpcError(-32602, `${method} requires params.uri.`);
|
|
5051
|
+
function readChatMessages(value) {
|
|
5052
|
+
if (!Array.isArray(value)) {
|
|
5053
|
+
return [];
|
|
4682
5054
|
}
|
|
4683
|
-
return
|
|
5055
|
+
return value.flatMap((entry) => {
|
|
5056
|
+
if (!entry || typeof entry !== "object") {
|
|
5057
|
+
return [];
|
|
5058
|
+
}
|
|
5059
|
+
const role = entry.role;
|
|
5060
|
+
const content = entry.content;
|
|
5061
|
+
if (role !== "user" && role !== "assistant" || typeof content !== "string") {
|
|
5062
|
+
return [];
|
|
5063
|
+
}
|
|
5064
|
+
return [{ role, content }];
|
|
5065
|
+
});
|
|
4684
5066
|
}
|
|
4685
|
-
function
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
return void 0;
|
|
5067
|
+
function normalizeToolParameters(schema) {
|
|
5068
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
5069
|
+
return { type: "object", properties: {} };
|
|
4689
5070
|
}
|
|
4690
|
-
const
|
|
4691
|
-
if (
|
|
4692
|
-
return
|
|
5071
|
+
const record = schema;
|
|
5072
|
+
if (record.type === "object" || record.properties) {
|
|
5073
|
+
return record;
|
|
4693
5074
|
}
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
5075
|
+
return {
|
|
5076
|
+
type: "object",
|
|
5077
|
+
properties: {}
|
|
5078
|
+
};
|
|
5079
|
+
}
|
|
5080
|
+
function uniqueOpenAiToolName(name, used) {
|
|
5081
|
+
const base = name.replace(/[^A-Za-z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^[-_]+/, "").slice(0, 58) || "tool";
|
|
5082
|
+
let candidate = base;
|
|
5083
|
+
let suffix = 2;
|
|
5084
|
+
while (used.has(candidate)) {
|
|
5085
|
+
const tag = `_${suffix}`;
|
|
5086
|
+
candidate = `${base.slice(0, 64 - tag.length)}${tag}`;
|
|
5087
|
+
suffix += 1;
|
|
4697
5088
|
}
|
|
4698
|
-
|
|
4699
|
-
|
|
5089
|
+
used.add(candidate);
|
|
5090
|
+
return candidate;
|
|
5091
|
+
}
|
|
5092
|
+
function parseToolArguments(value) {
|
|
5093
|
+
if (!value.trim()) {
|
|
5094
|
+
return {};
|
|
4700
5095
|
}
|
|
4701
|
-
|
|
5096
|
+
const parsed = JSON.parse(value);
|
|
5097
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5098
|
+
return {};
|
|
5099
|
+
}
|
|
5100
|
+
return parsed;
|
|
4702
5101
|
}
|
|
4703
|
-
function
|
|
4704
|
-
|
|
5102
|
+
function toolResultForModel(result) {
|
|
5103
|
+
const text = (result.content ?? []).map((block) => {
|
|
5104
|
+
if (typeof block === "string") return block;
|
|
5105
|
+
if (block && typeof block === "object" && typeof block.text === "string") {
|
|
5106
|
+
return block.text;
|
|
5107
|
+
}
|
|
5108
|
+
return JSON.stringify(block);
|
|
5109
|
+
}).filter(Boolean).join("\n");
|
|
5110
|
+
const structured = result.structuredContent === void 0 ? "" : `
|
|
5111
|
+
|
|
5112
|
+
structuredContent:
|
|
5113
|
+
${JSON.stringify(result.structuredContent, null, 2)}`;
|
|
5114
|
+
return `${text || "Tool returned no model-visible content."}${structured}`.slice(0, 8e4);
|
|
4705
5115
|
}
|
|
4706
|
-
function
|
|
4707
|
-
|
|
4708
|
-
|
|
5116
|
+
function createSseSink(response) {
|
|
5117
|
+
return {
|
|
5118
|
+
send(event, data) {
|
|
5119
|
+
response.write(`event: ${event}
|
|
5120
|
+
`);
|
|
5121
|
+
response.write(`data: ${JSON.stringify(data)}
|
|
5122
|
+
|
|
5123
|
+
`);
|
|
5124
|
+
}
|
|
5125
|
+
};
|
|
5126
|
+
}
|
|
5127
|
+
function updateDevState(state, body) {
|
|
5128
|
+
if (body.host === "chatgpt" || body.host === "claude" || body.host === "generic") {
|
|
5129
|
+
state.host = body.host;
|
|
4709
5130
|
}
|
|
4710
|
-
if (
|
|
4711
|
-
|
|
5131
|
+
if (body.theme === "light" || body.theme === "dark") {
|
|
5132
|
+
state.theme = body.theme;
|
|
4712
5133
|
}
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
}
|
|
4716
|
-
function operationToPaginationKey(operation) {
|
|
4717
|
-
switch (operation) {
|
|
4718
|
-
case "tools/list":
|
|
4719
|
-
return "toolsList";
|
|
4720
|
-
case "resources/list":
|
|
4721
|
-
return "resourcesList";
|
|
4722
|
-
case "resources/templates/list":
|
|
4723
|
-
return "resourceTemplatesList";
|
|
4724
|
-
case "prompts/list":
|
|
4725
|
-
return "promptsList";
|
|
5134
|
+
if (body.device === "desktop" || body.device === "mobile") {
|
|
5135
|
+
state.device = body.device;
|
|
4726
5136
|
}
|
|
4727
5137
|
}
|
|
4728
|
-
function
|
|
4729
|
-
const offset = cursor ? decodeCursor(cursor) : 0;
|
|
4730
|
-
const page = items.slice(offset, offset + pageSize);
|
|
4731
|
-
const nextOffset = offset + page.length;
|
|
5138
|
+
function devStatePayload(state, mcpUrl) {
|
|
4732
5139
|
return {
|
|
4733
|
-
|
|
4734
|
-
|
|
5140
|
+
host: state.host,
|
|
5141
|
+
theme: state.theme,
|
|
5142
|
+
device: state.device,
|
|
5143
|
+
target: state.target,
|
|
5144
|
+
model: state.model,
|
|
5145
|
+
mcpUrl
|
|
4735
5146
|
};
|
|
4736
5147
|
}
|
|
4737
|
-
function
|
|
4738
|
-
|
|
5148
|
+
function openStateEvents(response, clients, state, mcpUrl) {
|
|
5149
|
+
response.writeHead(200, {
|
|
5150
|
+
"cache-control": "no-cache, no-transform",
|
|
5151
|
+
"connection": "keep-alive",
|
|
5152
|
+
"content-type": "text/event-stream; charset=utf-8"
|
|
5153
|
+
});
|
|
5154
|
+
clients.add(response);
|
|
5155
|
+
createSseSink(response).send("state", devStatePayload(state, mcpUrl));
|
|
5156
|
+
response.on("close", () => clients.delete(response));
|
|
4739
5157
|
}
|
|
4740
|
-
function
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
const offset = decoded.offset;
|
|
4744
|
-
if (!Number.isInteger(offset) || typeof offset !== "number" || offset < 0) {
|
|
4745
|
-
throw new Error("invalid offset");
|
|
4746
|
-
}
|
|
4747
|
-
return offset;
|
|
4748
|
-
} catch {
|
|
4749
|
-
throw new JsonRpcError(-32602, "Invalid pagination cursor.");
|
|
5158
|
+
function broadcastState(clients, state, mcpUrl) {
|
|
5159
|
+
for (const client of clients) {
|
|
5160
|
+
createSseSink(client).send("state", devStatePayload(state, mcpUrl));
|
|
4750
5161
|
}
|
|
4751
5162
|
}
|
|
4752
|
-
function
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
result.body.error_description ?? result.body.error,
|
|
4756
|
-
{
|
|
4757
|
-
status: result.status,
|
|
4758
|
-
headers: headersToRecord(result.headers),
|
|
4759
|
-
body: result.body
|
|
4760
|
-
}
|
|
4761
|
-
);
|
|
5163
|
+
function sendHtml(response, html) {
|
|
5164
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
5165
|
+
response.end(html);
|
|
4762
5166
|
}
|
|
4763
|
-
function
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
if (!version) {
|
|
4767
|
-
return;
|
|
4768
|
-
}
|
|
4769
|
-
const supported = /* @__PURE__ */ new Set([SIDECAR_MCP_PROTOCOL_VERSION]);
|
|
4770
|
-
if (!supported.has(version)) {
|
|
4771
|
-
throw new JsonRpcHttpError(
|
|
4772
|
-
400,
|
|
4773
|
-
-32600,
|
|
4774
|
-
`Unsupported MCP-Protocol-Version "${version}".`
|
|
4775
|
-
);
|
|
4776
|
-
}
|
|
5167
|
+
function sendJson(response, body, status = 200) {
|
|
5168
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
5169
|
+
response.end(JSON.stringify(body));
|
|
4777
5170
|
}
|
|
4778
|
-
function
|
|
4779
|
-
const
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
406,
|
|
4789
|
-
-32600,
|
|
4790
|
-
"POST Accept must include application/json and text/event-stream."
|
|
4791
|
-
);
|
|
5171
|
+
async function readJson(request) {
|
|
5172
|
+
const chunks = [];
|
|
5173
|
+
let size = 0;
|
|
5174
|
+
for await (const chunk of request) {
|
|
5175
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5176
|
+
size += buffer.byteLength;
|
|
5177
|
+
if (size > 2e6) {
|
|
5178
|
+
throw new HttpError(413, "Request body is too large.");
|
|
5179
|
+
}
|
|
5180
|
+
chunks.push(buffer);
|
|
4792
5181
|
}
|
|
5182
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
5183
|
+
return text ? JSON.parse(text) : {};
|
|
4793
5184
|
}
|
|
4794
|
-
function
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
"GET Accept must include text/event-stream."
|
|
4802
|
-
);
|
|
5185
|
+
function readAuthToken(value) {
|
|
5186
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
5187
|
+
}
|
|
5188
|
+
function readBearerFromCookie(request) {
|
|
5189
|
+
const cookie = request.headers.cookie;
|
|
5190
|
+
if (!cookie) {
|
|
5191
|
+
return void 0;
|
|
4803
5192
|
}
|
|
5193
|
+
const match = cookie.match(/(?:^|;\s*)sidecar_dev_bearer=([^;]+)/);
|
|
5194
|
+
return match?.[1] ? decodeURIComponent(match[1]) : void 0;
|
|
4804
5195
|
}
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
5196
|
+
function normalizeHttpError(error) {
|
|
5197
|
+
if (error instanceof HttpError) {
|
|
5198
|
+
return {
|
|
5199
|
+
error: "sidecar_dev_error",
|
|
5200
|
+
status: error.status,
|
|
5201
|
+
message: error.message
|
|
4810
5202
|
};
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
5203
|
+
}
|
|
5204
|
+
return {
|
|
5205
|
+
error: "sidecar_dev_error",
|
|
5206
|
+
message: error instanceof Error ? error.message : String(error)
|
|
5207
|
+
};
|
|
5208
|
+
}
|
|
5209
|
+
function listenOnLocalhost(server, port) {
|
|
5210
|
+
return new Promise((resolve, reject) => {
|
|
5211
|
+
const onError = (error) => reject(error);
|
|
5212
|
+
server.once("error", onError);
|
|
5213
|
+
server.listen(port, "127.0.0.1", () => {
|
|
5214
|
+
server.off("error", onError);
|
|
5215
|
+
const address = server.address();
|
|
5216
|
+
if (!address) {
|
|
5217
|
+
reject(new Error("Dev harness did not expose a bound address."));
|
|
5218
|
+
return;
|
|
5219
|
+
}
|
|
5220
|
+
resolve(address.port);
|
|
5221
|
+
});
|
|
4816
5222
|
});
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
if (
|
|
4822
|
-
|
|
5223
|
+
}
|
|
5224
|
+
function closeServer(server) {
|
|
5225
|
+
return new Promise((resolve, reject) => {
|
|
5226
|
+
server.close((error) => {
|
|
5227
|
+
if (error) {
|
|
5228
|
+
reject(error);
|
|
5229
|
+
return;
|
|
4823
5230
|
}
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
try {
|
|
4828
|
-
return await Promise.race([
|
|
4829
|
-
promise,
|
|
4830
|
-
abortPromise,
|
|
4831
|
-
new Promise((_resolve, reject) => {
|
|
4832
|
-
timeout = setTimeout(() => {
|
|
4833
|
-
const error = new JsonRpcError(-32e3, "Tool execution timed out.");
|
|
4834
|
-
controller.abort(error);
|
|
4835
|
-
reject(error);
|
|
4836
|
-
}, timeoutMs);
|
|
4837
|
-
})
|
|
4838
|
-
]);
|
|
4839
|
-
} finally {
|
|
4840
|
-
if (timeout) {
|
|
4841
|
-
clearTimeout(timeout);
|
|
4842
|
-
}
|
|
4843
|
-
if (abortListener) {
|
|
4844
|
-
controller.signal.removeEventListener("abort", abortListener);
|
|
4845
|
-
}
|
|
4846
|
-
}
|
|
5231
|
+
resolve();
|
|
5232
|
+
});
|
|
5233
|
+
});
|
|
4847
5234
|
}
|
|
4848
|
-
function
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
}
|
|
4852
|
-
return new RequestCancelledError();
|
|
5235
|
+
async function streamdownClientScript() {
|
|
5236
|
+
streamdownClientScriptPromise ??= buildStreamdownClientScript();
|
|
5237
|
+
return await streamdownClientScriptPromise;
|
|
4853
5238
|
}
|
|
4854
|
-
function
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
5239
|
+
async function buildStreamdownClientScript() {
|
|
5240
|
+
const { build } = await import("esbuild");
|
|
5241
|
+
const result = await build({
|
|
5242
|
+
bundle: true,
|
|
5243
|
+
define: {
|
|
5244
|
+
"process.env.NODE_ENV": JSON.stringify("development")
|
|
5245
|
+
},
|
|
5246
|
+
format: "iife",
|
|
5247
|
+
globalName: "SidecarStreamdownBundle",
|
|
5248
|
+
jsx: "automatic",
|
|
5249
|
+
platform: "browser",
|
|
5250
|
+
stdin: {
|
|
5251
|
+
contents: `
|
|
5252
|
+
import React from "react";
|
|
5253
|
+
import { createRoot } from "react-dom/client";
|
|
5254
|
+
import { Streamdown } from "streamdown";
|
|
5255
|
+
|
|
5256
|
+
const roots = new WeakMap();
|
|
5257
|
+
window.SidecarStreamdown = {
|
|
5258
|
+
render(element, markdown) {
|
|
5259
|
+
let root = roots.get(element);
|
|
5260
|
+
if (!root) {
|
|
5261
|
+
root = createRoot(element);
|
|
5262
|
+
roots.set(element, root);
|
|
5263
|
+
}
|
|
5264
|
+
root.render(React.createElement(Streamdown, { parseIncompleteMarkdown: true }, markdown));
|
|
5265
|
+
}
|
|
5266
|
+
};
|
|
5267
|
+
`,
|
|
5268
|
+
loader: "tsx",
|
|
5269
|
+
resolveDir: path20.dirname(fileURLToPath(import.meta.url))
|
|
5270
|
+
},
|
|
5271
|
+
write: false
|
|
5272
|
+
});
|
|
5273
|
+
const output = result.outputFiles[0]?.text;
|
|
5274
|
+
if (!output) {
|
|
5275
|
+
throw new Error("Failed to build the Sidecar dev markdown renderer.");
|
|
4861
5276
|
}
|
|
4862
|
-
return
|
|
5277
|
+
return output;
|
|
4863
5278
|
}
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
5279
|
+
var HttpError = class extends Error {
|
|
5280
|
+
constructor(status, message) {
|
|
5281
|
+
super(message);
|
|
5282
|
+
this.status = status;
|
|
5283
|
+
this.name = "HttpError";
|
|
4867
5284
|
}
|
|
4868
|
-
|
|
4869
|
-
|
|
5285
|
+
status;
|
|
5286
|
+
};
|
|
5287
|
+
function devHarnessBrowserScript(initialBearerToken) {
|
|
5288
|
+
return String.raw`
|
|
5289
|
+
const MCP_APPS_PROTOCOL_VERSION = ${JSON.stringify(MCP_APPS_PROTOCOL_VERSION)};
|
|
5290
|
+
const state = {
|
|
5291
|
+
host: document.documentElement.dataset.sidecarHost || "chatgpt",
|
|
5292
|
+
theme: document.documentElement.dataset.sidecarTheme || "light",
|
|
5293
|
+
device: document.documentElement.dataset.sidecarDevice || "desktop",
|
|
5294
|
+
};
|
|
5295
|
+
const messages = [];
|
|
5296
|
+
const frameContexts = new Map();
|
|
5297
|
+
const markdownSources = new WeakMap();
|
|
5298
|
+
let markdownRendererPromise;
|
|
5299
|
+
const messagesEl = document.getElementById("messages");
|
|
5300
|
+
const form = document.getElementById("chatForm");
|
|
5301
|
+
const promptEl = document.getElementById("prompt");
|
|
5302
|
+
const statusEl = document.getElementById("status");
|
|
5303
|
+
const authEl = document.getElementById("authToken");
|
|
5304
|
+
const authTriggerEl = document.getElementById("authTrigger");
|
|
5305
|
+
const authModalEl = document.getElementById("authModal");
|
|
5306
|
+
const authCloseEl = document.getElementById("authClose");
|
|
5307
|
+
const authCancelEl = document.getElementById("authCancel");
|
|
5308
|
+
const authSaveEl = document.getElementById("authSave");
|
|
5309
|
+
const authErrorEl = document.getElementById("authError");
|
|
5310
|
+
const surfaceTitleEl = document.getElementById("surfaceTitle");
|
|
5311
|
+
const surfaceMetaEl = document.getElementById("surfaceMeta");
|
|
5312
|
+
const initialBearerToken = ${JSON.stringify(initialBearerToken ?? "")};
|
|
5313
|
+
let bearerToken = initialBearerToken || localStorage.getItem("sidecar.dev.bearer") || "";
|
|
5314
|
+
|
|
5315
|
+
authEl.value = bearerToken;
|
|
5316
|
+
setAuthCookie(bearerToken);
|
|
5317
|
+
updateAuthTrigger();
|
|
5318
|
+
authTriggerEl.addEventListener("click", openAuthModal);
|
|
5319
|
+
authCloseEl.addEventListener("click", closeAuthModal);
|
|
5320
|
+
authCancelEl.addEventListener("click", closeAuthModal);
|
|
5321
|
+
authSaveEl.addEventListener("click", saveBearerToken);
|
|
5322
|
+
authModalEl.addEventListener("click", (event) => {
|
|
5323
|
+
if (event.target === authModalEl) {
|
|
5324
|
+
closeAuthModal();
|
|
4870
5325
|
}
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
}
|
|
5326
|
+
});
|
|
5327
|
+
document.addEventListener("keydown", (event) => {
|
|
5328
|
+
if (event.key === "Escape" && authModalEl.dataset.open === "true") {
|
|
5329
|
+
closeAuthModal();
|
|
4876
5330
|
}
|
|
4877
|
-
|
|
4878
|
-
|
|
5331
|
+
});
|
|
5332
|
+
|
|
5333
|
+
const resizePrompt = () => {
|
|
5334
|
+
promptEl.style.height = "auto";
|
|
5335
|
+
promptEl.style.height = promptEl.scrollHeight + "px";
|
|
5336
|
+
};
|
|
5337
|
+
promptEl.addEventListener("input", resizePrompt);
|
|
5338
|
+
promptEl.addEventListener("keydown", (event) => {
|
|
5339
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
5340
|
+
event.preventDefault();
|
|
5341
|
+
form.requestSubmit();
|
|
5342
|
+
}
|
|
5343
|
+
});
|
|
5344
|
+
resizePrompt();
|
|
5345
|
+
|
|
5346
|
+
document.querySelectorAll("[data-host]").forEach((button) => {
|
|
5347
|
+
button.addEventListener("click", () => setState({ host: button.dataset.host }));
|
|
5348
|
+
});
|
|
5349
|
+
document.querySelectorAll("[data-theme]").forEach((button) => {
|
|
5350
|
+
button.addEventListener("click", () => setState({ theme: button.dataset.theme }));
|
|
5351
|
+
});
|
|
5352
|
+
document.querySelectorAll("[data-device]").forEach((button) => {
|
|
5353
|
+
button.addEventListener("click", () => setState({ device: button.dataset.device }));
|
|
5354
|
+
});
|
|
5355
|
+
|
|
5356
|
+
const events = new EventSource("/__sidecar/dev/events");
|
|
5357
|
+
events.addEventListener("state", (event) => {
|
|
5358
|
+
applyState(JSON.parse(event.data));
|
|
5359
|
+
});
|
|
5360
|
+
|
|
5361
|
+
form.addEventListener("submit", async (event) => {
|
|
5362
|
+
event.preventDefault();
|
|
5363
|
+
const text = promptEl.value.trim();
|
|
5364
|
+
if (!text) return;
|
|
5365
|
+
promptEl.value = "";
|
|
5366
|
+
resizePrompt();
|
|
5367
|
+
clearEmpty();
|
|
5368
|
+
messages.push({ role: "user", content: text });
|
|
5369
|
+
appendMessage("user", text);
|
|
5370
|
+
const assistant = appendMessage("assistant", "");
|
|
5371
|
+
statusEl.classList.remove("error");
|
|
5372
|
+
statusEl.textContent = "Thinking...";
|
|
5373
|
+
try {
|
|
5374
|
+
await streamChat(assistant.querySelector(".content"));
|
|
5375
|
+
statusEl.classList.remove("error");
|
|
5376
|
+
statusEl.textContent = "";
|
|
5377
|
+
} catch (error) {
|
|
5378
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5379
|
+
statusEl.classList.remove("error");
|
|
5380
|
+
statusEl.textContent = "";
|
|
5381
|
+
const assistantError = renderAssistantError(assistant.querySelector(".content"), message);
|
|
5382
|
+
messages.push({ role: "assistant", content: assistantError });
|
|
4879
5383
|
}
|
|
4880
|
-
|
|
4881
|
-
|
|
5384
|
+
});
|
|
5385
|
+
|
|
5386
|
+
window.addEventListener("message", async (event) => {
|
|
5387
|
+
const message = event.data;
|
|
5388
|
+
if (!message || message.jsonrpc !== "2.0" || typeof message.method !== "string") {
|
|
5389
|
+
return;
|
|
4882
5390
|
}
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
5391
|
+
const frame = [...frameContexts.entries()].find(([source]) => source === event.source);
|
|
5392
|
+
const context = frame?.[1];
|
|
5393
|
+
try {
|
|
5394
|
+
if (message.method === "ui/initialize") {
|
|
5395
|
+
respond(event.source, message.id, {
|
|
5396
|
+
protocolVersion: MCP_APPS_PROTOCOL_VERSION,
|
|
5397
|
+
hostInfo: { name: "Sidecar dev", version: "0.0.0-dev" },
|
|
5398
|
+
hostCapabilities: hostCapabilities(),
|
|
5399
|
+
hostContext: hostContext(),
|
|
5400
|
+
});
|
|
5401
|
+
return;
|
|
4887
5402
|
}
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
return `${path21} must be an object.`;
|
|
5403
|
+
if (message.method === "ui/notifications/initialized") {
|
|
5404
|
+
sendFrameContext(event.source);
|
|
5405
|
+
return;
|
|
4892
5406
|
}
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
}
|
|
5407
|
+
if (message.method === "tools/call") {
|
|
5408
|
+
const result = await rpc("tools/call", message.params);
|
|
5409
|
+
respond(event.source, message.id, result);
|
|
5410
|
+
return;
|
|
4898
5411
|
}
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
}
|
|
5412
|
+
if (message.method === "resources/list" || message.method === "resources/read") {
|
|
5413
|
+
const result = await rpc(message.method, message.params);
|
|
5414
|
+
respond(event.source, message.id, result);
|
|
5415
|
+
return;
|
|
4904
5416
|
}
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
}
|
|
4911
|
-
|
|
4912
|
-
for (const [key, entry] of Object.entries(record)) {
|
|
4913
|
-
if (!allowed.has(key)) {
|
|
4914
|
-
const failure = schemaFailure(schema.additionalProperties, entry, `${path21}.${key}`);
|
|
4915
|
-
if (failure) return failure;
|
|
4916
|
-
}
|
|
5417
|
+
if (message.method === "ui/open-link") {
|
|
5418
|
+
const url = message.params?.url;
|
|
5419
|
+
if (typeof url === "string" && /^(https?:|mailto:)/.test(url)) {
|
|
5420
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
5421
|
+
respond(event.source, message.id, { isError: false });
|
|
5422
|
+
} else {
|
|
5423
|
+
respond(event.source, message.id, { isError: true });
|
|
4917
5424
|
}
|
|
5425
|
+
return;
|
|
4918
5426
|
}
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
return `${path21} must be an array.`;
|
|
4923
|
-
}
|
|
4924
|
-
if (schema.minItems !== void 0 && value.length < schema.minItems) {
|
|
4925
|
-
return `${path21} must contain at least ${schema.minItems} items.`;
|
|
4926
|
-
}
|
|
4927
|
-
if (schema.maxItems !== void 0 && value.length > schema.maxItems) {
|
|
4928
|
-
return `${path21} must contain at most ${schema.maxItems} items.`;
|
|
5427
|
+
if (message.method === "ui/request-display-mode") {
|
|
5428
|
+
respond(event.source, message.id, { mode: message.params?.mode || "inline" });
|
|
5429
|
+
return;
|
|
4929
5430
|
}
|
|
4930
|
-
if (
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
if (failure) return failure;
|
|
4934
|
-
}
|
|
5431
|
+
if (message.method === "ui/message" || message.method === "ui/update-model-context") {
|
|
5432
|
+
respond(event.source, message.id, { isError: false });
|
|
5433
|
+
return;
|
|
4935
5434
|
}
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
if (schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
4939
|
-
return `${path21} is shorter than ${schema.minLength}.`;
|
|
5435
|
+
if (message.method.startsWith("ui/notifications/") || message.method === "notifications/message") {
|
|
5436
|
+
return;
|
|
4940
5437
|
}
|
|
4941
|
-
if (
|
|
4942
|
-
|
|
5438
|
+
if (message.id !== undefined) {
|
|
5439
|
+
respondError(event.source, message.id, -32601, "Unsupported dev harness method: " + message.method);
|
|
4943
5440
|
}
|
|
4944
|
-
|
|
4945
|
-
|
|
5441
|
+
} catch (error) {
|
|
5442
|
+
if (message.id !== undefined) {
|
|
5443
|
+
respondError(event.source, message.id, -32000, error instanceof Error ? error.message : String(error));
|
|
4946
5444
|
}
|
|
4947
5445
|
}
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
5446
|
+
});
|
|
5447
|
+
|
|
5448
|
+
async function streamChat(contentEl) {
|
|
5449
|
+
const response = await fetch("/__sidecar/dev/chat", {
|
|
5450
|
+
method: "POST",
|
|
5451
|
+
headers: { "content-type": "application/json" },
|
|
5452
|
+
body: JSON.stringify({
|
|
5453
|
+
messages,
|
|
5454
|
+
authToken: currentAuthToken(),
|
|
5455
|
+
}),
|
|
5456
|
+
});
|
|
5457
|
+
if (!response.ok || !response.body) {
|
|
5458
|
+
throw new Error("Chat request failed.");
|
|
5459
|
+
}
|
|
5460
|
+
const reader = response.body.getReader();
|
|
5461
|
+
const decoder = new TextDecoder();
|
|
5462
|
+
let buffer = "";
|
|
5463
|
+
let assistantText = "";
|
|
5464
|
+
while (true) {
|
|
5465
|
+
const { done, value } = await reader.read();
|
|
5466
|
+
if (done) break;
|
|
5467
|
+
buffer += decoder.decode(value, { stream: true });
|
|
5468
|
+
let boundary = buffer.indexOf("\n\n");
|
|
5469
|
+
while (boundary !== -1) {
|
|
5470
|
+
const raw = buffer.slice(0, boundary);
|
|
5471
|
+
buffer = buffer.slice(boundary + 2);
|
|
5472
|
+
const parsed = parseEvent(raw);
|
|
5473
|
+
if (parsed) {
|
|
5474
|
+
if (parsed.event === "delta") {
|
|
5475
|
+
assistantText += parsed.data.text || "";
|
|
5476
|
+
renderMarkdown(contentEl, assistantText);
|
|
5477
|
+
} else if (parsed.event === "tool_start") {
|
|
5478
|
+
appendToolStart(parsed.data);
|
|
5479
|
+
} else if (parsed.event === "tool_result") {
|
|
5480
|
+
appendToolResult(parsed.data);
|
|
5481
|
+
} else if (parsed.event === "error") {
|
|
5482
|
+
throw new Error(parsed.data.message || "Sidecar dev chat failed.");
|
|
5483
|
+
}
|
|
5484
|
+
}
|
|
5485
|
+
boundary = buffer.indexOf("\n\n");
|
|
4954
5486
|
}
|
|
4955
5487
|
}
|
|
4956
|
-
|
|
4957
|
-
}
|
|
4958
|
-
function matchesJsonSchemaType(type, value) {
|
|
4959
|
-
switch (type) {
|
|
4960
|
-
case "object":
|
|
4961
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
4962
|
-
case "array":
|
|
4963
|
-
return Array.isArray(value);
|
|
4964
|
-
case "string":
|
|
4965
|
-
return typeof value === "string";
|
|
4966
|
-
case "number":
|
|
4967
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
4968
|
-
case "integer":
|
|
4969
|
-
return Number.isInteger(value);
|
|
4970
|
-
case "boolean":
|
|
4971
|
-
return typeof value === "boolean";
|
|
4972
|
-
case "null":
|
|
4973
|
-
return value === null;
|
|
4974
|
-
default:
|
|
4975
|
-
return true;
|
|
5488
|
+
if (assistantText.trim()) {
|
|
5489
|
+
messages.push({ role: "assistant", content: assistantText });
|
|
4976
5490
|
}
|
|
4977
5491
|
}
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
return
|
|
4987
|
-
|
|
4988
|
-
);
|
|
5492
|
+
|
|
5493
|
+
function parseEvent(raw) {
|
|
5494
|
+
let event = "message";
|
|
5495
|
+
let data = "";
|
|
5496
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
5497
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
5498
|
+
if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
5499
|
+
}
|
|
5500
|
+
if (!data) return null;
|
|
5501
|
+
return { event, data: JSON.parse(data) };
|
|
4989
5502
|
}
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
5503
|
+
|
|
5504
|
+
function appendMessage(role, content) {
|
|
5505
|
+
clearEmpty();
|
|
5506
|
+
const article = document.createElement("article");
|
|
5507
|
+
article.className = "message";
|
|
5508
|
+
article.dataset.role = role;
|
|
5509
|
+
article.innerHTML = '<div class="role"></div><div class="content"></div>';
|
|
5510
|
+
article.querySelector(".role").textContent = role;
|
|
5511
|
+
const contentEl = article.querySelector(".content");
|
|
5512
|
+
if (role === "assistant") {
|
|
5513
|
+
contentEl.classList.add("markdown");
|
|
5514
|
+
renderMarkdown(contentEl, content);
|
|
5515
|
+
} else {
|
|
5516
|
+
contentEl.textContent = content;
|
|
4993
5517
|
}
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
5518
|
+
messagesEl.append(article);
|
|
5519
|
+
article.scrollIntoView({ block: "end" });
|
|
5520
|
+
return article;
|
|
5521
|
+
}
|
|
5522
|
+
|
|
5523
|
+
function renderMarkdown(element, markdown) {
|
|
5524
|
+
markdownSources.set(element, markdown);
|
|
5525
|
+
if (window.SidecarStreamdown?.render) {
|
|
5526
|
+
window.SidecarStreamdown.render(element, markdown);
|
|
5527
|
+
return;
|
|
4998
5528
|
}
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5529
|
+
element.textContent = markdown;
|
|
5530
|
+
if (!markdownRendererPromise) {
|
|
5531
|
+
markdownRendererPromise = import("/__sidecar/dev/streamdown-client.js")
|
|
5532
|
+
.then(() => {
|
|
5533
|
+
document.querySelectorAll(".content.markdown").forEach((target) => {
|
|
5534
|
+
window.SidecarStreamdown?.render(target, markdownSources.get(target) || "");
|
|
5535
|
+
});
|
|
5536
|
+
})
|
|
5537
|
+
.catch((error) => {
|
|
5538
|
+
console.warn("Sidecar dev markdown renderer failed to load", error);
|
|
5539
|
+
});
|
|
5003
5540
|
}
|
|
5004
|
-
return {
|
|
5005
|
-
status,
|
|
5006
|
-
headers
|
|
5007
|
-
};
|
|
5008
5541
|
}
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5542
|
+
|
|
5543
|
+
function renderAssistantError(element, message) {
|
|
5544
|
+
const text = "Sidecar dev hit an error:\\n\\n" + message;
|
|
5545
|
+
element.classList.add("error");
|
|
5546
|
+
renderMarkdown(element, text);
|
|
5547
|
+
return text;
|
|
5548
|
+
}
|
|
5549
|
+
|
|
5550
|
+
function appendToolStart(tool) {
|
|
5551
|
+
const article = document.createElement("article");
|
|
5552
|
+
article.className = "tool-card";
|
|
5553
|
+
article.dataset.toolCallId = tool.id;
|
|
5554
|
+
article.innerHTML = '<div class="tool-head"><div class="tool-title"></div><div class="status">Running</div></div><div class="tool-body"></div>';
|
|
5555
|
+
article.querySelector(".tool-title").textContent = tool.title || tool.name;
|
|
5556
|
+
messagesEl.append(article);
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
function appendToolResult(event) {
|
|
5560
|
+
const article = document.querySelector('[data-tool-call-id="' + CSS.escape(event.id) + '"]') || document.createElement("article");
|
|
5561
|
+
if (!article.parentElement) {
|
|
5562
|
+
article.className = "tool-card";
|
|
5563
|
+
article.dataset.toolCallId = event.id;
|
|
5564
|
+
article.innerHTML = '<div class="tool-head"><div class="tool-title"></div><div class="status"></div></div><div class="tool-body"></div>';
|
|
5565
|
+
messagesEl.append(article);
|
|
5566
|
+
}
|
|
5567
|
+
article.querySelector(".tool-title").textContent = event.tool.title || event.tool.name;
|
|
5568
|
+
article.querySelector(".status").textContent = event.result?.isError ? "Error" : "Done";
|
|
5569
|
+
const body = article.querySelector(".tool-body");
|
|
5570
|
+
body.innerHTML = "";
|
|
5571
|
+
if (event.tool.resourceUri) {
|
|
5572
|
+
const iframe = document.createElement("iframe");
|
|
5573
|
+
iframe.src = "/__sidecar/dev/resource?uri=" + encodeURIComponent(event.tool.resourceUri);
|
|
5574
|
+
iframe.title = event.tool.title || event.tool.name;
|
|
5575
|
+
frameContexts.set(iframe.contentWindow, {
|
|
5576
|
+
result: event.result,
|
|
5577
|
+
tool: event.tool,
|
|
5578
|
+
arguments: event.arguments || {},
|
|
5579
|
+
});
|
|
5580
|
+
iframe.addEventListener("load", () => {
|
|
5581
|
+
frameContexts.set(iframe.contentWindow, {
|
|
5582
|
+
result: event.result,
|
|
5583
|
+
tool: event.tool,
|
|
5584
|
+
arguments: event.arguments || {},
|
|
5585
|
+
});
|
|
5586
|
+
window.setTimeout(() => sendFrameContext(iframe.contentWindow), 120);
|
|
5587
|
+
});
|
|
5588
|
+
body.append(iframe);
|
|
5589
|
+
} else {
|
|
5590
|
+
const pre = document.createElement("pre");
|
|
5591
|
+
pre.textContent = toolText(event.result);
|
|
5592
|
+
body.append(pre);
|
|
5021
5593
|
}
|
|
5022
|
-
};
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5594
|
+
article.scrollIntoView({ block: "end" });
|
|
5595
|
+
}
|
|
5596
|
+
|
|
5597
|
+
function toolText(result) {
|
|
5598
|
+
const blocks = result?.content || [];
|
|
5599
|
+
const text = blocks.map((block) => typeof block === "string" ? block : block?.text || JSON.stringify(block)).join("\n");
|
|
5600
|
+
return text || JSON.stringify(result?.structuredContent || result || {}, null, 2);
|
|
5601
|
+
}
|
|
5602
|
+
|
|
5603
|
+
async function rpc(method, params) {
|
|
5604
|
+
const response = await fetch("/__sidecar/dev/rpc", {
|
|
5605
|
+
method: "POST",
|
|
5606
|
+
headers: { "content-type": "application/json" },
|
|
5607
|
+
body: JSON.stringify({ method, params, authToken: currentAuthToken() }),
|
|
5608
|
+
});
|
|
5609
|
+
const json = await response.json();
|
|
5610
|
+
if (!response.ok) {
|
|
5611
|
+
throw new Error(json.message || json.error || "MCP request failed.");
|
|
5033
5612
|
}
|
|
5034
|
-
return
|
|
5613
|
+
return json.result;
|
|
5035
5614
|
}
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5615
|
+
|
|
5616
|
+
function openAuthModal() {
|
|
5617
|
+
authEl.value = bearerToken;
|
|
5618
|
+
authErrorEl.textContent = "";
|
|
5619
|
+
authErrorEl.dataset.visible = "false";
|
|
5620
|
+
authModalEl.dataset.open = "true";
|
|
5621
|
+
authModalEl.setAttribute("aria-hidden", "false");
|
|
5622
|
+
window.setTimeout(() => authEl.focus(), 40);
|
|
5623
|
+
}
|
|
5624
|
+
|
|
5625
|
+
function closeAuthModal() {
|
|
5626
|
+
authModalEl.dataset.open = "false";
|
|
5627
|
+
authModalEl.setAttribute("aria-hidden", "true");
|
|
5628
|
+
window.setTimeout(() => authTriggerEl.focus(), 40);
|
|
5629
|
+
}
|
|
5630
|
+
|
|
5631
|
+
async function saveBearerToken() {
|
|
5632
|
+
const nextToken = authEl.value.trim();
|
|
5633
|
+
authErrorEl.textContent = "";
|
|
5634
|
+
authErrorEl.dataset.visible = "false";
|
|
5635
|
+
authSaveEl.disabled = true;
|
|
5636
|
+
authSaveEl.textContent = nextToken ? "Testing..." : "Saving...";
|
|
5637
|
+
try {
|
|
5638
|
+
if (nextToken) {
|
|
5639
|
+
await testBearerToken(nextToken);
|
|
5640
|
+
}
|
|
5641
|
+
bearerToken = nextToken;
|
|
5642
|
+
if (bearerToken) {
|
|
5643
|
+
localStorage.setItem("sidecar.dev.bearer", bearerToken);
|
|
5644
|
+
} else {
|
|
5645
|
+
localStorage.removeItem("sidecar.dev.bearer");
|
|
5045
5646
|
}
|
|
5647
|
+
setAuthCookie(bearerToken);
|
|
5648
|
+
updateAuthTrigger();
|
|
5649
|
+
closeAuthModal();
|
|
5650
|
+
} catch (error) {
|
|
5651
|
+
authErrorEl.textContent = error instanceof Error ? error.message : String(error);
|
|
5652
|
+
authErrorEl.dataset.visible = "true";
|
|
5653
|
+
} finally {
|
|
5654
|
+
authSaveEl.disabled = false;
|
|
5655
|
+
authSaveEl.textContent = "Save";
|
|
5046
5656
|
}
|
|
5047
|
-
const url = baseUrl ? new URL(request.url ?? "/", baseUrl) : new URL(
|
|
5048
|
-
request.url ?? "/",
|
|
5049
|
-
`${headers.get("x-forwarded-proto") ?? "http"}://${headers.get("host") ?? "127.0.0.1"}`
|
|
5050
|
-
);
|
|
5051
|
-
return new Request(url, {
|
|
5052
|
-
headers,
|
|
5053
|
-
method: request.method
|
|
5054
|
-
});
|
|
5055
5657
|
}
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
}
|
|
5064
|
-
if (
|
|
5065
|
-
throw new
|
|
5658
|
+
|
|
5659
|
+
async function testBearerToken(token) {
|
|
5660
|
+
const response = await fetch("/__sidecar/dev/rpc", {
|
|
5661
|
+
method: "POST",
|
|
5662
|
+
headers: { "content-type": "application/json" },
|
|
5663
|
+
body: JSON.stringify({ method: "tools/list", params: {}, authToken: token }),
|
|
5664
|
+
});
|
|
5665
|
+
const json = await response.json().catch(() => ({}));
|
|
5666
|
+
if (!response.ok) {
|
|
5667
|
+
throw new Error(json.message || json.error || "Bearer token test failed.");
|
|
5066
5668
|
}
|
|
5067
|
-
return {
|
|
5068
|
-
jsonrpc: JSONRPC_VERSION2,
|
|
5069
|
-
id: typeof record.id === "string" || typeof record.id === "number" || record.id === null ? record.id : void 0,
|
|
5070
|
-
method: record.method,
|
|
5071
|
-
params: record.params
|
|
5072
|
-
};
|
|
5073
5669
|
}
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
}
|
|
5078
|
-
const record = value;
|
|
5079
|
-
if (record.jsonrpc !== JSONRPC_VERSION2 || !("id" in record) || !("result" in record || "error" in record)) {
|
|
5080
|
-
return false;
|
|
5081
|
-
}
|
|
5082
|
-
return record.id === null || typeof record.id === "string" || typeof record.id === "number";
|
|
5670
|
+
|
|
5671
|
+
function currentAuthToken() {
|
|
5672
|
+
return bearerToken.trim() || undefined;
|
|
5083
5673
|
}
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5674
|
+
|
|
5675
|
+
function updateAuthTrigger() {
|
|
5676
|
+
authTriggerEl.dataset.tokenSet = String(Boolean(currentAuthToken()));
|
|
5677
|
+
authTriggerEl.title = currentAuthToken() ? "Bearer token is set" : "Set bearer token";
|
|
5678
|
+
}
|
|
5679
|
+
|
|
5680
|
+
async function setState(next) {
|
|
5681
|
+
await fetch("/__sidecar/dev/state", {
|
|
5682
|
+
method: "POST",
|
|
5683
|
+
headers: { "content-type": "application/json" },
|
|
5684
|
+
body: JSON.stringify(next),
|
|
5685
|
+
});
|
|
5686
|
+
}
|
|
5687
|
+
|
|
5688
|
+
function applyState(next) {
|
|
5689
|
+
state.host = next.host || state.host;
|
|
5690
|
+
state.theme = next.theme || state.theme;
|
|
5691
|
+
state.device = next.device || state.device;
|
|
5692
|
+
document.documentElement.dataset.sidecarHost = state.host;
|
|
5693
|
+
document.documentElement.dataset.sidecarTheme = state.theme;
|
|
5694
|
+
document.documentElement.dataset.sidecarDevice = state.device;
|
|
5695
|
+
document.querySelectorAll("[data-host]").forEach((button) => {
|
|
5696
|
+
button.setAttribute("aria-pressed", String(button.dataset.host === state.host));
|
|
5697
|
+
});
|
|
5698
|
+
document.querySelectorAll("[data-theme]").forEach((button) => {
|
|
5699
|
+
button.setAttribute("aria-pressed", String(button.dataset.theme === state.theme));
|
|
5700
|
+
});
|
|
5701
|
+
document.querySelectorAll("[data-device]").forEach((button) => {
|
|
5702
|
+
button.setAttribute("aria-pressed", String(button.dataset.device === state.device));
|
|
5703
|
+
});
|
|
5704
|
+
syncSegmentedControl("[data-host]", state.host, "host");
|
|
5705
|
+
syncSegmentedControl("[data-theme]", state.theme, "theme");
|
|
5706
|
+
syncSegmentedControl("[data-device]", state.device, "device");
|
|
5707
|
+
surfaceTitleEl.textContent = (state.host === "generic" ? "MCP" : state.host === "chatgpt" ? "ChatGPT" : "Claude") + " preview";
|
|
5708
|
+
surfaceMetaEl.textContent = state.device === "mobile" ? "Mobile" : "Desktop";
|
|
5709
|
+
for (const source of frameContexts.keys()) {
|
|
5710
|
+
notify(source, "ui/notifications/host-context-changed", hostContext());
|
|
5091
5711
|
}
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5712
|
+
}
|
|
5713
|
+
|
|
5714
|
+
function syncSegmentedControl(selector, value, datasetKey) {
|
|
5715
|
+
const buttons = [...document.querySelectorAll(selector)];
|
|
5716
|
+
const segmented = buttons[0]?.closest(".segmented");
|
|
5717
|
+
if (!segmented) {
|
|
5718
|
+
return;
|
|
5097
5719
|
}
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5720
|
+
const activeIndex = Math.max(0, buttons.findIndex((button) => button.dataset[datasetKey] === value));
|
|
5721
|
+
segmented.style.setProperty("--segments", String(buttons.length));
|
|
5722
|
+
segmented.style.setProperty("--active-index", String(activeIndex));
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5725
|
+
function sendFrameContext(source) {
|
|
5726
|
+
const context = frameContexts.get(source);
|
|
5727
|
+
if (!context) {
|
|
5728
|
+
return;
|
|
5103
5729
|
}
|
|
5730
|
+
notify(source, "ui/notifications/host-context-changed", hostContext());
|
|
5731
|
+
notify(source, "ui/notifications/tool-input", { arguments: context.arguments || {} });
|
|
5732
|
+
notify(source, "ui/notifications/tool-result", context.result);
|
|
5733
|
+
}
|
|
5734
|
+
|
|
5735
|
+
function hostContext() {
|
|
5104
5736
|
return {
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5737
|
+
theme: state.theme,
|
|
5738
|
+
device: state.device,
|
|
5739
|
+
userAgent: "Sidecar Dev " + state.host,
|
|
5740
|
+
displayMode: "inline",
|
|
5741
|
+
availableDisplayModes: ["inline", "fullscreen", "pip"],
|
|
5742
|
+
platform: "web",
|
|
5743
|
+
styles: { variables: hostVariables() },
|
|
5108
5744
|
};
|
|
5109
5745
|
}
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
};
|
|
5746
|
+
|
|
5747
|
+
function hostVariables() {
|
|
5748
|
+
if (state.host === "claude") {
|
|
5749
|
+
return state.theme === "dark"
|
|
5750
|
+
? { "--font-sans": '"Anthropic Sans", ui-sans-serif, system-ui, sans-serif', "--color-background-primary": "#30302e", "--color-text-primary": "#faf9f5" }
|
|
5751
|
+
: { "--font-sans": '"Anthropic Sans", ui-sans-serif, system-ui, sans-serif', "--color-background-primary": "#ffffff", "--color-text-primary": "#141413" };
|
|
5117
5752
|
}
|
|
5753
|
+
return {};
|
|
5754
|
+
}
|
|
5755
|
+
|
|
5756
|
+
function hostCapabilities() {
|
|
5118
5757
|
return {
|
|
5119
|
-
|
|
5120
|
-
|
|
5758
|
+
serverTools: {},
|
|
5759
|
+
serverResources: {},
|
|
5760
|
+
openLinks: {},
|
|
5761
|
+
logging: {},
|
|
5762
|
+
message: { text: {}, structuredContent: {} },
|
|
5763
|
+
updateModelContext: { structuredContent: {} },
|
|
5764
|
+
downloadFile: {},
|
|
5121
5765
|
};
|
|
5122
5766
|
}
|
|
5123
5767
|
|
|
5768
|
+
function respond(source, id, result) {
|
|
5769
|
+
if (id === undefined) return;
|
|
5770
|
+
source?.postMessage({ jsonrpc: "2.0", id, result }, "*");
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5773
|
+
function respondError(source, id, code, message) {
|
|
5774
|
+
source?.postMessage({ jsonrpc: "2.0", id, error: { code, message } }, "*");
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5777
|
+
function notify(source, method, params) {
|
|
5778
|
+
source?.postMessage({ jsonrpc: "2.0", method, params }, "*");
|
|
5779
|
+
}
|
|
5780
|
+
|
|
5781
|
+
function setAuthCookie(token) {
|
|
5782
|
+
document.cookie = "sidecar_dev_bearer=" + encodeURIComponent(token || "") + "; path=/; SameSite=Lax";
|
|
5783
|
+
}
|
|
5784
|
+
|
|
5785
|
+
function clearEmpty() {
|
|
5786
|
+
const empty = messagesEl.querySelector(".empty");
|
|
5787
|
+
empty?.remove();
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
applyState(state);
|
|
5791
|
+
`;
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5124
5794
|
// packages/cli/src/tunnel.ts
|
|
5125
5795
|
import { spawn, spawnSync } from "child_process";
|
|
5126
5796
|
import { createInterface } from "readline/promises";
|
|
@@ -5128,11 +5798,11 @@ import { stdin, stdout } from "process";
|
|
|
5128
5798
|
async function startTunnel(options) {
|
|
5129
5799
|
const provider = await resolveProvider(options.provider);
|
|
5130
5800
|
const localUrl = `http://127.0.0.1:${options.port}`;
|
|
5131
|
-
const
|
|
5801
|
+
const path22 = options.path ?? "/mcp";
|
|
5132
5802
|
if (provider === "cloudflared") {
|
|
5133
|
-
return startCloudflared(localUrl,
|
|
5803
|
+
return startCloudflared(localUrl, path22, options.timeoutMs);
|
|
5134
5804
|
}
|
|
5135
|
-
return startWrangler(localUrl,
|
|
5805
|
+
return startWrangler(localUrl, path22, options.timeoutMs);
|
|
5136
5806
|
}
|
|
5137
5807
|
async function validateTunnelEndpoint(options) {
|
|
5138
5808
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
@@ -5256,7 +5926,7 @@ function assertNpxAvailable() {
|
|
|
5256
5926
|
throw new Error(tunnelInstallMessage("wrangler"));
|
|
5257
5927
|
}
|
|
5258
5928
|
}
|
|
5259
|
-
async function startCloudflared(localUrl,
|
|
5929
|
+
async function startCloudflared(localUrl, path22, timeoutMs = 2e4) {
|
|
5260
5930
|
const child = spawn("cloudflared", ["tunnel", "--url", localUrl, "--no-autoupdate"], {
|
|
5261
5931
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5262
5932
|
});
|
|
@@ -5264,13 +5934,13 @@ async function startCloudflared(localUrl, path21, timeoutMs = 2e4) {
|
|
|
5264
5934
|
return {
|
|
5265
5935
|
provider: "cloudflared",
|
|
5266
5936
|
publicUrl,
|
|
5267
|
-
mcpUrl: appendPath(publicUrl,
|
|
5937
|
+
mcpUrl: appendPath(publicUrl, path22),
|
|
5268
5938
|
close() {
|
|
5269
5939
|
child.kill("SIGTERM");
|
|
5270
5940
|
}
|
|
5271
5941
|
};
|
|
5272
5942
|
}
|
|
5273
|
-
async function startWrangler(localUrl,
|
|
5943
|
+
async function startWrangler(localUrl, path22, timeoutMs = 2e4) {
|
|
5274
5944
|
const child = spawn("npx", ["--yes", "wrangler", "tunnel", "quick-start", localUrl], {
|
|
5275
5945
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5276
5946
|
});
|
|
@@ -5278,7 +5948,7 @@ async function startWrangler(localUrl, path21, timeoutMs = 2e4) {
|
|
|
5278
5948
|
return {
|
|
5279
5949
|
provider: "wrangler",
|
|
5280
5950
|
publicUrl,
|
|
5281
|
-
mcpUrl: appendPath(publicUrl,
|
|
5951
|
+
mcpUrl: appendPath(publicUrl, path22),
|
|
5282
5952
|
close() {
|
|
5283
5953
|
child.kill("SIGTERM");
|
|
5284
5954
|
}
|
|
@@ -5539,8 +6209,8 @@ function runInherited(command, args) {
|
|
|
5539
6209
|
});
|
|
5540
6210
|
});
|
|
5541
6211
|
}
|
|
5542
|
-
function appendPath(origin,
|
|
5543
|
-
return `${origin.replace(/\/+$/, "")}/${
|
|
6212
|
+
function appendPath(origin, path22) {
|
|
6213
|
+
return `${origin.replace(/\/+$/, "")}/${path22.replace(/^\/+/, "")}`;
|
|
5544
6214
|
}
|
|
5545
6215
|
function isRecord3(value) {
|
|
5546
6216
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -5568,10 +6238,17 @@ async function main(argv) {
|
|
|
5568
6238
|
);
|
|
5569
6239
|
console.log(`Host runtime: ${manifest.host}`);
|
|
5570
6240
|
if (manifest.host === "vercel") {
|
|
5571
|
-
console.log(`Vercel MCP function: ${
|
|
6241
|
+
console.log(`Vercel MCP function: ${path21.join(resolvedOutDir, VERCEL_ENTRYPOINT)}`);
|
|
5572
6242
|
} else {
|
|
5573
|
-
console.log(`Hostable MCP server: ${
|
|
6243
|
+
console.log(`Hostable MCP server: ${path21.join(resolvedOutDir, SERVER_ENTRYPOINT)}`);
|
|
5574
6244
|
}
|
|
6245
|
+
console.log(renderBuildUrlSummary({
|
|
6246
|
+
host: manifest.host,
|
|
6247
|
+
mcpPath: process.env.SIDECAR_MCP_PATH,
|
|
6248
|
+
publicMcpUrl: process.env.SIDECAR_MCP_URL,
|
|
6249
|
+
publicUrl: process.env.SIDECAR_PUBLIC_URL,
|
|
6250
|
+
port: process.env.SIDECAR_PORT ?? process.env.PORT
|
|
6251
|
+
}));
|
|
5575
6252
|
if (plugins ?? manifest.config.build.plugins) {
|
|
5576
6253
|
console.log("Built claude-plugin package.");
|
|
5577
6254
|
}
|
|
@@ -5599,70 +6276,69 @@ async function main(argv) {
|
|
|
5599
6276
|
return;
|
|
5600
6277
|
}
|
|
5601
6278
|
case "dev": {
|
|
5602
|
-
|
|
6279
|
+
await loadProjectEnv(rootDir);
|
|
6280
|
+
const port = Number(readOption(argv, "--mcp-port") ?? readOption(argv, "--port") ?? "3101");
|
|
6281
|
+
const harnessPort = Number(readOption(argv, "--harness-port") ?? "3000");
|
|
5603
6282
|
const target = readTarget(argv);
|
|
6283
|
+
const devHost = readDevHost(argv);
|
|
6284
|
+
const devTheme = readDevTheme(argv);
|
|
6285
|
+
const devDevice = readDevDevice(argv);
|
|
6286
|
+
const model = readOption(argv, "--model") ?? process.env.SIDECAR_DEV_MODEL ?? "gpt-4.1-mini";
|
|
5604
6287
|
const tunnelProvider = readTunnelProvider(argv);
|
|
5605
6288
|
const outDir = `.sidecar/dev/${target}`;
|
|
5606
|
-
const
|
|
5607
|
-
|
|
5608
|
-
const
|
|
5609
|
-
const runtimeToolEntries = await analyzeProjectTools(rootDir, { target });
|
|
5610
|
-
const runtimeTools = await loadRuntimeTools(rootDir, runtimeToolEntries);
|
|
5611
|
-
const manifest = await buildProject({ rootDir, outDir, target });
|
|
6289
|
+
const localMcpUrl = `http://127.0.0.1:${port}/mcp`;
|
|
6290
|
+
process.env.SIDECAR_MCP_URL = localMcpUrl;
|
|
6291
|
+
const manifest = await buildProject({ rootDir, outDir, target, host: "node" });
|
|
5612
6292
|
printDiagnostics(manifest.diagnostics ?? []);
|
|
5613
|
-
const tools = attachBuiltToolDescriptors(runtimeTools, manifest);
|
|
5614
6293
|
let tunnel;
|
|
5615
6294
|
if (tunnelProvider) {
|
|
5616
6295
|
tunnel = await startTunnel({ provider: tunnelProvider, port, path: "/mcp" });
|
|
5617
6296
|
process.env.SIDECAR_MCP_URL = tunnel.mcpUrl;
|
|
5618
6297
|
}
|
|
5619
|
-
const
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
const server = createSidecarHttpServer({
|
|
5623
|
-
name: "sidecar-dev",
|
|
5624
|
-
version: "0.0.0-dev",
|
|
5625
|
-
path: "/mcp",
|
|
5626
|
-
auth,
|
|
5627
|
-
proxy,
|
|
5628
|
-
tools,
|
|
5629
|
-
resources,
|
|
5630
|
-
prompts,
|
|
5631
|
-
capabilities: {
|
|
5632
|
-
tools: runtimeConfig?.tools ?? manifest.config.tools,
|
|
5633
|
-
resources: runtimeConfig?.resources ?? manifest.config.resources,
|
|
5634
|
-
prompts: runtimeConfig?.prompts ?? manifest.config.prompts
|
|
5635
|
-
},
|
|
5636
|
-
pagination: runtimeConfig?.pagination ?? {
|
|
5637
|
-
pageSize: manifest.config.pagination.pageSize
|
|
5638
|
-
}
|
|
5639
|
-
});
|
|
5640
|
-
let listening = false;
|
|
6298
|
+
const runtimeMcpUrl = tunnel?.mcpUrl ?? localMcpUrl;
|
|
6299
|
+
let mcpProcess;
|
|
6300
|
+
let harness;
|
|
5641
6301
|
try {
|
|
5642
|
-
await
|
|
5643
|
-
|
|
6302
|
+
mcpProcess = await startBuiltMcpServer({
|
|
6303
|
+
rootDir,
|
|
6304
|
+
outDir,
|
|
6305
|
+
port,
|
|
6306
|
+
mcpUrl: runtimeMcpUrl
|
|
6307
|
+
});
|
|
5644
6308
|
if (tunnel) {
|
|
5645
6309
|
await validateTunnelEndpoint({
|
|
5646
6310
|
mcpUrl: tunnel.mcpUrl,
|
|
5647
|
-
auth:
|
|
6311
|
+
auth: existsSync8(path21.join(rootDir, "auth.ts"))
|
|
5648
6312
|
});
|
|
5649
6313
|
}
|
|
5650
|
-
console.log(`MCP running on Streamable HTTP (${target})
|
|
5651
|
-
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"}.`);
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
6314
|
+
console.log(`MCP running on Streamable HTTP (${target}).`);
|
|
6315
|
+
console.log(`Loaded ${manifest.tools.length} 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"}.`);
|
|
6316
|
+
harness = await startDevHarness({
|
|
6317
|
+
rootDir,
|
|
6318
|
+
mcpUrl: localMcpUrl,
|
|
6319
|
+
host: devHost,
|
|
6320
|
+
theme: devTheme,
|
|
6321
|
+
device: devDevice,
|
|
6322
|
+
target,
|
|
6323
|
+
port: harnessPort,
|
|
6324
|
+
model,
|
|
6325
|
+
initialBearerToken: process.env.MCP_BEARER
|
|
6326
|
+
});
|
|
6327
|
+
console.log(renderDevUrlSummary({
|
|
6328
|
+
localMcpUrl,
|
|
6329
|
+
runtimeMcpUrl,
|
|
6330
|
+
tunnelProvider: tunnel?.provider,
|
|
6331
|
+
harnessUrl: harness.url
|
|
6332
|
+
}));
|
|
5656
6333
|
} catch (error) {
|
|
5657
6334
|
tunnel?.close();
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
}
|
|
6335
|
+
await harness?.close();
|
|
6336
|
+
await stopBuiltMcpServer(mcpProcess);
|
|
5661
6337
|
throw error;
|
|
5662
6338
|
}
|
|
5663
6339
|
const shutdown = () => {
|
|
5664
6340
|
tunnel?.close();
|
|
5665
|
-
|
|
6341
|
+
void harness?.close().then(() => stopBuiltMcpServer(mcpProcess)).finally(() => exit(0));
|
|
5666
6342
|
};
|
|
5667
6343
|
process.once("SIGINT", shutdown);
|
|
5668
6344
|
process.once("SIGTERM", shutdown);
|
|
@@ -5683,9 +6359,13 @@ async function main(argv) {
|
|
|
5683
6359
|
return;
|
|
5684
6360
|
}
|
|
5685
6361
|
case "preview": {
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
6362
|
+
await previewProject({
|
|
6363
|
+
rootDir,
|
|
6364
|
+
targets: readPreviewTargets(argv)
|
|
6365
|
+
});
|
|
6366
|
+
return;
|
|
6367
|
+
}
|
|
6368
|
+
case "component-preview": {
|
|
5689
6369
|
await previewComponents({
|
|
5690
6370
|
rootDir,
|
|
5691
6371
|
host: readOption(argv, "--host") ?? "chatgpt",
|
|
@@ -5704,9 +6384,10 @@ async function main(argv) {
|
|
|
5704
6384
|
Usage:
|
|
5705
6385
|
sidecar build [--cwd <dir>] [--target mcp|chatgpt|claude] [--host node|vercel] [--out <dir>] [--plugins|--no-plugins] [--strict]
|
|
5706
6386
|
sidecar check [--cwd <dir>] [--target mcp|chatgpt|claude] [--strict]
|
|
5707
|
-
sidecar dev [--cwd <dir>] [--target mcp|chatgpt|claude] [--port <port>] [--tunnel [cloudflared|wrangler]]
|
|
6387
|
+
sidecar dev [--cwd <dir>] [--target mcp|chatgpt|claude] [--port <mcp-port>] [--mcp-port <port>] [--harness-port <port>] [--host chatgpt|claude|generic] [--theme light|dark] [--device desktop|mobile] [--model <model>] [--tunnel [cloudflared|wrangler]]
|
|
5708
6388
|
sidecar inspect [--cwd <dir>] [--target mcp|chatgpt|claude]
|
|
5709
|
-
sidecar preview
|
|
6389
|
+
sidecar preview [--cwd <dir>] [--target mcp|chatgpt|claude]
|
|
6390
|
+
sidecar component-preview [--cwd <dir>] [--host chatgpt|claude|generic] [--compare native,openai] [--components representative|all] [--theme light|dark|both] [--port <port>] [--no-approve]
|
|
5710
6391
|
`);
|
|
5711
6392
|
}
|
|
5712
6393
|
}
|
|
@@ -5740,12 +6421,59 @@ function readOptionalHost(argv) {
|
|
|
5740
6421
|
}
|
|
5741
6422
|
throw new Error(`Unsupported Sidecar host "${host}". Expected node or vercel.`);
|
|
5742
6423
|
}
|
|
6424
|
+
function readDevHost(argv) {
|
|
6425
|
+
const host = readOption(argv, "--host") ?? "chatgpt";
|
|
6426
|
+
if (host === "chatgpt" || host === "claude" || host === "generic") {
|
|
6427
|
+
return host;
|
|
6428
|
+
}
|
|
6429
|
+
throw new Error(`Unsupported dev host "${host}". Expected chatgpt, claude, or generic.`);
|
|
6430
|
+
}
|
|
6431
|
+
function readDevTheme(argv) {
|
|
6432
|
+
const theme = readOption(argv, "--theme") ?? "light";
|
|
6433
|
+
if (theme === "light" || theme === "dark") {
|
|
6434
|
+
return theme;
|
|
6435
|
+
}
|
|
6436
|
+
throw new Error(`Unsupported dev theme "${theme}". Expected light or dark.`);
|
|
6437
|
+
}
|
|
6438
|
+
function readDevDevice(argv) {
|
|
6439
|
+
const device = readOption(argv, "--device") ?? "desktop";
|
|
6440
|
+
if (device === "desktop" || device === "mobile") {
|
|
6441
|
+
return device;
|
|
6442
|
+
}
|
|
6443
|
+
throw new Error(`Unsupported dev device "${device}". Expected desktop or mobile.`);
|
|
6444
|
+
}
|
|
5743
6445
|
function detectHostFromEnvironment() {
|
|
5744
6446
|
if (process.env.VERCEL === "1") {
|
|
5745
6447
|
return "vercel";
|
|
5746
6448
|
}
|
|
5747
6449
|
return void 0;
|
|
5748
6450
|
}
|
|
6451
|
+
async function loadProjectEnv(rootDir) {
|
|
6452
|
+
for (const filename of [".env", ".env.local", ".envb"]) {
|
|
6453
|
+
const text = await readFile8(path21.join(rootDir, filename), "utf8").catch(() => void 0);
|
|
6454
|
+
if (!text) {
|
|
6455
|
+
continue;
|
|
6456
|
+
}
|
|
6457
|
+
for (const line of text.split(/\r?\n/)) {
|
|
6458
|
+
const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)?\s*$/);
|
|
6459
|
+
if (!match) {
|
|
6460
|
+
continue;
|
|
6461
|
+
}
|
|
6462
|
+
const [, key, rawValue = ""] = match;
|
|
6463
|
+
if (!key || process.env[key] !== void 0) {
|
|
6464
|
+
continue;
|
|
6465
|
+
}
|
|
6466
|
+
process.env[key] = parseEnvValue(rawValue);
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
function parseEnvValue(value) {
|
|
6471
|
+
const trimmed = value.trim();
|
|
6472
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
6473
|
+
return trimmed.slice(1, -1).replace(/\\n/g, "\n");
|
|
6474
|
+
}
|
|
6475
|
+
return trimmed.replace(/\s+#.*$/, "");
|
|
6476
|
+
}
|
|
5749
6477
|
function readOptionalPlugins(argv) {
|
|
5750
6478
|
if (argv.includes("--plugins")) {
|
|
5751
6479
|
return true;
|
|
@@ -5755,9 +6483,87 @@ function readOptionalPlugins(argv) {
|
|
|
5755
6483
|
}
|
|
5756
6484
|
return void 0;
|
|
5757
6485
|
}
|
|
6486
|
+
function renderBuildUrlSummary(options) {
|
|
6487
|
+
const mcpPath = options.mcpPath ?? "/mcp";
|
|
6488
|
+
const lines = ["Sidecar URLs:"];
|
|
6489
|
+
if (options.host === "vercel") {
|
|
6490
|
+
lines.push(` Vercel MCP route: https://<project>.vercel.app${mcpPath}`);
|
|
6491
|
+
} else {
|
|
6492
|
+
lines.push(` Local Node MCP: http://127.0.0.1:${options.port ?? "3101"}${mcpPath}`);
|
|
6493
|
+
}
|
|
6494
|
+
lines.push(` Public MCP: ${options.publicMcpUrl ?? `set SIDECAR_MCP_URL=https://your-host.example.com${mcpPath}`}`);
|
|
6495
|
+
lines.push(` Public base: ${options.publicUrl ?? "set SIDECAR_PUBLIC_URL=https://your-host.example.com when callbacks need a base URL"}`);
|
|
6496
|
+
return lines.join("\n");
|
|
6497
|
+
}
|
|
6498
|
+
function renderDevUrlSummary(options) {
|
|
6499
|
+
const stateUrl = `${options.harnessUrl}/__sidecar/dev/state`;
|
|
6500
|
+
const lines = [
|
|
6501
|
+
"Sidecar URLs:",
|
|
6502
|
+
` Local MCP: ${options.localMcpUrl}`,
|
|
6503
|
+
` Dev harness: ${options.harnessUrl}`,
|
|
6504
|
+
` Dev harness state: ${stateUrl}`
|
|
6505
|
+
];
|
|
6506
|
+
if (options.runtimeMcpUrl !== options.localMcpUrl) {
|
|
6507
|
+
lines.push(` Public MCP (${options.tunnelProvider ?? "tunnel"}): ${options.runtimeMcpUrl}`);
|
|
6508
|
+
lines.push(` ChatGPT/Claude connector URL: ${options.runtimeMcpUrl}`);
|
|
6509
|
+
} else {
|
|
6510
|
+
lines.push(` Harness connector URL: ${options.localMcpUrl}`);
|
|
6511
|
+
}
|
|
6512
|
+
return lines.join("\n");
|
|
6513
|
+
}
|
|
6514
|
+
async function previewProject(options) {
|
|
6515
|
+
const builds = [];
|
|
6516
|
+
for (const target of options.targets) {
|
|
6517
|
+
const outDir = `.sidecar/preview/${target}`;
|
|
6518
|
+
const manifest = await buildProject({ rootDir: options.rootDir, outDir, target });
|
|
6519
|
+
printDiagnostics(manifest.diagnostics ?? []);
|
|
6520
|
+
builds.push({ target, outDir, manifest });
|
|
6521
|
+
}
|
|
6522
|
+
const server = createServer3(async (request, response) => {
|
|
6523
|
+
try {
|
|
6524
|
+
const url2 = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
6525
|
+
if (url2.pathname === "/") {
|
|
6526
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
6527
|
+
response.end(renderProjectPreviewHtml(builds));
|
|
6528
|
+
return;
|
|
6529
|
+
}
|
|
6530
|
+
if (url2.pathname === "/widget") {
|
|
6531
|
+
const target = url2.searchParams.get("target");
|
|
6532
|
+
const tool = url2.searchParams.get("tool");
|
|
6533
|
+
const theme = url2.searchParams.get("theme") === "dark" ? "dark" : "light";
|
|
6534
|
+
const build = builds.find((entry2) => entry2.target === target);
|
|
6535
|
+
const entry = build?.manifest.tools.find((item) => item.id === tool);
|
|
6536
|
+
if (!build || !entry?.widget?.outputFile) {
|
|
6537
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
6538
|
+
response.end("Widget not found.");
|
|
6539
|
+
return;
|
|
6540
|
+
}
|
|
6541
|
+
const html = await readFile8(path21.join(options.rootDir, build.outDir, entry.widget.outputFile), "utf8");
|
|
6542
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
6543
|
+
response.end(injectProjectPreviewBridge(html, entry, build.target, theme));
|
|
6544
|
+
return;
|
|
6545
|
+
}
|
|
6546
|
+
response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
6547
|
+
response.end("Not found.");
|
|
6548
|
+
} catch (error) {
|
|
6549
|
+
response.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
|
|
6550
|
+
response.end(error instanceof Error ? error.message : String(error));
|
|
6551
|
+
}
|
|
6552
|
+
});
|
|
6553
|
+
const port = await listenOnAnyPort(server);
|
|
6554
|
+
const url = `http://127.0.0.1:${port}`;
|
|
6555
|
+
console.log(`Sidecar preview running at ${url}`);
|
|
6556
|
+
console.log(`Targets: ${options.targets.join(", ")}`);
|
|
6557
|
+
console.log("Press Ctrl+C to stop.");
|
|
6558
|
+
const shutdown = () => {
|
|
6559
|
+
server.close(() => exit(0));
|
|
6560
|
+
};
|
|
6561
|
+
process.once("SIGINT", shutdown);
|
|
6562
|
+
process.once("SIGTERM", shutdown);
|
|
6563
|
+
}
|
|
5758
6564
|
async function previewComponents(options) {
|
|
5759
|
-
const cliDir =
|
|
5760
|
-
const css = await readFile8(
|
|
6565
|
+
const cliDir = path21.dirname(fileURLToPath2(import.meta.url));
|
|
6566
|
+
const css = await readFile8(path21.join(cliDir, "../../native/src/styles.css"), "utf8").catch(() => readFile8(path21.join(process.cwd(), "packages/native/src/styles.css"), "utf8")).catch(() => "");
|
|
5761
6567
|
const html = renderComponentPreviewHtml(
|
|
5762
6568
|
options.host,
|
|
5763
6569
|
options.compare,
|
|
@@ -5765,7 +6571,7 @@ async function previewComponents(options) {
|
|
|
5765
6571
|
options.themes,
|
|
5766
6572
|
options.componentSet
|
|
5767
6573
|
);
|
|
5768
|
-
const server =
|
|
6574
|
+
const server = createServer3((_request, response) => {
|
|
5769
6575
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
5770
6576
|
response.end(html);
|
|
5771
6577
|
});
|
|
@@ -5796,11 +6602,229 @@ async function previewComponents(options) {
|
|
|
5796
6602
|
}
|
|
5797
6603
|
server.close();
|
|
5798
6604
|
}
|
|
6605
|
+
function renderProjectPreviewHtml(builds) {
|
|
6606
|
+
const themes = ["light", "dark"];
|
|
6607
|
+
const sections = themes.map((theme) => renderProjectPreviewTheme(builds, theme)).join("");
|
|
6608
|
+
const toolCount = new Set(
|
|
6609
|
+
builds.flatMap((build) => build.manifest.tools.filter((tool) => tool.widget).map((tool) => tool.id))
|
|
6610
|
+
).size;
|
|
6611
|
+
return `<!doctype html>
|
|
6612
|
+
<html lang="en">
|
|
6613
|
+
<head>
|
|
6614
|
+
<meta charset="utf-8" />
|
|
6615
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6616
|
+
<title>Sidecar preview</title>
|
|
6617
|
+
<style>
|
|
6618
|
+
:root { color-scheme: light dark; }
|
|
6619
|
+
* { box-sizing: border-box; }
|
|
6620
|
+
body { background: #f4f4f5; color: #18181b; font: 14px/1.45 ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; }
|
|
6621
|
+
header { align-items: baseline; background: rgb(255 255 255 / 92%); border-bottom: 1px solid rgb(0 0 0 / 10%); display: flex; gap: 14px; padding: 14px 18px; position: sticky; top: 0; z-index: 1; }
|
|
6622
|
+
h1 { font-size: 18px; margin: 0; }
|
|
6623
|
+
.summary { color: #5f5f66; }
|
|
6624
|
+
.theme-section { display: grid; gap: 12px; padding: 18px; }
|
|
6625
|
+
.theme-title { color: #3f3f46; font-size: 12px; font-weight: 800; letter-spacing: .05em; margin: 0; text-transform: uppercase; }
|
|
6626
|
+
.target-grid { align-items: start; display: grid; gap: 14px; grid-template-columns: repeat(${Math.max(builds.length, 1)}, minmax(320px, 1fr)); overflow-x: auto; padding-bottom: 8px; }
|
|
6627
|
+
.target-column { background: rgb(255 255 255 / 72%); border: 1px solid rgb(0 0 0 / 10%); border-radius: 10px; display: grid; gap: 12px; min-width: 320px; padding: 12px; }
|
|
6628
|
+
.target-title { font-size: 13px; font-weight: 800; margin: 0; text-transform: uppercase; }
|
|
6629
|
+
.widget-card { border: 1px solid rgb(0 0 0 / 10%); border-radius: 9px; display: grid; gap: 8px; overflow: hidden; }
|
|
6630
|
+
.widget-label { align-items: baseline; background: rgb(250 250 250 / 92%); border-bottom: 1px solid rgb(0 0 0 / 8%); display: flex; justify-content: space-between; padding: 8px 10px; }
|
|
6631
|
+
.widget-name { font-weight: 700; }
|
|
6632
|
+
.widget-id { color: #71717a; font: 12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
6633
|
+
iframe { background: transparent; border: 0; height: 360px; width: 100%; }
|
|
6634
|
+
.empty { border: 1px dashed rgb(0 0 0 / 18%); border-radius: 9px; color: #71717a; padding: 18px; }
|
|
6635
|
+
.theme-section[data-theme="light"] { background: #f4f4f5; color: #18181b; }
|
|
6636
|
+
.theme-section[data-theme="dark"] { background: #111113; color: #f4f4f5; }
|
|
6637
|
+
.theme-section[data-theme="dark"] .theme-title { color: #d4d4d8; }
|
|
6638
|
+
.theme-section[data-theme="dark"] .target-column,
|
|
6639
|
+
.theme-section[data-theme="dark"] .widget-card { background: rgb(24 24 27 / 72%); border-color: rgb(255 255 255 / 12%); }
|
|
6640
|
+
.theme-section[data-theme="dark"] .widget-label { background: rgb(39 39 42 / 72%); border-bottom-color: rgb(255 255 255 / 10%); }
|
|
6641
|
+
.theme-section[data-theme="dark"] .widget-id,
|
|
6642
|
+
.theme-section[data-theme="dark"] .empty { color: #a1a1aa; }
|
|
6643
|
+
</style>
|
|
6644
|
+
</head>
|
|
6645
|
+
<body>
|
|
6646
|
+
<header>
|
|
6647
|
+
<h1>Sidecar preview</h1>
|
|
6648
|
+
<span class="summary">${toolCount} widget${toolCount === 1 ? "" : "s"} across ${builds.length} target${builds.length === 1 ? "" : "s"}</span>
|
|
6649
|
+
</header>
|
|
6650
|
+
${sections}
|
|
6651
|
+
</body>
|
|
6652
|
+
</html>`;
|
|
6653
|
+
}
|
|
6654
|
+
function renderProjectPreviewTheme(builds, theme) {
|
|
6655
|
+
const columns = builds.map((build) => renderProjectPreviewTarget(build, theme)).join("");
|
|
6656
|
+
return `<section class="theme-section" data-theme="${theme}">
|
|
6657
|
+
<h2 class="theme-title">${escapeHtml2(theme)}</h2>
|
|
6658
|
+
<div class="target-grid">${columns}</div>
|
|
6659
|
+
</section>`;
|
|
6660
|
+
}
|
|
6661
|
+
function renderProjectPreviewTarget(build, theme) {
|
|
6662
|
+
const widgets = build.manifest.tools.filter((tool) => tool.widget?.outputFile).map((tool) => renderProjectPreviewWidget(build.target, tool, theme)).join("");
|
|
6663
|
+
return `<section class="target-column">
|
|
6664
|
+
<h3 class="target-title">${escapeHtml2(previewTargetLabel(build.target))}</h3>
|
|
6665
|
+
${widgets || `<div class="empty">No widgets found for ${escapeHtml2(build.target)}.</div>`}
|
|
6666
|
+
</section>`;
|
|
6667
|
+
}
|
|
6668
|
+
function renderProjectPreviewWidget(target, tool, theme) {
|
|
6669
|
+
const src = `/widget?target=${encodeURIComponent(target)}&tool=${encodeURIComponent(tool.id)}&theme=${theme}`;
|
|
6670
|
+
return `<article class="widget-card">
|
|
6671
|
+
<div class="widget-label">
|
|
6672
|
+
<span class="widget-name">${escapeHtml2(tool.name)}</span>
|
|
6673
|
+
<span class="widget-id">${escapeHtml2(tool.id)}</span>
|
|
6674
|
+
</div>
|
|
6675
|
+
<iframe title="${escapeHtml2(`${tool.name} ${target} ${theme}`)}" src="${escapeHtml2(src)}"></iframe>
|
|
6676
|
+
</article>`;
|
|
6677
|
+
}
|
|
6678
|
+
function injectProjectPreviewBridge(html, tool, target, theme) {
|
|
6679
|
+
const host = previewHostName(target);
|
|
6680
|
+
const preview = {
|
|
6681
|
+
hostContext: {
|
|
6682
|
+
name: host,
|
|
6683
|
+
theme,
|
|
6684
|
+
source: "mcp-apps",
|
|
6685
|
+
raw: {
|
|
6686
|
+
theme,
|
|
6687
|
+
userAgent: `Sidecar Preview ${previewTargetLabel(target)}`,
|
|
6688
|
+
displayMode: "inline",
|
|
6689
|
+
availableDisplayModes: ["inline", "fullscreen", "pip"]
|
|
6690
|
+
}
|
|
6691
|
+
},
|
|
6692
|
+
hostCapabilities: {
|
|
6693
|
+
openLinks: {},
|
|
6694
|
+
serverTools: {},
|
|
6695
|
+
serverResources: {},
|
|
6696
|
+
logging: {},
|
|
6697
|
+
message: { text: {}, structuredContent: {} },
|
|
6698
|
+
updateModelContext: { structuredContent: {} }
|
|
6699
|
+
},
|
|
6700
|
+
toolInput: {
|
|
6701
|
+
arguments: previewToolArguments(tool)
|
|
6702
|
+
},
|
|
6703
|
+
toolResult: previewToolResult(tool)
|
|
6704
|
+
};
|
|
6705
|
+
const script = `<script>window.__sidecarPreview=${escapeScriptJson(preview)};</script>`;
|
|
6706
|
+
return html.replace('<html lang="en"', `<html lang="en" data-sidecar-host="${host}" data-sidecar-theme="${theme}" data-theme="${theme}"`).replace("</head>", `${script}
|
|
6707
|
+
</head>`);
|
|
6708
|
+
}
|
|
6709
|
+
function previewToolResult(tool) {
|
|
6710
|
+
const content = [
|
|
6711
|
+
`# ${tool.name}`,
|
|
6712
|
+
"",
|
|
6713
|
+
tool.description,
|
|
6714
|
+
"",
|
|
6715
|
+
"| Field | Value |",
|
|
6716
|
+
"| --- | --- |",
|
|
6717
|
+
`| Tool | ${escapeMarkdownTableCell(tool.id)} |`,
|
|
6718
|
+
"| Preview | Ready |"
|
|
6719
|
+
].join("\n");
|
|
6720
|
+
const structuredContent = {
|
|
6721
|
+
tool: tool.id,
|
|
6722
|
+
ok: true,
|
|
6723
|
+
text: content,
|
|
6724
|
+
preview: {
|
|
6725
|
+
kind: previewKind(tool),
|
|
6726
|
+
title: tool.name,
|
|
6727
|
+
summary: tool.description,
|
|
6728
|
+
content,
|
|
6729
|
+
items: [
|
|
6730
|
+
{
|
|
6731
|
+
title: `${tool.name} preview`,
|
|
6732
|
+
body: tool.description
|
|
6733
|
+
},
|
|
6734
|
+
{
|
|
6735
|
+
title: "Table rendering",
|
|
6736
|
+
body: "Preview data includes a Markdown table."
|
|
6737
|
+
}
|
|
6738
|
+
]
|
|
6739
|
+
},
|
|
6740
|
+
upstream: {
|
|
6741
|
+
isError: false
|
|
6742
|
+
}
|
|
6743
|
+
};
|
|
6744
|
+
return {
|
|
6745
|
+
structuredContent,
|
|
6746
|
+
content: [{ type: "text", text: tool.description }],
|
|
6747
|
+
_meta: {
|
|
6748
|
+
sidecarPreview: true,
|
|
6749
|
+
tool: tool.id
|
|
6750
|
+
},
|
|
6751
|
+
isError: false
|
|
6752
|
+
};
|
|
6753
|
+
}
|
|
6754
|
+
function previewToolArguments(tool) {
|
|
6755
|
+
const properties = tool.descriptor.inputSchema?.properties;
|
|
6756
|
+
if (!properties || typeof properties !== "object" || Array.isArray(properties)) {
|
|
6757
|
+
return {};
|
|
6758
|
+
}
|
|
6759
|
+
return Object.fromEntries(
|
|
6760
|
+
Object.entries(properties).map(([key, schema]) => [key, previewValueForSchema(schema)])
|
|
6761
|
+
);
|
|
6762
|
+
}
|
|
6763
|
+
function previewValueForSchema(schema) {
|
|
6764
|
+
if (!schema || typeof schema !== "object") {
|
|
6765
|
+
return "preview";
|
|
6766
|
+
}
|
|
6767
|
+
const record = schema;
|
|
6768
|
+
if (Array.isArray(record.enum) && record.enum.length) {
|
|
6769
|
+
return record.enum[0];
|
|
6770
|
+
}
|
|
6771
|
+
switch (record.type) {
|
|
6772
|
+
case "number":
|
|
6773
|
+
case "integer":
|
|
6774
|
+
return 1;
|
|
6775
|
+
case "boolean":
|
|
6776
|
+
return true;
|
|
6777
|
+
case "array":
|
|
6778
|
+
return [];
|
|
6779
|
+
case "object":
|
|
6780
|
+
return {};
|
|
6781
|
+
default:
|
|
6782
|
+
return "preview";
|
|
6783
|
+
}
|
|
6784
|
+
}
|
|
6785
|
+
function previewKind(tool) {
|
|
6786
|
+
const id = tool.id.toLowerCase();
|
|
6787
|
+
if (id.includes("search") || id.includes("query")) {
|
|
6788
|
+
return "search";
|
|
6789
|
+
}
|
|
6790
|
+
if (id.includes("create") || id.includes("update") || id.includes("move") || id.includes("duplicate")) {
|
|
6791
|
+
return "write";
|
|
6792
|
+
}
|
|
6793
|
+
if (id.includes("fetch") || id.includes("read") || id.includes("get")) {
|
|
6794
|
+
return "read";
|
|
6795
|
+
}
|
|
6796
|
+
return "metadata";
|
|
6797
|
+
}
|
|
6798
|
+
function previewHostName(target) {
|
|
6799
|
+
if (target === "chatgpt") {
|
|
6800
|
+
return "chatgpt";
|
|
6801
|
+
}
|
|
6802
|
+
if (target === "claude") {
|
|
6803
|
+
return "claude";
|
|
6804
|
+
}
|
|
6805
|
+
return "generic";
|
|
6806
|
+
}
|
|
6807
|
+
function previewTargetLabel(target) {
|
|
6808
|
+
switch (target) {
|
|
6809
|
+
case "chatgpt":
|
|
6810
|
+
return "ChatGPT";
|
|
6811
|
+
case "claude":
|
|
6812
|
+
return "Claude";
|
|
6813
|
+
default:
|
|
6814
|
+
return "MCP";
|
|
6815
|
+
}
|
|
6816
|
+
}
|
|
6817
|
+
function escapeScriptJson(value) {
|
|
6818
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
6819
|
+
}
|
|
6820
|
+
function escapeMarkdownTableCell(value) {
|
|
6821
|
+
return value.replace(/\|/g, "\\|");
|
|
6822
|
+
}
|
|
5799
6823
|
async function writeComponentApproval(rootDir, approval) {
|
|
5800
|
-
const dir =
|
|
6824
|
+
const dir = path21.join(rootDir, ".sidecar", "approvals");
|
|
5801
6825
|
await mkdir10(dir, { recursive: true });
|
|
5802
6826
|
await writeFile10(
|
|
5803
|
-
|
|
6827
|
+
path21.join(dir, `components.${approval.host}.json`),
|
|
5804
6828
|
`${JSON.stringify(approval, null, 2)}
|
|
5805
6829
|
`
|
|
5806
6830
|
);
|
|
@@ -6094,6 +7118,28 @@ function previewComponentNames(componentSet) {
|
|
|
6094
7118
|
"TextLink"
|
|
6095
7119
|
];
|
|
6096
7120
|
}
|
|
7121
|
+
function readPreviewTargets(argv) {
|
|
7122
|
+
const values = [];
|
|
7123
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
7124
|
+
if (argv[index] !== "--target") {
|
|
7125
|
+
continue;
|
|
7126
|
+
}
|
|
7127
|
+
const value = argv[index + 1];
|
|
7128
|
+
if (value) {
|
|
7129
|
+
values.push(...value.split(",").map((entry) => entry.trim()).filter(Boolean));
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
if (!values.length) {
|
|
7133
|
+
return ["mcp", "chatgpt", "claude"];
|
|
7134
|
+
}
|
|
7135
|
+
const targets = values.map((target) => {
|
|
7136
|
+
if (target === "mcp" || target === "chatgpt" || target === "claude") {
|
|
7137
|
+
return target;
|
|
7138
|
+
}
|
|
7139
|
+
throw new Error(`Unsupported Sidecar target "${target}". Expected mcp, chatgpt, or claude.`);
|
|
7140
|
+
});
|
|
7141
|
+
return Array.from(new Set(targets));
|
|
7142
|
+
}
|
|
6097
7143
|
function readPreviewComponentSet(value) {
|
|
6098
7144
|
if (!value || value === "representative") {
|
|
6099
7145
|
return "representative";
|
|
@@ -6151,174 +7197,85 @@ function printDiagnostics(diagnostics) {
|
|
|
6151
7197
|
console.warn(formatDiagnostic(diagnostic));
|
|
6152
7198
|
}
|
|
6153
7199
|
}
|
|
6154
|
-
async function
|
|
6155
|
-
const
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
throw new Error("auth.ts must default-export auth({ ... }) from sidecar-ai.");
|
|
6169
|
-
}
|
|
6170
|
-
return defaultExport;
|
|
6171
|
-
}
|
|
6172
|
-
async function loadRuntimeProxy(rootDir) {
|
|
6173
|
-
const proxyPath = path20.join(rootDir, "proxy.ts");
|
|
6174
|
-
if (!existsSync7(proxyPath)) {
|
|
6175
|
-
return void 0;
|
|
6176
|
-
}
|
|
6177
|
-
const parentURL = pathToFileURL3(path20.join(rootDir, "sidecar.config.ts")).href;
|
|
6178
|
-
const tsconfigPath = path20.join(rootDir, "tsconfig.json");
|
|
6179
|
-
const tsconfig = existsSync7(tsconfigPath) ? tsconfigPath : false;
|
|
6180
|
-
const module = await tsImport2(pathToFileURL3(proxyPath).href, {
|
|
6181
|
-
parentURL,
|
|
6182
|
-
tsconfig
|
|
7200
|
+
async function startBuiltMcpServer(options) {
|
|
7201
|
+
const rootDir = path21.resolve(options.rootDir);
|
|
7202
|
+
const entrypoint = path21.join(rootDir, options.outDir, SERVER_ENTRYPOINT);
|
|
7203
|
+
const child = spawn2(execPath, [entrypoint], {
|
|
7204
|
+
cwd: rootDir,
|
|
7205
|
+
env: {
|
|
7206
|
+
...process.env,
|
|
7207
|
+
HOST: "127.0.0.1",
|
|
7208
|
+
PORT: String(options.port),
|
|
7209
|
+
SIDECAR_HOST: "127.0.0.1",
|
|
7210
|
+
SIDECAR_MCP_URL: options.mcpUrl,
|
|
7211
|
+
SIDECAR_PORT: String(options.port)
|
|
7212
|
+
},
|
|
7213
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
6183
7214
|
});
|
|
6184
|
-
const
|
|
6185
|
-
|
|
6186
|
-
|
|
7215
|
+
const output = [];
|
|
7216
|
+
child.stdout?.on("data", (chunk) => output.push(String(chunk)));
|
|
7217
|
+
child.stderr?.on("data", (chunk) => output.push(String(chunk)));
|
|
7218
|
+
try {
|
|
7219
|
+
await waitForBuiltMcpServer(child, options.port, output);
|
|
7220
|
+
} catch (error) {
|
|
7221
|
+
await stopBuiltMcpServer(child);
|
|
7222
|
+
throw error;
|
|
6187
7223
|
}
|
|
6188
|
-
return
|
|
7224
|
+
return child;
|
|
6189
7225
|
}
|
|
6190
|
-
async function
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
return void 0;
|
|
6194
|
-
}
|
|
6195
|
-
const parentURL = pathToFileURL3(configPath).href;
|
|
6196
|
-
const tsconfigPath = path20.join(rootDir, "tsconfig.json");
|
|
6197
|
-
const tsconfig = existsSync7(tsconfigPath) ? tsconfigPath : false;
|
|
6198
|
-
const module = await tsImport2(pathToFileURL3(configPath).href, {
|
|
6199
|
-
parentURL,
|
|
6200
|
-
tsconfig
|
|
6201
|
-
});
|
|
6202
|
-
const defaultExport = unwrapRuntimeDefault2(module.default);
|
|
6203
|
-
if (!defaultExport || typeof defaultExport !== "object") {
|
|
6204
|
-
throw new Error("sidecar.config.ts must default-export defineConfig({ ... }) from sidecar-ai.");
|
|
7226
|
+
async function stopBuiltMcpServer(child) {
|
|
7227
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) {
|
|
7228
|
+
return;
|
|
6205
7229
|
}
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
const module = await tsImport2(pathToFileURL3(sourcePath).href, {
|
|
6216
|
-
parentURL,
|
|
6217
|
-
tsconfig
|
|
6218
|
-
});
|
|
6219
|
-
const defaultExport = unwrapRuntimeDefault2(module.default);
|
|
6220
|
-
if (!isSidecarTool(defaultExport)) {
|
|
6221
|
-
throw new Error(`${entry.sourceFile} did not default-export a Sidecar tool.`);
|
|
6222
|
-
}
|
|
6223
|
-
loaded.push({
|
|
6224
|
-
sourceFile: entry.sourceFile,
|
|
6225
|
-
tool: defaultExport,
|
|
6226
|
-
descriptor: entry.descriptor
|
|
7230
|
+
child.kill("SIGTERM");
|
|
7231
|
+
await new Promise((resolve) => {
|
|
7232
|
+
const timeout = setTimeout(() => {
|
|
7233
|
+
child.kill("SIGKILL");
|
|
7234
|
+
resolve();
|
|
7235
|
+
}, 2e3);
|
|
7236
|
+
child.once("exit", () => {
|
|
7237
|
+
clearTimeout(timeout);
|
|
7238
|
+
resolve();
|
|
6227
7239
|
});
|
|
6228
|
-
}
|
|
6229
|
-
return loaded;
|
|
6230
|
-
}
|
|
6231
|
-
function attachBuiltToolDescriptors(loadedTools, manifest) {
|
|
6232
|
-
const descriptorsBySource = new Map(
|
|
6233
|
-
manifest.tools.map((entry) => [entry.sourceFile, entry.descriptor])
|
|
6234
|
-
);
|
|
6235
|
-
return loadedTools.map(({ sourceFile, tool, descriptor }) => ({
|
|
6236
|
-
tool,
|
|
6237
|
-
descriptor: descriptorsBySource.get(sourceFile) ?? descriptor
|
|
6238
|
-
}));
|
|
7240
|
+
});
|
|
6239
7241
|
}
|
|
6240
|
-
async function
|
|
6241
|
-
const
|
|
6242
|
-
|
|
6243
|
-
if (
|
|
6244
|
-
|
|
7242
|
+
async function waitForBuiltMcpServer(child, port, output) {
|
|
7243
|
+
const deadline = Date.now() + 2e4;
|
|
7244
|
+
while (Date.now() < deadline) {
|
|
7245
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
7246
|
+
throw new Error(`Sidecar dev MCP process exited before startup.
|
|
7247
|
+
${output.join("").trim()}`);
|
|
6245
7248
|
}
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
});
|
|
6255
|
-
}
|
|
6256
|
-
const parentURL = pathToFileURL3(path20.join(rootDir, "sidecar.config.ts")).href;
|
|
6257
|
-
const tsconfigPath = path20.join(rootDir, "tsconfig.json");
|
|
6258
|
-
const tsconfig = existsSync7(tsconfigPath) ? tsconfigPath : false;
|
|
6259
|
-
for (const entry of manifest.resources) {
|
|
6260
|
-
const sourcePath = path20.join(rootDir, entry.sourceFile);
|
|
6261
|
-
const module = await tsImport2(pathToFileURL3(sourcePath).href, {
|
|
6262
|
-
parentURL,
|
|
6263
|
-
tsconfig
|
|
6264
|
-
});
|
|
6265
|
-
const defaultExport = unwrapRuntimeDefault2(module.default);
|
|
6266
|
-
if (!isSidecarResource(defaultExport)) {
|
|
6267
|
-
throw new Error(`${entry.sourceFile} did not default-export a Sidecar resource.`);
|
|
6268
|
-
}
|
|
6269
|
-
resources.push({
|
|
6270
|
-
uri: entry.uri,
|
|
6271
|
-
descriptor: entry.descriptor,
|
|
6272
|
-
resource: defaultExport
|
|
6273
|
-
});
|
|
6274
|
-
}
|
|
6275
|
-
return resources;
|
|
6276
|
-
}
|
|
6277
|
-
async function loadRuntimePrompts(rootDir, manifest) {
|
|
6278
|
-
const parentURL = pathToFileURL3(path20.join(rootDir, "sidecar.config.ts")).href;
|
|
6279
|
-
const tsconfigPath = path20.join(rootDir, "tsconfig.json");
|
|
6280
|
-
const tsconfig = existsSync7(tsconfigPath) ? tsconfigPath : false;
|
|
6281
|
-
const loaded = [];
|
|
6282
|
-
for (const entry of manifest.prompts) {
|
|
6283
|
-
const sourcePath = path20.join(rootDir, entry.sourceFile);
|
|
6284
|
-
const module = await tsImport2(pathToFileURL3(sourcePath).href, {
|
|
6285
|
-
parentURL,
|
|
6286
|
-
tsconfig
|
|
6287
|
-
});
|
|
6288
|
-
const defaultExport = unwrapRuntimeDefault2(module.default);
|
|
6289
|
-
if (!isSidecarPrompt(defaultExport)) {
|
|
6290
|
-
throw new Error(`${entry.sourceFile} did not default-export a Sidecar prompt.`);
|
|
7249
|
+
try {
|
|
7250
|
+
await fetch(`http://127.0.0.1:${port}/mcp`, {
|
|
7251
|
+
method: "GET",
|
|
7252
|
+
headers: { accept: "application/json, text/event-stream" }
|
|
7253
|
+
});
|
|
7254
|
+
return;
|
|
7255
|
+
} catch {
|
|
7256
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
6291
7257
|
}
|
|
6292
|
-
loaded.push({
|
|
6293
|
-
prompt: defaultExport,
|
|
6294
|
-
descriptor: entry.descriptor
|
|
6295
|
-
});
|
|
6296
7258
|
}
|
|
6297
|
-
|
|
6298
|
-
}
|
|
6299
|
-
function unwrapRuntimeDefault2(value) {
|
|
6300
|
-
if (value && typeof value === "object" && "default" in value && Object.keys(value).every((key) => key === "default" || key === "__esModule")) {
|
|
6301
|
-
return unwrapRuntimeDefault2(value.default);
|
|
6302
|
-
}
|
|
6303
|
-
return value;
|
|
7259
|
+
throw new Error(`Timed out waiting for Sidecar dev MCP server on 127.0.0.1:${port}.
|
|
7260
|
+
${output.join("").trim()}`);
|
|
6304
7261
|
}
|
|
6305
|
-
function
|
|
7262
|
+
function listenOnAnyPort(server) {
|
|
6306
7263
|
return new Promise((resolve, reject) => {
|
|
6307
7264
|
const onError = (error) => {
|
|
6308
7265
|
reject(error);
|
|
6309
7266
|
};
|
|
6310
7267
|
server.once("error", onError);
|
|
6311
|
-
server.listen(
|
|
7268
|
+
server.listen(0, "127.0.0.1", () => {
|
|
6312
7269
|
server.off("error", onError);
|
|
6313
|
-
|
|
7270
|
+
const address = server.address();
|
|
7271
|
+
if (!address) {
|
|
7272
|
+
reject(new Error("Preview server did not expose a bound address."));
|
|
7273
|
+
return;
|
|
7274
|
+
}
|
|
7275
|
+
resolve(address.port);
|
|
6314
7276
|
});
|
|
6315
7277
|
});
|
|
6316
7278
|
}
|
|
6317
|
-
function closeServer(server) {
|
|
6318
|
-
return new Promise((resolve) => {
|
|
6319
|
-
server.close(() => resolve());
|
|
6320
|
-
});
|
|
6321
|
-
}
|
|
6322
7279
|
function readOption(argv, name) {
|
|
6323
7280
|
const index = argv.indexOf(name);
|
|
6324
7281
|
if (index === -1) {
|
|
@@ -6341,11 +7298,16 @@ if (isDirectRun()) {
|
|
|
6341
7298
|
});
|
|
6342
7299
|
}
|
|
6343
7300
|
export {
|
|
7301
|
+
injectProjectPreviewBridge,
|
|
6344
7302
|
main,
|
|
6345
7303
|
previewComponentNames,
|
|
6346
7304
|
readPreviewComponentSet,
|
|
7305
|
+
readPreviewTargets,
|
|
6347
7306
|
readPreviewThemes,
|
|
7307
|
+
renderBuildUrlSummary,
|
|
6348
7308
|
renderComponentPreviewFrame,
|
|
6349
|
-
renderComponentPreviewHtml
|
|
7309
|
+
renderComponentPreviewHtml,
|
|
7310
|
+
renderDevUrlSummary,
|
|
7311
|
+
renderProjectPreviewHtml
|
|
6350
7312
|
};
|
|
6351
7313
|
//# sourceMappingURL=index.js.map
|