@lifeaitools/clauth 1.5.82 → 1.5.84

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.
@@ -141,11 +141,12 @@ See `references/keys-guide.md` for where to find every credential.
141
141
  ```
142
142
  clauth install [--ref R] [--pat P] First-time: provision Supabase + install skill
143
143
  clauth setup [--admin-token T] [-p P] Register this machine
144
- clauth status [-p P] All services + state
145
- clauth test [-p P] Verify HMAC connection
146
- clauth list [-p P] Service names
147
-
148
- clauth write key <service> [-p P] Store a credential
144
+ clauth status [-p P] All services + state
145
+ clauth test [-p P] Verify HMAC connection
146
+ clauth list [-p P] Service names
147
+ clauth search <query> [-p P] Search names, metadata, and redacted server addresses
148
+
149
+ clauth write key <service> [-p P] Store a credential
149
150
  clauth write pw [-p P] Change password
150
151
  clauth enable <svc|all> [-p P] Activate service
151
152
  clauth disable <svc|all> [-p P] Suspend service
package/README.md CHANGED
@@ -72,16 +72,20 @@ clauth get github
72
72
  ```
73
73
  clauth install Provision Supabase + install Claude skill
74
74
  clauth setup Register this machine with the vault
75
- clauth status All services + state
76
- clauth test Verify connection
75
+ clauth status All services + state
76
+ clauth search <query> Find services by name, project, description, or redacted address
77
+ clauth test Verify connection
77
78
 
78
79
  clauth write key <service> Store a credential
79
80
  clauth write pw Change password
80
81
  clauth enable <svc|all> Activate service
81
82
  clauth disable <svc|all> Suspend service
82
- clauth get <service> Retrieve a key
83
-
84
- clauth add service <n> Register new service
83
+ clauth get <service> Retrieve a key
84
+ clauth npm whoami Verify npm token without PowerShell secret plumbing
85
+ clauth npm sync-github-secret LIFEAI/rdc-skills
86
+ Update GitHub NPM_TOKEN from clauth
87
+
88
+ clauth add service <n> Register new service
85
89
  clauth remove service <n> Remove service
86
90
  clauth revoke <svc|all> Delete key (destructive)
87
91
  ```
@@ -0,0 +1,123 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { spawnSync } from "child_process";
5
+
6
+ const CLAUTH_NPM_URL = "http://127.0.0.1:52437/v/npm";
7
+
8
+ function run(cmd, args, opts = {}) {
9
+ const useCmd = process.platform === "win32" && ["npm", "gh"].includes(cmd);
10
+ const executable = useCmd ? "cmd.exe" : cmd;
11
+ const finalArgs = useCmd ? ["/d", "/s", "/c", cmd, ...args] : args;
12
+ const result = spawnSync(executable, finalArgs, {
13
+ encoding: "utf8",
14
+ ...opts,
15
+ env: { ...process.env, ...(opts.env || {}) }
16
+ });
17
+ if (result.error) throw result.error;
18
+ return result;
19
+ }
20
+
21
+ function redactNpmTokenList(text) {
22
+ return String(text || "").replace(/npm_[A-Za-z0-9]+/g, token => `${token.slice(0, 9)}...${token.slice(-4)}`);
23
+ }
24
+
25
+ async function fetchNpmToken() {
26
+ const response = await fetch(CLAUTH_NPM_URL);
27
+ if (!response.ok) throw new Error(`failed to fetch npm token from clauth daemon: HTTP ${response.status}`);
28
+ const token = (await response.text()).trim();
29
+ if (!token) throw new Error("clauth npm token is empty");
30
+ if (!token.startsWith("npm_")) throw new Error("clauth npm token does not look like an npm token");
31
+ return token;
32
+ }
33
+
34
+ function withNpmAuth(token, fn) {
35
+ const npmrc = path.join(os.tmpdir(), `clauth-npm-${process.pid}-${Date.now()}.npmrc`);
36
+ try {
37
+ fs.writeFileSync(npmrc, `//registry.npmjs.org/:_authToken=${token}`, { encoding: "utf8", mode: 0o600 });
38
+ return fn(npmrc);
39
+ } finally {
40
+ try { fs.rmSync(npmrc, { force: true }); } catch {}
41
+ }
42
+ }
43
+
44
+ function runNpmWithToken(token, args) {
45
+ return withNpmAuth(token, npmrc => run("npm", ["--userconfig", npmrc, ...args]));
46
+ }
47
+
48
+ function printResult(result, { redact = true } = {}) {
49
+ const stdout = redact ? redactNpmTokenList(result.stdout) : result.stdout;
50
+ const stderr = redact ? redactNpmTokenList(result.stderr) : result.stderr;
51
+ if (stdout) process.stdout.write(stdout);
52
+ if (stderr) process.stderr.write(stderr);
53
+ }
54
+
55
+ function usage() {
56
+ console.log(`clauth npm <action>
57
+
58
+ Actions:
59
+ whoami Verify the clauth npm token identity
60
+ tokens List npm token metadata with token strings redacted
61
+ set-local Write the clauth npm token to the user npm config
62
+ sync-github-secret <repo> Set repo secret NPM_TOKEN from clauth, e.g. LIFEAI/rdc-skills
63
+ rerun <run-id> --repo <repo> Rerun a failed GitHub Actions workflow
64
+ `);
65
+ }
66
+
67
+ export async function runNpm(action = "help", opts = {}) {
68
+ if (action === "help") {
69
+ usage();
70
+ return;
71
+ }
72
+
73
+ const token = await fetchNpmToken();
74
+
75
+ if (action === "whoami") {
76
+ const result = runNpmWithToken(token, ["whoami", "--registry=https://registry.npmjs.org/"]);
77
+ printResult(result);
78
+ if (result.status !== 0) process.exitCode = result.status;
79
+ return;
80
+ }
81
+
82
+ if (action === "tokens") {
83
+ const result = runNpmWithToken(token, ["token", "list", "--json", "--registry=https://registry.npmjs.org/"]);
84
+ printResult(result);
85
+ if (result.status !== 0) process.exitCode = result.status;
86
+ return;
87
+ }
88
+
89
+ if (action === "set-local") {
90
+ const result = run("npm", ["config", "set", "//registry.npmjs.org/:_authToken", token, "--location=user"]);
91
+ if (result.status !== 0) {
92
+ printResult(result);
93
+ process.exitCode = result.status;
94
+ return;
95
+ }
96
+ console.log("local npm auth updated from clauth service 'npm'");
97
+ return;
98
+ }
99
+
100
+ if (action === "sync-github-secret") {
101
+ const repo = opts.repo || opts.args?.[0];
102
+ if (!repo) throw new Error("repo is required, e.g. clauth npm sync-github-secret LIFEAI/rdc-skills");
103
+ const result = run("gh", ["secret", "set", "NPM_TOKEN", "--repo", repo], { input: token });
104
+ printResult(result);
105
+ if (result.status !== 0) process.exitCode = result.status;
106
+ else console.log(`GitHub secret NPM_TOKEN updated for ${repo}`);
107
+ return;
108
+ }
109
+
110
+ if (action === "rerun") {
111
+ const runId = opts.args?.[0];
112
+ const repo = opts.repo;
113
+ if (!runId || !repo) throw new Error("usage: clauth npm rerun <run-id> --repo LIFEAI/rdc-skills");
114
+ const result = run("gh", ["run", "rerun", runId, "--repo", repo, "--failed"]);
115
+ printResult(result);
116
+ if (result.status !== 0) process.exitCode = result.status;
117
+ else console.log(`rerun requested for ${repo} run ${runId}`);
118
+ return;
119
+ }
120
+
121
+ usage();
122
+ throw new Error(`unknown clauth npm action: ${action}`);
123
+ }
@@ -607,9 +607,14 @@ function dashboardHtml(port, whitelist, isStaged = false) {
607
607
  .project-tab{background:none;border:none;border-bottom:2px solid transparent;color:#64748b;padding:8px 16px;font-size:.82rem;font-weight:500;cursor:pointer;white-space:nowrap;transition:all .15s}
608
608
  .project-tab:hover{color:#94a3b8;background:rgba(59,130,246,.05)}
609
609
  .project-tab.active{color:#60a5fa;border-bottom-color:#3b82f6;background:rgba(59,130,246,.08)}
610
- .project-tab .tab-count{font-size:.7rem;color:#475569;margin-left:4px;font-weight:400}
611
- .project-tab.active .tab-count{color:#3b82f6}
612
- .project-edit{display:none;margin-top:8px;padding:8px 10px;background:#0f172a;border:1px solid #334155;border-radius:6px}
610
+ .project-tab .tab-count{font-size:.7rem;color:#475569;margin-left:4px;font-weight:400}
611
+ .project-tab.active .tab-count{color:#3b82f6}
612
+ .service-search{display:flex;align-items:center;gap:10px;margin:-.35rem 0 1rem;background:#0f172a;border:1px solid #1e293b;border-radius:8px;padding:9px 12px}
613
+ .service-search-label{font-size:.76rem;color:#64748b;font-weight:600;letter-spacing:.02em;text-transform:uppercase;white-space:nowrap}
614
+ .service-search-input{flex:1;min-width:180px;background:#0a0f1a;border:1px solid #334155;border-radius:6px;color:#e2e8f0;font-family:'Courier New',monospace;font-size:.88rem;padding:8px 11px;outline:none;transition:border-color .15s}
615
+ .service-search-input:focus{border-color:#3b82f6}
616
+ .service-search-count{font-size:.78rem;color:#64748b;white-space:nowrap}
617
+ .project-edit{display:none;margin-top:8px;padding:8px 10px;background:#0f172a;border:1px solid #334155;border-radius:6px}
613
618
  .project-edit.open{display:flex;gap:6px;align-items:center}
614
619
  .project-edit input{background:#1e293b;border:1px solid #334155;border-radius:4px;color:#e2e8f0;font-size:.78rem;padding:4px 8px;outline:none;flex:1;font-family:'Courier New',monospace;transition:border-color .15s}
615
620
  .project-edit input:focus{border-color:#3b82f6}
@@ -863,8 +868,13 @@ function dashboardHtml(port, whitelist, isStaged = false) {
863
868
  <div class="wizard-foot" id="wizard-foot"></div>
864
869
  </div>
865
870
 
866
- <div id="project-tabs" class="project-tabs" style="display:none"></div>
867
- <div id="grid" class="grid"><p class="loading">Loading services…</p></div>
871
+ <div id="project-tabs" class="project-tabs" style="display:none"></div>
872
+ <div id="service-search" class="service-search">
873
+ <span class="service-search-label">Search</span>
874
+ <input id="service-search-input" class="service-search-input" type="search" placeholder="service name or display name" autocomplete="off" spellcheck="false" oninput="setServiceSearch(this.value)">
875
+ <span id="service-search-count" class="service-search-count"></span>
876
+ </div>
877
+ <div id="grid" class="grid"><p class="loading">Loading services…</p></div>
868
878
  <div class="footer">localhost:${port} · 127.0.0.1 only · 10-strike lockout</div>
869
879
  </div>
870
880
 
@@ -1158,8 +1168,25 @@ async function stopDaemon() {
1158
1168
  }
1159
1169
 
1160
1170
  // ── Load services ───────────────────────────
1161
- let allServices = [];
1162
- let activeProjectTab = "all";
1171
+ let allServices = [];
1172
+ let activeProjectTab = "all";
1173
+ let serviceSearchQuery = "";
1174
+
1175
+ function serviceSort(a, b) {
1176
+ return String(a.name || "").localeCompare(String(b.name || ""), undefined, { sensitivity: "base", numeric: true });
1177
+ }
1178
+
1179
+ function matchesServiceSearch(s, query) {
1180
+ if (!query) return true;
1181
+ const q = query.toLowerCase();
1182
+ return String(s.name || "").toLowerCase().includes(q) ||
1183
+ String(s.label || "").toLowerCase().includes(q);
1184
+ }
1185
+
1186
+ function setServiceSearch(value) {
1187
+ serviceSearchQuery = value || "";
1188
+ renderServiceGrid(allServices);
1189
+ }
1163
1190
 
1164
1191
  function renderProjectTabs(services) {
1165
1192
  const tabsEl = document.getElementById("project-tabs");
@@ -1185,21 +1212,35 @@ function renderProjectTabs(services) {
1185
1212
  ).join("");
1186
1213
  }
1187
1214
 
1188
- function switchProjectTab(key) {
1189
- activeProjectTab = key;
1190
- renderProjectTabs(allServices);
1191
- renderServiceGrid(allServices);
1192
- }
1215
+ function switchProjectTab(key) {
1216
+ activeProjectTab = key;
1217
+ renderProjectTabs(allServices);
1218
+ renderServiceGrid(allServices);
1219
+ }
1193
1220
 
1194
1221
  function renderServiceGrid(services) {
1195
1222
  const grid = document.getElementById("grid");
1196
1223
  let filtered = services;
1197
1224
  if (activeProjectTab === "unassigned") {
1198
1225
  filtered = services.filter(s => !s.project);
1199
- } else if (activeProjectTab !== "all") {
1200
- filtered = services.filter(s => s.project === activeProjectTab);
1201
- }
1202
- if (!filtered.length) { grid.innerHTML = '<p class="loading">No services in this group.</p>'; return; }
1226
+ } else if (activeProjectTab !== "all") {
1227
+ filtered = services.filter(s => s.project === activeProjectTab);
1228
+ }
1229
+ filtered = filtered
1230
+ .filter(s => matchesServiceSearch(s, serviceSearchQuery))
1231
+ .slice()
1232
+ .sort(serviceSort);
1233
+ const searchCount = document.getElementById("service-search-count");
1234
+ if (searchCount) {
1235
+ const trimmed = serviceSearchQuery.trim();
1236
+ searchCount.textContent = trimmed ? filtered.length + " match" + (filtered.length === 1 ? "" : "es") : filtered.length + " shown";
1237
+ }
1238
+ if (!filtered.length) {
1239
+ grid.innerHTML = serviceSearchQuery.trim()
1240
+ ? '<p class="loading">No services match that search.</p>'
1241
+ : '<p class="loading">No services in this group.</p>';
1242
+ return;
1243
+ }
1203
1244
  grid.innerHTML = filtered.map(s => \`
1204
1245
  <div class="card">
1205
1246
  <div style="display:flex;align-items:flex-start;justify-content:space-between">
@@ -5076,8 +5117,8 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5076
5117
  }
5077
5118
 
5078
5119
  // POST /update-service — update service metadata (project, label, description)
5079
- if (method === "POST" && reqPath === "/update-service") {
5080
- if (lockedGuard(res)) return;
5120
+ if (method === "POST" && reqPath === "/update-service") {
5121
+ if (lockedGuard(res)) return;
5081
5122
 
5082
5123
  let body;
5083
5124
  try { body = await readBody(req); } catch {
@@ -5108,10 +5149,39 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
5108
5149
  return ok(res, { ok: true, service: service.toLowerCase(), ...updates });
5109
5150
  } catch (err) {
5110
5151
  return strike(res, 502, err.message);
5111
- }
5112
- }
5113
-
5114
- // Unknown route a wrong URL is not an auth failure. Log it, return 404,
5152
+ }
5153
+ }
5154
+
5155
+ if (method === "GET" && reqPath === "/search") {
5156
+ if (lockedGuard(res)) return;
5157
+ const query = (url.searchParams.get("q") || url.searchParams.get("query") || "").trim();
5158
+ const project = (url.searchParams.get("project") || "").trim() || undefined;
5159
+ const includeAddresses = url.searchParams.get("addresses") !== "false";
5160
+ if (!query) {
5161
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
5162
+ return res.end(JSON.stringify({ error: "q query parameter is required" }));
5163
+ }
5164
+
5165
+ try {
5166
+ const { token, timestamp } = deriveToken(password, machineHash);
5167
+ const result = await searchServices({
5168
+ password,
5169
+ machineHash,
5170
+ token,
5171
+ timestamp,
5172
+ project,
5173
+ query,
5174
+ includeAddresses,
5175
+ whitelist
5176
+ });
5177
+ if (result.error) return strike(res, 502, result.error);
5178
+ return ok(res, result);
5179
+ } catch (err) {
5180
+ return strike(res, 502, err.message);
5181
+ }
5182
+ }
5183
+
5184
+ // Unknown route — a wrong URL is not an auth failure. Log it, return 404,
5115
5185
  // but do NOT increment failCount (which locks the vault at MAX_FAILS).
5116
5186
  // Auth failures (wrong password, wrong token) still strike via /auth and /get/:service.
5117
5187
  try {
@@ -6086,9 +6156,9 @@ function stopCodevelopSession(session_id) {
6086
6156
  return { stopped: true, session_id };
6087
6157
  }
6088
6158
 
6089
- const ENV_MAP = {
6090
- "github": "GITHUB_TOKEN",
6091
- "supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
6159
+ const ENV_MAP = {
6160
+ "github": "GITHUB_TOKEN",
6161
+ "supabase-anon": "NEXT_PUBLIC_SUPABASE_ANON_KEY",
6092
6162
  "supabase-service": "SUPABASE_SERVICE_ROLE_KEY",
6093
6163
  "supabase-db": "SUPABASE_DB_URL",
6094
6164
  "vercel": "VERCEL_TOKEN",
@@ -6100,11 +6170,116 @@ const ENV_MAP = {
6100
6170
  "rocketreach": "ROCKETREACH_API_KEY",
6101
6171
  "npm": "NPM_TOKEN",
6102
6172
  "namecheap": "NAMECHEAP_API_KEY",
6103
- "gmail": "GMAIL_CREDENTIALS",
6104
- };
6105
-
6106
- // ── Filesystem service config loaded from clauth vault ──
6107
- let _fsMountsCache = null;
6173
+ "gmail": "GMAIL_CREDENTIALS",
6174
+ };
6175
+
6176
+ const ADDRESS_KEY_TYPES = new Set(["connstring", "fileserver", "oauth"]);
6177
+ const ADDRESS_FIELDS = new Set(["url", "uri", "host", "hostname", "server", "address", "base_url", "endpoint", "path", "root"]);
6178
+
6179
+ function normalizeSearchText(value) {
6180
+ return String(value || "").toLowerCase();
6181
+ }
6182
+
6183
+ function redactUrlish(value) {
6184
+ const text = String(value || "").trim();
6185
+ if (!text) return "";
6186
+ try {
6187
+ const url = new URL(text);
6188
+ if (url.username) url.username = "***";
6189
+ if (url.password) url.password = "***";
6190
+ return url.toString();
6191
+ } catch {
6192
+ return text.replace(/:\/\/([^:@/\s]+):([^@/\s]+)@/g, "://***:***@");
6193
+ }
6194
+ }
6195
+
6196
+ function collectAddressHints(value, keyType) {
6197
+ if (!ADDRESS_KEY_TYPES.has(String(keyType || "").toLowerCase())) return [];
6198
+ const hints = new Set();
6199
+
6200
+ function add(candidate) {
6201
+ if (candidate === undefined || candidate === null) return;
6202
+ const text = redactUrlish(candidate);
6203
+ if (text) hints.add(text);
6204
+ }
6205
+
6206
+ function walk(node, fieldName = "") {
6207
+ if (node === undefined || node === null) return;
6208
+ if (typeof node === "string") {
6209
+ if (fieldName && ADDRESS_FIELDS.has(fieldName.toLowerCase())) add(node);
6210
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(node) || /^[A-Za-z]:[\\/]/.test(node) || node.startsWith("\\\\")) add(node);
6211
+ return;
6212
+ }
6213
+ if (Array.isArray(node)) {
6214
+ for (const item of node) walk(item, fieldName);
6215
+ return;
6216
+ }
6217
+ if (typeof node === "object") {
6218
+ for (const [key, child] of Object.entries(node)) walk(child, key);
6219
+ }
6220
+ }
6221
+
6222
+ try {
6223
+ walk(JSON.parse(value));
6224
+ } catch {
6225
+ walk(value);
6226
+ }
6227
+
6228
+ return [...hints];
6229
+ }
6230
+
6231
+ async function searchServices({ password, machineHash, token, timestamp, query, project, includeAddresses = true, whitelist }) {
6232
+ const q = normalizeSearchText(query);
6233
+ if (!q) return { error: "query is required" };
6234
+
6235
+ const result = await api.status(password, machineHash, token, timestamp, project);
6236
+ if (result.error) return { error: result.error };
6237
+
6238
+ let services = result.services || [];
6239
+ if (whitelist) services = services.filter(s => whitelist.includes(String(s.name || "").toLowerCase()));
6240
+
6241
+ const matches = [];
6242
+ for (const s of services) {
6243
+ const fields = {
6244
+ name: s.name,
6245
+ label: s.label,
6246
+ project: s.project,
6247
+ type: s.key_type,
6248
+ description: s.description
6249
+ };
6250
+ const matched = Object.entries(fields)
6251
+ .filter(([, value]) => normalizeSearchText(value).includes(q))
6252
+ .map(([field]) => field);
6253
+
6254
+ let addressHints = [];
6255
+ if (includeAddresses && ADDRESS_KEY_TYPES.has(String(s.key_type || "").toLowerCase()) && s.vault_key) {
6256
+ const secret = await api.retrieve(password, machineHash, token, timestamp, s.name);
6257
+ if (!secret.error) {
6258
+ addressHints = collectAddressHints(secret.value, s.key_type);
6259
+ if (addressHints.some(h => normalizeSearchText(h).includes(q))) matched.push("address");
6260
+ }
6261
+ }
6262
+
6263
+ if (matched.length) {
6264
+ matches.push({
6265
+ name: s.name,
6266
+ label: s.label || null,
6267
+ project: s.project || null,
6268
+ key_type: s.key_type,
6269
+ enabled: !!s.enabled,
6270
+ has_key: !!s.vault_key,
6271
+ description: s.description || null,
6272
+ matched: [...new Set(matched)],
6273
+ address_hints: addressHints
6274
+ });
6275
+ }
6276
+ }
6277
+
6278
+ return { query, count: matches.length, matches };
6279
+ }
6280
+
6281
+ // ── Filesystem service config — loaded from clauth vault ──
6282
+ let _fsMountsCache = null;
6108
6283
  let _fsMountsCacheTime = 0;
6109
6284
  const FS_CACHE_TTL = 60000; // 1 minute
6110
6285
 
@@ -6269,14 +6444,28 @@ const MCP_TOOLS = [
6269
6444
  description: "List all services with type, enabled state, key presence, and last retrieval time",
6270
6445
  inputSchema: { type: "object", properties: {}, additionalProperties: false }
6271
6446
  },
6272
- {
6273
- name: "clauth_list",
6274
- description: "List registered service names",
6275
- inputSchema: { type: "object", properties: {}, additionalProperties: false }
6276
- },
6277
- {
6278
- name: "clauth_get",
6279
- description: "Retrieve a secret and deliver to a temp file (default), clipboard, or stdout. Temp files auto-delete after 30 seconds.",
6447
+ {
6448
+ name: "clauth_list",
6449
+ description: "List registered service names",
6450
+ inputSchema: { type: "object", properties: {}, additionalProperties: false }
6451
+ },
6452
+ {
6453
+ name: "clauth_search",
6454
+ description: "Search registered services by name, label, project, description, type, or redacted address hints from address-bearing secrets",
6455
+ inputSchema: {
6456
+ type: "object",
6457
+ properties: {
6458
+ query: { type: "string", description: "Search text, such as part of a service name, project, host, URL, or filesystem path" },
6459
+ project: { type: "string", description: "Optional project scope" },
6460
+ addresses: { type: "boolean", default: true, description: "Include redacted address hints from connstring/fileserver/oauth secrets" }
6461
+ },
6462
+ required: ["query"],
6463
+ additionalProperties: false
6464
+ }
6465
+ },
6466
+ {
6467
+ name: "clauth_get",
6468
+ description: "Retrieve a secret and deliver to a temp file (default), clipboard, or stdout. Temp files auto-delete after 30 seconds.",
6280
6469
  inputSchema: {
6281
6470
  type: "object",
6282
6471
  properties: {
@@ -6993,10 +7182,10 @@ async function handleMcpTool(vault, name, args) {
6993
7182
  }
6994
7183
  }
6995
7184
 
6996
- case "clauth_list": {
6997
- if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
6998
- try {
6999
- const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
7185
+ case "clauth_list": {
7186
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
7187
+ try {
7188
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
7000
7189
  const result = await api.status(vault.password, vault.machineHash, token, timestamp);
7001
7190
  if (result.error) return mcpError(result.error);
7002
7191
  let services = result.services || [];
@@ -7005,13 +7194,34 @@ async function handleMcpTool(vault, name, args) {
7005
7194
  }
7006
7195
  return mcpResult(services.map(s => s.name).join(", "));
7007
7196
  } catch (err) {
7008
- return mcpError(err.message);
7009
- }
7010
- }
7011
-
7012
- case "clauth_get": {
7013
- if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
7014
- const service = (args.service || "").toLowerCase();
7197
+ return mcpError(err.message);
7198
+ }
7199
+ }
7200
+
7201
+ case "clauth_search": {
7202
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
7203
+ try {
7204
+ const { token, timestamp } = deriveToken(vault.password, vault.machineHash);
7205
+ const result = await searchServices({
7206
+ password: vault.password,
7207
+ machineHash: vault.machineHash,
7208
+ token,
7209
+ timestamp,
7210
+ query: args.query,
7211
+ project: args.project,
7212
+ includeAddresses: args.addresses !== false,
7213
+ whitelist: vault.whitelist
7214
+ });
7215
+ if (result.error) return mcpError(result.error);
7216
+ return mcpResult(JSON.stringify(result, null, 2));
7217
+ } catch (err) {
7218
+ return mcpError(err.message);
7219
+ }
7220
+ }
7221
+
7222
+ case "clauth_get": {
7223
+ if (!vault.password) return mcpError("Vault is locked — call clauth_unlock first");
7224
+ const service = (args.service || "").toLowerCase();
7015
7225
  const target = args.target || "file";
7016
7226
  if (!service) return mcpError("service is required");
7017
7227
  if (vault.whitelist && !vault.whitelist.includes(service)) {
package/cli/index.js CHANGED
@@ -40,6 +40,97 @@ async function getAuth(pw) {
40
40
  return { password, machineHash, token, timestamp };
41
41
  }
42
42
 
43
+ const ADDRESS_KEY_TYPES = new Set(["connstring", "fileserver", "oauth"]);
44
+ const ADDRESS_FIELDS = new Set(["url", "uri", "host", "hostname", "server", "address", "base_url", "endpoint", "path", "root"]);
45
+
46
+ function normalizeSearchText(value) {
47
+ return String(value || "").toLowerCase();
48
+ }
49
+
50
+ function redactUrlish(value) {
51
+ const text = String(value || "").trim();
52
+ if (!text) return "";
53
+ try {
54
+ const url = new URL(text);
55
+ if (url.username) url.username = "***";
56
+ if (url.password) url.password = "***";
57
+ return url.toString();
58
+ } catch {
59
+ return text.replace(/:\/\/([^:@/\s]+):([^@/\s]+)@/g, "://***:***@");
60
+ }
61
+ }
62
+
63
+ function collectAddressHints(value, keyType) {
64
+ if (!ADDRESS_KEY_TYPES.has(String(keyType || "").toLowerCase())) return [];
65
+ const hints = new Set();
66
+
67
+ function add(candidate) {
68
+ if (candidate === undefined || candidate === null) return;
69
+ const text = redactUrlish(candidate);
70
+ if (text) hints.add(text);
71
+ }
72
+
73
+ function walk(node, fieldName = "") {
74
+ if (node === undefined || node === null) return;
75
+ if (typeof node === "string") {
76
+ if (fieldName && ADDRESS_FIELDS.has(fieldName.toLowerCase())) add(node);
77
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(node) || /^[A-Za-z]:[\\/]/.test(node) || node.startsWith("\\\\")) add(node);
78
+ return;
79
+ }
80
+ if (Array.isArray(node)) {
81
+ for (const item of node) walk(item, fieldName);
82
+ return;
83
+ }
84
+ if (typeof node === "object") {
85
+ for (const [key, child] of Object.entries(node)) walk(child, key);
86
+ }
87
+ }
88
+
89
+ try {
90
+ walk(JSON.parse(value));
91
+ } catch {
92
+ walk(value);
93
+ }
94
+
95
+ return [...hints];
96
+ }
97
+
98
+ async function searchServices(auth, query, opts = {}) {
99
+ const q = normalizeSearchText(query);
100
+ if (!q) throw new Error("Search query is required");
101
+ const result = await api.status(auth.password, auth.machineHash, auth.token, auth.timestamp, opts.project);
102
+ if (result.error) throw new Error(result.error);
103
+
104
+ const services = result.services || [];
105
+ const rows = [];
106
+
107
+ for (const s of services) {
108
+ const fields = {
109
+ name: s.name,
110
+ label: s.label,
111
+ project: s.project,
112
+ type: s.key_type,
113
+ description: s.description
114
+ };
115
+ const matched = Object.entries(fields)
116
+ .filter(([, value]) => normalizeSearchText(value).includes(q))
117
+ .map(([field]) => field);
118
+
119
+ let addressHints = [];
120
+ if (opts.addresses !== false && ADDRESS_KEY_TYPES.has(String(s.key_type || "").toLowerCase()) && s.vault_key) {
121
+ const secret = await api.retrieve(auth.password, auth.machineHash, auth.token, auth.timestamp, s.name);
122
+ if (!secret.error) {
123
+ addressHints = collectAddressHints(secret.value, s.key_type);
124
+ if (addressHints.some(h => normalizeSearchText(h).includes(q))) matched.push("address");
125
+ }
126
+ }
127
+
128
+ if (matched.length) rows.push({ ...s, matched: [...new Set(matched)], addressHints });
129
+ }
130
+
131
+ return rows;
132
+ }
133
+
43
134
  // ============================================================
44
135
  // Program
45
136
  // ============================================================
@@ -58,6 +149,7 @@ import { runUninstall } from './commands/uninstall.js';
58
149
  import { runScrub } from './commands/scrub.js';
59
150
  import { runServe } from './commands/serve.js';
60
151
  import { runCodevelop } from './commands/codevelop.js';
152
+ import { runNpm } from './commands/npm.js';
61
153
 
62
154
  program
63
155
  .command('install')
@@ -126,6 +218,16 @@ program
126
218
  await runCodevelop({ ...opts, action });
127
219
  });
128
220
 
221
+ program
222
+ .command("npm")
223
+ .description("Operate npm auth safely through the clauth npm service")
224
+ .argument("[action]", "whoami | tokens | set-local | sync-github-secret | rerun | help", "help")
225
+ .argument("[args...]", "Action arguments")
226
+ .option("--repo <repo>", "GitHub repo, e.g. LIFEAI/rdc-skills")
227
+ .action(async (action, args, opts) => {
228
+ await runNpm(action, { ...opts, args });
229
+ });
230
+
129
231
  // ──────────────────────────────────────────────
130
232
  // clauth setup
131
233
  // ──────────────────────────────────────────────
@@ -410,6 +512,38 @@ program
410
512
  console.log();
411
513
  });
412
514
 
515
+ program
516
+ .command("search <query>")
517
+ .description("Search services by name, label, project, description, type, or redacted address hints")
518
+ .option("-p, --pw <password>")
519
+ .option("--project <name>", "Filter by project scope")
520
+ .option("--no-addresses", "Skip address-bearing secret metadata scans")
521
+ .action(async (query, opts) => {
522
+ const auth = await getAuth(opts.pw);
523
+ const spinner = ora("Searching services...").start();
524
+ try {
525
+ const rows = await searchServices(auth, query, { project: opts.project, addresses: opts.addresses });
526
+ spinner.stop();
527
+ console.log(chalk.cyan(`\n Search results for "${query}":\n`));
528
+ if (!rows.length) {
529
+ console.log(chalk.gray(" No matching services found.\n"));
530
+ return;
531
+ }
532
+ console.log(chalk.bold(" " + "SERVICE".padEnd(24) + "TYPE".padEnd(12) + "PROJECT".padEnd(20) + "MATCHED"));
533
+ console.log(" " + "─".repeat(78));
534
+ for (const s of rows) {
535
+ const project = s.project || "global";
536
+ console.log(` ${chalk.bold(s.name.padEnd(24))}${String(s.key_type || "").padEnd(12)}${project.padEnd(20)}${s.matched.join(", ")}`);
537
+ if (s.label) console.log(chalk.gray(` label: ${s.label}`));
538
+ if (s.description) console.log(chalk.gray(` description: ${s.description}`));
539
+ for (const hint of s.addressHints || []) console.log(chalk.gray(` address: ${hint}`));
540
+ }
541
+ console.log();
542
+ } catch (err) {
543
+ spinner.fail(chalk.red(err.message));
544
+ }
545
+ });
546
+
413
547
  // ──────────────────────────────────────────────
414
548
  // clauth test <service|all>
415
549
  // ──────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.5.82",
3
+ "version": "1.5.84",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {