@flow-engineer/flow 0.2.1 → 0.2.3
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/README.md +167 -10
- package/coding-agents/auth-skill.md +137 -0
- package/coding-agents/context-skill.md +94 -0
- package/coding-agents/skill.md +219 -17
- package/coding-agents/test-skill.md +3 -1
- package/coding-agents/tools-skill.md +78 -1
- package/dist/cli/src/auth.d.ts +11 -0
- package/dist/cli/src/auth.js +147 -0
- package/dist/cli/src/commands.d.ts +119 -0
- package/dist/cli/src/commands.js +212 -0
- package/dist/cli/src/index.d.ts +2 -0
- package/dist/cli/src/index.js +5 -0
- package/dist/cli/src/run.d.ts +28 -0
- package/dist/cli/src/run.js +523 -0
- package/dist/cli/src/terminal.d.ts +3 -0
- package/dist/cli/src/terminal.js +135 -0
- package/dist/flow-messaging/src/client.d.ts +2 -2
- package/dist/flow-messaging/src/client.js +3 -3
- package/dist/flow-messaging/src/index.js +17 -9
- package/dist/mcp/src/client.d.ts +54 -0
- package/dist/mcp/src/client.js +175 -0
- package/dist/mcp/src/describe.d.ts +41 -0
- package/dist/mcp/src/describe.js +110 -0
- package/dist/mcp/src/index.d.ts +4 -0
- package/dist/mcp/src/index.js +7 -0
- package/dist/mcp/src/server.d.ts +70 -0
- package/dist/mcp/src/server.js +252 -0
- package/dist/mcp/src/terminal.d.ts +24 -0
- package/dist/mcp/src/terminal.js +54 -0
- package/dist/simulators/src/index.d.ts +1 -1
- package/dist/simulators/src/page.html +267 -49
- package/dist/simulators/src/server.d.ts +27 -0
- package/dist/simulators/src/server.js +264 -40
- package/dist/src/agent.d.ts +69 -3
- package/dist/src/agent.js +162 -8
- package/dist/src/channels.d.ts +6 -4
- package/dist/src/channels.js +4 -3
- package/dist/src/conversation.d.ts +17 -1
- package/dist/src/conversation.js +170 -16
- package/dist/src/events.d.ts +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/model.d.ts +27 -0
- package/dist/src/model.js +85 -13
- package/dist/src/skills.d.ts +4 -0
- package/dist/src/skills.js +23 -0
- package/dist/src/store.d.ts +2 -0
- package/dist/src/store.js +18 -0
- package/dist/src/tools.d.ts +69 -0
- package/dist/src/tools.js +17 -1
- package/dist/src/traces.js +32 -4
- package/dist/src/types.d.ts +5 -0
- package/dist/testing/src/account.d.ts +1 -1
- package/dist/testing/src/account.js +368 -57
- package/dist/testing/src/chat.js +24 -16
- package/dist/testing/src/check.d.ts +4 -0
- package/dist/testing/src/check.js +90 -0
- package/dist/testing/src/cli.js +77 -19
- package/dist/testing/src/flow-login.d.ts +33 -0
- package/dist/testing/src/flow-login.js +30 -5
- package/dist/testing/src/new.d.ts +14 -5
- package/dist/testing/src/new.js +54 -20
- package/dist/testing/src/plugin.d.ts +4 -0
- package/dist/testing/src/plugin.js +116 -0
- package/docs/agents.md +87 -12
- package/examples/cafe/src/cli.ts +24 -0
- package/examples/cafe/src/mcp.ts +22 -0
- package/examples/cafe/src/tools.ts +13 -0
- package/flow-messaging/README.md +3 -1
- package/package.json +28 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// An agent's tools and skills as an MCP server (Streamable HTTP, spec 2026-07-28 with the 2025
|
|
2
|
+
// fallback), on the official SDK. It needs no model, channels or store: mount `fetch` on any
|
|
3
|
+
// web-standard runtime (Cloudflare Workers, Deno, Bun) or `node` on node:http.
|
|
4
|
+
//
|
|
5
|
+
// Sign-in is the host's OAuth against the business's own authorization server; this server only
|
|
6
|
+
// verifies the token on every request (`auth.verify`) and turns it into the person. A call that
|
|
7
|
+
// needs sign-in and has none is answered HTTP 401 with WWW-Authenticate before the SDK runs: hosts
|
|
8
|
+
// start sign-in only on that, never on a tool result. A token without a needed scope gets 403
|
|
9
|
+
// insufficient_scope. Without `auth`, every tool answers and the person is anonymous.
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { createMcpHandler, hostHeaderValidationResponse, localhostAllowedHostnames, localhostAllowedOrigins, originValidationResponse, ProtocolError, ProtocolErrorCode, Server, } from "@modelcontextprotocol/server";
|
|
12
|
+
import { emitter } from "../../src/events.js";
|
|
13
|
+
import { loadSkills, mcpInstructions, readAgentFolder, readSkillTool } from "../../src/skills.js";
|
|
14
|
+
import { traces, tracesFromEnv } from "../../src/traces.js";
|
|
15
|
+
import { render, toolContext } from "../../src/tools.js";
|
|
16
|
+
import { describe, scopesFor, signInOf, toolsProblems } from "./describe.js";
|
|
17
|
+
const LOCAL = new Set(localhostAllowedHostnames());
|
|
18
|
+
export function mcpServer(options) {
|
|
19
|
+
if (options.dir !== undefined && options.files !== undefined)
|
|
20
|
+
throw new Error("@flow-engineer/flow/mcp: give dir or files, not both");
|
|
21
|
+
const skills = options.dir !== undefined || options.files !== undefined ? loadSkills(options.files ?? readAgentFolder(options.dir)) : [];
|
|
22
|
+
const tools = [...options.tools, ...(skills.length ? [readSkillTool(skills)] : [])];
|
|
23
|
+
const problems = toolsProblems(tools);
|
|
24
|
+
if (problems.length)
|
|
25
|
+
throw new Error(`@flow-engineer/flow/mcp: these tools cannot be served as declared:\n- ${problems.join("\n- ")}`);
|
|
26
|
+
const scopes = options.auth?.scopes;
|
|
27
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
28
|
+
const descriptors = tools.map((t) => (options.descriptor ? options.descriptor(t, describe(t, scopes)) : describe(t, scopes)));
|
|
29
|
+
const path = options.path ?? "/mcp";
|
|
30
|
+
const pluginPath = options.plugin === false ? undefined : options.plugin?.path ?? `${path}/plugin`;
|
|
31
|
+
for (const endpoint of [path, pluginPath].filter((p) => p !== undefined)) {
|
|
32
|
+
if (!endpoint.startsWith("/") || /[?#]/.test(endpoint))
|
|
33
|
+
throw new Error("@flow-engineer/flow/mcp: paths must be absolute URL paths");
|
|
34
|
+
}
|
|
35
|
+
if (pluginPath === path)
|
|
36
|
+
throw new Error("@flow-engineer/flow/mcp: plugin and standalone paths must differ");
|
|
37
|
+
const pluginDescriptors = descriptors.slice(0, options.tools.length);
|
|
38
|
+
const traceOptions = options.traces === false ? undefined : options.traces ?? tracesFromEnv();
|
|
39
|
+
const trace = traceOptions ? traces(traceOptions, { service: options.name, kindOf: () => undefined }) : undefined;
|
|
40
|
+
const emit = emitter([...(options.events ?? []), ...(trace ? [trace.sink] : [])]);
|
|
41
|
+
// Legacy HTTP carries clientInfo only on initialize. Keep bounded metadata, never auth.
|
|
42
|
+
const sessions = new Map();
|
|
43
|
+
const SESSION_TTL = 24 * 60 * 60 * 1000;
|
|
44
|
+
function endpointHandler(plugin) {
|
|
45
|
+
const instructions = options.instructions ?? (plugin ? "" : mcpInstructions(skills));
|
|
46
|
+
const listed = plugin ? pluginDescriptors : descriptors;
|
|
47
|
+
const available = plugin ? new Map(options.tools.map((t) => [t.name, t])) : byName;
|
|
48
|
+
return createMcpHandler(({ requestInfo }) => {
|
|
49
|
+
const server = new Server({ name: options.name, version: options.version }, { capabilities: { tools: {} }, ...(instructions ? { instructions } : {}) });
|
|
50
|
+
server.setRequestHandler("tools/list", async () => ({ tools: listed }));
|
|
51
|
+
server.setRequestHandler("tools/call", async (request, ctx) => {
|
|
52
|
+
const t = available.get(request.params.name);
|
|
53
|
+
if (!t)
|
|
54
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `No tool named ${request.params.name}.`);
|
|
55
|
+
const parsed = t.input.safeParse(request.params.arguments ?? {});
|
|
56
|
+
if (!parsed.success)
|
|
57
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid arguments for ${t.name}: ${parsed.error.message}`);
|
|
58
|
+
const identity = identityOf(ctx.http?.authInfo);
|
|
59
|
+
const person = identity ? `key:${identity.key}` : "mcp:anonymous";
|
|
60
|
+
const meta = ctx.mcpReq._meta ?? {};
|
|
61
|
+
const sentKey = meta["chinwag/idempotency-key"];
|
|
62
|
+
const session = requestInfo?.headers.get("mcp-session-id") ?? undefined;
|
|
63
|
+
const host = server.getClientVersion() ?? (session ? sessions.get(session)?.client : undefined);
|
|
64
|
+
const detail = {
|
|
65
|
+
surface: meta["chinwag/surface"] === "cli" ? "cli" : plugin ? "mcp-plugin" : "mcp",
|
|
66
|
+
client: host?.name,
|
|
67
|
+
clientVersion: host?.version,
|
|
68
|
+
session: ctx.sessionId ?? session,
|
|
69
|
+
person,
|
|
70
|
+
command: typeof meta["chinwag/command"] === "string" ? meta["chinwag/command"] : undefined,
|
|
71
|
+
};
|
|
72
|
+
const context = toolContext({
|
|
73
|
+
conversationId: `mcp:${person}`,
|
|
74
|
+
person,
|
|
75
|
+
key: identity?.key ?? null,
|
|
76
|
+
channel: "mcp",
|
|
77
|
+
callId: randomUUID(),
|
|
78
|
+
// CLI retries can preserve a call key; other hosts get one per request.
|
|
79
|
+
idempotencyKey: typeof sentKey === "string" && sentKey.length > 0 && sentKey.length <= 256 ? sentKey : randomUUID(),
|
|
80
|
+
signal: ctx.mcpReq?.signal ?? new AbortController().signal,
|
|
81
|
+
}, ctx.http?.authInfo?.extra?.credentials ?? options.credentials);
|
|
82
|
+
const started = Date.now();
|
|
83
|
+
emit(context.conversationId, "tool.start", { callId: context.callId, tool: t.name, input: parsed.data, ...detail });
|
|
84
|
+
const result = await callTool(t, parsed.data, context);
|
|
85
|
+
emit(context.conversationId, "tool.end", { callId: context.callId, tool: t.name, ms: Date.now() - started, ok: !result.isError, output: result.structuredContent ?? result.content, error: result.isError ? result.content.filter(c => c.type === "text").map(c => c.text).join("\n") : undefined, ...detail });
|
|
86
|
+
return result;
|
|
87
|
+
});
|
|
88
|
+
options.extend?.(server);
|
|
89
|
+
return server;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const handler = endpointHandler(false);
|
|
93
|
+
const pluginHandler = pluginPath ? endpointHandler(true) : undefined;
|
|
94
|
+
async function serve(request) {
|
|
95
|
+
const url = new URL(request.url);
|
|
96
|
+
const metadataPrefix = "/.well-known/oauth-protected-resource";
|
|
97
|
+
const plugin = pluginPath !== undefined && (url.pathname === pluginPath || url.pathname === `${metadataPrefix}${pluginPath}`);
|
|
98
|
+
const servedPath = plugin ? pluginPath : path;
|
|
99
|
+
if (request.method === "GET" && (url.pathname === metadataPrefix || url.pathname === `${metadataPrefix}${servedPath}`)) {
|
|
100
|
+
if (!options.auth)
|
|
101
|
+
return new Response("Not found", { status: 404 });
|
|
102
|
+
return Response.json({
|
|
103
|
+
resource: resourceOf(url, servedPath),
|
|
104
|
+
authorization_servers: [options.auth.issuer].flat(),
|
|
105
|
+
bearer_methods_supported: ["header"],
|
|
106
|
+
...(scopes && (scopes.read || scopes.write) ? { scopes_supported: [...new Set([scopes.read, scopes.write].filter(Boolean))] } : {}),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (url.pathname !== servedPath)
|
|
110
|
+
return new Response("Not found", { status: 404 });
|
|
111
|
+
const rejected = options.allowedHosts
|
|
112
|
+
? hostHeaderValidationResponse(request, options.allowedHosts)
|
|
113
|
+
: LOCAL.has(url.hostname)
|
|
114
|
+
? (hostHeaderValidationResponse(request, localhostAllowedHostnames()) ?? originValidationResponse(request, localhostAllowedOrigins()))
|
|
115
|
+
: undefined;
|
|
116
|
+
if (rejected)
|
|
117
|
+
return rejected;
|
|
118
|
+
for (const [id, saved] of sessions)
|
|
119
|
+
if (Date.now() - saved.at > SESSION_TTL)
|
|
120
|
+
sessions.delete(id);
|
|
121
|
+
const session = request.headers.get("mcp-session-id");
|
|
122
|
+
if (session && sessions.get(session)?.path !== servedPath)
|
|
123
|
+
return new Response("Session not found", { status: 404 });
|
|
124
|
+
let authInfo;
|
|
125
|
+
const bearer = /^Bearer\s+(.+)$/i.exec(request.headers.get("authorization") ?? "")?.[1];
|
|
126
|
+
if (options.auth && bearer) {
|
|
127
|
+
const identity = await options.auth.verify(bearer, request);
|
|
128
|
+
if (!identity)
|
|
129
|
+
return challenge(url, servedPath, 401, "invalid_token", "The token is not valid.", []);
|
|
130
|
+
authInfo = { token: bearer, clientId: "", scopes: identity.scopes ?? [], extra: { key: identity.key } };
|
|
131
|
+
if (identity.credentials)
|
|
132
|
+
Object.defineProperty(authInfo.extra, "credentials", { value: identity.credentials, enumerable: false });
|
|
133
|
+
}
|
|
134
|
+
let parsedBody;
|
|
135
|
+
if (request.method === "POST") {
|
|
136
|
+
try {
|
|
137
|
+
parsedBody = JSON.parse(await request.clone().text());
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
parsedBody = undefined;
|
|
141
|
+
}
|
|
142
|
+
if (options.auth) {
|
|
143
|
+
for (const message of [parsedBody].flat()) {
|
|
144
|
+
const call = message;
|
|
145
|
+
if (!call || call.method !== "tools/call" || typeof call.params?.name !== "string")
|
|
146
|
+
continue;
|
|
147
|
+
const t = plugin && call.params.name === "read_skill" ? undefined : byName.get(call.params.name);
|
|
148
|
+
if (!t || signInOf(t) === "none")
|
|
149
|
+
continue;
|
|
150
|
+
const needed = scopesFor(t, scopes);
|
|
151
|
+
if (!authInfo) {
|
|
152
|
+
if (signInOf(t) === "required")
|
|
153
|
+
return challenge(url, servedPath, 401, "invalid_token", `Sign in to use ${t.name}.`, needed);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const missing = needed.filter((s) => !authInfo.scopes.includes(s));
|
|
157
|
+
if (missing.length)
|
|
158
|
+
return challenge(url, servedPath, 403, "insufficient_scope", `${t.name} needs ${missing.join(" ")}.`, [...new Set([...authInfo.scopes, ...needed])]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (request.method === "DELETE" && session) {
|
|
163
|
+
sessions.delete(session);
|
|
164
|
+
return new Response(null, { status: 200 });
|
|
165
|
+
}
|
|
166
|
+
const response = await (plugin ? pluginHandler : handler).fetch(request, { authInfo, parsedBody });
|
|
167
|
+
const init = parsedBody;
|
|
168
|
+
if (response.ok && init?.method === "initialize") {
|
|
169
|
+
const client = init.params?.clientInfo;
|
|
170
|
+
if (typeof client?.name === "string" && typeof client.version === "string") {
|
|
171
|
+
const id = randomUUID();
|
|
172
|
+
sessions.set(id, { client: { name: client.name.slice(0, 256), version: client.version.slice(0, 256) }, path: servedPath, at: Date.now() });
|
|
173
|
+
if (sessions.size > 4096)
|
|
174
|
+
sessions.delete(sessions.keys().next().value);
|
|
175
|
+
const headers = new Headers(response.headers);
|
|
176
|
+
headers.set("mcp-session-id", id);
|
|
177
|
+
return new Response(response.body, { status: response.status, headers });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return response;
|
|
181
|
+
}
|
|
182
|
+
function resourceOf(url, servedPath) {
|
|
183
|
+
const resource = options.auth?.resource ?? `${url.origin}${path}`;
|
|
184
|
+
if (servedPath === path)
|
|
185
|
+
return resource;
|
|
186
|
+
const pluginResource = new URL(resource);
|
|
187
|
+
pluginResource.pathname = servedPath;
|
|
188
|
+
return pluginResource.href;
|
|
189
|
+
}
|
|
190
|
+
function challenge(url, servedPath, status, error, description, scope) {
|
|
191
|
+
const metadata = `${new URL(resourceOf(url, servedPath)).origin}/.well-known/oauth-protected-resource${servedPath}`;
|
|
192
|
+
const header = `Bearer error="${error}", error_description="${description}", resource_metadata="${metadata}"${scope.length ? `, scope="${scope.join(" ")}"` : ""}`;
|
|
193
|
+
return Response.json({ error, error_description: description }, { status, headers: { "WWW-Authenticate": header } });
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
fetch: serve,
|
|
197
|
+
node: (req, res) => serveNode(serve, req, res),
|
|
198
|
+
descriptors,
|
|
199
|
+
flushTraces: () => trace?.flush() ?? Promise.resolve(),
|
|
200
|
+
...(pluginPath ? { plugin: { path: pluginPath, descriptors: pluginDescriptors } } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function identityOf(authInfo) {
|
|
204
|
+
const key = authInfo?.extra?.key;
|
|
205
|
+
return typeof key === "string" ? { key, scopes: authInfo.scopes } : null;
|
|
206
|
+
}
|
|
207
|
+
async function callTool(t, input, context) {
|
|
208
|
+
let output;
|
|
209
|
+
try {
|
|
210
|
+
output = await t.run(input, context);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return { isError: true, content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }] };
|
|
214
|
+
}
|
|
215
|
+
if (!t.output)
|
|
216
|
+
return { content: [{ type: "text", text: render(output) }] };
|
|
217
|
+
const checked = t.output.safeParse(output);
|
|
218
|
+
if (!checked.success)
|
|
219
|
+
return { isError: true, content: [{ type: "text", text: `${t.name} returned a result that does not match its output schema.` }] };
|
|
220
|
+
return { content: [{ type: "text", text: render(checked.data) }], structuredContent: checked.data };
|
|
221
|
+
}
|
|
222
|
+
/** node:http in front of the web-standard handler, streaming the answer (event streams included). */
|
|
223
|
+
async function serveNode(serve, req, res) {
|
|
224
|
+
const abort = new AbortController();
|
|
225
|
+
res.on("close", () => abort.abort());
|
|
226
|
+
const secure = req.socket.encrypted === true;
|
|
227
|
+
const url = `${secure ? "https" : "http"}://${req.headers.host ?? "localhost"}${req.url ?? "/"}`;
|
|
228
|
+
const headers = new Headers();
|
|
229
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
230
|
+
if (value !== undefined)
|
|
231
|
+
headers.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
232
|
+
}
|
|
233
|
+
let body;
|
|
234
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
235
|
+
const chunks = [];
|
|
236
|
+
for await (const chunk of req)
|
|
237
|
+
chunks.push(chunk);
|
|
238
|
+
const all = Buffer.concat(chunks);
|
|
239
|
+
body = all.buffer.slice(all.byteOffset, all.byteOffset + all.byteLength);
|
|
240
|
+
}
|
|
241
|
+
const response = await serve(new Request(url, { method: req.method, headers, body, signal: abort.signal }));
|
|
242
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
243
|
+
if (!response.body)
|
|
244
|
+
return void res.end();
|
|
245
|
+
try {
|
|
246
|
+
for await (const chunk of response.body)
|
|
247
|
+
res.write(chunk);
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
res.end();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Agent } from "../../src/agent.ts";
|
|
2
|
+
import type { McpAuth } from "./server.ts";
|
|
3
|
+
export interface TerminalServerOptions {
|
|
4
|
+
/** A getter lets the channel be declared before building the agent that uses it. */
|
|
5
|
+
agent: () => Agent;
|
|
6
|
+
auth: Pick<McpAuth, "verify">;
|
|
7
|
+
path?: string;
|
|
8
|
+
channel?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function terminalServer(options: TerminalServerOptions): {
|
|
11
|
+
path: string;
|
|
12
|
+
channel: {
|
|
13
|
+
name: string;
|
|
14
|
+
kind: import("../../src/channels.ts").ChannelKind;
|
|
15
|
+
info: import("../../src/types.ts").ChannelInfo;
|
|
16
|
+
phone?: boolean;
|
|
17
|
+
params?: Record<string, import("zod").ZodType>;
|
|
18
|
+
instructions?: string;
|
|
19
|
+
keywords?: import("../../src/channels.ts").Keywords;
|
|
20
|
+
send(address: string, outbound: import("../../src/types.ts").Outbound): Promise<void> | void;
|
|
21
|
+
traceChannel: string;
|
|
22
|
+
};
|
|
23
|
+
fetch(request: Request): Promise<Response>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// An authenticated terminal channel beside MCP. Mount fetch wherever the agent's HTTP routes live.
|
|
2
|
+
import { customUI } from "../../src/channels.js";
|
|
3
|
+
export function terminalServer(options) {
|
|
4
|
+
const path = options.path ?? "/agent/terminal";
|
|
5
|
+
const channel = { ...customUI(options.channel ?? "terminal", { sendMessage: async () => { throw new Error("No terminal request is listening"); } }), traceChannel: "cli" };
|
|
6
|
+
let inFlight = 0;
|
|
7
|
+
const pending = new Map();
|
|
8
|
+
return {
|
|
9
|
+
path,
|
|
10
|
+
channel,
|
|
11
|
+
async fetch(request) {
|
|
12
|
+
if (new URL(request.url).pathname !== path)
|
|
13
|
+
return new Response("Not found", { status: 404 });
|
|
14
|
+
if (request.method !== "POST")
|
|
15
|
+
return new Response("Method not allowed", { status: 405, headers: { allow: "POST" } });
|
|
16
|
+
const token = /^Bearer\s+(.+)$/i.exec(request.headers.get("authorization") ?? "")?.[1];
|
|
17
|
+
let identity;
|
|
18
|
+
try {
|
|
19
|
+
identity = token ? await options.auth.verify(token, request) : null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
identity = null;
|
|
23
|
+
}
|
|
24
|
+
if (!identity?.key)
|
|
25
|
+
return Response.json({ error: "sign_in_required" }, { status: 401, headers: { "www-authenticate": "Bearer" } });
|
|
26
|
+
let input;
|
|
27
|
+
try {
|
|
28
|
+
input = await request.json();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return Response.json({ error: "invalid_request" }, { status: 400 });
|
|
32
|
+
}
|
|
33
|
+
if (!input || typeof input.text !== "string" || !input.text.trim() || input.text.length > 32_768 || (input.conversationId !== undefined && typeof input.conversationId !== "string"))
|
|
34
|
+
return Response.json({ error: "invalid_request" }, { status: 400 });
|
|
35
|
+
if (inFlight >= 128)
|
|
36
|
+
return Response.json({ error: "busy" }, { status: 429 });
|
|
37
|
+
inFlight++;
|
|
38
|
+
const previous = pending.get(identity.key) ?? Promise.resolve();
|
|
39
|
+
const work = previous.catch(() => undefined).then(() => options.agent().request({ channel: channel.name, key: identity.key, text: input.text, conversationId: input.conversationId, credentials: identity.credentials }));
|
|
40
|
+
pending.set(identity.key, work);
|
|
41
|
+
try {
|
|
42
|
+
return Response.json(await work);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return Response.json({ error: "turn_failed" }, { status: 500 });
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
inFlight--;
|
|
49
|
+
if (pending.get(identity.key) === work)
|
|
50
|
+
pending.delete(identity.key);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { playgroundClock, simulators, type AgentConnection, type PlaygroundClock, type Line, type LinkCard, type Playground, type PlaygroundAgent, type SimulatorsOptions, type TestCloudSimulators } from "./server.ts";
|
|
1
|
+
export { playgroundClock, simulators, type AgentConnection, type PlaygroundClock, type Line, type LinkCard, type LocalCliOptions, type Playground, type PlaygroundAgent, type SimulatorsOptions, type TestCloudSimulators } from "./server.ts";
|
|
2
2
|
export { PLATFORMS, platform, channelOf, smsSegments, type Platform } from "./platforms.ts";
|