@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2

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 (174) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/auth.d.ts +2 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +401 -314
  5. package/dist/auth.js.map +1 -1
  6. package/dist/capabilities/connection.js +1 -1
  7. package/dist/capabilities/intake.js +1 -1
  8. package/dist/capabilities/version-control.js +1 -1
  9. package/dist/factory.d.ts.map +1 -1
  10. package/dist/factory.js +441 -39455
  11. package/dist/factory.js.map +1 -1
  12. package/dist/index.js +2 -39466
  13. package/dist/integrations/base.js +1 -1
  14. package/dist/integrations/github/config.js +36 -22
  15. package/dist/integrations/github/config.js.map +1 -1
  16. package/dist/integrations/github/factory-session.js +33 -37
  17. package/dist/integrations/github/factory-session.js.map +1 -1
  18. package/dist/integrations/github/integration.js +1037 -3624
  19. package/dist/integrations/github/integration.js.map +1 -1
  20. package/dist/integrations/github/issue-triage.js +71 -71
  21. package/dist/integrations/github/issue-triage.js.map +1 -1
  22. package/dist/integrations/github/pat.js +47 -36
  23. package/dist/integrations/github/pat.js.map +1 -1
  24. package/dist/integrations/github/provenance.js +72 -101
  25. package/dist/integrations/github/provenance.js.map +1 -1
  26. package/dist/integrations/github/routes.d.ts.map +1 -1
  27. package/dist/integrations/github/routes.js +1272 -1833
  28. package/dist/integrations/github/routes.js.map +1 -1
  29. package/dist/integrations/github/rules.js +203 -471
  30. package/dist/integrations/github/rules.js.map +1 -1
  31. package/dist/integrations/github/sandbox.d.ts +2 -0
  32. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  33. package/dist/integrations/github/sandbox.js +401 -326
  34. package/dist/integrations/github/sandbox.js.map +1 -1
  35. package/dist/integrations/github/session-subscriptions.js +165 -223
  36. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  37. package/dist/integrations/github/subscriptions.js +43 -51
  38. package/dist/integrations/github/subscriptions.js.map +1 -1
  39. package/dist/integrations/github/token-refresh.js +15 -18
  40. package/dist/integrations/github/token-refresh.js.map +1 -1
  41. package/dist/integrations/github/webhook.js +359 -343
  42. package/dist/integrations/github/webhook.js.map +1 -1
  43. package/dist/integrations/linear/agent-tools.js +77 -100
  44. package/dist/integrations/linear/agent-tools.js.map +1 -1
  45. package/dist/integrations/linear/integration.js +611 -1309
  46. package/dist/integrations/linear/integration.js.map +1 -1
  47. package/dist/integrations/linear/routes.js +227 -230
  48. package/dist/integrations/linear/routes.js.map +1 -1
  49. package/dist/integrations/linear/rules.js +126 -346
  50. package/dist/integrations/linear/rules.js.map +1 -1
  51. package/dist/integrations/linear/storage.js +1 -1
  52. package/dist/integrations/platform/api-client.js +131 -135
  53. package/dist/integrations/platform/api-client.js.map +1 -1
  54. package/dist/integrations/platform/github/event-worker.js +275 -573
  55. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  56. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  57. package/dist/integrations/platform/github/integration.js +845 -3942
  58. package/dist/integrations/platform/github/integration.js.map +1 -1
  59. package/dist/integrations/platform/linear/integration.js +457 -1309
  60. package/dist/integrations/platform/linear/integration.js.map +1 -1
  61. package/dist/integrations/workos/integration.js +80 -85
  62. package/dist/integrations/workos/integration.js.map +1 -1
  63. package/dist/routes/config.d.ts.map +1 -1
  64. package/dist/routes/config.js +933 -915
  65. package/dist/routes/config.js.map +1 -1
  66. package/dist/routes/custom-provider-source.js +110 -93
  67. package/dist/routes/custom-provider-source.js.map +1 -1
  68. package/dist/routes/fs.js +401 -349
  69. package/dist/routes/fs.js.map +1 -1
  70. package/dist/routes/intake.js +165 -165
  71. package/dist/routes/intake.js.map +1 -1
  72. package/dist/routes/oauth.js +447 -25166
  73. package/dist/routes/oauth.js.map +1 -1
  74. package/dist/routes/projects.js +437 -376
  75. package/dist/routes/projects.js.map +1 -1
  76. package/dist/routes/provider-credentials.js +76 -58
  77. package/dist/routes/provider-credentials.js.map +1 -1
  78. package/dist/routes/route.js +12 -8
  79. package/dist/routes/route.js.map +1 -1
  80. package/dist/routes/skills.js +160 -173
  81. package/dist/routes/skills.js.map +1 -1
  82. package/dist/routes/surface.js +250 -28618
  83. package/dist/routes/surface.js.map +1 -1
  84. package/dist/routes/tenant-credentials.js +141 -166
  85. package/dist/routes/tenant-credentials.js.map +1 -1
  86. package/dist/routes/work-items.js +654 -862
  87. package/dist/routes/work-items.js.map +1 -1
  88. package/dist/rules/binding-context.js +23 -35
  89. package/dist/rules/binding-context.js.map +1 -1
  90. package/dist/rules/defaults.js +200 -309
  91. package/dist/rules/defaults.js.map +1 -1
  92. package/dist/rules/dispatcher.d.ts.map +1 -1
  93. package/dist/rules/dispatcher.js +438 -677
  94. package/dist/rules/dispatcher.js.map +1 -1
  95. package/dist/rules/index.js +5 -599
  96. package/dist/rules/processor.js +344 -596
  97. package/dist/rules/processor.js.map +1 -1
  98. package/dist/rules/resolve.js +21 -18
  99. package/dist/rules/resolve.js.map +1 -1
  100. package/dist/rules/start-coordinator.js +168 -157
  101. package/dist/rules/start-coordinator.js.map +1 -1
  102. package/dist/rules/tools.js +50 -85
  103. package/dist/rules/tools.js.map +1 -1
  104. package/dist/rules/transition-service.js +183 -454
  105. package/dist/rules/transition-service.js.map +1 -1
  106. package/dist/rules/types.js +33 -29
  107. package/dist/rules/types.js.map +1 -1
  108. package/dist/rules/validation.js +266 -281
  109. package/dist/rules/validation.js.map +1 -1
  110. package/dist/sandbox/fleet.js +272 -250
  111. package/dist/sandbox/fleet.js.map +1 -1
  112. package/dist/sandbox/reattach.js +13 -6
  113. package/dist/sandbox/reattach.js.map +1 -1
  114. package/dist/server-error.js +12 -16
  115. package/dist/server-error.js.map +1 -1
  116. package/dist/skills/service.js +30 -35
  117. package/dist/skills/service.js.map +1 -1
  118. package/dist/spa-static.d.ts.map +1 -1
  119. package/dist/spa-static.js +91 -64
  120. package/dist/spa-static.js.map +1 -1
  121. package/dist/state-signing.js +73 -44
  122. package/dist/state-signing.js.map +1 -1
  123. package/dist/storage/domains/audit/agent-audit.js +60 -59
  124. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  125. package/dist/storage/domains/audit/base.js +169 -112
  126. package/dist/storage/domains/audit/base.js.map +1 -1
  127. package/dist/storage/domains/audit/domain.js +155 -153
  128. package/dist/storage/domains/audit/domain.js.map +1 -1
  129. package/dist/storage/domains/credentials/base.js +259 -243
  130. package/dist/storage/domains/credentials/base.js.map +1 -1
  131. package/dist/storage/domains/custom-providers/base.js +138 -143
  132. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  133. package/dist/storage/domains/intake/base.js +75 -57
  134. package/dist/storage/domains/intake/base.js.map +1 -1
  135. package/dist/storage/domains/integrations/base.js +310 -245
  136. package/dist/storage/domains/integrations/base.js.map +1 -1
  137. package/dist/storage/domains/memory-settings/base.js +117 -102
  138. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  139. package/dist/storage/domains/model-packs/base.js +89 -88
  140. package/dist/storage/domains/model-packs/base.js.map +1 -1
  141. package/dist/storage/domains/projects/base.js +97 -92
  142. package/dist/storage/domains/projects/base.js.map +1 -1
  143. package/dist/storage/domains/queue-health/base.js +111 -76
  144. package/dist/storage/domains/queue-health/base.js.map +1 -1
  145. package/dist/storage/domains/source-control/base.d.ts +9 -0
  146. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  147. package/dist/storage/domains/source-control/base.js +777 -702
  148. package/dist/storage/domains/source-control/base.js.map +1 -1
  149. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  150. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  151. package/dist/storage/domains/source-control/inmemory.js +322 -344
  152. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  153. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  154. package/dist/storage/domains/work-items/base.js +1266 -1132
  155. package/dist/storage/domains/work-items/base.js.map +1 -1
  156. package/dist/storage/domains/work-items/metrics.js +202 -184
  157. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  158. package/dist/timing.d.ts +15 -0
  159. package/dist/timing.d.ts.map +1 -0
  160. package/dist/timing.js +34 -0
  161. package/dist/timing.js.map +1 -0
  162. package/dist/workspace.d.ts +2 -3
  163. package/dist/workspace.d.ts.map +1 -1
  164. package/dist/workspace.js +236 -438
  165. package/dist/workspace.js.map +1 -1
  166. package/factory-skills/factory-review/SKILL.md +9 -2
  167. package/package.json +9 -9
  168. package/dist/capabilities/connection.js.map +0 -1
  169. package/dist/capabilities/intake.js.map +0 -1
  170. package/dist/capabilities/version-control.js.map +0 -1
  171. package/dist/index.js.map +0 -1
  172. package/dist/integrations/base.js.map +0 -1
  173. package/dist/integrations/linear/storage.js.map +0 -1
  174. package/dist/rules/index.js.map +0 -1
@@ -1,946 +1,964 @@
1
- // src/routes/config.ts
2
- import { DEFAULT_OM_MODEL_ID } from "@mastra/code-sdk/constants";
3
- import { getAvailableModePacks } from "@mastra/code-sdk/onboarding/packs";
4
- import { getCustomProviderId, THREAD_ACTIVE_MODEL_PACK_ID_KEY } from "@mastra/code-sdk/onboarding/settings";
1
+ import { WEB_OAUTH_FLOW_KINDS, getAuthProviderId, listTenantCredentialsForRequest, resolveCredentialContext, tenantOrgId } from "./provider-credentials.js";
2
+ import { Route } from "./route.js";
5
3
  import { registerApiRoute } from "@mastra/core/server";
6
-
7
- // src/routes/provider-credentials.ts
8
- function getAuthProviderId(provider) {
9
- return provider === "openai" ? "openai-codex" : provider;
10
- }
11
- var WEB_OAUTH_FLOW_KINDS = {
12
- anthropic: "paste-code",
13
- openai: "device-code",
14
- "github-copilot": "device-code",
15
- xai: "device-code"
16
- };
17
- async function getTenantCredentialsStorage(credentials) {
18
- if (!credentials) return void 0;
19
- try {
20
- await credentials.ensureReady();
21
- } catch {
22
- return void 0;
23
- }
24
- return credentials;
25
- }
26
- function tenantOrgId(tenant) {
27
- return tenant.orgId ?? `user:${tenant.userId}`;
28
- }
29
- async function resolveCredentialContext({
30
- c,
31
- auth,
32
- credentials
33
- }) {
34
- await auth.ensureUser(c);
35
- const tenant = auth.tenant(c);
36
- if (!tenant) {
37
- if (auth.enabled()) return { response: c.json({ error: "unauthorized" }, 401) };
38
- return { mode: "local" };
39
- }
40
- const storage = await getTenantCredentialsStorage(credentials);
41
- if (!storage) {
42
- return {
43
- response: c.json(
44
- {
45
- error: "credentials_unavailable",
46
- message: "Tenant credential storage is unavailable \u2014 the app database is not configured or failed to start."
47
- },
48
- 503
49
- )
50
- };
51
- }
52
- return { mode: "tenant", storage, orgId: tenantOrgId(tenant), userId: tenant.userId };
53
- }
54
- async function listTenantCredentialsForRequest({
55
- c,
56
- auth,
57
- credentials
58
- }) {
59
- await auth.ensureUser(c);
60
- const tenant = auth.tenant(c);
61
- if (!tenant) return auth.enabled() ? [] : void 0;
62
- const storage = await getTenantCredentialsStorage(credentials);
63
- if (!storage) return [];
64
- return storage.listCredentials(tenantOrgId(tenant), tenant.userId);
65
- }
66
-
67
- // src/routes/route.ts
68
- var Route = class {
69
- deps;
70
- constructor(deps) {
71
- this.deps = deps;
72
- }
73
- };
74
-
75
- // src/routes/config.ts
4
+ import { DEFAULT_OM_MODEL_ID } from "@mastra/code-sdk/constants";
5
+ import { getAvailableModePacks, resolveProviderOMDefault } from "@mastra/code-sdk/onboarding/packs";
6
+ import { THREAD_ACTIVE_MODEL_PACK_ID_KEY, getCustomProviderId } from "@mastra/code-sdk/onboarding/settings";
7
+ //#region src/routes/config.ts
8
+ /** Widen a route-local Hono context to the plain `Context` the auth helpers take. */
76
9
  function loose(c) {
77
- return c;
10
+ return c;
78
11
  }
79
- async function listProviders({
80
- controller,
81
- authStorage,
82
- tenantCredentials
83
- }) {
84
- const models = await controller.listAvailableModels();
85
- const seen = /* @__PURE__ */ new Map();
86
- for (const model of models) {
87
- if (seen.has(model.provider)) continue;
88
- const authProviderId = getAuthProviderId(model.provider);
89
- let source = "none";
90
- if (tenantCredentials) {
91
- const userRec = tenantCredentials.find((r) => r.scope === "user" && r.provider === authProviderId);
92
- const orgRec = tenantCredentials.find((r) => r.scope === "org" && r.provider === authProviderId);
93
- if (userRec?.credential.type === "oauth") {
94
- source = "oauth-user";
95
- } else if (userRec?.credential.type === "api_key") {
96
- source = "stored-user";
97
- } else if (orgRec?.credential.type === "api_key") {
98
- source = "stored-org";
99
- }
100
- } else if (authStorage?.isLoggedIn(authProviderId)) {
101
- source = "oauth";
102
- } else if (authStorage?.hasStoredApiKey(model.provider)) {
103
- source = "stored";
104
- } else if (model.apiKeyEnvVar && process.env[model.apiKeyEnvVar]) {
105
- source = "env";
106
- } else if (model.hasApiKey) {
107
- source = "env";
108
- }
109
- const flowKind = WEB_OAUTH_FLOW_KINDS[model.provider];
110
- seen.set(model.provider, {
111
- provider: model.provider,
112
- envVar: model.apiKeyEnvVar,
113
- source,
114
- ...flowKind ? { oauth: { supported: true, modes: [flowKind] } } : {}
115
- });
116
- }
117
- return Array.from(seen.values()).sort((a, b) => a.provider.localeCompare(b.provider));
12
+ /**
13
+ * Build a deduplicated, sorted list of providers from the model catalog,
14
+ * annotated with where each provider's credential currently comes from.
15
+ * Mirrors the TUI's `/api-keys` provider list.
16
+ *
17
+ * When `tenantCredentials` is given (deployed mode), sources reflect the
18
+ * *caller's* tenant rows with user > org precedence and the server-global
19
+ * `authStorage` is ignored; otherwise the local `auth.json` view is reported.
20
+ */
21
+ async function listProviders({ controller, authStorage, tenantCredentials }) {
22
+ const models = await controller.listAvailableModels();
23
+ const seen = /* @__PURE__ */ new Map();
24
+ for (const model of models) {
25
+ if (seen.has(model.provider)) continue;
26
+ const authProviderId = getAuthProviderId(model.provider);
27
+ let source = "none";
28
+ if (tenantCredentials) {
29
+ const userRec = tenantCredentials.find((r) => r.scope === "user" && r.provider === authProviderId);
30
+ const orgRec = tenantCredentials.find((r) => r.scope === "org" && r.provider === authProviderId);
31
+ if (userRec?.credential.type === "oauth") source = "oauth-user";
32
+ else if (userRec?.credential.type === "api_key") source = "stored-user";
33
+ else if (orgRec?.credential.type === "api_key") source = "stored-org";
34
+ } else if (authStorage?.isLoggedIn(authProviderId)) source = "oauth";
35
+ else if (authStorage?.hasStoredApiKey(model.provider)) source = "stored";
36
+ else if (model.apiKeyEnvVar && process.env[model.apiKeyEnvVar]) source = "env";
37
+ else if (model.hasApiKey) source = "env";
38
+ const flowKind = WEB_OAUTH_FLOW_KINDS[model.provider];
39
+ seen.set(model.provider, {
40
+ provider: model.provider,
41
+ envVar: model.apiKeyEnvVar,
42
+ source,
43
+ ...flowKind ? { oauth: {
44
+ supported: true,
45
+ modes: [flowKind]
46
+ } } : {}
47
+ });
48
+ }
49
+ return Array.from(seen.values()).sort((a, b) => a.provider.localeCompare(b.provider));
118
50
  }
51
+ /** Redact a stored custom-provider row for the client (key presence only). */
119
52
  function toCustomProviderInfo(record) {
120
- return {
121
- id: record.providerId,
122
- name: record.name,
123
- url: record.url,
124
- hasApiKey: Boolean(record.apiKey),
125
- models: record.models
126
- };
53
+ return {
54
+ id: record.providerId,
55
+ name: record.name,
56
+ url: record.url,
57
+ hasApiKey: Boolean(record.apiKey),
58
+ models: record.models
59
+ };
127
60
  }
128
- async function resolveCustomProvidersContext({
129
- c,
130
- auth,
131
- customProviders
132
- }) {
133
- await auth.ensureUser(c);
134
- const tenant = auth.tenant(c);
135
- if (!tenant && auth.enabled()) return { response: c.json({ error: "unauthorized" }, 401) };
136
- if (customProviders) {
137
- try {
138
- await customProviders.ensureReady();
139
- return tenant ? { storage: customProviders, orgId: tenantOrgId(tenant), userId: tenant.userId } : { storage: customProviders, orgId: "local", userId: "local" };
140
- } catch {
141
- }
142
- }
143
- return {
144
- response: c.json(
145
- {
146
- error: "custom_providers_unavailable",
147
- message: "Custom provider storage is unavailable \u2014 the app database is not configured or failed to start."
148
- },
149
- 503
150
- )
151
- };
61
+ /**
62
+ * Resolve the custom-providers context for a request, or a ready-to-return
63
+ * error response. Same posture as memory settings: tenant rows in deployed
64
+ * mode, a sentinel `local` org in no-auth mode — never settings.json.
65
+ */
66
+ async function resolveCustomProvidersContext({ c, auth, customProviders }) {
67
+ await auth.ensureUser(c);
68
+ const tenant = auth.tenant(c);
69
+ if (!tenant && auth.enabled()) return { response: c.json({ error: "unauthorized" }, 401) };
70
+ if (customProviders) try {
71
+ await customProviders.ensureReady();
72
+ return tenant ? {
73
+ storage: customProviders,
74
+ orgId: tenantOrgId(tenant),
75
+ userId: tenant.userId
76
+ } : {
77
+ storage: customProviders,
78
+ orgId: "local",
79
+ userId: "local"
80
+ };
81
+ } catch {}
82
+ return { response: c.json({
83
+ error: "custom_providers_unavailable",
84
+ message: "Custom provider storage is unavailable — the app database is not configured or failed to start."
85
+ }, 503) };
152
86
  }
87
+ /** Validate + coerce a request body into a CustomProviderSetting. */
153
88
  function parseCustomProviderBody(body) {
154
- if (!body || typeof body !== "object") return { error: "Invalid JSON body" };
155
- const b = body;
156
- const name = typeof b.name === "string" ? b.name.trim() : "";
157
- if (!name) return { error: "Missing required field: name" };
158
- const url = typeof b.url === "string" ? b.url.trim() : "";
159
- if (!url) return { error: "Missing required field: url" };
160
- try {
161
- const parsed = new URL(url);
162
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
163
- return { error: "url must be an http(s) URL" };
164
- }
165
- } catch {
166
- return { error: "url must be a valid URL" };
167
- }
168
- const apiKey = typeof b.apiKey === "string" && b.apiKey.trim() ? b.apiKey.trim() : void 0;
169
- const models = Array.isArray(b.models) ? b.models.filter((m) => typeof m === "string" && m.trim().length > 0).map((m) => m.trim()) : [];
170
- return { name, url, apiKey, models };
89
+ if (!body || typeof body !== "object") return { error: "Invalid JSON body" };
90
+ const b = body;
91
+ const name = typeof b.name === "string" ? b.name.trim() : "";
92
+ if (!name) return { error: "Missing required field: name" };
93
+ const url = typeof b.url === "string" ? b.url.trim() : "";
94
+ if (!url) return { error: "Missing required field: url" };
95
+ try {
96
+ const parsed = new URL(url);
97
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return { error: "url must be an http(s) URL" };
98
+ } catch {
99
+ return { error: "url must be a valid URL" };
100
+ }
101
+ return {
102
+ name,
103
+ url,
104
+ apiKey: typeof b.apiKey === "string" && b.apiKey.trim() ? b.apiKey.trim() : void 0,
105
+ models: Array.isArray(b.models) ? b.models.filter((m) => typeof m === "string" && m.trim().length > 0).map((m) => m.trim()) : []
106
+ };
171
107
  }
172
- async function buildProviderAccess({
173
- controller,
174
- authStorage,
175
- tenantCredentials
176
- }) {
177
- const models = await controller.listAvailableModels();
178
- const hasModelKey = (provider) => models.some((m) => m.provider === provider && m.hasApiKey);
179
- const accessLevel = (provider) => {
180
- const authProviderId = getAuthProviderId(provider);
181
- if (tenantCredentials) {
182
- const userRec = tenantCredentials.find((r) => r.scope === "user" && r.provider === authProviderId);
183
- const orgRec = tenantCredentials.find((r) => r.scope === "org" && r.provider === authProviderId);
184
- const credential = userRec?.credential ?? orgRec?.credential;
185
- if (credential?.type === "oauth") return "oauth";
186
- if (credential?.type === "api_key" && credential.key.trim().length > 0) return "apikey";
187
- return false;
188
- }
189
- const oauthCredential = authStorage?.get(authProviderId);
190
- if (oauthCredential?.type === "oauth") return "oauth";
191
- if (authStorage?.hasStoredApiKey(provider)) return "apikey";
192
- const directCredential = authStorage?.get(provider);
193
- if (directCredential?.type === "api_key" && directCredential.key.trim().length > 0) return "apikey";
194
- return hasModelKey(provider) ? "apikey" : false;
195
- };
196
- const access = {
197
- anthropic: accessLevel("anthropic"),
198
- openai: accessLevel("openai"),
199
- cerebras: accessLevel("cerebras"),
200
- google: accessLevel("google"),
201
- deepseek: accessLevel("deepseek"),
202
- "github-copilot": accessLevel("github-copilot")
203
- };
204
- const seen = new Set(Object.keys(access));
205
- for (const m of models) {
206
- if (!seen.has(m.provider)) {
207
- access[m.provider] = accessLevel(m.provider);
208
- seen.add(m.provider);
209
- }
210
- }
211
- return access;
108
+ /**
109
+ * Compute which providers the user can reach, mirroring the TUI's
110
+ * `/models-pack` access derivation: OAuth/api-key from the credential store for
111
+ * the named providers, plus any other provider that has a usable key.
112
+ */
113
+ async function buildProviderAccess({ controller, authStorage, tenantCredentials }) {
114
+ const models = await controller.listAvailableModels();
115
+ const hasModelKey = (provider) => models.some((m) => m.provider === provider && m.hasApiKey);
116
+ const accessLevel = (provider) => {
117
+ const authProviderId = getAuthProviderId(provider);
118
+ if (tenantCredentials) {
119
+ const userRec = tenantCredentials.find((r) => r.scope === "user" && r.provider === authProviderId);
120
+ const orgRec = tenantCredentials.find((r) => r.scope === "org" && r.provider === authProviderId);
121
+ const credential = userRec?.credential ?? orgRec?.credential;
122
+ if (credential?.type === "oauth") return "oauth";
123
+ if (credential?.type === "api_key" && credential.key.trim().length > 0) return "apikey";
124
+ return false;
125
+ }
126
+ if ((authStorage?.get(authProviderId))?.type === "oauth") return "oauth";
127
+ if (authStorage?.hasStoredApiKey(provider)) return "apikey";
128
+ const directCredential = authStorage?.get(provider);
129
+ if (directCredential?.type === "api_key" && directCredential.key.trim().length > 0) return "apikey";
130
+ return hasModelKey(provider) ? "apikey" : false;
131
+ };
132
+ const access = {
133
+ anthropic: accessLevel("anthropic"),
134
+ openai: accessLevel("openai"),
135
+ cerebras: accessLevel("cerebras"),
136
+ google: accessLevel("google"),
137
+ deepseek: accessLevel("deepseek"),
138
+ "github-copilot": accessLevel("github-copilot")
139
+ };
140
+ const seen = new Set(Object.keys(access));
141
+ for (const m of models) if (!seen.has(m.provider)) {
142
+ access[m.provider] = accessLevel(m.provider);
143
+ seen.add(m.provider);
144
+ }
145
+ return access;
212
146
  }
213
147
  function canUseModelProvider(access, provider) {
214
- return Boolean(access[provider]);
148
+ return Boolean(access[provider]);
215
149
  }
216
- async function resolvePackContext({
217
- c,
218
- auth,
219
- modelPacks
220
- }) {
221
- await auth.ensureUser(c);
222
- const tenant = auth.tenant(c);
223
- if (!tenant && auth.enabled()) return { response: c.json({ error: "unauthorized" }, 401) };
224
- if (modelPacks) {
225
- try {
226
- await modelPacks.ensureReady();
227
- return tenant ? { storage: modelPacks, orgId: tenantOrgId(tenant), userId: tenant.userId } : { storage: modelPacks, orgId: "local", userId: "local" };
228
- } catch {
229
- }
230
- }
231
- return {
232
- response: c.json(
233
- {
234
- error: "model_packs_unavailable",
235
- message: "Model pack storage is unavailable \u2014 the app database is not configured or failed to start."
236
- },
237
- 503
238
- )
239
- };
150
+ /** Resolve the pack context for a request, or a ready-to-return error response. */
151
+ async function resolvePackContext({ c, auth, modelPacks }) {
152
+ await auth.ensureUser(c);
153
+ const tenant = auth.tenant(c);
154
+ if (!tenant && auth.enabled()) return { response: c.json({ error: "unauthorized" }, 401) };
155
+ if (modelPacks) try {
156
+ await modelPacks.ensureReady();
157
+ return tenant ? {
158
+ storage: modelPacks,
159
+ orgId: tenantOrgId(tenant),
160
+ userId: tenant.userId
161
+ } : {
162
+ storage: modelPacks,
163
+ orgId: "local",
164
+ userId: "local"
165
+ };
166
+ } catch {}
167
+ return { response: c.json({
168
+ error: "model_packs_unavailable",
169
+ message: "Model pack storage is unavailable the app database is not configured or failed to start."
170
+ }, 503) };
240
171
  }
172
+ /** DB row → the `ModePack` shape the packs list and activation flow consume. */
241
173
  function recordToModePack(record) {
242
- return { id: `custom:${record.id}`, name: record.name, description: "Saved custom pack", models: record.models };
174
+ return {
175
+ id: `custom:${record.id}`,
176
+ name: record.name,
177
+ description: "Saved custom pack",
178
+ models: record.models
179
+ };
243
180
  }
244
- async function listModelPacks({
245
- controller,
246
- authStorage,
247
- tenantCredentials,
248
- packContext,
249
- activePackId
250
- }) {
251
- const access = await buildProviderAccess({ controller, authStorage, tenantCredentials });
252
- const packs = [
253
- ...getAvailableModePacks(access),
254
- ...(await packContext.storage.list({ orgId: packContext.orgId })).map(recordToModePack)
255
- ];
256
- return packs.filter((p) => p.id !== "custom").map((p) => ({
257
- ...p,
258
- custom: p.id.startsWith("custom:"),
259
- active: activePackId != null && p.id === activePackId
260
- }));
181
+ /**
182
+ * List available model packs (built-in, gated by provider access, plus saved
183
+ * custom packs from the request's pack context). Drops the synthetic
184
+ * "New Custom" placeholder — the web client has its own create flow. `active`
185
+ * is set from the given session's thread when a resourceId is supplied.
186
+ */
187
+ async function listModelPacks({ controller, authStorage, tenantCredentials, packContext, activePackId }) {
188
+ return [...getAvailableModePacks(await buildProviderAccess({
189
+ controller,
190
+ authStorage,
191
+ tenantCredentials
192
+ })), ...(await packContext.storage.list({ orgId: packContext.orgId })).map(recordToModePack)].filter((p) => p.id !== "custom").map((p) => ({
193
+ ...p,
194
+ custom: p.id.startsWith("custom:"),
195
+ active: activePackId != null && p.id === activePackId
196
+ }));
261
197
  }
198
+ /** Resolve the active pack id for a session by reading its current thread. */
262
199
  async function resolveActivePackId(session) {
263
- if (!session) return null;
264
- const threadId = session.thread.getId();
265
- if (!threadId) return null;
266
- const thread = (await session.thread.list()).find((t) => t.id === threadId);
267
- const value = thread?.metadata?.[THREAD_ACTIVE_MODEL_PACK_ID_KEY];
268
- return typeof value === "string" ? value : null;
200
+ if (!session) return null;
201
+ const threadId = session.thread.getId();
202
+ if (!threadId) return null;
203
+ const value = (await session.thread.list()).find((t) => t.id === threadId)?.metadata?.[THREAD_ACTIVE_MODEL_PACK_ID_KEY];
204
+ return typeof value === "string" ? value : null;
269
205
  }
270
- async function applyPackToSession({
271
- controller,
272
- session,
273
- pack
274
- }) {
275
- const modes = controller.listModes?.() ?? [];
276
- const packModels = pack.models;
277
- for (const mode of modes) {
278
- const modelId = packModels[mode.id];
279
- if (modelId) {
280
- mode.defaultModelId = modelId;
281
- await session.thread.setSetting({ key: `modeModelId_${mode.id}`, value: modelId });
282
- }
283
- }
284
- const currentModeModel = packModels[session.mode.get()];
285
- if (currentModeModel) {
286
- await session.model.switch({ modelId: currentModeModel });
287
- }
288
- const subagentModeMap = { explore: "fast", plan: "plan", execute: "build" };
289
- for (const [agentType, modeId] of Object.entries(subagentModeMap)) {
290
- const saModelId = packModels[modeId];
291
- if (saModelId) {
292
- await session.subagents.model.set({ modelId: saModelId, agentType });
293
- }
294
- }
295
- await session.thread.setSetting({ key: THREAD_ACTIVE_MODEL_PACK_ID_KEY, value: pack.id });
296
- }
297
- var DEFAULT_OBSERVATION_THRESHOLD = 3e4;
298
- var DEFAULT_REFLECTION_THRESHOLD = 4e4;
299
- function providerOMModelId(providerId, factoryModelId) {
300
- switch (providerId) {
301
- case "anthropic":
302
- return "anthropic/claude-haiku-4-5";
303
- case "openai":
304
- return "openai/gpt-5.4-mini";
305
- default:
306
- return factoryModelId || DEFAULT_OM_MODEL_ID;
307
- }
206
+ /**
207
+ * Apply a pack to a session: seed each mode's default model, switch the current
208
+ * mode's model, set per-subagent models, and tag the thread with the active
209
+ * pack id. Mirrors the TUI `applyPack` orchestration.
210
+ */
211
+ async function applyPackToSession({ controller, session, pack }) {
212
+ const modes = controller.listModes?.() ?? [];
213
+ const packModels = pack.models;
214
+ for (const mode of modes) {
215
+ const modelId = packModels[mode.id];
216
+ if (modelId) {
217
+ mode.defaultModelId = modelId;
218
+ await session.thread.setSetting({
219
+ key: `modeModelId_${mode.id}`,
220
+ value: modelId
221
+ });
222
+ }
223
+ }
224
+ const currentModeModel = packModels[session.mode.get()];
225
+ if (currentModeModel) await session.model.switch({ modelId: currentModeModel });
226
+ for (const [agentType, modeId] of Object.entries({
227
+ explore: "fast",
228
+ plan: "plan",
229
+ execute: "build"
230
+ })) {
231
+ const saModelId = packModels[modeId];
232
+ if (saModelId) await session.subagents.model.set({
233
+ modelId: saModelId,
234
+ agentType
235
+ });
236
+ }
237
+ await session.thread.setSetting({
238
+ key: THREAD_ACTIVE_MODEL_PACK_ID_KEY,
239
+ value: pack.id
240
+ });
308
241
  }
242
+ /** Default thresholds mirror the TUI `/om` fallbacks. */
243
+ const DEFAULT_OBSERVATION_THRESHOLD = 3e4;
244
+ const DEFAULT_REFLECTION_THRESHOLD = 4e4;
309
245
  function readOMConfig(session) {
310
- const state = session.state.get() ?? {};
311
- const observeAttachments = state.observeAttachments;
312
- return {
313
- observerModelId: session.om.observer.modelId() ?? "",
314
- reflectorModelId: session.om.reflector.modelId() ?? "",
315
- observationThreshold: session.om.observer.threshold() ?? DEFAULT_OBSERVATION_THRESHOLD,
316
- reflectionThreshold: session.om.reflector.threshold() ?? DEFAULT_REFLECTION_THRESHOLD,
317
- observeAttachments: observeAttachments === true || observeAttachments === false ? observeAttachments : "auto"
318
- };
246
+ const observeAttachments = (session.state.get() ?? {}).observeAttachments;
247
+ return {
248
+ observerModelId: session.om.observer.modelId() ?? "",
249
+ reflectorModelId: session.om.reflector.modelId() ?? "",
250
+ observationThreshold: session.om.observer.threshold() ?? DEFAULT_OBSERVATION_THRESHOLD,
251
+ reflectionThreshold: session.om.reflector.threshold() ?? DEFAULT_REFLECTION_THRESHOLD,
252
+ observeAttachments: observeAttachments === true || observeAttachments === false ? observeAttachments : "auto"
253
+ };
319
254
  }
320
255
  function readStoredOMConfig(record) {
321
- return {
322
- observerModelId: record?.observerModelId ?? DEFAULT_OM_MODEL_ID,
323
- reflectorModelId: record?.reflectorModelId ?? DEFAULT_OM_MODEL_ID,
324
- observationThreshold: record?.observationThreshold ?? DEFAULT_OBSERVATION_THRESHOLD,
325
- reflectionThreshold: record?.reflectionThreshold ?? DEFAULT_REFLECTION_THRESHOLD,
326
- observeAttachments: record?.observeAttachments ?? "auto"
327
- };
256
+ return {
257
+ observerModelId: record?.observerModelId ?? DEFAULT_OM_MODEL_ID,
258
+ reflectorModelId: record?.reflectorModelId ?? DEFAULT_OM_MODEL_ID,
259
+ observationThreshold: record?.observationThreshold ?? DEFAULT_OBSERVATION_THRESHOLD,
260
+ reflectionThreshold: record?.reflectionThreshold ?? DEFAULT_REFLECTION_THRESHOLD,
261
+ observeAttachments: record?.observeAttachments ?? "auto"
262
+ };
328
263
  }
329
- async function resolveMemorySettingsContext({
330
- c,
331
- auth,
332
- memorySettings
333
- }) {
334
- await auth.ensureUser(c);
335
- const tenant = auth.tenant(c);
336
- if (!tenant && auth.enabled()) return { response: c.json({ error: "unauthorized" }, 401) };
337
- if (memorySettings) {
338
- try {
339
- await memorySettings.ensureReady();
340
- return tenant ? { storage: memorySettings, orgId: tenantOrgId(tenant), userId: tenant.userId } : { storage: memorySettings, orgId: "local", userId: "local" };
341
- } catch {
342
- }
343
- }
344
- return {
345
- response: c.json(
346
- {
347
- error: "memory_settings_unavailable",
348
- message: "Memory settings storage is unavailable \u2014 the app database is not configured or failed to start."
349
- },
350
- 503
351
- )
352
- };
264
+ /** Resolve the memory-settings context for a request, or a ready-to-return error response. */
265
+ async function resolveMemorySettingsContext({ c, auth, memorySettings }) {
266
+ await auth.ensureUser(c);
267
+ const tenant = auth.tenant(c);
268
+ if (!tenant && auth.enabled()) return { response: c.json({ error: "unauthorized" }, 401) };
269
+ if (memorySettings) try {
270
+ await memorySettings.ensureReady();
271
+ return tenant ? {
272
+ storage: memorySettings,
273
+ orgId: tenantOrgId(tenant),
274
+ userId: tenant.userId
275
+ } : {
276
+ storage: memorySettings,
277
+ orgId: "local",
278
+ userId: "local"
279
+ };
280
+ } catch {}
281
+ return { response: c.json({
282
+ error: "memory_settings_unavailable",
283
+ message: "Memory settings storage is unavailable the app database is not configured or failed to start."
284
+ }, 503) };
353
285
  }
286
+ /** Persist an OM knob change to the caller's memory-settings row. */
354
287
  async function persistMemorySettings(context, patch, fillIfUnset) {
355
- await context.storage.patch({ orgId: context.orgId, userId: context.userId, patch, fillIfUnset });
288
+ await context.storage.patch({
289
+ orgId: context.orgId,
290
+ userId: context.userId,
291
+ patch,
292
+ fillIfUnset
293
+ });
356
294
  }
295
+ /**
296
+ * Apply the stored memory-settings row onto the session, so the DB — not
297
+ * whatever happens to sit in persisted session state (e.g. a stale boot-time
298
+ * seed from before memory settings moved to the DB) — is what the web surface
299
+ * reads and what the session's OM actually runs with. The row is authoritative:
300
+ * knobs without a stored value reset to the built-in defaults.
301
+ */
357
302
  async function hydrateSessionMemorySettings(session, record) {
358
- for (const role of ["observer", "reflector"]) {
359
- const stored = role === "observer" ? record?.observerModelId : record?.reflectorModelId;
360
- const target = stored ?? DEFAULT_OM_MODEL_ID;
361
- if (session.om[role].modelId() !== target) {
362
- await session.om[role].switchModel({ modelId: target });
363
- }
364
- }
365
- const state = session.state.get() ?? {};
366
- const updates = {};
367
- const observationThreshold = record?.observationThreshold ?? DEFAULT_OBSERVATION_THRESHOLD;
368
- if (state.observationThreshold !== observationThreshold) {
369
- updates.observationThreshold = observationThreshold;
370
- }
371
- const reflectionThreshold = record?.reflectionThreshold ?? DEFAULT_REFLECTION_THRESHOLD;
372
- if (state.reflectionThreshold !== reflectionThreshold) {
373
- updates.reflectionThreshold = reflectionThreshold;
374
- }
375
- const observeAttachments = record?.observeAttachments ?? "auto";
376
- if ((state.observeAttachments ?? "auto") !== observeAttachments) {
377
- updates.observeAttachments = observeAttachments;
378
- }
379
- if (Object.keys(updates).length > 0) await session.state.set(updates);
303
+ for (const role of ["observer", "reflector"]) {
304
+ const target = (role === "observer" ? record?.observerModelId : record?.reflectorModelId) ?? DEFAULT_OM_MODEL_ID;
305
+ if (session.om[role].modelId() !== target) await session.om[role].switchModel({ modelId: target });
306
+ }
307
+ const state = session.state.get() ?? {};
308
+ const updates = {};
309
+ const observationThreshold = record?.observationThreshold ?? DEFAULT_OBSERVATION_THRESHOLD;
310
+ if (state.observationThreshold !== observationThreshold) updates.observationThreshold = observationThreshold;
311
+ const reflectionThreshold = record?.reflectionThreshold ?? DEFAULT_REFLECTION_THRESHOLD;
312
+ if (state.reflectionThreshold !== reflectionThreshold) updates.reflectionThreshold = reflectionThreshold;
313
+ const observeAttachments = record?.observeAttachments ?? "auto";
314
+ if ((state.observeAttachments ?? "auto") !== observeAttachments) updates.observeAttachments = observeAttachments;
315
+ if (Object.keys(updates).length > 0) await session.state.set(updates);
380
316
  }
317
+ /**
318
+ * The web config routes as Mastra `apiRoutes`:
319
+ * - `GET /web/config/providers` — list providers + key source
320
+ * - `PUT /web/config/providers/:provider/key` — set/update a provider's API key
321
+ * - `DELETE /web/config/providers/:provider/key` — remove a stored API key
322
+ * - `GET /web/config/models` — list available models (credentialed providers)
323
+ * - `GET /web/config/custom-providers` — list custom OpenAI-compatible providers
324
+ * - `POST /web/config/custom-providers` — create/update a custom provider
325
+ * - `DELETE /web/config/custom-providers/:id` — remove a custom provider
326
+ * - `GET /web/config/om` — read OM models/thresholds/observe-attachments
327
+ * - `PUT /web/config/om/:role/model` — switch observer/reflector model
328
+ * - `PUT /web/config/om/thresholds` — set observation/reflection thresholds
329
+ * - `PUT /web/config/om/observe-attachments` — set observe-attachments (auto/on/off)
330
+ */
381
331
  var ConfigRoutes = class extends Route {
382
- routes() {
383
- const options = this.deps;
384
- const { controller, authStorage, auth } = options;
385
- const onCredentialsChanged = options.onCredentialsChanged ?? (() => {
386
- });
387
- const onCustomProvidersChanged = options.onCustomProvidersChanged ?? (() => {
388
- });
389
- return [
390
- registerApiRoute("/web/config/providers", {
391
- method: "GET",
392
- requiresAuth: false,
393
- handler: async (c) => {
394
- try {
395
- const tenantCredentials = await listTenantCredentialsForRequest({
396
- c: loose(c),
397
- auth,
398
- credentials: options.modelCredentials
399
- });
400
- return c.json({
401
- providers: await listProviders({
402
- controller,
403
- authStorage: tenantCredentials ? void 0 : authStorage,
404
- tenantCredentials
405
- })
406
- });
407
- } catch (error) {
408
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
409
- }
410
- }
411
- }),
412
- registerApiRoute("/web/config/providers/:provider/key", {
413
- method: "PUT",
414
- requiresAuth: false,
415
- handler: async (c) => {
416
- const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: options.modelCredentials });
417
- if ("response" in ctx) return ctx.response;
418
- const provider = c.req.param("provider");
419
- let body;
420
- try {
421
- body = await c.req.json();
422
- } catch {
423
- return c.json({ error: "Invalid JSON body" }, 400);
424
- }
425
- const key = typeof body.key === "string" ? body.key.trim() : "";
426
- if (!key) return c.json({ error: "Missing required field: key" }, 400);
427
- const envVar = typeof body.envVar === "string" ? body.envVar : void 0;
428
- const scope = body.scope === "org" ? "org" : "user";
429
- try {
430
- if (ctx.mode === "tenant") {
431
- if (scope === "org" && !await auth.isOrganizationAdmin(loose(c), ctx.orgId)) {
432
- return c.json({ error: "organization_admin_required" }, 403);
433
- }
434
- const tenant = scope === "org" ? { orgId: ctx.orgId } : { orgId: ctx.orgId, userId: ctx.userId };
435
- await ctx.storage.setCredential(tenant, getAuthProviderId(provider), { type: "api_key", key });
436
- onCredentialsChanged(tenant);
437
- const records = await ctx.storage.listCredentials(ctx.orgId, ctx.userId);
438
- const providers2 = await listProviders({ controller, tenantCredentials: records });
439
- return c.json({ ok: true, provider: providers2.find((p) => p.provider === provider) });
440
- }
441
- if (!authStorage) return c.json({ error: "Credential storage is not available" }, 503);
442
- authStorage.setStoredApiKey(provider, key, envVar);
443
- const providers = await listProviders({ controller, authStorage });
444
- return c.json({ ok: true, provider: providers.find((p) => p.provider === provider) });
445
- } catch (error) {
446
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
447
- }
448
- }
449
- }),
450
- registerApiRoute("/web/config/providers/:provider/key", {
451
- method: "DELETE",
452
- requiresAuth: false,
453
- handler: async (c) => {
454
- const ctx = await resolveCredentialContext({ c: loose(c), auth, credentials: options.modelCredentials });
455
- if ("response" in ctx) return ctx.response;
456
- const provider = c.req.param("provider");
457
- const scope = c.req.query("scope") === "org" ? "org" : "user";
458
- try {
459
- if (ctx.mode === "tenant") {
460
- if (scope === "org" && !await auth.isOrganizationAdmin(loose(c), ctx.orgId)) {
461
- return c.json({ error: "organization_admin_required" }, 403);
462
- }
463
- const tenant = scope === "org" ? { orgId: ctx.orgId } : { orgId: ctx.orgId, userId: ctx.userId };
464
- await ctx.storage.removeCredential(tenant, getAuthProviderId(provider));
465
- onCredentialsChanged(tenant);
466
- const records = await ctx.storage.listCredentials(ctx.orgId, ctx.userId);
467
- const providers2 = await listProviders({ controller, tenantCredentials: records });
468
- return c.json({ ok: true, provider: providers2.find((p) => p.provider === provider) });
469
- }
470
- if (!authStorage) return c.json({ error: "Credential storage is not available" }, 503);
471
- authStorage.remove(`apikey:${provider}`);
472
- const providers = await listProviders({ controller, authStorage });
473
- return c.json({ ok: true, provider: providers.find((p) => p.provider === provider) });
474
- } catch (error) {
475
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
476
- }
477
- }
478
- }),
479
- // ── Custom providers (OpenAI-compatible endpoints) ──────────────────────
480
- // Mirrors the TUI's /custom-providers command, but backed by the
481
- // `custom-providers` domain (org rows in tenant mode, a sentinel `local`
482
- // org in no-auth mode) — the server never reads settings.json for these.
483
- registerApiRoute("/web/config/custom-providers", {
484
- method: "GET",
485
- requiresAuth: false,
486
- handler: async (c) => {
487
- const ctx = await resolveCustomProvidersContext({
488
- c: loose(c),
489
- auth,
490
- customProviders: options.customProviders
491
- });
492
- if ("response" in ctx) return ctx.response;
493
- try {
494
- const records = await ctx.storage.list({ orgId: ctx.orgId });
495
- return c.json({ providers: records.map(toCustomProviderInfo) });
496
- } catch (error) {
497
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
498
- }
499
- }
500
- }),
501
- registerApiRoute("/web/config/custom-providers", {
502
- method: "POST",
503
- requiresAuth: false,
504
- handler: async (c) => {
505
- const ctx = await resolveCustomProvidersContext({
506
- c: loose(c),
507
- auth,
508
- customProviders: options.customProviders
509
- });
510
- if ("response" in ctx) return ctx.response;
511
- let body;
512
- try {
513
- body = await c.req.json();
514
- } catch {
515
- return c.json({ error: "Invalid JSON body" }, 400);
516
- }
517
- const parsed = parseCustomProviderBody(body);
518
- if ("error" in parsed) return c.json({ error: parsed.error }, 400);
519
- const previousId = body && typeof body === "object" && typeof body.previousId === "string" ? body.previousId : void 0;
520
- try {
521
- const record = await ctx.storage.upsert({
522
- orgId: ctx.orgId,
523
- userId: ctx.userId,
524
- input: {
525
- providerId: getCustomProviderId(parsed.name),
526
- name: parsed.name,
527
- url: parsed.url,
528
- apiKey: parsed.apiKey,
529
- models: parsed.models
530
- },
531
- previousProviderId: previousId
532
- });
533
- onCustomProvidersChanged({ orgId: ctx.orgId });
534
- return c.json({ ok: true, provider: toCustomProviderInfo(record) });
535
- } catch (error) {
536
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
537
- }
538
- }
539
- }),
540
- registerApiRoute("/web/config/custom-providers/:id", {
541
- method: "DELETE",
542
- requiresAuth: false,
543
- handler: async (c) => {
544
- const ctx = await resolveCustomProvidersContext({
545
- c: loose(c),
546
- auth,
547
- customProviders: options.customProviders
548
- });
549
- if ("response" in ctx) return ctx.response;
550
- const id = c.req.param("id");
551
- try {
552
- await ctx.storage.delete({ orgId: ctx.orgId, providerId: id });
553
- onCustomProvidersChanged({ orgId: ctx.orgId });
554
- return c.json({ ok: true });
555
- } catch (error) {
556
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
557
- }
558
- }
559
- }),
560
- // ── Available models ────────────────────────────────────────────────────
561
- // Session-independent model catalog for settings pickers (Factory default
562
- // model, pack editors). Only models whose provider has a credential are
563
- // returned — the same filter the session-scoped hook applies client-side.
564
- registerApiRoute("/web/config/models", {
565
- method: "GET",
566
- requiresAuth: false,
567
- handler: async (c) => {
568
- try {
569
- const tenantCredentials = await listTenantCredentialsForRequest({
570
- c: loose(c),
571
- auth,
572
- credentials: options.modelCredentials
573
- });
574
- const [models, access] = await Promise.all([
575
- controller.listAvailableModels(),
576
- buildProviderAccess({
577
- controller,
578
- authStorage: tenantCredentials ? void 0 : authStorage,
579
- tenantCredentials
580
- })
581
- ]);
582
- const catalog = models.filter((m) => canUseModelProvider(access, m.provider) && typeof m.id === "string").map((m) => ({ id: m.id, provider: m.provider, modelName: m.modelName, hasApiKey: true }));
583
- if (options.customProviders) {
584
- try {
585
- const ctx = await resolveCustomProvidersContext({
586
- c: loose(c),
587
- auth,
588
- customProviders: options.customProviders
589
- });
590
- if (!("response" in ctx)) {
591
- const known = new Set(catalog.map((m) => m.id));
592
- for (const record of await ctx.storage.list({ orgId: ctx.orgId })) {
593
- for (const model of record.models) {
594
- const id = `${record.providerId}/${model}`;
595
- if (known.has(id)) continue;
596
- known.add(id);
597
- catalog.push({ id, provider: record.providerId, modelName: model, hasApiKey: true });
598
- }
599
- }
600
- }
601
- } catch {
602
- }
603
- }
604
- return c.json({
605
- models: catalog.sort(
606
- (a, b) => a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)
607
- )
608
- });
609
- } catch (error) {
610
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
611
- }
612
- }
613
- }),
614
- // ── Model packs ─────────────────────────────────────────────────────────
615
- // Mirrors the TUI's /models-pack command. Custom-pack CRUD lives in the
616
- // model-packs storage domain (org-scoped, sentinel `local` org in no-auth
617
- // mode never settings.json); activation is session-scoped and resolves
618
- // the session from the controller registry by resourceId.
619
- registerApiRoute("/web/config/model-packs", {
620
- method: "GET",
621
- requiresAuth: false,
622
- handler: async (c) => {
623
- const packContext = await resolvePackContext({ c: loose(c), auth, modelPacks: options.modelPacks });
624
- if ("response" in packContext) return packContext.response;
625
- const resourceId = c.req.query("resourceId");
626
- const scope = c.req.query("scope") || void 0;
627
- try {
628
- const session = resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0;
629
- const activePackId = await resolveActivePackId(session);
630
- const tenantCredentials = await listTenantCredentialsForRequest({
631
- c: loose(c),
632
- auth,
633
- credentials: options.modelCredentials
634
- });
635
- return c.json({
636
- packs: await listModelPacks({
637
- controller,
638
- authStorage: tenantCredentials ? void 0 : authStorage,
639
- tenantCredentials,
640
- packContext,
641
- activePackId
642
- }),
643
- activePackId
644
- });
645
- } catch (error) {
646
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
647
- }
648
- }
649
- }),
650
- registerApiRoute("/web/config/model-packs", {
651
- method: "POST",
652
- requiresAuth: false,
653
- handler: async (c) => {
654
- const packContext = await resolvePackContext({ c: loose(c), auth, modelPacks: options.modelPacks });
655
- if ("response" in packContext) return packContext.response;
656
- let body;
657
- try {
658
- body = await c.req.json();
659
- } catch {
660
- return c.json({ error: "Invalid JSON body" }, 400);
661
- }
662
- const name = typeof body.name === "string" ? body.name.trim() : "";
663
- if (!name) return c.json({ error: "Missing required field: name" }, 400);
664
- const m = body.models ?? {};
665
- const build = typeof m.build === "string" ? m.build.trim() : "";
666
- const plan = typeof m.plan === "string" ? m.plan.trim() : "";
667
- const fast = typeof m.fast === "string" ? m.fast.trim() : "";
668
- if (!build || !plan || !fast) {
669
- return c.json({ error: "models.build, models.plan and models.fast are required" }, 400);
670
- }
671
- try {
672
- const record = await packContext.storage.upsert({
673
- orgId: packContext.orgId,
674
- userId: packContext.userId,
675
- input: { name, models: { build, plan, fast } }
676
- });
677
- return c.json({ ok: true, pack: recordToModePack(record) });
678
- } catch (error) {
679
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
680
- }
681
- }
682
- }),
683
- registerApiRoute("/web/config/model-packs/:id", {
684
- method: "DELETE",
685
- requiresAuth: false,
686
- handler: async (c) => {
687
- const packContext = await resolvePackContext({ c: loose(c), auth, modelPacks: options.modelPacks });
688
- if ("response" in packContext) return packContext.response;
689
- const id = decodeURIComponent(c.req.param("id"));
690
- try {
691
- const recordId = id.startsWith("custom:") ? id.slice("custom:".length) : id;
692
- const deleted = await packContext.storage.delete({ orgId: packContext.orgId, id: recordId });
693
- return deleted ? c.json({ ok: true }) : c.json({ error: `Unknown pack "${id}"` }, 404);
694
- } catch (error) {
695
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
696
- }
697
- }
698
- }),
699
- registerApiRoute("/web/config/model-packs/:id/activate", {
700
- method: "POST",
701
- requiresAuth: false,
702
- handler: async (c) => {
703
- const packContext = await resolvePackContext({ c: loose(c), auth, modelPacks: options.modelPacks });
704
- if ("response" in packContext) return packContext.response;
705
- const id = decodeURIComponent(c.req.param("id"));
706
- let body;
707
- try {
708
- body = await c.req.json();
709
- } catch {
710
- return c.json({ error: "Invalid JSON body" }, 400);
711
- }
712
- const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
713
- const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
714
- if (!resourceId) return c.json({ error: "Missing required field: resourceId" }, 400);
715
- try {
716
- const session = await controller.getSessionByResource?.(resourceId, scope);
717
- if (!session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
718
- const tenantCredentials = await listTenantCredentialsForRequest({
719
- c: loose(c),
720
- auth,
721
- credentials: options.modelCredentials
722
- });
723
- const packs = await listModelPacks({
724
- controller,
725
- authStorage: tenantCredentials ? void 0 : authStorage,
726
- tenantCredentials,
727
- packContext
728
- });
729
- const pack = packs.find((p) => p.id === id);
730
- if (!pack) return c.json({ error: `Unknown pack "${id}"` }, 404);
731
- await applyPackToSession({ controller, session, pack });
732
- return c.json({ ok: true, activePackId: pack.id });
733
- } catch (error) {
734
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
735
- }
736
- }
737
- }),
738
- registerApiRoute("/web/config/om/provider-defaults", {
739
- method: "POST",
740
- requiresAuth: false,
741
- handler: async (c) => {
742
- let body;
743
- try {
744
- body = await c.req.json();
745
- } catch {
746
- return c.json({ error: "Invalid JSON body" }, 400);
747
- }
748
- const providerId = typeof body.providerId === "string" ? body.providerId.trim() : "";
749
- const factoryModelId = typeof body.factoryModelId === "string" ? body.factoryModelId.trim() : "";
750
- if (!providerId) return c.json({ error: "Missing required field: providerId" }, 400);
751
- const context = await resolveMemorySettingsContext({
752
- c: loose(c),
753
- auth,
754
- memorySettings: options.memorySettings
755
- });
756
- if ("response" in context) return context.response;
757
- try {
758
- const tenantCredentials = await listTenantCredentialsForRequest({
759
- c: loose(c),
760
- auth,
761
- credentials: options.modelCredentials
762
- });
763
- const access = await buildProviderAccess({
764
- controller,
765
- authStorage: tenantCredentials ? void 0 : authStorage,
766
- tenantCredentials
767
- });
768
- if (!access[providerId]) return c.json({ error: `Provider "${providerId}" is not configured` }, 400);
769
- const modelId = providerOMModelId(providerId, factoryModelId);
770
- const record = await context.storage.patch({
771
- orgId: context.orgId,
772
- userId: context.userId,
773
- patch: {},
774
- fillIfUnset: { observerModelId: modelId, reflectorModelId: modelId }
775
- });
776
- return c.json({ ok: true, config: readStoredOMConfig(record) });
777
- } catch (error) {
778
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
779
- }
780
- }
781
- }),
782
- // ── Observational memory ──────────────────────────────────────────────────
783
- // Mirrors the TUI's /om command. All five knobs are durably stored in the
784
- // per-(org, user) `memory-settings` app table — never settings.json. When a
785
- // session is supplied, changes are also applied to its state and thread.
786
- registerApiRoute("/web/config/om", {
787
- method: "GET",
788
- requiresAuth: false,
789
- handler: async (c) => {
790
- const resourceId = c.req.query("resourceId");
791
- const scope = c.req.query("scope") || void 0;
792
- const context = await resolveMemorySettingsContext({
793
- c: loose(c),
794
- auth,
795
- memorySettings: options.memorySettings
796
- });
797
- if ("response" in context) return context.response;
798
- try {
799
- const record = await context.storage.get({ orgId: context.orgId, userId: context.userId });
800
- if (!resourceId) return c.json({ config: readStoredOMConfig(record) });
801
- const session = await controller.getSessionByResource?.(resourceId, scope);
802
- if (!session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
803
- await hydrateSessionMemorySettings(session, record);
804
- return c.json({ config: readOMConfig(session) });
805
- } catch (error) {
806
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
807
- }
808
- }
809
- }),
810
- registerApiRoute("/web/config/om/:role/model", {
811
- method: "PUT",
812
- requiresAuth: false,
813
- handler: async (c) => {
814
- const role = c.req.param("role");
815
- if (role !== "observer" && role !== "reflector") {
816
- return c.json({ error: `Unknown OM role "${role}"` }, 400);
817
- }
818
- let body;
819
- try {
820
- body = await c.req.json();
821
- } catch {
822
- return c.json({ error: "Invalid JSON body" }, 400);
823
- }
824
- const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
825
- const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
826
- const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
827
- if (!modelId) return c.json({ error: "Missing required field: modelId" }, 400);
828
- const context = await resolveMemorySettingsContext({
829
- c: loose(c),
830
- auth,
831
- memorySettings: options.memorySettings
832
- });
833
- if ("response" in context) return context.response;
834
- try {
835
- const session = resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0;
836
- if (resourceId && !session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
837
- const otherRole = session ? role === "observer" ? session.om.reflector : session.om.observer : void 0;
838
- const otherRoleCurrentModelId = otherRole?.modelId() ?? null;
839
- await session?.om[role].switchModel({ modelId });
840
- const otherKey = role === "observer" ? "reflectorModelId" : "observerModelId";
841
- await persistMemorySettings(
842
- context,
843
- { [role === "observer" ? "observerModelId" : "reflectorModelId"]: modelId },
844
- otherRoleCurrentModelId ? { [otherKey]: otherRoleCurrentModelId } : void 0
845
- );
846
- const config = session ? readOMConfig(session) : readStoredOMConfig(await context.storage.get({ orgId: context.orgId, userId: context.userId }));
847
- return c.json({ ok: true, config });
848
- } catch (error) {
849
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
850
- }
851
- }
852
- }),
853
- registerApiRoute("/web/config/om/thresholds", {
854
- method: "PUT",
855
- requiresAuth: false,
856
- handler: async (c) => {
857
- let body;
858
- try {
859
- body = await c.req.json();
860
- } catch {
861
- return c.json({ error: "Invalid JSON body" }, 400);
862
- }
863
- const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
864
- const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
865
- const observation = typeof body.observationThreshold === "number" && body.observationThreshold > 0 ? Math.round(body.observationThreshold) : void 0;
866
- const reflection = typeof body.reflectionThreshold === "number" && body.reflectionThreshold > 0 ? Math.round(body.reflectionThreshold) : void 0;
867
- if (observation === void 0 && reflection === void 0) {
868
- return c.json({ error: "Provide observationThreshold and/or reflectionThreshold (positive numbers)" }, 400);
869
- }
870
- const context = await resolveMemorySettingsContext({
871
- c: loose(c),
872
- auth,
873
- memorySettings: options.memorySettings
874
- });
875
- if ("response" in context) return context.response;
876
- try {
877
- const session = resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0;
878
- if (resourceId && !session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
879
- if (observation !== void 0 && session) {
880
- await session.state.set({ observationThreshold: observation });
881
- await session.thread.setSetting({ key: "observationThreshold", value: observation });
882
- }
883
- if (reflection !== void 0 && session) {
884
- await session.state.set({ reflectionThreshold: reflection });
885
- await session.thread.setSetting({ key: "reflectionThreshold", value: reflection });
886
- }
887
- await persistMemorySettings(context, {
888
- ...observation !== void 0 ? { observationThreshold: observation } : {},
889
- ...reflection !== void 0 ? { reflectionThreshold: reflection } : {}
890
- });
891
- const config = session ? readOMConfig(session) : readStoredOMConfig(await context.storage.get({ orgId: context.orgId, userId: context.userId }));
892
- return c.json({ ok: true, config });
893
- } catch (error) {
894
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
895
- }
896
- }
897
- }),
898
- registerApiRoute("/web/config/om/observe-attachments", {
899
- method: "PUT",
900
- requiresAuth: false,
901
- handler: async (c) => {
902
- let body;
903
- try {
904
- body = await c.req.json();
905
- } catch {
906
- return c.json({ error: "Invalid JSON body" }, 400);
907
- }
908
- const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
909
- const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
910
- const raw = body.value;
911
- const value = raw === "auto" || raw === true || raw === false ? raw : "auto";
912
- if (raw !== "auto" && raw !== true && raw !== false) {
913
- return c.json({ error: "value must be 'auto', true, or false" }, 400);
914
- }
915
- const context = await resolveMemorySettingsContext({
916
- c: loose(c),
917
- auth,
918
- memorySettings: options.memorySettings
919
- });
920
- if ("response" in context) return context.response;
921
- try {
922
- const session = resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0;
923
- if (resourceId && !session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
924
- if (session) {
925
- await session.state.set({ observeAttachments: value });
926
- await session.thread.setSetting({ key: "observeAttachments", value });
927
- }
928
- await persistMemorySettings(context, { observeAttachments: value });
929
- const config = session ? readOMConfig(session) : readStoredOMConfig(await context.storage.get({ orgId: context.orgId, userId: context.userId }));
930
- return c.json({ ok: true, config });
931
- } catch (error) {
932
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
933
- }
934
- }
935
- })
936
- ];
937
- }
938
- };
939
- export {
940
- ConfigRoutes,
941
- buildProviderAccess,
942
- listModelPacks,
943
- listProviders,
944
- readOMConfig
332
+ routes() {
333
+ const options = this.deps;
334
+ const { controller, authStorage, auth } = options;
335
+ const onCredentialsChanged = options.onCredentialsChanged ?? (() => {});
336
+ const onCustomProvidersChanged = options.onCustomProvidersChanged ?? (() => {});
337
+ return [
338
+ registerApiRoute("/web/config/providers", {
339
+ method: "GET",
340
+ requiresAuth: false,
341
+ handler: async (c) => {
342
+ try {
343
+ const tenantCredentials = await listTenantCredentialsForRequest({
344
+ c: loose(c),
345
+ auth,
346
+ credentials: options.modelCredentials
347
+ });
348
+ return c.json({ providers: await listProviders({
349
+ controller,
350
+ authStorage: tenantCredentials ? void 0 : authStorage,
351
+ tenantCredentials
352
+ }) });
353
+ } catch (error) {
354
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
355
+ }
356
+ }
357
+ }),
358
+ registerApiRoute("/web/config/providers/:provider/key", {
359
+ method: "PUT",
360
+ requiresAuth: false,
361
+ handler: async (c) => {
362
+ const ctx = await resolveCredentialContext({
363
+ c: loose(c),
364
+ auth,
365
+ credentials: options.modelCredentials
366
+ });
367
+ if ("response" in ctx) return ctx.response;
368
+ const provider = c.req.param("provider");
369
+ let body;
370
+ try {
371
+ body = await c.req.json();
372
+ } catch {
373
+ return c.json({ error: "Invalid JSON body" }, 400);
374
+ }
375
+ const key = typeof body.key === "string" ? body.key.trim() : "";
376
+ if (!key) return c.json({ error: "Missing required field: key" }, 400);
377
+ const envVar = typeof body.envVar === "string" ? body.envVar : void 0;
378
+ const scope = body.scope === "org" ? "org" : "user";
379
+ try {
380
+ if (ctx.mode === "tenant") {
381
+ if (scope === "org" && !await auth.isOrganizationAdmin(loose(c), ctx.orgId)) return c.json({ error: "organization_admin_required" }, 403);
382
+ const tenant = scope === "org" ? { orgId: ctx.orgId } : {
383
+ orgId: ctx.orgId,
384
+ userId: ctx.userId
385
+ };
386
+ await ctx.storage.setCredential(tenant, getAuthProviderId(provider), {
387
+ type: "api_key",
388
+ key
389
+ });
390
+ onCredentialsChanged(tenant);
391
+ const records = await ctx.storage.listCredentials(ctx.orgId, ctx.userId);
392
+ const providers = await listProviders({
393
+ controller,
394
+ tenantCredentials: records
395
+ });
396
+ return c.json({
397
+ ok: true,
398
+ provider: providers.find((p) => p.provider === provider)
399
+ });
400
+ }
401
+ if (!authStorage) return c.json({ error: "Credential storage is not available" }, 503);
402
+ authStorage.setStoredApiKey(provider, key, envVar);
403
+ const providers = await listProviders({
404
+ controller,
405
+ authStorage
406
+ });
407
+ return c.json({
408
+ ok: true,
409
+ provider: providers.find((p) => p.provider === provider)
410
+ });
411
+ } catch (error) {
412
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
413
+ }
414
+ }
415
+ }),
416
+ registerApiRoute("/web/config/providers/:provider/key", {
417
+ method: "DELETE",
418
+ requiresAuth: false,
419
+ handler: async (c) => {
420
+ const ctx = await resolveCredentialContext({
421
+ c: loose(c),
422
+ auth,
423
+ credentials: options.modelCredentials
424
+ });
425
+ if ("response" in ctx) return ctx.response;
426
+ const provider = c.req.param("provider");
427
+ const scope = c.req.query("scope") === "org" ? "org" : "user";
428
+ try {
429
+ if (ctx.mode === "tenant") {
430
+ if (scope === "org" && !await auth.isOrganizationAdmin(loose(c), ctx.orgId)) return c.json({ error: "organization_admin_required" }, 403);
431
+ const tenant = scope === "org" ? { orgId: ctx.orgId } : {
432
+ orgId: ctx.orgId,
433
+ userId: ctx.userId
434
+ };
435
+ await ctx.storage.removeCredential(tenant, getAuthProviderId(provider));
436
+ onCredentialsChanged(tenant);
437
+ const records = await ctx.storage.listCredentials(ctx.orgId, ctx.userId);
438
+ const providers = await listProviders({
439
+ controller,
440
+ tenantCredentials: records
441
+ });
442
+ return c.json({
443
+ ok: true,
444
+ provider: providers.find((p) => p.provider === provider)
445
+ });
446
+ }
447
+ if (!authStorage) return c.json({ error: "Credential storage is not available" }, 503);
448
+ authStorage.remove(`apikey:${provider}`);
449
+ const providers = await listProviders({
450
+ controller,
451
+ authStorage
452
+ });
453
+ return c.json({
454
+ ok: true,
455
+ provider: providers.find((p) => p.provider === provider)
456
+ });
457
+ } catch (error) {
458
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
459
+ }
460
+ }
461
+ }),
462
+ registerApiRoute("/web/config/custom-providers", {
463
+ method: "GET",
464
+ requiresAuth: false,
465
+ handler: async (c) => {
466
+ const ctx = await resolveCustomProvidersContext({
467
+ c: loose(c),
468
+ auth,
469
+ customProviders: options.customProviders
470
+ });
471
+ if ("response" in ctx) return ctx.response;
472
+ try {
473
+ const records = await ctx.storage.list({ orgId: ctx.orgId });
474
+ return c.json({ providers: records.map(toCustomProviderInfo) });
475
+ } catch (error) {
476
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
477
+ }
478
+ }
479
+ }),
480
+ registerApiRoute("/web/config/custom-providers", {
481
+ method: "POST",
482
+ requiresAuth: false,
483
+ handler: async (c) => {
484
+ const ctx = await resolveCustomProvidersContext({
485
+ c: loose(c),
486
+ auth,
487
+ customProviders: options.customProviders
488
+ });
489
+ if ("response" in ctx) return ctx.response;
490
+ let body;
491
+ try {
492
+ body = await c.req.json();
493
+ } catch {
494
+ return c.json({ error: "Invalid JSON body" }, 400);
495
+ }
496
+ const parsed = parseCustomProviderBody(body);
497
+ if ("error" in parsed) return c.json({ error: parsed.error }, 400);
498
+ const previousId = body && typeof body === "object" && typeof body.previousId === "string" ? body.previousId : void 0;
499
+ try {
500
+ const record = await ctx.storage.upsert({
501
+ orgId: ctx.orgId,
502
+ userId: ctx.userId,
503
+ input: {
504
+ providerId: getCustomProviderId(parsed.name),
505
+ name: parsed.name,
506
+ url: parsed.url,
507
+ apiKey: parsed.apiKey,
508
+ models: parsed.models
509
+ },
510
+ previousProviderId: previousId
511
+ });
512
+ onCustomProvidersChanged({ orgId: ctx.orgId });
513
+ return c.json({
514
+ ok: true,
515
+ provider: toCustomProviderInfo(record)
516
+ });
517
+ } catch (error) {
518
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
519
+ }
520
+ }
521
+ }),
522
+ registerApiRoute("/web/config/custom-providers/:id", {
523
+ method: "DELETE",
524
+ requiresAuth: false,
525
+ handler: async (c) => {
526
+ const ctx = await resolveCustomProvidersContext({
527
+ c: loose(c),
528
+ auth,
529
+ customProviders: options.customProviders
530
+ });
531
+ if ("response" in ctx) return ctx.response;
532
+ const id = c.req.param("id");
533
+ try {
534
+ await ctx.storage.delete({
535
+ orgId: ctx.orgId,
536
+ providerId: id
537
+ });
538
+ onCustomProvidersChanged({ orgId: ctx.orgId });
539
+ return c.json({ ok: true });
540
+ } catch (error) {
541
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
542
+ }
543
+ }
544
+ }),
545
+ registerApiRoute("/web/config/models", {
546
+ method: "GET",
547
+ requiresAuth: false,
548
+ handler: async (c) => {
549
+ try {
550
+ const tenantCredentials = await listTenantCredentialsForRequest({
551
+ c: loose(c),
552
+ auth,
553
+ credentials: options.modelCredentials
554
+ });
555
+ const [models, access] = await Promise.all([controller.listAvailableModels(), buildProviderAccess({
556
+ controller,
557
+ authStorage: tenantCredentials ? void 0 : authStorage,
558
+ tenantCredentials
559
+ })]);
560
+ const catalog = models.filter((m) => canUseModelProvider(access, m.provider) && typeof m.id === "string").map((m) => ({
561
+ id: m.id,
562
+ provider: m.provider,
563
+ modelName: m.modelName,
564
+ hasApiKey: true
565
+ }));
566
+ if (options.customProviders) try {
567
+ const ctx = await resolveCustomProvidersContext({
568
+ c: loose(c),
569
+ auth,
570
+ customProviders: options.customProviders
571
+ });
572
+ if (!("response" in ctx)) {
573
+ const known = new Set(catalog.map((m) => m.id));
574
+ for (const record of await ctx.storage.list({ orgId: ctx.orgId })) for (const model of record.models) {
575
+ const id = `${record.providerId}/${model}`;
576
+ if (known.has(id)) continue;
577
+ known.add(id);
578
+ catalog.push({
579
+ id,
580
+ provider: record.providerId,
581
+ modelName: model,
582
+ hasApiKey: true
583
+ });
584
+ }
585
+ }
586
+ } catch {}
587
+ return c.json({ models: catalog.sort((a, b) => a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)) });
588
+ } catch (error) {
589
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
590
+ }
591
+ }
592
+ }),
593
+ registerApiRoute("/web/config/model-packs", {
594
+ method: "GET",
595
+ requiresAuth: false,
596
+ handler: async (c) => {
597
+ const packContext = await resolvePackContext({
598
+ c: loose(c),
599
+ auth,
600
+ modelPacks: options.modelPacks
601
+ });
602
+ if ("response" in packContext) return packContext.response;
603
+ const resourceId = c.req.query("resourceId");
604
+ const scope = c.req.query("scope") || void 0;
605
+ try {
606
+ const activePackId = await resolveActivePackId(resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0);
607
+ const tenantCredentials = await listTenantCredentialsForRequest({
608
+ c: loose(c),
609
+ auth,
610
+ credentials: options.modelCredentials
611
+ });
612
+ return c.json({
613
+ packs: await listModelPacks({
614
+ controller,
615
+ authStorage: tenantCredentials ? void 0 : authStorage,
616
+ tenantCredentials,
617
+ packContext,
618
+ activePackId
619
+ }),
620
+ activePackId
621
+ });
622
+ } catch (error) {
623
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
624
+ }
625
+ }
626
+ }),
627
+ registerApiRoute("/web/config/model-packs", {
628
+ method: "POST",
629
+ requiresAuth: false,
630
+ handler: async (c) => {
631
+ const packContext = await resolvePackContext({
632
+ c: loose(c),
633
+ auth,
634
+ modelPacks: options.modelPacks
635
+ });
636
+ if ("response" in packContext) return packContext.response;
637
+ let body;
638
+ try {
639
+ body = await c.req.json();
640
+ } catch {
641
+ return c.json({ error: "Invalid JSON body" }, 400);
642
+ }
643
+ const name = typeof body.name === "string" ? body.name.trim() : "";
644
+ if (!name) return c.json({ error: "Missing required field: name" }, 400);
645
+ const m = body.models ?? {};
646
+ const build = typeof m.build === "string" ? m.build.trim() : "";
647
+ const plan = typeof m.plan === "string" ? m.plan.trim() : "";
648
+ const fast = typeof m.fast === "string" ? m.fast.trim() : "";
649
+ if (!build || !plan || !fast) return c.json({ error: "models.build, models.plan and models.fast are required" }, 400);
650
+ try {
651
+ const record = await packContext.storage.upsert({
652
+ orgId: packContext.orgId,
653
+ userId: packContext.userId,
654
+ input: {
655
+ name,
656
+ models: {
657
+ build,
658
+ plan,
659
+ fast
660
+ }
661
+ }
662
+ });
663
+ return c.json({
664
+ ok: true,
665
+ pack: recordToModePack(record)
666
+ });
667
+ } catch (error) {
668
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
669
+ }
670
+ }
671
+ }),
672
+ registerApiRoute("/web/config/model-packs/:id", {
673
+ method: "DELETE",
674
+ requiresAuth: false,
675
+ handler: async (c) => {
676
+ const packContext = await resolvePackContext({
677
+ c: loose(c),
678
+ auth,
679
+ modelPacks: options.modelPacks
680
+ });
681
+ if ("response" in packContext) return packContext.response;
682
+ const id = decodeURIComponent(c.req.param("id"));
683
+ try {
684
+ const recordId = id.startsWith("custom:") ? id.slice(7) : id;
685
+ return await packContext.storage.delete({
686
+ orgId: packContext.orgId,
687
+ id: recordId
688
+ }) ? c.json({ ok: true }) : c.json({ error: `Unknown pack "${id}"` }, 404);
689
+ } catch (error) {
690
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
691
+ }
692
+ }
693
+ }),
694
+ registerApiRoute("/web/config/model-packs/:id/activate", {
695
+ method: "POST",
696
+ requiresAuth: false,
697
+ handler: async (c) => {
698
+ const packContext = await resolvePackContext({
699
+ c: loose(c),
700
+ auth,
701
+ modelPacks: options.modelPacks
702
+ });
703
+ if ("response" in packContext) return packContext.response;
704
+ const id = decodeURIComponent(c.req.param("id"));
705
+ let body;
706
+ try {
707
+ body = await c.req.json();
708
+ } catch {
709
+ return c.json({ error: "Invalid JSON body" }, 400);
710
+ }
711
+ const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
712
+ const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
713
+ if (!resourceId) return c.json({ error: "Missing required field: resourceId" }, 400);
714
+ try {
715
+ const session = await controller.getSessionByResource?.(resourceId, scope);
716
+ if (!session) return c.json({ error: `No session for resourceId "${resourceId}"` }, 404);
717
+ const tenantCredentials = await listTenantCredentialsForRequest({
718
+ c: loose(c),
719
+ auth,
720
+ credentials: options.modelCredentials
721
+ });
722
+ const pack = (await listModelPacks({
723
+ controller,
724
+ authStorage: tenantCredentials ? void 0 : authStorage,
725
+ tenantCredentials,
726
+ packContext
727
+ })).find((p) => p.id === id);
728
+ if (!pack) return c.json({ error: `Unknown pack "${id}"` }, 404);
729
+ await applyPackToSession({
730
+ controller,
731
+ session,
732
+ pack
733
+ });
734
+ return c.json({
735
+ ok: true,
736
+ activePackId: pack.id
737
+ });
738
+ } catch (error) {
739
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
740
+ }
741
+ }
742
+ }),
743
+ registerApiRoute("/web/config/om/provider-defaults", {
744
+ method: "POST",
745
+ requiresAuth: false,
746
+ handler: async (c) => {
747
+ let body;
748
+ try {
749
+ body = await c.req.json();
750
+ } catch {
751
+ return c.json({ error: "Invalid JSON body" }, 400);
752
+ }
753
+ const providerId = typeof body.providerId === "string" ? body.providerId.trim() : "";
754
+ const factoryModelId = typeof body.factoryModelId === "string" ? body.factoryModelId.trim() : "";
755
+ if (!providerId) return c.json({ error: "Missing required field: providerId" }, 400);
756
+ const context = await resolveMemorySettingsContext({
757
+ c: loose(c),
758
+ auth,
759
+ memorySettings: options.memorySettings
760
+ });
761
+ if ("response" in context) return context.response;
762
+ try {
763
+ const tenantCredentials = await listTenantCredentialsForRequest({
764
+ c: loose(c),
765
+ auth,
766
+ credentials: options.modelCredentials
767
+ });
768
+ if (!(await buildProviderAccess({
769
+ controller,
770
+ authStorage: tenantCredentials ? void 0 : authStorage,
771
+ tenantCredentials
772
+ }))[providerId]) return c.json({ error: `Provider "${providerId}" is not configured` }, 400);
773
+ const modelId = resolveProviderOMDefault(providerId, factoryModelId).modelId;
774
+ const record = await context.storage.patch({
775
+ orgId: context.orgId,
776
+ userId: context.userId,
777
+ patch: {},
778
+ fillIfUnset: {
779
+ observerModelId: modelId,
780
+ reflectorModelId: modelId
781
+ }
782
+ });
783
+ return c.json({
784
+ ok: true,
785
+ config: readStoredOMConfig(record)
786
+ });
787
+ } catch (error) {
788
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
789
+ }
790
+ }
791
+ }),
792
+ registerApiRoute("/web/config/om", {
793
+ method: "GET",
794
+ requiresAuth: false,
795
+ handler: async (c) => {
796
+ const resourceId = c.req.query("resourceId");
797
+ const scope = c.req.query("scope") || void 0;
798
+ const context = await resolveMemorySettingsContext({
799
+ c: loose(c),
800
+ auth,
801
+ memorySettings: options.memorySettings
802
+ });
803
+ if ("response" in context) return context.response;
804
+ try {
805
+ const record = await context.storage.get({
806
+ orgId: context.orgId,
807
+ userId: context.userId
808
+ });
809
+ if (!resourceId) return c.json({ config: readStoredOMConfig(record) });
810
+ const session = await controller.getSessionByResource?.(resourceId, scope);
811
+ if (!session) return c.json({ config: readStoredOMConfig(record) });
812
+ await hydrateSessionMemorySettings(session, record);
813
+ return c.json({ config: readOMConfig(session) });
814
+ } catch (error) {
815
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
816
+ }
817
+ }
818
+ }),
819
+ registerApiRoute("/web/config/om/:role/model", {
820
+ method: "PUT",
821
+ requiresAuth: false,
822
+ handler: async (c) => {
823
+ const role = c.req.param("role");
824
+ if (role !== "observer" && role !== "reflector") return c.json({ error: `Unknown OM role "${role}"` }, 400);
825
+ let body;
826
+ try {
827
+ body = await c.req.json();
828
+ } catch {
829
+ return c.json({ error: "Invalid JSON body" }, 400);
830
+ }
831
+ const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
832
+ const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
833
+ const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
834
+ if (!modelId) return c.json({ error: "Missing required field: modelId" }, 400);
835
+ const context = await resolveMemorySettingsContext({
836
+ c: loose(c),
837
+ auth,
838
+ memorySettings: options.memorySettings
839
+ });
840
+ if ("response" in context) return context.response;
841
+ try {
842
+ const session = resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0;
843
+ const otherRoleCurrentModelId = (session ? role === "observer" ? session.om.reflector : session.om.observer : void 0)?.modelId() ?? null;
844
+ await session?.om[role].switchModel({ modelId });
845
+ const otherKey = role === "observer" ? "reflectorModelId" : "observerModelId";
846
+ await persistMemorySettings(context, { [role === "observer" ? "observerModelId" : "reflectorModelId"]: modelId }, otherRoleCurrentModelId ? { [otherKey]: otherRoleCurrentModelId } : void 0);
847
+ const config = session ? readOMConfig(session) : readStoredOMConfig(await context.storage.get({
848
+ orgId: context.orgId,
849
+ userId: context.userId
850
+ }));
851
+ return c.json({
852
+ ok: true,
853
+ config
854
+ });
855
+ } catch (error) {
856
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
857
+ }
858
+ }
859
+ }),
860
+ registerApiRoute("/web/config/om/thresholds", {
861
+ method: "PUT",
862
+ requiresAuth: false,
863
+ handler: async (c) => {
864
+ let body;
865
+ try {
866
+ body = await c.req.json();
867
+ } catch {
868
+ return c.json({ error: "Invalid JSON body" }, 400);
869
+ }
870
+ const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
871
+ const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
872
+ const observation = typeof body.observationThreshold === "number" && body.observationThreshold > 0 ? Math.round(body.observationThreshold) : void 0;
873
+ const reflection = typeof body.reflectionThreshold === "number" && body.reflectionThreshold > 0 ? Math.round(body.reflectionThreshold) : void 0;
874
+ if (observation === void 0 && reflection === void 0) return c.json({ error: "Provide observationThreshold and/or reflectionThreshold (positive numbers)" }, 400);
875
+ const context = await resolveMemorySettingsContext({
876
+ c: loose(c),
877
+ auth,
878
+ memorySettings: options.memorySettings
879
+ });
880
+ if ("response" in context) return context.response;
881
+ try {
882
+ const session = resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0;
883
+ if (observation !== void 0 && session) {
884
+ await session.state.set({ observationThreshold: observation });
885
+ await session.thread.setSetting({
886
+ key: "observationThreshold",
887
+ value: observation
888
+ });
889
+ }
890
+ if (reflection !== void 0 && session) {
891
+ await session.state.set({ reflectionThreshold: reflection });
892
+ await session.thread.setSetting({
893
+ key: "reflectionThreshold",
894
+ value: reflection
895
+ });
896
+ }
897
+ await persistMemorySettings(context, {
898
+ ...observation !== void 0 ? { observationThreshold: observation } : {},
899
+ ...reflection !== void 0 ? { reflectionThreshold: reflection } : {}
900
+ });
901
+ const config = session ? readOMConfig(session) : readStoredOMConfig(await context.storage.get({
902
+ orgId: context.orgId,
903
+ userId: context.userId
904
+ }));
905
+ return c.json({
906
+ ok: true,
907
+ config
908
+ });
909
+ } catch (error) {
910
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
911
+ }
912
+ }
913
+ }),
914
+ registerApiRoute("/web/config/om/observe-attachments", {
915
+ method: "PUT",
916
+ requiresAuth: false,
917
+ handler: async (c) => {
918
+ let body;
919
+ try {
920
+ body = await c.req.json();
921
+ } catch {
922
+ return c.json({ error: "Invalid JSON body" }, 400);
923
+ }
924
+ const resourceId = typeof body.resourceId === "string" ? body.resourceId : "";
925
+ const scope = typeof body.scope === "string" && body.scope ? body.scope : void 0;
926
+ const raw = body.value;
927
+ const value = raw === "auto" || raw === true || raw === false ? raw : "auto";
928
+ if (raw !== "auto" && raw !== true && raw !== false) return c.json({ error: "value must be 'auto', true, or false" }, 400);
929
+ const context = await resolveMemorySettingsContext({
930
+ c: loose(c),
931
+ auth,
932
+ memorySettings: options.memorySettings
933
+ });
934
+ if ("response" in context) return context.response;
935
+ try {
936
+ const session = resourceId ? await controller.getSessionByResource?.(resourceId, scope) : void 0;
937
+ if (session) {
938
+ await session.state.set({ observeAttachments: value });
939
+ await session.thread.setSetting({
940
+ key: "observeAttachments",
941
+ value
942
+ });
943
+ }
944
+ await persistMemorySettings(context, { observeAttachments: value });
945
+ const config = session ? readOMConfig(session) : readStoredOMConfig(await context.storage.get({
946
+ orgId: context.orgId,
947
+ userId: context.userId
948
+ }));
949
+ return c.json({
950
+ ok: true,
951
+ config
952
+ });
953
+ } catch (error) {
954
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 500);
955
+ }
956
+ }
957
+ })
958
+ ];
959
+ }
945
960
  };
961
+ //#endregion
962
+ export { ConfigRoutes, buildProviderAccess, listModelPacks, listProviders, readOMConfig };
963
+
946
964
  //# sourceMappingURL=config.js.map