feat(agents): add Prime Agent as a supported TUI agent with session history (#12935)

* feat(agents): add Prime Agent as a supported TUI agent with session history

Wire Prime Intellect's prime-agent CLI (a Pi fork) into the desktop and
mobile agent catalogs following the Trae registration pattern, and into
the Agent Session History browser following the OMP pattern:

- types.ts, tui-agent-config.ts: register 'prime-agent' with argv prompt
  injection behind a `--` separator (its own help documents `--` as
  "treat all following arguments as messages"; without it, prompts
  starting with `help`/`agents`/`-…` dispatch as subcommands or
  flags), plus csi-u Shift+Enter encoding matching the Pi TUI it embeds.
- agent-kind.ts, telemetry-events.ts, agent-status-types.ts,
  agent-type-label.ts, tui-agent-display-names.ts,
  tui-agent-selection.ts, skills-cli-agent-keys.ts: standard per-agent
  registrations.
- agent-headless-command.ts: `-p/--print` one-shot runs share the
  print-mode matcher with Claude/Trae so they are not mistaken for live
  interactive panes.
- agent-process-recognition: the npm shim launches a generic bundled
  cli.js, so only the exact package path is an authoritative identity
  (same as Pi and cursor-agent). The three per-agent regex branches are
  now one table in agent-node-entrypoint-identities.ts — the module was
  at its max-lines budget and a table makes the next agent one entry.
- AI Vault: sessions are Pi's message-graph JSONL under
  ~/.prime/agent/sessions (override PRIME_AGENT_CODING_AGENT_DIR —
  Prime Agent brands Pi's env contract instead of sharing
  PI_CODING_AGENT_DIR); parsed by the shared message-graph parser with
  incremental append-resume; discovered locally, in WSL homes, and over
  remote SSH; resumes by absolute transcript path
  (`prime-agent --resume <path>`) like OMP, with session-id fallback.
- skill-discovery-sources.ts: ~/.prime/agent/skills home source.
- Catalog, i18n (en/es/ja/ko/zh), mobile registries, and a bundled
  64x64 favicon (required by mobile's offline-icon invariant).

Scanner-test fixtures for OMP and Prime Agent move into
session-scanner-test-fixtures.ts and the incremental fixture into its
own module, keeping every touched file inside its max-lines budget
without ratchet bumps.

* fix(ai-vault): map custom Prime Agent roots to their sessions child

PRIME_AGENT_CODING_AGENT_DIR is consumed verbatim by the CLI as its agent
config dir, with transcripts always in <agentDir>/sessions — unlike
PI_CODING_AGENT_DIR's <home>/agent/sessions shape the shared normalizer
models. A custom root with a non-special basename (or a `.prime` leaf)
was therefore scanned as-is instead of its sessions child. Dedicated
normalizePrimeAgentSessionsDir appends `sessions` to every configured
root, taking only an explicit `.../sessions` path as-is; the shared
Pi/OMP normalizer drops the `.prime` widening it no longer needs.

Raised in review on #12935.

* fix(ai-vault): guard degenerate Prime Agent roots and cover the remote source

normalizePrimeAgentSessionsDir stripped a filesystem-root value ('/' or '//')
to '', which then joined into the relative root 'sessions' and would walk the
main-process cwd. session-scanner-roots.ts already carries this guard for the
OMP variant; apply the same fallback here.

The remote SSH source had no test: deleting jsonlSource('prime-agent', ...)
left the suite green, unlike the local path which is pinned by the
AI_VAULT_AGENTS exhaustiveness assertion in session-scanner.test.ts. Add a
case that fixes the .prime/agent/sessions root segments, the .jsonl
extension, and parser routing.

Raised in review on #12935.

* fix(ai-vault): honor Prime Agent's sessions-root env and non-interactive modes

Verified against upstream PrimeIntellect-ai/prime-agent source rather than
inferred from the CLI's help text.

config.ts getSessionsDir() reads PRIME_AGENT_SESSION_DIR (and its legacy
PRIME_AGENT_CODING_AGENT_SESSION_DIR alias) ahead of the agent dir and uses it
verbatim; setting either left the vault silently empty. It also appends
`sessions` to the agent dir unconditionally, with no basename escape hatch, so
PRIME_AGENT_CODING_AGENT_DIR=/data/sessions writes to /data/sessions/sessions
while Orca scanned /data/sessions. getAgentDir() and the session-dir override
both run through expandTildePath, so a `~` value set outside a shell resolves.

cli/args.ts also spells the non-interactive runs `--mode json|rpc|acp|daemon`,
which the shared print-mode matcher does not know, so those panes were counted
as live interactive agents and the paste-submit path would write user text into
a JSON-RPC/ACP stream. Match upstream exactly: only the space-separated form,
since `--mode=json` is not parsed by the CLI and does start the TUI.

Raised in review on #12935.

* fix(ai-vault): keep Prime Agent roots absolute and remote segments posix

Two holes in the previous commit.

The degenerate-root guard only rejected pure-separator values, so a relative
env value still resolved against the main-process cwd:
PRIME_AGENT_CODING_AGENT_DIR='.' scanned '<cwd>/sessions' and, worse,
PRIME_AGENT_SESSION_DIR='.' scanned the cwd itself. Require an absolute path in
both branches and fall back to the default otherwise.

remotePrimeAgentSessionsSegments() built its segments with the local-platform
join, so on a Windows client scanning a posix SSH host it produced
'\.prime\agent\sessions' and split('/') collapsed it to one bogus segment —
remote discovery would have found nothing. Remote roots are posix regardless of
client platform, so keep them literal. Pi and OMP are unaffected: their
normalizer returns a '.../sessions' input unchanged and never joins.

Raised in review on #12935.

* test(ai-vault): pin Windows drive roots to the Prime Agent default fallback

'C:\' and 'C:/' strip to the drive-relative 'C:', which isAbsolute
rejects on every platform — assert they land in the default fallback so
a looser truthiness check can't reintroduce a 'C:sessions' scan root.

Raised in review on #12935.

* test(ai-vault): pin the drive-relative root form and state what the posix runner can assert

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
This commit is contained in:
Evgenii 2026-08-10 04:50:02 +07:00 committed by GitHub
parent 45c1cb979a
commit 63af0bdd24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 624 additions and 71 deletions

View File

@ -10279,7 +10279,7 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
'@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@6.0.3)
@ -10299,7 +10299,7 @@ snapshots:
dependencies:
'@typescript-eslint/scope-manager': 8.59.2
'@typescript-eslint/types': 8.59.2
'@typescript-eslint/typescript-estree': 8.59.2(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3)
'@typescript-eslint/visitor-keys': 8.59.2
debug: 4.4.3
eslint: 9.39.4
@ -10316,6 +10316,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.59.2(typescript@6.0.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3)
'@typescript-eslint/types': 8.59.2
debug: 4.4.3
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.59.2':
dependencies:
'@typescript-eslint/types': 8.59.2
@ -10325,6 +10334,10 @@ snapshots:
dependencies:
typescript: 5.9.3
'@typescript-eslint/tsconfig-utils@8.59.2(typescript@6.0.3)':
dependencies:
typescript: 6.0.3
'@typescript-eslint/type-utils@8.59.2(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.59.2
@ -10354,6 +10367,21 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3)':
dependencies:
'@typescript-eslint/project-service': 8.59.2(typescript@6.0.3)
'@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3)
'@typescript-eslint/types': 8.59.2
'@typescript-eslint/visitor-keys': 8.59.2
debug: 4.4.3
minimatch: 10.2.5
semver: 7.7.4
tinyglobby: 0.2.17
ts-api-utils: 2.5.0(typescript@6.0.3)
typescript: 6.0.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.59.2(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4)
@ -11633,11 +11661,11 @@ snapshots:
eslint-config-universe@15.0.4(eslint@9.39.4)(prettier@2.8.8)(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/eslint-plugin': 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@5.9.3)
'@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@6.0.3)
eslint: 9.39.4
eslint-config-prettier: 9.1.2(eslint@9.39.4)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)
eslint-plugin-n: 17.24.0(eslint@9.39.4)(typescript@5.9.3)
eslint-plugin-node: 11.1.0(eslint@9.39.4)
eslint-plugin-prettier: 5.5.5(eslint-config-prettier@9.1.2(eslint@9.39.4))(eslint@9.39.4)(prettier@2.8.8)
@ -11661,7 +11689,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4):
dependencies:
debug: 3.2.7
optionalDependencies:
@ -11684,7 +11712,7 @@ snapshots:
eslint-utils: 2.1.0
regexpp: 3.2.0
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@ -11695,7 +11723,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.4
eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4)
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@9.39.4)
hasown: 2.0.3
is-core-module: 2.16.2
is-glob: 4.0.3
@ -14948,6 +14976,10 @@ snapshots:
dependencies:
typescript: 5.9.3
ts-api-utils@2.5.0(typescript@6.0.3):
dependencies:
typescript: 6.0.3
ts-declaration-location@1.0.7(typescript@5.9.3):
dependencies:
picomatch: 4.0.4

View File

@ -17,6 +17,7 @@ export const MOBILE_AGENT_ICON_ASSETS: Partial<Record<TuiAgent, ImageSourcePropT
'mimo-code': require('../../../src/shared/agent-icons/mimo-code.png'),
ante: require('../../../src/shared/agent-icons/ante.png'),
trae: require('../../../src/shared/agent-icons/trae.png'),
'prime-agent': require('../../../src/shared/agent-icons/prime-agent.png'),
gemini: require('../../../src/shared/agent-icons/gemini.png'),
antigravity: require('../../../src/shared/agent-icons/antigravity.png'),
goose: require('../../../src/shared/agent-icons/goose.png'),

View File

@ -16,6 +16,7 @@ export const MOBILE_TUI_AGENT_AUTO_PICK_ORDER = [
'trae',
'pi',
'omp',
'prime-agent',
'gemini',
'antigravity',
'aider',
@ -54,6 +55,7 @@ export const MOBILE_TUI_AGENT_LABELS: Record<TuiAgent, string> = {
trae: 'Trae',
pi: 'Pi',
omp: 'OMP',
'prime-agent': 'Prime Agent',
gemini: 'Gemini',
antigravity: 'Antigravity',
aider: 'Aider',
@ -88,6 +90,7 @@ export const MOBILE_TUI_AGENT_FAVICON_DOMAINS: Partial<Record<TuiAgent, string>>
ante: 'antigma.ai',
trae: 'www.trae.cn',
omp: 'omp.sh',
'prime-agent': 'primeintellect.ai',
gemini: 'gemini.google.com',
antigravity: 'antigravity.google',
goose: 'goose-docs.ai',

View File

@ -101,6 +101,13 @@ export function remoteSessionSources(
// partition keeps the children out of the top-level list (#9330).
partitionSubagentTranscripts: partitionOmpSubagentTranscriptPaths
},
jsonlSource(
'prime-agent',
remoteHome,
hostPlatform,
remotePrimeAgentSessionsSegments(),
primeAgentParser
),
jsonlSource(
'droid',
remoteHome,
@ -264,6 +271,16 @@ function ompParser(
return parseMessageGraphSessionContent('omp', file, content, platform, options, signal)
}
function primeAgentParser(
file: FileWithMtime,
content: string,
platform: NodeJS.Platform,
options: RemoteParserOptions,
signal?: AbortSignal
): Promise<AiVaultSession | null> {
return parseMessageGraphSessionContent('prime-agent', file, content, platform, options, signal)
}
function openClawParser(
file: FileWithMtime,
content: string,
@ -285,3 +302,10 @@ function remotePiSessionsSegments(): string[] {
function remoteOmpSessionsSegments(): string[] {
return normalizeAgentSessionsDir('/.omp/agent/sessions', '.omp').split('/').filter(Boolean)
}
// Why: remote roots are posix regardless of the client platform, so these stay literal
// rather than round-tripping through a local-platform path join that would emit
// backslashes on a Windows client and collapse into a single bogus segment.
function remotePrimeAgentSessionsSegments(): string[] {
return ['.prime', 'agent', 'sessions']
}

View File

@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { getRemoteHostPlatform } from '../ssh/ssh-remote-platform'
import { scanRemoteAiVaultSessions } from './remote-session-scanner'
import { MemoryRemoteProvider, jsonLines } from './remote-session-scanner-test-fixtures'
import { primeAgentFixture } from './session-scanner-prime-agent-fixtures'
describe('scanRemoteAiVaultSessions', () => {
it('parses remote default and Orca-managed Codex homes with SSH host ids', async () => {
@ -160,6 +161,34 @@ describe('scanRemoteAiVaultSessions', () => {
})
})
it('discovers Prime Agent transcripts under the remote home sessions root', async () => {
const provider = new MemoryRemoteProvider()
const fixture = primeAgentFixture()
provider.addFile(
`/home/ada/.prime/agent/sessions/${fixture.fileName}`,
[...fixture.seedLines, ...fixture.appendLines].join('\n'),
40
)
const result = await scanRemoteAiVaultSessions({
provider,
executionHostId: 'ssh:dev-box',
remoteHome: '/home/ada',
hostPlatform: getRemoteHostPlatform('linux-x64')
})
expect(result.issues).toEqual([])
expect(result.sessions).toHaveLength(1)
expect(result.sessions[0]).toMatchObject({
executionHostId: 'ssh:dev-box',
executionHostPlatform: 'linux',
agent: 'prime-agent',
title: 'prime-agent seed question',
model: 'inference/big-model',
filePath: `/home/ada/.prime/agent/sessions/${fixture.fileName}`
})
})
it('parses only canonical Antigravity transcripts on SSH hosts', async () => {
const provider = new MemoryRemoteProvider()
const sessionId = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'

View File

@ -68,6 +68,8 @@ export async function parseAgentSessionFile(
return parseMessageGraphSessionFile('pi', candidate.file, platform)
case 'omp':
return parseMessageGraphSessionFile('omp', candidate.file, platform)
case 'prime-agent':
return parseMessageGraphSessionFile('prime-agent', candidate.file, platform)
case 'droid':
return parseDroidSessionFile(candidate.file, platform)
case 'devin':

View File

@ -9,7 +9,7 @@ import { OMP_SESSION_ARTIFACT_DIR_PATTERN } from './session-scanner-omp-subagent
import { claudeProjectsRootDirs, OMP_SESSIONS_DIR, sessionRootDirs } from './session-scanner-roots'
import { SUBAGENT_DIR_NAME } from './session-scanner-subagent-transcripts'
import type { AiVaultScanOptions } from './session-scanner-types'
import { normalizeAgentSessionsDir } from './session-scanner-values'
import { normalizeAgentSessionsDir, primeAgentSessionsDirFromEnv } from './session-scanner-values'
export const DEFAULT_CODEX_HOME_DIR = join(homedir(), '.codex')
const CODEX_SESSIONS_DIR = join(
@ -29,6 +29,10 @@ const PI_SESSIONS_DIR = normalizeAgentSessionsDir(
process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), '.pi', 'agent', 'sessions'),
'.pi'
)
// Why: Prime Agent brands Pi's env contract with its own prefix and adds a
// dedicated sessions-root override, so resolution differs from Pi/OMP in shape
// as well as in variable name.
const PRIME_AGENT_SESSIONS_DIR = primeAgentSessionsDirFromEnv()
// Why: Devin ATIF transcripts are stored under <DEVIN_HOME>/transcripts.
const DEVIN_TRANSCRIPTS_DIR = join(
process.env.DEVIN_HOME?.trim() || join(homedir(), '.local', 'share', 'devin', 'cli'),
@ -180,6 +184,15 @@ export const AI_VAULT_AGENT_SOURCES: AiVaultAgentSourceTable = {
// Depth 0 is the workspace dir, which is never an artifact dir.
directoryPredicate: (name, depth) => depth === 0 || !OMP_SESSION_ARTIFACT_DIR_PATTERN.test(name)
},
'prime-agent': {
rootDirs: (options, wslHomeDirs) =>
sessionRootDirs(options.primeAgentSessionsDir ?? PRIME_AGENT_SESSIONS_DIR, wslHomeDirs, [
'.prime',
'agent',
'sessions'
]),
extensions: ['.jsonl']
},
openclaw: {
// Sessions live under <stateDir>/agents; a stateDir already ending in
// `agents` is used as-is. The current and legacy state dirs are the same

View File

@ -119,6 +119,7 @@ describe('scanAiVaultSessions Codex worker sessions', () => {
openclawLegacyStateDir: join(root, 'openclaw-legacy-state'),
piSessionsDir: join(root, 'pi-sessions'),
ompSessionsDir: join(root, 'omp-sessions'),
primeAgentSessionsDir: join(root, 'prime-agent-sessions'),
droidSessionsDir: join(root, 'droid-sessions'),
droidProjectsDir: join(root, 'droid-projects'),
kimiSessionsDir: join(root, 'kimi-sessions'),

View File

@ -166,8 +166,9 @@ export function rovoPartsText(parts: unknown[], role: 'user' | 'assistant'): str
}
// Agents whose transcripts are append-only message-graph JSONL (session +
// model_change + message records). OMP is a Pi fork and shares the format.
export type MessageGraphAgent = 'openclaw' | 'pi' | 'omp'
// model_change + message records). OMP and Prime Agent are Pi forks and
// share the format.
export type MessageGraphAgent = 'openclaw' | 'pi' | 'omp' | 'prime-agent'
export async function parseMessageGraphSessionFile(
agent: MessageGraphAgent,
@ -246,7 +247,7 @@ export function createMessageGraphSessionResumeState(
)
// Why: only OMP materializes task-subagent transcripts beside its sessions
// (in the same-named artifact dir); the row UI shows the count without
// expanding details. Pi/OpenClaw have no such layout — skip the readdir.
// expanding details. Pi/OpenClaw/Prime Agent have no such layout — skip the readdir.
return agent === 'omp' ? withOmpSubagentTranscriptCount(state, file.path) : state
}

View File

@ -2,6 +2,7 @@ import type { AiVaultAgent } from '../../shared/ai-vault-types'
import { antigravityFixture } from './session-scanner-antigravity-fixtures'
import { codexFixture } from './session-scanner-codex-fixtures'
import { primeAgentFixture } from './session-scanner-prime-agent-fixtures'
// Line builders for the incremental-parse differential tests: each agent gets
// a seed transcript, an appended continuation, and a truncated rewrite, all in
@ -287,6 +288,7 @@ export function allIncrementalAgentFixtures(): IncrementalAgentFixture[] {
openclawFixture(),
piFixture(),
ompFixture(),
primeAgentFixture(),
geminiJsonlFixture(),
antigravityFixture()
]

View File

@ -51,7 +51,8 @@ function isolatedScanRoots(root: string) {
droidSessionsDir: join(root, 'droid-sessions'),
droidProjectsDir: join(root, 'droid-projects'),
kimiSessionsDir: join(root, 'kimi-sessions'),
ompSessionsDir: join(root, 'omp-sessions')
ompSessionsDir: join(root, 'omp-sessions'),
primeAgentSessionsDir: join(root, 'prime-agent-sessions')
}
}

View File

@ -60,7 +60,8 @@ function resumableStateFactoryFor(
return () => createDroidSessionResumeState(candidate.file)
case 'openclaw':
case 'pi':
case 'omp': {
case 'omp':
case 'prime-agent': {
const agent = candidate.agent
return () => createMessageGraphSessionResumeState(agent, candidate.file)
}

View File

@ -0,0 +1,50 @@
import type { IncrementalAgentFixture } from './session-scanner-incremental-fixtures'
// Prime Agent is a Pi fork sharing the message-graph format and Pi's
// `modelId` key; its own fixture exercises the registry's 'prime-agent'
// branch with the v3 tree-session header its releases write.
export function primeAgentFixture(): IncrementalAgentFixture {
return {
agent: 'prime-agent',
fileName: 'dddddddd-eeee-4fff-8aaa-111111111111.jsonl',
seedLines: [
JSON.stringify({
type: 'session',
version: 3,
id: 'dddddddd-eeee-4fff-8aaa-111111111111',
cwd: '/repo/app',
timestamp: '2026-05-01T10:00:00.000Z'
}),
JSON.stringify({
type: 'model_change',
provider: 'prime-intellect',
modelId: 'inference/big-model',
timestamp: '2026-05-01T10:00:01.000Z'
}),
JSON.stringify({
type: 'message',
message: { role: 'user', content: [{ type: 'text', text: 'prime-agent seed question' }] },
timestamp: '2026-05-01T10:00:05.000Z'
})
],
appendLines: [
JSON.stringify({
type: 'message',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'prime-agent incremental answer' }],
model: 'inference/big-model',
usage: { input: 30, output: 10, totalTokens: 40 }
},
timestamp: '2026-05-01T10:01:00.000Z'
})
],
truncatedLines: [
JSON.stringify({
type: 'session',
id: 'dddddddd-eeee-4fff-8aaa-111111111111',
timestamp: '2026-05-01T10:00:00.000Z'
})
]
}
}

View File

@ -21,6 +21,7 @@ export function isolatedScanRoots(root: string) {
openclawLegacyStateDir: join(root, 'openclaw-legacy-state'),
piSessionsDir: join(root, 'pi-sessions'),
ompSessionsDir: join(root, 'omp-sessions'),
primeAgentSessionsDir: join(root, 'prime-agent-sessions'),
droidSessionsDir: join(root, 'droid-sessions'),
droidProjectsDir: join(root, 'droid-projects'),
kimiSessionsDir: join(root, 'kimi-sessions')
@ -50,6 +51,72 @@ export function writeAntigravityHistory(brainDir: string, records: unknown[]): P
return writeJsonlFile(join(dirname(brainDir), 'history.jsonl'), records)
}
// Message-graph fixtures for the Pi forks: each writes one session transcript
// and returns its path, since both agents resume by absolute transcript path.
export async function writeOmpScannerFixture(sessionsDir: string): Promise<string> {
const sessionFile = join(sessionsDir, 'omp-session.jsonl')
await writeJsonlFile(sessionFile, [
{
type: 'session',
version: 3,
id: 'omp-session',
title: 'OMP session title',
timestamp: '2026-05-01T10:08:30.000Z',
cwd: '/tmp/omp'
},
{
type: 'model_change',
model: 'gpt-5.4-mini',
timestamp: '2026-05-01T10:08:30.500Z'
},
{
type: 'message',
timestamp: '2026-05-01T10:08:31.000Z',
message: { role: 'user', content: [{ type: 'text', text: 'OMP title' }] }
},
{
type: 'message',
timestamp: '2026-05-01T10:08:32.000Z',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'OMP answer' }],
model: 'gpt-5.4-mini',
// totalTokens deliberately != input+output so the assertion proves
// the explicit-total field is read, not an input/output sum.
usage: { input: 10, output: 5, totalTokens: 160 }
}
}
])
return sessionFile
}
// Prime Agent shares Pi's message-graph format (and its `modelId` key) but
// reads its own ~/.prime/agent/sessions root.
export async function writePrimeAgentScannerFixture(sessionsDir: string): Promise<string> {
const sessionFile = join(sessionsDir, 'prime-agent-session.jsonl')
await writeJsonlFile(sessionFile, [
{
type: 'session',
version: 3,
id: 'prime-agent-session',
timestamp: '2026-05-01T10:08:40.000Z',
cwd: '/tmp/prime-agent'
},
{
type: 'model_change',
provider: 'prime-intellect',
modelId: 'inference/big-model',
timestamp: '2026-05-01T10:08:40.500Z'
},
{
type: 'message',
timestamp: '2026-05-01T10:08:41.000Z',
message: { role: 'user', content: [{ type: 'text', text: 'Prime Agent title' }] }
}
])
return sessionFile
}
export function writeAntigravityScannerFixture(
brainDir: string,
sessionId: string

View File

@ -30,6 +30,7 @@ export type AiVaultScanOptions = {
openclawLegacyStateDir?: string
piSessionsDir?: string
ompSessionsDir?: string
primeAgentSessionsDir?: string
droidSessionsDir?: string
droidProjectsDir?: string
kimiSessionsDir?: string

View File

@ -1,9 +1,13 @@
import { homedir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import {
extractFullFirstUserPromptText,
extractPreviewContentText,
normalizeAgentSessionsDir,
normalizeFullFirstUserPromptText,
normalizePrimeAgentSessionsDir,
primeAgentSessionsDirFromEnv,
normalizePreviewText,
normalizeTitleText
} from './session-scanner-values'
@ -87,4 +91,97 @@ describe('AI Vault session scanner text values', () => {
'/agents/.omp/agent/sessions'
)
})
// Prime Agent's env var is its agent dir verbatim and the CLI writes to
// `<agentDir>/sessions`, so every configured dir maps to that child.
it('maps any Prime Agent agent dir to its sessions child', () => {
expect(normalizePrimeAgentSessionsDir('/tmp/prime-agent')).toBe('/tmp/prime-agent/sessions')
expect(normalizePrimeAgentSessionsDir('/tmp/prime-agent///')).toBe('/tmp/prime-agent/sessions')
expect(normalizePrimeAgentSessionsDir('/agents/.prime')).toBe('/agents/.prime/sessions')
expect(normalizePrimeAgentSessionsDir('/agents/.prime/agent')).toBe(
'/agents/.prime/agent/sessions'
)
})
// Why: the CLI appends `sessions` unconditionally, so an agent dir that is itself
// named `sessions` nests one deeper rather than being taken as the transcripts root.
it('still appends sessions when the agent dir is itself named sessions', () => {
expect(normalizePrimeAgentSessionsDir('/data/sessions')).toBe('/data/sessions/sessions')
})
it('expands a leading tilde in the agent dir', () => {
expect(normalizePrimeAgentSessionsDir('~/work/prime')).toBe(
join(homedir(), 'work', 'prime', 'sessions')
)
})
// Why: any non-absolute root would resolve against the main-process cwd. The
// drive-shaped values ('C:foo' is the form that resolves against a per-drive
// cwd on Windows) are non-absolute on posix too, so they assert the same
// fallback here; real win32 semantics cannot be pinned on a posix runner.
it('falls back to the default root for every non-absolute agent dir', () => {
const fallback = join(homedir(), '.prime', 'agent', 'sessions')
for (const value of [
'/',
'//',
' ',
'',
'.',
'..',
'sessions',
'rel/path',
'C:\\',
'C:/',
'C:foo'
]) {
expect(normalizePrimeAgentSessionsDir(value)).toBe(fallback)
}
})
describe('primeAgentSessionsDirFromEnv', () => {
const defaultDir = join(homedir(), '.prime', 'agent', 'sessions')
it('defaults to the home agent dir when nothing is configured', () => {
expect(primeAgentSessionsDirFromEnv({})).toBe(defaultDir)
})
it('appends sessions to a configured agent dir', () => {
expect(primeAgentSessionsDirFromEnv({ PRIME_AGENT_CODING_AGENT_DIR: '/opt/prime' })).toBe(
'/opt/prime/sessions'
)
})
// Why: upstream reads the sessions-root overrides before the agent dir and uses
// them verbatim; ignoring them left the vault silently empty.
it('prefers the sessions-root overrides verbatim over the agent dir', () => {
expect(
primeAgentSessionsDirFromEnv({
PRIME_AGENT_CODING_AGENT_DIR: '/opt/prime',
PRIME_AGENT_SESSION_DIR: '/mnt/transcripts'
})
).toBe('/mnt/transcripts')
expect(
primeAgentSessionsDirFromEnv({
PRIME_AGENT_CODING_AGENT_DIR: '/opt/prime',
PRIME_AGENT_CODING_AGENT_SESSION_DIR: '/mnt/legacy'
})
).toBe('/mnt/legacy')
expect(
primeAgentSessionsDirFromEnv({
PRIME_AGENT_SESSION_DIR: '/mnt/wins',
PRIME_AGENT_CODING_AGENT_SESSION_DIR: '/mnt/legacy'
})
).toBe('/mnt/wins')
})
it('expands a tilde and rejects a non-absolute sessions-root override', () => {
expect(primeAgentSessionsDirFromEnv({ PRIME_AGENT_SESSION_DIR: '~/t' })).toBe(
join(homedir(), 't')
)
// Why: '.' would otherwise scan the main-process cwd outright.
for (const value of ['/', '.', '..', 'rel/path', ' ', 'C:foo']) {
expect(primeAgentSessionsDirFromEnv({ PRIME_AGENT_SESSION_DIR: value })).toBe(defaultDir)
}
})
})
})

View File

@ -1,5 +1,5 @@
import { homedir } from 'node:os'
import { basename, dirname, join } from 'node:path'
import { basename, dirname, isAbsolute, join } from 'node:path'
import { readFile } from 'node:fs/promises'
import { asRecord } from './session-scanner-record-value'
@ -153,6 +153,41 @@ export function normalizeAgentSessionsDir(
return normalized
}
function defaultPrimeAgentSessionsDir(): string {
return join(homedir(), '.prime', 'agent', 'sessions')
}
// Why: the CLI expands a leading `~` itself, so a value set outside a shell
// (config file, plist, quoted assignment) still resolves against the home dir.
// Returns null for anything that is not an absolute root, since a relative value
// ('', '.', '..', 'sessions') would resolve against the main-process cwd.
function absoluteConfiguredDir(rawValue: string): string | null {
const expanded = rawValue === '~' ? homedir() : rawValue.replace(/^~(?=[\\/])/, homedir())
const normalized = expanded.replace(/[\\/]+$/, '')
return normalized && isAbsolute(normalized) ? normalized : null
}
// Prime Agent takes PRIME_AGENT_CODING_AGENT_DIR verbatim as its agent config dir
// (no `/agent` suffixing) and always writes transcripts to `<agentDir>/sessions` —
// unconditionally, so a root that is itself named `sessions` still nests one deeper.
export function normalizePrimeAgentSessionsDir(rawAgentDir: string): string {
const agentDir = absoluteConfiguredDir(rawAgentDir.trim())
return agentDir ? join(agentDir, 'sessions') : defaultPrimeAgentSessionsDir()
}
// PRIME_AGENT_SESSION_DIR (and its legacy PRIME_AGENT_CODING_AGENT_SESSION_DIR alias)
// point straight at the transcripts root and outrank the agent dir upstream, so they
// are used verbatim with no `sessions` child.
export function primeAgentSessionsDirFromEnv(env: NodeJS.ProcessEnv = process.env): string {
const sessionDir =
env.PRIME_AGENT_SESSION_DIR?.trim() || env.PRIME_AGENT_CODING_AGENT_SESSION_DIR?.trim()
if (sessionDir) {
return absoluteConfiguredDir(sessionDir) ?? defaultPrimeAgentSessionsDir()
}
const agentDir = env.PRIME_AGENT_CODING_AGENT_DIR?.trim()
return agentDir ? normalizePrimeAgentSessionsDir(agentDir) : defaultPrimeAgentSessionsDir()
}
export function clampPositiveInteger(value: number | undefined, fallback: number): number {
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : fallback
}

View File

@ -7,7 +7,9 @@ import { scanAiVaultSessions } from './session-scanner'
import {
isolatedScanRoots,
jsonLines,
writeAntigravityScannerFixture
writeAntigravityScannerFixture,
writeOmpScannerFixture,
writePrimeAgentScannerFixture
} from './session-scanner-test-fixtures'
let tempRoots: string[] = []
@ -595,43 +597,8 @@ describe('scanAiVaultSessions', () => {
])
)
const ompSessionFile = join(roots.ompSessionsDir, 'omp-session.jsonl')
await mkdir(roots.ompSessionsDir, { recursive: true })
await writeFile(
ompSessionFile,
jsonLines([
{
type: 'session',
version: 3,
id: 'omp-session',
title: 'OMP session title',
timestamp: '2026-05-01T10:08:30.000Z',
cwd: '/tmp/omp'
},
{
type: 'model_change',
model: 'gpt-5.4-mini',
timestamp: '2026-05-01T10:08:30.500Z'
},
{
type: 'message',
timestamp: '2026-05-01T10:08:31.000Z',
message: { role: 'user', content: [{ type: 'text', text: 'OMP title' }] }
},
{
type: 'message',
timestamp: '2026-05-01T10:08:32.000Z',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'OMP answer' }],
model: 'gpt-5.4-mini',
// totalTokens deliberately != input+output so the assertion proves
// the explicit-total field is read, not an input/output sum.
usage: { input: 10, output: 5, totalTokens: 160 }
}
}
])
)
const ompSessionFile = await writeOmpScannerFixture(roots.ompSessionsDir)
const primeAgentSessionFile = await writePrimeAgentScannerFixture(roots.primeAgentSessionsDir)
await mkdir(roots.devinTranscriptsDir, { recursive: true })
await writeFile(
@ -767,6 +734,10 @@ describe('scanAiVaultSessions', () => {
expect(commandByAgent.get('pi')).toBe("cd '/tmp/pi' && pi --session 'pi-session'")
// OMP resumes by absolute transcript path, not by internal session id.
expect(commandByAgent.get('omp')).toBe(`cd '/tmp/omp' && omp --resume '${ompSessionFile}'`)
// Prime Agent's `--resume <path|id>` takes the same absolute-path form as OMP.
expect(commandByAgent.get('prime-agent')).toBe(
`cd '/tmp/prime-agent' && prime-agent --resume '${primeAgentSessionFile}'`
)
expect(commandByAgent.get('devin')).toBe("cd '/tmp/devin' && devin --resume 'devin-session'")
expect(commandByAgent.get('droid')).toBe("cd '/tmp/droid' && droid --resume 'droid-session'")
expect(commandByAgent.get('kimi')).toBe(
@ -776,6 +747,12 @@ describe('scanAiVaultSessions', () => {
const ompSession = result.sessions.find((session) => session.agent === 'omp')
expect(ompSession?.model).toBe('gpt-5.4-mini')
expect(ompSession?.totalTokens).toBe(160)
// Prime Agent keeps Pi's `model_change.modelId` key, so the pre-reply model
// must come through even though no assistant message was written yet.
const primeAgentSession = result.sessions.find((session) => session.agent === 'prime-agent')
expect(primeAgentSession?.model).toBe('inference/big-model')
expect(primeAgentSession?.title).toBe('Prime Agent title')
})
it('captures an in-progress OMP model from model_change before any assistant reply', async () => {

View File

@ -139,6 +139,14 @@ export function buildSkillDiscoverySources(
['agent-skills'],
'omp'
),
source(
'home-prime-agent',
'Prime Agent home',
pathApi.join(home, '.prime', 'agent', 'skills'),
'home',
['agent-skills'],
'prime-agent'
),
source(
'home-gemini',
'Gemini home',

View File

@ -419,6 +419,7 @@
"fc80296033": "Devin",
"da41abbdd4": "Ante",
"060d152fb5": "Trae",
"d443a47995": "Prime Agent",
"mimo_code_label": "MiMo Code"
},
"skill": {

View File

@ -344,6 +344,7 @@
"fc80296033": "Devin",
"da41abbdd4": "Ante",
"060d152fb5": "Trae",
"d443a47995": "Prime Agent",
"mimo_code_label": "MiMo Code"
},
"skill": {

View File

@ -344,6 +344,7 @@
"fc80296033": "Devin",
"da41abbdd4": "Ante",
"060d152fb5": "Trae",
"d443a47995": "Prime Agent",
"mimo_code_label": "MiMo Code"
},
"skill": {

View File

@ -344,6 +344,7 @@
"fc80296033": "Devin",
"da41abbdd4": "Ante",
"060d152fb5": "Trae",
"d443a47995": "Prime Agent",
"mimo_code_label": "MiMo Code"
},
"skill": {

View File

@ -344,6 +344,7 @@
"fc80296033": "Devin",
"da41abbdd4": "Ante",
"060d152fb5": "Trae",
"d443a47995": "Prime Agent",
"mimo_code_label": "MiMo Code"
},
"skill": {

View File

@ -127,6 +127,13 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] =>
// favicon fallback would never be reached.
homepageUrl: 'https://omp.sh'
},
{
id: 'prime-agent',
label: translate('auto.lib.agent.catalog.d443a47995', 'Prime Agent'),
cmd: 'prime-agent',
faviconDomain: 'primeintellect.ai',
homepageUrl: 'https://github.com/PrimeIntellect-ai/prime-agent'
},
{
id: 'gemini',
label: translate('auto.lib.agent.catalog.12e6baa4f7', 'Gemini'),

View File

@ -3,6 +3,7 @@ import grokUrl from '../../../shared/agent-icons/grok.png?url'
import mimoCodeUrl from '../../../shared/agent-icons/mimo-code.png?url'
import anteUrl from '../../../shared/agent-icons/ante.png?url'
import traeUrl from '../../../shared/agent-icons/trae.png?url'
import primeAgentUrl from '../../../shared/agent-icons/prime-agent.png?url'
import geminiUrl from '../../../shared/agent-icons/gemini.png?url'
import antigravityUrl from '../../../shared/agent-icons/antigravity.png?url'
import gooseUrl from '../../../shared/agent-icons/goose.png?url'
@ -36,6 +37,7 @@ export const AGENT_FAVICON_ASSETS: Partial<Record<TuiAgent, string>> = {
'mimo-code': mimoCodeUrl,
ante: anteUrl,
trae: traeUrl,
'prime-agent': primeAgentUrl,
gemini: geminiUrl,
antigravity: antigravityUrl,
goose: gooseUrl,

View File

@ -864,6 +864,10 @@ describe('formatAgentTypeLabel', () => {
expect(formatAgentTypeLabel('trae')).toBe('Trae')
})
it("maps 'prime-agent' to 'Prime Agent'", () => {
expect(formatAgentTypeLabel('prime-agent')).toBe('Prime Agent')
})
it('passes through arbitrary custom agent names as-is', () => {
expect(formatAgentTypeLabel('weirdo')).toBe('weirdo')
})
@ -889,6 +893,7 @@ describe('agentTypeToIconAgent', () => {
expect(agentTypeToIconAgent('command-code')).toBe('command-code')
expect(agentTypeToIconAgent('ante')).toBe('ante')
expect(agentTypeToIconAgent('trae')).toBe('trae')
expect(agentTypeToIconAgent('prime-agent')).toBe('prime-agent')
})
it('returns null for arbitrary non-iconable strings', () => {

View File

@ -105,6 +105,7 @@ const ICONABLE_AGENT_TYPES: Record<TuiAgent, true> = {
'mimo-code': true,
pi: true,
omp: true,
'prime-agent': true,
gemini: true,
antigravity: true,
aider: true,

View File

@ -144,6 +144,35 @@ describe('buildAgentStartupPlan', () => {
).toBe("traecli -- 'help me name this config'")
})
it('passes the prompt to Prime Agent as a positional argv behind a `--` separator', () => {
expect(
buildAgentStartupPlan({
agent: 'prime-agent',
prompt: 'Summarize the failing tests',
cmdOverrides: {},
platform: 'linux'
})
).toEqual({
agent: 'prime-agent',
launchCommand: "prime-agent -- 'Summarize the failing tests'",
expectedProcess: 'prime-agent',
followupPrompt: null,
launchConfig: emptyLaunchConfig('prime-agent')
})
})
// Why: without the separator these dispatch to Prime Agent's `help`/`agents` subcommands instead.
it('keeps subcommand-shaped Prime Agent prompts as the positional prompt', () => {
expect(
buildAgentStartupPlan({
agent: 'prime-agent',
prompt: 'help me name this config',
cmdOverrides: {},
platform: 'linux'
})?.launchCommand
).toBe("prime-agent -- 'help me name this config'")
})
it('uses cursor-agent as the actual launch binary', () => {
expect(
buildAgentStartupPlan({

View File

@ -1,14 +1,17 @@
import { isAnteHeadlessOneShotCommand } from './ante-headless-command'
import { isPrimeAgentHeadlessOneShotCommand } from './prime-agent-headless-command'
import { isPrintModeHeadlessOneShotCommand } from './print-mode-headless-command'
import type { TuiAgent } from './types'
// Why: a table (not an if-chain) so adding an agent is one entry; Claude and Trae share
// the same `--print` one-shot contract, Ante's `--prompt` form needs its own matcher.
// the same `--print` one-shot contract, Ante's `--prompt` form and Prime Agent's
// `--mode` forms need their own matchers.
const HEADLESS_ONE_SHOT_MATCHERS: Partial<
Record<TuiAgent, (tokens: readonly string[]) => boolean>
> = {
claude: isPrintModeHeadlessOneShotCommand,
trae: isPrintModeHeadlessOneShotCommand,
'prime-agent': isPrimeAgentHeadlessOneShotCommand,
ante: isAnteHeadlessOneShotCommand
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

View File

@ -23,6 +23,7 @@ const TUI_AGENT_KIND_BY_AGENT = {
'mimo-code': 'mimo-code',
pi: 'pi',
omp: 'omp',
'prime-agent': 'prime-agent',
gemini: 'gemini',
antigravity: 'antigravity',
aider: 'aider',

View File

@ -0,0 +1,30 @@
import type { TuiAgent } from './types'
// Node CLIs whose shims launch a generic script (cli.js, versioned index.js), so
// only the exact install path is an authoritative identity signal.
export const EXACT_NODE_ENTRYPOINT_IDENTITIES: readonly {
pattern: RegExp
agent: TuiAgent
processName: string
}[] = [
// Why: Cursor's native Windows launcher runs a generic versioned index.js,
// so its install path is the only stable identity that avoids ordinary Node apps.
{
pattern: /(?:^|\/)cursor-agent\/versions\/[^/]+\/index\.js$/,
agent: 'cursor',
processName: 'cursor-agent'
},
// Why: Pi's npm shim launches a generic cli.js; only the exact package path is authoritative.
{
pattern:
/(?:^|\/)node_modules\/@(?:earendil-works|mariozechner)\/pi-coding-agent\/dist\/cli\.js$/,
agent: 'pi',
processName: 'pi'
},
// Why: same shape as Pi — Prime Agent's npm shim launches a generic bundled cli.js.
{
pattern: /(?:^|\/)node_modules\/prime-agent\/dist\/bundle\/cli\.js$/,
agent: 'prime-agent',
processName: 'prime-agent'
}
]

View File

@ -242,6 +242,61 @@ describe('agent process recognition', () => {
).toEqual({ agent: 'pi', processName: 'pi' })
})
it('recognizes Prime Agent by its binary and npm entrypoint', () => {
expect(recognizeAgentProcess('prime-agent')).toEqual({
agent: 'prime-agent',
processName: 'prime-agent'
})
expect(recognizeAgentProcess('/opt/homebrew/bin/prime-agent')).toEqual({
agent: 'prime-agent',
processName: 'prime-agent'
})
expect(
recognizeAgentProcessFromCommandLine(
'node /opt/homebrew/lib/node_modules/prime-agent/dist/bundle/cli.js'
)
).toEqual({ agent: 'prime-agent', processName: 'prime-agent' })
expect(
recognizeAgentProcessFromCommandLine(
String.raw`node.exe C:\Users\dev\AppData\Roaming\npm\node_modules\prime-agent\dist\bundle\cli.js`
)
).toEqual({ agent: 'prime-agent', processName: 'prime-agent' })
})
it('does not recognize Prime Agent headless one-shot commands as interactive agents', () => {
expect(recognizeAgentProcessFromCommandLine('prime-agent -p "summarize this diff"')).toBeNull()
expect(recognizeAgentProcessFromCommandLine('prime-agent --print "review this"')).toBeNull()
expect(recognizeAgentProcessFromCommandLine('prime-agent --resume abc123')).toEqual({
agent: 'prime-agent',
processName: 'prime-agent'
})
// Why: past `--` nothing is a flag, so this is the interactive pane Orca itself launches.
expect(
recognizeAgentProcessFromCommandLine('prime-agent -- "--print the release notes"')
).toEqual({ agent: 'prime-agent', processName: 'prime-agent' })
})
it('does not recognize Prime Agent non-interactive --mode runs as interactive agents', () => {
for (const mode of ['json', 'rpc', 'acp', 'daemon']) {
expect(recognizeAgentProcessFromCommandLine(`prime-agent --mode ${mode}`)).toBeNull()
}
// Why: `text` is the interactive TUI mode Orca hosts.
expect(recognizeAgentProcessFromCommandLine('prime-agent --mode text')).toEqual({
agent: 'prime-agent',
processName: 'prime-agent'
})
// Why: the CLI only parses `--mode <value>` as separate tokens, so `--mode=json`
// is ignored by it and still starts the interactive mode.
expect(recognizeAgentProcessFromCommandLine('prime-agent --mode=json')).toEqual({
agent: 'prime-agent',
processName: 'prime-agent'
})
expect(recognizeAgentProcessFromCommandLine('prime-agent -- --mode rpc')).toEqual({
agent: 'prime-agent',
processName: 'prime-agent'
})
})
it('recognizes only the agent subcommand of the generic Orca CLI', () => {
expect(recognizeAgentProcessFromCommandLine('orca claude-teams')).toEqual({
agent: 'claude-agent-teams',

View File

@ -1,4 +1,5 @@
import { getTuiAgentDetectCommands, TUI_AGENT_CONFIG } from './tui-agent-config'
import { EXACT_NODE_ENTRYPOINT_IDENTITIES } from './agent-node-entrypoint-identities'
import type { AgentType } from './agent-status-types'
import type { TuiAgent } from './types'
import { filterHeadlessOneShotAgentCommand } from './agent-headless-command'
@ -52,9 +53,6 @@ const NODE_PACKAGE_SCRIPT_ENTRYPOINTS: Record<string, readonly string[]> = {
codex: ['node_modules/@openai/codex/'],
gemini: ['node_modules/@google/gemini-cli/']
}
const CURSOR_AGENT_NODE_ENTRYPOINT_RE = /(?:^|\/)cursor-agent\/versions\/[^/]+\/index\.js$/
const PI_AGENT_NODE_ENTRYPOINT_RE =
/(?:^|\/)node_modules\/@(?:earendil-works|mariozechner)\/pi-coding-agent\/dist\/cli\.js$/
const PYTHON_SCRIPT_ENTRYPOINT_DIRECTORIES = ['/bin/', '/scripts/', '/site-packages/']
const PROCESS_TO_AGENT = new Map<string, TuiAgent>()
@ -206,14 +204,10 @@ function comparablePath(token: string): string {
function recognizeNodeScriptEntrypoint(token: string): RecognizedAgentProcess | null {
const path = comparablePath(token)
// Why: Cursor's native Windows launcher runs a generic versioned index.js,
// so its install path is the only stable identity that avoids ordinary Node apps.
if (CURSOR_AGENT_NODE_ENTRYPOINT_RE.test(path)) {
return { agent: 'cursor', processName: 'cursor-agent' }
}
// Why: Pi's npm shim launches a generic cli.js; only the exact package path is authoritative.
if (PI_AGENT_NODE_ENTRYPOINT_RE.test(path)) {
return { agent: 'pi', processName: 'pi' }
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]

View File

@ -31,6 +31,7 @@ export type WellKnownAgentType =
| 'aider'
| 'pi'
| 'omp'
| 'prime-agent'
| 'droid'
| 'command-code'
| 'grok'

View File

@ -16,6 +16,7 @@ const WELL_KNOWN_LABELS: Record<string, string> = {
aider: 'Aider',
pi: 'Pi',
omp: 'OMP',
'prime-agent': 'Prime Agent',
droid: 'Droid',
'command-code': 'Command Code',
grok: 'Grok',

View File

@ -122,4 +122,31 @@ describe('buildAiVaultResumeCommand', () => {
})
).toBe("cd '/Users/ada/repo' && omp --resume '019f27cd-4268-7000-96e7-62f42a55c144'")
})
it('resumes Prime Agent by absolute transcript path like OMP', () => {
expect(
buildAiVaultResumeCommand({
agent: 'prime-agent',
sessionId: 'dddddddd-eeee-4fff-8aaa-111111111111',
resumeFilePath:
'/Users/ada/.prime/agent/sessions/dddddddd-eeee-4fff-8aaa-111111111111.jsonl',
cwd: '/Users/ada/repo',
platform: 'darwin'
})
).toBe(
"cd '/Users/ada/repo' && prime-agent --resume '/Users/ada/.prime/agent/sessions/dddddddd-eeee-4fff-8aaa-111111111111.jsonl'"
)
})
it('falls back to the session id when no Prime Agent transcript path is known', () => {
expect(
buildAiVaultResumeCommand({
agent: 'prime-agent',
sessionId: 'dddddddd-eeee-4fff-8aaa-111111111111',
resumeFilePath: null,
cwd: '/Users/ada/repo',
platform: 'darwin'
})
).toBe("cd '/Users/ada/repo' && prime-agent --resume 'dddddddd-eeee-4fff-8aaa-111111111111'")
})
})

View File

@ -22,11 +22,15 @@ export function buildAiVaultResumeCommand(args: {
const { agent, sessionId, cwd, platform, commandOverride, codexHome, resumeFilePath, shell } =
args
const baseCommand = commandOverride?.trim() || defaultAiVaultResumeCommandBase(agent)
// Why: OMP's `--resume` accepts an absolute transcript path, which resolves
// regardless of which session-dir root (custom OMP_CODING_AGENT_DIR / WSL
// home) the file was discovered under, where an id-prefix lookup scoped to
// the default store would miss it. Falls back to the id if no path is known.
const resumeTarget = agent === 'omp' && resumeFilePath?.trim() ? resumeFilePath.trim() : sessionId
// Why: OMP's and Prime Agent's `--resume` accept an absolute transcript path,
// which resolves regardless of which session-dir root (custom
// OMP_CODING_AGENT_DIR / PRIME_AGENT_CODING_AGENT_DIR / WSL home) the file was
// discovered under, where an id-prefix lookup scoped to the default store
// would miss it. Falls back to the id if no path is known.
const resumeTarget =
(agent === 'omp' || agent === 'prime-agent') && resumeFilePath?.trim()
? resumeFilePath.trim()
: sessionId
const sessionArg =
shell === 'cmd'
? quoteWindowsCmdArg(resumeTarget)
@ -167,10 +171,12 @@ function buildAgentResumeInvocation(
case 'devin':
case 'openclaw':
case 'droid':
// Why: OMP resumes by absolute transcript path (see buildAiVaultResumeCommand),
// but the `--resume <arg>` invocation form is identical to the others here.
// Why: OMP and Prime Agent resume by absolute transcript path (see
// buildAiVaultResumeCommand), but the `--resume <arg>` invocation form is
// identical to the others here.
// falls through
case 'omp':
case 'prime-agent':
return `${baseCommand} --resume ${sessionArg}`
case 'antigravity':
return `${baseCommand} --conversation ${sessionArg}`

View File

@ -7,6 +7,7 @@ export const AI_VAULT_AGENTS = [
'hermes',
'pi',
'omp',
'prime-agent',
'cursor',
'gemini',
'antigravity',
@ -47,6 +48,7 @@ export const AI_VAULT_AGENT_LABELS = {
hermes: 'Hermes',
pi: 'Pi',
omp: 'OMP',
'prime-agent': 'Prime Agent',
cursor: 'Cursor',
gemini: 'Gemini',
antigravity: 'Antigravity',

View File

@ -0,0 +1,22 @@
import { isPrintModeHeadlessOneShotCommand } from './print-mode-headless-command'
// Why: Prime Agent's non-interactive runs are `--mode <json|rpc|acp|daemon>` as well as
// `--print`; only `text` hosts the TUI. Matching upstream, the value must be a separate
// token — `--mode=json` is not parsed by the CLI, so it still starts the interactive mode.
const NON_INTERACTIVE_MODES = new Set(['json', 'rpc', 'acp', 'daemon'])
export function isPrimeAgentHeadlessOneShotCommand(tokens: readonly string[]): boolean {
if (isPrintModeHeadlessOneShotCommand(tokens)) {
return true
}
for (let index = 1; index < tokens.length; index += 1) {
// Why: `--` ends option parsing, so a prompt that reads like `--mode` is still a prompt.
if (tokens[index] === '--') {
return false
}
if (tokens[index] === '--mode' && NON_INTERACTIVE_MODES.has(tokens[index + 1] ?? '')) {
return true
}
}
return false
}

View File

@ -22,6 +22,7 @@ export const SKILLS_CLI_AGENT_KEY_BY_TUI_AGENT = {
'mimo-code': null,
pi: 'pi',
omp: null,
'prime-agent': null,
gemini: 'gemini-cli',
antigravity: 'antigravity',
aider: null,

View File

@ -77,6 +77,7 @@ export const AGENT_KIND_VALUES = [
'mimo-code',
'pi',
'omp',
'prime-agent',
'gemini',
'antigravity',
'aider',

View File

@ -144,6 +144,18 @@ export const TUI_AGENT_CONFIG: Record<TuiAgent, TuiAgentConfig> = {
promptInjectionMode: 'argv',
draftPromptEnvVar: 'ORCA_OMP_PREFILL'
},
'prime-agent': {
detectCmd: 'prime-agent',
launchCmd: 'prime-agent',
expectedProcess: 'prime-agent',
// Why: `prime-agent [options] [@files...] [message...]` takes the task as positional argv.
promptInjectionMode: 'argv',
// Why: separator so prompts starting with `help`/`agents`/`-…` aren't parsed as a
// subcommand or flag — its help documents `--` as "treat all following arguments as messages".
argvPromptSeparator: '--',
// Why: Prime Agent embeds Pi's TUI and decodes CSI-u the same way (see pi above).
windowsShiftEnterEncoding: 'csi-u'
},
gemini: {
detectCmd: 'gemini',
launchCmd: 'gemini',

View File

@ -18,6 +18,7 @@ export const TUI_AGENT_DISPLAY_NAMES: Record<TuiAgent, string> = {
'mimo-code': 'MiMo Code',
pi: 'Pi',
omp: 'OMP',
'prime-agent': 'Prime Agent',
gemini: 'Gemini',
antigravity: 'Antigravity',
aider: 'Aider',

View File

@ -16,6 +16,7 @@ export const TUI_AGENT_AUTO_PICK_ORDER = [
'trae',
'pi',
'omp',
'prime-agent',
'gemini',
'antigravity',
'aider',

View File

@ -2626,6 +2626,7 @@ export type TuiAgent =
| 'devin' // Devin CLI
| 'ante' // Ante (Antigma Labs)
| 'trae' // Trae CLI
| 'prime-agent' // Prime Agent (Prime Intellect)
export type TaskViewPresetId = 'all' | 'issues' | 'review' | 'my-issues' | 'my-prs' | 'prs'