@thirdfy/agent-cli 0.2.77 → 0.2.79

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
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to `@thirdfy/agent-cli` are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
4
4
 
5
+ ## Unreleased
6
+
7
+ ## [0.2.79] - 2026-09-30
8
+
9
+ ### Added
10
+
11
+ - **`thirdfy-agent signals [--provider <id>] [--role direction|filter|context] [--json]`** lists the data provider apps (CoinMarketCap, Nansen, Allora, ...) and the signals they publish, from the Thirdfy action catalog's app manifests (`apps[]` and each read's `signals[]`, Thirdfy API thirdfy-api-v2#682): id, title, what it measures, fields with paths, types and units, freshness (cadence, max age, horizon), roles, rules, the read that returns it, credits per call with the billing tier, attribution and whether values may be displayed. Human mode prints one block per signal. An unknown `--role` fails with `SIGNAL_ROLE_INVALID`. An API without app manifests lists nothing. Pin: `test/signals-command.test.cjs`.
12
+ - **`thirdfy-agent actions --data-category <category[,category]>`** keeps only data reads of those categories (`market_data`, `signal`, `research`, `wallet`), case-insensitive.
13
+ - **`run` shows a data read's typed reading first in human mode**: `typed`, `parseIssues`, `freshness` and `attribution` (Thirdfy API typed results, thirdfy-api-v2#681), then the full result. `--json` is unchanged. Pin: `test/typed-reading-output.test.cjs`.
14
+
15
+ ### Fixed
16
+
17
+ - **Large output is no longer cut when it goes to a pipe.** The envelope was written with `fs.writeSync(1)`, which ignored a partial write: once Node has opened `process.stdout` (or the reader side made the shared pipe non-blocking), a pipe is non-blocking and `writeSync` writes only what fits in the pipe buffer (64 KiB on macOS). That is how `actions --data-category signal,research --json` piped to another program was once cut at 65,514 characters. Output now goes through `process.stdout.write`, and every command that exits with a status (failed `run` / `preflight` / `wallet` / `doctor` checks, missing flags in `condor`, `hummingbot`, `polymarket`, Commander errors, unhandled errors) sets `process.exitCode` and exits only after stdout and stderr have drained (`exitAfterOutput` in `src/core/envelope.mjs`). A reader that stops reading cannot hold the CLI open: after 10 s (`THIRDFY_AGENT_EXIT_DRAIN_TIMEOUT_MS`) it exits with the intended code anyway. Exit codes are unchanged. Pin: `test/output-drain.test.cjs` (over 1 MB of JSON through a slow pipe reader, blocking and non-blocking stdout, the exit path, and a reader that never reads).
18
+
19
+ ## [0.2.78] - 2026-09-24
20
+
21
+ ### Changed
22
+
23
+ - **Avantis is now Veranta.** The Base perps venue rebranded (ticker AVNT unchanged). Help, errors, provider hints and docs say Veranta. Provider hints for `avantis` / `avantis-vault` list Veranta action names (`place_veranta_perps_order`, `deposit_veranta_vault`, ...) and carry `displayName: "Veranta"` and `providerAliases`.
24
+
25
+ ### Added
26
+
27
+ - **Veranta names work against every API deployment.** A Veranta action name (`*_veranta_*`) resolves to the catalog row the API in hand has: the Veranta row on an API with Veranta names, the Avantis row on one without. Avantis names keep working, including against a catalog that lists them only as aliases of Veranta rows. With no catalog to check, a Veranta name is sent in the Avantis spelling, which every deployment accepts. `pricing quote` does the same.
28
+ - **`veranta` / `veranta-vault` provider aliases.** `--provider veranta` lists the same actions as `--provider avantis` (and `veranta-vault` as `avantis-vault`). `venue readiness|setup|fund veranta` calls the API with the `avantis` venue id. `avantis` is still accepted everywhere.
29
+ - **Hardcoded venue lists recognise both spellings:** the offchain-venue lane guard (`OFFCHAIN_VENUE_ORDER_SELF_UNSUPPORTED`), the 180s long timeout for onboarding and market open/close, the `agent_wallet` `traderAddress` default, the `complete_*_onboarding` `estimatedAmountUsd` default, and `certify` venue fixtures (a live Veranta row replaces the Avantis fixture; the schema digest is spelling-independent).
30
+ - Error codes and internal identifiers are unchanged (`avantis_delegate`, `AVANTIS_*` setup blockers, provider ids `avantis` / `avantis-vault`). Pin: `test/veranta-rebrand.test.cjs`.
31
+
5
32
  ## [0.2.77] - 2026-09-24
6
33
 
7
34
  ### Changed
package/README.md CHANGED
@@ -40,13 +40,13 @@ Run without global install:
40
40
  npx @thirdfy/agent-cli --help
41
41
  ```
42
42
 
43
- ## What's new in v0.2.77
43
+ ## What's new in v0.2.79
44
44
 
45
- - The agent API key is sent in the `x-agent-api-key` header, never in a URL.
46
- - A write the API answers from an earlier identical one is marked `idempotentReplay: true`. Send a new `--idempotency-key` to place another.
47
- - `agent_wallet` reads make one API call (no separate wallet preflight when the read does not need it).
48
- - `deposit_earn_position` accepts `amountHuman` / `amountInHuman` as `amount`. Unknown actions report `blockedReason: UNKNOWN_ACTION` with `suggestions`.
49
- - Built on commander 14 and ethers 6.17; Node 20+ is still supported.
45
+ - **`thirdfy-agent signals`** lists the data provider apps (CoinMarketCap, Nansen, Allora, ...) and the signals they publish: what each measures, its fields and units, freshness, roles, the read that returns it, credits per call and attribution. Filter with `--provider <id>` and `--role direction|filter|context`; add `--json` for the envelope. No key is needed to browse.
46
+ - **`thirdfy-agent actions --data-category <category[,category]>`** keeps only data reads of those categories (`market_data`, `signal`, `research`, `wallet`).
47
+ - **`run` shows a data read's typed reading first** in human mode (`typed`, `parseIssues`, `freshness`, `attribution`), then the full result. `--json` is unchanged.
48
+ - Large `--json` output piped to another program is no longer cut at the pipe buffer size. Exit codes are unchanged.
49
+ - Signals and typed readings need Thirdfy API 3.17.0 or later; an older API lists no signals.
50
50
  - See [CHANGELOG.md](./CHANGELOG.md) and GitHub Releases for older versions.
51
51
 
52
52
  ## Quick start
@@ -323,10 +323,10 @@ Provider and venue guides are kept outside `README` so this page stays stable as
323
323
 
324
324
  ## Command groups
325
325
 
326
- - Discovery: `catalogs list`, `actions` (optional `--provider`, `--chain-id`; see [`docs/command-reference.md`](./docs/command-reference.md) for trading vs earn vs prediction providers and case-insensitive matching)
326
+ - Discovery: `catalogs list`, `signals` (data provider apps and their signals: fields with units, freshness, roles, credits, attribution; `--provider`, `--role`), `actions` (optional `--provider`, `--chain-id`, `--data-category`; see [`docs/command-reference.md`](./docs/command-reference.md) for trading vs earn vs prediction providers and case-insensitive matching)
327
327
  - Execution: `preflight`, `run`, `intent-status`, `jeff preflight`, `jeff trade`, `jeff status`
328
328
  - Polymarket: `polymarket status`, `polymarket setup`, `polymarket dry-run`
329
- - Venue tooling: `venue readiness`, `venue setup`, `venue fund` (`polymarket`, `hyperliquid`, `lighter`, `avantis`)
329
+ - Venue tooling: `venue readiness`, `venue setup`, `venue fund` (`polymarket`, `hyperliquid`, `lighter`, `veranta`)
330
330
  - Hummingbot: `hummingbot status`, `connectors`, `executor-schema`, `executor-create`, `executor-stop`, `gateway-networks`, `gateway-connectors`
331
331
  - Condor Pattern A: `condor policy-status`, `condor preflight`
332
332
  - Managed signer execution: `wallet execute`, `wallet sign`, `wallet submit`, `agent run`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.2.77",
3
+ "version": "0.2.79",
4
4
  "description": "Thirdfy Agent CLI for onboarding, governance preflight, execute-intent, and status polling.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@
23
23
  ],
24
24
  "scripts": {
25
25
  "test": "node --test test/*.test.cjs",
26
- "lint": "node --check ./bin/thirdfy-agent.mjs && node --check ./src/core/*.mjs && node --check ./src/commands/*.mjs && node --check ./src/cli/*.mjs && node --check ./src/cli/options/*.mjs && node --check ./src/runtime/*.mjs && node --check ./src/runtime/execution/*.mjs && node --check ./src/runtime/actions/*.mjs && node --check ./src/runtime/validation/*.mjs && node --check ./src/runtime/certification/*.mjs",
26
+ "lint": "node --check ./bin/thirdfy-agent.mjs && node --check ./src/core/*.mjs && node --check ./src/commands/*.mjs && node --check ./src/commands/signals/*.mjs && node --check ./src/cli/*.mjs && node --check ./src/cli/options/*.mjs && node --check ./src/runtime/*.mjs && node --check ./src/runtime/execution/*.mjs && node --check ./src/runtime/actions/*.mjs && node --check ./src/runtime/validation/*.mjs && node --check ./src/runtime/certification/*.mjs",
27
27
  "validate:cli-manifest-handlers": "node ./scripts/validate-cli-manifest-handlers.mjs",
28
28
  "validate:runtime-boundaries": "node ./scripts/validate-runtime-boundaries.mjs",
29
29
  "validate:npm-pack": "node ./scripts/validate-npm-pack.mjs",
@@ -9,6 +9,7 @@ const REQUIRED_FILES = [
9
9
  'preflight.response.schema.json',
10
10
  'execute-intent.response.schema.json',
11
11
  'intent-status.response.schema.json',
12
+ 'signals.response.schema.json',
12
13
  ];
13
14
 
14
15
  const REQUIRED_KEYS = ['$schema', 'title', 'type'];
@@ -103,7 +103,7 @@ export const CLI_MANIFEST = [
103
103
  path: ['venue', 'readiness'],
104
104
  handler: 'commandVenueReadiness',
105
105
  options: ['auth', 'venue'],
106
- description: 'Normalized venue readiness (polymarket, hyperliquid, lighter, avantis)',
106
+ description: 'Normalized venue readiness (polymarket, hyperliquid, lighter, veranta)',
107
107
  args: { query: '<venue>' },
108
108
  tier: 'online',
109
109
  },
@@ -622,9 +622,16 @@ export const CLI_MANIFEST = [
622
622
  path: ['actions'],
623
623
  handler: 'commandActions',
624
624
  tier: 'online',
625
- options: ['auth', 'execution'],
625
+ options: ['auth', 'execution', 'dataCategory'],
626
626
  description: 'List allowed actions',
627
627
  },
628
+ {
629
+ path: ['signals'],
630
+ handler: 'commandSignals',
631
+ tier: 'online',
632
+ options: ['signals'],
633
+ description: 'List data provider apps and the signals they publish (fields, units, freshness, roles, credits)',
634
+ },
628
635
  {
629
636
  path: ['preflight'],
630
637
  handler: 'commandPreflight',
@@ -0,0 +1,14 @@
1
+ /** Options of the data commands: `signals`, and the data-category filter of `actions`. */
2
+
3
+ export function attachSignalsOptions(cmd) {
4
+ return cmd
5
+ .option('--provider <id>', 'only this provider app (cmc, nansen, allora, ...)')
6
+ .option('--role <role>', 'only signals a strategy may give this role: direction|filter|context');
7
+ }
8
+
9
+ export function attachDataCategoryOptions(cmd) {
10
+ return cmd.option(
11
+ '--data-category <category>',
12
+ 'only data reads of this category (market_data, signal, research, wallet); comma-separated for several',
13
+ );
14
+ }
@@ -17,6 +17,7 @@ import { attachChatOptions } from './chat.mjs';
17
17
  import { attachVenueOptions } from './venue.mjs';
18
18
  import { attachBrainOptions } from './brain.mjs';
19
19
  import { attachCertificationOptions } from './certification.mjs';
20
+ import { attachDataCategoryOptions, attachSignalsOptions } from './data.mjs';
20
21
 
21
22
  const BUNDLES = {
22
23
  auth: attachAuthOptions,
@@ -37,6 +38,8 @@ const BUNDLES = {
37
38
  venue: attachVenueOptions,
38
39
  brain: attachBrainOptions,
39
40
  certification: attachCertificationOptions,
41
+ signals: attachSignalsOptions,
42
+ dataCategory: attachDataCategoryOptions,
40
43
  };
41
44
 
42
45
  /** @param {import('commander').Command} cmd */
@@ -1,11 +1,14 @@
1
+ import { toLegacyVenueActionKey } from '../core/venueBrand.mjs';
2
+
3
+ /**
4
+ * Veranta (formerly Avantis) helpers. Names keep the Avantis spelling (internal identifiers); every
5
+ * check accepts both the `*_avantis_*` and `*_veranta_*` action spellings.
6
+ */
1
7
  export function createAvantisHelpers(deps) {
2
8
  const { getPreparedParams, resolveManagedExecutionPreflight } = deps;
3
9
 
4
10
  function avantisDefaultAddressParamForAction(action) {
5
- const canonical = String(action || '')
6
- .trim()
7
- .toLowerCase()
8
- .replace(/_/g, '-');
11
+ const canonical = toLegacyVenueActionKey(action).replace(/_/g, '-');
9
12
  if (
10
13
  canonical === 'get-avantis-open-orders' ||
11
14
  canonical === 'get-avantis-positions' ||
@@ -1,4 +1,5 @@
1
1
  import { apiPost } from '../core/http.mjs';
2
+ import { exitAfterOutput } from '../core/envelope.mjs';
2
3
  import { parseJsonFlag } from '../core/args.mjs';
3
4
  import { negotiateCapabilities } from '../core/http.mjs';
4
5
  import { resolveConfigAgentKey } from './polymarket.mjs';
@@ -27,7 +28,7 @@ function failMissingIdentity(printEnvelope, ctx, message) {
27
28
  data: {},
28
29
  meta: { apiBase: ctx.apiBase },
29
30
  });
30
- process.exit(1);
31
+ return exitAfterOutput(1);
31
32
  }
32
33
 
33
34
  export function createCondorCommands({ commandRun, printEnvelope }) {
@@ -35,7 +36,7 @@ export function createCondorCommands({ commandRun, printEnvelope }) {
35
36
  const agentApiKey = agentApiKeyFromFlags(flags, ctx);
36
37
  const userDid = userDidFromFlags(flags);
37
38
  if (!agentApiKey || !userDid) {
38
- failMissingIdentity(
39
+ await failMissingIdentity(
39
40
  printEnvelope,
40
41
  ctx,
41
42
  'Provide --agent-api-key and --user-did for Condor policy status'
@@ -60,7 +61,7 @@ export function createCondorCommands({ commandRun, printEnvelope }) {
60
61
  const idempotencyKey = String(flags.idempotencyKey || flags.idempotency_key || '').trim();
61
62
  const estimatedAmountUsd = Number(flags.estimatedAmountUsd || flags.estimated_amount_usd || 0);
62
63
  if (!agentApiKey || !userDid) {
63
- failMissingIdentity(
64
+ await failMissingIdentity(
64
65
  printEnvelope,
65
66
  ctx,
66
67
  'Provide --agent-api-key and --user-did for Condor preflight'
@@ -74,7 +75,7 @@ export function createCondorCommands({ commandRun, printEnvelope }) {
74
75
  data: {},
75
76
  meta: { apiBase: ctx.apiBase },
76
77
  });
77
- process.exit(1);
78
+ await exitAfterOutput(1);
78
79
  }
79
80
  if (!Number.isFinite(estimatedAmountUsd) || estimatedAmountUsd <= 0) {
80
81
  printEnvelope({
@@ -84,7 +85,7 @@ export function createCondorCommands({ commandRun, printEnvelope }) {
84
85
  data: {},
85
86
  meta: { apiBase: ctx.apiBase },
86
87
  });
87
- process.exit(1);
88
+ await exitAfterOutput(1);
88
89
  }
89
90
  const params = parseJsonFlag(flags, 'params') || {};
90
91
  const orgId = orgIdFromFlags(flags);
@@ -1,5 +1,6 @@
1
1
  import { requireFlag } from '../core/args.mjs';
2
2
  import { apiGet, apiPost } from '../core/http.mjs';
3
+ import { toAvantisSpelling } from '../core/venueBrand.mjs';
3
4
  import {
4
5
  formatCreditsEstimateSummary,
5
6
  formatCreditsModelsSummary,
@@ -117,7 +118,8 @@ export function createCreditsCommands({ printEnvelope, getAuthToken }) {
117
118
  ctx,
118
119
  '/api/v1/credits/pricing/quote',
119
120
  {
120
- actionKey: action,
121
+ // Veranta names price as their Avantis spelling, which every API deployment knows.
122
+ actionKey: toAvantisSpelling(action),
121
123
  chainId: Number(flags.chainId || 8453),
122
124
  },
123
125
  { Authorization: `Bearer ${authToken}` },
@@ -3,6 +3,7 @@ import { requireFlag } from '../core/args.mjs';
3
3
  import { apiGet, requestWithRouteFallback } from '../core/http.mjs';
4
4
  import { loadProfileConfig, getProfileConfigPath, getCachedActionsCatalog } from '../core/context.mjs';
5
5
  import { DEFAULT_API_BASE, PROFILE_DEFAULTS } from '../core/constants.mjs';
6
+ import { exitAfterOutput } from '../core/envelope.mjs';
6
7
  import { normalizeRunMode, normalizeManagedRunMode } from '../core/runMode.mjs';
7
8
  import { buildExecutionCertificationReport } from '../runtime/certification/executionCertify.mjs';
8
9
 
@@ -87,7 +88,7 @@ async function commandDoctorSelf(ctx, flags) {
87
88
  rpcUrl,
88
89
  },
89
90
  });
90
- if (!ok) process.exit(1);
91
+ if (!ok) await exitAfterOutput(1);
91
92
  }
92
93
 
93
94
  function buildOnboardingHelp(ctx) {
@@ -233,7 +234,7 @@ async function commandDoctorAuth(ctx, _flags) {
233
234
  },
234
235
  meta: { apiBase: ctx.apiBase },
235
236
  });
236
- if (!ok) process.exit(1);
237
+ if (!ok) await exitAfterOutput(1);
237
238
  }
238
239
 
239
240
  async function commandManagedSetup(ctx, flags, capabilities) {
@@ -310,7 +311,7 @@ async function commandManagedSetup(ctx, flags, capabilities) {
310
311
  capabilitiesVersion: capabilities?.contractVersion || null,
311
312
  },
312
313
  });
313
- if (!success) process.exit(1);
314
+ if (!success) await exitAfterOutput(1);
314
315
  }
315
316
 
316
317
  async function commandDoctorCertifyExecution(ctx, flags) {
@@ -361,7 +362,7 @@ async function commandDoctorCertifyExecution(ctx, flags) {
361
362
  cliVersion: CLI_VERSION,
362
363
  },
363
364
  });
364
- if (!report.ok) process.exit(1);
365
+ if (!report.ok) await exitAfterOutput(1);
365
366
  }
366
367
 
367
368
  return {
@@ -1,5 +1,5 @@
1
1
  import { requireFlag } from '../core/args.mjs';
2
- import { createCliError } from '../core/envelope.mjs';
2
+ import { createCliError, exitAfterOutput } from '../core/envelope.mjs';
3
3
  import {
4
4
  getEffectiveRunMode,
5
5
  normalizeHybridWalletMode,
@@ -7,6 +7,7 @@ import {
7
7
  parseBooleanFlag,
8
8
  } from '../core/runMode.mjs';
9
9
  import { apiGet } from '../core/http.mjs';
10
+ import { findTypedReading, formatTypedReadingLead } from './signals/typedReading.mjs';
10
11
  import { createRequire } from 'module';
11
12
 
12
13
  const require = createRequire(import.meta.url);
@@ -84,7 +85,7 @@ async function commandPreflight(ctx, flags, capabilities) {
84
85
  capabilitiesVersion: capabilities?.contractVersion || null,
85
86
  },
86
87
  });
87
- if (!normalized.success) process.exit(1);
88
+ if (!normalized.success) await exitAfterOutput(1);
88
89
  }
89
90
 
90
91
 
@@ -144,25 +145,30 @@ async function commandRun(ctx, flags, capabilities) {
144
145
  },
145
146
  }
146
147
  : normalized;
147
- printEnvelope({
148
- success: normalized.success,
149
- code: backendResult.code,
150
- message: backendResult.message,
151
- data,
152
- meta: {
153
- apiBase: ctx.apiBase,
154
- requestedAction: resolved.requestedAction,
155
- resolvedAction: resolved.resolvedAction,
156
- idempotencyKey: backendResult.idempotencyKey || null,
157
- runMode,
158
- hybridWalletMode,
159
- profile: ctx.profile || null,
160
- skippedPreflight: skipPreflight,
161
- swapInput: flags.__swapInput || null,
162
- capabilitiesVersion: capabilities?.contractVersion || null,
148
+ // A data read's typed reading goes first in human output; --json keeps the envelope unchanged.
149
+ const typedReading = normalized?.success ? findTypedReading(data) : null;
150
+ printEnvelope(
151
+ {
152
+ success: normalized.success,
153
+ code: backendResult.code,
154
+ message: backendResult.message,
155
+ data,
156
+ meta: {
157
+ apiBase: ctx.apiBase,
158
+ requestedAction: resolved.requestedAction,
159
+ resolvedAction: resolved.resolvedAction,
160
+ idempotencyKey: backendResult.idempotencyKey || null,
161
+ runMode,
162
+ hybridWalletMode,
163
+ profile: ctx.profile || null,
164
+ skippedPreflight: skipPreflight,
165
+ swapInput: flags.__swapInput || null,
166
+ capabilitiesVersion: capabilities?.contractVersion || null,
167
+ },
163
168
  },
164
- });
165
- if (!normalized.success) process.exit(1);
169
+ typedReading ? { humanLead: formatTypedReadingLead(typedReading) } : {},
170
+ );
171
+ if (!normalized.success) await exitAfterOutput(1);
166
172
  }
167
173
 
168
174
 
@@ -224,7 +230,7 @@ async function commandIntentStatus(ctx, flags, capabilities) {
224
230
  capabilitiesVersion: capabilities?.contractVersion || null,
225
231
  },
226
232
  });
227
- if (!normalized.success) process.exit(1);
233
+ if (!normalized.success) await exitAfterOutput(1);
228
234
  }
229
235
 
230
236
 
@@ -251,7 +257,7 @@ async function commandSelfExec(ctx, flags, capabilities) {
251
257
  resolvedAction: resolved.resolvedAction,
252
258
  },
253
259
  });
254
- process.exit(1);
260
+ await exitAfterOutput(1);
255
261
  }
256
262
 
257
263
  const executed = await performSelfBroadcast(ctx, flags, resolved.resolvedAction, selfResult);
@@ -1,4 +1,5 @@
1
1
  import { apiGet, apiPost } from '../core/http.mjs';
2
+ import { exitAfterOutput } from '../core/envelope.mjs';
2
3
  import { parseJsonFlag } from '../core/args.mjs';
3
4
  import { negotiateCapabilities } from '../core/http.mjs';
4
5
  import { resolveConfigAgentKey } from './polymarket.mjs';
@@ -27,7 +28,7 @@ function failMissingIdentity(printEnvelope, ctx, message) {
27
28
  data: {},
28
29
  meta: { apiBase: ctx.apiBase },
29
30
  });
30
- process.exit(1);
31
+ return exitAfterOutput(1);
31
32
  }
32
33
 
33
34
  export function createHummingbotCommands({ printEnvelope, commandRun }) {
@@ -63,7 +64,7 @@ export function createHummingbotCommands({ printEnvelope, commandRun }) {
63
64
  const userDid = userDidFromFlags(flags);
64
65
  const params = parseJsonFlag(flags, 'params') || {};
65
66
  if (!agentApiKey || !userDid) {
66
- failMissingIdentity(
67
+ await failMissingIdentity(
67
68
  printEnvelope,
68
69
  ctx,
69
70
  'Provide --agent-api-key and --user-did for governed executor create'
@@ -97,7 +98,7 @@ export function createHummingbotCommands({ printEnvelope, commandRun }) {
97
98
  const executorId = String(flags.executorId || flags.executor_id || '').trim();
98
99
  const params = parseJsonFlag(flags, 'params') || {};
99
100
  if (!agentApiKey || !userDid) {
100
- failMissingIdentity(printEnvelope, ctx, 'Provide --agent-api-key and --user-did for governed executor stop');
101
+ await failMissingIdentity(printEnvelope, ctx, 'Provide --agent-api-key and --user-did for governed executor stop');
101
102
  }
102
103
  if (!executorId) {
103
104
  printEnvelope({
@@ -107,7 +108,7 @@ export function createHummingbotCommands({ printEnvelope, commandRun }) {
107
108
  data: {},
108
109
  meta: { apiBase: ctx.apiBase, orgId },
109
110
  });
110
- process.exit(1);
111
+ await exitAfterOutput(1);
111
112
  }
112
113
  const body = {
113
114
  ...params,
@@ -135,12 +136,12 @@ export function createHummingbotCommands({ printEnvelope, commandRun }) {
135
136
  data: {},
136
137
  meta: { apiBase: ctx.apiBase },
137
138
  });
138
- process.exit(1);
139
+ await exitAfterOutput(1);
139
140
  }
140
141
  const agentApiKey = agentApiKeyFromFlags(flags, ctx);
141
142
  const userDid = userDidFromFlags(flags);
142
143
  if (!agentApiKey || !userDid) {
143
- failMissingIdentity(
144
+ await failMissingIdentity(
144
145
  printEnvelope,
145
146
  ctx,
146
147
  'Provide --agent-api-key and --user-did for Gateway discovery reads'
@@ -1,6 +1,7 @@
1
1
  import { loadProfileConfig } from '../core/context.mjs';
2
2
  import { negotiateCapabilities } from '../core/http.mjs';
3
3
  import { parseBooleanFlag } from '../core/runMode.mjs';
4
+ import { exitAfterOutput } from '../core/envelope.mjs';
4
5
 
5
6
  function extractAgentKey(value) {
6
7
  if (!value || typeof value !== 'object') return '';
@@ -74,7 +75,7 @@ export function createPolymarketCommands({ commandRun, commandPreflight, printEn
74
75
  data: {},
75
76
  meta: { apiBase: ctx.apiBase },
76
77
  });
77
- process.exit(1);
78
+ await exitAfterOutput(1);
78
79
  }
79
80
  const capabilities = await negotiateCapabilities(ctx);
80
81
  if (!flags.runMode) flags.runMode = 'agent_wallet';
@@ -0,0 +1,39 @@
1
+ import { apiGet } from '../../core/http.mjs';
2
+ import { createCliError } from '../../core/envelope.mjs';
3
+ import { SIGNAL_ROLES, buildSignalsListing, formatSignalsListing } from './signalRows.mjs';
4
+
5
+ /**
6
+ * `thirdfy-agent signals`: the provider apps and signals the Thirdfy action catalog publishes (what
7
+ * each signal measures, fields with units, freshness, roles, credits, attribution). Reads the public
8
+ * catalog, which carries `apps[]`; no key needed.
9
+ */
10
+ export function createSignalsCommands({ printEnvelope }) {
11
+ async function commandSignals(ctx, flags, capabilities) {
12
+ const role = String(flags.role || '').trim().toLowerCase();
13
+ if (role && !SIGNAL_ROLES.includes(role)) {
14
+ throw createCliError('SIGNAL_ROLE_INVALID', `--role must be one of ${SIGNAL_ROLES.join(', ')}`, {
15
+ role: flags.role,
16
+ allowedRoles: SIGNAL_ROLES,
17
+ });
18
+ }
19
+ const provider = String(flags.provider || '').trim();
20
+ const response = await apiGet(ctx, '/api/v1/agent/actions/catalog');
21
+ const listing = buildSignalsListing(response, { provider, role });
22
+ printEnvelope(
23
+ {
24
+ success: true,
25
+ code: 'OK',
26
+ message: `Signals loaded: ${listing.signals.length} signal(s) from ${listing.apps.length} app(s)`,
27
+ data: listing,
28
+ meta: {
29
+ apiBase: ctx.apiBase,
30
+ filters: { provider: provider || null, role: role || null },
31
+ capabilitiesVersion: capabilities?.contractVersion || null,
32
+ },
33
+ },
34
+ { humanLead: formatSignalsListing(listing), humanData: false },
35
+ );
36
+ }
37
+
38
+ return { commandSignals };
39
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * The signals listing, built from the Thirdfy action catalog (provider app manifests, thirdfy-app.json
3
+ * v1): `apps[]` at the top level and `signals[]` on each data read. Pure: no HTTP, no printing.
4
+ */
5
+
6
+ /** The roles a strategy may give a signal (manifest v1). */
7
+ export const SIGNAL_ROLES = ['direction', 'filter', 'context'];
8
+
9
+ const text = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();
10
+ const lower = (value) => text(value).toLowerCase();
11
+ const isRecord = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
12
+ const snake = (action) => lower(action).replace(/-/g, '_');
13
+
14
+ function extractCatalogActions(response) {
15
+ if (Array.isArray(response)) return response;
16
+ if (Array.isArray(response?.actions)) return response.actions;
17
+ if (Array.isArray(response?.data?.actions)) return response.data.actions;
18
+ return [];
19
+ }
20
+
21
+ function extractApps(response) {
22
+ if (Array.isArray(response?.apps)) return response.apps;
23
+ if (Array.isArray(response?.data?.apps)) return response.data.apps;
24
+ return [];
25
+ }
26
+
27
+ /** 900 -> "15m", 3600 -> "1h", 45 -> "45s", 129600 -> "1.5d"; "" when absent. */
28
+ export function formatDuration(seconds) {
29
+ const value = Number(seconds);
30
+ if (!Number.isFinite(value) || value <= 0) return '';
31
+ for (const [size, unit] of [[86400, 'd'], [3600, 'h'], [60, 'm']]) {
32
+ if (value >= size) return `${Number((value / size).toFixed(1))}${unit}`;
33
+ }
34
+ return `${Number(value.toFixed(1))}s`;
35
+ }
36
+
37
+ function fieldRows(fields) {
38
+ if (!isRecord(fields)) return [];
39
+ return Object.entries(fields).map(([name, field]) => {
40
+ const spec = isRecord(field) ? field : {};
41
+ return {
42
+ name,
43
+ path: text(spec.path) || null,
44
+ type: text(spec.type) || null,
45
+ unit: text(spec.unit) || null,
46
+ ...(Array.isArray(spec.range) ? { range: spec.range } : {}),
47
+ ...(Array.isArray(spec.values) ? { values: spec.values } : {}),
48
+ ...(text(spec.description) ? { description: text(spec.description) } : {}),
49
+ };
50
+ });
51
+ }
52
+
53
+ function appSummary(app) {
54
+ const redistribution = isRecord(app.redistribution) ? app.redistribution : {};
55
+ return {
56
+ appId: text(app.appId),
57
+ title: text(app.title) || text(app.appId),
58
+ kind: text(app.kind) || null,
59
+ category: text(app.category) || null,
60
+ summary: text(app.summary) || null,
61
+ manifestVersion: Number.isFinite(Number(app.manifestVersion)) ? Number(app.manifestVersion) : null,
62
+ attribution: text(redistribution.attribution) || null,
63
+ displayValues: typeof redistribution.displayValues === 'boolean' ? redistribution.displayValues : null,
64
+ ...(text(app.docsUrl) ? { docsUrl: text(app.docsUrl) } : {}),
65
+ signalIds: Array.isArray(app.signalIds) ? app.signalIds.map(text).filter(Boolean) : [],
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Apps and signals from one catalog response, filtered by provider (app id or the action's
71
+ * provider id) and role. A catalog without manifest fields (an older API) gives empty lists.
72
+ */
73
+ export function buildSignalsListing(response, { provider, role } = {}) {
74
+ const apps = new Map(extractApps(response).filter((app) => isRecord(app) && text(app.appId)).map((app) => [lower(app.appId), app]));
75
+ const wantedProvider = lower(provider);
76
+ const wantedRole = lower(role);
77
+ const signals = [];
78
+ const seen = new Set();
79
+ for (const action of extractCatalogActions(response)) {
80
+ if (!isRecord(action) || !Array.isArray(action.signals) || action.enabled === false) continue;
81
+ const appId = lower(action.app?.appId) || lower(action.providerId);
82
+ if (wantedProvider && appId !== wantedProvider && lower(action.providerId) !== wantedProvider) continue;
83
+ const app = apps.get(appId);
84
+ const redistribution = isRecord(app?.redistribution) ? app.redistribution : {};
85
+ for (const signal of action.signals) {
86
+ const id = text(signal?.id);
87
+ if (!id || seen.has(id)) continue;
88
+ const roles = Array.isArray(signal.roles) ? signal.roles.map(lower).filter(Boolean) : [];
89
+ if (wantedRole && !roles.includes(wantedRole)) continue;
90
+ seen.add(id);
91
+ signals.push({
92
+ id,
93
+ title: text(signal.title) || id,
94
+ summary: text(signal.summary) || null,
95
+ measures: text(signal.measures) || null,
96
+ scope: text(signal.scope) || null,
97
+ universe: signal.universe ?? null,
98
+ fields: fieldRows(signal.fields),
99
+ asOfPath: text(signal.asOf?.path) || null,
100
+ freshness: {
101
+ cadenceSec: Number.isFinite(Number(signal.cadenceSec)) ? Number(signal.cadenceSec) : null,
102
+ freshnessSec: Number.isFinite(Number(signal.freshnessSec)) ? Number(signal.freshnessSec) : null,
103
+ horizonHours: Number.isFinite(Number(signal.horizonHours)) && signal.horizonHours !== null ? Number(signal.horizonHours) : null,
104
+ },
105
+ roles,
106
+ rules: Array.isArray(signal.rules) ? signal.rules.map(text).filter(Boolean) : [],
107
+ action: snake(action.action),
108
+ dataCategory: text(action.dataCategory) || null,
109
+ billingTier: text(action.billingTier) || null,
110
+ credits: typeof action.credits === 'number' && Number.isFinite(action.credits) ? action.credits : null,
111
+ appId: appId || null,
112
+ manifestVersion: Number.isFinite(Number(action.app?.manifestVersion)) ? Number(action.app.manifestVersion) : null,
113
+ attribution: text(signal.attribution) || text(redistribution.attribution) || null,
114
+ displayValues: typeof redistribution.displayValues === 'boolean' ? redistribution.displayValues : null,
115
+ ...(text(signal.docsUrl) ? { docsUrl: text(signal.docsUrl) } : {}),
116
+ });
117
+ }
118
+ }
119
+ const listedAppIds = new Set(signals.map((signal) => signal.appId).filter(Boolean));
120
+ // With a filter, only the apps that still have a signal listed.
121
+ const filtered = Boolean(wantedProvider || wantedRole);
122
+ const appRows = [...apps.values()].filter((app) => !filtered || listedAppIds.has(lower(app.appId))).map(appSummary);
123
+ return { apps: appRows, signals };
124
+ }
125
+
126
+ function fieldLabel(field) {
127
+ const details = [];
128
+ if (field.unit) details.push(field.unit);
129
+ if (Array.isArray(field.range) && field.range.length === 2) details.push(`${field.range[0]}-${field.range[1]}`);
130
+ if (!details.length && field.type) details.push(field.type);
131
+ return details.length ? `${field.name} (${details.join(', ')})` : field.name;
132
+ }
133
+
134
+ /** Human lines: one per signal, then the credit lines to show. */
135
+ export function formatSignalsListing({ apps, signals }) {
136
+ if (!signals.length) return 'No signals in the catalog for this filter.';
137
+ const lines = signals.map((signal) => {
138
+ const timing = [
139
+ signal.freshness.cadenceSec ? `every ${formatDuration(signal.freshness.cadenceSec)}` : '',
140
+ signal.freshness.freshnessSec ? `fresh ${formatDuration(signal.freshness.freshnessSec)}` : '',
141
+ signal.freshness.horizonHours ? `horizon ${formatDuration(signal.freshness.horizonHours * 3600)}` : '',
142
+ ].filter(Boolean);
143
+ const cost = signal.credits !== null ? `${signal.credits} credits` : '';
144
+ return [
145
+ `${signal.id} ${signal.title}`,
146
+ ` measures ${signal.measures || '?'} (${signal.scope || 'market'}); roles ${signal.roles.join(', ') || '-'}; ${[...timing, cost].filter(Boolean).join('; ')}`,
147
+ ` fields ${signal.fields.map(fieldLabel).join(', ') || '-'}; action ${signal.action}`,
148
+ ...(signal.attribution ? [` ${signal.attribution}`] : []),
149
+ ].join('\n');
150
+ });
151
+ const appLine = apps.length ? `Apps: ${apps.map((app) => `${app.appId} (${app.title})`).join(', ')}` : '';
152
+ return [appLine, ...lines].filter(Boolean).join('\n');
153
+ }