agentic-assistant 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,196 @@
1
+ // src/client.ts
2
+ var STATUS_CODES = {
3
+ 400: "INVALID_INPUT",
4
+ 401: "UNAUTHENTICATED",
5
+ 403: "FORBIDDEN",
6
+ 404: "NOT_FOUND",
7
+ 405: "METHOD_NOT_ALLOWED",
8
+ 409: "CONFLICT",
9
+ 429: "RATE_LIMITED"
10
+ };
11
+ var AssistantApiError = class extends Error {
12
+ code;
13
+ status;
14
+ /** Seconds to wait before a new try, from `retry-after` of a 429 reply. */
15
+ retryAfter;
16
+ constructor(code, status, message, retryAfter) {
17
+ super(message);
18
+ this.name = "AssistantApiError";
19
+ this.code = code;
20
+ this.status = status;
21
+ this.retryAfter = retryAfter;
22
+ }
23
+ };
24
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
25
+ var isString = (v) => typeof v === "string";
26
+ var RISKS = ["safe", "risky", "irreversible"];
27
+ var isTool = (v) => isObject(v) && isString(v.name) && isString(v.description) && isString(v.scope) && RISKS.includes(v.risk) && "inputSchema" in v;
28
+ var isPending = (v) => isObject(v) && isString(v.id) && isString(v.tool) && RISKS.includes(v.risk) && isString(v.digest) && isObject(v.input) && isObject(v.proposedBy) && isString(v.requestedBy) && typeof v.canDecide === "boolean";
29
+ var SHAPES = {
30
+ health: (v) => isObject(v) && typeof v.ok === "boolean" && isString(v.pack) && isString(v.version),
31
+ tools: (v) => isObject(v) && Array.isArray(v.tools) && v.tools.every(isTool),
32
+ approvals: (v) => isObject(v) && Array.isArray(v.approvals) && v.approvals.every(isPending),
33
+ decision: (v) => isObject(v) && isString(v.requestId) && typeof v.approved === "boolean" && isString(v.decidedBy) && typeof v.machineMade === "boolean"
34
+ };
35
+ var MAX_EVENT_CHARS = 4194304;
36
+ var EVENT_TYPES = ["run_start", "turn_start", "text_delta", "tool_call", "approval_request", "approval_decision", "tool_result", "turn_end", "done", "error"];
37
+ var isEvent = (v) => {
38
+ if (!isObject(v) || typeof v.type !== "string" || !EVENT_TYPES.includes(v.type)) return false;
39
+ switch (v.type) {
40
+ case "done":
41
+ return isString(v.reason) && isObject(v.totals);
42
+ case "error":
43
+ return isString(v.error);
44
+ case "text_delta":
45
+ return isString(v.text);
46
+ case "tool_result":
47
+ return isString(v.id) && isString(v.name) && isString(v.content) && typeof v.isError === "boolean";
48
+ case "tool_call":
49
+ return isString(v.id) && isString(v.name);
50
+ case "approval_request":
51
+ return isObject(v.request) && isString(v.request.id) && isString(v.request.digest);
52
+ case "approval_decision":
53
+ return isString(v.requestId) && typeof v.approved === "boolean";
54
+ default:
55
+ return true;
56
+ }
57
+ };
58
+ function createClient(options) {
59
+ if (typeof options.baseUrl !== "string" || options.baseUrl === "") throw new TypeError("createClient needs a baseUrl");
60
+ if (typeof options.token !== "string" || options.token === "") throw new TypeError("createClient needs a token");
61
+ let origin;
62
+ try {
63
+ origin = new URL(options.baseUrl);
64
+ } catch {
65
+ throw new TypeError("baseUrl must be an absolute URL");
66
+ }
67
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(origin.hostname);
68
+ if (origin.protocol !== "https:" && !(origin.protocol === "http:" && local)) {
69
+ throw new TypeError("baseUrl must be an https URL, or an http URL on localhost, 127.0.0.1, or [::1]");
70
+ }
71
+ if (origin.search !== "" || origin.hash !== "" || origin.username !== "" || origin.password !== "") {
72
+ throw new TypeError("baseUrl must have no query, hash, or credentials");
73
+ }
74
+ const prefix = origin.pathname.replace(/\/+$/, "");
75
+ const root = `${origin.origin}${prefix}/api/v1`;
76
+ const token = options.token;
77
+ const send = (input, init) => {
78
+ const run = options.fetch ?? globalThis.fetch;
79
+ return run(input, init);
80
+ };
81
+ const seg = (value) => {
82
+ if (typeof value !== "string" || /^(?:\.|%2e){0,2}$/i.test(value)) throw new TypeError("an id must be a non-empty string that is not . or ..");
83
+ return encodeURIComponent(value);
84
+ };
85
+ async function request(url, method, body, callOptions, authed = true) {
86
+ const headers = authed ? { authorization: `Bearer ${token}` } : {};
87
+ if (body !== void 0) headers["content-type"] = "application/json";
88
+ return send(url, {
89
+ method,
90
+ headers,
91
+ body: body === void 0 ? void 0 : JSON.stringify(body),
92
+ signal: callOptions?.signal,
93
+ // Never follow a redirect: it would send the token to another host.
94
+ redirect: "manual"
95
+ });
96
+ }
97
+ async function failure(response) {
98
+ let parsed;
99
+ try {
100
+ parsed = JSON.parse(await response.text());
101
+ } catch {
102
+ parsed = void 0;
103
+ }
104
+ const error = isObject(parsed) ? parsed.error : void 0;
105
+ const message = isObject(parsed) && typeof parsed.message === "string" ? parsed.message : typeof error === "string" ? error : `HTTP ${response.status}`;
106
+ let code = STATUS_CODES[response.status] ?? (response.status >= 500 ? "INTERNAL" : "UNKNOWN");
107
+ if (response.status === 503 && error === "not_configured") code = "NOT_CONFIGURED";
108
+ if (typeof error !== "string") code = response.status >= 500 ? "INTERNAL" : "UNKNOWN";
109
+ const after = response.headers.get("retry-after")?.trim() ?? "";
110
+ return new AssistantApiError(code, response.status, message, /^\d{1,9}$/.test(after) ? Number(after) : void 0);
111
+ }
112
+ async function call(url, method, body, callOptions, shape, authed = true) {
113
+ const response = await request(url, method, body, callOptions, authed);
114
+ if (!response.ok) throw await failure(response);
115
+ let parsed;
116
+ try {
117
+ parsed = JSON.parse(await response.text());
118
+ } catch {
119
+ parsed = void 0;
120
+ }
121
+ if (!SHAPES[shape](parsed)) throw new AssistantApiError("UNKNOWN", response.status, `the reply is not a ${shape}`);
122
+ return parsed;
123
+ }
124
+ async function* chat(message, callOptions) {
125
+ const response = await request(`${root}/chat`, "POST", { message }, callOptions);
126
+ if (!response.ok) throw await failure(response);
127
+ if (!(response.headers.get("content-type") ?? "").startsWith("text/event-stream") || response.body === null) {
128
+ await response.body?.cancel();
129
+ throw new AssistantApiError("UNKNOWN", response.status, "the reply is not an event stream");
130
+ }
131
+ const reader = response.body.getReader();
132
+ const decoder = new TextDecoder();
133
+ let buffer = "";
134
+ let ended = false;
135
+ function* drain(final) {
136
+ const text = final ? buffer : buffer.replace(/\r$/, "");
137
+ const normal = text.replace(/\r\n?/g, "\n");
138
+ const rest = buffer.slice(text.length);
139
+ let from = 0;
140
+ for (let end = normal.indexOf("\n\n", from); end >= 0; end = normal.indexOf("\n\n", from)) {
141
+ const event = parseBlock(normal.slice(from, end), response.status);
142
+ from = end + 2;
143
+ if (event === void 0) continue;
144
+ if (ended) throw new AssistantApiError("UNKNOWN", response.status, "an event came after the end of the stream");
145
+ ended = event.type === "done" || event.type === "error";
146
+ yield event;
147
+ }
148
+ buffer = normal.slice(from) + rest;
149
+ }
150
+ try {
151
+ for (; ; ) {
152
+ const chunk = await reader.read();
153
+ if (chunk.done) break;
154
+ buffer += decoder.decode(chunk.value, { stream: true });
155
+ yield* drain(false);
156
+ if (buffer.length > MAX_EVENT_CHARS) throw new AssistantApiError("UNKNOWN", response.status, "an event is too long");
157
+ }
158
+ buffer += decoder.decode();
159
+ yield* drain(true);
160
+ if (!ended || buffer.trim() !== "") throw new AssistantApiError("UNKNOWN", response.status, "the stream ended before a done event");
161
+ } finally {
162
+ await reader.cancel().catch(() => void 0);
163
+ }
164
+ }
165
+ function parseBlock(block, status) {
166
+ const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
167
+ if (data === "") return void 0;
168
+ let parsed;
169
+ try {
170
+ parsed = JSON.parse(data);
171
+ } catch {
172
+ parsed = void 0;
173
+ }
174
+ if (!isEvent(parsed)) throw new AssistantApiError("UNKNOWN", status, "an event is not valid");
175
+ return parsed;
176
+ }
177
+ const decided = (id, decision) => {
178
+ if (decision.requestId !== id) throw new AssistantApiError("UNKNOWN", 200, "the reply is for another approval");
179
+ return decision;
180
+ };
181
+ const methods = {
182
+ // /health is outside /api/v1 and needs no token, so the client sends none.
183
+ health: async (o) => call(`${origin.origin}${prefix}/health`, "GET", void 0, o, "health", false),
184
+ tools: async (o) => (await call(`${root}/tools`, "GET", void 0, o, "tools")).tools,
185
+ chat,
186
+ approvals: async (o) => (await call(`${root}/approvals`, "GET", void 0, o, "approvals")).approvals,
187
+ approve: async (id, digest, o) => decided(id, await call(`${root}/approvals/${seg(id)}/approve`, "POST", { digest }, o, "decision")),
188
+ deny: async (id, reason, o) => decided(id, await call(`${root}/approvals/${seg(id)}/deny`, "POST", reason === void 0 ? {} : { reason }, o, "decision"))
189
+ };
190
+ return methods;
191
+ }
192
+
193
+ export {
194
+ AssistantApiError,
195
+ createClient
196
+ };
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.3.0" : readPackageVersion();
3
+ var VERSION = true ? "0.5.0" : readPackageVersion();
4
4
 
5
5
  // src/approvals.ts
6
6
  import { createHash, randomUUID } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ProviderError
3
- } from "./chunk-3LKTJJ75.js";
3
+ } from "./chunk-RHGVOR6N.js";
4
4
 
5
5
  // src/providers/anthropic.ts
6
6
  import Anthropic from "@anthropic-ai/sdk";
package/dist/cli.d.ts CHANGED
@@ -1,14 +1,35 @@
1
1
  #!/usr/bin/env node
2
- import { C as ChatConfig } from './chat-CXbhePC5.js';
3
- import './loop-D4qWbRu6.js';
2
+ import { C as ChatConfig } from './chat-CGITyrxg.js';
3
+ import './loop-B6aowbyz.js';
4
4
  import './types-DGH8FS56.js';
5
5
 
6
+ interface RemoteConfig {
7
+ api: string;
8
+ apiTokenEnv: string;
9
+ }
10
+ type RemoteCommand = {
11
+ name: "tools";
12
+ } | {
13
+ name: "approvals";
14
+ } | {
15
+ name: "approve";
16
+ id: string;
17
+ digest: string;
18
+ } | {
19
+ name: "deny";
20
+ id: string;
21
+ reason?: string;
22
+ };
23
+
6
24
  declare class UsageError extends Error {
7
25
  constructor(message: string);
8
26
  }
9
27
  interface Parsed {
10
- command: "chat" | "mcp" | "help" | "version";
28
+ command: "chat" | "mcp" | "help" | "version" | "remote";
11
29
  config?: ChatConfig;
30
+ remote?: RemoteConfig;
31
+ allowApprove?: boolean;
32
+ remoteCommand?: RemoteCommand;
12
33
  message?: string;
13
34
  json: boolean;
14
35
  }
package/dist/cli.js CHANGED
@@ -2,17 +2,21 @@
2
2
  import {
3
3
  ConfigError,
4
4
  buildSetup,
5
+ clientFor,
6
+ describeFailure,
5
7
  readTextFile,
6
- runChat
7
- } from "./chunk-3X6KU636.js";
8
+ runChat,
9
+ runRemote
10
+ } from "./chunk-KSVH5NNA.js";
8
11
  import "./chunk-HYT732TN.js";
9
- import "./chunk-DXTIEMOT.js";
12
+ import "./chunk-XSRGG7SB.js";
10
13
  import {
11
14
  TOOL_RISKS,
12
15
  VERSION,
13
16
  printable,
14
17
  provenanceLabel
15
- } from "./chunk-3LKTJJ75.js";
18
+ } from "./chunk-RHGVOR6N.js";
19
+ import "./chunk-QTIEDY3V.js";
16
20
 
17
21
  // src/cli.ts
18
22
  import { realpathSync } from "fs";
@@ -30,9 +34,22 @@ An AI assistant that acts inside your apps with the user's own permissions.
30
34
 
31
35
  Usage:
32
36
  agentic-assistant chat [flags] [message] Send a message. Read stdin when there is no message.
33
- agentic-assistant mcp [flags] Start an MCP server with one tool, assistant_chat.
37
+ agentic-assistant mcp [flags] Start an MCP server with the tool assistant_chat. With --api it adds three more.
38
+ agentic-assistant tools [flags] List the tools of a running assistant API, with scope and risk.
39
+ agentic-assistant approvals [flags] List the calls that wait for a person.
40
+ agentic-assistant approve <id> --digest <d> [flags] Approve the exact call. Copy the digest from "approvals".
41
+ agentic-assistant deny <id> [--reason <text>] [flags] Deny the call. A denial is final.
42
+
43
+ Flags for tools, approvals, approve, and deny:
44
+ --api <url> The assistant API origin, like https://assistant.example.com. https, or localhost. Required.
45
+ --api-token-env <NAME> Read the bearer token from this environment variable. Required. Tokens never go in arguments.
46
+ --digest <digest> approve only: the digest of the pending call.
47
+ --reason <text> deny only: why.
48
+ --json Print the result as JSON on stdout.
34
49
 
35
50
  Flags for chat and mcp:
51
+ --api <url> --api-token-env <NAME> mcp only: also serve assistant_tools, assistant_approvals, assistant_deny.
52
+ --allow-approve mcp only, with --api: also serve assistant_approve. Use an agent token (it needs assistant:admin).
36
53
  --tools <target> An https MCP URL, or a command that starts an MCP server on stdio. Required.
37
54
  --tools-header-env <h=E> Send header h with the value of environment variable E (http only). Repeat it.
38
55
  --scope <scope> A scope the assistant may hold, like notes:read. Repeat it. Required.
@@ -82,6 +99,11 @@ var OPTIONS = {
82
99
  "max-tokens": { type: "string" },
83
100
  "max-cost": { type: "string" },
84
101
  "max-tool-calls": { type: "string" },
102
+ api: { type: "string" },
103
+ "api-token-env": { type: "string" },
104
+ "allow-approve": { type: "boolean" },
105
+ digest: { type: "string" },
106
+ reason: { type: "string" },
85
107
  json: { type: "boolean" },
86
108
  message: { type: "string" },
87
109
  help: { type: "boolean", short: "h" },
@@ -106,7 +128,26 @@ function parseCli(argv) {
106
128
  if (values.version) return { command: "version", json: false };
107
129
  const [command, ...rest] = positionals;
108
130
  if (values.help || command === void 0) return { command: "help", json: false };
109
- if (command !== "chat" && command !== "mcp") throw new UsageError(`unknown command "${command}"`);
131
+ const remoteNames = ["tools", "approvals", "approve", "deny"];
132
+ if (command !== "chat" && command !== "mcp" && !remoteNames.includes(command)) throw new UsageError(`unknown command "${command}"`);
133
+ if (values.api === "" || values["api-token-env"] === "") throw new UsageError("--api needs a URL, and --api-token-env a variable name");
134
+ if (values.api === void 0 !== (values["api-token-env"] === void 0)) throw new UsageError("--api and --api-token-env go together");
135
+ const remote = values.api !== void 0 && values["api-token-env"] !== void 0 ? { api: values.api, apiTokenEnv: values["api-token-env"] } : void 0;
136
+ if (command !== "mcp" && command !== "chat" && remote === void 0) throw new UsageError("--api and --api-token-env are required");
137
+ if (command === "chat" && remote !== void 0) throw new UsageError("--api works with mcp and the approval commands, not chat");
138
+ const wantsId = command === "approve" || command === "deny";
139
+ if (remoteNames.includes(command) && rest.length !== (wantsId ? 1 : 0)) throw new UsageError(wantsId ? `${command} needs one approval id` : `${command} takes no arguments`);
140
+ if (values["allow-approve"] && (command !== "mcp" || remote === void 0)) throw new UsageError("--allow-approve works with mcp and --api only");
141
+ if (command !== "approve" && values.digest !== void 0) throw new UsageError("--digest works with approve only");
142
+ if (command !== "deny" && values.reason !== void 0) throw new UsageError("--reason works with deny only");
143
+ if (remoteNames.includes(command)) {
144
+ const chatOnly = Object.keys(values).filter((key) => !["api", "api-token-env", "digest", "reason", "json", "help", "version"].includes(key));
145
+ if (chatOnly.length > 0) throw new UsageError(`--${chatOnly[0]} does not work with ${command}`);
146
+ const id = rest[0];
147
+ if (command === "approve" && !values.digest) throw new UsageError("approve needs --digest <digest>");
148
+ const remoteCommand = command === "approve" ? { name: "approve", id, digest: values.digest } : command === "deny" ? { name: "deny", id, ...values.reason !== void 0 ? { reason: values.reason } : {} } : { name: command };
149
+ return { command: "remote", remote, remoteCommand, json: values.json ?? false };
150
+ }
110
151
  if (!values.tools) throw new UsageError("--tools is required");
111
152
  if (!values["tool-scope"]) throw new UsageError("--tool-scope is required");
112
153
  const risk = values["tool-risk"] ?? "risky";
@@ -159,7 +200,7 @@ function parseCli(argv) {
159
200
  preApproved,
160
201
  approve
161
202
  };
162
- return { command, config, message: values.message ?? (rest.length > 0 ? rest.join(" ") : void 0), json: values.json ?? false };
203
+ return { command, config, remote, allowApprove: values["allow-approve"] ?? false, message: values.message ?? (rest.length > 0 ? rest.join(" ") : void 0), json: values.json ?? false };
163
204
  }
164
205
  function defaultIo() {
165
206
  return {
@@ -264,16 +305,30 @@ async function runCli(argv, io = defaultIo()) {
264
305
  `);
265
306
  return 0;
266
307
  }
308
+ if (parsed.command === "remote") {
309
+ try {
310
+ const client = clientFor(parsed.remote, io.env);
311
+ const { data, text } = await runRemote(client, parsed.remoteCommand);
312
+ io.out(parsed.json ? `${JSON.stringify(data)}
313
+ ` : `${text}
314
+ `);
315
+ return 0;
316
+ } catch (error) {
317
+ io.err(error instanceof ConfigError ? `${error.name}: ${oneLine(error.message, 500)}` : describeFailure(error));
318
+ return error instanceof ConfigError ? 2 : 1;
319
+ }
320
+ }
267
321
  const config = parsed.config;
268
322
  const chatIo = { env: io.env, readFile: io.readFile, ...config.approve === "ask" && io.isTTY ? { ask: io.ask } : {} };
269
323
  let setup;
270
324
  try {
325
+ const remote = parsed.command === "mcp" && parsed.remote !== void 0 ? clientFor(parsed.remote, io.env) : void 0;
271
326
  setup = await buildSetup(config, chatIo);
272
327
  for (const skip of setup.skipped.slice(0, MAX_SKIPPED_LINES)) io.err(`skipped tool ${oneLine(skip.name, 100)}: ${oneLine(skip.reason, 200)}`);
273
328
  if (setup.skipped.length > MAX_SKIPPED_LINES) io.err(`skipped ${setup.skipped.length - MAX_SKIPPED_LINES} more tools`);
274
329
  if (parsed.command === "mcp") {
275
- const { startAssistantMcp } = await import("./mcp-server-ZPCPYMCT.js");
276
- await startAssistantMcp(setup);
330
+ const { startAssistantMcp } = await import("./mcp-server-U3UQIB7H.js");
331
+ await startAssistantMcp(setup, remote, { approve: parsed.allowApprove ?? false });
277
332
  return 0;
278
333
  }
279
334
  const text = parsed.message ?? (io.isTTY ? void 0 : (await io.readStdin()).trim());
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as AssistantEvent, a as ApprovalRequest, b as Actor } from './loop-D4qWbRu6.js';
2
- export { D as DeniedReason, c as DoneReason, R as RunTotals } from './loop-D4qWbRu6.js';
1
+ import { h as AssistantEvent, e as ApprovalRequest, b as Actor } from './loop-B6aowbyz.js';
2
+ export { j as DeniedReason, k as DoneReason, p as RunTotals } from './loop-B6aowbyz.js';
3
3
  import './types-DGH8FS56.js';
4
4
 
5
5
  /** The error code of a failed call. The client derives it from the HTTP status and the error body. */
package/dist/client.js CHANGED
@@ -1,194 +1,7 @@
1
- // src/client.ts
2
- var STATUS_CODES = {
3
- 400: "INVALID_INPUT",
4
- 401: "UNAUTHENTICATED",
5
- 403: "FORBIDDEN",
6
- 404: "NOT_FOUND",
7
- 405: "METHOD_NOT_ALLOWED",
8
- 409: "CONFLICT",
9
- 429: "RATE_LIMITED"
10
- };
11
- var AssistantApiError = class extends Error {
12
- code;
13
- status;
14
- /** Seconds to wait before a new try, from `retry-after` of a 429 reply. */
15
- retryAfter;
16
- constructor(code, status, message, retryAfter) {
17
- super(message);
18
- this.name = "AssistantApiError";
19
- this.code = code;
20
- this.status = status;
21
- this.retryAfter = retryAfter;
22
- }
23
- };
24
- var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
25
- var isString = (v) => typeof v === "string";
26
- var RISKS = ["safe", "risky", "irreversible"];
27
- var isTool = (v) => isObject(v) && isString(v.name) && isString(v.description) && isString(v.scope) && RISKS.includes(v.risk) && "inputSchema" in v;
28
- var isPending = (v) => isObject(v) && isString(v.id) && isString(v.tool) && RISKS.includes(v.risk) && isString(v.digest) && isObject(v.input) && isObject(v.proposedBy) && isString(v.requestedBy) && typeof v.canDecide === "boolean";
29
- var SHAPES = {
30
- health: (v) => isObject(v) && typeof v.ok === "boolean" && isString(v.pack) && isString(v.version),
31
- tools: (v) => isObject(v) && Array.isArray(v.tools) && v.tools.every(isTool),
32
- approvals: (v) => isObject(v) && Array.isArray(v.approvals) && v.approvals.every(isPending),
33
- decision: (v) => isObject(v) && isString(v.requestId) && typeof v.approved === "boolean" && isString(v.decidedBy) && typeof v.machineMade === "boolean"
34
- };
35
- var MAX_EVENT_CHARS = 4194304;
36
- var EVENT_TYPES = ["run_start", "turn_start", "text_delta", "tool_call", "approval_request", "approval_decision", "tool_result", "turn_end", "done", "error"];
37
- var isEvent = (v) => {
38
- if (!isObject(v) || typeof v.type !== "string" || !EVENT_TYPES.includes(v.type)) return false;
39
- switch (v.type) {
40
- case "done":
41
- return isString(v.reason) && isObject(v.totals);
42
- case "error":
43
- return isString(v.error);
44
- case "text_delta":
45
- return isString(v.text);
46
- case "tool_result":
47
- return isString(v.id) && isString(v.name) && isString(v.content) && typeof v.isError === "boolean";
48
- case "tool_call":
49
- return isString(v.id) && isString(v.name);
50
- case "approval_request":
51
- return isObject(v.request) && isString(v.request.id) && isString(v.request.digest);
52
- case "approval_decision":
53
- return isString(v.requestId) && typeof v.approved === "boolean";
54
- default:
55
- return true;
56
- }
57
- };
58
- function createClient(options) {
59
- if (typeof options.baseUrl !== "string" || options.baseUrl === "") throw new TypeError("createClient needs a baseUrl");
60
- if (typeof options.token !== "string" || options.token === "") throw new TypeError("createClient needs a token");
61
- let origin;
62
- try {
63
- origin = new URL(options.baseUrl);
64
- } catch {
65
- throw new TypeError("baseUrl must be an absolute URL");
66
- }
67
- const local = ["localhost", "127.0.0.1", "[::1]"].includes(origin.hostname);
68
- if (origin.protocol !== "https:" && !(origin.protocol === "http:" && local)) {
69
- throw new TypeError("baseUrl must be an https URL, or an http URL on localhost, 127.0.0.1, or [::1]");
70
- }
71
- if (origin.search !== "" || origin.hash !== "" || origin.username !== "" || origin.password !== "") {
72
- throw new TypeError("baseUrl must have no query, hash, or credentials");
73
- }
74
- const prefix = origin.pathname.replace(/\/+$/, "");
75
- const root = `${origin.origin}${prefix}/api/v1`;
76
- const token = options.token;
77
- const send = (input, init) => {
78
- const run = options.fetch ?? globalThis.fetch;
79
- return run(input, init);
80
- };
81
- const seg = (value) => {
82
- if (typeof value !== "string" || /^(?:\.|%2e){0,2}$/i.test(value)) throw new TypeError("an id must be a non-empty string that is not . or ..");
83
- return encodeURIComponent(value);
84
- };
85
- async function request(url, method, body, callOptions, authed = true) {
86
- const headers = authed ? { authorization: `Bearer ${token}` } : {};
87
- if (body !== void 0) headers["content-type"] = "application/json";
88
- return send(url, {
89
- method,
90
- headers,
91
- body: body === void 0 ? void 0 : JSON.stringify(body),
92
- signal: callOptions?.signal,
93
- // Never follow a redirect: it would send the token to another host.
94
- redirect: "manual"
95
- });
96
- }
97
- async function failure(response) {
98
- let parsed;
99
- try {
100
- parsed = JSON.parse(await response.text());
101
- } catch {
102
- parsed = void 0;
103
- }
104
- const error = isObject(parsed) ? parsed.error : void 0;
105
- const message = isObject(parsed) && typeof parsed.message === "string" ? parsed.message : typeof error === "string" ? error : `HTTP ${response.status}`;
106
- let code = STATUS_CODES[response.status] ?? (response.status >= 500 ? "INTERNAL" : "UNKNOWN");
107
- if (response.status === 503 && error === "not_configured") code = "NOT_CONFIGURED";
108
- if (typeof error !== "string") code = response.status >= 500 ? "INTERNAL" : "UNKNOWN";
109
- const after = response.headers.get("retry-after")?.trim() ?? "";
110
- return new AssistantApiError(code, response.status, message, /^\d{1,9}$/.test(after) ? Number(after) : void 0);
111
- }
112
- async function call(url, method, body, callOptions, shape, authed = true) {
113
- const response = await request(url, method, body, callOptions, authed);
114
- if (!response.ok) throw await failure(response);
115
- let parsed;
116
- try {
117
- parsed = JSON.parse(await response.text());
118
- } catch {
119
- parsed = void 0;
120
- }
121
- if (!SHAPES[shape](parsed)) throw new AssistantApiError("UNKNOWN", response.status, `the reply is not a ${shape}`);
122
- return parsed;
123
- }
124
- async function* chat(message, callOptions) {
125
- const response = await request(`${root}/chat`, "POST", { message }, callOptions);
126
- if (!response.ok) throw await failure(response);
127
- if (!(response.headers.get("content-type") ?? "").startsWith("text/event-stream") || response.body === null) {
128
- await response.body?.cancel();
129
- throw new AssistantApiError("UNKNOWN", response.status, "the reply is not an event stream");
130
- }
131
- const reader = response.body.getReader();
132
- const decoder = new TextDecoder();
133
- let buffer = "";
134
- let ended = false;
135
- function* drain(final) {
136
- const text = final ? buffer : buffer.replace(/\r$/, "");
137
- const normal = text.replace(/\r\n?/g, "\n");
138
- const rest = buffer.slice(text.length);
139
- let from = 0;
140
- for (let end = normal.indexOf("\n\n", from); end >= 0; end = normal.indexOf("\n\n", from)) {
141
- const event = parseBlock(normal.slice(from, end), response.status);
142
- from = end + 2;
143
- if (event === void 0) continue;
144
- if (ended) throw new AssistantApiError("UNKNOWN", response.status, "an event came after the end of the stream");
145
- ended = event.type === "done" || event.type === "error";
146
- yield event;
147
- }
148
- buffer = normal.slice(from) + rest;
149
- }
150
- try {
151
- for (; ; ) {
152
- const chunk = await reader.read();
153
- if (chunk.done) break;
154
- buffer += decoder.decode(chunk.value, { stream: true });
155
- yield* drain(false);
156
- if (buffer.length > MAX_EVENT_CHARS) throw new AssistantApiError("UNKNOWN", response.status, "an event is too long");
157
- }
158
- buffer += decoder.decode();
159
- yield* drain(true);
160
- if (!ended || buffer.trim() !== "") throw new AssistantApiError("UNKNOWN", response.status, "the stream ended before a done event");
161
- } finally {
162
- await reader.cancel().catch(() => void 0);
163
- }
164
- }
165
- function parseBlock(block, status) {
166
- const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
167
- if (data === "") return void 0;
168
- let parsed;
169
- try {
170
- parsed = JSON.parse(data);
171
- } catch {
172
- parsed = void 0;
173
- }
174
- if (!isEvent(parsed)) throw new AssistantApiError("UNKNOWN", status, "an event is not valid");
175
- return parsed;
176
- }
177
- const decided = (id, decision) => {
178
- if (decision.requestId !== id) throw new AssistantApiError("UNKNOWN", 200, "the reply is for another approval");
179
- return decision;
180
- };
181
- const methods = {
182
- // /health is outside /api/v1 and needs no token, so the client sends none.
183
- health: async (o) => call(`${origin.origin}${prefix}/health`, "GET", void 0, o, "health", false),
184
- tools: async (o) => (await call(`${root}/tools`, "GET", void 0, o, "tools")).tools,
185
- chat,
186
- approvals: async (o) => (await call(`${root}/approvals`, "GET", void 0, o, "approvals")).approvals,
187
- approve: async (id, digest, o) => decided(id, await call(`${root}/approvals/${seg(id)}/approve`, "POST", { digest }, o, "decision")),
188
- deny: async (id, reason, o) => decided(id, await call(`${root}/approvals/${seg(id)}/deny`, "POST", reason === void 0 ? {} : { reason }, o, "decision"))
189
- };
190
- return methods;
191
- }
1
+ import {
2
+ AssistantApiError,
3
+ createClient
4
+ } from "./chunk-QTIEDY3V.js";
192
5
  export {
193
6
  AssistantApiError,
194
7
  createClient
package/dist/demo.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import {
2
2
  createStaticTokenAuth,
3
3
  createWorker
4
- } from "./chunk-GLU222OQ.js";
4
+ } from "./chunk-PRQTL5VS.js";
5
5
  import {
6
6
  AnthropicProvider
7
- } from "./chunk-DXTIEMOT.js";
7
+ } from "./chunk-XSRGG7SB.js";
8
8
  import {
9
9
  ToolRegistry,
10
- VERSION
11
- } from "./chunk-3LKTJJ75.js";
10
+ VERSION,
11
+ hasScope
12
+ } from "./chunk-RHGVOR6N.js";
12
13
 
13
14
  // src/demo-page.ts
14
15
  var DEMO_CSP = "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
@@ -103,6 +104,7 @@ var demoRegistry = () => new ToolRegistry().register({
103
104
  risk: "safe",
104
105
  handler: () => ({ now: (/* @__PURE__ */ new Date()).toISOString() })
105
106
  });
107
+ var DEMO_SCOPE = "demo:read";
106
108
  var DEMO_SYSTEM = "You are a demo assistant. You may use the demo_time tool. Answer in short, plain text.";
107
109
  function modelOptions(env) {
108
110
  if (!env.MODEL_KEY) return void 0;
@@ -117,6 +119,8 @@ async function buildDemo(env) {
117
119
  const auth = await createStaticTokenAuth(tokens);
118
120
  const worker = createWorker({
119
121
  auth,
122
+ // A token without `demo:read` signs in but may use no route.
123
+ routeScopes: { tools: DEMO_SCOPE, chat: DEMO_SCOPE, approvals: DEMO_SCOPE },
120
124
  registry: demoRegistry(),
121
125
  system: DEMO_SYSTEM,
122
126
  limits: { maxTurns: 4, maxTotalTokens: 2e4, maxOutputTokensPerTurn: 1024 },
@@ -148,7 +152,8 @@ var demo_default = {
148
152
  return notConfigured("this demo is not set up");
149
153
  }
150
154
  if (request.method === "POST" && url.pathname.replace(/\/$/, "") === "/api/v1/chat" && modelOptions(env) === void 0) {
151
- if (await demo.auth.authenticate(request.clone()) !== null) return notConfigured("this demo has no model key or model endpoint set");
155
+ const who = await demo.auth.authenticate(request.clone());
156
+ if (who !== null && hasScope(who.scopes, DEMO_SCOPE)) return notConfigured("this demo has no model key or model endpoint set");
152
157
  }
153
158
  return demo.worker.fetch(request, env);
154
159
  }