tenjin-cli 0.1.0-alpha.1 → 0.1.0-alpha.2

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.
Files changed (133) hide show
  1. package/NOTICE.md +33 -0
  2. package/README.md +164 -16
  3. package/dist/{_esm-ZH2J4QEH.js → _esm-C5CRWCYU.js} +2 -2
  4. package/dist/_esm-FEASBWRP.js +881 -0
  5. package/dist/buy-U35DQ6XM.js +1598 -0
  6. package/dist/buy-U35DQ6XM.js.map +1 -0
  7. package/dist/candidate-RLYCQMF5.js +127 -0
  8. package/dist/candidate-RLYCQMF5.js.map +1 -0
  9. package/dist/{ccip-UCLT5VOW.js → ccip-NQXCEK2U.js} +6 -5
  10. package/dist/ccip-XR5NE5QK.js +19 -0
  11. package/dist/chunk-27ONARHV.js +171 -0
  12. package/dist/chunk-27ONARHV.js.map +1 -0
  13. package/dist/chunk-2I4QTOLB.js +11 -0
  14. package/dist/chunk-2I4QTOLB.js.map +1 -0
  15. package/dist/chunk-3OV7E5G5.js +18 -0
  16. package/dist/chunk-3OV7E5G5.js.map +1 -0
  17. package/dist/chunk-5CLSD5YN.js +209 -0
  18. package/dist/chunk-5CLSD5YN.js.map +1 -0
  19. package/dist/chunk-7KUW4MJD.js +59 -0
  20. package/dist/chunk-7KUW4MJD.js.map +1 -0
  21. package/dist/{doctor-3QBYN46N.js → chunk-7PHSZKQS.js} +88 -93
  22. package/dist/chunk-7PHSZKQS.js.map +1 -0
  23. package/dist/chunk-CYOF7ROH.js +70 -0
  24. package/dist/chunk-CYOF7ROH.js.map +1 -0
  25. package/dist/chunk-HIVZG55D.js +204 -0
  26. package/dist/chunk-HIVZG55D.js.map +1 -0
  27. package/dist/chunk-HSDCI7OV.js +7806 -0
  28. package/dist/chunk-HSDCI7OV.js.map +1 -0
  29. package/dist/{chunk-EZROUI5U.js → chunk-IYLHQMT5.js} +85 -9
  30. package/dist/chunk-IYLHQMT5.js.map +1 -0
  31. package/dist/chunk-JPHG2JW3.js +96 -0
  32. package/dist/chunk-JPHG2JW3.js.map +1 -0
  33. package/dist/{chunk-ZPZFPVDN.js → chunk-MUBUQVC3.js} +1 -1
  34. package/dist/chunk-MUBUQVC3.js.map +1 -0
  35. package/dist/chunk-N62ROTMC.js +79 -0
  36. package/dist/chunk-N62ROTMC.js.map +1 -0
  37. package/dist/{chunk-T4LKNY7T.js → chunk-NCE5NSN6.js} +166 -11
  38. package/dist/chunk-NCE5NSN6.js.map +1 -0
  39. package/dist/{chunk-YG2DO46P.js → chunk-OYMYCW7J.js} +18 -3
  40. package/dist/{chunk-YG2DO46P.js.map → chunk-OYMYCW7J.js.map} +1 -1
  41. package/dist/chunk-QNP7H47L.js +152 -0
  42. package/dist/chunk-QNP7H47L.js.map +1 -0
  43. package/dist/{chunk-Q53IYV2G.js → chunk-QVX5MWJZ.js} +20 -17
  44. package/dist/{chunk-Q53IYV2G.js.map → chunk-QVX5MWJZ.js.map} +1 -1
  45. package/dist/{chunk-CUWUHTIL.js → chunk-RBJPAZFI.js} +16 -202
  46. package/dist/chunk-RBJPAZFI.js.map +1 -0
  47. package/dist/chunk-RRN3YTK7.js +6199 -0
  48. package/dist/chunk-RRN3YTK7.js.map +1 -0
  49. package/dist/chunk-RY4XKNV3.js +140 -0
  50. package/dist/chunk-RY4XKNV3.js.map +1 -0
  51. package/dist/chunk-S3IGX57D.js +14682 -0
  52. package/dist/chunk-S3IGX57D.js.map +1 -0
  53. package/dist/{config-CEFTL5ZL.js → chunk-TFGSOPWR.js} +115 -18
  54. package/dist/chunk-TFGSOPWR.js.map +1 -0
  55. package/dist/chunk-TLCMMYLY.js +126 -0
  56. package/dist/chunk-TLCMMYLY.js.map +1 -0
  57. package/dist/{chunk-IPOWB3G3.js → chunk-UGX7YPX2.js} +3314 -306
  58. package/dist/chunk-UGX7YPX2.js.map +1 -0
  59. package/dist/chunk-UO22YNY4.js +69 -0
  60. package/dist/chunk-UO22YNY4.js.map +1 -0
  61. package/dist/chunk-UPIEAMGH.js +160 -0
  62. package/dist/chunk-UPIEAMGH.js.map +1 -0
  63. package/dist/chunk-XJIW7USF.js +217 -0
  64. package/dist/chunk-XJIW7USF.js.map +1 -0
  65. package/dist/{chunk-BRJHUQSY.js → chunk-YALGASEU.js} +1548 -1452
  66. package/dist/chunk-YALGASEU.js.map +1 -0
  67. package/dist/chunk-YIYQ2ZNT.js +286 -0
  68. package/dist/chunk-YIYQ2ZNT.js.map +1 -0
  69. package/dist/{chunk-DXPYFVKG.js → chunk-YN5SHIXQ.js} +6 -6
  70. package/dist/chunk-YN5SHIXQ.js.map +1 -0
  71. package/dist/chunk-Z52RA3NC.js +137 -0
  72. package/dist/chunk-Z52RA3NC.js.map +1 -0
  73. package/dist/{chunk-EXBMF5X7.js → chunk-Z6MFJV5Z.js} +151 -19
  74. package/dist/chunk-Z6MFJV5Z.js.map +1 -0
  75. package/dist/chunk-ZABPQJ3M.js +83 -0
  76. package/dist/chunk-ZABPQJ3M.js.map +1 -0
  77. package/dist/{chunk-LPRVHM7P.js → chunk-ZMHYXMO6.js} +1655 -114
  78. package/dist/chunk-ZMHYXMO6.js.map +1 -0
  79. package/dist/{chunk-3IMLRUZE.js → chunk-ZXTAQYNA.js} +6 -2
  80. package/dist/{cli-WA65NP4Y.js → cli-4NNBEQYH.js} +177 -177
  81. package/dist/cli-4NNBEQYH.js.map +1 -0
  82. package/dist/config-2VSXWWHK.js +21 -0
  83. package/dist/doctor-JJPNIBYH.js +20 -0
  84. package/dist/doctor-JJPNIBYH.js.map +1 -0
  85. package/dist/ethersCompat-Df4a6QIZ-3YZIAXDK.js +18 -0
  86. package/dist/ethersCompat-Df4a6QIZ-3YZIAXDK.js.map +1 -0
  87. package/dist/index.js +1 -1
  88. package/dist/inspect-JGYJWFDP.js +96 -0
  89. package/dist/inspect-JGYJWFDP.js.map +1 -0
  90. package/dist/install-ZQRMSRGW.js +378 -0
  91. package/dist/install-ZQRMSRGW.js.map +1 -0
  92. package/dist/lookup-WYVTOMVY.js +113 -0
  93. package/dist/lookup-WYVTOMVY.js.map +1 -0
  94. package/dist/{mine.wasm-GQECMSGN.js → mine.wasm-6PIT3DK6.js} +2 -2
  95. package/dist/outcome-HKKT6QIA.js +66 -0
  96. package/dist/outcome-HKKT6QIA.js.map +1 -0
  97. package/dist/publish-D76B3MBK.js +1480 -0
  98. package/dist/publish-D76B3MBK.js.map +1 -0
  99. package/dist/{secp256k1-BEZS642W.js → secp256k1-N3ZHU7LB.js} +5 -4
  100. package/dist/secp256k1-N3ZHU7LB.js.map +1 -0
  101. package/dist/usdc-JIAJW4SV.js +22 -0
  102. package/dist/usdc-JIAJW4SV.js.map +1 -0
  103. package/dist/viemAdapter-By13KInL-ZZE6UV4Y.js +14 -0
  104. package/dist/viemAdapter-By13KInL-ZZE6UV4Y.js.map +1 -0
  105. package/dist/{wallet-XD576HF3.js → wallet-6QKS6UHW.js} +28 -22
  106. package/dist/{wallet-XD576HF3.js.map → wallet-6QKS6UHW.js.map} +1 -1
  107. package/dist/wallet-PA54CRBL.js +30 -0
  108. package/dist/wallet-PA54CRBL.js.map +1 -0
  109. package/package.json +19 -16
  110. package/skills/tenjin/SKILL.md +199 -0
  111. package/skills/tenjin-publish/SKILL.md +96 -0
  112. package/skills/tenjin-search/SKILL.md +122 -0
  113. package/dist/chunk-BRJHUQSY.js.map +0 -1
  114. package/dist/chunk-CUWUHTIL.js.map +0 -1
  115. package/dist/chunk-DXPYFVKG.js.map +0 -1
  116. package/dist/chunk-EXBMF5X7.js.map +0 -1
  117. package/dist/chunk-EZROUI5U.js.map +0 -1
  118. package/dist/chunk-IPOWB3G3.js.map +0 -1
  119. package/dist/chunk-LPRVHM7P.js.map +0 -1
  120. package/dist/chunk-T4LKNY7T.js.map +0 -1
  121. package/dist/chunk-ZPZFPVDN.js.map +0 -1
  122. package/dist/cli-WA65NP4Y.js.map +0 -1
  123. package/dist/config-CEFTL5ZL.js.map +0 -1
  124. package/dist/doctor-3QBYN46N.js.map +0 -1
  125. package/dist/usdc-44R6XZF2.js +0 -18
  126. package/dist/wallet-ELBTBMXO.js +0 -20
  127. /package/dist/{_esm-ZH2J4QEH.js.map → _esm-C5CRWCYU.js.map} +0 -0
  128. /package/dist/{ccip-UCLT5VOW.js.map → _esm-FEASBWRP.js.map} +0 -0
  129. /package/dist/{chunk-3IMLRUZE.js.map → ccip-NQXCEK2U.js.map} +0 -0
  130. /package/dist/{secp256k1-BEZS642W.js.map → ccip-XR5NE5QK.js.map} +0 -0
  131. /package/dist/{usdc-44R6XZF2.js.map → chunk-ZXTAQYNA.js.map} +0 -0
  132. /package/dist/{wallet-ELBTBMXO.js.map → config-2VSXWWHK.js.map} +0 -0
  133. /package/dist/{mine.wasm-GQECMSGN.js.map → mine.wasm-6PIT3DK6.js.map} +0 -0
@@ -0,0 +1,286 @@
1
+ import { createRequire as __tenjinCreateRequire } from 'node:module'; const require = __tenjinCreateRequire(import.meta.url);
2
+ import {
3
+ ATOMIC_RE,
4
+ UUID_RE
5
+ } from "./chunk-3OV7E5G5.js";
6
+ import {
7
+ CLIENT_HEADER,
8
+ httpRequest,
9
+ trimSlash
10
+ } from "./chunk-QNP7H47L.js";
11
+ import {
12
+ CliError,
13
+ external_exports
14
+ } from "./chunk-OYMYCW7J.js";
15
+
16
+ // src/lib/agent-api.ts
17
+ var FRESH_WITHIN_RE = /^P(\d+)[DWMY]$/;
18
+ var CANONICAL_KEY_RE = /^[a-z][a-z0-9_]{0,31}$/;
19
+ function buildLookupRequest(input) {
20
+ const question = input.question.trim();
21
+ if (question.length === 0 || question.length > 512) {
22
+ throw new CliError("USAGE", "question must be 1 to 512 characters", {
23
+ fix: "Pass a non-empty question under 512 characters."
24
+ });
25
+ }
26
+ if (input.freshWithin !== void 0) {
27
+ const digits = FRESH_WITHIN_RE.exec(input.freshWithin)?.[1];
28
+ if (digits === void 0 || Number(digits) === 0) {
29
+ throw new CliError("USAGE", `Invalid --fresh-within: ${JSON.stringify(input.freshWithin)}`, {
30
+ fix: "Use a nonzero ISO-like window P<n>[DWMY], e.g. P30D, P2W, P1Y."
31
+ });
32
+ }
33
+ }
34
+ if (input.maxPrice !== void 0 && !ATOMIC_RE.test(input.maxPrice)) {
35
+ throw new CliError("USAGE", `Invalid --max-price: ${JSON.stringify(input.maxPrice)}`, {
36
+ fix: "Pass an atomic USDC integer, e.g. 100000 for $0.10."
37
+ });
38
+ }
39
+ const limit = input.limit ?? 5;
40
+ if (!Number.isInteger(limit) || limit < 1 || limit > 10) {
41
+ throw new CliError("USAGE", `Invalid --limit: ${JSON.stringify(input.limit)}`, {
42
+ fix: "Pass an integer between 1 and 10."
43
+ });
44
+ }
45
+ if (input.appliesTo !== void 0) {
46
+ const keys = Object.keys(input.appliesTo);
47
+ if (keys.length > 8) {
48
+ throw new CliError("USAGE", "Too many --applies-to keys (max 8).");
49
+ }
50
+ for (const [key, values] of Object.entries(input.appliesTo)) {
51
+ if (!CANONICAL_KEY_RE.test(key)) {
52
+ throw new CliError("USAGE", `Invalid appliesTo key: ${JSON.stringify(key)}`, {
53
+ fix: "Keys must be canonical lowercase identifiers, e.g. products=Vercel."
54
+ });
55
+ }
56
+ if (values.length === 0) {
57
+ throw new CliError("USAGE", `appliesTo key ${JSON.stringify(key)} has no values`, {
58
+ fix: "Give each --applies-to key at least one value."
59
+ });
60
+ }
61
+ if (values.length > 20) {
62
+ throw new CliError("USAGE", `Too many --applies-to values for ${key} (max 20).`);
63
+ }
64
+ for (const value of values) {
65
+ if (value.length === 0 || value.length > 120) {
66
+ throw new CliError(
67
+ "USAGE",
68
+ `Invalid --applies-to value for ${key}: must be 1 to 120 characters.`
69
+ );
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return {
75
+ schemaVersion: 1,
76
+ question,
77
+ ...input.freshWithin !== void 0 ? { freshWithin: input.freshWithin } : {},
78
+ ...input.maxPrice !== void 0 ? { maxPrice: input.maxPrice } : {},
79
+ ...input.appliesTo !== void 0 ? { appliesTo: input.appliesTo } : {},
80
+ limit
81
+ };
82
+ }
83
+ var lookupCandidateSchema = external_exports.object({
84
+ resourceId: external_exports.string().regex(UUID_RE, "resourceId must be a uuid"),
85
+ url: external_exports.string(),
86
+ title: external_exports.string(),
87
+ artifactType: external_exports.string(),
88
+ price: external_exports.string().regex(ATOMIC_RE, "price must be an atomic integer string"),
89
+ asOf: external_exports.string().nullable(),
90
+ validUntil: external_exports.string().nullable(),
91
+ temporalMode: external_exports.string(),
92
+ appliesTo: external_exports.record(external_exports.string(), external_exports.array(external_exports.string())),
93
+ questionsAnswered: external_exports.array(external_exports.string()),
94
+ tasksSupported: external_exports.array(external_exports.string()),
95
+ scope: external_exports.string().nullable(),
96
+ exclusions: external_exports.string().nullable(),
97
+ matchReasons: external_exports.array(external_exports.string()),
98
+ estimatedTokens: external_exports.number(),
99
+ creator: external_exports.object({ handle: external_exports.string() }).passthrough()
100
+ }).passthrough();
101
+ var lookupResponseSchema = external_exports.object({
102
+ schemaVersion: external_exports.literal(1),
103
+ lookupId: external_exports.string().regex(UUID_RE, "lookupId must be a uuid"),
104
+ decision: external_exports.enum(["CANDIDATES", "MISS"]),
105
+ calibration: external_exports.string(),
106
+ candidates: external_exports.array(lookupCandidateSchema).optional()
107
+ });
108
+ function apiFailure(url, result) {
109
+ const code = result.kind === "network" || result.kind === "timeout" ? "NETWORK_ERROR" : "API_UNREACHABLE";
110
+ return new CliError(code, `${url}: ${result.message}`, {
111
+ fix: "Check --base-url and your network, then retry."
112
+ });
113
+ }
114
+ function rateLimitError(url, header) {
115
+ const retryAfter = header("retry-after");
116
+ const seconds = retryAfter !== void 0 ? Number(retryAfter) : void 0;
117
+ return new CliError("RATE_LIMITED", `${url}: rate limited by the server.`, {
118
+ fix: seconds !== void 0 && Number.isFinite(seconds) ? `Retry after ${seconds}s.` : "Back off and retry shortly.",
119
+ details: {
120
+ ...seconds !== void 0 && Number.isFinite(seconds) ? { retryAfterSeconds: seconds } : {}
121
+ }
122
+ });
123
+ }
124
+ function serverErrorMessage(json) {
125
+ if (typeof json === "object" && json !== null) {
126
+ const rec = json;
127
+ const err = rec.error;
128
+ if (typeof err === "string") return err;
129
+ if (typeof err === "object" && err !== null) {
130
+ const m = err.message;
131
+ if (typeof m === "string") return m;
132
+ }
133
+ if (typeof rec.message === "string") return rec.message;
134
+ }
135
+ return void 0;
136
+ }
137
+ async function postLookup(body, opts) {
138
+ const url = `${trimSlash(opts.baseUrl)}/api/agent/lookup`;
139
+ const res = await httpRequest(url, {
140
+ method: "POST",
141
+ timeoutMs: opts.timeoutMs,
142
+ headers: {
143
+ "x-tenjin-client": CLIENT_HEADER,
144
+ ...opts.evalCohort === true ? { "x-tenjin-eval-cohort": "1" } : {}
145
+ },
146
+ jsonBody: body,
147
+ fetchImpl: opts.fetchImpl
148
+ });
149
+ if (!res.ok) throw apiFailure(url, res);
150
+ if (res.status === 429) throw rateLimitError(url, (n) => res.header(n));
151
+ if (res.status !== 200) {
152
+ throw new CliError(
153
+ "API_UNREACHABLE",
154
+ serverErrorMessage(res.json) ?? `Lookup failed (${res.status})`,
155
+ {
156
+ fix: "Retry; if it persists the lookup endpoint may be unavailable.",
157
+ details: res.json
158
+ }
159
+ );
160
+ }
161
+ const parsed = lookupResponseSchema.safeParse(res.json);
162
+ if (!parsed.success) {
163
+ throw new CliError("CONTRACT_MISMATCH", "Lookup response did not match the expected contract", {
164
+ fix: "Update tenjin-cli; the server contract may have changed.",
165
+ details: parsed.error.issues
166
+ });
167
+ }
168
+ return truncateResponse(parsed.data);
169
+ }
170
+ var CAND_BOUNDS = {
171
+ title: 200,
172
+ scope: 240,
173
+ exclusions: 240,
174
+ listItems: 4,
175
+ listItemChars: 160,
176
+ matchReasons: 3,
177
+ matchReasonChars: 80,
178
+ appliesToKeys: 6,
179
+ appliesToValues: 5,
180
+ appliesToValueChars: 80
181
+ };
182
+ function truncateResponse(res) {
183
+ if (res.candidates === void 0) return res;
184
+ return { ...res, candidates: res.candidates.map(truncateCandidate) };
185
+ }
186
+ function truncateCandidate(c) {
187
+ const cap = (s, n) => s.length > n ? s.slice(0, n) : s;
188
+ const capList = (list, items, chars) => list.slice(0, items).map((s) => cap(s, chars));
189
+ const appliesTo = {};
190
+ for (const key of Object.keys(c.appliesTo).slice(0, CAND_BOUNDS.appliesToKeys)) {
191
+ appliesTo[cap(key, CAND_BOUNDS.appliesToValueChars)] = capList(
192
+ c.appliesTo[key] ?? [],
193
+ CAND_BOUNDS.appliesToValues,
194
+ CAND_BOUNDS.appliesToValueChars
195
+ );
196
+ }
197
+ return {
198
+ ...c,
199
+ title: cap(c.title, CAND_BOUNDS.title),
200
+ scope: c.scope === null ? null : cap(c.scope, CAND_BOUNDS.scope),
201
+ exclusions: c.exclusions === null ? null : cap(c.exclusions, CAND_BOUNDS.exclusions),
202
+ questionsAnswered: capList(
203
+ c.questionsAnswered,
204
+ CAND_BOUNDS.listItems,
205
+ CAND_BOUNDS.listItemChars
206
+ ),
207
+ tasksSupported: capList(c.tasksSupported, CAND_BOUNDS.listItems, CAND_BOUNDS.listItemChars),
208
+ matchReasons: capList(c.matchReasons, CAND_BOUNDS.matchReasons, CAND_BOUNDS.matchReasonChars),
209
+ appliesTo
210
+ };
211
+ }
212
+ var OUTCOME_STATUSES = [
213
+ "used",
214
+ "partially_used",
215
+ "rejected",
216
+ "regenerated",
217
+ "purchase_declined"
218
+ ];
219
+ var OUTCOME_STATUS_VALUES = OUTCOME_STATUSES;
220
+ var CONTENT_HASH_RE = /^sha256:[0-9a-f]{64}$/;
221
+ function buildOutcomeItem(input) {
222
+ if (!OUTCOME_STATUS_VALUES.includes(input.status)) {
223
+ throw new CliError("USAGE", `Invalid outcome status: ${JSON.stringify(input.status)}`, {
224
+ fix: `Use one of: ${OUTCOME_STATUSES.join(", ")}.`
225
+ });
226
+ }
227
+ if (input.resourceId !== void 0 && !UUID_RE.test(input.resourceId)) {
228
+ throw new CliError("USAGE", `Invalid --resource id: ${JSON.stringify(input.resourceId)}`, {
229
+ fix: "Pass the resourceId (a uuid) from a lookup candidate."
230
+ });
231
+ }
232
+ if (input.contentHash !== void 0 && !CONTENT_HASH_RE.test(input.contentHash)) {
233
+ throw new CliError("USAGE", `Invalid content hash: ${JSON.stringify(input.contentHash)}`, {
234
+ fix: 'A content hash is "sha256:<64 hex>".'
235
+ });
236
+ }
237
+ return {
238
+ status: input.status,
239
+ ...input.resourceId !== void 0 ? { resourceId: input.resourceId } : {},
240
+ ...input.contentHash !== void 0 ? { contentHash: input.contentHash } : {}
241
+ };
242
+ }
243
+ var LOOKUP_ID_RE = UUID_RE;
244
+ async function postOutcomes(lookupId, items, opts) {
245
+ if (!LOOKUP_ID_RE.test(lookupId)) {
246
+ throw new CliError("USAGE", `Invalid lookup id: ${JSON.stringify(lookupId)}`, {
247
+ fix: "Pass the lookupId from a prior lookup (or use --last)."
248
+ });
249
+ }
250
+ if (items.length === 0 || items.length > 10) {
251
+ throw new CliError("USAGE", "An outcome report carries 1 to 10 items", {
252
+ fix: "Report between 1 and 10 outcomes per call."
253
+ });
254
+ }
255
+ const url = `${trimSlash(opts.baseUrl)}/api/agent/lookups/${lookupId}/outcomes`;
256
+ const res = await httpRequest(url, {
257
+ method: "POST",
258
+ timeoutMs: opts.timeoutMs,
259
+ headers: { "x-tenjin-client": CLIENT_HEADER },
260
+ jsonBody: items.length === 1 ? items[0] : items,
261
+ fetchImpl: opts.fetchImpl
262
+ });
263
+ if (!res.ok) throw apiFailure(url, res);
264
+ if (res.status === 429) throw rateLimitError(url, (n) => res.header(n));
265
+ if (res.status !== 202) {
266
+ throw new CliError(
267
+ "API_UNREACHABLE",
268
+ serverErrorMessage(res.json) ?? `Outcome report failed (${res.status})`,
269
+ {
270
+ fix: "Check the status vocabulary and lookup id, then retry.",
271
+ details: res.json
272
+ }
273
+ );
274
+ }
275
+ const accepted = typeof res.json === "object" && res.json !== null && "accepted" in res.json ? Number(res.json.accepted) : items.length;
276
+ return { accepted: Number.isFinite(accepted) ? accepted : items.length };
277
+ }
278
+
279
+ export {
280
+ buildLookupRequest,
281
+ rateLimitError,
282
+ postLookup,
283
+ buildOutcomeItem,
284
+ postOutcomes
285
+ };
286
+ //# sourceMappingURL=chunk-YIYQ2ZNT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/agent-api.ts"],"sourcesContent":["import { z } from 'zod';\nimport { CliError } from './errors';\nimport { httpRequest, type HttpResult } from './http';\nimport { CLIENT_HEADER } from './client-meta';\nimport { ATOMIC_RE, UUID_RE } from './ids';\nimport { trimSlash } from './url';\n\n/**\n * The lookup + outcome HTTP contract (A2, tenjin#370). Request building and\n * response validation live here; the wire shape is validated defensively, an\n * unknown `schemaVersion` degrades to a parse refusal rather than a guess, and\n * unrecognized candidate fields are ignored (forward-compatible), per spec 10.\n *\n * These endpoints are anonymous: no wallet, no SIWX. The only header of note is\n * `X-Tenjin-Client`, which attributes a later purchase back to the lookup flow.\n */\n\nconst FRESH_WITHIN_RE = /^P(\\d+)[DWMY]$/;\nconst CANONICAL_KEY_RE = /^[a-z][a-z0-9_]{0,31}$/;\n\n/** Client-side lookup request (mirrors lib/lookup.ts lookupRequestSchema bounds\n * so a malformed flag fails locally as USAGE, before a round trip). */\nexport interface LookupInput {\n question: string;\n freshWithin?: string;\n maxPrice?: string;\n appliesTo?: Record<string, string[]>;\n limit?: number;\n}\n\nexport interface LookupRequestBody {\n schemaVersion: 1;\n question: string;\n freshWithin?: string;\n maxPrice?: string;\n appliesTo?: Record<string, string[]>;\n limit: number;\n}\n\nexport function buildLookupRequest(input: LookupInput): LookupRequestBody {\n const question = input.question.trim();\n if (question.length === 0 || question.length > 512) {\n throw new CliError('USAGE', 'question must be 1 to 512 characters', {\n fix: 'Pass a non-empty question under 512 characters.',\n });\n }\n if (input.freshWithin !== undefined) {\n // The server rejects ANY zero-valued duration (P0D, P0W, P00M, ...), so a\n // zero window fails here as USAGE instead of a remote 400.\n const digits = FRESH_WITHIN_RE.exec(input.freshWithin)?.[1];\n if (digits === undefined || Number(digits) === 0) {\n throw new CliError('USAGE', `Invalid --fresh-within: ${JSON.stringify(input.freshWithin)}`, {\n fix: 'Use a nonzero ISO-like window P<n>[DWMY], e.g. P30D, P2W, P1Y.',\n });\n }\n }\n if (input.maxPrice !== undefined && !ATOMIC_RE.test(input.maxPrice)) {\n throw new CliError('USAGE', `Invalid --max-price: ${JSON.stringify(input.maxPrice)}`, {\n fix: 'Pass an atomic USDC integer, e.g. 100000 for $0.10.',\n });\n }\n const limit = input.limit ?? 5;\n if (!Number.isInteger(limit) || limit < 1 || limit > 10) {\n throw new CliError('USAGE', `Invalid --limit: ${JSON.stringify(input.limit)}`, {\n fix: 'Pass an integer between 1 and 10.',\n });\n }\n if (input.appliesTo !== undefined) {\n // Mirror the server's strictObject bounds (<=8 keys, <=20 values/key,\n // 1..120 chars/value) so a bad filter fails locally, never as a remote 400.\n const keys = Object.keys(input.appliesTo);\n if (keys.length > 8) {\n throw new CliError('USAGE', 'Too many --applies-to keys (max 8).');\n }\n for (const [key, values] of Object.entries(input.appliesTo)) {\n if (!CANONICAL_KEY_RE.test(key)) {\n throw new CliError('USAGE', `Invalid appliesTo key: ${JSON.stringify(key)}`, {\n fix: 'Keys must be canonical lowercase identifiers, e.g. products=Vercel.',\n });\n }\n if (values.length === 0) {\n throw new CliError('USAGE', `appliesTo key ${JSON.stringify(key)} has no values`, {\n fix: 'Give each --applies-to key at least one value.',\n });\n }\n if (values.length > 20) {\n throw new CliError('USAGE', `Too many --applies-to values for ${key} (max 20).`);\n }\n for (const value of values) {\n if (value.length === 0 || value.length > 120) {\n throw new CliError(\n 'USAGE',\n `Invalid --applies-to value for ${key}: must be 1 to 120 characters.`,\n );\n }\n }\n }\n }\n return {\n schemaVersion: 1,\n question,\n ...(input.freshWithin !== undefined ? { freshWithin: input.freshWithin } : {}),\n ...(input.maxPrice !== undefined ? { maxPrice: input.maxPrice } : {}),\n ...(input.appliesTo !== undefined ? { appliesTo: input.appliesTo } : {}),\n limit,\n };\n}\n\n// Response schemas. `.passthrough()` on the candidate keeps unknown future fields\n// instead of stripping them (forward-compatible), while still requiring the\n// contract fields this CLI reads. `decision` is uppercase on the wire.\nexport const lookupCandidateSchema = z\n .object({\n resourceId: z.string().regex(UUID_RE, 'resourceId must be a uuid'),\n url: z.string(),\n title: z.string(),\n artifactType: z.string(),\n price: z.string().regex(ATOMIC_RE, 'price must be an atomic integer string'),\n asOf: z.string().nullable(),\n validUntil: z.string().nullable(),\n temporalMode: z.string(),\n appliesTo: z.record(z.string(), z.array(z.string())),\n questionsAnswered: z.array(z.string()),\n tasksSupported: z.array(z.string()),\n scope: z.string().nullable(),\n exclusions: z.string().nullable(),\n matchReasons: z.array(z.string()),\n estimatedTokens: z.number(),\n creator: z.object({ handle: z.string() }).passthrough(),\n })\n .passthrough();\n\nexport type LookupCandidate = z.infer<typeof lookupCandidateSchema>;\n\nexport const lookupResponseSchema = z.object({\n schemaVersion: z.literal(1),\n lookupId: z.string().regex(UUID_RE, 'lookupId must be a uuid'),\n decision: z.enum(['CANDIDATES', 'MISS']),\n calibration: z.string(),\n candidates: z.array(lookupCandidateSchema).optional(),\n});\n\nexport type LookupResponse = z.infer<typeof lookupResponseSchema>;\n\nexport interface AgentApiOptions {\n baseUrl: string;\n timeoutMs: number;\n fetchImpl?: typeof fetch;\n /** Spec 09 §3 evaluation-cohort opt-in: sends X-Tenjin-Eval-Cohort: 1. */\n evalCohort?: boolean;\n}\n\n/** Turn a non-2xx / transport HttpResult into the CLI error contract. */\nfunction apiFailure(url: string, result: Exclude<HttpResult, { ok: true }>): CliError {\n const code =\n result.kind === 'network' || result.kind === 'timeout' ? 'NETWORK_ERROR' : 'API_UNREACHABLE';\n return new CliError(code, `${url}: ${result.message}`, {\n fix: 'Check --base-url and your network, then retry.',\n });\n}\n\n/** 429 with Retry-After: a recoverable pause, not an outage; agents branch on it. */\nexport function rateLimitError(\n url: string,\n header: (name: string) => string | undefined,\n): CliError {\n const retryAfter = header('retry-after');\n const seconds = retryAfter !== undefined ? Number(retryAfter) : undefined;\n return new CliError('RATE_LIMITED', `${url}: rate limited by the server.`, {\n fix:\n seconds !== undefined && Number.isFinite(seconds)\n ? `Retry after ${seconds}s.`\n : 'Back off and retry shortly.',\n details: {\n ...(seconds !== undefined && Number.isFinite(seconds) ? { retryAfterSeconds: seconds } : {}),\n },\n });\n}\n\nfunction serverErrorMessage(json: unknown): string | undefined {\n if (typeof json === 'object' && json !== null) {\n const rec = json as Record<string, unknown>;\n const err = rec.error;\n if (typeof err === 'string') return err;\n if (typeof err === 'object' && err !== null) {\n const m = (err as Record<string, unknown>).message;\n if (typeof m === 'string') return m;\n }\n if (typeof rec.message === 'string') return rec.message;\n }\n return undefined;\n}\n\nexport async function postLookup(\n body: LookupRequestBody,\n opts: AgentApiOptions,\n): Promise<LookupResponse> {\n const url = `${trimSlash(opts.baseUrl)}/api/agent/lookup`;\n const res = await httpRequest(url, {\n method: 'POST',\n timeoutMs: opts.timeoutMs,\n headers: {\n 'x-tenjin-client': CLIENT_HEADER,\n ...(opts.evalCohort === true ? { 'x-tenjin-eval-cohort': '1' } : {}),\n },\n jsonBody: body,\n fetchImpl: opts.fetchImpl,\n });\n if (!res.ok) throw apiFailure(url, res);\n if (res.status === 429) throw rateLimitError(url, (n) => res.header(n));\n if (res.status !== 200) {\n throw new CliError(\n 'API_UNREACHABLE',\n serverErrorMessage(res.json) ?? `Lookup failed (${res.status})`,\n {\n fix: 'Retry; if it persists the lookup endpoint may be unavailable.',\n details: res.json,\n },\n );\n }\n const parsed = lookupResponseSchema.safeParse(res.json);\n if (!parsed.success) {\n throw new CliError('CONTRACT_MISMATCH', 'Lookup response did not match the expected contract', {\n fix: 'Update tenjin-cli; the server contract may have changed.',\n details: parsed.error.issues,\n });\n }\n return truncateResponse(parsed.data);\n}\n\n/**\n * The server bounds every card string (response budget ~1k tokens, spec 09);\n * re-apply the same caps defensively so a misbehaving server cannot blow up an\n * agent transcript through the CLI (spec 10 response-size discipline).\n */\nconst CAND_BOUNDS = {\n title: 200,\n scope: 240,\n exclusions: 240,\n listItems: 4,\n listItemChars: 160,\n matchReasons: 3,\n matchReasonChars: 80,\n appliesToKeys: 6,\n appliesToValues: 5,\n appliesToValueChars: 80,\n} as const;\n\nfunction truncateResponse(res: LookupResponse): LookupResponse {\n if (res.candidates === undefined) return res;\n return { ...res, candidates: res.candidates.map(truncateCandidate) };\n}\n\nfunction truncateCandidate(c: LookupCandidate): LookupCandidate {\n const cap = (s: string, n: number): string => (s.length > n ? s.slice(0, n) : s);\n const capList = (list: string[], items: number, chars: number): string[] =>\n list.slice(0, items).map((s) => cap(s, chars));\n const appliesTo: Record<string, string[]> = {};\n for (const key of Object.keys(c.appliesTo).slice(0, CAND_BOUNDS.appliesToKeys)) {\n appliesTo[cap(key, CAND_BOUNDS.appliesToValueChars)] = capList(\n c.appliesTo[key] ?? [],\n CAND_BOUNDS.appliesToValues,\n CAND_BOUNDS.appliesToValueChars,\n );\n }\n return {\n ...c,\n title: cap(c.title, CAND_BOUNDS.title),\n scope: c.scope === null ? null : cap(c.scope, CAND_BOUNDS.scope),\n exclusions: c.exclusions === null ? null : cap(c.exclusions, CAND_BOUNDS.exclusions),\n questionsAnswered: capList(\n c.questionsAnswered,\n CAND_BOUNDS.listItems,\n CAND_BOUNDS.listItemChars,\n ),\n tasksSupported: capList(c.tasksSupported, CAND_BOUNDS.listItems, CAND_BOUNDS.listItemChars),\n matchReasons: capList(c.matchReasons, CAND_BOUNDS.matchReasons, CAND_BOUNDS.matchReasonChars),\n appliesTo,\n };\n}\n\nconst OUTCOME_STATUSES = [\n 'used',\n 'partially_used',\n 'rejected',\n 'regenerated',\n 'purchase_declined',\n] as const;\nexport type OutcomeStatus = (typeof OUTCOME_STATUSES)[number];\nexport const OUTCOME_STATUS_VALUES: readonly string[] = OUTCOME_STATUSES;\n\nconst CONTENT_HASH_RE = /^sha256:[0-9a-f]{64}$/;\n\nexport interface OutcomeInput {\n status: string;\n resourceId?: string;\n contentHash?: string;\n}\n\nexport interface OutcomeBodyItem {\n status: OutcomeStatus;\n resourceId?: string;\n contentHash?: string;\n}\n\nexport function buildOutcomeItem(input: OutcomeInput): OutcomeBodyItem {\n if (!OUTCOME_STATUS_VALUES.includes(input.status)) {\n throw new CliError('USAGE', `Invalid outcome status: ${JSON.stringify(input.status)}`, {\n fix: `Use one of: ${OUTCOME_STATUSES.join(', ')}.`,\n });\n }\n if (input.resourceId !== undefined && !UUID_RE.test(input.resourceId)) {\n throw new CliError('USAGE', `Invalid --resource id: ${JSON.stringify(input.resourceId)}`, {\n fix: 'Pass the resourceId (a uuid) from a lookup candidate.',\n });\n }\n if (input.contentHash !== undefined && !CONTENT_HASH_RE.test(input.contentHash)) {\n throw new CliError('USAGE', `Invalid content hash: ${JSON.stringify(input.contentHash)}`, {\n fix: 'A content hash is \"sha256:<64 hex>\".',\n });\n }\n return {\n status: input.status as OutcomeStatus,\n ...(input.resourceId !== undefined ? { resourceId: input.resourceId } : {}),\n ...(input.contentHash !== undefined ? { contentHash: input.contentHash } : {}),\n };\n}\n\nconst LOOKUP_ID_RE = UUID_RE;\n\nexport async function postOutcomes(\n lookupId: string,\n items: OutcomeBodyItem[],\n opts: AgentApiOptions,\n): Promise<{ accepted: number }> {\n if (!LOOKUP_ID_RE.test(lookupId)) {\n throw new CliError('USAGE', `Invalid lookup id: ${JSON.stringify(lookupId)}`, {\n fix: 'Pass the lookupId from a prior lookup (or use --last).',\n });\n }\n if (items.length === 0 || items.length > 10) {\n throw new CliError('USAGE', 'An outcome report carries 1 to 10 items', {\n fix: 'Report between 1 and 10 outcomes per call.',\n });\n }\n const url = `${trimSlash(opts.baseUrl)}/api/agent/lookups/${lookupId}/outcomes`;\n const res = await httpRequest(url, {\n method: 'POST',\n timeoutMs: opts.timeoutMs,\n headers: { 'x-tenjin-client': CLIENT_HEADER },\n jsonBody: items.length === 1 ? items[0] : items,\n fetchImpl: opts.fetchImpl,\n });\n if (!res.ok) throw apiFailure(url, res);\n if (res.status === 429) throw rateLimitError(url, (n) => res.header(n));\n // 202 is the only success; the body is a uniform { accepted } regardless of\n // whether the lookupId existed (no existence oracle, by design).\n if (res.status !== 202) {\n throw new CliError(\n 'API_UNREACHABLE',\n serverErrorMessage(res.json) ?? `Outcome report failed (${res.status})`,\n {\n fix: 'Check the status vocabulary and lookup id, then retry.',\n details: res.json,\n },\n );\n }\n const accepted =\n typeof res.json === 'object' && res.json !== null && 'accepted' in res.json\n ? Number((res.json as { accepted: unknown }).accepted)\n : items.length;\n return { accepted: Number.isFinite(accepted) ? accepted : items.length };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,IAAM,kBAAkB;AACxB,IAAM,mBAAmB;AAqBlB,SAAS,mBAAmB,OAAuC;AACxE,QAAM,WAAW,MAAM,SAAS,KAAK;AACrC,MAAI,SAAS,WAAW,KAAK,SAAS,SAAS,KAAK;AAClD,UAAM,IAAI,SAAS,SAAS,wCAAwC;AAAA,MAClE,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AACA,MAAI,MAAM,gBAAgB,QAAW;AAGnC,UAAM,SAAS,gBAAgB,KAAK,MAAM,WAAW,IAAI,CAAC;AAC1D,QAAI,WAAW,UAAa,OAAO,MAAM,MAAM,GAAG;AAChD,YAAM,IAAI,SAAS,SAAS,2BAA2B,KAAK,UAAU,MAAM,WAAW,CAAC,IAAI;AAAA,QAC1F,KAAK;AAAA,MACP,CAAC;AAAA,IACH;AAAA,EACF;AACA,MAAI,MAAM,aAAa,UAAa,CAAC,UAAU,KAAK,MAAM,QAAQ,GAAG;AACnE,UAAM,IAAI,SAAS,SAAS,wBAAwB,KAAK,UAAU,MAAM,QAAQ,CAAC,IAAI;AAAA,MACpF,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AACA,QAAM,QAAQ,MAAM,SAAS;AAC7B,MAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,KAAK,QAAQ,IAAI;AACvD,UAAM,IAAI,SAAS,SAAS,oBAAoB,KAAK,UAAU,MAAM,KAAK,CAAC,IAAI;AAAA,MAC7E,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AACA,MAAI,MAAM,cAAc,QAAW;AAGjC,UAAM,OAAO,OAAO,KAAK,MAAM,SAAS;AACxC,QAAI,KAAK,SAAS,GAAG;AACnB,YAAM,IAAI,SAAS,SAAS,qCAAqC;AAAA,IACnE;AACA,eAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,MAAM,SAAS,GAAG;AAC3D,UAAI,CAAC,iBAAiB,KAAK,GAAG,GAAG;AAC/B,cAAM,IAAI,SAAS,SAAS,0BAA0B,KAAK,UAAU,GAAG,CAAC,IAAI;AAAA,UAC3E,KAAK;AAAA,QACP,CAAC;AAAA,MACH;AACA,UAAI,OAAO,WAAW,GAAG;AACvB,cAAM,IAAI,SAAS,SAAS,iBAAiB,KAAK,UAAU,GAAG,CAAC,kBAAkB;AAAA,UAChF,KAAK;AAAA,QACP,CAAC;AAAA,MACH;AACA,UAAI,OAAO,SAAS,IAAI;AACtB,cAAM,IAAI,SAAS,SAAS,oCAAoC,GAAG,YAAY;AAAA,MACjF;AACA,iBAAW,SAAS,QAAQ;AAC1B,YAAI,MAAM,WAAW,KAAK,MAAM,SAAS,KAAK;AAC5C,gBAAM,IAAI;AAAA,YACR;AAAA,YACA,kCAAkC,GAAG;AAAA,UACvC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AAAA,IACL,eAAe;AAAA,IACf;AAAA,IACA,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,YAAY,IAAI,CAAC;AAAA,IAC5E,GAAI,MAAM,aAAa,SAAY,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;AAAA,IACnE,GAAI,MAAM,cAAc,SAAY,EAAE,WAAW,MAAM,UAAU,IAAI,CAAC;AAAA,IACtE;AAAA,EACF;AACF;AAKO,IAAM,wBAAwB,iBAClC,OAAO;AAAA,EACN,YAAY,iBAAE,OAAO,EAAE,MAAM,SAAS,2BAA2B;AAAA,EACjE,KAAK,iBAAE,OAAO;AAAA,EACd,OAAO,iBAAE,OAAO;AAAA,EAChB,cAAc,iBAAE,OAAO;AAAA,EACvB,OAAO,iBAAE,OAAO,EAAE,MAAM,WAAW,wCAAwC;AAAA,EAC3E,MAAM,iBAAE,OAAO,EAAE,SAAS;AAAA,EAC1B,YAAY,iBAAE,OAAO,EAAE,SAAS;AAAA,EAChC,cAAc,iBAAE,OAAO;AAAA,EACvB,WAAW,iBAAE,OAAO,iBAAE,OAAO,GAAG,iBAAE,MAAM,iBAAE,OAAO,CAAC,CAAC;AAAA,EACnD,mBAAmB,iBAAE,MAAM,iBAAE,OAAO,CAAC;AAAA,EACrC,gBAAgB,iBAAE,MAAM,iBAAE,OAAO,CAAC;AAAA,EAClC,OAAO,iBAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,YAAY,iBAAE,OAAO,EAAE,SAAS;AAAA,EAChC,cAAc,iBAAE,MAAM,iBAAE,OAAO,CAAC;AAAA,EAChC,iBAAiB,iBAAE,OAAO;AAAA,EAC1B,SAAS,iBAAE,OAAO,EAAE,QAAQ,iBAAE,OAAO,EAAE,CAAC,EAAE,YAAY;AACxD,CAAC,EACA,YAAY;AAIR,IAAM,uBAAuB,iBAAE,OAAO;AAAA,EAC3C,eAAe,iBAAE,QAAQ,CAAC;AAAA,EAC1B,UAAU,iBAAE,OAAO,EAAE,MAAM,SAAS,yBAAyB;AAAA,EAC7D,UAAU,iBAAE,KAAK,CAAC,cAAc,MAAM,CAAC;AAAA,EACvC,aAAa,iBAAE,OAAO;AAAA,EACtB,YAAY,iBAAE,MAAM,qBAAqB,EAAE,SAAS;AACtD,CAAC;AAaD,SAAS,WAAW,KAAa,QAAqD;AACpF,QAAM,OACJ,OAAO,SAAS,aAAa,OAAO,SAAS,YAAY,kBAAkB;AAC7E,SAAO,IAAI,SAAS,MAAM,GAAG,GAAG,KAAK,OAAO,OAAO,IAAI;AAAA,IACrD,KAAK;AAAA,EACP,CAAC;AACH;AAGO,SAAS,eACd,KACA,QACU;AACV,QAAM,aAAa,OAAO,aAAa;AACvC,QAAM,UAAU,eAAe,SAAY,OAAO,UAAU,IAAI;AAChE,SAAO,IAAI,SAAS,gBAAgB,GAAG,GAAG,iCAAiC;AAAA,IACzE,KACE,YAAY,UAAa,OAAO,SAAS,OAAO,IAC5C,eAAe,OAAO,OACtB;AAAA,IACN,SAAS;AAAA,MACP,GAAI,YAAY,UAAa,OAAO,SAAS,OAAO,IAAI,EAAE,mBAAmB,QAAQ,IAAI,CAAC;AAAA,IAC5F;AAAA,EACF,CAAC;AACH;AAEA,SAAS,mBAAmB,MAAmC;AAC7D,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,UAAM,MAAM;AACZ,UAAM,MAAM,IAAI;AAChB,QAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAI,OAAO,QAAQ,YAAY,QAAQ,MAAM;AAC3C,YAAM,IAAK,IAAgC;AAC3C,UAAI,OAAO,MAAM,SAAU,QAAO;AAAA,IACpC;AACA,QAAI,OAAO,IAAI,YAAY,SAAU,QAAO,IAAI;AAAA,EAClD;AACA,SAAO;AACT;AAEA,eAAsB,WACpB,MACA,MACyB;AACzB,QAAM,MAAM,GAAG,UAAU,KAAK,OAAO,CAAC;AACtC,QAAM,MAAM,MAAM,YAAY,KAAK;AAAA,IACjC,QAAQ;AAAA,IACR,WAAW,KAAK;AAAA,IAChB,SAAS;AAAA,MACP,mBAAmB;AAAA,MACnB,GAAI,KAAK,eAAe,OAAO,EAAE,wBAAwB,IAAI,IAAI,CAAC;AAAA,IACpE;AAAA,IACA,UAAU;AAAA,IACV,WAAW,KAAK;AAAA,EAClB,CAAC;AACD,MAAI,CAAC,IAAI,GAAI,OAAM,WAAW,KAAK,GAAG;AACtC,MAAI,IAAI,WAAW,IAAK,OAAM,eAAe,KAAK,CAAC,MAAM,IAAI,OAAO,CAAC,CAAC;AACtE,MAAI,IAAI,WAAW,KAAK;AACtB,UAAM,IAAI;AAAA,MACR;AAAA,MACA,mBAAmB,IAAI,IAAI,KAAK,kBAAkB,IAAI,MAAM;AAAA,MAC5D;AAAA,QACE,KAAK;AAAA,QACL,SAAS,IAAI;AAAA,MACf;AAAA,IACF;AAAA,EACF;AACA,QAAM,SAAS,qBAAqB,UAAU,IAAI,IAAI;AACtD,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,IAAI,SAAS,qBAAqB,uDAAuD;AAAA,MAC7F,KAAK;AAAA,MACL,SAAS,OAAO,MAAM;AAAA,IACxB,CAAC;AAAA,EACH;AACA,SAAO,iBAAiB,OAAO,IAAI;AACrC;AAOA,IAAM,cAAc;AAAA,EAClB,OAAO;AAAA,EACP,OAAO;AAAA,EACP,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,eAAe;AAAA,EACf,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,qBAAqB;AACvB;AAEA,SAAS,iBAAiB,KAAqC;AAC7D,MAAI,IAAI,eAAe,OAAW,QAAO;AACzC,SAAO,EAAE,GAAG,KAAK,YAAY,IAAI,WAAW,IAAI,iBAAiB,EAAE;AACrE;AAEA,SAAS,kBAAkB,GAAqC;AAC9D,QAAM,MAAM,CAAC,GAAW,MAAuB,EAAE,SAAS,IAAI,EAAE,MAAM,GAAG,CAAC,IAAI;AAC9E,QAAM,UAAU,CAAC,MAAgB,OAAe,UAC9C,KAAK,MAAM,GAAG,KAAK,EAAE,IAAI,CAAC,MAAM,IAAI,GAAG,KAAK,CAAC;AAC/C,QAAM,YAAsC,CAAC;AAC7C,aAAW,OAAO,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,GAAG,YAAY,aAAa,GAAG;AAC9E,cAAU,IAAI,KAAK,YAAY,mBAAmB,CAAC,IAAI;AAAA,MACrD,EAAE,UAAU,GAAG,KAAK,CAAC;AAAA,MACrB,YAAY;AAAA,MACZ,YAAY;AAAA,IACd;AAAA,EACF;AACA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,OAAO,IAAI,EAAE,OAAO,YAAY,KAAK;AAAA,IACrC,OAAO,EAAE,UAAU,OAAO,OAAO,IAAI,EAAE,OAAO,YAAY,KAAK;AAAA,IAC/D,YAAY,EAAE,eAAe,OAAO,OAAO,IAAI,EAAE,YAAY,YAAY,UAAU;AAAA,IACnF,mBAAmB;AAAA,MACjB,EAAE;AAAA,MACF,YAAY;AAAA,MACZ,YAAY;AAAA,IACd;AAAA,IACA,gBAAgB,QAAQ,EAAE,gBAAgB,YAAY,WAAW,YAAY,aAAa;AAAA,IAC1F,cAAc,QAAQ,EAAE,cAAc,YAAY,cAAc,YAAY,gBAAgB;AAAA,IAC5F;AAAA,EACF;AACF;AAEA,IAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,IAAM,wBAA2C;AAExD,IAAM,kBAAkB;AAcjB,SAAS,iBAAiB,OAAsC;AACrE,MAAI,CAAC,sBAAsB,SAAS,MAAM,MAAM,GAAG;AACjD,UAAM,IAAI,SAAS,SAAS,2BAA2B,KAAK,UAAU,MAAM,MAAM,CAAC,IAAI;AAAA,MACrF,KAAK,eAAe,iBAAiB,KAAK,IAAI,CAAC;AAAA,IACjD,CAAC;AAAA,EACH;AACA,MAAI,MAAM,eAAe,UAAa,CAAC,QAAQ,KAAK,MAAM,UAAU,GAAG;AACrE,UAAM,IAAI,SAAS,SAAS,0BAA0B,KAAK,UAAU,MAAM,UAAU,CAAC,IAAI;AAAA,MACxF,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AACA,MAAI,MAAM,gBAAgB,UAAa,CAAC,gBAAgB,KAAK,MAAM,WAAW,GAAG;AAC/E,UAAM,IAAI,SAAS,SAAS,yBAAyB,KAAK,UAAU,MAAM,WAAW,CAAC,IAAI;AAAA,MACxF,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AACA,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,GAAI,MAAM,eAAe,SAAY,EAAE,YAAY,MAAM,WAAW,IAAI,CAAC;AAAA,IACzE,GAAI,MAAM,gBAAgB,SAAY,EAAE,aAAa,MAAM,YAAY,IAAI,CAAC;AAAA,EAC9E;AACF;AAEA,IAAM,eAAe;AAErB,eAAsB,aACpB,UACA,OACA,MAC+B;AAC/B,MAAI,CAAC,aAAa,KAAK,QAAQ,GAAG;AAChC,UAAM,IAAI,SAAS,SAAS,sBAAsB,KAAK,UAAU,QAAQ,CAAC,IAAI;AAAA,MAC5E,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AACA,MAAI,MAAM,WAAW,KAAK,MAAM,SAAS,IAAI;AAC3C,UAAM,IAAI,SAAS,SAAS,2CAA2C;AAAA,MACrE,KAAK;AAAA,IACP,CAAC;AAAA,EACH;AACA,QAAM,MAAM,GAAG,UAAU,KAAK,OAAO,CAAC,sBAAsB,QAAQ;AACpE,QAAM,MAAM,MAAM,YAAY,KAAK;AAAA,IACjC,QAAQ;AAAA,IACR,WAAW,KAAK;AAAA,IAChB,SAAS,EAAE,mBAAmB,cAAc;AAAA,IAC5C,UAAU,MAAM,WAAW,IAAI,MAAM,CAAC,IAAI;AAAA,IAC1C,WAAW,KAAK;AAAA,EAClB,CAAC;AACD,MAAI,CAAC,IAAI,GAAI,OAAM,WAAW,KAAK,GAAG;AACtC,MAAI,IAAI,WAAW,IAAK,OAAM,eAAe,KAAK,CAAC,MAAM,IAAI,OAAO,CAAC,CAAC;AAGtE,MAAI,IAAI,WAAW,KAAK;AACtB,UAAM,IAAI;AAAA,MACR;AAAA,MACA,mBAAmB,IAAI,IAAI,KAAK,0BAA0B,IAAI,MAAM;AAAA,MACpE;AAAA,QACE,KAAK;AAAA,QACL,SAAS,IAAI;AAAA,MACf;AAAA,IACF;AAAA,EACF;AACA,QAAM,WACJ,OAAO,IAAI,SAAS,YAAY,IAAI,SAAS,QAAQ,cAAc,IAAI,OACnE,OAAQ,IAAI,KAA+B,QAAQ,IACnD,MAAM;AACZ,SAAO,EAAE,UAAU,OAAO,SAAS,QAAQ,IAAI,WAAW,MAAM,OAAO;AACzE;","names":[]}
@@ -1,12 +1,15 @@
1
1
  import { createRequire as __tenjinCreateRequire } from 'node:module'; const require = __tenjinCreateRequire(import.meta.url);
2
+ import {
3
+ hasCode
4
+ } from "./chunk-2I4QTOLB.js";
2
5
  import {
3
6
  writeFileAtomicExclusive
4
- } from "./chunk-ZPZFPVDN.js";
7
+ } from "./chunk-MUBUQVC3.js";
5
8
  import {
6
9
  CliError,
7
10
  external_exports,
8
11
  walletPath
9
- } from "./chunk-YG2DO46P.js";
12
+ } from "./chunk-OYMYCW7J.js";
10
13
 
11
14
  // src/lib/wallet/store.ts
12
15
  import { readFile, stat } from "fs/promises";
@@ -112,9 +115,6 @@ function isPreEncryptionRecord(json) {
112
115
  function isNotFound(err) {
113
116
  return hasCode(err, "ENOENT");
114
117
  }
115
- function hasCode(err, code) {
116
- return typeof err === "object" && err !== null && "code" in err && err.code === code;
117
- }
118
118
 
119
119
  export {
120
120
  PRIVATE_KEY_RE,
@@ -123,4 +123,4 @@ export {
123
123
  readWalletRecord,
124
124
  writeWalletRecord
125
125
  };
126
- //# sourceMappingURL=chunk-DXPYFVKG.js.map
126
+ //# sourceMappingURL=chunk-YN5SHIXQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/wallet/store.ts"],"sourcesContent":["import { readFile, stat } from 'node:fs/promises';\nimport { z } from 'zod';\nimport * as Keystore from 'ox/Keystore';\nimport { CliError } from '../errors';\nimport { writeFileAtomicExclusive } from '../atomic-json';\nimport { hasCode } from '../errno';\nimport { walletPath } from '../paths';\n\n/** A 0x-prefixed 32-byte hex private key (case-insensitive). */\nexport const PRIVATE_KEY_RE = /^0x[0-9a-f]{64}$/i;\nconst ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;\n\n/**\n * The at-rest shape of a Keystore v3 document (Web3 Secret Storage). Validated\n * structurally on read so an obviously-corrupt keystore fails as\n * WALLET_INVALID_KEY before we ever ask for a passphrase; the real integrity\n * check is the keystore's own MAC, verified by ox at decrypt time. `kdfparams`\n * stays an open object because scrypt and pbkdf2 carry different fields and only\n * ox needs to interpret them.\n */\nconst KeystoreV3Schema = z.object({\n crypto: z.object({\n cipher: z.literal('aes-128-ctr'),\n ciphertext: z.string(),\n cipherparams: z.object({ iv: z.string() }),\n kdf: z.enum(['scrypt', 'pbkdf2']),\n kdfparams: z.record(z.string(), z.unknown()),\n mac: z.string(),\n }),\n id: z.string(),\n version: z.literal(3),\n});\n\n/**\n * The persisted wallet record (schema v2). The private key is NEVER stored in\n * cleartext: `keystore` is a Keystore v3 document (scrypt + AES-128-CTR) and the\n * key is recovered only by decrypting it with the wallet passphrase. `address`\n * stays top-level in cleartext ON PURPOSE so `show`/`balance`/`doctor` keep\n * working without a passphrase; only signing decrypts. The `provider`\n * discriminator keeps the schema from implying every future wallet embeds a\n * keystore. Validated on read (a corrupt file is WALLET_INVALID_KEY, never a\n * silent partial parse).\n */\nexport const WalletRecordSchema = z.object({\n schemaVersion: z.literal(2),\n provider: z.literal('local'),\n address: z.string().regex(ADDRESS_RE, 'expected a 0x-prefixed 20-byte address'),\n keystore: KeystoreV3Schema,\n createdAt: z.string(),\n});\nexport type WalletRecord = Omit<z.infer<typeof WalletRecordSchema>, 'keystore'> & {\n keystore: Keystore.Keystore;\n};\n\nexport async function walletFileExists(dir: string): Promise<boolean> {\n return (await walletFileMode(dir)) !== null;\n}\n\n/** File permission bits (`mode & 0o777`), or null when the wallet file is absent. */\nexport async function walletFileMode(dir: string): Promise<number | null> {\n try {\n return (await stat(walletPath(dir))).mode & 0o777;\n } catch (err) {\n if (isNotFound(err)) return null;\n throw err;\n }\n}\n\n/** Read + validate the wallet record; null when absent, WALLET_INVALID_KEY when corrupt. */\nexport async function readWalletRecord(dir: string): Promise<WalletRecord | null> {\n const path = walletPath(dir);\n let raw: string;\n try {\n raw = await readFile(path, 'utf8');\n } catch (err) {\n if (isNotFound(err)) return null;\n throw new CliError('WALLET_INVALID_KEY', `Could not read the wallet file at ${path}.`, {\n fix: `Check file permissions on ${path}.`,\n cause: err,\n });\n }\n let json: unknown;\n try {\n json = JSON.parse(raw);\n } catch (err) {\n throw new CliError('WALLET_INVALID_KEY', `The wallet file at ${path} is not valid JSON.`, {\n fix: `Move ${path} aside, then run \\`tenjin wallet create\\`.`,\n cause: err,\n });\n }\n // A pre-encryption record (schema v1 stored the raw key). There is no in-place\n // migration: the old key must be swept and a fresh encrypted wallet created.\n if (isPreEncryptionRecord(json)) {\n throw new CliError(\n 'WALLET_INVALID_KEY',\n `The wallet file at ${path} predates encrypted storage.`,\n {\n fix: `Move ${path} aside and run \\`tenjin wallet create\\`; sweep any funds from the old address first.`,\n },\n );\n }\n const parsed = WalletRecordSchema.safeParse(json);\n if (!parsed.success) {\n throw new CliError(\n 'WALLET_INVALID_KEY',\n `The wallet file at ${path} is not a valid wallet record.`,\n {\n fix: `Move ${path} aside, then run \\`tenjin wallet create\\`.`,\n details: parsed.error.issues,\n },\n );\n }\n return parsed.data as WalletRecord;\n}\n\n/**\n * Persist a validated record at 0600 in a 0700 dir, NO-CLOBBER. The exclusive\n * write — not an earlier existence check — is the authority: two concurrent\n * `create` runs can both pass a pre-check, but only one can win the atomic\n * commit; the loser surfaces as WALLET_EXISTS instead of silently overwriting\n * (and losing) a non-recoverable key.\n */\nexport async function writeWalletRecord(dir: string, record: WalletRecord): Promise<void> {\n const validated = WalletRecordSchema.parse(record);\n const path = walletPath(dir);\n try {\n await writeFileAtomicExclusive(path, `${JSON.stringify(validated, null, 2)}\\n`, {\n mode: 0o600,\n dirMode: 0o700,\n });\n } catch (err) {\n if (hasCode(err, 'EEXIST')) {\n throw new CliError('WALLET_EXISTS', `A wallet already exists at ${path}.`, {\n fix: `Keys are non-recoverable; move it aside first (e.g. \\`mv ${path} ${path}.bak\\`) to create a new one.`,\n cause: err,\n });\n }\n throw err;\n }\n}\n\n/** A cleartext-key record from before encrypted storage: schema v1 or a bare `privateKey`. */\nfunction isPreEncryptionRecord(json: unknown): boolean {\n if (typeof json !== 'object' || json === null) return false;\n const rec = json as Record<string, unknown>;\n return rec.schemaVersion === 1 || 'privateKey' in rec;\n}\n\nfunction isNotFound(err: unknown): boolean {\n return hasCode(err, 'ENOENT');\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,137 @@
1
+ import { createRequire as __tenjinCreateRequire } from 'node:module'; const require = __tenjinCreateRequire(import.meta.url);
2
+ import {
3
+ CliError,
4
+ external_exports
5
+ } from "./chunk-OYMYCW7J.js";
6
+
7
+ // src/lib/output.ts
8
+ import { styleText } from "util";
9
+ import { Stream } from "stream";
10
+
11
+ // src/schemas.ts
12
+ var SCHEMA_VERSION = 1;
13
+ var ErrorCodeSchema = external_exports.enum([
14
+ "NODE_UNSUPPORTED",
15
+ "USAGE",
16
+ "CONFIG_INVALID",
17
+ "WALLET_EXISTS",
18
+ "WALLET_MISSING",
19
+ "WALLET_INVALID_KEY",
20
+ "PROVIDER_ERROR",
21
+ "RPC_ERROR",
22
+ "API_UNREACHABLE",
23
+ "CONTRACT_MISMATCH",
24
+ "REFUSED",
25
+ "NETWORK_ERROR",
26
+ "INTERNAL",
27
+ "NOT_IMPLEMENTED",
28
+ // B2 (lookup/inspect/buy/outcome): a policy refusal (exit 3) has its own code
29
+ // so an agent can distinguish a spend-cap block from a generic REFUSED; a
30
+ // payment that failed after approval (exit 4) is PAYMENT_FAILED; the read
31
+ // route's own coded failures (402 preview parse, 409 gate) surface as these.
32
+ "POLICY_REFUSED",
33
+ "PAYMENT_FAILED",
34
+ "RESOURCE_NOT_FOUND",
35
+ "LOOKUP_NOT_FOUND",
36
+ // A 429 from the anonymous lookup/outcome/read limits; error.details carries
37
+ // retryAfterSeconds so a looping agent can back off instead of hammering.
38
+ "RATE_LIMITED",
39
+ // B3 (publish): a publish that needs the human's yes (exit 3) — soft scan
40
+ // findings, or `review` mode always. NEEDS_CONFIRMATION clears with --yes;
41
+ // PUBLISH_BLOCKED is a hard-block finding (a live secret) that no mode or --yes
42
+ // ever clears. PUBLISH_FAILED (exit 4) is a write that failed AFTER approval,
43
+ // mirroring PAYMENT_FAILED's post-decision failure class.
44
+ "NEEDS_CONFIRMATION",
45
+ "PUBLISH_BLOCKED",
46
+ "PUBLISH_FAILED"
47
+ ]);
48
+ var MoneySchema = external_exports.object({
49
+ atomic: external_exports.string(),
50
+ usd: external_exports.string()
51
+ });
52
+ var OutputErrorSchema = external_exports.object({
53
+ code: ErrorCodeSchema,
54
+ message: external_exports.string(),
55
+ fix: external_exports.string().optional(),
56
+ details: external_exports.unknown().optional()
57
+ });
58
+ var SuccessEnvelopeSchema = external_exports.object({
59
+ schemaVersion: external_exports.literal(SCHEMA_VERSION),
60
+ command: external_exports.string(),
61
+ ok: external_exports.literal(true),
62
+ data: external_exports.unknown()
63
+ });
64
+ var FailureEnvelopeSchema = external_exports.object({
65
+ schemaVersion: external_exports.literal(SCHEMA_VERSION),
66
+ command: external_exports.string(),
67
+ ok: external_exports.literal(false),
68
+ error: OutputErrorSchema
69
+ });
70
+ var OutputEnvelopeSchema = external_exports.discriminatedUnion("ok", [
71
+ SuccessEnvelopeSchema,
72
+ FailureEnvelopeSchema
73
+ ]);
74
+
75
+ // src/lib/output.ts
76
+ function defaultIo() {
77
+ return {
78
+ stdout: process.stdout,
79
+ stderr: process.stderr,
80
+ isTTY: Boolean(process.stdout.isTTY)
81
+ };
82
+ }
83
+ function emitSuccess(io, command, data, humanLines = [], opts = {}) {
84
+ writeJson(io.stdout, { schemaVersion: SCHEMA_VERSION, command, ok: true, data });
85
+ if (shouldRenderHuman(io, opts) && humanLines.length > 0) {
86
+ writeLines(io.stderr, humanLines);
87
+ }
88
+ }
89
+ function emitFailure(io, command, err, opts = {}) {
90
+ const cliErr = normalizeError(err);
91
+ const error = {
92
+ code: cliErr.code,
93
+ message: cliErr.message,
94
+ ...cliErr.fix !== void 0 ? { fix: cliErr.fix } : {},
95
+ ...cliErr.details !== void 0 ? { details: cliErr.details } : {}
96
+ };
97
+ writeJson(io.stdout, { schemaVersion: SCHEMA_VERSION, command, ok: false, error });
98
+ if (shouldRenderHuman(io, opts)) {
99
+ const lines = [paint(io, "red", `error: ${sanitizeForTerminal(cliErr.message)}`)];
100
+ if (cliErr.fix !== void 0) {
101
+ lines.push(paint(io, "dim", `fix: ${sanitizeForTerminal(cliErr.fix)}`));
102
+ }
103
+ writeLines(io.stderr, lines);
104
+ }
105
+ return cliErr;
106
+ }
107
+ function sanitizeForTerminal(text) {
108
+ return text.replace(/\x1b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_])/g, "").replace(/[\x00-\x08\x0a-\x1f\x7f-\x9f]/g, "");
109
+ }
110
+ function normalizeError(err) {
111
+ if (err instanceof CliError) return err;
112
+ if (err instanceof Error) return new CliError("INTERNAL", err.message, { cause: err });
113
+ return new CliError("INTERNAL", "Unexpected error", { details: err });
114
+ }
115
+ function shouldRenderHuman(io, opts) {
116
+ return io.isTTY && opts.json !== true;
117
+ }
118
+ function writeJson(stream, value) {
119
+ stream.write(`${JSON.stringify(value, null, 2)}
120
+ `);
121
+ }
122
+ function writeLines(stream, lines) {
123
+ if (lines.length > 0) stream.write(`${lines.join("\n")}
124
+ `);
125
+ }
126
+ function paint(io, format, text) {
127
+ if (io.stderr instanceof Stream) return styleText(format, text, { stream: io.stderr });
128
+ return styleText(format, text);
129
+ }
130
+
131
+ export {
132
+ defaultIo,
133
+ emitSuccess,
134
+ emitFailure,
135
+ sanitizeForTerminal
136
+ };
137
+ //# sourceMappingURL=chunk-Z52RA3NC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/output.ts","../src/schemas.ts"],"sourcesContent":["import { styleText } from 'node:util';\nimport { Stream } from 'node:stream';\nimport { CliError } from './errors';\nimport { SCHEMA_VERSION } from '../schemas';\nimport type { OutputError } from '../schemas';\n\n/**\n * Injected streams + TTY fact. Every command receives one via CommandContext,\n * so tests drive the CLI in-process with memory buffers and no child process.\n * `isTTY` is the real terminal fact; the separate `--json` flag (passed to the\n * emit functions) is what suppresses human rendering — the two are ANDed, never\n * conflated, so `--json` on a TTY still yields pure machine output.\n */\nexport interface Io {\n stdout: NodeJS.WritableStream;\n stderr: NodeJS.WritableStream;\n isTTY: boolean;\n}\n\nexport function defaultIo(): Io {\n return {\n stdout: process.stdout,\n stderr: process.stderr,\n isTTY: Boolean(process.stdout.isTTY),\n };\n}\n\nexport interface EmitOptions {\n /** The global --json flag. When true, all stderr human rendering is suppressed. */\n json?: boolean;\n}\n\n/**\n * Emit exactly one pretty-printed success envelope to stdout. When on a TTY and\n * --json is off, the optional human lines are rendered to stderr (never stdout —\n * stdout stays a clean single JSON object for the agent that piped us).\n */\nexport function emitSuccess(\n io: Io,\n command: string,\n data: unknown,\n humanLines: string[] = [],\n opts: EmitOptions = {},\n): void {\n writeJson(io.stdout, { schemaVersion: SCHEMA_VERSION, command, ok: true, data });\n if (shouldRenderHuman(io, opts) && humanLines.length > 0) {\n writeLines(io.stderr, humanLines);\n }\n}\n\n/**\n * Emit exactly one pretty-printed failure envelope to stdout and (on a TTY, with\n * --json off) a red error line plus a `fix` line to stderr. Accepts a CliError or\n * any thrown value; anything that is not a CliError normalizes to INTERNAL.\n * Returns the normalized CliError so the caller can read its `exitCode`.\n */\nexport function emitFailure(\n io: Io,\n command: string,\n err: unknown,\n opts: EmitOptions = {},\n): CliError {\n const cliErr = normalizeError(err);\n const error: OutputError = {\n code: cliErr.code,\n message: cliErr.message,\n ...(cliErr.fix !== undefined ? { fix: cliErr.fix } : {}),\n ...(cliErr.details !== undefined ? { details: cliErr.details } : {}),\n };\n writeJson(io.stdout, { schemaVersion: SCHEMA_VERSION, command, ok: false, error });\n if (shouldRenderHuman(io, opts)) {\n // Error messages can embed server-sourced text (api error passthrough) and\n // never carry intentional ANSI, so sanitize before painting.\n const lines = [paint(io, 'red', `error: ${sanitizeForTerminal(cliErr.message)}`)];\n if (cliErr.fix !== undefined) {\n lines.push(paint(io, 'dim', `fix: ${sanitizeForTerminal(cliErr.fix)}`));\n }\n writeLines(io.stderr, lines);\n }\n return cliErr;\n}\n\n/**\n * Strip C0/C1 control characters and ANSI escape sequences from a string headed\n * for a terminal. Commands apply this to every SERVER-sourced string (titles,\n * handles) they put into human lines or the buy confirm prompt; without it a\n * malicious deployment could use cursor-movement escapes to overwrite the very\n * price a human is being asked to confirm. It is not applied to whole human\n * lines here because trusted callers (doctor) paint their own ANSI colors. JSON\n * stdout is untouched (JSON.stringify escapes control characters itself).\n */\nexport function sanitizeForTerminal(text: string): string {\n return (\n text\n // CSI/OSC/charset escape sequences first, then any stray ESC and the rest\n // of C0 (except \\t) plus DEL and the C1 range.\n // eslint-disable-next-line no-control-regex\n .replace(/\\x1b(?:\\[[0-9;?]*[ -/]*[@-~]|\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)|[@-Z\\\\-_])/g, '')\n // eslint-disable-next-line no-control-regex\n .replace(/[\\x00-\\x08\\x0a-\\x1f\\x7f-\\x9f]/g, '')\n );\n}\n\n/** CliError passes through; every other thrown value becomes an INTERNAL CliError. */\nexport function normalizeError(err: unknown): CliError {\n if (err instanceof CliError) return err;\n if (err instanceof Error) return new CliError('INTERNAL', err.message, { cause: err });\n return new CliError('INTERNAL', 'Unexpected error', { details: err });\n}\n\nfunction shouldRenderHuman(io: Io, opts: EmitOptions): boolean {\n return io.isTTY && opts.json !== true;\n}\n\nfunction writeJson(stream: NodeJS.WritableStream, value: unknown): void {\n stream.write(`${JSON.stringify(value, null, 2)}\\n`);\n}\n\nfunction writeLines(stream: NodeJS.WritableStream, lines: string[]): void {\n if (lines.length > 0) stream.write(`${lines.join('\\n')}\\n`);\n}\n\n/**\n * Color the human line for stderr. styleText emits ANSI only when the target\n * stream is color-capable (a real TTY with color depth) and honors\n * NO_COLOR/FORCE_COLOR natively (verified on Node 22/23). The `stream` option is\n * passed only when stderr is a genuine Stream — styleText throws on anything\n * else, and a test/redirected sink is not one, so it falls back to the default\n * (process.stdout) capability check and simply comes out plain.\n */\nfunction paint(io: Io, format: Parameters<typeof styleText>[0], text: string): string {\n if (io.stderr instanceof Stream) return styleText(format, text, { stream: io.stderr });\n return styleText(format, text);\n}\n","import { z } from 'zod';\n\n/**\n * Bumped only on a breaking change to any stdout envelope shape. Agents pin on\n * this: a consumer that sees an unexpected value should refuse to parse rather\n * than guess. Every envelope carries it, success or failure.\n */\nexport const SCHEMA_VERSION = 1;\n\n/**\n * The closed set of machine error codes. `error.code` in a failure envelope is\n * always one of these; agents branch on it, humans read `message`/`fix`. The\n * exit-code mapping lives in lib/errors.ts (this enum is the single source of\n * the code names both sides agree on).\n */\nexport const ErrorCodeSchema = z.enum([\n 'NODE_UNSUPPORTED',\n 'USAGE',\n 'CONFIG_INVALID',\n 'WALLET_EXISTS',\n 'WALLET_MISSING',\n 'WALLET_INVALID_KEY',\n 'PROVIDER_ERROR',\n 'RPC_ERROR',\n 'API_UNREACHABLE',\n 'CONTRACT_MISMATCH',\n 'REFUSED',\n 'NETWORK_ERROR',\n 'INTERNAL',\n 'NOT_IMPLEMENTED',\n // B2 (lookup/inspect/buy/outcome): a policy refusal (exit 3) has its own code\n // so an agent can distinguish a spend-cap block from a generic REFUSED; a\n // payment that failed after approval (exit 4) is PAYMENT_FAILED; the read\n // route's own coded failures (402 preview parse, 409 gate) surface as these.\n 'POLICY_REFUSED',\n 'PAYMENT_FAILED',\n 'RESOURCE_NOT_FOUND',\n 'LOOKUP_NOT_FOUND',\n // A 429 from the anonymous lookup/outcome/read limits; error.details carries\n // retryAfterSeconds so a looping agent can back off instead of hammering.\n 'RATE_LIMITED',\n // B3 (publish): a publish that needs the human's yes (exit 3) — soft scan\n // findings, or `review` mode always. NEEDS_CONFIRMATION clears with --yes;\n // PUBLISH_BLOCKED is a hard-block finding (a live secret) that no mode or --yes\n // ever clears. PUBLISH_FAILED (exit 4) is a write that failed AFTER approval,\n // mirroring PAYMENT_FAILED's post-decision failure class.\n 'NEEDS_CONFIRMATION',\n 'PUBLISH_BLOCKED',\n 'PUBLISH_FAILED',\n]);\nexport type ErrorCode = z.infer<typeof ErrorCodeSchema>;\n\n/**\n * All money in machine output is dual-form: `atomic` is the 6-decimal USDC base\n * unit as a decimal string (never a JS number — precision), `usd` is the same\n * value formatted as decimal dollars. Emit both so agents compute on `atomic`\n * and humans read `usd` without either side re-deriving the other.\n */\nexport const MoneySchema = z.object({\n atomic: z.string(),\n usd: z.string(),\n});\nexport type Money = z.infer<typeof MoneySchema>;\n\nexport const OutputErrorSchema = z.object({\n code: ErrorCodeSchema,\n message: z.string(),\n fix: z.string().optional(),\n details: z.unknown().optional(),\n});\nexport type OutputError = z.infer<typeof OutputErrorSchema>;\n\nexport const SuccessEnvelopeSchema = z.object({\n schemaVersion: z.literal(SCHEMA_VERSION),\n command: z.string(),\n ok: z.literal(true),\n data: z.unknown(),\n});\nexport type SuccessEnvelope = z.infer<typeof SuccessEnvelopeSchema>;\n\nexport const FailureEnvelopeSchema = z.object({\n schemaVersion: z.literal(SCHEMA_VERSION),\n command: z.string(),\n ok: z.literal(false),\n error: OutputErrorSchema,\n});\nexport type FailureEnvelope = z.infer<typeof FailureEnvelopeSchema>;\n\n/** The one thing every invocation prints to stdout, discriminated on `ok`. */\nexport const OutputEnvelopeSchema = z.discriminatedUnion('ok', [\n SuccessEnvelopeSchema,\n FailureEnvelopeSchema,\n]);\nexport type OutputEnvelope = z.infer<typeof OutputEnvelopeSchema>;\n"],"mappings":"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","names":[]}