@descix/cli 1.0.0

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 (94) hide show
  1. package/README.md +95 -0
  2. package/SERVICE_README_sdk.md +399 -0
  3. package/agent-assets/guides/README.md +61 -0
  4. package/agent-assets/guides/dynamic-site-pattern.md +125 -0
  5. package/agent-assets/guides/knowledge-only-pattern.md +93 -0
  6. package/agent-assets/guides/microservice-pattern.md +152 -0
  7. package/agent-assets/guides/static-site-pattern.md +111 -0
  8. package/agent-assets/instructions/app-asset-validation.md +477 -0
  9. package/agent-assets/instructions/onboarding-flow.md +408 -0
  10. package/agent-assets/instructions/standard-folder-structure.md +125 -0
  11. package/agent-assets/reference/app-creation.md +381 -0
  12. package/agent-assets/reference/architecture.md +406 -0
  13. package/agent-assets/reference/local-dev.md +138 -0
  14. package/agent-assets/reference/mcp-integration.md +424 -0
  15. package/agent-assets/reference/workspace-config.md +415 -0
  16. package/agent-assets/rules/descix_mcp.template.mdc +243 -0
  17. package/bin/descix.js +3930 -0
  18. package/bin/mcp-server.js +382 -0
  19. package/descix-cli-1.0.0.tgz +0 -0
  20. package/lib/agent-files.js +162 -0
  21. package/lib/api-client.js +458 -0
  22. package/lib/auth-guard.js +67 -0
  23. package/lib/commands/app-wizard.js +627 -0
  24. package/lib/commands/auth.js +407 -0
  25. package/lib/commands/buy.js +207 -0
  26. package/lib/commands/clone.js +91 -0
  27. package/lib/commands/config.js +155 -0
  28. package/lib/commands/doctor.js +277 -0
  29. package/lib/commands/folder.js +291 -0
  30. package/lib/commands/init.js +257 -0
  31. package/lib/commands/kb.js +654 -0
  32. package/lib/commands/mcp.js +273 -0
  33. package/lib/commands/serve.js +12 -0
  34. package/lib/commands/status.js +239 -0
  35. package/lib/commands/update.js +653 -0
  36. package/lib/core/Chunker.js +610 -0
  37. package/lib/core/Hydrator.js +801 -0
  38. package/lib/core/PathContext.js +335 -0
  39. package/lib/core/Syncer.js +310 -0
  40. package/lib/core/index.js +32 -0
  41. package/lib/global-config.js +86 -0
  42. package/lib/google-storage-adc.js +511 -0
  43. package/lib/wallet-file.js +232 -0
  44. package/lib/wizard/setup.js +502 -0
  45. package/lib/workspace-config.js +712 -0
  46. package/lib/workspace-utils.js +126 -0
  47. package/package.json +41 -0
  48. package/templates/agent-claude.md +226 -0
  49. package/templates/agent-cline.md +133 -0
  50. package/templates/agent-copilot.md +133 -0
  51. package/templates/agent-cursor.md +133 -0
  52. package/templates/descix_mcp.mdc +122 -0
  53. package/templates/drive/README.md +42 -0
  54. package/templates/drive/app/assets/app_description.md +13 -0
  55. package/templates/drive/app/assets/icon.png +0 -0
  56. package/templates/drive/app/assets/system_instructions.md +17 -0
  57. package/templates/drive/app/kb/General/README.md +25 -0
  58. package/templates/drive/community/Apps/.gitkeep +4 -0
  59. package/templates/drive/community/community_assets/community_description.md +15 -0
  60. package/templates/drive/community/community_assets/icon.png +0 -0
  61. package/templates/drive_templates/README.md +104 -0
  62. package/templates/drive_templates/agent_app_template/assets/app_description.txt +13 -0
  63. package/templates/drive_templates/agent_app_template/assets/system_instructions.txt +17 -0
  64. package/templates/drive_templates/agent_app_template/kb/General/README.md +25 -0
  65. package/templates/drive_templates/agent_app_template/microservice/.gitkeep +10 -0
  66. package/templates/drive_templates/agent_app_template/site/.gitkeep +4 -0
  67. package/templates/drive_templates/community_template/Apps/.gitkeep +4 -0
  68. package/templates/drive_templates/community_template/community_assets/community_description.txt +15 -0
  69. package/templates/scaffolds/README.md +62 -0
  70. package/templates/scaffolds/microservice/Dockerfile +19 -0
  71. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +399 -0
  72. package/templates/scaffolds/microservice/app.js +50 -0
  73. package/templates/scaffolds/microservice/app.yaml +8 -0
  74. package/templates/scaffolds/microservice/defaults-config.json +5 -0
  75. package/templates/scaffolds/microservice/dev-overrides.json.example +5 -0
  76. package/templates/scaffolds/microservice/manifest.json +49 -0
  77. package/templates/scaffolds/microservice/package-lock.json +2645 -0
  78. package/templates/scaffolds/microservice/package.json +29 -0
  79. package/templates/scaffolds/microservice/scripts/register.js +55 -0
  80. package/templates/scaffolds/microservice/services/apiFront.js +75 -0
  81. package/templates/scaffolds/microservice/services/commandHandlers/sampleCommands.js +57 -0
  82. package/templates/scaffolds/microservice/services/mcpClient.js +78 -0
  83. package/templates/scaffolds/microservice/services/utils.js +247 -0
  84. package/templates/scaffolds/microservice/setup-schema.json +18 -0
  85. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +143 -0
  86. package/templates/scaffolds/site/DeSciXAppSDK.js +62 -0
  87. package/templates/scaffolds/site/README.md +51 -0
  88. package/templates/scaffolds/site/app.js +22 -0
  89. package/templates/scaffolds/site/index.html +42 -0
  90. package/templates/scaffolds/site/styles.css +123 -0
  91. package/test-hydration.js +150 -0
  92. package/vendor/mcp/transports/http-sse-transport.js +196 -0
  93. package/vendor/mcp/transports/index.js +15 -0
  94. package/vendor/mcp/transports/stdio-transport.js +118 -0
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * DeSciX MCP Server
5
+ *
6
+ * Thin HTTP relay: curated app-dev tools inline, all calls dispatch
7
+ * to backend via DeSciXApiClient (POST /apifront/).
8
+ *
9
+ * descix_doctor is handled locally (no backend call needed).
10
+ *
11
+ * Uses official @modelcontextprotocol/sdk with stdio transport.
12
+ */
13
+
14
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import {
17
+ ListToolsRequestSchema,
18
+ CallToolRequestSchema,
19
+ } from '@modelcontextprotocol/sdk/types.js';
20
+ import { WorkspaceConfig } from '../lib/workspace-config.js';
21
+ import { DeSciXApiClient } from '../lib/api-client.js';
22
+ import { WalletFileManager } from '../lib/wallet-file.js';
23
+ import * as fs from 'fs/promises';
24
+ import * as path from 'path';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Curated app-dev tool definitions (inline — no external tools.js dependency)
28
+ // ---------------------------------------------------------------------------
29
+
30
+ const TOOLS = [
31
+ {
32
+ name: 'descix_doctor',
33
+ description:
34
+ 'Startup diagnostic: returns auth state, workspace context, active app/community, ' +
35
+ 'available MCP tools by category, and any mismatches between local config and platform. ' +
36
+ 'Call this FIRST when starting a new session to understand the environment.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ verify_remote: {
41
+ type: 'boolean',
42
+ description: 'Also verify app exists on the platform (requires network). Default: true',
43
+ },
44
+ },
45
+ },
46
+ },
47
+ {
48
+ name: 'ask_question_to_app',
49
+ description: 'Ask an AI-powered question to an app\'s knowledge base using RAG',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ app_id: { type: 'string', description: 'App ID to query' },
54
+ knowledgebase_name: { type: 'string', description: 'KB name (default: General)' },
55
+ user_input: { type: 'string', description: 'The question to ask' },
56
+ },
57
+ required: ['app_id', 'user_input'],
58
+ },
59
+ },
60
+ {
61
+ name: 'query_knowledge_base',
62
+ description: 'Search a knowledge base using vector similarity (returns raw chunks)',
63
+ inputSchema: {
64
+ type: 'object',
65
+ properties: {
66
+ app_id: { type: 'string', description: 'App ID' },
67
+ kb_id: { type: 'string', description: 'KB name (default: General)' },
68
+ query: { type: 'string', description: 'Search query' },
69
+ limit: { type: 'number', description: 'Max results (default: 5)' },
70
+ },
71
+ required: ['app_id', 'query'],
72
+ },
73
+ },
74
+ {
75
+ name: 'find_communities',
76
+ description: 'List available communities on the platform',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ filter: { type: 'string', description: 'Optional filter string' },
81
+ },
82
+ },
83
+ },
84
+ {
85
+ name: 'list_apps_for_community',
86
+ description: 'List apps in a community',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: {
90
+ community_id: { type: 'string', description: 'Community ID' },
91
+ },
92
+ required: ['community_id'],
93
+ },
94
+ },
95
+ {
96
+ name: 'tell_me_how',
97
+ description:
98
+ 'Discover platform tools and services by asking a natural language question. ' +
99
+ 'Searches the entire service mesh for relevant capabilities. ' +
100
+ 'If results are empty, try scope "discovery" or rephrase your question.',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {
104
+ question: { type: 'string', description: 'What do you want to do?' },
105
+ scope: {
106
+ type: 'string',
107
+ enum: ['project', 'entitlements', 'discovery'],
108
+ description: 'Search scope. "entitlements" = your purchased tools, "discovery" = all platform capabilities. Default: entitlements.',
109
+ },
110
+ },
111
+ required: ['question'],
112
+ },
113
+ },
114
+ {
115
+ name: 'resolve_invite',
116
+ description:
117
+ 'Resolve a DeSciX invite token into app configuration. Call this when .descix/app.json ' +
118
+ 'contains an invite_token field. Returns app context, community info, and the agent_hint ' +
119
+ 'authored by the app creator. The agent_hint tells you about the user\'s skill level and ' +
120
+ 'goals — use it to adapt your approach.',
121
+ inputSchema: {
122
+ type: 'object',
123
+ properties: {
124
+ invite_token: {
125
+ type: 'string',
126
+ description: 'The invite token from .descix/app.json',
127
+ },
128
+ },
129
+ required: ['invite_token'],
130
+ },
131
+ },
132
+ ];
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Server bootstrap
136
+ // ---------------------------------------------------------------------------
137
+
138
+ console.error('[MCP] Starting DeSciX MCP server...');
139
+
140
+ try {
141
+ const workspaceRoot = process.cwd();
142
+ console.error(`[MCP] Workspace root: ${workspaceRoot}`);
143
+
144
+ // Load workspace config (optional — server works without it)
145
+ let workspaceConfig = null;
146
+ try {
147
+ workspaceConfig = await WorkspaceConfig.load(workspaceRoot);
148
+ console.error(`[MCP] Loaded config for community: ${workspaceConfig.primaryCommunity}`);
149
+ } catch {
150
+ console.error('[MCP] No workspace config found — using defaults');
151
+ }
152
+
153
+ // Create HTTP-only API client
154
+ const apiClient = new DeSciXApiClient({
155
+ projectRoot: workspaceRoot,
156
+ baseUrl: workspaceConfig?.apiUrl || null,
157
+ });
158
+
159
+ // Load wallet credentials
160
+ let walletInfo = null;
161
+ const walletPath = await WalletFileManager.findWalletFile(workspaceRoot);
162
+ if (walletPath) {
163
+ walletInfo = await WalletFileManager.loadWalletFile(walletPath);
164
+ if (walletInfo) {
165
+ console.error(`[MCP] Authenticated as: ${walletInfo.userId || 'unknown'}`);
166
+ apiClient.setCredentials({
167
+ userId: walletInfo.userId || null,
168
+ accessToken: walletInfo.sessionToken || null,
169
+ walletAddress: walletInfo.walletAddress || null,
170
+ signature: walletInfo.signature || null,
171
+ });
172
+ }
173
+ } else {
174
+ console.error('[MCP] No wallet file — some tools may require authentication');
175
+ }
176
+
177
+ // Default context from workspace config — NO hardcoded fallbacks.
178
+ // When workspace isn't configured, defaultContext stays null so tools
179
+ // don't silently target the wrong app.
180
+ const defaultContext = workspaceConfig?.defaultContext || null;
181
+
182
+ // -------------------------------------------------------------------------
183
+ // descix_doctor — local diagnostic (no backend call)
184
+ // -------------------------------------------------------------------------
185
+
186
+ async function runDoctor(args) {
187
+ const verifyRemote = args?.verify_remote !== false;
188
+ const report = {
189
+ auth: { connected: false },
190
+ workspace: { configured: false },
191
+ app_context: null,
192
+ tools: {
193
+ diagnostic: ['descix_doctor'],
194
+ knowledge: ['ask_question_to_app', 'query_knowledge_base'],
195
+ discovery: ['find_communities', 'list_apps_for_community', 'tell_me_how'],
196
+ },
197
+ warnings: [],
198
+ };
199
+
200
+ // Auth state
201
+ if (walletInfo) {
202
+ report.auth = {
203
+ connected: true,
204
+ user_id: walletInfo.userId || null,
205
+ session_valid: WalletFileManager.hasValidSession?.(walletInfo) ?? true,
206
+ };
207
+ } else {
208
+ report.warnings.push('Not authenticated. Run "DeSciX: Connect" or "descix login" to authenticate.');
209
+ }
210
+
211
+ // Workspace config — reload fresh so we pick up workspace.json created mid-session
212
+ let freshConfig = null;
213
+ let freshContext = null;
214
+ try {
215
+ freshConfig = await WorkspaceConfig.load(workspaceRoot || process.cwd());
216
+ freshContext = freshConfig.defaultContext || null;
217
+ } catch { /* no workspace.json present */ }
218
+
219
+ if (freshConfig) {
220
+ const platform = freshConfig.env?.platform || {};
221
+ const products = freshConfig.env?.products || [];
222
+ report.workspace = {
223
+ configured: true,
224
+ version: freshConfig.version || 'unknown',
225
+ environment: freshConfig.env?.environment || 'unknown',
226
+ };
227
+ if (freshContext) {
228
+ report.app_context = {
229
+ app_id: freshContext.appId,
230
+ community_id: freshContext.communityId,
231
+ kb_id: freshContext.kbId || 'General',
232
+ api_url: freshConfig.apiUrl || apiClient.baseUrl,
233
+ local_apps: [
234
+ platform.appId,
235
+ ...products.map(p => p.appId),
236
+ ].filter(Boolean),
237
+ };
238
+ }
239
+ } else {
240
+ report.workspace = { configured: false };
241
+ report.setup_needed = true;
242
+ report.next_steps = [
243
+ 'This project is not configured yet. Guide the user through setup:',
244
+ '',
245
+ '## Before asking questions, scan the repo',
246
+ 'List the working directory to detect existing content (docs/, src/, package.json, kb/, site/).',
247
+ 'Use findings to tailor your checkpoint questions.',
248
+ '',
249
+ '## Checkpoints (ask the user at each step)',
250
+ '1. **Objective:** "Do you want to explore existing apps, or build something new?"',
251
+ '2. **Community/App:** Use find_communities + list_apps_for_community, then ask: "Which community and app should this project target?"',
252
+ '3. **Environment:** "Should this run against local dev backend (localhost:4000) or hosted API (descix.net)?"',
253
+ '',
254
+ '## Canonical setup commands (run in terminal)',
255
+ '```',
256
+ 'descix whoami # confirm identity',
257
+ 'descix app list # see available apps',
258
+ 'descix init -c <community> -a <app> -p . # create workspace.json',
259
+ 'descix app init -a <app> -c <community> # register app on platform',
260
+ '```',
261
+ '',
262
+ '## Important: use explicit flags',
263
+ 'Always use -c <community> -a <app> flags for all commands until workspace context is verified working.',
264
+ '',
265
+ '## Integrating existing content',
266
+ 'If the repo has existing content (HTML app, React app, SDK with docs):',
267
+ '- Do NOT copy files into site/ or kb/. Integrate in place.',
268
+ '- Existing HTML/JS: serve as-is, add DeSciXAppSDK.js for DeSciX integration.',
269
+ '- React/Vite: wrap root in <AppShell appId="app_id"> from @descix/app-sdk/AppShell.',
270
+ '- Existing docs: copy *.md to kb/General/ for sync pipeline. Keep originals as source of truth.',
271
+ '',
272
+ 'DeSciX is like a virtual university: communities are departments, apps are courses/textbooks/services.',
273
+ ];
274
+ }
275
+
276
+ // Check agent instruction files
277
+ const checkRoot = freshConfig?.getWorkspaceRoot?.() || workspaceRoot || process.cwd();
278
+ const agentFiles = ['CLAUDE.md', '.github/copilot-instructions.md', '.cursorrules', '.clinerules'];
279
+ const foundFiles = [];
280
+ for (const f of agentFiles) {
281
+ try {
282
+ await fs.access(path.join(checkRoot, f));
283
+ foundFiles.push(f);
284
+ } catch { /* not present */ }
285
+ }
286
+ report.agent_files = foundFiles.length > 0 ? foundFiles : null;
287
+ if (!report.agent_files) {
288
+ report.warnings.push('No agent instruction files found. Run "descix quickstart" to generate them.');
289
+ }
290
+
291
+ // Remote verification (if requested, authenticated, and workspace configured)
292
+ if (verifyRemote && walletInfo && freshContext?.appId) {
293
+ try {
294
+ const apps = await apiClient.invoke('list_apps_for_community', {
295
+ community_id: freshContext.communityId,
296
+ });
297
+ const appList = Array.isArray(apps) ? apps : (apps?.apps || apps?.products || []);
298
+ const appIds = appList.map(a => a.app_id || a.appId || a.id).filter(Boolean);
299
+ report.remote_apps = appIds;
300
+
301
+ if (!appIds.includes(freshContext.appId)) {
302
+ report.warnings.push(
303
+ `Local app_id "${freshContext.appId}" not found on platform for community "${freshContext.communityId}". ` +
304
+ `Available: ${appIds.join(', ') || 'none'}. Run "descix app init -a ${freshContext.appId}" to register it.`
305
+ );
306
+ }
307
+ } catch (err) {
308
+ report.warnings.push(`Could not verify remote state: ${err.message}`);
309
+ }
310
+ }
311
+
312
+ if (report.warnings.length === 0) {
313
+ report.status = 'healthy';
314
+ } else {
315
+ report.status = 'warnings';
316
+ }
317
+
318
+ return report;
319
+ }
320
+
321
+ // Create MCP server
322
+ const server = new Server(
323
+ { name: 'DeSciX MCP Server', version: '1.0.0' },
324
+ { capabilities: { tools: {} } },
325
+ );
326
+
327
+ // tools/list — return curated tool definitions
328
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
329
+ console.error(`[MCP] tools/list — returning ${TOOLS.length} tools`);
330
+ return { tools: TOOLS };
331
+ });
332
+
333
+ // tools/call — dispatch to backend via HTTP (except doctor which is local)
334
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
335
+ const { name, arguments: args } = request.params;
336
+ console.error(`[MCP] tools/call — ${name}`);
337
+
338
+ try {
339
+ // descix_doctor is handled locally
340
+ if (name === 'descix_doctor') {
341
+ const report = await runDoctor(args);
342
+ return {
343
+ content: [{ type: 'text', text: JSON.stringify(report, null, 2) }],
344
+ };
345
+ }
346
+
347
+ // Merge default context for convenience (caller can override).
348
+ // Only auto-fill when workspace is configured — prevents silently
349
+ // targeting wrong app/community.
350
+ const params = { ...args };
351
+ if (defaultContext) {
352
+ if (!params.app_id && defaultContext.appId) params.app_id = defaultContext.appId;
353
+ if (!params.kb_id && (defaultContext.kbId || 'General')) params.kb_id = params.kb_id || defaultContext.kbId || 'General';
354
+ if (!params.community_id && defaultContext.communityId) params.community_id = defaultContext.communityId;
355
+ }
356
+
357
+ const result = await apiClient.invoke(name, params);
358
+ return {
359
+ content: [{
360
+ type: 'text',
361
+ text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
362
+ }],
363
+ };
364
+ } catch (error) {
365
+ console.error(`[MCP] Tool error: ${error.message}`);
366
+ return {
367
+ content: [{ type: 'text', text: `Error: ${error.message}` }],
368
+ isError: true,
369
+ };
370
+ }
371
+ });
372
+
373
+ // Connect stdio transport
374
+ const transport = new StdioServerTransport();
375
+ console.error('[MCP] Connecting stdio transport...');
376
+ await server.connect(transport);
377
+ console.error('[MCP] Server connected and running');
378
+ } catch (error) {
379
+ console.error(`[MCP] Fatal error: ${error.message}`);
380
+ console.error(error.stack);
381
+ process.exit(1);
382
+ }
Binary file
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Agent Files — Multi-format instruction file generator
3
+ *
4
+ * Reads workspace.json and generates agent instruction files for
5
+ * Claude Code, GitHub Copilot, Cursor, and Cline from shared templates.
6
+ *
7
+ * Shared between CLI (descix quickstart) and VS Code extension.
8
+ */
9
+
10
+ import * as fs from 'fs/promises';
11
+ import * as path from 'path';
12
+ import { fileURLToPath } from 'url';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+ const TEMPLATES_DIR = path.join(__dirname, '../templates');
17
+
18
+ /**
19
+ * Template → output file mapping
20
+ */
21
+ const AGENT_FILES = [
22
+ { template: 'agent-claude.md', output: 'CLAUDE.md' },
23
+ { template: 'agent-copilot.md', output: '.github/copilot-instructions.md' },
24
+ { template: 'agent-cursor.md', output: '.cursorrules' },
25
+ { template: 'agent-cline.md', output: '.clinerules' },
26
+ ];
27
+
28
+ /**
29
+ * Read workspace.json and extract template variables
30
+ */
31
+ async function loadContext(workspaceRoot) {
32
+ const wsPath = path.join(workspaceRoot, '.descix', 'workspace.json');
33
+ try {
34
+ const raw = await fs.readFile(wsPath, 'utf-8');
35
+ const config = JSON.parse(raw);
36
+
37
+ // Derive from v2.1 format or legacy
38
+ const platform = config.env?.platform || {};
39
+ const appId = platform.appId || config.defaultContext?.appId || 'my-app';
40
+ const communityId = platform.communityId || config.defaultContext?.communityId || config.primaryCommunity || 'descix';
41
+
42
+ // Derive API URL
43
+ let apiUrl = config.apiUrl || null;
44
+ if (!apiUrl && platform.microservice?.port) {
45
+ apiUrl = `https://localhost:${platform.microservice.port}`;
46
+ }
47
+ apiUrl = apiUrl || 'https://descix.net';
48
+
49
+ // Friendly name: capitalize app_id or use as-is
50
+ const appName = appId.charAt(0).toUpperCase() + appId.slice(1);
51
+
52
+ return { appId, communityId, apiUrl, appName };
53
+ } catch {
54
+ // No workspace.json — use defaults
55
+ return {
56
+ appId: path.basename(workspaceRoot),
57
+ communityId: 'descix',
58
+ apiUrl: 'https://descix.net',
59
+ appName: path.basename(workspaceRoot),
60
+ };
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Substitute {{placeholder}} tokens in a template string
66
+ */
67
+ function render(template, vars) {
68
+ return template
69
+ .replace(/\{\{appId\}\}/g, vars.appId)
70
+ .replace(/\{\{communityId\}\}/g, vars.communityId)
71
+ .replace(/\{\{apiUrl\}\}/g, vars.apiUrl)
72
+ .replace(/\{\{appName\}\}/g, vars.appName);
73
+ }
74
+
75
+ /**
76
+ * Generate all agent instruction files in the workspace root.
77
+ *
78
+ * @param {string} workspaceRoot - Project root directory
79
+ * @returns {Promise<string[]>} List of files written
80
+ */
81
+ export async function generateAgentFiles(workspaceRoot) {
82
+ const vars = await loadContext(workspaceRoot);
83
+ const written = [];
84
+
85
+ for (const { template, output } of AGENT_FILES) {
86
+ const templatePath = path.join(TEMPLATES_DIR, template);
87
+ const outputPath = path.join(workspaceRoot, output);
88
+
89
+ try {
90
+ // Check if file exists and has been customized
91
+ try {
92
+ const existing = await fs.readFile(outputPath, 'utf-8');
93
+ if (!existing.includes('<!-- BOOTSTRAP')) {
94
+ // File has been customized — don't overwrite
95
+ continue;
96
+ }
97
+ } catch {
98
+ // File doesn't exist — proceed with writing
99
+ }
100
+
101
+ const raw = await fs.readFile(templatePath, 'utf-8');
102
+ const content = render(raw, vars);
103
+
104
+ // Ensure parent directory exists (e.g., .github/)
105
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
106
+ await fs.writeFile(outputPath, content, 'utf-8');
107
+ written.push(output);
108
+ } catch (err) {
109
+ // Template missing — skip silently
110
+ console.error(`[agent-files] Skipped ${template}: ${err.message}`);
111
+ }
112
+ }
113
+
114
+ return written;
115
+ }
116
+
117
+ /**
118
+ * Generate .vscode/mcp.json for non-extension users.
119
+ * Skips if the DeSciX VS Code extension is installed (it registers MCP natively).
120
+ *
121
+ * @param {string} workspaceRoot - Project root directory
122
+ * @returns {Promise<boolean>} True if file was written, false if skipped
123
+ */
124
+ export async function generateMcpConfig(workspaceRoot) {
125
+ // Skip if the DeSciX extension is installed — it registers the MCP server
126
+ // natively via registerMcpServerDefinitionProvider, so mcp.json would create
127
+ // a duplicate namespace.
128
+ try {
129
+ const extensionsDir = path.join(
130
+ process.env.HOME || process.env.USERPROFILE || '',
131
+ '.vscode', 'extensions'
132
+ );
133
+ const entries = await fs.readdir(extensionsDir);
134
+ if (entries.some(e => e.startsWith('descix.'))) {
135
+ return false; // Extension installed — skip mcp.json
136
+ }
137
+ } catch {
138
+ // Can't read extensions dir — proceed with mcp.json
139
+ }
140
+
141
+ const outputPath = path.join(workspaceRoot, '.vscode', 'mcp.json');
142
+
143
+ // Preserve existing servers if file already exists
144
+ let existing = { servers: {} };
145
+ try {
146
+ const raw = await fs.readFile(outputPath, 'utf-8');
147
+ existing = JSON.parse(raw);
148
+ if (!existing.servers) existing.servers = {};
149
+ } catch {
150
+ // No existing file — start fresh
151
+ }
152
+
153
+ // Add/update DeSciX MCP server
154
+ existing.servers['descix'] = {
155
+ command: 'descix',
156
+ args: ['mcp-serve'],
157
+ };
158
+
159
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
160
+ await fs.writeFile(outputPath, JSON.stringify(existing, null, 2), 'utf-8');
161
+ return true;
162
+ }