Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ These are absolute rules — never violate them:
- **Clipboard capture whitespace stripping**: `detectKeysInClipboard()` strips all whitespace and newlines from clipboard content before pattern matching (`components(separatedBy: .whitespacesAndNewlines).joined()`). API keys never contain spaces, but clipboard copy may introduce line breaks from word-wrap.
- **Clipboard capture built-in patterns**: `ClipboardEngine.builtInCapturePatterns` contains 22 well-known API key patterns (OpenAI, Anthropic, GitHub, AWS, Google, Stripe, etc.) for detecting NEW keys not yet in the vault. This is separate from `patternCacheEntries()` which only matches stored keys.
- **NSAlert in menu bar app**: Must call `alert.layout()` then set `alert.window.level = .floating` + `orderFrontRegardless()` before `runModal()`. Without this, the alert either doesn't appear or creates a dock icon. Do NOT use `setActivationPolicy(.regular)`.
- **Terminal masking sync block buffering** (experimental): Shielded Terminal buffers PTY output by detecting DEC 2026 sync block markers (`\x1b[?2026h`/`\x1b[?2026l`). Complete sync blocks are masked atomically. Non-sync data uses 30ms timeout buffer. This matches claude-chill's approach.
- **Terminal masking ANSI-aware matching**: `maskTerminalOutput()` strips ANSI escape codes AND all whitespace (spaces, tabs, newlines) before regex matching. Ink word-wraps long keys with `\r\n` + indentation; stripping all whitespace allows regex to match keys across visual line breaks. Structural characters (ANSI + whitespace) within matched ranges are preserved in output via `extractStructural()`.
- **Terminal masking node-pty loading**: Triple fallback: (1) `require('node-pty')`, (2) `require(vscode.env.appRoot + '/node_modules.asar.unpacked/node-pty')`, (3) `require(vscode.env.appRoot + '/node_modules/node-pty')`. Falls back to `child_process.spawn` line-mode terminal.

## Documentation

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ See the [docs/](docs/) directory for detailed specifications:
- [x] Smart Key Extraction confirmation dialog (full Chrome ↔ Swift Core IPC: detect → submit → Keychain store → pattern sync)
- [x] Linked Key Groups (sequential paste with ⌘V→Tab automation, Settings UI CRUD, pre-fetch Keychain)
- [x] Clipboard Capture (⌃⌥⌘V hotkey, 22 built-in patterns, 3-tier confidence routing, whitespace-tolerant)
- [x] 🧪 Terminal masking — Shielded Terminal (node-pty proxy, DEC 2026 sync block buffering, ANSI-aware masking, Claude Code compatible)
- [ ] API Key rotation & deployment sync
- [ ] Terminal masking (node-pty proxy)
- [ ] System-wide masking (Accessibility API)

## Contributing
Expand Down
1 change: 1 addition & 0 deletions docs/01-product-spec/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
|------|--------------|---------|---------|
| ~~`⌃⌥⌘V` capture clipboard~~ | Spec §4.4 | ~~中~~ | ✅ HotkeyManager → ClipboardEngine.detectKeysInClipboard() → 內建 22 種 pattern 偵測 → confidence 三階路由 → VaultManager 存儲 |
| ~~Linked Key Groups (sequential paste)~~ | Spec §6.3 | ~~中~~ | ✅ `LinkedGroup`/`GroupEntry`/`SequentialPasteEngine` 完成、Settings UI 群組管理(CRUD)、`request_paste_group` IPC handler |
| ~~Terminal masking (Shielded Terminal)~~ | Spec §3.2 | ~~中~~ | 🧪 實驗性。node-pty proxy + DEC 2026 sync block buffering + ANSI-aware masking。Known limitation: Rewind 確認頁部分洩漏 |
| Shortcut conflict detection | Spec §4.4 | 低 | |
| Import / Export vault | Spec §9.1 | 低 | |

Expand Down
1 change: 1 addition & 0 deletions docs/en/01-product-spec/implementation-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
|---------|-------------|----------|---------|
| ~~`⌃⌥⌘V` capture clipboard~~ | Spec §4.4 | ~~Medium~~ | ✅ HotkeyManager → ClipboardEngine.detectKeysInClipboard() → 22 built-in patterns → 3-tier confidence routing → VaultManager store |
| ~~Linked Key Groups (sequential paste)~~ | Spec §6.3 | ~~Medium~~ | ✅ `LinkedGroup`/`GroupEntry`/`SequentialPasteEngine` complete, Settings UI group management (CRUD), `request_paste_group` IPC handler |
| ~~Terminal masking (Shielded Terminal)~~ | Spec §3.2 | ~~Medium~~ | 🧪 Experimental. node-pty proxy + DEC 2026 sync block buffering + ANSI-aware masking. Known limitation: Rewind confirmation page partial leak |
| Shortcut conflict detection | Spec §4.4 | Low | |
| Import / Export vault | Spec §9.1 | Low | |

Expand Down
17 changes: 16 additions & 1 deletion packages/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
"command": "demosafe.pasteKey",
"title": "DemoSafe: Paste Key",
"icon": "$(key)"
},
{
"command": "demosafe.openShieldedTerminal",
"title": "DemoSafe: Open Shielded Terminal",
"icon": "$(shield)"
},
{
"command": "demosafe.openTerminalWithCommand",
"title": "DemoSafe: Open Terminal With Command",
"icon": "$(terminal)"
}
],
"keybindings": [
Expand All @@ -38,11 +48,16 @@
"command": "demosafe.pasteKey",
"key": "ctrl+alt+space",
"mac": "ctrl+alt+space"
},
{
"command": "demosafe.openShieldedTerminal",
"key": "ctrl+shift+t",
"mac": "ctrl+shift+t"
}
]
},
"scripts": {
"build": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node",
"build": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --external:node-pty --format=cjs --platform=node",
"watch": "npm run build -- --watch",
"type-check": "tsc --noEmit",
"lint": "eslint --ext .ts src/",
Expand Down
76 changes: 76 additions & 0 deletions packages/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ import { StatusBarManager, ConnectionState } from './statusbar/statusbar-manager
import { PatternScanner } from './core/pattern-scanner';
import { PatternCache } from './core/pattern-cache';
import { pasteKeyCommand } from './commands/paste-key';
import { ShieldedTerminal, FallbackShieldedTerminal } from './terminal/shielded-terminal';
import { buildTerminalPatterns } from './terminal/terminal-patterns';

let ipcClient: IPCClient;
let decorationManager: DecorationManager;
let statusBarManager: StatusBarManager;
let patternScanner: PatternScanner;
let patternCache: PatternCache;
let isDemoMode = false;
let isShieldActive = false;
let outputChannel: vscode.OutputChannel;

// Track all shielded terminals for state sync
const shieldedTerminals: Set<ShieldedTerminal | FallbackShieldedTerminal> = new Set();

export function activate(context: vscode.ExtensionContext) {
outputChannel = vscode.window.createOutputChannel('DemoSafe');
outputChannel.appendLine('[DemoSafe] Extension activating...');
Expand All @@ -38,6 +44,19 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('demosafe.pasteKey', () => {
pasteKeyCommand(patternCache, ipcClient);
}),
vscode.commands.registerCommand('demosafe.openShieldedTerminal', () => {
openShieldedTerminal();
}),
vscode.commands.registerCommand('demosafe.openTerminalWithCommand', async () => {
const cmd = await vscode.window.showInputBox({
prompt: 'Command to run in shielded terminal',
placeHolder: 'e.g. claude, npm start, python app.py',
value: 'claude',
});
if (cmd) {
openShieldedTerminal(cmd);
}
}),
);

// IPC log forwarding
Expand All @@ -59,6 +78,8 @@ export function activate(context: vscode.ExtensionContext) {

ipcClient.on('stateChanged', (state: { isDemoMode: boolean; activeContext: { name: string } | null }) => {
isDemoMode = state.isDemoMode;
isShieldActive = state.isDemoMode; // Sync shield with Demo Mode
syncShieldState();
statusBarManager.update({
isDemoMode: state.isDemoMode,
contextName: state.activeContext?.name ?? null,
Expand All @@ -68,6 +89,11 @@ export function activate(context: vscode.ExtensionContext) {

ipcClient.on('patternsUpdated', () => {
updateMasking();
// Update terminal patterns when Core syncs new patterns
const patterns = buildTerminalPatterns(patternCache);
for (const t of shieldedTerminals) {
t.updatePatterns(patterns);
}
});

ipcClient.on('clipboardCleared', () => {
Expand Down Expand Up @@ -153,7 +179,57 @@ function updateConnectionState(state: ConnectionState) {
}
}

// --- Shielded Terminal ---

function openShieldedTerminal(initialCommand?: string) {
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.env.HOME || '/';
const patterns = buildTerminalPatterns(patternCache);

// Shielded Terminal always starts with shield ON — that's its purpose
const shieldOn = true;
let pty: ShieldedTerminal | FallbackShieldedTerminal;
let terminalName: string;

try {
pty = new ShieldedTerminal(cwd, shieldOn, patterns, outputChannel);
terminalName = 'Shield';
} catch {
pty = new FallbackShieldedTerminal(cwd, shieldOn, patterns);
terminalName = 'Shield (Fallback)';
}

shieldedTerminals.add(pty);

const terminal = vscode.window.createTerminal({
name: terminalName,
pty,
location: vscode.TerminalLocation.Editor,
});

terminal.show();

// Send initial command after shell starts
if (initialCommand) {
setTimeout(() => {
for (const char of initialCommand) {
pty.handleInput(char);
}
pty.handleInput('\r');
}, 500);
}
}

function syncShieldState() {
for (const t of shieldedTerminals) {
t.shieldEnabled = isShieldActive;
}
}

export function deactivate() {
for (const t of shieldedTerminals) {
t.close();
}
shieldedTerminals.clear();
ipcClient?.disconnect();
decorationManager?.dispose();
statusBarManager?.dispose();
Expand Down
Loading
Loading