lens by cortex
Cortex Lens is a paid commercial product. This extension is the client component and requires an active Cortex Lens tenant to operate. See LICENSE for terms.
extension for VS Code, Cursor, Windsurf and other VS Code-family editors that ships token, cost, and tool-call telemetry into the multi-tenant cortex-lens dashboard. it lives next to connectors/claude-code-hook and connectors/lens-cli and uses the same env contract so a workspace can adopt all three with one token.
status: 0.3.3, published as questo.lens-cursor-extension, with detected-fork attribution, encrypted-keychain tenant token, typed heartbeat, identity attribution, session boundaries, and the no-paste device connect (Lens: connect this editor with a code pairs the editor through the dashboard approval page and stores the granted token in the encrypted keychain).
what it looks like
once activated, the extension surfaces a status bar widget at the bottom-right of the editor window showing live session savings (lens: $0.00 saved). click the widget to open the configured dashboard at <lens.baseUrl> in the browser, where the SavingsCounter, Brain Advisor whisper card, and Improvements drill-down chart aggregate every tool call captured by this extension and its sibling connectors.
screenshots: three banner-style placeholder mockups live in images/: status-bar.png (1200x300, the bottom-of-IDE widget), dashboard-preview.png (1200x600, the Lens dashboard top nav with the savings counter), and command-palette.png (1200x400, the lens: commands the extension registers). these are composed placeholders, not real captures from a running install; they ship with the Marketplace listing and are replaced with real screenshots once the first design-partner is on the dashboard.
install
pnpm --filter lens-cursor-extension build
pnpm --filter lens-cursor-extension package
cursor --install-extension lens-cursor-extension-0.3.3.vsix
VS Code and Windsurf accept the same vsix via their own --install-extension flag, or via the Extensions panel "Install from VSIX" picker.
getting connected
connect with a code (recommended, no token paste)
Lens: connect this editor with a code is the one-command path and needs nothing typed. run it from the command palette: the extension shows a short pairing code, a signed-in developer approves it in the dashboard, and the extension collects the granted token over the poll channel and writes it straight into the encrypted SecretStorage keychain. the token never appears in the notification and never touches settings.json.
- zero typing: the pairing runs against the hosted dashboard (
https://lens.questo.media) by default, so a fresh install connects with one command. a self-hosted deployment is reached by setting lens.baseUrl (or LENS_BASE_URL) before connecting; either overrides the default.
- self-configuring: on approval the extension persists the deployment-advertised ingest origin as
lens.baseUrl and the approving developer's email as lens.seatEmail, so capture reports to the right place and attributes the right seat with no manually-keyed detail.
settings
every value is optional. adjust them in Settings > Extensions > Lens by corteX (the panel label is identical across VS Code, Cursor, and Windsurf) or in your workspace .vscode/settings.json:
| setting |
required |
purpose |
lens.baseUrl |
no |
lens dashboard and ingest origin. the extension posts events to <lens.baseUrl>/api/ingest/ide. when blank it reads LENS_BASE_URL, then falls back to the hosted default https://lens.questo.media. device connect fills it from the deployment-advertised ingest origin. |
lens.tenantToken |
no |
per-tenant bearer token of shape <tenant_uuid>:<secret>. device connect provides this for you; set it by hand only for the fallback path below. sent only on the authorization header; migrates into the encrypted SecretStorage keychain on first run. falls back to LENS_TENANT_TOKEN. |
lens.seatEmail |
no |
work email of the developer who approved device connect, filled automatically on connect and used to attribute this seat when no lens.developerId is set. honored only from account-scope settings; a workspace value a repo could commit is ignored. not a secret; sent in the event body, never as a credential. |
lens.enabled |
no |
toggle capture on or off without uninstalling. default true. |
lens.developerId |
no |
organization-managed seat id or work email used to attribute activity in the per-developer dashboard. honored only when it arrives through an organization-managed settings policy; hand-typed values in personal or workspace settings are ignored (no environment fallback), and captured activity is otherwise attributed automatically from the seat email or the git author email. not a secret; sent in the event body, never as a credential. |
fallback: paste a token
device connect is the primary path and needs no token handling. the manual token path remains for headless installs, CI, and org-managed rollouts where a policy distributes the values centrally.
token placement warning: a token written into workspace .vscode/settings.json is committed with the repository unless that file is git-ignored. put lens.tenantToken in editor settings at the account scope rather than the workspace scope (or set LENS_TENANT_TOKEN in the environment) and keep workspace settings for non-secrets such as lens.baseUrl.
self-serve without editing settings.json
a lone developer without an org policy can configure the extension entirely from the shell, because both values fall back to the environment (LENS_BASE_URL and LENS_TENANT_TOKEN). the claude-code-hook installer writes both into one locked env file. from a checkout of the cortex-lens repository:
python -m pip install ./connectors/claude-code-hook
LENS_TENANT_TOKEN='<tenant_id>:<token_secret>' python -m lens_code_hook.install --endpoint https://<your-lens-host>/api/ingest/ide
set -a; . ~/.cortex/lens-code-hook.env; set +a
on Windows, dot-source the generated loader instead of the third line: . $env:USERPROFILE\.cortex\lens-code-hook.ps1.
then launch the editor from that same shell (for example cursor ., code ., or windsurf .). the extension picks up LENS_BASE_URL and LENS_TENANT_TOKEN from the inherited environment, so you never hand-edit editor settings, and the token never touches a committable .vscode/settings.json.
honest limitation: an editor launched from the dock or Start menu does not inherit shell environment. either launch the editor from the sourced shell or set the values in editor settings at the account scope. the settings panel remains the path for org-managed rollouts, where an organization policy distributes lens.baseUrl and the developer id centrally.
what it captures
- mcp tool calls forwarded from the host editor's internal mcp layer (pre and post phases). args are redacted client-side via
lib/redact for obvious credentials; the receiver applies the full removal catalog server-side.
- status bar widget bottom-right showing current session savings. click opens the configured dashboard.
- claude-code-in-terminal detector: polls integrated terminals every 30 seconds. when claude code is running in the same workspace, the de-dup adapter in
packages/adapters uses this signal to drop duplicate events.
- editor activity (file opens, selection changes) and the detected IDE fork (vscode | cursor | windsurf | vscodium | unknown) ride on every event so the dashboard can attribute spend to the right editor and the right developer.
heartbeat wire contract (one-sided)
besides events, the extension posts a periodic typed heartbeat to <lens.baseUrl>/api/ingest/heartbeat so the dashboard install-coverage panel can tell which machines are alive (the server upserts one row per tenant, machine, and ide fork). that wire contract is exercised one-sided today, which matters before extending either end:
- the client (
src/lib/heartbeat.ts) sends the seven-key payload and reads only the http response status. the response body, which carries the server's E-11 state-machine classification (heartbeat_state, transitioned, inserted, tuple, last_seen_at), is discarded.
- the operator dashboard is the only shipped consumer of heartbeat state. the in-ide pulse the server route comment describes (surfacing
heartbeat_state in the status bar) is unshipped client work, not a wired path.
- consequence: the server may extend its response body without breaking deployed extensions, but a client change that starts reading the body must treat every field as optional so older lens deployments stay compatible.
commands
| id |
description |
lens.openDashboard |
open <lens.baseUrl> in the default browser |
lens.toggleEnabled |
pause or resume capture for the current workspace |
lens.viewSavings |
quick-pick recent session savings |
lens.showUsage |
open the per-developer usage panel |
lens.requestLimitIncrease |
queue a policy limit-increase request for operator review |
lens.connectWithCode |
pair this editor with the dashboard with zero typing: shows a short code, waits for approval at /connect/device, then stores the granted token in the encrypted keychain and persists the deployment-advertised ingest origin as lens.baseUrl and the approver email as lens.seatEmail |
privacy
- the tenant token never lands in logs, stdout, stderr, or the post body. only the authorization header carries it.
- arg values matching common credential patterns are replaced with
[redacted] before they leave the workstation. server-side, the removal catalog applies again over the message text and tool args.
- the extension only reads files the host editor already has open or terminals already running. it does not read arbitrary disk.
carry-forwards (future scope)
the following are documented as out of scope for the current ship and land in follow-on waves:
- completion telemetry: cursor tab token usage. requires hooking cursor's internal completion api (still in flux as of 2026-05).
- inline chat capture: the host editor's inline chat panel events. depends on the editor exposing a public event api.
- real mcp transport binding: this build still uses a no-op mcp source pending a stable event stream from each editor fork.
layout
connectors/cursor-extension/
package.json
tsconfig.json
vitest.config.ts
README.md
src/
extension.ts activate/deactivate, status bar wire, command registration
index.ts public re-exports for tests
status-bar.ts LensStatusBar widget
mcp-proxy.ts MCPProxy + MCPEventSource interface
claude-code-detector.ts ClaudeCodeDetector poll
lib/
redact.ts client-side credential redactor stub
ingest-client.ts batched HTTP transport
tests/
vscode-mock.ts vscode runtime mock for vitest
extension.test.ts 4 cases
mcp-proxy.test.ts 3 cases
claude-code-detector.test.ts 4 cases
status-bar.test.ts 2 cases
ingest-client.test.ts 4 cases
connectors/claude-code-hook ships hook events for claude code itself (python).
connectors/lens-cli wraps coding-agent cli invocations from the outside (node).
packages/adapters/src/claude-code-to-agent-trace.ts is the receiver-side adapter for the hook source. the cursor adapter lands in pc-2.
apps/enterprise/app/api/ingest/ide/route.ts is the receiver this extension posts to (the ide path).
verifying that lens is actually loaded inside the host editor
cursor 3.5.x and some windsurf builds occasionally hide side-loaded vsix entries from the search filter inside the extensions panel even when the extension is fully activated and running. the activation log is the source of truth, not the panel search box. follow this procedure to verify in under one minute. vs code stable surfaces the panel entry directly, so step 4 is mostly a cursor and windsurf concern.
1. confirm activation in the log
open the most recent log session under %APPDATA%/Cursor/logs/<TIMESTAMP>/window1/exthost/exthost.log (cursor), %APPDATA%/Code/logs/... (vs code), or %APPDATA%/Windsurf/logs/... (windsurf), and search for questo.lens-cursor-extension. you should see two lines per editor restart:
ExtensionService#_doActivateExtension questo.lens-cursor-extension, startup: false, activationEvent: 'onStartupFinished'
Extension activated success: questo.lens-cursor-extension - 10ms (code loading: 8ms, activate call: 2ms, activate resolved: 0ms)
if those two lines are present, the extension is loaded and running. visibility in the extensions panel is a separate ui concern and does not gate functionality.
after activation, the extension calls vscode.window.createStatusBarItem(...).show(). look at the bottom-right of the host editor status bar. you will see one of two texts:
lens: $0.00 saved when capture is enabled
lens: paused when capture is paused
if the widget is visible, lens is alive end-to-end. clicking the widget runs lens.openDashboard.
3. confirm command registration
open the command palette with ctrl+shift+p and type lens:. three commands should appear:
Lens: open dashboard
Lens: toggle capture
Lens: view recent session savings
if the palette finds them, the extension is registered correctly. command palette lookup is independent of the extensions panel filter and is the second most reliable signal after the activation log.
this is the known cursor 3.5.x ui state: side-loaded vsix entries can disappear from the installed-tab search filter when the editor's publisher registry sync fails. functionality is unaffected. options:
- ignore it: the extension is running. the panel entry is only needed to uninstall via the gui.
- restart the editor with
--reload-extensions: forces a rebuild of the panel state from ~/.cursor/extensions/extensions.json or the equivalent vs code or windsurf path.
- uninstall via cli if needed:
cursor --uninstall-extension questo.lens-cursor-extension (substitute code or windsurf for the host editor). the entry comes back on the next reinstall in some cases.
5. if the activation log does not show the two lines
then the extension truly failed to load. likely causes and fixes, in order of probability:
| cause |
how to detect |
fix |
| engines mismatch |
log line Cannot activate ... engines.vscode '^A.B.C' incompatible with cursor |
edit package.json in the install dir to lower engines.vscode (current shipped value: ^1.85.0). |
| out/extension.js syntax error |
log line with stack trace at activate call |
rebuild via pnpm --filter @cortex/lens build and replace the out/ dir. |
| publisher rejected |
extension absent from ~/.cursor/extensions/extensions.json after install |
publisher in package.json is questo. if the host editor rejects it, change to local or omit the publisher, then repackage with vsce package --no-dependencies and reinstall. |
| extension registry file corrupted |
extensions.json is missing the questo.lens-cursor-extension entry but the install dir exists |
delete the install dir, then cursor --install-extension <path-to-vsix> (substitute code or windsurf for the host editor). |
6. quick repackage and reinstall procedure
if you need to rebuild and reinstall from source after editing the manifest:
cd C:/Intel/questo/projects/cortex-lens/connectors/cursor-extension
pnpm --filter lens-cursor-extension build
pnpm --filter lens-cursor-extension package
cursor --install-extension lens-cursor-extension-0.3.3.vsix
then fully quit the editor (taskkill any leftover Cursor.exe, Code.exe, or Windsurf.exe) and reopen. re-verify steps 1 through 3 above.