@lifeaitools/clauth 1.5.4 → 1.5.6

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 (2) hide show
  1. package/cli/commands/serve.js +212 -25
  2. package/package.json +1 -1
@@ -690,6 +690,7 @@ function dashboardHtml(port, whitelist, isStaged = false) {
690
690
  <div class="lock-title">clauth vault</div>
691
691
  <div class="lock-sub" id="lock-sub">Paste your password to unlock</div>
692
692
  <form onsubmit="unlock();return false;" autocomplete="on">
693
+ <input type="text" name="username" value="clauth" autocomplete="username" style="display:none">
693
694
  <input class="lock-input" id="lock-input" type="password" placeholder="••••••••••••" autocomplete="current-password">
694
695
  <button class="btn-unlock" id="unlock-btn" type="submit">Unlock</button>
695
696
  </form>
@@ -746,8 +747,12 @@ function dashboardHtml(port, whitelist, isStaged = false) {
746
747
  <h3>Add New Service</h3>
747
748
  <div class="add-row">
748
749
  <div class="add-field">
749
- <label>Service name</label>
750
- <input class="add-input" id="add-name" type="text" placeholder="e.g. coolify-admin" autocomplete="off" spellcheck="false">
750
+ <label>Slug <span style="color:#475569;font-weight:400">(used in <code style="font-size:.72rem">clauth get &lt;slug&gt;</code>)</span></label>
751
+ <input class="add-input" id="add-name" type="text" placeholder="e.g. coolify-admin" autocomplete="off" spellcheck="false" style="font-family:'Courier New',monospace">
752
+ </div>
753
+ <div class="add-field">
754
+ <label>Label <span style="color:#475569;font-weight:400">(display name)</span></label>
755
+ <input class="add-input" id="add-label" type="text" placeholder="e.g. Coolify Admin Token" autocomplete="off" spellcheck="false">
751
756
  </div>
752
757
  <div class="add-field">
753
758
  <label>Key type</label>
@@ -755,6 +760,12 @@ function dashboardHtml(port, whitelist, isStaged = false) {
755
760
  <option value="">Loading…</option>
756
761
  </select>
757
762
  </div>
763
+ </div>
764
+ <div class="add-row">
765
+ <div class="add-field">
766
+ <label>Description <span style="color:#475569;font-weight:400">(optional)</span></label>
767
+ <input class="add-input" id="add-description" type="text" placeholder="e.g. API token for Coolify deployments" autocomplete="off" spellcheck="false" style="width:420px">
768
+ </div>
758
769
  <div class="add-field">
759
770
  <label>Project <span style="color:#475569;font-weight:400">(optional)</span></label>
760
771
  <input class="add-input" id="add-project" type="text" placeholder="e.g. marketing-engine" autocomplete="off" spellcheck="false">
@@ -770,6 +781,7 @@ function dashboardHtml(port, whitelist, isStaged = false) {
770
781
  <div class="chpw-panel" id="chpw-panel">
771
782
  <h3>Change Master Password</h3>
772
783
  <form onsubmit="changePassword();return false;" autocomplete="on">
784
+ <input type="text" name="username" value="clauth" autocomplete="username" style="display:none">
773
785
  <div class="chpw-row">
774
786
  <div class="chpw-field">
775
787
  <label>New password</label>
@@ -1174,14 +1186,18 @@ function renderServiceGrid(services) {
1174
1186
  grid.innerHTML = filtered.map(s => \`
1175
1187
  <div class="card">
1176
1188
  <div style="display:flex;align-items:flex-start;justify-content:space-between">
1177
- <div>
1178
- <div class="card-name">\${s.name}</div>
1189
+ <div style="flex:1;min-width:0">
1190
+ <div style="display:flex;align-items:baseline;gap:8px;flex-wrap:wrap">
1191
+ <span class="card-name" id="label-display-\${s.name}">\${s.label && s.label !== s.name ? s.label : s.name}</span>
1192
+ <code style="font-family:'Courier New',monospace;font-size:.75rem;color:#64748b;background:rgba(100,116,139,.1);padding:1px 6px;border-radius:3px;letter-spacing:.3px">\${s.name}</code>
1193
+ </div>
1179
1194
  <div style="display:flex;align-items:center;gap:6px;margin-top:2px">
1180
1195
  <div class="card-type">\${s.key_type || "secret"}</div>
1181
1196
  <span class="svc-badge \${s.enabled === false ? "off" : "on"}" id="badge-\${s.name}">\${s.enabled === false ? "disabled" : "enabled"}</span>
1182
1197
  <span class="expiry-badge" id="expiry-\${s.name}" style="font-size:.65rem;border-radius:3px;padding:1px 6px;display:none"></span>
1183
1198
  \${s.project ? \`<span style="font-size:.68rem;color:#3b82f6;background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.2);border-radius:3px;padding:1px 6px">\${s.project}</span>\` : ""}
1184
1199
  </div>
1200
+ \${s.description ? \`<div style="font-size:.78rem;color:#64748b;margin-top:4px;line-height:1.3">\${s.description}</div>\` : ""}
1185
1201
  \${KEY_URLS[s.name] ? \`<a class="card-getkey" href="\${KEY_URLS[s.name]}" target="_blank" rel="noopener">↗ Get / rotate key</a>\` : ""}
1186
1202
  \${(EXTRA_LINKS[s.name] || []).map(l => \`<a class="card-getkey" href="\${l.url}" target="_blank" rel="noopener" style="margin-left:0">\${l.label}</a>\`).join("")}
1187
1203
  </div>
@@ -1195,14 +1211,22 @@ function renderServiceGrid(services) {
1195
1211
  <button class="btn-project" onclick="toggleProjectEdit('\${s.name}')">\${s.project ? "✎ Project" : "+ Project"}</button>
1196
1212
  <button class="btn \${s.enabled === false ? "btn-enable" : "btn-disable"}" id="togbtn-\${s.name}" onclick="toggleService('\${s.name}')">\${s.enabled === false ? "Enable" : "Disable"}</button>
1197
1213
  <button class="btn-rotate" id="rotbtn-\${s.name}" style="display:none;background:#0e7490;border:1px solid #06b6d4;color:#cffafe;font-size:.75rem;padding:3px 8px;border-radius:4px;cursor:pointer" onclick="rotateKey('\${s.name}')">↻ Rotate</button>
1198
- <button class="btn-rename" onclick="toggleRename('\${s.name}')" title="Rename service">✎</button>
1214
+ <button class="btn-rename" onclick="toggleLabelEdit('\${s.name}')" title="Edit display label">✏️</button>
1199
1215
  <button class="btn-delete" onclick="deleteService('\${s.name}')" title="Delete service">✕</button>
1200
1216
  </div>
1201
1217
  <div class="rename-panel" id="rn-\${s.name}">
1202
- <input class="rename-input" id="rn-input-\${s.name}" value="\${s.name}" spellcheck="false" autocomplete="off" placeholder="New name…">
1203
- <button class="btn" onclick="saveRename('\${s.name}')" style="padding:4px 10px;font-size:.8rem">Save</button>
1204
- <button class="btn" onclick="toggleRename('\${s.name}')" style="padding:4px 10px;font-size:.8rem;background:#1e293b">Cancel</button>
1205
- <span class="rename-msg" id="rn-msg-\${s.name}"></span>
1218
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
1219
+ <span style="font-size:.72rem;color:#64748b">Edit label for</span>
1220
+ <code style="font-family:'Courier New',monospace;font-size:.75rem;color:#94a3b8;background:rgba(100,116,139,.1);padding:1px 5px;border-radius:3px">\${s.name}</code>
1221
+ <span style="font-size:.68rem;color:#475569">(slug unchanged)</span>
1222
+ </div>
1223
+ <div style="display:flex;gap:6px;align-items:center">
1224
+ <input class="rename-input" id="rn-input-\${s.name}" value="\${s.label || s.name}" spellcheck="false" autocomplete="off" placeholder="Human-readable label…"
1225
+ onkeydown="if(event.key==='Enter')saveLabel('\${s.name}');if(event.key==='Escape')toggleLabelEdit('\${s.name}')">
1226
+ <button class="btn" onclick="saveLabel('\${s.name}')" style="padding:4px 10px;font-size:.8rem">Save</button>
1227
+ <button class="btn" onclick="toggleLabelEdit('\${s.name}')" style="padding:4px 10px;font-size:.8rem;background:#1e293b">Cancel</button>
1228
+ <span class="rename-msg" id="rn-msg-\${s.name}"></span>
1229
+ </div>
1206
1230
  </div>
1207
1231
  <div class="project-edit" id="pe-\${s.name}">
1208
1232
  <input type="text" id="pe-input-\${s.name}" value="\${s.project || ""}" placeholder="Project name…" spellcheck="false" autocomplete="off">
@@ -1426,39 +1450,50 @@ async function clearProject(name) {
1426
1450
  }
1427
1451
 
1428
1452
  // ── Rename service ───────────────────────────
1429
- function toggleRename(name) {
1453
+ function toggleLabelEdit(name) {
1430
1454
  const panel = document.getElementById("rn-" + name);
1431
1455
  const open = panel.style.display === "block";
1432
1456
  panel.style.display = open ? "none" : "block";
1433
1457
  if (!open) {
1434
1458
  const inp = document.getElementById("rn-input-" + name);
1435
- inp.value = name;
1459
+ const svc = allServices.find(s => s.name === name);
1460
+ inp.value = (svc && svc.label) ? svc.label : name;
1436
1461
  inp.focus(); inp.select();
1437
1462
  document.getElementById("rn-msg-" + name).textContent = "";
1438
1463
  }
1439
1464
  }
1440
1465
 
1441
- async function saveRename(oldName) {
1442
- const inp = document.getElementById("rn-input-" + oldName);
1443
- const msg = document.getElementById("rn-msg-" + oldName);
1444
- const newName = inp.value.trim();
1445
- if (!newName || newName === oldName) { toggleRename(oldName); return; }
1466
+ async function saveLabel(name) {
1467
+ const inp = document.getElementById("rn-input-" + name);
1468
+ const msg = document.getElementById("rn-msg-" + name);
1469
+ const newLabel = inp.value.trim();
1470
+ if (!newLabel) { toggleLabelEdit(name); return; }
1471
+ const svc = allServices.find(s => s.name === name);
1472
+ if (svc && newLabel === svc.label) { toggleLabelEdit(name); return; }
1446
1473
  msg.style.color = "#94a3b8"; msg.textContent = "Saving…";
1447
1474
  try {
1448
- const r = await fetch(BASE + "/rename/" + oldName, {
1475
+ const r = await fetch(BASE + "/update-service", {
1449
1476
  method: "POST",
1450
1477
  headers: { "Content-Type": "application/json" },
1451
- body: JSON.stringify({ new_name: newName })
1478
+ body: JSON.stringify({ service: name, label: newLabel })
1452
1479
  }).then(r => r.json());
1453
1480
  if (r.locked) { showLockScreen(false); return; }
1454
1481
  if (r.error) throw new Error(r.error);
1455
- msg.style.color = "#4ade80"; msg.textContent = "✓ Renamed";
1456
- setTimeout(() => loadServices(), 800);
1482
+ msg.style.color = "#4ade80"; msg.textContent = "✓ Label updated";
1483
+ // Update in-memory and DOM immediately
1484
+ if (svc) svc.label = newLabel;
1485
+ const labelEl = document.getElementById("label-display-" + name);
1486
+ if (labelEl) labelEl.textContent = newLabel;
1487
+ setTimeout(() => { toggleLabelEdit(name); }, 800);
1457
1488
  } catch (e) {
1458
1489
  msg.style.color = "#f87171"; msg.textContent = "✗ " + e.message;
1459
1490
  }
1460
1491
  }
1461
1492
 
1493
+ // Legacy rename (slug) — kept for API compatibility but hidden from UI
1494
+ function toggleRename(name) { toggleLabelEdit(name); }
1495
+ async function saveRename(oldName) { await saveLabel(oldName); }
1496
+
1462
1497
  // ── Delete service ───────────────────────────
1463
1498
  async function deleteService(name) {
1464
1499
  if (!confirm(\`Delete service "\${name}"? This cannot be undone.\`)) return;
@@ -1704,6 +1739,8 @@ async function toggleAddService() {
1704
1739
  panel.style.display = open ? "none" : "block";
1705
1740
  if (!open) {
1706
1741
  document.getElementById("add-name").value = "";
1742
+ document.getElementById("add-label").value = "";
1743
+ document.getElementById("add-description").value = "";
1707
1744
  document.getElementById("add-project").value = "";
1708
1745
  document.getElementById("add-msg").textContent = "";
1709
1746
  // Fetch available key types from server
@@ -1726,16 +1763,19 @@ async function toggleAddService() {
1726
1763
 
1727
1764
  async function addService() {
1728
1765
  const name = document.getElementById("add-name").value.trim().toLowerCase();
1766
+ const label = document.getElementById("add-label").value.trim();
1767
+ const description = document.getElementById("add-description").value.trim();
1729
1768
  const type = document.getElementById("add-type").value;
1730
1769
  const project = document.getElementById("add-project").value.trim();
1731
1770
  const msg = document.getElementById("add-msg");
1732
1771
 
1733
- if (!name) { msg.className = "add-msg fail"; msg.textContent = "Service name is required."; return; }
1734
- if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) { msg.className = "add-msg fail"; msg.textContent = "Lowercase letters, numbers, hyphens, underscores only."; return; }
1772
+ if (!name) { msg.className = "add-msg fail"; msg.textContent = "Slug is required."; return; }
1773
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(name)) { msg.className = "add-msg fail"; msg.textContent = "Slug: lowercase letters, numbers, hyphens, underscores only."; return; }
1735
1774
 
1736
1775
  msg.className = "add-msg"; msg.textContent = "Creating…";
1737
1776
  try {
1738
- const payload = { name, key_type: type, label: name };
1777
+ const payload = { name, key_type: type, label: label || name };
1778
+ if (description) payload.description = description;
1739
1779
  if (project) payload.project = project;
1740
1780
  const r = await fetch(BASE + "/add-service", {
1741
1781
  method: "POST",
@@ -1762,8 +1802,9 @@ document.addEventListener("DOMContentLoaded", () => {
1762
1802
  document.getElementById("lock-input").addEventListener("keydown", e => {
1763
1803
  if (e.key === "Enter") unlock();
1764
1804
  });
1765
- document.getElementById("add-name").addEventListener("keydown", e => {
1766
- if (e.key === "Enter") addService();
1805
+ ["add-name", "add-label", "add-description", "add-project"].forEach(id => {
1806
+ const el = document.getElementById(id);
1807
+ if (el) el.addEventListener("keydown", e => { if (e.key === "Enter") addService(); });
1767
1808
  });
1768
1809
  });
1769
1810
 
@@ -4669,6 +4710,88 @@ const MCP_TOOLS = [
4669
4710
  description: "Test whether the clauth MCP connector is reachable via the Cloudflare tunnel. Returns connectivity status and tunnel URL.",
4670
4711
  inputSchema: { type: "object", properties: {}, additionalProperties: false }
4671
4712
  },
4713
+
4714
+ // ── Google Workspace (gws CLI) ──────────────────────────────────────────
4715
+ {
4716
+ name: "gws_run",
4717
+ description: "Run any gws CLI command for Google Workspace (Drive, Gmail, Calendar, Docs, Sheets, Slides, Tasks, People, Chat, etc.).\nCLI pattern: gws <service> <resource> [sub_resource] <method> [--params JSON] [--body JSON]",
4718
+ inputSchema: {
4719
+ type: "object",
4720
+ properties: {
4721
+ service: { type: "string", description: "Google Workspace service: drive, sheets, gmail, calendar, docs, slides, tasks, people, chat, classroom, forms, keep, meet, script" },
4722
+ resource: { type: "string", description: "Resource noun, e.g. 'files', 'users', 'events'" },
4723
+ sub_resource: { type: "string", description: "Optional sub-resource, e.g. 'messages' in 'users messages'" },
4724
+ method: { type: "string", description: "Method verb: list, get, insert, update, delete, send, create" },
4725
+ params: { type: "object", description: "Query/path params passed as --params JSON" },
4726
+ body: { type: "object", description: "Request body passed as --json JSON" }
4727
+ },
4728
+ required: ["service", "resource", "method"],
4729
+ additionalProperties: false
4730
+ }
4731
+ },
4732
+ {
4733
+ name: "gws_gmail_list",
4734
+ description: "List Gmail messages. Supports Gmail search syntax (e.g. 'from:alice subject:meeting is:unread').",
4735
+ inputSchema: {
4736
+ type: "object",
4737
+ properties: {
4738
+ query: { type: "string", description: "Gmail search query" },
4739
+ max_results: { type: "number", description: "Max messages (1-500, default 10)" }
4740
+ },
4741
+ additionalProperties: false
4742
+ }
4743
+ },
4744
+ {
4745
+ name: "gws_gmail_read",
4746
+ description: "Read a single Gmail message by ID. Returns full message with headers and body.",
4747
+ inputSchema: {
4748
+ type: "object",
4749
+ properties: { message_id: { type: "string", description: "Gmail message ID" } },
4750
+ required: ["message_id"],
4751
+ additionalProperties: false
4752
+ }
4753
+ },
4754
+ {
4755
+ name: "gws_gmail_send",
4756
+ description: "Send an email via Gmail.",
4757
+ inputSchema: {
4758
+ type: "object",
4759
+ properties: {
4760
+ to: { type: "string", description: "Recipient email address" },
4761
+ subject: { type: "string", description: "Email subject" },
4762
+ body: { type: "string", description: "Plain-text email body" },
4763
+ from: { type: "string", description: "Sender name/address (optional)" }
4764
+ },
4765
+ required: ["to", "subject", "body"],
4766
+ additionalProperties: false
4767
+ }
4768
+ },
4769
+ {
4770
+ name: "gws_calendar_list",
4771
+ description: "List upcoming Google Calendar events.",
4772
+ inputSchema: {
4773
+ type: "object",
4774
+ properties: {
4775
+ calendar_id: { type: "string", description: "Calendar ID (default: primary)" },
4776
+ max_results: { type: "number", description: "Max events (default 10)" },
4777
+ time_min: { type: "string", description: "Start bound ISO 8601, e.g. '2026-04-10T00:00:00Z'" },
4778
+ time_max: { type: "string", description: "End bound ISO 8601" }
4779
+ },
4780
+ additionalProperties: false
4781
+ }
4782
+ },
4783
+ {
4784
+ name: "gws_drive_list",
4785
+ description: "List files in Google Drive. Supports Drive search query syntax.",
4786
+ inputSchema: {
4787
+ type: "object",
4788
+ properties: {
4789
+ query: { type: "string", description: "Drive search query, e.g. \"name contains 'report'\"" },
4790
+ max_results: { type: "number", description: "Max files (default 10)" }
4791
+ },
4792
+ additionalProperties: false
4793
+ }
4794
+ },
4672
4795
  ];
4673
4796
 
4674
4797
  function writeTempSecret(service, value) {
@@ -4961,6 +5084,70 @@ async function handleMcpTool(vault, name, args) {
4961
5084
  return mcpResult(results.join("\n"));
4962
5085
  }
4963
5086
 
5087
+ // ── Google Workspace (gws CLI) ───────────────────────────────────────
5088
+ case "gws_run": {
5089
+ const { service, resource, sub_resource, method, params, body } = args;
5090
+ const cmdArgs = [service, resource];
5091
+ if (sub_resource) cmdArgs.push(sub_resource);
5092
+ cmdArgs.push(method);
5093
+ if (params) cmdArgs.push("--params", `'${JSON.stringify(params)}'`);
5094
+ if (body) cmdArgs.push("--json", `'${JSON.stringify(body)}'`);
5095
+ try {
5096
+ const raw = execSyncTop(["gws", ...cmdArgs].join(" "), { encoding: "utf8", timeout: 30000, windowsHide: true });
5097
+ try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5098
+ } catch (err) {
5099
+ return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`);
5100
+ }
5101
+ }
5102
+
5103
+ case "gws_gmail_list": {
5104
+ const p = { userId: "me", maxResults: args.max_results ?? 10 };
5105
+ if (args.query) p.q = args.query;
5106
+ try {
5107
+ const raw = execSyncTop(`gws gmail users messages list --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5108
+ try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5109
+ } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5110
+ }
5111
+
5112
+ case "gws_gmail_read": {
5113
+ const p = { userId: "me", id: args.message_id, format: "full" };
5114
+ try {
5115
+ const raw = execSyncTop(`gws gmail users messages get --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5116
+ try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5117
+ } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5118
+ }
5119
+
5120
+ case "gws_gmail_send": {
5121
+ const lines = [];
5122
+ if (args.from) lines.push(`From: ${args.from}`);
5123
+ lines.push(`To: ${args.to}`, `Subject: ${args.subject}`, "Content-Type: text/plain; charset=utf-8", "MIME-Version: 1.0", "", args.body);
5124
+ const encoded = Buffer.from(lines.join("\r\n")).toString("base64url");
5125
+ const bodyObj = { userId: "me", resource: { raw: encoded } };
5126
+ try {
5127
+ const raw = execSyncTop(`gws gmail users messages send --json '${JSON.stringify(bodyObj)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5128
+ try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5129
+ } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5130
+ }
5131
+
5132
+ case "gws_calendar_list": {
5133
+ const p = { calendarId: args.calendar_id ?? "primary", maxResults: args.max_results ?? 10, singleEvents: true, orderBy: "startTime" };
5134
+ if (args.time_min) p.timeMin = args.time_min;
5135
+ if (args.time_max) p.timeMax = args.time_max;
5136
+ try {
5137
+ const raw = execSyncTop(`gws calendar events list --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5138
+ try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5139
+ } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5140
+ }
5141
+
5142
+ case "gws_drive_list": {
5143
+ const p = { pageSize: args.max_results ?? 10, fields: "files(id,name,mimeType,modifiedTime,size,webViewLink)" };
5144
+ if (args.query) p.q = args.query;
5145
+ try {
5146
+ const raw = execSyncTop(`gws drive files list --params '${JSON.stringify(p)}'`, { encoding: "utf8", timeout: 30000, windowsHide: true });
5147
+ try { return mcpResult(JSON.stringify(JSON.parse(raw.trim()), null, 2)); } catch { return mcpResult(raw); }
5148
+ } catch (err) { return mcpError(`gws failed: ${err.stderr || err.stdout || err.message}`); }
5149
+ }
5150
+
4964
5151
  default:
4965
5152
  return mcpError(`Unknown tool: ${name}`);
4966
5153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {