mixdog 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +158 -105
  2. package/package.json +11 -4
  3. package/src/defaults/skills/setup/references/actions.md +26 -6
  4. package/src/defaults/skills/setup/references/surfaces.md +8 -3
  5. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +16 -7
  6. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +6 -2
  7. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +0 -3
  8. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +0 -2
  9. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +24 -2
  10. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +25 -0
  11. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +14 -1
  12. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +3 -45
  13. package/src/runtime/channels/lib/config.mjs +0 -1
  14. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +1 -1
  15. package/src/runtime/channels/lib/scheduler.mjs +15 -56
  16. package/src/runtime/channels/lib/worker-main.mjs +2 -9
  17. package/src/runtime/shared/config.mjs +66 -1
  18. package/src/runtime/shared/llm/usage-ledger-quota.mjs +13 -2
  19. package/src/runtime/shared/path-executable.mjs +58 -0
  20. package/src/session-runtime/boot/apis.mjs +6 -2
  21. package/src/session-runtime/boot/begin.mjs +0 -2
  22. package/src/session-runtime/boot/providers.mjs +3 -0
  23. package/src/session-runtime/boot/tools.mjs +0 -1
  24. package/src/session-runtime/config-lifecycle.mjs +11 -0
  25. package/src/session-runtime/internal-tool-executor.mjs +0 -2
  26. package/src/session-runtime/runtime-feature-gates.mjs +2 -3
  27. package/src/session-runtime/schedule-session-run.mjs +0 -1
  28. package/src/session-runtime/services/channel-admin.mjs +8 -26
  29. package/src/session-runtime/settings-compaction-api.mjs +5 -1
  30. package/src/session-runtime/settings-system-api.mjs +2 -20
  31. package/src/session-runtime/setup-tool/executor.mjs +61 -3
  32. package/src/session-runtime/setup-tool/extended-actions.mjs +31 -6
  33. package/src/session-runtime/setup-tool/settings-contract.mjs +27 -3
  34. package/src/session-runtime/setup-tool/tool-defs.mjs +35 -5
  35. package/src/session-runtime/webhook-session-run.mjs +0 -2
  36. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +13 -3
  37. package/src/standalone/daemon-stored-session-views.mjs +4 -0
  38. package/src/standalone/session-protocol.mjs +0 -1
  39. package/src/standalone/session-service/session-calls/session-views.mjs +10 -4
  40. package/src/standalone/session-service/stored-reader.mjs +20 -2
  41. package/src/standalone/session-service/viewers.mjs +3 -3
  42. package/src/standalone/session-service.mjs +3 -1
  43. package/src/tui/App.jsx +6 -1
  44. package/src/tui/app/create-app-pickers.mjs +1 -0
  45. package/src/tui/app/doctor.mjs +6 -14
  46. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +8 -3
  47. package/src/tui/app/settings-picker/settings-rows.mjs +5 -1
  48. package/src/tui/app/use-ui-open-request.mjs +13 -3
  49. package/src/tui/dist/index.mjs +41 -9
  50. package/src/tui/session/session-api/settings.mjs +0 -6
@@ -20,6 +20,7 @@ import {
20
20
  normalizeSystemShellCommand,
21
21
  normalizeAutoClearConfig,
22
22
  autoClearIdleMsForProvider,
23
+ resolveAutoClearIdleMs,
23
24
  autoClearProviderDefaults,
24
25
  normalizeCompactionConfig,
25
26
  setModuleEnabledInConfig,
@@ -75,7 +76,10 @@ export function wireApis(boot) {
75
76
  });
76
77
  boot.lifecycleApi = lifecycleApiFor(boot);
77
78
  boot.resourceApi = resourceApiFor(boot);
78
- boot.disposeGlobalExtensionSubscription = () => boot.resourceApi.disposeGlobalExtensionSubscription?.();
79
+ boot.disposeGlobalExtensionSubscription = () => {
80
+ boot.disposeConfigSync?.();
81
+ boot.resourceApi.disposeGlobalExtensionSubscription?.();
82
+ };
79
83
  boot.modelRouteApi = modelRouteApiFor(boot);
80
84
  boot.workflowAgentsApi = workflowAgentsApiFor(boot);
81
85
  boot.sessionTurnApi = sessionTurnApiFor(boot);
@@ -207,6 +211,7 @@ function settingsApiFor(boot) {
207
211
  hasOwn,
208
212
  normalizeAutoClearConfig,
209
213
  autoClearIdleMsForProvider,
214
+ resolveAutoClearIdleMs,
210
215
  autoClearProviderDefaults,
211
216
  normalizeCompactionConfig,
212
217
  normalizeSystemShellConfig,
@@ -229,7 +234,6 @@ function settingsApiFor(boot) {
229
234
  tidyToolEnabledFn: boot.tidyToolEnabledFn,
230
235
  localProviderEnabledFn: boot.localProviderEnabledFn,
231
236
  webSearchEnabled: boot.webSearchEnabled,
232
- channelsEnabled: boot.channelsEnabled,
233
237
  autoUpdateEnabled: selfUpdate.autoUpdateEnabled,
234
238
  getUpdateCheckState: () => selfUpdate.getCheckState(),
235
239
  getUpdateProcessState: () => selfUpdate.getProcessState(),
@@ -86,7 +86,6 @@ export async function loadModules(boot) {
86
86
  localProviderEnabledFn,
87
87
  mediaToolEnabledFn,
88
88
  tidyToolEnabledFn,
89
- channelsEnabled,
90
89
  featureDisallowedTools,
91
90
  } = createRuntimeFeatureGates({
92
91
  getConfig: () => rt.config,
@@ -114,7 +113,6 @@ export async function loadModules(boot) {
114
113
  localProviderEnabledFn,
115
114
  mediaToolEnabledFn,
116
115
  tidyToolEnabledFn,
117
- channelsEnabled,
118
116
  featureDisallowedTools,
119
117
  getMemoryModule,
120
118
  getWebSearchModule,
@@ -71,9 +71,11 @@ function wireConfigLifecycle(boot) {
71
71
  LAZY_SECRET_PROVIDERS,
72
72
  clean,
73
73
  resolve,
74
+ onConfigReloaded: () => boot.invalidateContextStatusCache?.(),
74
75
  STANDALONE_DATA_DIR,
75
76
  });
76
77
  const {
78
+ disposeConfigSync,
77
79
  getOutputStyleStatusCached,
78
80
  invalidateOutputStyleStatusCache,
79
81
  seedOutputStyleStatusCache,
@@ -89,6 +91,7 @@ function wireConfigLifecycle(boot) {
89
91
  ensureConfigForRouteProvider,
90
92
  } = lifecycle;
91
93
  Object.assign(boot, {
94
+ disposeConfigSync,
92
95
  getOutputStyleStatusCached,
93
96
  invalidateOutputStyleStatusCache,
94
97
  seedOutputStyleStatusCache,
@@ -232,7 +232,6 @@ function wireInternalTools(boot) {
232
232
  officeToolsEnabled: boot.officeToolsEnabledFn,
233
233
  mediaToolEnabled: boot.mediaToolEnabledFn,
234
234
  tidyToolEnabled: boot.tidyToolEnabledFn,
235
- channelsEnabled: boot.channelsEnabled,
236
235
  getWebSearchModule: boot.getWebSearchModule,
237
236
  getMemoryModule: boot.getMemoryModule,
238
237
  getCodeGraphModule: boot.getCodeGraphModule,
@@ -66,6 +66,7 @@ export function createConfigLifecycle({
66
66
  clean,
67
67
  resolve,
68
68
  performanceNow = () => performance.now(),
69
+ onConfigReloaded,
69
70
  STANDALONE_DATA_DIR,
70
71
  }) {
71
72
  const outputStyleCache = createOutputStyleStatusCache({
@@ -160,7 +161,17 @@ export function createConfigLifecycle({
160
161
  return ensureFullConfig();
161
162
  }
162
163
 
164
+ // Settings persisted by any other runtime (or process) reach this runtime's
165
+ // in-memory config. reloadFullConfig keeps our own pending edits, and the
166
+ // shared layer coalesces bursts into one pass per subscriber.
167
+ const unsubscribeConfigChange = sharedCfgMod?.subscribeConfigChange?.(() => {
168
+ reloadFullConfig();
169
+ outputStyleCache.invalidateOutputStyleStatusCache();
170
+ onConfigReloaded?.();
171
+ });
172
+
163
173
  return {
174
+ disposeConfigSync: () => unsubscribeConfigChange?.(),
164
175
  // output-style cache
165
176
  ...outputStyleCache,
166
177
  // adopt / save
@@ -22,7 +22,6 @@ export function createInternalToolExecutor({
22
22
  officeToolsEnabled,
23
23
  mediaToolEnabled,
24
24
  tidyToolEnabled = () => true,
25
- channelsEnabled,
26
25
  getWebSearchModule,
27
26
  getMemoryModule,
28
27
  getCodeGraphModule,
@@ -68,7 +67,6 @@ export function createInternalToolExecutor({
68
67
  if (callerCtx?.invocationSource === 'model-tool') guardModelCall(name);
69
68
  if (Object.hasOwn(handlers, name)) return handlers[name](args, { name, callerCtx, callerCwd });
70
69
  if (channels.isChannelTool(name)) {
71
- if (!channelsEnabled()) throw new Error('channels are disabled in settings');
72
70
  return await channels.execute(name, args || {});
73
71
  }
74
72
  throw new Error(`unknown standalone internal tool: ${name}`);
@@ -5,7 +5,7 @@
5
5
  // Key names mirror the dependency names of settings-api.mjs / the session-build
6
6
  // deps, so the runtime can hand them straight through.
7
7
  import { builtinFeatureActive, featureDisallowedToolsFor, localGitToolsActive } from '../runtime/agent/orchestrator/runtime-core/builtin-features.mjs';
8
- import { moduleEnabled, recapEnabled } from '../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
8
+ import { recapEnabled } from '../runtime/agent/orchestrator/runtime-core/config-helpers.mjs';
9
9
  import { browserBridgeAvailableSync } from '../runtime/browser-bridge/client.mjs';
10
10
  import { computerBridgeAvailableSync } from '../runtime/computer-bridge/client.mjs';
11
11
 
@@ -17,12 +17,11 @@ export function createRuntimeFeatureGates({ getConfig, getToolProfile }) {
17
17
  recapEnabledFn: () => recapEnabled(getConfig(), true),
18
18
  memoryToolsEnabledFn: () => builtinFeatureActive(getConfig(), 'memory'),
19
19
  webSearchEnabled: () => builtinFeatureActive(getConfig(), 'webSearch'),
20
- gitToolsEnabledFn: () => localGitToolsActive(getConfig(), getToolProfile()),
20
+ gitToolsEnabledFn: () => localGitToolsActive(getConfig()),
21
21
  officeToolsEnabledFn: () => builtinFeatureActive(getConfig(), 'office'),
22
22
  localProviderEnabledFn: () => builtinFeatureActive(getConfig(), 'localProvider'),
23
23
  mediaToolEnabledFn: () => builtinFeatureActive(getConfig(), 'media'),
24
24
  tidyToolEnabledFn: () => builtinFeatureActive(getConfig(), 'tidy'),
25
- channelsEnabled: () => moduleEnabled(getConfig(), 'channels', true),
26
25
  // Browser/Computer activate on live desktop-bridge presence, so the probe
27
26
  // runs per call instead of being captured at boot.
28
27
  featureDisallowedTools: () =>
@@ -48,7 +48,6 @@ export async function runScheduleSession(schedule, { config = null, prompt: prom
48
48
  owner: 'user',
49
49
  sourceType: 'schedule',
50
50
  sourceName: schedule.name,
51
- sourceDelivery: schedule.delivery || null,
52
51
  ...(cwd ? { cwd } : {}),
53
52
  desktopSession: cwd
54
53
  ? { classification: 'project', projectPath: cwd }
@@ -188,17 +188,13 @@ function scheduleToDisplay(s) {
188
188
  whenAt: s.whenAt || undefined,
189
189
  whenCron: s.whenCron || undefined,
190
190
  timezone: s.timezone || undefined,
191
- channel: s.channelId || undefined,
192
191
  model: s.model || undefined,
193
192
  cwd: s.cwd || undefined,
194
193
  workflow: s.workflow || undefined,
195
194
  attachments: s.attachments || undefined,
196
- // Delivery mode: legacy channel-target rows (pre-delivery) behaved as
197
- // relay+visible, which maps to 'both'.
198
- delivery: s.delivery || (s.target === 'channel' ? 'both' : 'app'),
199
195
  enabled: s.enabled !== false,
200
196
  instructions: s.prompt,
201
- route: s.target === 'channel' ? `channel:${s.channelId}` : 'session',
197
+ route: 'session',
202
198
  };
203
199
  }
204
200
 
@@ -210,7 +206,7 @@ export async function listSchedules() {
210
206
  // Register or update a schedule in the PG store. Recurring input maps `time`
211
207
  // (+ optional `days`) to a cron; one-shot input maps an `at` datetime; the two
212
208
  // are mutually exclusive (also enforced by the store's when_at/when_cron XOR).
213
- // `channel` selects a channel target; otherwise session. Both require a model.
209
+ // Every schedule runs as a visible app session and requires a model.
214
210
  export async function saveSchedule({
215
211
  name,
216
212
  description = '',
@@ -218,12 +214,10 @@ export async function saveSchedule({
218
214
  at,
219
215
  timezone,
220
216
  days,
221
- channel,
222
217
  model,
223
218
  cwd,
224
219
  workflow,
225
220
  attachments,
226
- delivery,
227
221
  enabled,
228
222
  instructions,
229
223
  overwrite = false,
@@ -233,11 +227,6 @@ export async function saveSchedule({
233
227
  if (!body) throw new Error('schedule instructions are required');
234
228
  const scheduleModel = String(model || '').trim();
235
229
  if (!scheduleModel) throw new Error('schedule model is required — choose a model before saving');
236
- // 'app' → session-only; 'channel'/'both' → the run result relays to the
237
- // main channel (target 'channel', channelId resolved at fire time).
238
- const requestedMode = String(delivery || '').trim();
239
- let mode = channel ? 'both' : 'app';
240
- if (['app', 'channel', 'both'].includes(requestedMode)) mode = requestedMode;
241
230
  const hasTime = time != null && String(time).trim() !== '';
242
231
  const hasAt = at != null && String(at).trim() !== '';
243
232
  if (hasTime && hasAt) throw new Error('provide either `time` (recurring) or `at` (one-shot), not both');
@@ -254,13 +243,13 @@ export async function saveSchedule({
254
243
  whenCron,
255
244
  whenAt,
256
245
  timezone: scheduleTimezone,
257
- target: mode === 'app' ? 'session' : 'channel',
258
- channelId: channel ? String(channel).trim() : null,
246
+ target: 'session',
247
+ channelId: null,
259
248
  model: scheduleModel,
260
249
  cwd: cwd ? String(cwd).trim() : null,
261
250
  workflow: workflow ? String(workflow).trim() : null,
262
251
  attachments: normalizeAutomationAttachments(attachments),
263
- delivery: mode,
252
+ delivery: 'app',
264
253
  prompt: body,
265
254
  enabled: enabled !== false,
266
255
  nextFireAt: whenAt,
@@ -294,19 +283,17 @@ async function listWebhooks() {
294
283
  name: ep.name,
295
284
  description: ep.description || '',
296
285
  parser: ep.parser || 'github',
297
- ...(ep.channelId ? { channel: ep.channelId } : {}),
298
286
  ...(ep.model ? { model: ep.model } : {}),
299
287
  ...(ep.cwd ? { cwd: ep.cwd } : {}),
300
288
  ...(ep.workflow ? { workflow: ep.workflow } : {}),
301
289
  ...(ep.attachments ? { attachments: ep.attachments } : {}),
302
- delivery: ep.delivery || 'app',
303
290
  enabled: ep.enabled,
304
291
  // The store never projects the plaintext secret through list paths; it
305
292
  // exposes a presence flag (secretSet) instead.
306
293
  secretSet: ep.secretSet === true,
307
294
  secret: undefined,
308
295
  instructions: ep.instructions,
309
- route: ep.channelId ? `channel:${ep.channelId}` : 'session',
296
+ route: 'session',
310
297
  }));
311
298
  }
312
299
 
@@ -315,12 +302,10 @@ export async function saveWebhook({
315
302
  description = '',
316
303
  parser = 'github',
317
304
  secret,
318
- channel,
319
305
  model,
320
306
  cwd,
321
307
  workflow,
322
308
  attachments,
323
- delivery,
324
309
  enabled,
325
310
  instructions,
326
311
  overwrite = false,
@@ -334,7 +319,6 @@ export async function saveWebhook({
334
319
  }
335
320
  const body = String(instructions || '').trim();
336
321
  if (!body) throw new Error('webhook instructions are required');
337
- if (channel && !model) throw new Error('model is required when channel is set');
338
322
  if (overwrite !== true && (await dbLoadEndpoint(id))) {
339
323
  throw new Error(`webhook "${id}" already exists`);
340
324
  }
@@ -350,12 +334,12 @@ export async function saveWebhook({
350
334
  name: id,
351
335
  description: String(description || '').trim(),
352
336
  parser: nextParser,
353
- channelId: channel ? String(channel).trim() : null,
337
+ channelId: null,
354
338
  model: model ? String(model).trim() : null,
355
339
  cwd: cwd ? String(cwd).trim() : null,
356
340
  workflow: workflow ? String(workflow).trim() : null,
357
341
  attachments: normalizeAutomationAttachments(attachments),
358
- delivery: ['app', 'channel', 'both'].includes(String(delivery || '').trim()) ? String(delivery).trim() : 'app',
342
+ delivery: 'app',
359
343
  secret: secretValue,
360
344
  instructions: body,
361
345
  enabled: enabled !== false,
@@ -365,12 +349,10 @@ export async function saveWebhook({
365
349
  name: id,
366
350
  description: saved.description,
367
351
  parser: saved.parser,
368
- ...(saved.channelId ? { channel: saved.channelId } : {}),
369
352
  ...(saved.model ? { model: saved.model } : {}),
370
353
  ...(saved.cwd ? { cwd: saved.cwd } : {}),
371
354
  ...(saved.workflow ? { workflow: saved.workflow } : {}),
372
355
  ...(saved.attachments ? { attachments: saved.attachments } : {}),
373
- delivery: saved.delivery || 'app',
374
356
  ...(enabled === false ? { enabled: false } : {}),
375
357
  secret: secretValue,
376
358
  instructions: body,
@@ -39,6 +39,7 @@ export function createCompactionSettings({
39
39
  hasOwn,
40
40
  normalizeAutoClearConfig,
41
41
  autoClearIdleMsForProvider,
42
+ resolveAutoClearIdleMs,
42
43
  normalizeCompactionConfig,
43
44
  autoClearProviderDefaults,
44
45
  parseDurationMs,
@@ -96,7 +97,9 @@ export function createCompactionSettings({
96
97
  const normalized = normalizeAutoClearConfig(config.autoClear);
97
98
  const provider = route?.provider || null;
98
99
  const providerDefault = autoClearIdleMsForProvider(provider, normalized.providerIdleMs);
99
- const idleMs = normalized.custom ? normalized.idleMs : providerDefault;
100
+ const idleMs = resolveAutoClearIdleMs(config, provider);
101
+ const key = String(provider || '').toLowerCase();
102
+ const providerCustom = Boolean(key && key !== 'default' && normalized.providerIdleMs?.[key]);
100
103
  // Advanced picker shows only providers the user actually has enabled
101
104
  // (config.providers[*].enabled), plus the active route provider, any
102
105
  // provider with a custom override, and the 'default' fallback row —
@@ -114,6 +117,7 @@ export function createCompactionSettings({
114
117
  enabled: normalized.enabled,
115
118
  idleMs,
116
119
  custom: normalized.custom,
120
+ providerCustom,
117
121
  providerDefault,
118
122
  provider,
119
123
  providerDefaults,
@@ -1,39 +1,21 @@
1
- // Channels, the system shell and self-update settings.
1
+ // Automation worker status, the system shell and self-update settings.
2
2
  export function createSystemSettings({
3
3
  getConfig,
4
4
  saveConfigAndAdopt,
5
5
  normalizeSystemShellConfig,
6
6
  normalizeSystemShellCommand,
7
7
  setConfiguredShell,
8
- setModuleEnabledInConfig,
9
8
  localPackageVersion,
10
- channelsEnabled,
11
9
  autoUpdateEnabled,
12
10
  getUpdateCheckState,
13
11
  getUpdateProcessState,
14
- invalidatePreSessionToolSurface,
15
12
  channels,
16
- clearChannelStartTimer,
17
13
  checkForUpdateInternal,
18
14
  runUpdateNowInternal,
19
15
  }) {
20
16
  return {
21
17
  getChannelSettings(options = {}) {
22
- return {
23
- enabled: channelsEnabled(),
24
- ...(options?.includeStatus === false ? {} : { status: channels.status() }),
25
- };
26
- },
27
- async setChannelsEnabled(enabled) {
28
- const config = getConfig();
29
- const nextConfig = setModuleEnabledInConfig({ ...config }, 'channels', enabled !== false);
30
- saveConfigAndAdopt(nextConfig);
31
- if (!channelsEnabled()) {
32
- clearChannelStartTimer();
33
- await channels.stop('settings-disabled', { waitForExit: false }).catch(() => {});
34
- }
35
- invalidatePreSessionToolSurface();
36
- return this.getChannelSettings();
18
+ return options?.includeStatus === false ? {} : { status: channels.status() };
37
19
  },
38
20
  getSystemShell() {
39
21
  const config = getConfig();
@@ -18,6 +18,12 @@ import {
18
18
  } from './tool-defs.mjs';
19
19
  import { schemaValueError } from '../../runtime/shared/schema-value-error.mjs';
20
20
  import { clean } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
21
+ import profileConfig from '../../runtime/shared/profile-config.cjs';
22
+
23
+ const PROFILE_VALUE_IDS = Object.freeze({
24
+ language: profileConfig.PROFILE_LANGUAGES.map((entry) => entry.id),
25
+ experienceLevel: ['', ...profileConfig.PROFILE_EXPERIENCE_LEVELS.map((entry) => entry.id)],
26
+ });
21
27
 
22
28
  const ACTION_APPLIES_TO = {
23
29
  set_recap_enabled: 'background Memory cycles; no restart required',
@@ -49,6 +55,9 @@ const OPEN_TARGET_HINTS = Object.freeze({
49
55
  usage: 'Desktop: /usage in the composer · TUI: /usage',
50
56
  doctor: 'Desktop: Settings → System → Doctor, or /doctor in the composer · TUI: /doctor',
51
57
  context: 'Desktop: /context in the composer · TUI: /context',
58
+ developer: 'Desktop: Settings → Developer options · TUI: /setting → Developer',
59
+ voice: 'Desktop: Extensions → Plugin → Built-in (Voice) · TUI: /setting → Voice',
60
+ connection: 'Desktop only: Settings → Connection',
52
61
  });
53
62
 
54
63
  function requireEnum(value, allowed, label) {
@@ -123,6 +132,20 @@ function publicProviderRows(setup) {
123
132
  };
124
133
  }
125
134
 
135
+ /** OAuth account roster without usage meters or provider-side identities. */
136
+ function publicAccounts(pool) {
137
+ return {
138
+ selectedId: pool.selectedId,
139
+ auto: pool.auto,
140
+ accounts: pool.accounts.map(({ id, label, authenticated, reauthRequired }) => ({
141
+ id,
142
+ label,
143
+ authenticated,
144
+ reauthRequired,
145
+ })),
146
+ };
147
+ }
148
+
126
149
  function mcpRows(status) {
127
150
  return {
128
151
  connectedCount: Number(status?.connectedCount || 0),
@@ -241,7 +264,14 @@ const SETUP_STATUS_READERS = {
241
264
  };
242
265
  },
243
266
  shell: (rt) => rt.getSystemShell?.() || {},
244
- providers: async (rt) => publicProviderRows(await rt.getProviderSetup?.({})),
267
+ providers: async (rt) => {
268
+ const rows = publicProviderRows(await rt.getProviderSetup?.({}));
269
+ rows.oauth = rows.oauth.map((row) =>
270
+ row.authenticated ? { ...row, accounts: publicAccounts(rt.getProviderAccounts(row.id)) } : row
271
+ );
272
+ return rows;
273
+ },
274
+ developer: (rt) => rt.getDeveloperSettings(),
245
275
  mcp: (rt) => mcpRows(rt.mcpStatus?.()),
246
276
  skills: (rt) => ({ ...(rt.skillsStatus?.() || {}), disabled: rt.getDisabledSkills?.()?.disabled || [] }),
247
277
  plugins: (rt) => rt.pluginsStatus?.() || {},
@@ -274,7 +304,7 @@ function validateSetupInput(args) {
274
304
  throw new Error('route.contextPercent is only accepted by set_route');
275
305
  }
276
306
  }
277
- for (const field of ['desktop', 'appearance', 'webhook', 'compaction']) {
307
+ for (const field of ['desktop', 'appearance', 'webhook', 'compaction', 'providerAccount']) {
278
308
  if (args[field] && !Object.keys(args[field]).length) throw new Error(`${field} requires at least one setting`);
279
309
  }
280
310
  return action;
@@ -326,6 +356,11 @@ const SETUP_ACTION_HANDLERS = {
326
356
  const profile = args.profile && typeof args.profile === 'object' ? args.profile : null;
327
357
  if (!profile || !Object.keys(profile).length)
328
358
  throw new Error('profile with title, language, or experienceLevel is required');
359
+ for (const [field, ids] of Object.entries(PROFILE_VALUE_IDS)) {
360
+ if (Object.hasOwn(profile, field) && !ids.includes(profile[field])) {
361
+ throw new Error(`profile.${field} must be one of ${ids.map((id) => JSON.stringify(id)).join(', ')}`);
362
+ }
363
+ }
329
364
  const result = rt.setProfile(profile);
330
365
  return {
331
366
  title: result.title || '',
@@ -414,7 +449,23 @@ const SETUP_ACTION_HANDLERS = {
414
449
  rt.deleteLocalProviderModel(requireText(args.confirmationToken, 'confirmationToken')),
415
450
  set_system_shell: (rt, args) => rt.setSystemShell({ command: clean(args.command) }),
416
451
  set_auto_update: (rt, args) => rt.setAutoUpdate(requireBoolean(args.enabled)),
417
- forget_provider_auth: (rt, args) => rt.forgetProviderAuth(requireText(args.name, 'name')),
452
+ forget_provider_auth: async (rt, args) => {
453
+ const name = requireText(args.name, 'name');
454
+ const setup = await rt.getProviderSetup({});
455
+ if (!(setup?.oauth || []).some((row) => row.id === name)) {
456
+ if (Object.hasOwn(args, 'accountId')) throw new Error('accountId applies only to OAuth providers');
457
+ return rt.forgetProviderAuth(name);
458
+ }
459
+ const { accounts } = rt.getProviderAccounts(name);
460
+ if (!accounts.length) throw new Error(`${name} has no connected account`);
461
+ if (!Object.hasOwn(args, 'accountId') && accounts.length > 1) {
462
+ throw new Error(`${name} has ${accounts.length} accounts; pass accountId from status providers`);
463
+ }
464
+ const accountId = Object.hasOwn(args, 'accountId') ? requireText(args.accountId, 'accountId') : accounts[0].id;
465
+ return { ...(await rt.forgetProviderAuth(name, accountId)), accountId };
466
+ },
467
+ set_provider_account: async (rt, args) =>
468
+ publicAccounts(await rt.updateProviderAccounts(requireText(args.name, 'name'), args.providerAccount)),
418
469
  add_mcp_server: async (rt, args) => {
419
470
  const server = mcpServerInput(args);
420
471
  requireText(server.name, 'server.name');
@@ -456,6 +507,13 @@ const SETUP_ACTION_HANDLERS = {
456
507
  remove_plugin: async (rt, args) => ({
457
508
  plugin: (await rt.removePlugin(requireText(args.name, 'name')))?.plugin || null,
458
509
  }),
510
+ enable_plugin_mcp: async (rt, args) => {
511
+ const name = requireText(args.name, 'name');
512
+ const plugin = ((await rt.pluginsStatus())?.plugins || []).find((row) => row.name === name || row.id === name);
513
+ if (!plugin) throw new Error(`plugin "${name}" not found`);
514
+ const result = await rt.enablePluginMcp(plugin);
515
+ return { serverName: result.serverName, mcp: mcpRows(result.status) };
516
+ },
459
517
  };
460
518
 
461
519
  const READ_ONLY_SETUP_ACTIONS = new Set([
@@ -1,4 +1,3 @@
1
- import { parseSkillDocument } from '../../runtime/shared/skill-document.mjs';
2
1
  import { SETUP_EXTENDED_PROPERTIES, SETUP_DESKTOP_ACTIONS } from './settings-contract.mjs';
3
2
 
4
3
  const own = (value, key) => Object.hasOwn(value || {}, key);
@@ -76,15 +75,16 @@ export function validateMcpInput(server) {
76
75
  async function readDefinition(rt, kind, name) {
77
76
  if (kind === 'workflow') return rt.getWorkflowPack(name);
78
77
  if (kind === 'agent') return rt.getAgentDefinition(name);
78
+ // skillContent already returns the SKILL.md body without frontmatter; the
79
+ // frontmatter fields come from the catalog row.
79
80
  const resource = await rt.skillContent(name);
80
81
  const metadata = (await rt.skillsStatus()).skills.find((row) => row.name === name) || {};
81
- const parsed = parseSkillDocument(resource.content);
82
82
  return {
83
83
  name,
84
84
  originalName: name,
85
- description: metadata.description || parsed.description || '',
86
- whenToUse: metadata.whenToUse || parsed.whenToUse || '',
87
- body: parsed.body,
85
+ description: metadata.description || '',
86
+ whenToUse: metadata.whenToUse || '',
87
+ body: resource.content,
88
88
  toolDependencies: metadata.toolDependencies || [],
89
89
  editable: metadata.editable === true,
90
90
  };
@@ -103,6 +103,19 @@ async function saveDefinition(rt, args) {
103
103
  const id = input.originalName || input.id || input.name;
104
104
  if (!creating && !String(id || '').trim()) throw new Error('An existing definition identity is required');
105
105
  const current = creating ? {} : await readDefinition(rt, kind, id);
106
+ if (kind === 'skill' && !creating && !current.editable) {
107
+ // Built-in and plugin skills keep their document; like the Skills editor,
108
+ // only their tool dependencies are user-editable (null restores them).
109
+ const edits = Object.keys(input).filter((key) => key !== 'originalName' && key !== 'name');
110
+ if (edits.length !== 1 || edits[0] !== 'toolDependencies' || (own(input, 'name') && input.name !== current.name)) {
111
+ throw new Error('Built-in and plugin skills accept only toolDependencies edits');
112
+ }
113
+ return rt.saveSkill({
114
+ originalName: current.originalName,
115
+ dependenciesOnly: true,
116
+ toolDependencies: input.toolDependencies,
117
+ });
118
+ }
106
119
  const next = { ...pick(current, definitionKeys), ...input };
107
120
  if (!String(next.body || '').trim()) throw new Error('definition.body must not be empty');
108
121
  if (kind === 'workflow') {
@@ -115,7 +128,6 @@ async function saveDefinition(rt, args) {
115
128
  }
116
129
  return rt.saveAgentDefinition(creating ? next : { ...next, id: current.id });
117
130
  }
118
- if (!creating && !current.editable) throw new Error('Only machine-global user skills can be edited');
119
131
  return creating ? rt.addSkill(next) : rt.saveSkill({ ...next, originalName: current.originalName });
120
132
  }
121
133
 
@@ -188,6 +200,19 @@ export const EXTENDED_SETUP_HANDLERS = Object.freeze({
188
200
  args.automationKind === 'schedule'
189
201
  ? rt.setScheduleEnabled(args.name, args.enabled)
190
202
  : rt.setWebhookEnabled(args.name, args.enabled),
203
+ async set_developer_option(rt, args) {
204
+ const option = rt
205
+ .getDeveloperSettings()
206
+ .sections.flatMap((section) => section.options)
207
+ .find((row) => row.id === args.name);
208
+ if (!option) throw new Error(`Unknown developer option "${args.name}"; read status developer`);
209
+ if (args.enabled && option.warning && args.riskAccepted !== true) {
210
+ throw new Error(
211
+ `Enabling ${option.label} requires riskAccepted:true after the user explicitly accepts: ${option.warning}`
212
+ );
213
+ }
214
+ return { developer: await rt.setDeveloperOption(option.id, args.enabled) };
215
+ },
191
216
  async set_webhook_config(rt, args) {
192
217
  await rt.setWebhookConfig(args.webhook);
193
218
  const status = await rt.getChannelSetup();
@@ -31,10 +31,26 @@ export const SETUP_EXTENDED_ACTION_FIELDS = Object.freeze({
31
31
  save_project: 'project',
32
32
  remove_project: 'projectPath',
33
33
  revoke_linked_device: 'name',
34
+ set_activity_rail_pins: 'activityRailPins',
35
+ set_developer_option: 'name enabled riskAccepted?',
34
36
  });
35
37
 
38
+ /** Mirrors ACTIVITY_RAIL_PIN_IDS in apps/desktop/src/shared/activity-rail-pins.ts. */
39
+ export const SETUP_ACTIVITY_RAIL_PIN_IDS = Object.freeze([
40
+ 'sessions',
41
+ 'agents',
42
+ 'schedules',
43
+ 'workflows',
44
+ 'projects',
45
+ 'extensions',
46
+ 'source-control',
47
+ 'search',
48
+ 'webhooks',
49
+ ]);
50
+
36
51
  export const SETUP_EXTENDED_DOMAINS = Object.freeze([
37
52
  'capabilities',
53
+ 'developer',
38
54
  'desktop',
39
55
  'appearance',
40
56
  'projects',
@@ -49,6 +65,7 @@ export const SETUP_DESKTOP_ACTIONS = Object.freeze([
49
65
  'save_project',
50
66
  'remove_project',
51
67
  'revoke_linked_device',
68
+ 'set_activity_rail_pins',
52
69
  ]);
53
70
  export const SETUP_DESKTOP_DOMAINS = Object.freeze(['desktop', 'appearance', 'projects', 'connection']);
54
71
 
@@ -128,7 +145,6 @@ export const SETUP_EXTENDED_PROPERTIES = Object.freeze({
128
145
  timezone: text,
129
146
  days: { ...text, description: 'Schedule day selector, e.g. weekdays or mon,wed,fri.' },
130
147
  parser: { type: 'string', enum: ['github', 'generic', 'stripe', 'sentry'] },
131
- channel: text,
132
148
  model: {
133
149
  ...text,
134
150
  description:
@@ -153,7 +169,6 @@ export const SETUP_EXTENDED_PROPERTIES = Object.freeze({
153
169
  ['kind', 'data']
154
170
  ),
155
171
  },
156
- delivery: { type: 'string', enum: ['app', 'channel', 'both'] },
157
172
  enabled: boolean,
158
173
  overwrite: { ...boolean, description: 'Explicitly update an existing entry; omitted fields are preserved.' },
159
174
  },
@@ -178,10 +193,19 @@ export const SETUP_EXTENDED_PROPERTIES = Object.freeze({
178
193
  description: 'Desktop UI language id from status appearance; independent of profile language.',
179
194
  },
180
195
  sidePanels: { type: 'string', enum: ['close-left', 'close-right', 'close-both', 'keep-open'] },
181
- zoom: { type: 'number', minimum: 0.5, maximum: 2 },
182
196
  }),
183
197
  project: object({ path: text, alias: text }, ['path']),
184
198
  projectPath: { ...text, description: 'Exact registered Project root.' },
199
+ activityRailPins: {
200
+ type: 'array',
201
+ items: { type: 'string', enum: [...SETUP_ACTIVITY_RAIL_PIN_IDS] },
202
+ description: 'set_activity_rail_pins: complete ordered list of pinned activity rail items; it replaces the current list.',
203
+ },
204
+ riskAccepted: {
205
+ ...boolean,
206
+ description:
207
+ "set_developer_option: the user's explicit acceptance of the option warning; required to enable an option that has one.",
208
+ },
185
209
  });
186
210
 
187
211
  export const SETUP_HANDOFFS = Object.freeze({