tenjin-cli 0.1.0-alpha.0 → 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 (138) hide show
  1. package/NOTICE.md +33 -0
  2. package/README.md +198 -18
  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-FXWZDDF5.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-MVLZRVPU.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-LTTRND4A.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-SXQQ2ZSR.js → chunk-NCE5NSN6.js} +186 -18
  38. package/dist/chunk-NCE5NSN6.js.map +1 -0
  39. package/dist/{chunk-FIYZLITH.js → chunk-OYMYCW7J.js} +23 -4
  40. package/dist/{chunk-FIYZLITH.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-TX5T3LKJ.js → chunk-QVX5MWJZ.js} +21 -17
  44. package/dist/{chunk-TX5T3LKJ.js.map → chunk-QVX5MWJZ.js.map} +1 -1
  45. package/dist/{chunk-CPUZRZ2A.js → chunk-RBJPAZFI.js} +25 -170
  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-XVXZ3V2Q.js → chunk-TFGSOPWR.js} +117 -62
  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-PAAL45FJ.js → chunk-UGX7YPX2.js} +3446 -448
  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-F5DZJLU3.js → chunk-YALGASEU.js} +2065 -1258
  66. package/dist/chunk-YALGASEU.js.map +1 -0
  67. package/dist/chunk-YBMMV4Y4.js +51 -0
  68. package/dist/chunk-YBMMV4Y4.js.map +1 -0
  69. package/dist/chunk-YIYQ2ZNT.js +286 -0
  70. package/dist/chunk-YIYQ2ZNT.js.map +1 -0
  71. package/dist/{chunk-376LHMK2.js → chunk-YN5SHIXQ.js} +36 -10
  72. package/dist/chunk-YN5SHIXQ.js.map +1 -0
  73. package/dist/chunk-Z52RA3NC.js +137 -0
  74. package/dist/chunk-Z52RA3NC.js.map +1 -0
  75. package/dist/chunk-Z6MFJV5Z.js +1330 -0
  76. package/dist/chunk-Z6MFJV5Z.js.map +1 -0
  77. package/dist/chunk-ZABPQJ3M.js +83 -0
  78. package/dist/chunk-ZABPQJ3M.js.map +1 -0
  79. package/dist/{chunk-WOHCAMDQ.js → chunk-ZMHYXMO6.js} +1887 -1029
  80. package/dist/chunk-ZMHYXMO6.js.map +1 -0
  81. package/dist/{chunk-3IMLRUZE.js → chunk-ZXTAQYNA.js} +6 -2
  82. package/dist/{cli-LMHGCZ5S.js → cli-4NNBEQYH.js} +175 -179
  83. package/dist/cli-4NNBEQYH.js.map +1 -0
  84. package/dist/config-2VSXWWHK.js +21 -0
  85. package/dist/doctor-JJPNIBYH.js +20 -0
  86. package/dist/doctor-JJPNIBYH.js.map +1 -0
  87. package/dist/ethersCompat-Df4a6QIZ-3YZIAXDK.js +18 -0
  88. package/dist/ethersCompat-Df4a6QIZ-3YZIAXDK.js.map +1 -0
  89. package/dist/index.js +1 -1
  90. package/dist/inspect-JGYJWFDP.js +96 -0
  91. package/dist/inspect-JGYJWFDP.js.map +1 -0
  92. package/dist/install-ZQRMSRGW.js +378 -0
  93. package/dist/install-ZQRMSRGW.js.map +1 -0
  94. package/dist/lookup-WYVTOMVY.js +113 -0
  95. package/dist/lookup-WYVTOMVY.js.map +1 -0
  96. package/dist/{mine.wasm-GQECMSGN.js → mine.wasm-6PIT3DK6.js} +2 -2
  97. package/dist/outcome-HKKT6QIA.js +66 -0
  98. package/dist/outcome-HKKT6QIA.js.map +1 -0
  99. package/dist/publish-D76B3MBK.js +1480 -0
  100. package/dist/publish-D76B3MBK.js.map +1 -0
  101. package/dist/{secp256k1-35YNNB6F.js → secp256k1-N3ZHU7LB.js} +5 -4
  102. package/dist/secp256k1-N3ZHU7LB.js.map +1 -0
  103. package/dist/usdc-JIAJW4SV.js +22 -0
  104. package/dist/usdc-JIAJW4SV.js.map +1 -0
  105. package/dist/viemAdapter-By13KInL-ZZE6UV4Y.js +14 -0
  106. package/dist/viemAdapter-By13KInL-ZZE6UV4Y.js.map +1 -0
  107. package/dist/wallet-6QKS6UHW.js +179 -0
  108. package/dist/wallet-6QKS6UHW.js.map +1 -0
  109. package/dist/wallet-PA54CRBL.js +30 -0
  110. package/dist/wallet-PA54CRBL.js.map +1 -0
  111. package/package.json +20 -15
  112. package/skills/tenjin/SKILL.md +199 -0
  113. package/skills/tenjin-publish/SKILL.md +96 -0
  114. package/skills/tenjin-search/SKILL.md +122 -0
  115. package/dist/chunk-376LHMK2.js.map +0 -1
  116. package/dist/chunk-CPUZRZ2A.js.map +0 -1
  117. package/dist/chunk-F5DZJLU3.js.map +0 -1
  118. package/dist/chunk-LTTRND4A.js.map +0 -1
  119. package/dist/chunk-PAAL45FJ.js.map +0 -1
  120. package/dist/chunk-QHIEYBLV.js +0 -320
  121. package/dist/chunk-QHIEYBLV.js.map +0 -1
  122. package/dist/chunk-SXQQ2ZSR.js.map +0 -1
  123. package/dist/chunk-WOHCAMDQ.js.map +0 -1
  124. package/dist/chunk-ZPZFPVDN.js.map +0 -1
  125. package/dist/cli-LMHGCZ5S.js.map +0 -1
  126. package/dist/config-XVXZ3V2Q.js.map +0 -1
  127. package/dist/doctor-MVLZRVPU.js.map +0 -1
  128. package/dist/usdc-OQUOFF6R.js +0 -18
  129. package/dist/wallet-NRKWLKHX.js +0 -153
  130. package/dist/wallet-NRKWLKHX.js.map +0 -1
  131. package/dist/wallet-QUWK2PA4.js +0 -22
  132. /package/dist/{_esm-ZH2J4QEH.js.map → _esm-C5CRWCYU.js.map} +0 -0
  133. /package/dist/{ccip-FXWZDDF5.js.map → _esm-FEASBWRP.js.map} +0 -0
  134. /package/dist/{chunk-3IMLRUZE.js.map → ccip-NQXCEK2U.js.map} +0 -0
  135. /package/dist/{secp256k1-35YNNB6F.js.map → ccip-XR5NE5QK.js.map} +0 -0
  136. /package/dist/{usdc-OQUOFF6R.js.map → chunk-ZXTAQYNA.js.map} +0 -0
  137. /package/dist/{wallet-QUWK2PA4.js.map → config-2VSXWWHK.js.map} +0 -0
  138. /package/dist/{mine.wasm-GQECMSGN.js.map → mine.wasm-6PIT3DK6.js.map} +0 -0
@@ -0,0 +1,51 @@
1
+ import { createRequire as __tenjinCreateRequire } from 'node:module'; const require = __tenjinCreateRequire(import.meta.url);
2
+
3
+ // src/lib/lock.ts
4
+ import { mkdir, rm, writeFile } from "fs/promises";
5
+ import { join } from "path";
6
+ var DEFAULTS = { timeoutMs: 5e3, retryMs: 25 };
7
+ var LockTimeoutError = class extends Error {
8
+ lockPath;
9
+ constructor(lockPath, timeoutMs) {
10
+ super(
11
+ `Timed out waiting for lock at ${lockPath} after ${timeoutMs}ms; if no other tenjin process is running, remove that directory and retry.`
12
+ );
13
+ this.name = "LockTimeoutError";
14
+ this.lockPath = lockPath;
15
+ }
16
+ };
17
+ async function withFileLock(lockPath, fn, opts = {}) {
18
+ const { timeoutMs, retryMs } = { ...DEFAULTS, ...opts };
19
+ const metaPath = join(lockPath, "meta");
20
+ const deadline = Date.now() + timeoutMs;
21
+ for (; ; ) {
22
+ try {
23
+ await mkdir(lockPath);
24
+ break;
25
+ } catch (err) {
26
+ if (!isEexist(err)) throw err;
27
+ if (Date.now() >= deadline) throw new LockTimeoutError(lockPath, timeoutMs);
28
+ await delay(retryMs);
29
+ }
30
+ }
31
+ await writeFile(metaPath, JSON.stringify({ pid: process.pid, acquiredAt: Date.now() })).catch(
32
+ () => void 0
33
+ );
34
+ try {
35
+ return await fn();
36
+ } finally {
37
+ await rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
38
+ }
39
+ }
40
+ function isEexist(err) {
41
+ return typeof err === "object" && err !== null && err.code === "EEXIST";
42
+ }
43
+ function delay(ms) {
44
+ return new Promise((resolve) => setTimeout(resolve, ms));
45
+ }
46
+
47
+ export {
48
+ LockTimeoutError,
49
+ withFileLock
50
+ };
51
+ //# sourceMappingURL=chunk-YBMMV4Y4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/lock.ts"],"sourcesContent":["import { mkdir, rm, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\n\nexport interface FileLockOptions {\n /** Give up acquiring after this long (ms). */\n timeoutMs?: number;\n /** Poll interval between acquisition attempts (ms). */\n retryMs?: number;\n}\n\nconst DEFAULTS = { timeoutMs: 5000, retryMs: 25 };\n\n/** Thrown when the lock cannot be acquired within `timeoutMs`. Carries the lock\n * path so a caller can surface the manual-recovery step in its own error contract. */\nexport class LockTimeoutError extends Error {\n readonly lockPath: string;\n constructor(lockPath: string, timeoutMs: number) {\n super(\n `Timed out waiting for lock at ${lockPath} after ${timeoutMs}ms; if no other tenjin process is running, remove that directory and retry.`,\n );\n this.name = 'LockTimeoutError';\n this.lockPath = lockPath;\n }\n}\n\n/**\n * Minimal cross-process mutex: the lock IS a directory, and mkdir is atomic, so a\n * second holder gets EEXIST and retries with backoff until `timeoutMs`, then\n * throws LockTimeoutError. There is deliberately NO auto-steal of a \"stale\" lock:\n * concurrent waiters each judging a lock stale could remove a fresh holder's\n * directory and lose its update. A crashed holder's lock is recovered by hand (the\n * timeout error names the path); the pid+acquiredAt meta exists only to make that\n * manual call diagnosable. Always released in the finally.\n */\nexport async function withFileLock<T>(\n lockPath: string,\n fn: () => Promise<T>,\n opts: FileLockOptions = {},\n): Promise<T> {\n const { timeoutMs, retryMs } = { ...DEFAULTS, ...opts };\n const metaPath = join(lockPath, 'meta');\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n try {\n await mkdir(lockPath);\n break; // acquired\n } catch (err) {\n if (!isEexist(err)) throw err;\n if (Date.now() >= deadline) throw new LockTimeoutError(lockPath, timeoutMs);\n await delay(retryMs);\n }\n }\n await writeFile(metaPath, JSON.stringify({ pid: process.pid, acquiredAt: Date.now() })).catch(\n () => undefined,\n );\n try {\n return await fn();\n } finally {\n // With no stealing, nothing else ever removes or replaces this directory, so\n // the lock here is provably the one we created — the unconditional rm is sound.\n await rm(lockPath, { recursive: true, force: true }).catch(() => undefined);\n }\n}\n\nfunction isEexist(err: unknown): boolean {\n return typeof err === 'object' && err !== null && (err as { code?: unknown }).code === 'EEXIST';\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":";;;AAAA,SAAS,OAAO,IAAI,iBAAiB;AACrC,SAAS,YAAY;AASrB,IAAM,WAAW,EAAE,WAAW,KAAM,SAAS,GAAG;AAIzC,IAAM,mBAAN,cAA+B,MAAM;AAAA,EACjC;AAAA,EACT,YAAY,UAAkB,WAAmB;AAC/C;AAAA,MACE,iCAAiC,QAAQ,UAAU,SAAS;AAAA,IAC9D;AACA,SAAK,OAAO;AACZ,SAAK,WAAW;AAAA,EAClB;AACF;AAWA,eAAsB,aACpB,UACA,IACA,OAAwB,CAAC,GACb;AACZ,QAAM,EAAE,WAAW,QAAQ,IAAI,EAAE,GAAG,UAAU,GAAG,KAAK;AACtD,QAAM,WAAW,KAAK,UAAU,MAAM;AACtC,QAAM,WAAW,KAAK,IAAI,IAAI;AAC9B,aAAS;AACP,QAAI;AACF,YAAM,MAAM,QAAQ;AACpB;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,CAAC,SAAS,GAAG,EAAG,OAAM;AAC1B,UAAI,KAAK,IAAI,KAAK,SAAU,OAAM,IAAI,iBAAiB,UAAU,SAAS;AAC1E,YAAM,MAAM,OAAO;AAAA,IACrB;AAAA,EACF;AACA,QAAM,UAAU,UAAU,KAAK,UAAU,EAAE,KAAK,QAAQ,KAAK,YAAY,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE;AAAA,IACtF,MAAM;AAAA,EACR;AACA,MAAI;AACF,WAAO,MAAM,GAAG;AAAA,EAClB,UAAE;AAGA,UAAM,GAAG,UAAU,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,EAC5E;AACF;AAEA,SAAS,SAAS,KAAuB;AACvC,SAAO,OAAO,QAAQ,YAAY,QAAQ,QAAS,IAA2B,SAAS;AACzF;AAEA,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;","names":[]}
@@ -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":"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","names":[]}
@@ -1,22 +1,37 @@
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-FIYZLITH.js";
12
+ } from "./chunk-OYMYCW7J.js";
10
13
 
11
14
  // src/lib/wallet/store.ts
12
15
  import { readFile, stat } from "fs/promises";
13
16
  var PRIVATE_KEY_RE = /^0x[0-9a-f]{64}$/i;
14
17
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
18
+ var KeystoreV3Schema = external_exports.object({
19
+ crypto: external_exports.object({
20
+ cipher: external_exports.literal("aes-128-ctr"),
21
+ ciphertext: external_exports.string(),
22
+ cipherparams: external_exports.object({ iv: external_exports.string() }),
23
+ kdf: external_exports.enum(["scrypt", "pbkdf2"]),
24
+ kdfparams: external_exports.record(external_exports.string(), external_exports.unknown()),
25
+ mac: external_exports.string()
26
+ }),
27
+ id: external_exports.string(),
28
+ version: external_exports.literal(3)
29
+ });
15
30
  var WalletRecordSchema = external_exports.object({
16
- schemaVersion: external_exports.literal(1),
31
+ schemaVersion: external_exports.literal(2),
17
32
  provider: external_exports.literal("local"),
18
33
  address: external_exports.string().regex(ADDRESS_RE, "expected a 0x-prefixed 20-byte address"),
19
- privateKey: external_exports.string().regex(PRIVATE_KEY_RE, "expected a 0x-prefixed 32-byte hex key"),
34
+ keystore: KeystoreV3Schema,
20
35
  createdAt: external_exports.string()
21
36
  });
22
37
  async function walletFileExists(dir) {
@@ -47,17 +62,26 @@ async function readWalletRecord(dir) {
47
62
  json = JSON.parse(raw);
48
63
  } catch (err) {
49
64
  throw new CliError("WALLET_INVALID_KEY", `The wallet file at ${path} is not valid JSON.`, {
50
- fix: `Move ${path} aside, then run \`tenjin wallet create\` or \`tenjin wallet import\`.`,
65
+ fix: `Move ${path} aside, then run \`tenjin wallet create\`.`,
51
66
  cause: err
52
67
  });
53
68
  }
69
+ if (isPreEncryptionRecord(json)) {
70
+ throw new CliError(
71
+ "WALLET_INVALID_KEY",
72
+ `The wallet file at ${path} predates encrypted storage.`,
73
+ {
74
+ fix: `Move ${path} aside and run \`tenjin wallet create\`; sweep any funds from the old address first.`
75
+ }
76
+ );
77
+ }
54
78
  const parsed = WalletRecordSchema.safeParse(json);
55
79
  if (!parsed.success) {
56
80
  throw new CliError(
57
81
  "WALLET_INVALID_KEY",
58
82
  `The wallet file at ${path} is not a valid wallet record.`,
59
83
  {
60
- fix: `Move ${path} aside, then run \`tenjin wallet create\` or \`tenjin wallet import\`.`,
84
+ fix: `Move ${path} aside, then run \`tenjin wallet create\`.`,
61
85
  details: parsed.error.issues
62
86
  }
63
87
  );
@@ -83,12 +107,14 @@ async function writeWalletRecord(dir, record) {
83
107
  throw err;
84
108
  }
85
109
  }
110
+ function isPreEncryptionRecord(json) {
111
+ if (typeof json !== "object" || json === null) return false;
112
+ const rec = json;
113
+ return rec.schemaVersion === 1 || "privateKey" in rec;
114
+ }
86
115
  function isNotFound(err) {
87
116
  return hasCode(err, "ENOENT");
88
117
  }
89
- function hasCode(err, code) {
90
- return typeof err === "object" && err !== null && "code" in err && err.code === code;
91
- }
92
118
 
93
119
  export {
94
120
  PRIVATE_KEY_RE,
@@ -97,4 +123,4 @@ export {
97
123
  readWalletRecord,
98
124
  writeWalletRecord
99
125
  };
100
- //# sourceMappingURL=chunk-376LHMK2.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