|
// ab-batch-type: Fast keyboard typing for canvas-based web apps via agent-browser daemon. |
|
// Sends all keystrokes through the daemon's keyboard action (trusted CDP events, single event path). |
|
// |
|
// Usage: |
|
// node <this-script> "text" Tab "text" Enter ... # args mode |
|
// echo '"text" Tab "text" Enter ...' | node <this-script> # stdin mode (unlimited size) |
|
// |
|
// Stdin mode reads all input, splits on whitespace (respecting "quoted strings"), |
|
// and processes identically to arg mode. Use for large payloads that exceed shell |
|
// argument limits. |
|
// |
|
// Text strings typed char-by-char. Special keys: Tab Enter Escape Backspace Delete Arrow*. |
|
// |
|
// Use for: Google Sheets, Google Docs, Excel Online, Figma, Monaco/code-server, |
|
// Airtable, Notion, terminal emulators (xterm.js) — any app where ab fill/type fails |
|
// because targets are canvas-rendered or custom inputs, not DOM <input> elements. |
|
// |
|
// CRITICAL: all events must go through the daemon's keyboard action. |
|
// Do NOT mix with input_keyboard (CDP direct) — they have separate focus tracking |
|
// and mixing them breaks cell navigation (Enter won't move to next row). |
|
// |
|
// Performance: pipelines ALL commands over the socket in one write — zero artificial |
|
// delays. The daemon processes them sequentially via CDP; Google Sheets handles the |
|
// pace naturally. ~10x faster than send-one-await-one with pauses. |
|
// |
|
// Requires: agent-browser daemon running (any agent-browser command starts it). |
|
|
|
const net = require('net'); |
|
const path = require('path'); |
|
const os = require('os'); |
|
const session = process.env.AGENT_BROWSER_SESSION || 'default'; |
|
const socketPath = path.join(os.homedir(), '.agent-browser', session + '.sock'); |
|
|
|
const SPECIAL = new Set([ |
|
'Tab', 'Enter', 'Escape', 'Backspace', 'Delete', |
|
'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', |
|
]); |
|
|
|
function parseTokens(input) { |
|
const tokens = []; |
|
const re = /"([^"]*)"|([\S]+)/g; |
|
let m; |
|
while ((m = re.exec(input)) !== null) { |
|
tokens.push(m[1] !== undefined ? m[1] : m[2]); |
|
} |
|
return tokens; |
|
} |
|
|
|
function buildCommands(tokens) { |
|
const commands = []; |
|
let id = 0; |
|
for (const token of tokens) { |
|
if (SPECIAL.has(token)) { |
|
commands.push({ id: String(++id), action: 'keyboard', keys: token }); |
|
} else { |
|
for (const ch of token) { |
|
commands.push({ id: String(++id), action: 'keyboard', keys: ch }); |
|
} |
|
} |
|
} |
|
return commands; |
|
} |
|
|
|
function readStdin() { |
|
return new Promise((resolve) => { |
|
let data = ''; |
|
process.stdin.setEncoding('utf8'); |
|
process.stdin.on('data', chunk => { data += chunk; }); |
|
process.stdin.on('end', () => resolve(data)); |
|
}); |
|
} |
|
|
|
async function getTokens() { |
|
const args = process.argv.slice(2); |
|
if (args.length > 0) return args; |
|
// Stdin mode |
|
const input = await readStdin(); |
|
return parseTokens(input); |
|
} |
|
|
|
async function main() { |
|
const tokens = await getTokens(); |
|
const commands = buildCommands(tokens); |
|
|
|
if (commands.length === 0) { |
|
console.error('Usage: node ab-batch-type.js "text" Tab "text" Enter ...'); |
|
console.error(' or: echo \'"text" Tab "text" Enter\' | node ab-batch-type.js'); |
|
console.error('Special keys: Tab Enter Escape Backspace Delete ArrowDown ArrowUp ArrowLeft ArrowRight'); |
|
process.exit(1); |
|
} |
|
|
|
const sock = net.connect(socketPath); |
|
await new Promise((resolve, reject) => { |
|
sock.on('connect', resolve); |
|
sock.on('error', reject); |
|
}); |
|
|
|
const start = Date.now(); |
|
|
|
// Pipeline: write ALL commands in one shot |
|
sock.write(commands.map(c => JSON.stringify(c)).join('\n') + '\n'); |
|
|
|
// Collect all responses |
|
let buffer = ''; |
|
let resolved = 0; |
|
|
|
await new Promise((resolve, reject) => { |
|
sock.on('data', d => { |
|
buffer += d.toString(); |
|
while (buffer.includes('\n')) { |
|
const nlIdx = buffer.indexOf('\n'); |
|
const line = buffer.substring(0, nlIdx).trim(); |
|
buffer = buffer.substring(nlIdx + 1); |
|
if (!line) continue; |
|
const resp = JSON.parse(line); |
|
if (!resp.success) { |
|
reject(new Error('Failed at key ' + resolved + ': ' + JSON.stringify(resp))); |
|
return; |
|
} |
|
resolved++; |
|
if (resolved >= commands.length) { |
|
resolve(); |
|
return; |
|
} |
|
} |
|
}); |
|
sock.on('error', reject); |
|
}); |
|
|
|
const elapsed = Date.now() - start; |
|
console.log('Done — ' + commands.length + ' keys in ' + elapsed + 'ms'); |
|
sock.end(); |
|
process.exit(0); |
|
} |
|
|
|
main().catch(e => { console.error(e.message); process.exit(1); }); |
|
setTimeout(() => { console.error('timeout'); process.exit(1); }, 120000); |