dot-studio 0.0.1 → 0.0.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 (148) hide show
  1. package/README.md +20 -200
  2. package/client/assets/ActFrame-BYOBkLYW.js +1 -0
  3. package/client/assets/ActFrame-C_WEt6bv.css +1 -0
  4. package/client/assets/ActInspectorPanel-C3VlS7tB.js +1 -0
  5. package/client/assets/ActInspectorPanel-CE6s6GYv.css +1 -0
  6. package/client/assets/AssistantChat-BOyW0K79.js +1 -0
  7. package/client/assets/AssistantChat-DoVmHvMJ.css +1 -0
  8. package/client/assets/CanvasTerminalFrame-BC-79q9U.css +1 -0
  9. package/client/assets/CanvasTerminalFrame-DxKbexK6.js +4 -0
  10. package/client/assets/CanvasTrackingFrame-DumxhNwg.js +1 -0
  11. package/client/assets/CanvasTrackingFrame-G4rRrfne.css +1 -0
  12. package/client/assets/CanvasWindowFrame-ziJeVfHG.js +1 -0
  13. package/client/assets/DanceBundleEditorFrame-CH8VDUMK.js +1 -0
  14. package/client/assets/DanceBundleEditorFrame-DaLqMflT.css +1 -0
  15. package/client/assets/MarkdownEditorFrame-DVecIZpZ.css +1 -0
  16. package/client/assets/MarkdownEditorFrame-Dwpgs2GX.js +2 -0
  17. package/client/assets/MarkdownRenderer-Cz8A4AgP.js +1 -0
  18. package/client/assets/PublishModal-DUlHz0fT.js +1 -0
  19. package/client/assets/TodoDock-DcVf7zQG.js +1 -0
  20. package/client/assets/WorkspaceToolbar-CXYi_sMD.js +2 -0
  21. package/client/assets/WorkspaceToolbar-CiQvVocC.css +1 -0
  22. package/client/assets/chat-message-visibility-YwJ-AQno.js +11 -0
  23. package/client/assets/dnd-vendor-CIAZE2P2.js +5 -0
  24. package/client/assets/flow-vendor-BZV40eAE.css +1 -0
  25. package/client/assets/flow-vendor-C868rU-6.js +23 -0
  26. package/client/assets/icon-vendor-I2JVIi1s.js +501 -0
  27. package/client/assets/index-BMY4hrBP.js +3 -0
  28. package/client/assets/index-C-vnj9y3.js +1 -0
  29. package/client/assets/index-C9HTqfZw.css +1 -0
  30. package/client/assets/index-CWrv6O3o.js +64 -0
  31. package/client/assets/index-DMS12-Q2.js +8 -0
  32. package/client/assets/index-Dn7t_Y7G.js +1 -0
  33. package/client/assets/index-p-wk7iGH.css +1 -0
  34. package/client/assets/markdown-vendor-BSTcku12.css +10 -0
  35. package/client/assets/markdown-vendor-DnTJ9hmR.js +35 -0
  36. package/client/assets/participant-labels-Cf3qP3GB.js +1 -0
  37. package/client/assets/queries-Dm1jEHfc.js +1 -0
  38. package/client/assets/query-vendor-_taqgrbn.js +1 -0
  39. package/client/assets/react-vendor-DzpMUNDT.js +49 -0
  40. package/client/assets/settings-utils-l7KCS3Ev.js +1 -0
  41. package/client/assets/terminal-vendor-6GBZ9nXN.css +32 -0
  42. package/client/assets/terminal-vendor-D0xRnmbI.js +112 -0
  43. package/client/index.html +13 -3
  44. package/dist/cli.js +25 -3
  45. package/dist/server/app.js +72 -0
  46. package/dist/server/index.js +2 -62
  47. package/dist/server/lib/act-session-policy.js +31 -0
  48. package/dist/server/lib/chat-session.js +101 -0
  49. package/dist/server/lib/config.js +18 -4
  50. package/dist/server/lib/dot-authoring.js +171 -102
  51. package/dist/server/lib/dot-loader.js +9 -8
  52. package/dist/server/lib/dot-login.js +8 -190
  53. package/dist/server/lib/dot-source.js +11 -0
  54. package/dist/server/lib/model-catalog.js +74 -15
  55. package/dist/server/lib/opencode-auth.js +4 -1
  56. package/dist/server/lib/opencode-errors.js +70 -38
  57. package/dist/server/lib/opencode-sidecar.js +5 -2
  58. package/dist/server/lib/project-config.js +8 -0
  59. package/dist/server/lib/runtime-tools.js +46 -8
  60. package/dist/server/lib/safe-mode.js +410 -0
  61. package/dist/server/lib/session-execution.js +81 -0
  62. package/dist/server/lib/sse.js +22 -0
  63. package/dist/server/routes/act-runtime-threads.js +156 -0
  64. package/dist/server/routes/act-runtime-tools.js +157 -0
  65. package/dist/server/routes/act-runtime.js +7 -0
  66. package/dist/server/routes/adapter.js +32 -0
  67. package/dist/server/routes/assets-collection.js +16 -0
  68. package/dist/server/routes/assets-detail.js +38 -0
  69. package/dist/server/routes/assets.js +4 -158
  70. package/dist/server/routes/chat-messages.js +104 -0
  71. package/dist/server/routes/chat-sessions.js +104 -0
  72. package/dist/server/routes/chat-stream.js +15 -0
  73. package/dist/server/routes/chat.js +6 -353
  74. package/dist/server/routes/compile.js +5 -91
  75. package/dist/server/routes/dot-assets.js +77 -0
  76. package/dist/server/routes/dot-core.js +62 -0
  77. package/dist/server/routes/dot-performer.js +80 -0
  78. package/dist/server/routes/dot.js +6 -267
  79. package/dist/server/routes/drafts-collection.js +40 -0
  80. package/dist/server/routes/drafts-dance-bundle.js +113 -0
  81. package/dist/server/routes/drafts-item.js +86 -0
  82. package/dist/server/routes/drafts.js +9 -0
  83. package/dist/server/routes/health.js +18 -33
  84. package/dist/server/routes/opencode-core.js +120 -0
  85. package/dist/server/routes/opencode-file.js +67 -0
  86. package/dist/server/routes/opencode-mcp.js +74 -0
  87. package/dist/server/routes/opencode-provider.js +41 -0
  88. package/dist/server/routes/opencode.js +8 -418
  89. package/dist/server/routes/route-errors.js +10 -0
  90. package/dist/server/routes/safe-actions.js +60 -0
  91. package/dist/server/routes/safe-summary.js +20 -0
  92. package/dist/server/routes/safe.js +7 -0
  93. package/dist/server/routes/workspaces.js +47 -0
  94. package/dist/server/services/act-runtime/act-context-builder.js +81 -0
  95. package/dist/server/services/act-runtime/act-runtime-service.js +313 -0
  96. package/dist/server/services/act-runtime/act-runtime-utils.js +10 -0
  97. package/dist/server/services/act-runtime/act-tool-projection.js +26 -0
  98. package/dist/server/services/act-runtime/act-tools.js +151 -0
  99. package/dist/server/services/act-runtime/board-persistence.js +38 -0
  100. package/dist/server/services/act-runtime/event-logger.js +73 -0
  101. package/dist/server/services/act-runtime/event-router.js +102 -0
  102. package/dist/server/services/act-runtime/mailbox.js +149 -0
  103. package/dist/server/services/act-runtime/safety-guard.js +162 -0
  104. package/dist/server/services/act-runtime/session-queue.js +114 -0
  105. package/dist/server/services/act-runtime/thread-manager.js +351 -0
  106. package/dist/server/services/act-runtime/wake-cascade.js +306 -0
  107. package/dist/server/services/act-runtime/wake-evaluator.js +43 -0
  108. package/dist/server/services/act-runtime/wake-performer-resolver.js +68 -0
  109. package/dist/server/services/act-runtime/wake-prompt-builder.js +77 -0
  110. package/dist/server/services/adapter-view-service.js +6 -0
  111. package/dist/server/services/asset-service.js +366 -0
  112. package/dist/server/services/chat-event-stream-service.js +157 -0
  113. package/dist/server/services/chat-service.js +207 -0
  114. package/dist/server/services/chat-session-service.js +203 -0
  115. package/dist/server/services/compile-service.js +4 -0
  116. package/dist/server/services/dance-bundle-service.js +222 -0
  117. package/dist/server/services/dot-add-service.js +59 -0
  118. package/dist/server/services/dot-service.js +178 -0
  119. package/dist/server/services/draft-service.js +367 -0
  120. package/dist/server/services/opencode-projection/dance-compiler.js +164 -0
  121. package/dist/server/services/opencode-projection/performer-compiler.js +195 -0
  122. package/dist/server/services/opencode-projection/preview-service.js +31 -0
  123. package/dist/server/services/opencode-projection/projection-manifest.js +98 -0
  124. package/dist/server/services/opencode-projection/stage-projection-service.js +188 -0
  125. package/dist/server/services/opencode-service.js +338 -0
  126. package/dist/server/services/safe-service.js +33 -0
  127. package/dist/server/services/studio-assistant/assistant-service.js +172 -0
  128. package/dist/server/services/studio-service.js +69 -0
  129. package/dist/server/services/workspace-service.js +224 -0
  130. package/dist/server/terminal.js +57 -11
  131. package/dist/shared/act-types.js +4 -0
  132. package/dist/shared/adapter-view.js +1 -0
  133. package/dist/shared/asset-contracts.js +1 -0
  134. package/dist/shared/assistant-actions.js +1 -0
  135. package/dist/shared/chat-contracts.js +1 -0
  136. package/dist/shared/dot-contracts.js +1 -0
  137. package/dist/shared/dot-types.js +4 -0
  138. package/dist/shared/draft-contracts.js +2 -0
  139. package/dist/shared/model-types.js +2 -0
  140. package/dist/shared/performer-mcp-portability.js +10 -0
  141. package/dist/shared/safe-mode.js +1 -0
  142. package/dist/shared/session-metadata.js +4 -3
  143. package/package.json +6 -4
  144. package/client/assets/index-C2eIILoa.css +0 -41
  145. package/client/assets/index-DUPZ_Lw5.js +0 -616
  146. package/dist/server/lib/act-runtime.js +0 -1282
  147. package/dist/server/lib/prompt.js +0 -222
  148. package/dist/server/routes/stages.js +0 -137
@@ -1,6 +1,15 @@
1
1
  import { getOpencode } from './opencode.js';
2
2
  import { readStoredProviderAuthType } from './opencode-auth.js';
3
3
  import { normalizeRuntimeVariants, } from '../../shared/model-variants.js';
4
+ function responseData(response) {
5
+ if (!response || typeof response !== 'object' || !('data' in response)) {
6
+ return undefined;
7
+ }
8
+ return response.data;
9
+ }
10
+ function asRecord(value) {
11
+ return value && typeof value === 'object' ? value : {};
12
+ }
4
13
  const incompatibleModelsByAuthType = {
5
14
  openai: {
6
15
  // ChatGPT account-backed OpenAI auth rejects these at runtime today.
@@ -11,9 +20,7 @@ const incompatibleModelsByAuthType = {
11
20
  },
12
21
  };
13
22
  function readCapabilityFlag(model, ...keys) {
14
- const capabilityRecord = model.capabilities && typeof model.capabilities === 'object'
15
- ? model.capabilities
16
- : {};
23
+ const capabilityRecord = asRecord(model.capabilities);
17
24
  for (const key of keys) {
18
25
  if (typeof capabilityRecord[key] === 'boolean') {
19
26
  return capabilityRecord[key];
@@ -25,18 +32,17 @@ function readCapabilityFlag(model, ...keys) {
25
32
  return false;
26
33
  }
27
34
  function readModalities(model) {
28
- const capabilityRecord = model.capabilities && typeof model.capabilities === 'object'
29
- ? model.capabilities
30
- : {};
35
+ const capabilityRecord = asRecord(model.capabilities);
36
+ const modalityRecord = asRecord(model.modalities);
31
37
  const input = Array.isArray(capabilityRecord.input)
32
38
  ? capabilityRecord.input.filter((value) => typeof value === 'string')
33
- : Array.isArray(model.modalities?.input)
34
- ? model.modalities.input.filter((value) => typeof value === 'string')
39
+ : Array.isArray(modalityRecord.input)
40
+ ? modalityRecord.input.filter((value) => typeof value === 'string')
35
41
  : ['text'];
36
42
  const output = Array.isArray(capabilityRecord.output)
37
43
  ? capabilityRecord.output.filter((value) => typeof value === 'string')
38
- : Array.isArray(model.modalities?.output)
39
- ? model.modalities.output.filter((value) => typeof value === 'string')
44
+ : Array.isArray(modalityRecord.output)
45
+ ? modalityRecord.output.filter((value) => typeof value === 'string')
40
46
  : ['text'];
41
47
  return { input, output };
42
48
  }
@@ -50,10 +56,62 @@ function isModelVisibleForAuthType(providerId, modelId, authType) {
50
56
  }
51
57
  return !blocked.has(modelId);
52
58
  }
59
+ // ── Cached provider.list() ──────────────────────────────
60
+ // Both /api/providers and /api/models need the same raw data from
61
+ // oc.provider.list(). We cache for a short window to avoid duplicate
62
+ // round trips when the two routes are hit close together (which is the
63
+ // common case — the client fetches both on Settings open / refresh).
64
+ const CACHE_TTL_MS = 3_000;
65
+ let _cachedPromise = null;
66
+ let _cachedCwd = null;
67
+ let _cacheTs = 0;
68
+ /**
69
+ * Fetch the raw oc.provider.list() data with a short TTL cache
70
+ * keyed on the working directory.
71
+ */
72
+ export async function fetchProviderListData(cwd) {
73
+ const now = Date.now();
74
+ if (_cachedPromise && _cachedCwd === cwd && now - _cacheTs < CACHE_TTL_MS) {
75
+ return _cachedPromise;
76
+ }
77
+ _cachedCwd = cwd;
78
+ _cacheTs = now;
79
+ _cachedPromise = (async () => {
80
+ const oc = await getOpencode();
81
+ const res = await oc.provider.list({ directory: cwd });
82
+ return responseData(res);
83
+ })();
84
+ // On failure, clear the cache so the next call retries immediately.
85
+ _cachedPromise.catch(() => {
86
+ _cachedPromise = null;
87
+ });
88
+ return _cachedPromise;
89
+ }
90
+ /** Invalidate the cache (e.g. after auth changes). */
91
+ export function invalidateProviderListCache() {
92
+ _cachedPromise = null;
93
+ _cachedCwd = null;
94
+ _cacheTs = 0;
95
+ }
96
+ export async function listProviderSummaries(cwd) {
97
+ const data = await fetchProviderListData(cwd);
98
+ if (!data?.all || !Array.isArray(data.all)) {
99
+ return [];
100
+ }
101
+ const connected = new Set((data?.connected || []));
102
+ return data.all.map((provider) => ({
103
+ id: typeof provider.id === 'string' ? provider.id : '',
104
+ name: typeof provider.name === 'string' ? provider.name : (typeof provider.id === 'string' ? provider.id : ''),
105
+ source: typeof provider.source === 'string' ? provider.source : 'builtin',
106
+ env: Array.isArray(provider.env) ? provider.env.filter((value) => typeof value === 'string') : [],
107
+ connected: typeof provider.id === 'string' ? connected.has(provider.id) : false,
108
+ modelCount: provider.models ? Object.keys(provider.models).length : 0,
109
+ defaultModel: typeof provider.id === 'string' ? data?.default?.[provider.id] || null : null,
110
+ }));
111
+ }
112
+ // ── Model catalog (used by /api/models) ─────────────────
53
113
  export async function listRuntimeModels(cwd) {
54
- const oc = await getOpencode();
55
- const res = await oc.provider.list({ directory: cwd });
56
- const data = res.data;
114
+ const data = await fetchProviderListData(cwd);
57
115
  if (!data?.all || !Array.isArray(data.all)) {
58
116
  return [];
59
117
  }
@@ -82,14 +140,15 @@ export async function listRuntimeModels(cwd) {
82
140
  if (!isModelVisibleForAuthType(providerId, id, authType)) {
83
141
  continue;
84
142
  }
143
+ const limitRecord = asRecord(record.limit);
85
144
  models.push({
86
145
  provider: providerId,
87
146
  providerName,
88
147
  id,
89
148
  name: typeof record.name === 'string' ? record.name : id,
90
149
  connected,
91
- context: Number(record.limit?.context || 0),
92
- output: Number(record.limit?.output || 0),
150
+ context: Number(limitRecord.context || 0),
151
+ output: Number(limitRecord.output || 0),
93
152
  toolCall: readCapabilityFlag(record, 'toolcall', 'toolCall', 'tool_call'),
94
153
  reasoning: readCapabilityFlag(record, 'reasoning'),
95
154
  attachment: readCapabilityFlag(record, 'attachment'),
@@ -1,6 +1,9 @@
1
1
  import fs from 'fs/promises';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
+ function isErrnoException(error) {
5
+ return error instanceof Error;
6
+ }
4
7
  function authStoreCandidates() {
5
8
  const home = os.homedir();
6
9
  const candidates = [
@@ -55,7 +58,7 @@ export async function clearStoredProviderAuth(providerId) {
55
58
  current = JSON.parse(raw);
56
59
  }
57
60
  catch (error) {
58
- if (error?.code !== 'ENOENT') {
61
+ if (!isErrnoException(error) || error.code !== 'ENOENT') {
59
62
  throw error;
60
63
  }
61
64
  }
@@ -1,3 +1,25 @@
1
+ function asRecord(value) {
2
+ return value && typeof value === 'object' ? value : undefined;
3
+ }
4
+ function readPath(value, ...keys) {
5
+ let current = value;
6
+ for (const key of keys) {
7
+ const record = asRecord(current);
8
+ if (!record) {
9
+ return undefined;
10
+ }
11
+ current = record[key];
12
+ }
13
+ return current;
14
+ }
15
+ function readString(value, ...keys) {
16
+ const candidate = readPath(value, ...keys);
17
+ return typeof candidate === 'string' ? candidate : undefined;
18
+ }
19
+ function readNumber(value, ...keys) {
20
+ const candidate = readPath(value, ...keys);
21
+ return typeof candidate === 'number' && Number.isFinite(candidate) ? candidate : undefined;
22
+ }
1
23
  export class StudioValidationError extends Error {
2
24
  action;
3
25
  status;
@@ -10,16 +32,16 @@ export class StudioValidationError extends Error {
10
32
  }
11
33
  function extractStatus(err) {
12
34
  const candidates = [
13
- err?.status,
14
- err?.statusCode,
15
- err?.data?.statusCode,
16
- err?.response?.status,
17
- err?.cause?.status,
18
- err?.cause?.statusCode,
19
- err?.cause?.response?.status,
35
+ readNumber(err, 'status'),
36
+ readNumber(err, 'statusCode'),
37
+ readNumber(err, 'data', 'statusCode'),
38
+ readNumber(err, 'response', 'status'),
39
+ readNumber(err, 'cause', 'status'),
40
+ readNumber(err, 'cause', 'statusCode'),
41
+ readNumber(err, 'cause', 'response', 'status'),
20
42
  ];
21
43
  for (const candidate of candidates) {
22
- if (typeof candidate === 'number' && Number.isFinite(candidate)) {
44
+ if (candidate !== undefined) {
23
45
  return candidate;
24
46
  }
25
47
  }
@@ -31,13 +53,13 @@ function extractBodyMessage(body) {
31
53
  }
32
54
  try {
33
55
  const parsed = JSON.parse(body);
34
- if (parsed && typeof parsed === 'object') {
35
- if (typeof parsed.error === 'string' && parsed.error.trim()) {
36
- return parsed.error.trim();
37
- }
38
- if (typeof parsed.message === 'string' && parsed.message.trim()) {
39
- return parsed.message.trim();
40
- }
56
+ const error = readString(parsed, 'error');
57
+ if (error?.trim()) {
58
+ return error.trim();
59
+ }
60
+ const message = readString(parsed, 'message');
61
+ if (message?.trim()) {
62
+ return message.trim();
41
63
  }
42
64
  }
43
65
  catch {
@@ -47,20 +69,31 @@ function extractBodyMessage(body) {
47
69
  }
48
70
  function extractMessage(err) {
49
71
  const message = [
50
- err?.data?.message,
51
- err?.message,
52
- err?.error?.message,
53
- err?.cause?.data?.message,
54
- err?.cause?.message,
55
- extractBodyMessage(err?.data?.responseBody),
56
- extractBodyMessage(err?.responseBody),
72
+ readString(err, 'data', 'message'),
73
+ readString(err, 'message'),
74
+ readString(err, 'error', 'message'),
75
+ readString(err, 'cause', 'data', 'message'),
76
+ readString(err, 'cause', 'message'),
77
+ extractBodyMessage(readPath(err, 'data', 'responseBody')),
78
+ extractBodyMessage(readPath(err, 'responseBody')),
57
79
  ].find((candidate) => typeof candidate === 'string' && candidate.trim().length > 0);
58
- return message || 'OpenCode request failed.';
80
+ if (message) {
81
+ return message;
82
+ }
83
+ // Stringify raw error for debuggability — truncate if huge
84
+ try {
85
+ const raw = JSON.stringify(err, null, 2);
86
+ const truncated = raw.length > 500 ? raw.slice(0, 500) + '…' : raw;
87
+ return `OpenCode request failed. Raw: ${truncated}`;
88
+ }
89
+ catch {
90
+ return 'OpenCode request failed.';
91
+ }
59
92
  }
60
93
  function extractProviderId(err, context) {
61
94
  return context.providerId?.trim()
62
- || err?.data?.providerID
63
- || err?.providerId
95
+ || readString(err, 'data', 'providerID')
96
+ || readString(err, 'providerId')
64
97
  || context.model?.provider
65
98
  || undefined;
66
99
  }
@@ -103,17 +136,16 @@ export function normalizeOpencodeError(err, context = {}) {
103
136
  status: err.status,
104
137
  };
105
138
  }
106
- const raw = err;
107
- const name = typeof raw?.name === 'string'
108
- ? raw.name
109
- : typeof raw?.error?.name === 'string'
110
- ? raw.error.name
139
+ const name = typeof readString(err, 'name') === 'string'
140
+ ? readString(err, 'name')
141
+ : typeof readString(err, 'error', 'name') === 'string'
142
+ ? readString(err, 'error', 'name')
111
143
  : 'UnknownError';
112
- const detail = extractMessage(raw);
113
- const status = extractStatus(raw);
114
- const providerId = extractProviderId(raw, context);
144
+ const detail = extractMessage(err);
145
+ const status = extractStatus(err);
146
+ const providerId = extractProviderId(err, context);
115
147
  const modelId = context.model?.modelId;
116
- const retryable = raw?.data?.isRetryable === true || (!!status && status >= 500);
148
+ const retryable = readPath(err, 'data', 'isRetryable') === true || (!!status && status >= 500);
117
149
  if (isProviderAuthError(name, detail, status)) {
118
150
  return {
119
151
  error: `Provider authentication is missing or expired${providerId ? ` for ${providerId}` : ''}. Reconnect it in Settings and try again.`,
@@ -202,14 +234,14 @@ export function jsonOpencodeError(c, err, context = {}) {
202
234
  return c.json(payload, payload.status);
203
235
  }
204
236
  export function unwrapOpencodeResult(result) {
205
- const value = result;
206
- if (value && typeof value === 'object' && 'error' in value && value.error) {
237
+ const value = asRecord(result);
238
+ if (value && 'error' in value && value.error) {
207
239
  throw value.error;
208
240
  }
209
- if (value && typeof value === 'object' && 'data' in value) {
241
+ if (value && 'data' in value) {
210
242
  return value.data;
211
243
  }
212
- return value;
244
+ return result;
213
245
  }
214
246
  export function unwrapPromptResult(result) {
215
247
  const data = unwrapOpencodeResult(result);
@@ -1,6 +1,6 @@
1
1
  import { spawn } from 'child_process';
2
2
  import path from 'path';
3
- import { DEFAULT_PROJECT_DIR, OPENCODE_MANAGED, OPENCODE_URL } from './config.js';
3
+ import { DEFAULT_PROJECT_DIR, OPENCODE_MANAGED, OPENCODE_URL, STUDIO_DIR } from './config.js';
4
4
  import { resolvePackageBin } from './package-bin.js';
5
5
  const STARTUP_TIMEOUT_MS = 15_000;
6
6
  const HEALTHCHECK_INTERVAL_MS = 250;
@@ -105,7 +105,10 @@ export async function ensureOpencodeSidecar() {
105
105
  }
106
106
  const opencode = spawn(resolveCommand(), ['--port', String(resolvePort()), path.resolve(DEFAULT_PROJECT_DIR)], {
107
107
  cwd: path.resolve(DEFAULT_PROJECT_DIR),
108
- env: process.env,
108
+ env: {
109
+ ...process.env,
110
+ OPENCODE_CONFIG_DIR: path.join(STUDIO_DIR, 'opencode'),
111
+ },
109
112
  stdio: 'ignore',
110
113
  });
111
114
  child = opencode;
@@ -25,6 +25,9 @@ export function summarizeProjectMcpCatalog(catalog, liveStatus) {
25
25
  const config = catalog[name];
26
26
  const live = liveStatus[name];
27
27
  const status = live?.status || (config ? (projectMcpEntryEnabled(config) ? 'disconnected' : 'disabled') : 'unknown');
28
+ const oauthConfig = config && 'type' in config && config.type === 'remote'
29
+ ? config.oauth
30
+ : undefined;
28
31
  return {
29
32
  name,
30
33
  status,
@@ -34,6 +37,11 @@ export function summarizeProjectMcpCatalog(catalog, liveStatus) {
34
37
  defined: !!config,
35
38
  configType: projectMcpEntryType(config),
36
39
  authStatus: status === 'needs_auth' ? 'needs_auth' : status === 'connected' ? 'ready' : 'n/a',
40
+ error: typeof live?.error === 'string' ? live.error : undefined,
41
+ oauthConfigured: !!(oauthConfig
42
+ && typeof oauthConfig === 'object'
43
+ && (oauthConfig.clientId || oauthConfig.clientSecret || oauthConfig.scope)),
44
+ clientRegistrationRequired: status === 'needs_client_registration',
37
45
  };
38
46
  });
39
47
  }
@@ -4,6 +4,23 @@ import { readProjectMcpCatalog } from './project-config.js';
4
4
  function unique(values) {
5
5
  return Array.from(new Set(values.filter(Boolean)));
6
6
  }
7
+ function responseData(response, fallback) {
8
+ if (!response || typeof response !== 'object' || !('data' in response)) {
9
+ return fallback;
10
+ }
11
+ return (response.data ?? fallback);
12
+ }
13
+ function errorMessage(error, fallback) {
14
+ return error instanceof Error && error.message ? error.message : fallback;
15
+ }
16
+ function toolNames(entry) {
17
+ return (entry?.tools || []).map((tool) => {
18
+ if (!tool || typeof tool !== 'object' || !('name' in tool) || typeof tool.name !== 'string') {
19
+ return '';
20
+ }
21
+ return tool.name;
22
+ }).filter(Boolean);
23
+ }
7
24
  export function describeUnavailableRuntimeTools(resolution) {
8
25
  if (resolution.selectedMcpServers.length === 0 || resolution.unavailableDetails.length === 0) {
9
26
  return null;
@@ -15,6 +32,9 @@ export function describeUnavailableRuntimeTools(resolution) {
15
32
  if (detail.reason === 'needs_auth') {
16
33
  return `${detail.serverName}: authentication required`;
17
34
  }
35
+ if (detail.reason === 'needs_client_registration') {
36
+ return `${detail.serverName}: OAuth client registration required`;
37
+ }
18
38
  if (detail.reason === 'disabled') {
19
39
  return `${detail.serverName}: disabled in project config`;
20
40
  }
@@ -47,7 +67,7 @@ function emptyResolution(selectedMcpServers) {
47
67
  }
48
68
  async function currentMcpStatus(oc, cwd) {
49
69
  const res = await oc.mcp.status({ directory: cwd });
50
- return (res.data || {});
70
+ return responseData(res, {});
51
71
  }
52
72
  async function ensureConnectedServer(oc, cwd, serverName, catalog, statusMap) {
53
73
  const config = catalog[serverName];
@@ -88,6 +108,16 @@ async function ensureConnectedServer(oc, cwd, serverName, catalog, statusMap) {
88
108
  },
89
109
  };
90
110
  }
111
+ if (current?.status === 'needs_client_registration') {
112
+ return {
113
+ statusMap,
114
+ unavailable: {
115
+ serverName,
116
+ reason: 'needs_client_registration',
117
+ detail: current?.error || 'Server requires OAuth client registration before it can connect.',
118
+ },
119
+ };
120
+ }
91
121
  try {
92
122
  await oc.mcp.connect({
93
123
  name: serverName,
@@ -100,7 +130,7 @@ async function ensureConnectedServer(oc, cwd, serverName, catalog, statusMap) {
100
130
  unavailable: {
101
131
  serverName,
102
132
  reason: 'connect_failed',
103
- detail: error?.message || 'Connection attempt failed.',
133
+ detail: errorMessage(error, 'Connection attempt failed.'),
104
134
  },
105
135
  };
106
136
  }
@@ -122,6 +152,16 @@ async function ensureConnectedServer(oc, cwd, serverName, catalog, statusMap) {
122
152
  },
123
153
  };
124
154
  }
155
+ if (next?.status === 'needs_client_registration') {
156
+ return {
157
+ statusMap: refreshed,
158
+ unavailable: {
159
+ serverName,
160
+ reason: 'needs_client_registration',
161
+ detail: next?.error || 'Server requires OAuth client registration before it can connect.',
162
+ },
163
+ };
164
+ }
125
165
  return {
126
166
  statusMap: refreshed,
127
167
  unavailable: {
@@ -147,8 +187,7 @@ export async function resolveRuntimeTools(cwd, model, mcpServerNames) {
147
187
  unavailableDetails.push(ensured.unavailable);
148
188
  }
149
189
  }
150
- const requestedTools = unique(selectedMcpServers.flatMap((serverName) => (mcpStatus[serverName]?.tools || [])
151
- .map((tool) => tool.name || '')));
190
+ const requestedTools = unique(selectedMcpServers.flatMap((serverName) => toolNames(mcpStatus[serverName])));
152
191
  if (requestedTools.length === 0) {
153
192
  return {
154
193
  ...emptyResolution(selectedMcpServers),
@@ -162,22 +201,21 @@ export async function resolveRuntimeTools(cwd, model, mcpServerNames) {
162
201
  model: model.modelId,
163
202
  directory: cwd,
164
203
  });
165
- const items = (toolListRes.data || []);
204
+ const items = responseData(toolListRes, []);
166
205
  availableTools = unique(items.map((item) => item.id || ''));
167
206
  }
168
207
  else {
169
208
  const toolIdsRes = await oc.tool.ids({
170
209
  directory: cwd,
171
210
  });
172
- availableTools = unique((toolIdsRes.data || []));
211
+ availableTools = unique(responseData(toolIdsRes, []));
173
212
  }
174
213
  const availableSet = new Set(availableTools);
175
214
  const resolvedTools = requestedTools.filter((toolId) => availableSet.has(toolId));
176
215
  const unavailableTools = requestedTools.filter((toolId) => !availableSet.has(toolId));
177
216
  const toolServerNames = new Map();
178
217
  for (const serverName of selectedMcpServers) {
179
- for (const tool of (mcpStatus[serverName]?.tools || [])) {
180
- const toolId = tool.name || '';
218
+ for (const toolId of toolNames(mcpStatus[serverName])) {
181
219
  if (!toolId)
182
220
  continue;
183
221
  const current = toolServerNames.get(toolId) || [];