agentic-assistant 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +139 -7
- package/SKILL.md +17 -0
- package/dist/chat-CXbhePC5.d.ts +120 -0
- package/dist/chunk-3LKTJJ75.js +846 -0
- package/dist/chunk-3X6KU636.js +569 -0
- package/dist/chunk-DXTIEMOT.js +142 -0
- package/dist/chunk-GLU222OQ.js +302 -0
- package/dist/chunk-HYT732TN.js +36 -0
- package/dist/chunk-T5WVSGYQ.js +82 -0
- package/dist/cli.d.ts +27 -2
- package/dist/cli.js +303 -19
- package/dist/client.d.ts +78 -0
- package/dist/client.js +195 -0
- package/dist/demo.d.ts +21 -0
- package/dist/demo.js +158 -0
- package/dist/index.d.ts +38 -63
- package/dist/index.js +55 -975
- package/dist/{loop-CDbbDwSs.d.ts → loop-D4qWbRu6.d.ts} +66 -7
- package/dist/mcp-server-ZPCPYMCT.js +12 -0
- package/dist/react.d.ts +2 -2
- package/dist/react.js +9 -3
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +3 -32
- package/dist/{types-C47SyHH5.d.ts → types-DGH8FS56.d.ts} +13 -1
- package/dist/worker.d.ts +44 -3
- package/dist/worker.js +10 -14
- package/llms.txt +4 -0
- package/package.json +17 -4
- package/dist/chunk-4QE7E5UQ.js +0 -7
package/dist/cli.js
CHANGED
|
@@ -1,30 +1,307 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
ConfigError,
|
|
4
|
+
buildSetup,
|
|
5
|
+
readTextFile,
|
|
6
|
+
runChat
|
|
7
|
+
} from "./chunk-3X6KU636.js";
|
|
8
|
+
import "./chunk-HYT732TN.js";
|
|
9
|
+
import "./chunk-DXTIEMOT.js";
|
|
10
|
+
import {
|
|
11
|
+
TOOL_RISKS,
|
|
12
|
+
VERSION,
|
|
13
|
+
printable,
|
|
14
|
+
provenanceLabel
|
|
15
|
+
} from "./chunk-3LKTJJ75.js";
|
|
5
16
|
|
|
6
17
|
// src/cli.ts
|
|
7
18
|
import { realpathSync } from "fs";
|
|
19
|
+
import { createInterface } from "readline/promises";
|
|
8
20
|
import { pathToFileURL } from "url";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
import { parseArgs } from "util";
|
|
22
|
+
var UsageError = class extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "UsageError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var HELP = `agentic-assistant ${VERSION}
|
|
29
|
+
An AI assistant that acts inside your apps with the user's own permissions.
|
|
30
|
+
|
|
31
|
+
Usage:
|
|
32
|
+
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.
|
|
34
|
+
|
|
35
|
+
Flags for chat and mcp:
|
|
36
|
+
--tools <target> An https MCP URL, or a command that starts an MCP server on stdio. Required.
|
|
37
|
+
--tools-header-env <h=E> Send header h with the value of environment variable E (http only). Repeat it.
|
|
38
|
+
--scope <scope> A scope the assistant may hold, like notes:read. Repeat it. Required.
|
|
39
|
+
--tool-scope <scope> The scope each tool needs. Required.
|
|
40
|
+
--tool-risk <risk> safe, risky, or irreversible, for each tool. Default risky.
|
|
41
|
+
--approve-call <json> Approve one exact call before the run: {"tool":"...","input":{...}}. Repeat it.
|
|
42
|
+
--approve <ask|deny> ask: question a person on the terminal (chat only). Default deny.
|
|
43
|
+
--provider <name> anthropic (default), openai-compatible, or scripted (for tests).
|
|
44
|
+
--model <id> Model id. Default for anthropic: claude-sonnet-5-5.
|
|
45
|
+
--fallback-model <id> A second model on the same provider. It answers once when --model refuses or fails.
|
|
46
|
+
--base-url <url> Endpoint. Required for openai-compatible. https, or localhost.
|
|
47
|
+
--use-default-endpoint Call api.anthropic.com, a US-hosted endpoint. Anthropic only.
|
|
48
|
+
--api-key-env <NAME> Read the API key from this environment variable. Keys never go in arguments.
|
|
49
|
+
--price-in <n> --price-out <n> US dollars per million tokens, for the cost cap.
|
|
50
|
+
--script <file> Turns for --provider scripted.
|
|
51
|
+
--system <text> Extra system text.
|
|
52
|
+
--user <id> The person the assistant acts for. Default cli-user.
|
|
53
|
+
--max-turns <n> --max-tokens <n> --max-cost <usd> --max-tool-calls <n> Run limits.
|
|
54
|
+
--json Print one JSON event per line on stdout (chat).
|
|
55
|
+
|
|
56
|
+
Other:
|
|
57
|
+
--help, -h Print this help.
|
|
58
|
+
--version, -V Print the version.
|
|
59
|
+
|
|
60
|
+
Exit codes: 0 the run ended normally, 1 it stopped for another reason or failed, 2 bad usage.
|
|
61
|
+
`;
|
|
62
|
+
var OPTIONS = {
|
|
63
|
+
tools: { type: "string" },
|
|
64
|
+
"tools-header-env": { type: "string", multiple: true },
|
|
65
|
+
scope: { type: "string", multiple: true },
|
|
66
|
+
"tool-scope": { type: "string" },
|
|
67
|
+
"tool-risk": { type: "string" },
|
|
68
|
+
"approve-call": { type: "string", multiple: true },
|
|
69
|
+
approve: { type: "string" },
|
|
70
|
+
provider: { type: "string" },
|
|
71
|
+
model: { type: "string" },
|
|
72
|
+
"fallback-model": { type: "string" },
|
|
73
|
+
"base-url": { type: "string" },
|
|
74
|
+
"use-default-endpoint": { type: "boolean" },
|
|
75
|
+
"api-key-env": { type: "string" },
|
|
76
|
+
"price-in": { type: "string" },
|
|
77
|
+
"price-out": { type: "string" },
|
|
78
|
+
script: { type: "string" },
|
|
79
|
+
system: { type: "string" },
|
|
80
|
+
user: { type: "string" },
|
|
81
|
+
"max-turns": { type: "string" },
|
|
82
|
+
"max-tokens": { type: "string" },
|
|
83
|
+
"max-cost": { type: "string" },
|
|
84
|
+
"max-tool-calls": { type: "string" },
|
|
85
|
+
json: { type: "boolean" },
|
|
86
|
+
message: { type: "string" },
|
|
87
|
+
help: { type: "boolean", short: "h" },
|
|
88
|
+
version: { type: "boolean", short: "V" }
|
|
89
|
+
};
|
|
90
|
+
function number(name, value, integer) {
|
|
91
|
+
if (value === void 0) return void 0;
|
|
92
|
+
const parsed = Number(value);
|
|
93
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || integer && !Number.isInteger(parsed)) {
|
|
94
|
+
throw new UsageError(`${name} needs a positive ${integer ? "whole " : ""}number, got "${value}"`);
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
function parseCli(argv) {
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new UsageError(error instanceof Error ? error.message : String(error));
|
|
104
|
+
}
|
|
105
|
+
const { values, positionals } = parsed;
|
|
106
|
+
if (values.version) return { command: "version", json: false };
|
|
107
|
+
const [command, ...rest] = positionals;
|
|
108
|
+
if (values.help || command === void 0) return { command: "help", json: false };
|
|
109
|
+
if (command !== "chat" && command !== "mcp") throw new UsageError(`unknown command "${command}"`);
|
|
110
|
+
if (!values.tools) throw new UsageError("--tools is required");
|
|
111
|
+
if (!values["tool-scope"]) throw new UsageError("--tool-scope is required");
|
|
112
|
+
const risk = values["tool-risk"] ?? "risky";
|
|
113
|
+
if (!TOOL_RISKS.includes(risk)) throw new UsageError(`--tool-risk must be one of ${TOOL_RISKS.join(", ")}`);
|
|
114
|
+
const provider = values.provider ?? "anthropic";
|
|
115
|
+
if (provider !== "anthropic" && provider !== "openai-compatible" && provider !== "scripted") {
|
|
116
|
+
throw new UsageError("--provider must be anthropic, openai-compatible, or scripted");
|
|
117
|
+
}
|
|
118
|
+
const approve = values.approve ?? "deny";
|
|
119
|
+
if (approve !== "ask" && approve !== "deny") throw new UsageError("--approve must be ask or deny");
|
|
120
|
+
if (command === "mcp" && approve === "ask") throw new UsageError("--approve ask works with chat only");
|
|
121
|
+
const preApproved = (values["approve-call"] ?? []).map((text) => {
|
|
122
|
+
try {
|
|
123
|
+
const call = JSON.parse(text);
|
|
124
|
+
if (typeof call.tool !== "string" || typeof call.input !== "object" || call.input === null || Array.isArray(call.input)) throw new Error("shape");
|
|
125
|
+
return { tool: call.tool, input: call.input };
|
|
126
|
+
} catch {
|
|
127
|
+
throw new UsageError(`--approve-call needs JSON like {"tool":"name","input":{}}, got ${text.slice(0, 80)}`);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
if (values["fallback-model"] === "") throw new UsageError("--fallback-model needs a model id");
|
|
131
|
+
const priceIn = number("--price-in", values["price-in"], false);
|
|
132
|
+
const priceOut = number("--price-out", values["price-out"], false);
|
|
133
|
+
if (priceIn === void 0 !== (priceOut === void 0)) throw new UsageError("--price-in and --price-out go together");
|
|
134
|
+
const pricing = priceIn !== void 0 && priceOut !== void 0 ? { inputPerMTok: priceIn, outputPerMTok: priceOut } : void 0;
|
|
135
|
+
const limits = {
|
|
136
|
+
maxTurns: number("--max-turns", values["max-turns"], true),
|
|
137
|
+
maxTotalTokens: number("--max-tokens", values["max-tokens"], true),
|
|
138
|
+
maxCostUsd: number("--max-cost", values["max-cost"], false),
|
|
139
|
+
maxToolCallsPerTurn: number("--max-tool-calls", values["max-tool-calls"], true)
|
|
140
|
+
};
|
|
141
|
+
for (const key of Object.keys(limits)) if (limits[key] === void 0) delete limits[key];
|
|
142
|
+
const config = {
|
|
143
|
+
tools: values.tools,
|
|
144
|
+
toolHeaderEnv: values["tools-header-env"] ?? [],
|
|
145
|
+
scopes: values.scope ?? [],
|
|
146
|
+
toolScope: values["tool-scope"],
|
|
147
|
+
toolRisk: risk,
|
|
148
|
+
provider,
|
|
149
|
+
model: values.model,
|
|
150
|
+
fallbackModel: values["fallback-model"],
|
|
151
|
+
baseUrl: values["base-url"],
|
|
152
|
+
useDefaultEndpoint: values["use-default-endpoint"] ?? false,
|
|
153
|
+
apiKeyEnv: values["api-key-env"],
|
|
154
|
+
pricing,
|
|
155
|
+
script: values.script,
|
|
156
|
+
system: values.system,
|
|
157
|
+
user: values.user ?? "cli-user",
|
|
158
|
+
limits,
|
|
159
|
+
preApproved,
|
|
160
|
+
approve
|
|
161
|
+
};
|
|
162
|
+
return { command, config, message: values.message ?? (rest.length > 0 ? rest.join(" ") : void 0), json: values.json ?? false };
|
|
163
|
+
}
|
|
164
|
+
function defaultIo() {
|
|
165
|
+
return {
|
|
166
|
+
out: (text) => void process.stdout.write(text),
|
|
167
|
+
err: (line) => console.error(line),
|
|
168
|
+
env: process.env,
|
|
169
|
+
readFile: readTextFile,
|
|
170
|
+
readStdin: async () => {
|
|
171
|
+
const chunks = [];
|
|
172
|
+
let size = 0;
|
|
173
|
+
for await (const chunk of process.stdin) {
|
|
174
|
+
size += chunk.length;
|
|
175
|
+
if (size > MAX_STDIN_BYTES) throw new UsageError(`the message on stdin is over ${MAX_STDIN_BYTES} bytes`);
|
|
176
|
+
chunks.push(chunk);
|
|
177
|
+
}
|
|
178
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
179
|
+
},
|
|
180
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
181
|
+
ask: async (question) => {
|
|
182
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
183
|
+
try {
|
|
184
|
+
return await rl.question(question);
|
|
185
|
+
} finally {
|
|
186
|
+
rl.close();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
var oneLine = (value, max = 200) => {
|
|
192
|
+
const text = printable(typeof value === "string" ? value : JSON.stringify(value) ?? "null").replace(/\s+/g, " ");
|
|
193
|
+
const points = Array.from(text);
|
|
194
|
+
return points.length > max ? `${points.slice(0, max).join("")}...` : text;
|
|
195
|
+
};
|
|
196
|
+
var MAX_STDIN_BYTES = 1e6;
|
|
197
|
+
var MAX_SKIPPED_LINES = 20;
|
|
198
|
+
function render(event, io, state) {
|
|
199
|
+
const endText = () => {
|
|
200
|
+
if (state.inText) io.out("\n");
|
|
201
|
+
state.inText = false;
|
|
202
|
+
};
|
|
203
|
+
switch (event.type) {
|
|
204
|
+
case "text_delta":
|
|
205
|
+
if (!state.inText) io.out("Assistant (machine-made): ");
|
|
206
|
+
state.inText = true;
|
|
207
|
+
io.out(printable(event.text));
|
|
208
|
+
break;
|
|
209
|
+
case "turn_end":
|
|
210
|
+
endText();
|
|
211
|
+
if (event.answeredBy.fallback) {
|
|
212
|
+
io.err(`fallback model answered: ${oneLine(event.answeredBy.model, 100)} (first model failed: ${event.answeredBy.fallbackFor ?? "unknown"})`);
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
case "tool_call":
|
|
216
|
+
endText();
|
|
217
|
+
io.err(`[machine-made] tool call: ${oneLine(event.name, 100)} ${oneLine(event.input)}`);
|
|
218
|
+
break;
|
|
219
|
+
case "approval_request":
|
|
220
|
+
endText();
|
|
221
|
+
io.err(`[machine-made] approval needed: ${oneLine(event.request.tool, 100)} (${event.request.risk}) ${oneLine(event.request.input)}`);
|
|
222
|
+
break;
|
|
223
|
+
case "approval_decision":
|
|
224
|
+
io.err(`approval: ${event.approved ? `approved by ${oneLine(event.decidedBy ?? "unknown", 100)}` : `not approved (${oneLine(event.reason ?? "no reason", 200)})`}`);
|
|
225
|
+
break;
|
|
226
|
+
case "tool_result":
|
|
227
|
+
io.err(`tool result (${event.isError ? "error" : "ok"}${event.denied ? `, ${event.denied}` : ""}): ${oneLine(event.content)}`);
|
|
228
|
+
break;
|
|
229
|
+
case "done":
|
|
230
|
+
endText();
|
|
231
|
+
io.err(`done: ${event.reason}. tokens in ${event.totals.inputTokens}, out ${event.totals.outputTokens}. cost $${event.totals.costUsd.toFixed(6)} (estimate)${event.error ? `. error: ${oneLine(event.error)}` : ""}`);
|
|
232
|
+
if (event.provenance !== void 0) io.err(oneLine(provenanceLabel(event.provenance), 300));
|
|
233
|
+
break;
|
|
234
|
+
default:
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function runOnce(setup, messages, json, io) {
|
|
239
|
+
const state = { inText: false };
|
|
240
|
+
let last;
|
|
241
|
+
for await (const event of runChat(setup, messages)) {
|
|
242
|
+
if (json) io.out(`${JSON.stringify(event)}
|
|
243
|
+
`);
|
|
244
|
+
else render(event, io, state);
|
|
245
|
+
if (event.type === "done") last = event;
|
|
246
|
+
}
|
|
247
|
+
return { code: last?.reason === "end_turn" ? 0 : 1, transcript: last?.messages ?? messages };
|
|
248
|
+
}
|
|
249
|
+
async function runCli(argv, io = defaultIo()) {
|
|
250
|
+
let parsed;
|
|
251
|
+
try {
|
|
252
|
+
parsed = parseCli(argv);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
io.err(error instanceof Error ? error.message : String(error));
|
|
255
|
+
io.err('Run "agentic-assistant --help" for usage.');
|
|
256
|
+
return 2;
|
|
257
|
+
}
|
|
258
|
+
if (parsed.command === "help") {
|
|
259
|
+
io.out(HELP);
|
|
19
260
|
return 0;
|
|
20
261
|
}
|
|
21
|
-
if (
|
|
22
|
-
out(VERSION
|
|
262
|
+
if (parsed.command === "version") {
|
|
263
|
+
io.out(`${VERSION}
|
|
264
|
+
`);
|
|
23
265
|
return 0;
|
|
24
266
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
267
|
+
const config = parsed.config;
|
|
268
|
+
const chatIo = { env: io.env, readFile: io.readFile, ...config.approve === "ask" && io.isTTY ? { ask: io.ask } : {} };
|
|
269
|
+
let setup;
|
|
270
|
+
try {
|
|
271
|
+
setup = await buildSetup(config, chatIo);
|
|
272
|
+
for (const skip of setup.skipped.slice(0, MAX_SKIPPED_LINES)) io.err(`skipped tool ${oneLine(skip.name, 100)}: ${oneLine(skip.reason, 200)}`);
|
|
273
|
+
if (setup.skipped.length > MAX_SKIPPED_LINES) io.err(`skipped ${setup.skipped.length - MAX_SKIPPED_LINES} more tools`);
|
|
274
|
+
if (parsed.command === "mcp") {
|
|
275
|
+
const { startAssistantMcp } = await import("./mcp-server-ZPCPYMCT.js");
|
|
276
|
+
await startAssistantMcp(setup);
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
const text = parsed.message ?? (io.isTTY ? void 0 : (await io.readStdin()).trim());
|
|
280
|
+
let transcript = [];
|
|
281
|
+
if (text !== void 0 && text !== "") {
|
|
282
|
+
const { code: code2 } = await runOnce(setup, [{ role: "user", content: [{ type: "text", text }] }], parsed.json, io);
|
|
283
|
+
return code2;
|
|
284
|
+
}
|
|
285
|
+
if (!io.isTTY) throw new UsageError("give a message as an argument, with --message, or on stdin");
|
|
286
|
+
let code = 0;
|
|
287
|
+
for (; ; ) {
|
|
288
|
+
const line = (await io.ask("> ").catch(() => "exit")).trim();
|
|
289
|
+
if (line === "" || line === "exit") return code;
|
|
290
|
+
transcript = [...transcript, { role: "user", content: [{ type: "text", text: line }] }];
|
|
291
|
+
const result = await runOnce(setup, transcript, parsed.json, io);
|
|
292
|
+
transcript = result.transcript;
|
|
293
|
+
code = result.code;
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error instanceof ConfigError || error instanceof UsageError) {
|
|
297
|
+
io.err(`${error.name}: ${oneLine(error.message, 500)}`);
|
|
298
|
+
return 2;
|
|
299
|
+
}
|
|
300
|
+
io.err(`${error instanceof Error ? error.name : "Error"}: ${oneLine(error instanceof Error ? error.message : String(error), 500)}`);
|
|
301
|
+
return 1;
|
|
302
|
+
} finally {
|
|
303
|
+
await Promise.race([setup?.close().catch(() => void 0), new Promise((resolve) => setTimeout(resolve, 5e3).unref())]);
|
|
304
|
+
}
|
|
28
305
|
}
|
|
29
306
|
function isMainModule() {
|
|
30
307
|
const invoked = process.argv[1];
|
|
@@ -36,9 +313,16 @@ function isMainModule() {
|
|
|
36
313
|
}
|
|
37
314
|
}
|
|
38
315
|
if (isMainModule()) {
|
|
39
|
-
|
|
40
|
-
|
|
316
|
+
runCli(process.argv.slice(2)).then(
|
|
317
|
+
(code) => process.exit(code),
|
|
318
|
+
(error) => {
|
|
319
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
);
|
|
41
323
|
}
|
|
42
324
|
export {
|
|
325
|
+
UsageError,
|
|
326
|
+
parseCli,
|
|
43
327
|
runCli
|
|
44
328
|
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
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';
|
|
3
|
+
import './types-DGH8FS56.js';
|
|
4
|
+
|
|
5
|
+
/** The error code of a failed call. The client derives it from the HTTP status and the error body. */
|
|
6
|
+
type ApiErrorCode = "INVALID_INPUT" | "UNAUTHENTICATED" | "FORBIDDEN" | "NOT_FOUND" | "METHOD_NOT_ALLOWED" | "CONFLICT" | "RATE_LIMITED" | "NOT_CONFIGURED" | "INTERNAL" | "UNKNOWN";
|
|
7
|
+
/** Thrown for every reply that is not a success. `code` is the error code and `status` is the HTTP status. */
|
|
8
|
+
declare class AssistantApiError extends Error {
|
|
9
|
+
readonly code: ApiErrorCode;
|
|
10
|
+
readonly status: number;
|
|
11
|
+
/** Seconds to wait before a new try, from `retry-after` of a 429 reply. */
|
|
12
|
+
readonly retryAfter?: number;
|
|
13
|
+
constructor(code: ApiErrorCode, status: number, message: string, retryAfter?: number);
|
|
14
|
+
}
|
|
15
|
+
interface ClientOptions {
|
|
16
|
+
/** The Worker origin, for example https://assistant.example.com. The client adds /api/v1. */
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
/** A bearer credential that the Worker's auth accepts. */
|
|
19
|
+
token: string;
|
|
20
|
+
fetch?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
/** Every method takes an optional signal. An aborted call rejects with the signal reason, not an AssistantApiError. */
|
|
23
|
+
interface CallOptions {
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
interface ToolInfo {
|
|
27
|
+
name: string;
|
|
28
|
+
description: string;
|
|
29
|
+
scope: string;
|
|
30
|
+
risk: "safe" | "risky" | "irreversible";
|
|
31
|
+
inputSchema: unknown;
|
|
32
|
+
}
|
|
33
|
+
interface PendingApproval {
|
|
34
|
+
id: string;
|
|
35
|
+
tool: string;
|
|
36
|
+
risk: ApprovalRequest["risk"];
|
|
37
|
+
input: Record<string, unknown>;
|
|
38
|
+
/** Send this digest back to approve the exact call. */
|
|
39
|
+
digest: string;
|
|
40
|
+
proposedBy: Actor;
|
|
41
|
+
requestedBy: string;
|
|
42
|
+
/** False when this caller may not decide, for example an agent that asked for the turn. */
|
|
43
|
+
canDecide: boolean;
|
|
44
|
+
}
|
|
45
|
+
interface Decision {
|
|
46
|
+
requestId: string;
|
|
47
|
+
approved: boolean;
|
|
48
|
+
decidedBy: string;
|
|
49
|
+
/** True when the decider is not a person. */
|
|
50
|
+
machineMade: boolean;
|
|
51
|
+
}
|
|
52
|
+
interface Health {
|
|
53
|
+
ok: boolean;
|
|
54
|
+
pack: string;
|
|
55
|
+
version: string;
|
|
56
|
+
}
|
|
57
|
+
/** An event of the chat stream. `error` ends the stream: the run failed on the server. */
|
|
58
|
+
type ChatEvent = AssistantEvent | {
|
|
59
|
+
type: "error";
|
|
60
|
+
error: string;
|
|
61
|
+
};
|
|
62
|
+
interface AssistantClient {
|
|
63
|
+
health(options?: CallOptions): Promise<Health>;
|
|
64
|
+
tools(options?: CallOptions): Promise<ToolInfo[]>;
|
|
65
|
+
/**
|
|
66
|
+
* Yields the events of one new conversation, ending with `done` (or `error`). The request is sent on the
|
|
67
|
+
* first read, not when you call `chat`. The stream stays open while a risky call waits for a person.
|
|
68
|
+
* Stop reading, or abort the signal, to stop the run. A stream that ends with no `done` or `error` throws.
|
|
69
|
+
*/
|
|
70
|
+
chat(message: string, options?: CallOptions): AsyncGenerator<ChatEvent, void, undefined>;
|
|
71
|
+
approvals(options?: CallOptions): Promise<PendingApproval[]>;
|
|
72
|
+
/** Approves the exact call: `digest` comes from the pending call. A wrong digest is a CONFLICT. */
|
|
73
|
+
approve(id: string, digest: string, options?: CallOptions): Promise<Decision>;
|
|
74
|
+
deny(id: string, reason?: string, options?: CallOptions): Promise<Decision>;
|
|
75
|
+
}
|
|
76
|
+
declare function createClient(options: ClientOptions): AssistantClient;
|
|
77
|
+
|
|
78
|
+
export { Actor, type ApiErrorCode, ApprovalRequest, AssistantApiError, type AssistantClient, AssistantEvent, type CallOptions, type ChatEvent, type ClientOptions, type Decision, type Health, type PendingApproval, type ToolInfo, createClient };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
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
|
+
export {
|
|
193
|
+
AssistantApiError,
|
|
194
|
+
createClient
|
|
195
|
+
};
|
package/dist/demo.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface DemoEnv {
|
|
2
|
+
PACK_NAME?: string;
|
|
3
|
+
/** Secret. A JSON map of bearer token (32 bytes or more) to `{ "id": "...", "scopes": ["demo:read"] }`. Unset means every call is a 401. */
|
|
4
|
+
ASSISTANT_TOKENS?: string;
|
|
5
|
+
/** Secret. The model API key. Unset means chat answers 503 "not_configured". */
|
|
6
|
+
MODEL_KEY?: string;
|
|
7
|
+
/** Base URL of the model endpoint, for example an EU-hosted gateway. */
|
|
8
|
+
MODEL_URL?: string;
|
|
9
|
+
/** Set to "1" to call api.anthropic.com, a US-hosted endpoint, when `MODEL_URL` is not set. */
|
|
10
|
+
MODEL_USE_DEFAULT_ENDPOINT?: string;
|
|
11
|
+
MODEL_NAME?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The demo Worker. It serves a demo page at `/`, `/health`, and the `/api/v1` routes with bearer auth.
|
|
15
|
+
* It never fakes output: with no tokens every call is a 401, and with no model key chat answers 503.
|
|
16
|
+
*/
|
|
17
|
+
declare const _default: {
|
|
18
|
+
fetch(request: Request, env: DemoEnv): Promise<Response>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export { type DemoEnv, _default as default };
|