looking-glass-mcp 3.0.1 → 3.1.1

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.
package/README.md CHANGED
@@ -6,7 +6,21 @@
6
6
 
7
7
  <p align="center">
8
8
  <strong>The AI-native browser for agents.</strong><br/>
9
- 72 MCP tools. Self-healing interactions. Semantic change detection. Structured extraction. Credential vault. Enterprise-grade security. Deploy anywhere.
9
+ 71 MCP tools. Self-healing interactions. Semantic change detection. Structured extraction. Credential vault. Enterprise-grade security. Deploy anywhere.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/looking-glass-mcp"><img src="https://img.shields.io/npm/v/looking-glass-mcp?color=blue&label=npm" alt="npm" /></a>
14
+ <a href="https://www.npmjs.com/package/looking-glass-mcp"><img src="https://img.shields.io/npm/dm/looking-glass-mcp?color=orange&label=downloads" alt="downloads" /></a>
15
+ <a href="https://registry.modelcontextprotocol.io/servers/io.github.Sahib-Sawhney-WH/looking-glass-mcp"><img src="https://img.shields.io/badge/MCP_Registry-listed-brightgreen" alt="MCP Registry" /></a>
16
+ <a href="https://github.com/Sahib-Sawhney-WH/LookingGlass/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/Sahib-Sawhney-WH/LookingGlass/ci.yml?label=CI" alt="CI" /></a>
17
+ <a href="https://github.com/Sahib-Sawhney-WH/LookingGlass/blob/main/LICENSE"><img src="https://img.shields.io/github/license/Sahib-Sawhney-WH/LookingGlass?color=purple" alt="MIT License" /></a>
18
+ <a href="https://github.com/Sahib-Sawhney-WH/LookingGlass"><img src="https://img.shields.io/github/stars/Sahib-Sawhney-WH/LookingGlass?style=social" alt="GitHub Stars" /></a>
19
+ <img src="https://img.shields.io/badge/tools-71-ff69b4" alt="71 tools" />
20
+ <img src="https://img.shields.io/badge/tests-79-success" alt="79 tests" />
21
+ <img src="https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white" alt="Node 20+" />
22
+ <img src="https://img.shields.io/badge/Playwright-1.50+-2EAD33?logo=playwright&logoColor=white" alt="Playwright" />
23
+ <img src="https://img.shields.io/badge/Azure-ready-0078D4?logo=microsoftazure&logoColor=white" alt="Azure Ready" />
10
24
  </p>
11
25
 
12
26
  <p align="center">
@@ -30,7 +44,7 @@ Looking Glass v3.0 transforms from a browser automation tool into an **AI-native
30
44
  - **Intent-based navigation** -- `browser_go "settings page"` figures out how to get there
31
45
  - **Semantic waiting** -- `browser_wait_for "search results loaded"` instead of guessing CSS selectors
32
46
  - **Workflow tracking** -- every response includes page type, form progress, breadcrumbs, modal state, and step indicators
33
- - **Credential vault** -- AES-256-GCM encrypted credentials with PBKDF2 key derivation and blind injection (the agent never sees passwords)
47
+ - **Credential vault** -- AES-256-GCM encrypted credentials with PBKDF2 key derivation and blind injection (the agent never sees passwords). Smart field matching works on modern React forms with no `name` attributes
34
48
  - **HTTP transport** -- deploy as a cloud service with `StreamableHTTPServerTransport`
35
49
  - **Docker + Terraform** -- one-command Azure deployment with Key Vault, managed identity, and hardened security
36
50
  - **Enterprise security** -- timing-safe auth, rate limiting, auth lockout, deep audit logging, non-root containers
@@ -42,9 +56,22 @@ Looking Glass v3.0 transforms from a browser automation tool into an **AI-native
42
56
  **Claude Code:**
43
57
 
44
58
  ```bash
59
+ # Basic (headless, uses local-dev profile by default for stdio)
45
60
  claude mcp add looking-glass -- npx looking-glass-mcp
61
+
62
+ # With visible browser window
63
+ claude mcp add looking-glass -e BROWSER_HEADLESS=false -- npx looking-glass-mcp
64
+
65
+ # With all options
66
+ claude mcp add looking-glass \
67
+ -e BROWSER_HEADLESS=false \
68
+ -e BROWSER_SECURITY_PROFILE=local-dev \
69
+ -e VAULT_ENCRYPTION_KEY=your-passphrase-at-least-32-characters \
70
+ -- npx looking-glass-mcp
46
71
  ```
47
72
 
73
+ > **Tip:** After changing env vars, restart Claude Code (new conversation) for the changes to take effect.
74
+
48
75
  **GitHub Copilot / VS Code:**
49
76
 
50
77
  Add to `.vscode/mcp.json`:
@@ -185,16 +212,17 @@ Looking Glass ships with **72 tools** across 10 categories.
185
212
  | `browser_wait_for` | **NEW** Semantic condition waiting ("results loaded") |
186
213
  | `browser_workflow` | **NEW** On-demand workflow context |
187
214
 
188
- ### Credential Vault (5 tools)
215
+ ### Credential Vault (4 tools)
189
216
 
190
217
  | Tool | Description |
191
218
  |------|-------------|
192
- | `vault_store` | Store encrypted credential profile (AES-256-GCM + PBKDF2) |
193
219
  | `vault_list` | List profile names and timestamps (no values) |
194
220
  | `vault_delete` | Delete a credential profile |
195
221
  | `vault_login` | Login using a vault profile (blind injection) |
196
222
  | `vault_inject` | Fill form fields from vault without submitting |
197
223
 
224
+ > Credentials are stored via the CLI (`looking-glass vault store`), not through the agent. See [Credential Vault](#credential-vault) below.
225
+
198
226
  ### Observation & Debugging (10 tools)
199
227
 
200
228
  `browser_screenshot`, `browser_snapshot`, `browser_evaluate`, `browser_console_messages`, `browser_network_requests`, `browser_diagnose`, `browser_error_report`, `browser_snapshot_state` / `browser_diff_state`, `browser_action_history`
@@ -287,12 +315,20 @@ See [SECURITY.md](SECURITY.md) for the full security model.
287
315
 
288
316
  ### Security Profiles
289
317
 
290
- | Profile | URL Access | JS Execution | Tool Access | Rate Limit |
291
- |---------|-----------|--------------|-------------|------------|
292
- | `restricted` (default) | localhost only | Blocked | Observation only | 30/min |
293
- | `local-dev` | All HTTP/HTTPS | Allowed | All tools | 60/min |
294
- | `open` | Everything | Allowed | All tools | 120/min |
295
- | `sandbox` | Blocked | Blocked | Observation only | 10/min |
318
+ The default profile depends on transport mode:
319
+ - **stdio** (local agent): defaults to `local-dev` (full access)
320
+ - **http** (cloud deployment): defaults to `restricted` (observation only)
321
+
322
+ Override with `BROWSER_SECURITY_PROFILE` if needed.
323
+
324
+ | Profile | URL Access | JS Execution | Tool Access | Rate Limit | Use Case |
325
+ |---------|-----------|--------------|-------------|------------|----------|
326
+ | `local-dev` | All HTTP/HTTPS | Allowed | All tools | 60/min | Local development and testing |
327
+ | `restricted` | localhost only | Blocked | Observation only | 30/min | Production monitoring — agent can screenshot, audit, and inspect but cannot click, type, or navigate |
328
+ | `open` | Everything | Allowed | All tools | 120/min | Trusted cloud environments |
329
+ | `sandbox` | Blocked | Blocked | Observation only | 10/min | Maximum lockdown — blocks all URLs by default |
330
+
331
+ > **Note:** If you're using Looking Glass locally via stdio and the agent can't interact with pages, check that your profile is set to `local-dev`. The `restricted` profile is designed for read-only monitoring where the agent should never modify page state.
296
332
 
297
333
  ### HTTP Transport Security
298
334
 
@@ -303,10 +339,56 @@ See [SECURITY.md](SECURITY.md) for the full security model.
303
339
 
304
340
  ### Credential Vault
305
341
 
342
+ Credentials are stored via the CLI — the agent never sees your passwords.
343
+
344
+ **Setup:**
345
+
346
+ 1. Set your vault encryption key (add to your shell profile or MCP config):
347
+
348
+ ```bash
349
+ export VAULT_ENCRYPTION_KEY="your-passphrase-at-least-32-characters-long"
350
+ ```
351
+
352
+ 2. Store a credential profile (interactive — passwords are masked):
353
+
354
+ ```bash
355
+ npx looking-glass-mcp vault store linkedin
356
+ ```
357
+
358
+ You'll be prompted for field name/value pairs:
359
+
360
+ ```
361
+ Storing credential profile: linkedin
362
+ Field name (empty to finish): email
363
+ Value for "email": user@example.com
364
+ Field name (empty to finish): password
365
+ Value for "password": ********
366
+ Field name (empty to finish):
367
+ ✓ Stored profile "linkedin" with 2 fields
368
+ ```
369
+
370
+ 3. Use the profile in your agent conversation:
371
+
372
+ ```
373
+ "Log in to LinkedIn with vault profile linkedin"
374
+ ```
375
+
376
+ The agent calls `vault_login` which decrypts and injects credentials directly into the page — the agent never sees the plaintext values.
377
+
378
+ **CLI commands:**
379
+
380
+ | Command | Description |
381
+ |---------|-------------|
382
+ | `npx looking-glass-mcp vault store <profile>` | Store credentials interactively (masked input) |
383
+ | `npx looking-glass-mcp vault list` | List stored profiles and timestamps |
384
+ | `npx looking-glass-mcp vault delete <profile>` | Delete a stored profile |
385
+
386
+ **Security properties:**
387
+
306
388
  - AES-256-GCM encryption with PBKDF2 key derivation (100k iterations, SHA-512)
307
389
  - Field names and values encrypted together (no metadata leakage)
308
390
  - Vault file permissions restricted to owner (`0600`)
309
- - Agent never sees plaintext credentials -- blind injection only
391
+ - Credential storage happens exclusively through the CLI never through the agent channel
310
392
  - `createdAt` and `lastUsedAt` tracking per profile
311
393
 
312
394
  ### Audit Trail
@@ -422,12 +504,12 @@ MCP Client (Claude, Copilot, etc.)
422
504
  | - Audit Logging (deep redaction) |
423
505
  | - RBAC Policy Enforcement |
424
506
  | |
425
- | 72 Tools |
507
+ | 71 Tools |
426
508
  | - Intelligence (extract, go, wait_for) |
427
509
  | - Interaction (click, type, hover) |
428
510
  | - Observation (screenshot, snapshot) |
429
511
  | - Testing (scenarios, assertions) |
430
- | - Vault (store, inject, login) |
512
+ | - Vault (inject, login, list, delete) |
431
513
  | |
432
514
  | Browser Manager |
433
515
  | - Playwright (Chromium/Firefox/WebKit) |
@@ -1,2 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import '../build/index.js';
2
+
3
+ // Route "vault" subcommand to the vault CLI
4
+ if (process.argv[2] === 'vault') {
5
+ await import('../build/vault-cli.js');
6
+ } else {
7
+ await import('../build/index.js');
8
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../build/vault-cli.js';
package/build/index.js CHANGED
@@ -22641,7 +22641,9 @@ var SECURITY_PROFILES = {
22641
22641
  }
22642
22642
  };
22643
22643
  function getConfig() {
22644
- const profile = process.env.BROWSER_SECURITY_PROFILE || "restricted";
22644
+ const transport = process.env.MCP_TRANSPORT || "stdio";
22645
+ const defaultProfile = transport === "http" ? "restricted" : "local-dev";
22646
+ const profile = process.env.BROWSER_SECURITY_PROFILE || defaultProfile;
22645
22647
  return {
22646
22648
  headless: process.env.BROWSER_HEADLESS === "true",
22647
22649
  sessionsDir: resolve(process.env.BROWSER_SESSIONS_DIR || ".browser-sessions"),
@@ -23891,6 +23893,7 @@ function formatJournal(entries) {
23891
23893
  // src/middleware/toolWrapper.ts
23892
23894
  init_manager();
23893
23895
  var _middlewares = [];
23896
+ var _hasPreSnapshot = false;
23894
23897
  var _captureSnapshot = null;
23895
23898
  var _detectChanges = null;
23896
23899
  var _captureWorkflow = null;
@@ -23930,7 +23933,7 @@ function wrapToolRegistration(server) {
23930
23933
  const errorCountBefore = getConsoleMessages().filter((m) => m.type === "error").length;
23931
23934
  const failureCountBefore = getNetworkRequests().filter((r) => r.status && r.status >= 400).length;
23932
23935
  let preSnapshot = null;
23933
- if (isInteraction && _captureSnapshot) {
23936
+ if (isInteraction && _captureSnapshot && _hasPreSnapshot) {
23934
23937
  try {
23935
23938
  preSnapshot = await _captureSnapshot();
23936
23939
  } catch {
@@ -23986,6 +23989,7 @@ function wrapToolRegistration(server) {
23986
23989
  workflow = await _captureWorkflow();
23987
23990
  }
23988
23991
  setPendingIntelligence({ changes, workflow });
23992
+ _hasPreSnapshot = true;
23989
23993
  } catch {
23990
23994
  }
23991
23995
  }
@@ -27404,7 +27408,6 @@ var INTELLIGENCE_TOOLS = /* @__PURE__ */ new Set([
27404
27408
  "browser_wait_for"
27405
27409
  ]);
27406
27410
  var VAULT_WRITE_TOOLS = /* @__PURE__ */ new Set([
27407
- "vault_store",
27408
27411
  "vault_delete",
27409
27412
  "vault_login",
27410
27413
  "vault_inject"
@@ -28927,15 +28930,6 @@ async function saveVault(vault) {
28927
28930
  } catch {
28928
28931
  }
28929
28932
  }
28930
- function encrypt(data) {
28931
- const key = requireKey();
28932
- const iv = randomBytes(12);
28933
- const cipher = createCipheriv("aes-256-gcm", key, iv);
28934
- let encrypted = cipher.update(data, "utf-8", "hex");
28935
- encrypted += cipher.final("hex");
28936
- const tag = cipher.getAuthTag().toString("hex");
28937
- return { iv: iv.toString("hex"), encrypted, tag };
28938
- }
28939
28933
  function decrypt(iv, encrypted, tag) {
28940
28934
  const key = requireKey();
28941
28935
  const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
@@ -28944,18 +28938,6 @@ function decrypt(iv, encrypted, tag) {
28944
28938
  decrypted += decipher.final("utf-8");
28945
28939
  return decrypted;
28946
28940
  }
28947
- async function storeCredential(profile, fields) {
28948
- const vault = await loadVault();
28949
- const payload = JSON.stringify({ fieldNames: Object.keys(fields), fields });
28950
- const { iv, encrypted, tag } = encrypt(payload);
28951
- vault.profiles[profile] = {
28952
- iv,
28953
- encrypted,
28954
- tag,
28955
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
28956
- };
28957
- await saveVault(vault);
28958
- }
28959
28941
  async function listProfiles() {
28960
28942
  const vault = await loadVault();
28961
28943
  return Object.entries(vault.profiles).map(([name, p]) => ({
@@ -29028,6 +29010,117 @@ async function injectCredentials(profile) {
29028
29010
  await label.fill(value, { timeout: 3e3 });
29029
29011
  injected++;
29030
29012
  matched.push(name);
29013
+ filled = true;
29014
+ }
29015
+ } catch {
29016
+ }
29017
+ }
29018
+ if (!filled) {
29019
+ const keywords = nameLower.split(/[\s_-]+/).filter((w) => w.length > 2);
29020
+ if (keywords.length === 0) keywords.push(nameLower);
29021
+ for (const kw of keywords) {
29022
+ if (filled) break;
29023
+ try {
29024
+ const ariaLocator = page.locator(`input[aria-label*="${kw}" i], textarea[aria-label*="${kw}" i]`).first();
29025
+ if (await ariaLocator.isVisible({ timeout: 1e3 })) {
29026
+ await ariaLocator.fill(value, { timeout: 3e3 });
29027
+ injected++;
29028
+ matched.push(name);
29029
+ filled = true;
29030
+ }
29031
+ } catch {
29032
+ }
29033
+ }
29034
+ if (!filled) {
29035
+ for (const kw of keywords) {
29036
+ if (filled) break;
29037
+ try {
29038
+ const phLocator = page.getByPlaceholder(kw, { exact: false }).first();
29039
+ if (await phLocator.isVisible({ timeout: 1e3 })) {
29040
+ await phLocator.fill(value, { timeout: 3e3 });
29041
+ injected++;
29042
+ matched.push(name);
29043
+ filled = true;
29044
+ }
29045
+ } catch {
29046
+ }
29047
+ }
29048
+ }
29049
+ }
29050
+ if (!filled) {
29051
+ try {
29052
+ const keywords = nameLower.split(/[\s_-]+/).filter((w) => w.length > 2);
29053
+ if (keywords.length === 0) keywords.push(nameLower);
29054
+ const selector = await page.evaluate((kws) => {
29055
+ const labels = document.querySelectorAll("label");
29056
+ for (const label of labels) {
29057
+ const labelText = label.textContent?.trim().toLowerCase() || "";
29058
+ if (!kws.some((kw) => labelText.includes(kw))) continue;
29059
+ if (label.htmlFor) {
29060
+ const target = document.getElementById(label.htmlFor);
29061
+ if (target && ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName)) {
29062
+ return `#${CSS.escape(label.htmlFor)}`;
29063
+ }
29064
+ }
29065
+ const wrapped = label.querySelector("input, textarea, select");
29066
+ if (wrapped) {
29067
+ const all = [...document.querySelectorAll(wrapped.tagName.toLowerCase())];
29068
+ const idx = all.indexOf(wrapped);
29069
+ if (idx >= 0) return `${wrapped.tagName.toLowerCase()}:nth-of-type(${idx + 1})`;
29070
+ }
29071
+ }
29072
+ for (const input of document.querySelectorAll("input, textarea, select")) {
29073
+ const labelledBy = input.getAttribute("aria-labelledby");
29074
+ if (!labelledBy) continue;
29075
+ const labelEl = document.getElementById(labelledBy);
29076
+ const labelText = labelEl?.textContent?.trim().toLowerCase() || "";
29077
+ if (kws.some((kw) => labelText.includes(kw))) {
29078
+ return `[aria-labelledby="${CSS.escape(labelledBy)}"]`;
29079
+ }
29080
+ }
29081
+ return null;
29082
+ }, keywords);
29083
+ if (selector) {
29084
+ const locator = page.locator(selector).first();
29085
+ if (await locator.isVisible({ timeout: 1e3 })) {
29086
+ await locator.fill(value, { timeout: 3e3 });
29087
+ injected++;
29088
+ matched.push(name);
29089
+ filled = true;
29090
+ }
29091
+ }
29092
+ } catch {
29093
+ }
29094
+ }
29095
+ if (!filled && /email|username|user|login|account/i.test(nameLower)) {
29096
+ try {
29097
+ const hasPasswordField = await page.locator('input[type="password"]').count() > 0;
29098
+ if (hasPasswordField) {
29099
+ const candidates = page.locator('input[type="text"]:visible, input[type="email"]:visible');
29100
+ const count = await candidates.count();
29101
+ for (let i = 0; i < count && !filled; i++) {
29102
+ const candidate = candidates.nth(i);
29103
+ try {
29104
+ const isBeforePassword = await page.evaluate((idx) => {
29105
+ const inputs = [...document.querySelectorAll("input")];
29106
+ const visible = inputs.filter((inp) => {
29107
+ const t = inp.type?.toLowerCase() || "text";
29108
+ return (t === "text" || t === "email") && inp.offsetParent !== null;
29109
+ });
29110
+ const pw = inputs.find((inp) => inp.type === "password");
29111
+ if (!visible[idx] || !pw) return false;
29112
+ return inputs.indexOf(visible[idx]) < inputs.indexOf(pw);
29113
+ }, i);
29114
+ if (isBeforePassword) {
29115
+ await candidate.fill(value, { timeout: 3e3 });
29116
+ injected++;
29117
+ matched.push(name);
29118
+ filled = true;
29119
+ }
29120
+ } catch {
29121
+ continue;
29122
+ }
29123
+ }
29031
29124
  }
29032
29125
  } catch {
29033
29126
  }
@@ -29208,26 +29301,6 @@ ${JSON.stringify(ctx, null, 2)}` }
29208
29301
  }
29209
29302
  }
29210
29303
  );
29211
- server.tool(
29212
- "vault_store",
29213
- {
29214
- profile: external_exports.string().describe('Profile name (e.g. "staging-admin", "bank-login")'),
29215
- fields: external_exports.record(external_exports.string()).describe('Credential fields (e.g. { email: "user@example.com", password: "..." })')
29216
- },
29217
- async ({ profile, fields }) => {
29218
- if (!isVaultConfigured()) {
29219
- return { content: [{ type: "text", text: "Vault not configured. Set VAULT_ENCRYPTION_KEY env var." }], isError: true };
29220
- }
29221
- try {
29222
- await storeCredential(profile, fields);
29223
- return {
29224
- content: [{ type: "text", text: `Vault: stored profile "${profile}" with fields: ${Object.keys(fields).join(", ")}` }]
29225
- };
29226
- } catch (err) {
29227
- return { content: [{ type: "text", text: `Vault store failed: ${err.message}` }], isError: true };
29228
- }
29229
- }
29230
- );
29231
29304
  server.tool(
29232
29305
  "vault_list",
29233
29306
  {},
@@ -0,0 +1,261 @@
1
+ import { createRequire } from 'module';
2
+ const require = createRequire(import.meta.url);
3
+
4
+
5
+ // src/cli/vault.ts
6
+ import { createInterface } from "node:readline";
7
+ import { resolve } from "node:path";
8
+
9
+ // src/security/vault.ts
10
+ import { createCipheriv, createDecipheriv, randomBytes, pbkdf2Sync } from "node:crypto";
11
+ import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
12
+ import { dirname } from "node:path";
13
+
14
+ // src/browser/manager.ts
15
+ import { chromium, firefox, webkit } from "playwright-core";
16
+ var _readyResolve = null;
17
+ var _readyPromise = new Promise((r) => {
18
+ _readyResolve = r;
19
+ });
20
+
21
+ // src/security/vault.ts
22
+ var PBKDF2_ITERATIONS = 1e5;
23
+ var PBKDF2_KEY_LEN = 32;
24
+ var PBKDF2_DIGEST = "sha512";
25
+ var _vaultFile = "";
26
+ var _rawKey = null;
27
+ var _derivedKey = null;
28
+ var _vaultSalt = null;
29
+ function initVault(sessionsDir, encryptionKey) {
30
+ _vaultFile = `${sessionsDir}/vault.enc`;
31
+ if (encryptionKey && encryptionKey.length >= 32) {
32
+ _rawKey = encryptionKey;
33
+ }
34
+ }
35
+ function isVaultConfigured() {
36
+ return _rawKey !== null;
37
+ }
38
+ function requireKey() {
39
+ if (!_rawKey) {
40
+ throw new Error("Vault not configured. Set VAULT_ENCRYPTION_KEY environment variable (32+ chars).");
41
+ }
42
+ if (!_derivedKey) {
43
+ throw new Error("Vault key not derived. Call loadVault() first to initialize.");
44
+ }
45
+ return _derivedKey;
46
+ }
47
+ async function loadVault() {
48
+ try {
49
+ const data = await readFile(_vaultFile, "utf-8");
50
+ const vault = JSON.parse(data);
51
+ if (vault.salt && _rawKey) {
52
+ _vaultSalt = Buffer.from(vault.salt, "hex");
53
+ _derivedKey = pbkdf2Sync(_rawKey, _vaultSalt, PBKDF2_ITERATIONS, PBKDF2_KEY_LEN, PBKDF2_DIGEST);
54
+ }
55
+ return vault;
56
+ } catch {
57
+ if (_rawKey) {
58
+ _vaultSalt = randomBytes(32);
59
+ _derivedKey = pbkdf2Sync(_rawKey, _vaultSalt, PBKDF2_ITERATIONS, PBKDF2_KEY_LEN, PBKDF2_DIGEST);
60
+ }
61
+ return { version: 2, salt: _vaultSalt?.toString("hex") || "", profiles: {} };
62
+ }
63
+ }
64
+ async function saveVault(vault) {
65
+ await mkdir(dirname(_vaultFile), { recursive: true });
66
+ await writeFile(_vaultFile, JSON.stringify(vault, null, 2), "utf-8");
67
+ try {
68
+ await chmod(_vaultFile, 384);
69
+ } catch {
70
+ }
71
+ }
72
+ function encrypt(data) {
73
+ const key = requireKey();
74
+ const iv = randomBytes(12);
75
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
76
+ let encrypted = cipher.update(data, "utf-8", "hex");
77
+ encrypted += cipher.final("hex");
78
+ const tag = cipher.getAuthTag().toString("hex");
79
+ return { iv: iv.toString("hex"), encrypted, tag };
80
+ }
81
+ async function storeCredential(profile, fields) {
82
+ const vault = await loadVault();
83
+ const payload = JSON.stringify({ fieldNames: Object.keys(fields), fields });
84
+ const { iv, encrypted, tag } = encrypt(payload);
85
+ vault.profiles[profile] = {
86
+ iv,
87
+ encrypted,
88
+ tag,
89
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
90
+ };
91
+ await saveVault(vault);
92
+ }
93
+ async function listProfiles() {
94
+ const vault = await loadVault();
95
+ return Object.entries(vault.profiles).map(([name, p]) => ({
96
+ name,
97
+ createdAt: p.createdAt,
98
+ lastUsedAt: p.lastUsedAt
99
+ }));
100
+ }
101
+ async function deleteProfile(profile) {
102
+ const vault = await loadVault();
103
+ delete vault.profiles[profile];
104
+ await saveVault(vault);
105
+ }
106
+
107
+ // src/cli/vault.ts
108
+ var USAGE = `
109
+ Usage: looking-glass vault <command> [options]
110
+
111
+ Commands:
112
+ store <profile> Store credentials interactively (passwords masked)
113
+ list List stored profiles and timestamps
114
+ delete <profile> Delete a stored profile
115
+
116
+ Environment:
117
+ VAULT_ENCRYPTION_KEY Passphrase for encryption (32+ chars, required)
118
+ BROWSER_SESSIONS_DIR Sessions directory (default: .browser-sessions)
119
+
120
+ Examples:
121
+ npx looking-glass-mcp vault store linkedin
122
+ npx looking-glass-mcp vault list
123
+ npx looking-glass-mcp vault delete linkedin
124
+ `.trim();
125
+ function die(msg) {
126
+ console.error(`Error: ${msg}`);
127
+ process.exit(1);
128
+ }
129
+ function initFromEnv() {
130
+ const sessionsDir = resolve(process.env.BROWSER_SESSIONS_DIR || ".browser-sessions");
131
+ const key = process.env.VAULT_ENCRYPTION_KEY;
132
+ initVault(sessionsDir, key);
133
+ if (!isVaultConfigured()) {
134
+ die("VAULT_ENCRYPTION_KEY is not set or is shorter than 32 characters.");
135
+ }
136
+ }
137
+ function prompt(question) {
138
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
139
+ return new Promise((resolve2) => {
140
+ rl.question(question, (answer) => {
141
+ rl.close();
142
+ resolve2(answer.trim());
143
+ });
144
+ });
145
+ }
146
+ function promptMasked(question) {
147
+ return new Promise((resolve2) => {
148
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
149
+ process.stdout.write(question);
150
+ const stdin = process.stdin;
151
+ const wasRaw = stdin.isRaw;
152
+ if (stdin.isTTY && stdin.setRawMode) {
153
+ stdin.setRawMode(true);
154
+ }
155
+ let value = "";
156
+ const onData = (ch) => {
157
+ const c = ch.toString("utf-8");
158
+ if (c === "\n" || c === "\r" || c === "") {
159
+ if (stdin.isTTY && stdin.setRawMode) {
160
+ stdin.setRawMode(wasRaw ?? false);
161
+ }
162
+ stdin.removeListener("data", onData);
163
+ rl.close();
164
+ process.stdout.write("\n");
165
+ resolve2(value);
166
+ } else if (c === "") {
167
+ process.stdout.write("\n");
168
+ process.exit(1);
169
+ } else if (c === "\x7F" || c === "\b") {
170
+ if (value.length > 0) {
171
+ value = value.slice(0, -1);
172
+ process.stdout.write("\b \b");
173
+ }
174
+ } else {
175
+ value += c;
176
+ process.stdout.write("*");
177
+ }
178
+ };
179
+ stdin.on("data", onData);
180
+ });
181
+ }
182
+ function isSensitiveField(name) {
183
+ const lower = name.toLowerCase();
184
+ return ["password", "passwd", "pwd", "pass", "secret", "token", "key", "pin", "otp"].some(
185
+ (p) => lower.includes(p)
186
+ );
187
+ }
188
+ async function cmdStore(profileName) {
189
+ const profile = profileName || await prompt("Profile name: ");
190
+ if (!profile) die("Profile name is required.");
191
+ console.log(`
192
+ Storing credential profile: ${profile}`);
193
+ console.log("Enter field name/value pairs. Leave field name empty to finish.\n");
194
+ const fields = {};
195
+ while (true) {
196
+ const fieldName = await prompt("Field name (empty to finish): ");
197
+ if (!fieldName) break;
198
+ const sensitive = isSensitiveField(fieldName);
199
+ const value = sensitive ? await promptMasked(`Value for "${fieldName}": `) : await prompt(`Value for "${fieldName}": `);
200
+ if (!value) {
201
+ console.log(` Skipping empty field "${fieldName}"`);
202
+ continue;
203
+ }
204
+ fields[fieldName] = value;
205
+ }
206
+ if (Object.keys(fields).length === 0) {
207
+ die("No fields provided. Nothing stored.");
208
+ }
209
+ await storeCredential(profile, fields);
210
+ console.log(`
211
+ \u2713 Stored profile "${profile}" with ${Object.keys(fields).length} field(s)`);
212
+ }
213
+ async function cmdList() {
214
+ const profiles = await listProfiles();
215
+ if (profiles.length === 0) {
216
+ console.log("No profiles stored.");
217
+ return;
218
+ }
219
+ console.log("Vault Profiles:\n");
220
+ for (const p of profiles) {
221
+ const lastUsed = p.lastUsedAt ? `, last used: ${p.lastUsedAt}` : "";
222
+ console.log(` \u2022 ${p.name} (created: ${p.createdAt}${lastUsed})`);
223
+ }
224
+ }
225
+ async function cmdDelete(profileName) {
226
+ const profile = profileName || await prompt("Profile name to delete: ");
227
+ if (!profile) die("Profile name is required.");
228
+ await deleteProfile(profile);
229
+ console.log(`\u2713 Deleted profile "${profile}"`);
230
+ }
231
+ async function main() {
232
+ const args = process.argv.slice(2);
233
+ if (args[0] === "vault") args.shift();
234
+ const command = args[0];
235
+ const target = args[1];
236
+ if (!command || command === "--help" || command === "-h") {
237
+ console.log(USAGE);
238
+ process.exit(0);
239
+ }
240
+ initFromEnv();
241
+ switch (command) {
242
+ case "store":
243
+ await cmdStore(target);
244
+ break;
245
+ case "list":
246
+ await cmdList();
247
+ break;
248
+ case "delete":
249
+ await cmdDelete(target);
250
+ break;
251
+ default:
252
+ console.error(`Unknown command: ${command}
253
+ `);
254
+ console.log(USAGE);
255
+ process.exit(1);
256
+ }
257
+ }
258
+ main().catch((err) => {
259
+ console.error(`Fatal: ${err.message}`);
260
+ process.exit(1);
261
+ });
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "looking-glass-mcp",
3
- "version": "3.0.1",
3
+ "version": "3.1.1",
4
4
  "mcpName": "io.github.Sahib-Sawhney-WH/looking-glass-mcp",
5
5
  "description": "AI-native browser for agents — semantic change detection, self-healing interactions, structured extraction, credential vault, and enterprise Azure deployment",
6
6
  "main": "build/index.js",
7
7
  "type": "module",
8
8
  "bin": {
9
9
  "looking-glass-mcp": "bin/looking-glass-mcp.mjs",
10
- "looking-glass": "bin/looking-glass-mcp.mjs"
10
+ "looking-glass": "bin/looking-glass-mcp.mjs",
11
+ "looking-glass-vault": "bin/looking-glass-vault.mjs"
11
12
  },
12
13
  "scripts": {
13
14
  "build": "node esbuild.config.js",