@thirdfy/agent-cli 0.2.69 → 0.2.70

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/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to `@thirdfy/agent-cli` are documented here. The format is b
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.70] - 2026-09-16
8
+
9
+ ### Fixed
10
+
11
+ - `--action get_aave_markets` (and other snake/kebab pairs) resolves to the first-class catalog key before colliding earn aliases. Matches `thirdfy-mcp` **0.0.120**. Pin: `test/catalog-canonical-action-resolution.test.cjs`.
12
+
13
+ ### Changed
14
+
15
+ - Hosted runtime write idempotency is generic (`hostedRuntimeIdempotency`) instead of EarnOrg-only helpers. Keys still use the `earnclaw:{runtimeId}:` wire prefix so Thirdfy action-event join stays the same. Pairs with `thirdfy-mcp` **0.0.120** and Thirdfy API **3.14.48**.
16
+
7
17
  ## [0.2.69] - 2026-09-16
8
18
 
9
19
  ### Fixed
package/README.md CHANGED
@@ -40,10 +40,10 @@ Run without global install:
40
40
  npx @thirdfy/agent-cli --help
41
41
  ```
42
42
 
43
- ## What's new in v0.2.69
43
+ ## What's new in v0.2.70
44
44
 
45
- - Managed `agent_wallet` reads keep `/execute` errors such as Avantis `get_avantis_price` unknown `symbols`. They do not fall back to an empty execute-intent queue.
46
- - Execute-intent fallback stays limited to `does not support execute rail`.
45
+ - `--action get_aave_markets` resolves to the first-class catalog key before colliding earn aliases.
46
+ - Hosted runtime write keys stay scoped by runtime UUID so Thirdfy action events can join.
47
47
  - See [CHANGELOG.md](./CHANGELOG.md) and GitHub Releases for older versions.
48
48
 
49
49
  ## Quick start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.2.69",
3
+ "version": "0.2.70",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,3 +50,7 @@ export function mergeExecutionContext(body) {
50
50
  if (!executionContext.source) executionContext.source = 'hosted_runtime';
51
51
  return { ...body, executionContext };
52
52
  }
53
+
54
+ export {
55
+ wrapWriteIdempotencyKeyForHostedRuntime,
56
+ } from './hostedRuntimeIdempotency.mjs';
@@ -0,0 +1,44 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ /** Stable Thirdfy execute namespace for hosted runtimes (any control plane). Legacy wire token. */
4
+ export const HOSTED_RUNTIME_IDEMPOTENCY_NAMESPACE = 'earnclaw';
5
+
6
+ const UUID_RE =
7
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
8
+
9
+ export const HOSTED_RUNTIME_SCOPED_KEY_RE = new RegExp(
10
+ `^${HOSTED_RUNTIME_IDEMPOTENCY_NAMESPACE}:[0-9a-f-]{36}:`,
11
+ 'i',
12
+ );
13
+
14
+ export function readHostedRuntimeUuid(value) {
15
+ const trimmed = String(value || '').trim();
16
+ return UUID_RE.test(trimmed) ? trimmed.toLowerCase() : undefined;
17
+ }
18
+
19
+ export function isHostedRuntimeScopedIdempotencyKey(key) {
20
+ return HOSTED_RUNTIME_SCOPED_KEY_RE.test(String(key || '').trim());
21
+ }
22
+
23
+ export function formatHostedRuntimeScopedIdempotencyKey({ runtimeId, channel, suffix }) {
24
+ const rid = readHostedRuntimeUuid(runtimeId);
25
+ if (!rid) return String(suffix || '').trim();
26
+ const ch = String(channel || 'thirdfy')
27
+ .trim()
28
+ .toLowerCase()
29
+ .replace(/[^a-z0-9_-]+/g, '-')
30
+ .slice(0, 32);
31
+ const tail = String(suffix || '').trim() || `write-${Date.now()}:${randomUUID()}`;
32
+ return `${HOSTED_RUNTIME_IDEMPOTENCY_NAMESPACE}:${rid}:${ch}:${tail}`;
33
+ }
34
+
35
+ export function wrapWriteIdempotencyKeyForHostedRuntime(existingKey, runtimeId, _action, channel = 'thirdfy') {
36
+ const existing = String(existingKey || '').trim();
37
+ const rid = readHostedRuntimeUuid(runtimeId);
38
+ if (existing && isHostedRuntimeScopedIdempotencyKey(existing)) return existing;
39
+ if (rid) {
40
+ const suffix = existing || `cli:${Date.now()}:${randomUUID()}`;
41
+ return formatHostedRuntimeScopedIdempotencyKey({ runtimeId: rid, channel, suffix });
42
+ }
43
+ return existing;
44
+ }
@@ -35,6 +35,13 @@ function canonicalActionKey(action) {
35
35
  .toLowerCase();
36
36
  }
37
37
 
38
+ function canonicalizeCatalogActionKey(value) {
39
+ return String(value || '')
40
+ .trim()
41
+ .toLowerCase()
42
+ .replace(/_/g, '-');
43
+ }
44
+
38
45
  function inferActionProvider(action) {
39
46
  // API catalog metadata (`providerId`, `domain`, `category`) is the source of truth.
40
47
  // This fallback only keeps older catalog rows / older API deployments discoverable.
@@ -337,8 +344,12 @@ function getActionProvider(action) {
337
344
 
338
345
  async function resolveActionSelection(ctx, flags) {
339
346
  const requestedAction = requireFlag(flags, 'action', 'Missing --action');
340
- const requestedLower = requestedAction.toLowerCase();
341
347
  const actions = applyActionFilters(await getCachedActionsCatalog(ctx, flags), flags);
348
+ return resolveActionFromRows(requestedAction, actions, flags);
349
+ }
350
+
351
+ function resolveActionFromRows(requestedAction, actions, flags = {}) {
352
+ const requestedLower = String(requestedAction || '').toLowerCase();
342
353
  const keyed = actions
343
354
  .map((action) => ({
344
355
  key: getActionKey(action),
@@ -381,11 +392,50 @@ async function resolveActionSelection(ctx, flags) {
381
392
  );
382
393
  }
383
394
 
395
+ const requestedCanonical = canonicalizeCatalogActionKey(requestedAction);
396
+ const canonicalKey = keyed.filter(
397
+ (entry) => canonicalizeCatalogActionKey(entry.key) === requestedCanonical
398
+ );
399
+ if (canonicalKey.length === 1) {
400
+ return {
401
+ requestedAction,
402
+ resolvedAction: canonicalKey[0].key,
403
+ resolvedActionMeta: canonicalKey[0].action || null,
404
+ };
405
+ }
406
+ if (canonicalKey.length > 1) {
407
+ const identities = new Set(canonicalKey.map((entry) => canonicalizeCatalogActionKey(entry.key)));
408
+ if (identities.size === 1) {
409
+ const kebab = canonicalKey.find((entry) => entry.key.toLowerCase() === requestedCanonical);
410
+ const picked = kebab || canonicalKey[0];
411
+ return {
412
+ requestedAction,
413
+ resolvedAction: picked.key,
414
+ resolvedActionMeta: picked.action || null,
415
+ };
416
+ }
417
+ throw new Error(
418
+ `Ambiguous --action "${requestedAction}". Multiple exact matches found: ${formatActionList(
419
+ canonicalKey.map((v) => v.key)
420
+ )}.`
421
+ );
422
+ }
423
+
384
424
  const exactAlias = keyed.filter((entry) => entry.aliases.some((a) => a.toLowerCase() === requestedLower));
385
425
  if (exactAlias.length === 1) {
386
426
  return { requestedAction, resolvedAction: exactAlias[0].key, resolvedActionMeta: exactAlias[0].action || null };
387
427
  }
388
428
  if (exactAlias.length > 1) {
429
+ const preferred = exactAlias.filter(
430
+ (entry) => canonicalizeCatalogActionKey(entry.key) === requestedCanonical
431
+ );
432
+ if (preferred.length === 1) {
433
+ return {
434
+ requestedAction,
435
+ resolvedAction: preferred[0].key,
436
+ resolvedActionMeta: preferred[0].action || null,
437
+ };
438
+ }
389
439
  const providerHint = flags.provider ? getTradingProviderHint(flags.provider) : null;
390
440
  throw new Error(
391
441
  `Ambiguous --action "${requestedAction}". Alias matches multiple actions: ${formatActionList(
@@ -420,6 +470,8 @@ async function resolveActionSelection(ctx, flags) {
420
470
  normalizeProviderId,
421
471
  applyActionFilters,
422
472
  getActionProvider,
473
+ canonicalizeCatalogActionKey,
474
+ resolveActionFromRows,
423
475
  resolveActionSelection,
424
476
  };
425
477
  }
@@ -1,63 +1,7 @@
1
- import { createCliError } from '../../core/envelope.mjs';
2
-
3
- function readUuid(value) {
4
- const trimmed = String(value || '').trim();
5
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trimmed)
6
- ? trimmed
7
- : undefined;
8
- }
9
-
10
- const POLYMARKET_PLACE_ACTIONS = new Set([
11
- 'place_polymarket_order',
12
- 'place-polymarket-order',
13
- 'place_prediction_order',
14
- 'place-prediction-order',
15
- ]);
16
-
17
- export function resolveEarnclawRuntimeId(env = process.env) {
18
- const runtimeId = readUuid(env.THIRDFY_EXECUTION_RUNTIME_ID)
19
- || readUuid(env.EARNCLAW_RUNTIME_ID);
20
- return runtimeId ? runtimeId.toLowerCase() : '';
21
- }
22
-
23
- export function normalizePolymarketPlaceAction(action) {
24
- return String(action || '')
25
- .trim()
26
- .toLowerCase()
27
- .replace(/_/g, '-');
28
- }
29
-
30
- export function isPolymarketPlaceAction(action) {
31
- const key = String(action || '').trim().toLowerCase();
32
- if (POLYMARKET_PLACE_ACTIONS.has(key)) return true;
33
- return normalizePolymarketPlaceAction(key) === 'place-polymarket-order'
34
- || normalizePolymarketPlaceAction(key) === 'place-prediction-order';
35
- }
36
-
37
- /**
38
- * On EarnClaw-managed Hermes machines, ad-hoc CLI/MCP/dashboard tool calls can
39
- * place Polymarket orders without pack execute-gate. Require an earnclaw-scoped
40
- * idempotency key so only adapter/operator keys pass.
41
- */
42
- export function assertEarnclawPolymarketWriteIdempotency({
43
- action,
44
- idempotencyKey,
45
- paramsIdempotencyKey,
46
- env = process.env,
47
- } = {}) {
48
- if (!isPolymarketPlaceAction(action)) return null;
49
- const runtimeId = resolveEarnclawRuntimeId(env);
50
- if (!runtimeId) return null;
51
-
52
- const prefix = `earnclaw:${runtimeId}`;
53
- const keys = [idempotencyKey, paramsIdempotencyKey]
54
- .map((key) => String(key || '').trim())
55
- .filter(Boolean);
56
- if (keys.some((key) => key.toLowerCase().startsWith(prefix))) return null;
57
-
58
- throw createCliError(
59
- 'EARNCLAW_PM_IDEMPOTENCY_REQUIRED',
60
- `EarnClaw runtime ${runtimeId} requires Polymarket place orders to use idempotencyKey starting with "${prefix}". ` +
61
- 'Pack execute-gate and operator exits set this automatically. Bare CLI/dashboard tool writes are blocked.',
62
- );
63
- }
1
+ /** @deprecated Import from ./hostedRuntimeWriteGate.ts */
2
+ export {
3
+ assertHostedRuntimePolymarketWriteIdempotency as assertEarnclawPolymarketWriteIdempotency,
4
+ resolveHostedRuntimeIdFromEnv as resolveEarnclawRuntimeId,
5
+ isPolymarketPlaceAction,
6
+ normalizePolymarketPlaceAction,
7
+ } from './hostedRuntimeWriteGate.mjs';
@@ -0,0 +1,56 @@
1
+ import { createCliError } from '../../core/envelope.mjs';
2
+ import {
3
+ HOSTED_RUNTIME_IDEMPOTENCY_NAMESPACE,
4
+ readHostedRuntimeUuid,
5
+ } from '../../core/hostedRuntimeIdempotency.mjs';
6
+
7
+ const POLYMARKET_PLACE_ACTIONS = new Set([
8
+ 'place_polymarket_order',
9
+ 'place-polymarket-order',
10
+ 'place_prediction_order',
11
+ 'place-prediction-order',
12
+ ]);
13
+
14
+ export function resolveHostedRuntimeIdFromEnv(env = process.env) {
15
+ return (
16
+ readHostedRuntimeUuid(env.THIRDFY_EXECUTION_RUNTIME_ID)
17
+ || readHostedRuntimeUuid(env.EARNCLAW_RUNTIME_ID)
18
+ );
19
+ }
20
+
21
+ export function normalizePolymarketPlaceAction(action) {
22
+ return String(action || '')
23
+ .trim()
24
+ .toLowerCase()
25
+ .replace(/_/g, '-');
26
+ }
27
+
28
+ export function isPolymarketPlaceAction(action) {
29
+ const key = String(action || '').trim().toLowerCase();
30
+ if (POLYMARKET_PLACE_ACTIONS.has(key)) return true;
31
+ return normalizePolymarketPlaceAction(key) === 'place-polymarket-order'
32
+ || normalizePolymarketPlaceAction(key) === 'place-prediction-order';
33
+ }
34
+
35
+ export function assertHostedRuntimePolymarketWriteIdempotency({
36
+ action,
37
+ idempotencyKey,
38
+ paramsIdempotencyKey,
39
+ env = process.env,
40
+ } = {}) {
41
+ if (!isPolymarketPlaceAction(action)) return null;
42
+ const runtimeId = resolveHostedRuntimeIdFromEnv(env);
43
+ if (!runtimeId) return null;
44
+
45
+ const prefix = `${HOSTED_RUNTIME_IDEMPOTENCY_NAMESPACE}:${runtimeId}`;
46
+ const keys = [idempotencyKey, paramsIdempotencyKey]
47
+ .map((key) => String(key || '').trim())
48
+ .filter(Boolean);
49
+ if (keys.some((key) => key.toLowerCase().startsWith(prefix))) return null;
50
+
51
+ throw createCliError(
52
+ 'HOSTED_RUNTIME_PM_IDEMPOTENCY_REQUIRED',
53
+ `Hosted runtime ${runtimeId} requires Polymarket place orders to use idempotencyKey starting with "${prefix}". ` +
54
+ 'Pack execute-gate and operator exits set this automatically. Bare CLI/dashboard tool writes are blocked.',
55
+ );
56
+ }
@@ -1,9 +1,11 @@
1
1
  import { randomUUID } from 'crypto';
2
+ import { wrapWriteIdempotencyKeyForHostedRuntime } from '../../core/hostedRuntimeIdempotency.mjs';
3
+ import { resolveExecutionContextFromEnv } from '../../core/executionContext.mjs';
2
4
  import { requireFlag, parseJsonFlag } from '../../core/args.mjs';
3
5
  import { loadProfileConfig } from '../../core/context.mjs';
4
6
  import { createCliError } from '../../core/envelope.mjs';
5
7
  import { normalizeRunMode, normalizeManagedRunMode, normalizeHybridWalletMode } from '../../core/runMode.mjs';
6
- import { assertEarnclawPolymarketWriteIdempotency } from './earnclawRuntimeWriteGate.mjs';
8
+ import { assertHostedRuntimePolymarketWriteIdempotency } from './hostedRuntimeWriteGate.mjs';
7
9
  import { resolveTokenInForFunding } from '../../core/fundingTokenIn.mjs';
8
10
 
9
11
  function isTruthyFlag(value) {
@@ -157,7 +159,14 @@ function buildManagedExecutePayload(flags, options) {
157
159
  } else if (flags.idempotencyKey) {
158
160
  payload.executionIdempotencyKey = String(flags.idempotencyKey);
159
161
  }
160
- assertEarnclawPolymarketWriteIdempotency({
162
+ if (payload.executionIdempotencyKey) {
163
+ payload.executionIdempotencyKey = wrapWriteIdempotencyKeyForHostedRuntime(
164
+ payload.executionIdempotencyKey,
165
+ resolveExecutionContextFromEnv().runtimeId,
166
+ payload.action,
167
+ );
168
+ }
169
+ assertHostedRuntimePolymarketWriteIdempotency({
161
170
  action: payload.action,
162
171
  idempotencyKey: payload.executionIdempotencyKey,
163
172
  paramsIdempotencyKey: managedParams?.idempotencyKey,
@@ -203,8 +212,15 @@ function buildIntentPayload(flags, options) {
203
212
  } else if (flags.idempotencyKey) {
204
213
  payload.idempotencyKey = String(flags.idempotencyKey);
205
214
  }
215
+ if (payload.idempotencyKey) {
216
+ payload.idempotencyKey = wrapWriteIdempotencyKeyForHostedRuntime(
217
+ payload.idempotencyKey,
218
+ resolveExecutionContextFromEnv().runtimeId,
219
+ payload.action,
220
+ );
221
+ }
206
222
  if (!options.validationOnly) {
207
- assertEarnclawPolymarketWriteIdempotency({
223
+ assertHostedRuntimePolymarketWriteIdempotency({
208
224
  action: payload.action,
209
225
  idempotencyKey: payload.idempotencyKey,
210
226
  paramsIdempotencyKey: payload.params?.idempotencyKey,