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
@@ -1,30 +1,48 @@
1
1
  import { createRequire as __tenjinCreateRequire } from 'node:module'; const require = __tenjinCreateRequire(import.meta.url);
2
2
  import {
3
- LockTimeoutError,
4
- withFileLock
5
- } from "./chunk-YBMMV4Y4.js";
3
+ loadProjectConfig
4
+ } from "./chunk-UPIEAMGH.js";
6
5
  import {
7
6
  CONFIG_KEYS,
7
+ PUBLISH_CONFIG_KEYS,
8
+ PublishModeSchema,
8
9
  RawConfigSchema,
9
10
  loadRawConfig,
10
11
  parseUsdToAtomic,
11
12
  resolveSettings,
12
13
  toMoney
13
- } from "./chunk-EZROUI5U.js";
14
+ } from "./chunk-IYLHQMT5.js";
15
+ import {
16
+ LockTimeoutError,
17
+ withFileLock
18
+ } from "./chunk-YBMMV4Y4.js";
14
19
  import {
15
20
  writeFileAtomic
16
- } from "./chunk-ZPZFPVDN.js";
21
+ } from "./chunk-MUBUQVC3.js";
17
22
  import {
18
23
  CliError,
19
24
  configPath
20
- } from "./chunk-YG2DO46P.js";
21
- import "./chunk-3IMLRUZE.js";
25
+ } from "./chunk-OYMYCW7J.js";
22
26
 
23
27
  // src/commands/config.ts
24
28
  import { mkdir } from "fs/promises";
25
29
  import { styleText } from "util";
26
30
  var CONFIRM_ABOVE = "above:";
27
- var KEY_WIDTH = Math.max(...CONFIG_KEYS.map((key) => key.length));
31
+ var KEY_WIDTH = Math.max(...[...CONFIG_KEYS, ...PUBLISH_CONFIG_KEYS].map((key) => key.length));
32
+ var KEY_DESCRIPTIONS = {
33
+ maxAutoSpend: "auto-approve a read up to this amount",
34
+ sessionBudget: "cap on total auto-spend per session",
35
+ confirm: "when to ask before paying",
36
+ allowlistCreators: "only auto-pay these creators (empty = any)",
37
+ baseUrl: "Tenjin API base URL",
38
+ rpcUrl: "Base RPC endpoint for balance reads",
39
+ evalCohort: "opt in to the lookup evaluation cohort",
40
+ "publish.mode": "review=always ask, auto=ask on findings, full-auto=only hard blocks stop it",
41
+ "publish.defaultPrice": "price used when none is given"
42
+ };
43
+ function isPublishKey(key) {
44
+ return PUBLISH_CONFIG_KEYS.includes(key);
45
+ }
28
46
  async function runConfigList(ctx) {
29
47
  const settings = await resolveFromContext(ctx);
30
48
  const data = {};
@@ -32,38 +50,90 @@ async function runConfigList(ctx) {
32
50
  for (const key of CONFIG_KEYS) {
33
51
  const entry = renderSetting(key, settings[key].value, settings[key].source);
34
52
  data[key] = entry;
35
- humanLines.push(formatLine(key, entry));
53
+ humanLines.push(describedLine(key, entry));
54
+ }
55
+ for (const key of PUBLISH_CONFIG_KEYS) {
56
+ const entry = renderPublishSetting(key, settings);
57
+ data[key] = entry;
58
+ humanLines.push(describedLine(key, entry, downgradeNote(key, settings)));
36
59
  }
37
60
  return { data, humanLines };
38
61
  }
39
62
  async function runConfigGet({ key }, ctx) {
63
+ if (isPublishKey(key)) {
64
+ const settings2 = await resolveFromContext(ctx);
65
+ const entry2 = renderPublishSetting(key, settings2);
66
+ return {
67
+ data: { key, ...entry2 },
68
+ humanLines: [withNote(formatLine(key, entry2), downgradeNote(key, settings2))]
69
+ };
70
+ }
40
71
  const configKey = assertKey(key);
41
72
  const settings = await resolveFromContext(ctx);
42
73
  const entry = renderSetting(configKey, settings[configKey].value, settings[configKey].source);
43
74
  return { data: { key: configKey, ...entry }, humanLines: [formatLine(configKey, entry)] };
44
75
  }
45
76
  async function runConfigSet({ key, value }, ctx) {
77
+ if (isPublishKey(key)) return setPublishKey(key, value, ctx);
46
78
  const configKey = assertKey(key);
47
79
  const stored = parseValue(configKey, value);
48
- await persist(ctx.dataDir, configKey, stored);
80
+ await persist(ctx.dataDir, (existing) => ({ ...existing, [configKey]: stored }));
49
81
  const entry = renderSetting(configKey, stored, "file");
50
82
  return { data: { key: configKey, ...entry }, humanLines: [formatLine(configKey, entry)] };
51
83
  }
84
+ async function setPublishKey(key, value, ctx) {
85
+ const entry = key === "publish.mode" ? { value: parsePublishMode(value), source: "file" } : { value: toMoney(parseUsdToAtomic(value)), source: "file" };
86
+ const subkey = key === "publish.mode" ? "mode" : "defaultPrice";
87
+ const stored = key === "publish.mode" ? entry.value : entry.value.atomic;
88
+ await persist(ctx.dataDir, (existing) => ({
89
+ ...existing,
90
+ publish: { ...existing.publish, [subkey]: stored }
91
+ }));
92
+ return { data: { key, ...entry }, humanLines: [formatLine(key, entry)] };
93
+ }
94
+ function parsePublishMode(value) {
95
+ const parsed = PublishModeSchema.safeParse(value);
96
+ if (parsed.success) return parsed.data;
97
+ throw new CliError("USAGE", `Invalid publish mode: ${JSON.stringify(value)}`, {
98
+ fix: 'Use "review", "auto", or "full-auto".'
99
+ });
100
+ }
101
+ async function persistPublishMode(dir, mode) {
102
+ await persist(dir, (existing) => ({
103
+ ...existing,
104
+ publish: { ...existing.publish, mode }
105
+ }));
106
+ }
52
107
  async function resolveFromContext(ctx) {
53
108
  const config = await loadRawConfig(ctx.dataDir);
54
- return resolveSettings({ config, flags: { baseUrl: ctx.flags.baseUrl }, env: process.env });
109
+ const project = await loadProjectConfig(process.cwd());
110
+ return resolveSettings({
111
+ config,
112
+ flags: { baseUrl: ctx.flags.baseUrl },
113
+ env: process.env,
114
+ project: project?.layer
115
+ });
55
116
  }
56
117
  function assertKey(key) {
57
118
  if (CONFIG_KEYS.includes(key)) return key;
58
119
  throw new CliError("USAGE", `Unknown config key: ${JSON.stringify(key)}`, {
59
- fix: `Valid keys: ${CONFIG_KEYS.join(", ")}.`
120
+ fix: `Valid keys: ${[...CONFIG_KEYS, ...PUBLISH_CONFIG_KEYS].join(", ")}.`
60
121
  });
61
122
  }
62
123
  function renderSetting(key, stored, source) {
63
124
  return { ...renderValue(key, stored), source };
64
125
  }
126
+ function renderPublishSetting(key, settings) {
127
+ if (key === "publish.mode") {
128
+ return { value: settings.publishMode.value, source: settings.publishMode.source };
129
+ }
130
+ return {
131
+ value: toMoney(settings.publishDefaultPrice.value),
132
+ source: settings.publishDefaultPrice.source
133
+ };
134
+ }
65
135
  function renderValue(key, stored) {
66
- if (Array.isArray(stored)) return { value: stored };
136
+ if (Array.isArray(stored) || typeof stored === "boolean") return { value: stored };
67
137
  if (key === "maxAutoSpend" || key === "sessionBudget") return { value: toMoney(stored) };
68
138
  if (key === "confirm" && stored.startsWith(CONFIRM_ABOVE)) {
69
139
  return { value: stored, threshold: toMoney(stored.slice(CONFIRM_ABOVE.length)) };
@@ -83,8 +153,17 @@ function parseValue(key, value) {
83
153
  case "baseUrl":
84
154
  case "rpcUrl":
85
155
  return parseHttpUrl(value);
156
+ case "evalCohort":
157
+ return parseBoolean(value);
86
158
  }
87
159
  }
160
+ function parseBoolean(value) {
161
+ if (value === "true") return true;
162
+ if (value === "false") return false;
163
+ throw new CliError("USAGE", `Invalid boolean value: ${JSON.stringify(value)}`, {
164
+ fix: 'Use "true" or "false".'
165
+ });
166
+ }
88
167
  function parseConfirm(value) {
89
168
  if (value === "always") return "always";
90
169
  if (value.startsWith(CONFIRM_ABOVE)) {
@@ -121,13 +200,13 @@ function parseHttpUrl(value) {
121
200
  }
122
201
  return value;
123
202
  }
124
- async function persist(dir, key, stored) {
203
+ async function persist(dir, merge) {
125
204
  await mkdir(dir, { recursive: true, mode: 448 });
126
205
  const lockPath = `${configPath(dir)}.lock`;
127
206
  try {
128
207
  await withFileLock(lockPath, async () => {
129
208
  const existing = await loadRawConfig(dir);
130
- const merged = { ...existing, [key]: stored };
209
+ const merged = merge(existing);
131
210
  const validated = RawConfigSchema.parse(merged);
132
211
  await writeFileAtomic(configPath(dir), `${JSON.stringify(validated, null, 2)}
133
212
  `, {
@@ -149,16 +228,34 @@ function formatLine(key, entry) {
149
228
  const label = key.padEnd(KEY_WIDTH);
150
229
  return ` ${label} ${displayValue(entry)} ${styleText("dim", `(${entry.source})`)}`;
151
230
  }
231
+ function describedLine(key, entry, note) {
232
+ const description = KEY_DESCRIPTIONS[key];
233
+ let line = formatLine(key, entry);
234
+ if (description !== void 0) line += ` ${styleText("dim", `- ${description}`)}`;
235
+ return withNote(line, note);
236
+ }
237
+ function withNote(line, note) {
238
+ return note !== void 0 ? `${line} ${styleText("dim", `(${note})`)}` : line;
239
+ }
240
+ function downgradeNote(key, settings) {
241
+ if (key !== "publish.mode" || settings.publishMode.downgradedWarning === void 0) {
242
+ return void 0;
243
+ }
244
+ return "downgraded from full-auto: committed .tenjin.json (gitignore it to opt in)";
245
+ }
152
246
  function displayValue(entry) {
153
247
  const { value } = entry;
154
248
  if (Array.isArray(value)) return value.length > 0 ? value.join(", ") : "(empty)";
249
+ if (typeof value === "boolean") return value ? "true" : "false";
155
250
  if (typeof value === "object") return `${value.usd} USD`;
156
251
  if (entry.threshold !== void 0) return `above ${entry.threshold.usd} USD`;
157
252
  return value;
158
253
  }
254
+
159
255
  export {
160
- runConfigGet,
161
256
  runConfigList,
162
- runConfigSet
257
+ runConfigGet,
258
+ runConfigSet,
259
+ persistPublishMode
163
260
  };
164
- //# sourceMappingURL=config-CEFTL5ZL.js.map
261
+ //# sourceMappingURL=chunk-TFGSOPWR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/config.ts"],"sourcesContent":["import { mkdir } from 'node:fs/promises';\nimport { styleText } from 'node:util';\nimport { CliError } from '../lib/errors';\nimport {\n CONFIG_KEYS,\n PUBLISH_CONFIG_KEYS,\n PublishModeSchema,\n RawConfigSchema,\n loadRawConfig,\n resolveSettings,\n} from '../lib/config';\nimport type {\n EffectiveSettings,\n PartialConfig,\n Provenance,\n PublishConfigKey,\n PublishMode,\n ScalarConfigKey,\n} from '../lib/config';\nimport { loadProjectConfig } from '../lib/settings';\nimport { configPath } from '../lib/paths';\nimport { writeFileAtomic } from '../lib/atomic-json';\nimport { withFileLock, LockTimeoutError } from '../lib/lock';\nimport { parseUsdToAtomic, toMoney } from '../lib/money';\nimport type { Money } from '../schemas';\nimport type { CommandContext, CommandResult } from '../context';\n\n/**\n * How one config key is presented in `data`. `value` is the machine form: dual\n * Money for the spend keys, the stored string for `confirm`/URLs, the string[]\n * for the allowlist. `threshold` rides along only for `confirm: above:<atomic>`\n * so an agent reads the dollar amount without re-parsing the string.\n */\ninterface RenderedValue {\n value: Money | string | string[] | boolean;\n threshold?: Money;\n}\ninterface RenderedSetting extends RenderedValue {\n source: Provenance;\n}\n\nconst CONFIRM_ABOVE = 'above:';\nconst KEY_WIDTH = Math.max(...[...CONFIG_KEYS, ...PUBLISH_CONFIG_KEYS].map((key) => key.length));\n\n/**\n * A one-line human description per key, appended (dim) to the bare `config`\n * listing only. Machine `data` is unchanged; these are humanLines decoration.\n */\nconst KEY_DESCRIPTIONS: Record<string, string> = {\n maxAutoSpend: 'auto-approve a read up to this amount',\n sessionBudget: 'cap on total auto-spend per session',\n confirm: 'when to ask before paying',\n allowlistCreators: 'only auto-pay these creators (empty = any)',\n baseUrl: 'Tenjin API base URL',\n rpcUrl: 'Base RPC endpoint for balance reads',\n evalCohort: 'opt in to the lookup evaluation cohort',\n 'publish.mode': 'review=always ask, auto=ask on findings, full-auto=only hard blocks stop it',\n 'publish.defaultPrice': 'price used when none is given',\n};\n\nfunction isPublishKey(key: string): key is PublishConfigKey {\n return (PUBLISH_CONFIG_KEYS as readonly string[]).includes(key);\n}\n\n/**\n * Show every effective key with its value and provenance. `data` is keyed by\n * config key; provenance comes from resolveSettings over the *raw* file (not the\n * defaults-merged config), so a key the user never set reads `default`, not\n * `file`.\n */\nexport async function runConfigList(ctx: CommandContext): Promise<CommandResult> {\n const settings = await resolveFromContext(ctx);\n const data: Record<string, RenderedSetting> = {};\n const humanLines: string[] = [];\n for (const key of CONFIG_KEYS) {\n const entry = renderSetting(key, settings[key].value, settings[key].source);\n data[key] = entry;\n humanLines.push(describedLine(key, entry));\n }\n for (const key of PUBLISH_CONFIG_KEYS) {\n const entry = renderPublishSetting(key, settings);\n data[key] = entry;\n humanLines.push(describedLine(key, entry, downgradeNote(key, settings)));\n }\n return { data, humanLines };\n}\n\n/** Same per-key shape as list, for one key. Unknown key is a USAGE failure. */\nexport async function runConfigGet(\n { key }: { key: string },\n ctx: CommandContext,\n): Promise<CommandResult> {\n if (isPublishKey(key)) {\n const settings = await resolveFromContext(ctx);\n const entry = renderPublishSetting(key, settings);\n return {\n data: { key, ...entry },\n humanLines: [withNote(formatLine(key, entry), downgradeNote(key, settings))],\n };\n }\n const configKey = assertKey(key);\n const settings = await resolveFromContext(ctx);\n const entry = renderSetting(configKey, settings[configKey].value, settings[configKey].source);\n return { data: { key: configKey, ...entry }, humanLines: [formatLine(configKey, entry)] };\n}\n\n/**\n * Parse the value for this key, then persist it merged into the existing raw\n * file — never materializing defaults for keys the user did not set, so\n * provenance stays truthful. The written key now reads `file`.\n */\nexport async function runConfigSet(\n { key, value }: { key: string; value: string },\n ctx: CommandContext,\n): Promise<CommandResult> {\n if (isPublishKey(key)) return setPublishKey(key, value, ctx);\n const configKey = assertKey(key);\n const stored = parseValue(configKey, value);\n await persist(ctx.dataDir, (existing) => ({ ...existing, [configKey]: stored }));\n const entry = renderSetting(configKey, stored, 'file');\n return { data: { key: configKey, ...entry }, humanLines: [formatLine(configKey, entry)] };\n}\n\n/**\n * `config set publish.mode|publish.defaultPrice`. mode validates to the enum;\n * defaultPrice parses decimal USD at the edge into atomic (matching the spend\n * keys). The subkey is merged into the nested publish block, so a sibling subkey\n * (or an unknown one a newer CLI wrote) is preserved.\n */\nasync function setPublishKey(\n key: PublishConfigKey,\n value: string,\n ctx: CommandContext,\n): Promise<CommandResult> {\n const entry: RenderedSetting =\n key === 'publish.mode'\n ? { value: parsePublishMode(value), source: 'file' }\n : { value: toMoney(parseUsdToAtomic(value)), source: 'file' };\n const subkey = key === 'publish.mode' ? 'mode' : 'defaultPrice';\n const stored = key === 'publish.mode' ? (entry.value as string) : (entry.value as Money).atomic;\n await persist(ctx.dataDir, (existing) => ({\n ...existing,\n publish: { ...existing.publish, [subkey]: stored },\n }));\n return { data: { key, ...entry }, humanLines: [formatLine(key, entry)] };\n}\n\nfunction parsePublishMode(value: string): string {\n const parsed = PublishModeSchema.safeParse(value);\n if (parsed.success) return parsed.data;\n throw new CliError('USAGE', `Invalid publish mode: ${JSON.stringify(value)}`, {\n fix: 'Use \"review\", \"auto\", or \"full-auto\".',\n });\n}\n\n/**\n * Persist just `publish.mode` into the global config through the same locked\n * merge-write every `config set` uses (never a raw overwrite), so a sibling\n * subkey or an unknown block a newer CLI wrote is preserved. Used by `install`'s\n * setup prompt; the mode is a validated PublishMode.\n */\nexport async function persistPublishMode(dir: string, mode: PublishMode): Promise<void> {\n await persist(dir, (existing) => ({\n ...existing,\n publish: { ...existing.publish, mode },\n }));\n}\n\nasync function resolveFromContext(ctx: CommandContext): Promise<EffectiveSettings> {\n const config = await loadRawConfig(ctx.dataDir);\n // Feed the per-project `.tenjin.json` layer so the publish keys read out what a\n // real `publish` would resolve (project/env included), not a global-only guess.\n const project = await loadProjectConfig(process.cwd());\n return resolveSettings({\n config,\n flags: { baseUrl: ctx.flags.baseUrl },\n env: process.env,\n project: project?.layer,\n });\n}\n\nfunction assertKey(key: string): ScalarConfigKey {\n if ((CONFIG_KEYS as string[]).includes(key)) return key as ScalarConfigKey;\n throw new CliError('USAGE', `Unknown config key: ${JSON.stringify(key)}`, {\n fix: `Valid keys: ${[...CONFIG_KEYS, ...PUBLISH_CONFIG_KEYS].join(', ')}.`,\n });\n}\n\nfunction renderSetting(\n key: ScalarConfigKey,\n stored: string | string[] | boolean,\n source: Provenance,\n): RenderedSetting {\n return { ...renderValue(key, stored), source };\n}\n\n/**\n * The list/get shape for a publish key, read from the resolved effective\n * settings, which now include the per-project `.tenjin.json` layer (see\n * resolveFromContext) so the source reflects what a publish would actually use.\n */\nfunction renderPublishSetting(key: PublishConfigKey, settings: EffectiveSettings): RenderedSetting {\n if (key === 'publish.mode') {\n return { value: settings.publishMode.value, source: settings.publishMode.source };\n }\n return {\n value: toMoney(settings.publishDefaultPrice.value),\n source: settings.publishDefaultPrice.source,\n };\n}\n\nfunction renderValue(key: ScalarConfigKey, stored: string | string[] | boolean): RenderedValue {\n if (Array.isArray(stored) || typeof stored === 'boolean') return { value: stored };\n if (key === 'maxAutoSpend' || key === 'sessionBudget') return { value: toMoney(stored) };\n if (key === 'confirm' && stored.startsWith(CONFIRM_ABOVE)) {\n return { value: stored, threshold: toMoney(stored.slice(CONFIRM_ABOVE.length)) };\n }\n return { value: stored };\n}\n\n/** Per-key edge parsing. Returns the persisted form; throws USAGE on bad input. */\nfunction parseValue(key: ScalarConfigKey, value: string): string | string[] | boolean {\n switch (key) {\n case 'maxAutoSpend':\n case 'sessionBudget':\n return parseUsdToAtomic(value); // throws USAGE on a bad amount\n case 'confirm':\n return parseConfirm(value);\n case 'allowlistCreators':\n return parseAllowlist(value);\n case 'baseUrl':\n case 'rpcUrl':\n return parseHttpUrl(value);\n case 'evalCohort':\n return parseBoolean(value);\n }\n}\n\nfunction parseBoolean(value: string): boolean {\n if (value === 'true') return true;\n if (value === 'false') return false;\n throw new CliError('USAGE', `Invalid boolean value: ${JSON.stringify(value)}`, {\n fix: 'Use \"true\" or \"false\".',\n });\n}\n\nfunction parseConfirm(value: string): string {\n if (value === 'always') return 'always';\n if (value.startsWith(CONFIRM_ABOVE)) {\n return `${CONFIRM_ABOVE}${parseUsdToAtomic(value.slice(CONFIRM_ABOVE.length))}`;\n }\n throw new CliError('USAGE', `Invalid confirm value: ${JSON.stringify(value)}`, {\n fix: 'Use \"always\" or \"above:<usd>\", e.g. above:0.25.',\n });\n}\n\nfunction parseAllowlist(value: string): string[] {\n // \"\" clears to []; comma-split, trim, drop empties. Reject an entry with\n // internal whitespace — a creator handle/address is a single token.\n const entries = value\n .split(',')\n .map((entry) => entry.trim())\n .filter((entry) => entry.length > 0);\n for (const entry of entries) {\n if (/\\s/.test(entry)) {\n throw new CliError('USAGE', `Invalid creator entry: ${JSON.stringify(entry)}`, {\n fix: 'Creator entries cannot contain spaces; separate multiple with commas.',\n });\n }\n }\n return entries;\n}\n\nfunction parseHttpUrl(value: string): string {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n throw new CliError('USAGE', `Invalid URL: ${JSON.stringify(value)}`, {\n fix: 'Pass an absolute http(s) URL like https://tenjin.blog.',\n });\n }\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new CliError('USAGE', `URL must be http or https: ${JSON.stringify(value)}`, {\n fix: 'Pass an absolute http(s) URL like https://tenjin.blog.',\n });\n }\n return value;\n}\n\n/**\n * Apply `merge` to the raw file and write it back atomically. Uses the partial\n * schema and writeFileAtomic (not writeConfig, which would materialize every\n * default into the file and corrupt provenance). loadRawConfig surfaces a\n * corrupt existing file as CONFIG_INVALID, which propagates.\n *\n * The whole read-merge-write runs under a cross-process file lock: without it two\n * concurrent `config set` on different keys race on the read, and the last writer\n * drops the other's update (worst case, a zeroed maxAutoSpend resurrected). The\n * data dir is ensured first so the lock's mkdir has a parent on a fresh install.\n */\nasync function persist(\n dir: string,\n merge: (existing: PartialConfig) => PartialConfig,\n): Promise<void> {\n await mkdir(dir, { recursive: true, mode: 0o700 });\n const lockPath = `${configPath(dir)}.lock`;\n try {\n await withFileLock(lockPath, async () => {\n const existing = await loadRawConfig(dir);\n const merged = merge(existing);\n const validated = RawConfigSchema.parse(merged);\n await writeFileAtomic(configPath(dir), `${JSON.stringify(validated, null, 2)}\\n`, {\n mode: 0o644,\n dirMode: 0o700,\n });\n });\n } catch (err) {\n // A lock timeout is not the user's malformed input; surface it as INTERNAL with\n // the one manual step (there is no auto-steal), keeping the JSON error contract.\n if (err instanceof LockTimeoutError) {\n throw new CliError('INTERNAL', err.message, {\n fix: `If no other tenjin process is running, remove ${lockPath} and retry.`,\n cause: err,\n });\n }\n throw err;\n }\n}\n\nfunction formatLine(key: string, entry: RenderedSetting): string {\n const label = key.padEnd(KEY_WIDTH);\n return ` ${label} ${displayValue(entry)} ${styleText('dim', `(${entry.source})`)}`;\n}\n\n/** The list variant: the value line, a dim description, and an optional dim note. */\nfunction describedLine(key: string, entry: RenderedSetting, note?: string): string {\n const description = KEY_DESCRIPTIONS[key];\n let line = formatLine(key, entry);\n if (description !== undefined) line += ` ${styleText('dim', `- ${description}`)}`;\n return withNote(line, note);\n}\n\n/** Append a dim parenthetical note (e.g. the full-auto downgrade) when present. */\nfunction withNote(line: string, note?: string): string {\n return note !== undefined ? `${line} ${styleText('dim', `(${note})`)}` : line;\n}\n\n/**\n * The `publish.mode` line gains a downgrade note when the effective mode came from\n * a committed `.tenjin.json` asking for `full-auto` (the loosening gate demoted it\n * to `auto`). Human-only; the machine `data` shape is unchanged.\n */\nfunction downgradeNote(key: PublishConfigKey, settings: EffectiveSettings): string | undefined {\n if (key !== 'publish.mode' || settings.publishMode.downgradedWarning === undefined) {\n return undefined;\n }\n return 'downgraded from full-auto: committed .tenjin.json (gitignore it to opt in)';\n}\n\nfunction displayValue(entry: RenderedSetting): string {\n const { value } = entry;\n if (Array.isArray(value)) return value.length > 0 ? value.join(', ') : '(empty)';\n if (typeof value === 'boolean') return value ? 'true' : 'false'; // evalCohort\n if (typeof value === 'object') return `${value.usd} USD`; // Money (spend keys)\n if (entry.threshold !== undefined) return `above ${entry.threshold.usd} USD`; // confirm\n return value; // 'always', baseUrl, rpcUrl\n}\n"],"mappings":"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","names":["settings","entry"]}
@@ -0,0 +1,126 @@
1
+ import { createRequire as __tenjinCreateRequire } from 'node:module'; const require = __tenjinCreateRequire(import.meta.url);
2
+ import {
3
+ UUID_RE,
4
+ newId
5
+ } from "./chunk-3OV7E5G5.js";
6
+ import {
7
+ hasCode
8
+ } from "./chunk-2I4QTOLB.js";
9
+ import {
10
+ writeFileAtomicExclusive
11
+ } from "./chunk-MUBUQVC3.js";
12
+ import {
13
+ external_exports
14
+ } from "./chunk-OYMYCW7J.js";
15
+
16
+ // src/lib/candidate-store.ts
17
+ import { mkdir, readFile, readdir, rm, stat } from "fs/promises";
18
+ import { join } from "path";
19
+ var DRAFT_FILE = "draft.md";
20
+ var META_FILE = "meta.json";
21
+ var CandidateMetaSchema = external_exports.object({
22
+ schemaVersion: external_exports.literal(1),
23
+ lookupId: external_exports.string(),
24
+ question: external_exports.string().optional(),
25
+ // Fixed-width UTC ISO-8601 (a trailing Z, never an offset): listCandidates
26
+ // sorts newest-first lexicographically on this string, which is only a valid
27
+ // proxy for chronological order when every value is same-zone UTC. z.iso
28
+ // .datetime() rejects offsets and non-datetime strings, pinning that invariant.
29
+ created: external_exports.iso.datetime(),
30
+ /** The repo root the add ran in, or the cwd when not in a repo. */
31
+ sourceProject: external_exports.string()
32
+ });
33
+ function candidatesDir(dataDir) {
34
+ return join(dataDir, "candidates");
35
+ }
36
+ function candidateDir(dataDir, id) {
37
+ return join(candidatesDir(dataDir), id);
38
+ }
39
+ async function createCandidate(dataDir, input) {
40
+ const id = newId();
41
+ const dir = candidateDir(dataDir, id);
42
+ await mkdir(dir, { recursive: true, mode: 448 });
43
+ const draftPath = join(dir, DRAFT_FILE);
44
+ const meta = {
45
+ schemaVersion: 1,
46
+ lookupId: input.lookupId,
47
+ ...input.question !== void 0 ? { question: input.question } : {},
48
+ created: input.created,
49
+ sourceProject: input.sourceProject
50
+ };
51
+ try {
52
+ await writeFileAtomicExclusive(draftPath, input.draft, { mode: 384, dirMode: 448 });
53
+ await writeFileAtomicExclusive(join(dir, META_FILE), `${JSON.stringify(meta, null, 2)}
54
+ `, {
55
+ mode: 384,
56
+ dirMode: 448
57
+ });
58
+ } catch (err) {
59
+ if (!hasCode(err, "EEXIST")) {
60
+ await rm(dir, { recursive: true, force: true }).catch(() => void 0);
61
+ }
62
+ throw err;
63
+ }
64
+ return { id, meta, dir, draftPath };
65
+ }
66
+ async function readCandidate(dataDir, id) {
67
+ if (!UUID_RE.test(id)) return null;
68
+ const dir = candidateDir(dataDir, id);
69
+ const meta = await readMeta(dir);
70
+ if (meta === null) return null;
71
+ return { id, meta, dir, draftPath: join(dir, DRAFT_FILE) };
72
+ }
73
+ async function listCandidates(dataDir) {
74
+ let entries;
75
+ try {
76
+ entries = await readdir(candidatesDir(dataDir), { withFileTypes: true });
77
+ } catch {
78
+ return [];
79
+ }
80
+ const records = [];
81
+ for (const entry of entries) {
82
+ if (!entry.isDirectory() || !UUID_RE.test(entry.name)) continue;
83
+ const record = await readCandidate(dataDir, entry.name);
84
+ if (record !== null) records.push(record);
85
+ }
86
+ records.sort((a, b) => {
87
+ if (a.meta.created !== b.meta.created) return a.meta.created < b.meta.created ? 1 : -1;
88
+ return a.id < b.id ? 1 : -1;
89
+ });
90
+ return records;
91
+ }
92
+ async function dropCandidate(dataDir, id) {
93
+ if (!UUID_RE.test(id)) return false;
94
+ const dir = candidateDir(dataDir, id);
95
+ try {
96
+ if (!(await stat(dir)).isDirectory()) return false;
97
+ } catch {
98
+ return false;
99
+ }
100
+ await rm(dir, { recursive: true, force: true });
101
+ return true;
102
+ }
103
+ async function readMeta(dir) {
104
+ let raw;
105
+ try {
106
+ raw = await readFile(join(dir, META_FILE), "utf8");
107
+ } catch {
108
+ return null;
109
+ }
110
+ let json;
111
+ try {
112
+ json = JSON.parse(raw);
113
+ } catch {
114
+ return null;
115
+ }
116
+ const parsed = CandidateMetaSchema.safeParse(json);
117
+ return parsed.success ? parsed.data : null;
118
+ }
119
+
120
+ export {
121
+ createCandidate,
122
+ readCandidate,
123
+ listCandidates,
124
+ dropCandidate
125
+ };
126
+ //# sourceMappingURL=chunk-TLCMMYLY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/candidate-store.ts"],"sourcesContent":["import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises';\nimport type { Dirent } from 'node:fs';\nimport { join } from 'node:path';\nimport { z } from 'zod';\nimport { writeFileAtomicExclusive } from './atomic-json';\nimport { hasCode } from './errno';\nimport { newId, UUID_RE } from './ids';\n\n/**\n * The D40 candidate store: publish drafts parked locally that never touch the\n * network and never upload — only a later `tenjin publish --candidate <id>`\n * sends one, under the D38 consent scan. Each candidate is its OWN directory\n * `~/.tenjin/candidates/<id>/` holding the draft markdown plus a meta.json.\n *\n * Custody mirrors the wallet key discipline: the candidates tree is 0700 and\n * every file 0600, written create-once through writeFileAtomicExclusive so a\n * reader sees a complete file or none and a uuid collision is loud, never a\n * silent overwrite. Unlike lookup-store's single shared ledger, there is no shared\n * mutable file here — a fresh uuid per add means two concurrent adds land in two\n * distinct dirs and cannot contend — so no cross-process lock is needed. The\n * meta.json is written LAST and atomically, making it the commit point: `list`\n * skips any dir whose meta is absent or unreadable, so a torn or half-written\n * add reads as not-yet-there rather than a broken candidate, the same\n * best-effort posture loadLookups takes on a corrupt store. A crash mid-add is\n * cleaned up eagerly (the dir is removed on any write failure), and `drop` can\n * still remove a metaless orphan by id so a leaked draft is never stranded.\n */\n\nconst DRAFT_FILE = 'draft.md';\nconst META_FILE = 'meta.json';\n\nconst CandidateMetaSchema = z.object({\n schemaVersion: z.literal(1),\n lookupId: z.string(),\n question: z.string().optional(),\n // Fixed-width UTC ISO-8601 (a trailing Z, never an offset): listCandidates\n // sorts newest-first lexicographically on this string, which is only a valid\n // proxy for chronological order when every value is same-zone UTC. z.iso\n // .datetime() rejects offsets and non-datetime strings, pinning that invariant.\n created: z.iso.datetime(),\n /** The repo root the add ran in, or the cwd when not in a repo. */\n sourceProject: z.string(),\n});\nexport type CandidateMeta = z.infer<typeof CandidateMetaSchema>;\n\nexport interface CandidateRecord {\n id: string;\n meta: CandidateMeta;\n /** Absolute path of the candidate directory. */\n dir: string;\n /** Absolute path of the draft markdown inside the directory. */\n draftPath: string;\n}\n\nexport interface CreateCandidateInput {\n /** The draft markdown, copied verbatim into the candidate. */\n draft: string;\n lookupId: string;\n question?: string;\n /** ISO-8601 creation time; the command supplies it via its clock seam. */\n created: string;\n sourceProject: string;\n}\n\nfunction candidatesDir(dataDir: string): string {\n return join(dataDir, 'candidates');\n}\n\nfunction candidateDir(dataDir: string, id: string): string {\n return join(candidatesDir(dataDir), id);\n}\n\n/** Park a draft as a new candidate and return the created record. Both files are\n * written create-once (no-clobber, fsynced) at a fresh uuid path, meta.json\n * last, so the candidate is either fully present to `list`/`read` or absent,\n * never half-there. */\nexport async function createCandidate(\n dataDir: string,\n input: CreateCandidateInput,\n): Promise<CandidateRecord> {\n const id = newId();\n const dir = candidateDir(dataDir, id);\n await mkdir(dir, { recursive: true, mode: 0o700 });\n\n const draftPath = join(dir, DRAFT_FILE);\n const meta: CandidateMeta = {\n schemaVersion: 1,\n lookupId: input.lookupId,\n ...(input.question !== undefined ? { question: input.question } : {}),\n created: input.created,\n sourceProject: input.sourceProject,\n };\n try {\n // Exclusive (create-or-EEXIST, fsynced) so a uuid collision is loud, not a\n // silent overwrite of an existing candidate, and the crash-durable commit of\n // meta.json is the last, atomic step.\n await writeFileAtomicExclusive(draftPath, input.draft, { mode: 0o600, dirMode: 0o700 });\n await writeFileAtomicExclusive(join(dir, META_FILE), `${JSON.stringify(meta, null, 2)}\\n`, {\n mode: 0o600,\n dirMode: 0o700,\n });\n } catch (err) {\n // An EEXIST means something already lived at this fresh-uuid path (a real\n // collision, astronomically unlikely): surface it loudly and NEVER delete\n // what was already there. Any other failure is a partial write of our own\n // fresh dir — remove it so a crash mid-add can't strand a hidden dir holding\n // the (secrets-adjacent) draft that `list` skips and no one can name to `drop`.\n if (!hasCode(err, 'EEXIST')) {\n await rm(dir, { recursive: true, force: true }).catch(() => undefined);\n }\n throw err;\n }\n\n return { id, meta, dir, draftPath };\n}\n\n/** One candidate by id, or null when it is unknown (or the id is malformed, so a\n * stray argument can never traverse out of the store). */\nexport async function readCandidate(dataDir: string, id: string): Promise<CandidateRecord | null> {\n if (!UUID_RE.test(id)) return null;\n const dir = candidateDir(dataDir, id);\n const meta = await readMeta(dir);\n if (meta === null) return null;\n return { id, meta, dir, draftPath: join(dir, DRAFT_FILE) };\n}\n\n/** Every pending candidate, newest first by `created` (no auto-expiry — stale\n * candidates stay visible, D40). Tie-broken on id so the order is total. */\nexport async function listCandidates(dataDir: string): Promise<CandidateRecord[]> {\n let entries: Dirent[];\n try {\n entries = await readdir(candidatesDir(dataDir), { withFileTypes: true });\n } catch {\n return [];\n }\n const records: CandidateRecord[] = [];\n for (const entry of entries) {\n if (!entry.isDirectory() || !UUID_RE.test(entry.name)) continue;\n const record = await readCandidate(dataDir, entry.name);\n if (record !== null) records.push(record);\n }\n records.sort((a, b) => {\n if (a.meta.created !== b.meta.created) return a.meta.created < b.meta.created ? 1 : -1;\n return a.id < b.id ? 1 : -1;\n });\n return records;\n}\n\n/** Discard one candidate by id. Returns false when the id is malformed or no\n * such dir exists, so the command can surface a clean not-found; nothing is\n * ever auto-deleted. Removes the dir even when its meta is absent or corrupt,\n * so a crash-orphaned candidate can still be cleaned up by id (the id is\n * UUID-gated first, so a stray argument can never target a path outside the\n * store). */\nexport async function dropCandidate(dataDir: string, id: string): Promise<boolean> {\n if (!UUID_RE.test(id)) return false;\n const dir = candidateDir(dataDir, id);\n try {\n if (!(await stat(dir)).isDirectory()) return false;\n } catch {\n return false; // no such candidate\n }\n await rm(dir, { recursive: true, force: true });\n return true;\n}\n\n/** Read and validate a candidate's meta.json; null on any absence/corruption. */\nasync function readMeta(dir: string): Promise<CandidateMeta | null> {\n let raw: string;\n try {\n raw = await readFile(join(dir, META_FILE), 'utf8');\n } catch {\n return null;\n }\n let json: unknown;\n try {\n json = JSON.parse(raw);\n } catch {\n return null;\n }\n const parsed = CandidateMetaSchema.safeParse(json);\n return parsed.success ? parsed.data : null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA,SAAS,OAAO,UAAU,SAAS,IAAI,YAAY;AAEnD,SAAS,YAAY;AA0BrB,IAAM,aAAa;AACnB,IAAM,YAAY;AAElB,IAAM,sBAAsB,iBAAE,OAAO;AAAA,EACnC,eAAe,iBAAE,QAAQ,CAAC;AAAA,EAC1B,UAAU,iBAAE,OAAO;AAAA,EACnB,UAAU,iBAAE,OAAO,EAAE,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAK9B,SAAS,iBAAE,IAAI,SAAS;AAAA;AAAA,EAExB,eAAe,iBAAE,OAAO;AAC1B,CAAC;AAsBD,SAAS,cAAc,SAAyB;AAC9C,SAAO,KAAK,SAAS,YAAY;AACnC;AAEA,SAAS,aAAa,SAAiB,IAAoB;AACzD,SAAO,KAAK,cAAc,OAAO,GAAG,EAAE;AACxC;AAMA,eAAsB,gBACpB,SACA,OAC0B;AAC1B,QAAM,KAAK,MAAM;AACjB,QAAM,MAAM,aAAa,SAAS,EAAE;AACpC,QAAM,MAAM,KAAK,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAEjD,QAAM,YAAY,KAAK,KAAK,UAAU;AACtC,QAAM,OAAsB;AAAA,IAC1B,eAAe;AAAA,IACf,UAAU,MAAM;AAAA,IAChB,GAAI,MAAM,aAAa,SAAY,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;AAAA,IACnE,SAAS,MAAM;AAAA,IACf,eAAe,MAAM;AAAA,EACvB;AACA,MAAI;AAIF,UAAM,yBAAyB,WAAW,MAAM,OAAO,EAAE,MAAM,KAAO,SAAS,IAAM,CAAC;AACtF,UAAM,yBAAyB,KAAK,KAAK,SAAS,GAAG,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;AAAA,GAAM;AAAA,MACzF,MAAM;AAAA,MACN,SAAS;AAAA,IACX,CAAC;AAAA,EACH,SAAS,KAAK;AAMZ,QAAI,CAAC,QAAQ,KAAK,QAAQ,GAAG;AAC3B,YAAM,GAAG,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,IACvE;AACA,UAAM;AAAA,EACR;AAEA,SAAO,EAAE,IAAI,MAAM,KAAK,UAAU;AACpC;AAIA,eAAsB,cAAc,SAAiB,IAA6C;AAChG,MAAI,CAAC,QAAQ,KAAK,EAAE,EAAG,QAAO;AAC9B,QAAM,MAAM,aAAa,SAAS,EAAE;AACpC,QAAM,OAAO,MAAM,SAAS,GAAG;AAC/B,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,EAAE,IAAI,MAAM,KAAK,WAAW,KAAK,KAAK,UAAU,EAAE;AAC3D;AAIA,eAAsB,eAAe,SAA6C;AAChF,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,QAAQ,cAAc,OAAO,GAAG,EAAE,eAAe,KAAK,CAAC;AAAA,EACzE,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,QAAM,UAA6B,CAAC;AACpC,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,YAAY,KAAK,CAAC,QAAQ,KAAK,MAAM,IAAI,EAAG;AACvD,UAAM,SAAS,MAAM,cAAc,SAAS,MAAM,IAAI;AACtD,QAAI,WAAW,KAAM,SAAQ,KAAK,MAAM;AAAA,EAC1C;AACA,UAAQ,KAAK,CAAC,GAAG,MAAM;AACrB,QAAI,EAAE,KAAK,YAAY,EAAE,KAAK,QAAS,QAAO,EAAE,KAAK,UAAU,EAAE,KAAK,UAAU,IAAI;AACpF,WAAO,EAAE,KAAK,EAAE,KAAK,IAAI;AAAA,EAC3B,CAAC;AACD,SAAO;AACT;AAQA,eAAsB,cAAc,SAAiB,IAA8B;AACjF,MAAI,CAAC,QAAQ,KAAK,EAAE,EAAG,QAAO;AAC9B,QAAM,MAAM,aAAa,SAAS,EAAE;AACpC,MAAI;AACF,QAAI,EAAE,MAAM,KAAK,GAAG,GAAG,YAAY,EAAG,QAAO;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,GAAG,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC9C,SAAO;AACT;AAGA,eAAe,SAAS,KAA4C;AAClE,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,SAAS,KAAK,KAAK,SAAS,GAAG,MAAM;AAAA,EACnD,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,SAAS,oBAAoB,UAAU,IAAI;AACjD,SAAO,OAAO,UAAU,OAAO,OAAO;AACxC;","names":[]}