@sage-protocol/cli 0.2.9 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cli/commands/config.js +28 -0
  2. package/dist/cli/commands/doctor.js +87 -8
  3. package/dist/cli/commands/gov-config.js +81 -0
  4. package/dist/cli/commands/governance.js +152 -72
  5. package/dist/cli/commands/library.js +9 -0
  6. package/dist/cli/commands/proposals.js +187 -17
  7. package/dist/cli/commands/skills.js +737 -0
  8. package/dist/cli/commands/subdao.js +96 -132
  9. package/dist/cli/config/playbooks.json +62 -0
  10. package/dist/cli/config.js +15 -0
  11. package/dist/cli/governance-manager.js +25 -4
  12. package/dist/cli/index.js +6 -7
  13. package/dist/cli/library-manager.js +79 -0
  14. package/dist/cli/mcp-server-stdio.js +1387 -166
  15. package/dist/cli/schemas/manifest.schema.json +55 -0
  16. package/dist/cli/services/doctor/fixers.js +134 -0
  17. package/dist/cli/services/governance/doctor.js +140 -0
  18. package/dist/cli/services/governance/playbooks.js +97 -0
  19. package/dist/cli/services/mcp/bulk-operations.js +272 -0
  20. package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
  21. package/dist/cli/services/mcp/library-listing.js +2 -2
  22. package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
  23. package/dist/cli/services/mcp/manifest-downloader.js +5 -3
  24. package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
  25. package/dist/cli/services/mcp/manifest-workflows.js +127 -15
  26. package/dist/cli/services/mcp/quick-start.js +287 -0
  27. package/dist/cli/services/mcp/stdio-runner.js +30 -5
  28. package/dist/cli/services/mcp/template-manager.js +156 -0
  29. package/dist/cli/services/mcp/templates/default-templates.json +84 -0
  30. package/dist/cli/services/mcp/tool-args-validator.js +56 -0
  31. package/dist/cli/services/mcp/trending-formatter.js +1 -1
  32. package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
  33. package/dist/cli/services/metaprompt/designer.js +12 -5
  34. package/dist/cli/services/skills/discovery.js +99 -0
  35. package/dist/cli/services/subdao/applier.js +229 -0
  36. package/dist/cli/services/subdao/planner.js +142 -0
  37. package/dist/cli/subdao-manager.js +14 -0
  38. package/dist/cli/utils/aliases.js +28 -6
  39. package/dist/cli/utils/contract-error-decoder.js +61 -0
  40. package/dist/cli/utils/suggestions.js +25 -13
  41. package/package.json +3 -2
  42. package/src/schemas/manifest.schema.json +55 -0
@@ -0,0 +1,202 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { LibraryManager } = require('../../library-manager');
4
+
5
+ function createDependencyAnalyzer({
6
+ libraryManager = new LibraryManager(),
7
+ fsModule = fs,
8
+ pathModule = path,
9
+ logger = console,
10
+ } = {}) {
11
+ function resolvePinnedLibrary(libraryId) {
12
+ const all = libraryManager.listPinned() || [];
13
+ const id = String(libraryId || '').trim();
14
+ if (!id) {
15
+ throw new Error('library parameter is required');
16
+ }
17
+
18
+ const byCid = all.find((row) => row.cid === id);
19
+ if (byCid) {
20
+ const { manifest, path: manifestPath } = libraryManager.loadPinned(byCid.cid);
21
+ return { row: byCid, manifest, manifestPath };
22
+ }
23
+
24
+ const lower = id.toLowerCase();
25
+ const matches = all
26
+ .map((row) => {
27
+ try {
28
+ const { manifest, path: manifestPath } = libraryManager.loadPinned(row.cid);
29
+ const libName = String(manifest?.library?.name || row.name || '').toLowerCase();
30
+ return { row, manifest, manifestPath, libName };
31
+ } catch (e) {
32
+ logger?.warn?.('dep_analyzer_manifest_load_failed', { cid: row.cid, err: e.message || String(e) });
33
+ return null;
34
+ }
35
+ })
36
+ .filter(Boolean)
37
+ .filter((entry) => entry.libName === lower);
38
+
39
+ if (matches.length === 0) {
40
+ throw new Error(`No pinned library found matching "${libraryId}". Try using the CID from ~/.sage/libraries/*.json.`);
41
+ }
42
+ if (matches.length > 1) {
43
+ const cids = matches.map((m) => `${m.row.cid} (${m.manifest?.library?.name || m.row.name || 'unnamed'})`);
44
+ throw new Error(
45
+ `Ambiguous library match for "${libraryId}". Candidates:\n- ${cids.join('\n- ')}\nPass an explicit CID to disambiguate.`,
46
+ );
47
+ }
48
+
49
+ return matches[0];
50
+ }
51
+
52
+ function extractVariables(content) {
53
+ const text = String(content || '');
54
+ const pattern = /\$\{([A-Za-z0-9_]+)\}/g;
55
+ const vars = [];
56
+ let match;
57
+ while ((match = pattern.exec(text)) !== null) {
58
+ if (!vars.includes(match[1])) {
59
+ vars.push(match[1]);
60
+ }
61
+ }
62
+ return vars;
63
+ }
64
+
65
+ function groupInconsistencies(variableUsage) {
66
+ const names = Object.keys(variableUsage || {});
67
+ const lowerMap = new Map();
68
+ for (const name of names) {
69
+ const base = name.toLowerCase();
70
+ if (!lowerMap.has(base)) lowerMap.set(base, []);
71
+ lowerMap.get(base).push(name);
72
+ }
73
+
74
+ const groups = [];
75
+ for (const [base, variants] of lowerMap.entries()) {
76
+ if (variants.length <= 1) continue;
77
+ groups.push({
78
+ base,
79
+ variants,
80
+ totalPrompts: variants.reduce((acc, v) => acc + (variableUsage[v]?.count || 0), 0),
81
+ });
82
+ }
83
+ return groups;
84
+ }
85
+
86
+ async function analyzeDependencies({ library, analysis_type = 'variables' } = {}) {
87
+ const { row, manifest, manifestPath } = resolvePinnedLibrary(library);
88
+ const prompts = Array.isArray(manifest.prompts) ? manifest.prompts : [];
89
+ const libDir = pathModule.dirname(manifestPath);
90
+
91
+ const perPrompt = [];
92
+ const variableUsage = {};
93
+
94
+ for (const prompt of prompts) {
95
+ if (!prompt || typeof prompt !== 'object') continue;
96
+ const key = prompt.key || '(no-key)';
97
+ let content = '';
98
+
99
+ if (Array.isArray(prompt.files) && prompt.files.length) {
100
+ const firstFile = prompt.files[0];
101
+ const candidatePath = pathModule.isAbsolute(firstFile)
102
+ ? firstFile
103
+ : pathModule.join(libDir, firstFile);
104
+ try {
105
+ content = fsModule.readFileSync(candidatePath, 'utf8');
106
+ } catch (error) {
107
+ logger?.debug?.('dep_analyzer_read_failed', { file: candidatePath, err: error.message || String(error) });
108
+ }
109
+ }
110
+
111
+ const vars = extractVariables(content);
112
+ perPrompt.push({
113
+ key,
114
+ name: prompt.name || key,
115
+ variables: vars,
116
+ });
117
+
118
+ for (const v of vars) {
119
+ if (!variableUsage[v]) {
120
+ variableUsage[v] = { count: 0, prompts: [] };
121
+ }
122
+ variableUsage[v].count += 1;
123
+ variableUsage[v].prompts.push(key);
124
+ }
125
+ }
126
+
127
+ const sharedVariables = Object.keys(variableUsage).filter((v) => variableUsage[v].count > 1);
128
+ const uniqueVariables = Object.keys(variableUsage).filter((v) => variableUsage[v].count === 1);
129
+ const inconsistencyGroups = groupInconsistencies(variableUsage);
130
+
131
+ const lines = [];
132
+ lines.push(`📊 Variable Usage Analysis for "${manifest.library?.name || row.name || library}"`);
133
+ lines.push('');
134
+ if (!perPrompt.length) {
135
+ lines.push('No prompts found in this library.');
136
+ } else {
137
+ lines.push(`Prompts analyzed: ${perPrompt.length}`);
138
+ lines.push(`Total distinct variables: ${Object.keys(variableUsage).length}`);
139
+ lines.push('');
140
+ if (sharedVariables.length) {
141
+ lines.push('Shared variables (used in multiple prompts):');
142
+ sharedVariables.slice(0, 10).forEach((v) => {
143
+ lines.push(`- ${v} (${variableUsage[v].count} prompts)`);
144
+ });
145
+ if (sharedVariables.length > 10) {
146
+ lines.push(`… and ${sharedVariables.length - 10} more`);
147
+ }
148
+ lines.push('');
149
+ } else {
150
+ lines.push('No shared variables detected across prompts.');
151
+ lines.push('');
152
+ }
153
+
154
+ if (inconsistencyGroups.length) {
155
+ lines.push('Potential naming inconsistency groups:');
156
+ inconsistencyGroups.slice(0, 5).forEach((g) => {
157
+ lines.push(`- ${g.base}: ${g.variants.join(', ')} (approx. ${g.totalPrompts} prompt usages)`);
158
+ });
159
+ if (inconsistencyGroups.length > 5) {
160
+ lines.push(`… and ${inconsistencyGroups.length - 5} more groups`);
161
+ }
162
+ lines.push('');
163
+ }
164
+ }
165
+
166
+ return {
167
+ content: [
168
+ { type: 'text', text: lines.join('\n') },
169
+ {
170
+ type: 'text',
171
+ text:
172
+ '```json\n' +
173
+ JSON.stringify(
174
+ {
175
+ library: {
176
+ cid: row.cid,
177
+ name: manifest.library?.name || row.name || library,
178
+ },
179
+ perPrompt,
180
+ variableUsage,
181
+ sharedVariables,
182
+ uniqueVariables,
183
+ inconsistencyGroups,
184
+ },
185
+ null,
186
+ 2,
187
+ ) +
188
+ '\n```',
189
+ },
190
+ ],
191
+ };
192
+ }
193
+
194
+ return {
195
+ analyzeDependencies,
196
+ };
197
+ }
198
+
199
+ module.exports = {
200
+ createDependencyAnalyzer,
201
+ };
202
+
@@ -49,7 +49,7 @@ function createLibraryLister({
49
49
  return {
50
50
  content: [
51
51
  { type: 'text', text: formatted },
52
- { type: 'json', text: JSON.stringify(data ?? {}, null, DEFAULT_INDENT) },
52
+ { type: 'text', text: '```json\n' + JSON.stringify(data ?? {}, null, DEFAULT_INDENT) + '\n```' },
53
53
  ],
54
54
  };
55
55
  }
@@ -64,7 +64,7 @@ function createLibraryLister({
64
64
  return {
65
65
  content: [
66
66
  { type: 'text', text: formatted },
67
- { type: 'json', text: JSON.stringify(data ?? {}, null, DEFAULT_INDENT) },
67
+ { type: 'text', text: '```json\n' + JSON.stringify(data ?? {}, null, DEFAULT_INDENT) + '\n```' },
68
68
  ],
69
69
  };
70
70
  } catch (error) {
@@ -96,6 +96,7 @@ function collectLocalPrompts({
96
96
  if (normalizedQuery) {
97
97
  const matchesQuery =
98
98
  promptName.toLowerCase().includes(normalizedQuery) ||
99
+ (prompt.key && prompt.key.toLowerCase().includes(normalizedQuery)) ||
99
100
  promptDescription.toLowerCase().includes(normalizedQuery) ||
100
101
  loweredPromptTags.some((tag) => tag.includes(normalizedQuery));
101
102
  if (!matchesQuery) continue; // eslint-disable-line no-continue
@@ -1,18 +1,20 @@
1
1
  const DEFAULT_GATEWAYS = [
2
+ 'https://cloudflare-ipfs.com',
3
+ 'https://gateway.pinata.cloud',
4
+ 'https://ipfs.io',
2
5
  'https://dweb.link',
3
6
  'https://nftstorage.link',
4
- 'https://ipfs.io',
5
7
  ];
6
8
 
7
9
  function buildGatewayList({ cid, preferredGateway }) {
8
10
  const trimmedPreferred = preferredGateway ? preferredGateway.replace(/\/$/, '') : '';
9
11
  const candidates = [];
10
12
  if (trimmedPreferred) {
11
- candidates.push(`${trimmedPreferred}/ipfs/${cid}`);
13
+ candidates.push(`${trimmedPreferred} /ipfs/${cid} `);
12
14
  }
13
15
  for (const base of DEFAULT_GATEWAYS) {
14
16
  const formattedBase = base.replace(/\/$/, '');
15
- const url = `${formattedBase}/ipfs/${cid}`;
17
+ const url = `${formattedBase} /ipfs/${cid} `;
16
18
  if (!candidates.includes(url)) {
17
19
  candidates.push(url);
18
20
  }
@@ -22,6 +22,20 @@ function createManifestFetcher({
22
22
  }
23
23
 
24
24
  async function downloadManifest(manifestCid) {
25
+ // Check if this is a local manifest (stored in ~/.sage/libraries/)
26
+ if (manifestCid && manifestCid.startsWith('local_')) {
27
+ try {
28
+ const { loadPinnedManifest } = require('../library/local-cache');
29
+ const cliConfig = require('../../utils/cli-config');
30
+ const { manifest } = loadPinnedManifest({ config: cliConfig, cid: manifestCid });
31
+ return manifest;
32
+ } catch (error) {
33
+ logger?.debug?.('local_manifest_load_failed', { cid: manifestCid, err: error.message || String(error) });
34
+ throw new Error(`Failed to load local manifest ${manifestCid}: ${error.message}`);
35
+ }
36
+ }
37
+
38
+ // For IPFS CIDs, try gateways
25
39
  const gateways = buildGatewayList(manifestCid);
26
40
  let lastError;
27
41
  for (const gateway of gateways) {
@@ -84,7 +98,9 @@ function createManifestFetcher({
84
98
  let content = '';
85
99
  if (includeContent && prompt?.cid && ipfsManager) {
86
100
  try {
87
- content = await ipfsManager.downloadPrompt(prompt.cid);
101
+ // Download prompt JSON directly from IPFS and extract content
102
+ const data = await ipfsManager.downloadJson(prompt.cid);
103
+ content = data?.content || data?.prompt?.content || JSON.stringify(data);
88
104
  } catch (error) {
89
105
  logger?.debug?.('prompt_content_fetch_failed', { cid: prompt.cid, err: error.message || String(error) });
90
106
  }
@@ -18,6 +18,63 @@ function defaultAddFormats(ajv) {
18
18
  }
19
19
  }
20
20
 
21
+ // Lightweight embedded fallback schema for environments where docs/schemas are not present
22
+ const FALLBACK_MANIFEST_SCHEMA = {
23
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
24
+ $id: 'https://sage-protocol.org/schemas/manifest.schema.json',
25
+ title: 'Sage Library Manifest',
26
+ type: 'object',
27
+ required: ['version', 'library', 'prompts'],
28
+ additionalProperties: true,
29
+ properties: {
30
+ version: {
31
+ anyOf: [
32
+ { type: 'string', const: '2.0.0' },
33
+ { type: 'integer', const: 2 },
34
+ ],
35
+ },
36
+ library: {
37
+ type: 'object',
38
+ required: ['name'],
39
+ additionalProperties: true,
40
+ properties: {
41
+ name: { type: 'string', minLength: 1 },
42
+ description: { type: 'string' },
43
+ previous: { type: 'string' },
44
+ },
45
+ },
46
+ prompts: {
47
+ type: 'array',
48
+ minItems: 0,
49
+ items: {
50
+ type: 'object',
51
+ required: ['key'],
52
+ additionalProperties: true,
53
+ properties: {
54
+ key: { type: 'string', minLength: 1 },
55
+ cid: { type: 'string', minLength: 46 },
56
+ name: { type: 'string' },
57
+ description: { type: 'string' },
58
+ tags: {
59
+ type: 'array',
60
+ items: { type: 'string', minLength: 1 },
61
+ },
62
+ files: {
63
+ type: 'array',
64
+ items: { type: 'string', minLength: 1 },
65
+ minItems: 0,
66
+ },
67
+ },
68
+ },
69
+ },
70
+ compositions: { type: 'object' },
71
+ dependencies: {
72
+ type: 'array',
73
+ items: { type: 'string' },
74
+ },
75
+ },
76
+ };
77
+
21
78
  function createManifestWorkflows({
22
79
  provider,
23
80
  addressResolution,
@@ -27,8 +84,11 @@ function createManifestWorkflows({
27
84
  logger = console,
28
85
  processEnv = process.env,
29
86
  schemaPaths = [
87
+ // Package-relative path (for published CLI)
88
+ path.join(__dirname, '..', '..', 'schemas', 'manifest.schema.json'),
89
+ // Monorepo docs locations (dev)
30
90
  path.join(process.cwd(), 'docs', 'schemas', 'manifest.schema.json'),
31
- path.join(__dirname, '..', '..', '..', 'docs', 'schemas', 'manifest.schema.json'),
91
+ path.join(__dirname, '..', '..', '..', '..', 'docs', 'schemas', 'manifest.schema.json'),
32
92
  ],
33
93
  fsModule = fs,
34
94
  pathModule = path,
@@ -53,7 +113,9 @@ function createManifestWorkflows({
53
113
  logger?.warn?.('manifest_schema_load_failed', { path: schemaPath, err: error.message || String(error) });
54
114
  }
55
115
  }
56
- throw new Error('Manifest schema not found (docs/schemas/manifest.schema.json)');
116
+ // Fallback: embedded schema so MCP/CLI can still validate outside the monorepo.
117
+ logger?.warn?.('manifest_schema_fallback_used');
118
+ return FALLBACK_MANIFEST_SCHEMA;
57
119
  }
58
120
 
59
121
  async function validateManifest({ manifest }) {
@@ -67,8 +129,23 @@ function createManifestWorkflows({
67
129
  if (ok) {
68
130
  return { content: [{ type: 'text', text: '✅ Manifest is valid' }] };
69
131
  }
70
- const lines = (validate.errors || []).map((err) => `- ${err.instancePath || '/'} ${err.message || 'invalid'}`).join('\n');
71
- return { content: [{ type: 'text', text: `❌ Manifest validation failed:\n${lines}` }] };
132
+ const errors = validate.errors || [];
133
+ const lines = errors.map((err) => `- ${err.instancePath || '/'} ${err.message || 'invalid'}`).join('\n');
134
+
135
+ // Add targeted hints for common pitfalls (version, library)
136
+ const hasVersionError = errors.some((e) => (e.instancePath === '/version'));
137
+ const hasLibraryError = errors.some((e) => e.instancePath === '' && /library/.test(e.message || ''));
138
+
139
+ let hint = '';
140
+ if (hasVersionError) {
141
+ hint += '\n\nHint: use "version": 2 or "version": "2.0.0" for v2 manifests.';
142
+ }
143
+ if (hasLibraryError || !manifest?.library) {
144
+ hint += '\nHint: manifest must include a top-level "library" object, e.g.:\n' +
145
+ '{\n "version": 2,\n "library": { "name": "My Library", "description": "..." },\n "prompts": []\n}';
146
+ }
147
+
148
+ return { content: [{ type: 'text', text: `❌ Manifest validation failed:\n${lines}${hint}` }] };
72
149
  } catch (error) {
73
150
  return { content: [{ type: 'text', text: `Error validating manifest: ${error.message}` }] };
74
151
  }
@@ -81,7 +158,15 @@ function createManifestWorkflows({
81
158
  const cid = await ipfs.uploadJson(manifest, name);
82
159
  return { content: [{ type: 'text', text: `✅ Manifest uploaded to IPFS\nCID: ${cid}` }], cid };
83
160
  } catch (error) {
84
- return { content: [{ type: 'text', text: `Error uploading manifest: ${error.message}` }] };
161
+ const msg = error?.message || String(error);
162
+ let help = msg;
163
+ if (/All IPFS providers failed/i.test(msg)) {
164
+ help = 'All IPFS providers failed. Ensure your IPFS settings are configured.\n' +
165
+ '- For worker: set SAGE_IPFS_WORKER_URL (and SAGE_IPFS_UPLOAD_TOKEN if required)\n' +
166
+ '- For Pinata: set PINATA_JWT or PINATA_API_KEY/SECRET\n' +
167
+ '- Or switch provider via SAGE_IPFS_PROVIDER=pinata|worker';
168
+ }
169
+ return { content: [{ type: 'text', text: `Error uploading manifest: ${help}` }] };
85
170
  }
86
171
  }
87
172
 
@@ -117,7 +202,7 @@ function createManifestWorkflows({
117
202
  let mode = { operator: false, governance: 'Unknown' };
118
203
  try {
119
204
  mode = await detectGovMode({ provider, subdao, governor, timelock });
120
- } catch (_) {}
205
+ } catch (_) { }
121
206
 
122
207
  const payload = { targets, values, calldatas, description: desc };
123
208
  const hints = [];
@@ -202,7 +287,7 @@ function createManifestWorkflows({
202
287
  }
203
288
  }
204
289
 
205
- async function publishManifestFlow({ manifest, subdao = '', description = '' }) {
290
+ async function publishManifestFlow({ manifest, subdao = '', description = '', dry_run = false }) {
206
291
  try {
207
292
  const validation = await validateManifest({ manifest });
208
293
  const firstText = validation.content?.[0]?.text || '';
@@ -210,22 +295,49 @@ function createManifestWorkflows({
210
295
  return { content: [{ type: 'text', text: `❌ Validation failed. Fix issues first.\n\n${firstText}` }] };
211
296
  }
212
297
 
213
- const pushed = await pushManifestToIpfs({ manifest });
214
- const match = /CID:\s*([^\s]+)/.exec(pushed.content?.[0]?.text || '');
215
- const cid = match?.[1] || pushed.cid;
216
- if (!cid) {
217
- return { content: [{ type: 'text', text: '❌ Failed to upload manifest (no CID)' }] };
298
+ let cid = null;
299
+ let pushText = '';
300
+ if (!dry_run) {
301
+ const pushed = await pushManifestToIpfs({ manifest });
302
+ pushText = pushed.content?.[0]?.text || '';
303
+ const match = /CID:\s*([^\s]+)/.exec(pushText);
304
+ cid = match?.[1] || pushed.cid;
305
+ if (!cid) {
306
+ return { content: [{ type: 'text', text: '❌ Failed to upload manifest (no CID)' }] };
307
+ }
218
308
  }
219
309
 
220
310
  const proposed = await proposeManifest({
221
- cid,
311
+ cid: cid || '(dry-run)',
222
312
  subdao,
223
313
  description,
224
314
  promptCount: Array.isArray(manifest?.prompts) ? manifest.prompts.length : 0,
225
315
  manifest,
226
316
  });
227
- const text = `✅ Manifest valid\nCID: ${cid}\n\n${proposed.content?.[0]?.text || ''}`;
228
- return { content: [{ type: 'text', text }], cid, payload: proposed.payload };
317
+ const textLines = [];
318
+ textLines.push('✅ Manifest valid');
319
+ if (cid) textLines.push(`CID: ${cid}`);
320
+ if (dry_run) {
321
+ textLines.push('');
322
+ textLines.push('Dry run: no IPFS upload performed. Use this payload and CLI hints to publish when ready.');
323
+ textLines.push('');
324
+ textLines.push('When you are ready to publish from your terminal:');
325
+ textLines.push('1) Save this manifest JSON to a file (e.g. ./manifest.json).');
326
+ if (subdao) {
327
+ textLines.push(`2) Upload & schedule/propose via CLI:`);
328
+ textLines.push(` sage library push ./manifest.json --subdao ${subdao} --pin --wait`);
329
+ } else {
330
+ textLines.push('2) Upload & schedule/propose via CLI (replace SUBDAO):');
331
+ textLines.push(' sage library push ./manifest.json --subdao 0xYourSubDAO --pin --wait');
332
+ }
333
+ } else if (pushText) {
334
+ textLines.push('');
335
+ textLines.push(pushText);
336
+ }
337
+ textLines.push('');
338
+ textLines.push(proposed.content?.[0]?.text || '');
339
+
340
+ return { content: [{ type: 'text', text: textLines.join('\n') }], cid, payload: proposed.payload };
229
341
  } catch (error) {
230
342
  return { content: [{ type: 'text', text: `Error publishing manifest flow: ${error.message}` }] };
231
343
  }