@tabai/sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +401 -0
  3. package/bin/tab.mjs +23 -0
  4. package/dist/_shared/abi.d.ts +150 -0
  5. package/dist/_shared/abi.d.ts.map +1 -0
  6. package/dist/_shared/abi.js +197 -0
  7. package/dist/_shared/abi.js.map +1 -0
  8. package/dist/_shared/chains.d.ts +118 -0
  9. package/dist/_shared/chains.d.ts.map +1 -0
  10. package/dist/_shared/chains.js +89 -0
  11. package/dist/_shared/chains.js.map +1 -0
  12. package/dist/_shared/hex.d.ts +35 -0
  13. package/dist/_shared/hex.d.ts.map +1 -0
  14. package/dist/_shared/hex.js +40 -0
  15. package/dist/_shared/hex.js.map +1 -0
  16. package/dist/_shared/index.d.ts +14 -0
  17. package/dist/_shared/index.d.ts.map +1 -0
  18. package/dist/_shared/index.js +14 -0
  19. package/dist/_shared/index.js.map +1 -0
  20. package/dist/_shared/keccak256.d.ts +29 -0
  21. package/dist/_shared/keccak256.d.ts.map +1 -0
  22. package/dist/_shared/keccak256.js +145 -0
  23. package/dist/_shared/keccak256.js.map +1 -0
  24. package/dist/_shared/result.d.ts +78 -0
  25. package/dist/_shared/result.d.ts.map +1 -0
  26. package/dist/_shared/result.js +61 -0
  27. package/dist/_shared/result.js.map +1 -0
  28. package/dist/cli/client-config.d.ts +155 -0
  29. package/dist/cli/client-config.d.ts.map +1 -0
  30. package/dist/cli/client-config.js +382 -0
  31. package/dist/cli/client-config.js.map +1 -0
  32. package/dist/cli/connect.d.ts +76 -0
  33. package/dist/cli/connect.d.ts.map +1 -0
  34. package/dist/cli/connect.js +158 -0
  35. package/dist/cli/connect.js.map +1 -0
  36. package/dist/cli/doctor.d.ts +57 -0
  37. package/dist/cli/doctor.d.ts.map +1 -0
  38. package/dist/cli/doctor.js +253 -0
  39. package/dist/cli/doctor.js.map +1 -0
  40. package/dist/cli/index.d.ts +13 -0
  41. package/dist/cli/index.d.ts.map +1 -0
  42. package/dist/cli/index.js +13 -0
  43. package/dist/cli/index.js.map +1 -0
  44. package/dist/cli/main.d.ts +45 -0
  45. package/dist/cli/main.d.ts.map +1 -0
  46. package/dist/cli/main.js +371 -0
  47. package/dist/cli/main.js.map +1 -0
  48. package/dist/errors.d.ts +29 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +37 -0
  51. package/dist/errors.js.map +1 -0
  52. package/dist/http/client-402.d.ts +243 -0
  53. package/dist/http/client-402.d.ts.map +1 -0
  54. package/dist/http/client-402.js +515 -0
  55. package/dist/http/client-402.js.map +1 -0
  56. package/dist/http/headers.d.ts +173 -0
  57. package/dist/http/headers.d.ts.map +1 -0
  58. package/dist/http/headers.js +284 -0
  59. package/dist/http/headers.js.map +1 -0
  60. package/dist/http/index.d.ts +15 -0
  61. package/dist/http/index.d.ts.map +1 -0
  62. package/dist/http/index.js +15 -0
  63. package/dist/http/index.js.map +1 -0
  64. package/dist/http/metering-claim.d.ts +82 -0
  65. package/dist/http/metering-claim.d.ts.map +1 -0
  66. package/dist/http/metering-claim.js +99 -0
  67. package/dist/http/metering-claim.js.map +1 -0
  68. package/dist/index.d.ts +48 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +51 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/logger.d.ts +40 -0
  73. package/dist/logger.d.ts.map +1 -0
  74. package/dist/logger.js +50 -0
  75. package/dist/logger.js.map +1 -0
  76. package/dist/mcp/assets.d.ts +31 -0
  77. package/dist/mcp/assets.d.ts.map +1 -0
  78. package/dist/mcp/assets.js +78 -0
  79. package/dist/mcp/assets.js.map +1 -0
  80. package/dist/mcp/index.d.ts +19 -0
  81. package/dist/mcp/index.d.ts.map +1 -0
  82. package/dist/mcp/index.js +19 -0
  83. package/dist/mcp/index.js.map +1 -0
  84. package/dist/mcp/json-schema.d.ts +86 -0
  85. package/dist/mcp/json-schema.d.ts.map +1 -0
  86. package/dist/mcp/json-schema.js +215 -0
  87. package/dist/mcp/json-schema.js.map +1 -0
  88. package/dist/mcp/json.d.ts +43 -0
  89. package/dist/mcp/json.d.ts.map +1 -0
  90. package/dist/mcp/json.js +69 -0
  91. package/dist/mcp/json.js.map +1 -0
  92. package/dist/mcp/registry-client.d.ts +88 -0
  93. package/dist/mcp/registry-client.d.ts.map +1 -0
  94. package/dist/mcp/registry-client.js +158 -0
  95. package/dist/mcp/registry-client.js.map +1 -0
  96. package/dist/mcp/schemas.d.ts +82 -0
  97. package/dist/mcp/schemas.d.ts.map +1 -0
  98. package/dist/mcp/schemas.js +493 -0
  99. package/dist/mcp/schemas.js.map +1 -0
  100. package/dist/mcp/server.d.ts +97 -0
  101. package/dist/mcp/server.d.ts.map +1 -0
  102. package/dist/mcp/server.js +285 -0
  103. package/dist/mcp/server.js.map +1 -0
  104. package/dist/mcp/settings.d.ts +90 -0
  105. package/dist/mcp/settings.d.ts.map +1 -0
  106. package/dist/mcp/settings.js +160 -0
  107. package/dist/mcp/settings.js.map +1 -0
  108. package/dist/mcp/toolset.d.ts +231 -0
  109. package/dist/mcp/toolset.d.ts.map +1 -0
  110. package/dist/mcp/toolset.js +760 -0
  111. package/dist/mcp/toolset.js.map +1 -0
  112. package/dist/payments/abi.d.ts +9 -0
  113. package/dist/payments/abi.d.ts.map +1 -0
  114. package/dist/payments/abi.js +17 -0
  115. package/dist/payments/abi.js.map +1 -0
  116. package/dist/payments/config.d.ts +199 -0
  117. package/dist/payments/config.d.ts.map +1 -0
  118. package/dist/payments/config.js +259 -0
  119. package/dist/payments/config.js.map +1 -0
  120. package/dist/payments/index.d.ts +13 -0
  121. package/dist/payments/index.d.ts.map +1 -0
  122. package/dist/payments/index.js +13 -0
  123. package/dist/payments/index.js.map +1 -0
  124. package/dist/payments/kuru.d.ts +191 -0
  125. package/dist/payments/kuru.d.ts.map +1 -0
  126. package/dist/payments/kuru.js +377 -0
  127. package/dist/payments/kuru.js.map +1 -0
  128. package/dist/payments/monad.d.ts +69 -0
  129. package/dist/payments/monad.d.ts.map +1 -0
  130. package/dist/payments/monad.js +306 -0
  131. package/dist/payments/monad.js.map +1 -0
  132. package/dist/payments/permit2.d.ts +118 -0
  133. package/dist/payments/permit2.d.ts.map +1 -0
  134. package/dist/payments/permit2.js +366 -0
  135. package/dist/payments/permit2.js.map +1 -0
  136. package/dist/payments/registry.d.ts +119 -0
  137. package/dist/payments/registry.d.ts.map +1 -0
  138. package/dist/payments/registry.js +199 -0
  139. package/dist/payments/registry.js.map +1 -0
  140. package/dist/payments/strategy.d.ts +80 -0
  141. package/dist/payments/strategy.d.ts.map +1 -0
  142. package/dist/payments/strategy.js +103 -0
  143. package/dist/payments/strategy.js.map +1 -0
  144. package/dist/proxy/hooks.d.ts +90 -0
  145. package/dist/proxy/hooks.d.ts.map +1 -0
  146. package/dist/proxy/hooks.js +35 -0
  147. package/dist/proxy/hooks.js.map +1 -0
  148. package/dist/proxy/index.d.ts +9 -0
  149. package/dist/proxy/index.d.ts.map +1 -0
  150. package/dist/proxy/index.js +9 -0
  151. package/dist/proxy/index.js.map +1 -0
  152. package/dist/proxy/proxy.d.ts +156 -0
  153. package/dist/proxy/proxy.d.ts.map +1 -0
  154. package/dist/proxy/proxy.js +366 -0
  155. package/dist/proxy/proxy.js.map +1 -0
  156. package/dist/server/adapters/express.d.ts +89 -0
  157. package/dist/server/adapters/express.d.ts.map +1 -0
  158. package/dist/server/adapters/express.js +215 -0
  159. package/dist/server/adapters/express.js.map +1 -0
  160. package/dist/server/adapters/hono.d.ts +52 -0
  161. package/dist/server/adapters/hono.d.ts.map +1 -0
  162. package/dist/server/adapters/hono.js +61 -0
  163. package/dist/server/adapters/hono.js.map +1 -0
  164. package/dist/server/adapters/next.d.ts +52 -0
  165. package/dist/server/adapters/next.d.ts.map +1 -0
  166. package/dist/server/adapters/next.js +56 -0
  167. package/dist/server/adapters/next.js.map +1 -0
  168. package/dist/server/index.d.ts +30 -0
  169. package/dist/server/index.d.ts.map +1 -0
  170. package/dist/server/index.js +30 -0
  171. package/dist/server/index.js.map +1 -0
  172. package/dist/server/metering.d.ts +209 -0
  173. package/dist/server/metering.d.ts.map +1 -0
  174. package/dist/server/metering.js +365 -0
  175. package/dist/server/metering.js.map +1 -0
  176. package/dist/server/post-paid.d.ts +355 -0
  177. package/dist/server/post-paid.d.ts.map +1 -0
  178. package/dist/server/post-paid.js +512 -0
  179. package/dist/server/post-paid.js.map +1 -0
  180. package/dist/x402/client.d.ts +203 -0
  181. package/dist/x402/client.d.ts.map +1 -0
  182. package/dist/x402/client.js +337 -0
  183. package/dist/x402/client.js.map +1 -0
  184. package/dist/x402/hub.d.ts +79 -0
  185. package/dist/x402/hub.d.ts.map +1 -0
  186. package/dist/x402/hub.js +164 -0
  187. package/dist/x402/hub.js.map +1 -0
  188. package/dist/x402/index.d.ts +27 -0
  189. package/dist/x402/index.d.ts.map +1 -0
  190. package/dist/x402/index.js +27 -0
  191. package/dist/x402/index.js.map +1 -0
  192. package/dist/x402/proxy.d.ts +162 -0
  193. package/dist/x402/proxy.d.ts.map +1 -0
  194. package/dist/x402/proxy.js +198 -0
  195. package/dist/x402/proxy.js.map +1 -0
  196. package/dist/x402/server.d.ts +162 -0
  197. package/dist/x402/server.d.ts.map +1 -0
  198. package/dist/x402/server.js +306 -0
  199. package/dist/x402/server.js.map +1 -0
  200. package/dist/x402/wire.d.ts +104 -0
  201. package/dist/x402/wire.d.ts.map +1 -0
  202. package/dist/x402/wire.js +265 -0
  203. package/dist/x402/wire.js.map +1 -0
  204. package/package.json +61 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * `tab connect`: the one line of configuration.
3
+ *
4
+ * The whole adoption story is this command. It finds the MCP client's server
5
+ * list, reads the deployment keylessly so it can tell the caller what they are
6
+ * about to be connected to, merges one `mcpServers.tab` stanza, backs up what
7
+ * was there, and prints the diff.
8
+ *
9
+ * ## Read before write
10
+ *
11
+ * The keyless read happens first and its result is reported whether it succeeded
12
+ * or not. A `connect` that silently wrote a stanza pointing at a registry that
13
+ * does not answer would look like success and behave like a broken install, and
14
+ * the person running it would find out from a model instead of from the command.
15
+ * The read never blocks the write: a deployment that is temporarily down is not
16
+ * a reason to refuse to configure a client against it.
17
+ *
18
+ * ## Nothing here is a secret
19
+ *
20
+ * The stanza carries a command, its arguments, and at most two URLs. Every value
21
+ * in it is publishable. {@link assertNoSecret} runs over the merged document
22
+ * before anything is written, so the property is enforced rather than asserted
23
+ * in a comment.
24
+ *
25
+ * Requirements: 25.5, 25.6
26
+ */
27
+ import { ok } from "../_shared/index.js";
28
+ import { validationError } from "../errors.js";
29
+ import { silentLogger } from "../logger.js";
30
+ import { asArray, field } from "../mcp/json.js";
31
+ import { createRegistryReadClient } from "../mcp/registry-client.js";
32
+ import { buildTabStanza, chooseMcpClient, detectMcpClients, mergeTabStanza, readClientConfig, renderConfig, unifiedDiff, writeClientConfig, } from "./client-config.js";
33
+ /** The environment values that belong in a stanza. Publishable, all of them. */
34
+ function stanzaEnv(settings, env) {
35
+ const values = {};
36
+ const rpc = settings.rpcUrl ?? env["MONAD_RPC_URL"];
37
+ if (rpc !== undefined && rpc.trim() !== "")
38
+ values["MONAD_RPC_URL"] = rpc.trim();
39
+ values["MONAD_CHAIN_ID"] = String(settings.chainId);
40
+ const registry = settings.registryUrl ?? env["NEXT_PUBLIC_REGISTRY_API_URL"];
41
+ if (registry !== undefined && registry.trim() !== "")
42
+ values["NEXT_PUBLIC_REGISTRY_API_URL"] = registry.trim();
43
+ return values;
44
+ }
45
+ /** Reads the deployment, keylessly, so `connect` can say what it connected to. */
46
+ async function preflight(settings, fetchImpl, logger) {
47
+ if (settings.registryUrl === undefined) {
48
+ return {
49
+ registryUrl: undefined,
50
+ services: null,
51
+ note: "no registry read API is configured, so discovery and status will not work until one is. Calling and settling do not need it",
52
+ };
53
+ }
54
+ const registry = createRegistryReadClient({
55
+ baseUrl: settings.registryUrl,
56
+ logger,
57
+ ...(fetchImpl === undefined ? {} : { fetchImpl }),
58
+ });
59
+ const services = await registry.services(50);
60
+ if (!services.ok) {
61
+ return {
62
+ registryUrl: registry.baseUrl,
63
+ services: null,
64
+ note: `the read API did not answer: ${services.error.message}`,
65
+ };
66
+ }
67
+ const count = asArray(field(services.value, "services")).length;
68
+ const lastBlock = field(field(services.value, "index"), "lastBlock");
69
+ return {
70
+ registryUrl: registry.baseUrl,
71
+ services: count,
72
+ note: typeof lastBlock === "number"
73
+ ? `${count} Service(s) indexed at Monad block ${lastBlock}`
74
+ : `${count} Service(s) indexed`,
75
+ };
76
+ }
77
+ /** Resolves which file to write, from `--config`, then `--client`, then detection. */
78
+ function resolveTarget(options) {
79
+ if (options.configPath !== undefined) {
80
+ return ok({ id: options.client ?? "custom", configPath: options.configPath });
81
+ }
82
+ const detected = detectMcpClients({
83
+ ...(options.os === undefined ? {} : { os: options.os }),
84
+ ...(options.env === undefined ? {} : { env: options.env }),
85
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
86
+ });
87
+ if (options.client !== undefined) {
88
+ const named = detected.find((target) => target.id === options.client);
89
+ if (named === undefined || named.configPath === undefined) {
90
+ return validationError("CLIENT_UNKNOWN", `\`${options.client}\` is not an MCP client this command knows on this platform; the known ones are ${detected
91
+ .filter((target) => target.configPath !== undefined)
92
+ .map((target) => target.id)
93
+ .join(", ")}`, { details: { client: options.client } });
94
+ }
95
+ return ok({ id: named.id, configPath: named.configPath });
96
+ }
97
+ const chosen = chooseMcpClient(detected);
98
+ if (!chosen.ok)
99
+ return chosen;
100
+ if (chosen.value.configPath === undefined) {
101
+ return validationError("CLIENT_UNKNOWN", `\`${chosen.value.id}\` has no config path on this platform`);
102
+ }
103
+ return ok({ id: chosen.value.id, configPath: chosen.value.configPath });
104
+ }
105
+ /**
106
+ * Runs the command.
107
+ *
108
+ * Every failure is a `Result`, including a config file that will not parse. The
109
+ * one thing this never does is write over something it could not read.
110
+ */
111
+ export async function runConnect(options) {
112
+ const env = options.env ?? process.env;
113
+ const logger = options.logger ?? silentLogger;
114
+ const at = (options.now ?? (() => new Date()))();
115
+ const dryRun = options.dryRun ?? false;
116
+ const target = resolveTarget(options);
117
+ if (!target.ok)
118
+ return target;
119
+ const read = readClientConfig(target.value.configPath);
120
+ if (!read.ok)
121
+ return read;
122
+ const stanza = buildTabStanza(stanzaEnv(options.settings, env));
123
+ const merged = mergeTabStanza(read.value.document, stanza);
124
+ if (!merged.ok)
125
+ return merged;
126
+ const checked = await preflight(options.settings, options.fetchImpl, logger);
127
+ const before = read.value.existed ? renderConfig(read.value.document) : "";
128
+ const after = renderConfig(merged.value.merged);
129
+ const diff = merged.value.changed ? unifiedDiff(before, after) : "";
130
+ if (dryRun) {
131
+ return ok({
132
+ client: target.value.id,
133
+ configPath: target.value.configPath,
134
+ changed: merged.value.changed,
135
+ hadStanza: merged.value.hadStanza,
136
+ backupPath: undefined,
137
+ stanza,
138
+ diff,
139
+ preflight: checked,
140
+ dryRun: true,
141
+ });
142
+ }
143
+ const written = writeClientConfig(target.value.configPath, merged.value, read.value.existed, at);
144
+ if (!written.ok)
145
+ return written;
146
+ return ok({
147
+ client: target.value.id,
148
+ configPath: target.value.configPath,
149
+ changed: written.value.wrote,
150
+ hadStanza: merged.value.hadStanza,
151
+ backupPath: written.value.backupPath,
152
+ stanza,
153
+ diff,
154
+ preflight: checked,
155
+ dryRun: false,
156
+ });
157
+ }
158
+ //# sourceMappingURL=connect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../../src/cli/connect.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `tab connect`: the one line of configuration.\n *\n * The whole adoption story is this command. It finds the MCP client's server\n * list, reads the deployment keylessly so it can tell the caller what they are\n * about to be connected to, merges one `mcpServers.tab` stanza, backs up what\n * was there, and prints the diff.\n *\n * ## Read before write\n *\n * The keyless read happens first and its result is reported whether it succeeded\n * or not. A `connect` that silently wrote a stanza pointing at a registry that\n * does not answer would look like success and behave like a broken install, and\n * the person running it would find out from a model instead of from the command.\n * The read never blocks the write: a deployment that is temporarily down is not\n * a reason to refuse to configure a client against it.\n *\n * ## Nothing here is a secret\n *\n * The stanza carries a command, its arguments, and at most two URLs. Every value\n * in it is publishable. {@link assertNoSecret} runs over the merged document\n * before anything is written, so the property is enforced rather than asserted\n * in a comment.\n *\n * Requirements: 25.5, 25.6\n */\n\nimport type { Result } from \"../_shared/index.js\";\nimport { ok } from \"../_shared/index.js\";\n\nimport { validationError } from \"../errors.js\";\nimport type { Logger } from \"../logger.js\";\nimport { silentLogger } from \"../logger.js\";\nimport { asArray, field } from \"../mcp/json.js\";\nimport { createRegistryReadClient, type RegistryFetch } from \"../mcp/registry-client.js\";\nimport type { TabMcpSettings } from \"../mcp/settings.js\";\nimport {\n buildTabStanza,\n chooseMcpClient,\n detectMcpClients,\n mergeTabStanza,\n readClientConfig,\n renderConfig,\n unifiedDiff,\n writeClientConfig,\n type McpClientTarget,\n type StanzaEnv,\n type TabStanza,\n} from \"./client-config.js\";\n\nexport interface ConnectOptions {\n readonly settings: TabMcpSettings;\n /** `claude-code`, `claude-desktop`, `cursor`, `windsurf` or `vscode`. */\n readonly client?: string;\n /** An explicit config file, which beats `client` and skips detection. */\n readonly configPath?: string;\n readonly env?: NodeJS.ProcessEnv;\n readonly os?: string;\n readonly cwd?: string;\n /** Injectable clock, so a backup name is testable. */\n readonly now?: () => Date;\n /** Work out and print everything, and write nothing. */\n readonly dryRun?: boolean;\n readonly fetchImpl?: RegistryFetch;\n readonly logger?: Logger;\n}\n\n/** What the deployment said when it was asked, before anything was written. */\nexport interface ConnectPreflight {\n readonly registryUrl: string | undefined;\n /** Services indexed, or null when the read API did not answer. */\n readonly services: number | null;\n readonly note: string;\n}\n\nexport interface ConnectReport {\n readonly client: string;\n readonly configPath: string;\n /** False when the file already said exactly this. */\n readonly changed: boolean;\n /** True when a `tab` stanza was already there, so a message can say updated. */\n readonly hadStanza: boolean;\n readonly backupPath: string | undefined;\n readonly stanza: TabStanza;\n /** Empty when nothing changed. */\n readonly diff: string;\n readonly preflight: ConnectPreflight;\n readonly dryRun: boolean;\n}\n\n/** The environment values that belong in a stanza. Publishable, all of them. */\nfunction stanzaEnv(settings: TabMcpSettings, env: NodeJS.ProcessEnv): StanzaEnv {\n const values: Record<string, string> = {};\n const rpc = settings.rpcUrl ?? env[\"MONAD_RPC_URL\"];\n if (rpc !== undefined && rpc.trim() !== \"\") values[\"MONAD_RPC_URL\"] = rpc.trim();\n values[\"MONAD_CHAIN_ID\"] = String(settings.chainId);\n const registry = settings.registryUrl ?? env[\"NEXT_PUBLIC_REGISTRY_API_URL\"];\n if (registry !== undefined && registry.trim() !== \"\") values[\"NEXT_PUBLIC_REGISTRY_API_URL\"] = registry.trim();\n return values;\n}\n\n/** Reads the deployment, keylessly, so `connect` can say what it connected to. */\nasync function preflight(\n settings: TabMcpSettings,\n fetchImpl: RegistryFetch | undefined,\n logger: Logger,\n): Promise<ConnectPreflight> {\n if (settings.registryUrl === undefined) {\n return {\n registryUrl: undefined,\n services: null,\n note: \"no registry read API is configured, so discovery and status will not work until one is. Calling and settling do not need it\",\n };\n }\n const registry = createRegistryReadClient({\n baseUrl: settings.registryUrl,\n logger,\n ...(fetchImpl === undefined ? {} : { fetchImpl }),\n });\n const services = await registry.services(50);\n if (!services.ok) {\n return {\n registryUrl: registry.baseUrl,\n services: null,\n note: `the read API did not answer: ${services.error.message}`,\n };\n }\n const count = asArray(field(services.value, \"services\")).length;\n const lastBlock = field(field(services.value, \"index\"), \"lastBlock\");\n return {\n registryUrl: registry.baseUrl,\n services: count,\n note:\n typeof lastBlock === \"number\"\n ? `${count} Service(s) indexed at Monad block ${lastBlock}`\n : `${count} Service(s) indexed`,\n };\n}\n\n/** Resolves which file to write, from `--config`, then `--client`, then detection. */\nfunction resolveTarget(options: ConnectOptions): Result<{ id: string; configPath: string }> {\n if (options.configPath !== undefined) {\n return ok({ id: options.client ?? \"custom\", configPath: options.configPath });\n }\n const detected = detectMcpClients({\n ...(options.os === undefined ? {} : { os: options.os }),\n ...(options.env === undefined ? {} : { env: options.env }),\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n });\n if (options.client !== undefined) {\n const named: McpClientTarget | undefined = detected.find((target) => target.id === options.client);\n if (named === undefined || named.configPath === undefined) {\n return validationError(\n \"CLIENT_UNKNOWN\",\n `\\`${options.client}\\` is not an MCP client this command knows on this platform; the known ones are ${detected\n .filter((target) => target.configPath !== undefined)\n .map((target) => target.id)\n .join(\", \")}`,\n { details: { client: options.client } },\n );\n }\n return ok({ id: named.id, configPath: named.configPath });\n }\n const chosen = chooseMcpClient(detected);\n if (!chosen.ok) return chosen;\n if (chosen.value.configPath === undefined) {\n return validationError(\"CLIENT_UNKNOWN\", `\\`${chosen.value.id}\\` has no config path on this platform`);\n }\n return ok({ id: chosen.value.id, configPath: chosen.value.configPath });\n}\n\n/**\n * Runs the command.\n *\n * Every failure is a `Result`, including a config file that will not parse. The\n * one thing this never does is write over something it could not read.\n */\nexport async function runConnect(options: ConnectOptions): Promise<Result<ConnectReport>> {\n const env = options.env ?? process.env;\n const logger = options.logger ?? silentLogger;\n const at = (options.now ?? (() => new Date()))();\n const dryRun = options.dryRun ?? false;\n\n const target = resolveTarget(options);\n if (!target.ok) return target;\n\n const read = readClientConfig(target.value.configPath);\n if (!read.ok) return read;\n\n const stanza = buildTabStanza(stanzaEnv(options.settings, env));\n const merged = mergeTabStanza(read.value.document, stanza);\n if (!merged.ok) return merged;\n\n const checked = await preflight(options.settings, options.fetchImpl, logger);\n\n const before = read.value.existed ? renderConfig(read.value.document) : \"\";\n const after = renderConfig(merged.value.merged);\n const diff = merged.value.changed ? unifiedDiff(before, after) : \"\";\n\n if (dryRun) {\n return ok({\n client: target.value.id,\n configPath: target.value.configPath,\n changed: merged.value.changed,\n hadStanza: merged.value.hadStanza,\n backupPath: undefined,\n stanza,\n diff,\n preflight: checked,\n dryRun: true,\n });\n }\n\n const written = writeClientConfig(target.value.configPath, merged.value, read.value.existed, at);\n if (!written.ok) return written;\n\n return ok({\n client: target.value.id,\n configPath: target.value.configPath,\n changed: written.value.wrote,\n hadStanza: merged.value.hadStanza,\n backupPath: written.value.backupPath,\n stanza,\n diff,\n preflight: checked,\n dryRun: false,\n });\n}\n"]}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `tab doctor`: every check that can be made without a key.
3
+ *
4
+ * The point of the command is that a person who has just run `tab connect` can
5
+ * find out whether it worked before they ask a model to spend anything. So every
6
+ * check here is a read -- an `eth_chainId`, an `eth_getCode`, a `GET /healthz`.
7
+ * Nothing here signs, nothing here writes, and nothing here loads a private key.
8
+ * The one thing it says about a signing key is whether the environment carries
9
+ * one at all, reported as present or absent and never read, because "you have no
10
+ * key configured" is the answer to why `tab_settle` will refuse, and printing the
11
+ * key would be the worst possible way to say it.
12
+ *
13
+ * ## Three outcomes, not two
14
+ *
15
+ * A check answers `pass`, `warn`, `fail` or `skip`, and the distinction carries
16
+ * weight. A missing registry read API is a `warn`: discovery and status will not
17
+ * work, but calling and settling will, so it is not a broken installation. An
18
+ * RPC endpoint that answers the wrong chain id is a `fail`: every address in the
19
+ * configuration names a contract on a different chain, and anything built on top
20
+ * of it would be wrong rather than absent. `skip` is for a check whose input was
21
+ * not configured, which is a statement about the configuration and not about the
22
+ * deployment.
23
+ *
24
+ * Requirements: 25.6, 28.6
25
+ */
26
+ import type { Logger } from "../logger.js";
27
+ import { type RegistryFetch } from "../mcp/registry-client.js";
28
+ import type { TabMcpSettings } from "../mcp/settings.js";
29
+ export type CheckStatus = "pass" | "warn" | "fail" | "skip";
30
+ export interface DoctorCheck {
31
+ /** Short and stable, so a script can grep for one. */
32
+ readonly name: string;
33
+ readonly status: CheckStatus;
34
+ /** What was found, in one sentence. */
35
+ readonly detail: string;
36
+ /** What to do about it, when there is something to do. */
37
+ readonly hint?: string;
38
+ }
39
+ export interface DoctorReport {
40
+ readonly checks: readonly DoctorCheck[];
41
+ /** True when nothing failed. Warnings do not make a report unhealthy. */
42
+ readonly ok: boolean;
43
+ readonly counts: Readonly<Record<CheckStatus, number>>;
44
+ }
45
+ export interface DoctorOptions {
46
+ readonly settings: TabMcpSettings;
47
+ readonly env?: NodeJS.ProcessEnv;
48
+ /** The MCP client config to inspect, when the caller resolved one. */
49
+ readonly clientConfigPath?: string;
50
+ readonly fetchImpl?: RegistryFetch;
51
+ readonly logger?: Logger;
52
+ /** How long any one network read may take. Defaults to 10 seconds. */
53
+ readonly timeoutMs?: number;
54
+ }
55
+ /** Runs every check and answers the whole report. Never throws, never signs. */
56
+ export declare function runDoctor(options: DoctorOptions): Promise<DoctorReport>;
57
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAG3C,OAAO,EAA4B,KAAK,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACzF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,uCAAuC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;IACxC,yEAAyE;IACzE,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,sEAAsE;IACtE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAkCD,gFAAgF;AAChF,wBAAsB,SAAS,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC,CA4I7E"}
@@ -0,0 +1,253 @@
1
+ /**
2
+ * `tab doctor`: every check that can be made without a key.
3
+ *
4
+ * The point of the command is that a person who has just run `tab connect` can
5
+ * find out whether it worked before they ask a model to spend anything. So every
6
+ * check here is a read -- an `eth_chainId`, an `eth_getCode`, a `GET /healthz`.
7
+ * Nothing here signs, nothing here writes, and nothing here loads a private key.
8
+ * The one thing it says about a signing key is whether the environment carries
9
+ * one at all, reported as present or absent and never read, because "you have no
10
+ * key configured" is the answer to why `tab_settle` will refuse, and printing the
11
+ * key would be the worst possible way to say it.
12
+ *
13
+ * ## Three outcomes, not two
14
+ *
15
+ * A check answers `pass`, `warn`, `fail` or `skip`, and the distinction carries
16
+ * weight. A missing registry read API is a `warn`: discovery and status will not
17
+ * work, but calling and settling will, so it is not a broken installation. An
18
+ * RPC endpoint that answers the wrong chain id is a `fail`: every address in the
19
+ * configuration names a contract on a different chain, and anything built on top
20
+ * of it would be wrong rather than absent. `skip` is for a check whose input was
21
+ * not configured, which is a statement about the configuration and not about the
22
+ * deployment.
23
+ *
24
+ * Requirements: 25.6, 28.6
25
+ */
26
+ import { JsonRpcProvider, Network } from "ethers";
27
+ import { silentLogger } from "../logger.js";
28
+ import { asArray, field, isRecord } from "../mcp/json.js";
29
+ import { createRegistryReadClient } from "../mcp/registry-client.js";
30
+ import { readClientConfig, TAB_SERVER_KEY, assertNoSecret } from "./client-config.js";
31
+ /** The contracts a working deployment has code at, under their environment names. */
32
+ const CONTRACTS = [
33
+ ["SERVICE_REGISTRY_ADDRESS", "ServiceRegistry"],
34
+ ["BOND_ADDRESS", "Bond"],
35
+ ["TAB_BOOK_ADDRESS", "TabBook"],
36
+ ["TAB_SETTLEMENT_ADDRESS", "TabSettlement"],
37
+ ];
38
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
39
+ const pass = (name, detail) => ({ name, status: "pass", detail });
40
+ const warn = (name, detail, hint) => ({
41
+ name,
42
+ status: "warn",
43
+ detail,
44
+ ...(hint === undefined ? {} : { hint }),
45
+ });
46
+ const bad = (name, detail, hint) => ({
47
+ name,
48
+ status: "fail",
49
+ detail,
50
+ ...(hint === undefined ? {} : { hint }),
51
+ });
52
+ const skip = (name, detail, hint) => ({
53
+ name,
54
+ status: "skip",
55
+ detail,
56
+ ...(hint === undefined ? {} : { hint }),
57
+ });
58
+ const reason = (error) => (error instanceof Error ? error.message : String(error));
59
+ /** Runs every check and answers the whole report. Never throws, never signs. */
60
+ export async function runDoctor(options) {
61
+ const env = options.env ?? process.env;
62
+ const settings = options.settings;
63
+ const logger = options.logger ?? silentLogger;
64
+ const timeoutMs = options.timeoutMs ?? 10_000;
65
+ const checks = [];
66
+ // ---- the runtime
67
+ const major = Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10);
68
+ checks.push(major >= 20
69
+ ? pass("node", `Node ${process.versions.node}`)
70
+ : bad("node", `Node ${process.versions.node}`, "the MCP server needs Node 20.10 or later for its global fetch"));
71
+ // ---- what is configured
72
+ checks.push(settings.agent === undefined
73
+ ? warn("agent", "no Agent address is configured", "set `agent` in tab.config; tab_call and tab_settle need to know whose Open Tab to meter")
74
+ : pass("agent", `${settings.agent} (${settings.sources["agent"] ?? "unknown source"})`));
75
+ checks.push(settings.services.length === 0
76
+ ? warn("service-directory", "no Service endpoints are configured", "the chain records no URL for a Service, so add each one to the `services` list in tab.config or tab_call has nowhere to send a request")
77
+ : pass("service-directory", `${settings.services.length} Service endpoint(s) configured`));
78
+ // ---- Monad, keyless
79
+ if (settings.rpcUrl === undefined) {
80
+ checks.push(skip("monad-rpc", "no Monad RPC endpoint is configured", "set MONAD_RPC_URL"), skip("monad-contracts", "skipped: there is no endpoint to read from"));
81
+ }
82
+ else {
83
+ const expected = settings.chainId;
84
+ // Pinned to the configured chain so ethers never probes the network on its
85
+ // own, and `eth_chainId` is then asked directly: a static network would
86
+ // otherwise answer the configured id without the endpoint being consulted,
87
+ // which is the one check this exists to make.
88
+ const provider = new JsonRpcProvider(settings.rpcUrl, Network.from(expected), { staticNetwork: true });
89
+ try {
90
+ const [answered, head] = await Promise.all([
91
+ withTimeout(provider.send("eth_chainId", []), timeoutMs, "eth_chainId"),
92
+ withTimeout(provider.getBlockNumber(), timeoutMs, "eth_blockNumber"),
93
+ ]);
94
+ const chainId = typeof answered === "string" ? Number.parseInt(answered, 16) : Number.NaN;
95
+ checks.push(chainId === expected
96
+ ? pass("monad-rpc", `chain id ${chainId} at block ${head}`)
97
+ : bad("monad-rpc", `the endpoint answers chain id ${chainId} and the configuration expects ${expected}`, "every contract address in the configuration names a contract on one chain; reading a different one would produce confident wrong answers"));
98
+ checks.push(...(await contractChecks(provider, env, timeoutMs)));
99
+ }
100
+ catch (error) {
101
+ checks.push(bad("monad-rpc", `${settings.rpcUrl} could not be read: ${reason(error)}`, "check MONAD_RPC_URL"), skip("monad-contracts", "skipped: the endpoint did not answer"));
102
+ }
103
+ finally {
104
+ provider.destroy();
105
+ }
106
+ }
107
+ // ---- the registry read API, keyless
108
+ if (settings.registryUrl === undefined) {
109
+ checks.push(warn("registry-api", "no registry read API is configured", "tab_discover and tab_status read from it; set NEXT_PUBLIC_REGISTRY_API_URL or `registryUrl` in tab.config. tab_call and tab_settle work without it"));
110
+ }
111
+ else {
112
+ const registry = createRegistryReadClient({
113
+ baseUrl: settings.registryUrl,
114
+ timeoutMs,
115
+ logger,
116
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
117
+ });
118
+ const health = await registry.health();
119
+ if (!health.ok) {
120
+ checks.push(warn("registry-api", `${registry.baseUrl} did not answer: ${health.error.message}`, "start the registry read API, or point at a deployed one"));
121
+ }
122
+ else {
123
+ checks.push(pass("registry-api", `${registry.baseUrl} is healthy`));
124
+ const services = await registry.services(5);
125
+ if (!services.ok) {
126
+ checks.push(warn("registry-services", `the Service directory could not be read: ${services.error.message}`));
127
+ }
128
+ else {
129
+ const count = asArray(field(services.value, "services")).length;
130
+ const lastBlock = field(field(services.value, "index"), "lastBlock");
131
+ checks.push(count === 0
132
+ ? warn("registry-services", "the read API is healthy and no Service is registered yet", "register a Service, or wait for the indexer to catch up")
133
+ : pass("registry-services", `${count} Service(s) indexed${typeof lastBlock === "number" ? ` at Monad block ${lastBlock}` : ""}`));
134
+ }
135
+ }
136
+ }
137
+ // ---- each configured Service endpoint
138
+ checks.push(...(await endpointChecks(settings, options.fetchImpl, timeoutMs)));
139
+ // ---- the MCP client config
140
+ checks.push(clientConfigCheck(options.clientConfigPath));
141
+ // ---- the signing key, named and never read
142
+ const keyPresent = env["AGENT_PRIVATE_KEY"] !== undefined;
143
+ checks.push(keyPresent
144
+ ? pass("settlement-key", "AGENT_PRIVATE_KEY is set in this environment; its value is never read here or written anywhere")
145
+ : warn("settlement-key", "AGENT_PRIVATE_KEY is not set", "tab_settle signs the Settlement with the Agent's own key, read from the environment at signing time. Discovery, calling and status need no key"));
146
+ const counts = { pass: 0, warn: 0, fail: 0, skip: 0 };
147
+ for (const check of checks)
148
+ counts[check.status] += 1;
149
+ return { checks, ok: counts.fail === 0, counts };
150
+ }
151
+ /** `eth_getCode` at each configured contract address. */
152
+ async function contractChecks(provider, env, timeoutMs) {
153
+ const configured = [];
154
+ for (const [variable, label] of CONTRACTS) {
155
+ const address = env[variable];
156
+ // The zero address is the placeholder `.env.example` ships before a
157
+ // deployment fills it in, so it means "not configured" and not "no code".
158
+ if (address === undefined)
159
+ continue;
160
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address) || address.toLowerCase() === ZERO_ADDRESS)
161
+ continue;
162
+ configured.push({ variable, label, address });
163
+ }
164
+ if (configured.length === 0) {
165
+ return [
166
+ skip("monad-contracts", "no contract addresses are configured", "set SERVICE_REGISTRY_ADDRESS, BOND_ADDRESS, TAB_BOOK_ADDRESS and TAB_SETTLEMENT_ADDRESS"),
167
+ ];
168
+ }
169
+ const results = await Promise.all(configured.map(async (entry) => {
170
+ try {
171
+ // Pinned to `latest`, deliberately. Monad finalises within seconds, but a
172
+ // `finalized` read still lags the head, and a contract deployed within
173
+ // the lag would read as absent: a false failure at exactly the moment
174
+ // somebody is checking a fresh deployment.
175
+ const code = await withTimeout(provider.getCode(entry.address, "latest"), timeoutMs, "eth_getCode");
176
+ return { entry, hasCode: code !== undefined && code !== "0x" && code.length > 2, error: undefined };
177
+ }
178
+ catch (error) {
179
+ return { entry, hasCode: false, error: reason(error) };
180
+ }
181
+ }));
182
+ const missing = results.filter((result) => !result.hasCode);
183
+ if (missing.length === 0) {
184
+ return [pass("monad-contracts", `${results.length} contract(s) hold code: ${results.map((r) => r.entry.label).join(", ")}`)];
185
+ }
186
+ return [
187
+ bad("monad-contracts", `no code at ${missing.map((result) => `${result.entry.label} (${result.entry.address})`).join(", ")}`, "the addresses name nothing on this chain: check the environment against the deployment, and that the RPC endpoint is the right network"),
188
+ ];
189
+ }
190
+ /** A `GET /healthz` against each configured Service endpoint. */
191
+ async function endpointChecks(settings, fetchImpl, timeoutMs) {
192
+ if (settings.services.length === 0)
193
+ return [];
194
+ const send = fetchImpl ?? globalThis.fetch;
195
+ if (send === undefined)
196
+ return [skip("service-endpoints", "this host has no global fetch")];
197
+ const signal = globalThis.AbortSignal?.timeout?.(timeoutMs);
198
+ const results = await Promise.all(settings.services.map(async (entry) => {
199
+ const url = `${entry.endpoint.replace(/\/+$/, "")}/healthz`;
200
+ try {
201
+ const response = await send(url, { method: "GET", ...(signal === undefined ? {} : { signal }) });
202
+ return { entry, reachable: response.status >= 200 && response.status < 500, status: response.status };
203
+ }
204
+ catch (error) {
205
+ return { entry, reachable: false, status: reason(error) };
206
+ }
207
+ }));
208
+ const unreachable = results.filter((result) => !result.reachable);
209
+ if (unreachable.length === 0) {
210
+ return [pass("service-endpoints", `${results.length} Service endpoint(s) answered`)];
211
+ }
212
+ return [
213
+ warn("service-endpoints", `${unreachable.length} of ${results.length} Service endpoint(s) did not answer: ${unreachable
214
+ .map((result) => `${result.entry.serviceId} at ${result.entry.endpoint} (${String(result.status)})`)
215
+ .join(", ")}`, "a Service that is down is not a broken installation, but tab_call against it will fail"),
216
+ ];
217
+ }
218
+ /** Is the `tab` stanza in the client config, and is it free of credentials? */
219
+ function clientConfigCheck(path) {
220
+ if (path === undefined) {
221
+ return skip("mcp-client-config", "no MCP client config was named", "run `tab connect`, or pass --config");
222
+ }
223
+ const read = readClientConfig(path);
224
+ if (!read.ok)
225
+ return bad("mcp-client-config", read.error.message);
226
+ if (!read.value.existed) {
227
+ return warn("mcp-client-config", `\`${path}\` does not exist yet`, "run `tab connect` to write it");
228
+ }
229
+ const servers = field(read.value.document, "mcpServers");
230
+ const stanza = field(servers, TAB_SERVER_KEY);
231
+ if (stanza === undefined) {
232
+ return warn("mcp-client-config", `\`${path}\` has no \`${TAB_SERVER_KEY}\` server`, "run `tab connect`");
233
+ }
234
+ const secret = assertNoSecret(stanza, `the \`${TAB_SERVER_KEY}\` stanza in ${path}`);
235
+ if (!secret.ok)
236
+ return bad("mcp-client-config", secret.error.message);
237
+ const command = isRecord(stanza) ? stanza["command"] : undefined;
238
+ return pass("mcp-client-config", `\`${path}\` runs the tab server with \`${String(command)}\` and carries no credential`);
239
+ }
240
+ /** A promise with a deadline, so one slow endpoint does not hold the whole report. */
241
+ function withTimeout(promise, ms, label) {
242
+ return new Promise((resolve, reject) => {
243
+ const timer = setTimeout(() => reject(new Error(`${label} did not answer within ${ms}ms`)), ms);
244
+ promise.then((value) => {
245
+ clearTimeout(timer);
246
+ resolve(value);
247
+ }, (error) => {
248
+ clearTimeout(timer);
249
+ reject(error instanceof Error ? error : new Error(String(error)));
250
+ });
251
+ });
252
+ }
253
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `tab doctor`: every check that can be made without a key.\n *\n * The point of the command is that a person who has just run `tab connect` can\n * find out whether it worked before they ask a model to spend anything. So every\n * check here is a read -- an `eth_chainId`, an `eth_getCode`, a `GET /healthz`.\n * Nothing here signs, nothing here writes, and nothing here loads a private key.\n * The one thing it says about a signing key is whether the environment carries\n * one at all, reported as present or absent and never read, because \"you have no\n * key configured\" is the answer to why `tab_settle` will refuse, and printing the\n * key would be the worst possible way to say it.\n *\n * ## Three outcomes, not two\n *\n * A check answers `pass`, `warn`, `fail` or `skip`, and the distinction carries\n * weight. A missing registry read API is a `warn`: discovery and status will not\n * work, but calling and settling will, so it is not a broken installation. An\n * RPC endpoint that answers the wrong chain id is a `fail`: every address in the\n * configuration names a contract on a different chain, and anything built on top\n * of it would be wrong rather than absent. `skip` is for a check whose input was\n * not configured, which is a statement about the configuration and not about the\n * deployment.\n *\n * Requirements: 25.6, 28.6\n */\n\nimport { JsonRpcProvider, Network } from \"ethers\";\n\nimport type { Logger } from \"../logger.js\";\nimport { silentLogger } from \"../logger.js\";\nimport { asArray, field, isRecord } from \"../mcp/json.js\";\nimport { createRegistryReadClient, type RegistryFetch } from \"../mcp/registry-client.js\";\nimport type { TabMcpSettings } from \"../mcp/settings.js\";\nimport { readClientConfig, TAB_SERVER_KEY, assertNoSecret } from \"./client-config.js\";\n\nexport type CheckStatus = \"pass\" | \"warn\" | \"fail\" | \"skip\";\n\nexport interface DoctorCheck {\n /** Short and stable, so a script can grep for one. */\n readonly name: string;\n readonly status: CheckStatus;\n /** What was found, in one sentence. */\n readonly detail: string;\n /** What to do about it, when there is something to do. */\n readonly hint?: string;\n}\n\nexport interface DoctorReport {\n readonly checks: readonly DoctorCheck[];\n /** True when nothing failed. Warnings do not make a report unhealthy. */\n readonly ok: boolean;\n readonly counts: Readonly<Record<CheckStatus, number>>;\n}\n\nexport interface DoctorOptions {\n readonly settings: TabMcpSettings;\n readonly env?: NodeJS.ProcessEnv;\n /** The MCP client config to inspect, when the caller resolved one. */\n readonly clientConfigPath?: string;\n readonly fetchImpl?: RegistryFetch;\n readonly logger?: Logger;\n /** How long any one network read may take. Defaults to 10 seconds. */\n readonly timeoutMs?: number;\n}\n\n/** The contracts a working deployment has code at, under their environment names. */\nconst CONTRACTS = [\n [\"SERVICE_REGISTRY_ADDRESS\", \"ServiceRegistry\"],\n [\"BOND_ADDRESS\", \"Bond\"],\n [\"TAB_BOOK_ADDRESS\", \"TabBook\"],\n [\"TAB_SETTLEMENT_ADDRESS\", \"TabSettlement\"],\n] as const;\n\nconst ZERO_ADDRESS = \"0x0000000000000000000000000000000000000000\";\n\nconst pass = (name: string, detail: string): DoctorCheck => ({ name, status: \"pass\", detail });\nconst warn = (name: string, detail: string, hint?: string): DoctorCheck => ({\n name,\n status: \"warn\",\n detail,\n ...(hint === undefined ? {} : { hint }),\n});\nconst bad = (name: string, detail: string, hint?: string): DoctorCheck => ({\n name,\n status: \"fail\",\n detail,\n ...(hint === undefined ? {} : { hint }),\n});\nconst skip = (name: string, detail: string, hint?: string): DoctorCheck => ({\n name,\n status: \"skip\",\n detail,\n ...(hint === undefined ? {} : { hint }),\n});\n\nconst reason = (error: unknown): string => (error instanceof Error ? error.message : String(error));\n\n/** Runs every check and answers the whole report. Never throws, never signs. */\nexport async function runDoctor(options: DoctorOptions): Promise<DoctorReport> {\n const env = options.env ?? process.env;\n const settings = options.settings;\n const logger = options.logger ?? silentLogger;\n const timeoutMs = options.timeoutMs ?? 10_000;\n const checks: DoctorCheck[] = [];\n\n // ---- the runtime\n const major = Number.parseInt(process.versions.node.split(\".\")[0] ?? \"0\", 10);\n checks.push(\n major >= 20\n ? pass(\"node\", `Node ${process.versions.node}`)\n : bad(\"node\", `Node ${process.versions.node}`, \"the MCP server needs Node 20.10 or later for its global fetch\"),\n );\n\n // ---- what is configured\n checks.push(\n settings.agent === undefined\n ? warn(\n \"agent\",\n \"no Agent address is configured\",\n \"set `agent` in tab.config; tab_call and tab_settle need to know whose Open Tab to meter\",\n )\n : pass(\"agent\", `${settings.agent} (${settings.sources[\"agent\"] ?? \"unknown source\"})`),\n );\n checks.push(\n settings.services.length === 0\n ? warn(\n \"service-directory\",\n \"no Service endpoints are configured\",\n \"the chain records no URL for a Service, so add each one to the `services` list in tab.config or tab_call has nowhere to send a request\",\n )\n : pass(\"service-directory\", `${settings.services.length} Service endpoint(s) configured`),\n );\n\n // ---- Monad, keyless\n if (settings.rpcUrl === undefined) {\n checks.push(\n skip(\"monad-rpc\", \"no Monad RPC endpoint is configured\", \"set MONAD_RPC_URL\"),\n skip(\"monad-contracts\", \"skipped: there is no endpoint to read from\"),\n );\n } else {\n const expected = settings.chainId;\n // Pinned to the configured chain so ethers never probes the network on its\n // own, and `eth_chainId` is then asked directly: a static network would\n // otherwise answer the configured id without the endpoint being consulted,\n // which is the one check this exists to make.\n const provider = new JsonRpcProvider(settings.rpcUrl, Network.from(expected), { staticNetwork: true });\n try {\n const [answered, head] = await Promise.all([\n withTimeout(provider.send(\"eth_chainId\", []) as Promise<unknown>, timeoutMs, \"eth_chainId\"),\n withTimeout(provider.getBlockNumber(), timeoutMs, \"eth_blockNumber\"),\n ]);\n const chainId = typeof answered === \"string\" ? Number.parseInt(answered, 16) : Number.NaN;\n checks.push(\n chainId === expected\n ? pass(\"monad-rpc\", `chain id ${chainId} at block ${head}`)\n : bad(\n \"monad-rpc\",\n `the endpoint answers chain id ${chainId} and the configuration expects ${expected}`,\n \"every contract address in the configuration names a contract on one chain; reading a different one would produce confident wrong answers\",\n ),\n );\n checks.push(...(await contractChecks(provider, env, timeoutMs)));\n } catch (error) {\n checks.push(\n bad(\"monad-rpc\", `${settings.rpcUrl} could not be read: ${reason(error)}`, \"check MONAD_RPC_URL\"),\n skip(\"monad-contracts\", \"skipped: the endpoint did not answer\"),\n );\n } finally {\n provider.destroy();\n }\n }\n\n // ---- the registry read API, keyless\n if (settings.registryUrl === undefined) {\n checks.push(\n warn(\n \"registry-api\",\n \"no registry read API is configured\",\n \"tab_discover and tab_status read from it; set NEXT_PUBLIC_REGISTRY_API_URL or `registryUrl` in tab.config. tab_call and tab_settle work without it\",\n ),\n );\n } else {\n const registry = createRegistryReadClient({\n baseUrl: settings.registryUrl,\n timeoutMs,\n logger,\n ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),\n });\n const health = await registry.health();\n if (!health.ok) {\n checks.push(\n warn(\"registry-api\", `${registry.baseUrl} did not answer: ${health.error.message}`, \"start the registry read API, or point at a deployed one\"),\n );\n } else {\n checks.push(pass(\"registry-api\", `${registry.baseUrl} is healthy`));\n const services = await registry.services(5);\n if (!services.ok) {\n checks.push(warn(\"registry-services\", `the Service directory could not be read: ${services.error.message}`));\n } else {\n const count = asArray(field(services.value, \"services\")).length;\n const lastBlock = field(field(services.value, \"index\"), \"lastBlock\");\n checks.push(\n count === 0\n ? warn(\n \"registry-services\",\n \"the read API is healthy and no Service is registered yet\",\n \"register a Service, or wait for the indexer to catch up\",\n )\n : pass(\n \"registry-services\",\n `${count} Service(s) indexed${typeof lastBlock === \"number\" ? ` at Monad block ${lastBlock}` : \"\"}`,\n ),\n );\n }\n }\n }\n\n // ---- each configured Service endpoint\n checks.push(...(await endpointChecks(settings, options.fetchImpl, timeoutMs)));\n\n // ---- the MCP client config\n checks.push(clientConfigCheck(options.clientConfigPath));\n\n // ---- the signing key, named and never read\n const keyPresent = env[\"AGENT_PRIVATE_KEY\"] !== undefined;\n checks.push(\n keyPresent\n ? pass(\"settlement-key\", \"AGENT_PRIVATE_KEY is set in this environment; its value is never read here or written anywhere\")\n : warn(\n \"settlement-key\",\n \"AGENT_PRIVATE_KEY is not set\",\n \"tab_settle signs the Settlement with the Agent's own key, read from the environment at signing time. Discovery, calling and status need no key\",\n ),\n );\n\n const counts: Record<CheckStatus, number> = { pass: 0, warn: 0, fail: 0, skip: 0 };\n for (const check of checks) counts[check.status] += 1;\n return { checks, ok: counts.fail === 0, counts };\n}\n\n/** `eth_getCode` at each configured contract address. */\nasync function contractChecks(\n provider: JsonRpcProvider,\n env: NodeJS.ProcessEnv,\n timeoutMs: number,\n): Promise<readonly DoctorCheck[]> {\n const configured: { readonly variable: string; readonly label: string; readonly address: string }[] = [];\n for (const [variable, label] of CONTRACTS) {\n const address = env[variable];\n // The zero address is the placeholder `.env.example` ships before a\n // deployment fills it in, so it means \"not configured\" and not \"no code\".\n if (address === undefined) continue;\n if (!/^0x[a-fA-F0-9]{40}$/.test(address) || address.toLowerCase() === ZERO_ADDRESS) continue;\n configured.push({ variable, label, address });\n }\n\n if (configured.length === 0) {\n return [\n skip(\n \"monad-contracts\",\n \"no contract addresses are configured\",\n \"set SERVICE_REGISTRY_ADDRESS, BOND_ADDRESS, TAB_BOOK_ADDRESS and TAB_SETTLEMENT_ADDRESS\",\n ),\n ];\n }\n\n const results = await Promise.all(\n configured.map(async (entry) => {\n try {\n // Pinned to `latest`, deliberately. Monad finalises within seconds, but a\n // `finalized` read still lags the head, and a contract deployed within\n // the lag would read as absent: a false failure at exactly the moment\n // somebody is checking a fresh deployment.\n const code = await withTimeout(provider.getCode(entry.address, \"latest\"), timeoutMs, \"eth_getCode\");\n return { entry, hasCode: code !== undefined && code !== \"0x\" && code.length > 2, error: undefined };\n } catch (error) {\n return { entry, hasCode: false, error: reason(error) };\n }\n }),\n );\n\n const missing = results.filter((result) => !result.hasCode);\n if (missing.length === 0) {\n return [pass(\"monad-contracts\", `${results.length} contract(s) hold code: ${results.map((r) => r.entry.label).join(\", \")}`)];\n }\n return [\n bad(\n \"monad-contracts\",\n `no code at ${missing.map((result) => `${result.entry.label} (${result.entry.address})`).join(\", \")}`,\n \"the addresses name nothing on this chain: check the environment against the deployment, and that the RPC endpoint is the right network\",\n ),\n ];\n}\n\n/** A `GET /healthz` against each configured Service endpoint. */\nasync function endpointChecks(\n settings: TabMcpSettings,\n fetchImpl: RegistryFetch | undefined,\n timeoutMs: number,\n): Promise<readonly DoctorCheck[]> {\n if (settings.services.length === 0) return [];\n const send = fetchImpl ?? (globalThis as { fetch?: RegistryFetch }).fetch;\n if (send === undefined) return [skip(\"service-endpoints\", \"this host has no global fetch\")];\n\n const signal = (globalThis as { AbortSignal?: { timeout?: (ms: number) => unknown } }).AbortSignal?.timeout?.(timeoutMs);\n const results = await Promise.all(\n settings.services.map(async (entry) => {\n const url = `${entry.endpoint.replace(/\\/+$/, \"\")}/healthz`;\n try {\n const response = await send(url, { method: \"GET\", ...(signal === undefined ? {} : { signal }) });\n return { entry, reachable: response.status >= 200 && response.status < 500, status: response.status };\n } catch (error) {\n return { entry, reachable: false, status: reason(error) };\n }\n }),\n );\n\n const unreachable = results.filter((result) => !result.reachable);\n if (unreachable.length === 0) {\n return [pass(\"service-endpoints\", `${results.length} Service endpoint(s) answered`)];\n }\n return [\n warn(\n \"service-endpoints\",\n `${unreachable.length} of ${results.length} Service endpoint(s) did not answer: ${unreachable\n .map((result) => `${result.entry.serviceId} at ${result.entry.endpoint} (${String(result.status)})`)\n .join(\", \")}`,\n \"a Service that is down is not a broken installation, but tab_call against it will fail\",\n ),\n ];\n}\n\n/** Is the `tab` stanza in the client config, and is it free of credentials? */\nfunction clientConfigCheck(path: string | undefined): DoctorCheck {\n if (path === undefined) {\n return skip(\"mcp-client-config\", \"no MCP client config was named\", \"run `tab connect`, or pass --config\");\n }\n const read = readClientConfig(path);\n if (!read.ok) return bad(\"mcp-client-config\", read.error.message);\n if (!read.value.existed) {\n return warn(\"mcp-client-config\", `\\`${path}\\` does not exist yet`, \"run `tab connect` to write it\");\n }\n const servers = field(read.value.document, \"mcpServers\");\n const stanza = field(servers, TAB_SERVER_KEY);\n if (stanza === undefined) {\n return warn(\"mcp-client-config\", `\\`${path}\\` has no \\`${TAB_SERVER_KEY}\\` server`, \"run `tab connect`\");\n }\n const secret = assertNoSecret(stanza, `the \\`${TAB_SERVER_KEY}\\` stanza in ${path}`);\n if (!secret.ok) return bad(\"mcp-client-config\", secret.error.message);\n const command = isRecord(stanza) ? stanza[\"command\"] : undefined;\n return pass(\"mcp-client-config\", `\\`${path}\\` runs the tab server with \\`${String(command)}\\` and carries no credential`);\n}\n\n/** A promise with a deadline, so one slow endpoint does not hold the whole report. */\nfunction withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`${label} did not answer within ${ms}ms`)), ms);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (error: unknown) => {\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(String(error)));\n },\n );\n });\n}\n"]}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The CLI: the executable entry point of this package. (R25.5, R25.6)
3
+ *
4
+ * `connect` writes one `mcpServers` stanza into an MCP client's config, never a
5
+ * key. `doctor` checks the installation with reads alone. `status` and `settle`
6
+ * are the two tools reachable without a model, and `settle` is a dry run unless
7
+ * it is told otherwise.
8
+ */
9
+ export * from "./client-config.js";
10
+ export * from "./connect.js";
11
+ export * from "./doctor.js";
12
+ export * from "./main.js";
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The CLI: the executable entry point of this package. (R25.5, R25.6)
3
+ *
4
+ * `connect` writes one `mcpServers` stanza into an MCP client's config, never a
5
+ * key. `doctor` checks the installation with reads alone. `status` and `settle`
6
+ * are the two tools reachable without a model, and `settle` is a dry run unless
7
+ * it is told otherwise.
8
+ */
9
+ export * from "./client-config.js";
10
+ export * from "./connect.js";
11
+ export * from "./doctor.js";
12
+ export * from "./main.js";
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC","sourcesContent":["/**\n * The CLI: the executable entry point of this package. (R25.5, R25.6)\n *\n * `connect` writes one `mcpServers` stanza into an MCP client's config, never a\n * key. `doctor` checks the installation with reads alone. `status` and `settle`\n * are the two tools reachable without a model, and `settle` is a dry run unless\n * it is told otherwise.\n */\n\nexport * from \"./client-config.js\";\nexport * from \"./connect.js\";\nexport * from \"./doctor.js\";\nexport * from \"./main.js\";\n"]}