@sage-protocol/cli 0.4.0 → 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.
- package/dist/cli/browser-wallet-integration.js +0 -1
- package/dist/cli/cast-wallet-manager.js +0 -1
- package/dist/cli/commands/interview.js +149 -0
- package/dist/cli/commands/personal.js +138 -79
- package/dist/cli/commands/prompts.js +242 -87
- package/dist/cli/commands/stake-status.js +0 -2
- package/dist/cli/config.js +28 -8
- package/dist/cli/governance-manager.js +28 -19
- package/dist/cli/index.js +32 -8
- package/dist/cli/library-manager.js +16 -6
- package/dist/cli/mcp-server-stdio.js +759 -156
- package/dist/cli/mcp-server.js +4 -30
- package/dist/cli/metamask-integration.js +0 -1
- package/dist/cli/privy-wallet-manager.js +2 -2
- package/dist/cli/prompt-manager.js +0 -1
- package/dist/cli/services/artifact-manager.js +198 -0
- package/dist/cli/services/doctor/fixers.js +1 -1
- package/dist/cli/services/mcp/env-loader.js +2 -0
- package/dist/cli/services/mcp/prompt-result-formatter.js +8 -1
- package/dist/cli/services/mcp/quick-start.js +14 -15
- package/dist/cli/services/mcp/sage-tool-registry.js +322 -0
- package/dist/cli/services/mcp/tool-args-validator.js +43 -0
- package/dist/cli/services/metaprompt/anthropic-client.js +87 -0
- package/dist/cli/services/metaprompt/interview-driver.js +161 -0
- package/dist/cli/services/metaprompt/model-client.js +49 -0
- package/dist/cli/services/metaprompt/openai-client.js +67 -0
- package/dist/cli/services/metaprompt/persistence.js +86 -0
- package/dist/cli/services/metaprompt/prompt-builder.js +186 -0
- package/dist/cli/services/metaprompt/session.js +18 -80
- package/dist/cli/services/metaprompt/slot-planner.js +115 -0
- package/dist/cli/services/metaprompt/templates.json +130 -0
- package/dist/cli/services/project-context.js +98 -0
- package/dist/cli/subdao.js +0 -3
- package/dist/cli/sxxx-manager.js +0 -1
- package/dist/cli/utils/aliases.js +0 -6
- package/dist/cli/utils/tx-wait.js +0 -3
- package/dist/cli/wallet-manager.js +18 -19
- package/dist/cli/walletconnect-integration.js +0 -1
- package/dist/cli/wizard-manager.js +0 -1
- package/package.json +3 -1
- package/dist/cli/commands/prompt-test.js +0 -176
- package/dist/cli/commands/prompt.js +0 -2531
|
@@ -1,2531 +0,0 @@
|
|
|
1
|
-
const { Command } = require("commander");
|
|
2
|
-
const PromptManager = require("../prompt-manager");
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
|
-
const IPFSManager = require("../ipfs-manager");
|
|
6
|
-
const { spawn, spawnSync } = require("child_process");
|
|
7
|
-
const LibraryManager = require("../library-manager");
|
|
8
|
-
const { PromptCommerceClient } = require("../services/commerce/prompt-client");
|
|
9
|
-
const { DEFAULT_WORKER_BASE } = require("../services/ipfs/onboarding");
|
|
10
|
-
const { WorkerClient } = require("../services/ipfs/worker-client");
|
|
11
|
-
const { SandboxDiscoveryClient } = require("../services/ipfs/discovery-client");
|
|
12
|
-
const sandboxCache = require("../services/ipfs/sandbox-cache");
|
|
13
|
-
const { copyToClipboard } = require("../utils/clipboard");
|
|
14
|
-
const metapromptDesigner = require("../utils/metaprompt-designer");
|
|
15
|
-
const manifestSelector = require("../utils/manifest-selector");
|
|
16
|
-
const cliConfig = require("../config");
|
|
17
|
-
const os = require("os");
|
|
18
|
-
const { resolveArtifact } = require("../utils/artifacts");
|
|
19
|
-
const promptTestCommand = require("./prompt-test");
|
|
20
|
-
|
|
21
|
-
const libraryManager = new LibraryManager();
|
|
22
|
-
|
|
23
|
-
function slugifyKey(value) {
|
|
24
|
-
return (
|
|
25
|
-
String(value || "")
|
|
26
|
-
.toLowerCase()
|
|
27
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
28
|
-
.replace(/^-+|-+$/g, "")
|
|
29
|
-
.slice(0, 120) || `prompt-${Date.now()}`
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function parseTags(input) {
|
|
34
|
-
if (!input) return [];
|
|
35
|
-
if (Array.isArray(input))
|
|
36
|
-
return input.map((t) => String(t).trim()).filter(Boolean);
|
|
37
|
-
return String(input)
|
|
38
|
-
.split(/[,\s]+/)
|
|
39
|
-
.map((t) => t.trim())
|
|
40
|
-
.filter(Boolean);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function resolveWorkerSettings(opts = {}) {
|
|
44
|
-
const ipfsConfig =
|
|
45
|
-
typeof cliConfig.readIpfsConfig === "function"
|
|
46
|
-
? cliConfig.readIpfsConfig() || {}
|
|
47
|
-
: {};
|
|
48
|
-
const workerProfile = ipfsConfig.worker || {};
|
|
49
|
-
const baseUrl =
|
|
50
|
-
(opts.workerUrl ||
|
|
51
|
-
process.env.SAGE_IPFS_WORKER_URL ||
|
|
52
|
-
process.env.SAGE_IPFS_UPLOAD_URL ||
|
|
53
|
-
workerProfile.baseUrl ||
|
|
54
|
-
DEFAULT_WORKER_BASE || "")
|
|
55
|
-
.trim()
|
|
56
|
-
.replace(/\/$/, "");
|
|
57
|
-
if (!baseUrl) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
"Worker base URL not configured. Run `sage ipfs setup` or set SAGE_IPFS_WORKER_URL.",
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
return {
|
|
63
|
-
baseUrl,
|
|
64
|
-
token:
|
|
65
|
-
opts.workerToken ||
|
|
66
|
-
process.env.SAGE_IPFS_UPLOAD_TOKEN ||
|
|
67
|
-
workerProfile.token ||
|
|
68
|
-
"",
|
|
69
|
-
address:
|
|
70
|
-
opts.address ||
|
|
71
|
-
process.env.SAGE_SANDBOX_ADDRESS ||
|
|
72
|
-
workerProfile.address ||
|
|
73
|
-
"",
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function getDefaultEditor() {
|
|
78
|
-
if (process.env.EDITOR && process.env.EDITOR.trim())
|
|
79
|
-
return process.env.EDITOR.trim();
|
|
80
|
-
if (process.env.VISUAL && process.env.VISUAL.trim())
|
|
81
|
-
return process.env.VISUAL.trim();
|
|
82
|
-
return process.platform === "win32" ? "notepad" : "nano";
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function openEditorWithContent(initialContent = "", extension = ".md") {
|
|
86
|
-
const tmpFile = path.join(
|
|
87
|
-
os.tmpdir(),
|
|
88
|
-
`sage-prompt-${Date.now()}${extension}`,
|
|
89
|
-
);
|
|
90
|
-
fs.writeFileSync(tmpFile, initialContent, "utf8");
|
|
91
|
-
const editor = getDefaultEditor();
|
|
92
|
-
const result = spawnSync(editor, [tmpFile], {
|
|
93
|
-
stdio: "inherit",
|
|
94
|
-
shell: process.platform === "win32",
|
|
95
|
-
});
|
|
96
|
-
if (result.error) {
|
|
97
|
-
fs.rmSync(tmpFile, { force: true });
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Failed to launch editor '${editor}': ${result.error.message}`,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
const edited = fs.readFileSync(tmpFile, "utf8");
|
|
103
|
-
fs.rmSync(tmpFile, { force: true });
|
|
104
|
-
return edited;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function ensureManifestWritable(entry) {
|
|
108
|
-
if (entry.path && fs.existsSync(entry.path)) {
|
|
109
|
-
return { manifest: entry.manifest, path: entry.path };
|
|
110
|
-
}
|
|
111
|
-
const dir = libraryManager.ensureLibrariesDir();
|
|
112
|
-
const filename = entry.cid
|
|
113
|
-
? `${entry.cid}.json`
|
|
114
|
-
: `manifest-${Date.now()}.json`;
|
|
115
|
-
const file = path.join(dir, filename);
|
|
116
|
-
fs.writeFileSync(file, JSON.stringify(entry.manifest, null, 2));
|
|
117
|
-
entry.path = file;
|
|
118
|
-
entry.id = entry.id || `path:${file}`;
|
|
119
|
-
return { manifest: entry.manifest, path: file };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function writeManifest(manifest, filePath) {
|
|
123
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
124
|
-
fs.writeFileSync(filePath, JSON.stringify(manifest, null, 2));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function ensurePromptFile(manifestPath, key, content) {
|
|
128
|
-
const baseDir = path.dirname(manifestPath);
|
|
129
|
-
const promptsDir = path.join(baseDir, "prompts");
|
|
130
|
-
fs.mkdirSync(promptsDir, { recursive: true });
|
|
131
|
-
const filename = `${key}.md`;
|
|
132
|
-
const outPath = path.join(promptsDir, filename);
|
|
133
|
-
fs.writeFileSync(outPath, content, "utf8");
|
|
134
|
-
return { absolute: outPath, relative: path.relative(baseDir, outPath) };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function readStdin() {
|
|
138
|
-
return new Promise((resolve, reject) => {
|
|
139
|
-
let data = "";
|
|
140
|
-
process.stdin.setEncoding("utf8");
|
|
141
|
-
process.stdin.on("data", (chunk) => {
|
|
142
|
-
data += chunk;
|
|
143
|
-
});
|
|
144
|
-
process.stdin.on("end", () => resolve(data));
|
|
145
|
-
process.stdin.on("error", (err) => reject(err));
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function archivePrompt(manifestPath, prompt) {
|
|
150
|
-
const projectDir = cliConfig.getProjectDir
|
|
151
|
-
? cliConfig.getProjectDir()
|
|
152
|
-
: process.cwd();
|
|
153
|
-
const archiveDir = path.join(projectDir, ".sage", "archives", "prompts");
|
|
154
|
-
fs.mkdirSync(archiveDir, { recursive: true });
|
|
155
|
-
const safeKey = slugifyKey(prompt?.key || "prompt");
|
|
156
|
-
const filename = `${safeKey}-${Date.now()}.json`;
|
|
157
|
-
const out = path.join(archiveDir, filename);
|
|
158
|
-
const payload = {
|
|
159
|
-
removedAt: new Date().toISOString(),
|
|
160
|
-
manifestPath,
|
|
161
|
-
prompt,
|
|
162
|
-
};
|
|
163
|
-
fs.writeFileSync(out, JSON.stringify(payload, null, 2));
|
|
164
|
-
return out;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function resolveManifests({
|
|
168
|
-
manifest: manifestInput,
|
|
169
|
-
manifestCid,
|
|
170
|
-
subdao,
|
|
171
|
-
}) {
|
|
172
|
-
const candidates = await manifestSelector.gatherManifestCandidates({
|
|
173
|
-
manifest: manifestInput,
|
|
174
|
-
manifestCid,
|
|
175
|
-
subdao,
|
|
176
|
-
});
|
|
177
|
-
return candidates
|
|
178
|
-
.filter((entry) => entry && entry.manifest)
|
|
179
|
-
.map((entry) => ({
|
|
180
|
-
manifest: entry.manifest,
|
|
181
|
-
source: entry.source,
|
|
182
|
-
path: entry.path || null,
|
|
183
|
-
cid: entry.cid || null,
|
|
184
|
-
}));
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function findPromptInManifest(manifest, key) {
|
|
188
|
-
const prompts = Array.isArray(manifest?.prompts) ? manifest.prompts : [];
|
|
189
|
-
return prompts.find((p) => p.key === key || p.key === `${key}`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function locatePrompt(key, options) {
|
|
193
|
-
const targetKey = options.prompt || key;
|
|
194
|
-
if (!targetKey) throw new Error("Prompt key required.");
|
|
195
|
-
|
|
196
|
-
if (options.promptCid) {
|
|
197
|
-
const ipfs = new IPFSManager();
|
|
198
|
-
await ipfs.initialize();
|
|
199
|
-
const promptJson = await ipfs
|
|
200
|
-
.downloadJson(options.promptCid)
|
|
201
|
-
.catch(() => null);
|
|
202
|
-
if (promptJson && promptJson.content) {
|
|
203
|
-
return {
|
|
204
|
-
prompt: {
|
|
205
|
-
key: targetKey,
|
|
206
|
-
name: promptJson.name || targetKey,
|
|
207
|
-
description: promptJson.description || "",
|
|
208
|
-
tags: promptJson.tags || [],
|
|
209
|
-
cid: options.promptCid,
|
|
210
|
-
content: promptJson.content,
|
|
211
|
-
},
|
|
212
|
-
manifestMeta: { source: "ipfs", cid: options.promptCid },
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
// Fallback to raw text download
|
|
216
|
-
const tmp = path.join(os.tmpdir(), `sage-prompt-${Date.now()}.txt`);
|
|
217
|
-
const filePath = await ipfs.downloadPrompt(options.promptCid, tmp);
|
|
218
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
219
|
-
return {
|
|
220
|
-
prompt: {
|
|
221
|
-
key: targetKey,
|
|
222
|
-
name: targetKey,
|
|
223
|
-
description: "",
|
|
224
|
-
tags: [],
|
|
225
|
-
cid: options.promptCid,
|
|
226
|
-
content,
|
|
227
|
-
},
|
|
228
|
-
manifestMeta: { source: "ipfs", cid: options.promptCid },
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const manifests = await resolveManifests({
|
|
233
|
-
manifest: options.manifest,
|
|
234
|
-
manifestCid: options.manifestCid,
|
|
235
|
-
subdao: options.subdao,
|
|
236
|
-
});
|
|
237
|
-
for (const entry of manifests) {
|
|
238
|
-
const prompt = findPromptInManifest(entry.manifest, targetKey);
|
|
239
|
-
if (prompt) {
|
|
240
|
-
return { prompt, manifestMeta: entry };
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
throw new Error(`Prompt '${targetKey}' not found in manifest sources.`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function ensureDirectoryForFile(filePath) {
|
|
248
|
-
try {
|
|
249
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
250
|
-
} catch (_) {}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function appendToFile(filePath, content) {
|
|
254
|
-
ensureDirectoryForFile(filePath);
|
|
255
|
-
fs.appendFileSync(filePath, `${content}\n`, "utf8");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function openExternal(url) {
|
|
259
|
-
const platform = process.platform;
|
|
260
|
-
let command;
|
|
261
|
-
let args;
|
|
262
|
-
if (platform === "win32") {
|
|
263
|
-
command = "cmd";
|
|
264
|
-
args = ["/c", "start", '""', url];
|
|
265
|
-
} else if (platform === "darwin") {
|
|
266
|
-
command = "open";
|
|
267
|
-
args = [url];
|
|
268
|
-
} else {
|
|
269
|
-
command = "xdg-open";
|
|
270
|
-
args = [url];
|
|
271
|
-
}
|
|
272
|
-
try {
|
|
273
|
-
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
274
|
-
child.unref();
|
|
275
|
-
return { ok: true, command };
|
|
276
|
-
} catch (error) {
|
|
277
|
-
return { ok: false, error: error.message };
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function buildTemplate(prompt, manifestMeta) {
|
|
282
|
-
return {
|
|
283
|
-
key: prompt.key,
|
|
284
|
-
name: prompt.name,
|
|
285
|
-
description: prompt.description,
|
|
286
|
-
tags: prompt.tags || [],
|
|
287
|
-
cid: prompt.cid || null,
|
|
288
|
-
files: prompt.files || [],
|
|
289
|
-
manifest: {
|
|
290
|
-
cid: manifestMeta?.cid || null,
|
|
291
|
-
source: manifestMeta?.source || null,
|
|
292
|
-
path: manifestMeta?.path || null,
|
|
293
|
-
},
|
|
294
|
-
content: prompt.content || null,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async function resolvePromptContent(prompt, manifestMeta) {
|
|
299
|
-
if (prompt.content && String(prompt.content).trim()) {
|
|
300
|
-
return String(prompt.content);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (Array.isArray(prompt.files)) {
|
|
304
|
-
for (const file of prompt.files) {
|
|
305
|
-
const candidate = path.isAbsolute(file)
|
|
306
|
-
? file
|
|
307
|
-
: manifestMeta?.path
|
|
308
|
-
? path.join(path.dirname(manifestMeta.path), file)
|
|
309
|
-
: path.join(process.cwd(), file);
|
|
310
|
-
try {
|
|
311
|
-
if (fs.existsSync(candidate)) {
|
|
312
|
-
return fs.readFileSync(candidate, "utf8");
|
|
313
|
-
}
|
|
314
|
-
} catch (_) {
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (prompt.cid) {
|
|
321
|
-
const ipfs = new IPFSManager();
|
|
322
|
-
await ipfs.initialize();
|
|
323
|
-
const json = await ipfs.downloadJson(prompt.cid).catch(() => null);
|
|
324
|
-
if (json && json.content) {
|
|
325
|
-
return String(json.content);
|
|
326
|
-
}
|
|
327
|
-
try {
|
|
328
|
-
const tmp = path.join(os.tmpdir(), `sage-prompt-${Date.now()}.txt`);
|
|
329
|
-
const filePath = await ipfs.downloadPrompt(prompt.cid, tmp);
|
|
330
|
-
return fs.readFileSync(filePath, "utf8");
|
|
331
|
-
} catch (_) {}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return "";
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function register(program) {
|
|
338
|
-
const promptCommand = new Command("prompt")
|
|
339
|
-
.description("Prompt management commands")
|
|
340
|
-
.addCommand(
|
|
341
|
-
new Command("create")
|
|
342
|
-
.description(
|
|
343
|
-
"Generate a new prompt via OpenAI and add it to a manifest",
|
|
344
|
-
)
|
|
345
|
-
.option("--from-template", "Skip AI and start from a template", false)
|
|
346
|
-
.option("--manifest <path>", "Path to manifest.json to update")
|
|
347
|
-
.option("--manifest-cid <cid>", "Manifest CID to fetch before updating")
|
|
348
|
-
.option("--subdao <address>", "Filter selection to a SubDAO")
|
|
349
|
-
.option("--name <name>", "Prompt display name")
|
|
350
|
-
.option("--key <key>", "Prompt key override")
|
|
351
|
-
.option("--description <text>", "Prompt description")
|
|
352
|
-
.option("--tags <tags>", "Comma or space separated tags")
|
|
353
|
-
.option("--no-tag-prompt", "Do not show interactive tag suggestions", false)
|
|
354
|
-
.option("--topic <text>", "High-level goal for the prompt")
|
|
355
|
-
.option("--model <id>", "OpenAI model (default: gpt-4o-mini)")
|
|
356
|
-
.option("--skip-pin", "Do not upload the prompt to IPFS", false)
|
|
357
|
-
.action(async (opts) => {
|
|
358
|
-
try {
|
|
359
|
-
const inquirer = require("inquirer");
|
|
360
|
-
const axios = require("axios");
|
|
361
|
-
const ai = cliConfig.readAIConfig();
|
|
362
|
-
const apiKey =
|
|
363
|
-
process.env.OPENAI_API_KEY ||
|
|
364
|
-
process.env.SAGE_OPENAI_API_KEY ||
|
|
365
|
-
ai.openaiApiKey;
|
|
366
|
-
const useTemplate = !!opts.fromTemplate || !apiKey;
|
|
367
|
-
if (!apiKey && !opts.fromTemplate) {
|
|
368
|
-
console.log("ℹ️ No OpenAI API key found. Falling back to template flow. Use --from-template to skip AI explicitly.");
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const manifestEntry =
|
|
372
|
-
await manifestSelector.selectManifestInteractive({
|
|
373
|
-
manifest: opts.manifest,
|
|
374
|
-
manifestCid: opts.manifestCid,
|
|
375
|
-
subdao: opts.subdao,
|
|
376
|
-
message: "Select a manifest to update",
|
|
377
|
-
});
|
|
378
|
-
const { manifest, path: manifestPath } =
|
|
379
|
-
ensureManifestWritable(manifestEntry);
|
|
380
|
-
manifest.prompts = Array.isArray(manifest.prompts)
|
|
381
|
-
? manifest.prompts
|
|
382
|
-
: [];
|
|
383
|
-
|
|
384
|
-
const questions = [];
|
|
385
|
-
if (!opts.topic) {
|
|
386
|
-
questions.push({
|
|
387
|
-
name: "topic",
|
|
388
|
-
message: "What should this prompt help accomplish?",
|
|
389
|
-
type: "input",
|
|
390
|
-
validate: (v) => (v.trim() ? true : "Topic is required."),
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
if (!opts.name) {
|
|
394
|
-
questions.push({
|
|
395
|
-
name: "name",
|
|
396
|
-
message: "Prompt name",
|
|
397
|
-
type: "input",
|
|
398
|
-
default: "New Prompt",
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
if (!opts.description) {
|
|
402
|
-
questions.push({
|
|
403
|
-
name: "description",
|
|
404
|
-
message: "Short description (optional)",
|
|
405
|
-
type: "input",
|
|
406
|
-
default: "",
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
if (!opts.tags) {
|
|
410
|
-
questions.push({
|
|
411
|
-
name: "tags",
|
|
412
|
-
message: "Tags (comma or space separated, optional)",
|
|
413
|
-
type: "input",
|
|
414
|
-
default: "",
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
const answers = questions.length
|
|
418
|
-
? await inquirer.prompt(questions)
|
|
419
|
-
: {};
|
|
420
|
-
|
|
421
|
-
const topic = opts.topic || answers.topic;
|
|
422
|
-
const name = opts.name || answers.name || "New Prompt";
|
|
423
|
-
const description = opts.description || answers.description || "";
|
|
424
|
-
let tags = parseTags(opts.tags || answers.tags || []);
|
|
425
|
-
const key = slugifyKey(opts.key || name);
|
|
426
|
-
|
|
427
|
-
const existingIndex = manifest.prompts.findIndex(
|
|
428
|
-
(p) => p.key === key,
|
|
429
|
-
);
|
|
430
|
-
if (existingIndex !== -1) {
|
|
431
|
-
const { confirmOverwrite } = await inquirer.prompt([
|
|
432
|
-
{
|
|
433
|
-
type: "confirm",
|
|
434
|
-
name: "confirmOverwrite",
|
|
435
|
-
message: `Prompt key '${key}' exists. Overwrite?`,
|
|
436
|
-
default: false,
|
|
437
|
-
},
|
|
438
|
-
]);
|
|
439
|
-
if (!confirmOverwrite) {
|
|
440
|
-
console.log("❌ Aborted by user (prompt key already exists).");
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
let contentCandidate = '';
|
|
446
|
-
if (useTemplate) {
|
|
447
|
-
const template = [
|
|
448
|
-
"You are a helpful, precise assistant.",
|
|
449
|
-
"- Follow user instructions carefully.",
|
|
450
|
-
"- Ask for clarification when needed.",
|
|
451
|
-
"- Avoid revealing chain-of-thought; summarize when explaining.",
|
|
452
|
-
"",
|
|
453
|
-
"# Variables",
|
|
454
|
-
"- {{goal}}: High-level objective",
|
|
455
|
-
"- {{constraints}}: Any constraints",
|
|
456
|
-
"",
|
|
457
|
-
"# Instructions",
|
|
458
|
-
"1. Clarify the task",
|
|
459
|
-
"2. Plan briefly",
|
|
460
|
-
"3. Provide the result",
|
|
461
|
-
].join("\n");
|
|
462
|
-
contentCandidate = template;
|
|
463
|
-
} else {
|
|
464
|
-
const systemPrompt =
|
|
465
|
-
"You are an elite prompt engineer. Generate a production-ready system prompt in Markdown with clear structure, guardrails, variables, and usage guidance. Focus on clarity and practical alignment. Output only the prompt content.";
|
|
466
|
-
const userPrompt = [
|
|
467
|
-
`Goal: ${topic}`,
|
|
468
|
-
description ? `Context: ${description}` : null,
|
|
469
|
-
tags.length ? `Keywords: ${tags.join(", ")}` : null,
|
|
470
|
-
"Deliver a single Markdown prompt ready for use.",
|
|
471
|
-
]
|
|
472
|
-
.filter(Boolean)
|
|
473
|
-
.join("\n");
|
|
474
|
-
|
|
475
|
-
const model = opts.model || "gpt-4o-mini";
|
|
476
|
-
const response = await axios.post(
|
|
477
|
-
"https://api.openai.com/v1/chat/completions",
|
|
478
|
-
{
|
|
479
|
-
model,
|
|
480
|
-
messages: [
|
|
481
|
-
{ role: "system", content: systemPrompt },
|
|
482
|
-
{ role: "user", content: userPrompt },
|
|
483
|
-
],
|
|
484
|
-
temperature: 0.7,
|
|
485
|
-
},
|
|
486
|
-
{
|
|
487
|
-
headers: {
|
|
488
|
-
Authorization: `Bearer ${apiKey}`,
|
|
489
|
-
"Content-Type": "application/json",
|
|
490
|
-
},
|
|
491
|
-
},
|
|
492
|
-
);
|
|
493
|
-
contentCandidate = response.data?.choices?.[0]?.message?.content || "";
|
|
494
|
-
if (!contentCandidate.trim()) {
|
|
495
|
-
throw new Error("OpenAI response was empty.");
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
console.log("\n📝 Review and edit the prompt. Save and exit the editor to continue.");
|
|
500
|
-
const editedContent = openEditorWithContent(
|
|
501
|
-
String(contentCandidate || '').trim() + "\n",
|
|
502
|
-
);
|
|
503
|
-
const finalContent = editedContent.trim();
|
|
504
|
-
if (!finalContent) {
|
|
505
|
-
console.log(
|
|
506
|
-
"⚠️ Prompt content is empty after editing. Aborting.",
|
|
507
|
-
);
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Suggest tags (TTY only) if none were provided
|
|
512
|
-
if (!opts.tags && !opts.noTagPrompt) {
|
|
513
|
-
const { isTTY } = require('../utils/output-mode');
|
|
514
|
-
if (isTTY()) {
|
|
515
|
-
const { suggestTags } = require('../utils/tags');
|
|
516
|
-
const suggestions = suggestTags({ filename: `${key}.md`, content: finalContent });
|
|
517
|
-
if (suggestions.length) {
|
|
518
|
-
const inquirer = require('inquirer');
|
|
519
|
-
const { picked, custom } = await inquirer.prompt([
|
|
520
|
-
{ type: 'checkbox', name: 'picked', message: 'Select tags (optional)', choices: suggestions.map((t)=>({ name: t, value: t })) },
|
|
521
|
-
{ type: 'input', name: 'custom', message: 'Add custom tag (optional)', default: '' }
|
|
522
|
-
]);
|
|
523
|
-
const extra = (custom || '').split(/[,\s]+/).filter(Boolean);
|
|
524
|
-
tags = Array.from(new Set([...(tags||[]), ...picked, ...extra])).slice(0, 10);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const { absolute, relative } = ensurePromptFile(
|
|
530
|
-
manifestPath,
|
|
531
|
-
key,
|
|
532
|
-
finalContent + "\n",
|
|
533
|
-
);
|
|
534
|
-
let cid = null;
|
|
535
|
-
// Default: when using template fallback, do not pin unless explicitly requested
|
|
536
|
-
const shouldPin = !opts.skipPin && !useTemplate;
|
|
537
|
-
if (shouldPin) {
|
|
538
|
-
const ipfs = new IPFSManager();
|
|
539
|
-
await ipfs.initialize();
|
|
540
|
-
cid = await ipfs.uploadPrompt(absolute, name, description);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const promptEntry = {
|
|
544
|
-
key,
|
|
545
|
-
name,
|
|
546
|
-
description: description || undefined,
|
|
547
|
-
tags: tags.length ? tags : undefined,
|
|
548
|
-
files: [relative],
|
|
549
|
-
};
|
|
550
|
-
if (cid) promptEntry.cid = cid;
|
|
551
|
-
|
|
552
|
-
if (existingIndex !== -1) {
|
|
553
|
-
manifest.prompts[existingIndex] = Object.assign(
|
|
554
|
-
{},
|
|
555
|
-
manifest.prompts[existingIndex],
|
|
556
|
-
promptEntry,
|
|
557
|
-
);
|
|
558
|
-
} else {
|
|
559
|
-
manifest.prompts.push(promptEntry);
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
writeManifest(manifest, manifestPath);
|
|
563
|
-
console.log(`✅ Prompt '${key}' saved to ${manifestPath}`);
|
|
564
|
-
if (cid) console.log(`🔗 CID: ${cid}`);
|
|
565
|
-
console.log(`📁 Local file: ${absolute}`);
|
|
566
|
-
} catch (error) {
|
|
567
|
-
console.error("❌ prompt create failed:", error.message);
|
|
568
|
-
process.exit(1);
|
|
569
|
-
}
|
|
570
|
-
}),
|
|
571
|
-
)
|
|
572
|
-
.addCommand(
|
|
573
|
-
(() => {
|
|
574
|
-
const sandbox = new Command("sandbox")
|
|
575
|
-
.description(
|
|
576
|
-
"Ephemeral sandbox helpers (Sage infrastructure is used by default)",
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
sandbox
|
|
580
|
-
.command("push")
|
|
581
|
-
.description(
|
|
582
|
-
"Push a prompt reference to the sandbox (defaults to Sage managed worker)",
|
|
583
|
-
)
|
|
584
|
-
.requiredOption(
|
|
585
|
-
"--content-ref <ref>",
|
|
586
|
-
"Reference to prompt content (CID or storage path)",
|
|
587
|
-
)
|
|
588
|
-
.option(
|
|
589
|
-
"--visibility <tier>",
|
|
590
|
-
"Visibility tier: public, share, or private",
|
|
591
|
-
"public",
|
|
592
|
-
)
|
|
593
|
-
.option(
|
|
594
|
-
"--expires-in <hours>",
|
|
595
|
-
"Override expiry window in hours",
|
|
596
|
-
(value) => Number(value),
|
|
597
|
-
)
|
|
598
|
-
.option("--title <text>", "Optional title metadata")
|
|
599
|
-
.option("--tags <tags>", "Comma or space separated tags")
|
|
600
|
-
.option("--worker-url <url>", "Custom worker URL (optional)")
|
|
601
|
-
.option("--worker-token <token>", "Worker bearer token (optional)")
|
|
602
|
-
.option("--address <address>", "Wallet address for sandbox accounting")
|
|
603
|
-
.option("--json", "Output JSON", false)
|
|
604
|
-
.action(async (opts) => {
|
|
605
|
-
try {
|
|
606
|
-
const settings = resolveWorkerSettings(opts);
|
|
607
|
-
const client = new WorkerClient(settings);
|
|
608
|
-
const visibility = String(opts.visibility || "public").toLowerCase();
|
|
609
|
-
const payload = {
|
|
610
|
-
contentRef: opts.contentRef,
|
|
611
|
-
visibility,
|
|
612
|
-
title: opts.title,
|
|
613
|
-
tags: parseTags(opts.tags),
|
|
614
|
-
expiresInHours: Number.isFinite(opts.expiresIn)
|
|
615
|
-
? Number(opts.expiresIn)
|
|
616
|
-
: undefined,
|
|
617
|
-
};
|
|
618
|
-
const result = await client.sandboxPush(payload);
|
|
619
|
-
try {
|
|
620
|
-
sandboxCache.recordPrompt({
|
|
621
|
-
prompt: result.prompt,
|
|
622
|
-
contentRef: opts.contentRef,
|
|
623
|
-
manifest: opts.contentRef,
|
|
624
|
-
});
|
|
625
|
-
} catch (cacheError) {
|
|
626
|
-
if (process.env.SAGE_DEBUG_DISCOVERY === "1") {
|
|
627
|
-
console.warn(
|
|
628
|
-
"⚠️ sandbox cache write failed:",
|
|
629
|
-
cacheError.message,
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
if (opts.json || process.env.SAGE_QUIET_JSON) {
|
|
634
|
-
console.log(JSON.stringify(result, null, 2));
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
console.log(
|
|
638
|
-
`✅ Sandbox prompt stored (${result.prompt.visibility}) via ${settings.baseUrl}`,
|
|
639
|
-
);
|
|
640
|
-
console.log(` id: ${result.prompt.id}`);
|
|
641
|
-
console.log(` expires: ${result.prompt.expiresAt}`);
|
|
642
|
-
if (result.prompt.shareToken) {
|
|
643
|
-
console.log(` share token: ${result.prompt.shareToken}`);
|
|
644
|
-
}
|
|
645
|
-
if (result.quota) {
|
|
646
|
-
console.log(
|
|
647
|
-
` quota: ${result.quota.activeCount} active, ${result.quota.dailyRemaining} remaining today`,
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
} catch (error) {
|
|
651
|
-
console.error(`❌ sandbox push failed: ${error.message}`);
|
|
652
|
-
process.exit(1);
|
|
653
|
-
}
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
sandbox
|
|
657
|
-
.command("list")
|
|
658
|
-
.description("List sandbox prompts for your configured address")
|
|
659
|
-
.option("--visibility <tier>", "Filter by visibility tier")
|
|
660
|
-
.option("--worker-url <url>")
|
|
661
|
-
.option("--worker-token <token>")
|
|
662
|
-
.option("--address <address>")
|
|
663
|
-
.option("--json", "Output JSON", false)
|
|
664
|
-
.action(async (opts) => {
|
|
665
|
-
try {
|
|
666
|
-
const settings = resolveWorkerSettings(opts);
|
|
667
|
-
const client = new WorkerClient(settings);
|
|
668
|
-
const body = await client.sandboxList({ visibility: opts.visibility });
|
|
669
|
-
const prompts = Array.isArray(body.prompts) ? body.prompts : [];
|
|
670
|
-
if (opts.json || process.env.SAGE_QUIET_JSON) {
|
|
671
|
-
console.log(JSON.stringify(prompts, null, 2));
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
if (!prompts.length) {
|
|
675
|
-
console.log("ℹ️ No sandbox prompts found for this address.");
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
console.log(
|
|
679
|
-
`🔎 Sandbox prompts (${prompts.length}) using ${settings.baseUrl}`,
|
|
680
|
-
);
|
|
681
|
-
for (const prompt of prompts) {
|
|
682
|
-
console.log(
|
|
683
|
-
`— ${prompt.id} [${prompt.visibility}] expires ${prompt.expiresAt}`,
|
|
684
|
-
);
|
|
685
|
-
if (prompt.shareToken) console.log(` share token: ${prompt.shareToken}`);
|
|
686
|
-
if (prompt.title) console.log(` title: ${prompt.title}`);
|
|
687
|
-
if (Array.isArray(prompt.tags) && prompt.tags.length)
|
|
688
|
-
console.log(` tags: ${prompt.tags.join(", ")}`);
|
|
689
|
-
}
|
|
690
|
-
} catch (error) {
|
|
691
|
-
console.error(`❌ sandbox list failed: ${error.message}`);
|
|
692
|
-
process.exit(1);
|
|
693
|
-
}
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
sandbox
|
|
697
|
-
.command("share <id>")
|
|
698
|
-
.description("Rotate a share token for a sandbox prompt (requires ENS)")
|
|
699
|
-
.option("--worker-url <url>")
|
|
700
|
-
.option("--worker-token <token>")
|
|
701
|
-
.option("--address <address>")
|
|
702
|
-
.option("--json", "Output JSON", false)
|
|
703
|
-
.action(async (id, opts) => {
|
|
704
|
-
try {
|
|
705
|
-
const settings = resolveWorkerSettings(opts);
|
|
706
|
-
const client = new WorkerClient(settings);
|
|
707
|
-
const body = await client.sandboxShare(id, {
|
|
708
|
-
address: settings.address,
|
|
709
|
-
});
|
|
710
|
-
if (body?.prompt?.id) {
|
|
711
|
-
try {
|
|
712
|
-
const discovery = new SandboxDiscoveryClient(settings);
|
|
713
|
-
await discovery.recordEvent({
|
|
714
|
-
promptId: body.prompt.id,
|
|
715
|
-
type: 'save',
|
|
716
|
-
source: 'cli_share',
|
|
717
|
-
});
|
|
718
|
-
sandboxCache.recordPrompt({
|
|
719
|
-
prompt: body.prompt,
|
|
720
|
-
contentRef: body.prompt.contentRef || undefined,
|
|
721
|
-
});
|
|
722
|
-
} catch (error) {
|
|
723
|
-
if (process.env.SAGE_DEBUG_DISCOVERY === '1') {
|
|
724
|
-
console.warn('⚠️ failed to record discovery save event:', error.message);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
if (opts.json || process.env.SAGE_QUIET_JSON) {
|
|
729
|
-
console.log(JSON.stringify(body, null, 2));
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
console.log(`🔗 Share token: ${body.prompt?.shareToken || "(none)"}`);
|
|
733
|
-
} catch (error) {
|
|
734
|
-
console.error(`❌ sandbox share failed: ${error.message}`);
|
|
735
|
-
process.exit(1);
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
sandbox
|
|
740
|
-
.command("status")
|
|
741
|
-
.description("Show sandbox eligibility (ENS + governance participation)")
|
|
742
|
-
.option("--worker-url <url>")
|
|
743
|
-
.option("--worker-token <token>")
|
|
744
|
-
.option("--address <address>")
|
|
745
|
-
.option("--json", "Output JSON", false)
|
|
746
|
-
.action(async (opts) => {
|
|
747
|
-
try {
|
|
748
|
-
const settings = resolveWorkerSettings(opts);
|
|
749
|
-
const address = (opts.address || settings.address || '').trim();
|
|
750
|
-
if (!address) throw new Error('address required (set --address or configure profile/env)');
|
|
751
|
-
const discovery = new SandboxDiscoveryClient(settings);
|
|
752
|
-
const participation = await discovery.participation(address);
|
|
753
|
-
const payload = {
|
|
754
|
-
address,
|
|
755
|
-
ensVerified: Boolean(participation.ensVerified),
|
|
756
|
-
ensName: participation.ensName || null,
|
|
757
|
-
stakeThresholdMet: Boolean(participation.stakeThresholdMet),
|
|
758
|
-
actions30d: Number(participation.actions30d || 0),
|
|
759
|
-
allowed: Boolean(participation.allowed),
|
|
760
|
-
reason: participation.reason || null,
|
|
761
|
-
};
|
|
762
|
-
if (opts.json || process.env.SAGE_QUIET_JSON) {
|
|
763
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
console.log(`📮 Sandbox eligibility for ${address}`);
|
|
767
|
-
console.log(` ENS verified : ${payload.ensVerified ? '✅ yes' : '❌ no'}`);
|
|
768
|
-
if (payload.ensName) console.log(` ↳ ENS name : ${payload.ensName}`);
|
|
769
|
-
console.log(` Stake met : ${payload.stakeThresholdMet ? '✅ yes' : '❌ no'}`);
|
|
770
|
-
console.log(` Actions (30d): ${payload.actions30d}`);
|
|
771
|
-
console.log(
|
|
772
|
-
` Access result : ${payload.allowed ? '✅ allowed' : '🚫 blocked'}${payload.reason ? ` (${payload.reason})` : ''}`,
|
|
773
|
-
);
|
|
774
|
-
} catch (error) {
|
|
775
|
-
console.error(`❌ sandbox status failed: ${error.message}`);
|
|
776
|
-
process.exit(1);
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
|
|
780
|
-
sandbox
|
|
781
|
-
.command("discover")
|
|
782
|
-
.description("Show sandbox discovery feed (trending by default)")
|
|
783
|
-
.option("--filter <name>", "Filter: trending, fresh, needs-review", "trending")
|
|
784
|
-
.option("--limit <number>", "Maximum prompts to display", (value) => Number(value))
|
|
785
|
-
.option("--worker-url <url>")
|
|
786
|
-
.option("--worker-token <token>")
|
|
787
|
-
.option("--json", "Output JSON", false)
|
|
788
|
-
.action(async (opts) => {
|
|
789
|
-
try {
|
|
790
|
-
const settings = resolveWorkerSettings(opts);
|
|
791
|
-
const discovery = new SandboxDiscoveryClient(settings);
|
|
792
|
-
const body = await discovery.discover({
|
|
793
|
-
filter: opts.filter,
|
|
794
|
-
limit: Number.isFinite(opts.limit) ? Number(opts.limit) : undefined,
|
|
795
|
-
});
|
|
796
|
-
const prompts = Array.isArray(body.prompts) ? body.prompts : [];
|
|
797
|
-
if (opts.json || process.env.SAGE_QUIET_JSON) {
|
|
798
|
-
console.log(JSON.stringify(body, null, 2));
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
if (!prompts.length) {
|
|
802
|
-
console.log(
|
|
803
|
-
"ℹ️ Discovery feed empty (publish public prompts to populate this view).",
|
|
804
|
-
);
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
if (!opts.json && !process.env.SAGE_QUIET_JSON) {
|
|
808
|
-
const discovery = new SandboxDiscoveryClient(settings);
|
|
809
|
-
await Promise.allSettled(
|
|
810
|
-
prompts.map((prompt) =>
|
|
811
|
-
discovery.recordEvent({
|
|
812
|
-
promptId: prompt.id,
|
|
813
|
-
type: 'view',
|
|
814
|
-
source: 'cli_discover',
|
|
815
|
-
}),
|
|
816
|
-
),
|
|
817
|
-
);
|
|
818
|
-
}
|
|
819
|
-
console.log(
|
|
820
|
-
`🔥 ${body.filter || "trending"} prompts (${prompts.length}) via ${settings.baseUrl}`,
|
|
821
|
-
);
|
|
822
|
-
for (const prompt of prompts) {
|
|
823
|
-
console.log(
|
|
824
|
-
`— ${prompt.id} by ${prompt.ensName || prompt.owner || "unknown"} [${prompt.visibility}] created ${prompt.createdAt}`,
|
|
825
|
-
);
|
|
826
|
-
if (prompt.title) console.log(` title: ${prompt.title}`);
|
|
827
|
-
if (Array.isArray(prompt.tags) && prompt.tags.length)
|
|
828
|
-
console.log(` tags: ${prompt.tags.join(", ")}`);
|
|
829
|
-
}
|
|
830
|
-
} catch (error) {
|
|
831
|
-
console.error(`❌ sandbox discover failed: ${error.message}`);
|
|
832
|
-
process.exit(1);
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
sandbox
|
|
837
|
-
.command("report <id>")
|
|
838
|
-
.description("Submit a governance report/flag for a sandbox prompt")
|
|
839
|
-
.option("--reason <text>", "Short reason code, e.g. policy, abuse")
|
|
840
|
-
.option("--note <text>", "Optional additional context (stored off-chain)")
|
|
841
|
-
.option("--weight <number>", "Relative severity weight (default 1)")
|
|
842
|
-
.option("--worker-url <url>")
|
|
843
|
-
.option("--worker-token <token>")
|
|
844
|
-
.option("--address <address>")
|
|
845
|
-
.option("--json", "Output JSON response", false)
|
|
846
|
-
.action(async (id, opts) => {
|
|
847
|
-
try {
|
|
848
|
-
const settings = resolveWorkerSettings(opts);
|
|
849
|
-
const client = new WorkerClient(settings);
|
|
850
|
-
const response = await client.submitGovernanceReport({
|
|
851
|
-
promptId: id,
|
|
852
|
-
reason: opts.reason,
|
|
853
|
-
note: opts.note,
|
|
854
|
-
weight: opts.weight,
|
|
855
|
-
address: opts.address || settings.address,
|
|
856
|
-
});
|
|
857
|
-
if (opts.json || process.env.SAGE_QUIET_JSON) {
|
|
858
|
-
console.log(JSON.stringify(response, null, 2));
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
if (response?.throttled) {
|
|
862
|
-
console.log("⚠️ Report received recently; throttled duplicate within cooldown window.");
|
|
863
|
-
} else {
|
|
864
|
-
console.log("✅ Governance report submitted for review.");
|
|
865
|
-
}
|
|
866
|
-
} catch (error) {
|
|
867
|
-
console.error(`❌ sandbox report failed: ${error.message}`);
|
|
868
|
-
process.exit(1);
|
|
869
|
-
}
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
const reports = new Command("reports")
|
|
873
|
-
.description("Review sandbox governance reports");
|
|
874
|
-
|
|
875
|
-
reports
|
|
876
|
-
.command("list")
|
|
877
|
-
.description("List sandbox governance reports")
|
|
878
|
-
.option("--status <status>", "Status filter", "open")
|
|
879
|
-
.option("--limit <number>", "Maximum reports to fetch", (value) => Number(value), 25)
|
|
880
|
-
.option("--cursor <number>", "Cursor for pagination", (value) => Number(value))
|
|
881
|
-
.option("--worker-url <url>")
|
|
882
|
-
.option("--worker-token <token>")
|
|
883
|
-
.option("--json", "Output JSON response", false)
|
|
884
|
-
.action(async (opts) => {
|
|
885
|
-
try {
|
|
886
|
-
const settings = resolveWorkerSettings(opts);
|
|
887
|
-
const client = new WorkerClient(settings);
|
|
888
|
-
const response = await client.listGovernanceReports({
|
|
889
|
-
status: opts.status,
|
|
890
|
-
limit: opts.limit,
|
|
891
|
-
cursor: opts.cursor,
|
|
892
|
-
});
|
|
893
|
-
if (opts.json || process.env.SAGE_QUIET_JSON) {
|
|
894
|
-
console.log(JSON.stringify(response, null, 2));
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
const reports = Array.isArray(response.reports) ? response.reports : [];
|
|
898
|
-
if (!reports.length) {
|
|
899
|
-
console.log("ℹ️ No reports found for the selected filters.");
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
console.log(`🛡️ ${reports.length} ${response.status} report(s)`);
|
|
903
|
-
for (const report of reports) {
|
|
904
|
-
console.log(
|
|
905
|
-
`• ${report.id} prompt=${report.promptId} actor=${report.actor} status=${report.status} weight=${report.weight} received=${report.receivedAt}`,
|
|
906
|
-
);
|
|
907
|
-
if (report.reason) console.log(` reason: ${report.reason}`);
|
|
908
|
-
if (report.note) console.log(` note: ${report.note}`);
|
|
909
|
-
}
|
|
910
|
-
if (response.cursor !== null && response.cursor !== undefined) {
|
|
911
|
-
console.log(`→ more available (cursor=${response.cursor})`);
|
|
912
|
-
}
|
|
913
|
-
} catch (error) {
|
|
914
|
-
console.error(`❌ sandbox reports list failed: ${error.message}`);
|
|
915
|
-
process.exit(1);
|
|
916
|
-
}
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
reports
|
|
920
|
-
.command("review <id>")
|
|
921
|
-
.description("Resolve or quarantine a sandbox governance report")
|
|
922
|
-
.requiredOption("--status <status>", "Resolution status (resolved|quarantined|rejected)")
|
|
923
|
-
.option("--note <text>", "Resolution note")
|
|
924
|
-
.option("--action <text>", "Optional action tag (e.g., hide, warn)")
|
|
925
|
-
.option("--worker-url <url>")
|
|
926
|
-
.option("--worker-token <token>")
|
|
927
|
-
.option("--json", "Output JSON response", false)
|
|
928
|
-
.action(async (id, opts) => {
|
|
929
|
-
try {
|
|
930
|
-
const settings = resolveWorkerSettings(opts);
|
|
931
|
-
const client = new WorkerClient(settings);
|
|
932
|
-
const response = await client.reviewGovernanceReport({
|
|
933
|
-
id,
|
|
934
|
-
status: opts.status,
|
|
935
|
-
note: opts.note,
|
|
936
|
-
action: opts.action,
|
|
937
|
-
});
|
|
938
|
-
if (opts.json || process.env.SAGE_QUIET_JSON) {
|
|
939
|
-
console.log(JSON.stringify(response, null, 2));
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
console.log(
|
|
943
|
-
`✅ report ${response.report.id} marked ${response.report.status}; unresolved=${response.summary.open}`,
|
|
944
|
-
);
|
|
945
|
-
} catch (error) {
|
|
946
|
-
console.error(`❌ sandbox reports review failed: ${error.message}`);
|
|
947
|
-
process.exit(1);
|
|
948
|
-
}
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
sandbox.addCommand(reports);
|
|
952
|
-
|
|
953
|
-
return sandbox;
|
|
954
|
-
})(),
|
|
955
|
-
)
|
|
956
|
-
.addCommand(
|
|
957
|
-
new Command("sell")
|
|
958
|
-
.description("Register or update an off-chain prompt listing")
|
|
959
|
-
.requiredOption("--prompt-id <id>", "Listing identifier")
|
|
960
|
-
.requiredOption("--cid <cid>", "Prompt CID for gated content")
|
|
961
|
-
.requiredOption("--price-usd <number>", "Price in USD")
|
|
962
|
-
.option("--title <text>", "Listing title")
|
|
963
|
-
.option("--description <text>", "Listing description")
|
|
964
|
-
.option(
|
|
965
|
-
"--worker-url <url>",
|
|
966
|
-
"Worker base URL",
|
|
967
|
-
process.env.SAGE_IPFS_WORKER_URL ||
|
|
968
|
-
process.env.SAGE_IPFS_UPLOAD_URL ||
|
|
969
|
-
DEFAULT_WORKER_BASE,
|
|
970
|
-
)
|
|
971
|
-
.option(
|
|
972
|
-
"--worker-token <token>",
|
|
973
|
-
"Worker bearer token (optional)",
|
|
974
|
-
process.env.SAGE_IPFS_UPLOAD_TOKEN,
|
|
975
|
-
)
|
|
976
|
-
.option(
|
|
977
|
-
"--worker-address <address>",
|
|
978
|
-
"Listing address when using bearer token",
|
|
979
|
-
process.env.SAGE_PAY_TO_PIN_ADDRESS,
|
|
980
|
-
)
|
|
981
|
-
.action(async (opts) => {
|
|
982
|
-
try {
|
|
983
|
-
const price = Number(opts.priceUsd);
|
|
984
|
-
if (!Number.isFinite(price) || price <= 0)
|
|
985
|
-
throw new Error("price-usd must be positive");
|
|
986
|
-
const baseUrl = (opts.workerUrl || "").trim();
|
|
987
|
-
if (!baseUrl) throw new Error("worker-url required");
|
|
988
|
-
const client = new PromptCommerceClient({
|
|
989
|
-
baseUrl,
|
|
990
|
-
token: opts.workerToken,
|
|
991
|
-
address: opts.workerAddress,
|
|
992
|
-
});
|
|
993
|
-
const listing = await client.createListing({
|
|
994
|
-
promptId: opts.promptId,
|
|
995
|
-
cid: opts.cid,
|
|
996
|
-
priceUsd: price,
|
|
997
|
-
title: opts.title,
|
|
998
|
-
description: opts.description,
|
|
999
|
-
seller: opts.workerAddress,
|
|
1000
|
-
});
|
|
1001
|
-
console.log("✅ Prompt listing updated");
|
|
1002
|
-
console.log(JSON.stringify(listing, null, 2));
|
|
1003
|
-
} catch (error) {
|
|
1004
|
-
console.error("❌ prompt sell failed:", error.message);
|
|
1005
|
-
process.exit(1);
|
|
1006
|
-
}
|
|
1007
|
-
}),
|
|
1008
|
-
)
|
|
1009
|
-
.addCommand(
|
|
1010
|
-
new Command("buy")
|
|
1011
|
-
.description("Initiate checkout for an off-chain prompt listing")
|
|
1012
|
-
.requiredOption("--prompt-id <id>", "Listing identifier")
|
|
1013
|
-
.option(
|
|
1014
|
-
"--worker-url <url>",
|
|
1015
|
-
"Worker base URL",
|
|
1016
|
-
process.env.SAGE_IPFS_WORKER_URL ||
|
|
1017
|
-
process.env.SAGE_IPFS_UPLOAD_URL ||
|
|
1018
|
-
DEFAULT_WORKER_BASE,
|
|
1019
|
-
)
|
|
1020
|
-
.option(
|
|
1021
|
-
"--worker-token <token>",
|
|
1022
|
-
"Worker bearer token (optional)",
|
|
1023
|
-
process.env.SAGE_IPFS_UPLOAD_TOKEN,
|
|
1024
|
-
)
|
|
1025
|
-
.option(
|
|
1026
|
-
"--worker-address <address>",
|
|
1027
|
-
"Buyer address when using bearer token",
|
|
1028
|
-
process.env.SAGE_PAY_TO_PIN_ADDRESS,
|
|
1029
|
-
)
|
|
1030
|
-
.option("--json", "Print raw JSON output", false)
|
|
1031
|
-
.action(async (opts) => {
|
|
1032
|
-
try {
|
|
1033
|
-
const baseUrl = (opts.workerUrl || "").trim();
|
|
1034
|
-
if (!baseUrl) throw new Error("worker-url required");
|
|
1035
|
-
const client = new PromptCommerceClient({
|
|
1036
|
-
baseUrl,
|
|
1037
|
-
token: opts.workerToken,
|
|
1038
|
-
address: opts.workerAddress,
|
|
1039
|
-
});
|
|
1040
|
-
const listing = await client.getListing(opts.promptId);
|
|
1041
|
-
if (!listing) {
|
|
1042
|
-
console.error("❌ Listing not found");
|
|
1043
|
-
process.exit(1);
|
|
1044
|
-
}
|
|
1045
|
-
const session = await client.initiateCheckout(opts.promptId);
|
|
1046
|
-
if (opts.json || process.env.SAGE_QUIET_JSON) {
|
|
1047
|
-
console.log(JSON.stringify({ listing, session }, null, 2));
|
|
1048
|
-
} else {
|
|
1049
|
-
console.log(
|
|
1050
|
-
`🛒 Listing: ${listing.title || opts.promptId} (${listing.priceUsd} USD)`,
|
|
1051
|
-
);
|
|
1052
|
-
console.log(`📦 Prompt CID: ${listing.cid}`);
|
|
1053
|
-
console.log(`✅ Buyer: ${session.buyer}`);
|
|
1054
|
-
if (session.checkoutUrl) {
|
|
1055
|
-
console.log(`🔗 Checkout URL:\n${session.checkoutUrl}`);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
console.error("❌ prompt buy failed:", error.message);
|
|
1060
|
-
process.exit(1);
|
|
1061
|
-
}
|
|
1062
|
-
}),
|
|
1063
|
-
)
|
|
1064
|
-
.addCommand(
|
|
1065
|
-
new Command("entitlements")
|
|
1066
|
-
.description("Check prompt entitlements after purchase")
|
|
1067
|
-
.requiredOption("--prompt-id <id>", "Listing identifier")
|
|
1068
|
-
.option(
|
|
1069
|
-
"--worker-url <url>",
|
|
1070
|
-
"Worker base URL",
|
|
1071
|
-
process.env.SAGE_IPFS_WORKER_URL ||
|
|
1072
|
-
process.env.SAGE_IPFS_UPLOAD_URL ||
|
|
1073
|
-
DEFAULT_WORKER_BASE,
|
|
1074
|
-
)
|
|
1075
|
-
.option(
|
|
1076
|
-
"--worker-token <token>",
|
|
1077
|
-
"Worker bearer token (optional)",
|
|
1078
|
-
process.env.SAGE_IPFS_UPLOAD_TOKEN,
|
|
1079
|
-
)
|
|
1080
|
-
.option(
|
|
1081
|
-
"--worker-address <address>",
|
|
1082
|
-
"Buyer address when using bearer token",
|
|
1083
|
-
process.env.SAGE_PAY_TO_PIN_ADDRESS,
|
|
1084
|
-
)
|
|
1085
|
-
.option("--json", "Print raw JSON output", false)
|
|
1086
|
-
.action(async (opts) => {
|
|
1087
|
-
try {
|
|
1088
|
-
const baseUrl = (opts.workerUrl || "").trim();
|
|
1089
|
-
if (!baseUrl) throw new Error("worker-url required");
|
|
1090
|
-
const client = new PromptCommerceClient({
|
|
1091
|
-
baseUrl,
|
|
1092
|
-
token: opts.workerToken,
|
|
1093
|
-
address: opts.workerAddress,
|
|
1094
|
-
});
|
|
1095
|
-
const entitlement = await client.getEntitlement(opts.promptId);
|
|
1096
|
-
if (!entitlement) {
|
|
1097
|
-
console.error(
|
|
1098
|
-
"ℹ️ Entitlement not found (purchase may still be pending).",
|
|
1099
|
-
);
|
|
1100
|
-
process.exit(2);
|
|
1101
|
-
}
|
|
1102
|
-
if (opts.json || process.env.SAGE_QUIET_JSON) {
|
|
1103
|
-
console.log(JSON.stringify(entitlement, null, 2));
|
|
1104
|
-
} else {
|
|
1105
|
-
console.log("🎟️ Entitlement granted");
|
|
1106
|
-
console.log(JSON.stringify(entitlement, null, 2));
|
|
1107
|
-
}
|
|
1108
|
-
} catch (error) {
|
|
1109
|
-
console.error("❌ prompt entitlements failed:", error.message);
|
|
1110
|
-
process.exit(1);
|
|
1111
|
-
}
|
|
1112
|
-
}),
|
|
1113
|
-
)
|
|
1114
|
-
.addCommand(
|
|
1115
|
-
new Command("add")
|
|
1116
|
-
.description(
|
|
1117
|
-
"Add an existing or manually authored prompt to a manifest",
|
|
1118
|
-
)
|
|
1119
|
-
.option("--manifest <path>", "Path to manifest.json to update")
|
|
1120
|
-
.option("--manifest-cid <cid>", "Manifest CID to fetch before updating")
|
|
1121
|
-
.option("--subdao <address>", "Filter selection to a SubDAO")
|
|
1122
|
-
.option("--file <path>", "Use prompt content from a file")
|
|
1123
|
-
.option("--stdin", "Read prompt content from STDIN", false)
|
|
1124
|
-
.option("--name <name>", "Prompt display name")
|
|
1125
|
-
.option("--key <key>", "Prompt key override")
|
|
1126
|
-
.option("--description <text>", "Prompt description")
|
|
1127
|
-
.option("--tags <tags>", "Comma or space separated tags")
|
|
1128
|
-
.option("--no-pin", "Skip uploading the prompt to IPFS")
|
|
1129
|
-
.action(async (opts) => {
|
|
1130
|
-
try {
|
|
1131
|
-
const inquirer = require("inquirer");
|
|
1132
|
-
const manifestEntry =
|
|
1133
|
-
await manifestSelector.selectManifestInteractive({
|
|
1134
|
-
manifest: opts.manifest,
|
|
1135
|
-
manifestCid: opts.manifestCid,
|
|
1136
|
-
subdao: opts.subdao,
|
|
1137
|
-
message: "Select a manifest to update",
|
|
1138
|
-
});
|
|
1139
|
-
const { manifest, path: manifestPath } =
|
|
1140
|
-
ensureManifestWritable(manifestEntry);
|
|
1141
|
-
manifest.prompts = Array.isArray(manifest.prompts)
|
|
1142
|
-
? manifest.prompts
|
|
1143
|
-
: [];
|
|
1144
|
-
|
|
1145
|
-
let content = "";
|
|
1146
|
-
let absoluteFile = null;
|
|
1147
|
-
if (opts.file) {
|
|
1148
|
-
const candidate = path.isAbsolute(opts.file)
|
|
1149
|
-
? opts.file
|
|
1150
|
-
: path.join(process.cwd(), opts.file);
|
|
1151
|
-
if (!fs.existsSync(candidate)) {
|
|
1152
|
-
throw new Error(`Prompt file not found: ${candidate}`);
|
|
1153
|
-
}
|
|
1154
|
-
absoluteFile = candidate;
|
|
1155
|
-
content = fs.readFileSync(candidate, "utf8");
|
|
1156
|
-
} else if (opts.stdin) {
|
|
1157
|
-
if (process.stdin.isTTY) {
|
|
1158
|
-
console.log("⌨️ Waiting for STDIN (Ctrl+D to finish)...");
|
|
1159
|
-
}
|
|
1160
|
-
content = await readStdin();
|
|
1161
|
-
} else {
|
|
1162
|
-
console.log(
|
|
1163
|
-
"\n📝 Provide prompt content via editor. Save and exit to continue.",
|
|
1164
|
-
);
|
|
1165
|
-
content = openEditorWithContent("");
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
const trimmedContent = String(content || "").trim();
|
|
1169
|
-
if (!trimmedContent) {
|
|
1170
|
-
console.log("⚠️ Prompt content is empty. Aborting.");
|
|
1171
|
-
return;
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
const questions = [];
|
|
1175
|
-
if (!opts.name) {
|
|
1176
|
-
const defaultName = absoluteFile
|
|
1177
|
-
? path.basename(absoluteFile, path.extname(absoluteFile))
|
|
1178
|
-
: "New Prompt";
|
|
1179
|
-
questions.push({
|
|
1180
|
-
name: "name",
|
|
1181
|
-
message: "Prompt name",
|
|
1182
|
-
type: "input",
|
|
1183
|
-
default: defaultName,
|
|
1184
|
-
});
|
|
1185
|
-
}
|
|
1186
|
-
if (!opts.description) {
|
|
1187
|
-
questions.push({
|
|
1188
|
-
name: "description",
|
|
1189
|
-
message: "Short description (optional)",
|
|
1190
|
-
type: "input",
|
|
1191
|
-
default: "",
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
if (!opts.tags) {
|
|
1195
|
-
questions.push({
|
|
1196
|
-
name: "tags",
|
|
1197
|
-
message: "Tags (comma or space separated, optional)",
|
|
1198
|
-
type: "input",
|
|
1199
|
-
default: "",
|
|
1200
|
-
});
|
|
1201
|
-
}
|
|
1202
|
-
const answers = questions.length
|
|
1203
|
-
? await inquirer.prompt(questions)
|
|
1204
|
-
: {};
|
|
1205
|
-
|
|
1206
|
-
const name = opts.name || answers.name || "New Prompt";
|
|
1207
|
-
const description = opts.description || answers.description || "";
|
|
1208
|
-
const tags = parseTags(opts.tags || answers.tags || []);
|
|
1209
|
-
const key = slugifyKey(opts.key || name);
|
|
1210
|
-
|
|
1211
|
-
const existingIndex = manifest.prompts.findIndex(
|
|
1212
|
-
(p) => p.key === key,
|
|
1213
|
-
);
|
|
1214
|
-
if (existingIndex !== -1) {
|
|
1215
|
-
const { confirmOverwrite } = await inquirer.prompt([
|
|
1216
|
-
{
|
|
1217
|
-
type: "confirm",
|
|
1218
|
-
name: "confirmOverwrite",
|
|
1219
|
-
message: `Prompt key '${key}' exists. Overwrite?`,
|
|
1220
|
-
default: false,
|
|
1221
|
-
},
|
|
1222
|
-
]);
|
|
1223
|
-
if (!confirmOverwrite) {
|
|
1224
|
-
console.log("❌ Aborted by user (prompt key already exists).");
|
|
1225
|
-
return;
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
let relativePath;
|
|
1230
|
-
if (absoluteFile) {
|
|
1231
|
-
relativePath = path.relative(
|
|
1232
|
-
path.dirname(manifestPath),
|
|
1233
|
-
absoluteFile,
|
|
1234
|
-
);
|
|
1235
|
-
} else {
|
|
1236
|
-
const fileResult = ensurePromptFile(
|
|
1237
|
-
manifestPath,
|
|
1238
|
-
key,
|
|
1239
|
-
trimmedContent + "\n",
|
|
1240
|
-
);
|
|
1241
|
-
absoluteFile = fileResult.absolute;
|
|
1242
|
-
relativePath = fileResult.relative;
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
let cid = null;
|
|
1246
|
-
if (!opts.noPin) {
|
|
1247
|
-
const ipfs = new IPFSManager();
|
|
1248
|
-
await ipfs.initialize();
|
|
1249
|
-
cid = await ipfs.uploadPrompt(absoluteFile, name, description);
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
const promptEntry = {
|
|
1253
|
-
key,
|
|
1254
|
-
name,
|
|
1255
|
-
description: description || undefined,
|
|
1256
|
-
tags: tags.length ? tags : undefined,
|
|
1257
|
-
files: [relativePath],
|
|
1258
|
-
};
|
|
1259
|
-
if (cid) promptEntry.cid = cid;
|
|
1260
|
-
|
|
1261
|
-
if (existingIndex !== -1) {
|
|
1262
|
-
manifest.prompts[existingIndex] = Object.assign(
|
|
1263
|
-
{},
|
|
1264
|
-
manifest.prompts[existingIndex],
|
|
1265
|
-
promptEntry,
|
|
1266
|
-
);
|
|
1267
|
-
} else {
|
|
1268
|
-
manifest.prompts.push(promptEntry);
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
writeManifest(manifest, manifestPath);
|
|
1272
|
-
console.log(`✅ Prompt '${key}' saved to ${manifestPath}`);
|
|
1273
|
-
if (cid) console.log(`🔗 CID: ${cid}`);
|
|
1274
|
-
console.log(`📁 Local file: ${absoluteFile}`);
|
|
1275
|
-
} catch (error) {
|
|
1276
|
-
console.error("❌ prompt add failed:", error.message);
|
|
1277
|
-
process.exit(1);
|
|
1278
|
-
}
|
|
1279
|
-
}),
|
|
1280
|
-
)
|
|
1281
|
-
.addCommand(
|
|
1282
|
-
new Command("delete")
|
|
1283
|
-
.description("Remove a prompt entry from a manifest")
|
|
1284
|
-
.argument("<key>", "Prompt key to delete")
|
|
1285
|
-
.option("--manifest <path>", "Path to manifest.json to update")
|
|
1286
|
-
.option("--manifest-cid <cid>", "Manifest CID to fetch before updating")
|
|
1287
|
-
.option("--subdao <address>", "Filter selection to a SubDAO")
|
|
1288
|
-
.option(
|
|
1289
|
-
"--archive",
|
|
1290
|
-
"Archive removed prompt to .sage/archives/prompts",
|
|
1291
|
-
false,
|
|
1292
|
-
)
|
|
1293
|
-
.option("-y, --yes", "Skip confirmation prompt")
|
|
1294
|
-
.action(async (key, opts) => {
|
|
1295
|
-
try {
|
|
1296
|
-
const inquirer = require("inquirer");
|
|
1297
|
-
const manifestEntry =
|
|
1298
|
-
await manifestSelector.selectManifestInteractive({
|
|
1299
|
-
manifest: opts.manifest,
|
|
1300
|
-
manifestCid: opts.manifestCid,
|
|
1301
|
-
subdao: opts.subdao,
|
|
1302
|
-
message: "Select a manifest to modify",
|
|
1303
|
-
});
|
|
1304
|
-
const { manifest, path: manifestPath } =
|
|
1305
|
-
ensureManifestWritable(manifestEntry);
|
|
1306
|
-
manifest.prompts = Array.isArray(manifest.prompts)
|
|
1307
|
-
? manifest.prompts
|
|
1308
|
-
: [];
|
|
1309
|
-
const idx = manifest.prompts.findIndex((p) => p.key === key);
|
|
1310
|
-
if (idx === -1) {
|
|
1311
|
-
throw new Error(`Prompt '${key}' not found in manifest.`);
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
if (!opts.yes) {
|
|
1315
|
-
const { confirm } = await inquirer.prompt([
|
|
1316
|
-
{
|
|
1317
|
-
type: "confirm",
|
|
1318
|
-
name: "confirm",
|
|
1319
|
-
message: `Remove prompt '${key}' from manifest?`,
|
|
1320
|
-
default: false,
|
|
1321
|
-
},
|
|
1322
|
-
]);
|
|
1323
|
-
if (!confirm) {
|
|
1324
|
-
console.log("❌ Aborted by user.");
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
const [removed] = manifest.prompts.splice(idx, 1);
|
|
1330
|
-
writeManifest(manifest, manifestPath);
|
|
1331
|
-
console.log(`✅ Removed prompt '${key}' from ${manifestPath}`);
|
|
1332
|
-
|
|
1333
|
-
if (opts.archive) {
|
|
1334
|
-
const archivePath = archivePrompt(manifestPath, removed);
|
|
1335
|
-
console.log(`🗄️ Archived entry at ${archivePath}`);
|
|
1336
|
-
}
|
|
1337
|
-
} catch (error) {
|
|
1338
|
-
console.error("❌ prompt delete failed:", error.message);
|
|
1339
|
-
process.exit(1);
|
|
1340
|
-
}
|
|
1341
|
-
}),
|
|
1342
|
-
)
|
|
1343
|
-
.addCommand(
|
|
1344
|
-
new Command("batch-upload")
|
|
1345
|
-
.description(
|
|
1346
|
-
"Upload all .md prompts in a directory and add to a manifest",
|
|
1347
|
-
)
|
|
1348
|
-
.requiredOption("--dir <path>", "Directory containing prompt files")
|
|
1349
|
-
.requiredOption("--manifest <path>", "Path to manifest.json to update")
|
|
1350
|
-
.action(async (opts) => {
|
|
1351
|
-
try {
|
|
1352
|
-
const root = path.isAbsolute(opts.dir)
|
|
1353
|
-
? opts.dir
|
|
1354
|
-
: path.join(process.cwd(), opts.dir);
|
|
1355
|
-
const mf = path.isAbsolute(opts.manifest)
|
|
1356
|
-
? opts.manifest
|
|
1357
|
-
: path.join(process.cwd(), opts.manifest);
|
|
1358
|
-
if (!fs.existsSync(root))
|
|
1359
|
-
throw new Error(`Directory not found: ${root}`);
|
|
1360
|
-
if (!fs.existsSync(mf))
|
|
1361
|
-
throw new Error(`Manifest not found: ${mf}`);
|
|
1362
|
-
const walk = (dir) =>
|
|
1363
|
-
fs.readdirSync(dir).flatMap((f) => {
|
|
1364
|
-
const p = path.join(dir, f);
|
|
1365
|
-
const st = fs.statSync(p);
|
|
1366
|
-
if (st.isDirectory()) return walk(p);
|
|
1367
|
-
return [p];
|
|
1368
|
-
});
|
|
1369
|
-
const files = walk(root).filter((p) => p.endsWith(".md"));
|
|
1370
|
-
if (!files.length) throw new Error("No .md files found");
|
|
1371
|
-
const ipfs = new IPFSManager();
|
|
1372
|
-
await ipfs.initialize();
|
|
1373
|
-
const raw = fs.readFileSync(mf, "utf8");
|
|
1374
|
-
const manifest = JSON.parse(raw);
|
|
1375
|
-
manifest.prompts = Array.isArray(manifest.prompts)
|
|
1376
|
-
? manifest.prompts
|
|
1377
|
-
: [];
|
|
1378
|
-
const taken = new Set(manifest.prompts.map((p) => p.key));
|
|
1379
|
-
let added = 0;
|
|
1380
|
-
for (const f of files) {
|
|
1381
|
-
const key = path.basename(f, path.extname(f));
|
|
1382
|
-
if (taken.has(key)) continue;
|
|
1383
|
-
const name = key.replace(/[-_]/g, " ");
|
|
1384
|
-
const cid = await ipfs.uploadPrompt(f, name, "Batch upload");
|
|
1385
|
-
const rel = path.relative(path.dirname(mf), f);
|
|
1386
|
-
manifest.prompts.push({ key, name, files: [rel], cid });
|
|
1387
|
-
taken.add(key);
|
|
1388
|
-
added += 1;
|
|
1389
|
-
}
|
|
1390
|
-
fs.writeFileSync(mf, JSON.stringify(manifest, null, 2));
|
|
1391
|
-
console.log(`✅ Uploaded ${added} prompts and updated ${mf}`);
|
|
1392
|
-
} catch (e) {
|
|
1393
|
-
console.error("❌ batch-upload failed:", e.message);
|
|
1394
|
-
process.exit(1);
|
|
1395
|
-
}
|
|
1396
|
-
}),
|
|
1397
|
-
)
|
|
1398
|
-
.addCommand(
|
|
1399
|
-
new Command("list")
|
|
1400
|
-
.description("List prompts from a manifest, CID, or pinned libraries")
|
|
1401
|
-
.option("--manifest <path>", "Manifest JSON to read from")
|
|
1402
|
-
.option("--manifest-cid <cid>", "Manifest CID to fetch from IPFS")
|
|
1403
|
-
.option(
|
|
1404
|
-
"--subdao <address>",
|
|
1405
|
-
"Filter pinned manifests by SubDAO address",
|
|
1406
|
-
)
|
|
1407
|
-
.option("--json", "Emit JSON output", false)
|
|
1408
|
-
.action(async (opts) => {
|
|
1409
|
-
try {
|
|
1410
|
-
const manifests = await resolveManifests({
|
|
1411
|
-
manifest: opts.manifest,
|
|
1412
|
-
manifestCid: opts.manifestCid,
|
|
1413
|
-
subdao: opts.subdao,
|
|
1414
|
-
});
|
|
1415
|
-
if (!manifests.length) {
|
|
1416
|
-
console.log(
|
|
1417
|
-
"⚠️ No manifests discovered. Pin a library or supply --manifest/--cid.",
|
|
1418
|
-
);
|
|
1419
|
-
return;
|
|
1420
|
-
}
|
|
1421
|
-
const rows = manifests.map((entry) => {
|
|
1422
|
-
const prompts = Array.isArray(entry.manifest?.prompts)
|
|
1423
|
-
? entry.manifest.prompts
|
|
1424
|
-
: [];
|
|
1425
|
-
return {
|
|
1426
|
-
library: entry.manifest?.library?.name || "(unnamed library)",
|
|
1427
|
-
description: entry.manifest?.library?.description || "",
|
|
1428
|
-
cid: entry.cid || null,
|
|
1429
|
-
path: entry.path || null,
|
|
1430
|
-
promptCount: prompts.length,
|
|
1431
|
-
prompts: prompts.map((p) => ({
|
|
1432
|
-
key: p.key,
|
|
1433
|
-
name: p.name,
|
|
1434
|
-
description: p.description || "",
|
|
1435
|
-
})),
|
|
1436
|
-
};
|
|
1437
|
-
});
|
|
1438
|
-
if (opts.json) {
|
|
1439
|
-
console.log(JSON.stringify({ manifests: rows }, null, 2));
|
|
1440
|
-
return;
|
|
1441
|
-
}
|
|
1442
|
-
rows.forEach((row, index) => {
|
|
1443
|
-
console.log(`${index + 1}. 📚 ${row.library}`);
|
|
1444
|
-
if (row.description) console.log(` 📄 ${row.description}`);
|
|
1445
|
-
if (row.cid) console.log(` 🔗 CID: ${row.cid}`);
|
|
1446
|
-
if (row.path) console.log(` 📁 ${row.path}`);
|
|
1447
|
-
console.log(` 📝 Prompts (${row.promptCount}):`);
|
|
1448
|
-
row.prompts.forEach((p, i) => {
|
|
1449
|
-
console.log(
|
|
1450
|
-
` ${i + 1}. ${p.key} — ${p.name || "Untitled"}`,
|
|
1451
|
-
);
|
|
1452
|
-
});
|
|
1453
|
-
console.log("");
|
|
1454
|
-
});
|
|
1455
|
-
} catch (error) {
|
|
1456
|
-
console.error("❌ prompt list failed:", error.message);
|
|
1457
|
-
process.exit(1);
|
|
1458
|
-
}
|
|
1459
|
-
}),
|
|
1460
|
-
)
|
|
1461
|
-
.addCommand(
|
|
1462
|
-
new Command("info")
|
|
1463
|
-
.description("Show metadata for a specific prompt key")
|
|
1464
|
-
.argument("<key>", "Prompt key to inspect")
|
|
1465
|
-
.option(
|
|
1466
|
-
"--prompt <key>",
|
|
1467
|
-
"Override prompt key when argument is omitted or ambiguous",
|
|
1468
|
-
)
|
|
1469
|
-
.option("--manifest <path>", "Manifest JSON to read from")
|
|
1470
|
-
.option("--manifest-cid <cid>", "Manifest CID to fetch from IPFS")
|
|
1471
|
-
.option(
|
|
1472
|
-
"--prompt-cid <cid>",
|
|
1473
|
-
"Direct prompt CID (skips manifest lookup)",
|
|
1474
|
-
)
|
|
1475
|
-
.option(
|
|
1476
|
-
"--subdao <address>",
|
|
1477
|
-
"Filter pinned manifests by SubDAO address",
|
|
1478
|
-
)
|
|
1479
|
-
.option("--json", "Emit JSON output", false)
|
|
1480
|
-
.action(async (key, opts) => {
|
|
1481
|
-
try {
|
|
1482
|
-
const { prompt, manifestMeta } = await locatePrompt(key, {
|
|
1483
|
-
prompt: opts.prompt,
|
|
1484
|
-
manifest: opts.manifest,
|
|
1485
|
-
manifestCid: opts.manifestCid,
|
|
1486
|
-
subdao: opts.subdao,
|
|
1487
|
-
promptCid: opts.promptCid,
|
|
1488
|
-
});
|
|
1489
|
-
const template = buildTemplate(prompt, manifestMeta);
|
|
1490
|
-
if (opts.json) {
|
|
1491
|
-
console.log(JSON.stringify(template, null, 2));
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
console.log(`🔑 Key: ${template.key}`);
|
|
1495
|
-
console.log(`📛 Name: ${template.name || "Untitled"}`);
|
|
1496
|
-
console.log(`📄 Description: ${template.description || "None"}`);
|
|
1497
|
-
console.log(
|
|
1498
|
-
`🔖 Tags: ${template.tags.length ? template.tags.join(", ") : "None"}`,
|
|
1499
|
-
);
|
|
1500
|
-
if (template.cid) console.log(`🔗 Prompt CID: ${template.cid}`);
|
|
1501
|
-
if (template.files?.length)
|
|
1502
|
-
console.log(`📂 Files: ${template.files.join(", ")}`);
|
|
1503
|
-
if (template.manifest?.cid)
|
|
1504
|
-
console.log(`🗂️ Manifest CID: ${template.manifest.cid}`);
|
|
1505
|
-
if (template.manifest?.path)
|
|
1506
|
-
console.log(`📁 Manifest Path: ${template.manifest.path}`);
|
|
1507
|
-
} catch (error) {
|
|
1508
|
-
console.error("❌ prompt info failed:", error.message);
|
|
1509
|
-
process.exit(1);
|
|
1510
|
-
}
|
|
1511
|
-
}),
|
|
1512
|
-
)
|
|
1513
|
-
.addCommand(
|
|
1514
|
-
new Command("use")
|
|
1515
|
-
.description("Print a prompt from manifests, pinned libraries, or IPFS")
|
|
1516
|
-
.argument("<key>", "Prompt key to use")
|
|
1517
|
-
.option(
|
|
1518
|
-
"--prompt <key>",
|
|
1519
|
-
"Override prompt key when argument contains shell-sensitive characters",
|
|
1520
|
-
)
|
|
1521
|
-
.option("--manifest <path>", "Manifest JSON to read from")
|
|
1522
|
-
.option("--manifest-cid <cid>", "Manifest CID to fetch from IPFS")
|
|
1523
|
-
.option(
|
|
1524
|
-
"--prompt-cid <cid>",
|
|
1525
|
-
"Direct prompt CID (skips manifest lookup)",
|
|
1526
|
-
)
|
|
1527
|
-
.option(
|
|
1528
|
-
"--subdao <address>",
|
|
1529
|
-
"Filter pinned manifests by SubDAO address",
|
|
1530
|
-
)
|
|
1531
|
-
.option("--copy", "Copy prompt content to clipboard", false)
|
|
1532
|
-
.option("--file <path>", "Append prompt content to a file")
|
|
1533
|
-
.option("--chatgpt", "Open prompt in ChatGPT", false)
|
|
1534
|
-
.option("--claude", "Open prompt in Claude (best-effort)", false)
|
|
1535
|
-
.option("--template", "Output JSON template alongside text", false)
|
|
1536
|
-
.option(
|
|
1537
|
-
"--quiet",
|
|
1538
|
-
"Suppress raw prompt output (useful with --template)",
|
|
1539
|
-
false,
|
|
1540
|
-
)
|
|
1541
|
-
.action(async (key, opts) => {
|
|
1542
|
-
try {
|
|
1543
|
-
const { prompt, manifestMeta } = await locatePrompt(key, {
|
|
1544
|
-
prompt: opts.prompt,
|
|
1545
|
-
manifest: opts.manifest,
|
|
1546
|
-
cid: opts.cid,
|
|
1547
|
-
subdao: opts.subdao,
|
|
1548
|
-
promptCid: opts.promptCid,
|
|
1549
|
-
});
|
|
1550
|
-
prompt.content = await resolvePromptContent(prompt, manifestMeta);
|
|
1551
|
-
if (!prompt.content) {
|
|
1552
|
-
throw new Error(
|
|
1553
|
-
"Prompt content unavailable. Provide --prompt-cid or ensure local files are reachable.",
|
|
1554
|
-
);
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
if (!opts.quiet) {
|
|
1558
|
-
const headerLines = [];
|
|
1559
|
-
if (prompt.name) headerLines.push(`🧠 ${prompt.name}`);
|
|
1560
|
-
if (prompt.description)
|
|
1561
|
-
headerLines.push(`📄 ${prompt.description}`);
|
|
1562
|
-
if (prompt.tags?.length)
|
|
1563
|
-
headerLines.push(`🔖 Tags: ${prompt.tags.join(", ")}`);
|
|
1564
|
-
if (manifestMeta?.cid)
|
|
1565
|
-
headerLines.push(`🗂️ Manifest CID: ${manifestMeta.cid}`);
|
|
1566
|
-
if (manifestMeta?.path)
|
|
1567
|
-
headerLines.push(`📁 Source: ${manifestMeta.path}`);
|
|
1568
|
-
console.log(headerLines.join("\n"));
|
|
1569
|
-
if (headerLines.length) console.log("");
|
|
1570
|
-
console.log(prompt.content);
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
if (opts.template) {
|
|
1574
|
-
const template = buildTemplate(prompt, manifestMeta);
|
|
1575
|
-
template.content = prompt.content;
|
|
1576
|
-
console.log("\n" + JSON.stringify(template, null, 2));
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
if (opts.copy) {
|
|
1580
|
-
const copied = copyToClipboard(prompt.content);
|
|
1581
|
-
if (copied.ok) {
|
|
1582
|
-
console.log(`📋 Copied to clipboard using ${copied.command}`);
|
|
1583
|
-
} else {
|
|
1584
|
-
console.log(`⚠️ Clipboard copy failed: ${copied.reason}`);
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
if (opts.file) {
|
|
1589
|
-
appendToFile(opts.file, prompt.content);
|
|
1590
|
-
console.log(`📝 Appended prompt to ${opts.file}`);
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
if (opts.chatgpt) {
|
|
1594
|
-
const link = metapromptDesigner.generateChatGPTLink(
|
|
1595
|
-
prompt.content,
|
|
1596
|
-
);
|
|
1597
|
-
if (link) {
|
|
1598
|
-
const opened = openExternal(link);
|
|
1599
|
-
if (opened.ok) {
|
|
1600
|
-
console.log(
|
|
1601
|
-
`🌐 Opened ChatGPT with generated link (${link}).`,
|
|
1602
|
-
);
|
|
1603
|
-
} else {
|
|
1604
|
-
console.log(
|
|
1605
|
-
`⚠️ Failed to open ChatGPT automatically: ${opened.error || "unknown error"}. Link: ${link}`,
|
|
1606
|
-
);
|
|
1607
|
-
}
|
|
1608
|
-
} else {
|
|
1609
|
-
console.log(
|
|
1610
|
-
"ℹ️ Prompt too long to generate a ChatGPT launch link. Copy the prompt manually.",
|
|
1611
|
-
);
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
if (opts.claude) {
|
|
1616
|
-
const link = `https://claude.ai/new?initialText=${encodeURIComponent(prompt.content)}`;
|
|
1617
|
-
const opened = openExternal(link);
|
|
1618
|
-
if (opened.ok) {
|
|
1619
|
-
console.log(`🌐 Opened Claude with generated link (${link}).`);
|
|
1620
|
-
} else {
|
|
1621
|
-
console.log(
|
|
1622
|
-
`⚠️ Failed to open Claude automatically: ${opened.error}. Link: ${link}`,
|
|
1623
|
-
);
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
} catch (error) {
|
|
1627
|
-
console.error("❌ prompt use failed:", error.message);
|
|
1628
|
-
process.exit(1);
|
|
1629
|
-
}
|
|
1630
|
-
}),
|
|
1631
|
-
)
|
|
1632
|
-
.addCommand(
|
|
1633
|
-
new Command("set-rake")
|
|
1634
|
-
.description(
|
|
1635
|
-
"Admin: set protocol rake for PremiumPrompts (max 10% = 1000 bps)",
|
|
1636
|
-
)
|
|
1637
|
-
.requiredOption("--bps <bps>", "Basis points (0-1000)")
|
|
1638
|
-
.requiredOption("--treasury <address>", "Protocol treasury address")
|
|
1639
|
-
.option(
|
|
1640
|
-
"--prompts <address>",
|
|
1641
|
-
"PremiumPrompts contract (defaults to PREMIUM_PROMPTS_ADDRESS)",
|
|
1642
|
-
)
|
|
1643
|
-
.action(async (opts) => {
|
|
1644
|
-
try {
|
|
1645
|
-
const WM = require("@sage-protocol/wallet-manager");
|
|
1646
|
-
const { ethers } = require("ethers");
|
|
1647
|
-
const wm = new WM();
|
|
1648
|
-
await wm.connect();
|
|
1649
|
-
const signer = wm.getSigner();
|
|
1650
|
-
const promAddr =
|
|
1651
|
-
opts.prompts || process.env.PREMIUM_PROMPTS_ADDRESS;
|
|
1652
|
-
if (!promAddr) throw new Error("Missing PREMIUM_PROMPTS_ADDRESS");
|
|
1653
|
-
const bps = Number(opts.bps);
|
|
1654
|
-
if (!Number.isFinite(bps) || bps < 0 || bps > 1000)
|
|
1655
|
-
throw new Error("bps must be 0-1000");
|
|
1656
|
-
const iface = new ethers.Interface([
|
|
1657
|
-
"function setProtocolRake(uint96,address)",
|
|
1658
|
-
]);
|
|
1659
|
-
const tx = await signer.sendTransaction({
|
|
1660
|
-
to: promAddr,
|
|
1661
|
-
data: iface.encodeFunctionData("setProtocolRake", [
|
|
1662
|
-
bps,
|
|
1663
|
-
opts.treasury,
|
|
1664
|
-
]),
|
|
1665
|
-
});
|
|
1666
|
-
console.log("tx:", tx.hash);
|
|
1667
|
-
} catch (e) {
|
|
1668
|
-
console.error("❌ set-rake failed:", e.message);
|
|
1669
|
-
process.exit(1);
|
|
1670
|
-
}
|
|
1671
|
-
}),
|
|
1672
|
-
)
|
|
1673
|
-
.addCommand(
|
|
1674
|
-
new Command("generate")
|
|
1675
|
-
.description("Generate a new prompt file using an LLM")
|
|
1676
|
-
.requiredOption("--topic <text>", "What the prompt should be for")
|
|
1677
|
-
.option("--out <file>", "Output path", "prompts/generated-prompt.md")
|
|
1678
|
-
.option("--provider <name>", "openai|anthropic (auto-detect by key)")
|
|
1679
|
-
.option(
|
|
1680
|
-
"--model <id>",
|
|
1681
|
-
"Model id (default: gpt-4o-mini or claude-3.5-sonnet)",
|
|
1682
|
-
)
|
|
1683
|
-
.option(
|
|
1684
|
-
"--key <value>",
|
|
1685
|
-
"API key override (env/config used if omitted)",
|
|
1686
|
-
)
|
|
1687
|
-
.option(
|
|
1688
|
-
"--manifest <path>",
|
|
1689
|
-
"If set, add the new file to this manifest and upload to IPFS",
|
|
1690
|
-
)
|
|
1691
|
-
.action(async (opts) => {
|
|
1692
|
-
try {
|
|
1693
|
-
const fs = require("fs");
|
|
1694
|
-
const path = require("path");
|
|
1695
|
-
const axios = require("axios");
|
|
1696
|
-
const cliConfig = require("../config");
|
|
1697
|
-
const ai = cliConfig.readAIConfig();
|
|
1698
|
-
const provider =
|
|
1699
|
-
opts.provider ||
|
|
1700
|
-
(opts.key
|
|
1701
|
-
? "openai"
|
|
1702
|
-
: ai.openaiApiKey
|
|
1703
|
-
? "openai"
|
|
1704
|
-
: ai.anthropicApiKey
|
|
1705
|
-
? "anthropic"
|
|
1706
|
-
: null);
|
|
1707
|
-
if (!provider)
|
|
1708
|
-
throw new Error(
|
|
1709
|
-
"No API key. Use: sage config ai set --provider openai --key sk-...",
|
|
1710
|
-
);
|
|
1711
|
-
const isOpenAI = provider === "openai";
|
|
1712
|
-
const apiKey =
|
|
1713
|
-
opts.key || (isOpenAI ? ai.openaiApiKey : ai.anthropicApiKey);
|
|
1714
|
-
if (!apiKey) throw new Error(`${provider} API key not configured`);
|
|
1715
|
-
|
|
1716
|
-
const sys =
|
|
1717
|
-
"You are an expert prompt engineer. Generate a robust, reusable system prompt. Include: goals, role, constraints, style, variables, and example instructions. Output as Markdown.";
|
|
1718
|
-
const user = `Topic: ${opts.topic}`;
|
|
1719
|
-
|
|
1720
|
-
let content;
|
|
1721
|
-
if (isOpenAI) {
|
|
1722
|
-
const model = opts.model || "gpt-4o-mini";
|
|
1723
|
-
const resp = await axios.post(
|
|
1724
|
-
"https://api.openai.com/v1/chat/completions",
|
|
1725
|
-
{
|
|
1726
|
-
model,
|
|
1727
|
-
messages: [
|
|
1728
|
-
{ role: "system", content: sys },
|
|
1729
|
-
{ role: "user", content: user },
|
|
1730
|
-
],
|
|
1731
|
-
temperature: 0.7,
|
|
1732
|
-
},
|
|
1733
|
-
{
|
|
1734
|
-
headers: {
|
|
1735
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1736
|
-
"Content-Type": "application/json",
|
|
1737
|
-
},
|
|
1738
|
-
},
|
|
1739
|
-
);
|
|
1740
|
-
content = resp.data?.choices?.[0]?.message?.content || "";
|
|
1741
|
-
} else {
|
|
1742
|
-
const model = opts.model || "claude-3.5-sonnet-20240620";
|
|
1743
|
-
const resp = await axios.post(
|
|
1744
|
-
"https://api.anthropic.com/v1/messages",
|
|
1745
|
-
{
|
|
1746
|
-
model,
|
|
1747
|
-
max_tokens: 1024,
|
|
1748
|
-
system: sys,
|
|
1749
|
-
messages: [{ role: "user", content: user }],
|
|
1750
|
-
},
|
|
1751
|
-
{
|
|
1752
|
-
headers: {
|
|
1753
|
-
"x-api-key": apiKey,
|
|
1754
|
-
"anthropic-version": "2023-06-01",
|
|
1755
|
-
"content-type": "application/json",
|
|
1756
|
-
},
|
|
1757
|
-
},
|
|
1758
|
-
);
|
|
1759
|
-
content =
|
|
1760
|
-
resp.data?.content && Array.isArray(resp.data.content)
|
|
1761
|
-
? resp.data.content[0].text || ""
|
|
1762
|
-
: "";
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
// Write to file
|
|
1766
|
-
const outPath = path.isAbsolute(opts.out)
|
|
1767
|
-
? opts.out
|
|
1768
|
-
: path.join(process.cwd(), opts.out);
|
|
1769
|
-
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1770
|
-
fs.writeFileSync(outPath, String(content || "").trim() + "\n");
|
|
1771
|
-
console.log(`✅ Wrote prompt to ${outPath}`);
|
|
1772
|
-
|
|
1773
|
-
if (opts.manifest) {
|
|
1774
|
-
const mf = path.isAbsolute(opts.manifest)
|
|
1775
|
-
? opts.manifest
|
|
1776
|
-
: path.join(process.cwd(), opts.manifest);
|
|
1777
|
-
if (!fs.existsSync(mf))
|
|
1778
|
-
throw new Error(`Manifest not found: ${mf}`);
|
|
1779
|
-
const IPFSManager = require("../ipfs-manager");
|
|
1780
|
-
const ipfs = new IPFSManager();
|
|
1781
|
-
await ipfs.initialize();
|
|
1782
|
-
const key = path.basename(outPath, path.extname(outPath));
|
|
1783
|
-
const name = key.replace(/[-_]/g, " ");
|
|
1784
|
-
const cid = await ipfs.uploadPrompt(
|
|
1785
|
-
outPath,
|
|
1786
|
-
name,
|
|
1787
|
-
`AI-generated prompt for ${opts.topic}`,
|
|
1788
|
-
);
|
|
1789
|
-
const raw = fs.readFileSync(mf, "utf8");
|
|
1790
|
-
const manifest = JSON.parse(raw);
|
|
1791
|
-
manifest.prompts = Array.isArray(manifest.prompts)
|
|
1792
|
-
? manifest.prompts
|
|
1793
|
-
: [];
|
|
1794
|
-
manifest.prompts.push({
|
|
1795
|
-
key,
|
|
1796
|
-
name,
|
|
1797
|
-
description: `AI-generated prompt for ${opts.topic}`,
|
|
1798
|
-
cid,
|
|
1799
|
-
files: [path.relative(path.dirname(mf), outPath)],
|
|
1800
|
-
});
|
|
1801
|
-
fs.writeFileSync(mf, JSON.stringify(manifest, null, 2));
|
|
1802
|
-
console.log(`📦 Added to manifest: ${mf}`);
|
|
1803
|
-
console.log(`🔗 CID: ${cid}`);
|
|
1804
|
-
}
|
|
1805
|
-
} catch (error) {
|
|
1806
|
-
console.error("❌ prompt generate failed:", error.message);
|
|
1807
|
-
process.exit(1);
|
|
1808
|
-
}
|
|
1809
|
-
}),
|
|
1810
|
-
)
|
|
1811
|
-
.addCommand(
|
|
1812
|
-
new Command("run")
|
|
1813
|
-
.description("Run a prompt with an LLM via Echo or direct provider")
|
|
1814
|
-
.option("--cid <cid>", "Prompt CID (JSON with content field)")
|
|
1815
|
-
.option("--file <path>", "Local prompt file path")
|
|
1816
|
-
.option("--input <text>", "User input to send alongside the prompt")
|
|
1817
|
-
.option(
|
|
1818
|
-
"--provider <name>",
|
|
1819
|
-
"echo|openai|anthropic (auto-detect if omitted)",
|
|
1820
|
-
)
|
|
1821
|
-
.option(
|
|
1822
|
-
"--model <id>",
|
|
1823
|
-
"Model id (default: gpt-4o-mini or claude-3.5-sonnet)",
|
|
1824
|
-
)
|
|
1825
|
-
.option(
|
|
1826
|
-
"--key <value>",
|
|
1827
|
-
"API key override (env/config used if omitted)",
|
|
1828
|
-
)
|
|
1829
|
-
.option("--json", "Print raw JSON response", false)
|
|
1830
|
-
.action(async (opts) => {
|
|
1831
|
-
try {
|
|
1832
|
-
const axios = require("axios");
|
|
1833
|
-
const cliConfig = require("../config");
|
|
1834
|
-
const ai =
|
|
1835
|
-
typeof cliConfig.readAIConfig === "function"
|
|
1836
|
-
? cliConfig.readAIConfig()
|
|
1837
|
-
: { openaiApiKey: null, anthropicApiKey: null };
|
|
1838
|
-
const normalize = (value) =>
|
|
1839
|
-
typeof value === "string" ? value.toLowerCase() : null;
|
|
1840
|
-
const envEchoKey =
|
|
1841
|
-
process.env.ECHO_API_KEY ||
|
|
1842
|
-
process.env.NEXT_PUBLIC_ECHO_API_KEY ||
|
|
1843
|
-
null;
|
|
1844
|
-
let provider = normalize(opts.provider);
|
|
1845
|
-
if (!provider) {
|
|
1846
|
-
if (envEchoKey && !opts.key) {
|
|
1847
|
-
provider = "echo";
|
|
1848
|
-
} else if (opts.key) {
|
|
1849
|
-
provider = "openai";
|
|
1850
|
-
} else if (ai.openaiApiKey) {
|
|
1851
|
-
provider = "openai";
|
|
1852
|
-
} else if (ai.anthropicApiKey) {
|
|
1853
|
-
provider = "anthropic";
|
|
1854
|
-
} else {
|
|
1855
|
-
provider = null;
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
if (!provider) {
|
|
1860
|
-
console.log(
|
|
1861
|
-
"❌ No provider configured. Set ECHO_API_KEY (Echo), OPENAI_API_KEY, or ANTHROPIC_API_KEY.",
|
|
1862
|
-
);
|
|
1863
|
-
console.log(" Echo example: export ECHO_API_KEY=echo_sk_...");
|
|
1864
|
-
console.log(
|
|
1865
|
-
" OpenAI example: sage config ai set --provider openai --key sk-...",
|
|
1866
|
-
);
|
|
1867
|
-
console.log(
|
|
1868
|
-
" Anthropic example: sage config ai set --provider anthropic --key sk-ant-...",
|
|
1869
|
-
);
|
|
1870
|
-
process.exit(1);
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
const isEcho = provider === "echo";
|
|
1874
|
-
const isOpenAI = provider === "openai";
|
|
1875
|
-
const apiKey =
|
|
1876
|
-
opts.key ||
|
|
1877
|
-
(isEcho
|
|
1878
|
-
? envEchoKey
|
|
1879
|
-
: isOpenAI
|
|
1880
|
-
? ai.openaiApiKey
|
|
1881
|
-
: ai.anthropicApiKey);
|
|
1882
|
-
if (!apiKey) {
|
|
1883
|
-
throw new Error(`${provider} API key not configured`);
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
// Resolve prompt content
|
|
1887
|
-
let systemPrompt = "";
|
|
1888
|
-
if (opts.file) {
|
|
1889
|
-
const fs = require("fs");
|
|
1890
|
-
const path = require("path");
|
|
1891
|
-
const p = path.isAbsolute(opts.file)
|
|
1892
|
-
? opts.file
|
|
1893
|
-
: path.join(process.cwd(), opts.file);
|
|
1894
|
-
systemPrompt = fs.readFileSync(p, "utf8");
|
|
1895
|
-
} else if (opts.cid) {
|
|
1896
|
-
const IPFSManager = require("../ipfs-manager");
|
|
1897
|
-
const ipfs = new IPFSManager();
|
|
1898
|
-
await ipfs.initialize();
|
|
1899
|
-
try {
|
|
1900
|
-
const j = await ipfs.downloadJson(opts.cid);
|
|
1901
|
-
systemPrompt = j && (j.content || j.prompt || j.text || "");
|
|
1902
|
-
if (!systemPrompt)
|
|
1903
|
-
throw new Error("No content field in CID JSON");
|
|
1904
|
-
} catch (e) {
|
|
1905
|
-
// Fallback: try file download flow
|
|
1906
|
-
const os = require("os");
|
|
1907
|
-
const path = require("path");
|
|
1908
|
-
const tmp = path.join(
|
|
1909
|
-
os.tmpdir(),
|
|
1910
|
-
`sage-prompt-${Date.now()}.txt`,
|
|
1911
|
-
);
|
|
1912
|
-
const out = await ipfs.downloadPrompt(opts.cid, tmp);
|
|
1913
|
-
systemPrompt = require("fs").readFileSync(out, "utf8");
|
|
1914
|
-
}
|
|
1915
|
-
} else {
|
|
1916
|
-
throw new Error("Provide --file or --cid");
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
const userInput =
|
|
1920
|
-
opts.input || "Respond to the prompt instructions.";
|
|
1921
|
-
|
|
1922
|
-
if (isEcho) {
|
|
1923
|
-
const {
|
|
1924
|
-
createEchoOpenAI,
|
|
1925
|
-
} = require("@merit-systems/echo-typescript-sdk");
|
|
1926
|
-
const { generateText } = require("ai");
|
|
1927
|
-
const model =
|
|
1928
|
-
opts.model || process.env.ECHO_MODEL || "gpt-4o-mini";
|
|
1929
|
-
const echoConfig = {
|
|
1930
|
-
appId:
|
|
1931
|
-
process.env.ECHO_APP_ID ||
|
|
1932
|
-
process.env.NEXT_PUBLIC_ECHO_APP_ID ||
|
|
1933
|
-
undefined,
|
|
1934
|
-
baseRouterUrl: process.env.ECHO_ROUTER_URL || undefined,
|
|
1935
|
-
};
|
|
1936
|
-
const openai = createEchoOpenAI(echoConfig, async () => apiKey);
|
|
1937
|
-
const prompt = `${systemPrompt.trim()}
|
|
1938
|
-
|
|
1939
|
-
User:
|
|
1940
|
-
${userInput}`;
|
|
1941
|
-
const result = await generateText({
|
|
1942
|
-
model: openai(model),
|
|
1943
|
-
prompt,
|
|
1944
|
-
});
|
|
1945
|
-
if (opts.json) {
|
|
1946
|
-
console.log(JSON.stringify(result, null, 2));
|
|
1947
|
-
return;
|
|
1948
|
-
}
|
|
1949
|
-
console.log("\n🧠 Echo (OpenAI-compatible) Output:\n");
|
|
1950
|
-
console.log(result.text || "(no text in response)");
|
|
1951
|
-
if (result.usage) {
|
|
1952
|
-
console.log(
|
|
1953
|
-
`\nTokens — prompt: ${result.usage.promptTokens ?? "n/a"}, completion: ${result.usage.completionTokens ?? "n/a"}, total: ${result.usage.totalTokens ?? "n/a"}`,
|
|
1954
|
-
);
|
|
1955
|
-
}
|
|
1956
|
-
} else if (isOpenAI) {
|
|
1957
|
-
const model = opts.model || "gpt-4o-mini";
|
|
1958
|
-
const resp = await axios.post(
|
|
1959
|
-
"https://api.openai.com/v1/chat/completions",
|
|
1960
|
-
{
|
|
1961
|
-
model,
|
|
1962
|
-
messages: [
|
|
1963
|
-
{ role: "system", content: systemPrompt },
|
|
1964
|
-
{ role: "user", content: userInput },
|
|
1965
|
-
],
|
|
1966
|
-
temperature: 0.7,
|
|
1967
|
-
},
|
|
1968
|
-
{
|
|
1969
|
-
headers: {
|
|
1970
|
-
Authorization: `Bearer ${apiKey}`,
|
|
1971
|
-
"Content-Type": "application/json",
|
|
1972
|
-
},
|
|
1973
|
-
},
|
|
1974
|
-
);
|
|
1975
|
-
if (opts.json) {
|
|
1976
|
-
console.log(JSON.stringify(resp.data, null, 2));
|
|
1977
|
-
return;
|
|
1978
|
-
}
|
|
1979
|
-
const text = resp.data?.choices?.[0]?.message?.content || "";
|
|
1980
|
-
console.log("\n🧠 LLM Output:\n");
|
|
1981
|
-
console.log(text);
|
|
1982
|
-
} else {
|
|
1983
|
-
// Anthropic
|
|
1984
|
-
const model = opts.model || "claude-3.5-sonnet-20240620";
|
|
1985
|
-
const resp = await axios.post(
|
|
1986
|
-
"https://api.anthropic.com/v1/messages",
|
|
1987
|
-
{
|
|
1988
|
-
model,
|
|
1989
|
-
max_tokens: 1024,
|
|
1990
|
-
system: systemPrompt,
|
|
1991
|
-
messages: [{ role: "user", content: userInput }],
|
|
1992
|
-
},
|
|
1993
|
-
{
|
|
1994
|
-
headers: {
|
|
1995
|
-
"x-api-key": apiKey,
|
|
1996
|
-
"anthropic-version": "2023-06-01",
|
|
1997
|
-
"content-type": "application/json",
|
|
1998
|
-
},
|
|
1999
|
-
},
|
|
2000
|
-
);
|
|
2001
|
-
if (opts.json) {
|
|
2002
|
-
console.log(JSON.stringify(resp.data, null, 2));
|
|
2003
|
-
return;
|
|
2004
|
-
}
|
|
2005
|
-
const text =
|
|
2006
|
-
resp.data?.content && Array.isArray(resp.data.content)
|
|
2007
|
-
? resp.data.content[0].text || ""
|
|
2008
|
-
: "";
|
|
2009
|
-
console.log("\n🧠 LLM Output:\n");
|
|
2010
|
-
console.log(text);
|
|
2011
|
-
}
|
|
2012
|
-
} catch (error) {
|
|
2013
|
-
console.error("❌ prompt run failed:", error.message);
|
|
2014
|
-
process.exit(1);
|
|
2015
|
-
}
|
|
2016
|
-
}),
|
|
2017
|
-
)
|
|
2018
|
-
.addCommand(
|
|
2019
|
-
new Command("publish")
|
|
2020
|
-
.description("(Deprecated) Single-file prompt publish. Prefer `sage prompts publish` for workspace-aware flows.")
|
|
2021
|
-
.addHelpText('beforeAll', '\nDeprecated: Use `sage prompts publish` for build→validate→push→propose. This command will be removed in a future release.')
|
|
2022
|
-
.argument("<file>", "Path to prompt file")
|
|
2023
|
-
.option("--name <name>", "Prompt name override")
|
|
2024
|
-
.option("--description <text>", "Prompt description override")
|
|
2025
|
-
.option("--no-pin", "Skip IPFS pinning")
|
|
2026
|
-
.option(
|
|
2027
|
-
"--sync-subdao <address>",
|
|
2028
|
-
"Sync to on-chain registry via governance (SubDAO address)",
|
|
2029
|
-
)
|
|
2030
|
-
.option("--key <key>", "Optional prompt key (defaults to CID)")
|
|
2031
|
-
.option(
|
|
2032
|
-
"--build-payload",
|
|
2033
|
-
"Print governance payload JSON for addPrompt (no submit)",
|
|
2034
|
-
)
|
|
2035
|
-
.action(async (file, options) => {
|
|
2036
|
-
try {
|
|
2037
|
-
console.log('⚠️ Deprecated: Use `sage prompts publish` for workspace-aware publishing. This command will be removed.');
|
|
2038
|
-
const filePath = path.isAbsolute(file)
|
|
2039
|
-
? file
|
|
2040
|
-
: path.join(process.cwd(), file);
|
|
2041
|
-
if (!fs.existsSync(filePath))
|
|
2042
|
-
throw new Error(`File not found: ${filePath}`);
|
|
2043
|
-
|
|
2044
|
-
const ipfs = new IPFSManager();
|
|
2045
|
-
await ipfs.initialize();
|
|
2046
|
-
const cid = await ipfs.uploadPrompt(
|
|
2047
|
-
filePath,
|
|
2048
|
-
options.name,
|
|
2049
|
-
options.description,
|
|
2050
|
-
);
|
|
2051
|
-
|
|
2052
|
-
if (options.pin !== false) {
|
|
2053
|
-
// Pinning is handled by upload when using Pinata. For simulation/local, nothing to do.
|
|
2054
|
-
console.log(
|
|
2055
|
-
"📌 Pin requested: upload step handled pin where applicable",
|
|
2056
|
-
);
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
if (options.buildPayload) {
|
|
2060
|
-
// Build addPrompt(title, tags, contentCID, metadataCID, category)
|
|
2061
|
-
const title =
|
|
2062
|
-
options.name || path.basename(filePath, path.extname(filePath));
|
|
2063
|
-
const tags = [];
|
|
2064
|
-
const metadata = {
|
|
2065
|
-
name: title,
|
|
2066
|
-
description: options.description || "",
|
|
2067
|
-
tags,
|
|
2068
|
-
};
|
|
2069
|
-
const metaCid = await ipfs.uploadJson(
|
|
2070
|
-
metadata,
|
|
2071
|
-
`${title}-metadata`,
|
|
2072
|
-
);
|
|
2073
|
-
const { ethers } = require("ethers");
|
|
2074
|
-
const abi = [
|
|
2075
|
-
"function addPrompt(string title, bytes32[] tags, string contentCID, string metadataCID, uint8 category)",
|
|
2076
|
-
];
|
|
2077
|
-
const iface = new ethers.Interface(abi);
|
|
2078
|
-
const calldata = iface.encodeFunctionData("addPrompt", [
|
|
2079
|
-
title,
|
|
2080
|
-
[],
|
|
2081
|
-
cid,
|
|
2082
|
-
metaCid,
|
|
2083
|
-
0,
|
|
2084
|
-
]);
|
|
2085
|
-
// Resolve registry if subdao provided
|
|
2086
|
-
let registry = process.env.PROMPT_REGISTRY_ADDRESS;
|
|
2087
|
-
if (
|
|
2088
|
-
options.syncSubdao &&
|
|
2089
|
-
/^0x[0-9a-fA-F]{40}$/.test(String(options.syncSubdao))
|
|
2090
|
-
) {
|
|
2091
|
-
try {
|
|
2092
|
-
const { execSync } = require("child_process");
|
|
2093
|
-
const rpc = process.env.RPC_URL || "https://sepolia.base.org";
|
|
2094
|
-
registry = execSync(
|
|
2095
|
-
`cast call ${options.syncSubdao} "promptRegistry()" --rpc-url ${rpc} | cat`,
|
|
2096
|
-
{ encoding: "utf8" },
|
|
2097
|
-
).trim();
|
|
2098
|
-
} catch (_) {}
|
|
2099
|
-
}
|
|
2100
|
-
if (!registry) {
|
|
2101
|
-
console.log(
|
|
2102
|
-
"ℹ️ Set PROMPT_REGISTRY_ADDRESS or pass --sync-subdao to include target",
|
|
2103
|
-
);
|
|
2104
|
-
}
|
|
2105
|
-
const payload = {
|
|
2106
|
-
targets: registry ? [registry] : [],
|
|
2107
|
-
values: registry ? [0] : [],
|
|
2108
|
-
calldatas: [calldata],
|
|
2109
|
-
description: `Add prompt ${title} (${cid})`,
|
|
2110
|
-
};
|
|
2111
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
if (options.syncSubdao) {
|
|
2115
|
-
const mgr = new PromptManager();
|
|
2116
|
-
await mgr.initialize();
|
|
2117
|
-
const subdao = options.syncSubdao;
|
|
2118
|
-
const key = options.key || cid;
|
|
2119
|
-
console.log(
|
|
2120
|
-
"🔄 Syncing to on-chain registry requires a proposal via library flow.",
|
|
2121
|
-
);
|
|
2122
|
-
console.log(
|
|
2123
|
-
"ℹ️ Use `sage project propose` with a manifest, or your governance workflow.",
|
|
2124
|
-
);
|
|
2125
|
-
console.log(`CID: ${cid} Key: ${key} SubDAO: ${subdao}`);
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
console.log(`✅ Published: ${cid}`);
|
|
2129
|
-
} catch (error) {
|
|
2130
|
-
console.error(`❌ Failed: ${error.message}`);
|
|
2131
|
-
process.exit(1);
|
|
2132
|
-
}
|
|
2133
|
-
}),
|
|
2134
|
-
)
|
|
2135
|
-
.addCommand(
|
|
2136
|
-
new Command("preview")
|
|
2137
|
-
.description("Preview prompt content from IPFS")
|
|
2138
|
-
.argument("<cid>", "IPFS CID of the prompt")
|
|
2139
|
-
.action(async (cid) => {
|
|
2140
|
-
try {
|
|
2141
|
-
const manager = new PromptManager();
|
|
2142
|
-
await manager.initialize();
|
|
2143
|
-
await manager.previewPrompt(cid);
|
|
2144
|
-
} catch (error) {
|
|
2145
|
-
console.error("❌ Failed to preview prompt:", error.message);
|
|
2146
|
-
process.exit(1);
|
|
2147
|
-
}
|
|
2148
|
-
}),
|
|
2149
|
-
)
|
|
2150
|
-
.addCommand(
|
|
2151
|
-
new Command("compare")
|
|
2152
|
-
.description("Compare two prompts from IPFS")
|
|
2153
|
-
.argument("<cid1>", "First IPFS CID")
|
|
2154
|
-
.argument("<cid2>", "Second IPFS CID")
|
|
2155
|
-
.action(async (cid1, cid2) => {
|
|
2156
|
-
try {
|
|
2157
|
-
const manager = new PromptManager();
|
|
2158
|
-
await manager.initialize();
|
|
2159
|
-
await manager.comparePrompts(cid1, cid2);
|
|
2160
|
-
} catch (error) {
|
|
2161
|
-
console.error("❌ Failed to compare prompts:", error.message);
|
|
2162
|
-
process.exit(1);
|
|
2163
|
-
}
|
|
2164
|
-
}),
|
|
2165
|
-
)
|
|
2166
|
-
.addCommand(
|
|
2167
|
-
new Command("resolve-key")
|
|
2168
|
-
.description(
|
|
2169
|
-
"Resolve the prompt key for a given CID within a SubDAO registry",
|
|
2170
|
-
)
|
|
2171
|
-
.requiredOption("-s, --subdao <address>", "SubDAO address")
|
|
2172
|
-
.requiredOption("-c, --cid <cid>", "Prompt CID to resolve")
|
|
2173
|
-
.option("--from <block>", "Start block (default: last 100000)", "")
|
|
2174
|
-
.option(
|
|
2175
|
-
"--window <n>",
|
|
2176
|
-
"Block window size (max 50000; default 20000)",
|
|
2177
|
-
"20000",
|
|
2178
|
-
)
|
|
2179
|
-
.option("--json", "Output JSON", false)
|
|
2180
|
-
.action(async (options) => {
|
|
2181
|
-
try {
|
|
2182
|
-
const { ethers } = require("ethers");
|
|
2183
|
-
const WalletManager = require("@sage-protocol/wallet-manager");
|
|
2184
|
-
const wm = new WalletManager();
|
|
2185
|
-
await wm.connect();
|
|
2186
|
-
const provider = wm.getProvider();
|
|
2187
|
-
const subdao = ethers.getAddress(options.subdao);
|
|
2188
|
-
const targetCid = String(options.cid).trim();
|
|
2189
|
-
|
|
2190
|
-
// Resolve registry from SubDAO
|
|
2191
|
-
const SubDAOABI = resolveArtifact(
|
|
2192
|
-
"contracts/SubDAO.sol/SubDAO.json",
|
|
2193
|
-
).abi;
|
|
2194
|
-
const subDAO = new ethers.Contract(subdao, SubDAOABI, provider);
|
|
2195
|
-
const registryAddr = await subDAO.promptRegistry();
|
|
2196
|
-
if (!registryAddr || registryAddr === ethers.ZeroAddress)
|
|
2197
|
-
throw new Error("SubDAO has no PromptRegistry configured");
|
|
2198
|
-
|
|
2199
|
-
const PromptRegistryABI = resolveArtifact(
|
|
2200
|
-
"contracts/PromptRegistry.sol/PromptRegistry.json",
|
|
2201
|
-
).abi;
|
|
2202
|
-
const iface = new ethers.Interface(PromptRegistryABI);
|
|
2203
|
-
|
|
2204
|
-
// Determine scan range
|
|
2205
|
-
const latest = await provider.getBlockNumber();
|
|
2206
|
-
const fromBlock = options.from
|
|
2207
|
-
? String(options.from).startsWith("0x")
|
|
2208
|
-
? Number(options.from)
|
|
2209
|
-
: Number(options.from)
|
|
2210
|
-
: Math.max(0, latest - 100000);
|
|
2211
|
-
let window = parseInt(String(options.window || "20000"), 10);
|
|
2212
|
-
if (!Number.isFinite(window) || window <= 0) window = 20000;
|
|
2213
|
-
if (window > 50000) window = 50000;
|
|
2214
|
-
|
|
2215
|
-
let found = null;
|
|
2216
|
-
let start = fromBlock;
|
|
2217
|
-
while (start <= latest) {
|
|
2218
|
-
const end = Math.min(start + window - 1, latest);
|
|
2219
|
-
let logs;
|
|
2220
|
-
try {
|
|
2221
|
-
logs = await provider.getLogs({
|
|
2222
|
-
address: registryAddr,
|
|
2223
|
-
fromBlock: start,
|
|
2224
|
-
toBlock: end,
|
|
2225
|
-
});
|
|
2226
|
-
} catch (err) {
|
|
2227
|
-
const msg = String(err?.message || "");
|
|
2228
|
-
if (
|
|
2229
|
-
(msg.includes("exceed maximum block range") ||
|
|
2230
|
-
msg.includes("block range")) &&
|
|
2231
|
-
window > 2000
|
|
2232
|
-
) {
|
|
2233
|
-
window = Math.max(2000, Math.floor(window / 2));
|
|
2234
|
-
continue;
|
|
2235
|
-
}
|
|
2236
|
-
throw err;
|
|
2237
|
-
}
|
|
2238
|
-
// Walk newest-first within the chunk
|
|
2239
|
-
for (let i = logs.length - 1; i >= 0; i--) {
|
|
2240
|
-
let parsed;
|
|
2241
|
-
try {
|
|
2242
|
-
parsed = iface.parseLog(logs[i]);
|
|
2243
|
-
} catch {
|
|
2244
|
-
continue;
|
|
2245
|
-
}
|
|
2246
|
-
if (parsed && parsed.name === "PromptUpdated") {
|
|
2247
|
-
const key = parsed.args[0];
|
|
2248
|
-
const cid = parsed.args[1];
|
|
2249
|
-
if (String(cid) === targetCid) {
|
|
2250
|
-
const block = await provider
|
|
2251
|
-
.getBlock(logs[i].blockNumber)
|
|
2252
|
-
.catch(() => null);
|
|
2253
|
-
found = {
|
|
2254
|
-
subdao,
|
|
2255
|
-
registry: registryAddr,
|
|
2256
|
-
key,
|
|
2257
|
-
cid,
|
|
2258
|
-
blockNumber: logs[i].blockNumber,
|
|
2259
|
-
tx: logs[i].transactionHash,
|
|
2260
|
-
timestamp: block ? Number(block.timestamp) : null,
|
|
2261
|
-
};
|
|
2262
|
-
break;
|
|
2263
|
-
}
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
if (found) break;
|
|
2267
|
-
start = end + 1;
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
if (options.json) {
|
|
2271
|
-
console.log(
|
|
2272
|
-
JSON.stringify(
|
|
2273
|
-
found || {
|
|
2274
|
-
subdao,
|
|
2275
|
-
registry: registryAddr,
|
|
2276
|
-
key: null,
|
|
2277
|
-
cid: targetCid,
|
|
2278
|
-
blockNumber: null,
|
|
2279
|
-
tx: null,
|
|
2280
|
-
timestamp: null,
|
|
2281
|
-
},
|
|
2282
|
-
null,
|
|
2283
|
-
2,
|
|
2284
|
-
),
|
|
2285
|
-
);
|
|
2286
|
-
} else {
|
|
2287
|
-
if (found) {
|
|
2288
|
-
console.log(`SubDAO : ${found.subdao}`);
|
|
2289
|
-
console.log(`Registry : ${found.registry}`);
|
|
2290
|
-
console.log(`Key : ${found.key}`);
|
|
2291
|
-
console.log(`CID : ${found.cid}`);
|
|
2292
|
-
console.log(`Block : ${found.blockNumber}`);
|
|
2293
|
-
if (found.timestamp)
|
|
2294
|
-
console.log(
|
|
2295
|
-
`Timestamp : ${new Date(found.timestamp * 1000).toISOString()}`,
|
|
2296
|
-
);
|
|
2297
|
-
console.log(`Tx : ${found.tx}`);
|
|
2298
|
-
} else {
|
|
2299
|
-
console.log("No key found for CID in the recent scan window.");
|
|
2300
|
-
console.log(`SubDAO : ${subdao}`);
|
|
2301
|
-
console.log(`Registry : ${registryAddr}`);
|
|
2302
|
-
console.log(`CID : ${targetCid}`);
|
|
2303
|
-
console.log(
|
|
2304
|
-
"Tip: pass a larger --from block (earlier) to widen the scan.",
|
|
2305
|
-
);
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
} catch (error) {
|
|
2309
|
-
console.error("❌ resolve-key failed:", error.message);
|
|
2310
|
-
process.exit(1);
|
|
2311
|
-
}
|
|
2312
|
-
}),
|
|
2313
|
-
)
|
|
2314
|
-
.addCommand(
|
|
2315
|
-
new Command("validate")
|
|
2316
|
-
.description("Validate prompt content for quality")
|
|
2317
|
-
.argument("<file>", "Path to prompt file")
|
|
2318
|
-
.action(async (file) => {
|
|
2319
|
-
try {
|
|
2320
|
-
const content = fs.readFileSync(file, "utf8");
|
|
2321
|
-
const manager = new PromptManager();
|
|
2322
|
-
await manager.initialize();
|
|
2323
|
-
await manager.validatePrompt(content);
|
|
2324
|
-
} catch (error) {
|
|
2325
|
-
console.error("❌ Failed to validate prompt:", error.message);
|
|
2326
|
-
process.exit(1);
|
|
2327
|
-
}
|
|
2328
|
-
}),
|
|
2329
|
-
)
|
|
2330
|
-
.addCommand(
|
|
2331
|
-
new Command("fork")
|
|
2332
|
-
.description("Fork a prompt to create a new version")
|
|
2333
|
-
.requiredOption("-s, --subdao <address>", "SubDAO address")
|
|
2334
|
-
.requiredOption("-o, --old-cid <cid>", "Old prompt IPFS CID")
|
|
2335
|
-
.requiredOption("-n, --new-cid <cid>", "New prompt IPFS CID")
|
|
2336
|
-
.requiredOption(
|
|
2337
|
-
"-d, --description <description>",
|
|
2338
|
-
"New prompt description",
|
|
2339
|
-
)
|
|
2340
|
-
.option(
|
|
2341
|
-
"--name <name>",
|
|
2342
|
-
"New prompt name (defaults to derived from CID)",
|
|
2343
|
-
)
|
|
2344
|
-
.option(
|
|
2345
|
-
"--fee",
|
|
2346
|
-
"Pay stable fee instead of SXXX burn (requires factory stable prompt fork config)",
|
|
2347
|
-
false,
|
|
2348
|
-
)
|
|
2349
|
-
.option(
|
|
2350
|
-
"--permit [json]",
|
|
2351
|
-
"Optional EIP-2612 permit payload (JSON) for stable fee fork",
|
|
2352
|
-
)
|
|
2353
|
-
.action(async (options) => {
|
|
2354
|
-
try {
|
|
2355
|
-
const manager = new PromptManager();
|
|
2356
|
-
await manager.initialize();
|
|
2357
|
-
await manager.forkPrompt(
|
|
2358
|
-
options.subdao,
|
|
2359
|
-
options.oldCid,
|
|
2360
|
-
options.newCid,
|
|
2361
|
-
options.description,
|
|
2362
|
-
{
|
|
2363
|
-
name: options.name,
|
|
2364
|
-
fee: !!options.fee,
|
|
2365
|
-
permit: options.permit || null,
|
|
2366
|
-
},
|
|
2367
|
-
);
|
|
2368
|
-
} catch (error) {
|
|
2369
|
-
console.error("❌ Failed to fork prompt:", error.message);
|
|
2370
|
-
process.exit(1);
|
|
2371
|
-
}
|
|
2372
|
-
}),
|
|
2373
|
-
)
|
|
2374
|
-
.addCommand(
|
|
2375
|
-
new Command("get-prompt")
|
|
2376
|
-
.description("Get prompts from a SubDAO (alias for list)")
|
|
2377
|
-
.requiredOption("-s, --subdao <address>", "SubDAO address")
|
|
2378
|
-
.action(async (options) => {
|
|
2379
|
-
try {
|
|
2380
|
-
const manager = new PromptManager();
|
|
2381
|
-
await manager.initialize();
|
|
2382
|
-
await manager.listPrompts(options.subdao);
|
|
2383
|
-
} catch (error) {
|
|
2384
|
-
console.error("❌ Failed to get prompts:", error.message);
|
|
2385
|
-
process.exit(1);
|
|
2386
|
-
}
|
|
2387
|
-
}),
|
|
2388
|
-
)
|
|
2389
|
-
.addCommand(
|
|
2390
|
-
new Command("onchain-list")
|
|
2391
|
-
.description("List prompts from on-chain registry events")
|
|
2392
|
-
.requiredOption("-s, --subdao <address>", "SubDAO address")
|
|
2393
|
-
.option(
|
|
2394
|
-
"--from <block>",
|
|
2395
|
-
"From block number (hex or decimal) to start scanning",
|
|
2396
|
-
)
|
|
2397
|
-
.option("--rpc <url>", "Override RPC URL for this call")
|
|
2398
|
-
.action(async (options) => {
|
|
2399
|
-
try {
|
|
2400
|
-
if (options.rpc) process.env.RPC_URL = options.rpc;
|
|
2401
|
-
const manager = new PromptManager();
|
|
2402
|
-
await manager.initialize();
|
|
2403
|
-
await manager.listOnchainPrompts(
|
|
2404
|
-
options.subdao,
|
|
2405
|
-
options.from || null,
|
|
2406
|
-
);
|
|
2407
|
-
} catch (error) {
|
|
2408
|
-
console.error("❌ Failed to list on-chain prompts:", error.message);
|
|
2409
|
-
process.exit(1);
|
|
2410
|
-
}
|
|
2411
|
-
}),
|
|
2412
|
-
)
|
|
2413
|
-
.addCommand(promptTestCommand);
|
|
2414
|
-
|
|
2415
|
-
program.addCommand(promptCommand);
|
|
2416
|
-
|
|
2417
|
-
// Alias for get-prompt
|
|
2418
|
-
const getPromptCommand = new Command("get-prompt")
|
|
2419
|
-
.description("Get prompts from a SubDAO (alias for prompt list)")
|
|
2420
|
-
.requiredOption("-s, --subdao <address>", "SubDAO address")
|
|
2421
|
-
.action(async (options) => {
|
|
2422
|
-
try {
|
|
2423
|
-
const manager = new PromptManager();
|
|
2424
|
-
await manager.initialize();
|
|
2425
|
-
await manager.listPrompts(options.subdao);
|
|
2426
|
-
} catch (error) {
|
|
2427
|
-
console.error("❌ Failed to get prompts:", error.message);
|
|
2428
|
-
process.exit(1);
|
|
2429
|
-
}
|
|
2430
|
-
});
|
|
2431
|
-
program.addCommand(getPromptCommand);
|
|
2432
|
-
|
|
2433
|
-
// Alias for prompt-new
|
|
2434
|
-
const promptNewCommand = new Command("prompt-new")
|
|
2435
|
-
.description(
|
|
2436
|
-
"Scaffold a system prompt template into prompts/ (alias: prompt new)",
|
|
2437
|
-
)
|
|
2438
|
-
.option(
|
|
2439
|
-
"-o, --out <file>",
|
|
2440
|
-
"Output file path under prompts/",
|
|
2441
|
-
"prompts/system-template.txt",
|
|
2442
|
-
)
|
|
2443
|
-
.option("--manifest <path>", "Also add to a manifest.json at path")
|
|
2444
|
-
.option(
|
|
2445
|
-
"--key <key>",
|
|
2446
|
-
"Manifest key (e.g. folder/name) when adding to manifest",
|
|
2447
|
-
)
|
|
2448
|
-
.option("--upload", "Upload created file to IPFS and print CID")
|
|
2449
|
-
.option(
|
|
2450
|
-
"--push",
|
|
2451
|
-
"If --manifest is set, also push the manifest to IPFS and print CID",
|
|
2452
|
-
)
|
|
2453
|
-
.action(async (options) => {
|
|
2454
|
-
try {
|
|
2455
|
-
const outRel = options.out || "prompts/system-template.txt";
|
|
2456
|
-
const out = path.isAbsolute(outRel)
|
|
2457
|
-
? outRel
|
|
2458
|
-
: path.join(process.cwd(), outRel);
|
|
2459
|
-
const dir = path.dirname(out);
|
|
2460
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
2461
|
-
const content = [
|
|
2462
|
-
"You are a helpful, precise assistant. Follow the user's instructions carefully.",
|
|
2463
|
-
"Ask for clarification when needed. Avoid revealing chain-of-thought;",
|
|
2464
|
-
"provide concise final answers.",
|
|
2465
|
-
].join("\n");
|
|
2466
|
-
fs.writeFileSync(out, content, { flag: "wx" });
|
|
2467
|
-
console.log(`✅ Created ${out}`);
|
|
2468
|
-
console.log(
|
|
2469
|
-
"Next: add this file to your manifest and run `sage project validate` then `sage project push`/`propose`.",
|
|
2470
|
-
);
|
|
2471
|
-
|
|
2472
|
-
let createdCid = null;
|
|
2473
|
-
if (options.upload) {
|
|
2474
|
-
const IPFSManager = require("../ipfs-manager");
|
|
2475
|
-
const ipfs = new IPFSManager();
|
|
2476
|
-
await ipfs.initialize();
|
|
2477
|
-
createdCid = await ipfs.uploadPrompt(
|
|
2478
|
-
out,
|
|
2479
|
-
path.basename(out),
|
|
2480
|
-
"Scaffolded by sage prompt-new",
|
|
2481
|
-
);
|
|
2482
|
-
console.log(`🔗 CID: ${createdCid}`);
|
|
2483
|
-
}
|
|
2484
|
-
|
|
2485
|
-
if (options.manifest) {
|
|
2486
|
-
try {
|
|
2487
|
-
const manifestPath = path.isAbsolute(options.manifest)
|
|
2488
|
-
? options.manifest
|
|
2489
|
-
: path.join(process.cwd(), options.manifest);
|
|
2490
|
-
const raw = fs.readFileSync(manifestPath, "utf8");
|
|
2491
|
-
const manifest = JSON.parse(raw);
|
|
2492
|
-
manifest.prompts = Array.isArray(manifest.prompts)
|
|
2493
|
-
? manifest.prompts
|
|
2494
|
-
: [];
|
|
2495
|
-
const key = options.key || path.basename(out, path.extname(out));
|
|
2496
|
-
const entry = {
|
|
2497
|
-
key,
|
|
2498
|
-
name: key,
|
|
2499
|
-
files: [path.relative(path.dirname(manifestPath), out)],
|
|
2500
|
-
};
|
|
2501
|
-
if (createdCid) entry.cid = createdCid;
|
|
2502
|
-
manifest.prompts.push(entry);
|
|
2503
|
-
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
2504
|
-
console.log(`📝 Added prompt entry '${key}' to ${manifestPath}`);
|
|
2505
|
-
if (options.push) {
|
|
2506
|
-
const LibraryManager = require("../library-manager");
|
|
2507
|
-
const lm = new LibraryManager();
|
|
2508
|
-
lm.loadManifest(manifestPath);
|
|
2509
|
-
const mcid = await lm.push();
|
|
2510
|
-
console.log(`📦 Manifest pushed. CID: ${mcid}`);
|
|
2511
|
-
}
|
|
2512
|
-
} catch (e) {
|
|
2513
|
-
console.error("❌ Failed to update/push manifest:", e.message);
|
|
2514
|
-
process.exit(1);
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
} catch (err) {
|
|
2518
|
-
if (err && err.code === "EEXIST") {
|
|
2519
|
-
console.log(
|
|
2520
|
-
"ℹ️ File already exists; choose a different --out path.",
|
|
2521
|
-
);
|
|
2522
|
-
} else {
|
|
2523
|
-
console.error("❌ Failed to create prompt template:", err.message);
|
|
2524
|
-
process.exit(1);
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
});
|
|
2528
|
-
program.addCommand(promptNewCommand);
|
|
2529
|
-
}
|
|
2530
|
-
|
|
2531
|
-
module.exports = { register, resolveWorkerSettings };
|