Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pier/dir-density.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"reason": "debt: more surface vs chrome split beyond hooks/; composer paste edit bridge/dialog/actions pending structured-composer/; core comments status item"
},
"src/main/services/agents/integrations": {
"max": 43,
"max": 44,
"reason": "debt: provider files still flat; interactive-tool lifecycle helpers pending tools/ folder; evidence/transcript/hooks extracted"
},
"src/plugins/builtin/files/renderer/tree": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Grok:`turn_completed` cancelled/end_turn。
| openclaude | Claude 兼容嵌套 JSON hook | `full` | `SessionStart→SessionStart`;`UserPromptSubmit→PromptSubmit`;`PreToolUse→ToolStart/InteractionRequested`;`PostToolUse→ToolComplete/InteractionResolved`;`PostToolUseFailure→ToolComplete/InteractionResolved`;`PreCompact/PostCompact→processing`;`Stop→Stop`;`StopFailure→error`;`SubagentStart/SubagentStop/SessionEnd→同名` | `advisory` | 无 |
| pi | JavaScript 扩展 | `coarse` | `session_start→SessionStart`;`before_agent_start→PromptSubmit`;`tool_execution_start→ToolStart`;`tool_execution_end→ToolComplete`;`ui_prompt_start→InteractionRequested`;`ui_prompt_end→InteractionResolved`;`agent_settled→Stop`;`session_shutdown→SessionEnd` | `authoritative` | 无 |
| qodercli | Claude 兼容嵌套 JSON hook | `full` | `SessionStart→SessionStart`;`UserPromptSubmit→PromptSubmit`;`PreToolUse→ToolStart`;`PostToolUse/PostToolUseFailure→ToolComplete`;`Elicitation→InteractionRequested`;`ElicitationResult→InteractionResolved`;`PreCompact/PostCompact→processing`;`Stop→Stop`;`StopFailure→error`;`SubagentStart/SubagentStop/SessionEnd→同名` | `advisory` | 无 |
| fx | Herdr socket(宿主冒充 multiplexor;无配置文件) | `coarse` | `herdr.working→processing`;`herdr.idle→ActivityIdle`;`herdr.blocked.permission→processing`;`herdr.blocked.question→processing`;`herdr.blocked.recovery→processing` | `none` | 无 |
| qwen-code | Qwen Code hook | `full` | `SessionStart/SessionEnd→同名`;`UserPromptSubmit→PromptSubmit`;`Stop→Stop`;`StopFailure→error`;`PreToolUse→ToolStart`;`PostToolUse/PostToolUseFailure→ToolComplete`;`PreCompact/PostCompact→processing`;`SubagentStart/SubagentStop→同名` | `advisory` | 无 |

### 仅启动识别
Expand Down
15 changes: 15 additions & 0 deletions packages/plugin-api/src/peer-sync/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@ function ompDbPath(opts: PeerAvailabilityOptions): string {
return join(resolveHomeDir(opts), ".omp", "agent", "agent.db");
}

/**
* Sync-ready probe for fx.
*
* Evidence: `fx` on PATH or `~/.fx` exists. Matches the auth.json location
* written by fx peer sync. fx reads `~/.fx/grok-auth.json` when its provider
* is `grok` (vercel-labs/fx `src/core/auth/grok_session.zig`,
* `src/core/shared/profile_paths.zig`).
*/
export function isFxSyncReady(opts: PeerAvailabilityOptions = {}): boolean {
const home = resolveHomeDir(opts);
return (
existsSync(join(home, ".fx")) || commandExistsOnPath("fx", opts.pathEnv)
);
}
/**
* Sync-ready probe for OpenCode.
*
Expand Down Expand Up @@ -209,5 +223,6 @@ export function detectPeerAvailability(
pi,
piOauthCapable: pi ? isPiOauthCapable(opts) : false,
omp: isOmpSyncReady(opts),
fx: isFxSyncReady(opts),
};
}
1 change: 1 addition & 0 deletions packages/plugin-api/src/peer-sync/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
export {
ALL_PEER_SYNC_TARGETS,
detectPeerAvailability,
isFxSyncReady,
isOmpSyncReady,
isOpencodeSyncReady,
isPeerTargetAvailable,
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-api/src/peer-sync/notify-failures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ function isPeerSyncResults(
}

const TARGET_FALLBACK_LABELS: Record<PeerSyncTarget, string> = {
fx: "fx",
omp: "OMP",
opencode: "OpenCode",
pi: "Pi",
Expand Down
32 changes: 23 additions & 9 deletions packages/plugin-api/src/peer-sync/shared.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
/**
* Shared peer-tool helpers for official account plugins (Codex, Grok, …).
* Renderer-safe: no Node builtins. Detection probes live in `./main`.
*
* fx is a peer sync target for Grok subscription credentials: fx reads the
* same `~/.fx/grok-auth.json` session file when its provider is `grok`
* (vercel-labs/fx `src/core/auth/grok_session.zig`, `src/core/shared/
* profile_paths.zig`: `grok_auth_file_name = "grok-auth.json"`).
*/

export type PeerSyncTarget = "opencode" | "pi" | "omp";
export type PeerSyncTarget = "opencode" | "pi" | "omp" | "fx";

export interface PeerAvailability {
/** Grok-subscription peer; absent in older snapshots — treat as unavailable. */
fx?: boolean | undefined;
omp: boolean;
opencode: boolean;
/**
Expand All @@ -26,6 +33,7 @@ const PEER_TARGETS = [
"opencode",
"pi",
"omp",
"fx",
] as const satisfies readonly PeerSyncTarget[];

export const ALL_PEER_SYNC_TARGETS: readonly PeerSyncTarget[] = PEER_TARGETS;
Expand All @@ -34,18 +42,22 @@ export function isPeerTargetAvailable(
target: PeerSyncTarget,
availability: PeerAvailability
): boolean {
return availability[target];
return availability[target] ?? false;
}

/**
* Adjust install readiness for account credential kind.
* OIDC / subscription login needs pi xAI OAuth; API keys only need install.
* fx has no xAI API-key path (OIDC session only), so API-key sync hides fx.
*/
export function effectivePeerAvailabilityForKind(
accountKind: "api_key" | "oidc",
availability: PeerAvailability
): PeerAvailability {
if (accountKind === "api_key" || availability.piOauthCapable) {
if (accountKind === "api_key") {
return { ...availability, fx: false };
}
if (availability.piOauthCapable) {
return availability;
}
return {
Expand All @@ -58,16 +70,18 @@ export function effectivePeerAvailabilityForKind(
* Split protocol-eligible targets into sync-ready vs not-installed.
* Callers may further filter by protocol (e.g. Grok OIDC uses
* `effectivePeerAvailabilityForKind` so pi requires `piOauthCapable`).
* Generic over the caller's target union so plugins with a narrower
* protocol set (Codex/Claude without fx) keep their local types.
*/
export function partitionPeerTargets(
protocolTargets: readonly PeerSyncTarget[],
export function partitionPeerTargets<T extends PeerSyncTarget>(
protocolTargets: readonly T[],
availability: PeerAvailability
): {
available: PeerSyncTarget[];
unavailable: PeerSyncTarget[];
available: T[];
unavailable: T[];
} {
const available: PeerSyncTarget[] = [];
const unavailable: PeerSyncTarget[] = [];
const available: T[] = [];
const unavailable: T[] = [];
for (const target of protocolTargets) {
if (isPeerTargetAvailable(target, availability)) {
available.push(target);
Expand Down
12 changes: 12 additions & 0 deletions packages/plugin-grok/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,16 @@
"pier.grok.accounts.settings.syncPeersFailedOmp": "Couldn't sync credentials to OMP. Make sure OMP is installed and has been opened at least once on this device.",
"pier.grok.accounts.settings.syncPeersFailedOpencode": "Couldn't sync credentials to OpenCode. Make sure OpenCode is installed on this device.",
"pier.grok.accounts.settings.syncPeersFailedPi": "Couldn't sync credentials to Pi. Make sure Pi is installed on this device (0.80.8+ for login accounts).",
"pier.grok.accounts.settings.syncPeersFailedFx": "Couldn't sync credentials to fx. Make sure fx is installed on this device.",
"pier.grok.accounts.settings.syncPeersFailedFxApiKey": "fx can only receive login (OIDC) accounts. Switch to a login account to sync to fx.",
"pier.grok.accounts.settings.syncPeersPiOauthHint": "Pi is installed but needs version 0.80.8 or later to receive login accounts. API-key accounts can still sync to older Pi.",
"pier.grok.accounts.settings.syncPeersFailedNoActive": "Select a managed Grok account first, then try syncing again.",
"pier.grok.accounts.settings.syncPeersFailedNoTargets": "Select at least one tool to sync.",
"pier.grok.switch.syncSectionLabel": "Also switch the Grok account in:",
"pier.grok.switch.syncTarget.opencode": "OpenCode",
"pier.grok.switch.syncTarget.pi": "Pi",
"pier.grok.switch.syncTarget.omp": "OMP",
"pier.grok.switch.syncTarget.fx": "fx",
"pier.grok.accounts.settings.addDialogTabAccount": "Account login",
"pier.grok.accounts.settings.addDialogTabApiKey": "API key",
"pier.grok.accounts.settings.addDialogAccountDescription": "Sign in with a device code from the Grok CLI. The verification link and code appear here; the account is added automatically after authorization.",
Expand Down Expand Up @@ -238,13 +241,16 @@
"pier.grok.accounts.settings.syncPeersFailedOmp": "无法同步凭证到 OMP。请确认本机已安装 OMP,并至少打开过一次。",
"pier.grok.accounts.settings.syncPeersFailedOpencode": "无法同步凭证到 OpenCode。请确认本机已安装 OpenCode。",
"pier.grok.accounts.settings.syncPeersFailedPi": "无法同步凭证到 Pi。请确认本机已安装 Pi(登录类账号需 0.80.8+)。",
"pier.grok.accounts.settings.syncPeersFailedFx": "无法同步凭证到 fx。请确认本机已安装 fx。",
"pier.grok.accounts.settings.syncPeersFailedFxApiKey": "fx 只能接收登录类(OIDC)账号。请切换到登录类账号后再同步到 fx。",
"pier.grok.accounts.settings.syncPeersPiOauthHint": "已检测到 Pi,但登录类账号需要 0.80.8 或更高版本才能同步。API key 账号仍可同步到旧版 Pi。",
"pier.grok.accounts.settings.syncPeersFailedNoActive": "请先选择一个受管 Grok 账号,再尝试同步。",
"pier.grok.accounts.settings.syncPeersFailedNoTargets": "请至少选择一个要同步的工具。",
"pier.grok.switch.syncSectionLabel": "同时切换以下工具的 Grok 账号:",
"pier.grok.switch.syncTarget.opencode": "OpenCode",
"pier.grok.switch.syncTarget.pi": "Pi",
"pier.grok.switch.syncTarget.omp": "OMP",
"pier.grok.switch.syncTarget.fx": "fx",
"pier.grok.accounts.settings.addDialogTabAccount": "账号登录",
"pier.grok.accounts.settings.addDialogTabApiKey": "API key",
"pier.grok.accounts.settings.addDialogAccountDescription": "使用 Grok CLI 的设备码登录。验证链接与设备码会显示在这里;授权完成后,账号会自动添加。",
Expand Down Expand Up @@ -361,13 +367,16 @@
"pier.grok.accounts.settings.syncPeersFailedOmp": "OMP へ認証情報を同期できませんでした。OMP がインストールされ、このデバイスで一度は開かれていることを確認してください。",
"pier.grok.accounts.settings.syncPeersFailedOpencode": "OpenCode へ認証情報を同期できませんでした。このデバイスに OpenCode がインストールされていることを確認してください。",
"pier.grok.accounts.settings.syncPeersFailedPi": "Pi へ認証情報を同期できませんでした。このデバイスに Pi がインストールされていることを確認してください(ログインアカウントは 0.80.8 以上)。",
"pier.grok.accounts.settings.syncPeersFailedFx": "fxへ認証情報を同期できませんでした。このデバイスにfxがインストールされていることを確認してください。",
"pier.grok.accounts.settings.syncPeersFailedFxApiKey": "fxはログイン(OIDC)アカウントのみ受け付けます。ログインアカウントに切り替えてからfxに同期してください。",
"pier.grok.accounts.settings.syncPeersPiOauthHint": "Pi はインストールされていますが、ログインアカウントを受け取るには 0.80.8 以降が必要です。API キーアカウントは古い Pi にも同期できます。",
"pier.grok.accounts.settings.syncPeersFailedNoActive": "先に管理中の Grok アカウントを選んでから、もう一度同期してください。",
"pier.grok.accounts.settings.syncPeersFailedNoTargets": "同期するツールを 1 つ以上選んでください。",
"pier.grok.switch.syncSectionLabel": "あわせて次のツールの Grok アカウントも切り替え:",
"pier.grok.switch.syncTarget.opencode": "OpenCode",
"pier.grok.switch.syncTarget.pi": "Pi",
"pier.grok.switch.syncTarget.omp": "OMP",
"pier.grok.switch.syncTarget.fx": "fx",
"pier.grok.accounts.settings.addDialogTabAccount": "アカウントでログイン",
"pier.grok.accounts.settings.addDialogTabApiKey": "API キー",
"pier.grok.accounts.settings.addDialogAccountDescription": "Grok CLI のデバイスコードでログインします。確認リンクとコードがここに表示され、認可が終わるとアカウントが自動で追加されます。",
Expand Down Expand Up @@ -484,13 +493,16 @@
"pier.grok.accounts.settings.syncPeersFailedOmp": "OMP로 자격 증명을 동기화하지 못했습니다. OMP가 설치되어 있고 이 기기에서 한 번 이상 열렸는지 확인하세요.",
"pier.grok.accounts.settings.syncPeersFailedOpencode": "OpenCode로 자격 증명을 동기화하지 못했습니다. 이 기기에 OpenCode가 설치되어 있는지 확인하세요.",
"pier.grok.accounts.settings.syncPeersFailedPi": "Pi로 자격 증명을 동기화하지 못했습니다. 이 기기에 Pi가 설치되어 있는지 확인하세요(로그인 계정은 0.80.8 이상).",
"pier.grok.accounts.settings.syncPeersFailedFx": "fx로 자격 증명을 동기화하지 못했습니다. 이 기기에 fx가 설치되어 있는지 확인하세요.",
"pier.grok.accounts.settings.syncPeersFailedFxApiKey": "fx는 로그인(OIDC) 계정만 받을 수 있습니다. 로그인 계정으로 전환한 뒤 fx에 동기화하세요.",
"pier.grok.accounts.settings.syncPeersPiOauthHint": "Pi는 설치되어 있지만 로그인 계정을 받으려면 0.80.8 이상이 필요합니다. API 키 계정은 이전 Pi에도 동기화할 수 있습니다.",
"pier.grok.accounts.settings.syncPeersFailedNoActive": "먼저 관리 중인 Grok 계정을 선택한 다음 다시 동기화하세요.",
"pier.grok.accounts.settings.syncPeersFailedNoTargets": "동기화할 도구를 하나 이상 선택하세요.",
"pier.grok.switch.syncSectionLabel": "다음 도구의 Grok 계정도 함께 전환:",
"pier.grok.switch.syncTarget.opencode": "OpenCode",
"pier.grok.switch.syncTarget.pi": "Pi",
"pier.grok.switch.syncTarget.omp": "OMP",
"pier.grok.switch.syncTarget.fx": "fx",
"pier.grok.accounts.settings.addDialogTabAccount": "계정 로그인",
"pier.grok.accounts.settings.addDialogTabApiKey": "API 키",
"pier.grok.accounts.settings.addDialogAccountDescription": "Grok CLI의 디바이스 코드로 로그인합니다. 확인 링크와 코드가 여기에 나타나며, 승인이 끝나면 계정이 자동으로 추가됩니다.",
Expand Down
43 changes: 42 additions & 1 deletion packages/plugin-grok/src/main/cross-tool-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ import type { CrossToolSyncTarget } from "../shared/accounts.ts";
* - pi `~/.pi/agent/auth.json`: provider key `xai`
* - oauth (pi ≥ 0.80.8): `{ type: "oauth", access, refresh, expires, accountId? }`
* same xAI client_id as Grok CLI (`b1a00492-073a-47ea-816f-4c329264a828`)
* - api: `{ type: "api_key", key }` (not OpenCode's `"api"`)
* - omp `~/.omp/agent/agent.db` `auth_credentials`:
* - provider `xai-oauth`, credential_type `oauth`,
* identity_key `account:<user_id>`,
* data `{ access, refresh, expires }`
* - api_key rows use provider `xai`, credential_type `api_key`, data `{ key }`
* - fx `~/.fx/grok-auth.json`: `{ version: 1, access_token, refresh_token,
* expires_at_ms, account_id }` (vercel-labs/fx `src/core/auth/
* grok_session.zig` + `src/core/shared/profile_paths.zig`).
* fx has no xAI API-key path: OIDC only; api_key sync to fx must fail closed.
*/

export type GrokSyncCredential =
Expand Down Expand Up @@ -76,6 +79,41 @@ function ompDbPath(opts: CrossToolSyncOptions): string {
return join(home, ".omp", "agent", "agent.db");
}

function fxGrokAuthPath(opts: CrossToolSyncOptions): string {
const home = opts.homeDir ?? defaultHomeDir();
return join(home, ".fx", "grok-auth.json");
}

/** fx Grok session file: `{ version: 1, access_token, refresh_token, expires_at_ms, account_id }`. */
function fxGrokSessionEntry(
credential: GrokSyncCredential
): Record<string, unknown> {
if (credential.kind !== "oauth") {
// fx has no xAI API-key path: fail closed, never write a fake session.
throw new Error("fx has no xAI API-key path; sync an OIDC account instead");
}
return {
version: 1,
access_token: credential.accessToken,
refresh_token: credential.refreshToken,
expires_at_ms: credential.expiresAtMs,
account_id: credential.accountId,
};
}

async function syncFx(
credential: GrokSyncCredential,
opts: CrossToolSyncOptions
): Promise<void> {
const path = fxGrokAuthPath(opts);
await mkdir(dirname(path), { recursive: true });
await writeFileAtomic(
path,
`${JSON.stringify(fxGrokSessionEntry(credential), null, 2)}\n`,
{ mode: 0o600 }
);
}

async function readJsonObject(path: string): Promise<Record<string, unknown>> {
if (!existsSync(path)) return {};
try {
Expand Down Expand Up @@ -325,6 +363,9 @@ export async function syncCrossToolCredentials(
case "omp":
await syncOmp(credential, opts);
break;
case "fx":
await syncFx(credential, opts);
break;
default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ function samePeerAvailability(
right: PeerAvailability
): boolean {
return (
left.fx === right.fx &&
left.omp === right.omp &&
left.opencode === right.opencode &&
left.pi === right.pi &&
Expand Down
17 changes: 16 additions & 1 deletion packages/plugin-grok/src/renderer/format-account-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,21 @@ export function formatAccountError(err: unknown, t: Translate): string {
"Couldn't sync credentials to OpenCode. Make sure OpenCode is installed on this device."
);
}
if (lower.includes("fx has no xai api-key path")) {
return t(
"pier.grok.accounts.settings.syncPeersFailedFxApiKey",
"fx can only receive login (OIDC) accounts. Switch to a login account to sync to fx."
);
}
if (
/(^|;\s*)fx:/.test(raw) ||
(lower.includes("fx ") && lower.includes("not found"))
) {
return t(
"pier.grok.accounts.settings.syncPeersFailedFx",
"Couldn't sync credentials to fx. Make sure fx is installed on this device."
);
}
if (
/(^|;\s*)pi:/.test(raw) ||
(lower.includes("pi ") && lower.includes("not found"))
Expand All @@ -169,7 +184,7 @@ export function formatAccountError(err: unknown, t: Translate): string {
"Select at least one tool to sync."
);
}
if (/opencode:|pi:|omp:/.test(raw)) {
if (/opencode:|pi:|omp:|fx:/.test(raw)) {
return t(
"pier.grok.accounts.settings.syncPeersFailed",
"Couldn't sync credentials to the selected tools. Try again after opening those tools once."
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-grok/src/renderer/switch-confirm-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function isPeerAvailability(value: unknown): value is PeerAvailability {
}
const record = value as Record<string, unknown>;
return (
(record.fx === undefined || typeof record.fx === "boolean") &&
typeof record.omp === "boolean" &&
typeof record.opencode === "boolean" &&
typeof record.pi === "boolean" &&
Expand Down Expand Up @@ -114,6 +115,7 @@ function SwitchConfirmContent({
opencode: t("pier.grok.switch.syncTarget.opencode", "OpenCode"),
pi: t("pier.grok.switch.syncTarget.pi", "Pi"),
omp: t("pier.grok.switch.syncTarget.omp", "OMP"),
fx: t("pier.grok.switch.syncTarget.fx", "fx"),
};

const sectionLabel =
Expand Down
Loading
Loading