dot-studio 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -200
- package/client/assets/ActFrame-BYOBkLYW.js +1 -0
- package/client/assets/ActFrame-C_WEt6bv.css +1 -0
- package/client/assets/ActInspectorPanel-C3VlS7tB.js +1 -0
- package/client/assets/ActInspectorPanel-CE6s6GYv.css +1 -0
- package/client/assets/AssistantChat-BOyW0K79.js +1 -0
- package/client/assets/AssistantChat-DoVmHvMJ.css +1 -0
- package/client/assets/CanvasTerminalFrame-BC-79q9U.css +1 -0
- package/client/assets/CanvasTerminalFrame-DxKbexK6.js +4 -0
- package/client/assets/CanvasTrackingFrame-DumxhNwg.js +1 -0
- package/client/assets/CanvasTrackingFrame-G4rRrfne.css +1 -0
- package/client/assets/CanvasWindowFrame-ziJeVfHG.js +1 -0
- package/client/assets/DanceBundleEditorFrame-CH8VDUMK.js +1 -0
- package/client/assets/DanceBundleEditorFrame-DaLqMflT.css +1 -0
- package/client/assets/MarkdownEditorFrame-DVecIZpZ.css +1 -0
- package/client/assets/MarkdownEditorFrame-Dwpgs2GX.js +2 -0
- package/client/assets/MarkdownRenderer-Cz8A4AgP.js +1 -0
- package/client/assets/PublishModal-DUlHz0fT.js +1 -0
- package/client/assets/TodoDock-DcVf7zQG.js +1 -0
- package/client/assets/WorkspaceToolbar-CXYi_sMD.js +2 -0
- package/client/assets/WorkspaceToolbar-CiQvVocC.css +1 -0
- package/client/assets/chat-message-visibility-YwJ-AQno.js +11 -0
- package/client/assets/dnd-vendor-CIAZE2P2.js +5 -0
- package/client/assets/flow-vendor-BZV40eAE.css +1 -0
- package/client/assets/flow-vendor-C868rU-6.js +23 -0
- package/client/assets/icon-vendor-I2JVIi1s.js +501 -0
- package/client/assets/index-BMY4hrBP.js +3 -0
- package/client/assets/index-C-vnj9y3.js +1 -0
- package/client/assets/index-C9HTqfZw.css +1 -0
- package/client/assets/index-CWrv6O3o.js +64 -0
- package/client/assets/index-DMS12-Q2.js +8 -0
- package/client/assets/index-Dn7t_Y7G.js +1 -0
- package/client/assets/index-p-wk7iGH.css +1 -0
- package/client/assets/markdown-vendor-BSTcku12.css +10 -0
- package/client/assets/markdown-vendor-DnTJ9hmR.js +35 -0
- package/client/assets/participant-labels-Cf3qP3GB.js +1 -0
- package/client/assets/queries-Dm1jEHfc.js +1 -0
- package/client/assets/query-vendor-_taqgrbn.js +1 -0
- package/client/assets/react-vendor-DzpMUNDT.js +49 -0
- package/client/assets/settings-utils-l7KCS3Ev.js +1 -0
- package/client/assets/terminal-vendor-6GBZ9nXN.css +32 -0
- package/client/assets/terminal-vendor-D0xRnmbI.js +112 -0
- package/client/index.html +13 -3
- package/dist/cli.js +25 -3
- package/dist/server/app.js +72 -0
- package/dist/server/index.js +2 -62
- package/dist/server/lib/act-session-policy.js +31 -0
- package/dist/server/lib/chat-session.js +101 -0
- package/dist/server/lib/config.js +18 -4
- package/dist/server/lib/dot-authoring.js +171 -102
- package/dist/server/lib/dot-loader.js +9 -8
- package/dist/server/lib/dot-login.js +8 -190
- package/dist/server/lib/dot-source.js +11 -0
- package/dist/server/lib/model-catalog.js +74 -15
- package/dist/server/lib/opencode-auth.js +4 -1
- package/dist/server/lib/opencode-errors.js +70 -38
- package/dist/server/lib/opencode-sidecar.js +5 -2
- package/dist/server/lib/project-config.js +8 -0
- package/dist/server/lib/runtime-tools.js +46 -8
- package/dist/server/lib/safe-mode.js +410 -0
- package/dist/server/lib/session-execution.js +81 -0
- package/dist/server/lib/sse.js +22 -0
- package/dist/server/routes/act-runtime-threads.js +156 -0
- package/dist/server/routes/act-runtime-tools.js +157 -0
- package/dist/server/routes/act-runtime.js +7 -0
- package/dist/server/routes/adapter.js +32 -0
- package/dist/server/routes/assets-collection.js +16 -0
- package/dist/server/routes/assets-detail.js +38 -0
- package/dist/server/routes/assets.js +4 -158
- package/dist/server/routes/chat-messages.js +104 -0
- package/dist/server/routes/chat-sessions.js +104 -0
- package/dist/server/routes/chat-stream.js +15 -0
- package/dist/server/routes/chat.js +6 -353
- package/dist/server/routes/compile.js +5 -91
- package/dist/server/routes/dot-assets.js +77 -0
- package/dist/server/routes/dot-core.js +62 -0
- package/dist/server/routes/dot-performer.js +80 -0
- package/dist/server/routes/dot.js +6 -267
- package/dist/server/routes/drafts-collection.js +40 -0
- package/dist/server/routes/drafts-dance-bundle.js +113 -0
- package/dist/server/routes/drafts-item.js +86 -0
- package/dist/server/routes/drafts.js +9 -0
- package/dist/server/routes/health.js +18 -33
- package/dist/server/routes/opencode-core.js +120 -0
- package/dist/server/routes/opencode-file.js +67 -0
- package/dist/server/routes/opencode-mcp.js +74 -0
- package/dist/server/routes/opencode-provider.js +41 -0
- package/dist/server/routes/opencode.js +8 -418
- package/dist/server/routes/route-errors.js +10 -0
- package/dist/server/routes/safe-actions.js +60 -0
- package/dist/server/routes/safe-summary.js +20 -0
- package/dist/server/routes/safe.js +7 -0
- package/dist/server/routes/workspaces.js +47 -0
- package/dist/server/services/act-runtime/act-context-builder.js +81 -0
- package/dist/server/services/act-runtime/act-runtime-service.js +313 -0
- package/dist/server/services/act-runtime/act-runtime-utils.js +10 -0
- package/dist/server/services/act-runtime/act-tool-projection.js +26 -0
- package/dist/server/services/act-runtime/act-tools.js +151 -0
- package/dist/server/services/act-runtime/board-persistence.js +38 -0
- package/dist/server/services/act-runtime/event-logger.js +73 -0
- package/dist/server/services/act-runtime/event-router.js +102 -0
- package/dist/server/services/act-runtime/mailbox.js +149 -0
- package/dist/server/services/act-runtime/safety-guard.js +162 -0
- package/dist/server/services/act-runtime/session-queue.js +114 -0
- package/dist/server/services/act-runtime/thread-manager.js +351 -0
- package/dist/server/services/act-runtime/wake-cascade.js +306 -0
- package/dist/server/services/act-runtime/wake-evaluator.js +43 -0
- package/dist/server/services/act-runtime/wake-performer-resolver.js +68 -0
- package/dist/server/services/act-runtime/wake-prompt-builder.js +77 -0
- package/dist/server/services/adapter-view-service.js +6 -0
- package/dist/server/services/asset-service.js +366 -0
- package/dist/server/services/chat-event-stream-service.js +157 -0
- package/dist/server/services/chat-service.js +207 -0
- package/dist/server/services/chat-session-service.js +203 -0
- package/dist/server/services/compile-service.js +4 -0
- package/dist/server/services/dance-bundle-service.js +222 -0
- package/dist/server/services/dot-add-service.js +59 -0
- package/dist/server/services/dot-service.js +178 -0
- package/dist/server/services/draft-service.js +367 -0
- package/dist/server/services/opencode-projection/dance-compiler.js +164 -0
- package/dist/server/services/opencode-projection/performer-compiler.js +195 -0
- package/dist/server/services/opencode-projection/preview-service.js +31 -0
- package/dist/server/services/opencode-projection/projection-manifest.js +98 -0
- package/dist/server/services/opencode-projection/stage-projection-service.js +188 -0
- package/dist/server/services/opencode-service.js +338 -0
- package/dist/server/services/safe-service.js +33 -0
- package/dist/server/services/studio-assistant/assistant-service.js +172 -0
- package/dist/server/services/studio-service.js +69 -0
- package/dist/server/services/workspace-service.js +224 -0
- package/dist/server/terminal.js +57 -11
- package/dist/shared/act-types.js +4 -0
- package/dist/shared/adapter-view.js +1 -0
- package/dist/shared/asset-contracts.js +1 -0
- package/dist/shared/assistant-actions.js +1 -0
- package/dist/shared/chat-contracts.js +1 -0
- package/dist/shared/dot-contracts.js +1 -0
- package/dist/shared/dot-types.js +4 -0
- package/dist/shared/draft-contracts.js +2 -0
- package/dist/shared/model-types.js +2 -0
- package/dist/shared/performer-mcp-portability.js +10 -0
- package/dist/shared/safe-mode.js +1 -0
- package/dist/shared/session-metadata.js +4 -3
- package/package.json +7 -4
- package/client/assets/index-C2eIILoa.css +0 -41
- package/client/assets/index-DUPZ_Lw5.js +0 -616
- package/dist/server/lib/act-runtime.js +0 -1282
- package/dist/server/lib/prompt.js +0 -222
- package/dist/server/routes/stages.js +0 -137
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode-service.ts – Shared helpers for OpenCode SDK route handlers.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `routes/opencode.ts` to keep route handlers thin.
|
|
5
|
+
* Contains: response unwrapping, config merging, MCP mutation runner,
|
|
6
|
+
* MCP auth validation, and health meta.
|
|
7
|
+
*/
|
|
8
|
+
import { getOpencode } from '../lib/opencode.js';
|
|
9
|
+
import { invalidate } from '../lib/cache.js';
|
|
10
|
+
import { OPENCODE_URL } from '../lib/config.js';
|
|
11
|
+
import { isManagedOpencode, canRestartOpencodeSidecar, restartOpencodeSidecar } from '../lib/opencode-sidecar.js';
|
|
12
|
+
import { StudioValidationError, unwrapOpencodeResult } from '../lib/opencode-errors.js';
|
|
13
|
+
import { readProjectConfigFile, readProjectMcpCatalog, summarizeProjectMcpCatalog } from '../lib/project-config.js';
|
|
14
|
+
import { projectMcpEntryEnabled } from '../../shared/project-mcp.js';
|
|
15
|
+
import { invalidateProviderListCache } from '../lib/model-catalog.js';
|
|
16
|
+
import { clearStoredProviderAuth } from '../lib/opencode-auth.js';
|
|
17
|
+
function extractResponseData(response) {
|
|
18
|
+
if (!response || typeof response !== 'object' || !('data' in response)) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return response.data ?? undefined;
|
|
22
|
+
}
|
|
23
|
+
// ── Response helpers ────────────────────────────────────
|
|
24
|
+
export function opencodeModeMeta() {
|
|
25
|
+
return {
|
|
26
|
+
managed: isManagedOpencode(),
|
|
27
|
+
mode: isManagedOpencode() ? 'managed' : 'external',
|
|
28
|
+
restartAvailable: canRestartOpencodeSidecar(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function responseData(response, fallback) {
|
|
32
|
+
const data = extractResponseData(response);
|
|
33
|
+
return (data || fallback);
|
|
34
|
+
}
|
|
35
|
+
function isProviderAuthInput(value) {
|
|
36
|
+
if (!value || typeof value !== 'object')
|
|
37
|
+
return false;
|
|
38
|
+
const auth = value;
|
|
39
|
+
if (auth.type === 'oauth') {
|
|
40
|
+
return typeof auth.refresh === 'string' && typeof auth.access === 'string' && typeof auth.expires === 'number';
|
|
41
|
+
}
|
|
42
|
+
if (auth.type === 'api') {
|
|
43
|
+
return typeof auth.key === 'string';
|
|
44
|
+
}
|
|
45
|
+
if (auth.type === 'wellknown') {
|
|
46
|
+
return typeof auth.key === 'string' && typeof auth.token === 'string';
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
function normalizeMcpServerConfig(config) {
|
|
51
|
+
if ('url' in config) {
|
|
52
|
+
return {
|
|
53
|
+
type: 'remote',
|
|
54
|
+
url: config.url,
|
|
55
|
+
enabled: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
type: 'local',
|
|
60
|
+
command: [config.command, ...(config.args || [])],
|
|
61
|
+
environment: config.env,
|
|
62
|
+
enabled: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// ── Read-only OpenCode queries ─────────────────────────
|
|
66
|
+
export async function getOpenCodeHealth(directory) {
|
|
67
|
+
const oc = await getOpencode();
|
|
68
|
+
const res = await oc.project.current({ directory });
|
|
69
|
+
const data = responseData(res, null);
|
|
70
|
+
return {
|
|
71
|
+
connected: true,
|
|
72
|
+
url: OPENCODE_URL,
|
|
73
|
+
project: data,
|
|
74
|
+
...opencodeModeMeta(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function getOpenCodeUnavailableHealth(error) {
|
|
78
|
+
return {
|
|
79
|
+
connected: false,
|
|
80
|
+
error: error.message,
|
|
81
|
+
url: OPENCODE_URL,
|
|
82
|
+
...opencodeModeMeta(),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export async function listOpenCodeAgents(directory) {
|
|
86
|
+
const oc = await getOpencode();
|
|
87
|
+
const res = await oc.app.agents({ directory });
|
|
88
|
+
return responseData(res, []);
|
|
89
|
+
}
|
|
90
|
+
export async function listOpenCodeToolIds(directory) {
|
|
91
|
+
const oc = await getOpencode();
|
|
92
|
+
const res = await oc.tool.ids({ directory });
|
|
93
|
+
return responseData(res, []);
|
|
94
|
+
}
|
|
95
|
+
export async function listOpenCodeToolsForModel(directory, provider, model) {
|
|
96
|
+
const oc = await getOpencode();
|
|
97
|
+
const res = await oc.tool.list({
|
|
98
|
+
directory,
|
|
99
|
+
provider,
|
|
100
|
+
model,
|
|
101
|
+
});
|
|
102
|
+
return responseData(res, []);
|
|
103
|
+
}
|
|
104
|
+
export async function getOpenCodeConfig(directory) {
|
|
105
|
+
const oc = await getOpencode();
|
|
106
|
+
const res = await oc.config.get({ directory });
|
|
107
|
+
return responseData(res, {});
|
|
108
|
+
}
|
|
109
|
+
export async function getProviderAuthStatus(directory) {
|
|
110
|
+
const oc = await getOpencode();
|
|
111
|
+
return unwrapOpencodeResult(await oc.provider.auth({ directory })) || {};
|
|
112
|
+
}
|
|
113
|
+
export async function getLspStatus(directory) {
|
|
114
|
+
const oc = await getOpencode();
|
|
115
|
+
const res = await oc.lsp.status({ directory });
|
|
116
|
+
return responseData(res, []);
|
|
117
|
+
}
|
|
118
|
+
export async function listFiles(directory, targetPath) {
|
|
119
|
+
const oc = await getOpencode();
|
|
120
|
+
const res = await oc.file.list({ directory, path: targetPath });
|
|
121
|
+
return responseData(res, []);
|
|
122
|
+
}
|
|
123
|
+
export async function readFile(directory, targetPath) {
|
|
124
|
+
const oc = await getOpencode();
|
|
125
|
+
const res = await oc.file.read({ directory, path: targetPath });
|
|
126
|
+
return responseData(res, {});
|
|
127
|
+
}
|
|
128
|
+
export async function getFileStatus(directory) {
|
|
129
|
+
const oc = await getOpencode();
|
|
130
|
+
const res = await oc.file.status({ directory });
|
|
131
|
+
return responseData(res, []);
|
|
132
|
+
}
|
|
133
|
+
export async function findTextInProject(directory, pattern) {
|
|
134
|
+
const oc = await getOpencode();
|
|
135
|
+
const res = await oc.find.text({ directory, pattern });
|
|
136
|
+
return responseData(res, []);
|
|
137
|
+
}
|
|
138
|
+
export async function findFilesInProject(directory, pattern) {
|
|
139
|
+
const oc = await getOpencode();
|
|
140
|
+
const res = await oc.find.files({ directory, query: pattern });
|
|
141
|
+
return responseData(res, []);
|
|
142
|
+
}
|
|
143
|
+
export async function findSymbolsInProject(directory, pattern) {
|
|
144
|
+
const oc = await getOpencode();
|
|
145
|
+
const res = await oc.find.symbols({ directory, query: pattern });
|
|
146
|
+
return responseData(res, []);
|
|
147
|
+
}
|
|
148
|
+
export async function getVcsStatus(directory) {
|
|
149
|
+
const oc = await getOpencode();
|
|
150
|
+
const res = await oc.vcs.get({ directory });
|
|
151
|
+
return responseData(res, {});
|
|
152
|
+
}
|
|
153
|
+
// ── Mutations ──────────────────────────────────────────
|
|
154
|
+
export async function restartManagedOpenCode() {
|
|
155
|
+
await restartOpencodeSidecar();
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
managed: isManagedOpencode(),
|
|
159
|
+
mode: isManagedOpencode() ? 'managed' : 'external',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export async function updateOpenCodeConfig(directory, patch) {
|
|
163
|
+
const oc = await getOpencode();
|
|
164
|
+
const current = await readProjectConfigFile(directory);
|
|
165
|
+
const nextConfig = mergeProjectConfig(current, patch && typeof patch === 'object' ? patch : {});
|
|
166
|
+
const res = await oc.config.update({ directory, config: nextConfig });
|
|
167
|
+
invalidate('mcp-servers');
|
|
168
|
+
return responseData(res, {});
|
|
169
|
+
}
|
|
170
|
+
export async function authorizeProviderOauth(directory, providerId, method) {
|
|
171
|
+
const oc = await getOpencode();
|
|
172
|
+
return unwrapOpencodeResult(await oc.provider.oauth.authorize({
|
|
173
|
+
providerID: providerId,
|
|
174
|
+
directory,
|
|
175
|
+
method,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
export async function completeProviderOauth(directory, providerId, method, code) {
|
|
179
|
+
const oc = await getOpencode();
|
|
180
|
+
const data = unwrapOpencodeResult(await oc.provider.oauth.callback({
|
|
181
|
+
providerID: providerId,
|
|
182
|
+
directory,
|
|
183
|
+
method,
|
|
184
|
+
...(code ? { code } : {}),
|
|
185
|
+
}));
|
|
186
|
+
await oc.global.dispose();
|
|
187
|
+
invalidateProviderListCache();
|
|
188
|
+
return data;
|
|
189
|
+
}
|
|
190
|
+
export async function updateProviderAuth(_directory, providerId, auth) {
|
|
191
|
+
if (!isProviderAuthInput(auth)) {
|
|
192
|
+
throw new StudioValidationError('Invalid provider auth payload.');
|
|
193
|
+
}
|
|
194
|
+
const oc = await getOpencode();
|
|
195
|
+
const data = unwrapOpencodeResult(await oc.auth.set({
|
|
196
|
+
providerID: providerId,
|
|
197
|
+
auth,
|
|
198
|
+
}));
|
|
199
|
+
await oc.global.dispose();
|
|
200
|
+
invalidateProviderListCache();
|
|
201
|
+
return data;
|
|
202
|
+
}
|
|
203
|
+
export async function deleteProviderAuth(_directory, providerId) {
|
|
204
|
+
const oc = await getOpencode();
|
|
205
|
+
await clearStoredProviderAuth(providerId);
|
|
206
|
+
await oc.global.dispose();
|
|
207
|
+
invalidateProviderListCache();
|
|
208
|
+
return { ok: true };
|
|
209
|
+
}
|
|
210
|
+
export async function listMcpServers(directory) {
|
|
211
|
+
return cachedMcpServers(directory);
|
|
212
|
+
}
|
|
213
|
+
async function cachedMcpServers(cwd) {
|
|
214
|
+
const oc = await getOpencode();
|
|
215
|
+
const res = await oc.mcp.status({ directory: cwd });
|
|
216
|
+
const data = responseData(res, {});
|
|
217
|
+
const catalog = await readProjectMcpCatalog(cwd);
|
|
218
|
+
return summarizeProjectMcpCatalog(catalog, data);
|
|
219
|
+
}
|
|
220
|
+
export async function startMcpAuth(directory, name) {
|
|
221
|
+
await validateMcpAuthRequest(directory, name);
|
|
222
|
+
const oc = await getOpencode();
|
|
223
|
+
const data = unwrapOpencodeResult(await oc.mcp.auth.start({
|
|
224
|
+
name,
|
|
225
|
+
directory,
|
|
226
|
+
}));
|
|
227
|
+
invalidate('mcp-servers');
|
|
228
|
+
return data;
|
|
229
|
+
}
|
|
230
|
+
export async function completeMcpAuth(directory, name, code) {
|
|
231
|
+
const oc = await getOpencode();
|
|
232
|
+
const data = unwrapOpencodeResult(await oc.mcp.auth.callback({
|
|
233
|
+
name,
|
|
234
|
+
directory,
|
|
235
|
+
code,
|
|
236
|
+
}));
|
|
237
|
+
invalidate('mcp-servers');
|
|
238
|
+
return data;
|
|
239
|
+
}
|
|
240
|
+
export async function authenticateMcp(directory, name) {
|
|
241
|
+
await validateMcpAuthRequest(directory, name);
|
|
242
|
+
const oc = await getOpencode();
|
|
243
|
+
const data = unwrapOpencodeResult(await oc.mcp.auth.authenticate({
|
|
244
|
+
name,
|
|
245
|
+
directory,
|
|
246
|
+
}));
|
|
247
|
+
invalidate('mcp-servers');
|
|
248
|
+
return data;
|
|
249
|
+
}
|
|
250
|
+
export async function removeMcpAuth(directory, name) {
|
|
251
|
+
const oc = await getOpencode();
|
|
252
|
+
const data = unwrapOpencodeResult(await oc.mcp.auth.remove({
|
|
253
|
+
name,
|
|
254
|
+
directory,
|
|
255
|
+
}));
|
|
256
|
+
invalidate('mcp-servers');
|
|
257
|
+
return data;
|
|
258
|
+
}
|
|
259
|
+
// ── Config ──────────────────────────────────────────────
|
|
260
|
+
export async function readProjectConfigFromOpencode(directory) {
|
|
261
|
+
const oc = await getOpencode();
|
|
262
|
+
const res = await oc.file.read({
|
|
263
|
+
directory,
|
|
264
|
+
path: 'config.json',
|
|
265
|
+
});
|
|
266
|
+
const data = responseData(res, {});
|
|
267
|
+
const raw = typeof data?.content === 'string' ? data.content : '{}';
|
|
268
|
+
return {
|
|
269
|
+
cwd: directory,
|
|
270
|
+
config: JSON.parse(raw),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
export async function readProjectConfigSnapshot(directory) {
|
|
274
|
+
try {
|
|
275
|
+
const { cwd, config } = await readProjectConfigFromOpencode(directory);
|
|
276
|
+
return {
|
|
277
|
+
exists: true,
|
|
278
|
+
path: `${cwd}/config.json`,
|
|
279
|
+
config,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return {
|
|
284
|
+
exists: false,
|
|
285
|
+
path: `${directory}/config.json`,
|
|
286
|
+
config: {},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export function mergeProjectConfig(current, patch) {
|
|
291
|
+
return {
|
|
292
|
+
...current,
|
|
293
|
+
...patch,
|
|
294
|
+
...(patch.mcp && typeof patch.mcp === 'object' ? { mcp: patch.mcp } : {}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
// ── MCP ─────────────────────────────────────────────────
|
|
298
|
+
export async function runMcpMutation(_directory, action) {
|
|
299
|
+
const oc = await getOpencode();
|
|
300
|
+
const result = await action(oc);
|
|
301
|
+
invalidate('mcp-servers');
|
|
302
|
+
return responseData(result, {});
|
|
303
|
+
}
|
|
304
|
+
export async function addMcpServer(directory, input) {
|
|
305
|
+
return runMcpMutation(directory, (oc) => oc.mcp.add({
|
|
306
|
+
directory,
|
|
307
|
+
name: input.name,
|
|
308
|
+
config: normalizeMcpServerConfig(input.config),
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
export async function connectMcpServer(directory, name) {
|
|
312
|
+
return runMcpMutation(directory, (oc) => oc.mcp.connect({
|
|
313
|
+
name,
|
|
314
|
+
directory,
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
export async function disconnectMcpServer(directory, name) {
|
|
318
|
+
return runMcpMutation(directory, (oc) => oc.mcp.disconnect({
|
|
319
|
+
name,
|
|
320
|
+
directory,
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
export async function validateMcpAuthRequest(directory, name) {
|
|
324
|
+
const catalog = await readProjectMcpCatalog(directory);
|
|
325
|
+
const config = catalog[name];
|
|
326
|
+
if (!config) {
|
|
327
|
+
throw new StudioValidationError(`MCP server '${name}' is not defined in this project.`, 'fix_input', 404);
|
|
328
|
+
}
|
|
329
|
+
if (!projectMcpEntryEnabled(config)) {
|
|
330
|
+
throw new StudioValidationError(`MCP server '${name}' is disabled in this project.`, 'fix_input', 400);
|
|
331
|
+
}
|
|
332
|
+
if (!('type' in config) || config.type !== 'remote') {
|
|
333
|
+
throw new StudioValidationError(`MCP server '${name}' does not support OAuth authentication.`, 'fix_input', 400);
|
|
334
|
+
}
|
|
335
|
+
if (config.oauth === false) {
|
|
336
|
+
throw new StudioValidationError(`MCP server '${name}' does not support OAuth authentication.`, 'fix_input', 400);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { applySafeOwnerChanges, discardAllSafeOwnerChanges, discardSafeOwnerFile, getSafeOwnerSummary, undoLastSafeOwnerApply, } from '../lib/safe-mode.js';
|
|
2
|
+
const applyQueues = new Map();
|
|
3
|
+
export function parseSafeOwnerKind(value) {
|
|
4
|
+
return value === 'performer' || value === 'act' ? value : null;
|
|
5
|
+
}
|
|
6
|
+
async function runQueued(workingDir, task) {
|
|
7
|
+
const current = applyQueues.get(workingDir) || Promise.resolve();
|
|
8
|
+
const next = current.catch(() => undefined).then(task);
|
|
9
|
+
applyQueues.set(workingDir, next);
|
|
10
|
+
try {
|
|
11
|
+
return await next;
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
if (applyQueues.get(workingDir) === next) {
|
|
15
|
+
applyQueues.delete(workingDir);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function readSafeOwnerSummary(workingDir, ownerKind, ownerId) {
|
|
20
|
+
return getSafeOwnerSummary(workingDir, ownerKind, ownerId);
|
|
21
|
+
}
|
|
22
|
+
export async function applySafeOwnerSummary(workingDir, ownerKind, ownerId) {
|
|
23
|
+
return runQueued(workingDir, () => applySafeOwnerChanges(workingDir, ownerKind, ownerId));
|
|
24
|
+
}
|
|
25
|
+
export async function discardSafeOwnerSummaryFile(workingDir, ownerKind, ownerId, filePath) {
|
|
26
|
+
return discardSafeOwnerFile(workingDir, ownerKind, ownerId, filePath);
|
|
27
|
+
}
|
|
28
|
+
export async function discardAllSafeOwnerSummaryChanges(workingDir, ownerKind, ownerId) {
|
|
29
|
+
return discardAllSafeOwnerChanges(workingDir, ownerKind, ownerId);
|
|
30
|
+
}
|
|
31
|
+
export async function undoLastSafeOwnerSummaryApply(workingDir, ownerKind, ownerId) {
|
|
32
|
+
return runQueued(workingDir, () => undoLastSafeOwnerApply(workingDir, ownerKind, ownerId));
|
|
33
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* assistant-service.ts — Agent + skill projection for Studio Assistant.
|
|
3
|
+
*
|
|
4
|
+
* Produces:
|
|
5
|
+
* .opencode/agents/dot-studio/studio-assistant.md (agent file)
|
|
6
|
+
* .opencode/skills/dot-studio/studio-assistant-<name>/SKILL.md (one per builtin dance)
|
|
7
|
+
*
|
|
8
|
+
* Tool files are NOT written here — they are injected at send-time via
|
|
9
|
+
* chat-service.ts extraTools to avoid polluting the shared .opencode/tools/ dir.
|
|
10
|
+
*
|
|
11
|
+
* Called eagerly at stage save / project activate — NOT per-send.
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'fs/promises';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { getOpencode } from '../../lib/opencode.js';
|
|
17
|
+
export const ASSISTANT_PERFORMER_ID = 'studio-assistant';
|
|
18
|
+
const AGENT_FILENAME = 'studio-assistant.md';
|
|
19
|
+
// ── Source paths ───────────────────────────────────────
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const TAL_PATH = path.join(__dirname, 'tal', 'studio-assistant.md');
|
|
22
|
+
const DANCES_DIR = path.join(__dirname, 'dances');
|
|
23
|
+
// ── Target paths ──────────────────────────────────────
|
|
24
|
+
function agentFilePath(executionDir) {
|
|
25
|
+
return path.join(executionDir, '.opencode', 'agents', 'dot-studio', AGENT_FILENAME);
|
|
26
|
+
}
|
|
27
|
+
function skillDir(executionDir, skillName) {
|
|
28
|
+
return path.join(executionDir, '.opencode', 'skills', 'dot-studio', skillName);
|
|
29
|
+
}
|
|
30
|
+
function skillFilePath(executionDir, skillName) {
|
|
31
|
+
return path.join(skillDir(executionDir, skillName), 'SKILL.md');
|
|
32
|
+
}
|
|
33
|
+
// ── Read source assets ────────────────────────────────
|
|
34
|
+
async function readTal() {
|
|
35
|
+
return fs.readFile(TAL_PATH, 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
async function readBuiltinSkills() {
|
|
38
|
+
const entries = await fs.readdir(DANCES_DIR, { withFileTypes: true });
|
|
39
|
+
const skills = [];
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
42
|
+
const body = await fs.readFile(path.join(DANCES_DIR, entry.name), 'utf-8');
|
|
43
|
+
const baseName = entry.name.replace(/\.md$/, '');
|
|
44
|
+
const skillName = `studio-assistant-${baseName}`;
|
|
45
|
+
// Extract first heading as description, fallback to file name
|
|
46
|
+
const firstLine = body.trim().split('\n')[0] || '';
|
|
47
|
+
const description = firstLine.startsWith('#')
|
|
48
|
+
? firstLine.replace(/^#+\s*/, '').trim()
|
|
49
|
+
: baseName.replace(/-/g, ' ');
|
|
50
|
+
skills.push({ name: skillName, description, content: body.trim() });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return skills;
|
|
54
|
+
}
|
|
55
|
+
async function removeStaleBuiltinSkills(executionDir, expectedSkillNames) {
|
|
56
|
+
const skillsRoot = path.join(executionDir, '.opencode', 'skills', 'dot-studio');
|
|
57
|
+
const expected = new Set(expectedSkillNames);
|
|
58
|
+
let changed = false;
|
|
59
|
+
const entries = await fs.readdir(skillsRoot, { withFileTypes: true }).catch(() => []);
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!entry.isDirectory())
|
|
62
|
+
continue;
|
|
63
|
+
if (!entry.name.startsWith('studio-assistant-'))
|
|
64
|
+
continue;
|
|
65
|
+
if (expected.has(entry.name))
|
|
66
|
+
continue;
|
|
67
|
+
await fs.rm(path.join(skillsRoot, entry.name), { recursive: true, force: true });
|
|
68
|
+
changed = true;
|
|
69
|
+
}
|
|
70
|
+
return changed;
|
|
71
|
+
}
|
|
72
|
+
// ── Frontmatter ───────────────────────────────────────
|
|
73
|
+
function buildFrontmatter(skillNames) {
|
|
74
|
+
const lines = ['---'];
|
|
75
|
+
lines.push('description: "Studio Assistant"');
|
|
76
|
+
lines.push('mode: primary');
|
|
77
|
+
// Model is NOT specified here — passed via promptAsync() to avoid staleness.
|
|
78
|
+
// Permission-based tool/skill access (tools field is deprecated)
|
|
79
|
+
lines.push('permission:');
|
|
80
|
+
lines.push(' edit:');
|
|
81
|
+
lines.push(' "*": "allow"');
|
|
82
|
+
lines.push(' bash:');
|
|
83
|
+
lines.push(' "*": "allow"');
|
|
84
|
+
// Skill permissions: deny-by-default, allow only our builtin skills
|
|
85
|
+
lines.push(' skill:');
|
|
86
|
+
lines.push(' "*": "deny"');
|
|
87
|
+
for (const name of skillNames) {
|
|
88
|
+
lines.push(` ${JSON.stringify(name)}: "allow"`);
|
|
89
|
+
}
|
|
90
|
+
lines.push('---');
|
|
91
|
+
return lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
// ── Agent body ────────────────────────────────────────
|
|
94
|
+
function buildAgentBody(talContent) {
|
|
95
|
+
return talContent.trim();
|
|
96
|
+
}
|
|
97
|
+
// ── SKILL.md assembly ─────────────────────────────────
|
|
98
|
+
function buildSkillFile(skill) {
|
|
99
|
+
const frontmatter = [
|
|
100
|
+
'---',
|
|
101
|
+
`name: ${JSON.stringify(skill.name)}`,
|
|
102
|
+
`description: ${JSON.stringify(skill.description)}`,
|
|
103
|
+
'---',
|
|
104
|
+
].join('\n');
|
|
105
|
+
return `${frontmatter}\n\n${skill.content}`;
|
|
106
|
+
}
|
|
107
|
+
export function buildAssistantActionPrompt(context) {
|
|
108
|
+
const snapshot = JSON.stringify(context || { workingDir: '', performers: [], acts: [], drafts: [], availableModels: [] }, null, 2);
|
|
109
|
+
return [
|
|
110
|
+
'Current Workspace Snapshot:',
|
|
111
|
+
'```json',
|
|
112
|
+
snapshot,
|
|
113
|
+
'```',
|
|
114
|
+
'If you want to mutate the stage, append exactly one action block at the end of your reply.',
|
|
115
|
+
'Use this exact format with raw JSON only:',
|
|
116
|
+
'<assistant-actions>{"version":1,"actions":[...]}</assistant-actions>',
|
|
117
|
+
'Rules:',
|
|
118
|
+
'- Keep your user-facing explanation outside the action block.',
|
|
119
|
+
'- Omit the action block when no canvas mutation is needed.',
|
|
120
|
+
'- Valid action types:',
|
|
121
|
+
' Tal/Dance draft: createTalDraft, updateTalDraft, deleteTalDraft, createDanceDraft, updateDanceDraft, deleteDanceDraft',
|
|
122
|
+
' Performer: createPerformer (inline Tal/Dance/model/MCP), updatePerformer, deletePerformer',
|
|
123
|
+
' Act: createAct (inline participants/relations), updateAct, deleteAct',
|
|
124
|
+
' Participants: attachPerformerToAct, detachParticipantFromAct',
|
|
125
|
+
' Relations: connectPerformers, updateRelation, removeRelation',
|
|
126
|
+
'- Use exactly one assistant-actions block and place it at the end of the reply.',
|
|
127
|
+
'- The JSON inside the block must be valid and must not contain comments or trailing commas.',
|
|
128
|
+
'- Make the smallest correct set of mutations for the user request.',
|
|
129
|
+
'- Reuse performers, acts, drafts, and relations already present in the Workspace snapshot whenever possible.',
|
|
130
|
+
'- If you create something and refer to it later in the same block, assign a ref on the create action and use performerRef, actRef, or draftRef in later actions.',
|
|
131
|
+
'- Prefer explicit ids from the snapshot when available. If you do not know an id, you may use exact names.',
|
|
132
|
+
'- For models, use values from availableModels in the snapshot. Do not invent provider ids or model ids.',
|
|
133
|
+
'- Tal and Dance can only be created or updated as local drafts (not registry assets).',
|
|
134
|
+
'- Do not wrap the assistant-actions block in Markdown fences.',
|
|
135
|
+
'- If the request is ambiguous and you cannot produce a valid mutation safely, ask a short clarifying question instead of guessing.',
|
|
136
|
+
].join('\n');
|
|
137
|
+
}
|
|
138
|
+
// ── Write helper ──────────────────────────────────────
|
|
139
|
+
async function writeIfChanged(filePath, content) {
|
|
140
|
+
const current = await fs.readFile(filePath, 'utf-8').catch(() => null);
|
|
141
|
+
if (current === content)
|
|
142
|
+
return false;
|
|
143
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
144
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Ensure the assistant agent and builtin skill files exist.
|
|
149
|
+
* Returns the agent name for use with oc.session.promptAsync().
|
|
150
|
+
*
|
|
151
|
+
* Called at stage save / project activate time.
|
|
152
|
+
*/
|
|
153
|
+
export async function ensureAssistantAgent(executionDir) {
|
|
154
|
+
const talContent = await readTal();
|
|
155
|
+
const skills = await readBuiltinSkills();
|
|
156
|
+
let changed = false;
|
|
157
|
+
// 1. Agent file
|
|
158
|
+
const frontmatter = buildFrontmatter(skills.map((s) => s.name));
|
|
159
|
+
const body = buildAgentBody(talContent);
|
|
160
|
+
const agentContent = `${frontmatter}\n\n${body}`;
|
|
161
|
+
changed = (await writeIfChanged(agentFilePath(executionDir), agentContent)) || changed;
|
|
162
|
+
// 2. Skill files (one SKILL.md per builtin dance)
|
|
163
|
+
for (const skill of skills) {
|
|
164
|
+
changed = (await writeIfChanged(skillFilePath(executionDir, skill.name), buildSkillFile(skill))) || changed;
|
|
165
|
+
}
|
|
166
|
+
changed = (await removeStaleBuiltinSkills(executionDir, skills.map((skill) => skill.name))) || changed;
|
|
167
|
+
if (changed) {
|
|
168
|
+
const oc = await getOpencode();
|
|
169
|
+
await oc.instance.dispose({ directory: executionDir }).catch(() => { });
|
|
170
|
+
}
|
|
171
|
+
return `dot-studio/${AGENT_FILENAME.replace(/\.md$/, '')}`;
|
|
172
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { exec } from 'child_process';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import { ensureDotDir } from '../lib/dot-source.js';
|
|
7
|
+
import { getActiveProjectDir, setActiveProjectDir, readStudioConfig, writeStudioConfig, } from '../lib/config.js';
|
|
8
|
+
import { invalidateAll } from '../lib/cache.js';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
export async function pickWorkingDirectory() {
|
|
11
|
+
const { stdout } = await execAsync(`osascript -e 'POSIX path of (choose folder with prompt "Select Working Directory for Workspace")'`);
|
|
12
|
+
return { path: stdout.trim() };
|
|
13
|
+
}
|
|
14
|
+
export async function getStudioConfig(projectDir) {
|
|
15
|
+
const config = await readStudioConfig();
|
|
16
|
+
return { ...config, projectDir };
|
|
17
|
+
}
|
|
18
|
+
export async function updateStudioConfig(patch) {
|
|
19
|
+
return writeStudioConfig(patch);
|
|
20
|
+
}
|
|
21
|
+
export async function activateStudioProject(workingDir) {
|
|
22
|
+
if (!workingDir) {
|
|
23
|
+
return { ok: false, status: 400, error: 'workingDir is required' };
|
|
24
|
+
}
|
|
25
|
+
const resolved = path.resolve(workingDir.replace(/\/+$/, ''));
|
|
26
|
+
try {
|
|
27
|
+
const stat = await fs.stat(resolved);
|
|
28
|
+
if (!stat.isDirectory()) {
|
|
29
|
+
return { ok: false, status: 400, error: 'workingDir is not a directory' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return { ok: false, status: 400, error: `Directory not found: ${resolved}` };
|
|
34
|
+
}
|
|
35
|
+
await ensureDotDir(resolved);
|
|
36
|
+
setActiveProjectDir(resolved);
|
|
37
|
+
invalidateAll();
|
|
38
|
+
import('./studio-assistant/assistant-service.js').then(({ ensureAssistantAgent }) => ensureAssistantAgent(resolved).catch(() => { }));
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
activeProjectDir: getActiveProjectDir(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export async function openStudioPath(targetPath) {
|
|
45
|
+
if (!targetPath) {
|
|
46
|
+
return { ok: false, status: 400, error: 'path is required' };
|
|
47
|
+
}
|
|
48
|
+
const resolved = path.resolve(targetPath.replace(/\/+$/, ''));
|
|
49
|
+
try {
|
|
50
|
+
await fs.stat(resolved);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { ok: false, status: 404, error: `Path not found: ${resolved}` };
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
await open(resolved);
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
path: resolved,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
status: 500,
|
|
66
|
+
error: error instanceof Error ? error.message : 'Failed to open path',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|