browse
Version:
Unified Browserbase CLI for browser automation and cloud APIs.
113 lines (112 loc) • 4.36 kB
JavaScript
/**
* Suggestion engine for unknown commands.
*
* oclif's spaced-topic parsing glues unknown leading argv tokens into the
* attempted command id (e.g. `browse opne https://example.com` arrives as
* `opne:https://example.com`), so the id may contain user-provided values.
* Everything here works on a sanitized token prefix and never returns raw
* argv content beyond the tokens that matched a known command shape.
*/
import { distance } from "fastest-levenshtein";
/**
* Old Commander-era syntax (and common agent guesses) mapped to the current
* command tree. Keys and values are colon-separated oclif ids; values may
* also be topics (e.g. `cloud:contexts`, which prints topic help).
*/
export const aliasSuggestions = new Map([
["sessions", "cloud:sessions:list"],
["sessions:list", "cloud:sessions:list"],
["sessions:create", "cloud:sessions:create"],
["sessions:get", "cloud:sessions:get"],
["projects", "cloud:projects:list"],
["projects:list", "cloud:projects:list"],
["contexts", "cloud:contexts"],
["extensions", "cloud:extensions"],
["search", "cloud:search"],
["fetch", "cloud:fetch"],
["goto", "open"],
["navigate", "open"],
]);
const safeTokenPattern = /^[A-Za-z][A-Za-z0-9_-]*$/;
const maxCommandTokens = 4;
const maxSuggestionDistance = 5;
/**
* Extracts the leading command-shaped tokens from an attempted id, stopping
* at the first token that does not look like a command word (URLs, selectors,
* flags, and other argument-like values).
*/
export function extractCommandTokens(id) {
const tokens = [];
for (const token of id.split(":")) {
if (tokens.length >= maxCommandTokens || !safeTokenPattern.test(token)) {
break;
}
tokens.push(token.toLowerCase());
}
return tokens;
}
function tokenThreshold(token) {
return Math.max(2, Math.floor(token.length / 3));
}
/**
* Segment-aligned fuzzy distance between a token prefix and a command id.
* Only command ids with the same number of segments are considered, and every
* token must be within its own edit-distance threshold of the corresponding
* segment. This means a trailing token can only ever be retained in the
* attempted command when it itself looks like a typo of a real command
* segment — free-form user values can never ride along.
*/
function prefixDistance(tokens, commandId) {
const segments = commandId.split(":");
if (segments.length !== tokens.length) {
return null;
}
let total = 0;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i] ?? "";
const segmentDistance = distance(token, segments[i] ?? "");
if (segmentDistance > tokenThreshold(token)) {
return null;
}
total += segmentDistance;
}
return total > maxSuggestionDistance ? null : total;
}
/**
* Computes a suggestion for an unknown command id. Explicit aliases win over
* fuzzy matches, and longer token prefixes win over shorter ones so that
* `auth status` resolves before `auth`. Returns only the matched prefix as
* `attempted` (or the first token when nothing matches) so user-provided
* values never escape into messaging or telemetry.
*/
export function suggestCommand(id, commandIds) {
const tokens = extractCommandTokens(id);
if (tokens.length === 0) {
return { attempted: "", suggestion: null };
}
for (let length = tokens.length; length >= 1; length--) {
const attempted = tokens.slice(0, length).join(":");
const alias = aliasSuggestions.get(attempted);
if (alias) {
return { attempted, suggestion: alias };
}
}
let best;
for (let length = tokens.length; length >= 1; length--) {
const prefix = tokens.slice(0, length);
for (const commandId of commandIds) {
const prefixDist = prefixDistance(prefix, commandId);
if (prefixDist !== null && (!best || prefixDist < best.distance)) {
best = {
attempted: prefix.join(":"),
suggestion: commandId,
distance: prefixDist,
};
}
}
}
if (best) {
return { attempted: best.attempted, suggestion: best.suggestion };
}
return { attempted: tokens[0] ?? "", suggestion: null };
}