@repogo/plugins 0.1.42 → 0.1.44

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.
@@ -18,6 +18,7 @@ __export(exports_src, {
18
18
  upload: () => upload,
19
19
  toCallError: () => toCallError,
20
20
  stream: () => stream,
21
+ remotePlugin: () => remotePlugin,
21
22
  method: () => method,
22
23
  definePlugin: () => definePlugin,
23
24
  cached: () => cached,
@@ -43,36 +44,48 @@ function parseDuration(duration) {
43
44
  }
44
45
 
45
46
  // src/define.ts
47
+ var permissionModes = ["approval-required", "auto-accept-edits", "full-access"];
48
+ function httpAddresses(option) {
49
+ return option === true ? { public: "key" } : option;
50
+ }
46
51
  function definePlugin(plugin) {
47
52
  return plugin;
48
53
  }
49
54
  function method(input, run, options = {}) {
55
+ if ("kind" in options)
56
+ throw new Error('method: kind is now effect: "read", "write" or "destructive"');
57
+ if ("confirm" in options)
58
+ throw new Error('method: confirm is now approve: "Delete {{id}}?"');
59
+ if (options.approveUpTo !== undefined && !permissionModes.includes(options.approveUpTo)) {
60
+ throw new Error(`method: approveUpTo is one of ${permissionModes.join(", ")}`);
61
+ }
62
+ if (options.approveUpTo !== undefined && !options.approve)
63
+ throw new Error("method: approveUpTo needs an approve question to ask");
50
64
  return {
51
65
  input,
52
66
  run,
53
- kind: options.kind ?? "mutation",
67
+ effect: options.effect ?? "write",
54
68
  output: options.output,
55
69
  description: options.description,
56
- confirm: options.confirm,
57
- http: options.http === true ? "key" : options.http?.auth
70
+ approve: options.approve,
71
+ approveUpTo: options.approveUpTo,
72
+ http: httpAddresses(options.http)
58
73
  };
59
74
  }
60
75
  function cached(input, run, options) {
61
76
  return {
62
77
  input,
63
78
  run,
64
- kind: "query",
79
+ effect: "read",
65
80
  output: options.output,
66
81
  description: options.description,
82
+ http: httpAddresses(options.http),
67
83
  cache: { fresh: parseDuration(options.fresh), shared: options.shared ?? false, timeout: parseDuration(options.timeout ?? "1m") }
68
84
  };
69
85
  }
70
86
  function stream(input, run, options = {}) {
71
- return { input, run, kind: "stream", description: options.description };
87
+ return { input, run, effect: "read", stream: true, description: options.description };
72
88
  }
73
- // src/uploads.ts
74
- import { AsyncLocalStorage } from "async_hooks";
75
-
76
89
  // node_modules/zod/v4/classic/external.js
77
90
  var exports_external = {};
78
91
  __export(exports_external, {
@@ -19492,13 +19505,6 @@ function bigint3(params) {
19492
19505
  function date4(params) {
19493
19506
  return _coercedDate(ZodDate, params);
19494
19507
  }
19495
- // src/uploads.ts
19496
- var callFiles = new AsyncLocalStorage;
19497
- function upload() {
19498
- return exports_external.object({ id: exports_external.string(), name: exports_external.string(), type: exports_external.string(), size: exports_external.number(), path: exports_external.string() }).refine((file2) => (callFiles.getStore() ?? []).some((held) => held.id === file2.id && held.path === file2.path), {
19499
- message: "Not a file uploaded for this call."
19500
- });
19501
- }
19502
19508
  // src/errors.ts
19503
19509
  class MethodError extends Error {
19504
19510
  code;
@@ -19523,6 +19529,40 @@ function toCallError(error62) {
19523
19529
  }
19524
19530
  return { code: "INTERNAL", message: error62 instanceof Error ? error62.message : String(error62) };
19525
19531
  }
19532
+
19533
+ // src/remote.ts
19534
+ var codes = { 400: "VALIDATION", 403: "FORBIDDEN", 404: "NOT_FOUND" };
19535
+ async function remotePlugin(url2, name) {
19536
+ const base = `${url2.replace(/\/$/, "")}/${name}`;
19537
+ const response = await fetch(base);
19538
+ if (!response.ok)
19539
+ throw new Error(`remotePlugin: ${base} answered ${response.status}`);
19540
+ const listing = await response.json();
19541
+ const methods = {};
19542
+ for (const m of listing.methods) {
19543
+ methods[m.name] = method(exports_external.fromJSONSchema(m.input), (input2) => call(`${base}/${m.name}`, input2), {
19544
+ effect: m.effect,
19545
+ description: m.description ?? undefined,
19546
+ output: m.output ? exports_external.fromJSONSchema(m.output) : undefined
19547
+ });
19548
+ }
19549
+ return { description: listing.description ?? undefined, methods };
19550
+ }
19551
+ async function call(url2, input2) {
19552
+ const response = await fetch(url2, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(input2) });
19553
+ const answer = await response.json().catch(() => null);
19554
+ if (response.ok)
19555
+ return answer;
19556
+ throw new MethodError(codes[response.status] ?? "INTERNAL", answer?.error ?? `${url2} answered ${response.status}`, answer?.fields ?? undefined);
19557
+ }
19558
+ // src/uploads.ts
19559
+ import { AsyncLocalStorage } from "async_hooks";
19560
+ var callFiles = new AsyncLocalStorage;
19561
+ function upload() {
19562
+ return exports_external.object({ id: exports_external.string(), name: exports_external.string(), type: exports_external.string(), size: exports_external.number(), path: exports_external.string() }).refine((file2) => (callFiles.getStore() ?? []).some((held) => held.id === file2.id && held.path === file2.path), {
19563
+ message: "Not a file uploaded for this call."
19564
+ });
19565
+ }
19526
19566
  // src/workflow/scope.ts
19527
19567
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
19528
19568
  var attemptScope = new AsyncLocalStorage2;
@@ -19669,6 +19709,7 @@ function carryable(value) {
19669
19709
 
19670
19710
  // src/runtime.ts
19671
19711
  import { AsyncLocalStorage as AsyncLocalStorage5 } from "async_hooks";
19712
+ import { existsSync } from "fs";
19672
19713
  import { mkdir as mkdir3, readdir as readdir3 } from "fs/promises";
19673
19714
  import { join as join3 } from "path";
19674
19715
 
@@ -19732,7 +19773,7 @@ function recordRead(tag) {
19732
19773
  }
19733
19774
  function assertWritable(tag) {
19734
19775
  if (current.getStore()?.readOnly) {
19735
- throw new MethodError("FORBIDDEN", `A query can't change data (it tried to write ${tag}).`);
19776
+ throw new MethodError("FORBIDDEN", `A read can't change data (it tried to write ${tag}).`);
19736
19777
  }
19737
19778
  }
19738
19779
  function recordWrite(tag) {
@@ -20108,14 +20149,14 @@ class AnswerCache {
20108
20149
  checked_at integer not null
20109
20150
  )`);
20110
20151
  }
20111
- async read(key, options, fetch) {
20152
+ async read(key, options, fetch2) {
20112
20153
  const tag = `${this.namespace}.cached.${Bun.hash(key).toString(36)}`;
20113
20154
  recordRead(tag);
20114
20155
  const [row] = await this.database.sql.unsafe(`select value, checked_at from cached_answers where key = $1`, [key]);
20115
20156
  if (!row)
20116
- return this.refresh(key, tag, options, fetch);
20157
+ return this.refresh(key, tag, options, fetch2);
20117
20158
  if (Date.now() - row.checked_at >= options.fresh) {
20118
- this.refresh(key, tag, options, fetch).catch((error62) => record2("error", [error62]));
20159
+ this.refresh(key, tag, options, fetch2).catch((error62) => record2("error", [error62]));
20119
20160
  }
20120
20161
  return JSON.parse(row.value);
20121
20162
  }
@@ -20123,12 +20164,12 @@ class AnswerCache {
20123
20164
  while (this.inflight.size)
20124
20165
  await Promise.allSettled([...this.inflight.values()]);
20125
20166
  }
20126
- refresh(key, tag, options, fetch) {
20167
+ refresh(key, tag, options, fetch2) {
20127
20168
  const running = this.inflight.get(key);
20128
20169
  if (running)
20129
20170
  return running;
20130
20171
  const started = Date.now();
20131
- const work = isolated(async () => withTimeout(Promise.resolve(fetch()), options.timeout)).then(async (value) => {
20172
+ const work = isolated(async () => withTimeout(Promise.resolve(fetch2()), options.timeout)).then(async (value) => {
20132
20173
  await this.save(key, tag, value ?? null, started);
20133
20174
  return value ?? null;
20134
20175
  }, async (error62) => {
@@ -20529,32 +20570,32 @@ function kvMethods(name, kv, schema) {
20529
20570
  const entry = exports_external.object({ key: exports_external.string(), value: schema, expiresAt: exports_external.string().nullable(), createdAt: exports_external.string(), updatedAt: exports_external.string() });
20530
20571
  return {
20531
20572
  get: {
20532
- kind: "query",
20573
+ effect: "read",
20533
20574
  description: `Get one ${name} entry by key, or null`,
20534
20575
  input: exports_external.object({ key }),
20535
20576
  output: entry.nullable(),
20536
20577
  run: ({ key: key2 }) => kv.entry(key2)
20537
20578
  },
20538
20579
  list: {
20539
- kind: "query",
20580
+ effect: "read",
20540
20581
  description: `List ${name} entries in key order, optionally by prefix`,
20541
20582
  input: exports_external.object({ prefix: exports_external.string().optional(), limit }),
20542
20583
  output: exports_external.array(entry),
20543
20584
  run: (input2) => kv.list(input2)
20544
20585
  },
20545
20586
  set: {
20546
- kind: "mutation",
20587
+ effect: "write",
20547
20588
  description: `Set a ${name} key; ttl ("5m", "1h", "7d" or ms) makes it expire`,
20548
20589
  input: exports_external.object({ key, value: schema, ttl }),
20549
20590
  output: entry,
20550
20591
  run: ({ key: key2, value, ttl: ttl2 }) => kv.set(key2, value, { ttl: ttl2 })
20551
20592
  },
20552
20593
  delete: {
20553
- kind: "mutation",
20594
+ effect: "destructive",
20554
20595
  description: `Delete a ${name} key`,
20555
20596
  input: exports_external.object({ key }),
20556
20597
  output: exports_external.null(),
20557
- confirm: "Delete this key?",
20598
+ approve: "Delete this key?",
20558
20599
  run: async ({ key: key2 }) => (await kv.delete(key2), null)
20559
20600
  }
20560
20601
  };
@@ -20564,45 +20605,46 @@ function streamMethods(name, stream2, schema) {
20564
20605
  const line = exports_external.object({ id: exports_external.string(), key: exports_external.string(), offset: exports_external.number(), at: exports_external.string(), data: schema });
20565
20606
  return {
20566
20607
  tail: {
20567
- kind: "stream",
20608
+ effect: "read",
20609
+ stream: true,
20568
20610
  description: `Follow a ${name} key live: its last lines (100 unless last says), then each new one, until it ends`,
20569
20611
  input: exports_external.object({ key, from: exports_external.number().int().min(0).optional(), last: exports_external.number().int().positive().max(500).optional() }),
20570
20612
  run: ({ key: key2, from, last }, signal) => stream2.follow(key2, { from, last, signal })
20571
20613
  },
20572
20614
  read: {
20573
- kind: "query",
20615
+ effect: "read",
20574
20616
  description: `Read a ${name} key's lines from an offset`,
20575
20617
  input: exports_external.object({ key, from: exports_external.number().int().min(0).optional(), limit }),
20576
20618
  output: exports_external.array(line),
20577
20619
  run: ({ key: key2, ...options }) => stream2.read(key2, options)
20578
20620
  },
20579
20621
  keys: {
20580
- kind: "query",
20622
+ effect: "read",
20581
20623
  description: `List ${name} keys, most recently written first`,
20582
20624
  input: exports_external.object({ limit }),
20583
20625
  output: exports_external.array(exports_external.object({ key: exports_external.string(), lines: exports_external.number(), bytes: exports_external.number(), endedAt: exports_external.string().nullable(), updatedAt: exports_external.string() })),
20584
20626
  run: (input2) => stream2.keys(input2)
20585
20627
  },
20586
20628
  append: {
20587
- kind: "mutation",
20629
+ effect: "write",
20588
20630
  description: `Append a line to a ${name} key`,
20589
20631
  input: exports_external.object({ key, data: schema }),
20590
20632
  output: line,
20591
20633
  run: ({ key: key2, data }) => stream2.append(key2, data)
20592
20634
  },
20593
20635
  end: {
20594
- kind: "mutation",
20636
+ effect: "write",
20595
20637
  description: `End a ${name} key: followers stop and appends fail`,
20596
20638
  input: exports_external.object({ key }),
20597
20639
  output: exports_external.null(),
20598
20640
  run: async ({ key: key2 }) => (await stream2.end(key2), null)
20599
20641
  },
20600
20642
  delete: {
20601
- kind: "mutation",
20643
+ effect: "destructive",
20602
20644
  description: `Delete a ${name} key and its lines`,
20603
20645
  input: exports_external.object({ key }),
20604
20646
  output: exports_external.null(),
20605
- confirm: "Delete this stream and its lines?",
20647
+ approve: "Delete this stream and its lines?",
20606
20648
  run: async ({ key: key2 }) => (await stream2.delete(key2), null)
20607
20649
  }
20608
20650
  };
@@ -20627,28 +20669,28 @@ function bucketMethods(name, bucket, schema) {
20627
20669
  const revision = exports_external.number().int().optional();
20628
20670
  return {
20629
20671
  list: {
20630
- kind: "query",
20672
+ effect: "read",
20631
20673
  description: `List ${name} files, newest first, optionally in one folder or with one status`,
20632
20674
  input: exports_external.object({ folder: exports_external.string().optional(), status: status.optional(), limit }),
20633
20675
  output: exports_external.array(file2),
20634
20676
  run: (input2) => bucket.list(input2)
20635
20677
  },
20636
20678
  search: {
20637
- kind: "query",
20679
+ effect: "read",
20638
20680
  description: `Search ${name} files by name, folder and meta`,
20639
20681
  input: exports_external.object({ query: exports_external.string(), limit }),
20640
20682
  output: exports_external.array(file2),
20641
20683
  run: ({ query, limit: limit2 }) => bucket.search(query, { limit: limit2 })
20642
20684
  },
20643
20685
  get: {
20644
- kind: "query",
20686
+ effect: "read",
20645
20687
  description: `Get one ${name} file by id, or null`,
20646
20688
  input: exports_external.object({ id }),
20647
20689
  output: file2.nullable(),
20648
20690
  run: ({ id: id2 }) => bucket.get(id2)
20649
20691
  },
20650
20692
  put: {
20651
- kind: "mutation",
20693
+ effect: "write",
20652
20694
  description: `Add a file to ${name}: its name, and text or an absolute path to copy`,
20653
20695
  input: exports_external.object({
20654
20696
  name: exports_external.string(),
@@ -20665,25 +20707,25 @@ function bucketMethods(name, bucket, schema) {
20665
20707
  }
20666
20708
  },
20667
20709
  update: {
20668
- kind: "mutation",
20710
+ effect: "write",
20669
20711
  description: `Change fields of a ${name} file's meta; null removes one`,
20670
20712
  input: exports_external.object({ id, meta: exports_external.record(exports_external.string(), exports_external.unknown()), revision }),
20671
20713
  output: file2,
20672
20714
  run: ({ id: id2, meta: meta3, revision: revision2 }) => bucket.update(id2, meta3, { revision: revision2 })
20673
20715
  },
20674
20716
  move: {
20675
- kind: "mutation",
20717
+ effect: "write",
20676
20718
  description: `Rename a ${name} file or move it to another folder`,
20677
20719
  input: exports_external.object({ id, name: exports_external.string().optional(), folder: exports_external.string().optional(), revision }),
20678
20720
  output: file2,
20679
20721
  run: ({ id: id2, name: name2, folder, revision: revision2 }) => bucket.move(id2, { name: name2, folder }, { revision: revision2 })
20680
20722
  },
20681
20723
  delete: {
20682
- kind: "mutation",
20724
+ effect: "destructive",
20683
20725
  description: `Delete a ${name} file and its meta`,
20684
20726
  input: exports_external.object({ id }),
20685
20727
  output: exports_external.null(),
20686
- confirm: "Delete this file?",
20728
+ approve: "Delete this file?",
20687
20729
  run: async ({ id: id2 }) => (await bucket.delete(id2), null)
20688
20730
  }
20689
20731
  };
@@ -21092,7 +21134,7 @@ var RunMetadataSchema = exports_external.object({
21092
21134
  root: exports_external.string().refine(isAbsolute2),
21093
21135
  namespace: exports_external.string().regex(/^[a-z0-9][a-z0-9.-]*$/),
21094
21136
  plugin: exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/),
21095
- caller: exports_external.enum(["card", "agent", "cron", "code", "http"]),
21137
+ caller: exports_external.enum(["card", "agent", "cron", "code", "http", "device"]),
21096
21138
  workflow: exports_external.string().regex(/^[a-z][a-zA-Z0-9_-]*$/),
21097
21139
  input: exports_external.json(),
21098
21140
  key: exports_external.string().nullable(),
@@ -21115,29 +21157,29 @@ function workflowMethods(plugin, namespace, name, definition, ctx, bridge, direc
21115
21157
  return bridge({ operation, dir, plugin, namespace, workflow: name, root: ctx.root, caller: ctx.caller, input: input2, settings: workflowSettings(definition) });
21116
21158
  };
21117
21159
  const id = exports_external.object({ id: exports_external.string().uuid() }).strict();
21118
- const spec = (method2, kind, input2, description, confirm, output2) => ({
21160
+ const spec = (method2, effect, input2, description, approve, output2) => ({
21119
21161
  name: `${plugin}.${name}.${method2}`,
21120
21162
  plugin,
21121
- kind,
21163
+ effect,
21122
21164
  input: input2,
21123
21165
  description,
21124
- confirm,
21166
+ approve,
21125
21167
  output: output2,
21126
21168
  run: async (input3, raw) => {
21127
- if (kind === "query")
21169
+ if (effect === "read")
21128
21170
  recordRead(workflowTag(namespace));
21129
21171
  const request = method2 === "start" ? { ...input3, input: raw.input } : input3;
21130
21172
  return invoke(method2, request);
21131
21173
  }
21132
21174
  });
21133
21175
  return [
21134
- spec("list", "query", exports_external.object({ limit: exports_external.number().int().min(1).max(100).default(20), status: exports_external.string().optional(), cursor: exports_external.string().optional() }).strict(), "List workflow runs", undefined, exports_external.object({ items: exports_external.array(RunSummarySchema), cursor: exports_external.string().nullable() })),
21135
- spec("get", "query", id, "Get one workflow run"),
21136
- spec("start", "mutation", exports_external.object({ input: definition.input, key: exports_external.string().min(1).max(200).optional() }).strict(), definition.description ?? "Start a durable workflow", definition.confirm, RunSummarySchema),
21137
- spec("retry", "mutation", id, "Retry a safely failed query step", undefined, RunSummarySchema),
21138
- spec("restart", "mutation", id, "Start a run that needs attention over from the beginning, on the current code", undefined, RunSummarySchema),
21139
- spec("cancel", "mutation", id, "Request cancellation of a workflow", undefined, RunSummarySchema),
21140
- spec("cancelAll", "mutation", exports_external.object({ status: exports_external.string().optional() }).strict(), "Cancel every unfinished run, or only those in one status", undefined, exports_external.object({ cancelled: exports_external.number().int() }))
21176
+ spec("list", "read", exports_external.object({ limit: exports_external.number().int().min(1).max(100).default(20), status: exports_external.string().optional(), cursor: exports_external.string().optional() }).strict(), "List workflow runs", undefined, exports_external.object({ items: exports_external.array(RunSummarySchema), cursor: exports_external.string().nullable() })),
21177
+ spec("get", "read", id, "Get one workflow run"),
21178
+ spec("start", "write", exports_external.object({ input: definition.input, key: exports_external.string().min(1).max(200).optional() }).strict(), definition.description ?? "Start a durable workflow", definition.approve, RunSummarySchema),
21179
+ spec("retry", "write", id, "Retry a safely failed query step", undefined, RunSummarySchema),
21180
+ spec("restart", "write", id, "Start a run that needs attention over from the beginning, on the current code", undefined, RunSummarySchema),
21181
+ spec("cancel", "write", id, "Request cancellation of a workflow", undefined, RunSummarySchema),
21182
+ spec("cancelAll", "write", exports_external.object({ status: exports_external.string().optional() }).strict(), "Cancel every unfinished run, or only those in one status", undefined, exports_external.object({ cancelled: exports_external.number().int() }))
21141
21183
  ];
21142
21184
  }
21143
21185
  function workflowSettings(definition) {
@@ -21197,6 +21239,8 @@ class Runtime {
21197
21239
  continue;
21198
21240
  if (only && !only.includes(entry.name))
21199
21241
  continue;
21242
+ if (!existsSync(join3(pluginsDir, entry.name, "app.ts")))
21243
+ continue;
21200
21244
  const module = await logContext.run({ plugin: entry.name }, () => import(join3(pluginsDir, entry.name, "app.ts")));
21201
21245
  await this.add(entry.name, module.default);
21202
21246
  }
@@ -21322,9 +21366,11 @@ class Runtime {
21322
21366
  this.register({
21323
21367
  name: fullName,
21324
21368
  plugin: name,
21325
- kind: method2.kind,
21369
+ effect: method2.effect,
21370
+ stream: method2.stream,
21326
21371
  description: method2.description,
21327
- confirm: method2.confirm,
21372
+ approve: method2.approve,
21373
+ approveUpTo: method2.approveUpTo,
21328
21374
  http: method2.http,
21329
21375
  input: method2.input,
21330
21376
  output: method2.output,
@@ -21355,6 +21401,10 @@ class Runtime {
21355
21401
  })))));
21356
21402
  }
21357
21403
  async call(name, input2 = {}, options = {}) {
21404
+ if (options.signal) {
21405
+ const { signal, ...rest } = options;
21406
+ return this.callSignal.run(signal, () => this.call(name, input2, rest));
21407
+ }
21358
21408
  if (options.files) {
21359
21409
  const { files, ...rest } = options;
21360
21410
  const fields = input2 ?? {};
@@ -21378,17 +21428,17 @@ class Runtime {
21378
21428
  return runWith(where, () => this.call(name, input2, options));
21379
21429
  if (where !== logContext.getStore())
21380
21430
  return logContext.run(where, () => this.call(name, input2, options));
21381
- if ((options.expect === "query" || currentAccess()?.readOnly) && method2.kind === "mutation") {
21431
+ if ((options.expect === "read" || currentAccess()?.readOnly) && method2.effect !== "read") {
21382
21432
  throw new MethodError("FORBIDDEN", `${name} changes data, so it only runs when the user acts.`);
21383
21433
  }
21384
21434
  const raw = input2 ?? {};
21385
21435
  const parsed = method2.input.parse(raw);
21386
- if (method2.confirm && (options.confirmed === false || workflowScope.getStore()))
21387
- throw new MethodError("CONFIRM", fillConfirm(method2.confirm, parsed));
21388
- if (method2.kind === "stream")
21436
+ if (method2.approve && (options.confirmed === false || workflowScope.getStore()))
21437
+ throw new MethodError("CONFIRM", fillQuestion(method2.approve, parsed));
21438
+ if (method2.stream)
21389
21439
  return { result: await this.sample(name, input2), reads: [], invalidates: [] };
21390
21440
  const confirmed = this.callConfirmed.getStore() ?? false;
21391
- const run = await tracked(method2.kind === "query", (tags) => this.notifyInvalidated(tags), () => this.callConfirmed.run(confirmed || method2.confirm !== undefined, async () => {
21441
+ const run = await tracked(method2.effect === "read", (tags) => this.notifyInvalidated(tags), () => this.callConfirmed.run(confirmed || method2.approve !== undefined, async () => {
21392
21442
  const result = await method2.run(parsed, raw);
21393
21443
  await this.events.emit(name, { input: parsed, result });
21394
21444
  return result;
@@ -21407,8 +21457,8 @@ class Runtime {
21407
21457
  const method2 = this.methods.get(name);
21408
21458
  if (!method2)
21409
21459
  throw new MethodError("NOT_FOUND", `There is no method ${name}.`);
21410
- if (method2.kind !== "stream")
21411
- throw new MethodError("VALIDATION", `${name} is a ${method2.kind}, not a stream.`);
21460
+ if (!method2.stream)
21461
+ throw new MethodError("VALIDATION", `${name} is not a stream.`);
21412
21462
  const raw = input2 ?? {};
21413
21463
  const parsed = method2.input.parse(raw);
21414
21464
  const root = options.root ?? this.callRoot.getStore() ?? this.root;
@@ -21463,7 +21513,7 @@ class Runtime {
21463
21513
  const statuses = [];
21464
21514
  for (const entry of entries) {
21465
21515
  const timezone = entry.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
21466
- const signature = `${entry.plugin}.${entry.method} ${entry.schedule} ${timezone}`;
21516
+ const signature = `${entry.plugin}.${entry.method} ${entry.schedule} ${timezone} ${entry.root ?? ""}`;
21467
21517
  try {
21468
21518
  const job = kept.get(signature) ?? this.cronJob(entry, timezone, signature);
21469
21519
  jobs.push(job);
@@ -21495,9 +21545,9 @@ class Runtime {
21495
21545
  const method2 = this.methods.get(name);
21496
21546
  if (!method2 || method2.plugin !== entry.plugin)
21497
21547
  throw new Error(`${entry.method} isn't one of ${entry.plugin}'s methods`);
21498
- if (method2.kind === "stream")
21499
- throw new Error(`${name} is a stream; a cron runs a query or a mutation`);
21500
- if (method2.confirm)
21548
+ if (method2.stream)
21549
+ throw new Error(`${name} is a stream; a cron runs a read or a write`);
21550
+ if (method2.approve)
21501
21551
  throw new Error(`${name} asks before it runs, so nothing may run it on a schedule`);
21502
21552
  if (!method2.input.safeParse({}).success)
21503
21553
  throw new Error(`${name} needs input; a cron calls it with {}`);
@@ -21513,6 +21563,7 @@ class Runtime {
21513
21563
  return {
21514
21564
  signature,
21515
21565
  plugin: entry.plugin,
21566
+ root: entry.root,
21516
21567
  method: name,
21517
21568
  schedule: entry.schedule,
21518
21569
  timezone,
@@ -21537,7 +21588,7 @@ class Runtime {
21537
21588
  job.running = true;
21538
21589
  job.lastRunAt = now;
21539
21590
  const startedAt = Date.now();
21540
- started.push(this.call(job.method, {}, { caller: "cron" }).then(() => {
21591
+ started.push(this.call(job.method, {}, { caller: "cron", root: job.root }).then(() => {
21541
21592
  job.lastError = null;
21542
21593
  }, (error62) => {
21543
21594
  job.lastError = toCallError(error62).message;
@@ -21598,9 +21649,11 @@ class Runtime {
21598
21649
  description: plugin.description ?? null,
21599
21650
  methods: [...this.methods.values()].filter((method2) => method2.plugin === name).map((method2) => ({
21600
21651
  name: method2.name,
21601
- kind: method2.kind,
21652
+ effect: method2.effect,
21653
+ stream: method2.stream ?? false,
21602
21654
  description: method2.description ?? null,
21603
- confirm: method2.confirm ?? null,
21655
+ approve: method2.approve ?? null,
21656
+ approveUpTo: method2.approve ? method2.approveUpTo ?? "full-access" : null,
21604
21657
  http: method2.http ?? null,
21605
21658
  input: exports_external.toJSONSchema(method2.input, { io: "input", unrepresentable: "any" }),
21606
21659
  output: method2.output ? exports_external.toJSONSchema(method2.output, { io: "output", unrepresentable: "any" }) : null
@@ -21674,6 +21727,9 @@ class Runtime {
21674
21727
  register(method2) {
21675
21728
  if (this.methods.has(method2.name))
21676
21729
  throw new Error(`${method2.name} is defined twice; rename the method or the data`);
21730
+ if (method2.effect === "destructive" && !method2.approve) {
21731
+ throw new Error(`${method2.name} is destructive, so it needs an approve question, such as { approve: "Delete {{id}}?" }`);
21732
+ }
21677
21733
  this.methods.set(method2.name, method2);
21678
21734
  }
21679
21735
  registerGenerated(plugin, name, methods) {
@@ -21690,32 +21746,32 @@ class Runtime {
21690
21746
  const prefix = `${plugin}.${name}`;
21691
21747
  const id = exports_external.object({ id: exports_external.string() });
21692
21748
  const record3 = schema.extend({ id: exports_external.string(), createdAt: exports_external.string(), updatedAt: exports_external.string() });
21693
- const crud = (method2, kind, input2, output2, description, run, confirm) => this.register({
21749
+ const crud = (method2, effect, input2, output2, description, run, approve) => this.register({
21694
21750
  name: `${prefix}.${method2}`,
21695
21751
  plugin,
21696
- kind,
21752
+ effect,
21697
21753
  description,
21698
- confirm,
21754
+ approve,
21699
21755
  input: input2,
21700
21756
  output: output2,
21701
21757
  run: async (value, raw) => run(value, raw)
21702
21758
  });
21703
- crud("list", "query", exports_external.object({
21759
+ crud("list", "read", exports_external.object({
21704
21760
  limit: exports_external.number().int().positive().max(500).optional(),
21705
21761
  offset: exports_external.number().int().min(0).optional(),
21706
21762
  orderBy: exports_external.string().optional(),
21707
21763
  order: exports_external.enum(["asc", "desc"]).optional()
21708
21764
  }), exports_external.array(record3), `List ${name} records, newest first unless orderBy says`, (input2) => collection.list(input2));
21709
- crud("get", "query", id, record3.nullable(), `Get one ${name} record by id, or null`, (input2) => collection.get(input2.id));
21710
- crud("create", "mutation", schema, record3, `Create a ${name} record`, (input2) => collection.create(input2));
21711
- crud("update", "mutation", id.extend(schema.partial().shape), record3, `Update fields on a ${name} record`, ({ id: id2, ...fields }, raw) => collection.update(id2, Object.fromEntries(Object.entries(fields).filter(([key]) => Object.hasOwn(raw ?? {}, key)))));
21712
- crud("delete", "mutation", id, exports_external.null(), `Delete a ${name} record by id`, async (input2) => {
21765
+ crud("get", "read", id, record3.nullable(), `Get one ${name} record by id, or null`, (input2) => collection.get(input2.id));
21766
+ crud("create", "write", schema, record3, `Create a ${name} record`, (input2) => collection.create(input2));
21767
+ crud("update", "write", id.extend(schema.partial().shape), record3, `Update fields on a ${name} record`, ({ id: id2, ...fields }, raw) => collection.update(id2, Object.fromEntries(Object.entries(fields).filter(([key]) => Object.hasOwn(raw ?? {}, key)))));
21768
+ crud("delete", "destructive", id, exports_external.null(), `Delete a ${name} record by id`, async (input2) => {
21713
21769
  await collection.delete(input2.id);
21714
21770
  return null;
21715
21771
  }, "Delete this record?");
21716
21772
  }
21717
21773
  }
21718
- function fillConfirm(question, input2) {
21774
+ function fillQuestion(question, input2) {
21719
21775
  const fields = input2 ?? {};
21720
21776
  return question.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (_, field2) => {
21721
21777
  const value = Object.hasOwn(fields, field2) ? fields[field2] : undefined;
@@ -21738,9 +21794,9 @@ function cronState(job) {
21738
21794
  function mcpFailure(server, tool, error62) {
21739
21795
  switch (error62.code) {
21740
21796
  case "NOT_READ_ONLY":
21741
- return new MethodError("FORBIDDEN", `${server}.${tool} changes data, so it runs from a mutation, not a query.`);
21797
+ return new MethodError("FORBIDDEN", `${server}.${tool} changes data, so it runs from a write, not a read.`);
21742
21798
  case "CONFIRM":
21743
- return new MethodError("FORBIDDEN", `${server}.${tool} may destroy data, so it runs only from a method with a confirm.`);
21799
+ return new MethodError("FORBIDDEN", `${server}.${tool} may destroy data, so it runs only from a method with an approve.`);
21744
21800
  }
21745
21801
  return new MethodError("INTERNAL", error62.message);
21746
21802
  }
@@ -22848,12 +22904,12 @@ function runExecutor() {
22848
22904
  return;
22849
22905
  }
22850
22906
  if (message.type === "hosted") {
22851
- const call = waiting.get(message.id);
22907
+ const call2 = waiting.get(message.id);
22852
22908
  waiting.delete(message.id);
22853
22909
  if (message.error)
22854
- call?.reject(new MethodError(message.error.code, message.error.message));
22910
+ call2?.reject(new MethodError(message.error.code, message.error.message));
22855
22911
  else
22856
- call?.resolve(message.result);
22912
+ call2?.resolve(message.result);
22857
22913
  return;
22858
22914
  }
22859
22915
  if (launched)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@repogo/plugins",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
4
  "description": "Write RepoGo plugins: collections and methods over SQLite that agents and cards call",
5
5
  "type": "module",
6
6
  "exports": {
@@ -27,7 +27,7 @@
27
27
  "typecheck": "tsc --noEmit"
28
28
  },
29
29
  "dependencies": {
30
- "zod": "^4.1.0"
30
+ "zod": "^4.6.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/bun": "latest",