dot-studio 0.0.1
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/LICENSE +21 -0
- package/README.md +214 -0
- package/client/assets/index-C2eIILoa.css +41 -0
- package/client/assets/index-DUPZ_Lw5.js +616 -0
- package/client/assets/index.es-Btlrnc3g.js +1 -0
- package/client/index.html +14 -0
- package/dist/cli.js +196 -0
- package/dist/server/index.js +79 -0
- package/dist/server/lib/act-runtime.js +1282 -0
- package/dist/server/lib/cache.js +31 -0
- package/dist/server/lib/config.js +53 -0
- package/dist/server/lib/dot-authoring.js +245 -0
- package/dist/server/lib/dot-loader.js +61 -0
- package/dist/server/lib/dot-login.js +190 -0
- package/dist/server/lib/model-catalog.js +111 -0
- package/dist/server/lib/opencode-auth.js +69 -0
- package/dist/server/lib/opencode-errors.js +220 -0
- package/dist/server/lib/opencode-sidecar.js +144 -0
- package/dist/server/lib/opencode.js +12 -0
- package/dist/server/lib/package-bin.js +63 -0
- package/dist/server/lib/project-config.js +39 -0
- package/dist/server/lib/prompt.js +222 -0
- package/dist/server/lib/request-context.js +27 -0
- package/dist/server/lib/runtime-tools.js +208 -0
- package/dist/server/routes/assets.js +161 -0
- package/dist/server/routes/chat.js +356 -0
- package/dist/server/routes/compile.js +105 -0
- package/dist/server/routes/dot.js +270 -0
- package/dist/server/routes/health.js +56 -0
- package/dist/server/routes/opencode.js +421 -0
- package/dist/server/routes/stages.js +137 -0
- package/dist/server/start.js +23 -0
- package/dist/server/terminal.js +282 -0
- package/dist/shared/mcp-config.js +19 -0
- package/dist/shared/model-variants.js +50 -0
- package/dist/shared/project-mcp.js +22 -0
- package/dist/shared/session-metadata.js +26 -0
- package/package.json +103 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { getAssetPayload, readAsset } from 'dance-of-tal/lib/registry';
|
|
5
|
+
import { resolveRuntimeModel } from './model-catalog.js';
|
|
6
|
+
import { findRuntimeModelVariant } from '../../shared/model-variants.js';
|
|
7
|
+
import { StudioValidationError } from './opencode-errors.js';
|
|
8
|
+
const CAPABILITY_LOADER_TOOL_NAME = 'read';
|
|
9
|
+
function extractDraftTextContent(draft) {
|
|
10
|
+
if (!draft) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
if (typeof draft.content === 'string') {
|
|
14
|
+
return draft.content;
|
|
15
|
+
}
|
|
16
|
+
if (draft.content && typeof draft.content === 'object') {
|
|
17
|
+
const content = draft.content;
|
|
18
|
+
if (typeof content.content === 'string') {
|
|
19
|
+
return content.content;
|
|
20
|
+
}
|
|
21
|
+
if (typeof content.body === 'string') {
|
|
22
|
+
return content.body;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function extractDraftDescription(draft) {
|
|
28
|
+
if (!draft) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
if (typeof draft.description === 'string') {
|
|
32
|
+
return draft.description;
|
|
33
|
+
}
|
|
34
|
+
if (draft.content && typeof draft.content === 'object') {
|
|
35
|
+
const content = draft.content;
|
|
36
|
+
if (typeof content.description === 'string') {
|
|
37
|
+
return content.description;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
async function resolveTalContent(cwd, ref, drafts) {
|
|
43
|
+
if (!ref) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (ref.kind === 'registry') {
|
|
47
|
+
return getAssetPayload(cwd, ref.urn);
|
|
48
|
+
}
|
|
49
|
+
return extractDraftTextContent(drafts[ref.draftId]);
|
|
50
|
+
}
|
|
51
|
+
function draftDisplayName(ref, drafts) {
|
|
52
|
+
const draft = drafts[ref.draftId];
|
|
53
|
+
return draft?.name || draft?.description || `draft:${ref.draftId}`;
|
|
54
|
+
}
|
|
55
|
+
function runtimeCapabilityDir(cwd) {
|
|
56
|
+
return path.join(path.resolve(cwd), '.dot-studio', 'runtime-capabilities');
|
|
57
|
+
}
|
|
58
|
+
async function writeCapabilityDocument(cwd, refKey, payload) {
|
|
59
|
+
const dir = runtimeCapabilityDir(cwd);
|
|
60
|
+
await fs.mkdir(dir, { recursive: true });
|
|
61
|
+
const fileHash = createHash('sha1').update(refKey).digest('hex').slice(0, 16);
|
|
62
|
+
const filePath = path.join(dir, `${fileHash}.md`);
|
|
63
|
+
const doc = [
|
|
64
|
+
`# ${payload.title}`,
|
|
65
|
+
payload.description ? `Description: ${payload.description}` : 'Description: No description provided.',
|
|
66
|
+
'',
|
|
67
|
+
'---',
|
|
68
|
+
'',
|
|
69
|
+
payload.body,
|
|
70
|
+
].join('\n');
|
|
71
|
+
await fs.writeFile(filePath, doc, 'utf-8');
|
|
72
|
+
return filePath;
|
|
73
|
+
}
|
|
74
|
+
async function materializeCapabilityDocument(cwd, ref, drafts) {
|
|
75
|
+
if (ref.kind === 'registry') {
|
|
76
|
+
const asset = await readAsset(cwd, ref.urn);
|
|
77
|
+
const body = await getAssetPayload(cwd, ref.urn);
|
|
78
|
+
if (!body) {
|
|
79
|
+
throw new StudioValidationError(`Capability '${ref.urn}' was not found or has no content.`, 'fix_input');
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
urn: ref.urn,
|
|
83
|
+
description: typeof asset?.description === 'string' ? asset.description : '',
|
|
84
|
+
path: await writeCapabilityDocument(cwd, `registry:${ref.urn}`, {
|
|
85
|
+
title: ref.urn,
|
|
86
|
+
description: typeof asset?.description === 'string' ? asset.description : '',
|
|
87
|
+
body,
|
|
88
|
+
}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const draft = drafts[ref.draftId];
|
|
92
|
+
const body = extractDraftTextContent(draft);
|
|
93
|
+
if (!draft || !body) {
|
|
94
|
+
throw new StudioValidationError(`Capability draft '${draftDisplayName(ref, drafts)}' was not found or has no content.`, 'fix_input');
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
urn: `draft/${ref.draftId}`,
|
|
98
|
+
description: extractDraftDescription(draft) || draft.name || 'Draft capability',
|
|
99
|
+
path: await writeCapabilityDocument(cwd, `draft:${ref.draftId}`, {
|
|
100
|
+
title: draft.name || `draft/${ref.draftId}`,
|
|
101
|
+
description: extractDraftDescription(draft) || draft.name || 'Draft capability',
|
|
102
|
+
body,
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function buildSystemPreamble(toolName) {
|
|
107
|
+
const lines = [
|
|
108
|
+
'# Runtime Instructions',
|
|
109
|
+
'The section named Core Instructions is the always-on instruction layer for your role, rules, and operating logic.',
|
|
110
|
+
'The section named Optional Capability Catalog lists extra modules you may consult only when the task actually needs them.',
|
|
111
|
+
'Prefer the minimum capability context needed to complete the task well.',
|
|
112
|
+
'Do not mention internal runtime wiring, capability loading, or system sections unless the user asks about them directly.',
|
|
113
|
+
];
|
|
114
|
+
if (toolName) {
|
|
115
|
+
lines.push(`When optional capability context is needed, use the '${toolName}' tool to inspect the capability document path listed in the catalog.`);
|
|
116
|
+
}
|
|
117
|
+
return lines.join('\n');
|
|
118
|
+
}
|
|
119
|
+
function buildTalSection(talContent) {
|
|
120
|
+
if (!talContent) {
|
|
121
|
+
return [
|
|
122
|
+
'# Core Instructions',
|
|
123
|
+
'No core instruction asset is configured. Follow the user request directly and stay consistent with the current session context.',
|
|
124
|
+
].join('\n');
|
|
125
|
+
}
|
|
126
|
+
return [
|
|
127
|
+
'# Core Instructions',
|
|
128
|
+
talContent,
|
|
129
|
+
].join('\n\n');
|
|
130
|
+
}
|
|
131
|
+
function buildDanceSection(catalog, toolName) {
|
|
132
|
+
if (catalog.length === 0) {
|
|
133
|
+
return [
|
|
134
|
+
'# Optional Capability Catalog',
|
|
135
|
+
'No optional capability assets are configured.',
|
|
136
|
+
].join('\n');
|
|
137
|
+
}
|
|
138
|
+
const lines = [
|
|
139
|
+
'# Optional Capability Usage',
|
|
140
|
+
`Capability bodies are available on demand through '${toolName}'. Use the catalog below to decide whether to load one.`,
|
|
141
|
+
'',
|
|
142
|
+
'# Optional Capability Catalog',
|
|
143
|
+
];
|
|
144
|
+
for (const entry of catalog) {
|
|
145
|
+
lines.push(`- ${entry.urn}: ${entry.description || 'No description provided.'}${entry.path ? ` (path: ${entry.path})` : ''}`);
|
|
146
|
+
}
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
function buildRuntimePreferencesSection(input) {
|
|
150
|
+
if (!input.variantId) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const lines = [
|
|
154
|
+
'# Runtime Preferences',
|
|
155
|
+
`Preferred model variant: ${input.variantId}`,
|
|
156
|
+
];
|
|
157
|
+
if (input.variantSummary) {
|
|
158
|
+
lines.push(`Variant settings: ${input.variantSummary}`);
|
|
159
|
+
}
|
|
160
|
+
lines.push('Apply this preferred runtime profile when supported by the current host and model.');
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
export async function buildPromptEnvelope(input) {
|
|
164
|
+
if (!input.model) {
|
|
165
|
+
throw new Error('A model is required for this performer. Select a model before compiling or sending prompts.');
|
|
166
|
+
}
|
|
167
|
+
const runtimeModel = await resolveRuntimeModel(input.cwd, input.model);
|
|
168
|
+
const capabilitySnapshot = runtimeModel
|
|
169
|
+
? {
|
|
170
|
+
toolCall: runtimeModel.toolCall,
|
|
171
|
+
reasoning: runtimeModel.reasoning,
|
|
172
|
+
attachment: runtimeModel.attachment,
|
|
173
|
+
temperature: runtimeModel.temperature,
|
|
174
|
+
modalities: runtimeModel.modalities,
|
|
175
|
+
}
|
|
176
|
+
: null;
|
|
177
|
+
const selectedVariant = runtimeModel
|
|
178
|
+
? findRuntimeModelVariant([runtimeModel], input.model.provider, input.model.modelId, input.modelVariant || null)
|
|
179
|
+
: null;
|
|
180
|
+
const resolvedVariantId = runtimeModel
|
|
181
|
+
? selectedVariant?.id || null
|
|
182
|
+
: input.modelVariant || null;
|
|
183
|
+
const drafts = input.drafts || {};
|
|
184
|
+
if (input.danceRefs.length > 0 && !capabilitySnapshot?.toolCall) {
|
|
185
|
+
throw new StudioValidationError('The selected model does not support runtime capability loading. Choose a tool-capable model or remove saved capabilities.', 'choose_model');
|
|
186
|
+
}
|
|
187
|
+
const talContent = await resolveTalContent(input.cwd, input.talRef, drafts);
|
|
188
|
+
let deliveryMode = 'tool';
|
|
189
|
+
let toolName;
|
|
190
|
+
if (input.danceRefs.length > 0) {
|
|
191
|
+
toolName = CAPABILITY_LOADER_TOOL_NAME;
|
|
192
|
+
}
|
|
193
|
+
const danceCatalog = [];
|
|
194
|
+
for (const ref of input.danceRefs) {
|
|
195
|
+
const document = await materializeCapabilityDocument(input.cwd, ref, drafts);
|
|
196
|
+
danceCatalog.push({
|
|
197
|
+
urn: document.urn,
|
|
198
|
+
description: document.description,
|
|
199
|
+
loadMode: deliveryMode,
|
|
200
|
+
path: document.path,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (deliveryMode === 'tool' && danceCatalog.some((entry) => entry.loadMode === 'tool')) {
|
|
204
|
+
toolName = toolName || CAPABILITY_LOADER_TOOL_NAME;
|
|
205
|
+
}
|
|
206
|
+
const sections = [
|
|
207
|
+
buildSystemPreamble(toolName),
|
|
208
|
+
buildTalSection(talContent),
|
|
209
|
+
buildDanceSection(danceCatalog, toolName),
|
|
210
|
+
buildRuntimePreferencesSection({
|
|
211
|
+
variantId: resolvedVariantId,
|
|
212
|
+
variantSummary: selectedVariant?.summary || null,
|
|
213
|
+
}),
|
|
214
|
+
];
|
|
215
|
+
return {
|
|
216
|
+
system: sections.filter(Boolean).join('\n\n').trim() || '// No core instructions or optional capabilities configured',
|
|
217
|
+
danceCatalog,
|
|
218
|
+
deliveryMode,
|
|
219
|
+
capabilitySnapshot,
|
|
220
|
+
...(toolName ? { toolName } : {}),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { getActiveProjectDir } from './config.js';
|
|
3
|
+
const WORKING_DIR_HEADER = 'x-dot-working-dir';
|
|
4
|
+
function normalizeWorkingDir(input) {
|
|
5
|
+
const trimmed = input.trim().replace(/\/+$/, '');
|
|
6
|
+
if (!trimmed) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return path.resolve(trimmed);
|
|
10
|
+
}
|
|
11
|
+
export function extractRequestWorkingDir(c) {
|
|
12
|
+
const headerValue = c.req.header(WORKING_DIR_HEADER);
|
|
13
|
+
if (headerValue) {
|
|
14
|
+
return normalizeWorkingDir(headerValue);
|
|
15
|
+
}
|
|
16
|
+
const queryValue = c.req.query('workingDir');
|
|
17
|
+
if (queryValue) {
|
|
18
|
+
return normalizeWorkingDir(queryValue);
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
export function resolveRequestWorkingDir(c) {
|
|
23
|
+
return extractRequestWorkingDir(c) || getActiveProjectDir();
|
|
24
|
+
}
|
|
25
|
+
export function requestDirectoryQuery(c) {
|
|
26
|
+
return { directory: resolveRequestWorkingDir(c) };
|
|
27
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { getOpencode } from './opencode.js';
|
|
2
|
+
import { projectMcpEntryEnabled } from '../../shared/project-mcp.js';
|
|
3
|
+
import { readProjectMcpCatalog } from './project-config.js';
|
|
4
|
+
function unique(values) {
|
|
5
|
+
return Array.from(new Set(values.filter(Boolean)));
|
|
6
|
+
}
|
|
7
|
+
export function describeUnavailableRuntimeTools(resolution) {
|
|
8
|
+
if (resolution.selectedMcpServers.length === 0 || resolution.unavailableDetails.length === 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
const parts = resolution.unavailableDetails.map((detail) => {
|
|
12
|
+
if (detail.reason === 'connected_but_no_tools_for_model' && detail.toolId) {
|
|
13
|
+
return `${detail.serverName}: ${detail.toolId} is unavailable for the current model`;
|
|
14
|
+
}
|
|
15
|
+
if (detail.reason === 'needs_auth') {
|
|
16
|
+
return `${detail.serverName}: authentication required`;
|
|
17
|
+
}
|
|
18
|
+
if (detail.reason === 'disabled') {
|
|
19
|
+
return `${detail.serverName}: disabled in project config`;
|
|
20
|
+
}
|
|
21
|
+
if (detail.reason === 'not_defined') {
|
|
22
|
+
return `${detail.serverName}: not defined in project config`;
|
|
23
|
+
}
|
|
24
|
+
return `${detail.serverName}: connection failed`;
|
|
25
|
+
});
|
|
26
|
+
return parts.join('; ');
|
|
27
|
+
}
|
|
28
|
+
export function buildEnabledToolMap(toolIds) {
|
|
29
|
+
const enabled = unique(Array.from(toolIds));
|
|
30
|
+
if (enabled.length === 0) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return enabled.reduce((acc, id) => {
|
|
34
|
+
acc[id] = true;
|
|
35
|
+
return acc;
|
|
36
|
+
}, {});
|
|
37
|
+
}
|
|
38
|
+
function emptyResolution(selectedMcpServers) {
|
|
39
|
+
return {
|
|
40
|
+
selectedMcpServers,
|
|
41
|
+
requestedTools: [],
|
|
42
|
+
availableTools: [],
|
|
43
|
+
resolvedTools: [],
|
|
44
|
+
unavailableTools: [],
|
|
45
|
+
unavailableDetails: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function currentMcpStatus(oc, cwd) {
|
|
49
|
+
const res = await oc.mcp.status({ directory: cwd });
|
|
50
|
+
return (res.data || {});
|
|
51
|
+
}
|
|
52
|
+
async function ensureConnectedServer(oc, cwd, serverName, catalog, statusMap) {
|
|
53
|
+
const config = catalog[serverName];
|
|
54
|
+
if (!config) {
|
|
55
|
+
return {
|
|
56
|
+
statusMap,
|
|
57
|
+
unavailable: {
|
|
58
|
+
serverName,
|
|
59
|
+
reason: 'not_defined',
|
|
60
|
+
detail: 'Server is not defined in project config.json.',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (!projectMcpEntryEnabled(config)) {
|
|
65
|
+
return {
|
|
66
|
+
statusMap,
|
|
67
|
+
unavailable: {
|
|
68
|
+
serverName,
|
|
69
|
+
reason: 'disabled',
|
|
70
|
+
detail: 'Server is disabled in project config.json.',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const current = statusMap[serverName];
|
|
75
|
+
if (current?.status === 'connected') {
|
|
76
|
+
return {
|
|
77
|
+
statusMap,
|
|
78
|
+
unavailable: null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (current?.status === 'needs_auth') {
|
|
82
|
+
return {
|
|
83
|
+
statusMap,
|
|
84
|
+
unavailable: {
|
|
85
|
+
serverName,
|
|
86
|
+
reason: 'needs_auth',
|
|
87
|
+
detail: 'Server requires authentication before it can connect.',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
await oc.mcp.connect({
|
|
93
|
+
name: serverName,
|
|
94
|
+
directory: cwd,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
statusMap,
|
|
100
|
+
unavailable: {
|
|
101
|
+
serverName,
|
|
102
|
+
reason: 'connect_failed',
|
|
103
|
+
detail: error?.message || 'Connection attempt failed.',
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const refreshed = await currentMcpStatus(oc, cwd);
|
|
108
|
+
const next = refreshed[serverName];
|
|
109
|
+
if (next?.status === 'connected') {
|
|
110
|
+
return {
|
|
111
|
+
statusMap: refreshed,
|
|
112
|
+
unavailable: null,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (next?.status === 'needs_auth') {
|
|
116
|
+
return {
|
|
117
|
+
statusMap: refreshed,
|
|
118
|
+
unavailable: {
|
|
119
|
+
serverName,
|
|
120
|
+
reason: 'needs_auth',
|
|
121
|
+
detail: 'Server requires authentication before it can connect.',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
statusMap: refreshed,
|
|
127
|
+
unavailable: {
|
|
128
|
+
serverName,
|
|
129
|
+
reason: 'connect_failed',
|
|
130
|
+
detail: next?.error || 'Server did not reach connected state.',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export async function resolveRuntimeTools(cwd, model, mcpServerNames) {
|
|
135
|
+
const selectedMcpServers = unique(mcpServerNames);
|
|
136
|
+
if (selectedMcpServers.length === 0) {
|
|
137
|
+
return emptyResolution(selectedMcpServers);
|
|
138
|
+
}
|
|
139
|
+
const oc = await getOpencode();
|
|
140
|
+
const catalog = await readProjectMcpCatalog(cwd);
|
|
141
|
+
let mcpStatus = await currentMcpStatus(oc, cwd);
|
|
142
|
+
const unavailableDetails = [];
|
|
143
|
+
for (const serverName of selectedMcpServers) {
|
|
144
|
+
const ensured = await ensureConnectedServer(oc, cwd, serverName, catalog, mcpStatus);
|
|
145
|
+
mcpStatus = ensured.statusMap;
|
|
146
|
+
if (ensured.unavailable) {
|
|
147
|
+
unavailableDetails.push(ensured.unavailable);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const requestedTools = unique(selectedMcpServers.flatMap((serverName) => (mcpStatus[serverName]?.tools || [])
|
|
151
|
+
.map((tool) => tool.name || '')));
|
|
152
|
+
if (requestedTools.length === 0) {
|
|
153
|
+
return {
|
|
154
|
+
...emptyResolution(selectedMcpServers),
|
|
155
|
+
unavailableDetails,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
let availableTools = [];
|
|
159
|
+
if (model) {
|
|
160
|
+
const toolListRes = await oc.tool.list({
|
|
161
|
+
provider: model.provider,
|
|
162
|
+
model: model.modelId,
|
|
163
|
+
directory: cwd,
|
|
164
|
+
});
|
|
165
|
+
const items = (toolListRes.data || []);
|
|
166
|
+
availableTools = unique(items.map((item) => item.id || ''));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const toolIdsRes = await oc.tool.ids({
|
|
170
|
+
directory: cwd,
|
|
171
|
+
});
|
|
172
|
+
availableTools = unique((toolIdsRes.data || []));
|
|
173
|
+
}
|
|
174
|
+
const availableSet = new Set(availableTools);
|
|
175
|
+
const resolvedTools = requestedTools.filter((toolId) => availableSet.has(toolId));
|
|
176
|
+
const unavailableTools = requestedTools.filter((toolId) => !availableSet.has(toolId));
|
|
177
|
+
const toolServerNames = new Map();
|
|
178
|
+
for (const serverName of selectedMcpServers) {
|
|
179
|
+
for (const tool of (mcpStatus[serverName]?.tools || [])) {
|
|
180
|
+
const toolId = tool.name || '';
|
|
181
|
+
if (!toolId)
|
|
182
|
+
continue;
|
|
183
|
+
const current = toolServerNames.get(toolId) || [];
|
|
184
|
+
current.push(serverName);
|
|
185
|
+
toolServerNames.set(toolId, current);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const toolId of unavailableTools) {
|
|
189
|
+
for (const serverName of toolServerNames.get(toolId) || []) {
|
|
190
|
+
unavailableDetails.push({
|
|
191
|
+
serverName,
|
|
192
|
+
toolId,
|
|
193
|
+
reason: 'connected_but_no_tools_for_model',
|
|
194
|
+
detail: model
|
|
195
|
+
? `${toolId} is not available for ${model.provider}/${model.modelId}.`
|
|
196
|
+
: `${toolId} is not available in the current runtime.`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
selectedMcpServers,
|
|
202
|
+
requestedTools,
|
|
203
|
+
availableTools,
|
|
204
|
+
resolvedTools,
|
|
205
|
+
unavailableTools,
|
|
206
|
+
unavailableDetails,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Asset Routes — using DOT lib directly
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { assetFilePath, getDotDir, getGlobalCwd, getGlobalDotDir, readAsset } from 'dance-of-tal/lib/registry';
|
|
6
|
+
import { getRegistryPackage } from 'dance-of-tal/lib/installer';
|
|
7
|
+
import { cached, TTL } from '../lib/cache.js';
|
|
8
|
+
import { resolveRequestWorkingDir } from '../lib/request-context.js';
|
|
9
|
+
const assets = new Hono();
|
|
10
|
+
// ── Asset Scanning ──────────────────────────────────────
|
|
11
|
+
// Scans both local (.dance-of-tal/) and global (~/.dance-of-tal/) dirs
|
|
12
|
+
// Each asset is tagged with source: 'global' | 'stage'
|
|
13
|
+
// Stage assets override global for the same URN
|
|
14
|
+
function normalizeAsset(kind, urn, author, source, content, detail = false) {
|
|
15
|
+
const slug = urn.split('/')[2];
|
|
16
|
+
const normalizedAuthor = author.startsWith('@') ? author : `@${author}`;
|
|
17
|
+
const base = {
|
|
18
|
+
kind,
|
|
19
|
+
urn,
|
|
20
|
+
slug,
|
|
21
|
+
name: typeof content.name === 'string' && content.name.trim() ? content.name.trim() : slug,
|
|
22
|
+
author: normalizedAuthor,
|
|
23
|
+
source,
|
|
24
|
+
description: typeof content.description === 'string' ? content.description : '',
|
|
25
|
+
};
|
|
26
|
+
if (kind === 'performer') {
|
|
27
|
+
const danceRaw = content.dance;
|
|
28
|
+
const danceUrns = Array.isArray(danceRaw)
|
|
29
|
+
? danceRaw.filter((value) => typeof value === 'string')
|
|
30
|
+
: typeof danceRaw === 'string'
|
|
31
|
+
? [danceRaw]
|
|
32
|
+
: [];
|
|
33
|
+
return {
|
|
34
|
+
...base,
|
|
35
|
+
talUrn: typeof content.tal === 'string' ? content.tal : null,
|
|
36
|
+
danceUrns,
|
|
37
|
+
actUrn: typeof content.act === 'string' ? content.act : null,
|
|
38
|
+
model: typeof content.model === 'string' ? content.model : null,
|
|
39
|
+
mcpConfig: typeof content.mcp_config === 'object' && content.mcp_config !== null ? content.mcp_config : null,
|
|
40
|
+
tags: Array.isArray(content.tags) ? content.tags : [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (kind === 'act') {
|
|
44
|
+
return {
|
|
45
|
+
...base,
|
|
46
|
+
entryNode: typeof content.entryNode === 'string' ? content.entryNode : null,
|
|
47
|
+
nodeCount: typeof content.nodes === 'object' && content.nodes ? Object.keys(content.nodes).length : 0,
|
|
48
|
+
tags: Array.isArray(content.tags) ? content.tags : [],
|
|
49
|
+
...(detail ? {
|
|
50
|
+
nodes: typeof content.nodes === 'object' && content.nodes ? content.nodes : {},
|
|
51
|
+
edges: Array.isArray(content.edges) ? content.edges : [],
|
|
52
|
+
maxIterations: typeof content.maxIterations === 'number' ? content.maxIterations : undefined,
|
|
53
|
+
} : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
...base,
|
|
58
|
+
tags: Array.isArray(content.tags) ? content.tags : [],
|
|
59
|
+
...(detail && typeof content.content === 'string' ? { content: content.content } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function fetchRegistryAsset(kind, author, name) {
|
|
63
|
+
const pkg = await getRegistryPackage(kind, author, name);
|
|
64
|
+
const urn = typeof pkg.urn === 'string' && pkg.urn ? pkg.urn : `${kind}/@${author.replace(/^@/, '')}/${name}`;
|
|
65
|
+
const normalized = normalizeAsset(kind, urn, `@${author.replace(/^@/, '')}`, 'registry', pkg.payload, true);
|
|
66
|
+
return {
|
|
67
|
+
...normalized,
|
|
68
|
+
stars: typeof pkg.stars === 'number' ? pkg.stars : 0,
|
|
69
|
+
tier: typeof pkg.tier === 'string' ? pkg.tier : undefined,
|
|
70
|
+
updatedAt: typeof pkg.updatedAt === 'string' ? pkg.updatedAt : undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function scanAssetDir(baseDir, kind, source, resultsMap) {
|
|
74
|
+
const kindDir = path.join(baseDir, kind);
|
|
75
|
+
try {
|
|
76
|
+
const authors = await fs.readdir(kindDir);
|
|
77
|
+
for (const author of authors) {
|
|
78
|
+
if (!author.startsWith('@'))
|
|
79
|
+
continue;
|
|
80
|
+
const authorDir = path.join(kindDir, author);
|
|
81
|
+
const stat = await fs.stat(authorDir);
|
|
82
|
+
if (!stat.isDirectory())
|
|
83
|
+
continue;
|
|
84
|
+
const files = await fs.readdir(authorDir);
|
|
85
|
+
for (const file of files) {
|
|
86
|
+
if (!file.endsWith('.json'))
|
|
87
|
+
continue;
|
|
88
|
+
try {
|
|
89
|
+
const content = JSON.parse(await fs.readFile(path.join(authorDir, file), 'utf-8'));
|
|
90
|
+
const name = file.replace(/\.json$/, '');
|
|
91
|
+
const urn = `${kind}/${author}/${name}`;
|
|
92
|
+
resultsMap.set(urn, normalizeAsset(kind, urn, author, source, content, false));
|
|
93
|
+
}
|
|
94
|
+
catch { /* skip invalid files */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch { /* directory doesn't exist */ }
|
|
99
|
+
}
|
|
100
|
+
async function listAssets(cwd, kind) {
|
|
101
|
+
const resultsMap = new Map();
|
|
102
|
+
const globalDir = getGlobalDotDir();
|
|
103
|
+
const localDir = getDotDir(cwd);
|
|
104
|
+
// 1. Scan Global Directory (Base)
|
|
105
|
+
await scanAssetDir(globalDir, kind, 'global', resultsMap);
|
|
106
|
+
// 2. Scan Stage Directory (Overrides)
|
|
107
|
+
await scanAssetDir(localDir, kind, 'stage', resultsMap);
|
|
108
|
+
return Array.from(resultsMap.values());
|
|
109
|
+
}
|
|
110
|
+
async function resolveAssetSource(cwd, urn) {
|
|
111
|
+
try {
|
|
112
|
+
await fs.access(assetFilePath(cwd, urn));
|
|
113
|
+
return 'stage';
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
try {
|
|
117
|
+
await fs.access(assetFilePath(getGlobalCwd(), urn));
|
|
118
|
+
return 'global';
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return 'stage';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ── Routes ──────────────────────────────────────────────
|
|
126
|
+
assets.get('/api/assets/:kind', async (c) => {
|
|
127
|
+
const kind = c.req.param('kind');
|
|
128
|
+
if (!['tal', 'dance', 'performer', 'act'].includes(kind)) {
|
|
129
|
+
return c.json({ error: `Invalid kind: ${kind}` }, 400);
|
|
130
|
+
}
|
|
131
|
+
const cwd = resolveRequestWorkingDir(c);
|
|
132
|
+
const assetList = await cached(`assets-${kind}-${cwd}`, TTL.ASSETS, () => listAssets(cwd, kind));
|
|
133
|
+
return c.json(assetList);
|
|
134
|
+
});
|
|
135
|
+
assets.get('/api/assets/:kind/:author/:name', async (c) => {
|
|
136
|
+
const { kind, author, name } = c.req.param();
|
|
137
|
+
const urn = `${kind}/@${author}/${name}`;
|
|
138
|
+
try {
|
|
139
|
+
const cwd = resolveRequestWorkingDir(c);
|
|
140
|
+
const asset = await readAsset(cwd, urn);
|
|
141
|
+
if (!asset) {
|
|
142
|
+
return c.json({ error: `Asset not found: ${urn}` }, 404);
|
|
143
|
+
}
|
|
144
|
+
const source = await resolveAssetSource(cwd, urn);
|
|
145
|
+
return c.json(normalizeAsset(kind, urn, `@${author}`, source, asset, true));
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
return c.json({ error: err.message }, 404);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
assets.get('/api/assets/registry/:kind/:author/:name', async (c) => {
|
|
152
|
+
const { kind, author, name } = c.req.param();
|
|
153
|
+
try {
|
|
154
|
+
const detail = await cached(`registry-asset-${kind}-${author}-${name}`, TTL.PROVIDERS, () => fetchRegistryAsset(kind, author, name));
|
|
155
|
+
return c.json(detail);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
return c.json({ error: err.message }, 404);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
export default assets;
|