102eaa4543
Shares a saved session by id prefix or .jsonl path without launching the agent - same encrypted upload, store selection, and share.redactSecrets handling as the /share slash command.
309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
/**
|
|
* Top-level CLI command table.
|
|
*
|
|
* Lives in its own module (importable without side effects) so that tests can
|
|
* inspect the registered subcommands without triggering the side-effectful
|
|
* top-level await in `cli.ts`. Adding a new subcommand here is enough to make
|
|
* `runCli` route to it instead of forwarding the argv as a prompt to
|
|
* `launch` — see #1496 for the original "args silently leak to the LLM"
|
|
* regression that motivated the split.
|
|
*/
|
|
import type { CommandEntry } from "@oh-my-pi/pi-utils/cli";
|
|
import * as commandHelp from "./cli/command-help";
|
|
import { flagConsumesValue } from "./cli/flag-tables";
|
|
import { launchHelp } from "./commands/launch-help";
|
|
|
|
export const commands: CommandEntry[] = [
|
|
{ name: "launch", load: () => import("./commands/launch").then(m => m.default), help: launchHelp },
|
|
{
|
|
name: "acp",
|
|
load: () => import("./commands/acp").then(m => m.default),
|
|
help: commandHelp.acpHelp,
|
|
},
|
|
{
|
|
name: "auth-broker",
|
|
load: () => import("./commands/auth-broker").then(m => m.default),
|
|
help: commandHelp.authBrokerHelp,
|
|
},
|
|
{
|
|
name: "auth-gateway",
|
|
load: () => import("./commands/auth-gateway").then(m => m.default),
|
|
help: commandHelp.authGatewayHelp,
|
|
},
|
|
{
|
|
name: "agents",
|
|
load: () => import("./commands/agents").then(m => m.default),
|
|
help: commandHelp.agentsHelp,
|
|
},
|
|
{
|
|
name: "bench",
|
|
load: () => import("./commands/bench").then(m => m.default),
|
|
help: commandHelp.benchHelp,
|
|
},
|
|
{
|
|
name: "browser-relay",
|
|
load: () => import("./commands/browser-relay").then(m => m.default),
|
|
help: commandHelp.browserRelayHelp,
|
|
},
|
|
{
|
|
name: "cleanse",
|
|
load: () => import("./commands/cleanse").then(m => m.default),
|
|
help: commandHelp.cleanseHelp,
|
|
},
|
|
{
|
|
name: "commit",
|
|
load: () => import("./commands/commit").then(m => m.default),
|
|
help: commandHelp.commitHelp,
|
|
},
|
|
{
|
|
name: "completions",
|
|
load: () => import("./commands/completions").then(m => m.default),
|
|
help: commandHelp.completionsHelp,
|
|
},
|
|
{
|
|
name: "__complete",
|
|
load: () => import("./commands/complete").then(m => m.default),
|
|
help: commandHelp.completeHelp,
|
|
},
|
|
{
|
|
name: "config",
|
|
load: () => import("./commands/config").then(m => m.default),
|
|
help: commandHelp.configHelp,
|
|
},
|
|
{
|
|
name: "dry-balance",
|
|
load: () => import("./commands/dry-balance").then(m => m.default),
|
|
help: commandHelp.dryBalanceHelp,
|
|
},
|
|
{
|
|
name: "gc",
|
|
load: () => import("./commands/gc").then(m => m.default),
|
|
help: commandHelp.gcHelp,
|
|
},
|
|
{
|
|
name: "grep",
|
|
load: () => import("./commands/grep").then(m => m.default),
|
|
help: commandHelp.grepHelp,
|
|
},
|
|
{
|
|
name: "gallery",
|
|
load: () => import("./commands/gallery").then(m => m.default),
|
|
help: commandHelp.galleryHelp,
|
|
},
|
|
{
|
|
name: "grievances",
|
|
load: () => import("./commands/grievances").then(m => m.default),
|
|
help: commandHelp.grievancesHelp,
|
|
},
|
|
{
|
|
name: "install",
|
|
load: () => import("./commands/install").then(m => m.default),
|
|
help: commandHelp.installHelp,
|
|
},
|
|
{
|
|
name: "join",
|
|
load: () => import("./commands/join").then(m => m.default),
|
|
help: commandHelp.joinHelp,
|
|
},
|
|
{
|
|
name: "models",
|
|
load: () => import("./commands/models").then(m => m.default),
|
|
help: commandHelp.modelsHelp,
|
|
},
|
|
{
|
|
name: "plugin",
|
|
load: () => import("./commands/plugin").then(m => m.default),
|
|
help: commandHelp.pluginHelp,
|
|
},
|
|
{
|
|
name: "say",
|
|
load: () => import("./commands/say").then(m => m.default),
|
|
help: commandHelp.sayHelp,
|
|
},
|
|
{
|
|
name: "share",
|
|
load: () => import("./commands/share").then(m => m.default),
|
|
help: commandHelp.shareHelp,
|
|
},
|
|
{
|
|
name: "setup",
|
|
load: () => import("./commands/setup").then(m => m.default),
|
|
help: commandHelp.setupHelp,
|
|
},
|
|
{
|
|
name: "shell",
|
|
load: () => import("./commands/shell").then(m => m.default),
|
|
help: commandHelp.shellHelp,
|
|
},
|
|
{
|
|
name: "read",
|
|
load: () => import("./commands/read").then(m => m.default),
|
|
help: commandHelp.readHelp,
|
|
},
|
|
{
|
|
name: "ssh",
|
|
load: () => import("./commands/ssh").then(m => m.default),
|
|
help: commandHelp.sshHelp,
|
|
},
|
|
{
|
|
name: "stats",
|
|
load: () => import("./commands/stats").then(m => m.default),
|
|
help: commandHelp.statsHelp,
|
|
},
|
|
{
|
|
name: "update",
|
|
load: () => import("./commands/update").then(m => m.default),
|
|
help: commandHelp.updateHelp,
|
|
},
|
|
{
|
|
name: "usage",
|
|
load: () => import("./commands/usage").then(m => m.default),
|
|
help: commandHelp.usageHelp,
|
|
},
|
|
{
|
|
name: "tiny-models",
|
|
load: () => import("./commands/tiny-models").then(m => m.default),
|
|
help: commandHelp.tinyModelsHelp,
|
|
},
|
|
{
|
|
name: "token",
|
|
load: () => import("./commands/token").then(m => m.default),
|
|
help: commandHelp.tokenHelp,
|
|
},
|
|
{
|
|
name: "ttsr",
|
|
load: () => import("./commands/ttsr").then(m => m.default),
|
|
help: commandHelp.ttsrHelp,
|
|
},
|
|
{
|
|
name: "worktree",
|
|
load: () => import("./commands/worktree").then(m => m.default),
|
|
aliases: ["wt"],
|
|
help: commandHelp.worktreeHelp,
|
|
},
|
|
{
|
|
name: "search",
|
|
load: () => import("./commands/web-search").then(m => m.default),
|
|
aliases: ["q"],
|
|
help: commandHelp.searchHelp,
|
|
},
|
|
];
|
|
|
|
// Documented-looking plugin/marketplace verbs that are NOT registered top-level
|
|
// commands. Without a guard `resolveCliArgv` rewrites e.g. `omp marketplace add
|
|
// xyz` to `omp launch marketplace add xyz`, silently forwarding the argv to the
|
|
// model as a prompt instead of managing plugins (#4845; same class as the
|
|
// `list`/`remove` leak fixed in #2935 and the `install` leak in #1496/#1498).
|
|
// The real commands live under `omp plugin <action>`; each entry maps a verb to
|
|
// a hint pointing there. See {@link reservedTopLevelWordMessage} for when a hint
|
|
// fires vs. when the argv still falls through to `launch`.
|
|
const RESERVED_TOP_LEVEL_WORDS: Record<string, string> = {
|
|
extensions:
|
|
'`omp extensions` is not a management command. Use `omp plugin list` / `omp plugin install`, or run `omp launch extensions` if you meant to send "extensions" as a prompt.',
|
|
list: '`omp list` is not a top-level command. Use `omp plugin list` to list installed plugins, or run `omp launch list` if you meant to send "list" as a prompt.',
|
|
remove:
|
|
'`omp remove` is not a top-level command. Use `omp plugin uninstall <name>` to remove a plugin, or run `omp launch remove` if you meant to send "remove" as a prompt.',
|
|
uninstall:
|
|
'`omp uninstall` is not a top-level command. Use `omp plugin uninstall <name@marketplace>` to remove a plugin, or run `omp launch uninstall` if you meant to send "uninstall" as a prompt.',
|
|
marketplace:
|
|
'`omp marketplace` is not a top-level command. Use `omp plugin marketplace <add|remove|update|list>` to manage marketplaces, or run `omp launch marketplace` if you meant to send "marketplace" as a prompt.',
|
|
discover:
|
|
'`omp discover` is not a top-level command. Use `omp plugin discover [marketplace]` to browse available plugins, or run `omp launch discover` if you meant to send "discover" as a prompt.',
|
|
upgrade:
|
|
'`omp upgrade` is not a top-level command. Use `omp plugin upgrade [name@marketplace]` to upgrade plugins, or run `omp launch upgrade` if you meant to send "upgrade" as a prompt.',
|
|
enable:
|
|
'`omp enable` is not a top-level command. Use `omp plugin enable <name@marketplace>` to enable a plugin, or run `omp launch enable` if you meant to send "enable" as a prompt.',
|
|
disable:
|
|
'`omp disable` is not a top-level command. Use `omp plugin disable <name@marketplace>` to disable a plugin, or run `omp launch disable` if you meant to send "disable" as a prompt.',
|
|
};
|
|
|
|
// Sub-actions that make `omp marketplace <sub>` unambiguously a management
|
|
// command even when multi-word (the reporter's `omp marketplace add xyz`,
|
|
// #4845). Mirrors the switch in `handleMarketplace` (cli/plugin-cli.ts).
|
|
const MARKETPLACE_SUBCOMMANDS: Record<string, true> = { add: true, remove: true, rm: true, update: true, list: true };
|
|
|
|
/**
|
|
* Hint for a reserved plugin/marketplace verb used as a top-level command, or
|
|
* `undefined` when the argv should fall through to `launch`.
|
|
*
|
|
* A bare verb (`omp marketplace`) always hints. A multi-word invocation only
|
|
* hints when the arguments follow the documented plugin grammar — a marketplace
|
|
* sub-action (`omp marketplace add …`) or a `name@marketplace` plugin id
|
|
* (`omp uninstall foo@bar`) — so genuine prompts that merely begin with one of
|
|
* these words (`omp list all my files`, `omp upgrade the deps`) still launch.
|
|
*
|
|
* Flags (`-…`) and `@file` arguments in the verb slot are never management
|
|
* commands; those fall through to the default `launch` command.
|
|
*/
|
|
export function reservedTopLevelWordMessage(argv: readonly string[]): string | undefined {
|
|
const first = argv[0];
|
|
if (!first || first.startsWith("-") || first.startsWith("@")) return undefined;
|
|
const hint = RESERVED_TOP_LEVEL_WORDS[first];
|
|
if (!hint) return undefined;
|
|
const second = argv[1];
|
|
if (second === undefined) return hint;
|
|
if (first === "marketplace" && MARKETPLACE_SUBCOMMANDS[second]) return hint;
|
|
for (let index = 1; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (!arg.startsWith("-") && arg.includes("@")) return hint;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Return true when `first` matches a registered subcommand name or alias.
|
|
*
|
|
* Flags (`-…`) and `@file` arguments are never subcommands; for those the CLI
|
|
* runner skips ahead to the default `launch` command.
|
|
*/
|
|
export function isSubcommand(first: string | undefined): boolean {
|
|
if (!first || first.startsWith("-") || first.startsWith("@")) return false;
|
|
return commands.some(entry => entry.name === first || entry.aliases?.includes(first));
|
|
}
|
|
|
|
export type ResolvedCliArgv = { argv: string[] } | { error: string };
|
|
|
|
/**
|
|
* Index of the first argv token that names a registered subcommand, skipping
|
|
* leading global option flags (and any value they consume) with the same
|
|
* contract as the launch parser ({@link flagConsumesValue}). Returns -1 when
|
|
* scanning hits a non-subcommand positional, an end-of-options `--`, or the end
|
|
* of argv first.
|
|
*/
|
|
function leadingSubcommandIndex(argv: string[]): number {
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
if (arg === "--") return -1;
|
|
if (!arg.startsWith("-")) return isSubcommand(arg) ? index : -1;
|
|
if (flagConsumesValue(arg, argv[index + 1])) index += 1;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Decide what the CLI runner should do with raw argv: reject bare reserved
|
|
* management words, pass help/version through untouched, route a recognized
|
|
* subcommand (even behind leading global flags like `--approval-mode=yolo`) to
|
|
* that command with the flags preserved, and forward everything else to
|
|
* `launch` (#2970).
|
|
*/
|
|
export function resolveCliArgv(argv: string[]): ResolvedCliArgv {
|
|
const first = argv[0];
|
|
const reservedMessage = reservedTopLevelWordMessage(argv);
|
|
if (reservedMessage) return { error: reservedMessage };
|
|
if (first === "--help" || first === "-h" || first === "--version" || first === "-v" || first === "help") {
|
|
return { argv };
|
|
}
|
|
if (isSubcommand(first)) return { argv };
|
|
// A subcommand can hide behind leading global option flags
|
|
// (`omp --approval-mode=yolo acp`). `run` dispatches strictly on argv[0], so
|
|
// hoist the subcommand to the front and keep the leading flags as its own
|
|
// argv; the command's parser then applies them. Genuine launch prompts (no
|
|
// trailing subcommand) are untouched.
|
|
const subIndex = leadingSubcommandIndex(argv);
|
|
if (subIndex >= 0) {
|
|
return { argv: [argv[subIndex], ...argv.slice(0, subIndex), ...argv.slice(subIndex + 1)] };
|
|
}
|
|
return { argv: ["launch", ...argv] };
|
|
}
|