@sage-protocol/cli 0.4.1 → 0.4.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.
@@ -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,14 @@ 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
- return `${absoluteIndex}. **${promptName}**\n 📜 ${description}\n${libraryName}${cidLine} 🔖 Tags: ${tags}\n 🌐 Source: ${origin}\n`;
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
+
33
+ return `${absoluteIndex}. **${promptName}**\n 📜 ${description}\n${kind}${publishable}${status}${project}${libraryName}${cidLine} 🔖 Tags: ${tags}\n 🌐 Source: ${origin}\n`;
27
34
  }).join('\n');
28
35
 
29
36
  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 local workspace and/or on-chain libraries.',
27
- whenToUse: 'Use when you want a browseable list of prompts without a specific search query.',
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
- get_workspace_skill: {
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 SubDAO.',
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 SubDAO.',
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 SubDAOs for publishing a given library manifest.',
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 SubDAO to get concrete CLI commands for publishing.',
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 / SubDAO tools ─────────────
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 SubDAOs in the Sage Protocol.',
275
- whenToUse: 'Use when you want an overview of existing SubDAOs/treasuries.',
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 SubDAO.',
285
- whenToUse: 'Use when you want to see what libraries a SubDAO currently governs.',
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-protocol/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Sage Protocol CLI for managing AI prompt libraries",
5
5
  "bin": {
6
6
  "sage": "./bin/sage.js"
@@ -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;