@sage-protocol/cli 0.3.0 → 0.3.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 (37) hide show
  1. package/dist/cli/commands/doctor.js +87 -8
  2. package/dist/cli/commands/gov-config.js +81 -0
  3. package/dist/cli/commands/governance.js +152 -72
  4. package/dist/cli/commands/library.js +9 -0
  5. package/dist/cli/commands/proposals.js +187 -17
  6. package/dist/cli/commands/skills.js +175 -21
  7. package/dist/cli/commands/subdao.js +22 -2
  8. package/dist/cli/config/playbooks.json +15 -0
  9. package/dist/cli/governance-manager.js +25 -4
  10. package/dist/cli/index.js +5 -6
  11. package/dist/cli/library-manager.js +79 -0
  12. package/dist/cli/mcp-server-stdio.js +1374 -82
  13. package/dist/cli/schemas/manifest.schema.json +55 -0
  14. package/dist/cli/services/doctor/fixers.js +134 -0
  15. package/dist/cli/services/mcp/bulk-operations.js +272 -0
  16. package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
  17. package/dist/cli/services/mcp/library-listing.js +2 -2
  18. package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
  19. package/dist/cli/services/mcp/manifest-downloader.js +5 -3
  20. package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
  21. package/dist/cli/services/mcp/manifest-workflows.js +127 -15
  22. package/dist/cli/services/mcp/quick-start.js +287 -0
  23. package/dist/cli/services/mcp/stdio-runner.js +30 -5
  24. package/dist/cli/services/mcp/template-manager.js +156 -0
  25. package/dist/cli/services/mcp/templates/default-templates.json +84 -0
  26. package/dist/cli/services/mcp/tool-args-validator.js +56 -0
  27. package/dist/cli/services/mcp/trending-formatter.js +1 -1
  28. package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
  29. package/dist/cli/services/metaprompt/designer.js +12 -5
  30. package/dist/cli/services/subdao/applier.js +208 -196
  31. package/dist/cli/services/subdao/planner.js +41 -6
  32. package/dist/cli/subdao-manager.js +14 -0
  33. package/dist/cli/utils/aliases.js +17 -2
  34. package/dist/cli/utils/contract-error-decoder.js +61 -0
  35. package/dist/cli/utils/suggestions.js +17 -12
  36. package/package.json +3 -2
  37. package/src/schemas/manifest.schema.json +55 -0
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://sage-protocol.org/schemas/manifest.schema.json",
4
+ "title": "Sage Library Manifest",
5
+ "type": "object",
6
+ "required": ["version", "library", "prompts"],
7
+ "additionalProperties": true,
8
+ "properties": {
9
+ "version": {
10
+ "anyOf": [
11
+ { "type": "string", "const": "2.0.0" },
12
+ { "type": "integer", "const": 2 }
13
+ ]
14
+ },
15
+ "library": {
16
+ "type": "object",
17
+ "required": ["name"],
18
+ "additionalProperties": true,
19
+ "properties": {
20
+ "name": { "type": "string", "minLength": 1 },
21
+ "description": { "type": "string" },
22
+ "previous": { "type": "string" }
23
+ }
24
+ },
25
+ "prompts": {
26
+ "type": "array",
27
+ "minItems": 0,
28
+ "items": {
29
+ "type": "object",
30
+ "required": ["key"],
31
+ "additionalProperties": true,
32
+ "properties": {
33
+ "key": { "type": "string", "minLength": 1 },
34
+ "cid": { "type": "string", "minLength": 46 },
35
+ "name": { "type": "string" },
36
+ "description": { "type": "string" },
37
+ "tags": {
38
+ "type": "array",
39
+ "items": { "type": "string", "minLength": 1 }
40
+ },
41
+ "files": {
42
+ "type": "array",
43
+ "items": { "type": "string", "minLength": 1 },
44
+ "minItems": 0
45
+ }
46
+ }
47
+ }
48
+ },
49
+ "compositions": { "type": "object" },
50
+ "dependencies": {
51
+ "type": "array",
52
+ "items": { "type": "string" }
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,134 @@
1
+ const inquirer = (require('inquirer').default || require('inquirer'));
2
+ const config = require('../../config');
3
+ const { execSync } = require('child_process');
4
+ const { ethers } = require('ethers');
5
+
6
+ async function fixRpcUrl(context = {}) {
7
+ console.log('\nšŸ”§ Fixing RPC URL...');
8
+ const { diagnoseRpcIssues, checkRpcHealth } = require('../../utils/rpc-health');
9
+ const envCandidates = [
10
+ process.env.RPC_URL,
11
+ process.env.BASE_SEPOLIA_RPC,
12
+ 'https://base-sepolia.publicnode.com',
13
+ 'https://sepolia.base.org',
14
+ 'https://base-sepolia.gateway.tenderly.co'
15
+ ].filter(Boolean);
16
+
17
+ let recommended = null;
18
+ try {
19
+ const results = await diagnoseRpcIssues({ rpcUrls: envCandidates, timeout: 8000 });
20
+ const healthy = results.filter(r => r.healthy).sort((a,b)=> (a.latency ?? 1e9) - (b.latency ?? 1e9));
21
+ if (healthy.length) recommended = healthy[0].url;
22
+ } catch (_) { /* best-effort */ }
23
+
24
+ let url = recommended || envCandidates[0] || 'https://base-sepolia.publicnode.com';
25
+ if (!context.auto) {
26
+ const { url: picked } = await inquirer.prompt([
27
+ {
28
+ type: 'list',
29
+ name: 'url',
30
+ message: 'Select a new RPC URL:',
31
+ choices: [url, ...envCandidates.filter(u => u !== url)]
32
+ }
33
+ ]);
34
+ url = picked;
35
+ }
36
+
37
+ // Fetch chainId and persist
38
+ let chainId = null;
39
+ try {
40
+ const provider = new ethers.JsonRpcProvider(url);
41
+ const net = await provider.getNetwork();
42
+ chainId = Number(net.chainId);
43
+ } catch (e) {
44
+ console.log('āš ļø Failed to detect chainId:', e.message);
45
+ }
46
+
47
+ const profile = config.readProfiles().activeProfile || 'default';
48
+ const current = config.readProfiles().profiles[profile] || {};
49
+ config.writeProfileSettings({ ...current, rpcUrl: url, chainId: chainId ?? current.chainId }, profile);
50
+ console.log(`āœ… RPC URL updated to ${url} for profile '${profile}'${chainId ? ` (chainId=${chainId})` : ''}`);
51
+ }
52
+
53
+ async function fixIpfsKeys(context = {}) {
54
+ console.log('\nšŸ”§ Fixing IPFS Configuration...');
55
+ let provider = 'pinata';
56
+ if (!context.auto) {
57
+ const ans = await inquirer.prompt([
58
+ { type: 'list', name: 'provider', message: 'Select IPFS Provider:', choices: ['pinata'] }
59
+ ]);
60
+ provider = ans.provider;
61
+ }
62
+
63
+ if (provider === 'pinata') {
64
+ let answers = { apiKey: process.env.PINATA_API_KEY, secret: process.env.PINATA_SECRET_API_KEY, jwt: process.env.PINATA_JWT };
65
+ if (!context.auto) {
66
+ answers = await inquirer.prompt([
67
+ { type: 'input', name: 'apiKey', message: 'Pinata API Key:', default: answers.apiKey || '' },
68
+ { type: 'password', name: 'secret', message: 'Pinata Secret Key:', default: answers.secret || '' },
69
+ { type: 'password', name: 'jwt', message: 'Pinata JWT (optional):', default: answers.jwt || '' }
70
+ ]);
71
+ }
72
+
73
+ if (answers.apiKey && answers.secret) {
74
+ // Write to profile IPFS config
75
+ config.writeIpfsConfig({
76
+ provider: 'pinata',
77
+ pinataApiKey: answers.apiKey,
78
+ pinataSecret: answers.secret,
79
+ pinataJwt: answers.jwt || undefined
80
+ });
81
+ console.log('āœ… Saved Pinata configuration to profile (.sage/config.json)');
82
+
83
+ if (!context.auto) {
84
+ // Optional .env convenience (insecure for prod)
85
+ const { addToEnv } = await inquirer.prompt([{ type: 'confirm', name: 'addToEnv', message: 'Also add to .env for dev convenience? (insecure)', default: false }]);
86
+ if (addToEnv) {
87
+ const fs = require('fs');
88
+ const path = require('path');
89
+ const envPath = path.join(process.cwd(), '.env');
90
+ let envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
91
+ if (!envContent.includes('PINATA_API_KEY=')) envContent += `\nPINATA_API_KEY=${answers.apiKey}`;
92
+ if (!envContent.includes('PINATA_SECRET_API_KEY=')) envContent += `\nPINATA_SECRET_API_KEY=${answers.secret}`;
93
+ if (answers.jwt && !envContent.includes('PINATA_JWT=')) envContent += `\nPINATA_JWT=${answers.jwt}`;
94
+ fs.writeFileSync(envPath, envContent);
95
+ try { require('dotenv').config(); } catch(_){}
96
+ console.log('āœ… Added Pinata keys to .env');
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ async function fixAllowance(tokenAddress, spenderAddress, spenderName = 'Spender', context = {}) {
104
+ console.log(`\nšŸ”§ Fixing Allowance for ${spenderName}...`);
105
+ let proceed = !!context.auto;
106
+ if (!proceed) {
107
+ const ans = await inquirer.prompt([
108
+ { type: 'confirm', name: 'confirm', message: `Approve ${spenderName} to spend SXXX?`, default: true }
109
+ ]);
110
+ proceed = !!ans.confirm;
111
+ }
112
+
113
+ if (!proceed) return;
114
+
115
+ const walletArgs = config.getCastWalletArgs();
116
+ const rpcUrl = process.env.RPC_URL || 'https://base-sepolia.publicnode.com';
117
+
118
+ // Max uint256
119
+ const amount = '115792089237316195423570985008687907853269984665640564039457584007913129639935';
120
+
121
+ try {
122
+ console.log('ā³ Submitting approval...');
123
+ execSync(`cast send ${tokenAddress} "approve(address,uint256)" ${spenderAddress} ${amount} --rpc-url ${rpcUrl} ${walletArgs} --legacy`, { stdio: 'inherit' });
124
+ console.log('āœ… Approved.');
125
+ } catch (e) {
126
+ console.error('āŒ Approval failed:', e.message);
127
+ }
128
+ }
129
+
130
+ module.exports = {
131
+ fixRpcUrl,
132
+ fixIpfsKeys,
133
+ fixAllowance
134
+ };
@@ -0,0 +1,272 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { LibraryManager } = require('../../library-manager');
4
+
5
+ /**
6
+ * Bulk metadata helpers for pinned library manifests.
7
+ *
8
+ * These helpers intentionally mirror the semantics of quick-start flows:
9
+ * - Operate on pinned manifests in ~/.sage/libraries (or project .sage/libraries).
10
+ * - Reuse LibraryManager for directory resolution & manifest loading.
11
+ * - Back up prompt files when content changes.
12
+ */
13
+ function createBulkOperations({
14
+ libraryManager = new LibraryManager(),
15
+ fsModule = fs,
16
+ pathModule = path,
17
+ logger = console,
18
+ } = {}) {
19
+ function resolvePinnedLibrary(libraryId) {
20
+ const all = libraryManager.listPinned() || [];
21
+ const id = String(libraryId || '').trim();
22
+ if (!id) {
23
+ throw new Error('library parameter is required');
24
+ }
25
+
26
+ // Exact CID match first
27
+ const byCid = all.find((row) => row.cid === id);
28
+ if (byCid) {
29
+ const { manifest, path: manifestPath } = libraryManager.loadPinned(byCid.cid);
30
+ return { row: byCid, manifest, manifestPath };
31
+ }
32
+
33
+ const lower = id.toLowerCase();
34
+ const matches = all
35
+ .map((row) => {
36
+ try {
37
+ const { manifest, path: manifestPath } = libraryManager.loadPinned(row.cid);
38
+ const libName = String(manifest?.library?.name || row.name || '').toLowerCase();
39
+ return { row, manifest, manifestPath, libName };
40
+ } catch (e) {
41
+ logger?.warn?.('bulk_ops_manifest_load_failed', { cid: row.cid, err: e.message || String(e) });
42
+ return null;
43
+ }
44
+ })
45
+ .filter(Boolean)
46
+ .filter((entry) => entry.libName === lower);
47
+
48
+ if (matches.length === 0) {
49
+ throw new Error(`No pinned library found matching "${libraryId}". Try using the CID from ~/.sage/libraries/*.json.`);
50
+ }
51
+ if (matches.length > 1) {
52
+ const cids = matches.map((m) => `${m.row.cid} (${m.manifest?.library?.name || m.row.name || 'unnamed'})`);
53
+ throw new Error(
54
+ `Ambiguous library match for "${libraryId}". Candidates:\n- ${cids.join('\n- ')}\nPass an explicit CID to disambiguate.`,
55
+ );
56
+ }
57
+
58
+ return matches[0];
59
+ }
60
+
61
+ function normalizeTagsInput(tags) {
62
+ if (tags === undefined) return undefined;
63
+ if (Array.isArray(tags)) {
64
+ return tags.map((t) => String(t).trim()).filter(Boolean);
65
+ }
66
+ // Single string: split on commas/semicolons
67
+ const str = String(tags);
68
+ return str
69
+ .split(/[,;]/)
70
+ .map((t) => t.trim())
71
+ .filter(Boolean);
72
+ }
73
+
74
+ function uniq(arr) {
75
+ const out = [];
76
+ const seen = new Set();
77
+ for (const v of arr) {
78
+ if (!seen.has(v)) {
79
+ seen.add(v);
80
+ out.push(v);
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+
86
+ async function updateLibraryMetadata({
87
+ library,
88
+ name,
89
+ description,
90
+ tags,
91
+ apply_to_prompts = false,
92
+ merge_mode = 'merge',
93
+ } = {}) {
94
+ const { row, manifest, manifestPath } = resolvePinnedLibrary(library);
95
+ const updatedFields = [];
96
+
97
+ if (!manifest.library || typeof manifest.library !== 'object') {
98
+ manifest.library = {};
99
+ }
100
+
101
+ if (typeof name === 'string' && name.trim().length) {
102
+ manifest.library.name = name;
103
+ updatedFields.push('name');
104
+ }
105
+ if (typeof description === 'string') {
106
+ manifest.library.description = description;
107
+ updatedFields.push('description');
108
+ }
109
+
110
+ const normalizedTags = normalizeTagsInput(tags);
111
+ if (normalizedTags) {
112
+ manifest.library.tags = normalizedTags;
113
+ updatedFields.push('tags');
114
+ }
115
+
116
+ let updatedPromptCount = 0;
117
+ if (apply_to_prompts && normalizedTags && Array.isArray(manifest.prompts)) {
118
+ const mode = merge_mode === 'replace' ? 'replace' : 'merge';
119
+ for (const prompt of manifest.prompts) {
120
+ if (!prompt || typeof prompt !== 'object') continue;
121
+ const existing = Array.isArray(prompt.tags)
122
+ ? prompt.tags.map((t) => String(t).trim()).filter(Boolean)
123
+ : [];
124
+ if (mode === 'replace') {
125
+ prompt.tags = normalizedTags.slice();
126
+ } else {
127
+ prompt.tags = uniq([...existing, ...normalizedTags]);
128
+ }
129
+ updatedPromptCount += 1;
130
+ }
131
+ }
132
+
133
+ fsModule.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
134
+
135
+ return {
136
+ success: true,
137
+ library: manifest.library?.name || row.name || library,
138
+ cid: row.cid,
139
+ updatedLibraryFields: updatedFields,
140
+ updatedPromptCount,
141
+ manifestPath,
142
+ };
143
+ }
144
+
145
+ async function bulkUpdatePrompts({ library, updates, dry_run = false } = {}) {
146
+ if (!Array.isArray(updates) || updates.length === 0) {
147
+ throw new Error('updates must be a non-empty array');
148
+ }
149
+ const { row, manifest, manifestPath } = resolvePinnedLibrary(library);
150
+ const prompts = Array.isArray(manifest.prompts) ? manifest.prompts : [];
151
+ const byKey = new Map();
152
+ prompts.forEach((p) => {
153
+ if (p && typeof p.key === 'string') {
154
+ byKey.set(p.key, p);
155
+ }
156
+ });
157
+
158
+ const missingKeys = [];
159
+ const wouldUpdatePrompts = [];
160
+ const backedUpFiles = [];
161
+ const changedKeys = new Set();
162
+
163
+ const libDir = libraryManager.ensureLibrariesDir();
164
+
165
+ for (const update of updates) {
166
+ const key = String(update.key || '').trim();
167
+ if (!key) continue;
168
+ const prompt = byKey.get(key);
169
+ if (!prompt) {
170
+ missingKeys.push(key);
171
+ continue;
172
+ }
173
+
174
+ const changes = {};
175
+ let changedThisPrompt = false;
176
+
177
+ // Content update
178
+ if (typeof update.content === 'string') {
179
+ const files = Array.isArray(prompt.files) ? prompt.files : [];
180
+ let targetPath;
181
+ if (files.length > 0) {
182
+ const relativePath = files[0];
183
+ targetPath = pathModule.isAbsolute(relativePath)
184
+ ? relativePath
185
+ : pathModule.join(libDir, relativePath);
186
+ } else {
187
+ const promptsDir = pathModule.join(libDir, 'prompts');
188
+ if (!fsModule.existsSync(promptsDir)) {
189
+ fsModule.mkdirSync(promptsDir, { recursive: true });
190
+ }
191
+ const fileName = `${row.cid}_${key}.md`;
192
+ targetPath = pathModule.join(promptsDir, fileName);
193
+ prompt.files = [`prompts/${fileName}`];
194
+ }
195
+
196
+ const beforeContent = !dry_run && fsModule.existsSync(targetPath)
197
+ ? fsModule.readFileSync(targetPath, 'utf8')
198
+ : null;
199
+
200
+ if (dry_run) {
201
+ changes.content = { from: beforeContent === null ? '(new file)' : '(existing file)', to: '(would change)' };
202
+ changedThisPrompt = true;
203
+ } else {
204
+ if (beforeContent !== null) {
205
+ const backupPath = `${targetPath}.v${Date.now()}.bak`;
206
+ fsModule.copyFileSync(targetPath, backupPath);
207
+ backedUpFiles.push(backupPath);
208
+ }
209
+ fsModule.writeFileSync(targetPath, update.content, 'utf8');
210
+ changes.content = { from: '(previous content)', to: '(updated content)' };
211
+ changedThisPrompt = true;
212
+ }
213
+ }
214
+
215
+ // Name / description
216
+ if (typeof update.name === 'string') {
217
+ changes.name = { from: prompt.name || '', to: update.name };
218
+ if (!dry_run) {
219
+ prompt.name = update.name;
220
+ changedThisPrompt = true;
221
+ }
222
+ }
223
+ if (typeof update.description === 'string') {
224
+ changes.description = { from: prompt.description || '', to: update.description };
225
+ if (!dry_run) {
226
+ prompt.description = update.description;
227
+ changedThisPrompt = true;
228
+ }
229
+ }
230
+
231
+ if (update.tags !== undefined) {
232
+ const normalizedTags = normalizeTagsInput(update.tags) || [];
233
+ const beforeTags = Array.isArray(prompt.tags) ? prompt.tags.slice() : [];
234
+ changes.tags = { from: beforeTags, to: normalizedTags };
235
+ if (!dry_run) {
236
+ prompt.tags = normalizedTags;
237
+ changedThisPrompt = true;
238
+ }
239
+ }
240
+
241
+ if (Object.keys(changes).length > 0) {
242
+ wouldUpdatePrompts.push({ key, changes });
243
+ changedKeys.add(key);
244
+ }
245
+ }
246
+
247
+ if (!dry_run) {
248
+ fsModule.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
249
+ }
250
+
251
+ return {
252
+ success: true,
253
+ library: manifest.library?.name || row.name || library,
254
+ cid: row.cid,
255
+ dryRun: !!dry_run,
256
+ updatedCount: changedKeys.size,
257
+ missingKeys,
258
+ backedUpFiles: dry_run ? [] : backedUpFiles,
259
+ wouldUpdatePrompts,
260
+ manifestPath,
261
+ };
262
+ }
263
+
264
+ return {
265
+ updateLibraryMetadata,
266
+ bulkUpdatePrompts,
267
+ };
268
+ }
269
+
270
+ module.exports = {
271
+ createBulkOperations,
272
+ };
@@ -0,0 +1,202 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { LibraryManager } = require('../../library-manager');
4
+
5
+ function createDependencyAnalyzer({
6
+ libraryManager = new LibraryManager(),
7
+ fsModule = fs,
8
+ pathModule = path,
9
+ logger = console,
10
+ } = {}) {
11
+ function resolvePinnedLibrary(libraryId) {
12
+ const all = libraryManager.listPinned() || [];
13
+ const id = String(libraryId || '').trim();
14
+ if (!id) {
15
+ throw new Error('library parameter is required');
16
+ }
17
+
18
+ const byCid = all.find((row) => row.cid === id);
19
+ if (byCid) {
20
+ const { manifest, path: manifestPath } = libraryManager.loadPinned(byCid.cid);
21
+ return { row: byCid, manifest, manifestPath };
22
+ }
23
+
24
+ const lower = id.toLowerCase();
25
+ const matches = all
26
+ .map((row) => {
27
+ try {
28
+ const { manifest, path: manifestPath } = libraryManager.loadPinned(row.cid);
29
+ const libName = String(manifest?.library?.name || row.name || '').toLowerCase();
30
+ return { row, manifest, manifestPath, libName };
31
+ } catch (e) {
32
+ logger?.warn?.('dep_analyzer_manifest_load_failed', { cid: row.cid, err: e.message || String(e) });
33
+ return null;
34
+ }
35
+ })
36
+ .filter(Boolean)
37
+ .filter((entry) => entry.libName === lower);
38
+
39
+ if (matches.length === 0) {
40
+ throw new Error(`No pinned library found matching "${libraryId}". Try using the CID from ~/.sage/libraries/*.json.`);
41
+ }
42
+ if (matches.length > 1) {
43
+ const cids = matches.map((m) => `${m.row.cid} (${m.manifest?.library?.name || m.row.name || 'unnamed'})`);
44
+ throw new Error(
45
+ `Ambiguous library match for "${libraryId}". Candidates:\n- ${cids.join('\n- ')}\nPass an explicit CID to disambiguate.`,
46
+ );
47
+ }
48
+
49
+ return matches[0];
50
+ }
51
+
52
+ function extractVariables(content) {
53
+ const text = String(content || '');
54
+ const pattern = /\$\{([A-Za-z0-9_]+)\}/g;
55
+ const vars = [];
56
+ let match;
57
+ while ((match = pattern.exec(text)) !== null) {
58
+ if (!vars.includes(match[1])) {
59
+ vars.push(match[1]);
60
+ }
61
+ }
62
+ return vars;
63
+ }
64
+
65
+ function groupInconsistencies(variableUsage) {
66
+ const names = Object.keys(variableUsage || {});
67
+ const lowerMap = new Map();
68
+ for (const name of names) {
69
+ const base = name.toLowerCase();
70
+ if (!lowerMap.has(base)) lowerMap.set(base, []);
71
+ lowerMap.get(base).push(name);
72
+ }
73
+
74
+ const groups = [];
75
+ for (const [base, variants] of lowerMap.entries()) {
76
+ if (variants.length <= 1) continue;
77
+ groups.push({
78
+ base,
79
+ variants,
80
+ totalPrompts: variants.reduce((acc, v) => acc + (variableUsage[v]?.count || 0), 0),
81
+ });
82
+ }
83
+ return groups;
84
+ }
85
+
86
+ async function analyzeDependencies({ library, analysis_type = 'variables' } = {}) {
87
+ const { row, manifest, manifestPath } = resolvePinnedLibrary(library);
88
+ const prompts = Array.isArray(manifest.prompts) ? manifest.prompts : [];
89
+ const libDir = pathModule.dirname(manifestPath);
90
+
91
+ const perPrompt = [];
92
+ const variableUsage = {};
93
+
94
+ for (const prompt of prompts) {
95
+ if (!prompt || typeof prompt !== 'object') continue;
96
+ const key = prompt.key || '(no-key)';
97
+ let content = '';
98
+
99
+ if (Array.isArray(prompt.files) && prompt.files.length) {
100
+ const firstFile = prompt.files[0];
101
+ const candidatePath = pathModule.isAbsolute(firstFile)
102
+ ? firstFile
103
+ : pathModule.join(libDir, firstFile);
104
+ try {
105
+ content = fsModule.readFileSync(candidatePath, 'utf8');
106
+ } catch (error) {
107
+ logger?.debug?.('dep_analyzer_read_failed', { file: candidatePath, err: error.message || String(error) });
108
+ }
109
+ }
110
+
111
+ const vars = extractVariables(content);
112
+ perPrompt.push({
113
+ key,
114
+ name: prompt.name || key,
115
+ variables: vars,
116
+ });
117
+
118
+ for (const v of vars) {
119
+ if (!variableUsage[v]) {
120
+ variableUsage[v] = { count: 0, prompts: [] };
121
+ }
122
+ variableUsage[v].count += 1;
123
+ variableUsage[v].prompts.push(key);
124
+ }
125
+ }
126
+
127
+ const sharedVariables = Object.keys(variableUsage).filter((v) => variableUsage[v].count > 1);
128
+ const uniqueVariables = Object.keys(variableUsage).filter((v) => variableUsage[v].count === 1);
129
+ const inconsistencyGroups = groupInconsistencies(variableUsage);
130
+
131
+ const lines = [];
132
+ lines.push(`šŸ“Š Variable Usage Analysis for "${manifest.library?.name || row.name || library}"`);
133
+ lines.push('');
134
+ if (!perPrompt.length) {
135
+ lines.push('No prompts found in this library.');
136
+ } else {
137
+ lines.push(`Prompts analyzed: ${perPrompt.length}`);
138
+ lines.push(`Total distinct variables: ${Object.keys(variableUsage).length}`);
139
+ lines.push('');
140
+ if (sharedVariables.length) {
141
+ lines.push('Shared variables (used in multiple prompts):');
142
+ sharedVariables.slice(0, 10).forEach((v) => {
143
+ lines.push(`- ${v} (${variableUsage[v].count} prompts)`);
144
+ });
145
+ if (sharedVariables.length > 10) {
146
+ lines.push(`… and ${sharedVariables.length - 10} more`);
147
+ }
148
+ lines.push('');
149
+ } else {
150
+ lines.push('No shared variables detected across prompts.');
151
+ lines.push('');
152
+ }
153
+
154
+ if (inconsistencyGroups.length) {
155
+ lines.push('Potential naming inconsistency groups:');
156
+ inconsistencyGroups.slice(0, 5).forEach((g) => {
157
+ lines.push(`- ${g.base}: ${g.variants.join(', ')} (approx. ${g.totalPrompts} prompt usages)`);
158
+ });
159
+ if (inconsistencyGroups.length > 5) {
160
+ lines.push(`… and ${inconsistencyGroups.length - 5} more groups`);
161
+ }
162
+ lines.push('');
163
+ }
164
+ }
165
+
166
+ return {
167
+ content: [
168
+ { type: 'text', text: lines.join('\n') },
169
+ {
170
+ type: 'text',
171
+ text:
172
+ '```json\n' +
173
+ JSON.stringify(
174
+ {
175
+ library: {
176
+ cid: row.cid,
177
+ name: manifest.library?.name || row.name || library,
178
+ },
179
+ perPrompt,
180
+ variableUsage,
181
+ sharedVariables,
182
+ uniqueVariables,
183
+ inconsistencyGroups,
184
+ },
185
+ null,
186
+ 2,
187
+ ) +
188
+ '\n```',
189
+ },
190
+ ],
191
+ };
192
+ }
193
+
194
+ return {
195
+ analyzeDependencies,
196
+ };
197
+ }
198
+
199
+ module.exports = {
200
+ createDependencyAnalyzer,
201
+ };
202
+
@@ -49,7 +49,7 @@ function createLibraryLister({
49
49
  return {
50
50
  content: [
51
51
  { type: 'text', text: formatted },
52
- { type: 'json', text: JSON.stringify(data ?? {}, null, DEFAULT_INDENT) },
52
+ { type: 'text', text: '```json\n' + JSON.stringify(data ?? {}, null, DEFAULT_INDENT) + '\n```' },
53
53
  ],
54
54
  };
55
55
  }
@@ -64,7 +64,7 @@ function createLibraryLister({
64
64
  return {
65
65
  content: [
66
66
  { type: 'text', text: formatted },
67
- { type: 'json', text: JSON.stringify(data ?? {}, null, DEFAULT_INDENT) },
67
+ { type: 'text', text: '```json\n' + JSON.stringify(data ?? {}, null, DEFAULT_INDENT) + '\n```' },
68
68
  ],
69
69
  };
70
70
  } catch (error) {
@@ -96,6 +96,7 @@ function collectLocalPrompts({
96
96
  if (normalizedQuery) {
97
97
  const matchesQuery =
98
98
  promptName.toLowerCase().includes(normalizedQuery) ||
99
+ (prompt.key && prompt.key.toLowerCase().includes(normalizedQuery)) ||
99
100
  promptDescription.toLowerCase().includes(normalizedQuery) ||
100
101
  loweredPromptTags.some((tag) => tag.includes(normalizedQuery));
101
102
  if (!matchesQuery) continue; // eslint-disable-line no-continue