@maxceem/agw 0.12.1 → 0.13.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.
Files changed (2) hide show
  1. package/dist/agw.mjs +306 -110
  2. package/package.json +2 -1
package/dist/agw.mjs CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // <define:__AGW_MANIFEST__>
9
- var define_AGW_MANIFEST_default = { version: "0.12.1", wrangler: "4.96.0", upgradeFrom: ["0.12.1", "0.12.0", "0.11.2", "0.11.1", "0.11.0", "0.10.11", "0.10.10", "0.10.9", "0.10.8", "0.10.7", "0.10.6", "0.10.5", "0.10.4", "0.10.3", "0.10.2", "0.10.1", "0.10.0", "0.9.3", "0.9.2", "0.9.1", "0.9.0", "0.8.3", "0.8.2", "0.8.1", "0.8.0", "0.7.2", "0.7.1", "0.7.0", "0.6.4", "0.6.3", "0.6.2", "0.6.1", "0.6.0", "0.5.2", "0.5.1", "0.5.0", "0.4.0"], release: { url: "https://github.com/maxceem/app-ai-gateway/releases/download/v0.12.1/gateway-0.12.1.tar.gz", sha256: "c234a2d2f97d69a37e9ca2c6ac435e7d6718cceece957852719403dc1a4f421d", manifest: "1ae57d80d3c5f319c89b1fb34f365d59966311f897afd84227c48ded4707c791" } };
9
+ var define_AGW_MANIFEST_default = { version: "0.13.0", wrangler: "4.96.0", upgradeFrom: ["0.13.0", "0.12.1", "0.12.0", "0.11.2", "0.11.1", "0.11.0", "0.10.11", "0.10.10", "0.10.9", "0.10.8", "0.10.7", "0.10.6", "0.10.5", "0.10.4", "0.10.3", "0.10.2", "0.10.1", "0.10.0", "0.9.3", "0.9.2", "0.9.1", "0.9.0", "0.8.3", "0.8.2", "0.8.1", "0.8.0", "0.7.2", "0.7.1", "0.7.0", "0.6.4", "0.6.3", "0.6.2", "0.6.1", "0.6.0", "0.5.2", "0.5.1", "0.5.0", "0.4.0"], release: { url: "https://github.com/maxceem/app-ai-gateway/releases/download/v0.13.0/gateway-0.13.0.tar.gz", sha256: "02babcd3375a7412195b94c12ae93d4df10e4373ec29b15a496b900647abeb83", manifest: "f652db00f7a44d87c26779ce20a938a7cdefc98fdc8f5f52f2186f995d65a226" } };
10
10
 
11
11
  // src/main.ts
12
12
  import { Writable as Writable2 } from "node:stream";
@@ -15334,6 +15334,7 @@ var APP_ATTEST_VERIFY_ON = ["every_request", "token_exchange"];
15334
15334
  var ENDPOINT_SLUG = /^[a-z0-9-]{1,64}$/;
15335
15335
  var PROVIDER_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
15336
15336
  var HTTP_FIELD_NAME = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
15337
+ var CLIENT_TRACE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
15337
15338
  var MONTH_PATTERN = /^\d{4}-(?:0[1-9]|1[0-2])$/;
15338
15339
  var MONTH_FORMAT_MESSAGE = "month must use YYYY-MM format";
15339
15340
  var MonthSchema = external_exports.string().regex(MONTH_PATTERN, { error: MONTH_FORMAT_MESSAGE });
@@ -15426,6 +15427,8 @@ var RESERVED_APP_HEADERS = /* @__PURE__ */ new Set([
15426
15427
  "x-appattest-proof",
15427
15428
  ...PROVIDER_CREDENTIAL_HEADERS,
15428
15429
  "x-app-version",
15430
+ "x-client-trace-id",
15431
+ "x-conversation-storage",
15429
15432
  "content-type",
15430
15433
  "content-length",
15431
15434
  "host"
@@ -15577,6 +15580,22 @@ function identifiesEndUsers(authentication) {
15577
15580
  return authentication.end_user.source !== "none";
15578
15581
  }
15579
15582
  var scopeHasLimits = (scope) => scope.requests.per_minute !== null || scope.requests.per_day !== null || scope.spending.monthly_usd !== null;
15583
+ var CONVERSATION_RETENTION_DAYS = { default: 180, min: 1, max: 400 };
15584
+ var keptMedia = (field, what) => external_exports.boolean({ error: `conversations.keep.${field} must be true or false` }).default(true).meta({
15585
+ description: `Keep ${what} beside the conversation. Off, each is recorded by its type and size alone.`
15586
+ });
15587
+ var ConversationKeepSchema = external_exports.object({
15588
+ images: keptMedia("images", "images"),
15589
+ audio: keptMedia("audio", "audio: recordings sent for transcription, audio in a request, spoken replies"),
15590
+ files: keptMedia("files", "every other attached file, such as a PDF or a text document")
15591
+ }).strict();
15592
+ var ConversationsSchema = external_exports.object({
15593
+ store: external_exports.boolean({ error: "conversations.store must be true or false" }).default(false).meta({
15594
+ description: "Keep a copy of every request and response this app sends through the gateway, with the media `keep` allows."
15595
+ }),
15596
+ keep: ConversationKeepSchema.prefault({}),
15597
+ retention_days: external_exports.number({ error: "conversations.retention_days must be a whole number of days" }).int({ error: "conversations.retention_days must be a whole number of days" }).min(CONVERSATION_RETENTION_DAYS.min, { error: `conversations.retention_days must be at least ${CONVERSATION_RETENTION_DAYS.min}` }).max(CONVERSATION_RETENTION_DAYS.max, { error: `conversations.retention_days must be at most ${CONVERSATION_RETENTION_DAYS.max}` }).default(CONVERSATION_RETENTION_DAYS.default).meta({ description: "Days a stored conversation is kept before nightly maintenance deletes it." })
15598
+ }).strict();
15580
15599
  var AppConfigSchema = external_exports.object({
15581
15600
  authentication: external_exports.discriminatedUnion("type", [
15582
15601
  AppleAppAttestAuthenticationSchema,
@@ -15614,7 +15633,8 @@ var AppConfigSchema = external_exports.object({
15614
15633
  safeKey(external_exports.string().regex(ENDPOINT_SLUG)),
15615
15634
  EndpointSchema,
15616
15635
  { error: "is not a valid slug; use 1-64 characters from a-z, 0-9, and -" }
15617
- ).prefault({})
15636
+ ).prefault({}),
15637
+ conversations: ConversationsSchema.prefault({})
15618
15638
  }).strict().superRefine((config2, context) => {
15619
15639
  const authentication = config2.authentication;
15620
15640
  const perUser = config2.limits?.per_user;
@@ -16007,7 +16027,7 @@ async function prompt(label, flags, { hidden = false, defaultValue } = {}) {
16007
16027
  const wasRaw = stdin.isRaw;
16008
16028
  stdin.setRawMode(true);
16009
16029
  stdin.resume();
16010
- return new Promise((resolve3, reject2) => {
16030
+ return new Promise((resolve4, reject2) => {
16011
16031
  let value = "";
16012
16032
  let shown = 0;
16013
16033
  function mask() {
@@ -16021,7 +16041,7 @@ async function prompt(label, flags, { hidden = false, defaultValue } = {}) {
16021
16041
  stdin.pause();
16022
16042
  stderr.write("\n");
16023
16043
  if (error51) reject2(error51);
16024
- else resolve3(value);
16044
+ else resolve4(value);
16025
16045
  }
16026
16046
  function onData(chunk) {
16027
16047
  for (const char of chunk.toString()) {
@@ -16137,6 +16157,7 @@ var commands = {
16137
16157
  "app validate": { args: 0, flags: "file" },
16138
16158
  "app check": { args: 1, flags: "" },
16139
16159
  "app snippet": { args: 1, flags: "language provider endpoint output" },
16160
+ "app conversations": { args: 1, flags: "from to output media!" },
16140
16161
  "app remove": { args: 1, flags: "yes!" },
16141
16162
  "app key list": { args: 1, flags: "" },
16142
16163
  "app key add": { args: 1, flags: "name key-output" },
@@ -17396,9 +17417,138 @@ function snippetRemedy(code, appId, language) {
17396
17417
  }
17397
17418
  }
17398
17419
 
17420
+ // src/conversations.ts
17421
+ import { mkdir as mkdir2, readFile as readFile3, stat, writeFile } from "node:fs/promises";
17422
+ import { dirname, join as join2, resolve as resolve2 } from "node:path";
17423
+ import { gunzipSync } from "node:zlib";
17424
+
17425
+ // src/usage.ts
17426
+ var CLI_BREAKDOWNS = ["provider", "model", "status"];
17427
+ function month(value) {
17428
+ if (!MONTH_PATTERN.test(value))
17429
+ fail(
17430
+ "invalid_input",
17431
+ "Month must use YYYY-MM with a valid calendar month."
17432
+ );
17433
+ return value;
17434
+ }
17435
+ function date5(value) {
17436
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value)
17437
+ fail(
17438
+ "invalid_input",
17439
+ "Dates must be real UTC calendar dates in YYYY-MM-DD format."
17440
+ );
17441
+ return value;
17442
+ }
17443
+ function positive(value, max = 3600) {
17444
+ if (!/^\d+$/.test(String(value)) || Number(value) < 1 || Number(value) > max)
17445
+ fail("invalid_input", `Supply an integer between 1 and ${max}.`);
17446
+ return Number(value);
17447
+ }
17448
+ async function usageCommand(ctx, command, flags) {
17449
+ if (command === "usage show") {
17450
+ const m = month(flags.month ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 7));
17451
+ return flags.app ? (await ctx.call("getAppUsage", { params: { app: flags.app }, query: { month: m } })).data : (await ctx.call("getCliUsage", { query: { month: m } })).data;
17452
+ }
17453
+ const app2 = await required2(flags, "app");
17454
+ const by = CLI_BREAKDOWNS.find((dimension) => dimension === (flags.by ?? "model"));
17455
+ if (by === void 0)
17456
+ fail("invalid_input", "--by must be provider, model or status.");
17457
+ if (Boolean(flags.from) !== Boolean(flags.to))
17458
+ fail("invalid_input", "Supply both --from and --to or neither.");
17459
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
17460
+ const from = date5(flags.from ?? today.slice(0, 7) + "-01");
17461
+ const to = date5(flags.to ?? today);
17462
+ if (from > to) fail("invalid_input", "--from must not be later than --to.");
17463
+ const limit = positive(flags.limit ?? 50, 200);
17464
+ return {
17465
+ ...(await ctx.call("getAppUsageBreakdown", {
17466
+ params: { app: app2 },
17467
+ query: { from, to, by, limit }
17468
+ })).data,
17469
+ coverage: {
17470
+ from,
17471
+ to,
17472
+ source: "Raw events and retained daily aggregates; dimensions provider, model and status survive raw event retention."
17473
+ }
17474
+ };
17475
+ }
17476
+
17477
+ // src/conversations.ts
17478
+ var MEDIA_REFERENCE = /agw-media:(\d{4}-\d{2}-\d{2}\/media\/[0-9a-f]{64}\.[a-z0-9]{1,5})/g;
17479
+ async function sizeOf(path) {
17480
+ try {
17481
+ return (await stat(path)).size;
17482
+ } catch {
17483
+ return null;
17484
+ }
17485
+ }
17486
+ async function save(path, bytes) {
17487
+ await mkdir2(dirname(path), { recursive: true });
17488
+ await writeFile(path, bytes);
17489
+ }
17490
+ async function conversationsCommand(ctx, appId, flags) {
17491
+ const from = flags.from === void 0 ? null : date5(flags.from);
17492
+ const to = date5(flags.to ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
17493
+ if (from !== null && from > to) fail("invalid_input", "--from must not be later than --to.");
17494
+ const output = resolve2(flags.output ?? `conversations-${appId}`);
17495
+ await mkdir2(output, { recursive: true });
17496
+ const result = {
17497
+ app_id: appId,
17498
+ output,
17499
+ from,
17500
+ to,
17501
+ files: 0,
17502
+ downloaded: 0,
17503
+ media: 0,
17504
+ bytes: 0
17505
+ };
17506
+ const batches = [];
17507
+ let cursor;
17508
+ do {
17509
+ const page2 = (await ctx.call("listAppConversations", {
17510
+ params: { app: appId },
17511
+ query: { ...from ? { from } : {}, to, ...cursor ? { cursor } : {} }
17512
+ })).data;
17513
+ for (const file2 of page2.files) {
17514
+ result.files += 1;
17515
+ const path = join2(output, file2.name);
17516
+ batches.push(path);
17517
+ if (await sizeOf(path) === file2.size) continue;
17518
+ const bytes = await ctx.client.download("getAppConversationFile", {
17519
+ params: { app: appId },
17520
+ query: { name: file2.name }
17521
+ });
17522
+ await save(path, bytes);
17523
+ result.downloaded += 1;
17524
+ result.bytes += bytes.byteLength;
17525
+ }
17526
+ cursor = page2.next_cursor ?? void 0;
17527
+ } while (cursor !== void 0);
17528
+ if (flags.media) {
17529
+ const names = /* @__PURE__ */ new Set();
17530
+ for (const path of batches) {
17531
+ const text2 = gunzipSync(await readFile3(path)).toString("utf8");
17532
+ for (const match of text2.matchAll(MEDIA_REFERENCE)) names.add(match[1]);
17533
+ }
17534
+ for (const name of names) {
17535
+ const path = join2(output, name);
17536
+ if (await sizeOf(path) !== null) continue;
17537
+ const bytes = await ctx.client.download("getAppConversationFile", {
17538
+ params: { app: appId },
17539
+ query: { name }
17540
+ });
17541
+ await save(path, bytes);
17542
+ result.media += 1;
17543
+ result.bytes += bytes.byteLength;
17544
+ }
17545
+ }
17546
+ return result;
17547
+ }
17548
+
17399
17549
  // src/context.ts
17400
17550
  import { createHash as createHash2 } from "node:crypto";
17401
- import { resolve as resolve2 } from "node:path";
17551
+ import { resolve as resolve3 } from "node:path";
17402
17552
  import { spawn } from "node:child_process";
17403
17553
  import { setTimeout as delay2 } from "node:timers/promises";
17404
17554
 
@@ -17608,7 +17758,9 @@ var ConsoleCapabilitiesResponseSchema = external_exports.object({
17608
17758
  * has billing and can send email. The console still offers the menu only to
17609
17759
  * an organization on a paid plan, and the gateway refuses anyone else.
17610
17760
  */
17611
- supportEmail: external_exports.email().optional()
17761
+ supportEmail: external_exports.email().optional(),
17762
+ /** Whether this deployment can store conversations: an R2 bucket is bound for it. */
17763
+ conversationStorage: external_exports.boolean()
17612
17764
  });
17613
17765
  var GoogleSignInResponseSchema = external_exports.object({
17614
17766
  url: external_exports.url().meta({ description: "Provider authorization URL for the browser to navigate to." }),
@@ -17696,8 +17848,26 @@ var UsageEventSchema = external_exports.object({
17696
17848
  description: "1 when the client disconnected before the upstream finished streaming, which cancelled the provider call; null otherwise. Not a failure \u2014 the request was served as far as the caller wanted it \u2014 but an aborted stream often takes the provider's end-of-response usage with it, which is why such an event may carry cost_source unresolved."
17697
17849
  }),
17698
17850
  latency_ms: external_exports.number().int().nullable(),
17851
+ client_trace_id: external_exports.string().nullable().meta({
17852
+ description: "The client's X-Client-Trace-Id for this request, or null where it sent none."
17853
+ }),
17699
17854
  created_at: external_exports.string()
17700
17855
  }).meta({ id: "UsageEvent" });
17856
+ var ConversationFileSchema = external_exports.object({
17857
+ name: external_exports.string().meta({
17858
+ description: "The file's name within the app, `YYYY-MM-DD/HHMMSS-<batch>.jsonl.gz`; pass it to the download operation."
17859
+ }),
17860
+ size: external_exports.number().int().meta({ description: "Bytes, gzipped." }),
17861
+ records: external_exports.number().int().nullable().meta({ description: "Records in the file, where it says." }),
17862
+ uploaded_at: external_exports.string()
17863
+ }).meta({ id: "ConversationFile" });
17864
+ var ConversationFileListSchema = external_exports.object({
17865
+ app_id: external_exports.string(),
17866
+ files: external_exports.array(ConversationFileSchema),
17867
+ next_cursor: external_exports.string().nullable().meta({
17868
+ description: "Pass as cursor to continue the listing; null when it is complete."
17869
+ })
17870
+ }).meta({ id: "ConversationFileList" });
17701
17871
  var UsageEventListSchema = external_exports.object({
17702
17872
  app_id: external_exports.string(),
17703
17873
  limit: external_exports.number().int(),
@@ -18705,6 +18875,12 @@ var GatewayClientHeadersSchema = external_exports.object({
18705
18875
  }),
18706
18876
  "x-end-user-id": external_exports.string().optional().meta({
18707
18877
  description: "Optional configured end-user identity for issuer-less API-key applications."
18878
+ }),
18879
+ "x-client-trace-id": external_exports.string().regex(CLIENT_TRACE_ID_PATTERN).optional().meta({
18880
+ description: "Optional identifier of the client's own unit of work, such as one chat message that takes several requests. Recorded on the usage event and on the stored conversation, so both can be grouped and joined with the app's own data. 1-128 characters from A-Z, a-z, 0-9, `.`, `_`, `:` and `-`; anything else answers 400 invalid_request. Never forwarded to the provider."
18881
+ }),
18882
+ "x-conversation-storage": external_exports.enum(["off"]).optional().meta({
18883
+ description: "`off` keeps this request out of conversation storage even where the app stores conversations, for a user who has opted out. Any other value answers 400 invalid_request. Never forwarded to the provider."
18708
18884
  })
18709
18885
  });
18710
18886
  var MonthQuerySchema = external_exports.object({
@@ -19351,11 +19527,50 @@ var CATALOG = {
19351
19527
  user: external_exports.string().optional(),
19352
19528
  install: external_exports.string().optional().meta({ description: "Only events of this install: an App Attest key id." }),
19353
19529
  model: external_exports.string().optional(),
19530
+ trace: external_exports.string().regex(CLIENT_TRACE_ID_PATTERN).optional().meta({
19531
+ description: "Only the requests the client sent with this X-Client-Trace-Id."
19532
+ }),
19354
19533
  before_id: BeforeIdSchema
19355
19534
  }),
19356
19535
  response: UsageEventListSchema,
19357
19536
  responseDescription: "Paginated application usage events."
19358
19537
  },
19538
+ listAppConversations: {
19539
+ method: "GET",
19540
+ path: "/v1/admin/apps/{app}/conversations",
19541
+ tags: ["Admin operations"],
19542
+ summary: "List stored conversation files",
19543
+ description: "The batches of stored conversations for an app with conversations.store on, oldest first, by the UTC day of their earliest record. Each file is gzipped JSON lines, one request and its response per line. Media the app keeps (conversations.keep) is stored beside them as `<YYYY-MM-DD>/media/<sha256>.<ext>` and named in the records as `agw-media:<name>`.",
19544
+ security: "management",
19545
+ params: APP_PARAM,
19546
+ policy: { role: "admin", access: "read", grant: "read" },
19547
+ query: external_exports.object({
19548
+ from: DaySchema("from").optional().meta({
19549
+ description: "First UTC day, YYYY-MM-DD; defaults to the oldest stored."
19550
+ }),
19551
+ to: DaySchema("to").optional().meta({
19552
+ description: "Last UTC day, YYYY-MM-DD, inclusive; defaults to today."
19553
+ }),
19554
+ cursor: external_exports.string().max(1024).optional()
19555
+ }),
19556
+ response: ConversationFileListSchema,
19557
+ responseDescription: "One page of stored conversation files."
19558
+ },
19559
+ getAppConversationFile: {
19560
+ method: "GET",
19561
+ path: "/v1/admin/apps/{app}/conversations/file",
19562
+ tags: ["Admin operations"],
19563
+ summary: "Download a stored conversation file",
19564
+ description: "One file named by the listing, or an image a record names: a batch answers as application/gzip, an image as its own type.",
19565
+ security: "management",
19566
+ params: APP_PARAM,
19567
+ policy: { role: "admin", access: "read", grant: "read" },
19568
+ query: external_exports.object({
19569
+ name: external_exports.string().meta({ description: "A file name from the listing, or a media name a record gives, `<YYYY-MM-DD>/media/<sha256>.<ext>`." })
19570
+ }),
19571
+ response: external_exports.unknown(),
19572
+ responseDescription: "The file's bytes."
19573
+ },
19359
19574
  getAppAuthEventSummary: {
19360
19575
  method: "GET",
19361
19576
  path: "/v1/admin/apps/{app}/auth-events/summary",
@@ -19950,6 +20165,20 @@ var GatewayClient = class {
19950
20165
  throw error51;
19951
20166
  }
19952
20167
  }
20168
+ /**
20169
+ * One operation whose success is a file rather than JSON, with the
20170
+ * connection's management credential: its bytes, as the deployment sent them.
20171
+ */
20172
+ async download(name, { params, query } = {}) {
20173
+ const wire = await this.transport.request(
20174
+ this.source.url(),
20175
+ pathFor(name, params, query),
20176
+ { method: CATALOG[name].method, key: this.credential(name), binary: true }
20177
+ );
20178
+ if (!(wire.data instanceof Uint8Array))
20179
+ fail("invalid_response", `The deployment answered ${String(name)} with an unusable body.`, void 0, 3);
20180
+ return wire.data;
20181
+ }
19953
20182
  /**
19954
20183
  * The current management credential, or the refusal that names the way back.
19955
20184
  * An admin operation is also reachable from a plain deployment connection,
@@ -20003,8 +20232,8 @@ var FAILED_PAGE = page(
20003
20232
  );
20004
20233
  async function listenLoopback() {
20005
20234
  let deliver;
20006
- const code = new Promise((resolve3) => {
20007
- deliver = resolve3;
20235
+ const code = new Promise((resolve4) => {
20236
+ deliver = resolve4;
20008
20237
  });
20009
20238
  let waiting = null;
20010
20239
  let delivered = false;
@@ -20023,11 +20252,11 @@ async function listenLoopback() {
20023
20252
  waiting = response;
20024
20253
  deliver(value);
20025
20254
  });
20026
- await new Promise((resolve3, reject2) => {
20255
+ await new Promise((resolve4, reject2) => {
20027
20256
  server.once("error", reject2);
20028
20257
  server.listen(0, "127.0.0.1", () => {
20029
20258
  server.off("error", reject2);
20030
- resolve3();
20259
+ resolve4();
20031
20260
  });
20032
20261
  });
20033
20262
  const { port } = server.address();
@@ -20039,21 +20268,21 @@ async function listenLoopback() {
20039
20268
  const response = waiting;
20040
20269
  if (!response) return;
20041
20270
  waiting = null;
20042
- answered = new Promise((resolve3) => {
20043
- response.once("finish", resolve3);
20044
- response.once("close", resolve3);
20271
+ answered = new Promise((resolve4) => {
20272
+ response.once("finish", resolve4);
20273
+ response.once("close", resolve4);
20045
20274
  });
20046
20275
  response.writeHead(200, PAGE_HEADERS).end(connected ? CONNECTED_PAGE : FAILED_PAGE);
20047
20276
  },
20048
20277
  async close() {
20049
20278
  loopback.answer(false);
20050
20279
  await answered;
20051
- await new Promise((resolve3) => {
20280
+ await new Promise((resolve4) => {
20052
20281
  const force = setTimeout(() => server.closeAllConnections(), 1e3);
20053
20282
  force.unref();
20054
20283
  server.close(() => {
20055
20284
  clearTimeout(force);
20056
- resolve3();
20285
+ resolve4();
20057
20286
  });
20058
20287
  server.closeIdleConnections();
20059
20288
  });
@@ -20360,7 +20589,7 @@ Operation: ${data.id}
20360
20589
  );
20361
20590
  const target = origin(this.url);
20362
20591
  const [id, record2] = await this.reserveOperation(kind, payload, target);
20363
- const chosen = requestedPath ? resolve2(requestedPath) : record2.output?.path;
20592
+ const chosen = requestedPath ? resolve3(requestedPath) : record2.output?.path;
20364
20593
  if (record2.completedAt && chosen !== record2.output?.path)
20365
20594
  fail(
20366
20595
  "output_changed",
@@ -20861,29 +21090,29 @@ function assertTrustedApprovalUrl(url2, capabilities, target) {
20861
21090
  async function openBrowser(url2) {
20862
21091
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
20863
21092
  const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url2] : [url2];
20864
- return new Promise((resolve3) => {
21093
+ return new Promise((resolve4) => {
20865
21094
  const child = spawn(command, args, { stdio: "ignore", detached: true });
20866
- child.once("error", () => resolve3(false));
21095
+ child.once("error", () => resolve4(false));
20867
21096
  child.once("spawn", () => {
20868
21097
  child.unref();
20869
- resolve3(true);
21098
+ resolve4(true);
20870
21099
  });
20871
21100
  });
20872
21101
  }
20873
21102
 
20874
21103
  // src/deployment.ts
20875
21104
  import { createHash as createHash4, randomUUID } from "node:crypto";
20876
- import { writeFile as writeFile3, unlink as unlink2 } from "node:fs/promises";
20877
- import { join as join4 } from "node:path";
21105
+ import { writeFile as writeFile4, unlink as unlink2 } from "node:fs/promises";
21106
+ import { join as join5 } from "node:path";
20878
21107
 
20879
21108
  // src/release.ts
20880
- import { readFile as readFile3, lstat as lstat2, mkdir as mkdir2, rename as rename2, rm, writeFile } from "node:fs/promises";
20881
- import { join as join2, dirname } from "node:path";
21109
+ import { readFile as readFile4, lstat as lstat2, mkdir as mkdir3, rename as rename2, rm, writeFile as writeFile2 } from "node:fs/promises";
21110
+ import { join as join3, dirname as dirname2 } from "node:path";
20882
21111
  import { fileURLToPath } from "node:url";
20883
21112
  import { createHash as createHash3, randomBytes as randomBytes3 } from "node:crypto";
20884
21113
 
20885
21114
  // src/archive.ts
20886
- import { gunzipSync } from "node:zlib";
21115
+ import { gunzipSync as gunzipSync2 } from "node:zlib";
20887
21116
  var BLOCK = 512;
20888
21117
  var ARCHIVE_LIMIT = 64 * 1024 * 1024;
20889
21118
  function text(header, start, length) {
@@ -20920,7 +21149,7 @@ function safePath(raw, directory) {
20920
21149
  function readArchive(gzipped, limit = ARCHIVE_LIMIT) {
20921
21150
  let tar;
20922
21151
  try {
20923
- tar = gunzipSync(gzipped, { maxOutputLength: limit });
21152
+ tar = gunzipSync2(gzipped, { maxOutputLength: limit });
20924
21153
  } catch {
20925
21154
  fail("release_archive", "Release archive is not readable gzip within the size limit.");
20926
21155
  }
@@ -21001,26 +21230,26 @@ async function fetchArchive(url2) {
21001
21230
  return body;
21002
21231
  }
21003
21232
  async function extract(archive, directory) {
21004
- await mkdir2(directory, { recursive: true, mode: 448 });
21233
+ await mkdir3(directory, { recursive: true, mode: 448 });
21005
21234
  for (const entry of readArchive(archive)) {
21006
- const path = join2(directory, entry.path);
21235
+ const path = join3(directory, entry.path);
21007
21236
  if (entry.directory) {
21008
- await mkdir2(path, { recursive: true, mode: 448 });
21237
+ await mkdir3(path, { recursive: true, mode: 448 });
21009
21238
  continue;
21010
21239
  }
21011
- await mkdir2(dirname(path), { recursive: true, mode: 448 });
21012
- await writeFile(path, entry.data, { mode: 384, flag: "wx" });
21240
+ await mkdir3(dirname2(path), { recursive: true, mode: 448 });
21241
+ await writeFile2(path, entry.data, { mode: 384, flag: "wx" });
21013
21242
  }
21014
21243
  }
21015
21244
  async function cachedRelease(version2, integrity, source) {
21016
- const root = source.cache ?? join2(cacheDirectory(), "releases");
21017
- const directory = join2(root, version2);
21018
- const cached2 = await readFile3(join2(directory, "manifest.json")).catch(() => null);
21245
+ const root = source.cache ?? join3(cacheDirectory(), "releases");
21246
+ const directory = join3(root, version2);
21247
+ const cached2 = await readFile4(join3(directory, "manifest.json")).catch(() => null);
21019
21248
  if (cached2 && digest(cached2) === integrity.manifest) return directory;
21020
21249
  const supplied = source.archive ?? process.env["AGW_RELEASE_ARCHIVE"] ?? void 0;
21021
21250
  let archive;
21022
21251
  if (supplied) {
21023
- archive = await readFile3(supplied).catch(
21252
+ archive = await readFile4(supplied).catch(
21024
21253
  () => fail(
21025
21254
  "release_unavailable",
21026
21255
  `Cannot read the gateway release archive at ${supplied}.`,
@@ -21039,10 +21268,10 @@ async function cachedRelease(version2, integrity, source) {
21039
21268
  4
21040
21269
  );
21041
21270
  await protectedDirectory(root);
21042
- const staging = join2(root, `.${version2}-${randomBytes3(8).toString("hex")}`);
21271
+ const staging = join3(root, `.${version2}-${randomBytes3(8).toString("hex")}`);
21043
21272
  try {
21044
21273
  await extract(archive, staging);
21045
- const manifest2 = await readFile3(join2(staging, "manifest.json")).catch(() => null);
21274
+ const manifest2 = await readFile4(join3(staging, "manifest.json")).catch(() => null);
21046
21275
  if (!manifest2 || digest(manifest2) !== integrity.manifest)
21047
21276
  fail(
21048
21277
  "release_integrity",
@@ -21057,7 +21286,7 @@ async function cachedRelease(version2, integrity, source) {
21057
21286
  }
21058
21287
  return directory;
21059
21288
  }
21060
- async function release2(requested = VERSION, base = dirname(fileURLToPath(import.meta.url)), source = {}) {
21289
+ async function release2(requested = VERSION, base = dirname2(fileURLToPath(import.meta.url)), source = {}) {
21061
21290
  if (requested !== VERSION)
21062
21291
  fail(
21063
21292
  "unsupported_release",
@@ -21068,7 +21297,7 @@ async function release2(requested = VERSION, base = dirname(fileURLToPath(import
21068
21297
  if (major === void 0 || minor === void 0 || major < 22 || major === 22 && minor < 19)
21069
21298
  fail("unsupported_runtime", "Node.js 22.19.0 or newer is required.");
21070
21299
  const integrity = source.integrity === void 0 ? RELEASE : source.integrity;
21071
- const packaged = join2(base, "release", requested);
21300
+ const packaged = join3(base, "release", requested);
21072
21301
  const local = await lstat2(packaged).catch(() => null);
21073
21302
  let directory;
21074
21303
  if (local?.isDirectory()) directory = packaged;
@@ -21082,7 +21311,7 @@ async function release2(requested = VERSION, base = dirname(fileURLToPath(import
21082
21311
  );
21083
21312
  let serialized;
21084
21313
  try {
21085
- serialized = await readFile3(join2(directory, "manifest.json"), "utf8");
21314
+ serialized = await readFile4(join3(directory, "manifest.json"), "utf8");
21086
21315
  } catch {
21087
21316
  fail(
21088
21317
  "release_missing",
@@ -21102,16 +21331,16 @@ async function release2(requested = VERSION, base = dirname(fileURLToPath(import
21102
21331
  for (const [path, hash2] of Object.entries(manifest2.files)) {
21103
21332
  if (path.startsWith("/") || path.split(/[\\/]/).some((p) => p === ".." || p === ""))
21104
21333
  fail("release_integrity", "Release includes an unsafe path.");
21105
- const file2 = join2(directory, path);
21334
+ const file2 = join3(directory, path);
21106
21335
  const info = await lstat2(file2);
21107
- if (!info.isFile() || info.isSymbolicLink() || digest(await readFile3(file2)) !== hash2)
21336
+ if (!info.isFile() || info.isSymbolicLink() || digest(await readFile4(file2)) !== hash2)
21108
21337
  fail("release_integrity", "Release asset integrity check failed.");
21109
21338
  }
21110
21339
  return {
21111
21340
  directory,
21112
21341
  manifest: manifest2,
21113
21342
  config: JSON.parse(
21114
- await readFile3(join2(directory, "wrangler.json"), "utf8")
21343
+ await readFile4(join3(directory, "wrangler.json"), "utf8")
21115
21344
  )
21116
21345
  };
21117
21346
  }
@@ -21147,7 +21376,7 @@ function wranglerEnvironment({
21147
21376
  };
21148
21377
  }
21149
21378
  async function runWrangler(args, { cwd, input, interactive = false, redact } = {}) {
21150
- return new Promise((resolve3, reject2) => {
21379
+ return new Promise((resolve4, reject2) => {
21151
21380
  const child = spawn2(process.execPath, [require2.resolve("wrangler/bin/wrangler.js"), ...args], {
21152
21381
  ...cwd === void 0 ? {} : { cwd },
21153
21382
  env: wranglerEnvironment({ interactive }),
@@ -21177,7 +21406,7 @@ async function runWrangler(args, { cwd, input, interactive = false, redact } = {
21177
21406
  );
21178
21407
  child.once("close", (code) => {
21179
21408
  if (code !== 0) reject2(wranglerFailure(args, code, errors || output, redact));
21180
- else resolve3(output);
21409
+ else resolve4(output);
21181
21410
  });
21182
21411
  });
21183
21412
  }
@@ -21367,8 +21596,8 @@ async function whileWarming(run, sleep = delay3, report = (line) => process.stde
21367
21596
  }
21368
21597
 
21369
21598
  // src/installation.ts
21370
- import { cp, writeFile as writeFile2 } from "node:fs/promises";
21371
- import { join as join3 } from "node:path";
21599
+ import { cp, writeFile as writeFile3 } from "node:fs/promises";
21600
+ import { join as join4 } from "node:path";
21372
21601
  import { setTimeout as delay4 } from "node:timers/promises";
21373
21602
  async function advance(ctx, journal, phase, patch = {}) {
21374
21603
  const fields = journal;
@@ -21395,9 +21624,9 @@ function selectedInstallation(ctx) {
21395
21624
  };
21396
21625
  }
21397
21626
  async function configFile(ctx, journal, artifact) {
21398
- const directory = join3(ctx.store.directory, "deployments", journal.id);
21627
+ const directory = join4(ctx.store.directory, "deployments", journal.id);
21399
21628
  await protectedDirectory(directory);
21400
- await cp(artifact.directory, join3(directory, "release"), { recursive: true });
21629
+ await cp(artifact.directory, join4(directory, "release"), { recursive: true });
21401
21630
  const config2 = {
21402
21631
  ...artifact.config,
21403
21632
  name: journal.name,
@@ -21425,8 +21654,8 @@ async function configFile(ctx, journal, artifact) {
21425
21654
  }))
21426
21655
  } : {}
21427
21656
  };
21428
- const path = join3(directory, "wrangler.json");
21429
- await writeFile2(path, JSON.stringify(config2, null, 2), { mode: 384 });
21657
+ const path = join4(directory, "wrangler.json");
21658
+ await writeFile3(path, JSON.stringify(config2, null, 2), { mode: 384 });
21430
21659
  return { path, directory, config: config2 };
21431
21660
  }
21432
21661
  async function verifyDeployment(ctx, url2, id) {
@@ -21533,7 +21762,8 @@ async function currentInventory(cf, accountId, name, settings, expectedDatabaseI
21533
21762
  DB: "d1",
21534
21763
  USER_LIMITER: "durable_object_namespace",
21535
21764
  ORG_QUOTA: "durable_object_namespace",
21536
- ENDPOINT_RATE_LIMITER: "durable_object_namespace"
21765
+ ENDPOINT_RATE_LIMITER: "durable_object_namespace",
21766
+ CONVERSATION_LOG: "durable_object_namespace"
21537
21767
  };
21538
21768
  if (bindings.some(
21539
21769
  (b) => !["plain_text", "secret_text"].includes(b.type) && resourceTypes[b.name] !== b.type
@@ -21546,7 +21776,8 @@ async function currentInventory(cf, accountId, name, settings, expectedDatabaseI
21546
21776
  const localClasses = {
21547
21777
  USER_LIMITER: "UserLimiter",
21548
21778
  ORG_QUOTA: "OrgQuota",
21549
- ENDPOINT_RATE_LIMITER: "EndpointRateLimiter"
21779
+ ENDPOINT_RATE_LIMITER: "EndpointRateLimiter",
21780
+ CONVERSATION_LOG: "ConversationLog"
21550
21781
  };
21551
21782
  if (bindings.some(
21552
21783
  (binding) => binding.type === "durable_object_namespace" && (binding.class_name !== localClasses[binding.name] || binding.script_name && binding.script_name !== name || binding.environment && binding.environment !== "production")
@@ -21770,8 +22001,8 @@ async function install(ctx, cf, journal, artifact, url2, domain2) {
21770
22001
  // the account is this CLI's however soon anyone else finds the address.
21771
22002
  CLI_BOOTSTRAP_TOKEN_DIGEST: createHash4("sha256").update(journal.bootstrap.token).digest("hex")
21772
22003
  };
21773
- const secretPath = join4(file2.directory, "secrets.json");
21774
- await writeFile3(secretPath, JSON.stringify(secrets), {
22004
+ const secretPath = join5(file2.directory, "secrets.json");
22005
+ await writeFile4(secretPath, JSON.stringify(secrets), {
21775
22006
  mode: 384
21776
22007
  });
21777
22008
  try {
@@ -22012,8 +22243,23 @@ function render(command, result, context, style2) {
22012
22243
  return deployment(result, style2);
22013
22244
  case "support send":
22014
22245
  return support(result, style2);
22246
+ case "app conversations":
22247
+ return conversations(result, style2);
22015
22248
  }
22016
22249
  }
22250
+ function conversations(result, style2) {
22251
+ if (!("downloaded" in result)) return [];
22252
+ return [
22253
+ style2.headline(
22254
+ result.files === 0 ? `No stored conversations for ${result.app_id} in that range.` : `Exported ${result.files} conversation files for ${result.app_id}.`
22255
+ ),
22256
+ ...kv([
22257
+ ["Folder", result.output],
22258
+ ["Downloaded", `${result.downloaded} files${result.media ? `, ${result.media} media` : ""}`],
22259
+ ["Range", `${result.from ?? "oldest"} to ${result.to}`]
22260
+ ], style2)
22261
+ ];
22262
+ }
22017
22263
  function support(result, style2) {
22018
22264
  if (!("reference" in result)) return [];
22019
22265
  return [
@@ -22560,7 +22806,7 @@ var Transport = class {
22560
22806
  constructor(fetchImpl = fetch) {
22561
22807
  this.fetch = fetchImpl;
22562
22808
  }
22563
- async request(url2, path, { key, method = "GET", body, headers = {} } = {}) {
22809
+ async request(url2, path, { key, method = "GET", body, headers = {}, binary } = {}) {
22564
22810
  const base = origin(url2);
22565
22811
  if (!path.startsWith("/") || path.startsWith("//"))
22566
22812
  fail("invalid_request", "Invalid API path.");
@@ -22571,7 +22817,7 @@ var Transport = class {
22571
22817
  redirect: "manual",
22572
22818
  signal: AbortSignal.timeout(3e4),
22573
22819
  headers: {
22574
- accept: "application/json",
22820
+ accept: binary ? "*/*" : "application/json",
22575
22821
  "user-agent": `agw/${VERSION}`,
22576
22822
  ...body ? { "content-type": "application/json" } : {},
22577
22823
  ...key ? { authorization: `Bearer ${key}` } : {},
@@ -22594,6 +22840,7 @@ var Transport = class {
22594
22840
  "Connect explicitly to the intended deployment.",
22595
22841
  3
22596
22842
  );
22843
+ if (binary && response.ok) return { data: new Uint8Array(await response.arrayBuffer()) };
22597
22844
  let data;
22598
22845
  try {
22599
22846
  data = await response.json();
@@ -22637,63 +22884,12 @@ var Transport = class {
22637
22884
  }
22638
22885
  };
22639
22886
 
22640
- // src/usage.ts
22641
- var CLI_BREAKDOWNS = ["provider", "model", "status"];
22642
- function month(value) {
22643
- if (!MONTH_PATTERN.test(value))
22644
- fail(
22645
- "invalid_input",
22646
- "Month must use YYYY-MM with a valid calendar month."
22647
- );
22648
- return value;
22649
- }
22650
- function date5(value) {
22651
- if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value)
22652
- fail(
22653
- "invalid_input",
22654
- "Dates must be real UTC calendar dates in YYYY-MM-DD format."
22655
- );
22656
- return value;
22657
- }
22658
- function positive(value, max = 3600) {
22659
- if (!/^\d+$/.test(String(value)) || Number(value) < 1 || Number(value) > max)
22660
- fail("invalid_input", `Supply an integer between 1 and ${max}.`);
22661
- return Number(value);
22662
- }
22663
- async function usageCommand(ctx, command, flags) {
22664
- if (command === "usage show") {
22665
- const m = month(flags.month ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 7));
22666
- return flags.app ? (await ctx.call("getAppUsage", { params: { app: flags.app }, query: { month: m } })).data : (await ctx.call("getCliUsage", { query: { month: m } })).data;
22667
- }
22668
- const app2 = await required2(flags, "app");
22669
- const by = CLI_BREAKDOWNS.find((dimension) => dimension === (flags.by ?? "model"));
22670
- if (by === void 0)
22671
- fail("invalid_input", "--by must be provider, model or status.");
22672
- if (Boolean(flags.from) !== Boolean(flags.to))
22673
- fail("invalid_input", "Supply both --from and --to or neither.");
22674
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
22675
- const from = date5(flags.from ?? today.slice(0, 7) + "-01");
22676
- const to = date5(flags.to ?? today);
22677
- if (from > to) fail("invalid_input", "--from must not be later than --to.");
22678
- const limit = positive(flags.limit ?? 50, 200);
22679
- return {
22680
- ...(await ctx.call("getAppUsageBreakdown", {
22681
- params: { app: app2 },
22682
- query: { from, to, by, limit }
22683
- })).data,
22684
- coverage: {
22685
- from,
22686
- to,
22687
- source: "Raw events and retained daily aggregates; dimensions provider, model and status survive raw event retention."
22688
- }
22689
- };
22690
- }
22691
-
22692
22887
  // src/main.ts
22693
22888
  async function execute(parsed, ctx) {
22694
22889
  const { command, flags, args } = parsed;
22695
22890
  if (command.startsWith("provider"))
22696
22891
  return resourceCommand(ctx, command, args, flags);
22892
+ if (command === "app conversations") return conversationsCommand(ctx, args[0] ?? "", flags);
22697
22893
  if (command.startsWith("app ")) return appCommand(ctx, command, args, flags);
22698
22894
  if (command.startsWith("usage ")) return usageCommand(ctx, command, flags);
22699
22895
  if (command === "support send") return supportCommand(ctx, flags);
@@ -22755,9 +22951,9 @@ async function main(argv = process.argv.slice(2), {
22755
22951
  const text2 = json2 ? JSON.stringify(output) + "\n" : humanResult(parsed.command, rendered, context, styleFor(stdout));
22756
22952
  if (stdout instanceof Writable2) {
22757
22953
  await new Promise(
22758
- (resolve3, reject2) => stdout.write(
22954
+ (resolve4, reject2) => stdout.write(
22759
22955
  text2,
22760
- (error51) => error51 ? reject2(error51) : resolve3()
22956
+ (error51) => error51 ? reject2(error51) : resolve4()
22761
22957
  )
22762
22958
  );
22763
22959
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maxceem/agw",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Manage App AI Gateway from a terminal or coding agent: providers, iOS and server apps, usage, and self-hosted deployment on Cloudflare",
6
6
  "keywords": [
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "bugs": "https://github.com/maxceem/app-ai-gateway/issues",
28
28
  "upgradeFrom": [
29
+ "0.12.1",
29
30
  "0.12.0",
30
31
  "0.11.2",
31
32
  "0.11.1",