@sage-protocol/cli 0.3.0 ā 0.3.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/dist/cli/commands/doctor.js +87 -8
- package/dist/cli/commands/gov-config.js +81 -0
- package/dist/cli/commands/governance.js +152 -72
- package/dist/cli/commands/library.js +9 -0
- package/dist/cli/commands/proposals.js +187 -17
- package/dist/cli/commands/skills.js +175 -21
- package/dist/cli/commands/subdao.js +22 -2
- package/dist/cli/config/playbooks.json +15 -0
- package/dist/cli/governance-manager.js +25 -4
- package/dist/cli/index.js +5 -6
- package/dist/cli/library-manager.js +79 -0
- package/dist/cli/mcp-server-stdio.js +1655 -82
- package/dist/cli/schemas/manifest.schema.json +55 -0
- package/dist/cli/services/doctor/fixers.js +134 -0
- package/dist/cli/services/mcp/bulk-operations.js +272 -0
- package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
- package/dist/cli/services/mcp/library-listing.js +2 -2
- package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
- package/dist/cli/services/mcp/manifest-downloader.js +5 -3
- package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
- package/dist/cli/services/mcp/manifest-workflows.js +127 -15
- package/dist/cli/services/mcp/quick-start.js +287 -0
- package/dist/cli/services/mcp/stdio-runner.js +30 -5
- package/dist/cli/services/mcp/template-manager.js +156 -0
- package/dist/cli/services/mcp/templates/default-templates.json +84 -0
- package/dist/cli/services/mcp/tool-args-validator.js +66 -0
- package/dist/cli/services/mcp/trending-formatter.js +1 -1
- package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
- package/dist/cli/services/metaprompt/designer.js +12 -5
- package/dist/cli/services/subdao/applier.js +208 -196
- package/dist/cli/services/subdao/planner.js +41 -6
- package/dist/cli/subdao-manager.js +14 -0
- package/dist/cli/utils/aliases.js +17 -2
- package/dist/cli/utils/contract-error-decoder.js +61 -0
- package/dist/cli/utils/suggestions.js +17 -12
- package/package.json +3 -2
- 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: '
|
|
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: '
|
|
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
|