@thirdfy/agent-cli 0.2.78 → 0.2.80

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,27 @@
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.80] - 2026-10-01
8
+
9
+ ### Fixed
10
+
11
+ - **`actions --chain-id <id>` now filters.** It returned the whole catalog (359 rows) for any chain. It now keeps rows whose `supportedChains` includes the chain, plus rows with no `supportedChains` (chain-agnostic; several offchain venues have none yet). `run` and `preflight` are unchanged. Pin: `test/dogeos-provider-discovery.test.cjs`.
12
+ - **DogeOS quickstart and provider runbook used an action that does not exist.** `barkswap.swap` with `amountInRaw` failed with `PARAMS_SCHEMA_INVALID`. The docs now use the live catalog names (`dogeos-barkswap-get-quote`, `dogeos-barkswap-execute-swap` with `amountIn` in token units, `dogeos-laika-buy-token` with `dogeAmountWei`), list all 19 DogeOS actions across `dogeos-barkswap`, `dogeos-laika` and `dogeos-ecosystem`, and add the public testnet facts: RPC, explorer, faucet, WDOGE/USDC addresses, user-paid DOGE gas, and the allowlist 403.
13
+
14
+ ## [0.2.79] - 2026-09-30
15
+
16
+ ### Added
17
+
18
+ - **`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`.
19
+ - **`thirdfy-agent actions --data-category <category[,category]>`** keeps only data reads of those categories (`market_data`, `signal`, `research`, `wallet`), case-insensitive.
20
+ - **`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`.
21
+
22
+ ### Fixed
23
+
24
+ - **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).
25
+
5
26
  ## [0.2.78] - 2026-09-24
6
27
 
7
28
  ### Changed
package/README.md CHANGED
@@ -40,11 +40,10 @@ Run without global install:
40
40
  npx @thirdfy/agent-cli --help
41
41
  ```
42
42
 
43
- ## What's new in v0.2.78
43
+ ## What's new in v0.2.80
44
44
 
45
- - Avantis, the Base perps venue, is now **Veranta**. Use `--provider veranta` / `veranta-vault` and Veranta action names such as `place_veranta_perps_order` and `get_veranta_positions`.
46
- - Avantis names (`--provider avantis`, `place_avantis_perps_order`, `venue readiness avantis`) keep working. A Veranta name finds the matching action whether the Thirdfy API catalog lists Veranta or Avantis names.
47
- - `venue readiness|setup|fund veranta` works; `avantis` is still accepted.
45
+ - **`actions --chain-id <id>` now filters** to actions that run on that chain, plus chain-agnostic ones. Before, it returned the whole catalog for any chain.
46
+ - **DogeOS quickstart rewritten for the public testnet**: live action names (`dogeos-barkswap-get-quote`, `dogeos-barkswap-execute-swap`, `dogeos-laika-buy-token`), RPC, explorer, faucet, WDOGE/USDC addresses, and user-paid DOGE gas. See [docs/dogeos-quickstart.md](./docs/dogeos-quickstart.md).
48
47
  - See [CHANGELOG.md](./CHANGELOG.md) and GitHub Releases for older versions.
49
48
 
50
49
  ## Quick start
@@ -321,7 +320,7 @@ Provider and venue guides are kept outside `README` so this page stays stable as
321
320
 
322
321
  ## Command groups
323
322
 
324
- - 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)
323
+ - 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)
325
324
  - Execution: `preflight`, `run`, `intent-status`, `jeff preflight`, `jeff trade`, `jeff status`
326
325
  - Polymarket: `polymarket status`, `polymarket setup`, `polymarket dry-run`
327
326
  - Venue tooling: `venue readiness`, `venue setup`, `venue fund` (`polymarket`, `hyperliquid`, `lighter`, `veranta`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdfy/agent-cli",
3
- "version": "0.2.78",
3
+ "version": "0.2.80",
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'];
@@ -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,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);
@@ -3,6 +3,7 @@ import { apiGet, apiPost } from '../core/http.mjs';
3
3
  import { extractActions, getCachedActionsCatalog } from '../core/context.mjs';
4
4
  import { getEffectiveRunMode } from '../core/runMode.mjs';
5
5
  import { formatCreditsBalanceSummary } from '../lib/creditsUsdSummary.mjs';
6
+ import { filterActionsByChain } from '../runtime/actions/chainFilter.mjs';
6
7
  export function createDiscoveryCommands({ printEnvelope, applyActionFilters, getTradingProviderHint, resolveChainSupport, getAuthToken, resolveAgentApiKey }) {
7
8
  async function commandCatalogsList(ctx, flags, capabilities) {
8
9
  const response = await apiGet(ctx, '/api/v1/agent/actions/catalog');
@@ -32,10 +33,10 @@ async function commandCatalogsList(ctx, flags, capabilities) {
32
33
 
33
34
  async function commandActions(ctx, flags, capabilities) {
34
35
  const policyAware = Boolean(flags.agentApiKey);
35
- const actions = applyActionFilters(await getCachedActionsCatalog(ctx, flags), flags);
36
+ const chainIdFilter = Number(flags.chainId || 0) || undefined;
37
+ const actions = filterActionsByChain(applyActionFilters(await getCachedActionsCatalog(ctx, flags), flags), chainIdFilter);
36
38
  const providerHint = getTradingProviderHint(flags.provider);
37
39
  const effectiveRunMode = getEffectiveRunMode(ctx, flags);
38
- const chainIdFilter = Number(flags.chainId || 0) || undefined;
39
40
  const chainSupport = resolveChainSupport(capabilities, chainIdFilter, effectiveRunMode);
40
41
  printEnvelope({
41
42
  success: true,
@@ -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
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The typed reading of a data read (Thirdfy API typed results, thirdfy-api-v2#681): `typed`, with
3
+ * `parseIssues`, `freshness` and `attribution`, wherever the run result nests it. Human output shows
4
+ * it before the full result, so a reader does not dig through the provider's raw `payload`.
5
+ */
6
+
7
+ const isRecord = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
8
+
9
+ /** Keys never searched: the raw vendor reply can be large and never holds the typed reading. */
10
+ const SKIPPED_KEYS = new Set(['payload', 'telemetry', 'preflight', 'laneMetadata']);
11
+ const MAX_DEPTH = 6;
12
+
13
+ function parseJsonText(value) {
14
+ if (typeof value !== 'string') return value;
15
+ const trimmed = value.trim();
16
+ if (!trimmed.startsWith('{')) return value;
17
+ try {
18
+ return JSON.parse(trimmed);
19
+ } catch {
20
+ return value;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * The first object (breadth-first) that carries `typed`, returned as
26
+ * `{ typed, parseIssues, freshness?, attribution? }`; null when the result has no typed reading.
27
+ */
28
+ export function findTypedReading(result) {
29
+ let level = [result];
30
+ for (let depth = 0; depth <= MAX_DEPTH && level.length; depth += 1) {
31
+ const next = [];
32
+ for (const raw of level) {
33
+ const candidate = parseJsonText(raw);
34
+ if (!isRecord(candidate)) continue;
35
+ if (Object.prototype.hasOwnProperty.call(candidate, 'typed')) {
36
+ return {
37
+ typed: candidate.typed ?? null,
38
+ parseIssues: Array.isArray(candidate.parseIssues) ? candidate.parseIssues : [],
39
+ ...(isRecord(candidate.freshness) ? { freshness: candidate.freshness } : {}),
40
+ ...(typeof candidate.attribution === 'string' && candidate.attribution ? { attribution: candidate.attribution } : {}),
41
+ };
42
+ }
43
+ for (const [key, value] of Object.entries(candidate)) {
44
+ if (!SKIPPED_KEYS.has(key)) next.push(value);
45
+ }
46
+ }
47
+ level = next;
48
+ }
49
+ return null;
50
+ }
51
+
52
+ /** The human-mode block printed before the full result. */
53
+ export function formatTypedReadingLead(reading) {
54
+ return `Typed reading:\n${JSON.stringify(reading, null, 2)}\nFull result:`;
55
+ }
@@ -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 { loadProfileConfig } from '../core/context.mjs';
4
4
 
5
5
  export function createWalletCommands(deps) {
@@ -47,7 +47,7 @@ async function commandWalletExecute(ctx, flags, capabilities) {
47
47
  capabilitiesVersion: capabilities?.contractVersion || null,
48
48
  },
49
49
  });
50
- if (!result.success) process.exit(1);
50
+ if (!result.success) await exitAfterOutput(1);
51
51
  }
52
52
 
53
53
 
@@ -76,7 +76,7 @@ async function commandWalletSign(ctx, flags, capabilities) {
76
76
  capabilitiesVersion: capabilities?.contractVersion || null,
77
77
  },
78
78
  });
79
- if (!selfResult.success) process.exit(1);
79
+ if (!selfResult.success) await exitAfterOutput(1);
80
80
  }
81
81
 
82
82
 
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "CLI Endpoint: signals",
4
+ "type": "object",
5
+ "required": ["apps", "signals"],
6
+ "properties": {
7
+ "apps": {
8
+ "type": "array",
9
+ "items": {
10
+ "type": "object",
11
+ "required": ["appId", "title", "signalIds"],
12
+ "properties": {
13
+ "appId": { "type": "string" },
14
+ "title": { "type": "string" },
15
+ "kind": { "type": ["string", "null"] },
16
+ "category": { "type": ["string", "null"] },
17
+ "summary": { "type": ["string", "null"] },
18
+ "manifestVersion": { "type": ["number", "null"] },
19
+ "attribution": { "type": ["string", "null"] },
20
+ "displayValues": { "type": ["boolean", "null"] },
21
+ "docsUrl": { "type": "string" },
22
+ "signalIds": { "type": "array", "items": { "type": "string" } }
23
+ }
24
+ }
25
+ },
26
+ "signals": {
27
+ "type": "array",
28
+ "items": {
29
+ "type": "object",
30
+ "required": ["id", "title", "fields", "freshness", "roles", "action"],
31
+ "properties": {
32
+ "id": { "type": "string" },
33
+ "title": { "type": "string" },
34
+ "summary": { "type": ["string", "null"] },
35
+ "measures": { "type": ["string", "null"] },
36
+ "scope": { "type": ["string", "null"] },
37
+ "universe": {},
38
+ "fields": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": "object",
42
+ "required": ["name"],
43
+ "properties": {
44
+ "name": { "type": "string" },
45
+ "path": { "type": ["string", "null"] },
46
+ "type": { "type": ["string", "null"] },
47
+ "unit": { "type": ["string", "null"] },
48
+ "range": { "type": "array", "items": { "type": "number" } },
49
+ "values": { "type": "array", "items": { "type": "string" } },
50
+ "description": { "type": "string" }
51
+ }
52
+ }
53
+ },
54
+ "asOfPath": { "type": ["string", "null"] },
55
+ "freshness": {
56
+ "type": "object",
57
+ "properties": {
58
+ "cadenceSec": { "type": ["number", "null"] },
59
+ "freshnessSec": { "type": ["number", "null"] },
60
+ "horizonHours": { "type": ["number", "null"] }
61
+ }
62
+ },
63
+ "roles": { "type": "array", "items": { "type": "string" } },
64
+ "rules": { "type": "array", "items": { "type": "string" } },
65
+ "action": { "type": "string" },
66
+ "dataCategory": { "type": ["string", "null"] },
67
+ "billingTier": { "type": ["string", "null"] },
68
+ "credits": { "type": ["number", "null"] },
69
+ "appId": { "type": ["string", "null"] },
70
+ "manifestVersion": { "type": ["number", "null"] },
71
+ "attribution": { "type": ["string", "null"] },
72
+ "displayValues": { "type": ["boolean", "null"] },
73
+ "docsUrl": { "type": "string" }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
@@ -1,5 +1,3 @@
1
- import { writeSync } from 'node:fs';
2
-
3
1
  export function createCliError(code, message, payload = {}) {
4
2
  const error = new Error(message);
5
3
  error.payload = {
@@ -11,11 +9,52 @@ export function createCliError(code, message, payload = {}) {
11
9
  return error;
12
10
  }
13
11
 
14
- export function printEnvelope(payload, { jsonMode = false } = {}) {
15
- // writeSync: callers often process.exit() immediately after print. Async
16
- // process.stdout.write can truncate JSON when stdout is a pipe (tests/CI).
12
+ /**
13
+ * `humanLead`: text printed in human mode between the status line and the data JSON (a typed
14
+ * reading, a signal list). `humanData: false` prints the lead instead of the data JSON, with a
15
+ * pointer to --json. JSON mode ignores both: the envelope stays as it was.
16
+ */
17
+ export function printEnvelope(payload, { jsonMode = false, humanLead, humanData = true } = {}) {
18
+ // process.stdout.write queues the whole text. fs.writeSync(1) did not: once Node has opened
19
+ // process.stdout, a pipe is non-blocking and writeSync writes only what fits in the pipe buffer
20
+ // (64 KiB on macOS), so a large --json envelope read by a slow consumer was cut. Exits go
21
+ // through exitAfterOutput, which waits for this queue to drain.
22
+ const lead = typeof humanLead === 'string' && humanLead ? `${humanLead}\n` : '';
23
+ const body = lead && humanData === false ? 'Use --json for the full listing.' : JSON.stringify(payload.data || {}, null, 2);
17
24
  const text = jsonMode
18
25
  ? `${JSON.stringify(payload, null, 2)}\n`
19
- : `[${payload.success ? 'OK' : 'ERROR'}] ${payload.code}: ${payload.message}\n${JSON.stringify(payload.data || {}, null, 2)}\n`;
20
- writeSync(1, text);
26
+ : `[${payload.success ? 'OK' : 'ERROR'}] ${payload.code}: ${payload.message}\n${lead}${body}\n`;
27
+ process.stdout.write(text);
28
+ }
29
+
30
+ /** How long exitAfterOutput waits for output to drain before exiting anyway. */
31
+ export const DEFAULT_EXIT_DRAIN_TIMEOUT_MS = 10_000;
32
+
33
+ function exitDrainTimeoutMs() {
34
+ const override = Number(process.env.THIRDFY_AGENT_EXIT_DRAIN_TIMEOUT_MS);
35
+ return Number.isFinite(override) && override >= 0 ? override : DEFAULT_EXIT_DRAIN_TIMEOUT_MS;
36
+ }
37
+
38
+ /**
39
+ * Exit with `code` after stdout and stderr have written everything queued. process.exit() right
40
+ * after a write drops what a pipe has not taken yet. A reader that stops reading cannot hold the
41
+ * CLI open: after THIRDFY_AGENT_EXIT_DRAIN_TIMEOUT_MS (default 10 s) it exits with `code` anyway.
42
+ * The returned promise never settles, so `await exitAfterOutput(1)` (or `return
43
+ * exitAfterOutput(1)`) stops the caller where process.exit did.
44
+ */
45
+ export function exitAfterOutput(code = 0) {
46
+ process.exitCode = code;
47
+ const streams = [process.stdout, process.stderr].filter((stream) => stream && !stream.destroyed && stream.writable);
48
+ let pending = streams.length;
49
+ if (!pending) process.exit(code);
50
+ setTimeout(() => process.exit(code), exitDrainTimeoutMs());
51
+ for (const stream of streams) {
52
+ // The callback runs once every earlier write on this stream has been handed to the OS
53
+ // (or failed, for example EPIPE when the reader has gone).
54
+ stream.write('', () => {
55
+ pending -= 1;
56
+ if (pending === 0) process.exit(code);
57
+ });
58
+ }
59
+ return new Promise(() => {});
21
60
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `actions --chain-id <id>` keeps catalog rows that run on that chain.
3
+ * A row without `supportedChains` is chain-agnostic and stays.
4
+ */
5
+ export function filterActionsByChain(actions, chainId) {
6
+ const wanted = Number(chainId || 0);
7
+ if (!wanted || !Array.isArray(actions)) return actions;
8
+ return actions.filter((action) => {
9
+ const chains = action?.supportedChains;
10
+ if (!Array.isArray(chains) || chains.length === 0) return true;
11
+ return chains.some((value) => Number(value) === wanted);
12
+ });
13
+ }
@@ -348,6 +348,16 @@ function applyActionFilters(actions, flags) {
348
348
  const expectedProvider = normalizeProviderId(flags.provider);
349
349
  filtered = filtered.filter((a) => actionMatchesProvider(a, expectedProvider, filtered));
350
350
  }
351
+ if (flags.dataCategory) {
352
+ // Catalog `dataCategory` (Thirdfy API data contract); rows without one are not data reads.
353
+ const categories = new Set(
354
+ String(flags.dataCategory)
355
+ .split(',')
356
+ .map((value) => value.trim().toLowerCase())
357
+ .filter(Boolean),
358
+ );
359
+ filtered = filtered.filter((a) => categories.has(String(a?.dataCategory || '').trim().toLowerCase()));
360
+ }
351
361
  return filtered;
352
362
  }
353
363
 
@@ -11,6 +11,7 @@ import { createDelegationCommands } from '../commands/delegation.mjs';
11
11
  import { createConsoleMandateCommands } from '../commands/consoleMandate.mjs';
12
12
  import { createBootstrapCommands } from '../commands/bootstrap.mjs';
13
13
  import { createDiscoveryCommands } from '../commands/discovery.mjs';
14
+ import { createSignalsCommands } from '../commands/signals/command.mjs';
14
15
  import { createCreditsCommands } from '../commands/credits.mjs';
15
16
  import { createChatCommands } from '../commands/chat.mjs';
16
17
  import { createBrainCommands } from '../commands/brain.mjs';
@@ -67,6 +68,8 @@ export function createRuntimeHandlers(deps) {
67
68
  resolveAgentApiKey,
68
69
  });
69
70
 
71
+ const __signals = createSignalsCommands({ printEnvelope });
72
+
70
73
  const __credits = createCreditsCommands({
71
74
  printEnvelope,
72
75
  getAuthToken,
@@ -232,6 +235,7 @@ export function createRuntimeHandlers(deps) {
232
235
  commandManagedSetup: __doctor.commandManagedSetup,
233
236
  commandCatalogsList: __discovery.commandCatalogsList,
234
237
  commandActions: __discovery.commandActions,
238
+ commandSignals: __signals.commandSignals,
235
239
  commandCreditsBalance: __discovery.commandCreditsBalance,
236
240
  commandCreditsTransactions: __credits.commandCreditsTransactions,
237
241
  commandCreditsUsage: __credits.commandCreditsUsage,
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'module';
2
- import { printEnvelope as corePrintEnvelope } from '../core/envelope.mjs';
2
+ import { exitAfterOutput, printEnvelope as corePrintEnvelope } from '../core/envelope.mjs';
3
3
  import { negotiateCapabilities } from '../core/http.mjs';
4
4
  import {
5
5
  buildCliProgram,
@@ -38,7 +38,7 @@ export function createThirdfyAgentRuntime() {
38
38
  process.argv.includes('--json') ||
39
39
  (cliProgram ? cliProgram.opts()?.json : false)
40
40
  );
41
- corePrintEnvelope(payload, { jsonMode });
41
+ corePrintEnvelope(payload, { jsonMode, humanLead: options.humanLead, humanData: options.humanData });
42
42
  }
43
43
 
44
44
  function prepareContextFromFlags(flags) {
@@ -174,7 +174,7 @@ export function createThirdfyAgentRuntime() {
174
174
  return;
175
175
  }
176
176
  const exitCode = Number(error.exitCode ?? 1);
177
- process.exit(exitCode);
177
+ await exitAfterOutput(exitCode);
178
178
  }
179
179
  throw error;
180
180
  }
@@ -204,7 +204,7 @@ export function createThirdfyAgentRuntime() {
204
204
  },
205
205
  meta: { version: CLI_VERSION, apiBase: context.apiBase },
206
206
  });
207
- process.exit(1);
207
+ return exitAfterOutput(1);
208
208
  }
209
209
 
210
210
  return {