@moxxy/cli 0.13.0 → 0.13.1

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/dist/bin.js CHANGED
@@ -14,7 +14,7 @@ import Stream, { Readable as Readable$1, PassThrough as PassThrough$2, Stream as
14
14
  import http, { createServer } from 'http';
15
15
  import https from 'https';
16
16
  import zlib from 'zlib';
17
- import { webcrypto, randomBytes, createHash, randomUUID, scryptSync, createCipheriv, createDecipheriv, createHmac, timingSafeEqual } from 'crypto';
17
+ import { randomUUID, webcrypto, randomBytes, createHash, scryptSync, createCipheriv, createDecipheriv, createHmac, timingSafeEqual } from 'crypto';
18
18
  import 'zod/v3';
19
19
  import * as z4mini from 'zod/v4-mini';
20
20
  import * as z53 from 'zod/v4';
@@ -223,6 +223,8 @@ var init_log = __esm({
223
223
  this.now = opts.now ?? Date.now;
224
224
  for (const e3 of seed)
225
225
  this.events.push(e3);
226
+ if (seed.length > 0)
227
+ this.base = seed[0].seq;
226
228
  }
227
229
  get length() {
228
230
  return this.events.length;
@@ -447,15 +449,20 @@ async function streamChildEventToParent(parentSession, parentTurnId, label3, chi
447
449
  const mapped = mapChildEvent(label3, childSessionId, childEvt);
448
450
  if (!mapped)
449
451
  return;
450
- await parentSession.log.append({
451
- type: "plugin_event",
452
- sessionId: parentSession.id,
453
- turnId: parentTurnId,
454
- source: "plugin",
455
- pluginId: SUBAGENT_PLUGIN_ID,
456
- subtype: mapped.subtype,
457
- payload: mapped.payload
458
- });
452
+ try {
453
+ await parentSession.log.append({
454
+ type: "plugin_event",
455
+ sessionId: parentSession.id,
456
+ turnId: parentTurnId,
457
+ source: "plugin",
458
+ pluginId: SUBAGENT_PLUGIN_ID,
459
+ subtype: mapped.subtype,
460
+ payload: mapped.payload
461
+ });
462
+ } catch (err) {
463
+ process.stderr.write(`moxxy: dropped subagent progress event (${mapped.subtype}) \u2014 parent log append failed: ${err instanceof Error ? err.message : String(err)}
464
+ `);
465
+ }
459
466
  }
460
467
  function mapChildEvent(label3, childSessionId, childEvt) {
461
468
  const payload = {
@@ -1561,7 +1568,16 @@ var init_providers = __esm({
1561
1568
  if (instance)
1562
1569
  this.instances.set(def.name, instance);
1563
1570
  }
1564
- /** Overwrite an existing def (also drops the cached instance so the new createClient gets called). */
1571
+ /**
1572
+ * Overwrite an existing def (also drops the cached instance so the new
1573
+ * createClient gets called).
1574
+ *
1575
+ * Invariant: replacing the ACTIVE provider's def leaves `active` pointing at
1576
+ * it but with no cached instance, so `getActive()` throws until the caller
1577
+ * rebuilds the instance. Callers replacing the active provider MUST follow
1578
+ * with `setActive(name, config)` (replace can't rebuild itself — it has no
1579
+ * config). The sole production caller does exactly that.
1580
+ */
1565
1581
  replace(def, instance) {
1566
1582
  this.defs.set(def.name, def);
1567
1583
  this.instances.delete(def.name);
@@ -1654,8 +1670,12 @@ var init_modes = __esm({
1654
1670
  }
1655
1671
  replace(mode) {
1656
1672
  this.modes.set(mode.name, mode);
1657
- if (!this.active)
1673
+ if (!this.active) {
1658
1674
  this.activate(mode);
1675
+ } else if (this.active === mode.name) {
1676
+ for (const fn of this.changeListeners)
1677
+ fn();
1678
+ }
1659
1679
  }
1660
1680
  /**
1661
1681
  * Remove a mode. If it was active, the active slot is cleared —
@@ -1994,16 +2014,17 @@ function coerceValue(tag2, name, value, spec, errors2) {
1994
2014
  }
1995
2015
  return num;
1996
2016
  }
2017
+ const decoded = decodeEntities(value);
1997
2018
  if (spec.type === "enum") {
1998
- if (!spec.values?.includes(value)) {
2019
+ if (!spec.values?.includes(decoded)) {
1999
2020
  errors2.push({ message: `<${tag2}> attribute "${name}" must be one of: ${spec.values?.join(", ")}` });
2000
2021
  }
2001
- return value;
2022
+ return decoded;
2002
2023
  }
2003
- if ((name === "href" || name === "src") && !isSafeViewUrl(value, name)) {
2024
+ if ((name === "href" || name === "src") && !isSafeViewUrl(decoded, name)) {
2004
2025
  errors2.push({ message: `<${tag2}> attribute "${name}" has a disallowed URL scheme` });
2005
2026
  }
2006
- return value;
2027
+ return decoded;
2007
2028
  }
2008
2029
  function coerceAttrs(b3, spec, errors2) {
2009
2030
  const out = {};
@@ -2015,7 +2036,8 @@ function coerceAttrs(b3, spec, errors2) {
2015
2036
  }
2016
2037
  const aspec = spec?.attrs[a2.name];
2017
2038
  if (!aspec) {
2018
- errors2.push({ message: `<${b3.tag}> unknown attribute "${a2.name}"` });
2039
+ if (spec)
2040
+ errors2.push({ message: `<${b3.tag}> unknown attribute "${a2.name}"` });
2019
2041
  continue;
2020
2042
  }
2021
2043
  seen.add(a2.name);
@@ -2229,10 +2251,13 @@ var init_localhost = __esm({
2229
2251
  "../core/dist/tunnel/localhost.js"() {
2230
2252
  localhostTunnel = defineTunnelProvider({
2231
2253
  name: "localhost",
2232
- open: (opts) => Promise.resolve({
2233
- url: `http://${opts.host}:${opts.port}`,
2234
- close: () => Promise.resolve()
2235
- }),
2254
+ open: (opts) => {
2255
+ const host = opts.host.includes(":") ? `[${opts.host}]` : opts.host;
2256
+ return Promise.resolve({
2257
+ url: `http://${host}:${opts.port}`,
2258
+ close: () => Promise.resolve()
2259
+ });
2260
+ },
2236
2261
  isAvailable: () => Promise.resolve(true)
2237
2262
  });
2238
2263
  }
@@ -3092,7 +3117,7 @@ function matchRule(rule, call, intent) {
3092
3117
  if (!input || typeof input !== "object")
3093
3118
  return false;
3094
3119
  for (const [k3, v3] of Object.entries(rule.inputMatches)) {
3095
- const candidate = String(input[k3] ?? "");
3120
+ const candidate = stringifyCandidate(input[k3]);
3096
3121
  let re2;
3097
3122
  try {
3098
3123
  re2 = new RegExp(v3);
@@ -3108,6 +3133,18 @@ function matchRule(rule, call, intent) {
3108
3133
  }
3109
3134
  return true;
3110
3135
  }
3136
+ function stringifyCandidate(value) {
3137
+ if (value === null || value === void 0)
3138
+ return "";
3139
+ if (typeof value === "object") {
3140
+ try {
3141
+ return JSON.stringify(value) ?? "";
3142
+ } catch {
3143
+ return String(value);
3144
+ }
3145
+ }
3146
+ return String(value);
3147
+ }
3111
3148
  function warnBadPattern(ruleName, field, pattern, intent, err) {
3112
3149
  const detail = err instanceof Error ? err.message : String(err);
3113
3150
  const resolution = intent === "deny" ? "failing closed (rule still denies)" : "this field cannot match (rule will not grant)";
@@ -3760,7 +3797,16 @@ async function loadDir(dir, scope, logger) {
3760
3797
  if (!entry.isFile() || !entry.name.endsWith(".md"))
3761
3798
  continue;
3762
3799
  const full = path3.join(dir, entry.name);
3763
- const raw = await promises.readFile(full, "utf8");
3800
+ let raw;
3801
+ try {
3802
+ raw = await promises.readFile(full, "utf8");
3803
+ } catch (err) {
3804
+ logger?.warn("skill: unreadable file, skipping", {
3805
+ path: full,
3806
+ error: err instanceof Error ? err.message : String(err)
3807
+ });
3808
+ continue;
3809
+ }
3764
3810
  const { frontmatter, body } = parseSkillFile(raw);
3765
3811
  const parsed = skillFrontmatterSchema.safeParse(frontmatter);
3766
3812
  if (!parsed.success) {
@@ -4211,6 +4257,17 @@ var init_persistence = __esm({
4211
4257
  this.indexUpdateScheduled = false;
4212
4258
  await this.writeIndex();
4213
4259
  }
4260
+ /**
4261
+ * Resolve once every event-log write queued so far has settled. Appends are
4262
+ * enqueued fire-and-forget (`enqueueAppend`), so callers that need to observe
4263
+ * the on-disk result of prior appends — graceful shutdown, or a test that
4264
+ * mutates the filesystem between writes — await this to drain the queue
4265
+ * rather than guessing at timing. Enqueues a no-op at the tail of the same
4266
+ * mutex, so it can only resolve after all earlier appends/truncates have run.
4267
+ */
4268
+ async settleWrites() {
4269
+ await this.writeQueue.run(() => void 0);
4270
+ }
4214
4271
  /**
4215
4272
  * Manually update header fields (provider/model) when the user
4216
4273
  * switches mid-session. The /model picker calls this so the index
@@ -4233,7 +4290,7 @@ var init_persistence = __esm({
4233
4290
  };
4234
4291
  this.scheduleIndexWrite();
4235
4292
  const line = JSON.stringify(event) + "\n";
4236
- void this.writeQueue.run(() => promises.appendFile(this.logPath, line, "utf8")).then(() => this.noteWriteOk()).catch((err) => this.noteWriteFailure("append", err));
4293
+ void this.writeQueue.run(() => this.ensureReady().then(() => promises.appendFile(this.logPath, line, "utf8"))).then(() => this.noteWriteOk()).catch((err) => this.noteWriteFailure("append", err));
4237
4294
  }
4238
4295
  /**
4239
4296
  * True while event-log writes are failing (history is no longer being
@@ -4274,7 +4331,7 @@ var init_persistence = __esm({
4274
4331
  lastActivity: (/* @__PURE__ */ new Date()).toISOString()
4275
4332
  };
4276
4333
  this.scheduleIndexWrite();
4277
- void this.writeQueue.run(() => promises.writeFile(this.logPath, "", "utf8")).then(() => this.noteWriteOk()).catch((err) => this.noteWriteFailure("truncate", err));
4334
+ void this.writeQueue.run(() => this.ensureReady().then(() => promises.writeFile(this.logPath, "", "utf8"))).then(() => this.noteWriteOk()).catch((err) => this.noteWriteFailure("truncate", err));
4278
4335
  }
4279
4336
  scheduleIndexWrite() {
4280
4337
  if (this.indexUpdateScheduled)
@@ -30607,7 +30664,7 @@ var require_api = __commonJS({
30607
30664
  Object.defineProperty(exports, "__esModule", { value: true });
30608
30665
  exports.Api = void 0;
30609
30666
  var client_js_1 = require_client();
30610
- var Api = class {
30667
+ var Api2 = class {
30611
30668
  /**
30612
30669
  * Constructs a new instance of `Api`. It is independent from all other
30613
30670
  * instances of this class. For example, this lets you install a custom set
@@ -32942,7 +32999,7 @@ var require_api = __commonJS({
32942
32999
  return this.raw.getGameHighScores({ inline_message_id, user_id }, signal);
32943
33000
  }
32944
33001
  };
32945
- exports.Api = Api;
33002
+ exports.Api = Api2;
32946
33003
  }
32947
33004
  });
32948
33005
 
@@ -32986,7 +33043,7 @@ var require_bot = __commonJS({
32986
33043
  "chat_boost",
32987
33044
  "removed_chat_boost"
32988
33045
  ];
32989
- var Bot3 = class extends composer_js_1.Composer {
33046
+ var Bot2 = class extends composer_js_1.Composer {
32990
33047
  /**
32991
33048
  * Creates a new Bot with the given token.
32992
33049
  *
@@ -33352,7 +33409,7 @@ var require_bot = __commonJS({
33352
33409
  await sleep7(sleepSeconds);
33353
33410
  }
33354
33411
  };
33355
- exports.Bot = Bot3;
33412
+ exports.Bot = Bot2;
33356
33413
  async function withRetries(task, signal) {
33357
33414
  const INITIAL_DELAY = 50;
33358
33415
  let lastDelay = INITIAL_DELAY;
@@ -49252,11 +49309,25 @@ function renderResult(content, isError) {
49252
49309
  else if (block.type === "image")
49253
49310
  parts.push(`[image:${block.mimeType}]`);
49254
49311
  else if (block.type === "resource")
49255
- parts.push(`[resource]`);
49312
+ parts.push(renderResource(block.resource));
49256
49313
  }
49257
49314
  const text = parts.join("\n");
49258
49315
  return isError ? `[error] ${text}` : text;
49259
49316
  }
49317
+ function renderResource(resource) {
49318
+ if (resource && typeof resource === "object") {
49319
+ const r2 = resource;
49320
+ if (typeof r2.text === "string")
49321
+ return r2.text;
49322
+ const meta = [
49323
+ typeof r2.uri === "string" ? r2.uri : null,
49324
+ typeof r2.mimeType === "string" ? r2.mimeType : null
49325
+ ].filter((v3) => v3 !== null);
49326
+ if (meta.length > 0)
49327
+ return `[resource:${meta.join(" ")}]`;
49328
+ }
49329
+ return `[resource]`;
49330
+ }
49260
49331
  var MCP_CALL_TIMEOUT_MS;
49261
49332
  var init_wrap = __esm({
49262
49333
  "../plugin-mcp/dist/wrap.js"() {
@@ -49264,13 +49335,16 @@ var init_wrap = __esm({
49264
49335
  MCP_CALL_TIMEOUT_MS = 5 * 60 * 1e3;
49265
49336
  }
49266
49337
  });
49267
- var mcpStoredServerSchema, mcpStoredConfigSchema;
49338
+ var mcpStoredServerSchema, mcpStoredConfigRootSchema;
49268
49339
  var init_config_schema = __esm({
49269
49340
  "../plugin-mcp/dist/admin/config-schema.js"() {
49270
49341
  mcpStoredServerSchema = z$1.object({
49271
49342
  name: z$1.string().min(1)
49272
49343
  }).passthrough();
49273
- mcpStoredConfigSchema = z$1.object({
49344
+ mcpStoredConfigRootSchema = z$1.object({
49345
+ servers: z$1.array(z$1.unknown())
49346
+ }).passthrough();
49347
+ z$1.object({
49274
49348
  servers: z$1.array(mcpStoredServerSchema)
49275
49349
  }).passthrough().transform((value) => value);
49276
49350
  }
@@ -49281,10 +49355,17 @@ function mcpConfigPath() {
49281
49355
  async function readMcpConfig() {
49282
49356
  try {
49283
49357
  const raw = await promises.readFile(mcpConfigPath(), "utf8");
49284
- const parsed = mcpStoredConfigSchema.safeParse(JSON.parse(raw));
49285
- if (parsed.success) {
49286
- return parsed.data;
49358
+ const root = mcpStoredConfigRootSchema.safeParse(JSON.parse(raw));
49359
+ if (!root.success) {
49360
+ return { servers: [] };
49361
+ }
49362
+ const servers = [];
49363
+ for (const entry of root.data.servers) {
49364
+ const parsed = mcpStoredServerSchema.safeParse(entry);
49365
+ if (parsed.success)
49366
+ servers.push(parsed.data);
49287
49367
  }
49368
+ return { servers };
49288
49369
  } catch {
49289
49370
  }
49290
49371
  return { servers: [] };
@@ -49917,23 +49998,19 @@ __export(dist_exports2, {
49917
49998
  });
49918
49999
  async function createMcpPlugin(opts) {
49919
50000
  const factory2 = opts.clientFactory ?? defaultClientFactory;
49920
- const clients = [];
49921
- const tools = [];
49922
- try {
49923
- for (const server of opts.servers) {
49924
- const client = await factory2(server, opts);
49925
- clients.push(client);
49926
- const wrapped = await wrapMcpServerTools({
49927
- server,
49928
- client,
49929
- toolNamePrefix: opts.toolNamePrefix
49930
- });
49931
- tools.push(...wrapped);
49932
- }
49933
- } catch (err) {
49934
- await Promise.allSettled(clients.map((c2) => c2.close()));
49935
- throw err;
50001
+ const opened = [];
50002
+ const results = await Promise.allSettled(opts.servers.map(async (server) => {
50003
+ const client = await factory2(server, opts);
50004
+ opened.push(client);
50005
+ return wrapMcpServerTools({ server, client, toolNamePrefix: opts.toolNamePrefix });
50006
+ }));
50007
+ const failure = results.find((r2) => r2.status === "rejected");
50008
+ if (failure) {
50009
+ await Promise.allSettled(opened.map((c2) => c2.close()));
50010
+ throw failure.reason;
49936
50011
  }
50012
+ const tools = results.flatMap((r2) => r2.value);
50013
+ const clients = opened;
49937
50014
  return definePlugin({
49938
50015
  name: "@moxxy/plugin-mcp",
49939
50016
  version: "0.0.0",
@@ -51297,7 +51374,7 @@ var require_react_development = __commonJS({
51297
51374
  var dispatcher = resolveDispatcher();
51298
51375
  return dispatcher.useRef(initialValue);
51299
51376
  }
51300
- function useEffect19(create2, deps) {
51377
+ function useEffect18(create2, deps) {
51301
51378
  var dispatcher = resolveDispatcher();
51302
51379
  return dispatcher.useEffect(create2, deps);
51303
51380
  }
@@ -52080,7 +52157,7 @@ var require_react_development = __commonJS({
52080
52157
  exports.useContext = useContext7;
52081
52158
  exports.useDebugValue = useDebugValue;
52082
52159
  exports.useDeferredValue = useDeferredValue;
52083
- exports.useEffect = useEffect19;
52160
+ exports.useEffect = useEffect18;
52084
52161
  exports.useId = useId;
52085
52162
  exports.useImperativeHandle = useImperativeHandle;
52086
52163
  exports.useInsertionEffect = useInsertionEffect;
@@ -84849,7 +84926,11 @@ function parseInputChunk(chunk, ctx) {
84849
84926
  continue;
84850
84927
  }
84851
84928
  if (c2 === "") {
84852
- process.exit(0);
84929
+ if (ctx.onInterrupt)
84930
+ ctx.onInterrupt();
84931
+ else
84932
+ process.exit(0);
84933
+ return remainder;
84853
84934
  }
84854
84935
  if (c2 === "\x7F" || c2 === "\b") {
84855
84936
  ctx.dispatch({ type: "delete-back" });
@@ -84973,7 +85054,7 @@ var init_PromptInput = __esm({
84973
85054
  init_external_insert();
84974
85055
  init_reducer();
84975
85056
  init_parse_input();
84976
- PromptInput = ({ onSubmit, disabled, placeholder, slashCommands = BUILTIN_SLASH_COMMANDS, onPasteText, commandHotkeys, onShiftTab, externalInsert }) => {
85057
+ PromptInput = ({ onSubmit, disabled, placeholder, slashCommands = BUILTIN_SLASH_COMMANDS, onPasteText, commandHotkeys, onShiftTab, onInterrupt, externalInsert }) => {
84977
85058
  const [state, dispatch3] = (0, import_react26.useReducer)(reducer, INITIAL);
84978
85059
  const [slashCursor, setSlashCursor] = import_react26.default.useState(0);
84979
85060
  const stateRef = (0, import_react26.useRef)(state);
@@ -85033,6 +85114,8 @@ var init_PromptInput = __esm({
85033
85114
  commandHotkeysRef.current = commandHotkeys;
85034
85115
  const onShiftTabRef = (0, import_react26.useRef)(onShiftTab);
85035
85116
  onShiftTabRef.current = onShiftTab;
85117
+ const onInterruptRef = (0, import_react26.useRef)(onInterrupt);
85118
+ onInterruptRef.current = onInterrupt;
85036
85119
  const lastExternalInsertIdRef = (0, import_react26.useRef)(null);
85037
85120
  (0, import_react26.useEffect)(() => {
85038
85121
  const decision = nextExternalInsertAction(lastExternalInsertIdRef.current, externalInsert);
@@ -85058,6 +85141,13 @@ var init_PromptInput = __esm({
85058
85141
  onSlashDown: () => setSlashCursor((c2) => Math.min(slashMatchesRef.current.length - 1, c2 + 1)),
85059
85142
  onSlashAccept: handleSlashAccept,
85060
85143
  onShiftTab: () => onShiftTabRef.current?.(),
85144
+ onInterrupt: () => {
85145
+ const fn = onInterruptRef.current;
85146
+ if (fn)
85147
+ fn();
85148
+ else
85149
+ process.exit(0);
85150
+ },
85061
85151
  onPasteText: (text) => onPasteTextRef.current?.(text) ?? text,
85062
85152
  slashOpen: false,
85063
85153
  bufferRef: { current: { buffer: "", cursor: 0 } },
@@ -87122,8 +87212,37 @@ var init_image_attachments = __esm({
87122
87212
  };
87123
87213
  }
87124
87214
  });
87215
+ function reapStale(dir, now = Date.now()) {
87216
+ let entries;
87217
+ try {
87218
+ entries = readdirSync(dir);
87219
+ } catch {
87220
+ return;
87221
+ }
87222
+ for (const name of entries) {
87223
+ const m3 = CLIP_NAME_RE.exec(name);
87224
+ if (!m3)
87225
+ continue;
87226
+ const stamp = Number(m3[1]);
87227
+ let age = Number.isFinite(stamp) ? now - stamp : Number.NaN;
87228
+ if (!Number.isFinite(age)) {
87229
+ try {
87230
+ age = now - statSync(path3__default.join(dir, name)).mtimeMs;
87231
+ } catch {
87232
+ continue;
87233
+ }
87234
+ }
87235
+ if (age > CACHE_TTL_MS) {
87236
+ try {
87237
+ unlinkSync(path3__default.join(dir, name));
87238
+ } catch {
87239
+ }
87240
+ }
87241
+ }
87242
+ }
87125
87243
  function ensureCacheDir() {
87126
87244
  mkdirSync(CACHE_DIR, { recursive: true });
87245
+ reapStale(CACHE_DIR, Date.now());
87127
87246
  return CACHE_DIR;
87128
87247
  }
87129
87248
  function nextCachePath() {
@@ -87208,10 +87327,12 @@ function readClipboardImageSync() {
87208
87327
  return readClipboardImageLinux();
87209
87328
  return null;
87210
87329
  }
87211
- var CACHE_DIR;
87330
+ var CACHE_DIR, CACHE_TTL_MS, CLIP_NAME_RE;
87212
87331
  var init_clipboard_image = __esm({
87213
87332
  "../plugin-cli/dist/clipboard-image.js"() {
87214
87333
  CACHE_DIR = moxxyPath("image-cache");
87334
+ CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
87335
+ CLIP_NAME_RE = /^clip-(\d+)-[a-z0-9]+\.png$/;
87215
87336
  }
87216
87337
  });
87217
87338
 
@@ -89063,11 +89184,11 @@ var init_OverlayOrNotice = __esm({
89063
89184
  });
89064
89185
 
89065
89186
  // ../plugin-cli/dist/components/PermissionDialog.js
89066
- var import_jsx_runtime32, import_react60, PermissionDialog;
89187
+ var import_jsx_runtime32, PermissionDialog;
89067
89188
  var init_PermissionDialog = __esm({
89068
89189
  async "../plugin-cli/dist/components/PermissionDialog.js"() {
89069
89190
  import_jsx_runtime32 = __toESM(require_jsx_runtime());
89070
- import_react60 = __toESM(require_react());
89191
+ __toESM(require_react());
89071
89192
  await init_build2();
89072
89193
  init_theme();
89073
89194
  await init_Modal();
@@ -89083,8 +89204,6 @@ var init_PermissionDialog = __esm({
89083
89204
  else if (ch === "n" || key.escape)
89084
89205
  onDecide({ mode: "deny", reason: "user declined" });
89085
89206
  });
89086
- (0, import_react60.useEffect)(() => {
89087
- }, []);
89088
89207
  const title = queueDepth > 0 ? `Tool permission requested (${queueDepth} more queued)` : "Tool permission requested";
89089
89208
  return (0, import_jsx_runtime32.jsxs)(Modal, { title, hints: "y allow \xB7 a session \xB7 p always \xB7 n deny", children: [(0, import_jsx_runtime32.jsxs)(Text, { children: ["Tool: ", (0, import_jsx_runtime32.jsx)(Text, { bold: true, children: call.name }), toolDescription ? (0, import_jsx_runtime32.jsxs)(Text, { dimColor: true, children: [" \u2014 ", toolDescription] }) : null] }), (0, import_jsx_runtime32.jsxs)(Text, { dimColor: true, children: ["Input: ", JSON.stringify(call.input).slice(0, 200)] }), (0, import_jsx_runtime32.jsxs)(Text, { children: [(0, import_jsx_runtime32.jsx)(Text, { children: "[y]" }), (0, import_jsx_runtime32.jsx)(Text, { dimColor: true, children: " allow once \xB7 " }), (0, import_jsx_runtime32.jsx)(Text, { children: "[a]" }), (0, import_jsx_runtime32.jsx)(Text, { dimColor: true, children: " allow session \xB7 " }), (0, import_jsx_runtime32.jsx)(Text, { children: "[p]" }), (0, import_jsx_runtime32.jsx)(Text, { dimColor: true, children: " always \xB7 " }), (0, import_jsx_runtime32.jsx)(Text, { color: Colors.danger, children: "[n]" }), (0, import_jsx_runtime32.jsx)(Text, { dimColor: true, children: " deny" })] })] });
89090
89209
  };
@@ -89092,6 +89211,11 @@ var init_PermissionDialog = __esm({
89092
89211
  });
89093
89212
 
89094
89213
  // ../plugin-cli/dist/components/ApprovalDialog.js
89214
+ function jkScrolls(letter, renderedLineCount, options) {
89215
+ if (renderedLineCount <= MAX_BODY_LINES)
89216
+ return false;
89217
+ return !options.some((o2) => o2.hotkey === letter);
89218
+ }
89095
89219
  function isDiffBody(body) {
89096
89220
  return /```diff\b/.test(body) || /^\s*diff --git\b/m.test(body);
89097
89221
  }
@@ -89148,6 +89272,10 @@ var init_ApprovalDialog = __esm({
89148
89272
  const [cursor, setCursor] = (0, import_react61.useState)(initialCursor);
89149
89273
  const [textEntry, setTextEntry] = (0, import_react61.useState)(null);
89150
89274
  const [scrollOffset, setScrollOffset] = (0, import_react61.useState)(0);
89275
+ const rawLines = request.body.split("\n");
89276
+ const diffMode = isDiffBody(request.body);
89277
+ const rendered = diffMode ? renderDiffLines(rawLines) : rawLines.map((text) => ({ text }));
89278
+ const scrollClaims = (letter) => jkScrolls(letter, rendered.length, request.options);
89151
89279
  use_input_default((input, key) => {
89152
89280
  if (textEntry) {
89153
89281
  if (key.return) {
@@ -89171,11 +89299,12 @@ var init_ApprovalDialog = __esm({
89171
89299
  }
89172
89300
  return;
89173
89301
  }
89174
- if (input === "j" || key.pageDown) {
89175
- setScrollOffset((o2) => o2 + Math.max(1, Math.floor(MAX_BODY_LINES / 2)));
89302
+ const maxScroll = Math.max(0, rendered.length - MAX_BODY_LINES);
89303
+ if (input === "j" && scrollClaims("j") || key.pageDown) {
89304
+ setScrollOffset((o2) => Math.min(maxScroll, o2 + Math.max(1, Math.floor(MAX_BODY_LINES / 2))));
89176
89305
  return;
89177
89306
  }
89178
- if (input === "k" || key.pageUp) {
89307
+ if (input === "k" && scrollClaims("k") || key.pageUp) {
89179
89308
  setScrollOffset((o2) => Math.max(0, o2 - Math.max(1, Math.floor(MAX_BODY_LINES / 2))));
89180
89309
  return;
89181
89310
  }
@@ -89223,9 +89352,6 @@ var init_ApprovalDialog = __esm({
89223
89352
  }
89224
89353
  onDecide({ optionId: id });
89225
89354
  };
89226
- const rawLines = request.body.split("\n");
89227
- const diffMode = isDiffBody(request.body);
89228
- const rendered = diffMode ? renderDiffLines(rawLines) : rawLines.map((text) => ({ text }));
89229
89355
  const maxOffset = Math.max(0, rendered.length - MAX_BODY_LINES);
89230
89356
  const offset = Math.min(scrollOffset, maxOffset);
89231
89357
  const visible = rendered.slice(offset, offset + MAX_BODY_LINES);
@@ -113398,7 +113524,7 @@ async function provisionWorkspace(opts) {
113398
113524
  if (install.code !== 0) {
113399
113525
  const loose = await run("pnpm", ["install"], dir);
113400
113526
  if (loose.code !== 0)
113401
- return { ok: false, message: `pnpm install failed: ${trunc(install.output, 400)}`, repoDir: dir };
113527
+ return { ok: false, message: `pnpm install failed: ${trunc(loose.output, 400)}`, repoDir: dir };
113402
113528
  }
113403
113529
  return { ok: true, message: "workspace provisioned", repoDir: dir };
113404
113530
  }
@@ -114031,6 +114157,23 @@ async function escalate(deps, ctx, journal, reason) {
114031
114157
  }
114032
114158
 
114033
114159
  // src/argv.ts
114160
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
114161
+ "help",
114162
+ "h",
114163
+ "version",
114164
+ "v",
114165
+ "yes",
114166
+ "y",
114167
+ "verbose",
114168
+ "allow-all",
114169
+ "standalone",
114170
+ "attach",
114171
+ "reload",
114172
+ "all",
114173
+ "stop",
114174
+ "status",
114175
+ "background"
114176
+ ]);
114034
114177
  function parseArgv(argv) {
114035
114178
  const result = { command: "", flags: {}, positional: [] };
114036
114179
  if (argv.length === 0) {
@@ -114053,7 +114196,7 @@ function parseArgv(argv) {
114053
114196
  } else {
114054
114197
  const key = a2.slice(2);
114055
114198
  const next = argv[i2 + 1];
114056
- if (next && !next.startsWith("-")) {
114199
+ if (next && !next.startsWith("-") && !BOOLEAN_FLAGS.has(key)) {
114057
114200
  result.flags[key] = next;
114058
114201
  i2++;
114059
114202
  } else {
@@ -114063,7 +114206,7 @@ function parseArgv(argv) {
114063
114206
  } else if (a2.startsWith("-")) {
114064
114207
  const key = a2.slice(1);
114065
114208
  const next = argv[i2 + 1];
114066
- if (next && !next.startsWith("-")) {
114209
+ if (next && !next.startsWith("-") && !BOOLEAN_FLAGS.has(key)) {
114067
114210
  result.flags[key] = next;
114068
114211
  i2++;
114069
114212
  } else {
@@ -114317,17 +114460,19 @@ async function loadOne(filePath) {
114317
114460
  }
114318
114461
  return parsed.data;
114319
114462
  }
114320
- var cachedJiti = null;
114463
+ var cachedJiti = /* @__PURE__ */ new Map();
114321
114464
  async function getJiti2(cwd2) {
114322
- if (cachedJiti)
114323
- return cachedJiti;
114465
+ const existing = cachedJiti.get(cwd2);
114466
+ if (existing)
114467
+ return existing;
114324
114468
  try {
114325
114469
  const mod = await Promise.resolve().then(() => (init_jiti(), jiti_exports));
114326
114470
  const factory2 = mod.createJiti ?? mod.default;
114327
114471
  if (!factory2)
114328
114472
  return null;
114329
- cachedJiti = factory2(cwd2, { interopDefault: true });
114330
- return cachedJiti;
114473
+ const instance = factory2(cwd2, { interopDefault: true });
114474
+ cachedJiti.set(cwd2, instance);
114475
+ return instance;
114331
114476
  } catch {
114332
114477
  return null;
114333
114478
  }
@@ -114586,9 +114731,22 @@ function decrypt(blob, key) {
114586
114731
  return plaintext.toString("utf8");
114587
114732
  }
114588
114733
  function randomCode(digits = 6) {
114589
- const max = 10 ** digits;
114590
- const value = randomBytes(4).readUInt32BE(0) % max;
114591
- return value.toString().padStart(digits, "0");
114734
+ if (!Number.isInteger(digits) || digits < 1) {
114735
+ throw new Error(`randomCode: digits must be a positive integer, got ${digits}`);
114736
+ }
114737
+ const modulus = 10n ** BigInt(digits);
114738
+ const byteLen = Math.ceil(digits * Math.log2(10) / 8) + 1;
114739
+ const space = 1n << BigInt(byteLen * 8);
114740
+ const limit2 = space - space % modulus;
114741
+ for (; ; ) {
114742
+ let value = 0n;
114743
+ for (const byte of randomBytes(byteLen)) {
114744
+ value = value << 8n | BigInt(byte);
114745
+ }
114746
+ if (value < limit2) {
114747
+ return (value % modulus).toString().padStart(digits, "0");
114748
+ }
114749
+ }
114592
114750
  }
114593
114751
 
114594
114752
  // ../plugin-vault/dist/keysource.js
@@ -114609,10 +114767,10 @@ function createCombinedKeySource(opts) {
114609
114767
  },
114610
114768
  async obtain(salt) {
114611
114769
  const envName = opts.envVar ?? "MOXXY_VAULT_PASSPHRASE";
114612
- const envValue = process.env[envName];
114613
- if (envValue) {
114770
+ const envValue2 = process.env[envName];
114771
+ if (envValue2) {
114614
114772
  resolvedName = `env:${envName}`;
114615
- return deriveKey(envValue, salt);
114773
+ return deriveKey(envValue2, salt);
114616
114774
  }
114617
114775
  if (!opts.disableKeytar) {
114618
114776
  const fromKeychain = await tryKeychainGet();
@@ -115404,7 +115562,7 @@ async function safeRead(filePath) {
115404
115562
  const result = memoryFrontmatterSchema.safeParse(parsed.frontmatter);
115405
115563
  if (!result.success)
115406
115564
  return null;
115407
- return { frontmatter: result.data, body: parsed.body };
115565
+ return { frontmatter: result.data, body: parsed.body.trim() };
115408
115566
  } catch {
115409
115567
  return null;
115410
115568
  }
@@ -117036,6 +117194,14 @@ async function syncSkillSchedules(registry, store) {
117036
117194
  }
117037
117195
 
117038
117196
  // ../plugin-scheduler/dist/poller.js
117197
+ function cronBaseline(entry) {
117198
+ return entry.lastRunAt ?? entry.createdAt;
117199
+ }
117200
+ function nextCronFire(entry) {
117201
+ if (!entry.cron)
117202
+ return null;
117203
+ return nextFireTime(entry.cron, new Date(cronBaseline(entry)), entry.timeZone);
117204
+ }
117039
117205
  function isDue(entry, now) {
117040
117206
  if (!entry.enabled)
117041
117207
  return false;
@@ -117044,8 +117210,7 @@ function isDue(entry, now) {
117044
117210
  }
117045
117211
  if (!entry.cron)
117046
117212
  return false;
117047
- const since = entry.lastRunAt ?? entry.createdAt;
117048
- const next = nextFireTime(entry.cron, new Date(since), entry.timeZone);
117213
+ const next = nextCronFire(entry);
117049
117214
  if (!next)
117050
117215
  return false;
117051
117216
  return next.getTime() <= now;
@@ -117079,23 +117244,12 @@ var SchedulerPoller = class {
117079
117244
  await this.tickPromise.catch(() => void 0);
117080
117245
  }
117081
117246
  /** Fire any due schedules right now, ignoring the timer cadence.
117082
- * Returns the number of schedules that ran. */
117247
+ * Returns the number of due schedules that were attempted — counted at the
117248
+ * attempt point, so a schedule that fired but failed mid-run (e.g. a
117249
+ * store.update throw) is still counted, unlike piggy-backing on onFired
117250
+ * (which only fires on a clean run). */
117083
117251
  async tickOnce() {
117084
- let count = 0;
117085
- const original = this.opts.onFired;
117086
- let wrapped;
117087
- if (original) {
117088
- wrapped = (entry, outcome) => {
117089
- count += 1;
117090
- original(entry, outcome);
117091
- };
117092
- } else {
117093
- wrapped = () => {
117094
- count += 1;
117095
- };
117096
- }
117097
- await this.tickWith(wrapped);
117098
- return count;
117252
+ return this.tickWith(this.opts.onFired);
117099
117253
  }
117100
117254
  async tick() {
117101
117255
  if (!this.running)
@@ -117120,11 +117274,13 @@ var SchedulerPoller = class {
117120
117274
  this.opts.logger?.error?.("scheduler: failed to read store", {
117121
117275
  err: err instanceof Error ? err.message : String(err)
117122
117276
  });
117123
- return;
117277
+ return 0;
117124
117278
  }
117279
+ let attempted = 0;
117125
117280
  for (const entry of schedules) {
117126
117281
  if (!isDue(entry, now))
117127
117282
  continue;
117283
+ attempted += 1;
117128
117284
  try {
117129
117285
  const outcome = await runSchedule(entry, this.opts.runner, this.opts.store, this.opts.inbox);
117130
117286
  this.opts.logger?.info?.("scheduler: fired", {
@@ -117140,6 +117296,7 @@ var SchedulerPoller = class {
117140
117296
  });
117141
117297
  }
117142
117298
  }
117299
+ return attempted;
117143
117300
  }
117144
117301
  };
117145
117302
 
@@ -117382,11 +117539,9 @@ var cronOrTimestamp = z$1.object({
117382
117539
  message: "provide either `cron` or `runAt`"
117383
117540
  });
117384
117541
  function describeEntry(entry) {
117385
- const now = Date.now();
117386
117542
  let nextFireAt = null;
117387
117543
  if (entry.cron) {
117388
- const since = entry.lastRunAt ?? Math.max(entry.createdAt, now);
117389
- const next = nextFireTime(entry.cron, new Date(since), entry.timeZone);
117544
+ const next = nextCronFire(entry);
117390
117545
  nextFireAt = next ? next.getTime() : null;
117391
117546
  } else if (entry.runAt && entry.enabled) {
117392
117547
  nextFireAt = entry.runAt;
@@ -117680,7 +117835,8 @@ function defaultWebhookInboxDir() {
117680
117835
  async function writeInbox2(trigger, result, deliveryId, opts = {}) {
117681
117836
  const dir = opts.dir ?? defaultWebhookInboxDir();
117682
117837
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
117683
- const file = path3__default.join(dir, `${stamp}-${trigger.name}.md`);
117838
+ const suffix = deliveryId ? deliveryId.replace(/[^A-Za-z0-9._-]/g, "_") : randomUUID().slice(0, 8);
117839
+ const file = path3__default.join(dir, `${stamp}-${trigger.name}-${suffix}.md`);
117684
117840
  const header = [
117685
117841
  "---",
117686
117842
  `webhook: ${trigger.name}`,
@@ -118047,14 +118203,6 @@ var WebhookServer = class {
118047
118203
  res.end(JSON.stringify({ error: "verification_failed" }));
118048
118204
  return;
118049
118205
  }
118050
- if (!shouldFire(trigger.filters, { headers: req.headers, body })) {
118051
- this.opts.logger?.info?.("webhooks: filtered out, not firing", {
118052
- trigger: trigger.name
118053
- });
118054
- res.writeHead(200, { "content-type": "application/json" });
118055
- res.end(JSON.stringify({ status: "filtered" }));
118056
- return;
118057
- }
118058
118206
  const idempKey = idempotencyKey(trigger, req.headers);
118059
118207
  if (idempKey && !this.dedupe.check(trigger.id, idempKey)) {
118060
118208
  this.opts.logger?.info?.("webhooks: duplicate delivery, dropped", {
@@ -118065,6 +118213,14 @@ var WebhookServer = class {
118065
118213
  res.end(JSON.stringify({ status: "duplicate" }));
118066
118214
  return;
118067
118215
  }
118216
+ if (!shouldFire(trigger.filters, { headers: req.headers, body })) {
118217
+ this.opts.logger?.info?.("webhooks: filtered out, not firing", {
118218
+ trigger: trigger.name
118219
+ });
118220
+ res.writeHead(200, { "content-type": "application/json" });
118221
+ res.end(JSON.stringify({ status: "filtered" }));
118222
+ return;
118223
+ }
118068
118224
  res.writeHead(202, { "content-type": "application/json" });
118069
118225
  res.end(JSON.stringify({ status: "accepted", trigger: trigger.name }));
118070
118226
  const prompt = renderPrompt({
@@ -119138,17 +119294,28 @@ function isPathKey(key) {
119138
119294
  function isUrlKey(key) {
119139
119295
  return tokenize4(key).some((w4) => URL_WORDS.has(w4));
119140
119296
  }
119297
+ var INVALID_FILE_URL_PATH = "/\0moxxy-invalid-file-url";
119141
119298
  function extractPaths(input) {
119142
119299
  const out = [];
119143
119300
  walkStrings(input, (key, value) => {
119144
- if (isPathKey(key)) {
119301
+ if (value.startsWith("file://")) {
119302
+ out.push(fileUrlPath(value));
119303
+ } else if (isPathKey(key)) {
119145
119304
  out.push(value);
119146
- } else if (value.startsWith("file://")) {
119147
- out.push(value.slice("file://".length));
119148
119305
  }
119149
119306
  });
119150
119307
  return out;
119151
119308
  }
119309
+ function fileUrlPath(value) {
119310
+ try {
119311
+ const url2 = new URL(value);
119312
+ if (url2.host)
119313
+ return INVALID_FILE_URL_PATH;
119314
+ return decodeURIComponent(url2.pathname);
119315
+ } catch {
119316
+ return INVALID_FILE_URL_PATH;
119317
+ }
119318
+ }
119152
119319
  function extractUrls(input) {
119153
119320
  const out = [];
119154
119321
  walkStrings(input, (key, value) => {
@@ -119181,11 +119348,20 @@ function resolvePattern(pattern, cwd2) {
119181
119348
  }
119182
119349
  return path3.isAbsolute(pattern) ? path3.normalize(pattern) : path3.resolve(cwd2, pattern);
119183
119350
  }
119351
+ var globRegexCache = /* @__PURE__ */ new Map();
119352
+ function compileGlob(pattern) {
119353
+ let re2 = globRegexCache.get(pattern);
119354
+ if (!re2) {
119355
+ const src = "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "<<DOUBLESTAR>>").replace(/\*/g, "[^/]*").replace(/<<DOUBLESTAR>>/g, ".*") + "$";
119356
+ re2 = new RegExp(src);
119357
+ globRegexCache.set(pattern, re2);
119358
+ }
119359
+ return re2;
119360
+ }
119184
119361
  function matchesGlob(p3, pattern) {
119185
119362
  if (pattern.endsWith("/**") && p3 === pattern.slice(0, -3))
119186
119363
  return true;
119187
- const re2 = "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "<<DOUBLESTAR>>").replace(/\*/g, "[^/]*").replace(/<<DOUBLESTAR>>/g, ".*") + "$";
119188
- return new RegExp(re2).test(p3);
119364
+ return compileGlob(pattern).test(p3);
119189
119365
  }
119190
119366
  function hostMatches(host, pattern) {
119191
119367
  if (pattern.startsWith("*.")) {
@@ -119297,6 +119473,7 @@ var IsolatorRegistry2 = class {
119297
119473
  }
119298
119474
  };
119299
119475
  var MAX_BROKER_OUTPUT_BYTES = 8 * 1024 * 1024;
119476
+ var BROKER_KILL_GRACE_MS = 2e3;
119300
119477
  var MAX_FETCH_REDIRECTS = 5;
119301
119478
  var BLOCKED_HANDLER_MODULES = Object.freeze([
119302
119479
  "node:fs",
@@ -119589,12 +119766,21 @@ async function brokerExec(args, { caps, cwd: cwd2, signal }) {
119589
119766
  const errChunks = [];
119590
119767
  let total = 0;
119591
119768
  let settled = false;
119592
- const timer = opts.timeoutMs ? setTimeout(() => {
119769
+ let killTimer = null;
119770
+ const terminate2 = () => {
119593
119771
  child.kill("SIGTERM");
119772
+ if (killTimer)
119773
+ return;
119774
+ killTimer = setTimeout(() => child.kill("SIGKILL"), BROKER_KILL_GRACE_MS);
119775
+ killTimer.unref?.();
119776
+ };
119777
+ const timer = opts.timeoutMs ? setTimeout(() => {
119778
+ terminate2();
119594
119779
  finish(() => reject(new Error(`[broker:exec] '${command}' exceeded ${opts.timeoutMs}ms`)));
119595
119780
  }, opts.timeoutMs) : null;
119596
119781
  const onAbort = () => {
119597
- child.kill("SIGTERM");
119782
+ terminate2();
119783
+ finish(() => reject(new Error(`[broker:exec] '${command}' aborted`)));
119598
119784
  };
119599
119785
  const finish = (settle) => {
119600
119786
  if (settled)
@@ -119605,12 +119791,18 @@ async function brokerExec(args, { caps, cwd: cwd2, signal }) {
119605
119791
  signal.removeEventListener("abort", onAbort);
119606
119792
  settle();
119607
119793
  };
119794
+ const clearKill = () => {
119795
+ if (killTimer) {
119796
+ clearTimeout(killTimer);
119797
+ killTimer = null;
119798
+ }
119799
+ };
119608
119800
  const accumulate = (chunks, b3) => {
119609
119801
  if (settled)
119610
119802
  return;
119611
119803
  total += b3.byteLength;
119612
119804
  if (total > MAX_BROKER_OUTPUT_BYTES) {
119613
- child.kill("SIGTERM");
119805
+ terminate2();
119614
119806
  finish(() => reject(new Error(`[broker:exec] '${command}' output exceeded the ${MAX_BROKER_OUTPUT_BYTES}-byte limit`)));
119615
119807
  return;
119616
119808
  }
@@ -119620,9 +119812,11 @@ async function brokerExec(args, { caps, cwd: cwd2, signal }) {
119620
119812
  child.stderr.on("data", (b3) => accumulate(errChunks, b3));
119621
119813
  signal.addEventListener("abort", onAbort, { once: true });
119622
119814
  child.on("error", (e3) => {
119815
+ clearKill();
119623
119816
  finish(() => reject(e3));
119624
119817
  });
119625
119818
  child.on("close", (exitCode) => {
119819
+ clearKill();
119626
119820
  finish(() => resolve12({
119627
119821
  stdout: Buffer.concat(outChunks).toString("utf8"),
119628
119822
  stderr: Buffer.concat(errChunks).toString("utf8"),
@@ -119899,7 +120093,8 @@ function createWorkerIsolator(opts = {}) {
119899
120093
  let terminated = false;
119900
120094
  const hardTerminate = () => {
119901
120095
  terminated = true;
119902
- void worker.terminate();
120096
+ worker.terminate().catch(() => {
120097
+ });
119903
120098
  };
119904
120099
  const finish = (action, graceful = false) => {
119905
120100
  if (settled)
@@ -120263,6 +120458,7 @@ async function invoke(call, caps, _signal) {
120263
120458
  if (typeof exports.alloc !== "function") {
120264
120459
  throw new Error(`[security:wasm] module does not export 'alloc(size: i32) -> i32'`);
120265
120460
  }
120461
+ memoryHolder.alloc = (size) => exports.alloc(size);
120266
120462
  const handler = exports[call.moduleRef.export];
120267
120463
  if (typeof handler !== "function") {
120268
120464
  throw new Error(`[security:wasm] export '${call.moduleRef.export}' is ${typeof handler}, expected function`);
@@ -120295,8 +120491,10 @@ function buildWasmHostImports(memoryHolder, caps, cwd2) {
120295
120491
  return new TextDecoder().decode(new Uint8Array(memOf().buffer, ptr, len));
120296
120492
  };
120297
120493
  const sendBytes = (outPtrOut, outLenOut, bytes) => {
120298
- const region = reserveScratch(memOf(), bytes.length);
120299
- new Uint8Array(memOf().buffer, region, bytes.length).set(bytes);
120494
+ const region = bytes.length === 0 ? SCRATCH_BASE : memoryHolder.alloc ? memoryHolder.alloc(bytes.length) : reserveScratch(memOf(), bytes.length);
120495
+ if (bytes.length > 0) {
120496
+ new Uint8Array(memOf().buffer, region, bytes.length).set(bytes);
120497
+ }
120300
120498
  writePtrPair(memOf(), outPtrOut, outLenOut, region, bytes.length);
120301
120499
  };
120302
120500
  const sendStr = (outPtrOut, outLenOut, s2) => {
@@ -120337,13 +120535,13 @@ function buildWasmHostImports(memoryHolder, caps, cwd2) {
120337
120535
  */
120338
120536
  broker_fs_write_file: (pathPtr, pathLen, dataPtr, dataLen, outPtrOut, outLenOut) => {
120339
120537
  const filePath = readStr(pathPtr, pathLen);
120340
- const data = readStr(dataPtr, dataLen);
120538
+ const data = new Uint8Array(memOf().buffer, dataPtr, dataLen).slice();
120341
120539
  if (!pathInScope(filePath, caps.fs, cwd2, "write")) {
120342
120540
  return sendErr(outPtrOut, outLenOut, `[broker:fs.writeFile] path '${filePath}' is outside the tool's declared fs.write capability`);
120343
120541
  }
120344
120542
  try {
120345
120543
  mkdirSync(path3.dirname(filePath), { recursive: true });
120346
- writeFileSync(filePath, data, "utf8");
120544
+ writeFileSync(filePath, Buffer$1.from(data));
120347
120545
  writePtrPair(memOf(), outPtrOut, outLenOut, 0, 0);
120348
120546
  return SUCCESS;
120349
120547
  } catch (e3) {
@@ -124450,6 +124648,8 @@ var AnthropicProvider = class {
124450
124648
  const parts = [this.oauth?.systemPreamble, system, req.system].filter((s2) => typeof s2 === "string" && s2.length > 0);
124451
124649
  const systemForCount = parts.length > 0 ? parts.join("\n\n") : void 0;
124452
124650
  try {
124651
+ if (this.oauth)
124652
+ await this.ensureFreshOauth();
124453
124653
  const result = await this.client.messages.countTokens({
124454
124654
  model: req.model || this.defaultModel,
124455
124655
  ...systemForCount !== void 0 ? { system: systemForCount } : {},
@@ -130898,7 +131098,16 @@ var OpenAIProvider = class {
130898
131098
  ...tools ? { tools } : {},
130899
131099
  ...req.temperature !== void 0 ? { temperature: req.temperature } : {},
130900
131100
  ...req.maxTokens ? { [tokenLimitKey]: req.maxTokens } : {},
130901
- ...emitReasoning && usesCompletionTokens && reasoningEffort ? { reasoning_effort: reasoningEffort } : {},
131101
+ // Send `reasoning_effort` independently of the token-field heuristic.
131102
+ // The two are unrelated concerns: `usesCompletionTokens` picks the cap
131103
+ // FIELD NAME for the OpenAI-hosted reasoning models, while effort applies
131104
+ // to any reasoning backend. OpenAI-compatible vendors (z.ai GLM,
131105
+ // DeepSeek-R1, vLLM, Ollama) honor reasoning_effort but their model ids
131106
+ // never match the gpt-5/o1/o3 regex, so gating effort on it silently
131107
+ // dropped a user-requested effort for exactly those backends. The
131108
+ // descriptor's `supportsReasoning` already gates this upstream via
131109
+ // req.reasoning.
131110
+ ...emitReasoning && reasoningEffort ? { reasoning_effort: reasoningEffort } : {},
130902
131111
  stream: true,
130903
131112
  // OpenAI only emits the final `usage` chunk when this is set;
130904
131113
  // without it `raw.usage` is null on every chunk and token usage
@@ -131162,7 +131371,6 @@ function waitForCallback(opts) {
131162
131371
  if (err) {
131163
131372
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
131164
131373
  res.end(htmlPage("OAuth error", `${err}${errDesc ? `: ${errDesc}` : ""}`));
131165
- clearTimeout(timer);
131166
131374
  const denied = err === "access_denied";
131167
131375
  settle(() => reject(new MoxxyError({
131168
131376
  code: denied ? "OAUTH_FLOW_DENIED" : "AUTH_INVALID",
@@ -131177,7 +131385,6 @@ function waitForCallback(opts) {
131177
131385
  if (!code || !returnedState) {
131178
131386
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
131179
131387
  res.end(htmlPage("OAuth error", "callback was missing code or state"));
131180
- clearTimeout(timer);
131181
131388
  settle(() => reject(new MoxxyError({
131182
131389
  code: "AUTH_INVALID",
131183
131390
  message: "OAuth callback was missing code or state \u2014 the upstream redirect is malformed.",
@@ -131188,7 +131395,6 @@ function waitForCallback(opts) {
131188
131395
  if (returnedState !== opts.expectedState) {
131189
131396
  res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
131190
131397
  res.end(htmlPage("OAuth error", "state mismatch \u2014 possible CSRF, refusing"));
131191
- clearTimeout(timer);
131192
131398
  settle(() => reject(new MoxxyError({
131193
131399
  code: "OAUTH_FLOW_STATE_MISMATCH",
131194
131400
  message: "OAuth state mismatch \u2014 possible CSRF attempt, refusing to continue.",
@@ -131246,7 +131452,8 @@ async function exchangeCodeForToken(input, fetchImpl2 = fetch) {
131246
131452
  const res = await fetchImpl2(input.tokenUrl, {
131247
131453
  method: "POST",
131248
131454
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
131249
- body: body.toString()
131455
+ body: body.toString(),
131456
+ ...input.signal ? { signal: input.signal } : {}
131250
131457
  });
131251
131458
  if (!res.ok) {
131252
131459
  const text = await res.text().catch(() => "");
@@ -131362,7 +131569,10 @@ async function runAuthorizationCodeFlow(opts) {
131362
131569
  });
131363
131570
  }
131364
131571
  async function pollUntil(fn, opts) {
131365
- const state = { intervalMs: opts.intervalMs };
131572
+ const state = {
131573
+ intervalMs: opts.intervalMs,
131574
+ ...opts.signal ? { signal: opts.signal } : {}
131575
+ };
131366
131576
  const leadingWait = opts.leadingWait ?? true;
131367
131577
  const deadline = Date.now() + opts.timeoutMs;
131368
131578
  const label3 = opts.label ?? "poll";
@@ -131512,7 +131722,8 @@ async function pollOnce(opts, deviceCode, state) {
131512
131722
  const pollRes = await fetch(opts.tokenUrl, {
131513
131723
  method: "POST",
131514
131724
  headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
131515
- body: body.toString()
131725
+ body: body.toString(),
131726
+ ...state.signal ? { signal: state.signal } : {}
131516
131727
  });
131517
131728
  const pollJson = await pollRes.json().catch(() => ({}));
131518
131729
  return classifyDeviceTokenResponse(pollRes, pollJson, state);
@@ -132110,12 +132321,13 @@ function openaiDeviceFlow(opts) {
132110
132321
  }
132111
132322
  };
132112
132323
  },
132113
- async poll(init3, _state) {
132324
+ async poll(init3, state) {
132114
132325
  const { deviceAuthId, userCode, clientId } = init3.providerData;
132115
132326
  const res = await fetch(pollUrl, {
132116
132327
  method: "POST",
132117
132328
  headers: { "Content-Type": "application/json" },
132118
- body: JSON.stringify({ device_auth_id: deviceAuthId, user_code: userCode })
132329
+ body: JSON.stringify({ device_auth_id: deviceAuthId, user_code: userCode }),
132330
+ ...state.signal ? { signal: state.signal } : {}
132119
132331
  });
132120
132332
  if (res.ok) {
132121
132333
  const data = await res.json();
@@ -132125,7 +132337,8 @@ function openaiDeviceFlow(opts) {
132125
132337
  code: data.authorization_code,
132126
132338
  redirectUri: exchangeRedirectUri,
132127
132339
  clientId,
132128
- codeVerifier: data.code_verifier
132340
+ codeVerifier: data.code_verifier,
132341
+ ...state.signal ? { signal: state.signal } : {}
132129
132342
  })
132130
132343
  };
132131
132344
  }
@@ -132547,9 +132760,16 @@ async function* consumeResponsesSse(body, signal, emitReasoning = false) {
132547
132760
  if (errored)
132548
132761
  return;
132549
132762
  for (const entry of pending2.values()) {
132763
+ const outId = entry.callId || entry.id;
132764
+ if (!entry.emittedStart && entry.name) {
132765
+ entry.emittedStart = true;
132766
+ yield { type: "tool_use_start", id: outId, name: entry.name };
132767
+ }
132550
132768
  if (entry.emittedStart) {
132551
132769
  sawToolCall = true;
132552
- yield { type: "tool_use_end", id: entry.callId || entry.id, input: parseToolArgs(entry.args) };
132770
+ yield { type: "tool_use_end", id: outId, input: parseToolArgs(entry.args) };
132771
+ } else if (process.env.MOXXY_DEBUG) {
132772
+ console.error(`[openai-codex] dropping a truncated function_call with no name (id=${outId}, args=${entry.args.length}B)`);
132553
132773
  }
132554
132774
  }
132555
132775
  if (stopReason === "end_turn" && sawToolCall) {
@@ -132629,6 +132849,14 @@ var CodexProvider = class {
132629
132849
  yield toErrorEvent(err);
132630
132850
  return;
132631
132851
  }
132852
+ if (response.status === 401) {
132853
+ yield {
132854
+ type: "error",
132855
+ message: "ChatGPT OAuth credentials were rejected after a token refresh. Run `moxxy login openai-codex` to re-authenticate.",
132856
+ retryable: false
132857
+ };
132858
+ return;
132859
+ }
132632
132860
  }
132633
132861
  if (!response.ok || !response.body) {
132634
132862
  const text = await response.text().catch(() => "");
@@ -134823,6 +135051,18 @@ async function* runGoalMode(ctx) {
134823
135051
  continue;
134824
135052
  }
134825
135053
  reactiveCompactions = 0;
135054
+ if (reasoning) {
135055
+ yield await ctx.emit({
135056
+ type: "reasoning_message",
135057
+ sessionId: ctx.sessionId,
135058
+ turnId: ctx.turnId,
135059
+ source: "model",
135060
+ content: reasoning.text,
135061
+ ...reasoning.signature ? { signature: reasoning.signature } : {},
135062
+ ...reasoning.redacted ? { redacted: true } : {},
135063
+ ...reasoning.encrypted ? { encrypted: reasoning.encrypted } : {}
135064
+ });
135065
+ }
134826
135066
  if (totalTokens > GOAL_TOKEN_BUDGET) {
134827
135067
  yield await ctx.emit({
134828
135068
  type: "plugin_event",
@@ -134843,18 +135083,6 @@ async function* runGoalMode(ctx) {
134843
135083
  });
134844
135084
  return;
134845
135085
  }
134846
- if (reasoning) {
134847
- yield await ctx.emit({
134848
- type: "reasoning_message",
134849
- sessionId: ctx.sessionId,
134850
- turnId: ctx.turnId,
134851
- source: "model",
134852
- content: reasoning.text,
134853
- ...reasoning.signature ? { signature: reasoning.signature } : {},
134854
- ...reasoning.redacted ? { redacted: true } : {},
134855
- ...reasoning.encrypted ? { encrypted: reasoning.encrypted } : {}
134856
- });
134857
- }
134858
135086
  const stuck = yield* emitRequestsAndDetectStuck(ctx, toolUses, detector, {
134859
135087
  abortedResultMessage: "goal mode aborted (stuck pattern) before this call ran",
134860
135088
  nearHint: "against the same target (only volatile args varied)",
@@ -135455,6 +135683,16 @@ async function* runDeepResearchMode(ctx) {
135455
135683
  const synthesisText = await collectSynthesis(ctx, synthesisInput);
135456
135684
  if (synthesisText === null)
135457
135685
  return;
135686
+ if (ctx.signal.aborted) {
135687
+ yield await ctx.emit({
135688
+ type: "abort",
135689
+ sessionId: ctx.sessionId,
135690
+ turnId: ctx.turnId,
135691
+ source: "system",
135692
+ reason: "aborted during synthesis"
135693
+ });
135694
+ return;
135695
+ }
135458
135696
  yield await ctx.emit({
135459
135697
  type: "assistant_message",
135460
135698
  sessionId: ctx.sessionId,
@@ -135907,11 +136145,10 @@ async function createUnixSocketServer(socketPath, logger = stderrLogger) {
135907
136145
  } else {
135908
136146
  warnWindowsPipeAclOnce(logger, socketPath);
135909
136147
  }
135910
- const connectionHandlers = [];
136148
+ let connectionHandler;
135911
136149
  const server = net2.createServer((socket) => {
135912
136150
  const transport = new NdjsonTransport(socket);
135913
- for (const handler of connectionHandlers)
135914
- handler(transport);
136151
+ connectionHandler?.(transport);
135915
136152
  });
135916
136153
  await new Promise((resolve12, reject) => {
135917
136154
  server.once("error", reject);
@@ -135933,7 +136170,7 @@ async function createUnixSocketServer(socketPath, logger = stderrLogger) {
135933
136170
  return {
135934
136171
  address: socketPath,
135935
136172
  onConnection(handler) {
135936
- connectionHandlers.push(handler);
136173
+ connectionHandler = handler;
135937
136174
  },
135938
136175
  close() {
135939
136176
  return new Promise((resolve12) => {
@@ -139655,6 +139892,7 @@ var import_grammy6 = __toESM(require_mod2());
139655
139892
 
139656
139893
  // ../plugin-telegram/dist/channel.js
139657
139894
  var import_grammy5 = __toESM(require_mod2());
139895
+ init_dist();
139658
139896
 
139659
139897
  // ../plugin-telegram/dist/permission.js
139660
139898
  var TelegramPermissionResolver = class {
@@ -140111,7 +140349,16 @@ function composeFrame(snap) {
140111
140349
  return parts.join("\n\n");
140112
140350
  }
140113
140351
  function stripHtml(html) {
140114
- return html.replace(/<\/?[a-z][^>]*>/gi, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"');
140352
+ return html.replace(/<\/?[a-z][^>]*>/gi, "").replace(/&#(\d+);/g, (_2, dec) => safeFromCodePoint(parseInt(dec, 10))).replace(/&#x([0-9a-f]+);/gi, (_2, hex) => safeFromCodePoint(parseInt(hex, 16))).replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&amp;/g, "&");
140353
+ }
140354
+ function safeFromCodePoint(cp) {
140355
+ if (!Number.isFinite(cp) || cp < 0 || cp > 1114111)
140356
+ return "";
140357
+ try {
140358
+ return String.fromCodePoint(cp);
140359
+ } catch {
140360
+ return "";
140361
+ }
140115
140362
  }
140116
140363
  function truncate3(s2, n2) {
140117
140364
  return s2.length <= n2 ? s2 : s2.slice(0, n2) + "\u2026";
@@ -140882,13 +141129,11 @@ function mapChoice(choice) {
140882
141129
  }
140883
141130
 
140884
141131
  // ../plugin-telegram/dist/channel/turn-runner.js
140885
- init_dist();
140886
141132
  async function runUserTurn(ctx, deps, opts) {
140887
141133
  const { session, bot, framePump, typing, logger } = deps;
140888
- const { chatId, text, model, controller } = opts;
141134
+ const { chatId, text, model, controller, turnId } = opts;
140889
141135
  framePump.beginTurn(chatId);
140890
141136
  typing.start(bot, chatId);
140891
- const turnId = newTurnId();
140892
141137
  const unsubscribe = session.log.subscribe((event) => {
140893
141138
  if (event.turnId !== turnId)
140894
141139
  return;
@@ -141089,6 +141334,12 @@ var TelegramChannel = class {
141089
141334
  // without poisoning the session-level signal (which other channels
141090
141335
  // sharing the same Session would also observe).
141091
141336
  turnController = null;
141337
+ // turnIds of turns THIS channel initiated. mirrorForeignTurn filters on these
141338
+ // (invariant #8: filter event-log subscribers by turnId when multiplexing
141339
+ // turns on one Session) rather than the coarse `busy` flag alone — so an
141340
+ // assistant_message dispatched for our own turn AFTER `busy` flips false
141341
+ // (async event ordering / RemoteSession replay) isn't re-mirrored as foreign.
141342
+ ownTurnIds = /* @__PURE__ */ new Set();
141092
141343
  // When a user clicks an approval option that needs text follow-up
141093
141344
  // (e.g. plan-execute "Redraft with feedback"), we stash the
141094
141345
  // approval+option pair and capture the user's NEXT message as the
@@ -141234,6 +141485,13 @@ var TelegramChannel = class {
141234
141485
  const controller = new AbortController();
141235
141486
  this.turnController = controller;
141236
141487
  const effectiveModel = this.activeModelOverride ?? this.model;
141488
+ const turnId = newTurnId();
141489
+ this.ownTurnIds.add(turnId);
141490
+ if (this.ownTurnIds.size > 64) {
141491
+ const oldest = this.ownTurnIds.values().next().value;
141492
+ if (oldest !== void 0)
141493
+ this.ownTurnIds.delete(oldest);
141494
+ }
141237
141495
  try {
141238
141496
  await runUserTurn(ctx, {
141239
141497
  session: this.session,
@@ -141241,7 +141499,7 @@ var TelegramChannel = class {
141241
141499
  framePump: this.framePump,
141242
141500
  typing: this.typing,
141243
141501
  ...this.opts.logger ? { logger: this.opts.logger } : {}
141244
- }, { chatId, text, model: effectiveModel, controller });
141502
+ }, { chatId, text, model: effectiveModel, controller, turnId });
141245
141503
  } finally {
141246
141504
  this.busy = false;
141247
141505
  this.turnController = null;
@@ -141255,7 +141513,11 @@ var TelegramChannel = class {
141255
141513
  * avoid parse-mode pitfalls; the view itself lives on the web surface.
141256
141514
  */
141257
141515
  mirrorForeignTurn(event) {
141258
- if (event.type !== "assistant_message" || this.busy)
141516
+ if (event.type !== "assistant_message")
141517
+ return;
141518
+ if (this.ownTurnIds.has(event.turnId))
141519
+ return;
141520
+ if (this.busy)
141259
141521
  return;
141260
141522
  if (!this.bot || this.lastChatId == null)
141261
141523
  return;
@@ -142332,10 +142594,16 @@ async function runPairFlow(ctx) {
142332
142594
  await session.close("SIGINT").catch(() => void 0);
142333
142595
  process.exit(0);
142334
142596
  };
142335
- process.on("SIGINT", () => void shutdown());
142336
- process.on("SIGTERM", () => void shutdown());
142337
- await handle2.running;
142338
- return 0;
142597
+ const onSignal = () => void shutdown();
142598
+ process.once("SIGINT", onSignal);
142599
+ process.once("SIGTERM", onSignal);
142600
+ try {
142601
+ await handle2.running;
142602
+ return 0;
142603
+ } finally {
142604
+ process.removeListener("SIGINT", onSignal);
142605
+ process.removeListener("SIGTERM", onSignal);
142606
+ }
142339
142607
  }
142340
142608
 
142341
142609
  // ../plugin-telegram/dist/setup-wizard.js
@@ -142582,8 +142850,8 @@ function buildTelegramPlugin(opts) {
142582
142850
  if (!targetChat) {
142583
142851
  throw new Error("no authorized chat \u2014 run `moxxy channels telegram pair` first or pass `chatId` explicitly");
142584
142852
  }
142585
- const bot = new import_grammy6.Bot(token);
142586
- await bot.api.sendMessage(targetChat, text, parseMode ? { parse_mode: parseMode } : {});
142853
+ const api = new import_grammy6.Api(token);
142854
+ await api.sendMessage(targetChat, text, parseMode ? { parse_mode: parseMode } : {});
142587
142855
  return { delivered: true, chatId: targetChat, length: text.length };
142588
142856
  }
142589
142857
  }),
@@ -142816,6 +143084,9 @@ var HttpChannel = class {
142816
143084
  this.permissionResolver = opts.allowedTools && opts.allowedTools.length > 0 ? createAllowListResolver([...opts.allowedTools]) : denyByDefaultResolver;
142817
143085
  }
142818
143086
  async start(startOpts) {
143087
+ if (this.server) {
143088
+ throw new Error("HttpChannel is already started \u2014 call stop() before starting again.");
143089
+ }
142819
143090
  const ctx = {
142820
143091
  session: startOpts.session,
142821
143092
  authToken: this.authToken,
@@ -142874,11 +143145,16 @@ var HttpChannel = class {
142874
143145
  return {
142875
143146
  running,
142876
143147
  stop: async () => {
143148
+ const srv = this.server;
142877
143149
  await new Promise((resolve12) => {
142878
- if (!this.server)
143150
+ if (!srv)
142879
143151
  return resolve12();
142880
- this.server.close(() => resolve12());
143152
+ srv.close(() => resolve12());
142881
143153
  });
143154
+ if (this.server === srv) {
143155
+ this.server = null;
143156
+ this.boundPortValue = 0;
143157
+ }
142882
143158
  }
142883
143159
  };
142884
143160
  }
@@ -143052,13 +143328,19 @@ async function pidCommand2(pid) {
143052
143328
  function looksLikeMoxxy(command) {
143053
143329
  return command.length > 0 && /moxxy/i.test(command);
143054
143330
  }
143055
- async function freeTcpPortIfMoxxy(port, logger) {
143331
+ var realFreePortDeps = {
143332
+ pidsListeningOn,
143333
+ pidCommand: pidCommand2,
143334
+ kill: (pid, signal) => process.kill(pid, signal),
143335
+ graceMs: 400
143336
+ };
143337
+ async function freeTcpPortIfMoxxy(port, logger, deps = realFreePortDeps) {
143056
143338
  if (process.platform === "win32")
143057
143339
  return false;
143058
- const pids = (await pidsListeningOn(port)).filter((pid) => pid !== process.pid);
143340
+ const pids = (await deps.pidsListeningOn(port)).filter((pid) => pid !== process.pid);
143059
143341
  if (pids.length === 0)
143060
143342
  return false;
143061
- const holders = await Promise.all(pids.map(async (pid) => ({ pid, command: await pidCommand2(pid) })));
143343
+ const holders = await Promise.all(pids.map(async (pid) => ({ pid, command: await deps.pidCommand(pid) })));
143062
143344
  const foreign = holders.filter((h3) => !looksLikeMoxxy(h3.command));
143063
143345
  if (foreign.length > 0) {
143064
143346
  logger?.warn?.(`port ${port} is held by non-moxxy process(es); not killing them`, {
@@ -143066,17 +143348,29 @@ async function freeTcpPortIfMoxxy(port, logger) {
143066
143348
  });
143067
143349
  return false;
143068
143350
  }
143351
+ let attempted = false;
143069
143352
  for (const { pid } of holders) {
143353
+ if (!looksLikeMoxxy(await deps.pidCommand(pid)))
143354
+ continue;
143070
143355
  try {
143071
- process.kill(pid, "SIGTERM");
143356
+ deps.kill(pid, "SIGTERM");
143357
+ attempted = true;
143072
143358
  } catch {
143073
143359
  }
143074
143360
  }
143075
- await new Promise((r2) => setTimeout(r2, 400));
143361
+ if (!attempted)
143362
+ return false;
143363
+ await new Promise((r2) => setTimeout(r2, deps.graceMs ?? 400));
143076
143364
  for (const { pid } of holders) {
143077
143365
  try {
143078
- process.kill(pid, 0);
143079
- process.kill(pid, "SIGKILL");
143366
+ deps.kill(pid, 0);
143367
+ } catch {
143368
+ continue;
143369
+ }
143370
+ if (!looksLikeMoxxy(await deps.pidCommand(pid)))
143371
+ continue;
143372
+ try {
143373
+ deps.kill(pid, "SIGKILL");
143080
143374
  } catch {
143081
143375
  }
143082
143376
  }
@@ -143634,6 +143928,7 @@ async function createWebSocketTransportServer(opts) {
143634
143928
  let currentToken = opts.authToken;
143635
143929
  let currentAllowedOrigins = opts.allowedOrigins ?? [];
143636
143930
  let connections = 0;
143931
+ let pending2 = 0;
143637
143932
  const wss = new import_websocket_server.default({
143638
143933
  host,
143639
143934
  port: opts.port,
@@ -143643,11 +143938,14 @@ async function createWebSocketTransportServer(opts) {
143643
143938
  console.warn(`[moxxy] ws bridge: rejected browser-origin upgrade (Origin: ${String(info.req.headers.origin)})`);
143644
143939
  return false;
143645
143940
  }
143646
- if (connections >= maxConnections) {
143941
+ if (connections + pending2 >= maxConnections) {
143647
143942
  console.warn(`[moxxy] ws bridge: rejected upgrade \u2014 connection cap (${maxConnections}) reached`);
143648
143943
  return false;
143649
143944
  }
143650
- return checkWsAuth(info.req, currentToken, { allowQueryToken: opts.allowQueryToken });
143945
+ const ok = checkWsAuth(info.req, currentToken, { allowQueryToken: opts.allowQueryToken });
143946
+ if (ok)
143947
+ pending2 += 1;
143948
+ return ok;
143651
143949
  },
143652
143950
  // When the client offers subprotocols (the moxxy.bearer.* convention),
143653
143951
  // select the moxxy protocol WITHOUT echoing the token-bearing entry back.
@@ -143655,6 +143953,8 @@ async function createWebSocketTransportServer(opts) {
143655
143953
  });
143656
143954
  const connectionHandlers = [];
143657
143955
  wss.on("connection", (ws) => {
143956
+ if (pending2 > 0)
143957
+ pending2 -= 1;
143658
143958
  connections += 1;
143659
143959
  ws.once("close", () => {
143660
143960
  connections -= 1;
@@ -144105,6 +144405,7 @@ var MobileSessionHost = class {
144105
144405
  this.bus.handle("session.newSession", async () => {
144106
144406
  for (const controller of this.turns.values())
144107
144407
  controller.abort();
144408
+ this.autoApprove = false;
144108
144409
  if (this.session.reset)
144109
144410
  await this.session.reset();
144110
144411
  else
@@ -144251,6 +144552,8 @@ var MobileSessionHost = class {
144251
144552
  return { turnId };
144252
144553
  }
144253
144554
  openAsk(req) {
144555
+ if (this.disposed)
144556
+ return Promise.resolve({ mode: "deny" });
144254
144557
  const requestId = `ask-${++this.askCounter}`;
144255
144558
  return new Promise((resolve12) => {
144256
144559
  this.pendingAsks.set(requestId, resolve12);
@@ -144562,7 +144865,7 @@ function htmlToMarkdown(html, opts = {}) {
144562
144865
  ${"#".repeat(Number(lvl))} ${stripTags(inner).trim()}
144563
144866
 
144564
144867
  `);
144565
- body = body.replace(/<a\b[^>]*\bhref="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi, (_2, href, inner) => `[${stripTags(inner).trim()}](${href})`);
144868
+ body = body.replace(/<a\b[^>]*\bhref=(?:"([^"]+)"|'([^']+)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a>/gi, (_2, dq, sq, uq, inner) => `[${stripTags(inner).trim()}](${dq ?? sq ?? uq})`);
144566
144869
  body = body.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre>/gi, (_2, inner) => `
144567
144870
 
144568
144871
  \`\`\`
@@ -145479,6 +145782,8 @@ var TerminalProcessImpl = class {
145479
145782
  child.stderr.on("data", (b3) => this.emitData(b3.toString("utf8")));
145480
145783
  child.on("exit", (code) => this.emitExit(code ?? 0));
145481
145784
  child.on("error", () => this.emitExit(1));
145785
+ child.stdin.on("error", () => {
145786
+ });
145482
145787
  }
145483
145788
  }
145484
145789
  emitData(d2) {
@@ -145518,10 +145823,13 @@ var TerminalProcessImpl = class {
145518
145823
  write(data) {
145519
145824
  if (!this.alive)
145520
145825
  return;
145521
- if (this.pty)
145522
- this.pty.write(data);
145523
- else
145524
- this.child?.stdin.write(data);
145826
+ try {
145827
+ if (this.pty)
145828
+ this.pty.write(data);
145829
+ else
145830
+ this.child?.stdin.write(data);
145831
+ } catch {
145832
+ }
145525
145833
  }
145526
145834
  resize(cols, rows) {
145527
145835
  if (this.pty && this.alive) {
@@ -146137,16 +146445,16 @@ function buildProviderAdminPlugin(opts) {
146137
146445
  // ../plugin-usage-stats/dist/index.js
146138
146446
  init_dist();
146139
146447
  function buildUsageStatsPlugin(opts = {}) {
146140
- let cursor = 0;
146448
+ let initMaxSeq = null;
146141
146449
  return definePlugin({
146142
146450
  name: "@moxxy/plugin-usage-stats",
146143
146451
  version: "0.0.0",
146144
146452
  hooks: {
146145
146453
  onInit(ctx) {
146146
- cursor = ctx.log.length;
146454
+ initMaxSeq = maxSeq(ctx.log.slice());
146147
146455
  },
146148
146456
  async onShutdown(ctx) {
146149
- const live = ctx.log.slice(cursor);
146457
+ const live = initMaxSeq === null ? ctx.log.slice() : ctx.log.slice().filter((e3) => e3.seq > initMaxSeq);
146150
146458
  if (live.length === 0)
146151
146459
  return;
146152
146460
  const delta = summarizeTokensByModel(live);
@@ -146155,6 +146463,13 @@ function buildUsageStatsPlugin(opts = {}) {
146155
146463
  }
146156
146464
  });
146157
146465
  }
146466
+ function maxSeq(events) {
146467
+ let max = null;
146468
+ for (const e3 of events)
146469
+ if (max === null || e3.seq > max)
146470
+ max = e3.seq;
146471
+ return max;
146472
+ }
146158
146473
  var infoCmd = {
146159
146474
  name: "info",
146160
146475
  description: "Show provider \xB7 model \xB7 mode \xB7 plugin/skill counts",
@@ -146273,7 +146588,8 @@ async function compactSession(session) {
146273
146588
  ...result
146274
146589
  };
146275
146590
  await s2.log.append(emittable);
146276
- const compactedEvents = result.replacedRange[1] - result.replacedRange[0] + 1;
146591
+ const [fromSeq, toSeq] = result.replacedRange;
146592
+ const compactedEvents = events.filter((e3) => e3.seq >= fromSeq && e3.seq <= toSeq).length;
146277
146593
  return {
146278
146594
  kind: "text",
146279
146595
  text: `context compacted: ${formatCount2(compactedEvents)} ${plural2(compactedEvents, "event")}, ~${formatTokenCount2(result.tokensSaved)} tokens saved`
@@ -146348,15 +146664,26 @@ function buildViewPlugin(opts) {
146348
146664
  });
146349
146665
  }
146350
146666
  var IS_DARWIN = process.platform === "darwin";
146667
+ function procFailureCause(proc, timeoutMs) {
146668
+ if (proc.timedOut) {
146669
+ return timeoutMs ? `timed out after ${timeoutMs}ms` : "timed out";
146670
+ }
146671
+ if (proc.aborted)
146672
+ return "aborted (turn cancelled)";
146673
+ return "";
146674
+ }
146351
146675
  function runProcess2(cmd, args, opts = {}) {
146352
146676
  return new Promise((resolve12, reject) => {
146353
146677
  const child = spawn(cmd, [...args], { stdio: ["pipe", "pipe", "pipe"] });
146354
146678
  const stdoutChunks = [];
146355
146679
  let stderr = "";
146356
146680
  let settled = false;
146681
+ let timedOut = false;
146682
+ let aborted = false;
146357
146683
  const onAbort = () => {
146358
146684
  if (settled)
146359
146685
  return;
146686
+ aborted = true;
146360
146687
  try {
146361
146688
  child.kill("SIGTERM");
146362
146689
  } catch {
@@ -146366,6 +146693,7 @@ function runProcess2(cmd, args, opts = {}) {
146366
146693
  const timer = opts.timeoutMs ? setTimeout(() => {
146367
146694
  if (settled)
146368
146695
  return;
146696
+ timedOut = true;
146369
146697
  try {
146370
146698
  child.kill("SIGTERM");
146371
146699
  } catch {
@@ -146396,7 +146724,9 @@ function runProcess2(cmd, args, opts = {}) {
146396
146724
  resolve12({
146397
146725
  exitCode: code ?? -1,
146398
146726
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
146399
- stderr
146727
+ stderr,
146728
+ timedOut,
146729
+ aborted
146400
146730
  });
146401
146731
  });
146402
146732
  if (opts.input !== void 0) {
@@ -146429,10 +146759,11 @@ var applescriptTool = defineTool({
146429
146759
  timeoutMs: 3e4
146430
146760
  });
146431
146761
  if (proc.exitCode !== 0) {
146762
+ const cause = procFailureCause(proc, 3e4);
146432
146763
  throw new MoxxyError({
146433
146764
  code: "TOOL_ERROR",
146434
- message: `osascript failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(no error message)"}`,
146435
- context: { tool: "computer_applescript", exitCode: proc.exitCode }
146765
+ message: cause ? `osascript ${cause}` : `osascript failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(no error message)"}`,
146766
+ context: { tool: "computer_applescript", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146436
146767
  });
146437
146768
  }
146438
146769
  return { ok: true, output: proc.stdout.trim() };
@@ -146460,10 +146791,11 @@ end tell`;
146460
146791
  timeoutMs: 1e4
146461
146792
  });
146462
146793
  if (proc.exitCode !== 0) {
146794
+ const cause = procFailureCause(proc, 1e4);
146463
146795
  throw new MoxxyError({
146464
146796
  code: "TOOL_ERROR",
146465
- message: `click failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission in System Settings \u2192 Privacy & Security \u2192 Accessibility)"}`,
146466
- context: { tool: "computer_click", exitCode: proc.exitCode }
146797
+ message: cause ? `click ${cause}` : `click failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission in System Settings \u2192 Privacy & Security \u2192 Accessibility)"}`,
146798
+ context: { tool: "computer_click", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146467
146799
  });
146468
146800
  }
146469
146801
  return { ok: true, x: x4, y: y2, count: n2 };
@@ -146485,10 +146817,11 @@ var clipboardTool = defineTool({
146485
146817
  timeoutMs: 5e3
146486
146818
  });
146487
146819
  if (proc2.exitCode !== 0) {
146820
+ const cause = procFailureCause(proc2, 5e3);
146488
146821
  throw new MoxxyError({
146489
146822
  code: "TOOL_ERROR",
146490
- message: `pbpaste failed (exit ${proc2.exitCode}): ${proc2.stderr.trim()}`,
146491
- context: { tool: "computer_clipboard", exitCode: proc2.exitCode }
146823
+ message: cause ? `pbpaste ${cause}` : `pbpaste failed (exit ${proc2.exitCode}): ${proc2.stderr.trim()}`,
146824
+ context: { tool: "computer_clipboard", exitCode: proc2.exitCode, timedOut: proc2.timedOut ? 1 : 0 }
146492
146825
  });
146493
146826
  }
146494
146827
  return { ok: true, text: proc2.stdout };
@@ -146506,10 +146839,11 @@ var clipboardTool = defineTool({
146506
146839
  timeoutMs: 5e3
146507
146840
  });
146508
146841
  if (proc.exitCode !== 0) {
146842
+ const cause = procFailureCause(proc, 5e3);
146509
146843
  throw new MoxxyError({
146510
146844
  code: "TOOL_ERROR",
146511
- message: `pbcopy failed (exit ${proc.exitCode}): ${proc.stderr.trim()}`,
146512
- context: { tool: "computer_clipboard", exitCode: proc.exitCode }
146845
+ message: cause ? `pbcopy ${cause}` : `pbcopy failed (exit ${proc.exitCode}): ${proc.stderr.trim()}`,
146846
+ context: { tool: "computer_clipboard", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146513
146847
  });
146514
146848
  }
146515
146849
  return { ok: true, length: text.length };
@@ -146547,6 +146881,16 @@ var KEY_CODES = {
146547
146881
  f11: 103,
146548
146882
  f12: 111
146549
146883
  };
146884
+ var WHITESPACE_KEY_CODES = {
146885
+ " ": 49,
146886
+ // space
146887
+ " ": 48,
146888
+ // tab
146889
+ "\r": 36,
146890
+ // return
146891
+ "\n": 36
146892
+ // return
146893
+ };
146550
146894
  var keyTool = defineTool({
146551
146895
  name: "computer_key",
146552
146896
  description: "Send a single key chord with optional modifiers. Use this for shortcuts (cmd+c, cmd+tab, cmd+shift+4) and named keys (return, tab, escape, arrows, page_up/down, f1\u2013f12). For typing arbitrary text, use computer_type.",
@@ -146564,10 +146908,11 @@ var keyTool = defineTool({
146564
146908
  timeoutMs: 1e4
146565
146909
  });
146566
146910
  if (proc.exitCode !== 0) {
146911
+ const cause = procFailureCause(proc, 1e4);
146567
146912
  throw new MoxxyError({
146568
146913
  code: "TOOL_ERROR",
146569
- message: `key failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission)"}`,
146570
- context: { tool: "computer_key", exitCode: proc.exitCode }
146914
+ message: cause ? `key ${cause}` : `key failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission)"}`,
146915
+ context: { tool: "computer_key", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146571
146916
  });
146572
146917
  }
146573
146918
  return { ok: true, key, modifiers: mods };
@@ -146579,6 +146924,12 @@ function buildKeyScript(key, modifiers) {
146579
146924
  if (lower2 in KEY_CODES) {
146580
146925
  return `tell application "System Events" to key code ${KEY_CODES[lower2]}${usingClause}`;
146581
146926
  }
146927
+ if (key.length === 1) {
146928
+ const wsCode = WHITESPACE_KEY_CODES[key];
146929
+ if (wsCode !== void 0) {
146930
+ return `tell application "System Events" to key code ${wsCode}${usingClause}`;
146931
+ }
146932
+ }
146582
146933
  if (key.length === 1) {
146583
146934
  const literal3 = `"${key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
146584
146935
  return `tell application "System Events" to keystroke ${literal3}${usingClause}`;
@@ -146630,10 +146981,11 @@ var openTool = defineTool({
146630
146981
  timeoutMs: 1e4
146631
146982
  });
146632
146983
  if (proc.exitCode !== 0) {
146984
+ const cause = procFailureCause(proc, 1e4);
146633
146985
  throw new MoxxyError({
146634
146986
  code: "TOOL_ERROR",
146635
- message: `open failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(no error message)"}`,
146636
- context: { tool: "computer_open", exitCode: proc.exitCode }
146987
+ message: cause ? `open ${cause}` : `open failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(no error message)"}`,
146988
+ context: { tool: "computer_open", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146637
146989
  });
146638
146990
  }
146639
146991
  return { ok: true, app, target };
@@ -146664,7 +147016,8 @@ var screenshotTool = defineTool({
146664
147016
  const fmt2 = format ?? DEFAULT_FORMAT;
146665
147017
  const dim3 = maxDim ?? DEFAULT_MAX_DIM;
146666
147018
  const q3 = quality ?? DEFAULT_JPEG_QUALITY;
146667
- const captureTmp = path3.join(os5.tmpdir(), `moxxy-screencap-${process.pid}-${Date.now()}.png`);
147019
+ const uniq = randomUUID();
147020
+ const captureTmp = path3.join(os5.tmpdir(), `moxxy-screencap-${process.pid}-${Date.now()}-${uniq}.png`);
146668
147021
  const captureArgs = ["-x", "-t", "png"];
146669
147022
  if (region) {
146670
147023
  captureArgs.push("-R", `${region.x},${region.y},${region.width},${region.height}`);
@@ -146675,14 +147028,15 @@ var screenshotTool = defineTool({
146675
147028
  timeoutMs: 15e3
146676
147029
  });
146677
147030
  if (cap.exitCode !== 0) {
147031
+ const cause = procFailureCause(cap, 15e3);
146678
147032
  throw new MoxxyError({
146679
147033
  code: "TOOL_ERROR",
146680
- message: `screencapture failed (exit ${cap.exitCode}): ${cap.stderr.trim() || "(no stderr \u2014 likely Screen Recording permission missing \u2014 grant in System Settings \u2192 Privacy & Security)"}`,
146681
- context: { tool: "computer_screenshot", exitCode: cap.exitCode }
147034
+ message: cause ? `screencapture ${cause}` : `screencapture failed (exit ${cap.exitCode}): ${cap.stderr.trim() || "(no stderr \u2014 likely Screen Recording permission missing \u2014 grant in System Settings \u2192 Privacy & Security)"}`,
147035
+ context: { tool: "computer_screenshot", exitCode: cap.exitCode, timedOut: cap.timedOut ? 1 : 0 }
146682
147036
  });
146683
147037
  }
146684
147038
  const outExt = fmt2 === "jpeg" ? "jpg" : "png";
146685
- const outTmp = path3.join(os5.tmpdir(), `moxxy-screencap-${process.pid}-${Date.now()}-out.${outExt}`);
147039
+ const outTmp = path3.join(os5.tmpdir(), `moxxy-screencap-${process.pid}-${Date.now()}-${uniq}-out.${outExt}`);
146686
147040
  const sipsArgs = [
146687
147041
  "-Z",
146688
147042
  String(dim3),
@@ -146701,10 +147055,11 @@ var screenshotTool = defineTool({
146701
147055
  await promises.rm(captureTmp, { force: true });
146702
147056
  if (sip.exitCode !== 0) {
146703
147057
  await promises.rm(outTmp, { force: true });
147058
+ const cause = procFailureCause(sip, 15e3);
146704
147059
  throw new MoxxyError({
146705
147060
  code: "TOOL_ERROR",
146706
- message: `sips resize/convert failed (exit ${sip.exitCode}): ${sip.stderr.trim() || "(no error message)"}`,
146707
- context: { tool: "computer_screenshot", exitCode: sip.exitCode }
147061
+ message: cause ? `sips resize/convert ${cause}` : `sips resize/convert failed (exit ${sip.exitCode}): ${sip.stderr.trim() || "(no error message)"}`,
147062
+ context: { tool: "computer_screenshot", exitCode: sip.exitCode, timedOut: sip.timedOut ? 1 : 0 }
146708
147063
  });
146709
147064
  }
146710
147065
  try {
@@ -146747,10 +147102,11 @@ var typeTool = defineTool({
146747
147102
  timeoutMs: 3e4
146748
147103
  });
146749
147104
  if (proc.exitCode !== 0) {
147105
+ const cause = procFailureCause(proc, 3e4);
146750
147106
  throw new MoxxyError({
146751
147107
  code: "TOOL_ERROR",
146752
- message: `type failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission)"}`,
146753
- context: { tool: "computer_type", exitCode: proc.exitCode }
147108
+ message: cause ? `type ${cause}` : `type failed (exit ${proc.exitCode}): ${proc.stderr.trim() || "(check Accessibility permission)"}`,
147109
+ context: { tool: "computer_type", exitCode: proc.exitCode, timedOut: proc.timedOut ? 1 : 0 }
146754
147110
  });
146755
147111
  }
146756
147112
  return { ok: true, length: text.length };
@@ -147682,7 +148038,16 @@ async function loadDir2(dir, scope, logger) {
147682
148038
  if (!entry.isFile() || !/\.ya?ml$/i.test(entry.name))
147683
148039
  continue;
147684
148040
  const full = path3.join(dir, entry.name);
147685
- const raw = await promises.readFile(full, "utf8");
148041
+ let raw;
148042
+ try {
148043
+ raw = await promises.readFile(full, "utf8");
148044
+ } catch (err) {
148045
+ logger?.warn?.("workflow: unreadable file, skipping", {
148046
+ path: full,
148047
+ error: err instanceof Error ? err.message : String(err)
148048
+ });
148049
+ continue;
148050
+ }
147686
148051
  const result = parseWorkflowYaml(raw);
147687
148052
  if (!result.ok || !result.workflow) {
147688
148053
  logger?.warn?.("workflow: invalid file, skipping", { path: full, errors: result.errors });
@@ -147719,6 +148084,12 @@ var WorkflowStore = class {
147719
148084
  byName = /* @__PURE__ */ new Map();
147720
148085
  opts;
147721
148086
  loaded = false;
148087
+ // Per-instance lock (invariant: serialize whole-map RMW). A reload
148088
+ // (`load()` clears then refills byName) must never interleave with a save
148089
+ // (read byName → write file → set byName) — e.g. an autonomous onChanged
148090
+ // re-sync racing a user `/workflows enable`, or two concurrent toggles —
148091
+ // or the in-memory registry briefly desyncs from disk.
148092
+ mutex = createMutex();
147722
148093
  constructor(opts) {
147723
148094
  this.opts = opts;
147724
148095
  }
@@ -147730,6 +148101,14 @@ var WorkflowStore = class {
147730
148101
  }
147731
148102
  /** (Re)scan all sources and rebuild the in-memory map. */
147732
148103
  async load() {
148104
+ await this.mutex.run(() => this.loadUnlocked());
148105
+ }
148106
+ /**
148107
+ * Rebuild the map without acquiring the mutex — only call from a context
148108
+ * that already holds it (mutators below), so the clear+refill is atomic
148109
+ * with respect to other mutators.
148110
+ */
148111
+ async loadUnlocked() {
147733
148112
  const discovered = await discoverWorkflows({
147734
148113
  userDir: this.userDir(),
147735
148114
  projectDir: this.projectDir(),
@@ -147742,6 +148121,10 @@ var WorkflowStore = class {
147742
148121
  this.byName.set(wf.workflow.name, wf);
147743
148122
  this.loaded = true;
147744
148123
  }
148124
+ async ensureLoadedUnlocked() {
148125
+ if (!this.loaded)
148126
+ await this.loadUnlocked();
148127
+ }
147745
148128
  async ensureLoaded() {
147746
148129
  if (!this.loaded)
147747
148130
  await this.load();
@@ -147760,17 +148143,19 @@ var WorkflowStore = class {
147760
148143
  }
147761
148144
  /** Write a new workflow file and register it. Rejects duplicate names. */
147762
148145
  async create(workflow, scope) {
147763
- await this.ensureLoaded();
147764
- if (this.byName.has(workflow.name)) {
147765
- throw new Error(`workflow "${workflow.name}" already exists \u2014 use update instead`);
147766
- }
147767
- const dir = scope === "project" ? this.projectDir() : this.userDir();
147768
- await promises.mkdir(dir, { recursive: true });
147769
- const file = await uniqueFilename2(dir, workflow.name);
147770
- await writeFileAtomic(file, serializeWorkflow(workflow));
147771
- const entry = { workflow, path: file, scope };
147772
- this.byName.set(workflow.name, entry);
147773
- return entry;
148146
+ return this.mutex.run(async () => {
148147
+ await this.ensureLoadedUnlocked();
148148
+ if (this.byName.has(workflow.name)) {
148149
+ throw new Error(`workflow "${workflow.name}" already exists \u2014 use update instead`);
148150
+ }
148151
+ const dir = scope === "project" ? this.projectDir() : this.userDir();
148152
+ await promises.mkdir(dir, { recursive: true });
148153
+ const file = await uniqueFilename2(dir, workflow.name);
148154
+ await writeFileAtomic(file, serializeWorkflow(workflow));
148155
+ const entry = { workflow, path: file, scope };
148156
+ this.byName.set(workflow.name, entry);
148157
+ return entry;
148158
+ });
147774
148159
  }
147775
148160
  /**
147776
148161
  * Replace a workflow with a new definition. In-place rewrite for user/project
@@ -147781,7 +148166,10 @@ var WorkflowStore = class {
147781
148166
  * rename doesn't leave an orphaned duplicate file + stale entry behind.
147782
148167
  */
147783
148168
  async save(workflow, previousName) {
147784
- await this.ensureLoaded();
148169
+ return this.mutex.run(() => this.saveUnlocked(workflow, previousName));
148170
+ }
148171
+ async saveUnlocked(workflow, previousName) {
148172
+ await this.ensureLoadedUnlocked();
147785
148173
  const renamed = previousName != null && previousName !== workflow.name ? this.byName.get(previousName) : void 0;
147786
148174
  if (renamed && (renamed.scope === "user" || renamed.scope === "project")) {
147787
148175
  await promises.rm(renamed.path, { force: true });
@@ -147806,24 +148194,28 @@ var WorkflowStore = class {
147806
148194
  }
147807
148195
  /** Toggle a workflow's `enabled` flag, persisting the change. */
147808
148196
  async setEnabled(name, enabled) {
147809
- await this.ensureLoaded();
147810
- const existing = this.byName.get(name);
147811
- if (!existing)
147812
- return null;
147813
- return this.save({ ...existing.workflow, enabled });
148197
+ return this.mutex.run(async () => {
148198
+ await this.ensureLoadedUnlocked();
148199
+ const existing = this.byName.get(name);
148200
+ if (!existing)
148201
+ return null;
148202
+ return this.saveUnlocked({ ...existing.workflow, enabled });
148203
+ });
147814
148204
  }
147815
148205
  /** Delete a user/project workflow file. Read-only scopes cannot be deleted. */
147816
148206
  async delete(name) {
147817
- await this.ensureLoaded();
147818
- const existing = this.byName.get(name);
147819
- if (!existing)
147820
- return { ok: false, reason: "not found" };
147821
- if (existing.scope !== "user" && existing.scope !== "project") {
147822
- return { ok: false, reason: `cannot delete a ${existing.scope} workflow` };
147823
- }
147824
- await promises.rm(existing.path, { force: true });
147825
- this.byName.delete(name);
147826
- return { ok: true };
148207
+ return this.mutex.run(async () => {
148208
+ await this.ensureLoadedUnlocked();
148209
+ const existing = this.byName.get(name);
148210
+ if (!existing)
148211
+ return { ok: false, reason: "not found" };
148212
+ if (existing.scope !== "user" && existing.scope !== "project") {
148213
+ return { ok: false, reason: `cannot delete a ${existing.scope} workflow` };
148214
+ }
148215
+ await promises.rm(existing.path, { force: true });
148216
+ this.byName.delete(name);
148217
+ return { ok: true };
148218
+ });
147827
148219
  }
147828
148220
  };
147829
148221
  async function uniqueFilename2(dir, base2) {
@@ -149501,46 +149893,61 @@ function buildWorkflowRunner(args) {
149501
149893
  }
149502
149894
  async function resumeNow(runId, reply2) {
149503
149895
  const checkpoint = await defaultWorkflowRunStore.load(runId);
149504
- const turnId = session.startTurn().turnId;
149505
- const spawner = createSubagentSpawner({
149506
- parentSession: session,
149507
- parentTurnId: turnId,
149508
- parentSignal: session.signal,
149509
- parentModel: activeModel(session)
149510
- });
149511
- const result = await resumeWorkflowRun(
149512
- runId,
149513
- reply2,
149514
- {
149515
- spawner,
149516
- tools: session.tools,
149517
- lookup: {
149518
- skill: (n2) => session.skills.byName(n2),
149519
- workflow: (n2) => store.lookup(n2)
149520
- },
149521
- signal: session.signal,
149522
- now: () => Date.now(),
149523
- emit: (subtype, payload) => void session.log.append({
149524
- type: "plugin_event",
149525
- sessionId: session.id,
149526
- turnId,
149527
- source: "plugin",
149528
- pluginId: PLUGIN_ID3,
149529
- subtype,
149530
- payload
149531
- }),
149532
- ...logger ? { logger } : {}
149533
- },
149534
- defaultWorkflowRunStore
149535
- );
149536
- if (result.status === "paused") {
149537
- logger?.warn?.("workflows: run paused again awaiting operator input; not delivering to inbox", {
149538
- runId: result.runId
149896
+ const name = checkpoint?.workflow?.name;
149897
+ if (name && inFlight.has(name)) {
149898
+ return {
149899
+ ok: false,
149900
+ status: "failed",
149901
+ steps: [],
149902
+ output: "",
149903
+ error: `workflow "${name}" is already running`
149904
+ };
149905
+ }
149906
+ if (name) inFlight.add(name);
149907
+ try {
149908
+ const turnId = session.startTurn().turnId;
149909
+ const spawner = createSubagentSpawner({
149910
+ parentSession: session,
149911
+ parentTurnId: turnId,
149912
+ parentSignal: session.signal,
149913
+ parentModel: activeModel(session)
149539
149914
  });
149915
+ const result = await resumeWorkflowRun(
149916
+ runId,
149917
+ reply2,
149918
+ {
149919
+ spawner,
149920
+ tools: session.tools,
149921
+ lookup: {
149922
+ skill: (n2) => session.skills.byName(n2),
149923
+ workflow: (n2) => store.lookup(n2)
149924
+ },
149925
+ signal: session.signal,
149926
+ now: () => Date.now(),
149927
+ emit: (subtype, payload) => void session.log.append({
149928
+ type: "plugin_event",
149929
+ sessionId: session.id,
149930
+ turnId,
149931
+ source: "plugin",
149932
+ pluginId: PLUGIN_ID3,
149933
+ subtype,
149934
+ payload
149935
+ }),
149936
+ ...logger ? { logger } : {}
149937
+ },
149938
+ defaultWorkflowRunStore
149939
+ );
149940
+ if (result.status === "paused") {
149941
+ logger?.warn?.("workflows: run paused again awaiting operator input; not delivering to inbox", {
149942
+ runId: result.runId
149943
+ });
149944
+ return result;
149945
+ }
149946
+ if (checkpoint?.workflow) await deliverToInbox(checkpoint.workflow, result, logger);
149540
149947
  return result;
149948
+ } finally {
149949
+ if (name) inFlight.delete(name);
149541
149950
  }
149542
- if (checkpoint?.workflow) await deliverToInbox(checkpoint.workflow, result, logger);
149543
- return result;
149544
149951
  }
149545
149952
  return { runNow, resumeNow };
149546
149953
  }
@@ -150018,7 +150425,7 @@ function buildSchedulerRunner(session) {
150018
150425
  for await (const event of runTurn(session, prompt, model ? { model } : {})) {
150019
150426
  if (event.type === "assistant_message") {
150020
150427
  text = event.content;
150021
- if (event.stopReason === "error") lastError = "turn ended with error stop reason";
150428
+ lastError = event.stopReason === "error" ? "turn ended with error stop reason" : null;
150022
150429
  } else if (event.type === "error") {
150023
150430
  lastError = event.message;
150024
150431
  }
@@ -150043,7 +150450,7 @@ function buildWebhookRunner(session) {
150043
150450
  for await (const event of runTurn(target, prompt, model ? { model } : {})) {
150044
150451
  if (event.type === "assistant_message") {
150045
150452
  text = event.content;
150046
- if (event.stopReason === "error") lastError = "turn ended with error stop reason";
150453
+ lastError = event.stopReason === "error" ? "turn ended with error stop reason" : null;
150047
150454
  } else if (event.type === "error") {
150048
150455
  lastError = event.message;
150049
150456
  }
@@ -150910,7 +151317,7 @@ async function fetchLatest(pkg = DEFAULT_PKG, opts = {}) {
150910
151317
  }
150911
151318
 
150912
151319
  // src/update/check.ts
150913
- var CACHE_TTL_MS = 12 * 60 * 60 * 1e3;
151320
+ var CACHE_TTL_MS2 = 12 * 60 * 60 * 1e3;
150914
151321
  var PKG = "@moxxy/cli";
150915
151322
  function defaultCacheFile() {
150916
151323
  return moxxyPath("update-check.json");
@@ -150965,7 +151372,7 @@ async function checkForCliUpdate(current, opts = {}) {
150965
151372
  const file = opts.cacheFile ?? defaultCacheFile();
150966
151373
  if (!opts.force) {
150967
151374
  const cache4 = readCache(file);
150968
- if (cache4 && now - cache4.checkedAt < CACHE_TTL_MS) {
151375
+ if (cache4 && now - cache4.checkedAt < CACHE_TTL_MS2) {
150969
151376
  return shape(current, cache4.latest);
150970
151377
  }
150971
151378
  }
@@ -151179,12 +151586,14 @@ async function collectKey(providerId, controller) {
151179
151586
  if (!controller.testKey) return value;
151180
151587
  const s2 = ft2();
151181
151588
  s2.start(`Validating ${providerId} key`);
151589
+ let rejected = false;
151182
151590
  try {
151183
151591
  const result = await controller.testKey(providerId, value);
151184
151592
  if (result.ok) {
151185
151593
  s2.stop(`${colors.bold("\u2713")} ${providerId} key looks good`);
151186
151594
  return value;
151187
151595
  }
151596
+ rejected = true;
151188
151597
  s2.stop(`${colors.red("\u2717")} ${providerId} rejected the key: ${result.message}`);
151189
151598
  } catch (err) {
151190
151599
  s2.stop(
@@ -151197,6 +151606,7 @@ async function collectKey(providerId, controller) {
151197
151606
  });
151198
151607
  const retry = guard(retryRaw);
151199
151608
  if (!retry) {
151609
+ if (rejected) bail();
151200
151610
  return value;
151201
151611
  }
151202
151612
  }
@@ -152435,7 +152845,7 @@ ${available}`
152435
152845
  async function runChannelsCommand(argv) {
152436
152846
  const [name, sub, ...rest] = argv.positional;
152437
152847
  if (!name || name === "list") {
152438
- return runList2();
152848
+ return runList2(argv);
152439
152849
  }
152440
152850
  const outcome = await probeSession(
152441
152851
  argvToSetupOptions(argv, {
@@ -152489,12 +152899,13 @@ ${available}`
152489
152899
  if (outcome !== "run-channel") return outcome.code;
152490
152900
  return runChannelByName(name, argv);
152491
152901
  }
152492
- async function runList2() {
152902
+ async function runList2(argv) {
152493
152903
  const { entries, config } = await probeSession(
152494
- argvToSetupOptions(
152495
- { flags: {} },
152496
- { skipKeyPrompt: true, tolerateNoProvider: true, skipProviderActivation: true }
152497
- ),
152904
+ argvToSetupOptions(argv, {
152905
+ skipKeyPrompt: true,
152906
+ tolerateNoProvider: true,
152907
+ skipProviderActivation: true
152908
+ }),
152498
152909
  async ({ session, vault, config: config2 }) => ({
152499
152910
  config: config2,
152500
152911
  entries: await session.channels.listWithAvailability({
@@ -153171,7 +153582,7 @@ function unitPath(spec) {
153171
153582
  }
153172
153583
  function renderUnit(spec, ctx) {
153173
153584
  const execStart = [ctx.node, ctx.cli, ...spec.execArgs].map(quote).join(" ");
153174
- const envLines = Object.entries(spec.env ?? {}).map(([k3, v3]) => `Environment=${k3}=${v3}`).join("\n");
153585
+ const envLines = Object.entries(spec.env ?? {}).map(([k3, v3]) => `Environment=${k3}=${envValue(v3)}`).join("\n");
153175
153586
  return `[Unit]
153176
153587
  Description=${spec.description}
153177
153588
  After=network-online.target
@@ -153195,6 +153606,11 @@ function quote(s2) {
153195
153606
  if (!/[\s"]/.test(s2)) return s2;
153196
153607
  return '"' + s2.replace(/"/g, '\\"') + '"';
153197
153608
  }
153609
+ function envValue(v3) {
153610
+ const s2 = v3.replace(/[\r\n]+/g, " ");
153611
+ if (!/[\s"\\]/.test(s2)) return s2;
153612
+ return '"' + s2.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
153613
+ }
153198
153614
  var systemdService = {
153199
153615
  platform: "linux",
153200
153616
  async getStatus(spec) {
@@ -153423,21 +153839,23 @@ async function runDaemonBackground() {
153423
153839
  return result.ok ? 0 : 1;
153424
153840
  }
153425
153841
  async function runDaemonForeground() {
153426
- const { session, scheduler } = await setupSessionWithConfig({ cwd: process.cwd() });
153842
+ const { session } = await setupSessionWithConfig({ cwd: process.cwd() });
153427
153843
  process.stdout.write(
153428
153844
  `${colors.bold("scheduler daemon")} ${colors.dim("provider=" + (session.providers.getActiveName() ?? "(none)"))}
153429
153845
  ` + colors.dim(" ^C to stop. Schedules fire while this process is alive.\n")
153430
153846
  );
153431
153847
  let stopRequested = false;
153432
- const shutdown = async () => {
153848
+ const shutdown = async (signal) => {
153433
153849
  if (stopRequested) return;
153434
153850
  stopRequested = true;
153851
+ const force = setTimeout(() => process.exit(0), 4e3);
153852
+ force.unref?.();
153435
153853
  process.stdout.write("\nstopping scheduler\u2026\n");
153436
- await scheduler.poller.stop();
153854
+ await session.close(signal).catch(() => void 0);
153437
153855
  process.exit(0);
153438
153856
  };
153439
- process.on("SIGINT", () => void shutdown());
153440
- process.on("SIGTERM", () => void shutdown());
153857
+ process.on("SIGINT", () => void shutdown("SIGINT"));
153858
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
153441
153859
  setInterval(() => {
153442
153860
  }, 6e4);
153443
153861
  return await new Promise(() => {