@moxxy/cli 0.8.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { z as z$1, defineProvider, definePlugin, defineTool, MoxxyError, writeFileAtomic, asTurnId, defineMode, asPluginId, defineChannel, defineTunnelProvider, spawnCliTunnel, isCliTunnelAvailable, createMutex, defineWorkflowExecutor, toFriendlyError, estimateTextTokens, classifyHttpStatus, createStuckLoopDetector, runCompactionIfNeeded, runElisionIfNeeded, collectProviderStream, usageEventFields, isContextOverflowError, emitRequestsAndDetectStuck, executeToolUses, buildSystemPromptWithSkills, projectMessages, defineCompactor, defineCacheStrategy, denyByDefaultResolver, createAllowListResolver, moxxyPath, zodToJsonSchema, runSingleShotTurn, bearerTokenMatches, resolveChannelToken, rotateChannelToken, estimateContextTokens as estimateContextTokens$1, readRequestBody, MOXXY_WS_SUBPROTOCOL, defineEmbedder, bearerGuard, tokenFromWsProtocolHeader, skillFrontmatterSchema, asSkillId, getInstallHint, moxxyHome, defineTranscriber, summarizeTokensByModel, migrateModeName, createDeferredPermissionResolver, classifyNetworkError, addModelTotals, ISOLATION_RANK, moxxyPackageSchema, defineCommand, createCallbackResolver, autoAllowResolver, asSessionId, asToolCallId, defineViewRenderer, DEFAULT_VIEW_TAGS, isSafeViewUrl, evaluateToolRule, summarizeSessionTokensFromEvents, computeElisionState, toolResultStubbed, toolResultStub, toolResultBytes, conversationalStubbed, conversationalStub, asEventId } from '@moxxy/sdk';
3
+ import { z as z$1, defineProvider, definePlugin, defineTool, MoxxyError, writeFileAtomic, asTurnId, defineMode, asPluginId, defineChannel, defineTunnelProvider, spawnCliTunnel, isCliTunnelAvailable, createMutex, defineWorkflowExecutor, toFriendlyError, estimateTextTokens, classifyHttpStatus, createStuckLoopDetector, runCompactionIfNeeded, runElisionIfNeeded, collectProviderStream, usageEventFields, isContextOverflowError, emitRequestsAndDetectStuck, executeToolUses, buildSystemPromptWithSkills, projectMessages, defineCompactor, defineCacheStrategy, denyByDefaultResolver, createAllowListResolver, moxxyPath, zodToJsonSchema, runSingleShotTurn, bearerTokenMatches, resolveChannelToken, rotateChannelToken, estimateContextTokens as estimateContextTokens$1, readRequestBody, MOXXY_WS_SUBPROTOCOL, defineEmbedder, migrateModeName, bearerGuard, tokenFromWsProtocolHeader, skillFrontmatterSchema, asSkillId, getInstallHint, moxxyHome, defineTranscriber, summarizeTokensByModel, createDeferredPermissionResolver, classifyNetworkError, addModelTotals, ISOLATION_RANK, moxxyPackageSchema, defineCommand, createCallbackResolver, autoAllowResolver, asSessionId, asToolCallId, defineViewRenderer, DEFAULT_VIEW_TAGS, isSafeViewUrl, evaluateToolRule, summarizeSessionTokensFromEvents, computeElisionState, toolResultStubbed, toolResultStub, toolResultBytes, conversationalStubbed, conversationalStub, asEventId } from '@moxxy/sdk';
4
4
  import * as fs27 from 'fs';
5
5
  import fs27__default, { existsSync, promises, ReadStream, readFileSync, statSync, readdirSync, mkdirSync, writeFileSync, unlinkSync, renameSync, watch, createReadStream } from 'fs';
6
6
  import * as path3 from 'path';
@@ -1376,6 +1376,13 @@ var init_providers = __esm({
1376
1376
  defs = /* @__PURE__ */ new Map();
1377
1377
  instances = /* @__PURE__ */ new Map();
1378
1378
  active = null;
1379
+ /**
1380
+ * Names the user disabled. Kept name-based (not def-based) so it can be
1381
+ * seeded from preferences BEFORE the plugins register their defs — boot
1382
+ * order doesn't matter. A disabled provider stays registered/listable but
1383
+ * can't be activated.
1384
+ */
1385
+ disabled = /* @__PURE__ */ new Set();
1379
1386
  /**
1380
1387
  * Register a provider def. Throws on duplicate — use `replace()` for
1381
1388
  * explicit overwrite. Matches the semantics of `tools` and `channels`.
@@ -1404,10 +1411,32 @@ var init_providers = __esm({
1404
1411
  list() {
1405
1412
  return [...this.defs.values()];
1406
1413
  }
1414
+ /**
1415
+ * Enable/disable a provider by name. Disabling the ACTIVE provider is
1416
+ * refused — switch first, then disable — so a session never ends up with an
1417
+ * active-but-disabled provider. Unknown names are accepted (the set seeds
1418
+ * from preferences before plugins register), except when disabling via a
1419
+ * live toggle is meaningless because the provider is active.
1420
+ */
1421
+ setEnabled(name, enabled) {
1422
+ if (!enabled && this.active === name) {
1423
+ throw new Error(`Cannot disable the active provider "${name}" \u2014 switch providers first.`);
1424
+ }
1425
+ if (enabled)
1426
+ this.disabled.delete(name);
1427
+ else
1428
+ this.disabled.add(name);
1429
+ }
1430
+ isEnabled(name) {
1431
+ return !this.disabled.has(name);
1432
+ }
1407
1433
  setActive(name, config) {
1408
1434
  const def = this.defs.get(name);
1409
1435
  if (!def)
1410
1436
  throw new Error(`Provider not registered: ${name}`);
1437
+ if (this.disabled.has(name)) {
1438
+ throw new Error(`Provider "${name}" is disabled \u2014 enable it first.`);
1439
+ }
1411
1440
  let instance = this.instances.get(name);
1412
1441
  if (!instance) {
1413
1442
  instance = def.createClient(config ?? {});
@@ -3081,6 +3110,7 @@ var init_session = __esm({
3081
3110
  readyProviders;
3082
3111
  credentialResolver;
3083
3112
  mcpAdmin;
3113
+ providerAdmin;
3084
3114
  workflows;
3085
3115
  pluginsAdmin;
3086
3116
  dispatcher;
@@ -3262,7 +3292,8 @@ var init_session = __esm({
3262
3292
  // are the ones the desktop's "Fetch live" affordance targets;
3263
3293
  // they advertise this via the provider-admin factory by
3264
3294
  // setting `supportsLiveModelDiscovery: true` on their def.
3265
- supportsLiveModelDiscovery: p3.supportsLiveModelDiscovery === true
3295
+ supportsLiveModelDiscovery: p3.supportsLiveModelDiscovery === true,
3296
+ enabled: this.providers.isEnabled(p3.name)
3266
3297
  })),
3267
3298
  activeMode,
3268
3299
  activeModeBadge,
@@ -127441,6 +127472,15 @@ function noteTemperatureUnsupported() {
127441
127472
  console.debug("[openai-codex] ProviderRequest.temperature is not supported by the Codex Responses backend (gpt-5 reasoning models reject sampling params); ignoring it.");
127442
127473
  }
127443
127474
  }
127475
+ var maxTokensNoteShown = false;
127476
+ function noteMaxTokensUnsupported() {
127477
+ if (maxTokensNoteShown)
127478
+ return;
127479
+ maxTokensNoteShown = true;
127480
+ if (process.env.MOXXY_DEBUG) {
127481
+ console.debug("[openai-codex] ProviderRequest.maxTokens is not supported by the Codex Responses backend (400 Unsupported parameter: max_output_tokens); ignoring it.");
127482
+ }
127483
+ }
127444
127484
  function toResponsesBody(req, opts = {}) {
127445
127485
  const instructions = extractSystemText(req.messages, req.system) || DEFAULT_INSTRUCTIONS;
127446
127486
  const body = {
@@ -127458,7 +127498,7 @@ function toResponsesBody(req, opts = {}) {
127458
127498
  if (opts.sessionHint)
127459
127499
  body.prompt_cache_key = opts.sessionHint;
127460
127500
  if (req.maxTokens !== void 0)
127461
- body.max_output_tokens = req.maxTokens;
127501
+ noteMaxTokensUnsupported();
127462
127502
  if (req.temperature !== void 0)
127463
127503
  noteTemperatureUnsupported();
127464
127504
  return body;
@@ -134374,13 +134414,14 @@ async function createWebSocketTransportServer(opts) {
134374
134414
  const host = opts.host ?? "127.0.0.1";
134375
134415
  const maxConnections = opts.maxConnections ?? DEFAULT_MAX_CONNECTIONS;
134376
134416
  let currentToken = opts.authToken;
134417
+ let currentAllowedOrigins = opts.allowedOrigins ?? [];
134377
134418
  let connections = 0;
134378
134419
  const wss = new import_websocket_server.default({
134379
134420
  host,
134380
134421
  port: opts.port,
134381
134422
  maxPayload: opts.maxPayloadBytes ?? 64 * 1024 * 1024,
134382
134423
  verifyClient: (info) => {
134383
- if (!checkWsOrigin(info.req, opts.allowedOrigins)) {
134424
+ if (!checkWsOrigin(info.req, currentAllowedOrigins)) {
134384
134425
  console.warn(`[moxxy] ws bridge: rejected browser-origin upgrade (Origin: ${String(info.req.headers.origin)})`);
134385
134426
  return false;
134386
134427
  }
@@ -134420,6 +134461,9 @@ async function createWebSocketTransportServer(opts) {
134420
134461
  for (const client of wss.clients)
134421
134462
  client.terminate();
134422
134463
  },
134464
+ setAllowedOrigins(origins) {
134465
+ currentAllowedOrigins = [...origins];
134466
+ },
134423
134467
  clientCount() {
134424
134468
  return connections;
134425
134469
  },
@@ -134753,7 +134797,7 @@ function isRunnerUp(socketPath = runnerSocketPath()) {
134753
134797
 
134754
134798
  // ../runner/dist/server.js
134755
134799
  init_dist();
134756
- var RUNNER_PROTOCOL_VERSION = 6;
134800
+ var RUNNER_PROTOCOL_VERSION = 7;
134757
134801
  var MIN_COMPATIBLE_PROTOCOL_VERSION = 1;
134758
134802
  var RunnerMethod = {
134759
134803
  /** client->server: handshake; returns the initial info snapshot. */
@@ -134778,6 +134822,12 @@ var RunnerMethod = {
134778
134822
  ModeSetActive: "mode.setActive",
134779
134823
  /** client->server: switch the active provider (server resolves credentials). */
134780
134824
  ProviderSetActive: "provider.setActive",
134825
+ /** client->server: enable/disable a provider (v7; persists to preferences). */
134826
+ ProviderSetEnabled: "provider.setEnabled",
134827
+ /** client->server: re-probe every provider's credentials → readyProviders (v7). */
134828
+ ProviderRefreshReady: "provider.refreshReady",
134829
+ /** client->server: patch a stored (runtime-registered) provider's config (v7). */
134830
+ ProviderConfigure: "provider.configure",
134781
134831
  /** client->server: persist an allow-always permission rule. */
134782
134832
  PermissionAddAllow: "permission.addAllow",
134783
134833
  /** client->server: run a registered slash command on the runner. */
@@ -134873,6 +134923,19 @@ var providerSetActiveParamsSchema = z.object({
134873
134923
  name: z.string(),
134874
134924
  config: z.record(z.unknown()).optional()
134875
134925
  });
134926
+ var providerSetEnabledParamsSchema = z.object({
134927
+ name: z.string().min(1),
134928
+ enabled: z.boolean()
134929
+ });
134930
+ var providerConfigureParamsSchema = z.object({
134931
+ name: z.string().min(1),
134932
+ patch: z.object({
134933
+ baseURL: z.string().url().optional(),
134934
+ defaultModel: z.string().min(1).optional(),
134935
+ envVar: z.string().regex(/^[A-Z][A-Z0-9_]*$/).optional(),
134936
+ models: z.array(z.object({ id: z.string().min(1), contextWindow: z.number() }).passthrough()).min(1).optional()
134937
+ })
134938
+ });
134876
134939
  var permissionAddAllowParamsSchema = z.object({
134877
134940
  name: z.string(),
134878
134941
  reason: z.string().optional()
@@ -134979,6 +135042,9 @@ var RunnerServer = class {
134979
135042
  peer.handle(RunnerMethod.SetResolver, (raw) => this.handleSetResolver(client, raw));
134980
135043
  peer.handle(RunnerMethod.ModeSetActive, (raw) => this.handleModeSetActive(raw));
134981
135044
  peer.handle(RunnerMethod.ProviderSetActive, (raw) => this.handleProviderSetActive(raw));
135045
+ peer.handle(RunnerMethod.ProviderSetEnabled, (raw) => this.handleProviderSetEnabled(raw));
135046
+ peer.handle(RunnerMethod.ProviderRefreshReady, () => this.handleProviderRefreshReady());
135047
+ peer.handle(RunnerMethod.ProviderConfigure, (raw) => this.handleProviderConfigure(raw));
134982
135048
  peer.handle(RunnerMethod.PermissionAddAllow, (raw) => this.handlePermissionAddAllow(raw));
134983
135049
  peer.handle(RunnerMethod.CommandRun, (raw) => this.handleCommandRun(raw));
134984
135050
  peer.handle(RunnerMethod.Transcribe, (raw) => this.handleTranscribe(raw));
@@ -135054,6 +135120,7 @@ var RunnerServer = class {
135054
135120
  turnId,
135055
135121
  ...error2 ? { error: error2 } : {}
135056
135122
  });
135123
+ this.broadcastInfo();
135057
135124
  }
135058
135125
  });
135059
135126
  return { turnId };
@@ -135118,6 +135185,54 @@ var RunnerServer = class {
135118
135185
  this.broadcastInfo();
135119
135186
  return {};
135120
135187
  }
135188
+ async handleProviderSetEnabled(raw) {
135189
+ const { name, enabled } = providerSetEnabledParamsSchema.parse(raw);
135190
+ if (!this.session.providers.list().some((p3) => p3.name === name)) {
135191
+ throw new Error(`Provider not registered: ${name}`);
135192
+ }
135193
+ this.session.providers.setEnabled(name, enabled);
135194
+ void (async () => {
135195
+ const prefs = await loadPreferences();
135196
+ const current = new Set(prefs.disabledProviders ?? []);
135197
+ if (enabled)
135198
+ current.delete(name);
135199
+ else
135200
+ current.add(name);
135201
+ await savePreferences({ disabledProviders: [...current] });
135202
+ })();
135203
+ this.broadcastInfo();
135204
+ return {};
135205
+ }
135206
+ async handleProviderRefreshReady() {
135207
+ const resolver2 = this.session.credentialResolver;
135208
+ if (resolver2) {
135209
+ const ready = /* @__PURE__ */ new Set();
135210
+ const active = this.session.providers.getActiveName();
135211
+ if (active)
135212
+ ready.add(active);
135213
+ for (const p3 of this.session.providers.list()) {
135214
+ if (ready.has(p3.name))
135215
+ continue;
135216
+ try {
135217
+ await resolver2(p3.name);
135218
+ ready.add(p3.name);
135219
+ } catch {
135220
+ }
135221
+ }
135222
+ this.session.readyProviders = ready;
135223
+ }
135224
+ this.broadcastInfo();
135225
+ return {};
135226
+ }
135227
+ async handleProviderConfigure(raw) {
135228
+ const { name, patch } = providerConfigureParamsSchema.parse(raw);
135229
+ const admin = this.session.providerAdmin;
135230
+ if (!admin)
135231
+ throw new Error("provider admin not supported on this runner");
135232
+ await admin.configure(name, patch);
135233
+ this.broadcastInfo();
135234
+ return {};
135235
+ }
135121
135236
  async handlePermissionAddAllow(raw) {
135122
135237
  const { name, reason } = permissionAddAllowParamsSchema.parse(raw);
135123
135238
  await this.session.permissions.addAllow({ name, ...reason ? { reason } : {} });
@@ -135393,6 +135508,7 @@ var RemoteSession = class {
135393
135508
  requirements;
135394
135509
  permissions;
135395
135510
  mcpAdmin;
135511
+ providerAdmin;
135396
135512
  workflows;
135397
135513
  /**
135398
135514
  * Turns that completed before their `runTurn` stream was registered. A fast
@@ -135406,6 +135522,8 @@ var RemoteSession = class {
135406
135522
  permissionResolver = null;
135407
135523
  approvalResolver = null;
135408
135524
  info = null;
135525
+ /** Subscribers to `info.changed` pushes (see {@link onInfoChanged}). */
135526
+ infoListeners = /* @__PURE__ */ new Set();
135409
135527
  /**
135410
135528
  * The protocol version the SERVER reported at attach. Defaults to our own
135411
135529
  * version until the handshake resolves. Version-specific client methods (the
@@ -135431,6 +135549,12 @@ var RemoteSession = class {
135431
135549
  });
135432
135550
  this.peer.on(RunnerNotification.InfoChanged, (params) => {
135433
135551
  this.info = params.info;
135552
+ for (const fn of this.infoListeners) {
135553
+ try {
135554
+ fn(this.info);
135555
+ } catch {
135556
+ }
135557
+ }
135434
135558
  });
135435
135559
  this.peer.on(RunnerNotification.ReplayStart, (params) => {
135436
135560
  const { fromSeq } = params;
@@ -135468,6 +135592,7 @@ var RemoteSession = class {
135468
135592
  this.requirements = { check: () => ({ ready: false, issues: [] }) };
135469
135593
  this.permissions = this.makePermissionsView();
135470
135594
  this.mcpAdmin = this.makeMcpAdminView();
135595
+ this.providerAdmin = this.makeProviderAdminView();
135471
135596
  this.workflows = this.makeWorkflowsView();
135472
135597
  }
135473
135598
  /**
@@ -135533,6 +135658,16 @@ var RemoteSession = class {
135533
135658
  getInfo() {
135534
135659
  return this.requireInfo();
135535
135660
  }
135661
+ /**
135662
+ * Subscribe to runner `info.changed` pushes (registry snapshot changes —
135663
+ * provider/mode/MCP/workflow mutations, including ones made by tools inside
135664
+ * a turn). Fires after the local `getInfo()` mirror has been updated, so a
135665
+ * listener can re-read it synchronously. Returns an unsubscribe fn.
135666
+ */
135667
+ onInfoChanged(fn) {
135668
+ this.infoListeners.add(fn);
135669
+ return () => this.infoListeners.delete(fn);
135670
+ }
135536
135671
  async *runTurn(prompt, opts = {}) {
135537
135672
  const result = await this.peer.request(RunnerMethod.RunTurn, {
135538
135673
  prompt,
@@ -135736,6 +135871,27 @@ var RemoteSession = class {
135736
135871
  detach: (name) => this.peer.request(RunnerMethod.McpDetach, { name })
135737
135872
  };
135738
135873
  }
135874
+ // Provider management (protocol v7): backs the desktop's interactive
135875
+ // Settings → Providers tab. Gated on the SERVER's reported version so a v7
135876
+ // client attached to an older runner (a desktop whose JS hot-update outran
135877
+ // its bundled CLI) gets a clear "update the CLI" error instead of a raw
135878
+ // method-not-found.
135879
+ makeProviderAdminView() {
135880
+ return {
135881
+ setEnabled: async (name, enabled) => {
135882
+ this.requireServerProtocol(7, "Enabling/disabling a provider");
135883
+ await this.peer.request(RunnerMethod.ProviderSetEnabled, { name, enabled });
135884
+ },
135885
+ refreshReady: async () => {
135886
+ this.requireServerProtocol(7, "Re-probing provider credentials");
135887
+ await this.peer.request(RunnerMethod.ProviderRefreshReady, {});
135888
+ },
135889
+ configure: async (name, patch) => {
135890
+ this.requireServerProtocol(7, "Configuring a provider");
135891
+ await this.peer.request(RunnerMethod.ProviderConfigure, { name, patch });
135892
+ }
135893
+ };
135894
+ }
135739
135895
  makeWorkflowsView() {
135740
135896
  return {
135741
135897
  list: () => this.peer.request(RunnerMethod.WorkflowList),
@@ -135994,6 +136150,17 @@ var REMOTE_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
135994
136150
  "session.setMode",
135995
136151
  "session.newSession",
135996
136152
  "session.runCommand",
136153
+ // Multi-session conversations: list/create/switch/rename are conversation-
136154
+ // scoped — the same trust class as `session.newSession` (already allowed),
136155
+ // and what a paired phone needs to mirror the desktop's session list.
136156
+ // `sessions.remove` is deliberately NOT here: it deletes on-disk state
136157
+ // (the runner's session JSONL + the chat NDJSON transcript), a destructive
136158
+ // host mutation in the same class as `desks.remove`, which is also
136159
+ // host-only.
136160
+ "sessions.list",
136161
+ "sessions.create",
136162
+ "sessions.setActive",
136163
+ "sessions.rename",
135997
136164
  // Voice input (capability-probed; transcribe fails coded without a transcriber).
135998
136165
  "session.hasTranscriber",
135999
136166
  "session.transcribe",
@@ -136129,6 +136296,20 @@ var ipcInputSchemas = {
136129
136296
  id: z.string().min(1).max(256),
136130
136297
  name: z.string().min(1).max(200)
136131
136298
  }),
136299
+ // Sessions: create/rename persist the name into the desks JSON (bound it
136300
+ // like desks.create/rename); setActive spawns a runner and remove deletes
136301
+ // the session's on-disk logs, so their ids are bounded too. These commands
136302
+ // are also served to remote (WS) clients, so the bounds are load-bearing.
136303
+ "sessions.create": z.object({
136304
+ deskId: z.string().min(1).max(256).optional(),
136305
+ name: z.string().min(1).max(200).optional()
136306
+ }).optional(),
136307
+ "sessions.setActive": z.object({ id: z.string().min(1).max(256) }),
136308
+ "sessions.remove": z.object({ id: z.string().min(1).max(256) }),
136309
+ "sessions.rename": z.object({
136310
+ id: z.string().min(1).max(256),
136311
+ name: z.string().min(1).max(200)
136312
+ }),
136132
136313
  // Whitelist the fields a renderer may write — `version` is managed by
136133
136314
  // the main process; unknown keys are rejected (.strict()).
136134
136315
  "prefs.update": z.object({
@@ -136136,7 +136317,8 @@ var ipcInputSchemas = {
136136
136317
  clerkUserId: z.string().max(256).nullable().optional(),
136137
136318
  clerkDisplayName: z.string().max(256).nullable().optional(),
136138
136319
  signedInAt: z.number().nullable().optional(),
136139
- mobileGatewayEnabled: z.boolean().optional()
136320
+ mobileGatewayEnabled: z.boolean().optional(),
136321
+ theme: z.enum(["light", "dark", "system"]).optional()
136140
136322
  }).strict(),
136141
136323
  // Mobile-gateway control. Both no-arg variants pin the payload to "nothing"
136142
136324
  // so a hostile caller can't smuggle args; setEnabled is a strict boolean.
@@ -136524,14 +136706,41 @@ function isWildcardHost(host) {
136524
136706
  const h3 = host.trim().toLowerCase();
136525
136707
  return h3 === "0.0.0.0" || h3 === "::" || h3 === "[::]";
136526
136708
  }
136709
+ var VIRTUAL_IFACE = /^(?:utun|tun|tap|ppp|ipsec|wg|zt|ts|tailscale|vmnet|vnic|bridge|docker|veth|awdl|llw|ap|anpi)\d*$/i;
136710
+ function isLinkLocalV4(ip) {
136711
+ return ip.startsWith("169.254.");
136712
+ }
136713
+ function isRfc1918(ip) {
136714
+ if (ip.startsWith("10.") || ip.startsWith("192.168."))
136715
+ return true;
136716
+ const m3 = /^172\.(\d{1,3})\./.exec(ip);
136717
+ return m3 !== null && Number(m3[1]) >= 16 && Number(m3[1]) <= 31;
136718
+ }
136719
+ function isCgnat(ip) {
136720
+ const m3 = /^100\.(\d{1,3})\./.exec(ip);
136721
+ return m3 !== null && Number(m3[1]) >= 64 && Number(m3[1]) <= 127;
136722
+ }
136527
136723
  function lanHost(fallback) {
136528
- for (const list of Object.values(os5__default.networkInterfaces())) {
136724
+ let best = null;
136725
+ for (const [name, list] of Object.entries(os5__default.networkInterfaces())) {
136529
136726
  for (const ni of list ?? []) {
136530
- if (ni.family === "IPv4" && !ni.internal)
136531
- return ni.address;
136727
+ if (ni.family !== "IPv4" || ni.internal)
136728
+ continue;
136729
+ const virtual = VIRTUAL_IFACE.test(name);
136730
+ let rank;
136731
+ if (isLinkLocalV4(ni.address))
136732
+ rank = 5;
136733
+ else if (isRfc1918(ni.address))
136734
+ rank = virtual ? 3 : 1;
136735
+ else if (isCgnat(ni.address))
136736
+ rank = 4;
136737
+ else
136738
+ rank = virtual ? 4 : 2;
136739
+ if (!best || rank < best.rank)
136740
+ best = { address: ni.address, rank };
136532
136741
  }
136533
136742
  }
136534
- return fallback;
136743
+ return best?.address ?? fallback;
136535
136744
  }
136536
136745
  function advertisedHost(bindHost) {
136537
136746
  if (isWildcardHost(bindHost))
@@ -136547,6 +136756,20 @@ function buildConnectUrl(opts) {
136547
136756
  }
136548
136757
  return `ws://${opts.localHost}:${opts.port}/?t=${t2}`;
136549
136758
  }
136759
+ function connectUrlOrigin(url2) {
136760
+ const u2 = new URL(url2);
136761
+ const secure = u2.protocol === "wss:" || u2.protocol === "https:";
136762
+ return `${secure ? "https" : "http"}://${u2.host}`;
136763
+ }
136764
+ function advertisedOrigins(bindHost, port) {
136765
+ return [
136766
+ .../* @__PURE__ */ new Set([
136767
+ connectUrlOrigin(`ws://${advertisedHost(bindHost)}:${port}`),
136768
+ `http://127.0.0.1:${port}`,
136769
+ `http://localhost:${port}`
136770
+ ])
136771
+ ];
136772
+ }
136550
136773
 
136551
136774
  // ../plugin-channel-mobile/dist/tunnel.js
136552
136775
  function normalizeTunnelChoice(raw) {
@@ -136631,10 +136854,12 @@ var MobileChannel = class {
136631
136854
  this.host = host;
136632
136855
  host.register();
136633
136856
  host.wire();
136857
+ const localOrigins = advertisedOrigins(this.bindHost, this.port);
136634
136858
  const server = await startWsBridge(bus, {
136635
136859
  port: this.port,
136636
136860
  host: this.bindHost,
136637
136861
  authToken: this.token,
136862
+ allowedOrigins: localOrigins,
136638
136863
  // Back-compat ONLY: the QR this channel prints embeds the token as `?t=`
136639
136864
  // (pairing payload); current apps strip it and authenticate via the
136640
136865
  // Sec-WebSocket-Protocol bearer entry, but older installed builds still
@@ -136649,6 +136874,7 @@ var MobileChannel = class {
136649
136874
  try {
136650
136875
  this.tunnel = await provider.open({ port: this.port, host: this.bindHost });
136651
136876
  tunnelUrl = this.tunnel.url;
136877
+ server.setAllowedOrigins([...localOrigins, connectUrlOrigin(tunnelUrl)]);
136652
136878
  this.logger?.info?.("mobile tunnel open", { provider: provider.name, url: tunnelUrl });
136653
136879
  } catch (err) {
136654
136880
  this.logger?.warn?.("mobile tunnel failed; using the local URL", {
@@ -137963,6 +138189,51 @@ var testProviderInput = z$1.object({
137963
138189
  baseURL: z$1.string().url().describe("Vendor API base URL to probe, e.g. https://api.deepseek.com."),
137964
138190
  keyName: z$1.string().regex(/^[A-Z][A-Z0-9_]*$/).describe("NAME of the vault secret holding the API key (e.g. DEEPSEEK_API_KEY). The key is resolved from the vault inside the tool \u2014 never ask the user for the plaintext key and never pass one as a tool argument. Have them store it first: /vault set <NAME> <key>.")
137965
138191
  });
138192
+ function buildProviderAdminPluginWithApi(opts) {
138193
+ const { providerRegistry, configPath } = opts;
138194
+ const api = {
138195
+ configure: async (name, patch) => {
138196
+ const cfg = await readProvidersConfig(configPath);
138197
+ const entry = cfg.providers.find((p3) => p3.name === name);
138198
+ if (!entry) {
138199
+ throw new MoxxyError({
138200
+ code: "CONFIG_INVALID",
138201
+ message: `provider-admin: no stored provider named "${name}" \u2014 only runtime-registered (providers.json) providers are configurable; built-ins are code.`
138202
+ });
138203
+ }
138204
+ const next = {
138205
+ ...entry,
138206
+ ...patch.baseURL ? { baseURL: patch.baseURL } : {},
138207
+ ...patch.defaultModel ? { defaultModel: patch.defaultModel } : {},
138208
+ ...patch.envVar ? { envVar: patch.envVar } : {},
138209
+ ...patch.models && patch.models.length > 0 ? { models: patch.models } : {}
138210
+ };
138211
+ if (!next.models.some((m3) => m3.id === next.defaultModel)) {
138212
+ throw new MoxxyError({
138213
+ code: "CONFIG_INVALID",
138214
+ message: `provider-admin: defaultModel "${next.defaultModel}" is not in the models list (${next.models.map((m3) => m3.id).join(", ")}).`
138215
+ });
138216
+ }
138217
+ const def = buildProviderDef(next);
138218
+ const hadDef = providerRegistry.list().some((p3) => p3.name === name);
138219
+ if (hadDef)
138220
+ providerRegistry.replace(def);
138221
+ else
138222
+ providerRegistry.register(def);
138223
+ try {
138224
+ await upsertStoredProvider(next, configPath);
138225
+ } catch (err) {
138226
+ const prev = buildProviderDef(entry);
138227
+ if (hadDef)
138228
+ providerRegistry.replace(prev);
138229
+ else
138230
+ providerRegistry.unregister(name);
138231
+ throw err;
138232
+ }
138233
+ }
138234
+ };
138235
+ return { plugin: buildProviderAdminPlugin(opts), api };
138236
+ }
137966
138237
  function buildProviderAdminPlugin(opts) {
137967
138238
  const { providerRegistry, configPath } = opts;
137968
138239
  return definePlugin({
@@ -143953,20 +144224,25 @@ function formatCatalog(entries, emptyLabel) {
143953
144224
  }
143954
144225
  async function draftWorkflow(provider, model, intent, signal, opts = {}) {
143955
144226
  let accumulated = "";
144227
+ let truncated = false;
144228
+ const ceiling = provider.models.find((m3) => m3.id === model)?.maxOutputTokens;
144229
+ const budget = Math.min(opts.maxTokens ?? DEFAULT_MAX_TOKENS, ceiling ?? Number.POSITIVE_INFINITY);
143956
144230
  for await (const event of provider.stream({
143957
144231
  model,
143958
144232
  system: buildSystemPrompt(opts),
143959
144233
  messages: [{ role: "user", content: [{ type: "text", text: `Build a workflow for: ${intent}` }] }],
143960
- maxTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
144234
+ maxTokens: budget,
143961
144235
  signal
143962
144236
  })) {
143963
144237
  if (event.type === "text_delta")
143964
144238
  accumulated += event.delta;
144239
+ if (event.type === "message_end")
144240
+ truncated = event.stopReason === "max_tokens";
143965
144241
  if (event.type === "error")
143966
144242
  throw new Error(`workflow_create: provider error: ${event.message}`);
143967
144243
  }
143968
144244
  const raw = extractYamlBlock(accumulated);
143969
- return { raw, parse: parseWorkflowYaml(raw) };
144245
+ return { raw, parse: parseWorkflowYaml(raw), truncated };
143970
144246
  }
143971
144247
  function extractYamlBlock(s2) {
143972
144248
  const fence = /```(?:ya?ml)?\n([\s\S]*?)```/.exec(s2);
@@ -144045,7 +144321,7 @@ function createTool(deps) {
144045
144321
  if (!drafted.parse.ok || !drafted.parse.workflow) {
144046
144322
  throw new MoxxyError({
144047
144323
  code: "TOOL_ERROR",
144048
- message: `workflow_create: the model did not produce a valid workflow (${drafted.parse.errors.join("; ")}). Try a more specific intent.`
144324
+ message: drafted.truncated ? "workflow_create: the draft hit the output-token limit before the YAML was complete. Try a simpler intent, or split the workflow into smaller ones." : `workflow_create: the model did not produce a valid workflow (${drafted.parse.errors.join("; ")}). Try a more specific intent.`
144049
144325
  });
144050
144326
  }
144051
144327
  const created = await deps.store.create(drafted.parse.workflow, scope);
@@ -145218,10 +145494,11 @@ function buildBuiltinsCore(args) {
145218
145494
  // ~/.moxxy/providers.json; the plugin's onInit re-registers them on
145219
145495
  // every boot. Pairs with the `add-provider` skill which walks the
145220
145496
  // model through gathering baseURL + models + key.
145221
- {
145222
- name: "@moxxy/plugin-provider-admin",
145223
- plugin: buildProviderAdminPlugin({ providerRegistry: session.providers })
145224
- },
145497
+ (() => {
145498
+ const { plugin: plugin4, api } = buildProviderAdminPluginWithApi({ providerRegistry: session.providers });
145499
+ session.providerAdmin = api;
145500
+ return { name: "@moxxy/plugin-provider-admin", plugin: plugin4 };
145501
+ })(),
145225
145502
  // Admin tools (mcp_add_server, mcp_list_servers, mcp_remove_server,
145226
145503
  // mcp_test_server) plus the boot-time lazy attach. Passing the
145227
145504
  // session's live tool registry enables both hot-attach for runtime
@@ -145720,6 +145997,10 @@ async function activateProvider(args) {
145720
145997
  } else {
145721
145998
  for (let i2 = 0; i2 < candidates.length; i2++) {
145722
145999
  const candidate = candidates[i2];
146000
+ if (!session.providers.isEnabled(candidate)) {
146001
+ logger.warn("skipping disabled provider", { provider: candidate });
146002
+ continue;
146003
+ }
145723
146004
  const interactive = i2 === 0 && !skipKeyPrompt && process.stdin.isTTY === true;
145724
146005
  try {
145725
146006
  const resolved = await resolveProviderCredentials(candidate, vault, {
@@ -145928,6 +146209,13 @@ async function setupSessionWithConfig(opts) {
145928
146209
  });
145929
146210
  await selectEmbedder(session, rawConfig.embeddings, logger);
145930
146211
  for (const iso2 of session.isolators.list()) security.registry.register(iso2);
146212
+ try {
146213
+ const bootPrefs = await loadPreferences();
146214
+ for (const name of bootPrefs.disabledProviders ?? []) {
146215
+ session.providers.setEnabled(name, false);
146216
+ }
146217
+ } catch {
146218
+ }
145931
146219
  const { credentialResolver } = await activateProvider({
145932
146220
  session,
145933
146221
  config,