@thirdfy/agent-cli 0.2.78 → 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 +14 -0
- package/README.md +7 -5
- package/package.json +2 -2
- package/scripts/validate-cli-contract-schemas.mjs +1 -0
- package/src/cli/manifest.mjs +8 -1
- package/src/cli/options/data.mjs +14 -0
- package/src/cli/options/index.mjs +3 -0
- package/src/commands/condor.mjs +6 -5
- package/src/commands/doctor.mjs +5 -4
- package/src/commands/execute.mjs +28 -22
- package/src/commands/hummingbot.mjs +7 -6
- package/src/commands/polymarket.mjs +2 -1
- package/src/commands/signals/command.mjs +39 -0
- package/src/commands/signals/signalRows.mjs +153 -0
- package/src/commands/signals/typedReading.mjs +55 -0
- package/src/commands/wallet.mjs +3 -3
- package/src/contracts/cli/schemas/signals.response.schema.json +78 -0
- package/src/core/envelope.mjs +46 -7
- package/src/runtime/actions/selection.mjs +10 -0
- package/src/runtime/handlers.mjs +4 -0
- package/src/runtime/main.mjs +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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
|
+
|
|
5
19
|
## [0.2.78] - 2026-09-24
|
|
6
20
|
|
|
7
21
|
### Changed
|
package/README.md
CHANGED
|
@@ -40,11 +40,13 @@ Run without global install:
|
|
|
40
40
|
npx @thirdfy/agent-cli --help
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
## What's new in v0.2.
|
|
43
|
+
## What's new in v0.2.79
|
|
44
44
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
- `
|
|
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.
|
|
48
50
|
- See [CHANGELOG.md](./CHANGELOG.md) and GitHub Releases for older versions.
|
|
49
51
|
|
|
50
52
|
## Quick start
|
|
@@ -321,7 +323,7 @@ Provider and venue guides are kept outside `README` so this page stays stable as
|
|
|
321
323
|
|
|
322
324
|
## Command groups
|
|
323
325
|
|
|
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)
|
|
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)
|
|
325
327
|
- Execution: `preflight`, `run`, `intent-status`, `jeff preflight`, `jeff trade`, `jeff status`
|
|
326
328
|
- Polymarket: `polymarket status`, `polymarket setup`, `polymarket dry-run`
|
|
327
329
|
- 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.
|
|
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",
|
package/src/cli/manifest.mjs
CHANGED
|
@@ -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 */
|
package/src/commands/condor.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
+
await exitAfterOutput(1);
|
|
88
89
|
}
|
|
89
90
|
const params = parseJsonFlag(flags, 'params') || {};
|
|
90
91
|
const orgId = orgIdFromFlags(flags);
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -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)
|
|
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)
|
|
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)
|
|
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)
|
|
365
|
+
if (!report.ok) await exitAfterOutput(1);
|
|
365
366
|
}
|
|
366
367
|
|
|
367
368
|
return {
|
package/src/commands/execute.mjs
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/commands/wallet.mjs
CHANGED
|
@@ -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)
|
|
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)
|
|
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
|
+
}
|
package/src/core/envelope.mjs
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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${
|
|
20
|
-
|
|
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
|
}
|
|
@@ -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
|
|
package/src/runtime/handlers.mjs
CHANGED
|
@@ -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,
|
package/src/runtime/main.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
207
|
+
return exitAfterOutput(1);
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
return {
|