@thirdfy/agent-cli 0.2.76 → 0.2.78

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.
@@ -6,6 +6,47 @@ import {
6
6
  formatParamsSchemaValidationResult,
7
7
  } from './paramsSchema.mjs';
8
8
 
9
+ const tokensOf = (action) =>
10
+ String(action || '')
11
+ .toLowerCase()
12
+ .split(/[-_\s]+/)
13
+ .filter(Boolean);
14
+
15
+ function editDistance(a, b) {
16
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
17
+ for (let i = 1; i <= a.length; i += 1) {
18
+ let prev = row[0];
19
+ row[0] = i;
20
+ for (let j = 1; j <= b.length; j += 1) {
21
+ const tmp = row[j];
22
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
23
+ prev = tmp;
24
+ }
25
+ }
26
+ return row[b.length];
27
+ }
28
+
29
+ /**
30
+ * Catalog actions closest to an unknown name: shared words first, then edit distance, so
31
+ * `open_avantis_position` suggests `place_avantis_perps_order` and typos find their action.
32
+ * Mirrors thirdfy-mcp `suggestCatalogActions`.
33
+ */
34
+ export function suggestCatalogActions(requested, actions, limit = 5) {
35
+ const wanted = tokensOf(requested);
36
+ const target = wanted.join('_');
37
+ return (Array.isArray(actions) ? actions : [])
38
+ .filter((action) => action?.action && action.enabled !== false)
39
+ .map((action) => {
40
+ const name = tokensOf(action.action).join('_');
41
+ const shared = tokensOf(action.action).filter((token) => wanted.includes(token)).length;
42
+ return { name: action.action, shared, distance: editDistance(target, name) };
43
+ })
44
+ .filter((candidate) => candidate.shared > 0 || candidate.distance <= 3)
45
+ .sort((a, b) => b.shared - a.shared || a.distance - b.distance)
46
+ .slice(0, limit)
47
+ .map((candidate) => candidate.name);
48
+ }
49
+
9
50
  export function resolveActionParamsSchema(resolved) {
10
51
  const meta = resolved?.resolvedActionMeta;
11
52
  if (!meta || typeof meta !== 'object') return null;
@@ -19,12 +60,22 @@ export function isReadOnlyCatalogAction(resolved) {
19
60
  export function validateResolvedActionParams(resolved, params) {
20
61
  const meta = resolved?.resolvedActionMeta;
21
62
  if (!meta || typeof meta !== 'object') {
63
+ // Same shape as thirdfy-mcp 0.1.6+: `error` stays PARAMS_SCHEMA_INVALID (callers match on it);
64
+ // `blockedReason: UNKNOWN_ACTION` plus `suggestions` point at real actions. With an agent key
65
+ // the list is the agent's own policy list, so the action may exist but not be allowed.
66
+ const suggestions = Array.isArray(resolved?.suggestions) ? resolved.suggestions : [];
22
67
  return {
23
68
  valid: false,
24
69
  skipped: false,
25
- error: `Action "${resolved?.requestedAction || ''}" was not found in the Thirdfy catalog`,
70
+ error: `Action "${resolved?.requestedAction || ''}" is not in the Thirdfy catalog or is not allowed for this agent${
71
+ suggestions.length
72
+ ? `. Did you mean: ${suggestions.join(', ')}?`
73
+ : '. Use `thirdfy-agent actions` to list actions.'
74
+ }`,
26
75
  field: null,
27
76
  code: 'PARAMS_SCHEMA_INVALID',
77
+ blockedReason: 'UNKNOWN_ACTION',
78
+ suggestions,
28
79
  };
29
80
  }
30
81
 
@@ -57,11 +108,13 @@ export function enforceCatalogActionParams(resolved, flags, getPreparedParams) {
57
108
  return;
58
109
  }
59
110
  throw createCliError('PARAMS_SCHEMA_INVALID', result.error || 'Action params failed catalog schema validation', {
60
- blockedReason: 'PARAMS_SCHEMA_INVALID',
111
+ blockedReason: result.blockedReason || 'PARAMS_SCHEMA_INVALID',
112
+ ...(Array.isArray(result.suggestions) ? { suggestions: result.suggestions } : {}),
61
113
  blockedStage: 'client_validation',
62
114
  validationError: result.error || null,
63
115
  field: result.field || null,
64
- resolvedAction: resolved?.resolvedAction || null,
116
+ // An unknown action resolved to nothing; MCP reports `resolvedAction: null` for it too.
117
+ resolvedAction: result.blockedReason === 'UNKNOWN_ACTION' ? null : resolved?.resolvedAction || null,
65
118
  requestedAction: resolved?.requestedAction || null,
66
119
  });
67
120
  }