@kashdao/cli 0.1.0
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 +78 -0
- package/CONTRIBUTING.md +159 -0
- package/LICENSE +21 -0
- package/README.md +868 -0
- package/SECURITY.md +110 -0
- package/dist/account-AVFLEM5D.js +90 -0
- package/dist/account-AVFLEM5D.js.map +1 -0
- package/dist/auth-GUJCVKTD.js +239 -0
- package/dist/auth-GUJCVKTD.js.map +1 -0
- package/dist/chunk-BN2CUM42.js +9 -0
- package/dist/chunk-BN2CUM42.js.map +1 -0
- package/dist/chunk-BRK7KJ4O.js +154 -0
- package/dist/chunk-BRK7KJ4O.js.map +1 -0
- package/dist/chunk-KMBMQIZ7.js +393 -0
- package/dist/chunk-KMBMQIZ7.js.map +1 -0
- package/dist/chunk-LBIRQHX5.js +133 -0
- package/dist/chunk-LBIRQHX5.js.map +1 -0
- package/dist/chunk-MIXOZU2S.js +366 -0
- package/dist/chunk-MIXOZU2S.js.map +1 -0
- package/dist/chunk-QJMF73M5.js +129 -0
- package/dist/chunk-QJMF73M5.js.map +1 -0
- package/dist/chunk-UZNSYATZ.js +680 -0
- package/dist/chunk-UZNSYATZ.js.map +1 -0
- package/dist/chunk-VIADBYFY.js +94 -0
- package/dist/chunk-VIADBYFY.js.map +1 -0
- package/dist/chunk-YHCG2SUC.js +159 -0
- package/dist/chunk-YHCG2SUC.js.map +1 -0
- package/dist/chunk-YJX3JJ4M.js +174 -0
- package/dist/chunk-YJX3JJ4M.js.map +1 -0
- package/dist/client-IOM55ZCS.js +14 -0
- package/dist/client-IOM55ZCS.js.map +1 -0
- package/dist/completion-ZZGX5BQC.js +100 -0
- package/dist/completion-ZZGX5BQC.js.map +1 -0
- package/dist/config-XJL5TSYK.js +690 -0
- package/dist/config-XJL5TSYK.js.map +1 -0
- package/dist/config-store-3ZSYGXMQ.js +44 -0
- package/dist/config-store-3ZSYGXMQ.js.map +1 -0
- package/dist/docs-MDKKSJDC.js +102 -0
- package/dist/docs-MDKKSJDC.js.map +1 -0
- package/dist/eoa-IQ72EIHR.js +551 -0
- package/dist/eoa-IQ72EIHR.js.map +1 -0
- package/dist/errors-WMZIEGQI.js +18 -0
- package/dist/errors-WMZIEGQI.js.map +1 -0
- package/dist/explain-H4EH3KH3.js +165 -0
- package/dist/explain-H4EH3KH3.js.map +1 -0
- package/dist/global-options-XOLJUPTT.js +18 -0
- package/dist/global-options-XOLJUPTT.js.map +1 -0
- package/dist/health-VZEIII74.js +98 -0
- package/dist/health-VZEIII74.js.map +1 -0
- package/dist/help-footer-GTANVDNP.js +43 -0
- package/dist/help-footer-GTANVDNP.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +251 -0
- package/dist/index.js.map +1 -0
- package/dist/intro-PQQTYWR7.js +41 -0
- package/dist/intro-PQQTYWR7.js.map +1 -0
- package/dist/markets-C37JZDPE.js +295 -0
- package/dist/markets-C37JZDPE.js.map +1 -0
- package/dist/output-VCBZ3FM7.js +22 -0
- package/dist/output-VCBZ3FM7.js.map +1 -0
- package/dist/portfolio-2AEPIJIG.js +115 -0
- package/dist/portfolio-2AEPIJIG.js.map +1 -0
- package/dist/protocol-XEQXF2GW.js +1572 -0
- package/dist/protocol-XEQXF2GW.js.map +1 -0
- package/dist/quote-I4DVJ7ZB.js +144 -0
- package/dist/quote-I4DVJ7ZB.js.map +1 -0
- package/dist/schema-PIFQ65TS.js +410 -0
- package/dist/schema-PIFQ65TS.js.map +1 -0
- package/dist/setup-USZ6IODD.js +260 -0
- package/dist/setup-USZ6IODD.js.map +1 -0
- package/dist/stdin-YW2CEQXU.js +28 -0
- package/dist/stdin-YW2CEQXU.js.map +1 -0
- package/dist/trace-IZBYTUFO.js +103 -0
- package/dist/trace-IZBYTUFO.js.map +1 -0
- package/dist/trade-IIZSEXEI.js +586 -0
- package/dist/trade-IIZSEXEI.js.map +1 -0
- package/dist/version-JDK3PEP5.js +117 -0
- package/dist/version-JDK3PEP5.js.map +1 -0
- package/dist/version-check-TDCION37.js +138 -0
- package/dist/version-check-TDCION37.js.map +1 -0
- package/dist/webhooks-PTVRKICZ.js +680 -0
- package/dist/webhooks-PTVRKICZ.js.map +1 -0
- package/dist/with-retry-4FIZG3A7.js +223 -0
- package/dist/with-retry-4FIZG3A7.js.map +1 -0
- package/package.json +99 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readGlobals
|
|
4
|
+
} from "./chunk-LBIRQHX5.js";
|
|
5
|
+
import {
|
|
6
|
+
log,
|
|
7
|
+
print,
|
|
8
|
+
printJson,
|
|
9
|
+
style
|
|
10
|
+
} from "./chunk-VIADBYFY.js";
|
|
11
|
+
import "./chunk-MIXOZU2S.js";
|
|
12
|
+
import {
|
|
13
|
+
CliError,
|
|
14
|
+
toCliError
|
|
15
|
+
} from "./chunk-UZNSYATZ.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/setup.ts
|
|
18
|
+
import { Command } from "commander";
|
|
19
|
+
var KEY_REQUEST_EMAIL = "engineering@kash.bot";
|
|
20
|
+
var KEY_REQUEST_INSTRUCTIONS = `Email ${KEY_REQUEST_EMAIL} with your intended use case to request a \`kash_test_*\` staging key. Self-service issuance is coming with the production launch.`;
|
|
21
|
+
var setupCommand = new Command("setup").description("Interactive first-run wizard: configure an API key, verify, install completion.").option("-y, --yes", "accept all defaults and skip optional prompts (non-interactive)").option("--api-key <key>", "pass the API key non-interactively (skips the prompt)").addHelpText(
|
|
22
|
+
"after",
|
|
23
|
+
`
|
|
24
|
+
Examples:
|
|
25
|
+
$ kash setup
|
|
26
|
+
$ kash setup --api-key kash_live_\u2026 --yes --json
|
|
27
|
+
$ kash --profile staging setup --yes # write to a named profile
|
|
28
|
+
$ KASH_API_KEY=kash_live_\u2026 kash setup --yes # picks up the env var
|
|
29
|
+
|
|
30
|
+
Notes:
|
|
31
|
+
- Re-runnable. Existing profiles are updated, not duplicated.
|
|
32
|
+
- --yes accepts every default and skips optional prompts (no shell
|
|
33
|
+
completion install). Combine with --api-key for full automation.
|
|
34
|
+
- Use the top-level --profile flag (\`kash --profile <name> setup\`)
|
|
35
|
+
to write to a named profile non-interactively.
|
|
36
|
+
`
|
|
37
|
+
).action(async (options, cmd) => {
|
|
38
|
+
const globals = readGlobals(cmd);
|
|
39
|
+
try {
|
|
40
|
+
const result = await runSetup(options, globals);
|
|
41
|
+
if (globals.json) {
|
|
42
|
+
printJson(result);
|
|
43
|
+
}
|
|
44
|
+
} catch (cause) {
|
|
45
|
+
throw toCliError(cause);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
async function runSetup(options, globals) {
|
|
49
|
+
const jsonMode = globals.json;
|
|
50
|
+
const [{ confirm, input, password }, { updateConfig, setCurrentProfile }, { buildClient }] = await Promise.all([
|
|
51
|
+
import("@inquirer/prompts"),
|
|
52
|
+
import("./config-store-3ZSYGXMQ.js"),
|
|
53
|
+
import("./client-IOM55ZCS.js")
|
|
54
|
+
]);
|
|
55
|
+
if (!jsonMode) {
|
|
56
|
+
print("");
|
|
57
|
+
print(style.bold("Kash CLI setup"));
|
|
58
|
+
print(style.dim("Configure an API key, verify connectivity, install shell completion."));
|
|
59
|
+
print("");
|
|
60
|
+
}
|
|
61
|
+
const envKey = process.env["KASH_API_KEY"];
|
|
62
|
+
let apiKey;
|
|
63
|
+
if (options.apiKey) {
|
|
64
|
+
apiKey = options.apiKey;
|
|
65
|
+
if (!jsonMode) log.info("Using --api-key from the command line.");
|
|
66
|
+
} else if (envKey) {
|
|
67
|
+
apiKey = envKey;
|
|
68
|
+
if (!jsonMode) log.info("Using KASH_API_KEY from the environment.");
|
|
69
|
+
} else if (options.yes) {
|
|
70
|
+
throw new CliError("Cannot run --yes without an API key.", {
|
|
71
|
+
code: "INVALID_INPUT",
|
|
72
|
+
recoverable: true,
|
|
73
|
+
suggestion: `Pass --api-key <kash_\u2026>, set KASH_API_KEY, or run \`kash setup\` without --yes. ${KEY_REQUEST_INSTRUCTIONS}`
|
|
74
|
+
});
|
|
75
|
+
} else {
|
|
76
|
+
if (!jsonMode) {
|
|
77
|
+
print(`${style.dim("\u{1F9EA} Staging release.")} ${KEY_REQUEST_INSTRUCTIONS} (Ctrl-C to abort.)`);
|
|
78
|
+
}
|
|
79
|
+
apiKey = await password({
|
|
80
|
+
message: "Paste your API key:",
|
|
81
|
+
mask: "*",
|
|
82
|
+
validate: (value) => {
|
|
83
|
+
const trimmed = value.trim();
|
|
84
|
+
if (trimmed.length < 16) return "API key looks too short.";
|
|
85
|
+
if (!trimmed.startsWith("kash_")) return 'API keys start with "kash_".';
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
apiKey = apiKey.trim();
|
|
90
|
+
}
|
|
91
|
+
let profile = globals.profile;
|
|
92
|
+
if (profile === void 0 && !options.yes) {
|
|
93
|
+
profile = await input({
|
|
94
|
+
message: "Profile name:",
|
|
95
|
+
default: "default"
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
profile = (profile ?? "default").trim();
|
|
99
|
+
const writeOpts = {
|
|
100
|
+
profile,
|
|
101
|
+
...globals.configPath === void 0 ? {} : { configPath: globals.configPath }
|
|
102
|
+
};
|
|
103
|
+
const written = await updateConfig({ apiKey }, writeOpts);
|
|
104
|
+
await setCurrentProfile(
|
|
105
|
+
written.profile,
|
|
106
|
+
globals.configPath === void 0 ? void 0 : { configPath: globals.configPath }
|
|
107
|
+
);
|
|
108
|
+
if (!jsonMode) {
|
|
109
|
+
log.success(`Saved API key to profile "${written.profile}".`);
|
|
110
|
+
}
|
|
111
|
+
let healthOk = false;
|
|
112
|
+
let healthLatency;
|
|
113
|
+
let healthStatus;
|
|
114
|
+
let healthVersion;
|
|
115
|
+
try {
|
|
116
|
+
const { client } = await buildClient({
|
|
117
|
+
requireAuth: true,
|
|
118
|
+
// Reuse the resolved globals but pin the profile to the one we
|
|
119
|
+
// just wrote — otherwise a stale flag could cause us to verify
|
|
120
|
+
// a different profile than the one we configured.
|
|
121
|
+
globals: { ...globals, profile: written.profile }
|
|
122
|
+
});
|
|
123
|
+
const result = await client.healthCheck();
|
|
124
|
+
healthOk = result.ok;
|
|
125
|
+
healthLatency = result.latencyMs;
|
|
126
|
+
healthStatus = result.status;
|
|
127
|
+
healthVersion = result.version;
|
|
128
|
+
if (!jsonMode) {
|
|
129
|
+
if (healthOk) {
|
|
130
|
+
log.success(
|
|
131
|
+
`Health check OK in ${String(healthLatency)}ms${healthVersion ? ` (server ${healthVersion})` : ""}.`
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
log.warn("Health check returned not-ok. Verify your network or rerun later.");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch (cause) {
|
|
138
|
+
if (!jsonMode) {
|
|
139
|
+
log.warn(`Health check failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
140
|
+
log.info("Setup completed; verify later with `kash health`.");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const scopes = await probeReadScopes({ globals, profile: written.profile });
|
|
144
|
+
if (!jsonMode) {
|
|
145
|
+
renderScopesHuman(scopes);
|
|
146
|
+
}
|
|
147
|
+
let completionInstalled = false;
|
|
148
|
+
if (!options.yes && !jsonMode) {
|
|
149
|
+
const detectedShell = process.env["SHELL"] ?? "(unknown)";
|
|
150
|
+
const wantCompletion = await confirm({
|
|
151
|
+
message: `Install shell completion for ${detectedShell}?`,
|
|
152
|
+
default: true
|
|
153
|
+
});
|
|
154
|
+
if (wantCompletion) {
|
|
155
|
+
try {
|
|
156
|
+
const { default: omelette } = await import("omelette");
|
|
157
|
+
const completion = omelette("kash");
|
|
158
|
+
completion.setupShellInitFile();
|
|
159
|
+
completionInstalled = true;
|
|
160
|
+
log.success("Shell completions installed.");
|
|
161
|
+
log.info("Restart your shell or source your shell config to activate.");
|
|
162
|
+
} catch (cause) {
|
|
163
|
+
log.warn(
|
|
164
|
+
`Could not install completion: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!jsonMode) {
|
|
170
|
+
print("");
|
|
171
|
+
print(style.bold("You're all set."));
|
|
172
|
+
for (const line of pickNextStepHints(scopes)) {
|
|
173
|
+
print(` ${style.dim("Try:")} ${line}`);
|
|
174
|
+
}
|
|
175
|
+
print(` ${style.dim(" :")} kash docs --json --quiet | jq '.subcommands[].name'`);
|
|
176
|
+
print("");
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
ok: true,
|
|
180
|
+
profile: written.profile,
|
|
181
|
+
authenticated: true,
|
|
182
|
+
health: {
|
|
183
|
+
ok: healthOk,
|
|
184
|
+
...healthLatency === void 0 ? {} : { latencyMs: healthLatency },
|
|
185
|
+
...healthStatus === void 0 ? {} : { status: healthStatus },
|
|
186
|
+
...healthVersion === void 0 ? {} : { version: healthVersion }
|
|
187
|
+
},
|
|
188
|
+
scopes,
|
|
189
|
+
completionInstalled
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async function probeReadScopes(opts) {
|
|
193
|
+
const { buildClient } = await import("./client-IOM55ZCS.js");
|
|
194
|
+
const { client } = await buildClient({
|
|
195
|
+
requireAuth: true,
|
|
196
|
+
globals: { ...opts.globals, profile: opts.profile }
|
|
197
|
+
});
|
|
198
|
+
const probes = [
|
|
199
|
+
["markets:read", async () => client.markets.list({ limit: 1 })],
|
|
200
|
+
["trades:read", async () => client.trades.list({ limit: 1 })],
|
|
201
|
+
["webhooks:read", async () => client.webhooks.list({ limit: 1 })]
|
|
202
|
+
];
|
|
203
|
+
const result = {};
|
|
204
|
+
for (const [scope, call] of probes) {
|
|
205
|
+
result[scope] = await classifyProbe(call);
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
async function classifyProbe(call) {
|
|
210
|
+
try {
|
|
211
|
+
await call();
|
|
212
|
+
return "ok";
|
|
213
|
+
} catch (cause) {
|
|
214
|
+
if (cause !== null && typeof cause === "object") {
|
|
215
|
+
const code = cause.code;
|
|
216
|
+
if (code === "INSUFFICIENT_SCOPE") return "denied";
|
|
217
|
+
}
|
|
218
|
+
return "unknown";
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function renderScopesHuman(scopes) {
|
|
222
|
+
const entries = Object.entries(scopes);
|
|
223
|
+
if (entries.length === 0) return;
|
|
224
|
+
const allOk = entries.every(([, v]) => v === "ok");
|
|
225
|
+
if (allOk) {
|
|
226
|
+
log.success("Scope check OK \u2014 key carries every probed read scope.");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
log.info("Scope detection (probed via canary read calls):");
|
|
230
|
+
for (const [scope, status] of entries) {
|
|
231
|
+
const label = status === "ok" ? style.success("granted") : status === "denied" ? style.warn("not granted") : style.dim("inconclusive");
|
|
232
|
+
print(` ${style.dim(scope.padEnd(16))} ${label}`);
|
|
233
|
+
}
|
|
234
|
+
const denied = entries.filter(([, v]) => v === "denied").map(([k]) => k);
|
|
235
|
+
if (denied.length > 0) {
|
|
236
|
+
log.info(
|
|
237
|
+
`Request a key with broader scope by emailing ${KEY_REQUEST_EMAIL} if you need: ${denied.join(", ")}.`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function pickNextStepHints(scopes) {
|
|
242
|
+
const hints = [];
|
|
243
|
+
if (scopes["markets:read"] === "ok") {
|
|
244
|
+
hints.push("kash markets list --status ACTIVE");
|
|
245
|
+
}
|
|
246
|
+
if (scopes["trades:read"] === "ok") {
|
|
247
|
+
hints.push('kash trade list --json --quiet | jq -r ".data[].id"');
|
|
248
|
+
}
|
|
249
|
+
if (scopes["webhooks:read"] === "ok" && hints.length < 2) {
|
|
250
|
+
hints.push("kash webhooks list --status failed,retrying");
|
|
251
|
+
}
|
|
252
|
+
if (hints.length === 0) {
|
|
253
|
+
hints.push("kash --help");
|
|
254
|
+
}
|
|
255
|
+
return hints.slice(0, 2);
|
|
256
|
+
}
|
|
257
|
+
export {
|
|
258
|
+
setupCommand
|
|
259
|
+
};
|
|
260
|
+
//# sourceMappingURL=setup-USZ6IODD.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/setup.ts"],"sourcesContent":["/**\n * `kash setup` — first-run interactive wizard.\n *\n * Collapses what's normally a four-step onboarding into a single\n * guided flow:\n * 1. Open the dashboard URL so the user can issue an API key.\n * 2. Paste the key (validated for the `kash_` prefix).\n * 3. Pick a profile name (defaults to `default`).\n * 4. Verify connectivity via `kash health`.\n * 5. Optionally install shell completion.\n *\n * **Lazy-loaded.** `@inquirer/prompts` is heavy — the dynamic import\n * keeps it out of the cold path for `kash --version` and other\n * non-interactive invocations. The KashClient + completion modules\n * are also lazy.\n *\n * **Skippable.** Every prompt has a sensible default and `--yes`\n * accepts all defaults non-interactively (CI / automation). `--json`\n * emits a structured summary at the end so post-setup scripts can\n * verify state without re-reading the config file.\n */\n\nimport { Command } from 'commander';\n\nimport { CliError, toCliError } from '../errors.js';\nimport { readGlobals } from '../utils/global-options.js';\nimport { log, print, printJson, style } from '../utils/output.js';\n\nimport type { GlobalOptions } from '../utils/global-options.js';\n\n// Staging release: production endpoints aren't deployed yet and the\n// self-service key-issuance dashboard isn't live. For now, customers\n// request a `kash_test_*` key by email; the SDK + CLI auto-route test\n// keys to the staging environment so the developer experience is\n// otherwise unchanged.\nconst KEY_REQUEST_EMAIL = 'engineering@kash.bot';\nconst KEY_REQUEST_INSTRUCTIONS = `Email ${KEY_REQUEST_EMAIL} with your intended use case to request a \\`kash_test_*\\` staging key. Self-service issuance is coming with the production launch.`;\n\ntype SetupOptions = {\n yes?: boolean;\n apiKey?: string;\n};\n\n/**\n * Per-scope probe result. `'ok'` means the canary call succeeded;\n * `'denied'` means the API returned 403 / INSUFFICIENT_SCOPE for that\n * scope (the key is valid but doesn't carry the scope); `'unknown'`\n * means the probe failed for a non-scope reason (network, server\n * error) — we don't pretend to know.\n */\ntype ScopeProbeStatus = 'ok' | 'denied' | 'unknown';\n\n/**\n * Output envelope when `--json` is set. Stable contract — agents and\n * provisioning scripts pin to it.\n */\ntype SetupResult = {\n ok: true;\n profile: string;\n authenticated: boolean;\n health: { ok: boolean; latencyMs?: number; status?: string; version?: string };\n /**\n * Best-effort scope detection from canary read calls. Each entry is\n * a granted/denied/unknown verdict for a named scope. Does NOT probe\n * write scopes (that'd require a real trade).\n */\n scopes: Record<string, ScopeProbeStatus>;\n completionInstalled: boolean;\n};\n\nexport const setupCommand = new Command('setup')\n .description('Interactive first-run wizard: configure an API key, verify, install completion.')\n .option('-y, --yes', 'accept all defaults and skip optional prompts (non-interactive)')\n .option('--api-key <key>', 'pass the API key non-interactively (skips the prompt)')\n .addHelpText(\n 'after',\n `\nExamples:\n $ kash setup\n $ kash setup --api-key kash_live_… --yes --json\n $ kash --profile staging setup --yes # write to a named profile\n $ KASH_API_KEY=kash_live_… kash setup --yes # picks up the env var\n\nNotes:\n - Re-runnable. Existing profiles are updated, not duplicated.\n - --yes accepts every default and skips optional prompts (no shell\n completion install). Combine with --api-key for full automation.\n - Use the top-level --profile flag (\\`kash --profile <name> setup\\`)\n to write to a named profile non-interactively.\n`\n )\n .action(async (options: SetupOptions, cmd: Command) => {\n const globals = readGlobals(cmd);\n\n try {\n const result = await runSetup(options, globals);\n if (globals.json) {\n printJson(result);\n }\n } catch (cause) {\n throw toCliError(cause);\n }\n });\n\nasync function runSetup(options: SetupOptions, globals: GlobalOptions): Promise<SetupResult> {\n const jsonMode = globals.json;\n // Lazy imports — see file header.\n const [{ confirm, input, password }, { updateConfig, setCurrentProfile }, { buildClient }] =\n await Promise.all([\n import('@inquirer/prompts'),\n import('../utils/config-store.js'),\n import('../utils/client.js'),\n ]);\n\n if (!jsonMode) {\n print('');\n print(style.bold('Kash CLI setup'));\n print(style.dim('Configure an API key, verify connectivity, install shell completion.'));\n print('');\n }\n\n // Resolve the API key. Precedence: --api-key flag → env → prompt.\n const envKey = process.env['KASH_API_KEY'];\n let apiKey: string;\n if (options.apiKey) {\n apiKey = options.apiKey;\n if (!jsonMode) log.info('Using --api-key from the command line.');\n } else if (envKey) {\n apiKey = envKey;\n if (!jsonMode) log.info('Using KASH_API_KEY from the environment.');\n } else if (options.yes) {\n throw new CliError('Cannot run --yes without an API key.', {\n code: 'INVALID_INPUT',\n recoverable: true,\n suggestion: `Pass --api-key <kash_…>, set KASH_API_KEY, or run \\`kash setup\\` without --yes. ${KEY_REQUEST_INSTRUCTIONS}`,\n });\n } else {\n if (!jsonMode) {\n print(`${style.dim('🧪 Staging release.')} ${KEY_REQUEST_INSTRUCTIONS} (Ctrl-C to abort.)`);\n }\n // Use the masked-input prompt — `input()` would echo every\n // keystroke to the terminal, defeating the protection that\n // `kash setup` should provide for fresh API keys (shoulder-\n // surfing, screen recording, scroll-back capture). The validation\n // shape is identical to the previous `input` call.\n apiKey = await password({\n message: 'Paste your API key:',\n mask: '*',\n validate: (value: string) => {\n const trimmed = value.trim();\n if (trimmed.length < 16) return 'API key looks too short.';\n if (!trimmed.startsWith('kash_')) return 'API keys start with \"kash_\".';\n return true;\n },\n });\n apiKey = apiKey.trim();\n }\n\n // Resolve the profile name. Precedence: top-level --profile flag →\n // interactive prompt → 'default'. We deliberately reuse the global\n // flag rather than declaring our own to avoid two profile flags\n // with subtly different semantics.\n let profile = globals.profile;\n if (profile === undefined && !options.yes) {\n profile = await input({\n message: 'Profile name:',\n default: 'default',\n });\n }\n profile = (profile ?? 'default').trim();\n\n // Persist. `updateConfig` handles the file IO, mode 0600, and Zod\n // validation. We only update apiKey — baseUrl/chain are inferred\n // from the key prefix on first request. Both calls thread the\n // global `--config <path>` override so `kash setup --config\n // /tmp/test.json` writes to the user-specified file rather than\n // silently falling back to ~/.kash/config.json.\n const writeOpts = {\n profile,\n ...(globals.configPath === undefined ? {} : { configPath: globals.configPath }),\n };\n const written = await updateConfig({ apiKey }, writeOpts);\n await setCurrentProfile(\n written.profile,\n globals.configPath === undefined ? undefined : { configPath: globals.configPath }\n );\n\n if (!jsonMode) {\n log.success(`Saved API key to profile \"${written.profile}\".`);\n }\n\n // Verify connectivity. We pass the just-resolved profile so the\n // health check reads the key we just wrote (not whatever was the\n // active profile before).\n let healthOk = false;\n let healthLatency: number | undefined;\n let healthStatus: string | undefined;\n let healthVersion: string | undefined;\n try {\n const { client } = await buildClient({\n requireAuth: true,\n // Reuse the resolved globals but pin the profile to the one we\n // just wrote — otherwise a stale flag could cause us to verify\n // a different profile than the one we configured.\n globals: { ...globals, profile: written.profile },\n });\n const result = await client.healthCheck();\n healthOk = result.ok;\n healthLatency = result.latencyMs;\n healthStatus = result.status;\n healthVersion = result.version;\n if (!jsonMode) {\n if (healthOk) {\n log.success(\n `Health check OK in ${String(healthLatency)}ms${\n healthVersion ? ` (server ${healthVersion})` : ''\n }.`\n );\n } else {\n log.warn('Health check returned not-ok. Verify your network or rerun later.');\n }\n }\n } catch (cause) {\n // Don't fail setup on a flaky health check — the key may still be\n // good and the user just has temporary connectivity issues.\n if (!jsonMode) {\n log.warn(`Health check failed: ${cause instanceof Error ? cause.message : String(cause)}`);\n log.info('Setup completed; verify later with `kash health`.');\n }\n }\n\n // Probe a handful of read scopes via canary calls. We don't probe\n // write scopes (that'd require a real trade or webhook rotation),\n // and we don't fail setup on probe errors — the key still works,\n // we just couldn't tell which scopes it carries.\n const scopes = await probeReadScopes({ globals, profile: written.profile });\n if (!jsonMode) {\n renderScopesHuman(scopes);\n }\n\n // Optional: shell completion. Skip in --yes mode (no good default\n // for an action that mutates the user's shell rc files).\n let completionInstalled = false;\n if (!options.yes && !jsonMode) {\n const detectedShell = process.env['SHELL'] ?? '(unknown)';\n const wantCompletion = await confirm({\n message: `Install shell completion for ${detectedShell}?`,\n default: true,\n });\n if (wantCompletion) {\n try {\n const { default: omelette } = await import('omelette');\n const completion = omelette('kash');\n completion.setupShellInitFile();\n completionInstalled = true;\n log.success('Shell completions installed.');\n log.info('Restart your shell or source your shell config to activate.');\n } catch (cause) {\n log.warn(\n `Could not install completion: ${cause instanceof Error ? cause.message : String(cause)}`\n );\n }\n }\n }\n\n if (!jsonMode) {\n print('');\n print(style.bold(\"You're all set.\"));\n // Pick a \"Try:\" suggestion the user's key actually has scope for.\n // Pre-fix this was hardcoded to `markets list --status ACTIVE`,\n // which fails immediately for keys that only carry webhooks-read\n // / trades-read scope. Probe results drive the suggestion now.\n for (const line of pickNextStepHints(scopes)) {\n print(` ${style.dim('Try:')} ${line}`);\n }\n print(` ${style.dim(' :')} kash docs --json --quiet | jq '.subcommands[].name'`);\n print('');\n }\n\n return {\n ok: true,\n profile: written.profile,\n authenticated: true,\n health: {\n ok: healthOk,\n ...(healthLatency === undefined ? {} : { latencyMs: healthLatency }),\n ...(healthStatus === undefined ? {} : { status: healthStatus }),\n ...(healthVersion === undefined ? {} : { version: healthVersion }),\n },\n scopes,\n completionInstalled,\n };\n}\n\n/**\n * Probe-derived scope detection.\n *\n * Each named scope is mapped to a canary SDK call. We invoke the call\n * with a minimal pageSize and inspect the failure mode to classify:\n *\n * - call resolves → `'ok'` (scope present)\n * - throws KashAuthorizationError(INSUFFICIENT_SCOPE) → `'denied'`\n * - throws anything else → `'unknown'` (probe inconclusive,\n * don't claim the scope is missing)\n *\n * Read-only by design — write scopes (`trades:create`, `auth:manage`)\n * would require either a real mutation or a sentinel \"what scopes do\n * I have?\" endpoint, which the API doesn't currently expose.\n */\nasync function probeReadScopes(opts: {\n globals: GlobalOptions;\n profile: string;\n}): Promise<Record<string, ScopeProbeStatus>> {\n const { buildClient } = await import('../utils/client.js');\n const { client } = await buildClient({\n requireAuth: true,\n globals: { ...opts.globals, profile: opts.profile },\n });\n\n // Each canary is a (scope-name, call) pair. Call uses limit:1 to\n // minimise server load. Order is stable so the JSON envelope is\n // pinnable.\n const probes: ReadonlyArray<readonly [string, () => Promise<unknown>]> = [\n ['markets:read', async () => client.markets.list({ limit: 1 })],\n ['trades:read', async () => client.trades.list({ limit: 1 })],\n ['webhooks:read', async () => client.webhooks.list({ limit: 1 })],\n ];\n\n const result: Record<string, ScopeProbeStatus> = {};\n for (const [scope, call] of probes) {\n result[scope] = await classifyProbe(call);\n }\n return result;\n}\n\nasync function classifyProbe(call: () => Promise<unknown>): Promise<ScopeProbeStatus> {\n try {\n await call();\n return 'ok';\n } catch (cause) {\n // The SDK's KashAuthorizationError carries `code: 'INSUFFICIENT_SCOPE'`.\n // We duck-type rather than import the class to avoid a dependency\n // on the SDK's class identity (an instanceof check would couple\n // to the exact module-graph copy of @kashdao/sdk).\n if (cause !== null && typeof cause === 'object') {\n const code = (cause as { code?: unknown }).code;\n if (code === 'INSUFFICIENT_SCOPE') return 'denied';\n }\n return 'unknown';\n }\n}\n\nfunction renderScopesHuman(scopes: Record<string, ScopeProbeStatus>): void {\n const entries = Object.entries(scopes);\n if (entries.length === 0) return;\n const allOk = entries.every(([, v]) => v === 'ok');\n if (allOk) {\n log.success('Scope check OK — key carries every probed read scope.');\n return;\n }\n log.info('Scope detection (probed via canary read calls):');\n for (const [scope, status] of entries) {\n const label =\n status === 'ok'\n ? style.success('granted')\n : status === 'denied'\n ? style.warn('not granted')\n : style.dim('inconclusive');\n print(` ${style.dim(scope.padEnd(16))} ${label}`);\n }\n const denied = entries.filter(([, v]) => v === 'denied').map(([k]) => k);\n if (denied.length > 0) {\n log.info(\n `Request a key with broader scope by emailing ${KEY_REQUEST_EMAIL} if you need: ${denied.join(', ')}.`\n );\n }\n}\n\n/**\n * Pick `kash <…>` suggestions to surface in the post-setup banner,\n * gated on the scopes the probe confirmed. Order from most-common\n * to least-common; emit at most 2 lines so the banner stays terse.\n *\n * Falls back to a generic \"see `kash --help`\" when no probed scope\n * looks usable (rare — the probe already errored loudly in that\n * case, but we keep the wizard's last line useful regardless).\n */\nfunction pickNextStepHints(scopes: Record<string, ScopeProbeStatus>): readonly string[] {\n const hints: string[] = [];\n if (scopes['markets:read'] === 'ok') {\n hints.push('kash markets list --status ACTIVE');\n }\n if (scopes['trades:read'] === 'ok') {\n hints.push('kash trade list --json --quiet | jq -r \".data[].id\"');\n }\n if (scopes['webhooks:read'] === 'ok' && hints.length < 2) {\n hints.push('kash webhooks list --status failed,retrying');\n }\n if (hints.length === 0) {\n // Probe results were inconclusive or every read scope was denied.\n // The earlier `renderScopesHuman` already surfaced the per-scope\n // verdict; keep the banner non-actionable instead of pointing at\n // a command that 403s.\n hints.push('kash --help');\n }\n return hints.slice(0, 2);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/utils/stdin.ts
|
|
4
|
+
var UTF8_BOM = "\uFEFF";
|
|
5
|
+
async function readStdinTrimmed() {
|
|
6
|
+
const text = await slurp();
|
|
7
|
+
return stripBom(text).trim();
|
|
8
|
+
}
|
|
9
|
+
async function readStdinExact() {
|
|
10
|
+
const text = await slurp();
|
|
11
|
+
return stripBom(text);
|
|
12
|
+
}
|
|
13
|
+
function stripBom(s) {
|
|
14
|
+
return s.startsWith(UTF8_BOM) ? s.slice(1) : s;
|
|
15
|
+
}
|
|
16
|
+
async function slurp() {
|
|
17
|
+
const chunks = [];
|
|
18
|
+
for await (const chunk of process.stdin) {
|
|
19
|
+
chunks.push(chunk);
|
|
20
|
+
}
|
|
21
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
readStdinExact,
|
|
25
|
+
readStdinTrimmed,
|
|
26
|
+
stripBom
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=stdin-YW2CEQXU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/stdin.ts"],"sourcesContent":["/**\n * Shared helpers for reading from `process.stdin`.\n *\n * Every command that accepts piped input (the `--from-stdin` /\n * `--token-stdin` family, `webhooks replay -`, `config import -`,\n * `protocol userop submit -`) routes through this module so the\n * BOM-stripping and trim semantics stay uniform.\n *\n * **Why BOM-stripping matters.** Notepad, VSCode (when\n * `files.encoding=utf8bom`), some Excel exports, and many Windows\n * tools prepend U+FEFF to UTF-8 files. Pasting that file's contents\n * through a pipe sends those three bytes verbatim into the CLI. The\n * downstream consumer then sees `'\\uFEFFkash_live_…'` instead of\n * `'kash_live_…'` — the prefix check fails, the HMAC over the\n * captured webhook body diverges from what the receiver expects,\n * the JSON parser chokes on the leading non-whitespace character.\n *\n * One line of defence here removes a whole class of \"works on my\n * Mac, fails on Windows\" reports.\n */\n\n/** UTF-8 BOM (U+FEFF in UTF-8 form). Three bytes, code-point one. */\nconst UTF8_BOM = '\\uFEFF';\n\n/**\n * Slurp every byte from stdin, decode as UTF-8, strip a leading BOM,\n * and trim surrounding whitespace.\n *\n * Trim is included because most consumers want it — secret-store\n * commands (`pass`, `op read`) commonly emit a trailing newline that\n * fails downstream prefix/length checks. Callers that need the raw\n * shape (HMAC computation over webhook body bytes, where every byte\n * is signed) should use {@link readStdinExact} instead.\n */\nexport async function readStdinTrimmed(): Promise<string> {\n const text = await slurp();\n return stripBom(text).trim();\n}\n\n/**\n * Slurp every byte from stdin and decode as UTF-8 with a leading BOM\n * removed if present. Does NOT trim trailing whitespace — call sites\n * that sign the bytes (webhook replay) need byte-exact fidelity with\n * what the receiver will recompute.\n */\nexport async function readStdinExact(): Promise<string> {\n const text = await slurp();\n return stripBom(text);\n}\n\n/**\n * Strip a leading UTF-8 BOM, if present. Idempotent — calling it on\n * a string that doesn't have a BOM is a no-op.\n */\nexport function stripBom(s: string): string {\n return s.startsWith(UTF8_BOM) ? s.slice(1) : s;\n}\n\nasync function slurp(): Promise<string> {\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk as Buffer);\n }\n return Buffer.concat(chunks).toString('utf8');\n}\n"],"mappings":";;;AAsBA,IAAM,WAAW;AAYjB,eAAsB,mBAAoC;AACxD,QAAM,OAAO,MAAM,MAAM;AACzB,SAAO,SAAS,IAAI,EAAE,KAAK;AAC7B;AAQA,eAAsB,iBAAkC;AACtD,QAAM,OAAO,MAAM,MAAM;AACzB,SAAO,SAAS,IAAI;AACtB;AAMO,SAAS,SAAS,GAAmB;AAC1C,SAAO,EAAE,WAAW,QAAQ,IAAI,EAAE,MAAM,CAAC,IAAI;AAC/C;AAEA,eAAe,QAAyB;AACtC,QAAM,SAAmB,CAAC;AAC1B,mBAAiB,SAAS,QAAQ,OAAO;AACvC,WAAO,KAAK,KAAe;AAAA,EAC7B;AACA,SAAO,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM;AAC9C;","names":[]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
formatDate
|
|
4
|
+
} from "./chunk-BRK7KJ4O.js";
|
|
5
|
+
import {
|
|
6
|
+
buildClient
|
|
7
|
+
} from "./chunk-QJMF73M5.js";
|
|
8
|
+
import "./chunk-KMBMQIZ7.js";
|
|
9
|
+
import "./chunk-YHCG2SUC.js";
|
|
10
|
+
import "./chunk-BN2CUM42.js";
|
|
11
|
+
import {
|
|
12
|
+
readGlobals
|
|
13
|
+
} from "./chunk-LBIRQHX5.js";
|
|
14
|
+
import {
|
|
15
|
+
print,
|
|
16
|
+
printJson,
|
|
17
|
+
style
|
|
18
|
+
} from "./chunk-VIADBYFY.js";
|
|
19
|
+
import "./chunk-MIXOZU2S.js";
|
|
20
|
+
import {
|
|
21
|
+
CliValidationError,
|
|
22
|
+
toCliError
|
|
23
|
+
} from "./chunk-UZNSYATZ.js";
|
|
24
|
+
|
|
25
|
+
// src/commands/trace.ts
|
|
26
|
+
import { Command } from "commander";
|
|
27
|
+
var UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
28
|
+
var traceCommand = new Command("trace").description("Fetch the curated event timeline for a trade's correlation id.").argument(
|
|
29
|
+
"<correlationId>",
|
|
30
|
+
"correlation UUID \u2014 see `kash trade status <id> --json | jq -r .correlationId`"
|
|
31
|
+
).addHelpText(
|
|
32
|
+
"after",
|
|
33
|
+
`
|
|
34
|
+
Examples:
|
|
35
|
+
$ kash trace 33333333-3333-3333-3333-333333333333
|
|
36
|
+
$ kash trace 33333333-... --json --quiet | jq '.events[] | {type, occurredAt}'
|
|
37
|
+
|
|
38
|
+
Notes:
|
|
39
|
+
- Trace returns 404 (CONFLICT-style) if the correlation id is unknown OR your
|
|
40
|
+
API key does not own the trade behind it. This is by design \u2014 it prevents
|
|
41
|
+
enumeration of other users' correlation ids.
|
|
42
|
+
- Output is curated: raw event_data is NOT returned. Only allowlisted fields
|
|
43
|
+
(txHash, tokensOut, errorCode, etc.) appear.
|
|
44
|
+
`
|
|
45
|
+
).action(async (correlationId, _opts, cmd) => {
|
|
46
|
+
const globals = readGlobals(cmd);
|
|
47
|
+
if (!UUID_REGEX.test(correlationId)) {
|
|
48
|
+
throw new CliValidationError(
|
|
49
|
+
"<correlationId> must be a UUID.",
|
|
50
|
+
"Get one from `kash trade status <id> --json | jq -r .correlationId`.",
|
|
51
|
+
"correlationId"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
let trace;
|
|
55
|
+
try {
|
|
56
|
+
const { client } = await buildClient({ requireAuth: true, globals });
|
|
57
|
+
trace = await client.traces.get(correlationId);
|
|
58
|
+
} catch (cause) {
|
|
59
|
+
throw toCliError(cause);
|
|
60
|
+
}
|
|
61
|
+
if (globals.json) {
|
|
62
|
+
printJson(trace);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
renderHuman(trace);
|
|
66
|
+
});
|
|
67
|
+
function renderHuman(trace) {
|
|
68
|
+
print("");
|
|
69
|
+
print(` ${style.dim("Correlation ")} ${trace.correlationId}`);
|
|
70
|
+
print(` ${style.dim("Events ")} ${String(trace.events.length)}`);
|
|
71
|
+
print("");
|
|
72
|
+
if (trace.events.length === 0) {
|
|
73
|
+
print(style.dim(" (no events emitted yet \u2014 has the trade started?)"));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const event of trace.events) {
|
|
77
|
+
print(
|
|
78
|
+
` ${style.dim(formatDate(event.occurredAt))} ${formatType(event.type)} ${formatSummary(event)}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function formatType(type) {
|
|
83
|
+
return style.bold(type.replace(/^com\.kash\./, "").replace(/\.v\d+$/, ""));
|
|
84
|
+
}
|
|
85
|
+
function formatSummary(event) {
|
|
86
|
+
const d = event.data;
|
|
87
|
+
const parts = [];
|
|
88
|
+
if (d.tradeId) parts.push(`trade=${d.tradeId.slice(0, 8)}`);
|
|
89
|
+
if (d.side) parts.push(`side=${d.side}`);
|
|
90
|
+
if (d.outcomeIndex !== void 0) parts.push(`outcome=${String(d.outcomeIndex)}`);
|
|
91
|
+
if (d.amount) parts.push(`amount=${d.amount}`);
|
|
92
|
+
if (d.txHash) parts.push(`tx=${d.txHash.slice(0, 10)}\u2026`);
|
|
93
|
+
if (d.tokensOut) parts.push(`tokensOut=${d.tokensOut}`);
|
|
94
|
+
if (d.errorCode) parts.push(style.error(`error=${d.errorCode}`));
|
|
95
|
+
if (d.errorMessage) parts.push(style.error(`"${d.errorMessage}"`));
|
|
96
|
+
if (d.reason) parts.push(`reason="${d.reason}"`);
|
|
97
|
+
if (d.chainId !== void 0) parts.push(`chain=${String(d.chainId)}`);
|
|
98
|
+
return parts.length === 0 ? style.dim("(no curated fields)") : parts.join(" ");
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
traceCommand
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=trace-IZBYTUFO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/trace.ts"],"sourcesContent":["/**\n * `kash trace <correlationId>` — fetch the curated event timeline for\n * a single trade's correlation id.\n *\n * The correlation id is the identifier that ties together every event\n * a trade emits across the pipeline (intent parsing → funding →\n * bridge → execution → webhook delivery). Wraps the SDK's\n * `kash.traces.get(...)`. The server returns a sanitized timeline —\n * no raw event_data leaks — so this command is safe to share output\n * from.\n *\n * **Common workflow.** Get a trade's correlation id from\n * `kash trade status <id> --json | jq -r '.correlationId'`, then pass\n * it in here.\n */\n\nimport { Command } from 'commander';\n\nimport { CliValidationError, toCliError } from '../errors.js';\nimport { buildClient } from '../utils/client.js';\nimport { formatDate } from '../utils/formatting.js';\nimport { readGlobals } from '../utils/global-options.js';\nimport { print, printJson, style } from '../utils/output.js';\n\nimport type { TraceEvent, TraceResource } from '@kashdao/sdk';\n\n/** UUID v4-ish — the same shape the API enforces server-side. */\nconst UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\nexport const traceCommand = new Command('trace')\n .description(\"Fetch the curated event timeline for a trade's correlation id.\")\n .argument(\n '<correlationId>',\n 'correlation UUID — see `kash trade status <id> --json | jq -r .correlationId`'\n )\n .addHelpText(\n 'after',\n `\nExamples:\n $ kash trace 33333333-3333-3333-3333-333333333333\n $ kash trace 33333333-... --json --quiet | jq '.events[] | {type, occurredAt}'\n\nNotes:\n - Trace returns 404 (CONFLICT-style) if the correlation id is unknown OR your\n API key does not own the trade behind it. This is by design — it prevents\n enumeration of other users' correlation ids.\n - Output is curated: raw event_data is NOT returned. Only allowlisted fields\n (txHash, tokensOut, errorCode, etc.) appear.\n`\n )\n .action(async (correlationId: string, _opts, cmd: Command) => {\n const globals = readGlobals(cmd);\n\n if (!UUID_REGEX.test(correlationId)) {\n throw new CliValidationError(\n '<correlationId> must be a UUID.',\n 'Get one from `kash trade status <id> --json | jq -r .correlationId`.',\n 'correlationId'\n );\n }\n\n let trace: TraceResource;\n try {\n const { client } = await buildClient({ requireAuth: true, globals });\n trace = await client.traces.get(correlationId);\n } catch (cause) {\n throw toCliError(cause);\n }\n\n if (globals.json) {\n printJson(trace);\n return;\n }\n renderHuman(trace);\n });\n\n/**\n * Human-mode timeline. Each event is one line:\n * `[timestamp] event.type summary`\n * with summary derived from the curated `data` fields.\n */\nfunction renderHuman(trace: TraceResource): void {\n print('');\n print(` ${style.dim('Correlation ')} ${trace.correlationId}`);\n print(` ${style.dim('Events ')} ${String(trace.events.length)}`);\n print('');\n if (trace.events.length === 0) {\n print(style.dim(' (no events emitted yet — has the trade started?)'));\n return;\n }\n for (const event of trace.events) {\n print(\n ` ${style.dim(formatDate(event.occurredAt))} ${formatType(event.type)} ${formatSummary(event)}`\n );\n }\n}\n\n/**\n * Strip the `com.kash.` prefix and `.v1` suffix so the type column\n * stays readable: `com.kash.trade.executed.v1` → `trade.executed`.\n */\nfunction formatType(type: string): string {\n return style.bold(type.replace(/^com\\.kash\\./, '').replace(/\\.v\\d+$/, ''));\n}\n\n/**\n * One-line summary of the curated `data` bag. Picks the fields that\n * matter per event flavour without printing the full record.\n */\nfunction formatSummary(event: TraceEvent): string {\n const d = event.data;\n const parts: string[] = [];\n if (d.tradeId) parts.push(`trade=${d.tradeId.slice(0, 8)}`);\n if (d.side) parts.push(`side=${d.side}`);\n if (d.outcomeIndex !== undefined) parts.push(`outcome=${String(d.outcomeIndex)}`);\n if (d.amount) parts.push(`amount=${d.amount}`);\n if (d.txHash) parts.push(`tx=${d.txHash.slice(0, 10)}…`);\n if (d.tokensOut) parts.push(`tokensOut=${d.tokensOut}`);\n if (d.errorCode) parts.push(style.error(`error=${d.errorCode}`));\n if (d.errorMessage) parts.push(style.error(`\"${d.errorMessage}\"`));\n if (d.reason) parts.push(`reason=\"${d.reason}\"`);\n if (d.chainId !== undefined) parts.push(`chain=${String(d.chainId)}`);\n return parts.length === 0 ? style.dim('(no curated fields)') : parts.join(' ');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAgBA,SAAS,eAAe;AAWxB,IAAM,aAAa;AAEZ,IAAM,eAAe,IAAI,QAAQ,OAAO,EAC5C,YAAY,gEAAgE,EAC5E;AAAA,EACC;AAAA,EACA;AACF,EACC;AAAA,EACC;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAYF,EACC,OAAO,OAAO,eAAuB,OAAO,QAAiB;AAC5D,QAAM,UAAU,YAAY,GAAG;AAE/B,MAAI,CAAC,WAAW,KAAK,aAAa,GAAG;AACnC,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,YAAY,EAAE,aAAa,MAAM,QAAQ,CAAC;AACnE,YAAQ,MAAM,OAAO,OAAO,IAAI,aAAa;AAAA,EAC/C,SAAS,OAAO;AACd,UAAM,WAAW,KAAK;AAAA,EACxB;AAEA,MAAI,QAAQ,MAAM;AAChB,cAAU,KAAK;AACf;AAAA,EACF;AACA,cAAY,KAAK;AACnB,CAAC;AAOH,SAAS,YAAY,OAA4B;AAC/C,QAAM,EAAE;AACR,QAAM,KAAK,MAAM,IAAI,cAAc,CAAC,IAAI,MAAM,aAAa,EAAE;AAC7D,QAAM,KAAK,MAAM,IAAI,cAAc,CAAC,IAAI,OAAO,MAAM,OAAO,MAAM,CAAC,EAAE;AACrE,QAAM,EAAE;AACR,MAAI,MAAM,OAAO,WAAW,GAAG;AAC7B,UAAM,MAAM,IAAI,yDAAoD,CAAC;AACrE;AAAA,EACF;AACA,aAAW,SAAS,MAAM,QAAQ;AAChC;AAAA,MACE,KAAK,MAAM,IAAI,WAAW,MAAM,UAAU,CAAC,CAAC,KAAK,WAAW,MAAM,IAAI,CAAC,KAAK,cAAc,KAAK,CAAC;AAAA,IAClG;AAAA,EACF;AACF;AAMA,SAAS,WAAW,MAAsB;AACxC,SAAO,MAAM,KAAK,KAAK,QAAQ,gBAAgB,EAAE,EAAE,QAAQ,WAAW,EAAE,CAAC;AAC3E;AAMA,SAAS,cAAc,OAA2B;AAChD,QAAM,IAAI,MAAM;AAChB,QAAM,QAAkB,CAAC;AACzB,MAAI,EAAE,QAAS,OAAM,KAAK,SAAS,EAAE,QAAQ,MAAM,GAAG,CAAC,CAAC,EAAE;AAC1D,MAAI,EAAE,KAAM,OAAM,KAAK,QAAQ,EAAE,IAAI,EAAE;AACvC,MAAI,EAAE,iBAAiB,OAAW,OAAM,KAAK,WAAW,OAAO,EAAE,YAAY,CAAC,EAAE;AAChF,MAAI,EAAE,OAAQ,OAAM,KAAK,UAAU,EAAE,MAAM,EAAE;AAC7C,MAAI,EAAE,OAAQ,OAAM,KAAK,MAAM,EAAE,OAAO,MAAM,GAAG,EAAE,CAAC,QAAG;AACvD,MAAI,EAAE,UAAW,OAAM,KAAK,aAAa,EAAE,SAAS,EAAE;AACtD,MAAI,EAAE,UAAW,OAAM,KAAK,MAAM,MAAM,SAAS,EAAE,SAAS,EAAE,CAAC;AAC/D,MAAI,EAAE,aAAc,OAAM,KAAK,MAAM,MAAM,IAAI,EAAE,YAAY,GAAG,CAAC;AACjE,MAAI,EAAE,OAAQ,OAAM,KAAK,WAAW,EAAE,MAAM,GAAG;AAC/C,MAAI,EAAE,YAAY,OAAW,OAAM,KAAK,SAAS,OAAO,EAAE,OAAO,CAAC,EAAE;AACpE,SAAO,MAAM,WAAW,IAAI,MAAM,IAAI,qBAAqB,IAAI,MAAM,KAAK,GAAG;AAC/E;","names":[]}
|