@geminixiang/mikan 1.0.0-beta.94 → 1.0.0-beta.96

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 (198) hide show
  1. package/CHANGELOG.md +432 -378
  2. package/README.md +31 -40
  3. package/dist/adapters/commands/login.js +1 -1
  4. package/dist/adapters/commands/login.js.map +1 -1
  5. package/dist/adapters/commands/registry.d.ts +1 -1
  6. package/dist/adapters/commands/registry.d.ts.map +1 -1
  7. package/dist/adapters/commands/registry.js +1 -2
  8. package/dist/adapters/commands/registry.js.map +1 -1
  9. package/dist/adapters/commands/types.d.ts +2 -4
  10. package/dist/adapters/commands/types.d.ts.map +1 -1
  11. package/dist/adapters/commands/types.js.map +1 -1
  12. package/dist/adapters/github/bot.js +1 -1
  13. package/dist/adapters/github/bot.js.map +1 -1
  14. package/dist/adapters/github/policy.d.ts +2 -9
  15. package/dist/adapters/github/policy.d.ts.map +1 -1
  16. package/dist/adapters/github/policy.js +18 -26
  17. package/dist/adapters/github/policy.js.map +1 -1
  18. package/dist/adapters/slack/bot.d.ts +2 -4
  19. package/dist/adapters/slack/bot.d.ts.map +1 -1
  20. package/dist/adapters/slack/bot.js +28 -56
  21. package/dist/adapters/slack/bot.js.map +1 -1
  22. package/dist/adapters/slack/socket-agent.d.ts +9 -0
  23. package/dist/adapters/slack/socket-agent.d.ts.map +1 -0
  24. package/dist/adapters/slack/socket-agent.js +17 -0
  25. package/dist/adapters/slack/socket-agent.js.map +1 -0
  26. package/dist/adapters/telegram/bot.d.ts +1 -0
  27. package/dist/adapters/telegram/bot.d.ts.map +1 -1
  28. package/dist/adapters/telegram/bot.js +6 -1
  29. package/dist/adapters/telegram/bot.js.map +1 -1
  30. package/dist/adapters/telegram/types.d.ts +1 -0
  31. package/dist/adapters/telegram/types.d.ts.map +1 -1
  32. package/dist/adapters/telegram/types.js.map +1 -1
  33. package/dist/adapters/web/admin/portal.d.ts.map +1 -1
  34. package/dist/adapters/web/admin/portal.js +92 -173
  35. package/dist/adapters/web/admin/portal.js.map +1 -1
  36. package/dist/adapters/web/admin/types.d.ts +3 -8
  37. package/dist/adapters/web/admin/types.d.ts.map +1 -1
  38. package/dist/adapters/web/admin/types.js.map +1 -1
  39. package/dist/adapters/web/server.d.ts +1 -1
  40. package/dist/adapters/web/server.d.ts.map +1 -1
  41. package/dist/adapters/web/server.js.map +1 -1
  42. package/dist/cli/arg-grammar.d.ts +1 -3
  43. package/dist/cli/arg-grammar.d.ts.map +1 -1
  44. package/dist/cli/arg-grammar.js +2 -20
  45. package/dist/cli/arg-grammar.js.map +1 -1
  46. package/dist/cli/boot.d.ts.map +1 -1
  47. package/dist/cli/boot.js +5 -11
  48. package/dist/cli/boot.js.map +1 -1
  49. package/dist/cli/migrate.d.ts.map +1 -1
  50. package/dist/cli/migrate.js +22 -7
  51. package/dist/cli/migrate.js.map +1 -1
  52. package/dist/cli/office.d.ts.map +1 -1
  53. package/dist/cli/office.js +3 -5
  54. package/dist/cli/office.js.map +1 -1
  55. package/dist/cli/onboard.js +18 -20
  56. package/dist/cli/onboard.js.map +1 -1
  57. package/dist/cli/types.d.ts +1 -2
  58. package/dist/cli/types.d.ts.map +1 -1
  59. package/dist/cli/types.js.map +1 -1
  60. package/dist/env-manifest.d.ts +2 -1
  61. package/dist/env-manifest.d.ts.map +1 -1
  62. package/dist/env-manifest.js +76 -47
  63. package/dist/env-manifest.js.map +1 -1
  64. package/dist/events/index.d.ts +5 -5
  65. package/dist/events/index.d.ts.map +1 -1
  66. package/dist/events/index.js +1 -7
  67. package/dist/events/index.js.map +1 -1
  68. package/dist/file-guards.d.ts +1 -0
  69. package/dist/file-guards.d.ts.map +1 -1
  70. package/dist/file-guards.js +4 -1
  71. package/dist/file-guards.js.map +1 -1
  72. package/dist/harness/execution-resolver.d.ts.map +1 -1
  73. package/dist/harness/execution-resolver.js +2 -8
  74. package/dist/harness/execution-resolver.js.map +1 -1
  75. package/dist/harness/http.d.ts +1 -2
  76. package/dist/harness/http.d.ts.map +1 -1
  77. package/dist/harness/http.js +3 -16
  78. package/dist/harness/http.js.map +1 -1
  79. package/dist/harness/jev.d.ts +2 -1
  80. package/dist/harness/jev.d.ts.map +1 -1
  81. package/dist/harness/jev.js.map +1 -1
  82. package/dist/harness/models.d.ts +2 -2
  83. package/dist/harness/models.d.ts.map +1 -1
  84. package/dist/harness/models.js +4 -6
  85. package/dist/harness/models.js.map +1 -1
  86. package/dist/harness/prompt.d.ts.map +1 -1
  87. package/dist/harness/prompt.js +0 -6
  88. package/dist/harness/prompt.js.map +1 -1
  89. package/dist/harness/runner.js +1 -2
  90. package/dist/harness/runner.js.map +1 -1
  91. package/dist/harness/session.d.ts +2 -9
  92. package/dist/harness/session.d.ts.map +1 -1
  93. package/dist/harness/session.js +3 -3
  94. package/dist/harness/session.js.map +1 -1
  95. package/dist/harness/tools/event.d.ts +0 -1
  96. package/dist/harness/tools/event.d.ts.map +1 -1
  97. package/dist/harness/tools/event.js +1 -7
  98. package/dist/harness/tools/event.js.map +1 -1
  99. package/dist/harness/types.d.ts +4 -4
  100. package/dist/harness/types.d.ts.map +1 -1
  101. package/dist/harness/types.js.map +1 -1
  102. package/dist/index.d.ts +8 -31
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +3 -15
  105. package/dist/index.js.map +1 -1
  106. package/dist/main.js +29 -41
  107. package/dist/main.js.map +1 -1
  108. package/dist/migrations/index.d.ts +0 -1
  109. package/dist/migrations/index.d.ts.map +1 -1
  110. package/dist/migrations/index.js +4 -10
  111. package/dist/migrations/index.js.map +1 -1
  112. package/dist/migrations/legacy-office-files.d.ts +3 -0
  113. package/dist/migrations/legacy-office-files.d.ts.map +1 -0
  114. package/dist/migrations/legacy-office-files.js +29 -0
  115. package/dist/migrations/legacy-office-files.js.map +1 -0
  116. package/dist/migrations/session-v3.d.ts +57 -0
  117. package/dist/migrations/session-v3.d.ts.map +1 -0
  118. package/dist/migrations/session-v3.js +93 -0
  119. package/dist/migrations/session-v3.js.map +1 -0
  120. package/dist/migrations/sessions-sqlite.d.ts.map +1 -1
  121. package/dist/migrations/sessions-sqlite.js +98 -54
  122. package/dist/migrations/sessions-sqlite.js.map +1 -1
  123. package/dist/observability/instrument.js +2 -5
  124. package/dist/observability/instrument.js.map +1 -1
  125. package/dist/observability/sentry.js +1 -1
  126. package/dist/observability/sentry.js.map +1 -1
  127. package/dist/office/projection.js +2 -2
  128. package/dist/office/projection.js.map +1 -1
  129. package/dist/runtime/conversation-runtime.js +9 -14
  130. package/dist/runtime/conversation-runtime.js.map +1 -1
  131. package/dist/runtime/types.d.ts +2 -7
  132. package/dist/runtime/types.d.ts.map +1 -1
  133. package/dist/runtime/types.js.map +1 -1
  134. package/dist/sandbox/identity.d.ts +0 -1
  135. package/dist/sandbox/identity.d.ts.map +1 -1
  136. package/dist/sandbox/identity.js +0 -3
  137. package/dist/sandbox/identity.js.map +1 -1
  138. package/dist/sandbox/registry.d.ts +1 -5
  139. package/dist/sandbox/registry.d.ts.map +1 -1
  140. package/dist/sandbox/registry.js +4 -13
  141. package/dist/sandbox/registry.js.map +1 -1
  142. package/dist/sandbox/types.d.ts +1 -5
  143. package/dist/sandbox/types.d.ts.map +1 -1
  144. package/dist/sandbox/types.js.map +1 -1
  145. package/dist/sessions/types.d.ts +1 -1
  146. package/dist/sessions/types.d.ts.map +1 -1
  147. package/dist/sessions/types.js.map +1 -1
  148. package/dist/settings/apply.d.ts +1 -1
  149. package/dist/settings/apply.d.ts.map +1 -1
  150. package/dist/settings/apply.js +4 -5
  151. package/dist/settings/apply.js.map +1 -1
  152. package/dist/settings/index.d.ts +59 -5
  153. package/dist/settings/index.d.ts.map +1 -1
  154. package/dist/settings/index.js +75 -54
  155. package/dist/settings/index.js.map +1 -1
  156. package/dist/types.d.ts +3 -28
  157. package/dist/types.d.ts.map +1 -1
  158. package/dist/types.js.map +1 -1
  159. package/dist/vault/index.d.ts.map +1 -1
  160. package/dist/vault/index.js +2 -15
  161. package/dist/vault/index.js.map +1 -1
  162. package/package.json +4 -17
  163. package/dist/cli/download.d.ts +0 -2
  164. package/dist/cli/download.d.ts.map +0 -1
  165. package/dist/cli/download.js +0 -84
  166. package/dist/cli/download.js.map +0 -1
  167. package/dist/harness/index.d.ts +0 -20
  168. package/dist/harness/index.d.ts.map +0 -1
  169. package/dist/harness/index.js +0 -14
  170. package/dist/harness/index.js.map +0 -1
  171. package/dist/migrations/dream-checkpoints.d.ts +0 -3
  172. package/dist/migrations/dream-checkpoints.d.ts.map +0 -1
  173. package/dist/migrations/dream-checkpoints.js +0 -23
  174. package/dist/migrations/dream-checkpoints.js.map +0 -1
  175. package/dist/migrations/private-office-dirs.d.ts +0 -3
  176. package/dist/migrations/private-office-dirs.d.ts.map +0 -1
  177. package/dist/migrations/private-office-dirs.js +0 -24
  178. package/dist/migrations/private-office-dirs.js.map +0 -1
  179. package/dist/migrations/session-files.d.ts +0 -28
  180. package/dist/migrations/session-files.d.ts.map +0 -1
  181. package/dist/migrations/session-files.js +0 -80
  182. package/dist/migrations/session-files.js.map +0 -1
  183. package/dist/migrations/session-v4.d.ts +0 -77
  184. package/dist/migrations/session-v4.d.ts.map +0 -1
  185. package/dist/migrations/session-v4.js +0 -177
  186. package/dist/migrations/session-v4.js.map +0 -1
  187. package/dist/migrations/sessions-v3.d.ts +0 -8
  188. package/dist/migrations/sessions-v3.d.ts.map +0 -1
  189. package/dist/migrations/sessions-v3.js +0 -350
  190. package/dist/migrations/sessions-v3.js.map +0 -1
  191. package/dist/sandbox/cloudflare.d.ts +0 -17
  192. package/dist/sandbox/cloudflare.d.ts.map +0 -1
  193. package/dist/sandbox/cloudflare.js +0 -142
  194. package/dist/sandbox/cloudflare.js.map +0 -1
  195. package/dist/sandbox/index.d.ts +0 -7
  196. package/dist/sandbox/index.d.ts.map +0 -1
  197. package/dist/sandbox/index.js +0 -6
  198. package/dist/sandbox/index.js.map +0 -1
@@ -2,7 +2,7 @@ import { validateEventFilename } from "../../../events/index.js";
2
2
  import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, } from "node:fs";
3
3
  import { basename, join, matchesGlob, resolve as pathResolve, sep as pathSep } from "node:path";
4
4
  import { atomicWritePrivateFile } from "../../../file-guards.js";
5
- import { MikanModels } from "../../../harness/models.js";
5
+ import { defaultModelsJsonPath, MikanModels } from "../../../harness/models.js";
6
6
  import { formatSkillsForPrompt, loadOfficeSkills, resolveSkillEnabled, validateSkill, } from "../../../harness/skills.js";
7
7
  import { SessionStore } from "../../../sessions/session-store.js";
8
8
  import { InMemoryTokenStore } from "../token-store.js";
@@ -83,9 +83,9 @@ async function routeGetApiRequest(res, url, services) {
83
83
  case "/admin/api/conversation-state":
84
84
  return serveConversationState(res, url, services, token);
85
85
  case "/admin/api/settings/global":
86
- return serveGlobalSettings(res);
86
+ return serveGlobalSettings(res, services.workspace);
87
87
  case "/admin/api/models":
88
- return serveModelsList(res);
88
+ return serveModelsList(res, services.workspace);
89
89
  case "/admin/api/workspace/tree":
90
90
  return serveWorkspaceTree(res, url, services, token);
91
91
  case "/admin/api/workspace/file":
@@ -162,21 +162,12 @@ function resolveTargetConversation(body, token) {
162
162
  const platform = typeof body.platform === "string" ? body.platform.trim() : "";
163
163
  return resolveConversationScope(requested, platform, token);
164
164
  }
165
- function requireAdminWorkspace(res, services) {
166
- if (!services.workspace) {
167
- jsonRes(res, 503, { error: "Working directory not available" });
168
- return null;
169
- }
170
- return services.workspace;
171
- }
172
- function requireConversationWorkspace(res, body, services, token) {
173
- const scope = resolveTargetConversation(body, token);
165
+ function scopedOffice(res, services, scope) {
174
166
  if (scope.error) {
175
167
  jsonRes(res, 403, { error: scope.error });
176
168
  return undefined;
177
169
  }
178
- const workspace = requireAdminWorkspace(res, services);
179
- return workspace ? { scope, workspace } : undefined;
170
+ return services.workspace.office(scope.address);
180
171
  }
181
172
  function listAdminOffices(workspace) {
182
173
  return listRegisteredOffices(workspace.stateDir)
@@ -224,9 +215,7 @@ function conversationDisplayLabel(services, office) {
224
215
  return `${office.platform}:${office.conversationId}`;
225
216
  }
226
217
  function serveConversationsList(res, services) {
227
- const workspace = requireAdminWorkspace(res, services);
228
- if (!workspace)
229
- return;
218
+ const { workspace } = services;
230
219
  const running = services.runtime?.getRunningSessions() ?? [];
231
220
  const conversations = listAdminOffices(workspace).map((office) => ({
232
221
  platform: office.platform,
@@ -238,9 +227,7 @@ function serveConversationsList(res, services) {
238
227
  jsonRes(res, 200, { conversations });
239
228
  }
240
229
  async function serveSessionUsage(res, services) {
241
- const workspace = requireAdminWorkspace(res, services);
242
- if (!workspace)
243
- return;
230
+ const { workspace } = services;
244
231
  const usageLists = [];
245
232
  for (const office of listAdminOffices(workspace)) {
246
233
  usageLists.push(await listConversationSessionUsage(workspace, office, conversationDisplayLabel(services, office)));
@@ -315,9 +302,7 @@ function emptyBucket(date) {
315
302
  return { date, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0 };
316
303
  }
317
304
  async function serveConversationUsage(res, url, services) {
318
- const workspace = requireAdminWorkspace(res, services);
319
- if (!workspace)
320
- return;
305
+ const { workspace } = services;
321
306
  const conversationId = (url.searchParams.get("conversationId") ?? "").trim();
322
307
  const platformParam = (url.searchParams.get("platform") ?? "").trim();
323
308
  const office = listAdminOffices(workspace).find((candidate) => candidate.conversationId === conversationId &&
@@ -406,17 +391,12 @@ async function accumulateSessionUsageByDay(office, sessionKey, cutoff, buckets,
406
391
  catch { }
407
392
  }
408
393
  function serveConversationState(res, url, services, token) {
409
- const workspace = requireAdminWorkspace(res, services);
410
- if (!workspace)
394
+ const { workspace } = services;
395
+ const office = scopedOffice(res, services, resolveConversationFromQuery(url, token));
396
+ if (!office)
411
397
  return;
412
- const scope = resolveConversationFromQuery(url, token);
413
- if (scope.error) {
414
- jsonRes(res, 400, { error: scope.error });
415
- return;
416
- }
417
- const conversationId = scope.conversationId;
418
- const office = workspace.office(scope.address);
419
- const globalConfig = loadGlobalSettings();
398
+ const conversationId = office.address.conversationId;
399
+ const globalConfig = loadGlobalSettings(workspace.stateDir);
420
400
  const conversationConfig = resolveConversationSettings(office);
421
401
  const conversationWorkspace = resolveWorkspaceProjection(office);
422
402
  jsonRes(res, 200, {
@@ -436,9 +416,9 @@ function serveConversationState(res, url, services, token) {
436
416
  },
437
417
  });
438
418
  }
439
- function serveGlobalSettings(res) {
419
+ function serveGlobalSettings(res, workspace) {
440
420
  try {
441
- const config = loadGlobalSettings();
421
+ const config = loadGlobalSettings(workspace.stateDir);
442
422
  jsonRes(res, 200, {
443
423
  provider: config.provider,
444
424
  model: config.model,
@@ -457,9 +437,11 @@ function serveGlobalSettings(res) {
457
437
  jsonRes(res, 500, { error: errorMessage(err) });
458
438
  }
459
439
  }
460
- async function serveModelsList(res) {
440
+ async function serveModelsList(res, workspace) {
461
441
  try {
462
- const registry = MikanModels.create();
442
+ const registry = MikanModels.create({
443
+ modelsJsonPath: defaultModelsJsonPath(workspace.stateDir),
444
+ });
463
445
  const availableModels = await registry.getAvailable();
464
446
  const statuses = await resolveAdminModelAccessStatuses(registry, availableModels);
465
447
  const models = availableModels.map((model) => ({
@@ -478,24 +460,25 @@ async function serveModelsList(res) {
478
460
  jsonRes(res, 500, { error: errorMessage(err) });
479
461
  }
480
462
  }
481
- function serveConversationModelUpdate(res, body, services, token) {
463
+ function readModelSelection(res, body) {
482
464
  const provider = typeof body.provider === "string" ? body.provider.trim() : "";
483
465
  const model = typeof body.model === "string" ? body.model.trim() : "";
484
- const thinkingLevel = isThinkingLevel(body.thinkingLevel) ? body.thinkingLevel : undefined;
485
466
  if (!provider || !model) {
486
467
  jsonRes(res, 400, { error: "Missing provider or model" });
487
- return;
468
+ return undefined;
488
469
  }
489
- const target = requireConversationWorkspace(res, body, services, token);
490
- if (!target)
470
+ const thinkingLevel = isThinkingLevel(body.thinkingLevel) ? body.thinkingLevel : undefined;
471
+ return { provider, model, thinkingLevel };
472
+ }
473
+ function serveConversationModelUpdate(res, body, services, token) {
474
+ const selection = readModelSelection(res, body);
475
+ if (!selection)
476
+ return;
477
+ const office = scopedOffice(res, services, resolveTargetConversation(body, token));
478
+ if (!office)
491
479
  return;
492
- const { scope, workspace } = target;
493
480
  try {
494
- const result = applyConversationSettings(services.runtime, workspace.office(scope.address), {
495
- provider,
496
- model,
497
- thinkingLevel,
498
- });
481
+ const result = applyConversationSettings(services.runtime, office, selection);
499
482
  if (!result.ok) {
500
483
  jsonRes(res, 409, {
501
484
  error: "Conversation has a running job; retry after it finishes (or /stop it).",
@@ -513,12 +496,11 @@ function serveConversationVisibilityUpdate(res, body, services, token) {
513
496
  jsonRes(res, 400, { error: "visibility must be 'private' or 'default'" });
514
497
  return;
515
498
  }
516
- const target = requireConversationWorkspace(res, body, services, token);
517
- if (!target)
499
+ const office = scopedOffice(res, services, resolveTargetConversation(body, token));
500
+ if (!office)
518
501
  return;
519
- const { scope, workspace } = target;
520
502
  try {
521
- const result = applyOfficeVisibility(services.runtime, workspace.office(scope.address), body.visibility === "private" ? "private" : null);
503
+ const result = applyOfficeVisibility(services.runtime, office, body.visibility === "private" ? "private" : null);
522
504
  if (!result.ok) {
523
505
  jsonRes(res, 409, { error: "Conversation is busy; retry when the current run finishes" });
524
506
  return;
@@ -535,22 +517,20 @@ function serveConversationSlackUpdate(res, body, services, token) {
535
517
  jsonRes(res, 400, { error: "replyMode must be 'top-level' or 'thread'" });
536
518
  return;
537
519
  }
538
- const target = requireConversationWorkspace(res, body, services, token);
539
- if (!target)
520
+ const office = scopedOffice(res, services, resolveTargetConversation(body, token));
521
+ if (!office)
540
522
  return;
541
- const { scope, workspace } = target;
542
523
  respondWithSettingsUpdate(res, () => {
543
- applyConversationSettings(services.runtime, workspace.office(scope.address), {
524
+ applyConversationSettings(services.runtime, office, {
544
525
  slack: { replyMode },
545
526
  });
546
527
  return { ok: true };
547
528
  });
548
529
  }
549
530
  async function serveConversationSessionLink(res, body, services, token) {
550
- const target = requireConversationWorkspace(res, body, services, token);
551
- if (!target)
531
+ const office = scopedOffice(res, services, resolveTargetConversation(body, token));
532
+ if (!office)
552
533
  return;
553
- const { scope, workspace } = target;
554
534
  if (!services.sessionViewTokenStore) {
555
535
  jsonRes(res, 503, { error: "Session view token store not available" });
556
536
  return;
@@ -561,8 +541,7 @@ async function serveConversationSessionLink(res, body, services, token) {
561
541
  });
562
542
  return;
563
543
  }
564
- const office = workspace.office(scope.address);
565
- if (!(await SessionStore.exists(office, scope.conversationId))) {
544
+ if (!(await SessionStore.exists(office, office.address.conversationId))) {
566
545
  jsonRes(res, 404, { error: "No session found for this conversation" });
567
546
  return;
568
547
  }
@@ -570,8 +549,8 @@ async function serveConversationSessionLink(res, body, services, token) {
570
549
  const { token: viewToken } = services.sessionViewTokenStore.create({
571
550
  platform: token.platform,
572
551
  platformUserId: token.platformUserId,
573
- conversationId: scope.conversationId,
574
- sessionKey: scope.conversationId,
552
+ conversationId: office.address.conversationId,
553
+ sessionKey: office.address.conversationId,
575
554
  office,
576
555
  platformUserName: token.platformUserName,
577
556
  });
@@ -583,11 +562,9 @@ async function serveConversationSessionLink(res, body, services, token) {
583
562
  }
584
563
  }
585
564
  function serveConversationLoginLink(res, body, services, token) {
586
- const scope = resolveTargetConversation(body, token);
587
- if (scope.error) {
588
- jsonRes(res, 403, { error: scope.error });
565
+ const office = scopedOffice(res, services, resolveTargetConversation(body, token));
566
+ if (!office)
589
567
  return;
590
- }
591
568
  if (!services.portalBaseUrl) {
592
569
  jsonRes(res, 503, { error: "Portal URL not configured." });
593
570
  return;
@@ -610,7 +587,7 @@ function serveConversationLoginLink(res, body, services, token) {
610
587
  try {
611
588
  vaultId = credentialAuthorizationKey(services.sandbox, {
612
589
  userId: token.platformUserId,
613
- address: scope.address,
590
+ address: office.address,
614
591
  });
615
592
  }
616
593
  catch (err) {
@@ -619,7 +596,7 @@ function serveConversationLoginLink(res, body, services, token) {
619
596
  }
620
597
  }
621
598
  try {
622
- const { token: linkToken } = services.linkTokenStore.create(token.platform, token.platformUserId, scope.conversationId, vaultId, "");
599
+ const { token: linkToken } = services.linkTokenStore.create(token.platform, token.platformUserId, office.address.conversationId, vaultId, "");
623
600
  const url = `${services.portalBaseUrl}/link?token=${encodeURIComponent(linkToken)}`;
624
601
  jsonRes(res, 200, { ok: true, url, vaultId });
625
602
  }
@@ -628,19 +605,11 @@ function serveConversationLoginLink(res, body, services, token) {
628
605
  }
629
606
  }
630
607
  function serveGlobalModelUpdate(res, body, services) {
631
- const provider = typeof body.provider === "string" ? body.provider.trim() : "";
632
- const model = typeof body.model === "string" ? body.model.trim() : "";
633
- const thinkingLevel = isThinkingLevel(body.thinkingLevel) ? body.thinkingLevel : undefined;
634
- if (!provider || !model) {
635
- jsonRes(res, 400, { error: "Missing provider or model" });
608
+ const selection = readModelSelection(res, body);
609
+ if (!selection)
636
610
  return;
637
- }
638
611
  respondWithSettingsUpdate(res, () => {
639
- const result = applyGlobalSettings(services.runtime, {
640
- provider,
641
- model,
642
- thinkingLevel,
643
- });
612
+ const result = applyGlobalSettings(services.runtime, services.workspace.stateDir, selection);
644
613
  return {
645
614
  ok: true,
646
615
  staleConversations: result.staleConversations.map((office) => `${office.platform}:${office.conversationId}`),
@@ -654,7 +623,7 @@ function serveGlobalSlackUpdate(res, body, services) {
654
623
  return;
655
624
  }
656
625
  respondWithSettingsUpdate(res, () => {
657
- applyGlobalSettings(services.runtime, { slack: { replyMode } });
626
+ applyGlobalSettings(services.runtime, services.workspace.stateDir, { slack: { replyMode } });
658
627
  return { ok: true };
659
628
  });
660
629
  }
@@ -680,7 +649,7 @@ function serveGlobalSandboxUpdate(res, body, services) {
680
649
  return;
681
650
  }
682
651
  respondWithSettingsUpdate(res, () => {
683
- applyGlobalSettings(services.runtime, { sandbox: update });
652
+ applyGlobalSettings(services.runtime, services.workspace.stateDir, { sandbox: update });
684
653
  return { ok: true };
685
654
  });
686
655
  }
@@ -783,17 +752,12 @@ function buildTree(startDir, relPrefix) {
783
752
  return node;
784
753
  }
785
754
  function serveWorkspaceTree(res, url, services, token) {
786
- const scope = resolveConversationFromQuery(url, token);
787
- if (scope.error) {
788
- jsonRes(res, 403, { error: scope.error });
789
- return;
790
- }
791
- const workspace = requireAdminWorkspace(res, services);
792
- if (!workspace)
755
+ const office = scopedOffice(res, services, resolveConversationFromQuery(url, token));
756
+ if (!office)
793
757
  return;
794
- const convDir = workspace.office(scope.address).dir;
758
+ const convDir = office.dir;
795
759
  if (!existsSync(convDir)) {
796
- jsonRes(res, 200, { conversationId: scope.conversationId, tree: null });
760
+ jsonRes(res, 200, { conversationId: office.address.conversationId, tree: null });
797
761
  return;
798
762
  }
799
763
  const requestedSub = (url.searchParams.get("path") ?? "").trim();
@@ -808,7 +772,7 @@ function serveWorkspaceTree(res, url, services, token) {
808
772
  }
809
773
  const tree = buildTree(startSafe.absolute, requestedSub);
810
774
  jsonRes(res, 200, {
811
- conversationId: scope.conversationId,
775
+ conversationId: office.address.conversationId,
812
776
  root: requestedSub || ".",
813
777
  tree,
814
778
  });
@@ -877,13 +841,8 @@ function servePreviewFile(res, absolutePath, metadata, notFoundMessage) {
877
841
  });
878
842
  }
879
843
  function serveWorkspaceFile(res, url, services, token) {
880
- const scope = resolveConversationFromQuery(url, token);
881
- if (scope.error) {
882
- jsonRes(res, 403, { error: scope.error });
883
- return;
884
- }
885
- const workspace = requireAdminWorkspace(res, services);
886
- if (!workspace)
844
+ const office = scopedOffice(res, services, resolveConversationFromQuery(url, token));
845
+ if (!office)
887
846
  return;
888
847
  const requestedPath = (url.searchParams.get("path") ?? "").trim();
889
848
  if (!requestedPath) {
@@ -894,7 +853,7 @@ function serveWorkspaceFile(res, url, services, token) {
894
853
  jsonRes(res, 403, { error: "Workspace path is not exposed" });
895
854
  return;
896
855
  }
897
- const convDir = workspace.office(scope.address).dir;
856
+ const convDir = office.dir;
898
857
  const safe = safeJoinUnderRoot(convDir, requestedPath);
899
858
  if (safe.error) {
900
859
  jsonRes(res, 400, { error: safe.error });
@@ -903,17 +862,12 @@ function serveWorkspaceFile(res, url, services, token) {
903
862
  servePreviewFile(res, safe.absolute, { path: requestedPath }, "File not found");
904
863
  }
905
864
  function serveMcpServersList(res, url, services, token) {
906
- const scope = resolveConversationFromQuery(url, token);
907
- if (scope.error) {
908
- jsonRes(res, 403, { error: scope.error });
909
- return;
910
- }
911
- const workspace = requireAdminWorkspace(res, services);
912
- if (!workspace)
865
+ const office = scopedOffice(res, services, resolveConversationFromQuery(url, token));
866
+ if (!office)
913
867
  return;
914
- const servers = loadScopeMcpServers(workspace.office(scope.address));
868
+ const servers = loadScopeMcpServers(office);
915
869
  jsonRes(res, 200, {
916
- conversationId: scope.conversationId,
870
+ conversationId: office.address.conversationId,
917
871
  presets: listMcpPresets(),
918
872
  global: redactMcpServers(servers.global),
919
873
  conversation: redactMcpServers(servers.conversation),
@@ -1017,15 +971,10 @@ async function serveMcpServerMutation(res, body, services, token) {
1017
971
  return;
1018
972
  }
1019
973
  const mutationScope = body.scope === "global" ? "global" : "conversation";
1020
- const scope = resolveTargetConversation(body, token);
1021
- if (scope.error) {
1022
- jsonRes(res, 403, { error: scope.error });
1023
- return;
1024
- }
1025
- const workspace = requireAdminWorkspace(res, services);
1026
- if (!workspace)
974
+ const office = scopedOffice(res, services, resolveTargetConversation(body, token));
975
+ if (!office)
1027
976
  return;
1028
- const office = workspace.office(scope.address);
977
+ const { workspace } = services;
1029
978
  const maps = loadScopeMcpServers(office);
1030
979
  const current = mutationScope === "global" ? maps.global : maps.conversation;
1031
980
  if (action === "test") {
@@ -1045,7 +994,7 @@ async function serveMcpServerMutation(res, body, services, token) {
1045
994
  return;
1046
995
  }
1047
996
  const result = mutationScope === "global"
1048
- ? applyGlobalSettings(services.runtime, { mcpServers: plan.next })
997
+ ? applyGlobalSettings(services.runtime, workspace.stateDir, { mcpServers: plan.next })
1049
998
  : applyConversationSettings(services.runtime, office, { mcpServers: plan.next });
1050
999
  if (!result.ok) {
1051
1000
  jsonRes(res, 409, { error: "Conversation is busy; try again shortly." });
@@ -1105,36 +1054,27 @@ function officeSkillListing(workspace, office) {
1105
1054
  };
1106
1055
  }
1107
1056
  function serveSkillsList(res, url, services, token) {
1108
- const scope = resolveConversationFromQuery(url, token);
1109
- if (scope.error) {
1110
- jsonRes(res, 403, { error: scope.error });
1057
+ const office = scopedOffice(res, services, resolveConversationFromQuery(url, token));
1058
+ if (!office)
1111
1059
  return;
1112
- }
1113
- const workspace = requireAdminWorkspace(res, services);
1114
- if (!workspace)
1115
- return;
1116
- jsonRes(res, 200, officeSkillListing(workspace, workspace.office(scope.address)));
1060
+ const { workspace } = services;
1061
+ jsonRes(res, 200, officeSkillListing(workspace, office));
1117
1062
  }
1118
1063
  const SKILL_TOGGLE_STATES = {
1119
1064
  global: ["load", "unload"],
1120
1065
  conversation: ["inherit", "load", "unload"],
1121
1066
  };
1122
1067
  function serveSkillToggle(res, body, services, token) {
1123
- const scope = resolveTargetConversation(body, token);
1124
- if (scope.error) {
1125
- jsonRes(res, 403, { error: scope.error });
1126
- return;
1127
- }
1128
- const workspace = requireAdminWorkspace(res, services);
1129
- if (!workspace)
1068
+ const office = scopedOffice(res, services, resolveTargetConversation(body, token));
1069
+ if (!office)
1130
1070
  return;
1071
+ const { workspace } = services;
1131
1072
  const target = body.scope === "global" || body.scope === "conversation" ? body.scope : undefined;
1132
1073
  const state = typeof body.state === "string" ? body.state : "";
1133
1074
  if (!target || !SKILL_TOGGLE_STATES[target].includes(state)) {
1134
1075
  jsonRes(res, 400, { error: "Invalid scope or state" });
1135
1076
  return;
1136
1077
  }
1137
- const office = workspace.office(scope.address);
1138
1078
  const skill = officeSkillListing(workspace, office).skills.find((entry) => entry.source === body.source && entry.directory === body.directory);
1139
1079
  if (!skill) {
1140
1080
  jsonRes(res, 404, { error: "Skill not found" });
@@ -1143,18 +1083,14 @@ function serveSkillToggle(res, body, services, token) {
1143
1083
  const patterns = loadSkillPatterns(office)[target].filter((pattern) => pattern.slice(1) !== skill.directory);
1144
1084
  if (state !== "inherit")
1145
1085
  patterns.push(`${state === "load" ? "+" : "-"}${skill.directory}`);
1146
- updateSkillPatterns(target === "global" ? undefined : office, patterns);
1086
+ updateSkillPatterns(office, target, patterns);
1147
1087
  jsonRes(res, 200, officeSkillListing(workspace, office));
1148
1088
  }
1149
1089
  function serveSkillFile(res, url, services, token) {
1150
- const scope = resolveConversationFromQuery(url, token);
1151
- if (scope.error) {
1152
- jsonRes(res, 403, { error: scope.error });
1153
- return;
1154
- }
1155
- const workspace = requireAdminWorkspace(res, services);
1156
- if (!workspace)
1090
+ const office = scopedOffice(res, services, resolveConversationFromQuery(url, token));
1091
+ if (!office)
1157
1092
  return;
1093
+ const { workspace } = services;
1158
1094
  const source = (url.searchParams.get("source") ?? "").trim();
1159
1095
  const directory = (url.searchParams.get("directory") ?? "").trim();
1160
1096
  if (source !== "global" && source !== "conversation") {
@@ -1168,7 +1104,7 @@ function serveSkillFile(res, url, services, token) {
1168
1104
  jsonRes(res, 400, { error: "Invalid skill directory" });
1169
1105
  return;
1170
1106
  }
1171
- const skillsRoot = source === "global" ? workspace.skillsDir : workspace.office(scope.address).skillsDir;
1107
+ const skillsRoot = source === "global" ? workspace.skillsDir : office.skillsDir;
1172
1108
  const safe = safeJoinUnderRoot(skillsRoot, join(directory, "SKILL.md"));
1173
1109
  if (safe.error) {
1174
1110
  jsonRes(res, 400, { error: safe.error });
@@ -1177,12 +1113,12 @@ function serveSkillFile(res, url, services, token) {
1177
1113
  servePreviewFile(res, safe.absolute, { source, directory }, "Skill file not found");
1178
1114
  }
1179
1115
  const SKILL_DIRECTORY_PATTERN = /^[a-z0-9-]+$/;
1180
- function resolveSkillsRoot(workspace, scope, source) {
1116
+ function resolveSkillsRoot(office, source) {
1181
1117
  if (source !== "global" && source !== "conversation") {
1182
1118
  return { error: "Invalid skill source" };
1183
1119
  }
1184
1120
  return {
1185
- root: source === "global" ? workspace.skillsDir : workspace.office(scope.address).skillsDir,
1121
+ root: source === "global" ? office.workspace.skillsDir : office.skillsDir,
1186
1122
  source,
1187
1123
  };
1188
1124
  }
@@ -1192,15 +1128,10 @@ async function serveSkillMutation(res, body, services, token) {
1192
1128
  jsonRes(res, 400, { error: "action must be 'save' or 'delete'" });
1193
1129
  return;
1194
1130
  }
1195
- const scope = resolveTargetConversation(body, token);
1196
- if (scope.error) {
1197
- jsonRes(res, 403, { error: scope.error });
1198
- return;
1199
- }
1200
- const workspace = requireAdminWorkspace(res, services);
1201
- if (!workspace)
1131
+ const office = scopedOffice(res, services, resolveTargetConversation(body, token));
1132
+ if (!office)
1202
1133
  return;
1203
- const resolved = resolveSkillsRoot(workspace, scope, body.source);
1134
+ const resolved = resolveSkillsRoot(office, body.source);
1204
1135
  if ("error" in resolved) {
1205
1136
  jsonRes(res, 400, { error: resolved.error });
1206
1137
  return;
@@ -1286,9 +1217,7 @@ function requireAdminEventStore(res, services, office) {
1286
1217
  return services.eventStore(office);
1287
1218
  }
1288
1219
  async function serveEventsList(res, services) {
1289
- const workspace = requireAdminWorkspace(res, services);
1290
- if (!workspace)
1291
- return;
1220
+ const { workspace } = services;
1292
1221
  if (!services.eventStore) {
1293
1222
  jsonRes(res, 503, { error: "Working directory not available" });
1294
1223
  return;
@@ -1300,28 +1229,21 @@ async function serveEventsList(res, services) {
1300
1229
  jsonRes(res, 200, { events });
1301
1230
  }
1302
1231
  async function serveConversationEventsList(res, url, services, token) {
1303
- const scope = resolveConversationFromQuery(url, token);
1304
- if (scope.error) {
1305
- jsonRes(res, 403, { error: scope.error });
1232
+ const office = scopedOffice(res, services, resolveConversationFromQuery(url, token));
1233
+ if (!office)
1306
1234
  return;
1307
- }
1308
- const workspace = requireAdminWorkspace(res, services);
1309
- if (!workspace)
1310
- return;
1311
- const store = requireAdminEventStore(res, services, workspace.office(scope.address));
1235
+ const store = requireAdminEventStore(res, services, office);
1312
1236
  if (!store)
1313
1237
  return;
1314
1238
  jsonRes(res, 200, {
1315
- conversationId: scope.conversationId,
1239
+ conversationId: office.address.conversationId,
1316
1240
  events: await listOfficeEvents(store),
1317
1241
  });
1318
1242
  }
1319
1243
  async function serveConversationEventDelete(res, body, services, token) {
1320
- const scope = resolveTargetConversation(body, token);
1321
- if (scope.error) {
1322
- jsonRes(res, 403, { error: scope.error });
1244
+ const office = scopedOffice(res, services, resolveTargetConversation(body, token));
1245
+ if (!office)
1323
1246
  return;
1324
- }
1325
1247
  let name;
1326
1248
  try {
1327
1249
  name = validateEventFilename(typeof body.name === "string" ? body.name : "");
@@ -1330,10 +1252,7 @@ async function serveConversationEventDelete(res, body, services, token) {
1330
1252
  jsonRes(res, 400, { error: "Invalid name" });
1331
1253
  return;
1332
1254
  }
1333
- const workspace = requireAdminWorkspace(res, services);
1334
- if (!workspace)
1335
- return;
1336
- const store = requireAdminEventStore(res, services, workspace.office(scope.address));
1255
+ const store = requireAdminEventStore(res, services, office);
1337
1256
  if (!store)
1338
1257
  return;
1339
1258
  try {