mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
* Add Qoder session history and search with real CLI coverage * Allow the real Qoder marker file to end with a newline * Keep Qoder tool output out of history previews and search * Keep Qoder search pages readable by older clients * Verify persisted Qoder history after a real generated and resumed task * Negotiate Qoder filters before searching an older execution host * Combine search client imports for the CI plugin gate * Keep the relay search oracle aligned with legacy agent filtering * Register supervised Qoder China and Qwen lifecycle integration * Cover Qoder China mobile assets and mixed-host resume gates * Verify Qoder provider tags against the older released wire parser * Verify China and Qwen keep independent Windows hook scripts * Verify Qoder registrations against the installed older Windows release * test(qoder): align search capability contracts and pin old-host fencing * fix(qoder): rank exact picker identities and command aliases first * test(qoder): preserve the regional CLI shared icon expectation Keep the full bundled-asset and no-remote-image checks, with an explicit shared-logo basename for Qoder China. The map also works with older catalog type unions. * fix(qoder): align China catalog entry with fallback order --------- Co-authored-by: Orca Integration Recovery <orca-validation@invalid.example>
238 lines
8.8 KiB
TypeScript
238 lines
8.8 KiB
TypeScript
import { getTuiAgentDetectCommands, TUI_AGENT_CONFIG } from './tui-agent-config'
|
|
import { EXACT_NODE_ENTRYPOINT_IDENTITIES } from './agent-node-entrypoint-identities'
|
|
import { NODE_PACKAGE_SCRIPT_ENTRYPOINTS } from './agent-node-package-entrypoints'
|
|
import type { AgentType } from './agent-status-types'
|
|
import type { TuiAgent } from './tui-agent'
|
|
import { filterHeadlessOneShotAgentCommand } from './agent-headless-command'
|
|
import { getFirstCommandToken } from './command-token-scanner'
|
|
import {
|
|
comparablePath,
|
|
findInterpreterEntrypointToken,
|
|
PYTHON_PROCESS_RE,
|
|
tokenizeCommandLine
|
|
} from './agent-command-line-entrypoint'
|
|
import { isFreshOmpLaunchCommand } from './omp-fresh-launch'
|
|
|
|
export type RecognizedAgentProcess = { agent: TuiAgent; processName: string }
|
|
|
|
const PROCESS_EXTENSION_RE = /\.(?:exe|cmd|bat|ps1)$/i
|
|
const INTERPRETER_SCRIPT_EXTENSION_RE = /\.(?:js|mjs|cjs)$/i
|
|
const PYTHON_SCRIPT_EXTENSION_RE = /\.(?:py|pyw)$/i
|
|
|
|
function normalizeProcessName(
|
|
processName: string | null | undefined,
|
|
options: { stripInterpreterScriptExtension?: boolean } = {}
|
|
): string {
|
|
if (!processName) {
|
|
return ''
|
|
}
|
|
const unquoted = processName.trim().replace(/^["']|["']$/g, '')
|
|
const basename = unquoted.split(/[\\/]/).pop() ?? unquoted
|
|
const withoutProcessExtension = basename.toLowerCase().replace(PROCESS_EXTENSION_RE, '')
|
|
if (options.stripInterpreterScriptExtension === true) {
|
|
return withoutProcessExtension.replace(INTERPRETER_SCRIPT_EXTENSION_RE, '')
|
|
}
|
|
return withoutProcessExtension
|
|
}
|
|
|
|
const FOREGROUND_AGENT_WRAPPER_PROCESS_NAMES = new Set(['node', 'python', 'python3'])
|
|
const PYTHON_SCRIPT_ENTRYPOINT_DIRECTORIES = ['/bin/', '/scripts/', '/site-packages/']
|
|
|
|
const PROCESS_TO_AGENT = new Map<string, TuiAgent>()
|
|
const AGENT_TYPE_IDS = new Set<TuiAgent>()
|
|
|
|
for (const [agent, config] of Object.entries(TUI_AGENT_CONFIG) as [
|
|
TuiAgent,
|
|
(typeof TUI_AGENT_CONFIG)[TuiAgent]
|
|
][]) {
|
|
AGENT_TYPE_IDS.add(agent)
|
|
for (const candidate of [
|
|
config.expectedProcess,
|
|
...getTuiAgentDetectCommands(config),
|
|
getFirstCommandToken(config.launchCmd)
|
|
]) {
|
|
const normalized = normalizeProcessName(candidate)
|
|
if (normalized) {
|
|
// Why: claude-agent-teams is an Orca wrapper whose child process is the
|
|
// real `claude` binary. Do not let wrapper configs overwrite canonical
|
|
// CLI ownership for the same foreground process name.
|
|
if (!PROCESS_TO_AGENT.has(normalized)) {
|
|
PROCESS_TO_AGENT.set(normalized, agent)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function agentForNormalizedProcess(normalized: string): TuiAgent | undefined {
|
|
const exact = PROCESS_TO_AGENT.get(normalized)
|
|
if (exact) {
|
|
return exact
|
|
}
|
|
// Why: node-pty can report Codex's packaged platform binary
|
|
// (for example codex-aarch64-ap) instead of the launch command.
|
|
if (normalized.startsWith('codex-')) {
|
|
return PROCESS_TO_AGENT.get('codex')
|
|
}
|
|
// Qoder's launcher resolves to a versioned native binary.
|
|
if (/^(?:qoderclicn|qodercn)-\d/.test(normalized)) {
|
|
return PROCESS_TO_AGENT.get('qoderclicn')
|
|
}
|
|
if (/^qodercli-\d/.test(normalized)) {
|
|
return PROCESS_TO_AGENT.get('qodercli')
|
|
}
|
|
if (normalized.startsWith('grok-')) {
|
|
return PROCESS_TO_AGENT.get('grok')
|
|
}
|
|
// Why: the `muse` launcher script execs a versioned `muse-bin-<version>` binary, so
|
|
// the foreground name never equals `muse` itself. The `muse-bin-` prefix also covers
|
|
// comm-truncated rows (`muse-bin-1.0.3-R`) without matching unrelated `muse-*` tools.
|
|
if (normalized.startsWith('muse-bin-')) {
|
|
return PROCESS_TO_AGENT.get('muse')
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function recognizedAgentForProcess(normalized: string): RecognizedAgentProcess | null {
|
|
const agent = agentForNormalizedProcess(normalized)
|
|
return agent ? { agent, processName: normalized } : null
|
|
}
|
|
|
|
function recognizeNodeScriptEntrypoint(token: string): RecognizedAgentProcess | null {
|
|
const path = comparablePath(token)
|
|
for (const identity of EXACT_NODE_ENTRYPOINT_IDENTITIES) {
|
|
if (identity.pattern.test(path)) {
|
|
return { agent: identity.agent, processName: identity.processName }
|
|
}
|
|
}
|
|
const normalized = normalizeProcessName(token, { stripInterpreterScriptExtension: true })
|
|
const markers = NODE_PACKAGE_SCRIPT_ENTRYPOINTS[normalized]
|
|
if (!markers) {
|
|
return null
|
|
}
|
|
if (!markers.some((marker) => path.includes(marker))) {
|
|
return null
|
|
}
|
|
return recognizedAgentForProcess(normalized)
|
|
}
|
|
|
|
function recognizePythonModule(
|
|
moduleName: string | null | undefined
|
|
): RecognizedAgentProcess | null {
|
|
if (!moduleName || moduleName.startsWith('-')) {
|
|
return null
|
|
}
|
|
const normalized = moduleName.split('.', 1)[0]?.toLowerCase() ?? ''
|
|
return recognizedAgentForProcess(normalized)
|
|
}
|
|
|
|
function recognizePythonScriptEntrypoint(token: string): RecognizedAgentProcess | null {
|
|
const path = comparablePath(token)
|
|
if (!PYTHON_SCRIPT_EXTENSION_RE.test(path)) {
|
|
return null
|
|
}
|
|
if (!PYTHON_SCRIPT_ENTRYPOINT_DIRECTORIES.some((marker) => path.includes(marker))) {
|
|
return null
|
|
}
|
|
const basename = path.split('/').pop() ?? ''
|
|
const normalized = basename.replace(PYTHON_SCRIPT_EXTENSION_RE, '')
|
|
return recognizedAgentForProcess(normalized)
|
|
}
|
|
|
|
function recognizePythonEntrypoint(
|
|
tokens: string[],
|
|
entrypoint: string
|
|
): RecognizedAgentProcess | null {
|
|
const moduleFlagIndex = tokens.indexOf('-m')
|
|
if (moduleFlagIndex > 0) {
|
|
return recognizePythonModule(tokens[moduleFlagIndex + 1])
|
|
}
|
|
return recognizeAgentProcess(entrypoint) ?? recognizePythonScriptEntrypoint(entrypoint)
|
|
}
|
|
|
|
// Why: `muse` execs a versioned `muse-bin-<version>` binary (see above), so the
|
|
// exact-name check never matches and readiness/follow-up delivery would stall.
|
|
// Scoped to muse: a generic `-suffix` rule would misclassify short agent names
|
|
// (see the ante-obsidian test).
|
|
export function isExpectedAgentProcess(
|
|
processName: string | null | undefined,
|
|
expectedProcess: string
|
|
): boolean {
|
|
const normalizedProcess = normalizeProcessName(processName)
|
|
const normalizedExpected = normalizeProcessName(expectedProcess)
|
|
if (!normalizedProcess || !normalizedExpected) {
|
|
return false
|
|
}
|
|
return (
|
|
normalizedProcess === normalizedExpected ||
|
|
normalizedProcess.startsWith(`${normalizedExpected}.`) ||
|
|
(['qoderclicn', 'qodercn'].includes(normalizedExpected) &&
|
|
/^(?:qoderclicn|qodercn)(?:-\d.*)?$/.test(normalizedProcess)) ||
|
|
(normalizedExpected === 'qodercli' && /^qodercli-\d/.test(normalizedProcess)) ||
|
|
(normalizedExpected === 'muse' && normalizedProcess.startsWith('muse-bin-'))
|
|
)
|
|
}
|
|
|
|
export function recognizeAgentProcess(
|
|
processName: string | null | undefined
|
|
): RecognizedAgentProcess | null {
|
|
const normalized = normalizeProcessName(processName)
|
|
return recognizedAgentForProcess(normalized)
|
|
}
|
|
|
|
export function recognizeAgentProcessFromCommandLine(
|
|
commandLine: string | null | undefined,
|
|
// Why: TUI consumers (status hooks, shell shadows) filter out headless
|
|
// one-shots (`claude -p …`); non-interactivity guards include them — a
|
|
// one-shot agent can't answer a prompt either.
|
|
options?: { includeHeadlessOneShot?: boolean }
|
|
): RecognizedAgentProcess | null {
|
|
if (!commandLine) {
|
|
return null
|
|
}
|
|
if (isFreshOmpLaunchCommand(commandLine)) {
|
|
return recognizedAgentForProcess('omp')
|
|
}
|
|
const keep = options?.includeHeadlessOneShot === true
|
|
const tokens = tokenizeCommandLine(commandLine)
|
|
const firstNormalized = normalizeProcessName(tokens[0])
|
|
let direct = recognizedAgentForProcess(firstNormalized)
|
|
// Why: the generic Orca CLI is not an agent; only this subcommand launches its TUI mode.
|
|
if (direct?.agent === 'claude-agent-teams' && tokens[1]?.toLowerCase() !== 'claude-teams') {
|
|
direct = null
|
|
}
|
|
const directRecognition = keep ? direct : filterHeadlessOneShotAgentCommand(direct, tokens)
|
|
if (directRecognition) {
|
|
return directRecognition
|
|
}
|
|
const entrypoint = findInterpreterEntrypointToken(tokens, firstNormalized)
|
|
if (!entrypoint) {
|
|
return null
|
|
}
|
|
const viaEntrypoint = PYTHON_PROCESS_RE.test(firstNormalized)
|
|
? recognizePythonEntrypoint(tokens, entrypoint)
|
|
: (recognizeAgentProcess(entrypoint) ?? recognizeNodeScriptEntrypoint(entrypoint))
|
|
if (
|
|
viaEntrypoint?.agent === 'claude-agent-teams' &&
|
|
tokens[tokens.indexOf(entrypoint, 1) + 1]?.toLowerCase() !== 'claude-teams'
|
|
) {
|
|
return null
|
|
}
|
|
return keep ? viaEntrypoint : filterHeadlessOneShotAgentCommand(viaEntrypoint, tokens)
|
|
}
|
|
export function isAgentForegroundWrapperProcess(processName: string | null | undefined): boolean {
|
|
const normalized = normalizeProcessName(processName)
|
|
return (
|
|
FOREGROUND_AGENT_WRAPPER_PROCESS_NAMES.has(normalized) || PYTHON_PROCESS_RE.test(normalized)
|
|
)
|
|
}
|
|
|
|
export function isRecognizedAgentType(agentType: AgentType | null | undefined): boolean {
|
|
if (typeof agentType !== 'string') {
|
|
return false
|
|
}
|
|
return (
|
|
AGENT_TYPE_IDS.has(agentType as TuiAgent) ||
|
|
agentForNormalizedProcess(normalizeProcessName(agentType)) !== undefined
|
|
)
|
|
}
|