@elizaos/prompts 2.0.0-beta.1 → 2.0.3-beta.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.
@@ -1,17 +1,8 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Generate plugin action docs spec (best-effort).
4
- *
5
- * Scans plugins/** TypeScript action definitions and emits a merged spec file:
6
- * packages/prompts/specs/actions/plugins.generated.json
7
- *
8
- * This is intentionally dependency-free and uses a small brace-aware extractor to
9
- * locate `export const X: Action = { ... }` blocks.
10
- */
11
-
12
2
  import fs from "node:fs";
13
3
  import path from "node:path";
14
4
  import { fileURLToPath } from "node:url";
5
+ import { ensureDirectory, readJson, readText } from "./file-utils.js";
15
6
 
16
7
  const __filename = fileURLToPath(import.meta.url);
17
8
  const __dirname = path.dirname(__filename);
@@ -37,7 +28,16 @@ const RETIRED_IMPLEMENTATION_ONLY_ACTIONS = new Set([
37
28
  "ASK_USER_QUESTION",
38
29
  "CHECKIN",
39
30
  "CHECK_AVAILABILITY",
31
+ "CLEAR_HISTORY",
32
+ "CREATE_PLAN",
33
+ "CREATE_PAYMENT_REQUEST",
34
+ "DESKTOP",
35
+ "DEVICE_FILE_READ",
36
+ "DEVICE_FILE_WRITE",
37
+ "DEVICE_LIST_DIR",
40
38
  "DISCORD_SETUP_CREDENTIALS",
39
+ "DOC",
40
+ "DELIVER_PAYMENT_LINK",
41
41
  "EDIT",
42
42
  "ENTER_WORKTREE",
43
43
  "EXIT_WORKTREE",
@@ -67,13 +67,14 @@ const RETIRED_IMPLEMENTATION_ONLY_ACTIONS = new Set([
67
67
  "GOOGLE_CALENDAR",
68
68
  "LIFEOPS_PAUSE",
69
69
  "NOSTR_PUBLISH_PROFILE",
70
- "PAYMENT",
71
70
  "PLACE_CALL",
72
71
  "PLAY_AUDIO",
73
72
  "PLAYBACK",
74
73
  "READ",
74
+ "READING",
75
75
  "RELEASE_BLOCK",
76
76
  "SCREEN_TIME",
77
+ "SEND_TO_ADMIN",
77
78
  "TOGGLE_FEATURE",
78
79
  "TAILSCALE",
79
80
  "START_TUNNEL",
@@ -82,6 +83,11 @@ const RETIRED_IMPLEMENTATION_ONLY_ACTIONS = new Set([
82
83
  "WRITE",
83
84
  "LS",
84
85
  "MUSIC_LIBRARY",
86
+ "MYSTICISM_PAYMENT",
87
+ "VERIFY_PAYMENT_PAYLOAD",
88
+ "SETTLE_PAYMENT",
89
+ "AWAIT_PAYMENT_CALLBACK",
90
+ "CANCEL_PAYMENT_REQUEST",
85
91
  "LIST_OVERDUE_FOLLOWUPS",
86
92
  "MARK_FOLLOWUP_DONE",
87
93
  "SET_FOLLOWUP_THRESHOLD",
@@ -99,19 +105,17 @@ const RETIRED_IMPLEMENTATION_ONLY_ACTIONS = new Set([
99
105
  "GET_LINEAR_ACTIVITY",
100
106
  "CLEAR_LINEAR_ACTIVITY",
101
107
  "SEARCH_LINEAR_ISSUES",
108
+ "BROWSER_ACTIONS",
109
+ "WALLET_ACTIONS",
110
+ "CHARACTER_ACTIONS",
111
+ "SETTINGS_ACTIONS",
112
+ "CONNECTOR_ACTIONS",
113
+ "AUTOMATION_ACTIONS",
114
+ "PHONE_ACTIONS",
115
+ "OWNER_ACTIONS",
102
116
  ]);
103
117
 
104
- function readText(filePath) {
105
- return fs.readFileSync(filePath, "utf-8");
106
- }
107
-
108
- function readJson(filePath) {
109
- return JSON.parse(readText(filePath));
110
- }
111
-
112
118
  /**
113
- * Template literals are extracted as raw source text; expand known `${...}` patterns
114
- * so generated JSON/docs contain real strings (Biome flags `${` inside quoted TS strings).
115
119
  * @param {string} description
116
120
  * @param {string} actionFilePath
117
121
  * @returns {string}
@@ -123,7 +127,7 @@ function expandDescriptionTemplateLiterals(description, actionFilePath) {
123
127
  const emotesPath = path.join(path.dirname(actionFilePath), "emotes.ts");
124
128
  if (!fs.existsSync(emotesPath)) {
125
129
  console.warn(
126
- `[generate-plugin-action-spec] VALID_EMOTE_IDS placeholder but missing ${emotesPath}`,
130
+ `[generate-plugin-action-spec] VALID_EMOTE_IDS template expression but missing ${emotesPath}`,
127
131
  );
128
132
  return description;
129
133
  }
@@ -152,18 +156,6 @@ function expandDescriptionTemplateLiterals(description, actionFilePath) {
152
156
  return description.replace(/\$\{VALID_EMOTE_IDS\.join\([^)]*\)\}/g, joined);
153
157
  }
154
158
 
155
- /**
156
- * Ensures a directory exists, creating it and parent directories if necessary.
157
- * @param {string} dir - The directory path to ensure exists
158
- * @throws {Error} If the directory path is empty or whitespace-only
159
- */
160
- function ensureDir(dir) {
161
- if (!dir || dir.trim() === "") {
162
- throw new Error("Directory path cannot be empty");
163
- }
164
- fs.mkdirSync(dir, { recursive: true });
165
- }
166
-
167
159
  function listTsFiles(rootDir) {
168
160
  const out = [];
169
161
  if (!fs.existsSync(rootDir)) return out;
@@ -174,7 +166,6 @@ function listTsFiles(rootDir) {
174
166
  for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
175
167
  const full = path.join(dir, ent.name);
176
168
  if (ent.isDirectory()) {
177
- // ignore generated + dist trees
178
169
  if (
179
170
  ent.name === "dist" ||
180
171
  ent.name === "generated" ||
@@ -184,6 +175,7 @@ function listTsFiles(rootDir) {
184
175
  }
185
176
  stack.push(full);
186
177
  } else if (ent.isFile() && ent.name.endsWith(".ts")) {
178
+ if (!isActionCandidateFile(full)) continue;
187
179
  if (full.includes(`${path.sep}__tests__${path.sep}`)) continue;
188
180
  if (full.endsWith(".test.ts")) continue;
189
181
  out.push(full);
@@ -193,110 +185,323 @@ function listTsFiles(rootDir) {
193
185
  return out.sort((a, b) => a.localeCompare(b));
194
186
  }
195
187
 
196
- /**
197
- * Extract object literal source for `export const X: Action = { ... }`.
198
- * Returns array of `{ filePath, objectText }`.
199
- */
200
- function extractActionObjects(filePath, src) {
201
- const results = [];
202
- const re = /:\s*Action\s*=\s*\{/gm;
203
- for (;;) {
204
- const m = re.exec(src);
205
- if (m === null) break;
206
- const braceStart = m.index + m[0].lastIndexOf("{");
207
-
208
- // Parse balanced braces, skipping strings and comments.
209
- let depth = 0;
210
- let j = braceStart;
211
- let inSingle = false;
212
- let inDouble = false;
213
- let inTemplate = false;
214
- let inLineComment = false;
215
- let inBlockComment = false;
216
- let escaped = false;
217
-
218
- while (j < src.length) {
219
- const ch = src[j];
220
- const next = j + 1 < src.length ? src[j + 1] : "";
221
-
222
- if (inLineComment) {
223
- if (ch === "\n") inLineComment = false;
224
- j++;
188
+ function isActionCandidateFile(filePath) {
189
+ return (
190
+ filePath.includes(`${path.sep}actions${path.sep}`) ||
191
+ filePath.endsWith(`${path.sep}actions.ts`)
192
+ );
193
+ }
194
+
195
+ const registeredActionBindingsByPluginRoot = new Map();
196
+
197
+ function getPluginRoot(filePath) {
198
+ const relative = path.relative(PLUGINS_ROOT, filePath);
199
+ if (relative.startsWith("..") || path.isAbsolute(relative)) return null;
200
+ const [pluginName] = relative.split(path.sep);
201
+ return pluginName ? path.join(PLUGINS_ROOT, pluginName) : null;
202
+ }
203
+
204
+ function resolveTsImport(entryDir, importPath) {
205
+ if (!importPath.startsWith(".")) return null;
206
+ const base = path.resolve(entryDir, importPath);
207
+ const candidates = [
208
+ base,
209
+ base.replace(/\.js$/u, ".ts"),
210
+ `${base}.ts`,
211
+ path.join(base, "index.ts"),
212
+ ];
213
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
214
+ }
215
+
216
+ function readBalancedArraySource(src, bracketStart) {
217
+ let depth = 0;
218
+ let i = bracketStart;
219
+ let inSingle = false;
220
+ let inDouble = false;
221
+ let inTemplate = false;
222
+ let inLineComment = false;
223
+ let inBlockComment = false;
224
+ let escaped = false;
225
+
226
+ while (i < src.length) {
227
+ const ch = src[i];
228
+ const next = i + 1 < src.length ? src[i + 1] : "";
229
+
230
+ if (inLineComment) {
231
+ if (ch === "\n") inLineComment = false;
232
+ i++;
233
+ continue;
234
+ }
235
+ if (inBlockComment) {
236
+ if (ch === "*" && next === "/") {
237
+ inBlockComment = false;
238
+ i += 2;
225
239
  continue;
226
240
  }
227
- if (inBlockComment) {
228
- if (ch === "*" && next === "/") {
229
- inBlockComment = false;
230
- j += 2;
231
- continue;
232
- }
233
- j++;
241
+ i++;
242
+ continue;
243
+ }
244
+ if (!inSingle && !inDouble && !inTemplate) {
245
+ if (ch === "/" && next === "/") {
246
+ inLineComment = true;
247
+ i += 2;
248
+ continue;
249
+ }
250
+ if (ch === "/" && next === "*") {
251
+ inBlockComment = true;
252
+ i += 2;
234
253
  continue;
235
254
  }
255
+ }
256
+
257
+ if (inSingle) {
258
+ if (!escaped && ch === "'") inSingle = false;
259
+ escaped = !escaped && ch === "\\";
260
+ i++;
261
+ continue;
262
+ }
263
+ if (inDouble) {
264
+ if (!escaped && ch === '"') inDouble = false;
265
+ escaped = !escaped && ch === "\\";
266
+ i++;
267
+ continue;
268
+ }
269
+ if (inTemplate) {
270
+ if (!escaped && ch === "`") inTemplate = false;
271
+ escaped = !escaped && ch === "\\";
272
+ i++;
273
+ continue;
274
+ }
275
+
276
+ if (ch === "'") {
277
+ inSingle = true;
278
+ i++;
279
+ continue;
280
+ }
281
+ if (ch === '"') {
282
+ inDouble = true;
283
+ i++;
284
+ continue;
285
+ }
286
+ if (ch === "`") {
287
+ inTemplate = true;
288
+ i++;
289
+ continue;
290
+ }
291
+
292
+ if (ch === "[") depth++;
293
+ if (ch === "]") {
294
+ depth--;
295
+ if (depth === 0) return src.slice(bracketStart, i + 1);
296
+ }
297
+ i++;
298
+ }
299
+
300
+ return null;
301
+ }
302
+
303
+ function extractActionArraySources(src) {
304
+ const sources = [];
305
+ const re = /\bactions\s*:\s*\[/gm;
306
+ for (;;) {
307
+ const match = re.exec(src);
308
+ if (match === null) break;
309
+ const bracketStart = match.index + match[0].lastIndexOf("[");
310
+ const source = readBalancedArraySource(src, bracketStart);
311
+ if (source) sources.push(source);
312
+ re.lastIndex = Math.max(re.lastIndex, bracketStart + (source?.length ?? 1));
313
+ }
314
+ return sources;
315
+ }
316
+
317
+ function extractNamedImports(src, entryDir) {
318
+ const imports = new Map();
319
+ const re = /import\s+\{([\s\S]*?)\}\s+from\s+(["'])([^"']+)\2/gm;
320
+ for (;;) {
321
+ const match = re.exec(src);
322
+ if (match === null) break;
323
+ const resolved = resolveTsImport(entryDir, match[3]);
324
+ if (!resolved || !isActionCandidateFile(resolved)) continue;
325
+ for (const rawPart of match[1].split(",")) {
326
+ const part = rawPart.trim();
327
+ if (!part) continue;
328
+ const [importedRaw, localRaw] = part.split(/\s+as\s+/u);
329
+ const imported = importedRaw.trim();
330
+ const local = (localRaw ?? importedRaw).trim();
331
+ if (
332
+ /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(imported) &&
333
+ /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(local)
334
+ ) {
335
+ imports.set(local, imported);
336
+ }
337
+ }
338
+ }
339
+ return imports;
340
+ }
236
341
 
237
- if (!inSingle && !inDouble && !inTemplate) {
238
- if (ch === "/" && next === "/") {
239
- inLineComment = true;
240
- j += 2;
342
+ function entrypointCandidatesForPlugin(pluginRoot) {
343
+ return [
344
+ path.join(pluginRoot, "src", "plugin.ts"),
345
+ path.join(pluginRoot, "src", "index.ts"),
346
+ path.join(pluginRoot, "index.ts"),
347
+ ].filter((candidate) => fs.existsSync(candidate));
348
+ }
349
+
350
+ function getRegisteredActionBindings(pluginRoot) {
351
+ if (!pluginRoot) return null;
352
+ if (registeredActionBindingsByPluginRoot.has(pluginRoot)) {
353
+ return registeredActionBindingsByPluginRoot.get(pluginRoot);
354
+ }
355
+
356
+ const entrypoints = entrypointCandidatesForPlugin(pluginRoot);
357
+ const registeredLocalNames = new Set();
358
+ const registeredExportNames = new Set();
359
+ let foundActionArray = false;
360
+
361
+ for (const entrypoint of entrypoints) {
362
+ const src = readText(entrypoint);
363
+ const actionArrays = extractActionArraySources(src);
364
+ if (actionArrays.length === 0) continue;
365
+ foundActionArray = true;
366
+ const imports = extractNamedImports(src, path.dirname(entrypoint));
367
+ const actionArraySource = actionArrays.join("\n");
368
+ for (const [localName, importedName] of imports) {
369
+ const localRe = new RegExp(`\\b${localName}\\b`, "u");
370
+ if (localRe.test(actionArraySource)) {
371
+ registeredLocalNames.add(localName);
372
+ registeredExportNames.add(importedName);
373
+ }
374
+ }
375
+
376
+ const identifierRe = /\b[A-Za-z_$][A-Za-z0-9_$]*\b/g;
377
+ for (;;) {
378
+ const match = identifierRe.exec(actionArraySource);
379
+ if (match === null) break;
380
+ registeredLocalNames.add(match[0]);
381
+ }
382
+ }
383
+
384
+ const result = foundActionArray
385
+ ? { localNames: registeredLocalNames, exportNames: registeredExportNames }
386
+ : null;
387
+ registeredActionBindingsByPluginRoot.set(pluginRoot, result);
388
+ return result;
389
+ }
390
+
391
+ function isRegisteredActionObject(filePath, exportName) {
392
+ const registered = getRegisteredActionBindings(getPluginRoot(filePath));
393
+ if (!registered || !exportName) return true;
394
+ return (
395
+ registered.exportNames.has(exportName) ||
396
+ registered.localNames.has(exportName)
397
+ );
398
+ }
399
+
400
+ function extractActionObjects(filePath, src) {
401
+ const results = [];
402
+ const patterns = [
403
+ /\b(?:export\s+)?const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*:\s*Action(?:\s*&\s*\{[\s\S]*?\})?\s*=\s*\{/gm,
404
+ /\b(?:export\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\([^)]*\)\s*:\s*Action\s*\{[\s\S]*?\breturn\s+\{/gm,
405
+ ];
406
+ for (const re of patterns) {
407
+ for (;;) {
408
+ const m = re.exec(src);
409
+ if (m === null) break;
410
+ const exportName = m[1];
411
+ const braceStart = m.index + m[0].lastIndexOf("{");
412
+
413
+ let depth = 0;
414
+ let j = braceStart;
415
+ let inSingle = false;
416
+ let inDouble = false;
417
+ let inTemplate = false;
418
+ let inLineComment = false;
419
+ let inBlockComment = false;
420
+ let escaped = false;
421
+
422
+ while (j < src.length) {
423
+ const ch = src[j];
424
+ const next = j + 1 < src.length ? src[j + 1] : "";
425
+
426
+ if (inLineComment) {
427
+ if (ch === "\n") inLineComment = false;
428
+ j++;
241
429
  continue;
242
430
  }
243
- if (ch === "/" && next === "*") {
244
- inBlockComment = true;
245
- j += 2;
431
+ if (inBlockComment) {
432
+ if (ch === "*" && next === "/") {
433
+ inBlockComment = false;
434
+ j += 2;
435
+ continue;
436
+ }
437
+ j++;
246
438
  continue;
247
439
  }
248
- }
249
440
 
250
- if (inSingle) {
251
- if (!escaped && ch === "'") inSingle = false;
252
- escaped = !escaped && ch === "\\";
253
- j++;
254
- continue;
255
- }
256
- if (inDouble) {
257
- if (!escaped && ch === '"') inDouble = false;
258
- escaped = !escaped && ch === "\\";
259
- j++;
260
- continue;
261
- }
262
- if (inTemplate) {
263
- if (!escaped && ch === "`") {
264
- inTemplate = false;
441
+ if (!inSingle && !inDouble && !inTemplate) {
442
+ if (ch === "/" && next === "/") {
443
+ inLineComment = true;
444
+ j += 2;
445
+ continue;
446
+ }
447
+ if (ch === "/" && next === "*") {
448
+ inBlockComment = true;
449
+ j += 2;
450
+ continue;
451
+ }
452
+ }
453
+
454
+ if (inSingle) {
455
+ if (!escaped && ch === "'") inSingle = false;
456
+ escaped = !escaped && ch === "\\";
457
+ j++;
458
+ continue;
459
+ }
460
+ if (inDouble) {
461
+ if (!escaped && ch === '"') inDouble = false;
462
+ escaped = !escaped && ch === "\\";
463
+ j++;
464
+ continue;
465
+ }
466
+ if (inTemplate) {
467
+ if (!escaped && ch === "`") {
468
+ inTemplate = false;
469
+ j++;
470
+ continue;
471
+ }
472
+ escaped = !escaped && ch === "\\";
265
473
  j++;
266
474
  continue;
267
475
  }
268
- escaped = !escaped && ch === "\\";
269
- j++;
270
- continue;
271
- }
272
476
 
273
- if (ch === "'") {
274
- inSingle = true;
275
- j++;
276
- continue;
277
- }
278
- if (ch === '"') {
279
- inDouble = true;
280
- j++;
281
- continue;
282
- }
283
- if (ch === "`") {
284
- inTemplate = true;
285
- j++;
286
- continue;
287
- }
477
+ if (ch === "'") {
478
+ inSingle = true;
479
+ j++;
480
+ continue;
481
+ }
482
+ if (ch === '"') {
483
+ inDouble = true;
484
+ j++;
485
+ continue;
486
+ }
487
+ if (ch === "`") {
488
+ inTemplate = true;
489
+ j++;
490
+ continue;
491
+ }
288
492
 
289
- if (ch === "{") depth++;
290
- if (ch === "}") {
291
- depth--;
292
- if (depth === 0) {
293
- const objectText = src.slice(braceStart, j + 1);
294
- results.push({ filePath, objectText });
295
- break;
493
+ if (ch === "{") depth++;
494
+ if (ch === "}") {
495
+ depth--;
496
+ if (depth === 0) {
497
+ const objectText = src.slice(braceStart, j + 1);
498
+ results.push({ filePath, exportName, objectText });
499
+ break;
500
+ }
296
501
  }
297
- }
298
502
 
299
- j++;
503
+ j++;
504
+ }
300
505
  }
301
506
  }
302
507
 
@@ -329,7 +534,6 @@ function isWs(ch) {
329
534
  }
330
535
 
331
536
  function scanTopLevelPropertyValue(objText, propName) {
332
- // objText is `{ ... }`
333
537
  let i = 0;
334
538
  let braceDepth = 0;
335
539
  let bracketDepth = 0;
@@ -414,7 +618,6 @@ function scanTopLevelPropertyValue(objText, propName) {
414
618
 
415
619
  // Top-level inside the object: braceDepth === 1
416
620
  if (braceDepth === 1 && bracketDepth === 0) {
417
- // Look for `<propName>:` starting at identifier boundary.
418
621
  if (objText.startsWith(propName, i)) {
419
622
  const before = i > 0 ? objText[i - 1] : "";
420
623
  const after =
@@ -446,7 +649,6 @@ function scanTopLevelPropertyValue(objText, propName) {
446
649
  function extractTopLevelStringProp(objText, propName) {
447
650
  const tail = scanTopLevelPropertyValue(objText, propName);
448
651
  if (!tail) return null;
449
- // Parse a single string literal token.
450
652
  const first = tail.trimStart();
451
653
  if (
452
654
  !(first.startsWith('"') || first.startsWith("'") || first.startsWith("`"))
@@ -454,7 +656,6 @@ function extractTopLevelStringProp(objText, propName) {
454
656
  return null;
455
657
  }
456
658
 
457
- // Grab up to the end of the literal (best-effort, supports escaped quotes).
458
659
  const quote = first[0];
459
660
  let i = 1;
460
661
  let escaped = false;
@@ -765,13 +966,33 @@ function skipTypeAssertionSuffix(src, cursor) {
765
966
  const after = i + 2 < src.length ? src[i + 2] : "";
766
967
  if (/[A-Za-z0-9_$]/.test(before) || /[A-Za-z0-9_$]/.test(after)) break;
767
968
  i = skipTrivia(src, i + 2);
768
- while (i < src.length && ![",", "]", "}"].includes(src[i])) i++;
969
+ let bracketDepth = 0;
970
+ let angleDepth = 0;
971
+ let parenDepth = 0;
972
+ while (i < src.length) {
973
+ const ch = src[i];
974
+ if (
975
+ bracketDepth === 0 &&
976
+ angleDepth === 0 &&
977
+ parenDepth === 0 &&
978
+ [",", "]", "}"].includes(ch)
979
+ ) {
980
+ break;
981
+ }
982
+ if (ch === "[") bracketDepth++;
983
+ else if (ch === "]" && bracketDepth > 0) bracketDepth--;
984
+ else if (ch === "<") angleDepth++;
985
+ else if (ch === ">" && angleDepth > 0) angleDepth--;
986
+ else if (ch === "(") parenDepth++;
987
+ else if (ch === ")" && parenDepth > 0) parenDepth--;
988
+ i++;
989
+ }
769
990
  i = skipTrivia(src, i);
770
991
  }
771
992
  return i;
772
993
  }
773
994
 
774
- function parseTsLiteralValue(src, cursor = 0) {
995
+ function parseTsLiteralValue(src, cursor = 0, constants = new Map()) {
775
996
  let i = skipTrivia(src, cursor);
776
997
  const ch = src[i];
777
998
  if (ch === undefined) return { value: undefined, end: i };
@@ -793,9 +1014,15 @@ function parseTsLiteralValue(src, cursor = 0) {
793
1014
  break;
794
1015
  }
795
1016
  if (src.startsWith("...", i)) {
796
- i = skipUnknownExpression(src, i + 3);
1017
+ const ident = readIdentifier(src, skipTrivia(src, i + 3));
1018
+ if (ident && Array.isArray(constants.get(ident.value))) {
1019
+ arr.push(...constants.get(ident.value));
1020
+ i = ident.end;
1021
+ } else {
1022
+ i = skipUnknownExpression(src, i + 3);
1023
+ }
797
1024
  } else {
798
- const parsed = parseTsLiteralValue(src, i);
1025
+ const parsed = parseTsLiteralValue(src, i, constants);
799
1026
  if (parsed.value !== undefined) arr.push(parsed.value);
800
1027
  i = parsed.end;
801
1028
  }
@@ -845,7 +1072,7 @@ function parseTsLiteralValue(src, cursor = 0) {
845
1072
  if (src[i] === ",") i++;
846
1073
  continue;
847
1074
  }
848
- const parsed = parseTsLiteralValue(src, i + 1);
1075
+ const parsed = parseTsLiteralValue(src, i + 1, constants);
849
1076
  if (parsed.value !== undefined) obj[key] = parsed.value;
850
1077
  i = skipTrivia(src, parsed.end);
851
1078
  if (src[i] === ",") i++;
@@ -861,6 +1088,12 @@ function parseTsLiteralValue(src, cursor = 0) {
861
1088
 
862
1089
  const ident = readIdentifier(src, i);
863
1090
  if (ident) {
1091
+ if (constants.has(ident.value)) {
1092
+ return {
1093
+ value: constants.get(ident.value),
1094
+ end: skipTypeAssertionSuffix(src, ident.end),
1095
+ };
1096
+ }
864
1097
  if (ident.value === "true") {
865
1098
  return { value: true, end: skipTypeAssertionSuffix(src, ident.end) };
866
1099
  }
@@ -875,10 +1108,61 @@ function parseTsLiteralValue(src, cursor = 0) {
875
1108
  return { value: undefined, end: skipUnknownExpression(src, i) };
876
1109
  }
877
1110
 
878
- function extractTopLevelLiteralProp(objText, propName) {
1111
+ function extractTopLevelLiteralProp(objText, propName, constants = new Map()) {
879
1112
  const source = extractTopLevelValueSource(objText, propName);
880
1113
  if (!source) return undefined;
881
- return parseTsLiteralValue(source).value;
1114
+ return parseTsLiteralValue(source, 0, constants).value;
1115
+ }
1116
+
1117
+ function extractConstLiterals(src) {
1118
+ const constants = new Map();
1119
+ const re = /\bconst\s+([A-Za-z_$][A-Za-z0-9_$]*)(?:\s*:[^=]+)?\s*=\s*/gm;
1120
+ for (;;) {
1121
+ const match = re.exec(src);
1122
+ if (match === null) break;
1123
+ const name = match[1];
1124
+ const initializerStart = skipTrivia(src, re.lastIndex);
1125
+ if (src[initializerStart] === "{") {
1126
+ re.lastIndex = Math.max(
1127
+ skipUnknownExpression(src, initializerStart),
1128
+ re.lastIndex,
1129
+ );
1130
+ continue;
1131
+ }
1132
+ const parsed = parseTsLiteralValue(src, re.lastIndex, constants);
1133
+ if (
1134
+ typeof parsed.value === "string" ||
1135
+ typeof parsed.value === "number" ||
1136
+ typeof parsed.value === "boolean" ||
1137
+ Array.isArray(parsed.value)
1138
+ ) {
1139
+ constants.set(name, parsed.value);
1140
+ }
1141
+ re.lastIndex = Math.max(parsed.end, re.lastIndex);
1142
+ }
1143
+ return constants;
1144
+ }
1145
+
1146
+ function resolveRequireActionSpecName(src, source) {
1147
+ const match = source.trim().match(/^([A-Za-z_$][A-Za-z0-9_$]*)\.name$/);
1148
+ if (!match) return null;
1149
+ const specName = match[1];
1150
+ const specRe = new RegExp(
1151
+ `\\bconst\\s+${specName}\\s*=\\s*requireActionSpec\\((["'\`])([^"'\`]+)\\1\\)`,
1152
+ );
1153
+ return specRe.exec(src)?.[2] ?? null;
1154
+ }
1155
+
1156
+ function extractResolvedStringProp(objText, propName, constants, src) {
1157
+ const direct = extractTopLevelStringProp(objText, propName);
1158
+ if (typeof direct === "string") return direct;
1159
+
1160
+ const source = extractTopLevelValueSource(objText, propName);
1161
+ if (!source) return null;
1162
+ const parsed = parseTsLiteralValue(source, 0, constants).value;
1163
+ if (typeof parsed === "string") return parsed;
1164
+
1165
+ return resolveRequireActionSpecName(src, source);
882
1166
  }
883
1167
 
884
1168
  function isRecordValue(value) {
@@ -1176,13 +1460,6 @@ function main() {
1176
1460
  ["slippage", { description: "Max slippage percentage.", example: 1 }],
1177
1461
  ]);
1178
1462
 
1179
- function humanizeKey(key) {
1180
- return key
1181
- .replaceAll(/[_-]+/g, " ")
1182
- .replaceAll(/([a-z0-9])([A-Z])/g, "$1 $2")
1183
- .toLowerCase();
1184
- }
1185
-
1186
1463
  function inferParamType(objText, key) {
1187
1464
  const re = new RegExp(`state\\?\\.${key}\\s+as\\s+([A-Za-z0-9_]+)`, "g");
1188
1465
  const m = re.exec(objText);
@@ -1206,7 +1483,7 @@ function main() {
1206
1483
  const type = inferParamType(objText, key);
1207
1484
  const known = commonParamDocs.get(key);
1208
1485
  const description =
1209
- known?.description ?? `The ${humanizeKey(key)} to use.`;
1486
+ known?.description ?? `The ${humanizeParamKey(key)} to use.`;
1210
1487
  const example =
1211
1488
  known?.example ??
1212
1489
  (type === "boolean" ? false : type === "number" ? 1 : "example");
@@ -1269,20 +1546,25 @@ function main() {
1269
1546
  const actionDocsByName = new Map();
1270
1547
 
1271
1548
  for (const filePath of tsFiles) {
1272
- // Only consider files that look like they might define actions.
1273
- if (
1274
- !filePath.includes(`${path.sep}actions${path.sep}`) &&
1275
- !filePath.endsWith(`${path.sep}actions.ts`)
1276
- ) {
1277
- continue;
1549
+ if (process.env.DEBUG_ACTION_SPEC) {
1550
+ console.error(
1551
+ `[generate-plugin-action-spec] ${path.relative(REPO_ROOT, filePath)}`,
1552
+ );
1278
1553
  }
1279
1554
  const src = readText(filePath);
1280
1555
  if (!src.includes(": Action")) continue;
1556
+ const constants = extractConstLiterals(src);
1281
1557
 
1282
1558
  const objects = extractActionObjects(filePath, src);
1283
1559
  for (const obj of objects) {
1284
- const name = extractTopLevelStringProp(obj.objectText, "name");
1560
+ const name = extractResolvedStringProp(
1561
+ obj.objectText,
1562
+ "name",
1563
+ constants,
1564
+ src,
1565
+ );
1285
1566
  if (!name) continue;
1567
+ if (!isRegisteredActionObject(filePath, obj.exportName)) continue;
1286
1568
  if (RETIRED_IMPLEMENTATION_ONLY_ACTIONS.has(name)) continue;
1287
1569
  if (coreActionNames.has(name)) continue;
1288
1570
  const description = expandDescriptionTemplateLiterals(
@@ -1293,15 +1575,16 @@ function main() {
1293
1575
  obj.objectText,
1294
1576
  "descriptionCompressed",
1295
1577
  );
1296
- const similes = extractTopLevelStringArrayProp(obj.objectText, "similes");
1578
+ const similes = extractTopLevelStringArrayProp(
1579
+ obj.objectText,
1580
+ "similes",
1581
+ ).filter((simile) => !RETIRED_IMPLEMENTATION_ONLY_ACTIONS.has(simile));
1297
1582
  const explicitParameters = sanitizeParameters(
1298
- extractTopLevelLiteralProp(obj.objectText, "parameters"),
1583
+ extractTopLevelLiteralProp(obj.objectText, "parameters", constants),
1299
1584
  );
1300
1585
  const descriptionParameters = inferParametersFromDescription(description);
1301
1586
  const jsonTemplateParameters =
1302
- explicitParameters.length === 0 &&
1303
- descriptionParameters.length === 0 &&
1304
- (name.endsWith("") || description.toLowerCase().includes("router"))
1587
+ explicitParameters.length === 0 && descriptionParameters.length === 0
1305
1588
  ? inferParametersFromJsonTemplate(src)
1306
1589
  : [];
1307
1590
  const parameters =
@@ -1314,7 +1597,6 @@ function main() {
1314
1597
  : inferParameters(obj.objectText);
1315
1598
  const exampleCalls = buildExampleCallForAction(name, parameters);
1316
1599
 
1317
- // Do not overwrite existing entries; prefer the first seen (stable ordering).
1318
1600
  if (!actionDocsByName.has(name)) {
1319
1601
  actionDocsByName.set(name, {
1320
1602
  name,
@@ -1351,7 +1633,7 @@ function main() {
1351
1633
 
1352
1634
  const outRoot = { version, actions };
1353
1635
 
1354
- ensureDir(path.dirname(OUTPUT_PATH));
1636
+ ensureDirectory(path.dirname(OUTPUT_PATH));
1355
1637
  fs.writeFileSync(OUTPUT_PATH, `${JSON.stringify(outRoot, null, 2)}\n`);
1356
1638
  console.log(
1357
1639
  `Wrote ${actions.length} plugin actions to ${path.relative(REPO_ROOT, OUTPUT_PATH)}`,