@sage-protocol/cli 0.4.1 → 0.4.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/prompts.js +243 -87
- package/dist/cli/commands/wallet.js +34 -15
- package/dist/cli/config.js +13 -0
- package/dist/cli/index.js +2 -2
- package/dist/cli/mcp-server-stdio.js +226 -165
- package/dist/cli/services/artifact-manager.js +198 -0
- package/dist/cli/services/mcp/prompt-result-formatter.js +9 -1
- package/dist/cli/services/mcp/sage-tool-registry.js +25 -33
- package/dist/cli/services/mcp/tool-args-validator.js +12 -0
- package/dist/cli/services/project-context.js +98 -0
- package/dist/cli/utils/aliases.js +0 -6
- package/package.json +1 -1
- package/dist/cli/commands/prompt-test.js +0 -176
- package/dist/cli/commands/prompt.js +0 -2531
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
const { readWorkspace, computeChangeSet } = require('./prompts/workspace');
|
|
6
|
+
|
|
7
|
+
class ArtifactManager {
|
|
8
|
+
constructor(cwd = process.cwd()) {
|
|
9
|
+
this.cwd = cwd;
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.promptsDir = path.join(this.cwd, 'prompts');
|
|
12
|
+
this.sageDir = path.join(this.cwd, '.sage');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async initialize() {
|
|
16
|
+
if (!fs.existsSync(this.promptsDir)) fs.mkdirSync(this.promptsDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* List all artifacts, optionally filtered
|
|
21
|
+
* @param {Object} filter { kind, publishable }
|
|
22
|
+
*/
|
|
23
|
+
async listArtifacts(filter = {}) {
|
|
24
|
+
const artifacts = [];
|
|
25
|
+
const ws = readWorkspace();
|
|
26
|
+
const changes = ws ? computeChangeSet(ws) : { added: [], modified: [], removed: [] };
|
|
27
|
+
|
|
28
|
+
if (fs.existsSync(this.promptsDir)) {
|
|
29
|
+
const workspaceFiles = await this._scanDir(this.promptsDir);
|
|
30
|
+
for (const file of workspaceFiles) {
|
|
31
|
+
const artifact = await this._parseArtifact(file, ws, changes);
|
|
32
|
+
if (artifact) artifacts.push(artifact);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return artifacts.filter(a => {
|
|
37
|
+
if (filter.kind && a.kind !== filter.kind) return false;
|
|
38
|
+
if (filter.publishable !== undefined && a.publishable !== filter.publishable) return false;
|
|
39
|
+
return true;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getArtifact(key) {
|
|
44
|
+
// Try to find the file. Key could be "skills/coding-helper" -> prompts/skills/coding-helper.md
|
|
45
|
+
const potentialPath = path.join(this.promptsDir, key + '.md');
|
|
46
|
+
|
|
47
|
+
if (fs.existsSync(potentialPath)) {
|
|
48
|
+
const ws = readWorkspace();
|
|
49
|
+
const changes = ws ? computeChangeSet(ws) : { added: [], modified: [], removed: [] };
|
|
50
|
+
return await this._parseArtifact(potentialPath, ws, changes);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async saveArtifact(artifact) {
|
|
57
|
+
const { key, kind, body, meta, targets, publishable } = artifact;
|
|
58
|
+
|
|
59
|
+
let relPath = key;
|
|
60
|
+
if (!relPath.endsWith('.md')) relPath += '.md';
|
|
61
|
+
|
|
62
|
+
// Enforce directory conventions
|
|
63
|
+
if (kind === 'skill' && !relPath.startsWith('skills/')) {
|
|
64
|
+
relPath = `skills/${relPath}`;
|
|
65
|
+
} else if (kind === 'snippet' && !relPath.startsWith('snippets/')) {
|
|
66
|
+
relPath = `snippets/${relPath}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const filePath = path.join(this.promptsDir, relPath);
|
|
70
|
+
const dir = path.dirname(filePath);
|
|
71
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
72
|
+
|
|
73
|
+
// Update meta
|
|
74
|
+
const frontmatter = {
|
|
75
|
+
...meta,
|
|
76
|
+
kind,
|
|
77
|
+
publishable: publishable !== undefined ? publishable : (kind === 'prompt'),
|
|
78
|
+
targets: targets || meta?.targets || []
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Construct file content
|
|
82
|
+
const yaml = Object.entries(frontmatter)
|
|
83
|
+
.filter(([_, v]) => v !== undefined && v !== null)
|
|
84
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
85
|
+
.join('\n');
|
|
86
|
+
|
|
87
|
+
const content = `---\n${yaml}\n---\n\n${body}`;
|
|
88
|
+
|
|
89
|
+
fs.writeFileSync(filePath, content);
|
|
90
|
+
|
|
91
|
+
return this._parseArtifact(filePath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- Helpers ---
|
|
95
|
+
|
|
96
|
+
async _scanDir(dir, base = '') {
|
|
97
|
+
let results = [];
|
|
98
|
+
try {
|
|
99
|
+
const list = fs.readdirSync(dir);
|
|
100
|
+
for (const file of list) {
|
|
101
|
+
const fullPath = path.join(dir, file);
|
|
102
|
+
const stat = fs.statSync(fullPath);
|
|
103
|
+
if (stat && stat.isDirectory()) {
|
|
104
|
+
results = results.concat(await this._scanDir(fullPath));
|
|
105
|
+
} else {
|
|
106
|
+
if (file.endsWith('.md')) {
|
|
107
|
+
results.push(fullPath);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// ignore if dir doesn't exist
|
|
113
|
+
}
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async _parseArtifact(filePath, ws = null, changes = null) {
|
|
118
|
+
try {
|
|
119
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
120
|
+
const { attributes, body } = this._parseFrontMatter(content);
|
|
121
|
+
|
|
122
|
+
// Infer key from path relative to prompts dir
|
|
123
|
+
const relative = path.relative(this.promptsDir, filePath);
|
|
124
|
+
const key = relative.replace(/\.md$/, '');
|
|
125
|
+
|
|
126
|
+
// Infer kind
|
|
127
|
+
let kind = attributes.kind;
|
|
128
|
+
if (!kind) {
|
|
129
|
+
if (key.startsWith('skills/') || key.includes('/skills/')) kind = 'skill';
|
|
130
|
+
else if (key.startsWith('snippets/') || key.includes('/snippets/')) kind = 'snippet';
|
|
131
|
+
else kind = 'prompt';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Infer publishable
|
|
135
|
+
// Default: true for prompts, false for skills/snippets
|
|
136
|
+
let publishable = attributes.publishable;
|
|
137
|
+
if (publishable === undefined) {
|
|
138
|
+
publishable = (kind === 'prompt');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Derive publishing state
|
|
142
|
+
let status = 'unpublished';
|
|
143
|
+
if (ws && ws.files && ws.files[key]) {
|
|
144
|
+
if (changes && changes.modified.includes(key)) {
|
|
145
|
+
status = 'modified';
|
|
146
|
+
} else {
|
|
147
|
+
status = 'published'; // Assumed synced if in workspace and not modified
|
|
148
|
+
}
|
|
149
|
+
} else if (changes && changes.added.includes(key)) {
|
|
150
|
+
status = 'new';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
key,
|
|
155
|
+
kind,
|
|
156
|
+
publishable,
|
|
157
|
+
body,
|
|
158
|
+
meta: attributes,
|
|
159
|
+
targets: attributes.targets || [],
|
|
160
|
+
publishing: {
|
|
161
|
+
status,
|
|
162
|
+
lastSynced: ws?.files?.[key]?.mtimeMs || null
|
|
163
|
+
},
|
|
164
|
+
filePath
|
|
165
|
+
};
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.warn(`Failed to parse artifact at ${filePath}: `, e.message);
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_parseFrontMatter(content) {
|
|
173
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
174
|
+
if (match) {
|
|
175
|
+
const yaml = match[1];
|
|
176
|
+
const body = match[2];
|
|
177
|
+
const attributes = {};
|
|
178
|
+
yaml.split('\n').forEach(line => {
|
|
179
|
+
const parts = line.split(':');
|
|
180
|
+
if (parts.length >= 2) {
|
|
181
|
+
const k = parts[0].trim();
|
|
182
|
+
const v = parts.slice(1).join(':').trim();
|
|
183
|
+
if (k && v) {
|
|
184
|
+
try {
|
|
185
|
+
attributes[k] = JSON.parse(v);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
attributes[k] = v;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return { attributes, body: body.trim() };
|
|
193
|
+
}
|
|
194
|
+
return { attributes: {}, body: content.trim() };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = ArtifactManager;
|
|
@@ -23,7 +23,15 @@ function formatUnifiedResults(items = [], { total = 0, page = 1, pageSize = 10 }
|
|
|
23
23
|
const libraryName = entry?.library?.name ? ` 🗂️ Library: ${entry.library.name}\n` : '';
|
|
24
24
|
const cidLine = entry?.cid ? ` 🔗 CID: ${entry.cid}\n` : '';
|
|
25
25
|
const origin = entry?.origin || entry?.source || 'unknown';
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
// New unified fields
|
|
28
|
+
const kind = entry?.kind ? ` 🔹 Kind: ${entry.kind}\n` : '';
|
|
29
|
+
const publishable = entry?.publishable !== undefined ? ` 📡 Publishable: ${entry.publishable}\n` : '';
|
|
30
|
+
const status = entry?.publishing?.status ? ` 📊 Status: ${entry.publishing.status}\n` : '';
|
|
31
|
+
const project = entry?.project ? ` 🏗️ Project: ${entry.project}\n` : '';
|
|
32
|
+
const tools = Array.isArray(entry?.tools) && entry.tools.length ? ` 🛠️ Tools: ${entry.tools.join(', ')}\n` : '';
|
|
33
|
+
|
|
34
|
+
return `${absoluteIndex}. **${promptName}**\n 📜 ${description}\n${kind}${publishable}${status}${project}${tools}${libraryName}${cidLine} 🔖 Tags: ${tags}\n 🌐 Source: ${origin}\n`;
|
|
27
35
|
}).join('\n');
|
|
28
36
|
|
|
29
37
|
return head + lines;
|
|
@@ -21,12 +21,12 @@ const TOOL_REGISTRY = {
|
|
|
21
21
|
},
|
|
22
22
|
list_prompts: {
|
|
23
23
|
category: 'prompt_workspace',
|
|
24
|
-
keywords: ['list', 'browse', 'prompt', 'workspace'],
|
|
24
|
+
keywords: ['list', 'browse', 'prompt', 'workspace', 'skill', 'snippet'],
|
|
25
25
|
negativeKeywords: ['search', 'publish'],
|
|
26
|
-
description: 'List prompts from
|
|
27
|
-
whenToUse: 'Use when you want
|
|
26
|
+
description: 'List prompts, skills, or snippets from workspace and/or libraries. Use kind filter for skills.',
|
|
27
|
+
whenToUse: 'Use when you want to browse prompts. Filter by kind (prompt/skill/snippet) to see specific types.',
|
|
28
28
|
requiredParams: [],
|
|
29
|
-
optionalParams: ['source', 'library', 'limit'],
|
|
29
|
+
optionalParams: ['source', 'library', 'limit', 'kind', 'publishable'],
|
|
30
30
|
weight: 0.9,
|
|
31
31
|
},
|
|
32
32
|
get_prompt: {
|
|
@@ -79,16 +79,7 @@ const TOOL_REGISTRY = {
|
|
|
79
79
|
optionalParams: ['variables'],
|
|
80
80
|
weight: 1.0,
|
|
81
81
|
},
|
|
82
|
-
list_workspace_skills:
|
|
83
|
-
category: 'prompt_workspace',
|
|
84
|
-
keywords: ['skill', 'persona', 'workspace', 'list', 'browse'],
|
|
85
|
-
negativeKeywords: ['governance', 'publish'],
|
|
86
|
-
description: 'List skills/personas defined in the current project workspace.',
|
|
87
|
-
whenToUse: 'Use when you want to see available personas/skills for this project.',
|
|
88
|
-
requiredParams: [],
|
|
89
|
-
optionalParams: ['promptsDir'],
|
|
90
|
-
weight: 1.0,
|
|
91
|
-
},
|
|
82
|
+
// NOTE: list_workspace_skills removed - use list_prompts({ kind: 'skill' })
|
|
92
83
|
improve_prompt: {
|
|
93
84
|
category: 'prompt_workspace',
|
|
94
85
|
keywords: ['improve', 'refine', 'quality', 'analyze', 'prompt'],
|
|
@@ -109,16 +100,7 @@ const TOOL_REGISTRY = {
|
|
|
109
100
|
optionalParams: ['newKey', 'name'],
|
|
110
101
|
weight: 0.9,
|
|
111
102
|
},
|
|
112
|
-
|
|
113
|
-
category: 'prompt_workspace',
|
|
114
|
-
keywords: ['skill', 'persona', 'workspace', 'get', 'inspect'],
|
|
115
|
-
negativeKeywords: ['search'],
|
|
116
|
-
description: 'Load a specific workspace skill by path or key.',
|
|
117
|
-
whenToUse: 'Use when you know which skill you want to inspect or pass into an agent.',
|
|
118
|
-
requiredParams: ['path'],
|
|
119
|
-
optionalParams: [],
|
|
120
|
-
weight: 1.0,
|
|
121
|
-
},
|
|
103
|
+
// NOTE: get_workspace_skill removed - use get_prompt({ key })
|
|
122
104
|
|
|
123
105
|
// ───────────── Persona / metaprompt tools ─────────────
|
|
124
106
|
list_persona_templates: {
|
|
@@ -230,7 +212,7 @@ const TOOL_REGISTRY = {
|
|
|
230
212
|
keywords: ['publish', 'manifest', 'dao', 'subdao', 'governance'],
|
|
231
213
|
negativeKeywords: [],
|
|
232
214
|
description: 'Prepare a manifest for on-chain publishing and generate CLI commands.',
|
|
233
|
-
whenToUse: 'Use when you are ready to publish a library/manifest to a
|
|
215
|
+
whenToUse: 'Use when you are ready to publish a library/manifest to a DAO.',
|
|
234
216
|
requiredParams: ['manifest'],
|
|
235
217
|
optionalParams: ['subdao', 'description', 'dry_run'],
|
|
236
218
|
weight: 1.2,
|
|
@@ -239,7 +221,7 @@ const TOOL_REGISTRY = {
|
|
|
239
221
|
category: 'governance',
|
|
240
222
|
keywords: ['proposal', 'governance', 'list', 'dao', 'subdao'],
|
|
241
223
|
negativeKeywords: [],
|
|
242
|
-
description: 'List active proposals for a specific
|
|
224
|
+
description: 'List active governance proposals for a specific DAO.',
|
|
243
225
|
whenToUse: 'Use when you want to inspect or track current governance proposals.',
|
|
244
226
|
requiredParams: [],
|
|
245
227
|
optionalParams: ['state', 'subdao', 'limit'],
|
|
@@ -249,7 +231,7 @@ const TOOL_REGISTRY = {
|
|
|
249
231
|
category: 'governance',
|
|
250
232
|
keywords: ['subdao', 'suggest', 'library', 'publish', 'dao'],
|
|
251
233
|
negativeKeywords: [],
|
|
252
|
-
description: 'Suggest suitable
|
|
234
|
+
description: 'Suggest suitable DAOs for publishing a given library manifest.',
|
|
253
235
|
whenToUse: 'Use when you have a library and want advice on where to publish it.',
|
|
254
236
|
requiredParams: ['library'],
|
|
255
237
|
optionalParams: ['limit'],
|
|
@@ -260,19 +242,19 @@ const TOOL_REGISTRY = {
|
|
|
260
242
|
keywords: ['publish', 'commands', 'cli', 'dao', 'subdao'],
|
|
261
243
|
negativeKeywords: [],
|
|
262
244
|
description: 'Generate CLI commands for validating, uploading, and proposing a manifest.',
|
|
263
|
-
whenToUse: 'Use after selecting a
|
|
245
|
+
whenToUse: 'Use after selecting a DAO to get concrete CLI commands for publishing.',
|
|
264
246
|
requiredParams: ['library'],
|
|
265
247
|
optionalParams: ['target'],
|
|
266
248
|
weight: 1.0,
|
|
267
249
|
},
|
|
268
250
|
|
|
269
|
-
// ───────────── Treasury /
|
|
251
|
+
// ───────────── Treasury / DAO tools ─────────────
|
|
270
252
|
list_subdaos: {
|
|
271
253
|
category: 'treasury',
|
|
272
254
|
keywords: ['subdao', 'dao', 'list', 'treasury', 'governance'],
|
|
273
255
|
negativeKeywords: [],
|
|
274
|
-
description: 'List all available
|
|
275
|
-
whenToUse: 'Use when you want an overview of existing
|
|
256
|
+
description: 'List all available DAOs in the Sage Protocol.',
|
|
257
|
+
whenToUse: 'Use when you want an overview of existing DAOs and treasuries.',
|
|
276
258
|
requiredParams: [],
|
|
277
259
|
optionalParams: ['limit'],
|
|
278
260
|
weight: 0.9,
|
|
@@ -281,14 +263,24 @@ const TOOL_REGISTRY = {
|
|
|
281
263
|
category: 'treasury',
|
|
282
264
|
keywords: ['subdao', 'library', 'libraries', 'list'],
|
|
283
265
|
negativeKeywords: [],
|
|
284
|
-
description: 'List libraries associated with a specific
|
|
285
|
-
whenToUse: 'Use when you want to see what libraries a
|
|
266
|
+
description: 'List libraries associated with a specific DAO.',
|
|
267
|
+
whenToUse: 'Use when you want to see what libraries a DAO currently governs.',
|
|
286
268
|
requiredParams: ['subdao'],
|
|
287
269
|
optionalParams: [],
|
|
288
270
|
weight: 1.0,
|
|
289
271
|
},
|
|
290
272
|
|
|
291
273
|
// ───────────── Discovery / helper tools ─────────────
|
|
274
|
+
get_project_context: {
|
|
275
|
+
category: 'discovery',
|
|
276
|
+
keywords: ['project', 'context', 'workspace', 'config', 'subdao', 'registry'],
|
|
277
|
+
negativeKeywords: [],
|
|
278
|
+
description: 'Get context about the current Sage project (root, DAO, artifact counts, etc).',
|
|
279
|
+
whenToUse: 'Use at session start to understand the project, or before tool selection to check workspace state.',
|
|
280
|
+
requiredParams: [],
|
|
281
|
+
optionalParams: [],
|
|
282
|
+
weight: 1.1,
|
|
283
|
+
},
|
|
292
284
|
trending_prompts: {
|
|
293
285
|
category: 'discovery',
|
|
294
286
|
keywords: ['trending', 'popular', 'recent', 'discover', 'prompt'],
|
|
@@ -44,6 +44,18 @@ function createToolArgsValidator({ zodModule } = {}) {
|
|
|
44
44
|
page: Z.number().int().min(1).max(100).optional().default(1),
|
|
45
45
|
pageSize: Z.number().int().min(1).max(50).optional().default(10),
|
|
46
46
|
}),
|
|
47
|
+
list_prompts: Z.object({
|
|
48
|
+
source: Z.enum(['local', 'workspace', 'onchain', 'all']).optional().default('local'),
|
|
49
|
+
library: Z.string().max(200).optional().default(''),
|
|
50
|
+
limit: Z.number().int().min(1).max(100).optional().default(20),
|
|
51
|
+
kind: Z.enum(['prompt', 'skill', 'snippet']).optional(),
|
|
52
|
+
publishable: Z.boolean().optional(),
|
|
53
|
+
}),
|
|
54
|
+
get_prompt: Z.object({
|
|
55
|
+
key: Z.string().min(1).max(200),
|
|
56
|
+
library: Z.string().max(200).optional().default(''),
|
|
57
|
+
}),
|
|
58
|
+
get_project_context: Z.object({}),
|
|
47
59
|
get_prompt_content: Z.object({ cid: Z.string().min(10).max(200) }),
|
|
48
60
|
trending_prompts: Z.object({
|
|
49
61
|
limit: Z.number().int().min(1).max(50).optional().default(10),
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const config = require('../config');
|
|
4
|
+
const ArtifactManager = require('./artifact-manager');
|
|
5
|
+
|
|
6
|
+
class ProjectContextService {
|
|
7
|
+
constructor(cwd = process.cwd()) {
|
|
8
|
+
this.cwd = cwd;
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.artifactManager = new ArtifactManager(cwd);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getProjectContext() {
|
|
14
|
+
await this.artifactManager.initialize();
|
|
15
|
+
|
|
16
|
+
// Basic Project Info
|
|
17
|
+
const projectRoot = this.cwd;
|
|
18
|
+
const promptsDir = path.join(this.cwd, 'prompts');
|
|
19
|
+
|
|
20
|
+
// Load Sage Config
|
|
21
|
+
const profiles = this.config.readProfiles();
|
|
22
|
+
const activeProfile = profiles.profiles?.[profiles.activeProfile] || {};
|
|
23
|
+
|
|
24
|
+
// Load Workspace Config (.sage/workspace.json)
|
|
25
|
+
let workspaceConfig = {};
|
|
26
|
+
try {
|
|
27
|
+
const workspacePath = path.join(this.cwd, '.sage', 'workspace.json');
|
|
28
|
+
if (fs.existsSync(workspacePath)) {
|
|
29
|
+
workspaceConfig = JSON.parse(fs.readFileSync(workspacePath, 'utf8'));
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// ignore
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Artifact Counts
|
|
36
|
+
const artifacts = await this.artifactManager.listArtifacts({});
|
|
37
|
+
const counts = {
|
|
38
|
+
promptCount: artifacts.filter(a => a.kind === 'prompt').length,
|
|
39
|
+
skillCount: artifacts.filter(a => a.kind === 'skill').length,
|
|
40
|
+
snippetCount: artifacts.filter(a => a.kind === 'snippet').length,
|
|
41
|
+
total: artifacts.length
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Libraries (from workspace config or local discovery)
|
|
45
|
+
// For now, just return what's in workspace config or empty
|
|
46
|
+
const libraries = workspaceConfig.libraries || [];
|
|
47
|
+
|
|
48
|
+
// Get addresses from profile
|
|
49
|
+
const addresses = activeProfile.addresses || {};
|
|
50
|
+
|
|
51
|
+
// Agent surfaces: where other tools expect configuration / rules
|
|
52
|
+
const agentSurfaces = {
|
|
53
|
+
cursorRulesDir: null,
|
|
54
|
+
claudeManifest: null,
|
|
55
|
+
copilotPromptsDir: null,
|
|
56
|
+
agentsFiles: [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const cursorRulesDir = path.join(this.cwd, '.cursor', 'rules');
|
|
60
|
+
if (fs.existsSync(cursorRulesDir)) {
|
|
61
|
+
agentSurfaces.cursorRulesDir = '.cursor/rules';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const claudePath = path.join(this.cwd, 'CLAUDE.md');
|
|
65
|
+
if (fs.existsSync(claudePath)) {
|
|
66
|
+
agentSurfaces.claudeManifest = 'CLAUDE.md';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const copilotDir = path.join(this.cwd, '.github', 'prompts');
|
|
70
|
+
if (fs.existsSync(copilotDir)) {
|
|
71
|
+
agentSurfaces.copilotPromptsDir = '.github/prompts';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const agentsPath = path.join(this.cwd, 'AGENTS.md');
|
|
75
|
+
if (fs.existsSync(agentsPath)) {
|
|
76
|
+
agentSurfaces.agentsFiles.push('AGENTS.md');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
projectRoot,
|
|
81
|
+
promptsDir: 'prompts', // relative
|
|
82
|
+
subdao: addresses.SUBDAO || workspaceConfig.subdao || null,
|
|
83
|
+
registry: addresses.LIBRARY_REGISTRY || addresses.REGISTRY || workspaceConfig.registry || null,
|
|
84
|
+
network: process.env.SAGE_NETWORK || 'base-sepolia',
|
|
85
|
+
rpcUrl: activeProfile.rpcUrl || null,
|
|
86
|
+
workspace: counts,
|
|
87
|
+
libraries,
|
|
88
|
+
agentSurfaces,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Backwards compatibility for any callers still using getContext()
|
|
93
|
+
async getContext() {
|
|
94
|
+
return this.getProjectContext();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = ProjectContextService;
|
|
@@ -106,12 +106,6 @@ const COMMAND_CATALOG = {
|
|
|
106
106
|
ex: 'examples'
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
|
-
prompt: {
|
|
110
|
-
aliases: ['p'],
|
|
111
|
-
needsSubcommand: true,
|
|
112
|
-
subcommands: ['list', 'show', 'pull', 'push', 'publish', 'propose', 'doctor', 'run', 'test', 'preview', 'validate', 'compare', 'fork'],
|
|
113
|
-
subcommandAliases: { ls: 'list' }
|
|
114
|
-
},
|
|
115
109
|
dev: {
|
|
116
110
|
aliases: [],
|
|
117
111
|
needsSubcommand: true,
|
package/package.json
CHANGED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
const { Command } = require('commander');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const cliConfig = require('../config');
|
|
5
|
-
const { getProvider } = require('../utils/cli-session');
|
|
6
|
-
const { SageEchoExecutor } = require('@sage-protocol/sdk');
|
|
7
|
-
|
|
8
|
-
function readJsonFile(filePath) {
|
|
9
|
-
const resolved = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
|
|
10
|
-
const raw = fs.readFileSync(resolved, 'utf8');
|
|
11
|
-
return JSON.parse(raw);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function buildContext(options) {
|
|
15
|
-
if (options.contextFile) {
|
|
16
|
-
return readJsonFile(options.contextFile);
|
|
17
|
-
}
|
|
18
|
-
if (options.context) {
|
|
19
|
-
try {
|
|
20
|
-
return JSON.parse(options.context);
|
|
21
|
-
} catch (error) {
|
|
22
|
-
throw new Error(`Failed to parse --context JSON: ${error.message}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return {};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function resolveRegistry(passed) {
|
|
29
|
-
if (passed) return passed;
|
|
30
|
-
|
|
31
|
-
if (typeof cliConfig.readProfiles === 'function') {
|
|
32
|
-
const profiles = cliConfig.readProfiles();
|
|
33
|
-
const active = profiles.activeProfile || 'default';
|
|
34
|
-
const addresses = profiles.profiles?.[active]?.addresses || {};
|
|
35
|
-
if (addresses.PROMPT_REGISTRY_ADDRESS) return addresses.PROMPT_REGISTRY_ADDRESS;
|
|
36
|
-
if (addresses.LIBRARY_REGISTRY_ADDRESS) return addresses.LIBRARY_REGISTRY_ADDRESS;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return process.env.PROMPT_REGISTRY_ADDRESS || process.env.LIBRARY_REGISTRY_ADDRESS || null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function formatResult(result) {
|
|
43
|
-
if (!result || typeof result !== 'object') return result;
|
|
44
|
-
const { prompt, response, request } = result;
|
|
45
|
-
return {
|
|
46
|
-
prompt,
|
|
47
|
-
request,
|
|
48
|
-
response,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const promptTest = new Command('test')
|
|
53
|
-
.description('Execute a prompt from the registry via Echo')
|
|
54
|
-
.argument('<promptKey>', 'Key of the prompt stored in the registry')
|
|
55
|
-
.option('-r, --registry <address>', 'Prompt registry address (defaults to configured LIBRARY_REGISTRY_ADDRESS)')
|
|
56
|
-
.option('-g, --gateway <url>', 'IPFS gateway to fetch prompt content', process.env.SAGE_IPFS_GATEWAY)
|
|
57
|
-
.option('-m, --model <model>', 'Echo model to use (defaults to gpt-4o-mini or $ECHO_MODEL)', process.env.ECHO_MODEL)
|
|
58
|
-
.option('--user <text>', 'User message to send alongside the prompt template')
|
|
59
|
-
.option('--temperature <float>', 'Sampling temperature (e.g. 0.2)')
|
|
60
|
-
.option('--max-tokens <int>', 'Maximum number of tokens to generate')
|
|
61
|
-
.option('--top-p <float>', 'Top-p nucleus sampling value (0-1)')
|
|
62
|
-
.option('--context <json>', 'Inline JSON payload passed to the prompt execution context')
|
|
63
|
-
.option('--context-file <path>', 'Path to JSON file used as execution context')
|
|
64
|
-
.option('--json', 'Emit raw JSON response only')
|
|
65
|
-
.action(async (promptKey, options) => {
|
|
66
|
-
try {
|
|
67
|
-
if (typeof cliConfig.loadEnv === 'function') {
|
|
68
|
-
try { cliConfig.loadEnv(); } catch (_) {}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Read-only execution: only a provider is required
|
|
72
|
-
const provider = await getProvider({ preferWallet: true });
|
|
73
|
-
const registry = resolveRegistry(options.registry);
|
|
74
|
-
if (!registry) {
|
|
75
|
-
throw new Error('Prompt registry address not configured. Provide --registry or import addresses via `sage config addresses import`.');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const echoApiKey = process.env.ECHO_API_KEY;
|
|
79
|
-
const echoAppId = process.env.ECHO_APP_ID || process.env.NEXT_PUBLIC_ECHO_APP_ID;
|
|
80
|
-
|
|
81
|
-
if (!echoApiKey) {
|
|
82
|
-
throw new Error('ECHO_API_KEY is not set. Create an Echo API key in the Echo dashboard and export it before running this command.');
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const context = buildContext(options);
|
|
86
|
-
|
|
87
|
-
if (process.env.SAGE_DEBUG_PROMPT_TEST === '1') {
|
|
88
|
-
console.log('[debug] context loaded, initializing executor');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const executor = new SageEchoExecutor(provider, null, {
|
|
92
|
-
registry,
|
|
93
|
-
ipfsGateway: options.gateway,
|
|
94
|
-
apiKey: echoApiKey,
|
|
95
|
-
appId: echoAppId,
|
|
96
|
-
model: options.model,
|
|
97
|
-
routerUrl: process.env.ECHO_ROUTER_URL,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const execOptions = {
|
|
101
|
-
registry,
|
|
102
|
-
ipfsGateway: options.gateway,
|
|
103
|
-
model: options.model,
|
|
104
|
-
userMessage: options.user,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
if (typeof options.temperature !== 'undefined') {
|
|
108
|
-
const temp = Number(options.temperature);
|
|
109
|
-
if (Number.isNaN(temp)) {
|
|
110
|
-
throw new Error('Temperature must be a number.');
|
|
111
|
-
}
|
|
112
|
-
execOptions.temperature = temp;
|
|
113
|
-
}
|
|
114
|
-
if (typeof options.maxTokens !== 'undefined') {
|
|
115
|
-
const maxTokens = parseInt(options.maxTokens, 10);
|
|
116
|
-
if (Number.isNaN(maxTokens)) {
|
|
117
|
-
throw new Error('max-tokens must be an integer.');
|
|
118
|
-
}
|
|
119
|
-
execOptions.maxTokens = maxTokens;
|
|
120
|
-
}
|
|
121
|
-
if (typeof options.topP !== 'undefined') {
|
|
122
|
-
const topP = Number(options.topP);
|
|
123
|
-
if (Number.isNaN(topP)) {
|
|
124
|
-
throw new Error('top-p must be a number.');
|
|
125
|
-
}
|
|
126
|
-
execOptions.topP = topP;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const result = await executor.executePrompt(promptKey, context, execOptions);
|
|
130
|
-
|
|
131
|
-
if (process.env.SAGE_DEBUG_PROMPT_TEST === '1') {
|
|
132
|
-
console.log('[debug] execution complete');
|
|
133
|
-
}
|
|
134
|
-
const payload = formatResult(result);
|
|
135
|
-
const { response } = payload;
|
|
136
|
-
const raw = response?.raw || null;
|
|
137
|
-
const firstChoice = Array.isArray(raw?.choices) ? raw.choices[0] : null;
|
|
138
|
-
const completionText =
|
|
139
|
-
response?.text ||
|
|
140
|
-
firstChoice?.message?.content ||
|
|
141
|
-
firstChoice?.text ||
|
|
142
|
-
null;
|
|
143
|
-
const usage = response?.usage || raw?.usage || null;
|
|
144
|
-
const modelUsed = raw?.model || response?.model || execOptions.model || process.env.ECHO_MODEL || 'gpt-4o-mini';
|
|
145
|
-
const responseId = raw?.id || response?.id || null;
|
|
146
|
-
|
|
147
|
-
if (options.json || process.env.SAGE_QUIET_JSON === '1') {
|
|
148
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
149
|
-
} else {
|
|
150
|
-
console.log('✅ Echo prompt execution succeeded');
|
|
151
|
-
console.log(`Model: ${modelUsed}`);
|
|
152
|
-
if (usage) {
|
|
153
|
-
console.log(`Tokens — prompt: ${usage.prompt_tokens ?? 'n/a'}, completion: ${usage.completion_tokens ?? 'n/a'}, total: ${usage.total_tokens ?? 'n/a'}`);
|
|
154
|
-
}
|
|
155
|
-
if (responseId) {
|
|
156
|
-
console.log(`Response ID: ${responseId}`);
|
|
157
|
-
}
|
|
158
|
-
if (completionText) {
|
|
159
|
-
console.log('\n--- Assistant Response ---\n');
|
|
160
|
-
console.log(completionText);
|
|
161
|
-
console.log('\n--------------------------');
|
|
162
|
-
} else {
|
|
163
|
-
console.log('\n(No assistant message returned — inspect JSON output for details)');
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} catch (error) {
|
|
167
|
-
if (options.json || process.env.SAGE_QUIET_JSON === '1') {
|
|
168
|
-
console.error(JSON.stringify({ error: error.message, code: error.code || null }));
|
|
169
|
-
} else {
|
|
170
|
-
console.error(`❌ Failed to execute prompt: ${error.message}`);
|
|
171
|
-
}
|
|
172
|
-
process.exitCode = 1;
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
module.exports = promptTest;
|