@lifeaitools/clauth 2.1.1 → 2.10.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 (98) hide show
  1. package/.clauth-skill/references/keys-guide.md +270 -270
  2. package/cli/api.js +238 -238
  3. package/cli/commands/install.js +413 -396
  4. package/cli/commands/login.test.js +49 -1
  5. package/cli/commands/ops-install.js +11 -10
  6. package/cli/commands/ops.test.js +270 -0
  7. package/cli/commands/scrub.test.js +70 -0
  8. package/cli/commands/serve.js +2613 -11219
  9. package/cli/commands/uninstall.js +164 -164
  10. package/cli/dashboard/dashboard.js +592 -0
  11. package/cli/dashboard/global.css +65 -0
  12. package/cli/dashboard/panel-element.js +79 -0
  13. package/cli/dashboard/panels/build-status-panel.css +12 -0
  14. package/cli/dashboard/panels/build-status-panel.js +98 -0
  15. package/cli/dashboard/panels/panel-registry.js +58 -0
  16. package/cli/dashboard/panels/services-panel.css +95 -0
  17. package/cli/dashboard/panels/services-panel.js +872 -0
  18. package/cli/dashboard/panels/surfaces-panel.css +59 -0
  19. package/cli/dashboard/panels/surfaces-panel.js +474 -0
  20. package/cli/dashboard/panels/toolbar-panel.css +42 -0
  21. package/cli/dashboard/panels/toolbar-panel.js +374 -0
  22. package/cli/dashboard/panels/tunnel-panel.css +61 -0
  23. package/cli/dashboard/panels/tunnel-panel.js +688 -0
  24. package/cli/dashboard/panels/webdav-panel.css +38 -0
  25. package/cli/dashboard/panels/webdav-panel.js +166 -0
  26. package/cli/fingerprint.test.js +95 -0
  27. package/cli/http/components/callagent-terminal-component.js +310 -0
  28. package/cli/http/components/dashboard-component.js +123 -0
  29. package/cli/http/components/inbox-component.js +41 -0
  30. package/cli/http/components/mcp-transport-component.js +275 -0
  31. package/cli/http/components/oauth-component.js +207 -0
  32. package/cli/http/components/service-crud-component.js +288 -0
  33. package/cli/http/components/supervisor-component.js +104 -0
  34. package/cli/http/components/system-component.js +184 -0
  35. package/cli/http/components/tunnel-component.js +508 -0
  36. package/cli/http/components/vault-auth-component.js +264 -0
  37. package/cli/http/components/watchdog-component.js +28 -0
  38. package/cli/http/request-utils.js +21 -0
  39. package/cli/http/servers/ops-server.js +335 -0
  40. package/cli/index.js +5 -91
  41. package/cli/lib/fs-git.test.js +467 -0
  42. package/cli/mcp/providers/call-agent-provider.js +30 -0
  43. package/cli/mcp/providers/clauth-core-provider.js +461 -0
  44. package/cli/mcp/providers/ops-provider.js +42 -0
  45. package/cli/mcp/providers/terminal-provider.js +81 -0
  46. package/cli/mcp/tool-registry.js +48 -0
  47. package/cli/mcp/tool-schemas.js +822 -0
  48. package/cli/mcp/tool-schemas.test.js +49 -0
  49. package/cli/ops/pm2-preflight.js +54 -0
  50. package/cli/recovery.js +6 -0
  51. package/cli/recovery.test.js +179 -0
  52. package/cli/services/agent-pool-instance.js +38 -0
  53. package/cli/services/claude-binary.js +29 -0
  54. package/cli/services/delegation-lane-instance.js +35 -0
  55. package/cli/services/delegation-lane-instance.test.js +37 -0
  56. package/cli/services/file-io.js +38 -0
  57. package/cli/services/file-io.test.js +10 -0
  58. package/cli/services/fs-mount.js +66 -0
  59. package/cli/services/fs-mount.test.js +14 -0
  60. package/cli/services/fs-upload-sessions.js +20 -0
  61. package/cli/services/fs-upload-sessions.test.js +14 -0
  62. package/cli/services/git-exec.js +37 -0
  63. package/cli/services/git-exec.test.js +41 -0
  64. package/cli/services/git-import-guard.js +28 -0
  65. package/cli/services/git-import-guard.test.js +18 -0
  66. package/cli/services/service-status.js +191 -0
  67. package/cli/services/service-status.test.js +39 -0
  68. package/cli/services/terminal-dispatch.js +135 -0
  69. package/cli/services/terminal-dispatch.test.js +9 -0
  70. package/cli/services/terminal-registry.js +39 -0
  71. package/cli/services/terminal-registry.test.js +31 -0
  72. package/cli/services/terminal-sessions.js +100 -0
  73. package/cli/services/terminal-sessions.test.js +26 -0
  74. package/cli/services/terminal-window.js +106 -0
  75. package/cli/services/vault-read.js +14 -0
  76. package/cli/services/vault-read.test.js +35 -0
  77. package/cli/supervisor-registry.js +116 -0
  78. package/cli/supervisor-registry.test.js +165 -0
  79. package/cli/supervisor-ui.test.js +230 -30
  80. package/cli/webdav-config.js +84 -0
  81. package/cli/webdav-config.test.js +79 -0
  82. package/cli/webdav-obscure.js +23 -0
  83. package/cli/webdav-obscure.test.js +41 -0
  84. package/cli/webdav-rclone-discovery.js +44 -0
  85. package/cli/webdav-service.js +30 -118
  86. package/cli/webdav-service.test.js +107 -0
  87. package/install.ps1 +102 -102
  88. package/install.sh +49 -49
  89. package/package.json +2 -2
  90. package/scripts/bootstrap.cjs +121 -121
  91. package/supabase/functions/auth-vault/index.ts +350 -350
  92. package/supabase/migrations/001_clauth_schema.sql +94 -94
  93. package/supabase/migrations/002_vault_helpers.sql +90 -90
  94. package/supabase/migrations/20260317_lockout.sql +26 -26
  95. package/cli/assets/codevelop/launcher-active.cmd.template +0 -20
  96. package/cli/assets/codevelop/launcher-static.cmd.template +0 -7
  97. package/cli/assets/codevelop/windows-terminal.profiles.json +0 -48
  98. package/cli/commands/codevelop.js +0 -1190
@@ -0,0 +1,461 @@
1
+ // cli/mcp/providers/clauth-core-provider.js
2
+ // clauth's own core vault-operation MCP tools, as their own provider file
3
+ // registering against the shared cli/mcp/tool-registry.js. Third and
4
+ // largest file-level split (after terminal-provider.js and
5
+ // ops-provider.js) — 17 registrations: ping, unlock, lock, status,
6
+ // knowledge, list, search, get, inject, enable, disable, test, scrub,
7
+ // set_project, self_check, generate_token, enroll_machine.
8
+ //
9
+ // Unlike the two earlier providers, this group had no single pre-existing
10
+ // service module to lean on — its dependencies span several existing
11
+ // modules (fingerprint.js, api.js, services/service-status.js,
12
+ // recovery.js, conf-path.js, enrollment-script.js) plus two small,
13
+ // genuinely generic local helpers (writeTempSecret, copyToClipboard) that
14
+ // had no callers outside this group (confirmed via grep before moving) and
15
+ // moved alongside it. call_agent stayed in serve.js -- it depends on
16
+ // runCallAgent, a non-trivial serve.js-local function not yet extracted.
17
+ //
18
+ // LOG_FILE is intentionally NOT imported from serve.js: that constant has
19
+ // 67 references there (clauth's own daemon-log path, used well beyond MCP
20
+ // tools) and stays serve.js's own. clauth_generate_token's use of it here
21
+ // is the one place this provider needs it, so the same trivial one-line
22
+ // expression is repeated rather than threading an import for a single
23
+ // constant used once.
24
+ //
25
+ // serve.js imports this file for its side effect (the registerTool() calls
26
+ // below run at import time) — see the `import "../mcp/providers/
27
+ // clauth-core-provider.js";` line in cli/commands/serve.js.
28
+ import os from "node:os";
29
+ import path from "node:path";
30
+ import fs from "node:fs";
31
+ import crypto from "node:crypto";
32
+ import { execSync } from "node:child_process";
33
+ import Conf from "conf";
34
+ import { mcpResult, mcpError, registerTool, requireMcpWrite } from "../tool-registry.js";
35
+ import { deriveToken } from "../../fingerprint.js";
36
+ import * as api from "../../api.js";
37
+ import { getConfOptions } from "../../conf-path.js";
38
+ import { writeCredentialWithRecovery } from "../../recovery.js";
39
+ import { writeEnrollmentScript } from "../../enrollment-script.js";
40
+ import {
41
+ ENV_MAP,
42
+ invalidateServiceStatusCache,
43
+ sanitizeServiceMetadata,
44
+ getCachedServiceStatus,
45
+ searchServices,
46
+ } from "../../services/service-status.js";
47
+
48
+ const LOG_FILE = path.join(os.tmpdir(), "clauth-serve.log");
49
+
50
+ function writeTempSecret(service, value) {
51
+ const filePath = path.join(os.tmpdir(), `.clauth-${service}`);
52
+ fs.writeFileSync(filePath, value, { mode: 0o600 });
53
+ setTimeout(() => { try { fs.unlinkSync(filePath); } catch {} }, 30_000);
54
+ return filePath;
55
+ }
56
+
57
+ function copyToClipboard(value) {
58
+ const platform = os.platform();
59
+ if (platform === "win32") {
60
+ execSync("clip", { input: value, stdio: ["pipe", "pipe", "pipe"] });
61
+ } else if (platform === "darwin") {
62
+ execSync("pbcopy", { input: value, stdio: ["pipe", "pipe", "pipe"] });
63
+ } else {
64
+ execSync("xclip -selection clipboard", { input: value, stdio: ["pipe", "pipe", "pipe"] });
65
+ }
66
+ }
67
+
68
+ registerTool("clauth_ping", async (args, vault) => {
69
+ return mcpResult(
70
+ vault.password
71
+ ? `unlocked | pid: ${process.pid} | failures: ${vault.failCount}/${vault.MAX_FAILS}`
72
+ : `locked | pid: ${process.pid} | failures: ${vault.failCount}/${vault.MAX_FAILS}`
73
+ );
74
+ });
75
+
76
+ registerTool("clauth_unlock", async (args, vault) => {
77
+ const pw = args.password;
78
+ if (!pw) return mcpError("password is required");
79
+ try {
80
+ const { token, timestamp } = deriveToken(pw, vault.machineHash);
81
+ const result = await api.test(pw, vault.machineHash, token, timestamp);
82
+ if (result.error) return mcpError(`Unlock failed: ${result.error}`);
83
+ vault.password = pw;
84
+ return mcpResult("Vault unlocked");
85
+ } catch (err) {
86
+ return mcpError(`Unlock failed: ${err.message}`);
87
+ }
88
+ });
89
+
90
+ registerTool("clauth_lock", async (args, vault) => {
91
+ vault.password = null;
92
+ return mcpResult("Vault locked — password cleared from memory");
93
+ });
94
+
95
+ registerTool("clauth_status", async (args, vault) => {
96
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
97
+ try {
98
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
99
+ const result = await getCachedServiceStatus({
100
+ password: vault.password,
101
+ machineHash: vault.machineHash,
102
+ token,
103
+ timestamp,
104
+ whitelist: vault.whitelist,
105
+ });
106
+ if (result.error) return mcpError(result.error);
107
+ const services = result.services || [];
108
+ const lines = ["SERVICE TYPE PROJECT STATUS KEY LAST RETRIEVED",
109
+ "------- ---- ------- ------ --- --------------"];
110
+ for (const s of services) {
111
+ const hasStoredKey = !!(s.has_key || s.vault_key);
112
+ const status = s.enabled ? "ACTIVE" : (hasStoredKey ? "SUSPENDED" : "NO KEY");
113
+ const hasKey = hasStoredKey ? "yes" : "—";
114
+ const lastGet = s.last_retrieved ? new Date(s.last_retrieved).toLocaleDateString() : "never";
115
+ const proj = (s.project || "—").padEnd(22);
116
+ lines.push(`${s.name.padEnd(24)} ${(s.key_type || "").padEnd(12)} ${proj} ${status.padEnd(12)} ${hasKey.padEnd(6)} ${lastGet}`);
117
+ }
118
+ lines.push(`\nmetadata cache: ${result.cached ? "hit" : "miss"} (${result.cache_ttl_ms}ms TTL)`);
119
+ return mcpResult(lines.join("\n"));
120
+ } catch (err) {
121
+ return mcpError(err.message);
122
+ }
123
+ });
124
+
125
+ registerTool("clauth_knowledge", async (args, vault) => {
126
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
127
+ try {
128
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
129
+ const result = await getCachedServiceStatus({
130
+ password: vault.password,
131
+ machineHash: vault.machineHash,
132
+ token,
133
+ timestamp,
134
+ project: args.project,
135
+ whitelist: vault.whitelist,
136
+ force: args.refresh === true,
137
+ });
138
+ if (result.error) return mcpError(result.error);
139
+ return mcpResult(JSON.stringify({
140
+ services: (result.services || []).map(sanitizeServiceMetadata),
141
+ cached: !!result.cached,
142
+ cache_ttl_ms: result.cache_ttl_ms,
143
+ contract: {
144
+ safe_for_agents: true,
145
+ raw_secret_rule: "Fetch one exact /v/<service> only when a command needs that secret.",
146
+ bulk_rule: "Do not walk every service name or bulk-retrieve all secrets.",
147
+ },
148
+ }, null, 2));
149
+ } catch (err) {
150
+ return mcpError(err.message);
151
+ }
152
+ });
153
+
154
+ registerTool("clauth_list", async (args, vault) => {
155
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
156
+ try {
157
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
158
+ const result = await getCachedServiceStatus({
159
+ password: vault.password,
160
+ machineHash: vault.machineHash,
161
+ token,
162
+ timestamp,
163
+ whitelist: vault.whitelist,
164
+ });
165
+ if (result.error) return mcpError(result.error);
166
+ const services = result.services || [];
167
+ return mcpResult(services.map(s => s.name).join(", "));
168
+ } catch (err) {
169
+ return mcpError(err.message);
170
+ }
171
+ });
172
+
173
+ registerTool("clauth_search", async (args, vault) => {
174
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
175
+ try {
176
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
177
+ const result = await searchServices({
178
+ password: vault.password,
179
+ machineHash: vault.machineHash,
180
+ token,
181
+ timestamp,
182
+ query: args.query,
183
+ project: args.project,
184
+ includeAddresses: args.addresses === true,
185
+ whitelist: vault.whitelist
186
+ });
187
+ if (result.error) return mcpError(result.error);
188
+ return mcpResult(JSON.stringify(result, null, 2));
189
+ } catch (err) {
190
+ return mcpError(err.message);
191
+ }
192
+ });
193
+
194
+ registerTool("clauth_get", async (args, vault) => {
195
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
196
+ const service = (args.service || "").toLowerCase();
197
+ const target = args.target || "file";
198
+ if (!service) return mcpError("service is required");
199
+ if (vault.whitelist && !vault.whitelist.includes(service)) {
200
+ return mcpError(`Service '${service}' not in whitelist`);
201
+ }
202
+ try {
203
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
204
+ const result = await api.retrieve(vault.password, vault.machineHash, token, timestamp, service);
205
+ if (result.error) return mcpError(result.error);
206
+ const value = typeof result.value === "string" ? result.value : JSON.stringify(result.value);
207
+
208
+ if (target === "clipboard") {
209
+ try {
210
+ copyToClipboard(value);
211
+ return mcpResult(`${service} copied to clipboard`);
212
+ } catch (err) {
213
+ return mcpError(`Clipboard failed: ${err.message}. Use target 'file' instead.`);
214
+ }
215
+ }
216
+
217
+ if (target === "stdout") {
218
+ return {
219
+ content: [{ type: "text", text: `WARNING: secret in response — will appear in transcript. Use 'file' or 'clipboard' mode instead.\n\n${value}` }],
220
+ isError: true
221
+ };
222
+ }
223
+
224
+ // Remote caller (claude.ai via tunnel) — can't read local temp files
225
+ // Return value inline in MCP response (safe: stays in AI context, not a shell transcript)
226
+ if (vault.remote) {
227
+ const envVar = ENV_MAP[service] || service.toUpperCase().replace(/-/g, "_");
228
+ return mcpResult(`${envVar}=${value}`);
229
+ }
230
+
231
+ // Default: file (local callers)
232
+ const envVar = ENV_MAP[service] || service.toUpperCase().replace(/-/g, "_");
233
+ const filePath = writeTempSecret(service, value);
234
+ return mcpResult(`${service} → ${filePath} (auto-deletes in 30s)\nEnv var: ${envVar}\nUsage: export ${envVar}=$(cat ${filePath.replace(/\\/g, "/")})`);
235
+ } catch (err) {
236
+ return mcpError(err.message);
237
+ }
238
+ });
239
+
240
+ registerTool("clauth_inject", async (args, vault) => {
241
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
242
+ const services = args.services || [];
243
+ if (!services.length) return mcpError("services array is required");
244
+ if (services.length > 5 && args.allow_many !== true) {
245
+ return mcpError("Refusing to retrieve more than 5 services at once. Use clauth_knowledge for discovery, then request only the exact secrets needed. Pass allow_many=true only for an intentional bulk operation.");
246
+ }
247
+
248
+ const lines = [];
249
+ const errors = [];
250
+ for (const svc of services) {
251
+ const service = svc.toLowerCase();
252
+ if (vault.whitelist && !vault.whitelist.includes(service)) {
253
+ errors.push(`${service}: not in whitelist`);
254
+ continue;
255
+ }
256
+ try {
257
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
258
+ const result = await api.retrieve(vault.password, vault.machineHash, token, timestamp, service);
259
+ if (result.error) { errors.push(`${service}: ${result.error}`); continue; }
260
+ const value = typeof result.value === "string" ? result.value : JSON.stringify(result.value);
261
+ const envVar = ENV_MAP[service] || service.toUpperCase().replace(/-/g, "_");
262
+ lines.push(`export ${envVar}="${value.replace(/"/g, '\\"')}"`);
263
+ } catch (err) {
264
+ errors.push(`${service}: ${err.message}`);
265
+ }
266
+ }
267
+
268
+ if (!lines.length) return mcpError(`No services retrieved:\n${errors.join("\n")}`);
269
+
270
+ // Remote caller — return env vars inline
271
+ if (vault.remote) {
272
+ let msg = lines.join("\n");
273
+ if (errors.length) msg += `\n\nErrors:\n${errors.join("\n")}`;
274
+ return mcpResult(msg);
275
+ }
276
+
277
+ // Local caller — write temp env file
278
+ const envFilePath = path.join(os.tmpdir(), ".clauth-env");
279
+ fs.writeFileSync(envFilePath, `${lines.join("\n")}\n`, { mode: 0o600 });
280
+ setTimeout(() => { try { fs.unlinkSync(envFilePath); } catch {} }, 30_000);
281
+
282
+ let msg = `${lines.length} service(s) → ${envFilePath.replace(/\\/g, "/")} (auto-deletes in 30s)\nUsage: source ${envFilePath.replace(/\\/g, "/")}`;
283
+ if (errors.length) msg += `\n\nErrors:\n${errors.join("\n")}`;
284
+ return mcpResult(msg);
285
+ });
286
+
287
+ registerTool("clauth_enable", async (args, vault) => {
288
+ const writeError = requireMcpWrite(vault);
289
+ if (writeError) return writeError;
290
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
291
+ const service = (args.service || "").toLowerCase();
292
+ if (!service) return mcpError("service is required");
293
+ try {
294
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
295
+ const result = await api.enable(vault.password, vault.machineHash, token, timestamp, service, true);
296
+ if (result.error) return mcpError(result.error);
297
+ invalidateServiceStatusCache(vault.machineHash);
298
+ return mcpResult(`${service} enabled`);
299
+ } catch (err) {
300
+ return mcpError(err.message);
301
+ }
302
+ });
303
+
304
+ registerTool("clauth_disable", async (args, vault) => {
305
+ const writeError = requireMcpWrite(vault);
306
+ if (writeError) return writeError;
307
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
308
+ const service = (args.service || "").toLowerCase();
309
+ if (!service) return mcpError("service is required");
310
+ try {
311
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
312
+ const result = await api.enable(vault.password, vault.machineHash, token, timestamp, service, false);
313
+ if (result.error) return mcpError(result.error);
314
+ invalidateServiceStatusCache(vault.machineHash);
315
+ return mcpResult(`${service} disabled`);
316
+ } catch (err) {
317
+ return mcpError(err.message);
318
+ }
319
+ });
320
+
321
+ registerTool("clauth_test", async (args, vault) => {
322
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
323
+ try {
324
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
325
+ const result = await api.test(vault.password, vault.machineHash, token, timestamp);
326
+ if (result.error) return mcpError(`FAIL: ${result.error}`);
327
+ return mcpResult(`PASS — machine: ${vault.machineHash.slice(0, 16)}... | window: ${new Date(result.timestamp).toISOString()}`);
328
+ } catch (err) {
329
+ return mcpError(`FAIL: ${err.message}`);
330
+ }
331
+ });
332
+
333
+ registerTool("clauth_scrub", async (args, vault) => {
334
+ const target = args.target || "latest";
335
+ try {
336
+ const { runScrub: doScrub } = await import("../../commands/scrub.js");
337
+ // runScrub writes to stdout via chalk — capture isn't clean in MCP mode.
338
+ // Call it and trust it works; report success.
339
+ await doScrub(target === "all" ? "all" : undefined, {});
340
+ return mcpResult(`Scrub complete (target: ${target})`);
341
+ } catch (err) {
342
+ return mcpError(`Scrub failed: ${err.message}`);
343
+ }
344
+ });
345
+
346
+ registerTool("clauth_set_project", async (args, vault) => {
347
+ const writeError = requireMcpWrite(vault);
348
+ if (writeError) return writeError;
349
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
350
+ const service = (args.service || "").toLowerCase();
351
+ const project = args.project;
352
+ if (!service) return mcpError("service is required");
353
+ if (project === undefined) return mcpError("project is required (empty string to clear)");
354
+ try {
355
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
356
+ const result = await api.updateService(vault.password, vault.machineHash, token, timestamp, service, { project: project || "" });
357
+ if (result.error) return mcpError(result.error);
358
+ invalidateServiceStatusCache(vault.machineHash);
359
+ return mcpResult(project ? `${service} → project: ${project}` : `${service} → project cleared`);
360
+ } catch (err) {
361
+ return mcpError(err.message);
362
+ }
363
+ });
364
+
365
+ registerTool("clauth_self_check", async (args, vault) => {
366
+ // Test connectivity via the Cloudflare tunnel and/or localhost daemon
367
+ const tunnelUrl = vault.tunnelUrl;
368
+ const results = [];
369
+
370
+ // Check localhost daemon
371
+ try {
372
+ const r = await fetch("http://127.0.0.1:52437/ping", { signal: AbortSignal.timeout(3000) });
373
+ const data = await r.json();
374
+ results.push(`Local daemon: PASS (${data.locked ? "locked" : "unlocked"}, failures: ${data.failures ?? 0})`);
375
+ } catch (err) {
376
+ results.push(`Local daemon: FAIL — http://127.0.0.1:52437 not reachable (${err.message})`);
377
+ }
378
+
379
+ // Check tunnel
380
+ if (tunnelUrl) {
381
+ try {
382
+ const r = await fetch(`${tunnelUrl}/ping`, { signal: AbortSignal.timeout(5000) });
383
+ const data = await r.json();
384
+ results.push(`Tunnel: PASS — ${tunnelUrl} reachable (${data.locked ? "locked" : "unlocked"})`);
385
+ results.push(`claude.ai SSE endpoint: ${tunnelUrl}/sse`);
386
+ results.push(`claude.ai MCP endpoint: ${tunnelUrl}/mcp`);
387
+ } catch (err) {
388
+ results.push(`Tunnel: FAIL — ${tunnelUrl} not reachable (${err.message})`);
389
+ results.push("claude.ai MCP connector will not work until tunnel is restored.");
390
+ results.push("Fix: clauth serve start --tunnel clauth.prtrust.fund");
391
+ }
392
+ } else {
393
+ results.push("Tunnel: NOT RUNNING — claude.ai connector requires the tunnel.");
394
+ results.push("Start with: clauth serve start --tunnel clauth.prtrust.fund");
395
+ }
396
+
397
+ return mcpResult(results.join("\n"));
398
+ });
399
+
400
+ registerTool("clauth_generate_token", async (args, vault) => {
401
+ const writeError = requireMcpWrite(vault);
402
+ if (writeError) return writeError;
403
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
404
+ const service = (args.service || "").trim().toLowerCase();
405
+ const prefix = args.prefix || "";
406
+ if (!service) return mcpError("service name is required");
407
+ try {
408
+ const randomHex = crypto.randomBytes(32).toString("hex");
409
+ const generatedToken = `${prefix}${randomHex}`;
410
+ const { result } = await writeCredentialWithRecovery({
411
+ password: vault.password,
412
+ machineHash: vault.machineHash,
413
+ service,
414
+ value: generatedToken,
415
+ logFile: LOG_FILE,
416
+ normalize: false,
417
+ });
418
+ if (result.error) return mcpError(result.error);
419
+ invalidateServiceStatusCache(vault.machineHash);
420
+ return mcpResult(`Token generated and stored under "${service}"`);
421
+ } catch (err) {
422
+ return mcpError(`generate_token failed: ${err.message}`);
423
+ }
424
+ });
425
+
426
+ registerTool("clauth_enroll_machine", async (args, vault) => {
427
+ const writeError = requireMcpWrite(vault);
428
+ if (writeError) return writeError;
429
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
430
+ try {
431
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
432
+ const result = await api.createEnrollment(
433
+ vault.password,
434
+ vault.machineHash,
435
+ token,
436
+ timestamp,
437
+ args.label,
438
+ Number(args.ttl_minutes || 60),
439
+ args.install_id
440
+ );
441
+ if (result.error) return mcpError(result.error);
442
+ const localConfig = new Conf(getConfOptions());
443
+ const supabaseUrl = localConfig.get("supabase_url") || process.env.CLAUTH_SUPABASE_URL || "";
444
+ const anonKey = localConfig.get("supabase_anon_key") || process.env.CLAUTH_SUPABASE_ANON_KEY || "";
445
+ const scriptPath = writeEnrollmentScript({
446
+ supabaseUrl,
447
+ anonKey,
448
+ enrollmentCode: result.enrollment_code,
449
+ label: args.label,
450
+ });
451
+ return mcpResult(JSON.stringify({
452
+ enrollment_code: result.enrollment_code,
453
+ install_id: result.install_id,
454
+ expires_at: result.expires_at,
455
+ script_path: scriptPath,
456
+ new_computer_steps: [`Run ${scriptPath} on the new computer. It installs clauth, enrolls with this one-time code, installs startup, then deletes itself.`]
457
+ }, null, 2));
458
+ } catch (err) {
459
+ return mcpError(`enroll_machine failed: ${err.message}`);
460
+ }
461
+ });
@@ -0,0 +1,42 @@
1
+ // cli/mcp/providers/ops-provider.js
2
+ // The clauth_ops_* MCP tools (Vultr deployment-control passthrough), as
3
+ // their own provider file registering against the shared
4
+ // cli/mcp/tool-registry.js. Second file-level split (after
5
+ // terminal-provider.js) — chosen next because callOpsFromMcp's only two
6
+ // dependencies (vaultRetrieveValue, requestOps) already live in their own
7
+ // clean modules, and callOpsFromMcp itself is used by nothing outside these
8
+ // 7 registrations (confirmed via grep before moving it), so it's safe to
9
+ // move alongside them rather than keep shared.
10
+ //
11
+ // serve.js imports this file for its side effect (the registerTool() calls
12
+ // below run at import time) — see the `import "../mcp/providers/
13
+ // ops-provider.js";` line in cli/commands/serve.js.
14
+ import { mcpResult, mcpError, registerTool } from "../tool-registry.js";
15
+ import { vaultRetrieveValue } from "../../services/vault-read.js";
16
+ import { requestOps } from "../../commands/ops.js";
17
+
18
+ async function callOpsFromMcp(vault, method, requestPath, body, { write = false } = {}) {
19
+ if (write && !vault.writeEnabled) return mcpError("MCP write tools are disabled by default. Launch clauth with CLAUTH_MCP_WRITE=1 for an explicit write-capable session.");
20
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
21
+ const endpoint = process.env.CLAUTH_OPS_APPROVED_ORIGIN;
22
+ if (!endpoint) return mcpError("service_not_available");
23
+ try {
24
+ const url = new URL(endpoint);
25
+ if (url.protocol !== "https:") return mcpError("service_not_available");
26
+ const service = process.env.CLAUTH_OPS_TOKEN_SERVICE || "vultr-ops-api-token";
27
+ const credential = await vaultRetrieveValue(vault, service);
28
+ if (credential.error || !credential.value) return mcpError("service_not_available");
29
+ const payload = await requestOps({ endpoint: url.toString().replace(/\/$/, ""), token: String(credential.value), method, path: requestPath, body });
30
+ return mcpResult(JSON.stringify(payload, null, 2));
31
+ } catch {
32
+ return mcpError("service_not_available");
33
+ }
34
+ }
35
+
36
+ registerTool("clauth_ops_catalog", async (args, vault) => callOpsFromMcp(vault, "GET", "/v1/ops/catalog"));
37
+ registerTool("clauth_ops_processes", async (args, vault) => callOpsFromMcp(vault, "GET", "/v1/ops/processes"));
38
+ registerTool("clauth_ops_describe", async (args, vault) => callOpsFromMcp(vault, "GET", `/v1/ops/processes/${encodeURIComponent(args.application || "")}`));
39
+ registerTool("clauth_ops_deploy", async (args, vault) => callOpsFromMcp(vault, "POST", "/v1/ops/deployments", { application: args.application, ...(args.ref ? { ref: args.ref } : {}) }, { write: true }));
40
+ registerTool("clauth_ops_promote", async (args, vault) => callOpsFromMcp(vault, "POST", "/v1/ops/promotions", { application_uuid: args.application_uuid }, { write: true }));
41
+ registerTool("clauth_ops_job", async (args, vault) => callOpsFromMcp(vault, "GET", `/v1/ops/jobs/${encodeURIComponent(args.job_id || "")}`));
42
+ registerTool("clauth_ops_run", async (args, vault) => callOpsFromMcp(vault, "POST", "/v1/ops/operations", { operation: args.operation, input: args.input && typeof args.input === "object" ? args.input : {} }, { write: true }));
@@ -0,0 +1,81 @@
1
+ // cli/mcp/providers/terminal-provider.js
2
+ // The terminal-session + channel-event MCP tools, as their own provider file
3
+ // registering against the shared cli/mcp/tool-registry.js — the first true
4
+ // file-level split of what was, until now, one large registerTool(...) block
5
+ // living entirely inside cli/commands/serve.js. Chosen as the first slice to
6
+ // move because every dependency it needs (startTerminalSession et al.,
7
+ // channelEvents) already lives in its own clean service module — nothing
8
+ // here reaches back into serve.js-local state, unlike clauth_*'s vault
9
+ // operations or call_agent's runCallAgent (still serve.js-local, deferred).
10
+ //
11
+ // serve.js imports this file for its side effect (the registerTool() calls
12
+ // below run at import time) — see the `import "../mcp/providers/
13
+ // terminal-provider.js";` line in cli/commands/serve.js.
14
+ import path from "node:path";
15
+ import { mcpResult, mcpError, registerTool } from "../tool-registry.js";
16
+ import { channelEvents, defaultTerminalCwd } from "../../services/terminal-registry.js";
17
+ import { startTerminalSession, listTerminalSessions, stopTerminalSession } from "../../services/terminal-sessions.js";
18
+ import { sendTerminalMessage, recvTerminalResponse } from "../../services/terminal-dispatch.js";
19
+ import { showTerminalSession } from "../../services/terminal-window.js";
20
+
21
+ registerTool("terminal_start", async (args, vault) => {
22
+ const { name, knowledge_tier, context_md, use_warm, cwd: requestedCwd } = args;
23
+ if (!name) return mcpError("name required");
24
+ const cwd = requestedCwd ? path.resolve(String(requestedCwd)) : defaultTerminalCwd();
25
+ const result = startTerminalSession(name, knowledge_tier || 'db_only', context_md || null, cwd, use_warm === true);
26
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
27
+ return mcpResult(JSON.stringify(result));
28
+ });
29
+
30
+ registerTool("terminal_send", async (args, vault) => {
31
+ const { session_id, message } = args;
32
+ if (!session_id || !message) return mcpError("session_id and message required");
33
+ const result = sendTerminalMessage(session_id, message);
34
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
35
+ return mcpResult(JSON.stringify(result));
36
+ });
37
+
38
+ registerTool("terminal_list", async (args, vault) => {
39
+ return mcpResult(JSON.stringify(listTerminalSessions()));
40
+ });
41
+
42
+ registerTool("terminal_show", async (args, vault) => {
43
+ const { session_id } = args;
44
+ if (!session_id) return mcpError("session_id required");
45
+ const result = showTerminalSession(session_id);
46
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
47
+ return mcpResult(JSON.stringify(result));
48
+ });
49
+
50
+ registerTool("terminal_recv", async (args, vault) => {
51
+ const { session_id } = args;
52
+ if (!session_id) return mcpError("session_id required");
53
+ const result = recvTerminalResponse(session_id);
54
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
55
+ return mcpResult(JSON.stringify(result));
56
+ });
57
+
58
+ registerTool("terminal_stop", async (args, vault) => {
59
+ const { session_id } = args;
60
+ if (!session_id) return mcpError("session_id required");
61
+ const result = stopTerminalSession(session_id);
62
+ if (result.error) return mcpError(`${result.error}: ${result.message}`);
63
+ return mcpResult(JSON.stringify(result));
64
+ });
65
+
66
+ registerTool("channel_list", async (args, vault) => {
67
+ const { since, limit } = args || {};
68
+ let events = channelEvents.slice();
69
+ if (since) {
70
+ const cutoff = new Date(since).getTime();
71
+ events = events.filter((e) => new Date(e.created_at).getTime() > cutoff);
72
+ }
73
+ events = events.slice(-(limit ? Math.min(Number(limit), 100) : 50));
74
+ return mcpResult(JSON.stringify({ events, count: events.length }));
75
+ });
76
+
77
+ registerTool("channel_clear", async (args, vault) => {
78
+ const cleared = channelEvents.length;
79
+ channelEvents.length = 0;
80
+ return mcpResult(JSON.stringify({ cleared }));
81
+ });
@@ -0,0 +1,48 @@
1
+ // cli/mcp/tool-registry.js
2
+ // The shared interface every MCP tool provider registers against. Extracted
3
+ // out of cli/commands/serve.js so a provider can be its own file, importing
4
+ // this module, rather than every tool having to live inside serve.js itself
5
+ // (the same Dependency Inversion fix as the frontend's ClauthPanelRegistry
6
+ // (cli/dashboard/panels/panel-registry.js): serve.js and every provider now
7
+ // depend on THIS shared abstraction, not on each other directly).
8
+ //
9
+ // mcpResult/mcpError are generic MCP response shapers, not clauth-specific —
10
+ // any provider needs them, so they live here rather than being duplicated
11
+ // per file. registerTool/hasRegisteredTool/callRegisteredTool are the
12
+ // registry itself (register once, dispatch by lookup — Open/Closed: a new
13
+ // tool means one registerTool() call, never editing a central switch).
14
+ // requireMcpWrite is likewise tool-agnostic (checks vault.writeEnabled,
15
+ // nothing else) so it lives here too rather than being re-implemented by
16
+ // every provider that gates a write tool.
17
+
18
+ const mcpToolHandlers = new Map();
19
+
20
+ export function mcpResult(text) {
21
+ return { content: [{ type: "text", text }] };
22
+ }
23
+
24
+ export function mcpError(text) {
25
+ return { content: [{ type: "text", text }], isError: true };
26
+ }
27
+
28
+ export function registerTool(name, handler) {
29
+ if (mcpToolHandlers.has(name)) {
30
+ throw new Error(`MCP tool "${name}" already registered`);
31
+ }
32
+ mcpToolHandlers.set(name, handler);
33
+ }
34
+
35
+ export function hasRegisteredTool(name) {
36
+ return mcpToolHandlers.has(name);
37
+ }
38
+
39
+ export function callRegisteredTool(name, args, vault) {
40
+ return mcpToolHandlers.get(name)(args, vault);
41
+ }
42
+
43
+ // Takes vault explicitly (not a closure) so any provider's handler can call
44
+ // it with whichever vault it was itself given.
45
+ export function requireMcpWrite(vault) {
46
+ if (vault.writeEnabled) return null;
47
+ return mcpError("MCP write tools are disabled by default. Launch clauth with CLAUTH_MCP_WRITE=1 for an explicit write-capable session.");
48
+ }