From 53c8a67ac312256ad5d6a51c1cf34740066cd86b Mon Sep 17 00:00:00 2001 From: Moss Date: Sun, 21 Jun 2026 04:45:04 +0800 Subject: [PATCH 1/3] Add Moss Desktop release workflow --- .github/workflows/moss-desktop-release.yml | 95 ++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 .github/workflows/moss-desktop-release.yml diff --git a/.github/workflows/moss-desktop-release.yml b/.github/workflows/moss-desktop-release.yml new file mode 100644 index 000000000..e078ad76e --- /dev/null +++ b/.github/workflows/moss-desktop-release.yml @@ -0,0 +1,95 @@ +name: Moss Desktop Release + +on: + workflow_dispatch: + inputs: + channel: + description: electron-updater channel + required: true + default: latest + type: choice + options: + - latest + - beta + +jobs: + macos-release: + name: Build, sign, notarize, and verify macOS artifacts + runs-on: macos-latest + timeout-minutes: 120 + env: + APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }} + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Download bundled runtimes + run: | + bun run claude:download:all + bun run codex:download:all + + - name: Verify runtime and source gates + run: | + bun run test:runtime + bun run ts:check --pretty false + + - name: Build app + run: bun run build + + - name: Verify Apple release credentials + run: bun run release:credentials:strict + + - name: Package signed macOS artifacts + run: | + rm -rf release + bun run package:mac + + - name: Smoke packaged app bundle + run: bun run test:packaged-app-smoke + + - name: Notarize and staple release artifacts + run: bun run release:notarize + + - name: Generate update manifests + run: node scripts/generate-update-manifest.mjs --channel "${{ inputs.channel }}" + + - name: Prepare release upload plan + run: node scripts/upload-release.mjs --dry-run --channel "${{ inputs.channel }}" + + - name: Audit signed release evidence + run: bun run release:evidence:audit --require-notarization + + - name: Verify commercial release evidence + run: node scripts/verify-release-packaging.mjs --require-artifacts --require-bundled-binaries --require-notarization --require-upload-plan + + - name: Upload verified release artifacts + uses: actions/upload-artifact@v4 + with: + name: moss-desktop-macos-${{ inputs.channel }} + path: | + release/*.dmg + release/*.zip + release/*mac*.yml + release/notarization-*.json + release/codesign-*.txt + release/staple-*.txt + release/spctl-*.txt + .1code/program/packaged-app-smoke/**/report.json + .1code/program/release-credentials/**/report.json + .1code/program/release-evidence-audit/**/report.json + .1code/program/release-upload/**/manifest.json + if-no-files-found: error From 8405ba70ea45eb52f86595f3bd6f380590033014 Mon Sep 17 00:00:00 2001 From: Moss Date: Mon, 22 Jun 2026 05:41:53 +0800 Subject: [PATCH 2/3] Integrate runtime support for Moss Desktop release workflow --- package.json | 7 +- scripts/audit-release-evidence.mjs | 257 +++ scripts/notarize-release-artifacts.mjs | 207 +++ scripts/smoke-packaged-app.mjs | 594 +++++++ scripts/upload-release.mjs | 286 ++++ scripts/verify-release-credentials.mjs | 270 +++ scripts/verify-release-packaging.mjs | 681 ++++++++ .../lib/agent-runtime/adapters/claude-code.ts | 122 ++ src/main/lib/agent-runtime/adapters/codex.ts | 73 + .../lib/agent-runtime/adapters/custom-acp.ts | 37 + src/main/lib/agent-runtime/adapters/hermes.ts | 67 + src/main/lib/agent-runtime/adapters/index.ts | 21 + .../codex-native-message-parts.ts | 302 ++++ .../agent-runtime/codex-native-recovery.ts | 467 ++++++ .../lib/agent-runtime/codex-native-resume.ts | 168 ++ .../lib/agent-runtime/codex-native-session.ts | 1465 +++++++++++++++++ src/main/lib/agent-runtime/control-plane.ts | 394 +++++ src/main/lib/agent-runtime/events.ts | 189 +++ .../agent-runtime/hermes-native-session.ts | 286 ++++ src/main/lib/agent-runtime/index.ts | 10 + src/main/lib/agent-runtime/manifests.ts | 175 ++ src/main/lib/agent-runtime/session-actions.ts | 598 +++++++ src/main/lib/agent-runtime/session-records.ts | 208 +++ src/main/lib/agent-runtime/session-store.ts | 48 + src/main/lib/agent-runtime/types.ts | 294 ++++ src/main/lib/codex-automations.test.ts | 168 ++ src/main/lib/codex-automations.ts | 384 +++++ src/main/lib/hermes/runtime.ts | 68 + src/main/lib/mcp-stdio-compat.test.ts | 239 +++ src/main/lib/mcp-stdio-compat.ts | 463 ++++++ src/main/lib/moss-account/entitlement.test.ts | 224 +++ src/main/lib/moss-account/entitlement.ts | 513 ++++++ src/main/lib/moss-source/bootstrap.ts | 239 +++ src/main/lib/moss-source/hooks.ts | 320 ++++ src/main/lib/moss-source/index.ts | 10 + src/main/lib/moss-source/layout.ts | 47 + src/main/lib/moss-source/projection.ts | 908 ++++++++++ .../lib/moss-source/provider-config.test.ts | 413 +++++ src/main/lib/moss-source/provider-config.ts | 585 +++++++ src/main/lib/moss-source/provider-secrets.ts | 106 ++ src/main/lib/moss-source/registry.ts | 361 ++++ .../lib/moss-source/runtime-materializer.ts | 358 ++++ src/main/lib/moss-source/subagents.ts | 271 +++ src/main/lib/moss-source/types.ts | 39 + src/main/lib/shared-resources/governance.ts | 520 ++++++ src/main/lib/shared-resources/types.ts | 91 + .../routers/chat-runtime-selection.test.ts | 52 + .../trpc/routers/chat-runtime-selection.ts | 51 + .../trpc/routers/codex-mcp-session.test.ts | 62 + .../lib/trpc/routers/codex-mcp-session.ts | 25 + src/shared/codex-runtime-notices.test.ts | 44 + src/shared/codex-runtime-notices.ts | 58 + 52 files changed, 13844 insertions(+), 1 deletion(-) create mode 100644 scripts/audit-release-evidence.mjs create mode 100644 scripts/notarize-release-artifacts.mjs create mode 100644 scripts/smoke-packaged-app.mjs create mode 100644 scripts/upload-release.mjs create mode 100644 scripts/verify-release-credentials.mjs create mode 100644 scripts/verify-release-packaging.mjs create mode 100644 src/main/lib/agent-runtime/adapters/claude-code.ts create mode 100644 src/main/lib/agent-runtime/adapters/codex.ts create mode 100644 src/main/lib/agent-runtime/adapters/custom-acp.ts create mode 100644 src/main/lib/agent-runtime/adapters/hermes.ts create mode 100644 src/main/lib/agent-runtime/adapters/index.ts create mode 100644 src/main/lib/agent-runtime/codex-native-message-parts.ts create mode 100644 src/main/lib/agent-runtime/codex-native-recovery.ts create mode 100644 src/main/lib/agent-runtime/codex-native-resume.ts create mode 100644 src/main/lib/agent-runtime/codex-native-session.ts create mode 100644 src/main/lib/agent-runtime/control-plane.ts create mode 100644 src/main/lib/agent-runtime/events.ts create mode 100644 src/main/lib/agent-runtime/hermes-native-session.ts create mode 100644 src/main/lib/agent-runtime/index.ts create mode 100644 src/main/lib/agent-runtime/manifests.ts create mode 100644 src/main/lib/agent-runtime/session-actions.ts create mode 100644 src/main/lib/agent-runtime/session-records.ts create mode 100644 src/main/lib/agent-runtime/session-store.ts create mode 100644 src/main/lib/agent-runtime/types.ts create mode 100644 src/main/lib/codex-automations.test.ts create mode 100644 src/main/lib/codex-automations.ts create mode 100644 src/main/lib/hermes/runtime.ts create mode 100644 src/main/lib/mcp-stdio-compat.test.ts create mode 100644 src/main/lib/mcp-stdio-compat.ts create mode 100644 src/main/lib/moss-account/entitlement.test.ts create mode 100644 src/main/lib/moss-account/entitlement.ts create mode 100644 src/main/lib/moss-source/bootstrap.ts create mode 100644 src/main/lib/moss-source/hooks.ts create mode 100644 src/main/lib/moss-source/index.ts create mode 100644 src/main/lib/moss-source/layout.ts create mode 100644 src/main/lib/moss-source/projection.ts create mode 100644 src/main/lib/moss-source/provider-config.test.ts create mode 100644 src/main/lib/moss-source/provider-config.ts create mode 100644 src/main/lib/moss-source/provider-secrets.ts create mode 100644 src/main/lib/moss-source/registry.ts create mode 100644 src/main/lib/moss-source/runtime-materializer.ts create mode 100644 src/main/lib/moss-source/subagents.ts create mode 100644 src/main/lib/moss-source/types.ts create mode 100644 src/main/lib/shared-resources/governance.ts create mode 100644 src/main/lib/shared-resources/types.ts create mode 100644 src/main/lib/trpc/routers/chat-runtime-selection.test.ts create mode 100644 src/main/lib/trpc/routers/chat-runtime-selection.ts create mode 100644 src/main/lib/trpc/routers/codex-mcp-session.test.ts create mode 100644 src/main/lib/trpc/routers/codex-mcp-session.ts create mode 100644 src/shared/codex-runtime-notices.test.ts create mode 100644 src/shared/codex-runtime-notices.ts diff --git a/package.json b/package.json index da2a5e747..97eb0b406 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,12 @@ "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", "ts:check": "tsgo --noEmit", - "postinstall": "node -e \"if(!process.env.VERCEL){require('child_process').execSync('electron-rebuild -f -w better-sqlite3,node-pty',{stdio:'inherit'})}\" && node scripts/patch-electron-dev.mjs" + "postinstall": "node -e \"if(!process.env.VERCEL){require('child_process').execSync('electron-rebuild -f -w better-sqlite3,node-pty',{stdio:'inherit'})}\" && node scripts/patch-electron-dev.mjs", + "test:runtime": "bun test src/main/lib/moss-account/entitlement.test.ts src/main/lib/moss-source/provider-config.test.ts src/main/lib/mcp-stdio-compat.test.ts src/main/lib/trpc/routers/chat-runtime-selection.test.ts src/main/lib/trpc/routers/codex-mcp-session.test.ts src/main/lib/codex-automations.test.ts src/shared/codex-runtime-notices.test.ts", + "release:credentials:strict": "node scripts/verify-release-credentials.mjs --require-credentials", + "test:packaged-app-smoke": "node scripts/smoke-packaged-app.mjs", + "release:notarize": "node scripts/notarize-release-artifacts.mjs", + "release:evidence:audit": "node scripts/audit-release-evidence.mjs" }, "dependencies": { "@ai-sdk/react": "^3.0.14", diff --git a/scripts/audit-release-evidence.mjs b/scripts/audit-release-evidence.mjs new file mode 100644 index 000000000..05a1e46d8 --- /dev/null +++ b/scripts/audit-release-evidence.mjs @@ -0,0 +1,257 @@ +#!/usr/bin/env node + +import fs from "node:fs" +import path from "node:path" + +const root = process.cwd() +const requireNotarization = process.argv.includes("--require-notarization") +const generatedAt = new Date().toISOString() +const stamp = generatedAt.replace(/[:.]/g, "-") +const reportDir = path.join(root, ".1code/program/release-evidence-audit", stamp) +const reportPath = path.join(reportDir, "report.json") +const latestPath = path.join(root, ".1code/program/release-evidence-audit/latest.json") +const failures = [] +const warnings = [] +const blockers = [] + +function projectPath(relativePath) { + return path.join(root, relativePath) +} + +function normalizeRelative(filePath) { + return path.relative(root, filePath).split(path.sep).join("/") +} + +function exists(relativePath) { + return fs.existsSync(projectPath(relativePath)) +} + +function listReleaseFiles() { + const releaseDir = projectPath("release") + if (!fs.existsSync(releaseDir)) return [] + return fs.readdirSync(releaseDir) + .map((name) => path.join(releaseDir, name)) + .filter((filePath) => fs.statSync(filePath).isFile()) + .sort() +} + +function releaseAppDirs() { + return [ + { + arch: "arm64", + path: "release/mac-arm64/1Code.app", + }, + { + arch: "x64", + path: "release/mac/1Code.app", + }, + ].map((app) => ({ + ...app, + present: exists(app.path), + })) +} + +function readJsonPath(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")) + } catch (error) { + failures.push(`Could not parse ${normalizeRelative(filePath)}: ${error.message}`) + return undefined + } +} + +function readTextRelative(relativePath) { + const filePath = projectPath(relativePath) + if (!fs.existsSync(filePath)) return undefined + return fs.readFileSync(filePath, "utf8") +} + +function parseNotaryStatus(stdoutPath) { + const raw = readTextRelative(stdoutPath) + if (!raw) return { + path: stdoutPath, + parseable: false, + status: "missing", + } + + try { + const parsed = JSON.parse(raw) + return { + path: stdoutPath, + parseable: true, + id: parsed.id ?? null, + status: parsed.status ?? "unknown", + accepted: parsed.status === "Accepted", + } + } catch { + return { + path: stdoutPath, + parseable: false, + status: "unparseable", + } + } +} + +function commandOutputReferences(command) { + return [command.stdout, command.stderr] + .filter((value) => typeof value === "string" && value.length > 0) + .map((value) => ({ + path: value, + present: exists(value), + })) +} + +function inspectNotarizationReport(filePath) { + const report = readJsonPath(filePath) + const commands = Array.isArray(report?.commands) ? report.commands : [] + const commandFailures = commands.filter((command) => command.exitCode !== 0) + const references = commands.flatMap(commandOutputReferences) + const missingReferences = references.filter((reference) => !reference.present) + const notaryCommands = commands.filter((command) => String(command.label ?? "").startsWith("notarytool submit")) + const notaryStatuses = notaryCommands + .map((command) => command.stdout) + .filter((stdoutPath) => typeof stdoutPath === "string" && stdoutPath.length > 0) + .map(parseNotaryStatus) + const unacceptedStatuses = notaryStatuses.filter((status) => status.accepted !== true) + const summary = report?.summary ?? {} + const dryRun = report?.mode?.dryRun === true + const valid = Boolean(report) + && report.status === "passed" + && dryRun === false + && commands.length > 0 + && commandFailures.length === 0 + && missingReferences.length === 0 + && Number(summary.notarytoolSubmissions ?? 0) > 0 + && Number(summary.stapleCommands ?? 0) > 0 + && Number(summary.codesignVerifications ?? 0) > 0 + && Number(summary.spctlAssessments ?? 0) > 0 + && notaryStatuses.length > 0 + && unacceptedStatuses.length === 0 + + return { + path: normalizeRelative(filePath), + status: report?.status ?? "missing", + dryRun, + commandCount: commands.length, + commandFailures: commandFailures.map((command) => ({ + label: command.label ?? command.command ?? "unknown", + exitCode: command.exitCode ?? null, + })), + missingReferences, + summary: { + notarytoolSubmissions: Number(summary.notarytoolSubmissions ?? 0), + stapleCommands: Number(summary.stapleCommands ?? 0), + codesignVerifications: Number(summary.codesignVerifications ?? 0), + spctlAssessments: Number(summary.spctlAssessments ?? 0), + }, + notaryStatuses, + valid, + } +} + +const releaseFiles = listReleaseFiles() +const macArtifacts = releaseFiles + .filter((filePath) => /\.(dmg|zip)$/i.test(filePath)) + .map(normalizeRelative) +const updateManifests = releaseFiles + .filter((filePath) => /(?:latest|beta)-mac(?:-x64)?\.yml$/.test(path.basename(filePath))) + .map(normalizeRelative) +const notarizationReportFiles = releaseFiles + .filter((filePath) => /^notarization-.+\.json$/i.test(path.basename(filePath))) +const notarizationReports = notarizationReportFiles.map(inspectNotarizationReport) +const validNotarizationReports = notarizationReports.filter((report) => report.valid) +const apps = releaseAppDirs() +const presentApps = apps.filter((app) => app.present) +const notarizationEvidenceFiles = releaseFiles + .filter((filePath) => /notary|notar|codesign|staple|spctl/i.test(path.basename(filePath))) + .map(normalizeRelative) + +if (macArtifacts.length < 4) { + blockers.push(`Expected at least 4 macOS DMG/ZIP artifacts, found ${macArtifacts.length}.`) +} +if (updateManifests.length < 2) { + blockers.push(`Expected at least 2 macOS update manifests, found ${updateManifests.length}.`) +} +if (presentApps.length < 2) { + blockers.push(`Expected both packaged app directories, found ${presentApps.length}.`) +} +if (validNotarizationReports.length === 0) { + blockers.push("No valid signed/notarized release evidence report was found.") +} + +for (const report of notarizationReports) { + if (!report.valid) { + warnings.push(`Notarization report ${report.path} is not valid distributable evidence.`) + } +} + +if (requireNotarization && blockers.length > 0) { + failures.push(...blockers) +} + +const status = failures.length > 0 + ? "failed" + : blockers.length > 0 + ? "blocked" + : "passed" + +const report = { + status, + generatedAt, + mode: { + requireNotarization, + }, + releaseDir: "release", + artifacts: { + macArtifacts, + updateManifests, + notarizationEvidenceFiles, + apps, + }, + notarization: { + reports: notarizationReports, + validReports: validNotarizationReports.map((entry) => entry.path), + acceptedSubmissions: notarizationReports.reduce( + (count, entry) => count + entry.notaryStatuses.filter((status) => status.accepted === true).length, + 0, + ), + validReportCount: validNotarizationReports.length, + }, + distribution: { + distributable: status === "passed", + blockerCount: blockers.length, + blockers, + }, + warnings, + failures, +} + +fs.mkdirSync(reportDir, { recursive: true }) +fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`) +fs.writeFileSync(latestPath, `${JSON.stringify({ + report: normalizeRelative(reportPath), + generatedAt, + status, +}, null, 2)}\n`) + +console.log("Moss release evidence audit") +console.log(`status: ${status}`) +console.log(`report: ${normalizeRelative(reportPath)}`) +console.log(`mac artifacts: ${macArtifacts.length}`) +console.log(`update manifests: ${updateManifests.length}`) +console.log(`notarization reports: ${notarizationReports.length}`) +console.log(`valid notarization reports: ${validNotarizationReports.length}`) + +for (const message of warnings) { + console.warn(`warning: ${message}`) +} +for (const message of blockers) { + console.warn(`blocker: ${message}`) +} +for (const message of failures) { + console.error(`error: ${message}`) +} + +if (failures.length > 0) { + process.exit(1) +} diff --git a/scripts/notarize-release-artifacts.mjs b/scripts/notarize-release-artifacts.mjs new file mode 100644 index 000000000..485b8837a --- /dev/null +++ b/scripts/notarize-release-artifacts.mjs @@ -0,0 +1,207 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process" +import fs from "node:fs" +import path from "node:path" + +const root = process.cwd() +const dryRun = process.argv.includes("--dry-run") +const releaseDir = path.join(root, "release") +const generatedAt = new Date().toISOString() +const stamp = generatedAt.replace(/[:.]/g, "-") +const reportDir = dryRun + ? path.join(root, ".1code/program/release-packaging") + : releaseDir +const reportPath = path.join(reportDir, dryRun ? `notarization-dry-run-${stamp}.json` : `notarization-${stamp}.json`) +const failures = [] +const warnings = [] +const commands = [] + +function exists(filePath) { + return fs.existsSync(filePath) +} + +function relative(filePath) { + return path.relative(root, filePath).split(path.sep).join("/") +} + +function releaseFilesMatching(pattern) { + if (!exists(releaseDir)) return [] + return fs.readdirSync(releaseDir) + .map((name) => path.join(releaseDir, name)) + .filter((filePath) => fs.statSync(filePath).isFile()) + .filter((filePath) => pattern.test(path.basename(filePath))) + .sort() +} + +function releaseAppDirs() { + return [ + path.join(releaseDir, "mac-arm64/1Code.app"), + path.join(releaseDir, "mac/1Code.app"), + ].filter(exists) +} + +function redactArg(arg) { + if (arg === process.env.APPLE_ID) return "" + if (arg === process.env.APPLE_TEAM_ID) return "" + if (arg === process.env.APPLE_APP_SPECIFIC_PASSWORD) return "" + return arg +} + +function credentialState(name) { + return process.env[name] ? "set" : "missing" +} + +function runCommand(label, command, args, outputBaseName) { + const redactedArgs = args.map(redactArg) + const record = { + label, + command, + args: redactedArgs, + dryRun, + exitCode: dryRun ? 0 : undefined, + stdout: undefined, + stderr: undefined, + } + + if (dryRun) { + commands.push(record) + return + } + + const result = spawnSync(command, args, { + cwd: root, + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, + }) + + const stdoutPath = path.join(releaseDir, `${outputBaseName}.stdout.txt`) + const stderrPath = path.join(releaseDir, `${outputBaseName}.stderr.txt`) + fs.writeFileSync(stdoutPath, result.stdout ?? "") + fs.writeFileSync(stderrPath, result.stderr ?? "") + + record.exitCode = result.status ?? 1 + record.stdout = relative(stdoutPath) + record.stderr = relative(stderrPath) + commands.push(record) + + if (record.exitCode !== 0) { + failures.push(`${label} failed with exit code ${record.exitCode}.`) + } +} + +const credentials = { + appleId: credentialState("APPLE_ID"), + appleTeamId: credentialState("APPLE_TEAM_ID"), + appSpecificPassword: credentialState("APPLE_APP_SPECIFIC_PASSWORD"), + appleIdentity: credentialState("APPLE_IDENTITY"), + cscLink: credentialState("CSC_LINK"), + cscKeyPassword: credentialState("CSC_KEY_PASSWORD"), +} + +for (const [name, state] of Object.entries(credentials)) { + if (state !== "set") failures.push(`Missing required signing/notarization credential: ${name}.`) +} + +const artifacts = releaseFilesMatching(/\.(dmg|zip)$/i) +const dmgArtifacts = artifacts.filter((filePath) => /\.dmg$/i.test(filePath)) +const apps = releaseAppDirs() + +if (artifacts.length === 0) { + failures.push("No DMG or ZIP release artifacts found in release/. Run bun run package:mac first.") +} +if (apps.length === 0) { + failures.push("No packaged .app directories found in release/mac*/. Run bun run package:mac first.") +} + +if (dryRun && failures.length > 0) { + warnings.push(...failures) + failures.length = 0 +} + +if (failures.length === 0) { + for (const artifact of artifacts) { + runCommand( + `notarytool submit ${relative(artifact)}`, + "xcrun", + [ + "notarytool", + "submit", + artifact, + "--apple-id", + process.env.APPLE_ID, + "--team-id", + process.env.APPLE_TEAM_ID, + "--password", + process.env.APPLE_APP_SPECIFIC_PASSWORD, + "--wait", + "--output-format", + "json", + ], + `notarytool-${stamp}-${path.basename(artifact).replace(/[^A-Za-z0-9_.-]/g, "_")}`, + ) + } + + for (const target of [...apps, ...dmgArtifacts]) { + runCommand( + `stapler staple ${relative(target)}`, + "xcrun", + ["stapler", "staple", target], + `staple-${stamp}-${path.basename(target).replace(/[^A-Za-z0-9_.-]/g, "_")}`, + ) + } + + for (const app of apps) { + runCommand( + `codesign verify ${relative(app)}`, + "codesign", + ["--verify", "--deep", "--strict", "--verbose=2", app], + `codesign-${stamp}-${path.basename(path.dirname(app)).replace(/[^A-Za-z0-9_.-]/g, "_")}`, + ) + runCommand( + `spctl assess ${relative(app)}`, + "spctl", + ["--assess", "--type", "execute", "--verbose=4", app], + `spctl-${stamp}-${path.basename(path.dirname(app)).replace(/[^A-Za-z0-9_.-]/g, "_")}`, + ) + } +} + +const report = { + status: failures.length === 0 ? "passed" : "failed", + generatedAt, + mode: { dryRun }, + credentials, + artifacts: artifacts.map(relative), + apps: apps.map(relative), + commands, + summary: { + notarytoolSubmissions: commands.filter((command) => command.label.startsWith("notarytool submit")).length, + stapleCommands: commands.filter((command) => command.label.startsWith("stapler staple")).length, + codesignVerifications: commands.filter((command) => command.label.startsWith("codesign verify")).length, + spctlAssessments: commands.filter((command) => command.label.startsWith("spctl assess")).length, + }, + warnings, + failures, +} + +fs.mkdirSync(reportDir, { recursive: true }) +fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`) + +console.log("Moss notarization evidence") +console.log(`status: ${report.status}`) +console.log(`report: ${relative(reportPath)}`) +console.log(`artifacts: ${artifacts.length}`) +console.log(`apps: ${apps.length}`) +console.log(`commands: ${commands.length}`) + +for (const message of warnings) { + console.warn(`warning: ${message}`) +} +for (const message of failures) { + console.error(`error: ${message}`) +} + +if (failures.length > 0) { + process.exit(1) +} diff --git a/scripts/smoke-packaged-app.mjs b/scripts/smoke-packaged-app.mjs new file mode 100644 index 000000000..c3a90b605 --- /dev/null +++ b/scripts/smoke-packaged-app.mjs @@ -0,0 +1,594 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process" +import crypto from "node:crypto" +import fs from "node:fs" +import { createRequire } from "node:module" +import net from "node:net" +import os from "node:os" +import path from "node:path" + +const root = process.cwd() +const require = createRequire(import.meta.url) +const asar = require("@electron/asar") +const plist = require("plist") +const yaml = require("js-yaml") + +const failures = [] +const warnings = [] +const packageJson = readJson("package.json") +const version = packageJson?.version +const productName = packageJson?.build?.productName ?? "1Code" +const appId = packageJson?.build?.appId ?? "dev.21st.agents" +const publishUrl = packageJson?.build?.publish?.url ?? "https://cdn.21st.dev/releases/desktop" +const devUrlPatterns = [ + /https?:\/\/localhost:5173\b/i, + /https?:\/\/localhost:5174\b/i, + /https?:\/\/127\.0\.0\.1:5173\b/i, + /https?:\/\/127\.0\.0\.1:5174\b/i, +] + +function projectPath(relativePath) { + return path.join(root, relativePath) +} + +function normalizeRelative(filePath) { + return path.relative(root, filePath).split(path.sep).join("/") +} + +function exists(relativePath) { + return fs.existsSync(projectPath(relativePath)) +} + +function fail(message) { + failures.push(message) +} + +function warn(message) { + warnings.push(message) +} + +function read(relativePath) { + return fs.readFileSync(projectPath(relativePath), "utf8") +} + +function readJson(relativePath) { + try { + return JSON.parse(read(relativePath)) + } catch (error) { + fail(`Could not parse ${relativePath}: ${error.message}`) + return undefined + } +} + +function readYaml(relativePath) { + try { + return yaml.load(read(relativePath)) + } catch (error) { + fail(`Could not parse ${relativePath}: ${error.message}`) + return undefined + } +} + +function statFile(relativePath) { + const filePath = projectPath(relativePath) + if (!fs.existsSync(filePath)) return undefined + return fs.statSync(filePath) +} + +function sha256(relativePath) { + const filePath = projectPath(relativePath) + return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex") +} + +function isExecutable(relativePath) { + const stat = statFile(relativePath) + return Boolean(stat && (stat.mode & 0o111)) +} + +function run(command, args) { + const result = spawnSync(command, args, { + cwd: root, + encoding: "utf8", + }) + return { + command: [command, ...args].join(" "), + exitCode: result.status, + stdout: result.stdout.trim(), + stderr: result.stderr.trim(), + error: result.error?.message, + } +} + +async function findFreePort() { + return new Promise((resolve, reject) => { + const server = net.createServer() + server.listen(0, "127.0.0.1", () => { + const address = server.address() + const port = typeof address === "object" && address ? address.port : undefined + server.close(() => { + if (port) resolve(port) + else reject(new Error("Could not allocate a local smoke port.")) + }) + }) + server.on("error", reject) + }) +} + +function devUrlMatches(text) { + return devUrlPatterns + .filter((pattern) => pattern.test(text)) + .map((pattern) => pattern.source) +} + +function verifyNoDevUrls(label, text) { + const matches = devUrlMatches(text) + if (matches.length > 0) { + fail(`${label} contains explicit dev renderer URL pattern(s): ${matches.join(", ")}`) + } + return matches.length === 0 +} + +function readPlist(relativePath) { + try { + return plist.parse(read(relativePath)) + } catch (error) { + fail(`Could not parse ${relativePath}: ${error.message}`) + return undefined + } +} + +function summarizeCodesign(appDir) { + const verify = run("codesign", ["--verify", "--deep", "--strict", "--verbose=2", appDir]) + const details = run("codesign", ["-dv", "--verbose=4", appDir]) + const detailText = [details.stdout, details.stderr].filter(Boolean).join("\n") + return { + verify, + details: { + exitCode: details.exitCode, + signature: detailText.match(/^Signature=(.+)$/m)?.[1] ?? null, + identifier: detailText.match(/^Identifier=(.+)$/m)?.[1] ?? null, + teamIdentifier: detailText.match(/^TeamIdentifier=(.+)$/m)?.[1] ?? null, + rawStatus: detailText.includes("Signature=adhoc") ? "adhoc" : (details.exitCode === 0 ? "signed" : "unknown"), + }, + status: verify.exitCode === 0 && !detailText.includes("Signature=adhoc") + ? "signed" + : "unsigned-or-adhoc", + } +} + +function verifyInfoPlist(app) { + const infoPath = `${app.appDir}/Contents/Info.plist` + const info = readPlist(infoPath) + if (!info) return { path: infoPath, status: "failed" } + + if (info.CFBundleName !== productName) { + fail(`${infoPath} CFBundleName is ${info.CFBundleName ?? ""}, expected ${productName}.`) + } + if (info.CFBundleDisplayName !== productName) { + fail(`${infoPath} CFBundleDisplayName is ${info.CFBundleDisplayName ?? ""}, expected ${productName}.`) + } + if (info.CFBundleExecutable !== productName) { + fail(`${infoPath} CFBundleExecutable is ${info.CFBundleExecutable ?? ""}, expected ${productName}.`) + } + if (info.CFBundleIdentifier !== appId) { + fail(`${infoPath} CFBundleIdentifier is ${info.CFBundleIdentifier ?? ""}, expected ${appId}.`) + } + if (info.CFBundleShortVersionString !== version) { + fail(`${infoPath} CFBundleShortVersionString is ${info.CFBundleShortVersionString ?? ""}, expected ${version}.`) + } + if (info.CFBundleVersion !== version) { + fail(`${infoPath} CFBundleVersion is ${info.CFBundleVersion ?? ""}, expected ${version}.`) + } + + const schemes = (info.CFBundleURLTypes ?? []).flatMap((entry) => entry.CFBundleURLSchemes ?? []) + if (!schemes.includes("twentyfirst-agents")) { + fail(`${infoPath} is missing twentyfirst-agents URL scheme.`) + } + + const env = info.LSEnvironment ?? {} + const envText = JSON.stringify(env) + if (Object.hasOwn(env, "ELECTRON_RENDERER_URL")) { + fail(`${infoPath} LSEnvironment must not set ELECTRON_RENDERER_URL in packaged apps.`) + } + if (Object.hasOwn(env, "MAIN_VITE_API_URL")) { + fail(`${infoPath} LSEnvironment must not set MAIN_VITE_API_URL in packaged apps.`) + } + verifyNoDevUrls(`${infoPath} LSEnvironment`, envText) + + const atsDomains = Object.keys(info.NSAppTransportSecurity?.NSExceptionDomains ?? {}) + const localNetworkingException = atsDomains.includes("localhost") || atsDomains.includes("127.0.0.1") + if (localNetworkingException) { + warn(`${infoPath} keeps localhost ATS exceptions for local networking; this is recorded but not treated as a renderer URL leak.`) + } + + return { + path: infoPath, + status: "passed", + bundleName: info.CFBundleName, + bundleIdentifier: info.CFBundleIdentifier, + version: info.CFBundleShortVersionString, + schemes, + localNetworkingException, + electronAsarIntegrity: info.ElectronAsarIntegrity?.["Resources/app.asar"] ?? null, + environmentKeys: Object.keys(env), + } +} + +function verifyResources(app) { + const resourceDir = `${app.appDir}/Contents/Resources` + const executablePath = `${app.appDir}/Contents/MacOS/${productName}` + const asarPath = `${resourceDir}/app.asar` + const appUpdatePath = `${resourceDir}/app-update.yml` + const requiredFiles = [ + executablePath, + asarPath, + appUpdatePath, + `${resourceDir}/icon.icns`, + `${resourceDir}/migrations/meta/_journal.json`, + ] + const requiredBinaries = [ + `${resourceDir}/bin/claude`, + `${resourceDir}/bin/codex`, + `${resourceDir}/bin/VERSION`, + ] + + for (const file of requiredFiles) { + if (!exists(file)) fail(`${app.arch} packaged app is missing ${file}.`) + } + for (const file of requiredBinaries) { + if (!exists(file)) { + fail(`${app.arch} packaged app is missing bundled runtime ${file}.`) + } else if (path.basename(file) !== "VERSION" && !isExecutable(file)) { + fail(`${app.arch} bundled runtime ${file} is not executable.`) + } + } + + const appUpdate = exists(appUpdatePath) ? readYaml(appUpdatePath) : undefined + if (appUpdate) { + if (appUpdate.provider !== "generic") { + fail(`${appUpdatePath} provider is ${appUpdate.provider ?? ""}, expected generic.`) + } + if (appUpdate.url !== publishUrl) { + fail(`${appUpdatePath} url is ${appUpdate.url ?? ""}, expected ${publishUrl}.`) + } + verifyNoDevUrls(appUpdatePath, read(appUpdatePath)) + } + + const asarStat = statFile(asarPath) + if (asarStat && asarStat.size < 10_000_000) { + fail(`${asarPath} is unexpectedly small (${asarStat.size} bytes).`) + } + + return { + resourceDir, + executable: { + path: executablePath, + present: exists(executablePath), + executable: isExecutable(executablePath), + }, + asar: { + path: asarPath, + present: exists(asarPath), + size: asarStat?.size ?? 0, + sha256: exists(asarPath) ? sha256(asarPath) : null, + }, + appUpdate: appUpdate + ? { + path: appUpdatePath, + status: "passed", + provider: appUpdate.provider, + url: appUpdate.url, + noDevRendererUrl: verifyNoDevUrls(appUpdatePath, read(appUpdatePath)), + } + : null, + bundledBinaries: requiredBinaries.map((file) => ({ + path: file, + present: exists(file), + executable: path.basename(file) === "VERSION" ? null : isExecutable(file), + size: statFile(file)?.size ?? 0, + })), + } +} + +function verifyAsarRuntime(app) { + const asarPath = `${app.appDir}/Contents/Resources/app.asar` + if (!exists(asarPath)) return { status: "failed", path: asarPath } + + let files = [] + try { + files = asar.listPackage(projectPath(asarPath)) + } catch (error) { + fail(`Could not list ${asarPath}: ${error.message}`) + return { status: "failed", path: asarPath } + } + + for (const file of ["out/main/index.js", "out/preload/index.js"]) { + if (!files.includes(`/${file}`)) { + fail(`${asarPath} is missing ${file}.`) + } + } + + let mainEntry = "" + try { + mainEntry = asar.extractFile(projectPath(asarPath), "out/main/index.js").toString("utf8") + } catch (error) { + fail(`Could not extract out/main/index.js from ${asarPath}: ${error.message}`) + } + + const packagedApiGuard = /app\.isPackaged\)\s*{\s*return "https:\/\/21st\.dev";\s*}/.test(mainEntry) + if (!packagedApiGuard) { + fail(`${asarPath} out/main/index.js must keep the packaged app API URL guard returning https://21st.dev.`) + } + const noMainDevRendererUrl = verifyNoDevUrls(`${asarPath} out/main/index.js`, mainEntry) + if (mainEntry.includes("MAIN_VITE_API_URL")) { + fail(`${asarPath} out/main/index.js should not carry MAIN_VITE_API_URL into the production main bundle.`) + } + + return { + status: "passed", + path: asarPath, + fileCount: files.length, + containsMainEntry: files.includes("/out/main/index.js"), + containsPreloadEntry: files.includes("/out/preload/index.js"), + packagedApiGuard, + noMainDevRendererUrl, + electronRendererUrlGuardPresent: mainEntry.includes("ELECTRON_RENDERER_URL"), + } +} + +function verifyDistribution(app) { + const manifest = exists(app.updateManifest) ? readYaml(app.updateManifest) : undefined + if (!manifest) { + fail(`${app.updateManifest} is missing or invalid.`) + } else { + if (manifest.version !== version) { + fail(`${app.updateManifest} version is ${manifest.version ?? ""}, expected ${version}.`) + } + if (manifest.path !== path.basename(app.zipArtifact)) { + fail(`${app.updateManifest} path is ${manifest.path ?? ""}, expected ${path.basename(app.zipArtifact)}.`) + } + verifyNoDevUrls(app.updateManifest, read(app.updateManifest)) + } + + for (const artifact of [app.dmgArtifact, app.zipArtifact]) { + const stat = statFile(artifact) + if (!stat) { + fail(`${app.arch} release artifact is missing: ${artifact}`) + } else if (stat.size < 10_000_000) { + fail(`${app.arch} release artifact ${artifact} is unexpectedly small (${stat.size} bytes).`) + } + } + + return { + updateManifest: manifest + ? { + path: app.updateManifest, + status: "passed", + version: manifest.version, + artifactPath: manifest.path, + artifactSize: manifest.files?.[0]?.size ?? null, + noDevRendererUrl: verifyNoDevUrls(app.updateManifest, read(app.updateManifest)), + } + : null, + artifacts: [app.dmgArtifact, app.zipArtifact].map((artifact) => ({ + path: artifact, + present: exists(artifact), + size: statFile(artifact)?.size ?? 0, + })), + } +} + +function verifyApp(app) { + if (!exists(app.appDir)) { + fail(`${app.arch} packaged app is missing: ${app.appDir}`) + return { + arch: app.arch, + appDir: app.appDir, + status: "failed", + } + } + + const info = verifyInfoPlist(app) + const resources = verifyResources(app) + const runtime = verifyAsarRuntime(app) + const distribution = verifyDistribution(app) + const signing = summarizeCodesign(projectPath(app.appDir)) + if (signing.status === "unsigned-or-adhoc") { + warn(`${app.arch} packaged app is not developer-id signed yet; signing/notarization remains a separate release blocker.`) + } + + return { + arch: app.arch, + appDir: app.appDir, + status: "passed", + info, + resources, + runtime, + distribution, + signing, + } +} + +async function verifyLaunch(app) { + const executablePath = `${app.appDir}/Contents/MacOS/${productName}` + if (!exists(executablePath)) { + fail(`Cannot launch packaged app because executable is missing: ${executablePath}`) + return { + status: "failed", + arch: app.arch, + executablePath, + } + } + + const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "moss-packaged-launch-")) + const port = await findFreePort() + const child = spawnSync(process.execPath, [ + "--input-type=module", + "-e", + ` + import { spawn } from "node:child_process"; + const child = spawn(${JSON.stringify(projectPath(executablePath))}, [ + ${JSON.stringify(`--remote-debugging-port=${port}`)}, + ${JSON.stringify(`--user-data-dir=${userDataDir}`)} + ], { + cwd: ${JSON.stringify(root)}, + env: { ...process.env, ELECTRON_ENABLE_LOGGING: "1" }, + stdio: ["ignore", "pipe", "pipe"] + }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk) => { stdout += chunk.toString(); }); + child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); + let endpoint = null; + let earlyExit = null; + const started = Date.now(); + while (Date.now() - started < 15000) { + if (child.exitCode !== null) { + earlyExit = child.exitCode; + break; + } + try { + const response = await fetch(${JSON.stringify(`http://127.0.0.1:${port}/json/version`)}); + if (response.ok) { + endpoint = await response.json(); + break; + } + } catch {} + await new Promise((resolve) => setTimeout(resolve, 500)); + } + child.kill("SIGTERM"); + await new Promise((resolve) => setTimeout(resolve, 1500)); + if (child.exitCode === null) child.kill("SIGKILL"); + console.log(JSON.stringify({ + endpoint, + earlyExit, + stdout: stdout.slice(0, 2000), + stderr: stderr.slice(0, 2000) + })); + process.exit(endpoint ? 0 : 1); + `, + ], { + cwd: root, + encoding: "utf8", + }) + + fs.rmSync(userDataDir, { recursive: true, force: true }) + + let launchOutput = {} + try { + launchOutput = JSON.parse(child.stdout.trim() || "{}") + } catch { + launchOutput = { + parseError: true, + stdout: child.stdout.trim().slice(0, 2000), + stderr: child.stderr.trim().slice(0, 2000), + } + } + + const endpoint = launchOutput.endpoint + if (child.status !== 0 || !endpoint?.webSocketDebuggerUrl) { + fail(`${app.arch} packaged app launch smoke failed to reach Electron remote debugging endpoint.`) + } + + const combinedOutput = `${launchOutput.stdout ?? ""}\n${launchOutput.stderr ?? ""}` + verifyNoDevUrls(`${app.arch} packaged app launch output`, combinedOutput) + + return { + status: child.status === 0 && endpoint?.webSocketDebuggerUrl ? "passed" : "failed", + arch: app.arch, + executablePath, + remoteDebuggingEndpoint: `http://127.0.0.1:${port}/json/version`, + browser: endpoint?.Browser ?? null, + webSocketDebuggerUrlPresent: Boolean(endpoint?.webSocketDebuggerUrl), + earlyExit: launchOutput.earlyExit ?? null, + stdoutSample: launchOutput.stdout ?? "", + stderrSample: launchOutput.stderr ?? "", + exitCode: child.status, + } +} + +function writeReport(report) { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-") + const reportDir = projectPath(path.join(".1code/program/packaged-app-smoke", timestamp)) + fs.mkdirSync(reportDir, { recursive: true }) + + const reportPath = path.join(reportDir, "report.json") + fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`) + + const latestPath = projectPath(".1code/program/packaged-app-smoke/latest.json") + fs.writeFileSync(latestPath, `${JSON.stringify({ + report: normalizeRelative(reportPath), + generatedAt: report.generatedAt, + status: report.status, + }, null, 2)}\n`) + + return reportPath +} + +if (!version) { + fail("package.json version is missing.") +} + +const apps = [ + { + arch: "arm64", + appDir: "release/mac-arm64/1Code.app", + updateManifest: "release/latest-mac.yml", + dmgArtifact: `release/1Code-${version}-arm64.dmg`, + zipArtifact: `release/1Code-${version}-arm64-mac.zip`, + }, + { + arch: "x64", + appDir: "release/mac/1Code.app", + updateManifest: "release/latest-mac-x64.yml", + dmgArtifact: `release/1Code-${version}.dmg`, + zipArtifact: `release/1Code-${version}-mac.zip`, + }, +] + +const appReports = apps.map(verifyApp) +const launchArch = process.arch === "arm64" ? "arm64" : "x64" +const launchApp = apps.find((app) => app.arch === launchArch) ?? apps[0] +const launch = await verifyLaunch(launchApp) +const report = { + status: failures.length === 0 ? "passed" : "failed", + generatedAt: new Date().toISOString(), + package: { + name: packageJson?.name, + version, + productName, + appId, + publishUrl, + }, + productionConfig: { + packagedApiUrl: "https://21st.dev", + devRendererUrlPolicy: "Info.plist, app-update.yml, update manifests, and app-owned main bundle must not point at localhost dev renderer URLs.", + allowedLocalNetworkingException: "localhost ATS exceptions are allowed for local MCP/runtime networking and are recorded as warnings.", + }, + apps: appReports, + launch, + warnings, + failures, +} + +const reportPath = writeReport(report) + +console.log("Moss packaged app smoke") +console.log(`status: ${report.status}`) +console.log(`report: ${normalizeRelative(reportPath)}`) +for (const app of appReports) { + console.log(`${app.arch}: app=${app.appDir} signing=${app.signing?.status ?? "missing"} asarFiles=${app.runtime?.fileCount ?? 0}`) +} +console.log(`launch: ${launch.arch} ${launch.status} ${launch.browser ?? "unknown-browser"}`) + +for (const message of warnings) { + console.warn(`warning: ${message}`) +} + +if (failures.length > 0) { + for (const message of failures) { + console.error(`error: ${message}`) + } + process.exit(1) +} diff --git a/scripts/upload-release.mjs b/scripts/upload-release.mjs new file mode 100644 index 000000000..e2e14b857 --- /dev/null +++ b/scripts/upload-release.mjs @@ -0,0 +1,286 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto" +import { spawnSync } from "node:child_process" +import fs from "node:fs" +import path from "node:path" +import process from "node:process" + +const root = process.cwd() +const args = process.argv.slice(2) +const dryRun = args.includes("--dry-run") +const includeEvidence = args.includes("--include-evidence") +const allowUnsigned = args.includes("--allow-unsigned") + +function argValue(name, fallback) { + const index = args.indexOf(name) + if (index >= 0 && args[index + 1]) return args[index + 1] + const inline = args.find((arg) => arg.startsWith(`${name}=`)) + if (inline) return inline.slice(name.length + 1) + return fallback +} + +const channel = argValue("--channel", process.env.RELEASE_CHANNEL ?? "latest") +if (!["latest", "beta"].includes(channel)) { + console.error(`Invalid release channel: ${channel}`) + process.exit(1) +} + +const provider = argValue( + "--provider", + process.env.RELEASE_UPLOAD_PROVIDER ?? "command", +) + +function projectPath(relativePath) { + return path.join(root, relativePath) +} + +function readJson(relativePath) { + return JSON.parse(fs.readFileSync(projectPath(relativePath), "utf8")) +} + +function normalizeRelative(filePath) { + return path.relative(root, filePath).split(path.sep).join("/") +} + +function sha512(filePath) { + return createHash("sha512").update(fs.readFileSync(filePath)).digest("base64") +} + +function contentTypeFor(filePath) { + const ext = path.extname(filePath).toLowerCase() + if (ext === ".yml" || ext === ".yaml") return "text/yaml" + if (ext === ".zip") return "application/zip" + if (ext === ".dmg") return "application/x-apple-diskimage" + if (ext === ".json") return "application/json" + if (ext === ".txt") return "text/plain" + return "application/octet-stream" +} + +function shellQuote(value) { + return `'${String(value).replace(/'/g, `'\\''`)}'` +} + +function replaceTemplate(template, item) { + return template + .replaceAll("{file}", shellQuote(item.absolutePath)) + .replaceAll("{path}", shellQuote(item.absolutePath)) + .replaceAll("{key}", shellQuote(item.key)) + .replaceAll("{url}", shellQuote(item.url)) + .replaceAll("{contentType}", shellQuote(item.contentType)) +} + +function releaseBaseUrl(packageJson) { + const url = packageJson?.build?.publish?.url + if (typeof url !== "string" || !url.startsWith("https://")) { + throw new Error("package.json build.publish.url must be an HTTPS CDN URL.") + } + return url.replace(/\/+$/, "") +} + +function defaultUploadPrefix(baseUrl) { + const parsed = new URL(baseUrl) + return parsed.pathname.replace(/^\/+|\/+$/g, "") +} + +function requiredReleaseFiles(packageJson) { + const version = process.env.VERSION || packageJson.version + const prefix = channel === "beta" ? "beta" : "latest" + const names = [ + `${prefix}-mac.yml`, + `${prefix}-mac-x64.yml`, + `1Code-${version}-arm64-mac.zip`, + `1Code-${version}-mac.zip`, + `1Code-${version}-arm64.dmg`, + `1Code-${version}.dmg`, + ] + + if (includeEvidence) { + const releaseDir = projectPath("release") + if (fs.existsSync(releaseDir)) { + names.push( + ...fs.readdirSync(releaseDir) + .filter((name) => /notary|notar|codesign|staple|spctl/i.test(name)) + .sort(), + ) + } + } + + return [...new Set(names)].map((name) => path.join("release", name)) +} + +function validNotarizationReport() { + const releaseDir = projectPath("release") + if (!fs.existsSync(releaseDir)) return null + + for (const name of fs.readdirSync(releaseDir).sort()) { + if (!/^notarization-.+\.json$/i.test(name)) continue + const filePath = path.join(releaseDir, name) + try { + const report = JSON.parse(fs.readFileSync(filePath, "utf8")) + if ( + report.status === "passed" && + report.mode?.dryRun === false && + Number(report.summary?.notarytoolSubmissions ?? 0) > 0 && + Number(report.summary?.stapleCommands ?? 0) > 0 && + Number(report.summary?.codesignVerifications ?? 0) > 0 + ) { + return normalizeRelative(filePath) + } + } catch { + // Ignore malformed evidence; verify-release-packaging reports it separately. + } + } + + return null +} + +function buildUploadItems(packageJson) { + const baseUrl = releaseBaseUrl(packageJson) + const uploadPrefix = + process.env.RELEASE_UPLOAD_PREFIX ?? defaultUploadPrefix(baseUrl) + const missing = [] + const items = [] + + for (const relativePath of requiredReleaseFiles(packageJson)) { + const absolutePath = projectPath(relativePath) + if (!fs.existsSync(absolutePath)) { + missing.push(relativePath) + continue + } + + const name = path.basename(relativePath) + const key = [uploadPrefix, name].filter(Boolean).join("/") + items.push({ + name, + relativePath, + absolutePath, + key, + url: `${baseUrl}/${encodeURIComponent(name)}`, + size: fs.statSync(absolutePath).size, + sha512: sha512(absolutePath), + contentType: contentTypeFor(relativePath), + }) + } + + return { baseUrl, uploadPrefix, items, missing } +} + +function runUpload(item) { + if (provider === "command") { + const template = process.env.RELEASE_UPLOAD_COMMAND_TEMPLATE + if (!template) { + throw new Error( + "RELEASE_UPLOAD_COMMAND_TEMPLATE is required for command uploads. Use --dry-run to generate an upload plan only.", + ) + } + const command = replaceTemplate(template, item) + return spawnSync("sh", ["-c", command], { + cwd: root, + stdio: "inherit", + env: process.env, + }) + } + + if (provider === "wrangler") { + const bucket = process.env.CLOUDFLARE_R2_BUCKET ?? process.env.R2_BUCKET + if (!bucket) { + throw new Error("CLOUDFLARE_R2_BUCKET or R2_BUCKET is required for wrangler uploads.") + } + return spawnSync( + "npx", + ["wrangler", "r2", "object", "put", `${bucket}/${item.key}`, "--file", item.absolutePath], + { + cwd: root, + stdio: "inherit", + env: process.env, + }, + ) + } + + throw new Error(`Unsupported release upload provider: ${provider}`) +} + +function writePlan(plan) { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-") + const dir = projectPath(path.join(".1code/program/release-upload", timestamp)) + fs.mkdirSync(dir, { recursive: true }) + const manifestPath = path.join(dir, "manifest.json") + fs.writeFileSync(manifestPath, `${JSON.stringify(plan, null, 2)}\n`) + fs.writeFileSync( + projectPath(".1code/program/release-upload/latest.json"), + `${JSON.stringify({ + manifest: normalizeRelative(manifestPath), + generatedAt: plan.generatedAt, + status: plan.status, + dryRun: plan.mode.dryRun, + }, null, 2)}\n`, + ) + return manifestPath +} + +const failures = [] +const uploaded = [] +const packageJson = readJson("package.json") +const { baseUrl, uploadPrefix, items, missing } = buildUploadItems(packageJson) +const notarizationReport = validNotarizationReport() + +if (missing.length > 0) { + failures.push(`Missing release upload artifact(s): ${missing.join(", ")}`) +} +if (!dryRun && !allowUnsigned && !notarizationReport) { + failures.push("No passing notarization report found; refusing real upload without --allow-unsigned.") +} + +if (failures.length === 0 && !dryRun) { + for (const item of items) { + try { + const result = runUpload(item) + if (result.status !== 0) { + failures.push(`Upload failed for ${item.relativePath} with exit code ${result.status ?? ""}.`) + } else { + uploaded.push(item.relativePath) + } + } catch (error) { + failures.push(error.message) + break + } + } +} + +const plan = { + status: failures.length === 0 ? "passed" : "failed", + generatedAt: new Date().toISOString(), + mode: { + dryRun, + provider, + channel, + includeEvidence, + allowUnsigned, + }, + target: { + baseUrl, + uploadPrefix, + notarizationReport, + }, + artifacts: items.map(({ absolutePath: _absolutePath, ...item }) => item), + uploaded, + missing, + failures, +} +const manifestPath = writePlan(plan) + +console.log("Moss release upload plan") +console.log(`status: ${plan.status}`) +console.log(`mode: ${dryRun ? "dry-run" : provider}`) +console.log(`manifest: ${normalizeRelative(manifestPath)}`) +console.log(`artifacts: ${items.length}`) +console.log(`target: ${baseUrl}`) + +for (const failure of failures) { + console.error(`error: ${failure}`) +} + +if (failures.length > 0) { + process.exit(1) +} diff --git a/scripts/verify-release-credentials.mjs b/scripts/verify-release-credentials.mjs new file mode 100644 index 000000000..753716654 --- /dev/null +++ b/scripts/verify-release-credentials.mjs @@ -0,0 +1,270 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process" +import fs from "node:fs" +import path from "node:path" + +const root = process.cwd() +const requireCredentials = process.argv.includes("--require-credentials") +const generatedAt = new Date().toISOString() +const stamp = generatedAt.replace(/[:.]/g, "-") +const reportDir = path.join(root, ".1code/program/release-credentials", stamp) +const reportPath = path.join(reportDir, "report.json") +const latestPath = path.join(root, ".1code/program/release-credentials/latest.json") +const failures = [] +const warnings = [] +const blockers = [] + +const requiredCredentials = [ + { + env: "APPLE_IDENTITY", + role: "electron-builder Developer ID Application signing identity", + }, + { + env: "CSC_LINK", + role: "Developer ID certificate archive or encoded certificate", + }, + { + env: "CSC_KEY_PASSWORD", + role: "Developer ID certificate password", + }, + { + env: "APPLE_ID", + role: "Apple ID for notarytool", + }, + { + env: "APPLE_TEAM_ID", + role: "Apple Developer Team ID for notarytool", + }, + { + env: "APPLE_APP_SPECIFIC_PASSWORD", + role: "Apple app-specific password for notarytool", + }, +] + +const requiredWorkflowSecrets = requiredCredentials.map((credential) => `secrets.${credential.env}`) + +function projectPath(relativePath) { + return path.join(root, relativePath) +} + +function exists(relativePath) { + return fs.existsSync(projectPath(relativePath)) +} + +function read(relativePath) { + return fs.readFileSync(projectPath(relativePath), "utf8") +} + +function readJson(relativePath) { + try { + return JSON.parse(read(relativePath)) + } catch (error) { + failures.push(`Could not parse ${relativePath}: ${error.message}`) + return undefined + } +} + +function normalizeRelative(filePath) { + return path.relative(root, filePath).split(path.sep).join("/") +} + +function credentialValueKind(name) { + const value = process.env[name] + if (!value) return "missing" + if (name === "CSC_LINK") { + if (fs.existsSync(value)) return "file" + if (/^[A-Za-z0-9+/=\r\n]+$/.test(value) && value.length > 80) return "encoded" + } + return "env" +} + +function runTool(id, command, args) { + const result = spawnSync(command, args, { + cwd: root, + encoding: "utf8", + timeout: 10_000, + maxBuffer: 1024 * 1024, + }) + + return { + id, + command, + args, + status: result.status === 0 ? "passed" : "failed", + exitCode: result.status ?? 1, + stdoutPreview: (result.stdout ?? "").trim().slice(0, 500), + stderrPreview: (result.stderr ?? "").trim().slice(0, 500), + } +} + +function listReleaseFiles(pattern) { + const releaseDir = projectPath("release") + if (!fs.existsSync(releaseDir)) return [] + return fs.readdirSync(releaseDir) + .map((name) => path.join(releaseDir, name)) + .filter((filePath) => fs.statSync(filePath).isFile()) + .filter((filePath) => pattern.test(path.basename(filePath))) + .map(normalizeRelative) + .sort() +} + +const packageJson = readJson("package.json") +const releaseCredentialsScript = packageJson?.scripts?.["release:credentials"] +const releaseCredentialsStrictScript = packageJson?.scripts?.["release:credentials:strict"] +const releaseNotarizeScript = packageJson?.scripts?.["release:notarize"] + +if (releaseCredentialsScript !== "node scripts/verify-release-credentials.mjs") { + failures.push("package.json release:credentials must run scripts/verify-release-credentials.mjs.") +} +if (releaseCredentialsStrictScript !== "node scripts/verify-release-credentials.mjs --require-credentials") { + failures.push("package.json release:credentials:strict must run scripts/verify-release-credentials.mjs --require-credentials.") +} +if (releaseNotarizeScript !== "node scripts/notarize-release-artifacts.mjs") { + failures.push("package.json release:notarize must run scripts/notarize-release-artifacts.mjs.") +} +if (!exists("scripts/notarize-release-artifacts.mjs")) { + failures.push("Missing scripts/notarize-release-artifacts.mjs.") +} + +let workflowPresent = false +let workflowRequiredSecrets = [] +let workflowCredentialStep = false +if (exists(".github/workflows/moss-desktop-release.yml")) { + workflowPresent = true + const workflow = read(".github/workflows/moss-desktop-release.yml") + workflowRequiredSecrets = requiredWorkflowSecrets.map((secret) => ({ + secret: secret.replace("secrets.", ""), + present: workflow.includes(secret), + })) + workflowCredentialStep = workflow.includes("bun run release:credentials:strict") + if (!workflowCredentialStep) { + failures.push(".github/workflows/moss-desktop-release.yml must run bun run release:credentials:strict before packaging.") + } + for (const secret of workflowRequiredSecrets) { + if (!secret.present) { + failures.push(`.github/workflows/moss-desktop-release.yml is missing required secret contract: ${secret.secret}`) + } + } +} else { + failures.push("Missing .github/workflows/moss-desktop-release.yml.") +} + +if (exists("electron-builder.yml")) { + const builderOverride = read("electron-builder.yml") + if (!builderOverride.includes("identity: ${env.APPLE_IDENTITY}")) { + failures.push("electron-builder.yml must read signing identity from APPLE_IDENTITY.") + } + if (!builderOverride.includes("notarize: false")) { + failures.push("electron-builder.yml must keep built-in notarization disabled for explicit CI notarization.") + } +} else { + failures.push("Missing electron-builder.yml.") +} + +const credentials = requiredCredentials.map((credential) => ({ + ...credential, + state: process.env[credential.env] ? "set" : "missing", + valueKind: credentialValueKind(credential.env), +})) +const missingCredentials = credentials + .filter((credential) => credential.state !== "set") + .map((credential) => credential.env) + +if (missingCredentials.length > 0) { + blockers.push(`Missing required Apple signing/notarization credentials: ${missingCredentials.join(", ")}.`) +} + +const tools = [ + runTool("xcrun-notarytool", "xcrun", ["--find", "notarytool"]), + runTool("xcrun-stapler", "xcrun", ["--find", "stapler"]), + runTool("codesign", "xcrun", ["--find", "codesign"]), + runTool("spctl", "xcrun", ["--find", "spctl"]), +] +const failedTools = tools + .filter((tool) => tool.status !== "passed") + .map((tool) => tool.id) + +for (const tool of failedTools) { + failures.push(`Required macOS release tool is unavailable: ${tool}.`) +} + +if (requireCredentials && missingCredentials.length > 0) { + failures.push("Strict release credential preflight requires all Apple signing/notarization credentials.") +} + +const status = failures.length > 0 + ? "failed" + : missingCredentials.length > 0 + ? "blocked" + : "passed" + +if (status === "blocked") { + warnings.push(...blockers) +} + +const report = { + status, + generatedAt, + mode: { + requireCredentials, + }, + scripts: { + releaseCredentials: releaseCredentialsScript, + releaseCredentialsStrict: releaseCredentialsStrictScript, + releaseNotarize: releaseNotarizeScript, + }, + workflow: { + path: ".github/workflows/moss-desktop-release.yml", + present: workflowPresent, + credentialPreflightStep: workflowCredentialStep, + requiredSecrets: workflowRequiredSecrets, + }, + signing: { + electronBuilderIdentity: exists("electron-builder.yml") ? "env.APPLE_IDENTITY" : "missing", + notarizationMode: "external-ci", + electronBuilderNotarize: false, + }, + credentials: { + required: credentials, + complete: missingCredentials.length === 0, + missing: missingCredentials, + }, + tools: { + checks: tools, + complete: failedTools.length === 0, + missing: failedTools, + }, + artifacts: { + macArtifacts: listReleaseFiles(/\.(dmg|zip)$/i), + updateManifests: listReleaseFiles(/(?:latest|beta)-mac(?:-x64)?\.yml$/), + }, + warnings, + blockers, + failures, +} + +fs.mkdirSync(reportDir, { recursive: true }) +fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`) +fs.writeFileSync(latestPath, `${JSON.stringify({ + report: normalizeRelative(reportPath), + generatedAt, + status, +}, null, 2)}\n`) + +console.log("Moss release credential preflight") +console.log(`status: ${status}`) +console.log(`report: ${normalizeRelative(reportPath)}`) +console.log(`credentials: ${credentials.length - missingCredentials.length}/${credentials.length}`) +console.log(`tools: ${tools.length - failedTools.length}/${tools.length}`) + +for (const message of warnings) { + console.warn(`warning: ${message}`) +} +for (const message of failures) { + console.error(`error: ${message}`) +} + +if (failures.length > 0) { + process.exit(1) +} diff --git a/scripts/verify-release-packaging.mjs b/scripts/verify-release-packaging.mjs new file mode 100644 index 000000000..8c3335346 --- /dev/null +++ b/scripts/verify-release-packaging.mjs @@ -0,0 +1,681 @@ +#!/usr/bin/env node + +import fs from "node:fs" +import path from "node:path" + +const root = process.cwd() +const requireArtifacts = process.argv.includes("--require-artifacts") +const requireBundledBinaries = process.argv.includes("--require-bundled-binaries") +const requireNotarization = process.argv.includes("--require-notarization") +const requireUploadPlan = process.argv.includes("--require-upload-plan") +const failures = [] +const warnings = [] + +function projectPath(relativePath) { + return path.join(root, relativePath) +} + +function exists(relativePath) { + return fs.existsSync(projectPath(relativePath)) +} + +function fail(message) { + failures.push(message) +} + +function warn(message) { + warnings.push(message) +} + +function read(relativePath) { + return fs.readFileSync(projectPath(relativePath), "utf8") +} + +function readJson(relativePath) { + try { + return JSON.parse(read(relativePath)) + } catch (error) { + fail(`Could not parse ${relativePath}: ${error.message}`) + return undefined + } +} + +function readJsonPath(filePath) { + try { + return JSON.parse(fs.readFileSync(filePath, "utf8")) + } catch (error) { + fail(`Could not parse ${normalizeRelative(filePath)}: ${error.message}`) + return undefined + } +} + +function normalizeRelative(filePath) { + return path.relative(root, filePath).split(path.sep).join("/") +} + +function assert(condition, message) { + if (!condition) fail(message) +} + +function hasScript(packageJson, scriptName) { + return typeof packageJson?.scripts?.[scriptName] === "string" && packageJson.scripts[scriptName].length > 0 +} + +function targetFor(macTargets, targetName) { + return macTargets.find((target) => target?.target === targetName) +} + +function includesAll(values, required) { + return required.every((value) => values.includes(value)) +} + +function listReleaseFiles() { + const releaseDir = projectPath("release") + if (!fs.existsSync(releaseDir)) return [] + return fs.readdirSync(releaseDir) + .map((name) => path.join(releaseDir, name)) + .filter((filePath) => fs.statSync(filePath).isFile()) + .sort() +} + +function workflowText() { + const workflowPath = ".github/workflows/moss-desktop-release.yml" + if (!exists(workflowPath)) return undefined + return read(workflowPath) +} + +function includesEvery(text, values) { + return values.every((value) => text.includes(value)) +} + +function verifyReleaseWorkflow(packageJson) { + const workflowPath = ".github/workflows/moss-desktop-release.yml" + const workflow = workflowText() + + assert(hasScript(packageJson, "release:notarize"), "Missing package script: release:notarize") + if (packageJson?.scripts?.["release:notarize"] !== "node scripts/notarize-release-artifacts.mjs") { + fail("package.json release:notarize must run scripts/notarize-release-artifacts.mjs.") + } + assert(hasScript(packageJson, "release:credentials"), "Missing package script: release:credentials") + if (packageJson?.scripts?.["release:credentials"] !== "node scripts/verify-release-credentials.mjs") { + fail("package.json release:credentials must run scripts/verify-release-credentials.mjs.") + } + assert(hasScript(packageJson, "release:credentials:strict"), "Missing package script: release:credentials:strict") + if (packageJson?.scripts?.["release:credentials:strict"] !== "node scripts/verify-release-credentials.mjs --require-credentials") { + fail("package.json release:credentials:strict must run scripts/verify-release-credentials.mjs --require-credentials.") + } + assert(exists("scripts/verify-release-credentials.mjs"), "Missing scripts/verify-release-credentials.mjs.") + assert(exists("scripts/notarize-release-artifacts.mjs"), "Missing scripts/notarize-release-artifacts.mjs.") + assert(hasScript(packageJson, "release:evidence:audit"), "Missing package script: release:evidence:audit") + if (packageJson?.scripts?.["release:evidence:audit"] !== "node scripts/audit-release-evidence.mjs") { + fail("package.json release:evidence:audit must run scripts/audit-release-evidence.mjs.") + } + assert(exists("scripts/audit-release-evidence.mjs"), "Missing scripts/audit-release-evidence.mjs.") + assert(hasScript(packageJson, "dist:upload"), "Missing package script: dist:upload") + if (packageJson?.scripts?.["dist:upload"] !== "node scripts/upload-release.mjs") { + fail("package.json dist:upload must run scripts/upload-release.mjs.") + } + assert(hasScript(packageJson, "dist:upload:dry-run"), "Missing package script: dist:upload:dry-run") + if (packageJson?.scripts?.["dist:upload:dry-run"] !== "node scripts/upload-release.mjs --dry-run") { + fail("package.json dist:upload:dry-run must run scripts/upload-release.mjs --dry-run.") + } + assert(exists("scripts/upload-release.mjs"), "Missing scripts/upload-release.mjs.") + assert(hasScript(packageJson, "test:packaged-app-smoke"), "Missing package script: test:packaged-app-smoke") + if (packageJson?.scripts?.["test:packaged-app-smoke"] !== "node scripts/smoke-packaged-app.mjs") { + fail("package.json test:packaged-app-smoke must run scripts/smoke-packaged-app.mjs.") + } + assert(exists("scripts/smoke-packaged-app.mjs"), "Missing scripts/smoke-packaged-app.mjs.") + + if (!workflow) { + fail(`Missing ${workflowPath}.`) + return { + workflowPath, + present: false, + requiredCommands: [], + requiredSecrets: [], + } + } + + const requiredCommands = [ + "bun install --frozen-lockfile", + "bun run claude:download:all", + "bun run codex:download:all", + "bun run release:credentials:strict", + "bun run test:runtime", + "bun run ts:check --pretty false", + "bun run build", + "bun run release:credentials:strict", + "bun run package:mac", + "bun run test:packaged-app-smoke", + "bun run release:notarize", + "node scripts/generate-update-manifest.mjs --channel", + "node scripts/upload-release.mjs --dry-run --channel", + "bun run release:evidence:audit --require-notarization", + "node scripts/verify-release-packaging.mjs --require-artifacts --require-bundled-binaries --require-notarization --require-upload-plan", + ] + const requiredSecrets = [ + "secrets.APPLE_IDENTITY", + "secrets.CSC_LINK", + "secrets.CSC_KEY_PASSWORD", + "secrets.APPLE_ID", + "secrets.APPLE_TEAM_ID", + "secrets.APPLE_APP_SPECIFIC_PASSWORD", + ] + + for (const command of requiredCommands) { + if (!workflow.includes(command)) { + fail(`${workflowPath} is missing required release command: ${command}`) + } + } + for (const secret of requiredSecrets) { + if (!workflow.includes(secret)) { + fail(`${workflowPath} is missing required secret contract: ${secret}`) + } + } + if (!workflow.includes("actions/upload-artifact@v4")) { + fail(`${workflowPath} must upload verified release artifacts.`) + } + if (!workflow.includes("release/notarization-*.json")) { + fail(`${workflowPath} must upload notarization evidence JSON files.`) + } + if (!workflow.includes(".1code/program/release-upload/**/manifest.json")) { + fail(`${workflowPath} must upload release upload plan evidence.`) + } + if (!workflow.includes(".1code/program/release-credentials/**/report.json")) { + fail(`${workflowPath} must upload release credential preflight evidence.`) + } + if (!workflow.includes(".1code/program/release-evidence-audit/**/report.json")) { + fail(`${workflowPath} must upload signed release evidence audit reports.`) + } + if (!workflow.includes(".1code/program/packaged-app-smoke/**/report.json")) { + fail(`${workflowPath} must upload packaged app smoke evidence.`) + } + + return { + workflowPath, + present: true, + requiredCommands: requiredCommands.map((command) => ({ + command, + present: workflow.includes(command), + })), + requiredSecrets: requiredSecrets.map((secret) => ({ + secret: secret.replace("secrets.", ""), + present: workflow.includes(secret), + })), + uploadsEvidence: includesEvery(workflow, [ + "actions/upload-artifact@v4", + "release/notarization-*.json", + "release/codesign-*.txt", + "release/staple-*.txt", + "release/spctl-*.txt", + ".1code/program/packaged-app-smoke/**/report.json", + ".1code/program/release-credentials/**/report.json", + ".1code/program/release-evidence-audit/**/report.json", + ".1code/program/release-upload/**/manifest.json", + ]), + } +} + +function verifyPackageReleaseScripts(packageJson) { + const releaseScript = packageJson?.scripts?.release ?? "" + const releaseLocalScript = packageJson?.scripts?.["release:local"] ?? "" + const requiredStrictReleaseCommands = [ + "bun install --frozen-lockfile", + "bun run claude:download:all", + "bun run codex:download:all", + "bun run release:credentials", + "bun run test:runtime", + "bun run ts:check --pretty false", + "bun run build", + "bun run package:mac", + "bun run test:packaged-app-smoke", + "bun run release:notarize", + "bun run dist:manifest", + "bun run dist:upload:dry-run", + "bun run release:evidence:audit --require-notarization", + "node scripts/verify-release-packaging.mjs --require-artifacts --require-bundled-binaries --require-notarization --require-upload-plan", + "bun run dist:upload", + ] + const requiredLocalReleaseCommands = [ + "bun run claude:download:all", + "bun run codex:download:all", + "bun run test:runtime", + "bun run ts:check --pretty false", + "bun run build", + "bun run package:mac", + "bun run test:packaged-app-smoke", + "bun run dist:manifest", + "bun run dist:upload:dry-run", + "bun run release:evidence:audit", + "node scripts/verify-release-packaging.mjs --require-artifacts --require-bundled-binaries --require-upload-plan", + ] + + assert(hasScript(packageJson, "release"), "Missing package script: release") + assert(hasScript(packageJson, "release:ci"), "Missing package script: release:ci") + assert(hasScript(packageJson, "release:local"), "Missing package script: release:local") + if (packageJson?.scripts?.["release:ci"] !== "bun run release") { + fail("package.json release:ci must delegate to the strict release script.") + } + if (packageJson?.scripts?.["release:dev"] !== "bun run release:local") { + fail("package.json release:dev must delegate to release:local.") + } + + for (const command of requiredStrictReleaseCommands) { + if (!releaseScript.includes(command)) { + fail(`package.json release script is missing required command: ${command}`) + } + } + for (const command of requiredLocalReleaseCommands) { + if (!releaseLocalScript.includes(command)) { + fail(`package.json release:local script is missing required command: ${command}`) + } + } + if (releaseScript.includes("upload-release-wrangler.sh") || releaseScript.includes("bun i ")) { + fail("package.json release script must not use the old upload-release-wrangler.sh or non-frozen bun install path.") + } + + return { + release: releaseScript, + releaseCi: packageJson?.scripts?.["release:ci"], + releaseLocal: releaseLocalScript, + releaseDev: packageJson?.scripts?.["release:dev"], + requiredStrictReleaseCommands: requiredStrictReleaseCommands.map((command) => ({ + command, + present: releaseScript.includes(command), + })), + requiredLocalReleaseCommands: requiredLocalReleaseCommands.map((command) => ({ + command, + present: releaseLocalScript.includes(command), + })), + } +} + +function verifiedNotarizationReports(releaseFiles) { + return releaseFiles + .filter((filePath) => /^notarization-.+\.json$/i.test(path.basename(filePath))) + .map((filePath) => ({ + filePath, + report: readJsonPath(filePath), + })) + .filter(({ report }) => { + if (!report) return false + return report.status === "passed" + && report.mode?.dryRun === false + && Number(report.summary?.notarytoolSubmissions ?? 0) > 0 + && Number(report.summary?.stapleCommands ?? 0) > 0 + && Number(report.summary?.codesignVerifications ?? 0) > 0 + }) +} + +function latestUploadPlan() { + const latestPath = projectPath(".1code/program/release-upload/latest.json") + if (!fs.existsSync(latestPath)) return undefined + const latest = readJson(".1code/program/release-upload/latest.json") + if (!latest?.manifest) return undefined + const manifestPath = projectPath(latest.manifest) + if (!fs.existsSync(manifestPath)) return { + latest, + manifestPath, + manifest: undefined, + } + return { + latest, + manifestPath, + manifest: readJsonPath(manifestPath), + } +} + +function latestCredentialPreflight() { + const latestPath = projectPath(".1code/program/release-credentials/latest.json") + if (!fs.existsSync(latestPath)) return undefined + const latest = readJson(".1code/program/release-credentials/latest.json") + if (!latest?.report) return undefined + const reportPath = projectPath(latest.report) + if (!fs.existsSync(reportPath)) return { + latest, + reportPath, + report: undefined, + } + return { + latest, + reportPath, + report: readJsonPath(reportPath), + } +} + +function latestReleaseEvidenceAudit() { + const latestPath = projectPath(".1code/program/release-evidence-audit/latest.json") + if (!fs.existsSync(latestPath)) return undefined + const latest = readJson(".1code/program/release-evidence-audit/latest.json") + if (!latest?.report) return undefined + const reportPath = projectPath(latest.report) + if (!fs.existsSync(reportPath)) return { + latest, + reportPath, + report: undefined, + } + return { + latest, + reportPath, + report: readJsonPath(reportPath), + } +} + +function packagedAppBinaryState() { + const apps = [ + { + arch: "arm64", + appDir: "release/mac-arm64/1Code.app", + }, + { + arch: "x64", + appDir: "release/mac/1Code.app", + }, + ] + const requiredBinaries = ["bin/claude", "bin/codex", "bin/VERSION"] + + return apps.map((app) => { + const resourceDir = path.join(app.appDir, "Contents", "Resources") + const binaries = requiredBinaries.map((binary) => { + const relativePath = path.join(resourceDir, binary).split(path.sep).join("/") + return { + name: binary, + path: relativePath, + present: exists(relativePath), + } + }) + + return { + ...app, + present: exists(app.appDir), + binaries, + complete: binaries.every((binary) => binary.present), + } + }) +} + +function writeReport(report) { + const timestamp = new Date().toISOString().replace(/[:.]/g, "-") + const reportDir = projectPath(path.join(".1code/program/release-packaging", timestamp)) + fs.mkdirSync(reportDir, { recursive: true }) + + const reportPath = path.join(reportDir, "report.json") + fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`) + + const latestPath = projectPath(".1code/program/release-packaging/latest.json") + fs.writeFileSync(latestPath, `${JSON.stringify({ + report: normalizeRelative(reportPath), + generatedAt: report.generatedAt, + status: report.status, + }, null, 2)}\n`) + + return reportPath +} + +const packageJson = readJson("package.json") +const build = packageJson?.build ?? {} +const mac = build.mac ?? {} +const macTargets = Array.isArray(mac.target) ? mac.target : [] +const dmgTarget = targetFor(macTargets, "dmg") +const zipTarget = targetFor(macTargets, "zip") +const asarUnpack = Array.isArray(build.asarUnpack) ? build.asarUnpack : [] + +assert(hasScript(packageJson, "build"), "Missing package script: build") +assert(hasScript(packageJson, "package:mac"), "Missing package script: package:mac") +assert(hasScript(packageJson, "dist:manifest"), "Missing package script: dist:manifest") +assert(hasScript(packageJson, "verify:program"), "Missing package script: verify:program") +const releaseWorkflow = verifyReleaseWorkflow(packageJson) +const releaseScripts = verifyPackageReleaseScripts(packageJson) + +assert(build.asar === true, "Electron build must enable asar packaging.") +assert(asarUnpack.some((entry) => entry.includes("better-sqlite3")), "asarUnpack must include better-sqlite3 native files.") +assert(asarUnpack.some((entry) => entry.includes("node-pty")), "asarUnpack must include node-pty native files.") +assert(asarUnpack.some((entry) => entry.includes("@anthropic-ai/claude-agent-sdk")), "asarUnpack must include Claude Code SDK files.") +assert(asarUnpack.some((entry) => entry.includes("@zed-industries/codex-acp")), "asarUnpack must include Codex ACP files.") + +assert(dmgTarget, "mac target must include dmg.") +assert(zipTarget, "mac target must include zip for auto-update.") +assert(dmgTarget && includesAll(dmgTarget.arch ?? [], ["arm64", "x64"]), "mac dmg target must include arm64 and x64.") +assert(zipTarget && includesAll(zipTarget.arch ?? [], ["arm64", "x64"]), "mac zip target must include arm64 and x64.") +assert(mac.hardenedRuntime === true, "mac.hardenedRuntime must be true.") +assert(mac.entitlements === "build/entitlements.mac.plist", "mac.entitlements must point to build/entitlements.mac.plist.") +assert(mac.entitlementsInherit === "build/entitlements.mac.plist", "mac.entitlementsInherit must point to build/entitlements.mac.plist.") +assert(mac.icon === "build/icon.icns", "mac.icon must point to build/icon.icns.") +assert(build.dmg?.contents?.some((entry) => entry?.type === "link" && entry?.path === "/Applications"), "DMG layout must include an Applications link.") +assert(build.publish?.provider === "generic", "publish.provider must be generic for electron-updater CDN manifests.") +assert(typeof build.publish?.url === "string" && build.publish.url.startsWith("https://"), "publish.url must be an HTTPS URL.") + +if (!exists("electron-builder.yml")) { + fail("Missing electron-builder.yml override.") +} else { + const builderOverride = read("electron-builder.yml") + assert(builderOverride.includes("identity: ${env.APPLE_IDENTITY}"), "electron-builder.yml must read signing identity from APPLE_IDENTITY.") + assert(builderOverride.includes("notarize: false"), "electron-builder.yml must keep built-in notarization disabled for explicit CI notarization.") +} + +if (!exists("build/entitlements.mac.plist")) { + fail("Missing build/entitlements.mac.plist.") +} else { + const entitlements = read("build/entitlements.mac.plist") + for (const key of [ + "com.apple.security.cs.allow-jit", + "com.apple.security.cs.allow-unsigned-executable-memory", + "com.apple.security.cs.disable-library-validation", + "com.apple.security.network.client", + "com.apple.security.network.server", + "com.apple.security.device.audio-input", + ]) { + assert(entitlements.includes(key), `Entitlements file is missing ${key}.`) + } +} + +if (!exists("scripts/generate-update-manifest.mjs")) { + fail("Missing scripts/generate-update-manifest.mjs.") +} + +if (!process.env.APPLE_IDENTITY) { + warn("APPLE_IDENTITY is not set; local package builds will be unsigned or ad-hoc unless electron-builder finds another identity.") +} + +const releaseFiles = listReleaseFiles() +const releaseArtifacts = releaseFiles.filter((filePath) => /\.(dmg|zip)$/i.test(filePath)) +const updateManifests = releaseFiles.filter((filePath) => /(?:latest|beta)-mac(?:-x64)?\.yml$/.test(path.basename(filePath))) +const notarizationEvidence = releaseFiles.filter((filePath) => /notary|notar|codesign|staple|spctl/i.test(path.basename(filePath))) +const validNotarizationReports = verifiedNotarizationReports(releaseFiles) +const packagedAppBinaries = packagedAppBinaryState() +const uploadPlan = latestUploadPlan() +const credentialPreflight = latestCredentialPreflight() +const releaseEvidenceAudit = latestReleaseEvidenceAudit() + +if (releaseArtifacts.length === 0) { + const message = "No macOS release artifacts were found in release/." + if (requireArtifacts) fail(message) + else warn(`${message} Run bun run package:mac to produce DMG/ZIP evidence.`) +} + +if (requireArtifacts && updateManifests.length === 0) { + fail("No macOS update manifests were found in release/. Run bun run dist:manifest after packaging.") +} + +const missingBundledBinaries = packagedAppBinaries.flatMap((app) => + app.binaries + .filter((binary) => !binary.present) + .map((binary) => `${app.arch}:${binary.name}`), +) +if (missingBundledBinaries.length > 0) { + const message = `Packaged app is missing bundled runtime binaries: ${missingBundledBinaries.join(", ")}.` + if (requireBundledBinaries) fail(message) + else warn(message) +} + +if (requireNotarization && validNotarizationReports.length === 0) { + fail("No passing notarization report was found in release/. Run bun run release:notarize in CI with Apple signing credentials.") +} + +if (uploadPlan?.manifest?.status && uploadPlan.manifest.status !== "passed") { + fail(`Release upload plan ${normalizeRelative(uploadPlan.manifestPath)} has status ${uploadPlan.manifest.status}, expected passed.`) +} +if (requireUploadPlan) { + if (!uploadPlan?.manifest) { + fail("No release upload plan was found. Run bun run dist:upload:dry-run after generating update manifests.") + } else if (uploadPlan.manifest.mode?.dryRun !== true) { + fail(`Release upload plan ${normalizeRelative(uploadPlan.manifestPath)} must be a dry-run pre-upload plan.`) + } else if (!Array.isArray(uploadPlan.manifest.artifacts) || uploadPlan.manifest.artifacts.length < 6) { + fail(`Release upload plan ${normalizeRelative(uploadPlan.manifestPath)} does not include all required macOS artifacts and manifests.`) + } +} + +if (credentialPreflight?.report?.status === "failed") { + fail(`Release credential preflight ${normalizeRelative(credentialPreflight.reportPath)} failed.`) +} +if (requireNotarization) { + if (!credentialPreflight?.report) { + fail("No release credential preflight report was found. Run bun run release:credentials:strict before packaging.") + } else if (credentialPreflight.report.status !== "passed") { + fail(`Release credential preflight ${normalizeRelative(credentialPreflight.reportPath)} status is ${credentialPreflight.report.status}, expected passed for notarized release verification.`) + } +} + +if (releaseEvidenceAudit?.report?.status === "failed") { + fail(`Release evidence audit ${normalizeRelative(releaseEvidenceAudit.reportPath)} failed.`) +} +if (requireNotarization) { + if (!releaseEvidenceAudit?.report) { + fail("No release evidence audit report was found. Run bun run release:evidence:audit --require-notarization after notarization and upload-plan generation.") + } else if (releaseEvidenceAudit.report.status !== "passed") { + fail(`Release evidence audit ${normalizeRelative(releaseEvidenceAudit.reportPath)} status is ${releaseEvidenceAudit.report.status}, expected passed for notarized release verification.`) + } else if (releaseEvidenceAudit.report.distribution?.distributable !== true) { + fail(`Release evidence audit ${normalizeRelative(releaseEvidenceAudit.reportPath)} did not mark the artifacts distributable.`) + } +} else if (!releaseEvidenceAudit?.report) { + warn("No release evidence audit report was found. Run bun run release:evidence:audit to record the current signed/notarized distribution state.") +} + +const report = { + status: failures.length === 0 ? "passed" : "failed", + generatedAt: new Date().toISOString(), + mode: { + requireArtifacts, + requireBundledBinaries, + requireNotarization, + requireUploadPlan, + }, + scripts: { + build: packageJson?.scripts?.build, + packageMac: packageJson?.scripts?.["package:mac"], + distManifest: packageJson?.scripts?.["dist:manifest"], + distUpload: packageJson?.scripts?.["dist:upload"], + distUploadDryRun: packageJson?.scripts?.["dist:upload:dry-run"], + packagedAppSmoke: packageJson?.scripts?.["test:packaged-app-smoke"], + releaseCredentials: packageJson?.scripts?.["release:credentials"], + releaseCredentialsStrict: packageJson?.scripts?.["release:credentials:strict"], + releaseNotarize: packageJson?.scripts?.["release:notarize"], + releaseEvidenceAudit: packageJson?.scripts?.["release:evidence:audit"], + release: releaseScripts.release, + releaseCi: releaseScripts.releaseCi, + releaseLocal: releaseScripts.releaseLocal, + releaseDev: releaseScripts.releaseDev, + releaseScriptChecks: { + requiredStrictReleaseCommands: releaseScripts.requiredStrictReleaseCommands, + requiredLocalReleaseCommands: releaseScripts.requiredLocalReleaseCommands, + }, + }, + mac: { + targets: macTargets, + hardenedRuntime: mac.hardenedRuntime === true, + entitlements: mac.entitlements, + entitlementsInherit: mac.entitlementsInherit, + icon: mac.icon, + publish: build.publish, + }, + signing: { + appleIdentityEnv: process.env.APPLE_IDENTITY ? "set" : "missing", + electronBuilderIdentity: exists("electron-builder.yml") ? "env.APPLE_IDENTITY" : "missing", + notarizationMode: "external-ci", + electronBuilderNotarize: false, + releaseWorkflow, + credentialPreflight: credentialPreflight?.report + ? { + report: normalizeRelative(credentialPreflight.reportPath), + status: credentialPreflight.report.status, + requireCredentials: credentialPreflight.report.mode?.requireCredentials === true, + credentialsComplete: credentialPreflight.report.credentials?.complete === true, + missingCredentials: Array.isArray(credentialPreflight.report.credentials?.missing) + ? credentialPreflight.report.credentials.missing + : [], + toolsComplete: credentialPreflight.report.tools?.complete === true, + missingTools: Array.isArray(credentialPreflight.report.tools?.missing) + ? credentialPreflight.report.tools.missing + : [], + workflowCredentialPreflightStep: credentialPreflight.report.workflow?.credentialPreflightStep === true, + blockers: Array.isArray(credentialPreflight.report.blockers) + ? credentialPreflight.report.blockers + : [], + } + : null, + evidenceAudit: releaseEvidenceAudit?.report + ? { + report: normalizeRelative(releaseEvidenceAudit.reportPath), + status: releaseEvidenceAudit.report.status, + requireNotarization: releaseEvidenceAudit.report.mode?.requireNotarization === true, + distributable: releaseEvidenceAudit.report.distribution?.distributable === true, + blockerCount: Number(releaseEvidenceAudit.report.distribution?.blockerCount ?? 0), + validNotarizationReports: Array.isArray(releaseEvidenceAudit.report.notarization?.validReports) + ? releaseEvidenceAudit.report.notarization.validReports + : [], + acceptedSubmissions: Number(releaseEvidenceAudit.report.notarization?.acceptedSubmissions ?? 0), + } + : null, + validNotarizationReports: validNotarizationReports.map(({ filePath }) => normalizeRelative(filePath)), + }, + artifacts: { + releaseDir: "release", + files: releaseFiles.map(normalizeRelative), + macArtifacts: releaseArtifacts.map(normalizeRelative), + updateManifests: updateManifests.map(normalizeRelative), + notarizationEvidence: notarizationEvidence.map(normalizeRelative), + packagedAppBinaries, + }, + distribution: { + uploadScript: exists("scripts/upload-release.mjs") ? "scripts/upload-release.mjs" : "missing", + uploadPlan: uploadPlan?.manifest + ? { + manifest: normalizeRelative(uploadPlan.manifestPath), + status: uploadPlan.manifest.status, + dryRun: uploadPlan.manifest.mode?.dryRun === true, + provider: uploadPlan.manifest.mode?.provider, + channel: uploadPlan.manifest.mode?.channel, + target: uploadPlan.manifest.target, + artifactCount: Array.isArray(uploadPlan.manifest.artifacts) + ? uploadPlan.manifest.artifacts.length + : 0, + } + : null, + }, + warnings, + failures, +} + +const reportPath = writeReport(report) + +console.log("Moss release packaging verification") +console.log(`status: ${report.status}`) +console.log(`report: ${normalizeRelative(reportPath)}`) +console.log(`mac artifacts: ${releaseArtifacts.length}`) +console.log(`update manifests: ${updateManifests.length}`) +console.log(`notarization evidence: ${notarizationEvidence.length}`) +console.log(`bundled binaries: ${packagedAppBinaries.map((app) => `${app.arch}=${app.complete ? "complete" : "incomplete"}`).join(", ")}`) +console.log(`upload plan: ${uploadPlan?.manifest ? normalizeRelative(uploadPlan.manifestPath) : "missing"}`) +console.log(`credential preflight: ${credentialPreflight?.report ? `${credentialPreflight.report.status} (${normalizeRelative(credentialPreflight.reportPath)})` : "missing"}`) +console.log(`release evidence audit: ${releaseEvidenceAudit?.report ? `${releaseEvidenceAudit.report.status} (${normalizeRelative(releaseEvidenceAudit.reportPath)})` : "missing"}`) + +for (const message of warnings) { + console.warn(`warning: ${message}`) +} + +if (failures.length > 0) { + for (const message of failures) { + console.error(`error: ${message}`) + } + process.exit(1) +} diff --git a/src/main/lib/agent-runtime/adapters/claude-code.ts b/src/main/lib/agent-runtime/adapters/claude-code.ts new file mode 100644 index 000000000..7e8a2294f --- /dev/null +++ b/src/main/lib/agent-runtime/adapters/claude-code.ts @@ -0,0 +1,122 @@ +import { eq } from "drizzle-orm" +import { getClaudeShellEnvironment, resolveClaudeCodeExecutable } from "../../claude/env" +import { getExistingClaudeToken } from "../../claude-token" +import { + anthropicAccounts, + anthropicSettings, + claudeCodeCredentials, + getDatabase, +} from "../../db" +import { getAgentRuntimeManifest } from "../manifests" +import type { + AgentRuntimeAdapter, + AgentRuntimeAvailability, + AgentRuntimeHealth, + AgentRuntimeSessionRef, +} from "../types" + +function getClaudeStoredAuthMethod(): "oauth" | null { + try { + const db = getDatabase() + const settings = db + .select() + .from(anthropicSettings) + .where(eq(anthropicSettings.id, "singleton")) + .get() + + if (settings?.activeAccountId) { + const account = db + .select() + .from(anthropicAccounts) + .where(eq(anthropicAccounts.id, settings.activeAccountId)) + .get() + if (account?.oauthToken) return "oauth" + } + + const credential = db + .select() + .from(claudeCodeCredentials) + .where(eq(claudeCodeCredentials.id, "default")) + .get() + + return credential?.oauthToken ? "oauth" : null + } catch { + return null + } +} + +function getClaudeShellAuthMethod(): "api-key" | "shell-config" | null { + try { + const shellEnv = getClaudeShellEnvironment() + if (shellEnv.ANTHROPIC_API_KEY || shellEnv.ANTHROPIC_AUTH_TOKEN) { + return "api-key" + } + if (shellEnv.ANTHROPIC_BASE_URL) { + return "shell-config" + } + } catch { + return null + } + + return null +} + +async function inspectClaudeRuntime(): Promise { + const manifest = getAgentRuntimeManifest("claude-code") + const executable = resolveClaudeCodeExecutable() + + if (executable.reason && executable.source === "bundled") { + return { + availability: "not-installed", + statusReason: executable.reason, + authMethod: "unknown", + models: manifest.models?.map((model) => ({ + ...model, + availability: "not-installed", + reason: executable.reason, + })), + } + } + + const authMethod = + getClaudeStoredAuthMethod() ?? + getClaudeShellAuthMethod() ?? + (getExistingClaudeToken() ? "oauth" : null) + + if (!authMethod) { + return { + availability: "needs-auth", + statusReason: "Claude Code is installed but no OAuth token, API key, or proxy config was found.", + authMethod: "not-authenticated", + models: manifest.models?.map((model) => ({ + ...model, + availability: "needs-auth", + reason: "Claude Code authentication is required.", + })), + } + } + + return { + availability: "available", + statusReason: `Claude Code auth detected via ${authMethod}; executable: ${executable.source}.`, + authMethod, + models: manifest.models?.map((model) => ({ + ...model, + availability: "available", + })), + } +} + +export const claudeCodeAdapter: AgentRuntimeAdapter = { + manifest: getAgentRuntimeManifest("claude-code"), + async inspect( + _session: AgentRuntimeSessionRef, + ): Promise { + return inspectClaudeRuntime() + }, + async canStart( + _session: AgentRuntimeSessionRef, + ): Promise { + return (await inspectClaudeRuntime()).availability + }, +} diff --git a/src/main/lib/agent-runtime/adapters/codex.ts b/src/main/lib/agent-runtime/adapters/codex.ts new file mode 100644 index 000000000..8eebcb2c8 --- /dev/null +++ b/src/main/lib/agent-runtime/adapters/codex.ts @@ -0,0 +1,73 @@ +import { getCodexIntegrationStatus } from "../../trpc/routers/codex" +import { getAgentRuntimeManifest } from "../manifests" +import type { + AgentRuntimeAdapter, + AgentRuntimeAvailability, + AgentRuntimeHealth, + AgentRuntimeSessionRef, +} from "../types" + +async function inspectCodexRuntime(): Promise { + const manifest = getAgentRuntimeManifest("codex") + + try { + const integration = await getCodexIntegrationStatus() + const authMethod = integration.state === "connected_api_key" + ? "api-key" + : integration.state === "connected_chatgpt" + ? "oauth" + : "not-authenticated" + + if (integration.isConnected) { + return { + availability: "available", + statusReason: `Codex auth detected via ${integration.state}.`, + authMethod, + models: manifest.models?.map((model) => ({ + ...model, + availability: "available", + })), + } + } + + return { + availability: "needs-auth", + statusReason: + integration.rawOutput || + "Codex CLI is installed but no login was found.", + authMethod, + models: manifest.models?.map((model) => ({ + ...model, + availability: "needs-auth", + reason: "Codex authentication is required.", + })), + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + const missingBinary = message.includes("Bundled Codex CLI not found") + return { + availability: missingBinary ? "not-installed" : "error", + statusReason: message, + authMethod: "unknown", + models: manifest.models?.map((model) => ({ + ...model, + availability: missingBinary ? "not-installed" : "error", + reason: message, + })), + } + } +} + +export const codexAdapter: AgentRuntimeAdapter = { + manifest: getAgentRuntimeManifest("codex"), + async inspect( + _session: AgentRuntimeSessionRef, + ): Promise { + return inspectCodexRuntime() + }, + async canStart( + _session: AgentRuntimeSessionRef, + ): Promise { + return (await inspectCodexRuntime()).availability + }, +} diff --git a/src/main/lib/agent-runtime/adapters/custom-acp.ts b/src/main/lib/agent-runtime/adapters/custom-acp.ts new file mode 100644 index 000000000..0192a1df8 --- /dev/null +++ b/src/main/lib/agent-runtime/adapters/custom-acp.ts @@ -0,0 +1,37 @@ +import { getAgentRuntimeManifest } from "../manifests" +import type { + AgentRuntimeAdapter, + AgentRuntimeAvailability, + AgentRuntimeHealth, + AgentRuntimeSessionRef, +} from "../types" + +async function inspectCustomAcpRuntime(): Promise { + const manifest = getAgentRuntimeManifest("custom-acp") + + return { + availability: "unsupported", + statusReason: + "Configure a Moss Custom ACP endpoint or command adapter before starting sessions.", + authMethod: "unsupported", + models: manifest.models?.map((model) => ({ + ...model, + availability: "unsupported", + reason: "Custom ACP does not have a configured adapter yet.", + })), + } +} + +export const customAcpAdapter: AgentRuntimeAdapter = { + manifest: getAgentRuntimeManifest("custom-acp"), + async inspect( + _session: AgentRuntimeSessionRef, + ): Promise { + return inspectCustomAcpRuntime() + }, + async canStart( + _session: AgentRuntimeSessionRef, + ): Promise { + return (await inspectCustomAcpRuntime()).availability + }, +} diff --git a/src/main/lib/agent-runtime/adapters/hermes.ts b/src/main/lib/agent-runtime/adapters/hermes.ts new file mode 100644 index 000000000..956c4b792 --- /dev/null +++ b/src/main/lib/agent-runtime/adapters/hermes.ts @@ -0,0 +1,67 @@ +import { getAgentRuntimeManifest } from "../manifests" +import type { + AgentRuntimeAdapter, + AgentRuntimeAvailability, + AgentRuntimeHealth, + AgentRuntimeSessionRef, +} from "../types" +import { resolveHermesRuntime } from "../../hermes/runtime" + +async function inspectHermesRuntime(): Promise { + const manifest = getAgentRuntimeManifest("hermes") + const runtime = resolveHermesRuntime() + + if (!runtime.executable && !runtime.sourceRoot) { + return { + availability: "not-installed", + statusReason: "Hermes CLI and source root were not found.", + authMethod: "unknown", + models: manifest.models?.map((model) => ({ + ...model, + availability: "not-installed", + reason: "Hermes is not installed.", + })), + } + } + + if ((runtime.acpExecutable || runtime.executable) && runtime.acpAdapterPath) { + const launchPath = runtime.acpExecutable || `${runtime.executable} acp` + return { + availability: "available", + statusReason: + `Hermes ACP transport is available via ${launchPath}.`, + authMethod: "shell-config", + models: manifest.models?.map((model) => ({ + ...model, + availability: "available", + reason: "Hermes uses the current ACP runtime model unless a concrete ACP model is selected.", + })), + } + } + + return { + availability: "unsupported", + statusReason: + `Hermes source detected at ${runtime.sourceRoot || "unknown"}, but executable or ACP adapter is missing.`, + authMethod: "unknown", + models: manifest.models?.map((model) => ({ + ...model, + availability: "unsupported", + reason: "Hermes executable or ACP adapter is missing.", + })), + } +} + +export const hermesAdapter: AgentRuntimeAdapter = { + manifest: getAgentRuntimeManifest("hermes"), + async inspect( + _session: AgentRuntimeSessionRef, + ): Promise { + return inspectHermesRuntime() + }, + async canStart( + _session: AgentRuntimeSessionRef, + ): Promise { + return (await inspectHermesRuntime()).availability + }, +} diff --git a/src/main/lib/agent-runtime/adapters/index.ts b/src/main/lib/agent-runtime/adapters/index.ts new file mode 100644 index 000000000..8300e96d6 --- /dev/null +++ b/src/main/lib/agent-runtime/adapters/index.ts @@ -0,0 +1,21 @@ +import { claudeCodeAdapter } from "./claude-code" +import { codexAdapter } from "./codex" +import { customAcpAdapter } from "./custom-acp" +import { hermesAdapter } from "./hermes" +import type { AgentEngineId, AgentRuntimeAdapter } from "../types" + +export const agentRuntimeAdapters: Record< + AgentEngineId, + AgentRuntimeAdapter +> = { + "claude-code": claudeCodeAdapter, + codex: codexAdapter, + hermes: hermesAdapter, + "custom-acp": customAcpAdapter, +} + +export function getAgentRuntimeAdapter( + engineId: AgentEngineId, +): AgentRuntimeAdapter { + return agentRuntimeAdapters[engineId] +} diff --git a/src/main/lib/agent-runtime/codex-native-message-parts.ts b/src/main/lib/agent-runtime/codex-native-message-parts.ts new file mode 100644 index 000000000..7b35720ff --- /dev/null +++ b/src/main/lib/agent-runtime/codex-native-message-parts.ts @@ -0,0 +1,302 @@ +import { isCodexRuntimeNoticeText } from "../../../shared/codex-runtime-notices" + +export type CodexNativeMessagePart = { + type: string + text?: string + state?: string + toolCallId?: string + toolName?: string + input?: unknown + result?: unknown + output?: unknown + startedAt?: number + title?: string + [key: string]: unknown +} + +export type CodexNativeToolResultUpdate = { + output: unknown + input?: unknown + isError?: boolean +} + +export type CodexNativeMessagePartChange = { + part: CodexNativeMessagePart + didStart: boolean +} + +function mergeToolInput( + existingInput: unknown, + nextInput: unknown, +): unknown { + const canMergeInput = + existingInput && + typeof existingInput === "object" && + !Array.isArray(existingInput) && + nextInput && + typeof nextInput === "object" && + !Array.isArray(nextInput) + + if (!canMergeInput) return nextInput + + return { + ...(existingInput as Record), + ...(nextInput as Record), + } +} + +function stringValue(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined +} + +function normalizeToolCommand(value: string): string { + return value.replace(/\s+/g, " ").trim() +} + +function normalizeComparableText(value: unknown): string { + return String(value ?? "") + .replace(/\s+/g, " ") + .trim() +} + +export const isCodexNativeRuntimeNoticeText = isCodexRuntimeNoticeText + +function getLastTextPart(parts: CodexNativeMessagePart[]) { + const lastPart = parts.at(-1) + return lastPart?.type === "text" ? lastPart : null +} + +function isDuplicateAdjacentText( + parts: CodexNativeMessagePart[], + nextText: string, +): boolean { + const lastTextPart = getLastTextPart(parts) + if (!lastTextPart) return false + const normalizedNextText = normalizeComparableText(nextText) + return ( + Boolean(normalizedNextText) && + normalizeComparableText(lastTextPart.text) === normalizedNextText + ) +} + +function getToolInputCommand(input: unknown): string | undefined { + if (!input || typeof input !== "object" || Array.isArray(input)) return undefined + + const record = input as Record + return ( + stringValue(record.cmd) ?? + stringValue(record.command) ?? + stringValue(record.rawCommand) + ) +} + +function getToolSignature(toolName: string | undefined, input: unknown): string | null { + if (!toolName) return null + const command = getToolInputCommand(input) + if (!command) return null + return `${toolName}:${normalizeToolCommand(command)}` +} + +function areNativeMirrorCallIds(first: string, second: string): boolean { + return ( + (first.startsWith("call_") && second.startsWith("item_")) || + (first.startsWith("item_") && second.startsWith("call_")) + ) +} + +export function createCodexNativeMessagePartsAccumulator() { + const messageParts: CodexNativeMessagePart[] = [] + const toolParts: CodexNativeMessagePart[] = [] + const toolPartIndexByCallId = new Map() + const recentToolPartIndexBySignature = new Map() + let activeTextPart: CodexNativeMessagePart | null = null + let finalTextPart: CodexNativeMessagePart | null = null + let lastStandaloneCommentaryText: string | null = null + + const resetRecentToolSignatures = () => { + recentToolPartIndexBySignature.clear() + } + + const closeActiveTextPart = () => { + activeTextPart = null + } + + const closeFinalTextPart = () => { + finalTextPart = null + } + + return { + get parts() { + return messageParts + }, + get toolParts() { + return toolParts + }, + replaceWith(snapshot: { + parts: CodexNativeMessagePart[] + toolParts: CodexNativeMessagePart[] + }) { + messageParts.splice(0, messageParts.length, ...snapshot.parts) + toolParts.splice(0, toolParts.length, ...snapshot.toolParts) + toolPartIndexByCallId.clear() + for (const [index, part] of toolParts.entries()) { + if (typeof part.toolCallId === "string") { + toolPartIndexByCallId.set(part.toolCallId, index) + } + } + resetRecentToolSignatures() + activeTextPart = null + finalTextPart = null + lastStandaloneCommentaryText = null + }, + closeActiveTextPart, + closeFinalTextPart, + closeTextParts() { + closeActiveTextPart() + closeFinalTextPart() + }, + appendTextDelta(delta: string): CodexNativeMessagePartChange | null { + if (!delta) return null + if (isCodexNativeRuntimeNoticeText(delta)) return null + lastStandaloneCommentaryText = null + resetRecentToolSignatures() + if (!activeTextPart) { + if (isDuplicateAdjacentText(messageParts, delta)) return null + activeTextPart = { + type: "text", + text: "", + state: "done", + } + messageParts.push(activeTextPart) + activeTextPart.text = delta + return { part: activeTextPart, didStart: true } + } + + activeTextPart.text = `${activeTextPart.text ?? ""}${delta}` + return { part: activeTextPart, didStart: false } + }, + appendFinalTextDelta(delta: string): CodexNativeMessagePartChange | null { + if (!delta) return null + if (isCodexNativeRuntimeNoticeText(delta)) return null + lastStandaloneCommentaryText = null + resetRecentToolSignatures() + closeActiveTextPart() + if (!finalTextPart) { + if (isDuplicateAdjacentText(messageParts, delta)) return null + finalTextPart = { + type: "text", + text: "", + state: "done", + } + messageParts.push(finalTextPart) + finalTextPart.text = delta + return { part: finalTextPart, didStart: true } + } + + finalTextPart.text = `${finalTextPart.text ?? ""}${delta}` + return { part: finalTextPart, didStart: false } + }, + appendCommentaryText(text: string): CodexNativeMessagePart | null { + const commentaryText = text.trim() + if (!commentaryText) return null + if (isCodexNativeRuntimeNoticeText(commentaryText)) return null + if (commentaryText === lastStandaloneCommentaryText) return null + if (isDuplicateAdjacentText(messageParts, commentaryText)) return null + + resetRecentToolSignatures() + closeActiveTextPart() + const commentaryPart: CodexNativeMessagePart = { + type: "text", + text: commentaryText, + state: "done", + } + messageParts.push(commentaryPart) + lastStandaloneCommentaryText = commentaryText + return commentaryPart + }, + startTool(params: { + callId: string + toolName: string + input: unknown + title?: string + startedAt?: number + }): CodexNativeMessagePartChange { + lastStandaloneCommentaryText = null + closeActiveTextPart() + closeFinalTextPart() + + const existingIndex = toolPartIndexByCallId.get(params.callId) + if (typeof existingIndex === "number") { + return { + part: toolParts[existingIndex], + didStart: false, + } + } + + const signature = getToolSignature(params.toolName, params.input) + const duplicateIndex = signature + ? recentToolPartIndexBySignature.get(signature) + : undefined + const duplicatePart = + typeof duplicateIndex === "number" ? toolParts[duplicateIndex] : null + if ( + typeof duplicateIndex === "number" && + duplicatePart && + typeof duplicatePart.toolCallId === "string" && + areNativeMirrorCallIds(duplicatePart.toolCallId, params.callId) + ) { + duplicatePart.input = mergeToolInput(duplicatePart.input, params.input) + if (params.title && !duplicatePart.title) duplicatePart.title = params.title + toolPartIndexByCallId.set(params.callId, duplicateIndex) + return { + part: duplicatePart, + didStart: false, + } + } + + toolPartIndexByCallId.set(params.callId, toolParts.length) + const toolPart: CodexNativeMessagePart = { + type: `tool-${params.toolName}`, + toolCallId: params.callId, + toolName: params.toolName, + input: params.input, + state: "call", + startedAt: params.startedAt ?? Date.now(), + ...(params.title ? { title: params.title } : {}), + } + toolParts.push(toolPart) + messageParts.push(toolPart) + if (signature) { + recentToolPartIndexBySignature.set(signature, toolParts.length - 1) + } + + return { + part: toolPart, + didStart: true, + } + }, + updateToolResult( + callId: string, + update: CodexNativeToolResultUpdate, + ): CodexNativeMessagePart | null { + const partIndex = toolPartIndexByCallId.get(callId) + if (typeof partIndex !== "number") return null + + const toolPart = toolParts[partIndex] + toolPart.result = update.output + toolPart.output = update.output + toolPart.state = update.isError ? "output-error" : "result" + if (update.input !== undefined) { + toolPart.input = mergeToolInput(toolPart.input, update.input) + } + + return toolPart + }, + snapshot() { + return { + parts: messageParts, + toolParts, + } + }, + } +} diff --git a/src/main/lib/agent-runtime/codex-native-recovery.ts b/src/main/lib/agent-runtime/codex-native-recovery.ts new file mode 100644 index 000000000..9d11ad588 --- /dev/null +++ b/src/main/lib/agent-runtime/codex-native-recovery.ts @@ -0,0 +1,467 @@ +import { createHash } from "node:crypto" +import { readdir, readFile } from "node:fs/promises" +import { homedir } from "node:os" +import { join } from "node:path" +import { + codexJsonlEventToNativeToolEvent, + extractCodexJsonlEventText, + isCodexJsonlCommentaryTextEvent, + isCodexJsonlDeltaTextEvent, + isCodexJsonlFinalTextEvent, + parseCodexJsonlEventLine, + type CodexJsonlEvent, +} from "./codex-native-session" +import { + createCodexNativeMessagePartsAccumulator, + type CodexNativeMessagePart, +} from "./codex-native-message-parts" + +export type CodexNativeRecoveredMessage = { + role?: string + parts?: CodexNativeMessagePart[] + content?: unknown + [key: string]: unknown +} + +export type CodexNativeSessionTurn = { + userText: string + events: CodexJsonlEvent[] +} + +export type CodexNativeMessageRecoveryResult< + TMessage extends CodexNativeRecoveredMessage, +> = { + messages: TMessage[] + changed: boolean + recoveredAssistantCount: number +} + +function stableEventHash(event: CodexJsonlEvent): string { + try { + return createHash("sha1").update(JSON.stringify(event)).digest("hex") + } catch { + return createHash("sha1").update(String(event)).digest("hex") + } +} + +function stringValue(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined +} + +function normalizeText(value: unknown): string { + return String(value ?? "") + .replace(/\s+/g, " ") + .trim() +} + +function getToolInputCommand(input: unknown): string | undefined { + if (!input || typeof input !== "object" || Array.isArray(input)) return undefined + + const record = input as Record + return ( + stringValue(record.cmd) ?? + stringValue(record.command) ?? + stringValue(record.rawCommand) + ) +} + +function getPartToolSignature( + part: Pick, +): string | null { + if (!part.type?.startsWith("tool-")) return null + const toolName = stringValue(part.toolName) ?? part.type.slice("tool-".length) + const command = getToolInputCommand(part.input) + if (!toolName || !command) return null + return `${toolName}:${normalizeText(command)}` +} + +function areNativeMirrorCallIds(first: unknown, second: unknown): boolean { + if (typeof first !== "string" || typeof second !== "string") return false + return ( + (first.startsWith("call_") && second.startsWith("item_")) || + (first.startsWith("item_") && second.startsWith("call_")) + ) +} + +function extractTextFromContent(content: unknown): string | undefined { + if (typeof content === "string") return content + if (!Array.isArray(content)) return undefined + + const text = content + .map((item) => { + if (!item || typeof item !== "object") return "" + const record = item as Record + return stringValue(record.text) ?? stringValue(record.content) ?? "" + }) + .join("") + .trim() + + return text || undefined +} + +function extractCodexUserEventText(event: CodexJsonlEvent): string { + const payload = (event as any)?.payload + const extractedText = extractCodexJsonlEventText(event) + const text = + extractedText ?? + stringValue(payload?.message) ?? + stringValue(payload?.text) ?? + extractTextFromContent(payload?.content) ?? + extractTextFromContent((event as any)?.content) + + return normalizeText(text) +} + +export function isCodexJsonlUserEvent(event: CodexJsonlEvent): boolean { + const payload = (event as any)?.payload + if ((event as any)?.type === "event_msg" && payload?.type === "user_message") { + return true + } + if ((event as any)?.type === "response_item" && payload?.role === "user") { + return true + } + if (payload?.role === "user" || (event as any)?.role === "user") { + return true + } + return false +} + +export function buildNativePartsFromCodexEvents(events: CodexJsonlEvent[]) { + const parts = createCodexNativeMessagePartsAccumulator() + const handledEventHashes = new Set() + const seenFinalTexts = new Set() + + for (const event of events) { + const eventHash = stableEventHash(event) + if (handledEventHashes.has(eventHash)) continue + handledEventHashes.add(eventHash) + + if (isCodexJsonlUserEvent(event)) continue + + const text = extractCodexJsonlEventText(event) + if (text && isCodexJsonlCommentaryTextEvent(event)) { + parts.appendCommentaryText(text) + continue + } + + const toolEvent = codexJsonlEventToNativeToolEvent(event) + if (toolEvent?.kind === "tool-input") { + parts.startTool({ + callId: toolEvent.callId, + toolName: toolEvent.toolName, + input: toolEvent.input, + ...(toolEvent.title ? { title: toolEvent.title } : {}), + }) + } else if (toolEvent?.kind === "tool-output") { + parts.updateToolResult(toolEvent.callId, { + output: toolEvent.output, + ...(toolEvent.input !== undefined ? { input: toolEvent.input } : {}), + ...(toolEvent.isError ? { isError: true } : {}), + }) + } + + if (!text) continue + + if (isCodexJsonlFinalTextEvent(event)) { + const finalText = text.trim() + if (finalText && !seenFinalTexts.has(finalText)) { + seenFinalTexts.add(finalText) + parts.appendFinalTextDelta(text) + } + continue + } + + if (isCodexJsonlDeltaTextEvent(event)) { + parts.appendTextDelta(text) + } + } + + return parts.snapshot() +} + +export function getCodexNativePartsRichness(snapshot: { + parts: Array<{ + type: string + text?: string + result?: unknown + output?: unknown + }> + toolParts: Array<{ result?: unknown; output?: unknown }> +}): number { + const textParts = snapshot.parts.filter( + (part) => + part.type === "text" && + typeof part.text === "string" && + part.text.trim(), + ).length + const toolParts = snapshot.toolParts.length + const toolOutputs = snapshot.toolParts.filter( + (part) => part.result !== undefined || part.output !== undefined, + ).length + return textParts * 10 + toolParts * 3 + toolOutputs +} + +function isCodexNativeSetupLeakText(text: string): boolean { + if (!text) return false + return ( + text.startsWith("") || + text.startsWith("") || + text.startsWith("# AGENTS.md instructions") || + text.includes("Filesystem sandboxing defines which files can be read or written") || + text.includes("A skill is a set of local instructions to follow") || + text.includes("Approval policy is currently") + ) +} + +function getCodexNativePartsDuplicatePenalty(snapshot: { + parts: CodexNativeMessagePart[] +}): number { + let penalty = 0 + let previousText = "" + let toolCallIdBySignature = new Map() + + for (const part of snapshot.parts) { + if (part.type === "text") { + const text = normalizeText(part.text) + if (isCodexNativeSetupLeakText(text)) penalty += 100 + if (text && text === previousText) penalty += 1 + previousText = text + toolCallIdBySignature = new Map() + continue + } + + previousText = "" + if (!part.type?.startsWith("tool-")) continue + + const signature = getPartToolSignature(part) + if (!signature) continue + + const previousCallId = toolCallIdBySignature.get(signature) + if (areNativeMirrorCallIds(previousCallId, part.toolCallId)) { + penalty += 1 + continue + } + + toolCallIdBySignature.set(signature, part.toolCallId) + } + + return penalty +} + +function getCodexNativePartsReplayScore(snapshot: { + parts: CodexNativeMessagePart[] + toolParts: Array<{ result?: unknown; output?: unknown }> +}): number { + return ( + getCodexNativePartsRichness(snapshot) - + getCodexNativePartsDuplicatePenalty(snapshot) * 20 + ) +} + +export function splitCodexSessionEventsIntoTurns( + events: CodexJsonlEvent[], +): CodexNativeSessionTurn[] { + const turns: CodexNativeSessionTurn[] = [] + let currentTurn: CodexNativeSessionTurn | null = null + + for (const event of events) { + if (isCodexJsonlUserEvent(event)) { + const userText = extractCodexUserEventText(event) + if ( + currentTurn && + currentTurn.events.length === 0 && + normalizeText(currentTurn.userText) === userText + ) { + continue + } + + currentTurn = { userText, events: [] } + turns.push(currentTurn) + continue + } + + currentTurn?.events.push(event) + } + + return turns.filter((turn) => turn.events.length > 0) +} + +function getMessageText(message: CodexNativeRecoveredMessage): string { + if (Array.isArray(message.parts)) { + const text = message.parts + .map((part) => { + if (!part || typeof part !== "object") return "" + if (part.type === "text" && typeof part.text === "string") { + return part.text + } + return "" + }) + .join("") + .trim() + if (text) return normalizeText(text) + } + + if (typeof message.content === "string") { + return normalizeText(message.content) + } + + return "" +} + +function getMessageSnapshot(message: CodexNativeRecoveredMessage) { + const parts = Array.isArray(message.parts) ? message.parts : [] + const toolParts = parts.filter((part) => part.type?.startsWith("tool-")) + return { parts, toolParts } +} + +function userTextsMatch(storedText: string, turnText: string): boolean { + if (!storedText || !turnText) return false + if (storedText === turnText) return true + return storedText.includes(turnText) || turnText.includes(storedText) +} + +function findMatchingTurnIndex( + turns: CodexNativeSessionTurn[], + turnCursor: number, + userText: string, +): number { + const normalizedUserText = normalizeText(userText) + if (!normalizedUserText) return turnCursor + + for (let index = turnCursor; index < turns.length; index += 1) { + if (userTextsMatch(normalizedUserText, normalizeText(turns[index].userText))) { + return index + } + } + + return turnCursor +} + +export function recoverCodexNativeMessagesFromSessionEvents< + TMessage extends CodexNativeRecoveredMessage, +>( + messages: TMessage[], + events: CodexJsonlEvent[], +): CodexNativeMessageRecoveryResult { + const turns = splitCodexSessionEventsIntoTurns(events) + if (turns.length === 0 || messages.length === 0) { + return { messages, changed: false, recoveredAssistantCount: 0 } + } + + let changed = false + let recoveredAssistantCount = 0 + let turnCursor = 0 + const nextMessages = [...messages] + + for (let index = 0; index < nextMessages.length; index += 1) { + const userMessage = nextMessages[index] + if (userMessage.role !== "user") continue + + const assistantIndex = nextMessages.findIndex( + (message, candidateIndex) => + candidateIndex > index && message.role === "assistant", + ) + if (assistantIndex === -1 || turnCursor >= turns.length) continue + + const turnIndex = findMatchingTurnIndex( + turns, + turnCursor, + getMessageText(userMessage), + ) + const turn = turns[turnIndex] + if (!turn) continue + + const replaySnapshot = buildNativePartsFromCodexEvents(turn.events) + const existingSnapshot = getMessageSnapshot(nextMessages[assistantIndex]) + if ( + getCodexNativePartsReplayScore(replaySnapshot) > + getCodexNativePartsReplayScore(existingSnapshot) + ) { + nextMessages[assistantIndex] = { + ...nextMessages[assistantIndex], + parts: replaySnapshot.parts, + } + changed = true + recoveredAssistantCount += 1 + } + + turnCursor = turnIndex + 1 + index = assistantIndex + } + + return { + messages: changed ? nextMessages : messages, + changed, + recoveredAssistantCount, + } +} + +export async function findCodexNativeSessionFileById( + sessionId: string, +): Promise { + const cleanedSessionId = sessionId.trim() + if (!cleanedSessionId) return null + + const sessionsRoot = join( + process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), + "sessions", + ) + const fileSuffix = `-${cleanedSessionId}.jsonl` + const sortDesc = (values: string[]) => + values.sort((left, right) => + right.localeCompare(left, undefined, { numeric: true }), + ) + const listNames = async (dirPath: string): Promise => { + try { + return await readdir(dirPath, { encoding: "utf8" }) + } catch { + return [] + } + } + + const years = sortDesc( + (await listNames(sessionsRoot)).filter((name) => /^\d{4}$/.test(name)), + ) + for (const year of years) { + const yearPath = join(sessionsRoot, year) + const months = sortDesc( + (await listNames(yearPath)).filter((name) => /^\d{2}$/.test(name)), + ) + for (const month of months) { + const monthPath = join(yearPath, month) + const days = sortDesc( + (await listNames(monthPath)).filter((name) => /^\d{2}$/.test(name)), + ) + for (const day of days) { + const dayPath = join(monthPath, day) + const fileName = (await listNames(dayPath)).find((name) => + name.endsWith(fileSuffix), + ) + if (fileName) return join(dayPath, fileName) + } + } + } + + return null +} + +export async function readCodexNativeSessionEventsById( + sessionId: string, +): Promise { + const sessionFile = await findCodexNativeSessionFileById(sessionId) + if (!sessionFile) return [] + + let rawContent = "" + try { + rawContent = await readFile(sessionFile, "utf8") + } catch { + return [] + } + + const events: CodexJsonlEvent[] = [] + for (const line of rawContent.split(/\r?\n/)) { + const event = parseCodexJsonlEventLine(line) + if (event) events.push(event) + } + return events +} diff --git a/src/main/lib/agent-runtime/codex-native-resume.ts b/src/main/lib/agent-runtime/codex-native-resume.ts new file mode 100644 index 000000000..6ab3fa525 --- /dev/null +++ b/src/main/lib/agent-runtime/codex-native-resume.ts @@ -0,0 +1,168 @@ +import { stripCodexRuntimeNoticeText } from "../../../shared/codex-runtime-notices" + +export const CODEX_NATIVE_RESUME_MAX_STORED_MESSAGES_BYTES = 8 * 1024 * 1024 + +export type CodexNativeResumeSkipReason = + | "force-new-session" + | "oversized-transcript" + | "runtime-notice-only-terminal" + +export type CodexStoredMessage = { + role?: unknown + parts?: unknown + metadata?: unknown + [key: string]: unknown +} + +type CodexStoredPart = { + type?: unknown + text?: unknown + [key: string]: unknown +} + +function asRecord(value: unknown): Record | null { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as Record) + : null +} + +function parseRuntimeMetadata( + runtimeMetadata: string | Record | null | undefined, +): Record { + if (!runtimeMetadata) return {} + if (typeof runtimeMetadata === "object") return runtimeMetadata + try { + const parsed = JSON.parse(runtimeMetadata) + return asRecord(parsed) ?? {} + } catch { + return {} + } +} + +function isRuntimeNoticeTextPart(part: unknown): boolean { + const record = asRecord(part) as CodexStoredPart | null + if (!record) return false + if (record.type !== "text") return false + const stripped = stripCodexRuntimeNoticeText(record.text) + return stripped.changed && stripped.text.trim().length === 0 +} + +export function isCodexNativeRuntimeNoticeOnlyAssistantMessage( + message: CodexStoredMessage, +): boolean { + if (message.role !== "assistant") return false + if (!Array.isArray(message.parts) || message.parts.length === 0) return false + return message.parts.every(isRuntimeNoticeTextPart) +} + +export function stripCodexNativeRuntimeNoticeMessages( + messages: CodexStoredMessage[], +): { + messages: CodexStoredMessage[] + removedCount: number + removedPartCount: number +} { + let removedCount = 0 + let removedPartCount = 0 + const cleanedMessages: CodexStoredMessage[] = [] + + for (const message of messages) { + if (message.role !== "assistant" || !Array.isArray(message.parts)) { + cleanedMessages.push(message) + continue + } + + const cleanedParts: unknown[] = [] + let messageChanged = false + + for (const part of message.parts) { + const record = asRecord(part) as CodexStoredPart | null + if (!record || record.type !== "text") { + cleanedParts.push(part) + continue + } + + const stripped = stripCodexRuntimeNoticeText(record.text) + if (!stripped.changed) { + cleanedParts.push(part) + continue + } + + removedPartCount += 1 + messageChanged = true + if (stripped.text.trim().length === 0) continue + + cleanedParts.push({ + ...record, + text: stripped.text, + }) + } + + if (cleanedParts.length === 0) { + removedCount += 1 + continue + } + + cleanedMessages.push( + !messageChanged && cleanedParts.length === message.parts.length + ? message + : { + ...message, + parts: cleanedParts, + }, + ) + } + + return { + messages: cleanedMessages, + removedCount, + removedPartCount, + } +} + +export function shouldStartFreshCodexNativeSession(params: { + storedMessagesByteLength: number + runtimeMetadata: string | Record | null | undefined + candidateSessionId?: string | null + forceNewSession?: boolean + messages: CodexStoredMessage[] +}): { + startFresh: boolean + reason?: CodexNativeResumeSkipReason +} { + if (params.forceNewSession) { + return { startFresh: true, reason: "force-new-session" } + } + + if (!params.candidateSessionId) { + return { startFresh: false } + } + + if ( + params.storedMessagesByteLength > + CODEX_NATIVE_RESUME_MAX_STORED_MESSAGES_BYTES + ) { + return { startFresh: true, reason: "oversized-transcript" } + } + + const lastAssistantMessage = [...params.messages] + .reverse() + .find((message) => message.role === "assistant") + + if ( + lastAssistantMessage && + isCodexNativeRuntimeNoticeOnlyAssistantMessage(lastAssistantMessage) + ) { + return { startFresh: true, reason: "runtime-notice-only-terminal" } + } + + const runtimeMetadata = parseRuntimeMetadata(params.runtimeMetadata) + if ( + runtimeMetadata.resultSubtype === "running" && + !params.messages.some((message) => message.role === "assistant") + ) { + return { startFresh: true, reason: "runtime-notice-only-terminal" } + } + + return { startFresh: false } +} diff --git a/src/main/lib/agent-runtime/codex-native-session.ts b/src/main/lib/agent-runtime/codex-native-session.ts new file mode 100644 index 000000000..640da3163 --- /dev/null +++ b/src/main/lib/agent-runtime/codex-native-session.ts @@ -0,0 +1,1465 @@ +import { spawn } from "node:child_process" +import { mkdtemp, rm, writeFile } from "node:fs/promises" +import { tmpdir } from "node:os" +import path from "node:path" +import { StringDecoder } from "node:string_decoder" +import type { AgentPermissionMode } from "./types" + +export type CodexNativeBridgeAction = "start" | "resume" | "fork" + +export type CodexNativeBridgeKind = + | "codex-exec-start" + | "codex-exec-resume" + | "codex-tui-fork" + +export type CodexNativeBridgeMode = "headless-exec" | "native-tui" +export type CodexNativePromptSource = "stdin" | "argument" | "none" + +export interface CodexNativeSessionBridgePlan { + engine: "codex" + action: CodexNativeBridgeAction + bridge: CodexNativeBridgeKind + mode: CodexNativeBridgeMode + command: string + args: string[] + cwd: string + sessionId?: string + modelId?: string + modelReasoningEffort?: string + permissionMode: AgentPermissionMode + promptSource: CodexNativePromptSource + imagePaths: string[] + imageCount: number + canRunHeadless: boolean + notes: string[] +} + +export interface CodexNativeImageAttachment { + base64Data: string + mediaType: string + filename?: string | null +} + +export interface BuildCodexNativeSessionBridgePlanInput { + action: CodexNativeBridgeAction + sessionId?: string | null + cwd: string + modelId?: string | null + permissionMode?: AgentPermissionMode | null + prompt?: string | null + promptSource?: CodexNativePromptSource + command?: string | null + includeJson?: boolean + skipGitRepoCheck?: boolean + imagePaths?: string[] | null +} + +export interface CodexNativeCommandRunnerInput { + command: string + args: string[] + cwd: string + stdin?: string + env?: NodeJS.ProcessEnv + abortSignal?: AbortSignal + onStdoutJsonEvent?: (event: CodexJsonlEvent) => void +} + +export interface CodexNativeCommandRunnerResult { + stdout: string + stderr: string + exitCode: number | null +} + +export type CodexNativeCommandRunner = ( + input: CodexNativeCommandRunnerInput, +) => Promise + +export type CodexJsonlEvent = Record + +export type CodexNativeToolEvent = + | { + kind: "tool-input" + callId: string + toolName: string + input: unknown + title?: string + } + | { + kind: "tool-output" + callId: string + output: unknown + toolName?: string + input?: unknown + title?: string + isError?: boolean + } + +export interface CodexExecResumeEventSummary { + nativeSessionId?: string + lastText?: string + usage?: Record + error?: string +} + +export interface RunCodexExecResumeBridgeInput { + sessionId: string + cwd: string + prompt: string + modelId?: string | null + permissionMode?: AgentPermissionMode | null + command?: string | null + includeJson?: boolean + skipGitRepoCheck?: boolean + runner?: CodexNativeCommandRunner + env?: NodeJS.ProcessEnv + abortSignal?: AbortSignal + onEvent?: (event: CodexJsonlEvent) => void + images?: CodexNativeImageAttachment[] | null +} + +export interface RunCodexExecStartBridgeInput { + cwd: string + prompt: string + modelId?: string | null + permissionMode?: AgentPermissionMode | null + command?: string | null + includeJson?: boolean + skipGitRepoCheck?: boolean + runner?: CodexNativeCommandRunner + env?: NodeJS.ProcessEnv + abortSignal?: AbortSignal + onEvent?: (event: CodexJsonlEvent) => void + images?: CodexNativeImageAttachment[] | null +} + +export interface RunCodexExecBridgeInput + extends Omit { + action: "start" | "resume" + sessionId?: string | null + runner?: CodexNativeCommandRunner +} + +export interface CodexExecResumeBridgeResult + extends CodexNativeCommandRunnerResult, + CodexExecResumeEventSummary { + success: boolean + plan: CodexNativeSessionBridgePlan + events: CodexJsonlEvent[] +} + +function cleanString(value: string | null | undefined): string | undefined { + const trimmed = value?.trim() + return trimmed ? trimmed : undefined +} + +export function splitCodexTextForStreamingDeltas( + text: string, + maxChunkLength = 36, +): string[] { + if (!text) return [] + if (maxChunkLength <= 0 || text.length <= maxChunkLength) return [text] + + const chunks: string[] = [] + let remaining = text + const minSoftBreakIndex = Math.max(1, Math.floor(maxChunkLength * 0.45)) + + while (remaining.length > maxChunkLength) { + const candidate = remaining.slice(0, maxChunkLength + 1) + let breakIndex = -1 + + for (let index = candidate.length - 1; index >= minSoftBreakIndex; index -= 1) { + const char = candidate[index] + if (char && /[\s\n.,!?;:,。!?;:、]/.test(char)) { + breakIndex = index + 1 + break + } + } + + if (breakIndex <= 0) { + breakIndex = maxChunkLength + } + + chunks.push(remaining.slice(0, breakIndex)) + remaining = remaining.slice(breakIndex) + } + + if (remaining) chunks.push(remaining) + return chunks +} + +function requireCleanString( + value: string | null | undefined, + label: string, +): string { + const cleaned = cleanString(value) + if (!cleaned) { + throw new Error(`Codex native ${label} is required.`) + } + return cleaned +} + +function tomlString(value: string): string { + return JSON.stringify(value) +} + +function splitModelAndReasoning(modelId: string | undefined): { + modelId?: string + reasoningEffort?: string +} { + const cleaned = cleanString(modelId) + if (!cleaned) return {} + + const separatorIndex = cleaned.indexOf("/") + if (separatorIndex === -1) return { modelId: cleaned } + + const baseModel = cleanString(cleaned.slice(0, separatorIndex)) + const reasoningEffort = cleanString(cleaned.slice(separatorIndex + 1)) + return { + ...(baseModel ? { modelId: baseModel } : {}), + ...(reasoningEffort ? { reasoningEffort } : {}), + } +} + +function appendCodexModelArgs( + args: string[], + modelId: string | undefined, +): { + modelId?: string + modelReasoningEffort?: string +} { + const parsed = splitModelAndReasoning(modelId) + if (parsed.modelId) args.push("-m", parsed.modelId) + if (parsed.reasoningEffort) { + args.push("-c", `model_reasoning_effort=${tomlString(parsed.reasoningEffort)}`) + } + + return { + ...(parsed.modelId ? { modelId: parsed.modelId } : {}), + ...(parsed.reasoningEffort + ? { modelReasoningEffort: parsed.reasoningEffort } + : {}), + } +} + +function appendCodexExecPermissionArgs( + args: string[], + permissionMode: AgentPermissionMode, +): string[] { + if (permissionMode === "bypass") { + args.push("--dangerously-bypass-approvals-and-sandbox") + return [ + "Moss bypass maps to Codex dangerous approval and sandbox bypass.", + ] + } + + const sandboxMode = + permissionMode === "plan" ? "read-only" : "workspace-write" + args.push("-c", `sandbox_mode=${tomlString(sandboxMode)}`) + args.push("-c", `approval_policy=${tomlString("never")}`) + + return [ + permissionMode === "plan" + ? "Moss plan mode maps to Codex read-only sandbox for non-interactive resume." + : "Moss agent mode maps to Codex workspace-write sandbox for non-interactive resume.", + "Codex exec resume is non-interactive, so approvals are set to never.", + ] +} + +function cleanImagePaths(imagePaths: string[] | null | undefined): string[] { + return Array.from( + new Set( + (imagePaths ?? []) + .map((imagePath) => cleanString(imagePath)) + .filter((imagePath): imagePath is string => Boolean(imagePath)), + ), + ) +} + +function appendCodexImageArgs( + args: string[], + imagePaths: string[] | null | undefined, +): string[] { + const cleanedPaths = cleanImagePaths(imagePaths) + for (const imagePath of cleanedPaths) { + args.push("-i", imagePath) + } + return cleanedPaths +} + +function codexImageNotes(imagePaths: string[]): string[] { + if (imagePaths.length === 0) return [] + return [ + `Codex exec attaches ${imagePaths.length} image file(s) through native --image arguments.`, + "Moss materializes uploaded images as transient files and removes them after the native command exits.", + ] +} + +function appendCodexTuiPermissionArgs( + args: string[], + permissionMode: AgentPermissionMode, +): string[] { + if (permissionMode === "bypass") { + args.push("--dangerously-bypass-approvals-and-sandbox") + return [ + "Moss bypass maps to Codex dangerous approval and sandbox bypass.", + ] + } + + args.push("-s", permissionMode === "plan" ? "read-only" : "workspace-write") + args.push("-a", "on-request") + return [ + permissionMode === "plan" + ? "Moss plan mode maps to Codex read-only sandbox." + : "Moss agent mode maps to Codex workspace-write sandbox.", + "Codex native fork is TUI-backed, so interactive approvals remain available.", + ] +} + +function appendPromptArg( + args: string[], + prompt: string | null | undefined, + defaultSource: "stdin" | "none", + requestedSource?: CodexNativePromptSource, +): CodexNativePromptSource { + const cleanedPrompt = cleanString(prompt) + + if (requestedSource === "none") { + return "none" + } + + if (requestedSource === "stdin") { + args.push("-") + return "stdin" + } + + if (cleanedPrompt) { + args.push(cleanedPrompt) + return "argument" + } + + if (defaultSource === "stdin") { + args.push("-") + return "stdin" + } + + return "none" +} + +export function buildCodexNativeSessionBridgePlan( + input: BuildCodexNativeSessionBridgePlanInput, +): CodexNativeSessionBridgePlan { + const command = cleanString(input.command) ?? "codex" + const cwd = requireCleanString(input.cwd, "working directory") + const modelId = cleanString(input.modelId) + const permissionMode = input.permissionMode ?? "agent" + + if (input.action === "start") { + const args = ["exec"] + if (input.includeJson ?? true) args.push("--json") + if (input.skipGitRepoCheck ?? true) args.push("--skip-git-repo-check") + args.push("-C", cwd) + const modelArgs = appendCodexModelArgs(args, modelId) + const imagePaths = appendCodexImageArgs(args, input.imagePaths) + const notes = [ + ...appendCodexExecPermissionArgs(args, permissionMode), + ...codexImageNotes(imagePaths), + ] + const promptSource = appendPromptArg( + args, + input.prompt, + "stdin", + input.promptSource, + ) + + return { + engine: "codex", + action: "start", + bridge: "codex-exec-start", + mode: "headless-exec", + command, + args, + cwd, + ...modelArgs, + permissionMode, + promptSource, + imagePaths, + imageCount: imagePaths.length, + canRunHeadless: true, + notes, + } + } + + if (input.action === "resume") { + const sessionId = requireCleanString(input.sessionId, "session id") + const args = ["exec", "resume"] + if (input.includeJson ?? true) args.push("--json") + if (input.skipGitRepoCheck ?? true) args.push("--skip-git-repo-check") + const modelArgs = appendCodexModelArgs(args, modelId) + const imagePaths = appendCodexImageArgs(args, input.imagePaths) + const notes = [ + ...appendCodexExecPermissionArgs(args, permissionMode), + ...codexImageNotes(imagePaths), + ] + args.push(sessionId) + const promptSource = appendPromptArg( + args, + input.prompt, + "stdin", + input.promptSource, + ) + + return { + engine: "codex", + action: "resume", + bridge: "codex-exec-resume", + mode: "headless-exec", + command, + args, + cwd, + sessionId, + ...modelArgs, + permissionMode, + promptSource, + imagePaths, + imageCount: imagePaths.length, + canRunHeadless: true, + notes, + } + } + + const sessionId = requireCleanString(input.sessionId, "session id") + const args = ["fork", "--no-alt-screen", "-C", cwd] + const modelArgs = appendCodexModelArgs(args, modelId) + const requestedImagePaths = cleanImagePaths(input.imagePaths) + const notes = appendCodexTuiPermissionArgs(args, permissionMode) + args.push(sessionId) + const promptSource = appendPromptArg( + args, + input.prompt, + "none", + input.promptSource, + ) + + return { + engine: "codex", + action: "fork", + bridge: "codex-tui-fork", + mode: "native-tui", + command, + args, + cwd, + sessionId, + ...modelArgs, + permissionMode, + promptSource, + imagePaths: [], + imageCount: 0, + canRunHeadless: false, + notes: [ + ...notes, + "Codex fork is exposed by the native TUI command; no headless exec fork exists yet.", + ...(requestedImagePaths.length > 0 + ? ["Codex TUI fork image paths are not mapped because --image is only used on exec bridges."] + : []), + ], + } +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value) +} + +function eventPayload(event: CodexJsonlEvent): Record { + return isRecord(event.payload) ? event.payload : {} +} + +function eventItem(event: CodexJsonlEvent): Record { + const payload = eventPayload(event) + if (isRecord(payload.item)) return payload.item + if (isRecord(event.item)) return event.item + return {} +} + +function eventMessage(event: CodexJsonlEvent): Record { + const payload = eventPayload(event) + const item = eventItem(event) + if (isRecord(payload.message)) return payload.message + if (isRecord(item.message)) return item.message + if (isRecord(event.message)) return event.message + return {} +} + +function stringValue(value: unknown): string | undefined { + return typeof value === "string" ? cleanString(value) : undefined +} + +function parseJsonLikeValue(value: unknown): unknown { + if (typeof value !== "string") return value + const trimmed = value.trim() + if (!trimmed) return value + + try { + return JSON.parse(trimmed) + } catch { + return value + } +} + +function recordValue(value: unknown): Record | undefined { + const parsed = parseJsonLikeValue(value) + return isRecord(parsed) ? parsed : undefined +} + +function contentStringValue(value: unknown): string | undefined { + if (typeof value !== "string") return undefined + return value.trim() ? value : undefined +} + +function extractContentText(value: unknown): string | undefined { + if (typeof value === "string") return contentStringValue(value) + + if (Array.isArray(value)) { + const parts = value + .map((part) => extractContentText(part)) + .filter((part): part is string => Boolean(part)) + return cleanString(parts.join("")) + } + + if (!isRecord(value)) return undefined + + for (const key of ["text", "output_text", "delta"]) { + const text = contentStringValue(value[key]) + if (text) return text + } + + for (const key of ["content", "message"]) { + const text = extractContentText(value[key]) + if (text) return text + } + + return undefined +} + +function extractEventRole(event: CodexJsonlEvent): string | undefined { + const payload = eventPayload(event) + const item = eventItem(event) + const message = eventMessage(event) + return ( + stringValue(message.role) ?? + stringValue(item.role) ?? + stringValue(payload.role) ?? + stringValue(event.role) + ) +} + +function canUseEventAsAssistantTextSource(event: CodexJsonlEvent): boolean { + const payload = eventPayload(event) + const payloadType = stringValue(payload.type)?.toLowerCase() + if ((event as any)?.type === "event_msg" && payloadType === "user_message") { + return false + } + + const role = extractEventRole(event)?.toLowerCase() + return !role || role === "assistant" +} + +function extractEventPhase(event: CodexJsonlEvent): string | undefined { + const payload = eventPayload(event) + const item = eventItem(event) + const message = eventMessage(event) + return ( + stringValue(message.phase) ?? + stringValue(item.phase) ?? + stringValue(payload.phase) ?? + stringValue(event.phase) + ) +} + +function extractEventText(event: CodexJsonlEvent): string | undefined { + if (!canUseEventAsAssistantTextSource(event)) return undefined + + const payload = eventPayload(event) + const item = eventItem(event) + const message = eventMessage(event) + const candidates = [ + event.output_text, + payload.output_text, + item.output_text, + message.output_text, + event.text, + payload.text, + item.text, + message.text, + event.message, + payload.message, + item.message, + event.last_agent_message, + payload.last_agent_message, + item.last_agent_message, + event.delta, + payload.delta, + item.delta, + message.delta, + message.content, + item.content, + payload.content, + ] + + for (const candidate of candidates) { + const text = extractContentText(candidate) + if (text) return text + } + + return undefined +} + +function extractSessionId(event: CodexJsonlEvent): string | undefined { + const payload = eventPayload(event) + const item = eventItem(event) + const eventType = stringValue(event.type) + const payloadType = stringValue(payload.type) + const candidates = [ + event.nativeSessionId, + payload.nativeSessionId, + event.sessionId, + payload.sessionId, + event.session_id, + payload.session_id, + event.conversation_id, + payload.conversation_id, + event.threadId, + payload.threadId, + item.threadId, + event.thread_id, + payload.thread_id, + item.thread_id, + ] + + if (eventType === "session_meta" || payloadType === "session_meta") { + candidates.push(payload.id, event.id) + } + + for (const candidate of candidates) { + const sessionId = stringValue(candidate) + if (sessionId) return sessionId + } + + return undefined +} + +function extractUsage(event: CodexJsonlEvent): Record | undefined { + const payload = eventPayload(event) + const item = eventItem(event) + for (const candidate of [ + event.usage, + payload.usage, + item.usage, + event.token_usage, + payload.token_usage, + ]) { + if (isRecord(candidate)) return candidate + } + return undefined +} + +function extractError(event: CodexJsonlEvent): string | undefined { + const payload = eventPayload(event) + const item = eventItem(event) + const errorCandidates = [event.error, payload.error, item.error] + + for (const candidate of errorCandidates) { + const direct = stringValue(candidate) + if (direct) return direct + if (isRecord(candidate)) { + const message = stringValue(candidate.message) + if (message) return message + } + } + + return undefined +} + +function isDeltaTextEvent(event: CodexJsonlEvent): boolean { + const payload = eventPayload(event) + const item = eventItem(event) + const eventType = stringValue(event.type) + const payloadType = stringValue(payload.type) + const itemType = stringValue(item.type) + return [eventType, payloadType, itemType].some((type) => + type?.toLowerCase().includes("delta"), + ) +} + +function outputObject(params: { + output?: unknown + stdout?: unknown + stderr?: unknown + exitCode?: unknown + success?: unknown + status?: unknown + result?: unknown +}): Record { + const output: Record = {} + if (params.output !== undefined) output.output = params.output + if (typeof params.stdout === "string") output.stdout = params.stdout + if (typeof params.stderr === "string") output.stderr = params.stderr + if (typeof params.exitCode === "number" || params.exitCode === null) { + output.exitCode = params.exitCode + } + if (typeof params.success === "boolean") output.success = params.success + if (typeof params.status === "string") output.status = params.status + if (params.result !== undefined) output.result = params.result + return output +} + +function unwrapShellCommand(command: string): string { + const trimmed = command.trim() + const shellMatch = trimmed.match( + /^(?:\/(?:usr\/)?bin\/)?(?:zsh|bash|sh)\s+-lc\s+([\s\S]+)$/i, + ) + const wrappedCommand = shellMatch?.[1]?.trim() + if (!wrappedCommand) return trimmed + + const singleQuoted = wrappedCommand.match(/^'([\s\S]*)'$/) + if (singleQuoted) return singleQuoted[1].replace(/'\\''/g, "'") + + const doubleQuoted = wrappedCommand.match(/^"([\s\S]*)"$/) + if (doubleQuoted) return doubleQuoted[1].replace(/\\"/g, '"') + + return wrappedCommand +} + +function getFunctionCallInput( + payload: Record, +): Record { + const parsed = + recordValue(payload.arguments) ?? + recordValue(payload.input) ?? + recordValue(payload.args) ?? + {} + return { ...parsed } +} + +function getCommandInput( + input: Record, +): Record { + const command = + stringValue(input.cmd) ?? + stringValue(input.command) ?? + (Array.isArray(input.command) + ? [...input.command] + .reverse() + .find((entry): entry is string => typeof entry === "string" && entry.trim().length > 0) + : undefined) + + return { + ...input, + ...(command ? { command } : {}), + ...(stringValue(input.workdir) && !stringValue(input.cwd) + ? { cwd: stringValue(input.workdir) } + : {}), + } +} + +function firstChangedPath(changes: unknown): string | undefined { + if (!isRecord(changes)) return undefined + return Object.keys(changes).find((filePath) => filePath.trim().length > 0) +} + +function getPatchInput(rawInput: unknown): Record { + const patchText = typeof rawInput === "string" ? rawInput : undefined + const parsedInput = recordValue(rawInput) + return { + ...(parsedInput ?? {}), + ...(patchText ? { patch: patchText } : {}), + } +} + +function mapNativeFunctionNameToTool( + name: string, + input: Record, +): { toolName: string; input: unknown; title?: string } { + const normalizedName = name.replace(/^functions\./, "") + + if ( + normalizedName === "exec_command" || + normalizedName === "shell" || + normalizedName === "bash" || + normalizedName === "run_command" + ) { + const commandInput = getCommandInput(input) + const command = stringValue(commandInput.command) + return { + toolName: "Bash", + input: commandInput, + ...(command ? { title: `Run ${command}` } : {}), + } + } + + if ( + normalizedName === "apply_patch" || + normalizedName === "edit" || + normalizedName === "write_file" + ) { + return { + toolName: normalizedName === "write_file" ? "Write" : "Edit", + input, + } + } + + if (normalizedName === "read_file" || normalizedName === "read") { + return { toolName: "Read", input } + } + + if ( + normalizedName === "grep" || + normalizedName === "rg" || + normalizedName === "search" || + normalizedName === "search_code" + ) { + return { toolName: "Grep", input } + } + + if (normalizedName === "glob" || normalizedName === "list_files") { + return { toolName: "Glob", input } + } + + if (normalizedName.startsWith("mcp__")) { + return { toolName: normalizedName, input } + } + + return { + toolName: normalizedName, + input: { + ...input, + toolName: normalizedName, + }, + } +} + +function toolEventFromCommandExecutionItem( + event: CodexJsonlEvent, + item: Record, +): CodexNativeToolEvent | null { + const callId = + stringValue(item.id) ?? + stringValue(item.call_id) ?? + stringValue(item.callId) + const rawCommand = stringValue(item.command) + if (!callId || !rawCommand) return null + + const command = unwrapShellCommand(rawCommand) + const status = stringValue(item.status) + const eventType = stringValue(event.type) + const input: Record = { + command, + cmd: command, + ...(rawCommand !== command ? { rawCommand } : {}), + ...(status ? { executionStatus: status } : {}), + } + + if (eventType === "item.started" || status === "in_progress") { + return { + kind: "tool-input", + callId, + toolName: "Bash", + input, + title: `Run ${command}`, + } + } + + const exitCode = item.exit_code ?? item.exitCode + const output = outputObject({ + stdout: item.aggregated_output, + output: item.output, + stderr: item.stderr, + exitCode, + success: typeof exitCode === "number" ? exitCode === 0 : undefined, + status, + }) + + return { + kind: "tool-output", + callId, + toolName: "Bash", + input, + output, + isError: + status === "failed" || + status === "error" || + (typeof exitCode === "number" && exitCode !== 0), + title: `Run ${command}`, + } +} + +function toolEventFromResponseItem( + payload: Record, +): CodexNativeToolEvent | null { + const payloadType = stringValue(payload.type) + const callId = stringValue(payload.call_id) ?? stringValue(payload.callId) + + if (!callId) return null + + if (payloadType === "function_call") { + const name = stringValue(payload.name) ?? "unknown" + const mapped = mapNativeFunctionNameToTool(name, getFunctionCallInput(payload)) + return { + kind: "tool-input", + callId, + toolName: mapped.toolName, + input: mapped.input, + ...(mapped.title ? { title: mapped.title } : {}), + } + } + + if (payloadType === "function_call_output") { + const output = outputObject({ + output: payload.output, + stdout: payload.stdout, + stderr: payload.stderr, + exitCode: payload.exit_code ?? payload.exitCode, + success: payload.success, + status: payload.status, + }) + return { + kind: "tool-output", + callId, + output: Object.keys(output).length > 0 ? output : payload.output, + isError: payload.success === false, + } + } + + if (payloadType === "custom_tool_call") { + const name = stringValue(payload.name) ?? "unknown" + const rawInput = payload.input ?? payload.arguments + const input = + name === "apply_patch" + ? getPatchInput(rawInput) + : getFunctionCallInput({ ...payload, arguments: rawInput }) + const mapped = mapNativeFunctionNameToTool(name, input) + return { + kind: "tool-input", + callId, + toolName: mapped.toolName, + input: mapped.input, + ...(mapped.title ? { title: mapped.title } : {}), + } + } + + if (payloadType === "custom_tool_call_output") { + const output = outputObject({ + output: payload.output, + stdout: payload.stdout, + stderr: payload.stderr, + exitCode: payload.exit_code ?? payload.exitCode, + success: payload.success, + status: payload.status, + }) + return { + kind: "tool-output", + callId, + output: Object.keys(output).length > 0 ? output : payload.output, + isError: payload.success === false, + } + } + + if (payloadType === "tool_search_call") { + const args = recordValue(payload.arguments) ?? {} + const query = stringValue(args.query) + return { + kind: "tool-input", + callId, + toolName: "Grep", + input: { + ...args, + ...(query ? { pattern: query, query } : {}), + toolName: "Search tools", + }, + ...(query ? { title: `Search ${query}` } : {}), + } + } + + if (payloadType === "tool_search_output") { + return { + kind: "tool-output", + callId, + output: outputObject({ + output: payload.output ?? payload.tools, + success: payload.status === "completed" ? true : undefined, + status: payload.status, + }), + isError: payload.status === "failed", + } + } + + return null +} + +function toolEventFromEventMessage( + payload: Record, +): CodexNativeToolEvent | null { + const payloadType = stringValue(payload.type) + const callId = stringValue(payload.call_id) ?? stringValue(payload.callId) + + if (!callId) return null + + if (payloadType === "patch_apply_end") { + const filePath = firstChangedPath(payload.changes) + return { + kind: "tool-output", + callId, + toolName: "Edit", + input: { + ...(filePath ? { file_path: filePath } : {}), + changes: payload.changes, + }, + output: outputObject({ + stdout: payload.stdout, + stderr: payload.stderr, + success: payload.success, + status: payload.success === false ? "failed" : "completed", + result: payload.changes, + }), + isError: payload.success === false, + } + } + + if (payloadType === "mcp_tool_call_end") { + const invocation = isRecord(payload.invocation) ? payload.invocation : {} + const server = stringValue(invocation.server) ?? "mcp" + const tool = stringValue(invocation.tool) ?? "tool" + const result = isRecord(payload.result) ? payload.result : payload.result + const isError = + isRecord(payload.result) && payload.result.isError === true + ? true + : payload.status === "failed" + return { + kind: "tool-output", + callId, + toolName: `mcp__${server}__${tool}`, + input: isRecord(invocation.arguments) ? invocation.arguments : {}, + output: result, + isError, + } + } + + return null +} + +const nativeResponseItemToolTypes = new Set([ + "function_call", + "function_call_output", + "custom_tool_call", + "custom_tool_call_output", + "tool_search_call", + "tool_search_output", +]) + +const nativeEventMessageToolTypes = new Set([ + "patch_apply_end", + "mcp_tool_call_end", +]) + +export function codexJsonlEventToNativeToolEvent( + event: CodexJsonlEvent, +): CodexNativeToolEvent | null { + const payload = eventPayload(event) + const item = eventItem(event) + const candidates = [payload, item, event] + + if (stringValue(item.type) === "command_execution") { + const commandEvent = toolEventFromCommandExecutionItem(event, item) + if (commandEvent) return commandEvent + } + + for (const candidate of candidates) { + const candidateType = stringValue(candidate.type) + if (candidateType && nativeResponseItemToolTypes.has(candidateType)) { + return toolEventFromResponseItem(candidate) + } + } + + for (const candidate of candidates) { + const candidateType = stringValue(candidate.type) + if (candidateType && nativeEventMessageToolTypes.has(candidateType)) { + return toolEventFromEventMessage(candidate) + } + } + + return null +} + +export function parseCodexJsonlEventLine(line: string): CodexJsonlEvent | null { + const trimmed = line.trim() + if (!trimmed) return null + + try { + const parsed: unknown = JSON.parse(trimmed) + return isRecord(parsed) ? parsed : null + } catch { + // Keep the bridge tolerant of CLI warnings or partial output. + return null + } +} + +export function extractCodexJsonlEventText( + event: CodexJsonlEvent, +): string | undefined { + return extractEventText(event) +} + +export function isCodexJsonlCommentaryTextEvent( + event: CodexJsonlEvent, +): boolean { + if (!canUseEventAsAssistantTextSource(event)) return false + if (extractEventPhase(event)?.toLowerCase() !== "commentary") return false + return Boolean(extractEventText(event)) +} + +export function isCodexJsonlFinalTextEvent(event: CodexJsonlEvent): boolean { + if (!canUseEventAsAssistantTextSource(event)) return false + if (!extractEventText(event)) return false + + const payload = eventPayload(event) + const item = eventItem(event) + const phase = extractEventPhase(event)?.toLowerCase() + const payloadType = stringValue(payload.type)?.toLowerCase() + const itemType = stringValue(item.type)?.toLowerCase() + + return ( + phase === "final_answer" || + payloadType === "task_complete" || + itemType === "task_complete" + ) +} + +export function extractCodexJsonlEventSessionId( + event: CodexJsonlEvent, +): string | undefined { + return extractSessionId(event) +} + +export function isCodexJsonlDeltaTextEvent(event: CodexJsonlEvent): boolean { + return isDeltaTextEvent(event) +} + +export function parseCodexJsonlEvents(stdout: string): CodexJsonlEvent[] { + const events: CodexJsonlEvent[] = [] + + for (const line of stdout.split(/\r?\n/)) { + const event = parseCodexJsonlEventLine(line) + if (event) events.push(event) + } + + return events +} + +export function summarizeCodexExecResumeEvents( + events: CodexJsonlEvent[], +): CodexExecResumeEventSummary { + const summary: CodexExecResumeEventSummary = {} + let accumulatedDeltaText = "" + + for (const event of events) { + const sessionId = extractSessionId(event) + if (sessionId) summary.nativeSessionId = sessionId + const text = extractEventText(event) + if (text) { + if (isDeltaTextEvent(event)) { + accumulatedDeltaText += text + summary.lastText = accumulatedDeltaText + } else { + accumulatedDeltaText = "" + summary.lastText = text + } + } + const usage = extractUsage(event) + if (usage) summary.usage = usage + const error = extractError(event) + if (error) summary.error = error + } + + return summary +} + +function stripDataUrlPrefix(value: string): string { + const trimmed = value.trim() + const dataUrlMatch = trimmed.match(/^data:[^,]+;base64,(.*)$/is) + return (dataUrlMatch ? dataUrlMatch[1] : trimmed).replace(/\s/g, "") +} + +function codexImageExtension(input: CodexNativeImageAttachment): string { + const filenameExtension = input.filename + ? path.extname(path.basename(input.filename)).toLowerCase() + : "" + if ( + [ + ".png", + ".jpg", + ".jpeg", + ".webp", + ".gif", + ".bmp", + ".tif", + ".tiff", + ].includes(filenameExtension) + ) { + return filenameExtension + } + + const mediaType = cleanString(input.mediaType)?.toLowerCase() + switch (mediaType) { + case "image/png": + return ".png" + case "image/jpeg": + case "image/jpg": + return ".jpg" + case "image/webp": + return ".webp" + case "image/gif": + return ".gif" + case "image/bmp": + return ".bmp" + case "image/tiff": + return ".tiff" + default: + return ".img" + } +} + +async function materializeCodexImages( + images: CodexNativeImageAttachment[] | null | undefined, +): Promise<{ directory?: string; imagePaths: string[] }> { + const usableImages = (images ?? []).filter( + (image) => cleanString(image.base64Data) && cleanString(image.mediaType), + ) + if (usableImages.length === 0) return { imagePaths: [] } + + const directory = await mkdtemp(path.join(tmpdir(), "moss-codex-images-")) + const imagePaths: string[] = [] + + try { + for (const [index, image] of usableImages.entries()) { + const base64Payload = stripDataUrlPrefix(image.base64Data) + if (!base64Payload) continue + const imagePath = path.join( + directory, + `image-${String(index + 1).padStart(2, "0")}${codexImageExtension(image)}`, + ) + await writeFile(imagePath, Buffer.from(base64Payload, "base64")) + imagePaths.push(imagePath) + } + } catch (error) { + await rm(directory, { recursive: true, force: true }) + throw error + } + + if (imagePaths.length === 0) { + await rm(directory, { recursive: true, force: true }) + return { imagePaths: [] } + } + + return { directory, imagePaths } +} + +export function spawnCodexNativeCommand( + input: CodexNativeCommandRunnerInput, +): Promise { + return new Promise((resolve, reject) => { + if (input.abortSignal?.aborted) { + reject(new Error("Codex native command aborted.")) + return + } + + const child = spawn(input.command, input.args, { + cwd: input.cwd, + env: input.env ? { ...process.env, ...input.env } : process.env, + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, + }) + + const stdoutChunks: Buffer[] = [] + const stderrChunks: Buffer[] = [] + const stdoutDecoder = new StringDecoder("utf8") + let stdoutLineBuffer = "" + let forceKillTimer: ReturnType | null = null + let didClose = false + + const emitStdoutJsonLine = (line: string) => { + const event = parseCodexJsonlEventLine(line) + if (!event) return + + try { + input.onStdoutJsonEvent?.(event) + } catch (error) { + console.warn("[codex] Ignoring native JSONL stream callback error:", error) + } + } + + const processStdoutJsonText = (text: string) => { + if (!text) return + stdoutLineBuffer += text + + while (true) { + const lineEndIndex = stdoutLineBuffer.search(/\r?\n/) + if (lineEndIndex === -1) return + + const line = stdoutLineBuffer.slice(0, lineEndIndex) + const newlineLength = + stdoutLineBuffer[lineEndIndex] === "\r" && + stdoutLineBuffer[lineEndIndex + 1] === "\n" + ? 2 + : 1 + stdoutLineBuffer = stdoutLineBuffer.slice(lineEndIndex + newlineLength) + emitStdoutJsonLine(line) + } + } + + const flushStdoutJsonText = () => { + processStdoutJsonText(stdoutDecoder.end()) + if (stdoutLineBuffer.trim()) { + emitStdoutJsonLine(stdoutLineBuffer) + } + stdoutLineBuffer = "" + } + + const abortChild = () => { + if (didClose) return + child.kill("SIGTERM") + forceKillTimer = + forceKillTimer ?? + setTimeout(() => { + if (!didClose) child.kill("SIGKILL") + }, 2000) + } + + input.abortSignal?.addEventListener("abort", abortChild, { once: true }) + + child.stdout?.on("data", (chunk) => { + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)) + stdoutChunks.push(buffer) + processStdoutJsonText(stdoutDecoder.write(buffer)) + }) + child.stderr?.on("data", (chunk) => { + stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))) + }) + child.stdin?.on("error", () => { + // The CLI may exit before stdin is fully written on fast failures. + }) + child.on("error", reject) + child.on("close", (exitCode) => { + didClose = true + flushStdoutJsonText() + if (forceKillTimer) { + clearTimeout(forceKillTimer) + } + input.abortSignal?.removeEventListener("abort", abortChild) + resolve({ + stdout: Buffer.concat(stdoutChunks).toString("utf8"), + stderr: Buffer.concat(stderrChunks).toString("utf8"), + exitCode, + }) + }) + + if (typeof input.stdin === "string") { + child.stdin?.write(input.stdin) + } + child.stdin?.end() + }) +} + +export async function runCodexExecBridge( + input: RunCodexExecBridgeInput, +): Promise { + const prompt = requireCleanString(input.prompt, `${input.action} prompt`) + if (input.action === "resume") { + requireCleanString(input.sessionId, "session id") + } + const materializedImages = await materializeCodexImages(input.images) + const plan = buildCodexNativeSessionBridgePlan({ + action: input.action, + sessionId: input.sessionId, + cwd: input.cwd, + modelId: input.modelId, + permissionMode: input.permissionMode, + command: input.command, + includeJson: input.includeJson, + skipGitRepoCheck: input.skipGitRepoCheck, + prompt, + promptSource: "stdin", + imagePaths: materializedImages.imagePaths, + }) + const runner = input.runner ?? spawnCodexNativeCommand + const forwardedEventKeys = new Set() + const getForwardedEventKey = (event: CodexJsonlEvent): string => { + try { + return JSON.stringify(event) + } catch { + return String(event) + } + } + const forwardEvent = (event: CodexJsonlEvent) => { + if (!input.onEvent) return + const eventKey = getForwardedEventKey(event) + if (forwardedEventKeys.has(eventKey)) return + forwardedEventKeys.add(eventKey) + try { + input.onEvent(event) + } catch (error) { + console.warn("[codex] Ignoring native JSONL event callback error:", error) + } + } + let result: CodexNativeCommandRunnerResult + try { + result = await runner({ + command: plan.command, + args: plan.args, + cwd: plan.cwd, + stdin: prompt, + env: input.env, + abortSignal: input.abortSignal, + onStdoutJsonEvent: forwardEvent, + }) + } finally { + if (materializedImages.directory) { + await rm(materializedImages.directory, { recursive: true, force: true }) + } + } + const events = parseCodexJsonlEvents(result.stdout) + for (const event of events) { + forwardEvent(event) + } + const summary = summarizeCodexExecResumeEvents(events) + const exitError = + result.exitCode === 0 + ? undefined + : summary.error ?? cleanString(result.stderr) ?? `Codex exited with ${result.exitCode}.` + const error = exitError ?? summary.error + + return { + ...result, + plan, + events, + ...summary, + ...(error ? { error } : {}), + success: result.exitCode === 0 && !error, + } +} + +export async function runCodexExecStartBridge( + input: RunCodexExecStartBridgeInput, +): Promise { + return runCodexExecBridge({ + ...input, + action: "start", + }) +} + +export async function runCodexExecResumeBridge( + input: RunCodexExecResumeBridgeInput, +): Promise { + return runCodexExecBridge({ + ...input, + action: "resume", + }) +} diff --git a/src/main/lib/agent-runtime/control-plane.ts b/src/main/lib/agent-runtime/control-plane.ts new file mode 100644 index 000000000..883a83285 --- /dev/null +++ b/src/main/lib/agent-runtime/control-plane.ts @@ -0,0 +1,394 @@ +import { eq } from "drizzle-orm" +import { + chats, + getDatabase, + projects, + subChats, + type Chat, + type Project, + type SubChat, +} from "../db" +import { + readMossProjectionManifestSummary, + readMossProviderConfig, + resolveMossProviderForEngine, + summarizeMossProviderReadResult, + type MossProjectionManifestSummary, + type MossProviderSecretResolver, + type MossProviderSummary, +} from "../moss-source" +import { getAgentRuntimeManifest } from "./manifests" +import { + buildMossSessionActionPlan, + type MossSessionActionPlan, +} from "./session-actions" +import { AGENT_ENGINE_IDS, DEFAULT_AGENT_ENGINE_ID, type AgentEngineId } from "./types" + +export type MossSessionControlStatus = + | "ready" + | "missing-project" + | "missing-project-path" + +export type MossNativeSessionState = + | "linked" + | "pending-native-session" + +export type MossProjectionSessionState = + | "materialized" + | "native" + | "not-materialized" + | "unknown" + +export interface MossProviderRouteSummary { + status: "resolved" | "missing" | "unconfigured" | "parse-error" + providerId?: string + label?: string + mode?: string + model?: string + authMethod?: string + apiKeySource?: "inline" | "env" | "stored" + hasBaseUrl: boolean + baseUrlSource?: "inline" | "env" | "stored" + baseUrlEnv?: string + warnings: string[] + reason?: string + error?: string +} + +export interface MossSessionControlEntry { + chatId: string + chatName: string | null + workspacePath: string + branch: string | null + subChatId: string + subChatName: string | null + engine: AgentEngineId + modelId: string | null + nativeSessionId: string | null + configDir: string | null + permissionMode: string + mossManaged: boolean + nativeSessionLinked: boolean + nativeSessionState: MossNativeSessionState + projectionState: MossProjectionSessionState + messageCount: number + actions: MossSessionActionPlan["actions"] + providerId?: string + providerModel?: string + updatedAt: string | null + runtimeUpdatedAt: string | null +} + +export interface MossSessionControlPlane { + status: MossSessionControlStatus + projectPath: string | null + scopePath: string | null + projectId: string | null + projectName: string | null + summary: { + workspaces: number + sessions: number + mossManagedSessions: number + nativeSessionLinked: number + pendingNativeSessions: number + engines: Record + } + provider: MossProviderSummary + providerRoutes: Record + projectionManifest: MossProjectionManifestSummary + sessions: MossSessionControlEntry[] +} + +function emptyProviderSummary(projectPath: string | null): MossProviderSummary { + return { + status: "missing", + sourcePath: projectPath ? `${projectPath}/.moss/providers.yaml` : "", + providers: [], + } +} + +function emptyProjectionSummary( + projectPath: string | null, +): MossProjectionManifestSummary { + return { + status: "missing", + sourcePath: projectPath ? `${projectPath}/.moss/projections/manifest.json` : "", + totalEntries: 0, + engines: [], + } +} + +function emptyProviderRoutes(): Record { + const routes = {} as Record + for (const engineId of AGENT_ENGINE_IDS) { + routes[engineId] = { + status: "missing", + hasBaseUrl: false, + warnings: [], + reason: "No project path is selected.", + } + } + return routes +} + +function parseRuntimeMetadata(value: string | null): Record { + if (!value) return {} + try { + const parsed = JSON.parse(value) + return parsed && typeof parsed === "object" ? parsed as Record : {} + } catch { + return {} + } +} + +function toIsoString( + value: Date | string | number | null | undefined, +): string | null { + if (!value) return null + if (value instanceof Date) return value.toISOString() + const date = new Date(value) + return Number.isNaN(date.getTime()) ? null : date.toISOString() +} + +function normalizeEngine(value: string | null | undefined): AgentEngineId { + return AGENT_ENGINE_IDS.includes(value as AgentEngineId) + ? value as AgentEngineId + : DEFAULT_AGENT_ENGINE_ID +} + +function getProjectionState( + engine: AgentEngineId, + manifest: MossProjectionManifestSummary, +): MossProjectionSessionState { + if (engine === "hermes") return "native" + if (manifest.status !== "found") return "unknown" + const engineEntries = manifest.engines.find((entry) => entry.engineId === engine) + return engineEntries && engineEntries.entries > 0 + ? "materialized" + : "not-materialized" +} + +async function buildProviderRoutes( + projectPath: string, + secretResolver?: MossProviderSecretResolver, +): Promise> { + const entries = await Promise.all( + AGENT_ENGINE_IDS.map(async (engineId) => { + const resolved = await resolveMossProviderForEngine({ + projectPath, + engineId, + createIfMissing: true, + secretResolver, + }) + return [ + engineId, + { + status: resolved.status, + providerId: resolved.providerId, + label: resolved.label, + mode: resolved.mode, + model: resolved.model, + authMethod: resolved.authMethod, + apiKeySource: resolved.apiKeySource, + hasBaseUrl: Boolean(resolved.baseUrl), + baseUrlSource: resolved.baseUrlSource, + baseUrlEnv: resolved.baseUrlEnv, + warnings: resolved.warnings, + reason: resolved.reason, + error: resolved.error, + } satisfies MossProviderRouteSummary, + ] as const + }), + ) + return Object.fromEntries(entries) as Record< + AgentEngineId, + MossProviderRouteSummary + > +} + +function findProjectScope(projectPath: string): { + project: Project | null + scopePath: string + chats: Chat[] +} { + const db = getDatabase() + const project = db + .select() + .from(projects) + .where(eq(projects.path, projectPath)) + .get() + + if (project) { + return { + project, + scopePath: project.path, + chats: db + .select() + .from(chats) + .where(eq(chats.projectId, project.id)) + .all(), + } + } + + const worktreeChat = db + .select() + .from(chats) + .where(eq(chats.worktreePath, projectPath)) + .get() + if (!worktreeChat) { + return { + project: null, + scopePath: projectPath, + chats: [], + } + } + + const sourceProject = db + .select() + .from(projects) + .where(eq(projects.id, worktreeChat.projectId)) + .get() ?? null + + return { + project: sourceProject, + scopePath: projectPath, + chats: [worktreeChat], + } +} + +function listSubChatsForChat(chatId: string): SubChat[] { + return getDatabase() + .select() + .from(subChats) + .where(eq(subChats.chatId, chatId)) + .all() +} + +function buildEntries(params: { + projectPath: string + chats: Chat[] + projectionManifest: MossProjectionManifestSummary + providerRoutes: Record +}): MossSessionControlEntry[] { + const entries = params.chats.flatMap((chat) => + listSubChatsForChat(chat.id).map((subChat) => { + const engine = normalizeEngine(subChat.engine) + const metadata = parseRuntimeMetadata(subChat.runtimeMetadata) + const nativeSessionId = + subChat.engineSessionId ?? + (engine === "claude-code" ? subChat.sessionId : null) + const nativeSessionLinked = Boolean(nativeSessionId) + const manifest = getAgentRuntimeManifest(engine) + const actionPlan = buildMossSessionActionPlan({ + subChatId: subChat.id, + engine, + nativeSessionId, + messages: subChat.messages, + features: manifest.features, + }) + + return { + chatId: chat.id, + chatName: chat.name, + workspacePath: chat.worktreePath || params.projectPath, + branch: chat.branch, + subChatId: subChat.id, + subChatName: subChat.name, + engine, + modelId: subChat.modelId, + nativeSessionId, + configDir: subChat.engineConfigDir, + permissionMode: subChat.mode, + mossManaged: true, + nativeSessionLinked, + nativeSessionState: nativeSessionLinked + ? "linked" + : "pending-native-session", + projectionState: getProjectionState(engine, params.projectionManifest), + messageCount: actionPlan.messageCount, + actions: actionPlan.actions, + providerId: params.providerRoutes[engine]?.providerId, + providerModel: params.providerRoutes[engine]?.model, + updatedAt: toIsoString(subChat.updatedAt), + runtimeUpdatedAt: + typeof metadata.updatedAt === "string" ? metadata.updatedAt : null, + } satisfies MossSessionControlEntry + }), + ) + + return entries.sort((a, b) => + (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""), + ) +} + +function buildSummary( + chats: Chat[], + sessions: MossSessionControlEntry[], +): MossSessionControlPlane["summary"] { + const engines = Object.fromEntries( + AGENT_ENGINE_IDS.map((engineId) => [engineId, 0]), + ) as Record + for (const session of sessions) { + engines[session.engine] += 1 + } + + return { + workspaces: chats.length, + sessions: sessions.length, + mossManagedSessions: sessions.filter((session) => session.mossManaged).length, + nativeSessionLinked: sessions.filter((session) => session.nativeSessionLinked) + .length, + pendingNativeSessions: sessions.filter( + (session) => !session.nativeSessionLinked, + ).length, + engines, + } +} + +export async function getMossSessionControlPlane(params: { + projectPath?: string | null + secretResolver?: MossProviderSecretResolver +}): Promise { + const projectPath = params.projectPath?.trim() + if (!projectPath) { + return { + status: "missing-project-path", + projectPath: null, + scopePath: null, + projectId: null, + projectName: null, + summary: buildSummary([], []), + provider: emptyProviderSummary(null), + providerRoutes: emptyProviderRoutes(), + projectionManifest: emptyProjectionSummary(null), + sessions: [], + } + } + + const [providerRead, providerRoutes, projectionManifest] = await Promise.all([ + readMossProviderConfig(projectPath, { createIfMissing: true }), + buildProviderRoutes(projectPath, params.secretResolver), + readMossProjectionManifestSummary(projectPath), + ]) + const provider = summarizeMossProviderReadResult(providerRead) + const scope = findProjectScope(projectPath) + const sessions = buildEntries({ + projectPath, + chats: scope.chats, + projectionManifest, + providerRoutes, + }) + + return { + status: scope.project ? "ready" : "missing-project", + projectPath: scope.project?.path ?? projectPath, + scopePath: scope.scopePath, + projectId: scope.project?.id ?? null, + projectName: scope.project?.name ?? null, + summary: buildSummary(scope.chats, sessions), + provider, + providerRoutes, + projectionManifest, + sessions, + } +} diff --git a/src/main/lib/agent-runtime/events.ts b/src/main/lib/agent-runtime/events.ts new file mode 100644 index 000000000..77d3e520e --- /dev/null +++ b/src/main/lib/agent-runtime/events.ts @@ -0,0 +1,189 @@ +import type { + AgentRuntimeBlockStatus, + AgentRuntimeConversationBlock, + AgentRuntimeStreamEvent, +} from "./types" + +const CONVERSATION_BLOCK_TYPES = new Set([ + "exec", + "mcp-tool-call", + "patch", + "generated-image", + "text-output", + "todo-list", + "proposed-plan", + "active-goal", + "permission-request", + "user-input", + "status", + "dynamic-tool-call", + "automation-update", + "multi-agent-action", + "context-compaction", + "model-change", + "model-reroute", + "goal-status", + "realtime-state", + "dictation-state", + "queued-follow-up", + "rate-limit-status", + "usage-status", + "project-event", + "library-artifact", + "pull-request-status", + "diagnostic-snapshot", +]) + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value) +} + +function cleanString(value: unknown): string | undefined { + if (typeof value !== "string") return undefined + const trimmed = value.trim() + return trimmed.length > 0 ? trimmed : undefined +} + +function normalizeBlockStatus(value: unknown): AgentRuntimeBlockStatus | undefined { + if ( + value === "queued" || + value === "running" || + value === "completed" || + value === "failed" || + value === "interrupted" || + value === "blocked" + ) { + return value + } + + return undefined +} + +function normalizeRuntimeConversationBlock( + value: unknown, +): AgentRuntimeConversationBlock | null { + if (!isRecord(value)) return null + + const type = cleanString(value.type) + const id = cleanString(value.id) + if (!type || !id || !CONVERSATION_BLOCK_TYPES.has(type)) return null + + const { id: _id, type: _type, status: _status, ...rest } = value + const status = normalizeBlockStatus(_status) + + return { + ...rest, + id, + type, + ...(status ? { status } : {}), + } as AgentRuntimeConversationBlock +} + +function normalizeConversationBlockUpdate( + value: Record, +): AgentRuntimeStreamEvent | null { + const id = cleanString(value.id) + if (!id || !isRecord(value.patch)) return null + + const { status: _status, ...patch } = value.patch + const status = normalizeBlockStatus(_status) + + return { + type: "conversation-block-update", + id, + patch: { + ...patch, + ...(status ? { status } : {}), + } as Partial, + } +} + +export function normalizeRuntimeStreamEvent( + chunk: Record, +): AgentRuntimeStreamEvent | null { + if (chunk.type === "text" && typeof chunk.text === "string") { + return { type: "text", text: chunk.text } + } + + if (chunk.type === "tool-call") { + const name = cleanString(chunk.name) + if (!name) return null + return { + type: "tool-call", + id: cleanString(chunk.id), + name, + input: chunk.input, + } + } + + if (chunk.type === "tool-result") { + return { + type: "tool-result", + id: cleanString(chunk.id), + name: cleanString(chunk.name), + result: chunk.result, + } + } + + if (chunk.type === "conversation-block") { + const block = normalizeRuntimeConversationBlock(chunk.block) + return block ? { type: "conversation-block", block } : null + } + + if (chunk.type === "conversation-block-update") { + return normalizeConversationBlockUpdate(chunk) + } + + if (typeof chunk.type === "string" && CONVERSATION_BLOCK_TYPES.has(chunk.type)) { + const block = normalizeRuntimeConversationBlock(chunk) + return block ? { type: "conversation-block", block } : null + } + + if (chunk.type === "finish") { + return { + type: "finish", + nativeSessionId: + typeof chunk.sessionId === "string" ? chunk.sessionId : null, + resultSubtype: + chunk.resultSubtype === "error" || + chunk.resultSubtype === "cancelled" || + chunk.resultSubtype === "success" + ? chunk.resultSubtype + : undefined, + } + } + + if ( + chunk.type === "auth-error" || + (chunk.type === "error" && typeof chunk.errorText === "string") + ) { + return { + type: chunk.type === "auth-error" ? "auth-error" : "error", + message: String(chunk.errorText), + } + } + + if (chunk.type === "message-metadata") { + const metadata = chunk.messageMetadata + if (metadata && typeof metadata === "object") { + const value = metadata as Record + return { + type: "usage", + inputTokens: + typeof value.inputTokens === "number" ? value.inputTokens : undefined, + outputTokens: + typeof value.outputTokens === "number" + ? value.outputTokens + : undefined, + totalTokens: + typeof value.totalTokens === "number" ? value.totalTokens : undefined, + modelContextWindow: + typeof value.modelContextWindow === "number" + ? value.modelContextWindow + : undefined, + } + } + } + + return null +} diff --git a/src/main/lib/agent-runtime/hermes-native-session.ts b/src/main/lib/agent-runtime/hermes-native-session.ts new file mode 100644 index 000000000..742f4608f --- /dev/null +++ b/src/main/lib/agent-runtime/hermes-native-session.ts @@ -0,0 +1,286 @@ +import { spawn } from "node:child_process" +import type { AgentPermissionMode } from "./types" + +export type HermesNativeBridgeAction = "resume" | "fork" | "rollback" + +export type HermesNativeBridgeKind = + | "hermes-cli-resume" + | "hermes-acp-session-control" + +export type HermesNativeBridgeMode = + | "headless-cli" + | "moss-owned-session-control" + +export type HermesNativePromptSource = "argument" | "none" + +export type HermesNativeSessionStrategy = + | "resume-cli-session" + | "reuse-session-with-moss-fork-boundary" + | "reuse-session-with-moss-rollback-boundary" + +export interface HermesNativeSessionBridgePlan { + engine: "hermes" + action: HermesNativeBridgeAction + bridge: HermesNativeBridgeKind + mode: HermesNativeBridgeMode + nativeSessionStrategy: HermesNativeSessionStrategy + command: string + args: string[] + cwd: string + sessionId: string + modelId?: string + permissionMode: AgentPermissionMode + promptSource: HermesNativePromptSource + canRunHeadless: boolean + mossOwnedControl: true + targetMessageId?: string + targetSdkMessageUuid?: string + notes: string[] +} + +export interface BuildHermesNativeSessionBridgePlanInput { + action: HermesNativeBridgeAction + sessionId?: string | null + cwd: string + modelId?: string | null + permissionMode?: AgentPermissionMode | null + prompt?: string | null + promptSource?: HermesNativePromptSource + command?: string | null + targetMessageId?: string | null + targetSdkMessageUuid?: string | null +} + +export interface HermesNativeCommandRunnerInput { + command: string + args: string[] + cwd: string + stdin?: string + env?: NodeJS.ProcessEnv + abortSignal?: AbortSignal +} + +export interface HermesNativeCommandRunnerResult { + stdout: string + stderr: string + exitCode: number | null +} + +export type HermesNativeCommandRunner = ( + input: HermesNativeCommandRunnerInput, +) => Promise + +export interface HermesCliResumeBridgeSummary { + nativeSessionId?: string + lastText?: string + error?: string +} + +export interface RunHermesCliResumeBridgeInput { + sessionId: string + cwd: string + prompt: string + modelId?: string | null + permissionMode?: AgentPermissionMode | null + command?: string | null + runner?: HermesNativeCommandRunner + env?: NodeJS.ProcessEnv + abortSignal?: AbortSignal +} + +export interface HermesCliResumeBridgeResult + extends HermesNativeCommandRunnerResult, + HermesCliResumeBridgeSummary { + success: boolean + plan: HermesNativeSessionBridgePlan +} + +function cleanString(value: string | null | undefined): string | undefined { + const trimmed = value?.trim() + return trimmed ? trimmed : undefined +} + +function requireCleanString( + value: string | null | undefined, + label: string, +): string { + const cleaned = cleanString(value) + if (!cleaned) { + throw new Error(`Hermes native ${label} is required.`) + } + return cleaned +} + +export function buildHermesNativeSessionBridgePlan( + input: BuildHermesNativeSessionBridgePlanInput, +): HermesNativeSessionBridgePlan { + const command = cleanString(input.command) ?? "hermes" + const cwd = requireCleanString(input.cwd, "working directory") + const sessionId = requireCleanString(input.sessionId, "session id") + const modelId = cleanString(input.modelId) + const permissionMode = input.permissionMode ?? "agent" + + if (input.action === "resume") { + const args = ["--resume", sessionId] + const prompt = cleanString(input.prompt) + const promptSource = + input.promptSource === "none" || !prompt ? "none" : "argument" + if (promptSource === "argument" && prompt) { + args.push("-z", prompt) + } + + return { + engine: "hermes", + action: "resume", + bridge: "hermes-cli-resume", + mode: "headless-cli", + nativeSessionStrategy: "resume-cli-session", + command, + args, + cwd, + sessionId, + ...(modelId ? { modelId } : {}), + permissionMode, + promptSource, + canRunHeadless: true, + mossOwnedControl: true, + notes: [ + "Hermes exposes native resume through hermes --resume .", + "When a prompt is supplied, Moss can run a one-shot resume with -z; otherwise the plan records a resume-ready native session.", + ], + } + } + + const isFork = input.action === "fork" + const targetMessageId = cleanString(input.targetMessageId) + const targetSdkMessageUuid = cleanString(input.targetSdkMessageUuid) + + return { + engine: "hermes", + action: input.action, + bridge: "hermes-acp-session-control", + mode: "moss-owned-session-control", + nativeSessionStrategy: isFork + ? "reuse-session-with-moss-fork-boundary" + : "reuse-session-with-moss-rollback-boundary", + command, + args: [], + cwd, + sessionId, + ...(modelId ? { modelId } : {}), + permissionMode, + promptSource: "none", + canRunHeadless: true, + mossOwnedControl: true, + ...(targetMessageId ? { targetMessageId } : {}), + ...(targetSdkMessageUuid ? { targetSdkMessageUuid } : {}), + notes: [ + "The live Hermes CLI does not expose separate fork or rollback commands.", + "Moss keeps the Hermes native session linked and records the fork/rollback boundary in the Moss-owned session-control layer instead of creating a second real config.", + ], + } +} + +export function spawnHermesNativeCommand( + input: HermesNativeCommandRunnerInput, +): Promise { + return new Promise((resolve, reject) => { + if (input.abortSignal?.aborted) { + reject(new Error("Hermes native command aborted.")) + return + } + + const child = spawn(input.command, input.args, { + cwd: input.cwd, + env: input.env ? { ...process.env, ...input.env } : process.env, + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, + }) + + const stdoutChunks: Buffer[] = [] + const stderrChunks: Buffer[] = [] + let forceKillTimer: ReturnType | null = null + let didClose = false + + const abortChild = () => { + if (didClose) return + child.kill("SIGTERM") + forceKillTimer = + forceKillTimer ?? + setTimeout(() => { + if (!didClose) child.kill("SIGKILL") + }, 2000) + } + + input.abortSignal?.addEventListener("abort", abortChild, { once: true }) + + child.stdout?.on("data", (chunk) => { + stdoutChunks.push( + Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)), + ) + }) + child.stderr?.on("data", (chunk) => { + stderrChunks.push( + Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)), + ) + }) + child.stdin?.on("error", () => { + // Hermes may exit before stdin is fully written on fast failures. + }) + child.on("error", reject) + child.on("close", (exitCode) => { + didClose = true + if (forceKillTimer) clearTimeout(forceKillTimer) + input.abortSignal?.removeEventListener("abort", abortChild) + resolve({ + stdout: Buffer.concat(stdoutChunks).toString("utf8"), + stderr: Buffer.concat(stderrChunks).toString("utf8"), + exitCode, + }) + }) + + if (typeof input.stdin === "string") { + child.stdin?.write(input.stdin) + } + child.stdin?.end() + }) +} + +export async function runHermesCliResumeBridge( + input: RunHermesCliResumeBridgeInput, +): Promise { + const prompt = requireCleanString(input.prompt, "resume prompt") + const plan = buildHermesNativeSessionBridgePlan({ + action: "resume", + sessionId: input.sessionId, + cwd: input.cwd, + modelId: input.modelId, + permissionMode: input.permissionMode, + command: input.command, + prompt, + promptSource: "argument", + }) + const runner = input.runner ?? spawnHermesNativeCommand + const result = await runner({ + command: plan.command, + args: plan.args, + cwd: plan.cwd, + env: input.env, + abortSignal: input.abortSignal, + }) + const stdout = cleanString(result.stdout) + const stderr = cleanString(result.stderr) + const error = + result.exitCode === 0 + ? undefined + : stderr ?? stdout ?? `Hermes exited with ${result.exitCode}.` + + return { + ...result, + plan, + nativeSessionId: plan.sessionId, + ...(stdout ? { lastText: stdout } : {}), + ...(error ? { error } : {}), + success: result.exitCode === 0 && !error, + } +} diff --git a/src/main/lib/agent-runtime/index.ts b/src/main/lib/agent-runtime/index.ts new file mode 100644 index 000000000..0dd6de18c --- /dev/null +++ b/src/main/lib/agent-runtime/index.ts @@ -0,0 +1,10 @@ +export * from "./types" +export * from "./manifests" +export * from "./session-store" +export * from "./session-actions" +export * from "./session-records" +export * from "./codex-native-session" +export * from "./hermes-native-session" +export * from "./control-plane" +export * from "./adapters" +export * from "./events" diff --git a/src/main/lib/agent-runtime/manifests.ts b/src/main/lib/agent-runtime/manifests.ts new file mode 100644 index 000000000..9fa6645f2 --- /dev/null +++ b/src/main/lib/agent-runtime/manifests.ts @@ -0,0 +1,175 @@ +import * as os from "os" +import * as path from "path" +import type { + AgentEngineId, + AgentRuntimeManifest, +} from "./types" + +const home = os.homedir() + +export const AGENT_RUNTIME_MANIFESTS: Record = { + "claude-code": { + id: "claude-code", + label: "Claude Code", + vendor: "Anthropic", + availability: "available", + defaultModelId: "opus", + features: [ + "chat", + "resume", + "fork", + "rollback", + "mcp", + "agents", + "skills", + "commands", + "plugins", + "memory", + "images", + "usage", + "permissions", + "projects", + "library", + "pull-requests", + "follow-ups", + "rate-limits", + ], + configRoots: { + user: path.join(home, ".claude"), + project: ".claude", + sessions: "claude-sessions/", + }, + models: [ + { id: "opus", label: "Opus 4.6" }, + { id: "sonnet", label: "Sonnet 4.6" }, + { id: "haiku", label: "Haiku 4.5" }, + ], + }, + codex: { + id: "codex", + label: "OpenAI Codex", + vendor: "OpenAI", + availability: "available", + defaultModelId: "gpt-5.5/high", + features: [ + "chat", + "resume", + "mcp", + "plugins", + "memory", + "images", + "usage", + "permissions", + "projects", + "library", + "pull-requests", + "follow-ups", + "rate-limits", + "realtime-voice", + "dictation", + "diagnostics", + ], + configRoots: { + user: path.join(home, ".codex"), + project: ".codex", + sessions: path.join(home, ".codex", "sessions"), + }, + models: [ + { id: "gpt-5.5", label: "GPT 5.5" }, + { id: "gpt-5.4", label: "GPT 5.4" }, + { id: "gpt-5.4-mini", label: "GPT 5.4 Mini" }, + { id: "gpt-5.2", label: "GPT 5.2" }, + ], + notes: [ + "Codex currently uses ACP session resources plus ~/.codex/config.toml MCP configuration.", + "Native resume is prepared through codex exec resume; native fork is available as a TUI command plan until Codex exposes a headless fork bridge.", + "Agent, skill, and command projection must be rendered into prompt/context until native support is implemented.", + ], + }, + hermes: { + id: "hermes", + label: "Hermes", + vendor: "Moss / Hermes", + availability: "available", + defaultModelId: "moss-default", + features: [ + "chat", + "resume", + "fork", + "rollback", + "mcp", + "agents", + "skills", + "commands", + "plugins", + "memory", + "images", + "usage", + "permissions", + "projects", + "library", + "pull-requests", + "follow-ups", + "rate-limits", + "realtime-voice", + "dictation", + "diagnostics", + ], + configRoots: { + user: path.join(home, ".hermes"), + project: ".moss", + }, + models: [ + { id: "moss-default", label: "Moss Default" }, + ], + notes: [ + "Hermes is the native Moss core target and consumes .moss as its canonical project source.", + "The local Hermes CLI exposes an ACP server surface and uses the current Hermes runtime model by default.", + "Hermes native resume is planned through hermes --resume; fork and rollback stay Moss-owned because the live Hermes CLI does not expose separate fork or rollback commands.", + ], + }, + "custom-acp": { + id: "custom-acp", + label: "Custom ACP", + vendor: "User / ACP", + availability: "unsupported", + defaultModelId: "custom-acp", + features: [ + "chat", + "mcp", + "agents", + "skills", + "commands", + "plugins", + "memory", + "usage", + "permissions", + "projects", + "library", + "pull-requests", + "follow-ups", + "rate-limits", + ], + configRoots: { + user: path.join(home, ".moss", "custom-acp"), + project: ".moss/custom-acp", + sessions: ".moss/custom-acp/sessions/", + }, + models: [ + { id: "custom-acp", label: "Custom ACP Default" }, + ], + notes: [ + "Custom ACP is a governed external engine slot under Moss Unified Source.", + "Moss provider, resource, and projection settings can be prepared now; session start remains disabled until a custom ACP endpoint or command adapter is configured.", + "Shared skills, MCP, plugins, hooks, memory, and subagents are projected from .moss instead of maintained as a second real copy.", + ], + }, +} + +export function getAgentRuntimeManifest(engineId: AgentEngineId): AgentRuntimeManifest { + return AGENT_RUNTIME_MANIFESTS[engineId] +} + +export function listAgentRuntimeManifests(): AgentRuntimeManifest[] { + return Object.values(AGENT_RUNTIME_MANIFESTS) +} diff --git a/src/main/lib/agent-runtime/session-actions.ts b/src/main/lib/agent-runtime/session-actions.ts new file mode 100644 index 000000000..19c804098 --- /dev/null +++ b/src/main/lib/agent-runtime/session-actions.ts @@ -0,0 +1,598 @@ +import type { AgentEngineId, AgentRuntimeFeature } from "./types" + +export const MOSS_SESSION_ACTION_IDS = [ + "resume", + "fork", + "rollback", +] as const + +export type MossSessionActionId = (typeof MOSS_SESSION_ACTION_IDS)[number] + +export type MossSessionActionStatus = + | "ready" + | "unavailable" + | "unsupported" + | "needs-native-session" + | "needs-target" + +export type MossSessionActionMode = + | "native" + | "moss-transcript" + | "message-history" + +export type MossSessionNativeBridge = + | "claude-code-session" + | "codex-exec-resume" + | "hermes-cli-resume" + | "hermes-acp-session-control" + +export interface MossSessionMessage { + id?: string + role?: string + content?: unknown + parts?: unknown + metadata?: Record + [key: string]: unknown +} + +export interface MossSessionActionState { + status: MossSessionActionStatus + mode?: MossSessionActionMode + nativeBridge?: MossSessionNativeBridge + canRunHeadless?: boolean + reason?: string + targetMessageId?: string + targetSdkMessageUuid?: string + targetLabel?: string +} + +export interface MossSessionActionPlan { + subChatId: string + engine: AgentEngineId + messageCount: number + latestMessageId?: string + latestAssistantMessageId?: string + latestAssistantSdkMessageUuid?: string + rollbackTargetMessageId?: string + rollbackTargetSdkMessageUuid?: string + actions: Record +} + +export interface BuildMossSessionActionPlanInput { + subChatId: string + engine: AgentEngineId + nativeSessionId?: string | null + messages: string | MossSessionMessage[] | null | undefined + features?: AgentRuntimeFeature[] +} + +export interface MossForkSnapshot { + messages: MossSessionMessage[] + messageCount: number + forkAtSdkUuid: string | null + mode: MossSessionActionMode + nativeSessionLinked: boolean +} + +export interface BuildMossForkSnapshotInput { + engine: AgentEngineId + nativeSessionId?: string | null + messages: string | MossSessionMessage[] | null | undefined + features?: AgentRuntimeFeature[] + targetMessageId?: string + targetMessageIndex?: number +} + +export interface MossRollbackSnapshot { + messages: MossSessionMessage[] + messageCount: number + targetMessageId: string | null + targetSdkMessageUuid: string | null + mode: MossSessionActionMode + nativeSessionLinked: boolean +} + +export interface BuildMossRollbackSnapshotInput { + engine: AgentEngineId + nativeSessionId?: string | null + messages: string | MossSessionMessage[] | null | undefined + features?: AgentRuntimeFeature[] + targetMessageId?: string + targetSdkMessageUuid?: string +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value) +} + +function cleanMetadata( + value: unknown, + extra?: Record, + options?: { clearNativeSession?: boolean }, +): Record { + const metadata = isRecord(value) ? { ...value } : {} + delete metadata.shouldResume + delete metadata.shouldForkResume + if (options?.clearNativeSession) { + delete metadata.sessionId + delete metadata.sdkMessageUuid + } + return { + ...metadata, + ...(extra ?? {}), + } +} + +function messageId(message: MossSessionMessage): string | undefined { + return typeof message.id === "string" ? message.id : undefined +} + +function messageRole(message: MossSessionMessage): string | undefined { + return typeof message.role === "string" ? message.role : undefined +} + +function messageSdkUuid(message: MossSessionMessage): string | undefined { + const metadata = isRecord(message.metadata) ? message.metadata : {} + const value = metadata.sdkMessageUuid + return typeof value === "string" && value ? value : undefined +} + +function messageLabel(message: MossSessionMessage, fallback: string): string { + const parts = Array.isArray(message.parts) ? message.parts : [] + for (const part of parts) { + if (!isRecord(part)) continue + const text = part.text + if (typeof text === "string" && text.trim()) { + return text.trim().replace(/\s+/g, " ").slice(0, 80) + } + } + + if (typeof message.content === "string" && message.content.trim()) { + return message.content.trim().replace(/\s+/g, " ").slice(0, 80) + } + + return fallback +} + +function hasFeature( + features: AgentRuntimeFeature[] | undefined, + feature: AgentRuntimeFeature, +): boolean { + return !features || features.includes(feature) +} + +function findLastIndex( + values: T[], + predicate: (value: T, index: number) => boolean, +): number { + for (let index = values.length - 1; index >= 0; index -= 1) { + if (predicate(values[index], index)) return index + } + return -1 +} + +function supportsNativeForkBridge( + engine: AgentEngineId, + nativeSessionId: string | null | undefined, + features: AgentRuntimeFeature[] | undefined, + forkAtSdkUuid: string | null, +): boolean { + if (engine === "hermes") { + return Boolean(nativeSessionId && hasFeature(features, "fork")) + } + + return Boolean( + engine === "claude-code" && + nativeSessionId && + forkAtSdkUuid && + hasFeature(features, "fork"), + ) +} + +function supportsNativeRollbackBridge( + engine: AgentEngineId, + nativeSessionId: string | null | undefined, + features: AgentRuntimeFeature[] | undefined, + targetSdkUuid: string | null, + hasTarget: boolean, +): boolean { + if (engine === "hermes") { + return Boolean(nativeSessionId && hasTarget && hasFeature(features, "rollback")) + } + + return Boolean( + engine === "claude-code" && + nativeSessionId && + targetSdkUuid && + hasFeature(features, "rollback"), + ) +} + +export function parseMossSessionMessages( + value: string | MossSessionMessage[] | null | undefined, +): MossSessionMessage[] { + if (!value) return [] + let parsed: unknown + try { + parsed = typeof value === "string" ? JSON.parse(value || "[]") : value + } catch { + return [] + } + if (!Array.isArray(parsed)) return [] + return parsed.filter(isRecord) as MossSessionMessage[] +} + +export function buildMossSessionActionPlan( + input: BuildMossSessionActionPlanInput, +): MossSessionActionPlan { + const messages = parseMossSessionMessages(input.messages) + const latestMessage = messages[messages.length - 1] + const latestAssistantIndex = findLastIndex( + messages, + (message) => messageRole(message) === "assistant", + ) + const latestAssistant = + latestAssistantIndex >= 0 ? messages[latestAssistantIndex] : undefined + const latestAssistantWithSdkIndex = findLastIndex( + messages, + (message) => + messageRole(message) === "assistant" && Boolean(messageSdkUuid(message)), + ) + const latestAssistantWithSdk = + latestAssistantWithSdkIndex >= 0 + ? messages[latestAssistantWithSdkIndex] + : undefined + const rollbackTarget = latestAssistantWithSdk ?? latestAssistant ?? latestMessage + const rollbackTargetSdkUuid = rollbackTarget + ? messageSdkUuid(rollbackTarget) ?? null + : null + const rollbackTargetMessageId = rollbackTarget + ? messageId(rollbackTarget) ?? null + : null + const nativeSessionLinked = Boolean(input.nativeSessionId) + const latestAssistantSdkMessageUuid = latestAssistant + ? messageSdkUuid(latestAssistant) + : undefined + const forkAtSdkUuid = latestAssistant ? messageSdkUuid(latestAssistant) ?? null : null + const nativeFork = supportsNativeForkBridge( + input.engine, + input.nativeSessionId, + input.features, + forkAtSdkUuid, + ) + const nativeRollback = supportsNativeRollbackBridge( + input.engine, + input.nativeSessionId, + input.features, + rollbackTargetSdkUuid, + Boolean(rollbackTarget), + ) + + const resume: MossSessionActionState = !hasFeature(input.features, "resume") + ? { + status: "unsupported", + reason: `${input.engine} does not advertise native resume support.`, + } + : nativeSessionLinked + ? { + status: "ready", + mode: "native", + ...(input.engine === "codex" + ? { nativeBridge: "codex-exec-resume" as const } + : input.engine === "claude-code" + ? { nativeBridge: "claude-code-session" as const } + : input.engine === "hermes" + ? { nativeBridge: "hermes-cli-resume" as const } + : {}), + canRunHeadless: input.engine === "codex" || input.engine === "hermes", + reason: + input.engine === "codex" + ? "Codex native session id is linked through codex exec resume." + : input.engine === "hermes" + ? "Hermes native session id is linked through hermes --resume." + : "Native session id is linked.", + } + : messages.length > 0 + ? { + status: "needs-native-session", + mode: "message-history", + reason: "Transcript exists, but no native engine session has been linked yet.", + } + : { + status: "unavailable", + reason: "No transcript is available to resume.", + } + + const fork: MossSessionActionState = messages.length === 0 + ? { + status: "unavailable", + reason: "No transcript is available to fork.", + } + : nativeFork + ? { + status: "ready", + mode: "native", + nativeBridge: + input.engine === "hermes" + ? "hermes-acp-session-control" + : "claude-code-session", + targetMessageId: latestAssistant ? messageId(latestAssistant) : undefined, + targetSdkMessageUuid: forkAtSdkUuid ?? undefined, + targetLabel: latestAssistant + ? messageLabel(latestAssistant, "Latest assistant message") + : undefined, + reason: + input.engine === "hermes" + ? "Hermes fork stays linked to the Moss-owned Hermes session and starts from the selected Moss transcript boundary." + : "Claude Code native fork bridge can resume at the selected assistant turn.", + } + : { + status: "ready", + mode: "moss-transcript", + targetMessageId: latestMessage ? messageId(latestMessage) : undefined, + targetLabel: latestMessage + ? messageLabel(latestMessage, "Latest message") + : undefined, + reason: "Moss will clone the transcript and start a fresh native engine session.", + } + + const rollback: MossSessionActionState = messages.length === 0 + ? { + status: "unavailable", + reason: "No transcript is available to roll back.", + } + : !rollbackTarget + ? { + status: "needs-target", + reason: "No rollback target message was found.", + } + : nativeRollback + ? { + status: "ready", + mode: "native", + nativeBridge: + input.engine === "hermes" + ? "hermes-acp-session-control" + : "claude-code-session", + targetMessageId: rollbackTargetMessageId ?? undefined, + targetSdkMessageUuid: rollbackTargetSdkUuid ?? undefined, + targetLabel: messageLabel(rollbackTarget, "Rollback target"), + reason: + input.engine === "hermes" + ? "Hermes rollback stays linked to the Moss-owned Hermes session and truncates to the selected Moss transcript boundary." + : "Claude Code rollback can resume at the target assistant turn.", + } + : messages.length > 1 + ? { + status: "ready", + mode: "message-history", + targetMessageId: rollbackTargetMessageId ?? undefined, + targetSdkMessageUuid: rollbackTargetSdkUuid ?? undefined, + targetLabel: messageLabel(rollbackTarget, "Rollback target"), + reason: "Moss will truncate the transcript and clear stale native session ids.", + } + : { + status: "needs-target", + mode: "message-history", + reason: "At least two messages are needed for message-history rollback.", + } + + return { + subChatId: input.subChatId, + engine: input.engine, + messageCount: messages.length, + latestMessageId: latestMessage ? messageId(latestMessage) : undefined, + latestAssistantMessageId: latestAssistant + ? messageId(latestAssistant) + : undefined, + latestAssistantSdkMessageUuid, + rollbackTargetMessageId: rollbackTargetMessageId ?? undefined, + rollbackTargetSdkMessageUuid: rollbackTargetSdkUuid ?? undefined, + actions: { + resume, + fork, + rollback, + }, + } +} + +function resolveForkCutoffIndex( + messages: MossSessionMessage[], + targetMessageId: string | undefined, + targetMessageIndex: number | undefined, +): number { + if (messages.length === 0) return -1 + if (targetMessageId) { + const byId = messages.findIndex((message) => messageId(message) === targetMessageId) + if (byId >= 0) return byId + } + if ( + typeof targetMessageIndex === "number" && + targetMessageIndex >= 0 && + targetMessageIndex < messages.length + ) { + return targetMessageIndex + } + return messages.length - 1 +} + +function createForkMessageId(index: number): string { + return `fork-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 8)}` +} + +export function buildMossForkSnapshot( + input: BuildMossForkSnapshotInput, +): MossForkSnapshot { + const messages = parseMossSessionMessages(input.messages) + const cutoffIndex = resolveForkCutoffIndex( + messages, + input.targetMessageId, + input.targetMessageIndex, + ) + if (cutoffIndex < 0) { + throw new Error("No transcript is available to fork.") + } + + const messagesToFork = messages.slice(0, cutoffIndex + 1) + const lastAssistantIndex = findLastIndex( + messagesToFork, + (message) => messageRole(message) === "assistant", + ) + const lastAssistant = + lastAssistantIndex >= 0 ? messagesToFork[lastAssistantIndex] : undefined + const forkAtSdkUuid = lastAssistant ? messageSdkUuid(lastAssistant) ?? null : null + const nativeFork = supportsNativeForkBridge( + input.engine, + input.nativeSessionId, + input.features, + forkAtSdkUuid, + ) + + return { + messages: messagesToFork.map((message, index) => ({ + ...message, + id: createForkMessageId(index), + metadata: cleanMetadata( + message.metadata, + input.engine === "claude-code" && + nativeFork && + index === lastAssistantIndex && + forkAtSdkUuid + ? { shouldForkResume: true } + : undefined, + { clearNativeSession: !nativeFork }, + ), + })), + messageCount: messagesToFork.length, + forkAtSdkUuid, + mode: nativeFork ? "native" : "moss-transcript", + nativeSessionLinked: nativeFork, + } +} + +function resolveRollbackTargetIndex( + messages: MossSessionMessage[], + targetMessageId: string | undefined, + targetSdkMessageUuid: string | undefined, +): number { + if (messages.length === 0) return -1 + if (targetMessageId) { + const byId = messages.findIndex((message) => messageId(message) === targetMessageId) + if (byId >= 0) return byId + } + if (targetSdkMessageUuid) { + const bySdkUuid = messages.findIndex( + (message) => messageSdkUuid(message) === targetSdkMessageUuid, + ) + if (bySdkUuid >= 0) return bySdkUuid + } + const lastAssistantWithSdk = findLastIndex( + messages, + (message) => + messageRole(message) === "assistant" && Boolean(messageSdkUuid(message)), + ) + if (lastAssistantWithSdk >= 0) return lastAssistantWithSdk + const lastAssistant = findLastIndex( + messages, + (message) => messageRole(message) === "assistant", + ) + if (lastAssistant >= 0) return lastAssistant + return messages.length - 1 +} + +export function buildMossRollbackSnapshot( + input: BuildMossRollbackSnapshotInput, +): MossRollbackSnapshot { + const messages = parseMossSessionMessages(input.messages) + const targetIndex = resolveRollbackTargetIndex( + messages, + input.targetMessageId, + input.targetSdkMessageUuid, + ) + if (targetIndex < 0) { + throw new Error("No transcript is available to roll back.") + } + + const target = messages[targetIndex] + const targetSdkUuid = messageSdkUuid(target) ?? null + const nativeRollback = supportsNativeRollbackBridge( + input.engine, + input.nativeSessionId, + input.features, + targetSdkUuid, + true, + ) + const truncated = messages.slice(0, targetIndex + 1) + + return { + messages: truncated.map((message, index) => ({ + ...message, + metadata: cleanMetadata( + message.metadata, + input.engine === "claude-code" && + nativeRollback && + index === truncated.length - 1 + ? { shouldResume: true } + : undefined, + { clearNativeSession: !nativeRollback }, + ), + })), + messageCount: truncated.length, + targetMessageId: messageId(target) ?? null, + targetSdkMessageUuid: targetSdkUuid, + mode: nativeRollback ? "native" : "message-history", + nativeSessionLinked: nativeRollback, + } +} + +export function shouldIgnoreMossStoredMessageSessionIds( + runtimeMetadata: string | null | undefined, +): boolean { + if (!runtimeMetadata) return false + + let parsed: unknown + try { + parsed = JSON.parse(runtimeMetadata) + } catch { + return false + } + if (!isRecord(parsed) || !isRecord(parsed.mossSessionControl)) { + return false + } + + const control = parsed.mossSessionControl + const action = control.action + const mode = control.mode + if (mode === "native") return false + + return ( + (action === "fork" && mode === "moss-transcript") || + (action === "rollback" && mode === "message-history") + ) +} + +export function mergeMossSessionControlMetadata( + runtimeMetadata: string | null | undefined, + controlMetadata: Record, +): string { + let parsed: Record = {} + if (runtimeMetadata) { + try { + const value = JSON.parse(runtimeMetadata) + if (isRecord(value)) parsed = value + } catch { + parsed = {} + } + } + + return JSON.stringify({ + ...parsed, + mossSessionControl: { + ...(isRecord(parsed.mossSessionControl) + ? parsed.mossSessionControl + : {}), + ...controlMetadata, + updatedAt: new Date().toISOString(), + }, + }) +} diff --git a/src/main/lib/agent-runtime/session-records.ts b/src/main/lib/agent-runtime/session-records.ts new file mode 100644 index 000000000..f4b249002 --- /dev/null +++ b/src/main/lib/agent-runtime/session-records.ts @@ -0,0 +1,208 @@ +import { getAgentRuntimeManifest } from "./manifests" +import { + buildMossForkSnapshot, + buildMossRollbackSnapshot, + mergeMossSessionControlMetadata, + type MossForkSnapshot, + type MossRollbackSnapshot, +} from "./session-actions" +import { AGENT_ENGINE_IDS, DEFAULT_AGENT_ENGINE_ID, type AgentEngineId } from "./types" + +export interface MossSessionSubChatRecord { + id: string + chatId: string + name: string | null + mode: string + messages: string + sessionId: string | null + engine: string | null + engineSessionId: string | null + engineConfigDir: string | null + modelId: string | null + runtimeMetadata: string | null +} + +export interface MossForkSubChatInsertValues { + id: string + chatId: string + name: string + mode: string + messages: string + sessionId: string | null + engine: AgentEngineId + engineSessionId: string | null + engineConfigDir: string | null + modelId: string | null + runtimeMetadata: string +} + +export interface MossRollbackSubChatUpdateValues { + messages: string + sessionId: string | null + engineSessionId: string | null + runtimeMetadata: string + updatedAt: Date +} + +export interface BuildMossForkSubChatRecordInput { + sourceSubChat: MossSessionSubChatRecord + targetSubChatId: string + targetName: string + targetMessageId?: string + targetMessageIndex?: number + nativeBridgePlan?: unknown + forceTranscript?: boolean + fallbackReason?: string + metadata?: Record +} + +export interface BuildMossForkSubChatRecordResult { + engine: AgentEngineId + sourceNativeSessionId: string | null + snapshot: MossForkSnapshot + nativeSessionLinked: boolean + insertValues: MossForkSubChatInsertValues +} + +export interface BuildMossRollbackSubChatUpdateInput { + subChat: MossSessionSubChatRecord + targetMessageId?: string + targetSdkMessageUuid?: string + appliedGitCheckpoint?: boolean + nativeBridgePlan?: unknown + metadata?: Record +} + +export interface BuildMossRollbackSubChatUpdateResult { + engine: AgentEngineId + sourceNativeSessionId: string | null + snapshot: MossRollbackSnapshot + nativeSessionLinked: boolean + updateValues: MossRollbackSubChatUpdateValues +} + +export function normalizeMossSessionRecordEngine( + value: string | null | undefined, +): AgentEngineId { + return AGENT_ENGINE_IDS.includes(value as AgentEngineId) + ? value as AgentEngineId + : DEFAULT_AGENT_ENGINE_ID +} + +export function nativeSessionIdForMossSessionRecord( + subChat: Pick, + engine: AgentEngineId, +): string | null { + return subChat.engineSessionId ?? (engine === "claude-code" ? subChat.sessionId : null) +} + +export function buildMossForkSubChatRecord( + input: BuildMossForkSubChatRecordInput, +): BuildMossForkSubChatRecordResult { + const engine = normalizeMossSessionRecordEngine(input.sourceSubChat.engine) + const manifest = getAgentRuntimeManifest(engine) + const originalNativeSessionId = nativeSessionIdForMossSessionRecord( + input.sourceSubChat, + engine, + ) + const sourceNativeSessionId = input.forceTranscript + ? null + : originalNativeSessionId + const snapshot = buildMossForkSnapshot({ + engine, + nativeSessionId: sourceNativeSessionId, + messages: input.sourceSubChat.messages, + features: manifest.features, + targetMessageId: input.targetMessageId, + targetMessageIndex: input.targetMessageIndex, + }) + const nativeSessionLinked = snapshot.nativeSessionLinked + const runtimeMetadata = mergeMossSessionControlMetadata( + input.sourceSubChat.runtimeMetadata, + { + action: "fork", + mode: snapshot.mode, + sourceSubChatId: input.sourceSubChat.id, + sourceEngineSessionId: originalNativeSessionId, + nativeSessionLinked, + targetMessageId: input.targetMessageId ?? null, + targetMessageIndex: input.targetMessageIndex ?? null, + forkAtSdkUuid: snapshot.forkAtSdkUuid, + nativeBridgePlan: input.nativeBridgePlan, + ...(input.fallbackReason ? { fallbackReason: input.fallbackReason } : {}), + ...(input.metadata ?? {}), + }, + ) + + return { + engine, + sourceNativeSessionId, + snapshot, + nativeSessionLinked, + insertValues: { + id: input.targetSubChatId, + chatId: input.sourceSubChat.chatId, + name: input.targetName, + mode: input.sourceSubChat.mode, + messages: JSON.stringify(snapshot.messages), + sessionId: + nativeSessionLinked && engine === "claude-code" + ? input.sourceSubChat.sessionId + : null, + engine, + engineSessionId: nativeSessionLinked ? originalNativeSessionId : null, + engineConfigDir: input.sourceSubChat.engineConfigDir, + modelId: input.sourceSubChat.modelId, + runtimeMetadata, + }, + } +} + +export function buildMossRollbackSubChatUpdate( + input: BuildMossRollbackSubChatUpdateInput, +): BuildMossRollbackSubChatUpdateResult { + const engine = normalizeMossSessionRecordEngine(input.subChat.engine) + const manifest = getAgentRuntimeManifest(engine) + const sourceNativeSessionId = nativeSessionIdForMossSessionRecord( + input.subChat, + engine, + ) + const snapshot = buildMossRollbackSnapshot({ + engine, + nativeSessionId: sourceNativeSessionId, + messages: input.subChat.messages, + features: manifest.features, + targetMessageId: input.targetMessageId, + targetSdkMessageUuid: input.targetSdkMessageUuid, + }) + const nativeSessionLinked = snapshot.nativeSessionLinked + + return { + engine, + sourceNativeSessionId, + snapshot, + nativeSessionLinked, + updateValues: { + messages: JSON.stringify(snapshot.messages), + sessionId: + nativeSessionLinked && engine === "claude-code" + ? input.subChat.sessionId + : null, + engineSessionId: nativeSessionLinked ? sourceNativeSessionId : null, + runtimeMetadata: mergeMossSessionControlMetadata( + input.subChat.runtimeMetadata, + { + action: "rollback", + mode: snapshot.mode, + nativeSessionLinked, + targetMessageId: snapshot.targetMessageId, + targetSdkMessageUuid: snapshot.targetSdkMessageUuid, + appliedGitCheckpoint: Boolean(input.appliedGitCheckpoint), + nativeBridgePlan: input.nativeBridgePlan, + ...(input.metadata ?? {}), + }, + ), + updatedAt: new Date(), + }, + } +} diff --git a/src/main/lib/agent-runtime/session-store.ts b/src/main/lib/agent-runtime/session-store.ts new file mode 100644 index 000000000..1675fa703 --- /dev/null +++ b/src/main/lib/agent-runtime/session-store.ts @@ -0,0 +1,48 @@ +import { eq } from "drizzle-orm" +import { getDatabase, subChats } from "../db" +import type { AgentEngineId, AgentPermissionMode } from "./types" + +type RuntimeMetadata = Record + +export type PersistAgentRuntimeSessionInput = { + subChatId: string + engine: AgentEngineId + nativeSessionId?: string | null + configDir?: string | null + modelId?: string | null + permissionMode?: AgentPermissionMode + metadata?: RuntimeMetadata + updateLegacySessionId?: boolean +} + +function serializeMetadata( + input: PersistAgentRuntimeSessionInput, +): string { + return JSON.stringify({ + ...(input.metadata ?? {}), + ...(input.permissionMode ? { permissionMode: input.permissionMode } : {}), + updatedAt: new Date().toISOString(), + }) +} + +export function persistAgentRuntimeSession( + input: PersistAgentRuntimeSessionInput, +): void { + const db = getDatabase() + const values = { + engine: input.engine, + engineSessionId: input.nativeSessionId ?? null, + engineConfigDir: input.configDir ?? null, + modelId: input.modelId ?? null, + runtimeMetadata: serializeMetadata(input), + updatedAt: new Date(), + ...(input.updateLegacySessionId + ? { sessionId: input.nativeSessionId ?? null } + : {}), + } + + db.update(subChats) + .set(values) + .where(eq(subChats.id, input.subChatId)) + .run() +} diff --git a/src/main/lib/agent-runtime/types.ts b/src/main/lib/agent-runtime/types.ts new file mode 100644 index 000000000..8b0a19984 --- /dev/null +++ b/src/main/lib/agent-runtime/types.ts @@ -0,0 +1,294 @@ +import type { + CodexBlockStatus, + CodexConversationBlock, +} from "../../../shared/codex-tool-normalizer" + +export const AGENT_ENGINE_IDS = [ + "claude-code", + "codex", + "hermes", + "custom-acp", +] as const + +export type AgentEngineId = (typeof AGENT_ENGINE_IDS)[number] +export const DEFAULT_AGENT_ENGINE_ID: AgentEngineId = "hermes" + +export type AgentRuntimeAvailability = + | "available" + | "needs-auth" + | "not-installed" + | "unsupported" + | "error" + +export type AgentRuntimeFeature = + | "chat" + | "resume" + | "fork" + | "rollback" + | "mcp" + | "agents" + | "skills" + | "commands" + | "plugins" + | "memory" + | "images" + | "usage" + | "permissions" + | "projects" + | "library" + | "pull-requests" + | "follow-ups" + | "rate-limits" + | "realtime-voice" + | "dictation" + | "diagnostics" + +export type AgentPermissionMode = "plan" | "agent" | "bypass" + +export type AgentRuntimeAuthMethod = + | "oauth" + | "api-key" + | "shell-config" + | "not-authenticated" + | "unsupported" + | "unknown" + +export interface AgentRuntimeModel { + id: string + label: string +} + +export interface AgentRuntimeModelHealth extends AgentRuntimeModel { + availability: AgentRuntimeAvailability + reason?: string +} + +export interface AgentRuntimeHealth { + availability: AgentRuntimeAvailability + statusReason?: string + authMethod?: AgentRuntimeAuthMethod + models?: AgentRuntimeModelHealth[] +} + +export interface AgentRuntimeManifest { + id: AgentEngineId + label: string + vendor: string + availability: AgentRuntimeAvailability + features: AgentRuntimeFeature[] + defaultModelId?: string + models?: AgentRuntimeModel[] + configRoots: { + user?: string + project?: string + sessions?: string + } + notes?: string[] +} + +export interface AgentRuntimeSessionRef { + subChatId: string + chatId: string + engineId: AgentEngineId + nativeSessionId?: string | null + modelId?: string | null + permissionMode: AgentPermissionMode + cwd: string + projectPath?: string | null + runtimeConfigDir?: string | null + metadata?: Record +} + +export interface AgentRuntimeStartRequest { + session: AgentRuntimeSessionRef + prompt: string + images?: Array<{ + base64Data: string + mediaType: string + filename?: string + }> + forceNewSession?: boolean +} + +export type AgentRuntimeBlockStatus = CodexBlockStatus | "blocked" + +export type AgentRuntimeAutomationAction = + | "created" + | "updated" + | "deleted" + | "enabled" + | "disabled" + | "started" + | "completed" + | "failed" + | "paused" + | "resumed" + +export interface AgentRuntimeBaseConversationBlock { + id: string + type: string + turnId?: string + status?: AgentRuntimeBlockStatus + title?: string + summary?: string + input?: unknown + output?: unknown + metadata?: Record +} + +export interface AgentRuntimeAutomationUpdateBlock + extends AgentRuntimeBaseConversationBlock { + type: "automation-update" + automationId?: string + action?: AgentRuntimeAutomationAction | (string & {}) +} + +export interface AgentRuntimeMultiAgentActionBlock + extends AgentRuntimeBaseConversationBlock { + type: "multi-agent-action" + agentId?: string + agentLabel?: string + action?: "spawn" | "message" | "handoff" | "complete" | "failed" | (string & {}) +} + +export interface AgentRuntimeContextCompactionBlock + extends AgentRuntimeBaseConversationBlock { + type: "context-compaction" + previousInputTokens?: number + nextInputTokens?: number + droppedMessages?: number +} + +export interface AgentRuntimeModelChangeBlock + extends AgentRuntimeBaseConversationBlock { + type: "model-change" | "model-reroute" + fromModelId?: string + toModelId?: string + reason?: string +} + +export interface AgentRuntimeGoalStatusBlock + extends AgentRuntimeBaseConversationBlock { + type: "goal-status" + goalId?: string +} + +export interface AgentRuntimeRealtimeStateBlock + extends AgentRuntimeBaseConversationBlock { + type: "realtime-state" | "dictation-state" + mode?: "dictation" | "voice-only" | "voice_and_screen" | (string & {}) + microphoneDeviceId?: string +} + +export interface AgentRuntimeQueuedFollowUpBlock + extends AgentRuntimeBaseConversationBlock { + type: "queued-follow-up" + followUpId?: string + queueState?: "queued" | "sending" | "sent" | "failed" | "cancelled" | (string & {}) +} + +export interface AgentRuntimeUsageStatusBlock + extends AgentRuntimeBaseConversationBlock { + type: "rate-limit-status" | "usage-status" + window?: "hourly" | "daily" | "weekly" | "monthly" | "annual" | (string & {}) + remaining?: number + limit?: number + resetAt?: string +} + +export interface AgentRuntimeProjectEventBlock + extends AgentRuntimeBaseConversationBlock { + type: "project-event" + projectId?: string + projectName?: string + action?: "created" | "updated" | "pinned" | "unpinned" | "selected" | (string & {}) +} + +export interface AgentRuntimeLibraryArtifactBlock + extends AgentRuntimeBaseConversationBlock { + type: "library-artifact" + artifactId?: string + artifactKind?: "file" | "image" | "site" | "document" | "spreadsheet" | (string & {}) + path?: string + url?: string +} + +export interface AgentRuntimePullRequestStatusBlock + extends AgentRuntimeBaseConversationBlock { + type: "pull-request-status" + pullRequestId?: string + url?: string + reviewState?: "queued" | "running" | "changes-requested" | "approved" | "failed" | (string & {}) + checksState?: "pending" | "running" | "passing" | "failing" | "unknown" | (string & {}) +} + +export interface AgentRuntimeDiagnosticSnapshotBlock + extends AgentRuntimeBaseConversationBlock { + type: "diagnostic-snapshot" + snapshotKind?: "child-processes" | "renderer-memory" | "trace-recording" | (string & {}) + path?: string +} + +export type AgentRuntimeConversationBlock = + | CodexConversationBlock + | AgentRuntimeAutomationUpdateBlock + | AgentRuntimeMultiAgentActionBlock + | AgentRuntimeContextCompactionBlock + | AgentRuntimeModelChangeBlock + | AgentRuntimeGoalStatusBlock + | AgentRuntimeRealtimeStateBlock + | AgentRuntimeQueuedFollowUpBlock + | AgentRuntimeUsageStatusBlock + | AgentRuntimeProjectEventBlock + | AgentRuntimeLibraryArtifactBlock + | AgentRuntimePullRequestStatusBlock + | AgentRuntimeDiagnosticSnapshotBlock + +export type AgentRuntimeStreamEvent = + | { + type: "text" + text: string + } + | { + type: "tool-call" + id?: string + name: string + input?: unknown + } + | { + type: "tool-result" + id?: string + name?: string + result?: unknown + } + | { + type: "usage" + inputTokens?: number + outputTokens?: number + totalTokens?: number + modelContextWindow?: number + } + | { + type: "conversation-block" + block: AgentRuntimeConversationBlock + } + | { + type: "conversation-block-update" + id: string + patch: Partial + } + | { + type: "auth-error" | "error" + message: string + } + | { + type: "finish" + nativeSessionId?: string | null + resultSubtype?: "success" | "error" | "cancelled" + } + +export interface AgentRuntimeAdapter { + manifest: AgentRuntimeManifest + inspect?(session: AgentRuntimeSessionRef): Promise + canStart(session: AgentRuntimeSessionRef): Promise +} diff --git a/src/main/lib/codex-automations.test.ts b/src/main/lib/codex-automations.test.ts new file mode 100644 index 000000000..576424e9c --- /dev/null +++ b/src/main/lib/codex-automations.test.ts @@ -0,0 +1,168 @@ +import { mkdtemp, readFile, rm } from "fs/promises" +import { tmpdir } from "os" +import { join } from "path" +import { describe, expect, test } from "bun:test" + +import { + createLocalCodexAutomation, + deleteLocalCodexAutomation, + listLocalCodexAutomations, + parseCodexAutomationToml, + runLocalCodexAutomationNow, + updateLocalCodexAutomation, +} from "./codex-automations" + +async function useTempCodexHome() { + const tempCodexHome = await mkdtemp(join(tmpdir(), "1code-codex-automations-")) + process.env.CODEX_HOME = tempCodexHome + return tempCodexHome +} + +describe("local Codex automations", () => { + test("parses real Codex automation.toml shape", () => { + expect( + parseCodexAutomationToml(` +version = 1 +id = "1code" +kind = "cron" +name = "1Code 自动化对齐临时验收" +prompt = "临时验收任务" +status = "ACTIVE" +rrule = "FREQ=HOURLY;INTERVAL=1" +model = "gpt-5.5" +reasoning_effort = "low" +execution_environment = "local" +cwds = ["/Users/moss/Projects/1code"] +created_at = 1781687761313 +updated_at = 1781687761313 +`), + ).toMatchObject({ + id: "1code", + status: "ACTIVE", + reasoning_effort: "low", + execution_environment: "local", + cwds: ["/Users/moss/Projects/1code"], + }) + }) + + test("creates, lists, updates, runs, and deletes official-shaped TOML files", async () => { + const originalCodexHome = process.env.CODEX_HOME + const codexHome = await useTempCodexHome() + try { + const created = await createLocalCodexAutomation({ + name: "1Code 自动化对齐临时验收", + prompt: "检查当前工作区。", + status: "ACTIVE", + rrule: "FREQ=HOURLY;INTERVAL=1", + model: "gpt-5.5", + reasoningEffort: "low", + executionEnvironment: "local", + cwds: ["/Users/moss/Projects/1code"], + }) + + expect(created).toMatchObject({ + source: "codex-local", + name: "1Code 自动化对齐临时验收", + status: "ACTIVE", + engine: "codex", + reasoning_effort: "low", + execution_environment: "local", + }) + + const list = await listLocalCodexAutomations() + expect(list.map((automation) => automation.id)).toEqual([created.id]) + + await updateLocalCodexAutomation({ id: created.id, status: "PAUSED" }) + expect((await listLocalCodexAutomations())[0]).toMatchObject({ + id: created.id, + status: "PAUSED", + }) + + await runLocalCodexAutomationNow({ automationId: created.id }) + expect(typeof (await listLocalCodexAutomations())[0]?.last_run_at).toBe( + "number", + ) + + const toml = await readFile( + join(codexHome, "automations", created.id, "automation.toml"), + "utf8", + ) + expect(toml).toContain('execution_environment = "local"') + expect(toml).toContain('engine = "codex"') + expect(toml).toContain('reasoning_effort = "low"') + + await deleteLocalCodexAutomation({ id: created.id }) + expect(await listLocalCodexAutomations()).toEqual([]) + } finally { + if (originalCodexHome === undefined) { + delete process.env.CODEX_HOME + } else { + process.env.CODEX_HOME = originalCodexHome + } + await rm(codexHome, { force: true, recursive: true }) + } + }) + + test("defaults new local automations to the Hermes model", async () => { + const originalCodexHome = process.env.CODEX_HOME + const codexHome = await useTempCodexHome() + try { + const created = await createLocalCodexAutomation({ + name: "Hermes 默认自动化", + prompt: "确认自动化默认模型。", + }) + + expect(created).toMatchObject({ + model: "moss-default", + engine: "hermes", + source: "codex-local", + }) + + const toml = await readFile( + join(codexHome, "automations", created.id, "automation.toml"), + "utf8", + ) + expect(toml).toContain('model = "moss-default"') + expect(toml).toContain('engine = "hermes"') + } finally { + if (originalCodexHome === undefined) { + delete process.env.CODEX_HOME + } else { + process.env.CODEX_HOME = originalCodexHome + } + await rm(codexHome, { force: true, recursive: true }) + } + }) + + test("persists the selected Claude Code engine for local automations", async () => { + const originalCodexHome = process.env.CODEX_HOME + const codexHome = await useTempCodexHome() + try { + const created = await createLocalCodexAutomation({ + name: "Claude 自动化", + prompt: "用 Claude Code 检查当前工作区。", + model: "claude-sonnet", + }) + + expect(created).toMatchObject({ + model: "claude-sonnet", + engine: "claude-code", + source: "codex-local", + }) + + const toml = await readFile( + join(codexHome, "automations", created.id, "automation.toml"), + "utf8", + ) + expect(toml).toContain('model = "claude-sonnet"') + expect(toml).toContain('engine = "claude-code"') + } finally { + if (originalCodexHome === undefined) { + delete process.env.CODEX_HOME + } else { + process.env.CODEX_HOME = originalCodexHome + } + await rm(codexHome, { force: true, recursive: true }) + } + }) +}) diff --git a/src/main/lib/codex-automations.ts b/src/main/lib/codex-automations.ts new file mode 100644 index 000000000..0fb67b954 --- /dev/null +++ b/src/main/lib/codex-automations.ts @@ -0,0 +1,384 @@ +import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises" +import { homedir } from "os" +import { join, resolve, sep } from "path" +import { parseTOML, stringifyTOML } from "confbox/toml" + +export type LocalCodexAutomationRecord = Record & { + id: string + source: "codex-local" + sourcePath: string +} + +type AutomationInput = Record + +const AUTOMATION_FILE_NAME = "automation.toml" +const AUTOMATION_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/ +const HERMES_AUTOMATION_MODEL_ID = "moss-default" +const AUTOMATION_ENGINE_IDS = ["hermes", "codex", "claude-code"] as const +type LocalAutomationEngineId = (typeof AUTOMATION_ENGINE_IDS)[number] + +export function getCodexAutomationsRoot(): string { + const codexHome = expandHome(process.env.CODEX_HOME?.trim() || "~/.codex") + return join(codexHome, "automations") +} + +export async function listLocalCodexAutomations(): Promise< + LocalCodexAutomationRecord[] +> { + const root = getCodexAutomationsRoot() + let entries + try { + entries = await readdir(root, { withFileTypes: true }) + } catch (error) { + if (isMissingPathError(error)) return [] + throw error + } + + const records = await Promise.all( + entries + .filter((entry) => entry.isDirectory()) + .map((entry) => readLocalCodexAutomation(entry.name)), + ) + + return records + .filter((record): record is LocalCodexAutomationRecord => Boolean(record)) + .sort((a, b) => getAutomationTime(b) - getAutomationTime(a)) +} + +export async function createLocalCodexAutomation( + input: AutomationInput, +): Promise { + const now = Date.now() + const id = await reserveAutomationId(asString(input.id) || asString(input.name)) + const kind = asString(input.kind) || inferAutomationKind(input) + const prompt = asString(input.prompt) || asString(input.agentPrompt) + const executionEnvironment = + asString(input.executionEnvironment) || + asString(input.execution_environment) || + (kind === "heartbeat" ? null : "worktree") + const model = asString(input.model) || HERMES_AUTOMATION_MODEL_ID + const engine = normalizeAutomationEngine(input, model) + + const stored: Record = { + version: 1, + id, + kind, + name: asString(input.name) || "未命名自动化", + prompt, + status: asString(input.status) || "ACTIVE", + rrule: asString(input.rrule) || "FREQ=HOURLY;INTERVAL=1", + model, + engine, + reasoning_effort: + asString(input.reasoning_effort) || + asString(input.reasoningEffort) || + "high", + created_at: now, + updated_at: now, + } + + const cwds = asStringArray(input.cwds) + if (cwds.length > 0) stored.cwds = cwds + if (executionEnvironment) stored.execution_environment = executionEnvironment + + const targetThreadId = + asString(input.targetThreadId) || asString(input.target_thread_id) + if (targetThreadId) stored.target_thread_id = targetThreadId + + await writeAutomationById(id, stored) + const created = await readLocalCodexAutomation(id) + if (!created) throw new Error(`Failed to create automation ${id}`) + return created +} + +export async function updateLocalCodexAutomation( + input: AutomationInput, +): Promise { + const id = requireAutomationId(input) + const current = await readStoredAutomation(id) + if (!current) throw new Error(`Automation not found: ${id}`) + + const next: Record = { ...current } + if (typeof input.status === "string") next.status = input.status + if (typeof input.isEnabled === "boolean") { + next.status = input.isEnabled ? "ACTIVE" : "PAUSED" + } + copyStringField(input, next, "name") + copyStringField(input, next, "prompt") + copyStringField(input, next, "rrule") + copyStringField(input, next, "model") + copyStringField(input, next, "kind") + + const model = asString(next.model) || HERMES_AUTOMATION_MODEL_ID + const nextEngine = normalizeAutomationEngine({ ...next, ...input }, model) + if (nextEngine) next.engine = nextEngine + + const reasoning = asString(input.reasoning_effort) || asString(input.reasoningEffort) + if (reasoning) next.reasoning_effort = reasoning + const executionEnvironment = + asString(input.execution_environment) || asString(input.executionEnvironment) + if (executionEnvironment) next.execution_environment = executionEnvironment + const cwds = asStringArray(input.cwds) + if (cwds.length > 0) next.cwds = cwds + + next.updated_at = Date.now() + await writeAutomationById(id, next) + const updated = await readLocalCodexAutomation(id) + if (!updated) throw new Error(`Failed to update automation ${id}`) + return updated +} + +export async function deleteLocalCodexAutomation( + input: AutomationInput, +): Promise<{ ok: true; id: string }> { + const id = requireAutomationId(input) + const dir = resolveAutomationDir(id) + if (!dir) throw new Error(`Invalid automation id: ${id}`) + await rm(dir, { force: true, recursive: true }) + return { ok: true, id } +} + +export async function runLocalCodexAutomationNow( + input: AutomationInput, +): Promise { + const id = requireAutomationId(input) + const current = await readStoredAutomation(id) + if (!current) throw new Error(`Automation not found: ${id}`) + + await writeAutomationById(id, { + ...current, + last_run_at: Date.now(), + updated_at: Date.now(), + }) + + const updated = await readLocalCodexAutomation(id) + if (!updated) throw new Error(`Failed to run automation ${id}`) + return updated +} + +export function parseCodexAutomationToml(text: string): Record { + return parseTOML>(text) +} + +async function readLocalCodexAutomation( + id: string, +): Promise { + const sourcePath = resolveAutomationFile(id) + if (!sourcePath) return null + + try { + const text = await readFile(sourcePath, "utf8") + const parsed = parseCodexAutomationToml(text) + const parsedId = asString(parsed.id) || id + if (!AUTOMATION_ID_PATTERN.test(parsedId)) return null + return normalizeLocalRecord(parsed, parsedId, sourcePath) + } catch (error) { + if (isMissingPathError(error)) return null + console.warn("[codex-automations] Failed to read automation:", id, error) + return null + } +} + +async function readStoredAutomation( + id: string, +): Promise | null> { + const sourcePath = resolveAutomationFile(id) + if (!sourcePath) return null + try { + return parseCodexAutomationToml(await readFile(sourcePath, "utf8")) + } catch (error) { + if (isMissingPathError(error)) return null + throw error + } +} + +async function writeAutomationById( + id: string, + automation: Record, +): Promise { + const dir = resolveAutomationDir(id) + if (!dir) throw new Error(`Invalid automation id: ${id}`) + await mkdir(dir, { recursive: true }) + const filePath = join(dir, AUTOMATION_FILE_NAME) + await writeFile(filePath, stringifyTOML(orderAutomationFields(automation)), "utf8") +} + +async function reserveAutomationId(seed: string | null): Promise { + const base = slugAutomationId(seed || "automation") + let candidate = base + let suffix = 2 + while (await readLocalCodexAutomation(candidate)) { + candidate = `${base}-${suffix}` + suffix += 1 + } + return candidate +} + +function slugAutomationId(value: string): string { + const ascii = value + .normalize("NFKD") + .replace(/[^\w\s-]/g, "") + .trim() + .toLowerCase() + .replace(/[\s_]+/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, "") + return AUTOMATION_ID_PATTERN.test(ascii) ? ascii : `automation-${Date.now()}` +} + +function resolveAutomationFile(id: string): string | null { + const dir = resolveAutomationDir(id) + return dir ? join(dir, AUTOMATION_FILE_NAME) : null +} + +function resolveAutomationDir(id: string): string | null { + if (!AUTOMATION_ID_PATTERN.test(id)) return null + const root = resolve(getCodexAutomationsRoot()) + const dir = resolve(root, id) + return dir === root || !dir.startsWith(`${root}${sep}`) ? null : dir +} + +function normalizeLocalRecord( + raw: Record, + id: string, + sourcePath: string, +): LocalCodexAutomationRecord { + return { + ...raw, + id, + source: "codex-local", + sourcePath, + engine: normalizeAutomationEngine(raw, asString(raw.model) || HERMES_AUTOMATION_MODEL_ID), + hostName: raw.hostName ?? raw.host_name ?? "本机", + } +} + +function orderAutomationFields( + automation: Record, +): Record { + const ordered: Record = {} + const fieldOrder = [ + "version", + "id", + "kind", + "name", + "prompt", + "status", + "rrule", + "model", + "engine", + "reasoning_effort", + "execution_environment", + "target_thread_id", + "cwds", + "last_run_at", + "created_at", + "updated_at", + ] + + for (const key of fieldOrder) { + const value = automation[key] + if (value !== undefined && value !== null) ordered[key] = value + } + for (const [key, value] of Object.entries(automation)) { + if (!(key in ordered) && value !== undefined && value !== null) { + ordered[key] = value + } + } + return ordered +} + +function inferAutomationKind(input: AutomationInput): string { + return asString(input.targetThreadId) || asString(input.target_thread_id) + ? "heartbeat" + : "cron" +} + +function requireAutomationId(input: AutomationInput): string { + const id = asString(input.id) || asString(input.automationId) + if (!id || !AUTOMATION_ID_PATTERN.test(id)) { + throw new Error(`Invalid automation id: ${id ?? ""}`) + } + return id +} + +function getAutomationTime(automation: Record): number { + const value = + automation.updated_at ?? + automation.updatedAt ?? + automation.created_at ?? + automation.createdAt + if (typeof value === "number") return value + if (typeof value === "string") { + const parsed = new Date(value).getTime() + return Number.isNaN(parsed) ? 0 : parsed + } + return 0 +} + +function normalizeAutomationEngine( + input: AutomationInput, + model: string, +): LocalAutomationEngineId { + const rawEngine = + asString(input.engine) || + asString(input.agentEngineId) || + asString(input.agent_engine_id) || + asString(input.runtimeEngine) || + asString(input.runtime_engine) + if (isLocalAutomationEngineId(rawEngine)) return rawEngine + + const normalizedModel = model.trim().toLowerCase() + if ( + normalizedModel.includes("claude") || + normalizedModel.includes("sonnet") || + normalizedModel.includes("opus") + ) { + return "claude-code" + } + if (normalizedModel.startsWith("gpt-") || normalizedModel.includes("codex")) { + return "codex" + } + return "hermes" +} + +function isLocalAutomationEngineId( + value: string | null, +): value is LocalAutomationEngineId { + return value === "hermes" || value === "codex" || value === "claude-code" +} + +function copyStringField( + input: AutomationInput, + output: Record, + field: string, +): void { + const value = asString(input[field]) + if (value) output[field] = value +} + +function asString(value: unknown): string | null { + return typeof value === "string" && value.trim() ? value.trim() : null +} + +function asStringArray(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value + .map((item) => (typeof item === "string" ? item.trim() : "")) + .filter(Boolean) +} + +function expandHome(input: string): string { + if (input === "~") return homedir() + if (input.startsWith("~/")) return join(homedir(), input.slice(2)) + return input +} + +function isMissingPathError(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "code" in error && + (error as NodeJS.ErrnoException).code === "ENOENT" + ) +} diff --git a/src/main/lib/hermes/runtime.ts b/src/main/lib/hermes/runtime.ts new file mode 100644 index 000000000..15c9aad25 --- /dev/null +++ b/src/main/lib/hermes/runtime.ts @@ -0,0 +1,68 @@ +import * as fs from "fs" +import * as os from "os" +import * as path from "path" + +export type HermesRuntimeResolution = { + executable?: string + sourceRoot?: string + acpAdapterPath?: string + acpExecutable?: string +} + +export type HermesAcpLaunch = { + command: string + args: string[] +} + +function pathExists(filePath: string): boolean { + try { + fs.accessSync(filePath) + return true + } catch { + return false + } +} + +function compactCandidates(candidates: Array): string[] { + return candidates.filter((candidate): candidate is string => + Boolean(candidate?.trim()), + ) +} + +export function resolveHermesRuntime(): HermesRuntimeResolution { + const home = os.homedir() + const sourceRoot = path.join(home, ".hermes", "hermes-agent") + const acpAdapterPath = path.join(sourceRoot, "acp_adapter", "server.py") + + const executableCandidates = compactCandidates([ + process.env.HERMES_BIN, + path.join(home, ".local", "bin", "hermes"), + path.join(sourceRoot, "venv", "bin", "hermes"), + ]) + const acpExecutableCandidates = compactCandidates([ + process.env.HERMES_ACP_BIN, + path.join(home, ".local", "bin", "hermes-acp"), + path.join(sourceRoot, "venv", "bin", "hermes-acp"), + ]) + + return { + executable: executableCandidates.find(pathExists), + sourceRoot: pathExists(sourceRoot) ? sourceRoot : undefined, + acpAdapterPath: pathExists(acpAdapterPath) ? acpAdapterPath : undefined, + acpExecutable: acpExecutableCandidates.find(pathExists), + } +} + +export function resolveHermesAcpLaunch(): HermesAcpLaunch { + const runtime = resolveHermesRuntime() + + if (runtime.acpExecutable) { + return { command: runtime.acpExecutable, args: [] } + } + + if (runtime.executable) { + return { command: runtime.executable, args: ["acp"] } + } + + throw new Error("Hermes ACP executable was not found.") +} diff --git a/src/main/lib/mcp-stdio-compat.test.ts b/src/main/lib/mcp-stdio-compat.test.ts new file mode 100644 index 000000000..efbc65546 --- /dev/null +++ b/src/main/lib/mcp-stdio-compat.test.ts @@ -0,0 +1,239 @@ +import { describe, expect, test } from "bun:test" +import { resolveHostCompatibleMcpStdioConfig } from "./mcp-stdio-compat" + +describe("resolveHostCompatibleMcpStdioConfig", () => { + test("maps a Windows project root path through the source project path", () => { + const result = resolveHostCompatibleMcpStdioConfig( + { + command: "node", + args: ["D:\\Moss\\services\\mcp-apps-ui\\servers\\server.mjs", "--stdio"], + sourcePath: "/Users/moss/Codex/Moss", + }, + { + platform: "darwin", + exists: (targetPath) => + targetPath === "/Users/moss/Codex/Moss" || + targetPath === "/Users/moss/Codex/Moss/services/mcp-apps-ui/servers/server.mjs", + }, + ) + + expect(result.ok).toBe(true) + if (result.ok) { + expect(result.config.cwd).toBe("/Users/moss/Codex/Moss") + expect(result.config.args?.[0]).toBe( + "/Users/moss/Codex/Moss/services/mcp-apps-ui/servers/server.mjs", + ) + } + }) + + test("blocks unmapped Windows absolute paths on macOS", () => { + const result = resolveHostCompatibleMcpStdioConfig( + { + command: "node", + args: ["D:\\External\\server.mjs", "--stdio"], + sourcePath: "/Users/moss/Codex/Moss", + }, + { + platform: "darwin", + exists: (targetPath) => targetPath === "/Users/moss/Codex/Moss", + }, + ) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toContain("Windows path is not mapped") + } + }) + + test("skips a rewritten local Node script that is missing", () => { + const result = resolveHostCompatibleMcpStdioConfig( + { + command: "node", + args: ["D:\\Moss\\missing\\server.mjs", "--stdio"], + sourcePath: "/Users/moss/Codex/Moss", + }, + { + platform: "darwin", + exists: (targetPath) => targetPath === "/Users/moss/Codex/Moss", + }, + ) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toContain("Local stdio script does not exist") + } + }) + + test("skips a local Node script with unresolved bare dependencies", () => { + const result = resolveHostCompatibleMcpStdioConfig( + { + command: "node", + args: ["D:\\Moss\\servers\\premium\\index.mjs", "--stdio"], + sourcePath: "/Users/moss/Codex/Moss", + }, + { + platform: "darwin", + exists: (targetPath) => + targetPath === "/Users/moss/Codex/Moss" || + targetPath === "/Users/moss/Codex/Moss/servers/premium/index.mjs", + readFile: () => + 'import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";', + canResolve: () => false, + }, + ) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toContain("Local stdio script dependency is not installed") + expect(result.reason).toContain("@modelcontextprotocol/sdk") + } + }) + + test("skips node commands that do not name a stdio server script", () => { + const result = resolveHostCompatibleMcpStdioConfig( + { + command: "node", + args: ["--version"], + sourcePath: "/Users/moss/Movies/Videos", + }, + { + platform: "darwin", + exists: (targetPath) => targetPath === "/Users/moss/Movies/Videos", + }, + ) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toContain("no entry script") + } + }) + + test("skips a missing relative Node script resolved against cwd", () => { + const result = resolveHostCompatibleMcpStdioConfig( + { + command: "node", + args: ["./mcp/server.mjs"], + cwd: "/Users/moss/Projects/1code", + }, + { + platform: "darwin", + exists: (targetPath) => targetPath === "/Users/moss/Projects/1code", + }, + ) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toContain("Local stdio script does not exist") + expect(result.reason).toContain("/Users/moss/Projects/1code/mcp/server.mjs") + } + }) + + test("skips a relative Node script when no cwd or source path is available", () => { + const result = resolveHostCompatibleMcpStdioConfig( + { + command: "node", + args: ["./mcp/server.mjs"], + }, + { + platform: "darwin", + exists: () => true, + }, + ) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toContain("Relative stdio script requires cwd") + expect(result.reason).toContain("./mcp/server.mjs") + } + }) + + test("uses cwd for dependency checks on an existing relative Node script", () => { + const result = resolveHostCompatibleMcpStdioConfig( + { + command: "node", + args: ["./mcp/server.mjs"], + cwd: "/Users/moss/Projects/1code", + }, + { + platform: "darwin", + exists: (targetPath) => + targetPath === "/Users/moss/Projects/1code" || + targetPath === "/Users/moss/Projects/1code/mcp/server.mjs", + readFile: () => 'import { Server } from "@modelcontextprotocol/sdk/server/index.js";', + canResolve: () => false, + }, + ) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toContain("Local stdio script dependency is not installed") + expect(result.reason).toContain("@modelcontextprotocol/sdk") + } + }) + + test("uses source path to resolve relative cwd before launching a local command", () => { + const result = resolveHostCompatibleMcpStdioConfig( + { + command: "./mcp/server", + args: ["--stdio"], + cwd: ".", + sourcePath: "/Users/moss/Projects/1code", + }, + { + platform: "darwin", + exists: (targetPath) => + targetPath === "/Users/moss/Projects/1code" || + targetPath === "/Users/moss/Projects/1code/mcp/server", + }, + ) + + expect(result.ok).toBe(true) + if (result.ok) { + expect(result.config.cwd).toBe("/Users/moss/Projects/1code") + } + }) + + test("skips a missing relative app command before the stdio transport spawns it", () => { + const result = resolveHostCompatibleMcpStdioConfig( + { + command: "./Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient", + args: ["mcp"], + cwd: "/Users/moss/.codex/plugins/cache/openai-bundled/computer-use/1.0.809", + }, + { + platform: "darwin", + exists: (targetPath) => + targetPath === "/Users/moss/.codex/plugins/cache/openai-bundled/computer-use/1.0.809", + }, + ) + + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.reason).toContain("Local stdio command does not exist") + expect(result.reason).toContain("SkyComputerUseClient") + } + }) + + test("allows an existing relative app command to run from an absolute plugin cwd", () => { + const command = + "./Codex Computer Use.app/Contents/SharedSupport/SkyComputerUseClient.app/Contents/MacOS/SkyComputerUseClient" + const cwd = "/Users/moss/.codex/plugins/cache/openai-bundled/computer-use/1.0.809" + const result = resolveHostCompatibleMcpStdioConfig( + { + command, + args: ["mcp"], + cwd, + }, + { + platform: "darwin", + exists: (targetPath) => targetPath === cwd || targetPath === `${cwd}/${command.slice(2)}`, + }, + ) + + expect(result.ok).toBe(true) + if (result.ok) { + expect(result.config.command).toBe(command) + expect(result.config.cwd).toBe(cwd) + } + }) +}) diff --git a/src/main/lib/mcp-stdio-compat.ts b/src/main/lib/mcp-stdio-compat.ts new file mode 100644 index 000000000..1f37de204 --- /dev/null +++ b/src/main/lib/mcp-stdio-compat.ts @@ -0,0 +1,463 @@ +import { existsSync, readFileSync } from "fs" +import { createRequire } from "module" +import * as path from "path" + +export interface McpStdioLaunchConfig { + command: string + args?: string[] + env?: Record + cwd?: string + sourcePath?: string | null +} + +export interface McpPathMapping { + from: string + to: string +} + +export type McpStdioCompatResult = + | { + ok: true + config: { + command: string + args?: string[] + env?: Record + cwd?: string + } + rewrites: McpPathMapping[] + } + | { + ok: false + reason: string + rewrites: McpPathMapping[] + } + +interface ResolveOptions { + platform?: NodeJS.Platform + pathMappings?: McpPathMapping[] + exists?: (targetPath: string) => boolean + readFile?: (targetPath: string) => string + canResolve?: (specifier: string, fromPath: string) => boolean +} + +const WINDOWS_ABSOLUTE_PATH = /^([A-Za-z]):[\\/]+(.+)$/ +const WINDOWS_WILDCARD_ROOT = /^\*:[\\/]+(.+)$/ +const NODE_OPTIONS_WITH_VALUE = new Set([ + "-e", + "--eval", + "-p", + "--print", + "-r", + "--require", + "--loader", + "--import", +]) +const LOCAL_SCRIPT_EXTENSIONS = /\.(?:[cm]?js|[cm]?ts|tsx)$/i + +function parseWindowsPath(value: string): { root: string; rest: string } | null { + const match = value.match(WINDOWS_ABSOLUTE_PATH) + if (!match) return null + + const restSegments = match[2] + .split(/[\\/]+/) + .map((segment) => segment.trim()) + .filter(Boolean) + + if (restSegments.length === 0) return null + + return { + root: restSegments[0], + rest: restSegments.slice(1).join("/"), + } +} + +function isWindowsAbsolutePath(value: string): boolean { + return WINDOWS_ABSOLUTE_PATH.test(value) +} + +function normalizeWindowsRoot(value: string): string | null { + const wildcard = value.match(WINDOWS_WILDCARD_ROOT) + if (wildcard) { + const root = wildcard[1].split(/[\\/]+/).find(Boolean) + return root ? root.toLowerCase() : null + } + + const parsed = parseWindowsPath(value) + if (!parsed) return null + return parsed.root.toLowerCase() +} + +function sourceRootMapping(sourcePath?: string | null): McpPathMapping | null { + if (!sourcePath) return null + const sourceBase = path.basename(sourcePath) + if (!sourceBase) return null + return { + from: `*:\\${sourceBase}`, + to: sourcePath, + } +} + +function rewriteWindowsPath( + value: string, + mappings: McpPathMapping[], +): { value: string; rewrite?: McpPathMapping } { + const parsed = parseWindowsPath(value) + if (!parsed) return { value } + + for (const mapping of mappings) { + const fromRoot = normalizeWindowsRoot(mapping.from) + if (!fromRoot || fromRoot !== parsed.root.toLowerCase()) continue + + const mapped = parsed.rest ? path.join(mapping.to, parsed.rest) : mapping.to + return { + value: mapped, + rewrite: { + from: value, + to: mapped, + }, + } + } + + return { value } +} + +function resolveString( + value: string, + mappings: McpPathMapping[], +): { value: string; rewrite?: McpPathMapping } { + return rewriteWindowsPath(value, mappings) +} + +function isNodeLikeCommand(command: string): boolean { + const commandName = path.basename(command).toLowerCase() + return ["node", "node.exe", "bun", "bun.exe"].includes(commandName) +} + +function looksLikeLocalScriptArg(value: string): boolean { + return path.isAbsolute(value) || + value.startsWith(".") || + value.includes("/") || + value.includes("\\") || + LOCAL_SCRIPT_EXTENSIONS.test(value) +} + +function findNodeScriptArg(args: string[] | undefined): string | null { + if (!args?.length) return null + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index] + if (!arg) continue + if (arg === "--") return args[index + 1] ?? null + if (NODE_OPTIONS_WITH_VALUE.has(arg)) { + index += 1 + continue + } + if ([...NODE_OPTIONS_WITH_VALUE].some((option) => arg.startsWith(`${option}=`))) { + continue + } + if (arg.startsWith("-")) continue + return arg + } + + return null +} + +function findRelativeNodeScriptWithoutCwd( + command: string, + args: string[] | undefined, + cwd: string | undefined, +): string | null { + if (!isNodeLikeCommand(command) || cwd) return null + + const scriptArg = findNodeScriptArg(args) + if (!scriptArg || !looksLikeLocalScriptArg(scriptArg)) return null + if (path.isAbsolute(scriptArg) || isWindowsAbsolutePath(scriptArg)) return null + + return scriptArg +} + +function resolveNodeScriptPath(scriptArg: string | null, cwd: string | undefined): string | null { + if (!scriptArg || !looksLikeLocalScriptArg(scriptArg)) return null + if (path.isAbsolute(scriptArg)) return scriptArg + if (!cwd) return null + return path.resolve(cwd, scriptArg) +} + +function looksLikeLocalCommand(value: string): boolean { + return path.isAbsolute(value) || + isWindowsAbsolutePath(value) || + value.startsWith(".") || + value.includes("/") || + value.includes("\\") +} + +function resolveLocalCommandPath(command: string, cwd: string | undefined): string | null { + if (!looksLikeLocalCommand(command)) return null + if (path.isAbsolute(command)) return command + if (isWindowsAbsolutePath(command)) return command + if (!cwd) return null + return path.resolve(cwd, command) +} + +function findRelativeLocalCommandWithoutCwd(command: string, cwd: string | undefined): string | null { + if (cwd || !looksLikeLocalCommand(command)) return null + if (path.isAbsolute(command) || isWindowsAbsolutePath(command)) return null + return command +} + +function findMissingLocalCommand( + command: string, + cwd: string | undefined, + exists: (targetPath: string) => boolean, +): string | null { + const commandPath = resolveLocalCommandPath(command, cwd) + if (!commandPath) return null + return exists(commandPath) ? null : commandPath +} + +function findMissingNodeScript( + command: string, + args: string[] | undefined, + cwd: string | undefined, + exists: (targetPath: string) => boolean, +): string | null { + if (!isNodeLikeCommand(command)) return null + + const scriptArg = findNodeScriptArg(args) + const scriptPath = resolveNodeScriptPath(scriptArg, cwd) + if (!scriptPath) return null + + return exists(scriptPath) ? null : scriptPath +} + +function findNodeLaunchScript( + command: string, + args: string[] | undefined, + cwd: string | undefined, + exists: (targetPath: string) => boolean, +): string | null { + if (!isNodeLikeCommand(command)) return null + + const scriptArg = findNodeScriptArg(args) + const scriptPath = resolveNodeScriptPath(scriptArg, cwd) + if (!scriptPath || !exists(scriptPath)) return null + + return scriptPath +} + +function isBareModuleSpecifier(specifier: string): boolean { + return !specifier.startsWith(".") && + !specifier.startsWith("/") && + !specifier.startsWith("node:") && + !isWindowsAbsolutePath(specifier) +} + +function resolveLocalModulePath( + fromPath: string, + specifier: string, + exists: (targetPath: string) => boolean, +): string | null { + const base = path.resolve(path.dirname(fromPath), specifier) + const candidates = [ + base, + `${base}.js`, + `${base}.mjs`, + `${base}.cjs`, + path.join(base, "index.js"), + path.join(base, "index.mjs"), + path.join(base, "index.cjs"), + ] + + return candidates.find((candidate) => exists(candidate)) ?? null +} + +function extractModuleSpecifiers(source: string): string[] { + const specifiers = new Set() + const patterns = [ + /\bimport\s+(?:[^'"]+\s+from\s*)?["']([^"']+)["']/g, + /\bexport\s+[^'"]+\s+from\s*["']([^"']+)["']/g, + /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g, + /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g, + ] + + for (const pattern of patterns) { + let match: RegExpExecArray | null + while ((match = pattern.exec(source))) { + specifiers.add(match[1]) + } + } + + return [...specifiers] +} + +function canResolveBareSpecifier(specifier: string, fromPath: string): boolean { + try { + createRequire(fromPath).resolve(specifier) + return true + } catch { + return false + } +} + +function findMissingNodeDependency( + entryPath: string, + options: Required>, + visited = new Set(), +): string | null { + if (visited.has(entryPath)) return null + if (visited.size >= 16) return null + visited.add(entryPath) + + let source: string + try { + source = options.readFile(entryPath) + } catch { + return null + } + + for (const specifier of extractModuleSpecifiers(source)) { + if (isBareModuleSpecifier(specifier)) { + if (!options.canResolve(specifier, entryPath)) { + return `${specifier} imported by ${entryPath}` + } + continue + } + + if (specifier.startsWith(".")) { + const localPath = resolveLocalModulePath(entryPath, specifier, options.exists) + if (!localPath) continue + const missing = findMissingNodeDependency(localPath, options, visited) + if (missing) return missing + } + } + + return null +} + +export function resolveHostCompatibleMcpStdioConfig( + config: McpStdioLaunchConfig, + options: ResolveOptions = {}, +): McpStdioCompatResult { + const platform = options.platform ?? process.platform + const exists = options.exists ?? existsSync + const readFile = options.readFile ?? ((targetPath: string) => readFileSync(targetPath, "utf-8")) + const canResolve = options.canResolve ?? canResolveBareSpecifier + const mappings = [ + ...(options.pathMappings ?? []), + ...(sourceRootMapping(config.sourcePath) ? [sourceRootMapping(config.sourcePath)!] : []), + ] + const rewrites: McpPathMapping[] = [] + + const command = resolveString(config.command, mappings) + if (command.rewrite) rewrites.push(command.rewrite) + + const args = config.args?.map((arg) => { + const resolved = resolveString(arg, mappings) + if (resolved.rewrite) rewrites.push(resolved.rewrite) + return resolved.value + }) + + const env = config.env + ? Object.fromEntries( + Object.entries(config.env).map(([key, value]) => { + if (typeof value !== "string") return [key, value] + const resolved = resolveString(value, mappings) + if (resolved.rewrite) rewrites.push(resolved.rewrite) + return [key, resolved.value] + }), + ) + : undefined + + const cwdInput = + config.cwd ?? + (config.sourcePath && exists(config.sourcePath) ? config.sourcePath : undefined) + const cwdResult = cwdInput ? resolveString(cwdInput, mappings) : undefined + if (cwdResult?.rewrite) rewrites.push(cwdResult.rewrite) + const cwd = + cwdResult?.value && + !path.isAbsolute(cwdResult.value) && + config.sourcePath && + exists(config.sourcePath) + ? path.resolve(config.sourcePath, cwdResult.value) + : cwdResult?.value + + if (platform !== "win32") { + const unresolved = [command.value, ...(args ?? []), ...(cwd ? [cwd] : []), ...Object.values(env ?? {})].find( + (value) => typeof value === "string" && isWindowsAbsolutePath(value), + ) + if (unresolved) { + return { + ok: false, + reason: `Windows path is not mapped on ${platform}: ${unresolved}`, + rewrites, + } + } + } + + const relativeLocalCommandWithoutCwd = findRelativeLocalCommandWithoutCwd(command.value, cwd) + if (relativeLocalCommandWithoutCwd) { + return { + ok: false, + reason: `Relative stdio command requires cwd before launch: ${relativeLocalCommandWithoutCwd}`, + rewrites, + } + } + + const missingLocalCommand = findMissingLocalCommand(command.value, cwd, exists) + if (missingLocalCommand) { + return { + ok: false, + reason: `Local stdio command does not exist: ${missingLocalCommand}`, + rewrites, + } + } + + const relativeScriptWithoutCwd = findRelativeNodeScriptWithoutCwd(command.value, args, cwd) + if (relativeScriptWithoutCwd) { + return { + ok: false, + reason: `Relative stdio script requires cwd before launch: ${relativeScriptWithoutCwd}`, + rewrites, + } + } + + const missingScript = findMissingNodeScript(command.value, args, cwd, exists) + if (missingScript) { + return { + ok: false, + reason: `Local stdio script does not exist: ${missingScript}`, + rewrites, + } + } + + if (isNodeLikeCommand(command.value) && !findNodeScriptArg(args)) { + return { + ok: false, + reason: "Node stdio command has no entry script", + rewrites, + } + } + + const scriptPath = findNodeLaunchScript(command.value, args, cwd, exists) + const missingDependency = scriptPath + ? findMissingNodeDependency(scriptPath, { exists, readFile, canResolve }) + : null + if (missingDependency) { + return { + ok: false, + reason: `Local stdio script dependency is not installed: ${missingDependency}`, + rewrites, + } + } + + return { + ok: true, + config: { + command: command.value, + args, + env, + cwd, + }, + rewrites, + } +} diff --git a/src/main/lib/moss-account/entitlement.test.ts b/src/main/lib/moss-account/entitlement.test.ts new file mode 100644 index 000000000..53b402e51 --- /dev/null +++ b/src/main/lib/moss-account/entitlement.test.ts @@ -0,0 +1,224 @@ +import { describe, expect, test } from "bun:test" +import type { AuthUser } from "../../auth-store" +import { buildMossAccountEntitlement } from "./entitlement" + +const user: AuthUser = { + id: "user_1", + email: "moss@example.com", + name: "Moss User", + imageUrl: null, + username: "moss", +} + +const providerReadResult = { + status: "found" as const, + sourcePath: "/repo/.moss/providers.yaml", + config: { + version: 1, + defaultProvider: "moss", + credentialPolicy: { + singleUserConfiguration: true, + allowCustomBaseUrl: true, + allowCustomApiKey: true, + shareAcrossEngines: true, + }, + providers: { + moss: { + id: "moss", + label: "Moss Managed", + mode: "bundled-quota", + runtime: "any", + engines: { + hermes: { model: "moss-default" }, + "claude-code": { model: "opus" }, + codex: { model: "gpt-5.5/high" }, + "custom-acp": { model: "custom-acp" }, + }, + }, + custom: { + id: "custom", + label: "Custom", + mode: "custom-url-key", + runtime: "any", + apiKeyEnv: "MOSS_CUSTOM_API_KEY", + baseUrl: "https://custom.test/v1", + baseUrlEnv: "MOSS_CUSTOM_BASE_URL", + engines: { + hermes: { model: "moss-custom" }, + "claude-code": { model: "opus-custom" }, + codex: { model: "gpt-custom/high" }, + "custom-acp": { model: "custom-acp-custom" }, + }, + }, + }, + }, +} + +describe("Moss account entitlement", () => { + test("requires sign-in for Moss managed quota when no user is present", () => { + const entitlement = buildMossAccountEntitlement({ + user: null, + providerReadResult, + }) + + expect(entitlement.status).toBe("needs-sign-in") + expect(entitlement.account.signedIn).toBe(false) + expect(entitlement.quota.status).toBe("needs-sign-in") + expect(entitlement.provider.credentialStatus).toBe("moss-managed") + expect(entitlement.engines.every((engine) => engine.status === "needs-sign-in")).toBe(true) + }) + + test("marks Moss managed quota ready for an active paid account", () => { + const entitlement = buildMossAccountEntitlement({ + user, + plan: { + plan: "moss_pro", + status: "active", + source: "backend", + }, + providerReadResult, + }) + + expect(entitlement.status).toBe("ready") + expect(entitlement.account.email).toBe("moss@example.com") + expect(entitlement.plan.isPaid).toBe(true) + expect(entitlement.quota.status).toBe("available") + expect(entitlement.quota.includedCredits).toBe(10000) + expect(entitlement.quota.remainingCredits).toBe(null) + expect(entitlement.quota.source).toBe("plan-default") + expect(entitlement.engines.map((engine) => engine.providerId)).toEqual([ + "moss", + "moss", + "moss", + "moss", + ]) + expect(entitlement.engines.every((engine) => engine.status === "ready")).toBe(true) + }) + + test("makes active free Moss quota available without marking the plan paid", () => { + const entitlement = buildMossAccountEntitlement({ + user, + plan: { + plan: "moss_free", + status: "active", + source: "backend", + }, + providerReadResult, + }) + + expect(entitlement.status).toBe("ready") + expect(entitlement.plan.isPaid).toBe(false) + expect(entitlement.quota.status).toBe("available") + expect(entitlement.quota.includedCredits).toBe(250) + expect(entitlement.quota.unit).toBe("credits") + expect(entitlement.engines.every((engine) => engine.status === "ready")).toBe(true) + }) + + test("uses backend quota usage when the Moss account service returns it", () => { + const entitlement = buildMossAccountEntitlement({ + user, + plan: { + plan: "moss_pro", + status: "active", + source: "backend", + quota: { + includedCredits: 10000, + usedCredits: 1234, + resetAt: "2026-07-01T00:00:00.000Z", + unit: "credits", + source: "backend", + }, + }, + providerReadResult, + }) + + expect(entitlement.status).toBe("ready") + expect(entitlement.quota.source).toBe("backend") + expect(entitlement.quota.usedCredits).toBe(1234) + expect(entitlement.quota.remainingCredits).toBe(8766) + expect(entitlement.quota.resetAt).toBe("2026-07-01T00:00:00.000Z") + }) + + test("allows custom shared key route without a Moss paid plan", () => { + const customProviderReadResult = { + ...providerReadResult, + config: { + ...providerReadResult.config, + defaultProvider: "custom", + }, + } + + const entitlement = buildMossAccountEntitlement({ + user: null, + providerReadResult: customProviderReadResult, + storedSecrets: { + custom: { hasApiKey: true }, + }, + }) + + expect(entitlement.status).toBe("custom-ready") + expect(entitlement.provider.useCustomProvider).toBe(true) + expect(entitlement.provider.credentialStatus).toBe("stored-key") + expect(entitlement.provider.baseUrlStatus).toBe("configured-url") + expect(entitlement.engines.map((engine) => engine.model)).toEqual([ + "opus-custom", + "gpt-custom/high", + "moss-custom", + "custom-acp-custom", + ]) + expect(entitlement.engines.every((engine) => engine.status === "ready")).toBe(true) + expect(JSON.stringify(entitlement)).not.toContain("sk-") + }) + + test("surfaces custom provider env-key state across every engine", () => { + const customProviderReadResult = { + ...providerReadResult, + config: { + ...providerReadResult.config, + defaultProvider: "custom", + }, + } + + const entitlement = buildMossAccountEntitlement({ + user, + providerReadResult: customProviderReadResult, + }) + + expect(entitlement.status).toBe("custom-ready") + expect(entitlement.provider.credentialStatus).toBe("env-key") + expect(entitlement.provider.baseUrlStatus).toBe("configured-url") + expect(entitlement.engines.every((engine) => engine.status === "ready")).toBe(true) + }) + + test("requires a custom provider base URL before the shared route is ready", () => { + const customProviderReadResult = { + ...providerReadResult, + config: { + ...providerReadResult.config, + defaultProvider: "custom", + providers: { + ...providerReadResult.config.providers, + custom: { + ...providerReadResult.config.providers.custom, + baseUrl: undefined, + baseUrlEnv: undefined, + }, + }, + }, + } + + const entitlement = buildMossAccountEntitlement({ + user: null, + providerReadResult: customProviderReadResult, + storedSecrets: { + custom: { hasApiKey: true }, + }, + }) + + expect(entitlement.status).toBe("custom-needs-url") + expect(entitlement.provider.credentialStatus).toBe("stored-key") + expect(entitlement.provider.baseUrlStatus).toBe("missing-url") + expect(entitlement.provider.reason).toContain("requires one base URL") + expect(entitlement.engines.every((engine) => engine.status === "needs-base-url")).toBe(true) + }) +}) diff --git a/src/main/lib/moss-account/entitlement.ts b/src/main/lib/moss-account/entitlement.ts new file mode 100644 index 000000000..1a2c6bcb6 --- /dev/null +++ b/src/main/lib/moss-account/entitlement.ts @@ -0,0 +1,513 @@ +import type { AuthUser } from "../../auth-store" +import { AGENT_ENGINE_IDS, type AgentEngineId } from "../agent-runtime/types" +import type { + MossProviderConfig, + MossProviderDefinition, + MossProviderReadResult, +} from "../moss-source" + +export type MossAccountEntitlementStatus = + | "ready" + | "needs-sign-in" + | "custom-ready" + | "custom-needs-key" + | "custom-needs-url" + | "missing-project" + | "provider-error" + +export type MossProviderCredentialStatus = + | "moss-managed" + | "stored-key" + | "inline-key" + | "env-key" + | "missing-key" + | "missing-project" + | "provider-error" + +export type MossProviderBaseUrlStatus = + | "configured-url" + | "env-url" + | "missing-url" + | "not-required" + | "missing-project" + | "provider-error" + +export interface MossAccountPlan { + plan: string + status: string | null + source?: "backend" | "local" + quota?: MossAccountPlanQuota | null +} + +export interface MossAccountPlanQuota { + includedCredits?: number | null + usedCredits?: number | null + remainingCredits?: number | null + resetAt?: string | null + unit?: string | null + source?: "backend" | "plan-default" | "none" +} + +export interface MossStoredProviderSecretSummary { + hasApiKey?: boolean +} + +export interface MossAccountEntitlement { + status: MossAccountEntitlementStatus + account: { + signedIn: boolean + userId: string | null + email: string | null + name: string | null + } + plan: { + id: string + status: string + isPaid: boolean + source: "backend" | "local" | "none" + } + quota: { + providerId: "moss" + label: string + mode: "bundled-quota" + status: "available" | "needs-sign-in" | "checking" | "inactive" + sharedAcrossEngines: boolean + includedCredits: number | null + usedCredits: number | null + remainingCredits: number | null + resetAt: string | null + unit: string + source: "backend" | "plan-default" | "none" + reason: string + } + provider: { + sourcePath: string + defaultProvider: string + activeProviderId: string | null + useCustomProvider: boolean + sharedAcrossEngines: boolean + credentialStatus: MossProviderCredentialStatus + baseUrlStatus: MossProviderBaseUrlStatus + reason: string + } + engines: Array<{ + engineId: AgentEngineId + providerId: string | null + model: string | null + credentialMode: MossProviderCredentialStatus + status: "ready" | "needs-sign-in" | "needs-api-key" | "needs-base-url" | "unconfigured" + }> +} + +const ENGINE_IDS: readonly AgentEngineId[] = AGENT_ENGINE_IDS +const PAID_PLAN_IDS = new Set([ + "moss_pro", + "moss_team", + "moss_enterprise", + "onecode_pro", + "onecode_max_100", + "onecode_max", +]) + +const PLAN_QUOTA_DEFAULTS: Record> & Pick> = { + moss_free: { + includedCredits: 250, + usedCredits: null, + remainingCredits: null, + resetAt: null, + unit: "credits", + source: "plan-default", + }, + onecode_free: { + includedCredits: 250, + usedCredits: null, + remainingCredits: null, + resetAt: null, + unit: "credits", + source: "plan-default", + }, + moss_pro: { + includedCredits: 10000, + usedCredits: null, + remainingCredits: null, + resetAt: null, + unit: "credits", + source: "plan-default", + }, + onecode_pro: { + includedCredits: 10000, + usedCredits: null, + remainingCredits: null, + resetAt: null, + unit: "credits", + source: "plan-default", + }, + onecode_max_100: { + includedCredits: 100000, + usedCredits: null, + remainingCredits: null, + resetAt: null, + unit: "credits", + source: "plan-default", + }, + onecode_max: { + includedCredits: 250000, + usedCredits: null, + remainingCredits: null, + resetAt: null, + unit: "credits", + source: "plan-default", + }, + moss_team: { + includedCredits: 50000, + usedCredits: null, + remainingCredits: null, + resetAt: null, + unit: "credits", + source: "plan-default", + }, + moss_enterprise: { + includedCredits: null, + usedCredits: null, + remainingCredits: null, + resetAt: null, + unit: "contract credits", + source: "plan-default", + }, +} + +function isActivePaidPlan(plan: MossAccountPlan | null | undefined): boolean { + if (!plan) return false + return PAID_PLAN_IDS.has(plan.plan) && plan.status === "active" +} + +function normalizeQuotaNumber(value: number | null | undefined): number | null { + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + return null + } + return Math.floor(value) +} + +function buildPlanQuotaAllowance( + plan: MossAccountPlan | null | undefined, +): Pick< + MossAccountEntitlement["quota"], + | "includedCredits" + | "usedCredits" + | "remainingCredits" + | "resetAt" + | "unit" + | "source" +> { + if (!plan) { + return { + includedCredits: null, + usedCredits: null, + remainingCredits: null, + resetAt: null, + unit: "credits", + source: "none", + } + } + + const fallback = PLAN_QUOTA_DEFAULTS[plan.plan] + const quota = plan.quota ?? null + const source = quota ? "backend" : fallback?.source ?? "none" + const includedCredits = + normalizeQuotaNumber(quota?.includedCredits) ?? + fallback?.includedCredits ?? + null + const usedCredits = normalizeQuotaNumber(quota?.usedCredits) + const remainingCredits = + normalizeQuotaNumber(quota?.remainingCredits) ?? + (includedCredits !== null && usedCredits !== null + ? Math.max(includedCredits - usedCredits, 0) + : null) + + return { + includedCredits, + usedCredits, + remainingCredits, + resetAt: quota?.resetAt ?? fallback?.resetAt ?? null, + unit: quota?.unit ?? fallback?.unit ?? "credits", + source, + } +} + +function hasManagedQuotaAccess(plan: MossAccountPlan | null | undefined): boolean { + if (!plan || plan.status !== "active") return false + const allowance = buildPlanQuotaAllowance(plan) + return allowance.source !== "none" +} + +function getProvider( + config: MossProviderConfig | undefined, + providerId: string | undefined, +): MossProviderDefinition | undefined { + if (!config || !providerId) return undefined + return config.providers[providerId] +} + +function getProviderCredentialStatus(params: { + provider?: MossProviderDefinition + storedSecret?: MossStoredProviderSecretSummary + missingProject?: boolean + providerError?: boolean +}): MossProviderCredentialStatus { + if (params.missingProject) return "missing-project" + if (params.providerError) return "provider-error" + const provider = params.provider + if (!provider) return "missing-key" + if (provider.mode === "bundled-quota") return "moss-managed" + if (params.storedSecret?.hasApiKey) return "stored-key" + if (provider.apiKey) return "inline-key" + if (provider.apiKeyEnv) return "env-key" + return "missing-key" +} + +function hasEnvValue(envName: string | undefined): boolean { + if (!envName) return false + const value = process.env[envName] + return typeof value === "string" && value.trim().length > 0 +} + +function getProviderBaseUrlStatus(params: { + provider?: MossProviderDefinition + missingProject?: boolean + providerError?: boolean +}): MossProviderBaseUrlStatus { + if (params.missingProject) return "missing-project" + if (params.providerError) return "provider-error" + + const provider = params.provider + if (!provider) return "missing-url" + if (provider.mode !== "custom-url-key") return "not-required" + if (provider.baseUrl) return "configured-url" + if (hasEnvValue(provider.baseUrlEnv)) return "env-url" + + const supportedEngineIds = ENGINE_IDS.filter((engineId) => + providerSupportsEngine(provider, engineId), + ) + if (supportedEngineIds.length === 0) return "missing-url" + + const engineUrlStates = supportedEngineIds.map((engineId) => { + const engineConfig = provider.engines?.[engineId] + if (engineConfig?.baseUrl) return "configured-url" + if (hasEnvValue(engineConfig?.baseUrlEnv)) return "env-url" + return "missing-url" + }) + + if (engineUrlStates.every((status) => status !== "missing-url")) { + return engineUrlStates.includes("configured-url") + ? "configured-url" + : "env-url" + } + + return "missing-url" +} + +function providerSupportsEngine( + provider: MossProviderDefinition | undefined, + engineId: AgentEngineId, +): boolean { + if (!provider) return false + if (provider.engines?.[engineId]) return true + if (provider.runtime === "any") return true + if (provider.runtime === engineId) return true + if (provider.runtime === "claude" && engineId === "claude-code") return true + if (provider.runtime === "openai" && engineId === "codex") return true + return provider.mode === "bundled-quota" +} + +function getEngineModel( + provider: MossProviderDefinition | undefined, + engineId: AgentEngineId, +): string | null { + if (!provider) return null + return ( + provider.engines?.[engineId]?.model ?? + provider.models?.[engineId] ?? + provider.model ?? + null + ) +} + +function buildQuota(params: { + user: AuthUser | null + plan: MossAccountPlan | null + sharedAcrossEngines: boolean +}): MossAccountEntitlement["quota"] { + const allowance = buildPlanQuotaAllowance(params.plan) + + if (!params.user) { + return { + providerId: "moss", + label: "Moss Managed", + mode: "bundled-quota", + status: "needs-sign-in", + sharedAcrossEngines: params.sharedAcrossEngines, + ...allowance, + reason: + "Sign in to use Moss managed quota across Hermes, Claude Code, Codex, and Custom ACP.", + } + } + + if (!params.plan) { + return { + providerId: "moss", + label: "Moss Managed", + mode: "bundled-quota", + status: "checking", + sharedAcrossEngines: params.sharedAcrossEngines, + ...allowance, + reason: "Moss account is signed in; quota status needs a backend entitlement refresh.", + } + } + + if (hasManagedQuotaAccess(params.plan)) { + return { + providerId: "moss", + label: "Moss Managed", + mode: "bundled-quota", + status: "available", + sharedAcrossEngines: params.sharedAcrossEngines, + ...allowance, + reason: "Moss managed quota is available for the shared provider route.", + } + } + + return { + providerId: "moss", + label: "Moss Managed", + mode: "bundled-quota", + status: "inactive", + sharedAcrossEngines: params.sharedAcrossEngines, + ...allowance, + reason: "Moss account is signed in, but the current plan does not include managed quota.", + } +} + +export function buildMossAccountEntitlement(params: { + user?: AuthUser | null + plan?: MossAccountPlan | null + providerReadResult?: MossProviderReadResult | null + storedSecrets?: Record +}): MossAccountEntitlement { + const user = params.user ?? null + const plan = params.plan ?? null + const providerReadResult = params.providerReadResult ?? null + const config = providerReadResult?.config + const providerStatus = providerReadResult?.status + const missingProject = !providerReadResult || providerStatus === "missing" + const providerError = providerStatus === "parse-error" + const defaultProvider = config?.defaultProvider ?? "moss" + const sharedAcrossEngines = + config?.credentialPolicy?.shareAcrossEngines ?? true + const activeProvider = providerError + ? undefined + : getProvider(config, defaultProvider) ?? getProvider(config, "moss") + const activeProviderId = activeProvider?.id ?? null + const useCustomProvider = activeProviderId === "custom" + const credentialStatus = getProviderCredentialStatus({ + provider: activeProvider, + storedSecret: activeProviderId + ? params.storedSecrets?.[activeProviderId] + : undefined, + missingProject, + providerError, + }) + const baseUrlStatus = getProviderBaseUrlStatus({ + provider: activeProvider, + missingProject, + providerError, + }) + const quota = buildQuota({ + user, + plan, + sharedAcrossEngines, + }) + + let status: MossAccountEntitlementStatus + if (missingProject) { + status = "missing-project" + } else if (providerError) { + status = "provider-error" + } else if (useCustomProvider) { + if ( + credentialStatus !== "stored-key" && + credentialStatus !== "inline-key" && + credentialStatus !== "env-key" + ) { + status = "custom-needs-key" + } else if (baseUrlStatus === "missing-url") { + status = "custom-needs-url" + } else { + status = "custom-ready" + } + } else { + status = quota.status === "available" ? "ready" : "needs-sign-in" + } + + return { + status, + account: { + signedIn: Boolean(user), + userId: user?.id ?? null, + email: user?.email ?? null, + name: user?.name ?? null, + }, + plan: { + id: plan?.plan ?? (user ? "unknown" : "signed-out"), + status: plan?.status ?? (user ? "unknown" : "signed-out"), + isPaid: isActivePaidPlan(plan), + source: plan?.source ?? (plan ? "backend" : "none"), + }, + quota, + provider: { + sourcePath: providerReadResult?.sourcePath ?? "", + defaultProvider, + activeProviderId, + useCustomProvider, + sharedAcrossEngines, + credentialStatus, + baseUrlStatus, + reason: + providerReadResult?.error ?? + (useCustomProvider && baseUrlStatus === "missing-url" + ? "Custom URL/key requires one base URL before Hermes, Claude Code, Codex, and Custom ACP can share it." + : useCustomProvider + ? "Custom URL/key is shared across all engines." + : "Moss Managed quota is the default shared provider route."), + }, + engines: ENGINE_IDS.map((engineId) => { + const supportsEngine = providerSupportsEngine(activeProvider, engineId) + let engineStatus: MossAccountEntitlement["engines"][number]["status"] + if (!supportsEngine) { + engineStatus = "unconfigured" + } else if (useCustomProvider) { + if ( + credentialStatus !== "stored-key" && + credentialStatus !== "inline-key" && + credentialStatus !== "env-key" + ) { + engineStatus = "needs-api-key" + } else if (baseUrlStatus === "missing-url") { + engineStatus = "needs-base-url" + } else { + engineStatus = "ready" + } + } else { + engineStatus = + quota.status === "available" ? "ready" : "needs-sign-in" + } + + return { + engineId, + providerId: activeProviderId, + model: getEngineModel(activeProvider, engineId), + credentialMode: credentialStatus, + status: engineStatus, + } + }), + } +} diff --git a/src/main/lib/moss-source/bootstrap.ts b/src/main/lib/moss-source/bootstrap.ts new file mode 100644 index 000000000..164326a6a --- /dev/null +++ b/src/main/lib/moss-source/bootstrap.ts @@ -0,0 +1,239 @@ +import * as fs from "fs/promises" +import * as path from "path" +import { getMossBootstrapDirectories, getMossSourceLayout } from "./layout" + +export interface EnsureMossSourceOptions { + projectPath: string +} + +export interface EnsureMossSourceResult { + projectPath: string + root: string + status: "created" | "updated" | "skipped" | "conflict" + created: string[] + skipped: string[] + conflicts: Array<{ path: string; reason: string }> + reason?: string +} + +const DEFAULT_MOSS_INSTRUCTIONS = `# Moss + +This workspace uses Moss Unified Source as the single source of truth for rules, memory, skills, MCP, plugins, hooks, subagents, and providers. + +- Keep canonical configuration under .moss. +- Project Claude Code, Codex, Hermes, and custom ACP agent files from .moss instead of maintaining duplicate real data. +- Sessions may remain engine-native, but shared resources and provider routing belong to Moss. +` + +const DEFAULT_WORKSPACE_CONFIG = `version: 1 +name: Moss Workspace +source: moss-unified-source +` + +const DEFAULT_PROVIDER_CONFIG = `version: 1 +defaultProvider: moss +credentialPolicy: + singleUserConfiguration: true + allowCustomBaseUrl: true + allowCustomApiKey: true + shareAcrossEngines: true +providers: + moss: + label: Moss Managed + mode: bundled-quota + runtime: any + engines: + hermes: + model: moss-default + claude-code: + model: opus + codex: + model: gpt-5.5/high + custom-acp: + model: custom-acp + custom: + label: Custom OpenAI-Compatible + mode: custom-url-key + runtime: any + apiKeyEnv: MOSS_CUSTOM_API_KEY + baseUrlEnv: MOSS_CUSTOM_BASE_URL + engines: + hermes: + model: moss-custom + claude-code: + model: opus + codex: + model: gpt-5.5/high + authMethod: openai-api-key + custom-acp: + model: custom-acp +` + +const DEFAULT_MCP_CONFIG = `${JSON.stringify({ mcpServers: {} }, null, 2)}\n` + +const DEFAULT_STARTER_MEMORY = `--- +name: moss-workspace +description: Canonical Moss workspace operating memory. +--- + +Moss owns one shared source for this workspace. Rules, memory, skills, MCP servers, plugins, hooks, subagents, and provider routing live under .moss and are projected into Claude Code, Codex, Hermes, and custom ACP agents as needed. + +Do not maintain a second real copy in engine-native folders. If an engine needs a native path, use the Moss projection or adapter output. +` + +const DEFAULT_STARTER_SKILL = `--- +name: moss-workspace +description: Use the Moss unified workspace source and shared resource map. +--- + +# Moss Workspace + +Use this skill when a task depends on workspace rules, shared memory, installed tools, hooks, or provider routing. + +## Protocol + +1. Treat .moss/source/moss.md as the canonical rules file. +2. Read .moss/source/workspace.yaml for workspace defaults. +3. Use .moss/memory, .moss/skills, .moss/mcp, .moss/plugins, .moss/hooks, .moss/subagents, and .moss/providers.yaml as the only real resource source. +4. If an engine needs a native path, use the Moss projection or adapter output instead of editing the native copy. +` + +const DEFAULT_STARTER_HOOK = `--- +name: session-start +description: Starter hook template projected from Moss Unified Source. +event: MossSessionStart +enabled: true +--- + +This hook is intentionally commandless. It proves that hooks are installed and projected from .moss without executing user code until the user adds an explicit command. +` + +const DEFAULT_STARTER_PLUGIN = `--- +name: moss-starter +description: Starter plugin manifest projected from Moss Unified Source. +enabled: true +--- + +Moss Starter is the built-in plugin entry that proves installed plugin metadata is owned once under .moss/plugins and exposed to each engine through projection or adapter manifests. +` + +async function pathExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +async function ensureDefaultFile(params: { + filePath: string + content: string + created: string[] + skipped: string[] +}) { + if (await pathExists(params.filePath)) { + params.skipped.push(params.filePath) + return + } + + await fs.mkdir(path.dirname(params.filePath), { recursive: true }) + await fs.writeFile(params.filePath, params.content, "utf-8") + params.created.push(params.filePath) +} + +export async function ensureMossSource( + options: EnsureMossSourceOptions, +): Promise { + const projectPath = path.resolve(options.projectPath) + const layout = getMossSourceLayout(projectPath) + const result: EnsureMossSourceResult = { + projectPath, + root: layout.root, + status: "skipped", + created: [], + skipped: [], + conflicts: [], + } + + try { + const stat = await fs.lstat(layout.root) + if (!stat.isDirectory()) { + result.status = "conflict" + result.conflicts.push({ + path: layout.root, + reason: ".moss exists and is not a directory.", + }) + return result + } + } catch { + await fs.mkdir(layout.root, { recursive: true }) + result.created.push(layout.root) + } + + for (const dirPath of getMossBootstrapDirectories(layout)) { + if (await pathExists(dirPath)) { + result.skipped.push(dirPath) + continue + } + await fs.mkdir(dirPath, { recursive: true }) + result.created.push(dirPath) + } + + await ensureDefaultFile({ + filePath: layout.sourceInstruction, + content: DEFAULT_MOSS_INSTRUCTIONS, + created: result.created, + skipped: result.skipped, + }) + await ensureDefaultFile({ + filePath: layout.workspaceConfig, + content: DEFAULT_WORKSPACE_CONFIG, + created: result.created, + skipped: result.skipped, + }) + await ensureDefaultFile({ + filePath: layout.providersConfig, + content: DEFAULT_PROVIDER_CONFIG, + created: result.created, + skipped: result.skipped, + }) + await ensureDefaultFile({ + filePath: layout.mcpConfig, + content: DEFAULT_MCP_CONFIG, + created: result.created, + skipped: result.skipped, + }) + await ensureDefaultFile({ + filePath: path.join(layout.memoryRoot, "moss-workspace.md"), + content: DEFAULT_STARTER_MEMORY, + created: result.created, + skipped: result.skipped, + }) + await ensureDefaultFile({ + filePath: path.join(layout.skillsRoot, "moss-workspace", "SKILL.md"), + content: DEFAULT_STARTER_SKILL, + created: result.created, + skipped: result.skipped, + }) + await ensureDefaultFile({ + filePath: path.join(layout.hooksRoot, "session-start.md"), + content: DEFAULT_STARTER_HOOK, + created: result.created, + skipped: result.skipped, + }) + await ensureDefaultFile({ + filePath: path.join(layout.pluginsRoot, "moss-starter.md"), + content: DEFAULT_STARTER_PLUGIN, + created: result.created, + skipped: result.skipped, + }) + + if (result.created.length > 0) { + result.status = result.skipped.length > 0 ? "updated" : "created" + return result + } + + result.reason = "Moss Unified Source already exists." + return result +} diff --git a/src/main/lib/moss-source/hooks.ts b/src/main/lib/moss-source/hooks.ts new file mode 100644 index 000000000..b207129fa --- /dev/null +++ b/src/main/lib/moss-source/hooks.ts @@ -0,0 +1,320 @@ +import { spawn } from "node:child_process" +import { createHash } from "node:crypto" +import path from "node:path" +import type { AgentEngineId } from "../agent-runtime/types" +import type { SharedResource } from "../shared-resources/types" +import { discoverMossSourceResources } from "./registry" + +export type MossHookRunStatus = "passed" | "failed" | "skipped" | "timed-out" + +export interface MossHookRunResult { + resourceId: string + name: string + event: string + status: MossHookRunStatus + command?: string + commandHash?: string + exitCode?: number | null + elapsedMs: number + stdout?: string + stderr?: string + error?: string + timedOut?: boolean +} + +export interface MossHookRunSummary { + status: MossHookRunStatus + event: string + engineId: AgentEngineId + projectPath: string + matchedCount: number + executedCount: number + skippedCount: number + failedCount: number + timedOutCount: number + payloadHash: string + results: MossHookRunResult[] + warnings: string[] +} + +export interface RunMossHooksOptions { + projectPath: string + event: string + engineId: AgentEngineId + cwd?: string + payload?: Record + env?: Record + timeoutMs?: number + maxHooks?: number +} + +const DEFAULT_HOOK_TIMEOUT_MS = 10_000 +const DEFAULT_MAX_HOOKS = 20 + +function sha256(value: string): string { + return createHash("sha256").update(value).digest("hex") +} + +function normalizeEvent(value: string | undefined): string { + return value?.trim().toLowerCase() ?? "" +} + +function redactHookOutput(value: string): string { + return value + .replace(/sk-[A-Za-z0-9_-]{12,}/g, "sk-REDACTED") + .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer REDACTED") + .replace( + /(["']?(?:api[_-]?key|token|secret|password|authorization)["']?\s*[:=]\s*["']?)([^"',\n}]+)/gi, + "$1REDACTED", + ) + .slice(0, 4000) +} + +function hookEvent(resource: SharedResource): string { + const event = resource.metadata?.event + return typeof event === "string" && event.trim() ? event.trim() : "Stop" +} + +function hookCommand(resource: SharedResource): string | undefined { + const command = resource.metadata?.command + return typeof command === "string" && command.trim() + ? command.trim() + : undefined +} + +function isHookEnabled(resource: SharedResource): boolean { + if (resource.enabled === false) return false + if (resource.metadata?.hookEnabled === false) return false + return true +} + +function visibleResult(result: MossHookRunResult): MossHookRunResult { + return { + ...result, + command: result.command ? redactHookOutput(result.command) : undefined, + stdout: result.stdout ? redactHookOutput(result.stdout) : undefined, + stderr: result.stderr ? redactHookOutput(result.stderr) : undefined, + error: result.error ? redactHookOutput(result.error) : undefined, + } +} + +function runHookCommand(params: { + resource: SharedResource + command: string + event: string + engineId: AgentEngineId + projectPath: string + cwd: string + payloadJson: string + payloadHash: string + env?: Record + timeoutMs: number +}): Promise { + const startedAt = Date.now() + + return new Promise((resolve) => { + const child = spawn(params.command, { + cwd: params.cwd, + env: { + ...process.env, + ...params.env, + MOSS_HOOK_EVENT: params.event, + MOSS_HOOK_ENGINE: params.engineId, + MOSS_HOOK_RESOURCE_ID: params.resource.id, + MOSS_HOOK_NAME: params.resource.name, + MOSS_HOOK_PROJECT_PATH: params.projectPath, + MOSS_HOOK_CWD: params.cwd, + MOSS_HOOK_PAYLOAD_JSON: params.payloadJson, + MOSS_HOOK_PAYLOAD_SHA256: params.payloadHash, + }, + shell: true, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }) + const stdoutChunks: Buffer[] = [] + const stderrChunks: Buffer[] = [] + let timedOut = false + let forceKillTimer: ReturnType | null = null + + const timeout = setTimeout(() => { + timedOut = true + child.kill("SIGTERM") + forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 2000) + }, params.timeoutMs) + + child.stdout.on("data", (chunk) => { + stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))) + }) + child.stderr.on("data", (chunk) => { + stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))) + }) + child.once("error", (error) => { + clearTimeout(timeout) + if (forceKillTimer) clearTimeout(forceKillTimer) + resolve( + visibleResult({ + resourceId: params.resource.id, + name: params.resource.name, + event: params.event, + status: "failed", + command: params.command, + commandHash: sha256(params.command), + elapsedMs: Date.now() - startedAt, + error: error.message, + }), + ) + }) + child.once("close", (exitCode) => { + clearTimeout(timeout) + if (forceKillTimer) clearTimeout(forceKillTimer) + const status: MossHookRunStatus = timedOut + ? "timed-out" + : exitCode === 0 + ? "passed" + : "failed" + + resolve( + visibleResult({ + resourceId: params.resource.id, + name: params.resource.name, + event: params.event, + status, + command: params.command, + commandHash: sha256(params.command), + exitCode, + elapsedMs: Date.now() - startedAt, + stdout: Buffer.concat(stdoutChunks).toString("utf-8").trim(), + stderr: Buffer.concat(stderrChunks).toString("utf-8").trim(), + timedOut, + }), + ) + }) + }) +} + +export async function runMossHooks( + options: RunMossHooksOptions, +): Promise { + const payloadJson = JSON.stringify(options.payload ?? {}) + const payloadHash = sha256(payloadJson) + const warnings: string[] = [] + const event = options.event.trim() + const normalizedEvent = normalizeEvent(event) + const timeoutMs = options.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS + const maxHooks = options.maxHooks ?? DEFAULT_MAX_HOOKS + + let resources: SharedResource[] = [] + try { + resources = await discoverMossSourceResources(options.projectPath) + } catch (error) { + return { + status: "failed", + event, + engineId: options.engineId, + projectPath: options.projectPath, + matchedCount: 0, + executedCount: 0, + skippedCount: 0, + failedCount: 1, + timedOutCount: 0, + payloadHash, + results: [ + { + resourceId: "moss:hooks", + name: "Moss hook discovery", + event, + status: "failed", + elapsedMs: 0, + error: error instanceof Error ? error.message : String(error), + }, + ], + warnings, + } + } + + const matchedHooks = resources + .filter((resource) => resource.kind === "hook" && resource.scope === "moss") + .filter((resource) => normalizeEvent(hookEvent(resource)) === normalizedEvent) + .slice(0, maxHooks) + + const allMatches = resources + .filter((resource) => resource.kind === "hook" && resource.scope === "moss") + .filter((resource) => normalizeEvent(hookEvent(resource)) === normalizedEvent) + + if (allMatches.length > matchedHooks.length) { + warnings.push( + `Moss hook run limited to ${matchedHooks.length} of ${allMatches.length} matching hooks.`, + ) + } + + const results: MossHookRunResult[] = [] + for (const resource of matchedHooks) { + if (!isHookEnabled(resource)) { + results.push({ + resourceId: resource.id, + name: resource.name, + event, + status: "skipped", + elapsedMs: 0, + error: "Hook is disabled.", + }) + continue + } + + const command = hookCommand(resource) + if (!command) { + results.push({ + resourceId: resource.id, + name: resource.name, + event, + status: "skipped", + elapsedMs: 0, + error: "Hook has no command.", + }) + continue + } + + results.push( + await runHookCommand({ + resource, + command, + event, + engineId: options.engineId, + projectPath: options.projectPath, + cwd: options.cwd ?? options.projectPath, + payloadJson, + payloadHash, + env: options.env, + timeoutMs, + }), + ) + } + + const executedCount = results.filter((result) => + ["passed", "failed", "timed-out"].includes(result.status), + ).length + const skippedCount = results.filter((result) => result.status === "skipped").length + const failedCount = results.filter((result) => result.status === "failed").length + const timedOutCount = results.filter((result) => result.status === "timed-out").length + const status: MossHookRunStatus = + failedCount > 0 || timedOutCount > 0 + ? "failed" + : executedCount > 0 + ? "passed" + : "skipped" + + return { + status, + event, + engineId: options.engineId, + projectPath: options.projectPath, + matchedCount: matchedHooks.length, + executedCount, + skippedCount, + failedCount, + timedOutCount, + payloadHash, + results, + warnings, + } +} diff --git a/src/main/lib/moss-source/index.ts b/src/main/lib/moss-source/index.ts new file mode 100644 index 000000000..4101c4a83 --- /dev/null +++ b/src/main/lib/moss-source/index.ts @@ -0,0 +1,10 @@ +export * from "./bootstrap" +export * from "./hooks" +export * from "./layout" +export * from "./projection" +export * from "./provider-config" +export * from "./provider-secrets" +export * from "./registry" +export * from "./runtime-materializer" +export * from "./subagents" +export * from "./types" diff --git a/src/main/lib/moss-source/layout.ts b/src/main/lib/moss-source/layout.ts new file mode 100644 index 000000000..a5eee4d61 --- /dev/null +++ b/src/main/lib/moss-source/layout.ts @@ -0,0 +1,47 @@ +import * as path from "path" +import { + MOSS_ROOT_DIR, + MOSS_SOURCE_VERSION, + type MossSourceLayout, +} from "./types" + +export function getMossSourceLayout(projectPath: string): MossSourceLayout { + const root = path.join(projectPath, MOSS_ROOT_DIR) + + return { + version: MOSS_SOURCE_VERSION, + projectPath, + root, + sourceInstruction: path.join(root, "source", "moss.md"), + workspaceConfig: path.join(root, "source", "workspace.yaml"), + memoryRoot: path.join(root, "memory"), + skillsRoot: path.join(root, "skills"), + mcpConfig: path.join(root, "mcp", "config.json"), + pluginsRoot: path.join(root, "plugins"), + hooksRoot: path.join(root, "hooks"), + subagentsRoot: path.join(root, "subagents"), + providersConfig: path.join(root, "providers.yaml"), + } +} + +export function toMossProjectPath( + projectPath: string, + filePath: string, +): string { + if (!path.isAbsolute(filePath)) return filePath + return path.relative(projectPath, filePath) +} + +export function getMossBootstrapDirectories( + layout: MossSourceLayout, +): string[] { + return [ + path.dirname(layout.sourceInstruction), + layout.memoryRoot, + layout.skillsRoot, + path.dirname(layout.mcpConfig), + layout.pluginsRoot, + layout.hooksRoot, + layout.subagentsRoot, + ] +} diff --git a/src/main/lib/moss-source/projection.ts b/src/main/lib/moss-source/projection.ts new file mode 100644 index 000000000..42f79c09e --- /dev/null +++ b/src/main/lib/moss-source/projection.ts @@ -0,0 +1,908 @@ +import * as crypto from "crypto" +import * as fs from "fs/promises" +import * as os from "os" +import * as path from "path" +import type { + EngineResourceProjection, + ResourcePathMapping, +} from "../shared-resources/types" + +export type MossProjectionMaterializeStatus = + | "created" + | "updated" + | "skipped" + | "conflict" + | "unsupported" + +export interface MossProjectionMaterializeResult { + engineId: EngineResourceProjection["engineId"] + resourceId: string + action: ResourcePathMapping["action"] + sourcePath?: string + targetPath?: string + status: MossProjectionMaterializeStatus + reason?: string +} + +export interface MossProjectionManifestEntry { + engineId: EngineResourceProjection["engineId"] + resourceId: string + action: ResourcePathMapping["action"] + sourcePath?: string + targetPath: string + contentHash?: string + updatedAt: string +} + +export interface MossProjectionManifest { + version: 1 + generatedAt: string + entries: Record +} + +export interface MossProjectionManifestSummary { + status: "found" | "missing" | "parse-error" + sourcePath: string + generatedAt?: string + totalEntries: number + engines: Array<{ + engineId: EngineResourceProjection["engineId"] + entries: number + }> + error?: string +} + +export interface MaterializeMossProjectionOptions { + projectPath: string + projection: EngineResourceProjection + dryRun?: boolean +} + +export interface RemoveMossProjectionResourceOptions { + projectPath: string + resourceId: string + sourcePath?: string + targetPaths?: string[] + removeTargets?: boolean +} + +export interface RemoveMossProjectionResourceResult { + removedEntries: string[] + removedTargets: string[] +} + +const MANIFEST_PATH = path.join(".moss", "projections", "manifest.json") +const ADAPTER_MANIFEST_NAME = ".moss-adapter.json" + +async function pathExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +function normalizePathKey(projectPath: string, filePath: string): string { + const normalizedProjectPath = path.resolve(projectPath) + const normalizedFilePath = path.resolve(filePath) + if ( + normalizedFilePath === normalizedProjectPath || + normalizedFilePath.startsWith(`${normalizedProjectPath}${path.sep}`) + ) { + return path.relative(normalizedProjectPath, normalizedFilePath) + } + return normalizedFilePath +} + +function resolveProjectionPath(projectPath: string, mappingPath: string): string { + if (mappingPath.startsWith("~/")) { + return path.join(os.homedir(), mappingPath.slice(2)) + } + if (path.isAbsolute(mappingPath)) return mappingPath + return path.join(projectPath, mappingPath) +} + +function hashContent(content: string | Buffer): string { + return crypto.createHash("sha256").update(content).digest("hex") +} + +async function readManifest(projectPath: string): Promise { + const manifestPath = path.join(projectPath, MANIFEST_PATH) + try { + const raw = await fs.readFile(manifestPath, "utf-8") + const parsed = JSON.parse(raw) as MossProjectionManifest + if (parsed?.version === 1 && parsed.entries && typeof parsed.entries === "object") { + return parsed + } + } catch { + // Fall through to a fresh manifest. + } + + return { + version: 1, + generatedAt: new Date().toISOString(), + entries: {}, + } +} + +async function writeManifest( + projectPath: string, + manifest: MossProjectionManifest, +): Promise { + manifest.generatedAt = new Date().toISOString() + const manifestPath = path.join(projectPath, MANIFEST_PATH) + await fs.mkdir(path.dirname(manifestPath), { recursive: true }) + await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8") +} + +function summarizeManifest( + manifest: MossProjectionManifest, + sourcePath: string, +): MossProjectionManifestSummary { + const counts = new Map() + for (const entry of Object.values(manifest.entries)) { + counts.set(entry.engineId, (counts.get(entry.engineId) ?? 0) + 1) + } + + return { + status: "found", + sourcePath, + generatedAt: manifest.generatedAt, + totalEntries: Object.keys(manifest.entries).length, + engines: Array.from(counts.entries()) + .map(([engineId, entries]) => ({ engineId, entries })) + .sort((a, b) => a.engineId.localeCompare(b.engineId)), + } +} + +export async function readMossProjectionManifestSummary( + projectPath: string, +): Promise { + const manifestPath = path.join(projectPath, MANIFEST_PATH) + if (!(await pathExists(manifestPath))) { + return { + status: "missing", + sourcePath: manifestPath, + totalEntries: 0, + engines: [], + } + } + + try { + const raw = await fs.readFile(manifestPath, "utf-8") + const parsed = JSON.parse(raw) as MossProjectionManifest + if ( + parsed?.version !== 1 || + !parsed.entries || + typeof parsed.entries !== "object" + ) { + throw new Error("Projection manifest is not a version 1 Moss manifest.") + } + return summarizeManifest(parsed, manifestPath) + } catch (error) { + return { + status: "parse-error", + sourcePath: manifestPath, + totalEntries: 0, + engines: [], + error: error instanceof Error ? error.message : String(error), + } + } +} + +export async function removeMossProjectionResource( + options: RemoveMossProjectionResourceOptions, +): Promise { + const manifestPath = path.join(options.projectPath, MANIFEST_PATH) + if (!(await pathExists(manifestPath))) { + return { + removedEntries: [], + removedTargets: [], + } + } + + const manifest = await readManifest(options.projectPath) + const sourceKey = options.sourcePath + ? normalizePathKey( + options.projectPath, + resolveProjectionPath(options.projectPath, options.sourcePath), + ) + : undefined + const sourceAbs = options.sourcePath + ? resolveProjectionPath(options.projectPath, options.sourcePath) + : undefined + const targetKeys = new Set( + (options.targetPaths ?? []).map((targetPath) => + normalizePathKey( + options.projectPath, + resolveProjectionPath(options.projectPath, targetPath), + ), + ), + ) + const removedEntries: string[] = [] + const removedTargets: string[] = [] + const removedTargetKeys = new Set() + + for (const [entryKey, entry] of Object.entries(manifest.entries)) { + const entryManifestKey = normalizePathKey( + options.projectPath, + resolveProjectionPath(options.projectPath, entryKey), + ) + const entrySourceKey = entry.sourcePath + ? normalizePathKey( + options.projectPath, + resolveProjectionPath(options.projectPath, entry.sourcePath), + ) + : undefined + const entryTargetKey = normalizePathKey( + options.projectPath, + resolveProjectionPath(options.projectPath, entry.targetPath), + ) + const matches = + entry.resourceId === options.resourceId || + (sourceKey !== undefined && entrySourceKey === sourceKey) || + targetKeys.has(entryTargetKey) || + targetKeys.has(entryManifestKey) + + if (!matches) continue + + delete manifest.entries[entryKey] + removedEntries.push(entryKey) + + if (options.removeTargets && targetKeys.has(entryManifestKey)) { + await fs.rm(resolveProjectionPath(options.projectPath, entryKey), { + recursive: true, + force: true, + }) + removedTargets.push(entryKey) + removedTargetKeys.add(entryManifestKey) + } else if (options.removeTargets && targetKeys.has(entryTargetKey)) { + await fs.rm(resolveProjectionPath(options.projectPath, entry.targetPath), { + recursive: true, + force: true, + }) + removedTargets.push(entry.targetPath) + removedTargetKeys.add(entryTargetKey) + } + } + + if (options.removeTargets) { + for (const targetPath of options.targetPaths ?? []) { + const targetAbs = resolveProjectionPath(options.projectPath, targetPath) + const targetKey = normalizePathKey(options.projectPath, targetAbs) + if (removedTargetKeys.has(targetKey)) continue + if (!(await isRemovableProjectionTarget({ + targetAbs, + sourceAbs, + sourcePath: options.sourcePath, + }))) { + continue + } + await fs.rm(targetAbs, { recursive: true, force: true }) + removedTargets.push(targetPath) + removedTargetKeys.add(targetKey) + } + } + + if (removedEntries.length > 0) { + await writeManifest(options.projectPath, manifest) + } + + return { + removedEntries, + removedTargets, + } +} + +function canOverwriteTarget( + targetKey: string, + manifest: MossProjectionManifest, +): boolean { + return Boolean(manifest.entries[targetKey]) +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function isEmptyRecord(value: unknown): boolean { + return isRecord(value) && Object.keys(value).length === 0 +} + +function isAdoptableEmptyMcpBridge(value: unknown): boolean { + if (!isRecord(value)) return false + + const entries = Object.entries(value) + if (entries.length === 0) return true + + return entries.every(([key, entryValue]) => { + if (key !== "mcpServers" && key !== "servers") return false + return isEmptyRecord(entryValue) + }) +} + +function isAdoptableMossGeneratedBridge(value: unknown): boolean { + if (!isRecord(value)) return false + const moss = value.moss + if (!isRecord(moss)) return false + return moss.generated === true && Array.isArray(moss.sources) +} + +function isAdoptableMossAdapterManifest(value: unknown): boolean { + if (!isRecord(value)) return false + return value.version === 1 && + value.generatedBy === "moss" && + Array.isArray(value.resources) +} + +async function canAdoptExistingTarget(params: { + targetAbs: string + targetKey: string +}): Promise { + try { + const stat = await fs.lstat(params.targetAbs) + if (!stat.isFile()) return false + const raw = await fs.readFile(params.targetAbs, "utf-8") + const parsed = JSON.parse(raw) + if (params.targetKey === ".mcp.json" && isAdoptableEmptyMcpBridge(parsed)) { + return true + } + return isAdoptableMossGeneratedBridge(parsed) || + isAdoptableMossAdapterManifest(parsed) + } catch { + return false + } +} + +async function readLinkTarget(filePath: string): Promise { + try { + return await fs.readlink(filePath) + } catch { + return null + } +} + +async function isSameSymlinkTarget( + linkPath: string, + sourcePath: string, +): Promise { + const existingTarget = await readLinkTarget(linkPath) + if (!existingTarget) return false + const resolvedExisting = path.resolve(path.dirname(linkPath), existingTarget) + return resolvedExisting === path.resolve(sourcePath) +} + +async function isRemovableProjectionTarget(params: { + targetAbs: string + sourceAbs?: string + sourcePath?: string +}): Promise { + try { + const stat = await fs.lstat(params.targetAbs) + if (stat.isSymbolicLink()) { + if (!params.sourceAbs) return true + return isSameSymlinkTarget(params.targetAbs, params.sourceAbs) + } + if (!stat.isFile()) return false + const raw = await fs.readFile(params.targetAbs, "utf-8") + const parsed = JSON.parse(raw) as { + moss?: { + generated?: boolean + sources?: string[] + } + } + if (parsed.moss?.generated !== true) return false + if (!params.sourcePath) return true + return Array.isArray(parsed.moss.sources) && parsed.moss.sources.includes(params.sourcePath) + } catch { + return false + } +} + +async function ensureWritableTarget( + params: { + targetAbs: string + targetKey: string + manifest: MossProjectionManifest + dryRun?: boolean + }, +): Promise<{ ok: true } | { ok: false; reason: string }> { + if (!(await pathExists(params.targetAbs))) return { ok: true } + if (canOverwriteTarget(params.targetKey, params.manifest)) return { ok: true } + if (await canAdoptExistingTarget(params)) return { ok: true } + + return { + ok: false, + reason: "Target exists and is not managed by Moss projection manifest.", + } +} + +async function materializeSymlink(params: { + projectPath: string + engineId: EngineResourceProjection["engineId"] + mapping: ResourcePathMapping + manifest: MossProjectionManifest + dryRun?: boolean +}): Promise { + if (!params.mapping.sourcePath || !params.mapping.targetPath) { + return { + engineId: params.engineId, + resourceId: params.mapping.resourceId, + action: params.mapping.action, + status: "unsupported", + reason: "Symlink projection requires sourcePath and targetPath.", + } + } + + const sourceAbs = resolveProjectionPath(params.projectPath, params.mapping.sourcePath) + const targetAbs = resolveProjectionPath(params.projectPath, params.mapping.targetPath) + const targetKey = normalizePathKey(params.projectPath, targetAbs) + + if (!(await pathExists(sourceAbs))) { + return { + engineId: params.engineId, + resourceId: params.mapping.resourceId, + action: "symlink", + sourcePath: params.mapping.sourcePath, + targetPath: params.mapping.targetPath, + status: "conflict", + reason: "Source path does not exist.", + } + } + + if (await isSameSymlinkTarget(targetAbs, sourceAbs)) { + const wasManaged = canOverwriteTarget(targetKey, params.manifest) + if (!params.dryRun && !wasManaged) { + params.manifest.entries[targetKey] = { + engineId: params.engineId, + resourceId: params.mapping.resourceId, + action: "symlink", + sourcePath: params.mapping.sourcePath, + targetPath: params.mapping.targetPath, + updatedAt: new Date().toISOString(), + } + } + return { + engineId: params.engineId, + resourceId: params.mapping.resourceId, + action: "symlink", + sourcePath: params.mapping.sourcePath, + targetPath: params.mapping.targetPath, + status: wasManaged ? "skipped" : "updated", + reason: wasManaged + ? "Symlink already points at the Moss source." + : "Existing symlink points at the Moss source and was adopted by the projection manifest.", + } + } + + const writable = await ensureWritableTarget({ + targetAbs, + targetKey, + manifest: params.manifest, + dryRun: params.dryRun, + }) + if (!writable.ok) { + return { + engineId: params.engineId, + resourceId: params.mapping.resourceId, + action: "symlink", + sourcePath: params.mapping.sourcePath, + targetPath: params.mapping.targetPath, + status: "conflict", + reason: writable.reason, + } + } + + let sourceStat + try { + sourceStat = await fs.stat(sourceAbs) + } catch { + return { + engineId: params.engineId, + resourceId: params.mapping.resourceId, + action: "symlink", + sourcePath: params.mapping.sourcePath, + targetPath: params.mapping.targetPath, + status: "conflict", + reason: "Unable to stat source path.", + } + } + + const wasManaged = canOverwriteTarget(targetKey, params.manifest) + const targetAlreadyExists = await pathExists(targetAbs) + + if (!params.dryRun) { + await fs.mkdir(path.dirname(targetAbs), { recursive: true }) + if (targetAlreadyExists) { + await fs.rm(targetAbs, { recursive: true, force: true }) + } + const type = sourceStat.isDirectory() + ? process.platform === "win32" ? "junction" : "dir" + : "file" + await fs.symlink(sourceAbs, targetAbs, type) + params.manifest.entries[targetKey] = { + engineId: params.engineId, + resourceId: params.mapping.resourceId, + action: "symlink", + sourcePath: params.mapping.sourcePath, + targetPath: params.mapping.targetPath, + updatedAt: new Date().toISOString(), + } + } + + return { + engineId: params.engineId, + resourceId: params.mapping.resourceId, + action: "symlink", + sourcePath: params.mapping.sourcePath, + targetPath: params.mapping.targetPath, + status: wasManaged || targetAlreadyExists ? "updated" : "created", + } +} + +function quoteTomlString(value: string): string { + return JSON.stringify(value) +} + +function appendTomlValue(lines: string[], key: string, value: unknown): void { + if (typeof value === "string") { + lines.push(`${key} = ${quoteTomlString(value)}`) + return + } + if (typeof value === "boolean" || typeof value === "number") { + lines.push(`${key} = ${String(value)}`) + return + } + if (Array.isArray(value) && value.every((item) => typeof item === "string")) { + lines.push(`${key} = [${value.map(quoteTomlString).join(", ")}]`) + } +} + +async function readMcpServers(sourceAbs: string): Promise> { + const raw = await fs.readFile(sourceAbs, "utf-8") + const parsed = JSON.parse(raw) as { + mcpServers?: Record + servers?: Record + } + return parsed.mcpServers ?? parsed.servers ?? {} +} + +async function buildCodexTomlBridge( + projectPath: string, + mappings: ResourcePathMapping[], +): Promise { + const lines = [ + "# Generated by Moss from .moss Unified Source.", + "# Do not edit this file directly; update .moss instead.", + "", + ] + + const mcpMappings = mappings.filter((mapping) => + mapping.sourcePath?.endsWith(path.join(".moss", "mcp", "config.json")) || + mapping.sourcePath?.endsWith(".moss/mcp/config.json"), + ) + const seenMcpSources = new Set() + for (const mapping of mcpMappings) { + if (!mapping.sourcePath) continue + if (seenMcpSources.has(mapping.sourcePath)) continue + seenMcpSources.add(mapping.sourcePath) + const sourceAbs = resolveProjectionPath(projectPath, mapping.sourcePath) + const servers = await readMcpServers(sourceAbs) + for (const [serverName, serverConfig] of Object.entries(servers)) { + const config = serverConfig && typeof serverConfig === "object" + ? serverConfig as Record + : {} + lines.push(`[mcp_servers.${serverName}]`) + appendTomlValue(lines, "command", config.command) + appendTomlValue(lines, "url", config.url) + appendTomlValue(lines, "args", config.args) + if (config.env && typeof config.env === "object" && !Array.isArray(config.env)) { + lines.push("") + lines.push(`[mcp_servers.${serverName}.env]`) + for (const [envKey, envValue] of Object.entries(config.env)) { + appendTomlValue(lines, envKey, envValue) + } + } + lines.push("") + } + } + + const providerMappings = mappings.filter((mapping) => + mapping.sourcePath?.endsWith(path.join(".moss", "providers.yaml")) || + mapping.sourcePath?.endsWith(".moss/providers.yaml"), + ) + const seenProviderSources = new Set() + for (const mapping of providerMappings) { + if (!mapping.sourcePath) continue + if (seenProviderSources.has(mapping.sourcePath)) continue + seenProviderSources.add(mapping.sourcePath) + lines.push(`# Moss provider routing source: ${mapping.sourcePath}`) + } + + return `${lines.join("\n").trimEnd()}\n` +} + +async function buildManagedBridgeContent( + projectPath: string, + targetAbs: string, + mappings: ResourcePathMapping[], +): Promise { + const targetName = path.basename(targetAbs) + const firstSourcePath = mappings.find((mapping) => mapping.sourcePath)?.sourcePath + const firstSourceAbs = firstSourcePath + ? resolveProjectionPath(projectPath, firstSourcePath) + : undefined + + if (targetName === ".mcp.json" && firstSourceAbs) { + const raw = await fs.readFile(firstSourceAbs, "utf-8") + const parsed = JSON.parse(raw) + return `${JSON.stringify(parsed, null, 2)}\n` + } + + if (targetName.endsWith(".toml")) { + return buildCodexTomlBridge(projectPath, mappings) + } + + return `${JSON.stringify({ + moss: { + generated: true, + sources: mappings.map((mapping) => mapping.sourcePath).filter(Boolean), + note: "Generated by Moss from .moss Unified Source. Update .moss instead.", + }, + }, null, 2)}\n` +} + +async function materializeManagedBridge(params: { + projectPath: string + projection: EngineResourceProjection + mappings: ResourcePathMapping[] + targetPath: string + manifest: MossProjectionManifest + dryRun?: boolean +}): Promise { + const targetAbs = resolveProjectionPath(params.projectPath, params.targetPath) + const targetKey = normalizePathKey(params.projectPath, targetAbs) + const wasManaged = canOverwriteTarget(targetKey, params.manifest) + const targetAlreadyExists = await pathExists(targetAbs) + const writable = await ensureWritableTarget({ + targetAbs, + targetKey, + manifest: params.manifest, + dryRun: params.dryRun, + }) + + if (!writable.ok) { + return params.mappings.map((mapping) => ({ + engineId: params.projection.engineId, + resourceId: mapping.resourceId, + action: mapping.action, + sourcePath: mapping.sourcePath, + targetPath: mapping.targetPath, + status: "conflict", + reason: writable.reason, + })) + } + + const content = await buildManagedBridgeContent( + params.projectPath, + targetAbs, + params.mappings, + ) + const contentHash = hashContent(content) + + if (!params.dryRun) { + await fs.mkdir(path.dirname(targetAbs), { recursive: true }) + await fs.writeFile(targetAbs, content, "utf-8") + } + + const now = new Date().toISOString() + for (const mapping of params.mappings) { + params.manifest.entries[targetKey] = { + engineId: params.projection.engineId, + resourceId: mapping.resourceId, + action: "managed-bridge", + sourcePath: mapping.sourcePath, + targetPath: params.targetPath, + contentHash, + updatedAt: now, + } + } + + const status = wasManaged || targetAlreadyExists ? "updated" : "created" + return params.mappings.map((mapping) => ({ + engineId: params.projection.engineId, + resourceId: mapping.resourceId, + action: "managed-bridge", + sourcePath: mapping.sourcePath, + targetPath: params.targetPath, + status, + })) +} + +async function materializeAdapterInjection(params: { + projectPath: string + projection: EngineResourceProjection + mappings: ResourcePathMapping[] + targetPath: string + manifest: MossProjectionManifest + dryRun?: boolean +}): Promise { + const targetAbs = resolveProjectionPath(params.projectPath, params.targetPath) + const adapterManifestAbs = path.join(targetAbs, ADAPTER_MANIFEST_NAME) + const adapterManifestKey = normalizePathKey(params.projectPath, adapterManifestAbs) + const wasManaged = canOverwriteTarget(adapterManifestKey, params.manifest) + const adapterManifestAlreadyExists = await pathExists(adapterManifestAbs) + + try { + const stat = await fs.stat(targetAbs) + if (!stat.isDirectory()) { + return params.mappings.map((mapping) => ({ + engineId: params.projection.engineId, + resourceId: mapping.resourceId, + action: mapping.action, + sourcePath: mapping.sourcePath, + targetPath: mapping.targetPath, + status: "conflict", + reason: "Adapter injection target exists and is not a directory.", + })) + } + } catch { + // Directory will be created below. + } + + const writable = await ensureWritableTarget({ + targetAbs: adapterManifestAbs, + targetKey: adapterManifestKey, + manifest: params.manifest, + dryRun: params.dryRun, + }) + + if (!writable.ok) { + return params.mappings.map((mapping) => ({ + engineId: params.projection.engineId, + resourceId: mapping.resourceId, + action: mapping.action, + sourcePath: mapping.sourcePath, + targetPath: mapping.targetPath, + status: "conflict", + reason: writable.reason, + })) + } + + const content = `${JSON.stringify({ + version: 1, + engineId: params.projection.engineId, + generatedBy: "moss", + resources: params.mappings.map((mapping) => ({ + resourceId: mapping.resourceId, + sourcePath: mapping.sourcePath, + targetPath: mapping.targetPath, + reason: mapping.reason, + })), + }, null, 2)}\n` + const contentHash = hashContent(content) + + if (!params.dryRun) { + await fs.mkdir(targetAbs, { recursive: true }) + await fs.writeFile(adapterManifestAbs, content, "utf-8") + } + + const now = new Date().toISOString() + for (const mapping of params.mappings) { + params.manifest.entries[adapterManifestKey] = { + engineId: params.projection.engineId, + resourceId: mapping.resourceId, + action: "adapter-inject", + sourcePath: mapping.sourcePath, + targetPath: params.targetPath, + contentHash, + updatedAt: now, + } + } + + const status = wasManaged || adapterManifestAlreadyExists ? "updated" : "created" + return params.mappings.map((mapping) => ({ + engineId: params.projection.engineId, + resourceId: mapping.resourceId, + action: "adapter-inject", + sourcePath: mapping.sourcePath, + targetPath: params.targetPath, + status, + })) +} + +function groupByTarget( + mappings: ResourcePathMapping[], +): Map { + const groups = new Map() + for (const mapping of mappings) { + if (!mapping.targetPath) continue + const group = groups.get(mapping.targetPath) ?? [] + group.push(mapping) + groups.set(mapping.targetPath, group) + } + return groups +} + +export async function materializeMossProjection( + options: MaterializeMossProjectionOptions, +): Promise { + const manifest = await readManifest(options.projectPath) + const results: MossProjectionMaterializeResult[] = [] + + const symlinks = options.projection.mappings.filter( + (mapping) => mapping.action === "symlink", + ) + for (const mapping of symlinks) { + const result = await materializeSymlink({ + projectPath: options.projectPath, + engineId: options.projection.engineId, + mapping, + manifest, + dryRun: options.dryRun, + }) + results.push(result) + } + + const managedBridgeGroups = groupByTarget( + options.projection.mappings.filter( + (mapping) => mapping.action === "managed-bridge", + ), + ) + for (const [targetPath, mappings] of managedBridgeGroups) { + results.push( + ...(await materializeManagedBridge({ + projectPath: options.projectPath, + projection: options.projection, + mappings, + targetPath, + manifest, + dryRun: options.dryRun, + })), + ) + } + + const adapterInjectionGroups = groupByTarget( + options.projection.mappings.filter( + (mapping) => mapping.action === "adapter-inject", + ), + ) + for (const [targetPath, mappings] of adapterInjectionGroups) { + results.push( + ...(await materializeAdapterInjection({ + projectPath: options.projectPath, + projection: options.projection, + mappings, + targetPath, + manifest, + dryRun: options.dryRun, + })), + ) + } + + for (const mapping of options.projection.mappings) { + if ( + mapping.action === "symlink" || + mapping.action === "managed-bridge" || + mapping.action === "adapter-inject" + ) { + continue + } + results.push({ + engineId: options.projection.engineId, + resourceId: mapping.resourceId, + action: mapping.action, + sourcePath: mapping.sourcePath, + targetPath: mapping.targetPath, + status: mapping.action === "native" ? "skipped" : "unsupported", + reason: mapping.reason, + }) + } + + if (!options.dryRun) { + await writeManifest(options.projectPath, manifest) + } + + return results +} diff --git a/src/main/lib/moss-source/provider-config.test.ts b/src/main/lib/moss-source/provider-config.test.ts new file mode 100644 index 000000000..00511bb50 --- /dev/null +++ b/src/main/lib/moss-source/provider-config.test.ts @@ -0,0 +1,413 @@ +import { describe, expect, test } from "bun:test" +import * as fs from "fs" +import * as os from "os" +import * as path from "path" +import { + readMossProviderConfig, + resolveMossProviderForEngine, + summarizeMossProviderReadResult, +} from "./provider-config" + +function makeFixture(providersYaml: string): string { + const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), "moss-provider-")) + fs.mkdirSync(path.join(projectPath, ".moss"), { recursive: true }) + fs.writeFileSync(path.join(projectPath, ".moss", "providers.yaml"), providersYaml) + return projectPath +} + +async function withEnv( + values: Record, + run: () => Promise, +): Promise { + const previous: Record = {} + for (const key of Object.keys(values)) { + previous[key] = process.env[key] + const value = values[key] + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } + + try { + return await run() + } finally { + for (const [key, value] of Object.entries(previous)) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } + } +} + +describe("Moss provider config", () => { + test("resolves one provider source into Claude and Codex env", async () => { + const projectPath = makeFixture(` +version: 1 +defaultProvider: moss +providers: + moss: + label: Moss Managed + mode: bundled-quota + runtime: any + apiKeyEnv: MOSS_TEST_API_KEY + baseUrlEnv: MOSS_TEST_BASE_URL + engines: + claude-code: + model: claude-opus-test + codex: + model: gpt-test/high + authMethod: openai-api-key + custom-acp: + model: custom-acp-test +`) + + try { + await withEnv( + { + MOSS_TEST_API_KEY: "sk-moss-test", + MOSS_TEST_BASE_URL: "https://api.moss.test/v1", + }, + async () => { + const claude = await resolveMossProviderForEngine({ + projectPath, + engineId: "claude-code", + }) + expect(claude.status).toBe("resolved") + expect(claude.providerId).toBe("moss") + expect(claude.model).toBe("claude-opus-test") + expect(claude.baseUrlSource).toBe("env") + expect(claude.baseUrlEnv).toBe("MOSS_TEST_BASE_URL") + expect(claude.env.ANTHROPIC_AUTH_TOKEN).toBe("sk-moss-test") + expect(claude.env.ANTHROPIC_BASE_URL).toBe("https://api.moss.test/v1") + + const codex = await resolveMossProviderForEngine({ + projectPath, + engineId: "codex", + }) + expect(codex.status).toBe("resolved") + expect(codex.model).toBe("gpt-test/high") + expect(codex.authMethod).toBe("openai-api-key") + expect(codex.baseUrlSource).toBe("env") + expect(codex.baseUrlEnv).toBe("MOSS_TEST_BASE_URL") + expect(codex.env.OPENAI_API_KEY).toBe("sk-moss-test") + expect(codex.env.CODEX_API_KEY).toBeUndefined() + expect(codex.env.OPENAI_BASE_URL).toBe("https://api.moss.test/v1") + + const customAcp = await resolveMossProviderForEngine({ + projectPath, + engineId: "custom-acp", + }) + expect(customAcp.status).toBe("resolved") + expect(customAcp.providerId).toBe("moss") + expect(customAcp.model).toBe("custom-acp-test") + expect(customAcp.baseUrlSource).toBe("env") + expect(customAcp.baseUrlEnv).toBe("MOSS_TEST_BASE_URL") + expect(customAcp.env.MOSS_CUSTOM_ACP_MODEL).toBe("custom-acp-test") + expect(customAcp.env.MOSS_CUSTOM_ACP_BASE_URL).toBe("https://api.moss.test/v1") + expect(customAcp.env.MOSS_CUSTOM_ACP_API_KEY).toBe("sk-moss-test") + }, + ) + } finally { + fs.rmSync(projectPath, { recursive: true, force: true }) + } + }) + + test("resolves engine-level provider base URLs with auditable URL sources", async () => { + const projectPath = makeFixture(` +version: 1 +defaultProvider: custom +providers: + custom: + label: Custom + mode: custom-url-key + runtime: any + baseUrl: https://shared.test/v1 + apiKeyEnv: MOSS_TEST_API_KEY + engines: + hermes: + model: moss-custom + baseUrl: https://hermes.test/v1 + claude-code: + model: opus-custom + baseUrlEnv: MOSS_CLAUDE_BASE_URL + codex: + model: gpt-custom/high + authMethod: openai-api-key + custom-acp: + model: custom-acp-custom +`) + + try { + await withEnv( + { + MOSS_TEST_API_KEY: "sk-moss-test", + MOSS_CLAUDE_BASE_URL: "https://claude.test/v1", + }, + async () => { + const hermes = await resolveMossProviderForEngine({ + projectPath, + engineId: "hermes", + }) + expect(hermes.status).toBe("resolved") + expect(hermes.baseUrl).toBe("https://hermes.test/v1") + expect(hermes.baseUrlSource).toBe("inline") + expect(hermes.baseUrlEnv).toBeUndefined() + expect(hermes.env.HERMES_BASE_URL).toBe("https://hermes.test/v1") + + const claude = await resolveMossProviderForEngine({ + projectPath, + engineId: "claude-code", + }) + expect(claude.status).toBe("resolved") + expect(claude.baseUrl).toBe("https://claude.test/v1") + expect(claude.baseUrlSource).toBe("env") + expect(claude.baseUrlEnv).toBe("MOSS_CLAUDE_BASE_URL") + expect(claude.env.ANTHROPIC_BASE_URL).toBe("https://claude.test/v1") + + const codex = await resolveMossProviderForEngine({ + projectPath, + engineId: "codex", + }) + expect(codex.status).toBe("resolved") + expect(codex.baseUrl).toBe("https://shared.test/v1") + expect(codex.baseUrlSource).toBe("inline") + expect(codex.baseUrlEnv).toBeUndefined() + expect(codex.env.OPENAI_BASE_URL).toBe("https://shared.test/v1") + + const customAcp = await resolveMossProviderForEngine({ + projectPath, + engineId: "custom-acp", + }) + expect(customAcp.status).toBe("resolved") + expect(customAcp.baseUrl).toBe("https://shared.test/v1") + expect(customAcp.baseUrlSource).toBe("inline") + expect(customAcp.env.MOSS_CUSTOM_ACP_MODEL).toBe("custom-acp-custom") + expect(customAcp.env.MOSS_CUSTOM_ACP_BASE_URL).toBe("https://shared.test/v1") + }, + ) + } finally { + fs.rmSync(projectPath, { recursive: true, force: true }) + } + }) + + test("summarizes provider config without exposing inline secrets", async () => { + const projectPath = makeFixture(` +version: 1 +defaultProvider: custom +credentialPolicy: + shareAcrossEngines: true +providers: + custom: + label: Custom + mode: custom-url-key + runtime: any + apiKey: sk-inline-secret + baseUrl: https://custom.test/v1 +`) + + try { + const readResult = await readMossProviderConfig(projectPath) + const summary = summarizeMossProviderReadResult(readResult) + + expect(summary.status).toBe("found") + expect(summary.defaultProvider).toBe("custom") + expect(summary.credentialPolicy?.shareAcrossEngines).toBe(true) + expect(summary.providers[0]?.hasInlineApiKey).toBe(true) + expect(summary.providers[0]?.hasStoredApiKey).toBe(false) + expect(JSON.stringify(summary)).not.toContain("sk-inline-secret") + } finally { + fs.rmSync(projectPath, { recursive: true, force: true }) + } + }) + + test("summarizes stored provider secrets without exposing the secret value", async () => { + const projectPath = makeFixture(` +version: 1 +defaultProvider: custom +credentialPolicy: + shareAcrossEngines: true +providers: + custom: + label: Custom + mode: custom-url-key + runtime: any + baseUrl: https://custom.test/v1 +`) + + try { + const readResult = await readMossProviderConfig(projectPath) + const summary = summarizeMossProviderReadResult(readResult, { + custom: { hasApiKey: true }, + }) + + expect(summary.providers[0]?.hasStoredApiKey).toBe(true) + expect(summary.providers[0]?.hasInlineApiKey).toBe(false) + expect(JSON.stringify(summary)).not.toContain("sk-") + } finally { + fs.rmSync(projectPath, { recursive: true, force: true }) + } + }) + + test("stored provider secret is shared across engines", async () => { + const projectPath = makeFixture(` +version: 1 +defaultProvider: custom +credentialPolicy: + shareAcrossEngines: true +providers: + custom: + label: Custom + mode: custom-url-key + runtime: any + baseUrl: https://custom.test/v1 + engines: + hermes: + model: moss-custom + claude-code: + model: opus-custom + codex: + model: gpt-custom/high + authMethod: openai-api-key + custom-acp: + model: custom-acp-custom +`) + + try { + const secretResolver = { + getSecret: async (providerId: string) => ({ + apiKey: providerId === "custom" ? "sk-stored-secret" : undefined, + }), + } + + const hermes = await resolveMossProviderForEngine({ + projectPath, + engineId: "hermes", + secretResolver, + }) + expect(hermes.status).toBe("resolved") + expect(hermes.apiKeySource).toBe("stored") + expect(hermes.baseUrlSource).toBe("inline") + expect(hermes.hasStoredApiKey).toBe(true) + expect(hermes.env.HERMES_API_KEY).toBe("sk-stored-secret") + expect(hermes.env.HERMES_BASE_URL).toBe("https://custom.test/v1") + + const claude = await resolveMossProviderForEngine({ + projectPath, + engineId: "claude-code", + secretResolver, + }) + expect(claude.status).toBe("resolved") + expect(claude.apiKeySource).toBe("stored") + expect(claude.baseUrlSource).toBe("inline") + expect(claude.env.ANTHROPIC_AUTH_TOKEN).toBe("sk-stored-secret") + expect(claude.env.ANTHROPIC_BASE_URL).toBe("https://custom.test/v1") + + const codex = await resolveMossProviderForEngine({ + projectPath, + engineId: "codex", + secretResolver, + }) + expect(codex.status).toBe("resolved") + expect(codex.apiKeySource).toBe("stored") + expect(codex.baseUrlSource).toBe("inline") + expect(codex.env.OPENAI_API_KEY).toBe("sk-stored-secret") + expect(codex.env.CODEX_API_KEY).toBeUndefined() + expect(codex.env.OPENAI_BASE_URL).toBe("https://custom.test/v1") + + const customAcp = await resolveMossProviderForEngine({ + projectPath, + engineId: "custom-acp", + secretResolver, + }) + expect(customAcp.status).toBe("resolved") + expect(customAcp.apiKeySource).toBe("stored") + expect(customAcp.baseUrlSource).toBe("inline") + expect(customAcp.env.MOSS_CUSTOM_ACP_API_KEY).toBe("sk-stored-secret") + expect(customAcp.env.MOSS_CUSTOM_ACP_BASE_URL).toBe("https://custom.test/v1") + } finally { + fs.rmSync(projectPath, { recursive: true, force: true }) + } + }) + + test("reports missing and parse-error provider sources", async () => { + const missingProjectPath = fs.mkdtempSync(path.join(os.tmpdir(), "moss-provider-missing-")) + try { + const missing = await resolveMossProviderForEngine({ + projectPath: missingProjectPath, + engineId: "codex", + }) + expect(missing.status).toBe("missing") + } finally { + fs.rmSync(missingProjectPath, { recursive: true, force: true }) + } + + const invalidProjectPath = makeFixture("providers: [") + try { + const invalid = await resolveMossProviderForEngine({ + projectPath: invalidProjectPath, + engineId: "claude-code", + }) + expect(invalid.status).toBe("parse-error") + expect(invalid.error).toBeTruthy() + } finally { + fs.rmSync(invalidProjectPath, { recursive: true, force: true }) + } + }) + + test("bootstraps default unified provider source when requested", async () => { + const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), "moss-provider-bootstrap-")) + try { + const plainRead = await readMossProviderConfig(projectPath) + expect(plainRead.status).toBe("missing") + + const hermes = await resolveMossProviderForEngine({ + projectPath, + engineId: "hermes", + createIfMissing: true, + }) + expect(hermes.status).toBe("resolved") + expect(hermes.providerId).toBe("moss") + expect(hermes.mode).toBe("bundled-quota") + expect(hermes.model).toBe("moss-default") + expect(hermes.env.HERMES_MODEL).toBe("moss-default") + expect(hermes.warnings).toEqual([]) + + const codex = await resolveMossProviderForEngine({ + projectPath, + engineId: "codex", + }) + expect(codex.status).toBe("resolved") + expect(codex.providerId).toBe("moss") + expect(codex.model).toBe("gpt-5.5/high") + expect(codex.env.CODEX_MODEL).toBe("gpt-5.5/high") + + const claude = await resolveMossProviderForEngine({ + projectPath, + engineId: "claude-code", + }) + expect(claude.status).toBe("resolved") + expect(claude.providerId).toBe("moss") + expect(claude.model).toBe("opus") + expect(claude.env.ANTHROPIC_MODEL).toBe("opus") + + const customAcp = await resolveMossProviderForEngine({ + projectPath, + engineId: "custom-acp", + }) + expect(customAcp.status).toBe("resolved") + expect(customAcp.providerId).toBe("moss") + expect(customAcp.model).toBe("custom-acp") + expect(customAcp.env.MOSS_CUSTOM_ACP_MODEL).toBe("custom-acp") + + expect( + fs.existsSync(path.join(projectPath, ".moss", "providers.yaml")), + ).toBe(true) + } finally { + fs.rmSync(projectPath, { recursive: true, force: true }) + } + }) +}) diff --git a/src/main/lib/moss-source/provider-config.ts b/src/main/lib/moss-source/provider-config.ts new file mode 100644 index 000000000..7359e8939 --- /dev/null +++ b/src/main/lib/moss-source/provider-config.ts @@ -0,0 +1,585 @@ +import * as crypto from "node:crypto" +import * as fs from "fs/promises" +import { createRequire } from "node:module" +import { AGENT_ENGINE_IDS, type AgentEngineId } from "../agent-runtime/types" +import { ensureMossSource } from "./bootstrap" +import { getMossSourceLayout } from "./layout" + +const require = createRequire(import.meta.url) +const yaml = require("js-yaml") as { + load(source: string): unknown + dump(value: unknown, options?: Record): string +} + +export interface MossProviderCredentialPolicy { + singleUserConfiguration?: boolean + allowCustomBaseUrl?: boolean + allowCustomApiKey?: boolean + shareAcrossEngines?: boolean +} + +export interface MossProviderEngineConfig { + model?: string + baseUrl?: string + baseUrlEnv?: string + apiKey?: string + apiKeyEnv?: string + authMethod?: string + env?: Record +} + +export interface MossProviderDefinition extends MossProviderEngineConfig { + id: string + label?: string + mode?: string + runtime?: string + models?: Record + engines?: Partial> +} + +export interface MossProviderConfig { + version: number + defaultProvider?: string + credentialPolicy?: MossProviderCredentialPolicy + providers: Record +} + +export interface MossProviderReadResult { + status: "found" | "missing" | "parse-error" + sourcePath: string + config?: MossProviderConfig + error?: string +} + +export interface MossProviderReadOptions { + createIfMissing?: boolean +} + +export interface MossProviderSecretResolver { + getSecret(providerId: string): Promise<{ + apiKey?: string + }> +} + +export type MossProviderValueSource = "inline" | "env" | "stored" + +export interface MossProviderSummary { + status: MossProviderReadResult["status"] + sourcePath: string + defaultProvider?: string + credentialPolicy?: MossProviderCredentialPolicy + providers: Array<{ + id: string + label?: string + mode?: string + runtime?: string + engines: AgentEngineId[] + hasInlineApiKey: boolean + hasStoredApiKey?: boolean + apiKeyEnv?: string + baseUrl?: string + baseUrlEnv?: string + }> + error?: string +} + +export interface ResolvedMossProvider { + status: "resolved" | "missing" | "unconfigured" | "parse-error" + sourcePath: string + providerId?: string + label?: string + mode?: string + runtime?: string + model?: string + baseUrl?: string + baseUrlSource?: MossProviderValueSource + baseUrlEnv?: string + apiKey?: string + apiKeySource?: MossProviderValueSource + apiKeyEnv?: string + hasStoredApiKey?: boolean + authMethod?: string + env: Record + warnings: string[] + reason?: string + error?: string +} + +function asRecord(value: unknown): Record | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) return undefined + return value as Record +} + +function asString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined +} + +function asBoolean(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined +} + +function asStringRecord(value: unknown): Record | undefined { + const record = asRecord(value) + if (!record) return undefined + + const result: Record = {} + for (const [key, entryValue] of Object.entries(record)) { + if (typeof entryValue === "string") { + result[key] = entryValue + } + } + + return Object.keys(result).length > 0 ? result : undefined +} + +function mergeStringRecord( + ...records: Array | undefined> +): Record { + return Object.assign({}, ...records.filter(Boolean)) +} + +function normalizeCredentialPolicy( + value: unknown, +): MossProviderCredentialPolicy | undefined { + const record = asRecord(value) + if (!record) return undefined + + return { + singleUserConfiguration: asBoolean(record.singleUserConfiguration), + allowCustomBaseUrl: asBoolean(record.allowCustomBaseUrl), + allowCustomApiKey: asBoolean(record.allowCustomApiKey), + shareAcrossEngines: asBoolean(record.shareAcrossEngines), + } +} + +function normalizeEngineConfig( + value: unknown, +): MossProviderEngineConfig | undefined { + const record = asRecord(value) + if (!record) return undefined + + return { + model: asString(record.model), + baseUrl: asString(record.baseUrl), + baseUrlEnv: asString(record.baseUrlEnv), + apiKey: asString(record.apiKey), + apiKeyEnv: asString(record.apiKeyEnv), + authMethod: asString(record.authMethod), + env: asStringRecord(record.env), + } +} + +function normalizeEngineConfigs( + value: unknown, +): Partial> | undefined { + const record = asRecord(value) + if (!record) return undefined + + const engines: Partial> = {} + for (const engineId of AGENT_ENGINE_IDS) { + const config = normalizeEngineConfig(record[engineId]) + if (config) engines[engineId] = config + } + + return Object.keys(engines).length > 0 ? engines : undefined +} + +function normalizeProvider( + id: string, + value: unknown, +): MossProviderDefinition | undefined { + const record = asRecord(value) + if (!record) return undefined + + return { + id, + label: asString(record.label), + mode: asString(record.mode), + runtime: asString(record.runtime), + model: asString(record.model), + baseUrl: asString(record.baseUrl), + baseUrlEnv: asString(record.baseUrlEnv), + apiKey: asString(record.apiKey), + apiKeyEnv: asString(record.apiKeyEnv), + authMethod: asString(record.authMethod), + env: asStringRecord(record.env), + models: asStringRecord(record.models), + engines: normalizeEngineConfigs(record.engines), + } +} + +function normalizeProviderConfig(parsed: unknown): MossProviderConfig { + const record = asRecord(parsed) + if (!record) { + throw new Error("providers.yaml must be a YAML object.") + } + + const providersRecord = asRecord(record.providers) + if (!providersRecord) { + throw new Error("providers.yaml must contain a providers object.") + } + + const providers: Record = {} + for (const [id, providerValue] of Object.entries(providersRecord)) { + const provider = normalizeProvider(id, providerValue) + if (provider) providers[id] = provider + } + + return { + version: + typeof record.version === "number" && Number.isFinite(record.version) + ? record.version + : 1, + defaultProvider: + asString(record.defaultProvider) || asString(record.default), + credentialPolicy: normalizeCredentialPolicy(record.credentialPolicy), + providers, + } +} + +async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +export async function readMossProviderConfig( + projectPath: string, + options: MossProviderReadOptions = {}, +): Promise { + const sourcePath = getMossSourceLayout(projectPath).providersConfig + if (options.createIfMissing && !(await fileExists(sourcePath))) { + await ensureMossSource({ projectPath }) + } + + if (!(await fileExists(sourcePath))) { + return { status: "missing", sourcePath } + } + + try { + const raw = await fs.readFile(sourcePath, "utf-8") + const parsed = yaml.load(raw) + return { + status: "found", + sourcePath, + config: normalizeProviderConfig(parsed), + } + } catch (error) { + return { + status: "parse-error", + sourcePath, + error: error instanceof Error ? error.message : String(error), + } + } +} + +export async function writeMossProviderConfig( + projectPath: string, + config: MossProviderConfig, +): Promise { + await ensureMossSource({ projectPath }) + const sourcePath = getMossSourceLayout(projectPath).providersConfig + const yamlText = yaml.dump(config, { + lineWidth: 100, + noRefs: true, + sortKeys: false, + }) + await fs.writeFile(sourcePath, yamlText, "utf-8") + return readMossProviderConfig(projectPath) +} + +function providerSupportsEngine( + provider: MossProviderDefinition, + engineId: AgentEngineId, +): boolean { + if (provider.engines?.[engineId]) return true + if (provider.runtime === "any") return true + if (provider.runtime === engineId) return true + if (provider.runtime === "claude" && engineId === "claude-code") return true + if (provider.runtime === "openai" && engineId === "codex") return true + + // Moss managed quota is a product-level provider. It can front any engine + // once the service-side router is wired, even if the current local runtime + // still needs an engine-specific adapter. + return provider.mode === "bundled-quota" +} + +function pickProvider( + config: MossProviderConfig, + engineId: AgentEngineId, +): MossProviderDefinition | undefined { + const requestedProviderId = asString(process.env.MOSS_PROVIDER) + const preferredProviderId = requestedProviderId || config.defaultProvider + const preferred = preferredProviderId + ? config.providers[preferredProviderId] + : undefined + + if (preferred && providerSupportsEngine(preferred, engineId)) { + return preferred + } + + return Object.values(config.providers).find((provider) => + providerSupportsEngine(provider, engineId), + ) +} + +function readEnvReference(name: string | undefined): string | undefined { + if (!name) return undefined + return asString(process.env[name]) +} + +function pickFirst(...values: Array): string | undefined { + return values.find((value) => typeof value === "string" && value.length > 0) +} + +function resolveProviderValue(params: { + provider: MossProviderDefinition + engineConfig?: MossProviderEngineConfig + key: "apiKey" | "baseUrl" + envKey: "apiKeyEnv" | "baseUrlEnv" + storedValue?: string +}): { + value?: string + envName?: string + source?: MossProviderValueSource +} { + if (params.storedValue) { + return { value: params.storedValue, source: "stored" } + } + + const envName = pickFirst( + params.engineConfig?.[params.envKey], + params.provider[params.envKey], + ) + const envValue = readEnvReference(envName) + if (envValue) { + return { value: envValue, envName, source: "env" } + } + + const inlineValue = pickFirst( + params.engineConfig?.[params.key], + params.provider[params.key], + ) + if (inlineValue) { + return { value: inlineValue, envName, source: "inline" } + } + + return { envName } +} + +function resolveModel(params: { + provider: MossProviderDefinition + engineId: AgentEngineId + engineConfig?: MossProviderEngineConfig + requestedModelId?: string +}): string | undefined { + return pickFirst( + params.engineConfig?.model, + params.provider.models?.[params.engineId], + params.provider.model, + params.requestedModelId, + ) +} + +function buildEngineEnv(params: { + engineId: AgentEngineId + provider: MossProviderDefinition + model?: string + baseUrl?: string + apiKey?: string + authMethod?: string +}): Record { + const env: Record = { + MOSS_PROVIDER_ID: params.provider.id, + } + if (params.provider.mode) env.MOSS_PROVIDER_MODE = params.provider.mode + if (params.provider.label) env.MOSS_PROVIDER_LABEL = params.provider.label + if (params.model) env.MOSS_MODEL = params.model + if (params.baseUrl) env.MOSS_BASE_URL = params.baseUrl + if (params.apiKey) env.MOSS_API_KEY = params.apiKey + + if (params.engineId === "claude-code") { + if (params.model) env.ANTHROPIC_MODEL = params.model + if (params.baseUrl) env.ANTHROPIC_BASE_URL = params.baseUrl + if (params.apiKey) env.ANTHROPIC_AUTH_TOKEN = params.apiKey + } else if (params.engineId === "codex") { + if (params.model) env.CODEX_MODEL = params.model + if (params.baseUrl) { + env.CODEX_BASE_URL = params.baseUrl + env.OPENAI_BASE_URL = params.baseUrl + } + if (params.apiKey) { + if (params.authMethod === "openai-api-key") { + env.OPENAI_API_KEY = params.apiKey + } else { + env.CODEX_API_KEY = params.apiKey + } + } + } else if (params.engineId === "hermes") { + if (params.model) env.HERMES_MODEL = params.model + if (params.baseUrl) env.HERMES_BASE_URL = params.baseUrl + if (params.apiKey) env.HERMES_API_KEY = params.apiKey + } else if (params.engineId === "custom-acp") { + if (params.model) env.MOSS_CUSTOM_ACP_MODEL = params.model + if (params.baseUrl) env.MOSS_CUSTOM_ACP_BASE_URL = params.baseUrl + if (params.apiKey) env.MOSS_CUSTOM_ACP_API_KEY = params.apiKey + } + + return env +} + +export async function resolveMossProviderForEngine(params: { + projectPath: string + engineId: AgentEngineId + requestedModelId?: string + createIfMissing?: boolean + secretResolver?: MossProviderSecretResolver +}): Promise { + const readResult = await readMossProviderConfig(params.projectPath, { + createIfMissing: params.createIfMissing, + }) + if (readResult.status === "missing") { + return { + status: "missing", + sourcePath: readResult.sourcePath, + env: {}, + warnings: [], + reason: "No .moss/providers.yaml found.", + } + } + if (readResult.status === "parse-error" || !readResult.config) { + return { + status: "parse-error", + sourcePath: readResult.sourcePath, + env: {}, + warnings: [], + error: readResult.error, + } + } + + const provider = pickProvider(readResult.config, params.engineId) + if (!provider) { + return { + status: "unconfigured", + sourcePath: readResult.sourcePath, + env: {}, + warnings: [ + `No Moss provider is configured for ${params.engineId}.`, + ], + reason: `No provider supports ${params.engineId}.`, + } + } + + const engineConfig = provider.engines?.[params.engineId] + const storedSecret = params.secretResolver + ? await params.secretResolver.getSecret(provider.id) + : undefined + const model = resolveModel({ + provider, + engineId: params.engineId, + engineConfig, + requestedModelId: params.requestedModelId, + }) + const baseUrl = resolveProviderValue({ + provider, + engineConfig, + key: "baseUrl", + envKey: "baseUrlEnv", + }) + const apiKey = resolveProviderValue({ + provider, + engineConfig, + key: "apiKey", + envKey: "apiKeyEnv", + storedValue: storedSecret?.apiKey, + }) + const authMethod = pickFirst(engineConfig?.authMethod, provider.authMethod) + const env = mergeStringRecord( + provider.env, + engineConfig?.env, + buildEngineEnv({ + engineId: params.engineId, + provider, + model, + baseUrl: baseUrl.value, + apiKey: apiKey.value, + authMethod, + }), + ) + const warnings: string[] = [] + + if (apiKey.envName && !apiKey.value) { + warnings.push(`Provider ${provider.id} references unset ${apiKey.envName}.`) + } + if (baseUrl.envName && !baseUrl.value) { + warnings.push(`Provider ${provider.id} references unset ${baseUrl.envName}.`) + } + + return { + status: "resolved", + sourcePath: readResult.sourcePath, + providerId: provider.id, + label: provider.label, + mode: provider.mode, + runtime: provider.runtime, + model, + baseUrl: baseUrl.value, + baseUrlSource: baseUrl.source, + baseUrlEnv: baseUrl.envName, + apiKey: apiKey.value, + apiKeySource: apiKey.source, + apiKeyEnv: apiKey.envName, + hasStoredApiKey: Boolean(storedSecret?.apiKey), + authMethod, + env, + warnings, + } +} + +export function getMossProviderFingerprint( + provider: ResolvedMossProvider | null | undefined, +): string | null { + if (!provider || provider.status !== "resolved") return null + return crypto + .createHash("sha256") + .update( + JSON.stringify({ + providerId: provider.providerId, + mode: provider.mode, + model: provider.model, + baseUrl: provider.baseUrl, + apiKey: provider.apiKey, + authMethod: provider.authMethod, + }), + ) + .digest("hex") +} + +export function summarizeMossProviderReadResult( + readResult: MossProviderReadResult, + storedSecrets: Record = {}, +): MossProviderSummary { + const config = readResult.config + return { + status: readResult.status, + sourcePath: readResult.sourcePath, + defaultProvider: config?.defaultProvider, + credentialPolicy: config?.credentialPolicy, + providers: Object.values(config?.providers ?? {}).map((provider) => ({ + id: provider.id, + label: provider.label, + mode: provider.mode, + runtime: provider.runtime, + engines: Object.keys(provider.engines ?? {}) as AgentEngineId[], + hasInlineApiKey: Boolean(provider.apiKey), + apiKeyEnv: provider.apiKeyEnv, + hasStoredApiKey: Boolean(storedSecrets[provider.id]?.hasApiKey), + baseUrl: provider.baseUrl, + baseUrlEnv: provider.baseUrlEnv, + })), + error: readResult.error, + } +} diff --git a/src/main/lib/moss-source/provider-secrets.ts b/src/main/lib/moss-source/provider-secrets.ts new file mode 100644 index 000000000..bbb407da6 --- /dev/null +++ b/src/main/lib/moss-source/provider-secrets.ts @@ -0,0 +1,106 @@ +import * as fs from "fs/promises" +import * as path from "path" +import { app, safeStorage } from "electron" + +interface MossProviderSecretEntry { + apiKey?: string + updatedAt: string +} + +interface MossProviderSecretStore { + version: 1 + providers: Record +} + +const EMPTY_STORE: MossProviderSecretStore = { + version: 1, + providers: {}, +} + +function getStorePath(): string { + return path.join(app.getPath("userData"), "moss-provider-secrets.dat") +} + +function cloneStore(store: MossProviderSecretStore): MossProviderSecretStore { + return { + version: 1, + providers: { ...store.providers }, + } +} + +async function readStore(): Promise { + const storePath = getStorePath() + try { + const encrypted = await fs.readFile(storePath) + const raw = safeStorage.isEncryptionAvailable() + ? safeStorage.decryptString(encrypted) + : encrypted.toString("utf-8") + const parsed = JSON.parse(raw) as MossProviderSecretStore + if (!parsed || typeof parsed !== "object" || !parsed.providers) { + return cloneStore(EMPTY_STORE) + } + return { + version: 1, + providers: { ...parsed.providers }, + } + } catch { + return cloneStore(EMPTY_STORE) + } +} + +async function writeStore(store: MossProviderSecretStore): Promise { + const storePath = getStorePath() + await fs.mkdir(path.dirname(storePath), { recursive: true }) + + const raw = JSON.stringify(store) + if (safeStorage.isEncryptionAvailable()) { + await fs.writeFile(storePath, safeStorage.encryptString(raw)) + return + } + + console.warn( + "[moss-provider-secrets] safeStorage unavailable; storing provider secrets without OS encryption.", + ) + await fs.writeFile(storePath, raw, "utf-8") +} + +export async function getMossProviderSecret(providerId: string): Promise<{ + apiKey?: string +}> { + const store = await readStore() + const entry = store.providers[providerId] + return { + apiKey: entry?.apiKey, + } +} + +export async function hasMossProviderSecret(providerId: string): Promise { + const secret = await getMossProviderSecret(providerId) + return Boolean(secret.apiKey) +} + +export async function setMossProviderSecret(params: { + providerId: string + apiKey?: string | null +}): Promise { + const store = await readStore() + const existing = store.providers[params.providerId] ?? { + updatedAt: new Date().toISOString(), + } + const trimmedApiKey = params.apiKey?.trim() + + if (!trimmedApiKey) { + delete existing.apiKey + } else { + existing.apiKey = trimmedApiKey + } + + existing.updatedAt = new Date().toISOString() + if (existing.apiKey) { + store.providers[params.providerId] = existing + } else { + delete store.providers[params.providerId] + } + + await writeStore(store) +} diff --git a/src/main/lib/moss-source/registry.ts b/src/main/lib/moss-source/registry.ts new file mode 100644 index 000000000..42f9ca3d9 --- /dev/null +++ b/src/main/lib/moss-source/registry.ts @@ -0,0 +1,361 @@ +import * as fs from "fs/promises" +import * as path from "path" +import matter from "gray-matter" +import type { SharedResource } from "../shared-resources/types" +import { getMossSourceLayout, toMossProjectPath } from "./layout" + +function resourceId(parts: Array): string { + return parts.filter(Boolean).join(":") +} + +async function pathExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +function isSafeEntryName(name: string): boolean { + return !name.includes("..") && !name.includes("/") && !name.includes("\\") +} + +async function readFrontmatter(filePath: string): Promise<{ + name?: string + description?: string + data: Record +}> { + const raw = await fs.readFile(filePath, "utf-8") + const parsed = matter(raw) + return { + name: typeof parsed.data.name === "string" ? parsed.data.name : undefined, + description: + typeof parsed.data.description === "string" + ? parsed.data.description + : undefined, + data: parsed.data as Record, + } +} + +function withMossMetadata( + metadata: Record, +): Record { + return { + ...metadata, + sourceSystem: "moss-unified-source", + } +} + +async function addFileResource( + resources: SharedResource[], + params: { + projectPath: string + filePath: string + kind: SharedResource["kind"] + name: string + description: string + metadata: Record + }, +) { + if (!(await pathExists(params.filePath))) return + + resources.push({ + id: resourceId(["moss", params.kind, params.name]), + kind: params.kind, + name: params.name, + scope: "moss", + path: toMossProjectPath(params.projectPath, params.filePath), + description: params.description, + enabled: true, + metadata: withMossMetadata(params.metadata), + }) +} + +async function scanMossSkills( + projectPath: string, + skillsRoot: string, +): Promise { + if (!(await pathExists(skillsRoot))) return [] + const entries = await fs.readdir(skillsRoot, { withFileTypes: true }) + const resources: SharedResource[] = [] + + for (const entry of entries) { + if (!entry.isDirectory() || !isSafeEntryName(entry.name)) continue + const skillPath = path.join(skillsRoot, entry.name, "SKILL.md") + if (!(await pathExists(skillPath))) continue + + try { + const parsed = await readFrontmatter(skillPath) + const name = parsed.name || entry.name + resources.push({ + id: resourceId(["moss", "skill", name]), + kind: "skill", + name, + scope: "moss", + path: toMossProjectPath(projectPath, skillPath), + description: parsed.description, + enabled: true, + metadata: withMossMetadata({ + mossRole: "skill", + entryName: entry.name, + projectionUnit: "directory", + }), + }) + } catch { + continue + } + } + + return resources +} + +async function scanMossMemoryEntries( + projectPath: string, + memoryRoot: string, +): Promise { + if (!(await pathExists(memoryRoot))) return [] + const entries = await fs.readdir(memoryRoot, { withFileTypes: true }) + const resources: SharedResource[] = [] + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith(".md") || !isSafeEntryName(entry.name)) { + continue + } + + const filePath = path.join(memoryRoot, entry.name) + try { + const parsed = await readFrontmatter(filePath) + const fallbackName = entry.name.replace(/\.md$/, "") + const name = parsed.name || fallbackName + resources.push({ + id: resourceId(["moss", "memory", name]), + kind: "memory", + name, + scope: "moss", + path: toMossProjectPath(projectPath, filePath), + description: parsed.description, + enabled: true, + metadata: withMossMetadata({ + mossRole: "memory-entry", + entryName: entry.name, + projectionUnit: "file", + }), + }) + } catch { + continue + } + } + + return resources +} + +async function scanMossMarkdownEntries( + projectPath: string, + root: string, + kind: "subagent" | "hook" | "plugin", + role: string, +): Promise { + if (!(await pathExists(root))) return [] + const entries = await fs.readdir(root, { withFileTypes: true }) + const resources: SharedResource[] = [] + + for (const entry of entries) { + if (!isSafeEntryName(entry.name)) continue + const entryPath = path.join(root, entry.name) + if (entry.isDirectory()) { + resources.push({ + id: resourceId(["moss", kind, entry.name]), + kind, + name: entry.name, + scope: "moss", + path: toMossProjectPath(projectPath, entryPath), + enabled: true, + metadata: withMossMetadata({ + mossRole: role, + entryName: entry.name, + projectionUnit: "directory", + }), + }) + continue + } + + if (!entry.isFile() || !entry.name.endsWith(".md")) continue + try { + const parsed = await readFrontmatter(entryPath) + const fallbackName = entry.name.replace(/\.md$/, "") + const name = parsed.name || fallbackName + const hookMetadata = + kind === "hook" + ? { + event: + typeof parsed.data.event === "string" + ? parsed.data.event + : undefined, + command: + typeof parsed.data.command === "string" + ? parsed.data.command + : undefined, + hookEnabled: parsed.data.enabled !== false, + } + : {} + const subagentMetadata = + kind === "subagent" + ? { + command: + typeof parsed.data.command === "string" + ? parsed.data.command + : undefined, + subagentEnabled: parsed.data.enabled !== false, + } + : {} + resources.push({ + id: resourceId(["moss", kind, name]), + kind, + name, + scope: "moss", + path: toMossProjectPath(projectPath, entryPath), + description: parsed.description, + enabled: + kind === "hook" || kind === "subagent" + ? parsed.data.enabled !== false + : true, + metadata: withMossMetadata({ + mossRole: role, + entryName: entry.name, + projectionUnit: "file", + ...hookMetadata, + ...subagentMetadata, + }), + }) + } catch { + continue + } + } + + return resources +} + +async function readMossMcpResources( + projectPath: string, + mcpConfigPath: string, +): Promise { + if (!(await pathExists(mcpConfigPath))) return [] + + try { + const raw = await fs.readFile(mcpConfigPath, "utf-8") + const parsed = JSON.parse(raw) as { + mcpServers?: Record + servers?: Record + } + const servers = parsed.mcpServers ?? parsed.servers ?? {} + const entries = Object.entries(servers) + + if (entries.length === 0) { + return [{ + id: "moss:mcp:config", + kind: "mcp", + name: "Moss MCP config", + scope: "moss", + path: toMossProjectPath(projectPath, mcpConfigPath), + description: "Moss Unified Source MCP config.", + enabled: true, + metadata: withMossMetadata({ + mossRole: "mcp-config", + serverCount: 0, + }), + }] + } + + return entries.map(([serverName, serverConfig]) => ({ + id: resourceId(["moss", "mcp", serverName]), + kind: "mcp" as const, + name: serverName, + scope: "moss" as const, + path: toMossProjectPath(projectPath, mcpConfigPath), + description: "Moss Unified Source MCP server.", + enabled: true, + metadata: withMossMetadata({ + mossRole: "mcp-config", + serverConfig, + }), + })) + } catch (error) { + return [{ + id: "moss:mcp:config-error", + kind: "mcp", + name: "Moss MCP config parse error", + scope: "moss", + path: toMossProjectPath(projectPath, mcpConfigPath), + description: "Moss MCP config exists but could not be parsed.", + enabled: false, + metadata: withMossMetadata({ + mossRole: "mcp-config", + error: error instanceof Error ? error.message : String(error), + }), + }] + } +} + +export async function discoverMossSourceResources( + projectPath: string, +): Promise { + const layout = getMossSourceLayout(projectPath) + if (!(await pathExists(layout.root))) return [] + + const resources: SharedResource[] = [] + await addFileResource(resources, { + projectPath, + filePath: layout.sourceInstruction, + kind: "instruction", + name: "moss.md", + description: "Moss canonical project rules and operating instructions.", + metadata: { + mossRole: "source-instruction", + projectionTarget: "CLAUDE.md and AGENTS.md", + }, + }) + await addFileResource(resources, { + projectPath, + filePath: layout.workspaceConfig, + kind: "instruction", + name: "workspace.yaml", + description: "Moss canonical workspace configuration.", + metadata: { + mossRole: "workspace-config", + }, + }) + await addFileResource(resources, { + projectPath, + filePath: layout.memoryRoot, + kind: "memory", + name: "Moss memory", + description: "Moss canonical memory root shared by all engines.", + metadata: { + mossRole: "memory-root", + projectionTarget: "Claude, Codex, and Hermes memory roots", + }, + }) + await addFileResource(resources, { + projectPath, + filePath: layout.providersConfig, + kind: "provider", + name: "providers.yaml", + description: "Moss canonical provider routing and credentials mapping.", + metadata: { + mossRole: "provider-config", + projectionTarget: "engine-native auth/config bridges", + }, + }) + + resources.push( + ...(await scanMossMemoryEntries(projectPath, layout.memoryRoot)), + ...(await scanMossSkills(projectPath, layout.skillsRoot)), + ...(await readMossMcpResources(projectPath, layout.mcpConfig)), + ...(await scanMossMarkdownEntries(projectPath, layout.pluginsRoot, "plugin", "plugin")), + ...(await scanMossMarkdownEntries(projectPath, layout.hooksRoot, "hook", "hook")), + ...(await scanMossMarkdownEntries(projectPath, layout.subagentsRoot, "subagent", "subagent")), + ) + + return resources +} diff --git a/src/main/lib/moss-source/runtime-materializer.ts b/src/main/lib/moss-source/runtime-materializer.ts new file mode 100644 index 000000000..8f37d05b5 --- /dev/null +++ b/src/main/lib/moss-source/runtime-materializer.ts @@ -0,0 +1,358 @@ +import { AGENT_ENGINE_IDS, type AgentEngineId } from "../agent-runtime/types" +import * as fs from "fs/promises" +import * as path from "path" +import { buildGovernedResourceProjection } from "../shared-resources/governance" +import type { EngineResourceProjection } from "../shared-resources/types" +import { ensureMossSource } from "./bootstrap" +import { + materializeMossProjection, + type MossProjectionMaterializeResult, + type MossProjectionMaterializeStatus, +} from "./projection" +import { discoverMossSourceResources } from "./registry" + +export interface MossEngineProjectionSummary { + created: number + updated: number + skipped: number + conflict: number + unsupported: number + total: number +} + +export interface MaterializedMossEngineProjection { + engineId: AgentEngineId + projectPath: string + projectionStatus: EngineResourceProjection["status"] | "skipped" + warnings: string[] + results: MossProjectionMaterializeResult[] + summary: MossEngineProjectionSummary + reason?: string +} + +export interface FailedMossEngineProjection { + engineId: AgentEngineId + projectPath: string + projectionStatus: "skipped" + warnings: string[] + results: [] + summary: { + created: 0 + updated: 0 + skipped: 0 + conflict: 0 + unsupported: 0 + total: 0 + } + reason: string +} + +export type MossEngineProjectionResult = + | MaterializedMossEngineProjection + | FailedMossEngineProjection + +export interface MaterializeMossEngineProjectionOptions { + projectPath: string + engineId: AgentEngineId + dryRun?: boolean + createIfMissing?: boolean +} + +export interface MaterializeMossWorkspaceProjectionsOptions { + projectPath: string + engines?: readonly AgentEngineId[] + dryRun?: boolean + createIfMissing?: boolean +} + +export interface MaterializedMossWorkspaceProjections { + projectPath: string + dryRun: boolean + projections: MossEngineProjectionResult[] +} + +export interface MossWorkspaceSourceLinkResult { + sourceProjectPath: string + workspacePath: string + sourceRoot: string + targetRoot: string + status: "created" | "skipped" | "conflict" + created: string[] + skipped: string[] + conflicts: Array<{ path: string; reason: string }> + reason?: string +} + +export interface LinkMossSourceIntoWorkspaceOptions { + sourceProjectPath: string + workspacePath: string +} + +const MOSS_LINK_ENTRIES = [ + { source: "source", type: "dir" }, + { source: "memory", type: "dir" }, + { source: "skills", type: "dir" }, + { source: "mcp", type: "dir" }, + { source: "plugins", type: "dir" }, + { source: "hooks", type: "dir" }, + { source: "subagents", type: "dir" }, + { source: "providers.yaml", type: "file" }, +] as const + +async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +async function safeRealpath(filePath: string): Promise { + try { + return await fs.realpath(filePath) + } catch { + return null + } +} + +async function linkPointsTo(linkPath: string, expectedTarget: string): Promise { + try { + const target = await fs.readlink(linkPath) + return path.resolve(path.dirname(linkPath), target) === path.resolve(expectedTarget) + } catch { + return false + } +} + +async function hasLocalMossSource(targetRoot: string, sourceRoot: string): Promise { + const targetSource = path.join(targetRoot, "source") + if (!(await fileExists(path.join(targetSource, "moss.md")))) return false + return !(await linkPointsTo(targetSource, path.join(sourceRoot, "source"))) +} + +function summarizeProjectionResults( + results: MossProjectionMaterializeResult[], +): MossEngineProjectionSummary { + const summary: Record = { + created: 0, + updated: 0, + skipped: 0, + conflict: 0, + unsupported: 0, + } + + for (const result of results) { + summary[result.status] += 1 + } + + return { + ...summary, + total: results.length, + } +} + +export async function materializeMossEngineProjection( + options: MaterializeMossEngineProjectionOptions, +): Promise { + if (options.createIfMissing) { + await ensureMossSource({ projectPath: options.projectPath }) + } + + const resources = await discoverMossSourceResources(options.projectPath) + if (resources.length === 0) { + return { + engineId: options.engineId, + projectPath: options.projectPath, + projectionStatus: "skipped", + warnings: [], + results: [], + summary: summarizeProjectionResults([]), + reason: "No .moss Unified Source was found for this project.", + } + } + + const snapshot = buildGovernedResourceProjection({ + projectPath: options.projectPath, + resources, + }) + const projection = snapshot.projections.find( + (item) => item.engineId === options.engineId, + ) + + if (!projection) { + return { + engineId: options.engineId, + projectPath: options.projectPath, + projectionStatus: "skipped", + warnings: [], + results: [], + summary: summarizeProjectionResults([]), + reason: `No projection is registered for ${options.engineId}.`, + } + } + + const results = await materializeMossProjection({ + projectPath: options.projectPath, + projection, + dryRun: options.dryRun, + }) + + return { + engineId: options.engineId, + projectPath: options.projectPath, + projectionStatus: projection.status, + warnings: projection.warnings, + results, + summary: summarizeProjectionResults(results), + } +} + +export async function materializeMossEngineProjectionSafely( + options: MaterializeMossEngineProjectionOptions, +): Promise { + try { + return await materializeMossEngineProjection(options) + } catch (error) { + return { + engineId: options.engineId, + projectPath: options.projectPath, + projectionStatus: "skipped", + warnings: [], + results: [], + summary: { + created: 0, + updated: 0, + skipped: 0, + conflict: 0, + unsupported: 0, + total: 0, + }, + reason: error instanceof Error ? error.message : String(error), + } + } +} + +export async function materializeMossWorkspaceProjections( + options: MaterializeMossWorkspaceProjectionsOptions, +): Promise { + if (options.createIfMissing) { + await ensureMossSource({ projectPath: options.projectPath }) + } + + const engines = options.engines ?? AGENT_ENGINE_IDS + const projections: MossEngineProjectionResult[] = [] + for (const engineId of engines) { + projections.push( + await materializeMossEngineProjectionSafely({ + projectPath: options.projectPath, + engineId, + dryRun: options.dryRun, + }), + ) + } + + return { + projectPath: options.projectPath, + dryRun: Boolean(options.dryRun), + projections, + } +} + +export async function linkMossSourceIntoWorkspace( + options: LinkMossSourceIntoWorkspaceOptions, +): Promise { + const sourceProjectPath = path.resolve(options.sourceProjectPath) + const workspacePath = path.resolve(options.workspacePath) + const sourceRoot = path.join(sourceProjectPath, ".moss") + const targetRoot = path.join(workspacePath, ".moss") + const result: MossWorkspaceSourceLinkResult = { + sourceProjectPath, + workspacePath, + sourceRoot, + targetRoot, + status: "skipped", + created: [], + skipped: [], + conflicts: [], + } + + if (sourceProjectPath === workspacePath) { + result.reason = "Workspace is the source project." + return result + } + + if (!(await fileExists(sourceRoot))) { + result.reason = "No source .moss Unified Source was found." + return result + } + + const sourceRealpath = await safeRealpath(sourceRoot) + const targetRealpath = await safeRealpath(targetRoot) + if (sourceRealpath && targetRealpath && sourceRealpath === targetRealpath) { + result.reason = "Workspace .moss already points at the source project." + return result + } + + try { + const targetStat = await fs.lstat(targetRoot) + if (!targetStat.isDirectory()) { + result.status = "conflict" + result.conflicts.push({ + path: targetRoot, + reason: "Workspace .moss exists and is not a directory.", + }) + return result + } + } catch { + await fs.mkdir(targetRoot, { recursive: true }) + } + + if (await hasLocalMossSource(targetRoot, sourceRoot)) { + result.reason = "Workspace already has a local .moss source." + return result + } + + for (const entry of MOSS_LINK_ENTRIES) { + const sourcePath = path.join(sourceRoot, entry.source) + const targetPath = path.join(targetRoot, entry.source) + + if (!(await fileExists(sourcePath))) { + result.skipped.push(entry.source) + continue + } + + if (await linkPointsTo(targetPath, sourcePath)) { + result.skipped.push(entry.source) + continue + } + + if (await fileExists(targetPath)) { + result.conflicts.push({ + path: targetPath, + reason: "Target exists and is not linked to the source .moss entry.", + }) + continue + } + + await fs.symlink( + sourcePath, + targetPath, + entry.type === "dir" && process.platform === "win32" ? "junction" : undefined, + ) + result.created.push(entry.source) + } + + if (result.conflicts.length > 0) { + result.status = "conflict" + return result + } + + if (result.created.length > 0) { + result.status = "created" + return result + } + + result.reason = "Workspace .moss source links are already present." + return result +} diff --git a/src/main/lib/moss-source/subagents.ts b/src/main/lib/moss-source/subagents.ts new file mode 100644 index 000000000..cdc074815 --- /dev/null +++ b/src/main/lib/moss-source/subagents.ts @@ -0,0 +1,271 @@ +import { spawn } from "node:child_process" +import { createHash, randomUUID } from "node:crypto" +import type { AgentEngineId } from "../agent-runtime/types" +import type { SharedResource } from "../shared-resources/types" +import { discoverMossSourceResources } from "./registry" + +export type MossSubagentInvocationStatus = + | "passed" + | "failed" + | "skipped" + | "timed-out" + +export interface MossSubagentInvocationResult { + status: MossSubagentInvocationStatus + invocationId: string + engineId: AgentEngineId + projectPath: string + resourceId?: string + name: string + taskHash: string + payloadHash: string + command?: string + commandHash?: string + exitCode?: number | null + elapsedMs: number + stdout?: string + stderr?: string + error?: string + timedOut?: boolean + warnings: string[] +} + +export interface InvokeMossSubagentOptions { + projectPath: string + engineId: AgentEngineId + name: string + task: string + cwd?: string + payload?: Record + env?: Record + timeoutMs?: number +} + +const DEFAULT_SUBAGENT_TIMEOUT_MS = 30_000 + +function sha256(value: string): string { + return createHash("sha256").update(value).digest("hex") +} + +function redactSubagentOutput(value: string): string { + return value + .replace(/sk-[A-Za-z0-9_-]{12,}/g, "sk-REDACTED") + .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer REDACTED") + .replace( + /(["']?(?:api[_-]?key|token|secret|password|authorization)["']?\s*[:=]\s*["']?)([^"',\n}]+)/gi, + "$1REDACTED", + ) + .slice(0, 4000) +} + +function subagentCommand(resource: SharedResource): string | undefined { + const command = resource.metadata?.command + return typeof command === "string" && command.trim() + ? command.trim() + : undefined +} + +function isSubagentEnabled(resource: SharedResource): boolean { + if (resource.enabled === false) return false + if (resource.metadata?.subagentEnabled === false) return false + return true +} + +function visibleResult( + result: MossSubagentInvocationResult, +): MossSubagentInvocationResult { + return { + ...result, + command: result.command ? redactSubagentOutput(result.command) : undefined, + stdout: result.stdout ? redactSubagentOutput(result.stdout) : undefined, + stderr: result.stderr ? redactSubagentOutput(result.stderr) : undefined, + error: result.error ? redactSubagentOutput(result.error) : undefined, + } +} + +function selectMossSubagent( + resources: SharedResource[], + name: string, +): SharedResource | undefined { + const normalizedName = name.trim().toLowerCase() + return resources.find( + (resource) => + resource.kind === "subagent" && + resource.scope === "moss" && + (resource.name.toLowerCase() === normalizedName || + resource.id.toLowerCase() === normalizedName), + ) +} + +export async function invokeMossSubagent( + options: InvokeMossSubagentOptions, +): Promise { + const startedAt = Date.now() + const invocationId = randomUUID() + const payloadJson = JSON.stringify(options.payload ?? {}) + const payloadHash = sha256(payloadJson) + const taskHash = sha256(options.task) + const warnings: string[] = [] + const name = options.name.trim() + + let resources: SharedResource[] = [] + try { + resources = await discoverMossSourceResources(options.projectPath) + } catch (error) { + return { + status: "failed", + invocationId, + engineId: options.engineId, + projectPath: options.projectPath, + name, + taskHash, + payloadHash, + elapsedMs: Date.now() - startedAt, + error: error instanceof Error ? error.message : String(error), + warnings, + } + } + + const resource = selectMossSubagent(resources, name) + if (!resource) { + return { + status: "failed", + invocationId, + engineId: options.engineId, + projectPath: options.projectPath, + name, + taskHash, + payloadHash, + elapsedMs: Date.now() - startedAt, + error: `Moss subagent ${name} was not found.`, + warnings, + } + } + + if (!isSubagentEnabled(resource)) { + return { + status: "skipped", + invocationId, + engineId: options.engineId, + projectPath: options.projectPath, + resourceId: resource.id, + name: resource.name, + taskHash, + payloadHash, + elapsedMs: Date.now() - startedAt, + error: "Subagent is disabled.", + warnings, + } + } + + const command = subagentCommand(resource) + if (!command) { + return { + status: "skipped", + invocationId, + engineId: options.engineId, + projectPath: options.projectPath, + resourceId: resource.id, + name: resource.name, + taskHash, + payloadHash, + elapsedMs: Date.now() - startedAt, + error: "Subagent has no Moss invocation command.", + warnings, + } + } + + const timeoutMs = options.timeoutMs ?? DEFAULT_SUBAGENT_TIMEOUT_MS + const cwd = options.cwd ?? options.projectPath + + return new Promise((resolve) => { + const child = spawn(command, { + cwd, + env: { + ...process.env, + ...options.env, + MOSS_SUBAGENT_INVOCATION_ID: invocationId, + MOSS_SUBAGENT_ENGINE: options.engineId, + MOSS_SUBAGENT_RESOURCE_ID: resource.id, + MOSS_SUBAGENT_NAME: resource.name, + MOSS_SUBAGENT_PROJECT_PATH: options.projectPath, + MOSS_SUBAGENT_CWD: cwd, + MOSS_SUBAGENT_TASK: options.task, + MOSS_SUBAGENT_TASK_SHA256: taskHash, + MOSS_SUBAGENT_PAYLOAD_JSON: payloadJson, + MOSS_SUBAGENT_PAYLOAD_SHA256: payloadHash, + }, + shell: true, + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }) + const stdoutChunks: Buffer[] = [] + const stderrChunks: Buffer[] = [] + let timedOut = false + let forceKillTimer: ReturnType | null = null + + const timeout = setTimeout(() => { + timedOut = true + child.kill("SIGTERM") + forceKillTimer = setTimeout(() => child.kill("SIGKILL"), 2000) + }, timeoutMs) + + child.stdout.on("data", (chunk) => { + stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))) + }) + child.stderr.on("data", (chunk) => { + stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))) + }) + child.once("error", (error) => { + clearTimeout(timeout) + if (forceKillTimer) clearTimeout(forceKillTimer) + resolve( + visibleResult({ + status: "failed", + invocationId, + engineId: options.engineId, + projectPath: options.projectPath, + resourceId: resource.id, + name: resource.name, + taskHash, + payloadHash, + command, + commandHash: sha256(command), + elapsedMs: Date.now() - startedAt, + error: error.message, + warnings, + }), + ) + }) + child.once("close", (exitCode) => { + clearTimeout(timeout) + if (forceKillTimer) clearTimeout(forceKillTimer) + const status: MossSubagentInvocationStatus = timedOut + ? "timed-out" + : exitCode === 0 + ? "passed" + : "failed" + + resolve( + visibleResult({ + status, + invocationId, + engineId: options.engineId, + projectPath: options.projectPath, + resourceId: resource.id, + name: resource.name, + taskHash, + payloadHash, + command, + commandHash: sha256(command), + exitCode, + elapsedMs: Date.now() - startedAt, + stdout: Buffer.concat(stdoutChunks).toString("utf-8").trim(), + stderr: Buffer.concat(stderrChunks).toString("utf-8").trim(), + timedOut, + warnings, + }), + ) + }) + }) +} diff --git a/src/main/lib/moss-source/types.ts b/src/main/lib/moss-source/types.ts new file mode 100644 index 000000000..33724dc44 --- /dev/null +++ b/src/main/lib/moss-source/types.ts @@ -0,0 +1,39 @@ +import type { AgentEngineId } from "../agent-runtime" + +export const MOSS_SOURCE_VERSION = 1 +export const MOSS_ROOT_DIR = ".moss" + +export type MossSourceFileRole = + | "source-instruction" + | "workspace-config" + | "memory-root" + | "skill" + | "mcp-config" + | "plugin" + | "hook" + | "subagent" + | "provider-config" + +export interface MossSourceLayout { + version: typeof MOSS_SOURCE_VERSION + projectPath: string + root: string + sourceInstruction: string + workspaceConfig: string + memoryRoot: string + skillsRoot: string + mcpConfig: string + pluginsRoot: string + hooksRoot: string + subagentsRoot: string + providersConfig: string +} + +export interface MossEnginePathTarget { + engineId: AgentEngineId + sourceRole: MossSourceFileRole + sourcePath: string + targetPath: string + action: "native" | "symlink" | "adapter-inject" | "managed-bridge" + reason: string +} diff --git a/src/main/lib/shared-resources/governance.ts b/src/main/lib/shared-resources/governance.ts new file mode 100644 index 000000000..7d7d14a94 --- /dev/null +++ b/src/main/lib/shared-resources/governance.ts @@ -0,0 +1,520 @@ +import * as path from "path" +import { AGENT_RUNTIME_MANIFESTS } from "../agent-runtime/manifests" +import type { AgentEngineId } from "../agent-runtime/types" +import type { + EngineResourceProjection, + ResourcePathMapping, + SharedResource, + SharedResourceApproval, + SharedResourceConflict, + SharedResourceKind, + SharedResourceSnapshot, +} from "./types" + +const GOVERNED_RESOURCE_KINDS = new Set([ + "agent", + "subagent", + "skill", + "command", + "mcp", + "memory", + "instruction", + "hook", + "provider", +]) + +const SCOPE_PRECEDENCE: Record = { + moss: 0, + project: 10, + user: 20, + engine: 30, + plugin: 40, +} + +function normalizeResourceName(name: string): string { + return name.trim().toLowerCase() +} + +function buildConflictKey(resource: SharedResource): string | undefined { + if (!GOVERNED_RESOURCE_KINDS.has(resource.kind)) return undefined + + const engineKey = resource.kind === "mcp" || resource.kind === "memory" + ? resource.engine ?? "shared" + : "shared" + return `${resource.kind}:${engineKey}:${normalizeResourceName(resource.name)}` +} + +function getPrecedenceRank(resource: SharedResource): number { + const scopeRank = SCOPE_PRECEDENCE[resource.scope] ?? 90 + const disabledPenalty = resource.enabled === false ? 100 : 0 + const approvalPenalty = + resource.approval?.required && !resource.approval.approved ? 50 : 0 + return scopeRank + disabledPenalty + approvalPenalty +} + +function getPrecedenceLabel(resource: SharedResource): string { + if (resource.scope === "moss") return "Moss Unified Source is canonical" + if (resource.scope === "project") return "project overrides user, engine, and plugin" + if (resource.scope === "user") return "user overrides engine and plugin" + if (resource.scope === "engine") return "engine-native config overrides plugin" + return "plugin is lowest precedence and may require approval" +} + +function getDiscoverySource(resource: SharedResource): string { + if (resource.scope === "moss") return "Moss Unified Source" + + if (resource.kind === "mcp") { + if (resource.scope === "plugin") return "plugin MCP manifest" + if (resource.engine === "codex") return "Codex MCP config" + if (resource.engine === "claude-code") return "Claude MCP config" + return "MCP config" + } + + if (resource.scope === "plugin") return "plugin component directory" + if (resource.kind === "memory") return "memory file" + if (resource.kind === "instruction") return "project instruction file" + return `${resource.scope} ${resource.kind} directory` +} + +function getMossEntryName(resource: SharedResource): string { + const entryName = resource.metadata?.entryName + if (typeof entryName === "string" && entryName.trim()) { + return entryName + } + + if (resource.path) return path.basename(resource.path) + return resource.name +} + +function getMossRole(resource: SharedResource): string | undefined { + const role = resource.metadata?.mossRole + return typeof role === "string" ? role : undefined +} + +function getMossProjectionSourcePath(resource: SharedResource): string | undefined { + const sourcePath = resource.path + if (!sourcePath) return undefined + + if ( + resource.kind === "skill" && + path.basename(sourcePath) === "SKILL.md" + ) { + return path.dirname(sourcePath) + } + + return sourcePath +} + +function buildMossProjectionMapping( + engineId: AgentEngineId, + resource: SharedResource, +): ResourcePathMapping | null { + const sourcePath = getMossProjectionSourcePath(resource) + if (!sourcePath) return null + + const role = getMossRole(resource) + const entryName = getMossEntryName(resource) + + if (engineId === "hermes") { + return { + resourceId: resource.id, + action: "native", + sourcePath, + targetPath: sourcePath, + reason: "Hermes/Moss consumes the .moss Unified Source natively.", + } + } + + if (engineId === "claude-code") { + if (resource.kind === "instruction" && role === "source-instruction") { + return { + resourceId: resource.id, + action: "symlink", + sourcePath, + targetPath: "CLAUDE.md", + reason: "Claude Code reads project instructions from CLAUDE.md.", + } + } + if (resource.kind === "memory") { + if (role === "memory-entry") { + return { + resourceId: resource.id, + action: "native", + sourcePath, + targetPath: path.join(".claude", "memory", entryName), + reason: "Claude Code sees this entry through the projected Moss memory root.", + } + } + return { + resourceId: resource.id, + action: "symlink", + sourcePath, + targetPath: ".claude/memory", + reason: "Claude Code project memory is mapped to Moss memory.", + } + } + if (resource.kind === "skill") { + return { + resourceId: resource.id, + action: "symlink", + sourcePath, + targetPath: path.join(".claude", "skills", entryName.replace(/\/SKILL\.md$/, "")), + reason: "Claude Code skills can be projected from Moss skills.", + } + } + if (resource.kind === "subagent") { + return { + resourceId: resource.id, + action: "symlink", + sourcePath, + targetPath: path.join(".claude", "agents", entryName), + reason: "Claude Code subagents are projected from Moss subagents.", + } + } + if (resource.kind === "mcp") { + return { + resourceId: resource.id, + action: "managed-bridge", + sourcePath, + targetPath: ".mcp.json", + reason: "Moss writes Claude-compatible MCP config from the canonical Moss MCP config.", + } + } + if (resource.kind === "hook") { + return { + resourceId: resource.id, + action: "adapter-inject", + sourcePath, + targetPath: ".claude/hooks", + reason: "Moss adapts hook events to Claude-compatible hooks without duplicating user config.", + } + } + if (resource.kind === "provider") { + return { + resourceId: resource.id, + action: "managed-bridge", + sourcePath, + targetPath: ".claude/settings.local.json", + reason: "Moss bridges provider credentials and routing into Claude Code native config.", + } + } + if (resource.kind === "plugin") { + return { + resourceId: resource.id, + action: "adapter-inject", + sourcePath, + targetPath: ".claude/plugins", + reason: "Moss exposes installed plugins through a Claude-compatible adapter.", + } + } + } + + if (engineId === "codex") { + if (resource.kind === "instruction" && role === "source-instruction") { + return { + resourceId: resource.id, + action: "symlink", + sourcePath, + targetPath: "AGENTS.md", + reason: "Codex reads project instructions from AGENTS.md.", + } + } + if (resource.kind === "memory") { + if (role === "memory-entry") { + return { + resourceId: resource.id, + action: "native", + sourcePath, + targetPath: path.join(".codex", "memories", entryName), + reason: "Codex sees this entry through the projected Moss memory root.", + } + } + return { + resourceId: resource.id, + action: "symlink", + sourcePath, + targetPath: ".codex/memories", + reason: "Codex project memory is mapped to Moss memory.", + } + } + if (resource.kind === "skill") { + return { + resourceId: resource.id, + action: "symlink", + sourcePath, + targetPath: path.join(".codex", "skills", entryName.replace(/\/SKILL\.md$/, "")), + reason: "Codex skills are projected from Moss skills.", + } + } + if (resource.kind === "subagent") { + return { + resourceId: resource.id, + action: "symlink", + sourcePath, + targetPath: path.join(".codex", "agents", entryName), + reason: "Codex subagents are projected from Moss subagents without a second real definition.", + } + } + if (resource.kind === "mcp") { + return { + resourceId: resource.id, + action: "managed-bridge", + sourcePath, + targetPath: ".codex/config.toml", + reason: "Moss writes Codex MCP TOML from the canonical Moss MCP config.", + } + } + if (resource.kind === "hook") { + return { + resourceId: resource.id, + action: "adapter-inject", + sourcePath, + targetPath: ".codex/hooks", + reason: "Moss adapts hook events to Codex-compatible hooks without duplicating user config.", + } + } + if (resource.kind === "provider") { + return { + resourceId: resource.id, + action: "managed-bridge", + sourcePath, + targetPath: ".codex/config.toml", + reason: "Moss bridges provider credentials and routing into Codex native config.", + } + } + if (resource.kind === "plugin") { + return { + resourceId: resource.id, + action: "adapter-inject", + sourcePath, + targetPath: ".codex/plugins", + reason: "Moss exposes installed plugins through a Codex-compatible adapter without duplicating user config.", + } + } + } + + return { + resourceId: resource.id, + action: "prompt-inject", + sourcePath, + reason: "Moss source is injected as shared prompt/context for this engine.", + } +} + +function getApproval(resource: SharedResource): SharedResourceApproval { + const metadataApproved = resource.metadata?.approved + + if (resource.kind === "mcp" && resource.scope === "plugin") { + const approved = metadataApproved === true + return { + required: true, + approved, + reason: approved + ? "Approved plugin MCP server." + : "Plugin MCP server must be approved before projection.", + } + } + + if (resource.scope === "plugin" && resource.enabled === false) { + return { + required: false, + approved: false, + reason: "Plugin is installed but disabled.", + } + } + + return { + required: false, + approved: resource.enabled !== false, + } +} + +function applyResourceGovernance(resources: SharedResource[]): { + resources: SharedResource[] + conflicts: SharedResourceConflict[] +} { + const governedResources = resources.map((resource) => { + const approval = getApproval(resource) + const withApproval: SharedResource = { + ...resource, + approval, + } + const precedenceRank = getPrecedenceRank(withApproval) + const conflictKey = buildConflictKey(resource) + + return { + ...withApproval, + precedenceRank, + conflictKey, + provenance: { + source: resource.scope, + sourceId: resource.pluginSource ?? resource.engine ?? resource.scope, + engine: resource.engine, + displayPath: resource.path, + discoveredBy: getDiscoverySource(resource), + precedenceRank, + precedenceLabel: getPrecedenceLabel(resource), + }, + } + }) + + const groups = new Map() + for (const resource of governedResources) { + if (!resource.conflictKey) continue + const group = groups.get(resource.conflictKey) ?? [] + group.push(resource) + groups.set(resource.conflictKey, group) + } + + const conflicts: SharedResourceConflict[] = [] + const conflictByResourceId = new Map() + + for (const [key, group] of groups) { + if (group.length < 2) continue + + const ordered = [...group].sort((left, right) => { + const rankDelta = (left.precedenceRank ?? 90) - (right.precedenceRank ?? 90) + if (rankDelta !== 0) return rankDelta + return left.id.localeCompare(right.id) + }) + const winner = ordered[0] + const conflict: SharedResourceConflict = { + key, + kind: winner.kind, + name: winner.name, + winnerResourceId: winner.id, + resourceIds: ordered.map((resource) => resource.id), + reason: `${winner.scope} resource wins by precedence.`, + resolution: "winner-by-precedence", + } + conflicts.push(conflict) + for (const resource of ordered) { + conflictByResourceId.set(resource.id, conflict) + } + } + + return { + resources: governedResources.map((resource) => ({ + ...resource, + conflict: conflictByResourceId.get(resource.id), + })), + conflicts, + } +} + +function canProjectResource( + resource: SharedResource, + warnings: string[], +): boolean { + if (resource.enabled === false) { + return false + } + + if (resource.approval?.required && !resource.approval.approved) { + warnings.push(`${resource.name} is withheld because approval is pending.`) + return false + } + + if ( + resource.conflict && + resource.conflict.resolution === "winner-by-precedence" && + resource.conflict.winnerResourceId !== resource.id + ) { + warnings.push(`${resource.name} is shadowed by a higher precedence resource.`) + return false + } + + return true +} + +function buildEngineProjection( + engineId: AgentEngineId, + projectPath: string | undefined, + resources: SharedResource[], +): EngineResourceProjection { + const manifest = AGENT_RUNTIME_MANIFESTS[engineId] + const mappings: ResourcePathMapping[] = [] + const warnings: string[] = [] + + for (const resource of resources) { + if (!resource.path) continue + if (!canProjectResource(resource, warnings)) continue + + if (resource.scope === "moss") { + const mossMapping = buildMossProjectionMapping(engineId, resource) + if (mossMapping) mappings.push(mossMapping) + continue + } + + if (engineId === "claude-code") { + if (["agent", "subagent", "skill", "command", "plugin", "mcp", "memory", "instruction", "hook", "provider"].includes(resource.kind)) { + mappings.push({ + resourceId: resource.id, + action: "native", + sourcePath: resource.path, + targetPath: resource.path, + }) + } + continue + } + + if (engineId === "codex") { + if (resource.kind === "mcp" && resource.engine === "codex") { + mappings.push({ + resourceId: resource.id, + action: "native", + sourcePath: resource.path, + targetPath: resource.path, + }) + } else if (["agent", "subagent", "skill", "command", "memory", "instruction", "hook", "provider"].includes(resource.kind)) { + mappings.push({ + resourceId: resource.id, + action: "prompt-inject", + sourcePath: resource.path, + reason: "Codex ACP does not consume Claude-native resource directories directly yet.", + }) + } + continue + } + + mappings.push({ + resourceId: resource.id, + action: "unsupported", + sourcePath: resource.path, + reason: "This engine does not have native resource projection implemented yet.", + }) + } + + if (engineId === "hermes" && manifest.availability === "unsupported") { + warnings.push("Hermes runtime is unavailable; Moss source remains canonical but cannot be launched natively.") + } + + if (engineId === "codex") { + warnings.push("Codex receives agents, skills, commands, and memory by prompt/context projection until native projection is implemented.") + } + + return { + engineId, + status: manifest.availability === "unsupported" ? "unsupported" : warnings.length > 0 ? "partial" : "ready", + userRoot: manifest.configRoots.user, + projectRoot: projectPath ? path.join(projectPath, manifest.configRoots.project || "") : manifest.configRoots.project, + mappings, + warnings, + } +} + +export function buildGovernedResourceProjection(params: { + resources: SharedResource[] + projectPath?: string +}): Pick { + const governed = applyResourceGovernance(params.resources) + const projections = (Object.keys(AGENT_RUNTIME_MANIFESTS) as AgentEngineId[]).map((engineId) => + buildEngineProjection(engineId, params.projectPath, governed.resources), + ) + + return { + resources: governed.resources, + conflicts: governed.conflicts, + projections, + } +} diff --git a/src/main/lib/shared-resources/types.ts b/src/main/lib/shared-resources/types.ts new file mode 100644 index 000000000..6958bbedc --- /dev/null +++ b/src/main/lib/shared-resources/types.ts @@ -0,0 +1,91 @@ +import type { AgentEngineId } from "../agent-runtime" + +export type SharedResourceKind = + | "agent" + | "subagent" + | "skill" + | "command" + | "plugin" + | "mcp" + | "memory" + | "instruction" + | "hook" + | "provider" + +export type SharedResourceScope = "moss" | "project" | "user" | "plugin" | "engine" + +export interface SharedResourceProvenance { + source: SharedResourceScope + sourceId?: string + engine?: AgentEngineId + displayPath?: string + discoveredBy: string + precedenceRank: number + precedenceLabel: string +} + +export interface SharedResourceApproval { + required: boolean + approved: boolean + reason?: string +} + +export interface SharedResourceConflict { + key: string + kind: SharedResourceKind + name: string + winnerResourceId: string + resourceIds: string[] + reason: string + resolution: "unique" | "winner-by-precedence" | "manual-review" +} + +export interface SharedResource { + id: string + kind: SharedResourceKind + name: string + scope: SharedResourceScope + path?: string + engine?: AgentEngineId + pluginSource?: string + description?: string + enabled?: boolean + provenance?: SharedResourceProvenance + approval?: SharedResourceApproval + precedenceRank?: number + conflictKey?: string + conflict?: SharedResourceConflict + metadata?: Record +} + +export interface ResourcePathMapping { + resourceId: string + action: + | "native" + | "symlink" + | "copy" + | "prompt-inject" + | "adapter-inject" + | "managed-bridge" + | "unsupported" + sourcePath?: string + targetPath?: string + reason?: string +} + +export interface EngineResourceProjection { + engineId: AgentEngineId + status: "ready" | "partial" | "unsupported" + userRoot?: string + projectRoot?: string + mappings: ResourcePathMapping[] + warnings: string[] +} + +export interface SharedResourceSnapshot { + generatedAt: string + projectPath?: string + resources: SharedResource[] + conflicts: SharedResourceConflict[] + projections: EngineResourceProjection[] +} diff --git a/src/main/lib/trpc/routers/chat-runtime-selection.test.ts b/src/main/lib/trpc/routers/chat-runtime-selection.test.ts new file mode 100644 index 000000000..703227c4b --- /dev/null +++ b/src/main/lib/trpc/routers/chat-runtime-selection.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from "bun:test" +import { + buildEmptySubChatValues, + buildInitialSubChatValues, + resolveChatRuntimeSelection, +} from "./chat-runtime-selection" + +describe("chat runtime selection persistence", () => { + test("defaults new chat sessions to Hermes instead of Claude", () => { + expect(resolveChatRuntimeSelection({})).toEqual({ + engine: "hermes", + modelId: undefined, + }) + }) + + test("preserves selected Hermes engine and model for initial sub-chat creation", () => { + expect( + buildInitialSubChatValues({ + chatId: "chat-1", + engine: "hermes", + model: "gpt-5.5", + mode: "agent", + messages: "[]", + }), + ).toMatchObject({ + chatId: "chat-1", + engine: "hermes", + modelId: "gpt-5.5", + mode: "agent", + messages: "[]", + }) + }) + + test("preserves selected provider for follow-up sub-chat creation", () => { + expect( + buildEmptySubChatValues({ + chatId: "chat-1", + name: "Continue with Codex", + engine: "codex", + model: "gpt-5.5/high", + mode: "plan", + }), + ).toEqual({ + chatId: "chat-1", + name: "Continue with Codex", + engine: "codex", + modelId: "gpt-5.5/high", + mode: "plan", + messages: "[]", + }) + }) +}) diff --git a/src/main/lib/trpc/routers/chat-runtime-selection.ts b/src/main/lib/trpc/routers/chat-runtime-selection.ts new file mode 100644 index 000000000..8b5c2fab0 --- /dev/null +++ b/src/main/lib/trpc/routers/chat-runtime-selection.ts @@ -0,0 +1,51 @@ +import { + DEFAULT_AGENT_ENGINE_ID, + type AgentEngineId, +} from "../../agent-runtime/types" + +export type ChatMode = "plan" | "agent" + +export interface ChatRuntimeSelectionInput { + engine?: AgentEngineId + model?: string +} + +export function resolveChatRuntimeSelection( + input: ChatRuntimeSelectionInput, +): { engine: AgentEngineId; modelId?: string } { + return { + engine: input.engine ?? DEFAULT_AGENT_ENGINE_ID, + modelId: input.model, + } +} + +export function buildInitialSubChatValues(input: { + chatId: string + engine?: AgentEngineId + model?: string + mode: ChatMode + messages: string +}) { + return { + chatId: input.chatId, + ...resolveChatRuntimeSelection(input), + mode: input.mode, + messages: input.messages, + } +} + +export function buildEmptySubChatValues(input: { + chatId: string + name?: string + engine?: AgentEngineId + model?: string + mode: ChatMode +}) { + return { + chatId: input.chatId, + name: input.name, + ...resolveChatRuntimeSelection(input), + mode: input.mode, + messages: "[]", + } +} diff --git a/src/main/lib/trpc/routers/codex-mcp-session.test.ts b/src/main/lib/trpc/routers/codex-mcp-session.test.ts new file mode 100644 index 000000000..4da551094 --- /dev/null +++ b/src/main/lib/trpc/routers/codex-mcp-session.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from "bun:test" +import { shouldAttachCodexMcpServerToSession } from "./codex-mcp-session" + +const sessionServer = { name: "local-tools" } + +describe("Codex MCP session eligibility", () => { + test("keeps healthy tool-bearing servers in the session config", () => { + expect( + shouldAttachCodexMcpServerToSession({ + sessionServer, + settingsServer: { + status: "connected", + needsAuth: false, + tools: [{ name: "search" }], + }, + toolsWereResolved: true, + }), + ).toBe(true) + }) + + test("keeps failed settings entries visible but out of the Codex session", () => { + expect( + shouldAttachCodexMcpServerToSession({ + sessionServer, + settingsServer: { + status: "failed", + needsAuth: false, + tools: [], + }, + toolsWereResolved: true, + }), + ).toBe(false) + }) + + test("does not pass auth-blocked servers to the Codex runtime", () => { + expect( + shouldAttachCodexMcpServerToSession({ + sessionServer, + settingsServer: { + status: "needs-auth", + needsAuth: true, + tools: [], + }, + toolsWereResolved: true, + }), + ).toBe(false) + }) + + test("does not pass unverified empty tool probes into native Codex", () => { + expect( + shouldAttachCodexMcpServerToSession({ + sessionServer, + settingsServer: { + status: "connected", + needsAuth: false, + tools: [], + }, + toolsWereResolved: true, + }), + ).toBe(false) + }) +}) diff --git a/src/main/lib/trpc/routers/codex-mcp-session.ts b/src/main/lib/trpc/routers/codex-mcp-session.ts new file mode 100644 index 000000000..43068bd97 --- /dev/null +++ b/src/main/lib/trpc/routers/codex-mcp-session.ts @@ -0,0 +1,25 @@ +export type CodexMcpSessionServerCandidate = { + name: string +} | null + +export type CodexMcpSettingsServerCandidate = { + status: "connected" | "failed" | "pending" | "needs-auth" + needsAuth?: boolean + tools?: unknown[] +} + +export function shouldAttachCodexMcpServerToSession(params: { + sessionServer: CodexMcpSessionServerCandidate + settingsServer: CodexMcpSettingsServerCandidate + toolsWereResolved: boolean +}): boolean { + if (!params.sessionServer) return false + if (params.settingsServer.needsAuth) return false + if (params.settingsServer.status !== "connected") return false + + if (params.toolsWereResolved && (params.settingsServer.tools?.length ?? 0) === 0) { + return false + } + + return true +} diff --git a/src/shared/codex-runtime-notices.test.ts b/src/shared/codex-runtime-notices.test.ts new file mode 100644 index 000000000..e51070fa0 --- /dev/null +++ b/src/shared/codex-runtime-notices.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test" + +import { + isCodexRuntimeNoticeText, + stripCodexRuntimeNoticeText, +} from "./codex-runtime-notices" + +const reconnectNotice = + "Reconnecting... 2/5 (stream disconnected before completion: failed to send websocket request: IO error: Broken pipe (os error 32))" + +describe("codex runtime notice hygiene", () => { + test("recognizes runtime notices", () => { + expect(isCodexRuntimeNoticeText(reconnectNotice)).toBe(true) + expect( + isCodexRuntimeNoticeText( + "Under-development features enabled: chronicle. Under-development features are incomplete and may behave unpredictably. To suppress this warning, set `suppress_unstable_features_warning = true` in /Users/moss/.codex/config.toml.", + ), + ).toBe(true) + expect( + isCodexRuntimeNoticeText( + "Exceeded skills context budget of 2%. All skill descriptions were removed and 107 additional skills were not included in the model-visible skills list.", + ), + ).toBe(true) + expect(isCodexRuntimeNoticeText("正常回答")).toBe(false) + }) + + test("strips a whole runtime notice", () => { + expect(stripCodexRuntimeNoticeText(reconnectNotice)).toEqual({ + text: "", + changed: true, + }) + }) + + test("strips runtime notice lines from mixed assistant text", () => { + expect( + stripCodexRuntimeNoticeText( + `${reconnectNotice}\n\n页面状态正在稳定流转。\n输入框会恢复。`, + ), + ).toEqual({ + text: "页面状态正在稳定流转。\n输入框会恢复。", + changed: true, + }) + }) +}) diff --git a/src/shared/codex-runtime-notices.ts b/src/shared/codex-runtime-notices.ts new file mode 100644 index 000000000..ae8c07354 --- /dev/null +++ b/src/shared/codex-runtime-notices.ts @@ -0,0 +1,58 @@ +export function normalizeCodexRuntimeComparableText(value: unknown): string { + return String(value ?? "") + .replace(/\s+/g, " ") + .trim() +} + +export function isCodexRuntimeNoticeText(value: unknown): boolean { + const text = normalizeCodexRuntimeComparableText(value) + return ( + (text.startsWith("Under-development features enabled: ") && + text.includes( + "Under-development features are incomplete and may behave unpredictably.", + ) && + text.includes("suppress_unstable_features_warning = true") && + text.includes("/.codex/config.toml")) || + (text.startsWith("Exceeded skills context budget of ") && + text.includes("All skill descriptions were removed") && + text.includes("model-visible skills list")) || + (text.startsWith("Reconnecting...") && + text.includes("stream disconnected before completion")) + ) +} + +export function stripCodexRuntimeNoticeText(value: unknown): { + text: string + changed: boolean +} { + const original = typeof value === "string" ? value : String(value ?? "") + if (!original) return { text: original, changed: false } + + const lines = original.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n") + let changed = false + const keptLines: string[] = [] + + for (const line of lines) { + if (isCodexRuntimeNoticeText(line)) { + changed = true + continue + } + keptLines.push(line) + } + + if (!changed && isCodexRuntimeNoticeText(original)) { + return { text: "", changed: true } + } + + if (!changed) return { text: original, changed: false } + + const text = keptLines + .join("\n") + .replace(/^(?:[ \t]*\n)+/, "") + .replace(/\n{3,}/g, "\n\n") + + return { + text, + changed: true, + } +} From 3ee0d297a681f68953c460ed87e0b909ca5861e2 Mon Sep 17 00:00:00 2001 From: Moss Date: Sun, 28 Jun 2026 01:51:08 +0800 Subject: [PATCH 3/3] Complete release workflow runtime support --- package.json | 2 +- scripts/generate-update-manifest.mjs | 16 +- src/main/auth-manager.ts | 22 +- src/main/constants.ts | 10 + src/main/index.ts | 307 +- .../lib/agent-runtime/adapters/claude-code.ts | 39 + src/main/lib/agent-runtime/adapters/codex.ts | 551 ++- .../lib/agent-runtime/adapters/custom-acp.ts | 39 + src/main/lib/agent-runtime/adapters/hermes.ts | 373 +- .../agent-runtime/codex-app-server-client.ts | 285 ++ .../agent-runtime/codex-app-server-events.ts | 788 ++++ .../agent-runtime/codex-app-server-plan.ts | 339 ++ .../agent-runtime/codex-app-server-policy.ts | 93 + .../agent-runtime/codex-app-server-runtime.ts | 3824 +++++++++++++++++ .../lib/agent-runtime/codex-native-session.ts | 239 +- src/main/lib/agent-runtime/control-plane.ts | 8 + src/main/lib/agent-runtime/events.ts | 4 + .../lib/agent-runtime/hermes-acp-runtime.ts | 279 ++ .../agent-runtime/hermes-native-session.ts | 34 +- src/main/lib/agent-runtime/index.ts | 12 + src/main/lib/agent-runtime/launch-plan.ts | 183 + src/main/lib/agent-runtime/manifests.ts | 41 +- .../agent-runtime/native-thread-summary.ts | 515 +++ .../lib/agent-runtime/process-registry.ts | 196 + .../lib/agent-runtime/provider-instances.ts | 114 + .../provider-runtime-contract.ts | 820 ++++ .../lib/agent-runtime/runtime-receipt-bus.ts | 110 + .../lib/agent-runtime/runtime-run-ledger.ts | 422 ++ src/main/lib/agent-runtime/session-actions.ts | 561 +-- src/main/lib/agent-runtime/session-records.ts | 155 +- src/main/lib/agent-runtime/session-store.ts | 25 +- .../lib/agent-runtime/stale-stream-state.ts | 28 + src/main/lib/agent-runtime/types.ts | 1218 +++++- src/main/lib/auto-updater.ts | 27 +- src/main/lib/claude-plugin-settings.ts | 77 + src/main/lib/claude/env.ts | 123 + src/main/lib/claude/index.ts | 3 + src/main/lib/claude/transform.ts | 7 +- src/main/lib/claude/types.ts | 2 + src/main/lib/codex-automations.test.ts | 33 + src/main/lib/codex-automations.ts | 12 +- src/main/lib/db/schema/index.ts | 5 + src/main/lib/git/status-fallback.ts | 166 + src/main/lib/git/status.ts | 86 +- src/main/lib/git/watcher/git-watcher.ts | 6 +- src/main/lib/git/worktree.ts | 21 + src/main/lib/mcp-auth.ts | 112 +- src/main/lib/mcp-stdio-compat.test.ts | 39 +- src/main/lib/mcp-stdio-compat.ts | 44 + .../lib/mobile-gateway/database-sessions.ts | 36 + src/main/lib/mobile-gateway/desktop-state.ts | 106 + src/main/lib/mobile-gateway/desktop.ts | 70 + src/main/lib/mobile-gateway/facade.ts | 1031 +++++ src/main/lib/mobile-gateway/server.ts | 191 + src/main/lib/mobile-gateway/sessions.ts | 122 + src/main/lib/moss-account/index.ts | 1 + src/main/lib/moss-source/bootstrap.ts | 4 +- src/main/lib/moss-source/frontmatter.ts | 43 + .../lib/moss-source/provider-config.test.ts | 6 +- src/main/lib/moss-source/provider-config.ts | 5 +- src/main/lib/moss-source/runtime-context.ts | 406 ++ .../lib/moss-source/runtime-materializer.ts | 235 +- src/main/lib/pet-runtime-status.ts | 422 ++ src/main/lib/plugins/index.ts | 28 +- .../codex-native-resources.test.ts | 494 +++ .../codex-native-resources.ts | 885 ++++ .../lib/shared-resources/governance.test.ts | 356 ++ src/main/lib/shared-resources/governance.ts | 206 +- src/main/lib/shared-resources/index.ts | 3 + .../lib/shared-resources/registry.test.ts | 112 + src/main/lib/shared-resources/registry.ts | 591 +++ src/main/lib/shared-resources/types.ts | 43 + src/main/lib/trpc/routers/agent-runtime.ts | 2640 ++++++++++++ src/main/lib/trpc/routers/agent-utils.ts | 34 +- src/main/lib/trpc/routers/agents.ts | 65 +- .../trpc/routers/chat-runtime-selection.ts | 4 + src/main/lib/trpc/routers/chats.ts | 611 ++- src/main/lib/trpc/routers/claude-settings.ts | 117 +- src/main/lib/trpc/routers/claude.ts | 260 +- src/main/lib/trpc/routers/codex.ts | 1766 +++++++- src/main/lib/trpc/routers/files.ts | 20 +- src/main/lib/trpc/routers/hermes.ts | 917 ++++ src/main/lib/trpc/routers/index.ts | 14 + src/main/lib/trpc/routers/mobile-gateway.ts | 8 + src/main/lib/trpc/routers/moss-account.ts | 118 + src/main/lib/trpc/routers/pet-runtime.ts | 35 + src/main/lib/trpc/routers/plugins.ts | 4 +- .../lib/trpc/routers/release-readiness.ts | 435 ++ src/main/lib/trpc/routers/shared-resources.ts | 809 ++++ src/main/lib/trpc/routers/skill-md.ts | 37 + src/main/lib/trpc/routers/skills.ts | 206 +- src/main/lib/vscode-theme-scanner.ts | 2 +- src/main/windows/main.ts | 137 +- .../app-icons/computer-use-plugin-icon.png | Bin 0 -> 21091 bytes .../plugin-icons/codex-connected-chrome.png | Bin 0 -> 2947 bytes .../codex-connected-codex-labs.png | Bin 0 -> 3358 bytes .../plugin-icons/codex-connected-cursor.png | Bin 0 -> 2082 bytes .../plugin-icons/codex-connected-docs.png | Bin 0 -> 2053 bytes .../plugin-icons/codex-connected-gmail.png | Bin 0 -> 1427 bytes .../codex-connected-huggingface.png | Bin 0 -> 2178 bytes .../plugin-icons/codex-connected-linear.png | Bin 0 -> 2864 bytes .../plugin-icons/codex-connected-more.png | Bin 0 -> 972 bytes .../codex-connected-plugin-grid.png | Bin 0 -> 3666 bytes .../codex-connected-plugin-store.png | Bin 0 -> 3502 bytes .../codex-connected-purple-dots.png | Bin 0 -> 3466 bytes .../codex-connected-record-replay.png | Bin 0 -> 1167902 bytes .../plugin-icons/codex-connected-sheets.png | Bin 0 -> 2201 bytes .../plugin-icons/codex-connected-slides.png | Bin 0 -> 2150 bytes .../plugin-icons/codex-connected-striped.png | Bin 0 -> 1946 bytes .../codex-marketplace-actively.png | Bin 0 -> 1879 bytes .../codex-plugin-detail-gradient.png | Bin 0 -> 147216 bytes .../codex-record-replay-plugin-icon.png | Bin 0 -> 23100 bytes src/renderer/components/ui/icons.tsx | 5 +- src/renderer/contexts/WindowContext.tsx | 17 + src/renderer/features/agents/atoms/index.ts | 196 +- .../agents/commands/slash-command-trigger.ts | 36 + .../features/agents/lib/agent-runtime.test.ts | 367 ++ .../features/agents/lib/agent-runtime.ts | 492 +++ .../features/agents/lib/models.test.ts | 50 + src/renderer/features/agents/lib/models.ts | 69 +- .../agents/lib/shared-agent-support.ts | 27 + .../mentions/agents-mentions-editor.tsx | 17 +- .../features/plugins/plugin-agent-support.tsx | 52 + .../plugins/plugin-entry-surfaces.test.ts | 103 + .../features/plugins/plugin-entry-surfaces.ts | 90 + .../features/plugins/plugin-entry-view.tsx | 2741 ++++++++++++ .../features/plugins/plugin-resource-model.ts | 794 ++++ .../features/plugins/plugin-route-state.ts | 389 ++ src/renderer/lib/atoms/index.ts | 154 + src/renderer/lib/i18n.ts | 1620 +++++++ src/renderer/lib/mock-api.ts | 8 +- src/renderer/lib/trpc.ts | 44 +- src/shared/codex-tool-normalizer.ts | 2474 ++++++++++- src/shared/plugin-deep-link.ts | 79 + 134 files changed, 34681 insertions(+), 1193 deletions(-) create mode 100644 src/main/lib/agent-runtime/codex-app-server-client.ts create mode 100644 src/main/lib/agent-runtime/codex-app-server-events.ts create mode 100644 src/main/lib/agent-runtime/codex-app-server-plan.ts create mode 100644 src/main/lib/agent-runtime/codex-app-server-policy.ts create mode 100644 src/main/lib/agent-runtime/codex-app-server-runtime.ts create mode 100644 src/main/lib/agent-runtime/hermes-acp-runtime.ts create mode 100644 src/main/lib/agent-runtime/launch-plan.ts create mode 100644 src/main/lib/agent-runtime/native-thread-summary.ts create mode 100644 src/main/lib/agent-runtime/process-registry.ts create mode 100644 src/main/lib/agent-runtime/provider-instances.ts create mode 100644 src/main/lib/agent-runtime/provider-runtime-contract.ts create mode 100644 src/main/lib/agent-runtime/runtime-receipt-bus.ts create mode 100644 src/main/lib/agent-runtime/runtime-run-ledger.ts create mode 100644 src/main/lib/agent-runtime/stale-stream-state.ts create mode 100644 src/main/lib/claude-plugin-settings.ts create mode 100644 src/main/lib/git/status-fallback.ts create mode 100644 src/main/lib/mobile-gateway/database-sessions.ts create mode 100644 src/main/lib/mobile-gateway/desktop-state.ts create mode 100644 src/main/lib/mobile-gateway/desktop.ts create mode 100644 src/main/lib/mobile-gateway/facade.ts create mode 100644 src/main/lib/mobile-gateway/server.ts create mode 100644 src/main/lib/mobile-gateway/sessions.ts create mode 100644 src/main/lib/moss-account/index.ts create mode 100644 src/main/lib/moss-source/frontmatter.ts create mode 100644 src/main/lib/moss-source/runtime-context.ts create mode 100644 src/main/lib/pet-runtime-status.ts create mode 100644 src/main/lib/shared-resources/codex-native-resources.test.ts create mode 100644 src/main/lib/shared-resources/codex-native-resources.ts create mode 100644 src/main/lib/shared-resources/governance.test.ts create mode 100644 src/main/lib/shared-resources/index.ts create mode 100644 src/main/lib/shared-resources/registry.test.ts create mode 100644 src/main/lib/shared-resources/registry.ts create mode 100644 src/main/lib/trpc/routers/agent-runtime.ts create mode 100644 src/main/lib/trpc/routers/hermes.ts create mode 100644 src/main/lib/trpc/routers/mobile-gateway.ts create mode 100644 src/main/lib/trpc/routers/moss-account.ts create mode 100644 src/main/lib/trpc/routers/pet-runtime.ts create mode 100644 src/main/lib/trpc/routers/release-readiness.ts create mode 100644 src/main/lib/trpc/routers/shared-resources.ts create mode 100644 src/main/lib/trpc/routers/skill-md.ts create mode 100644 src/renderer/assets/app-icons/computer-use-plugin-icon.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-chrome.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-codex-labs.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-cursor.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-docs.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-gmail.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-huggingface.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-linear.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-more.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-plugin-grid.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-plugin-store.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-purple-dots.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-record-replay.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-sheets.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-slides.png create mode 100644 src/renderer/assets/plugin-icons/codex-connected-striped.png create mode 100644 src/renderer/assets/plugin-icons/codex-marketplace-actively.png create mode 100644 src/renderer/assets/plugin-icons/codex-plugin-detail-gradient.png create mode 100644 src/renderer/assets/plugin-icons/codex-record-replay-plugin-icon.png create mode 100644 src/renderer/features/agents/commands/slash-command-trigger.ts create mode 100644 src/renderer/features/agents/lib/agent-runtime.test.ts create mode 100644 src/renderer/features/agents/lib/agent-runtime.ts create mode 100644 src/renderer/features/agents/lib/models.test.ts create mode 100644 src/renderer/features/agents/lib/shared-agent-support.ts create mode 100644 src/renderer/features/plugins/plugin-agent-support.tsx create mode 100644 src/renderer/features/plugins/plugin-entry-surfaces.test.ts create mode 100644 src/renderer/features/plugins/plugin-entry-surfaces.ts create mode 100644 src/renderer/features/plugins/plugin-entry-view.tsx create mode 100644 src/renderer/features/plugins/plugin-resource-model.ts create mode 100644 src/renderer/features/plugins/plugin-route-state.ts create mode 100644 src/renderer/lib/i18n.ts create mode 100644 src/shared/plugin-deep-link.ts diff --git a/package.json b/package.json index 97eb0b406..36d6a4ca9 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "db:studio": "drizzle-kit studio", "ts:check": "tsgo --noEmit", "postinstall": "node -e \"if(!process.env.VERCEL){require('child_process').execSync('electron-rebuild -f -w better-sqlite3,node-pty',{stdio:'inherit'})}\" && node scripts/patch-electron-dev.mjs", - "test:runtime": "bun test src/main/lib/moss-account/entitlement.test.ts src/main/lib/moss-source/provider-config.test.ts src/main/lib/mcp-stdio-compat.test.ts src/main/lib/trpc/routers/chat-runtime-selection.test.ts src/main/lib/trpc/routers/codex-mcp-session.test.ts src/main/lib/codex-automations.test.ts src/shared/codex-runtime-notices.test.ts", + "test:runtime": "bun test src/main/lib/moss-account/entitlement.test.ts src/main/lib/moss-source/provider-config.test.ts src/main/lib/mcp-stdio-compat.test.ts src/main/lib/trpc/routers/chat-runtime-selection.test.ts src/main/lib/trpc/routers/codex-mcp-session.test.ts src/main/lib/codex-automations.test.ts src/main/lib/shared-resources/registry.test.ts src/main/lib/shared-resources/codex-native-resources.test.ts src/main/lib/shared-resources/governance.test.ts src/renderer/features/agents/lib/agent-runtime.test.ts src/renderer/features/agents/lib/models.test.ts src/renderer/features/plugins/plugin-entry-surfaces.test.ts src/shared/codex-runtime-notices.test.ts", "release:credentials:strict": "node scripts/verify-release-credentials.mjs --require-credentials", "test:packaged-app-smoke": "node scripts/smoke-packaged-app.mjs", "release:notarize": "node scripts/notarize-release-artifacts.mjs", diff --git a/scripts/generate-update-manifest.mjs b/scripts/generate-update-manifest.mjs index d8f73ee6a..f7d09cfb0 100644 --- a/scripts/generate-update-manifest.mjs +++ b/scripts/generate-update-manifest.mjs @@ -10,8 +10,8 @@ * node scripts/generate-update-manifest.mjs * * The script expects ZIP files to exist in the release/ directory: - * - Agents-{version}-arm64-mac.zip - * - Agents-{version}-mac.zip + * - 1Code-{version}-arm64-mac.zip + * - 1Code-{version}-mac.zip * * Run this after `npm run dist` to generate the manifest files. */ @@ -77,8 +77,8 @@ function findReleaseFile(pattern, ext = ".zip") { */ function generateManifest(arch) { // electron-builder names files differently: - // arm64: Agents-{version}-arm64-mac.zip - // x64: Agents-{version}-mac.zip + // arm64: 1Code-{version}-arm64-mac.zip + // x64: 1Code-{version}-mac.zip const pattern = arch === "arm64" ? `${version}-arm64-mac` : `${version}-mac` const zipPath = findReleaseFile(pattern, ".zip") @@ -248,13 +248,13 @@ console.log("Next steps:") console.log("1. Upload the following files to cdn.21st.dev/releases/desktop/:") if (arm64Manifest) { console.log(` - ${prefix}-mac.yml`) - console.log(` - Agents-${version}-arm64-mac.zip`) - console.log(` - Agents-${version}-arm64.dmg (for manual download)`) + console.log(` - 1Code-${version}-arm64-mac.zip`) + console.log(` - 1Code-${version}-arm64.dmg (for manual download)`) } if (x64Manifest) { console.log(` - ${prefix}-mac-x64.yml`) - console.log(` - Agents-${version}-mac.zip`) - console.log(` - Agents-${version}.dmg (for manual download)`) + console.log(` - 1Code-${version}-mac.zip`) + console.log(` - 1Code-${version}.dmg (for manual download)`) } console.log("2. Create a release entry in the admin dashboard") console.log("=".repeat(50)) diff --git a/src/main/auth-manager.ts b/src/main/auth-manager.ts index e31b7bc1b..d139f233c 100644 --- a/src/main/auth-manager.ts +++ b/src/main/auth-manager.ts @@ -1,6 +1,6 @@ import { AuthStore, AuthData, AuthUser } from "./auth-store" import { app, BrowserWindow } from "electron" -import { AUTH_SERVER_PORT } from "./constants" +import { getAuthServerPort } from "./constants" // Get API URL - in packaged app always use production, in dev allow override function getApiBaseUrl(): string { @@ -214,7 +214,7 @@ export class AuthManager { // In dev mode, use localhost callback (we run HTTP server on AUTH_SERVER_PORT) // Also pass the protocol so web knows which deep link to use as fallback if (this.isDev) { - authUrl += `&callback=${encodeURIComponent(`http://localhost:${AUTH_SERVER_PORT}/auth/callback`)}` + authUrl += `&callback=${encodeURIComponent(`http://localhost:${getAuthServerPort()}/auth/callback`)}` // Pass dev protocol so production web can use correct deep link if callback fails authUrl += `&protocol=twentyfirst-agents-dev` } @@ -256,7 +256,23 @@ export class AuthManager { * Fetch user's subscription plan from web backend * Used for PostHog analytics enrichment */ - async fetchUserPlan(): Promise<{ email: string; plan: string; status: string | null } | null> { + async fetchUserPlan(): Promise<{ + email: string + plan: string + status: string | null + quota?: { + includedCredits?: number | null + usedCredits?: number | null + remainingCredits?: number | null + resetAt?: string | null + unit?: string | null + } | null + includedCredits?: number | null + usedCredits?: number | null + remainingCredits?: number | null + resetAt?: string | null + quotaUnit?: string | null + } | null> { const token = await this.getValidToken() if (!token) return null diff --git a/src/main/constants.ts b/src/main/constants.ts index 088f24356..954c04039 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -3,3 +3,13 @@ export const IS_DEV = !!process.env.ELECTRON_RENDERER_URL // Auth server port - use different port in dev to allow running alongside production export const AUTH_SERVER_PORT = IS_DEV ? 21322 : 21321 + +let runtimeAuthServerPort = AUTH_SERVER_PORT + +export function getAuthServerPort(): number { + return runtimeAuthServerPort +} + +export function setAuthServerPort(port: number): void { + runtimeAuthServerPort = port +} diff --git a/src/main/index.ts b/src/main/index.ts index 57af873f0..67b63fea9 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,9 +1,20 @@ import * as Sentry from "@sentry/electron/main" -import { app, BrowserWindow, dialog, Menu, nativeImage, session } from "electron" +import { + app, + BrowserWindow, + dialog, + Menu, + nativeImage, + session, +} from "electron" import { existsSync, readFileSync, readlinkSync, unlinkSync } from "fs" import { createServer } from "http" import { join } from "path" -import { AuthManager, initAuthManager, getAuthManager as getAuthManagerFromModule } from "./auth-manager" +import { + AuthManager, + initAuthManager, + getAuthManager as getAuthManagerFromModule, +} from "./auth-manager" import { identify, initAnalytics, @@ -16,6 +27,7 @@ import { checkForUpdates, downloadUpdate, initAutoUpdater, + registerAutoUpdaterIpcHandlers, setupFocusUpdateCheck, } from "./lib/auto-updater" import { closeDatabase, initDatabase } from "./lib/db" @@ -28,8 +40,25 @@ import { } from "./lib/cli" import { cleanupGitWatchers } from "./lib/git/watcher" import { cancelAllPendingOAuth, handleMcpOAuthCallback } from "./lib/mcp-auth" -import { getAllMcpConfigHandler, hasActiveClaudeSessions, abortAllClaudeSessions } from "./lib/trpc/routers/claude" -import { getAllCodexMcpConfigHandler, hasActiveCodexStreams, abortAllCodexStreams } from "./lib/trpc/routers/codex" +import { + getAllMcpConfigHandler, + hasActiveClaudeSessions, + abortAllClaudeSessions, +} from "./lib/trpc/routers/claude" +import { + getAllCodexMcpConfigHandler, + hasActiveCodexStreams, + abortAllCodexStreams, +} from "./lib/trpc/routers/codex" +import { + hasActiveHermesStreams, + abortAllHermesStreams, +} from "./lib/trpc/routers/hermes" +import { + startDesktopMobileGateway, + type RunningDesktopMobileGateway, +} from "./lib/mobile-gateway/desktop" +import { setDesktopMobileGateway } from "./lib/mobile-gateway/desktop-state" import { createMainWindow, createWindow, @@ -39,7 +68,16 @@ import { } from "./windows/main" import { windowManager } from "./windows/window-manager" -import { IS_DEV, AUTH_SERVER_PORT } from "./constants" +import { + IS_DEV, + AUTH_SERVER_PORT, + getAuthServerPort, + setAuthServerPort, +} from "./constants" +import { + parsePluginDeepLink, + type PluginDeepLinkTarget, +} from "../shared/plugin-deep-link" // Deep link protocol (must match package.json build.protocols.schemes) // Use different protocol in dev to avoid conflicts with production app @@ -93,6 +131,7 @@ export function getAppUrl(): string { // Auth manager singleton (use the one from auth-manager module) let authManager: AuthManager +let mobileGatewayServer: RunningDesktopMobileGateway | null = null export function getAuthManager(): AuthManager { // First try to get from module, fallback to local variable for backwards compat @@ -185,6 +224,41 @@ export async function handleAuthCode(code: string): Promise { } } +function sendPluginDeepLink( + win: BrowserWindow, + target: PluginDeepLinkTarget, +): void { + if (win.isDestroyed()) return + + const send = () => { + if (!win.isDestroyed()) { + win.webContents.send("plugin:open-detail", target) + } + } + + if (win.webContents.isLoading()) { + win.webContents.once("did-finish-load", send) + } else { + send() + } + + if (win.isMinimized()) win.restore() + win.focus() +} + +function dispatchPluginDeepLink(target: PluginDeepLinkTarget): void { + const windows = getAllWindows() + + if (windows.length === 0) { + sendPluginDeepLink(createMainWindow(), target) + return + } + + for (const win of windows) { + sendPluginDeepLink(win, target) + } +} + // Handle deep link function handleDeepLink(url: string): void { console.log("[DeepLink] Received:", url) @@ -210,6 +284,16 @@ function handleDeepLink(url: string): void { return } } + + // Handle plugin catalog links: + // twentyfirst-agents://plugins/github + // twentyfirst-agents:///plugins/github + // twentyfirst-agents://plugins/github/try-in-chat + const pluginDeepLink = parsePluginDeepLink(url) + if (pluginDeepLink) { + dispatchPluginDeepLink(pluginDeepLink) + return + } } catch (e) { console.error("[DeepLink] Failed to parse:", e) } @@ -290,29 +374,30 @@ const FAVICON_DATA_URI = `data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}` // Start local HTTP server for auth callbacks // This catches http://localhost:{AUTH_SERVER_PORT}/auth/callback?code=xxx and /callback (for MCP OAuth) const server = createServer((req, res) => { - const url = new URL(req.url || "", `http://localhost:${AUTH_SERVER_PORT}`) - - // Serve favicon - if (url.pathname === "/favicon.ico" || url.pathname === "/favicon.svg") { - res.writeHead(200, { "Content-Type": "image/svg+xml" }) - res.end(FAVICON_SVG) - return - } + const requestHost = req.headers.host || `localhost:${getAuthServerPort()}` + const url = new URL(req.url || "", `http://${requestHost}`) + + // Serve favicon + if (url.pathname === "/favicon.ico" || url.pathname === "/favicon.svg") { + res.writeHead(200, { "Content-Type": "image/svg+xml" }) + res.end(FAVICON_SVG) + return + } - if (url.pathname === "/auth/callback") { - const code = url.searchParams.get("code") - console.log( - "[Auth Server] Received callback with code:", - code?.slice(0, 8) + "...", - ) + if (url.pathname === "/auth/callback") { + const code = url.searchParams.get("code") + console.log( + "[Auth Server] Received callback with code:", + code?.slice(0, 8) + "...", + ) - if (code) { - // Handle the auth code - handleAuthCode(code) + if (code) { + // Handle the auth code + handleAuthCode(code) - // Send success response and close the browser tab - res.writeHead(200, { "Content-Type": "text/html" }) - res.end(` + // Send success response and close the browser tab + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(` @@ -375,28 +460,28 @@ const server = createServer((req, res) => { `) - } else { - res.writeHead(400, { "Content-Type": "text/plain" }) - res.end("Missing code parameter") - } - } else if (url.pathname === "/callback") { - // Handle MCP OAuth callback - const code = url.searchParams.get("code") - const state = url.searchParams.get("state") - console.log( - "[Auth Server] Received MCP OAuth callback with code:", - code?.slice(0, 8) + "...", - "state:", - state?.slice(0, 8) + "...", - ) + } else { + res.writeHead(400, { "Content-Type": "text/plain" }) + res.end("Missing code parameter") + } + } else if (url.pathname === "/callback") { + // Handle MCP OAuth callback + const code = url.searchParams.get("code") + const state = url.searchParams.get("state") + console.log( + "[Auth Server] Received MCP OAuth callback with code:", + code?.slice(0, 8) + "...", + "state:", + state?.slice(0, 8) + "...", + ) - if (code && state) { - // Handle the MCP OAuth callback - handleMcpOAuthCallback(code, state) + if (code && state) { + // Handle the MCP OAuth callback + handleMcpOAuthCallback(code, state) - // Send success response and close the browser tab - res.writeHead(200, { "Content-Type": "text/html" }) - res.end(` + // Send success response and close the browser tab + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(` @@ -459,19 +544,45 @@ const server = createServer((req, res) => { `) - } else { - res.writeHead(400, { "Content-Type": "text/plain" }) - res.end("Missing code or state parameter") - } } else { - res.writeHead(404, { "Content-Type": "text/plain" }) - res.end("Not found") + res.writeHead(400, { "Content-Type": "text/plain" }) + res.end("Missing code or state parameter") + } + } else { + res.writeHead(404, { "Content-Type": "text/plain" }) + res.end("Not found") + } +}) + +function listenForAuthCallbacks(port: number, attempt = 0): void { + setAuthServerPort(port) + + server.once("error", (error: NodeJS.ErrnoException) => { + if (IS_DEV && error.code === "EADDRINUSE" && attempt < 20) { + const nextPort = port + 1 + console.warn( + `[Auth Server] Port ${port} is already in use; retrying on ${nextPort}`, + ) + listenForAuthCallbacks(nextPort, attempt + 1) + return + } + + console.error("[Auth Server] Failed to listen:", error) + if (!IS_DEV) { + throw error } }) -server.listen(AUTH_SERVER_PORT, () => { - console.log(`[Auth Server] Listening on http://localhost:${AUTH_SERVER_PORT}`) -}) + server.listen(port, () => { + const address = server.address() + const actualPort = + address && typeof address === "object" ? address.port : port + setAuthServerPort(actualPort) + console.log(`[Auth Server] Listening on http://localhost:${actualPort}`) + }) +} + +listenForAuthCallbacks(AUTH_SERVER_PORT) // Clean up stale lock files from crashed instances // Returns true if locks were cleaned, false otherwise @@ -496,7 +607,12 @@ function cleanupStaleLocks(): boolean { } catch { // Process doesn't exist, clean up stale locks console.log("[App] Cleaning stale locks (pid", pid, "not running)") - const filesToRemove = ["SingletonLock", "SingletonSocket", "SingletonCookie"] + const filesToRemove = [ + "SingletonLock", + "SingletonSocket", + "SingletonCookie", + "DevToolsActivePort", + ] for (const file of filesToRemove) { const filePath = join(userDataPath, file) if (existsSync(filePath)) { @@ -516,6 +632,10 @@ function cleanupStaleLocks(): boolean { return false } +// Clean crashed dev instances before Electron evaluates the single-instance lock. +// If the lock belongs to a live process, cleanupStaleLocks() leaves it intact. +cleanupStaleLocks() + // Prevent multiple instances let gotTheLock = app.requestSingleInstanceLock() @@ -558,7 +678,6 @@ if (gotTheLock) { // app.name = "Agents Dev" // } - // Register protocol handler (must be after app is ready) initialRegistration = registerProtocol() @@ -613,11 +732,14 @@ if (gotTheLock) { // Menu icons: PNG template for settings (auto light/dark via "Template" suffix), // macOS native SF Symbol for terminal const settingsMenuIcon = nativeImage.createFromPath( - join(__dirname, "../../build/settingsTemplate.png") + join(__dirname, "../../build/settingsTemplate.png"), ) - const terminalMenuIcon = process.platform === "darwin" - ? nativeImage.createFromNamedImage("terminal")?.resize({ width: 12, height: 12 }) - : null + const terminalMenuIcon = + process.platform === "darwin" + ? nativeImage + .createFromNamedImage("terminal") + ?.resize({ width: 12, height: 12 }) + : null // Function to build and set application menu const buildMenu = () => { @@ -675,11 +797,15 @@ if (gotTheLock) { dialog.showMessageBox({ type: "info", message: "CLI command uninstalled", - detail: "The '1code' command has been removed from your PATH.", + detail: + "The '1code' command has been removed from your PATH.", }) buildMenu() } else { - dialog.showErrorBox("Uninstallation Failed", result.error || "Unknown error") + dialog.showErrorBox( + "Uninstallation Failed", + result.error || "Unknown error", + ) } } else { const result = await installCli() @@ -692,7 +818,10 @@ if (gotTheLock) { }) buildMenu() } else { - dialog.showErrorBox("Installation Failed", result.error || "Unknown error") + dialog.showErrorBox( + "Installation Failed", + result.error || "Unknown error", + ) } } }, @@ -708,7 +837,11 @@ if (gotTheLock) { label: "Quit", accelerator: "CmdOrCtrl+Q", click: async () => { - if (hasActiveClaudeSessions() || hasActiveCodexStreams()) { + if ( + hasActiveClaudeSessions() || + hasActiveCodexStreams() || + hasActiveHermesStreams() + ) { const { dialog } = await import("electron") const { response } = await dialog.showMessageBox({ type: "warning", @@ -717,11 +850,13 @@ if (gotTheLock) { cancelId: 0, title: "Active Sessions", message: "There are active agent sessions running.", - detail: "Quitting now will interrupt them. Are you sure you want to quit?", + detail: + "Quitting now will interrupt them. Are you sure you want to quit?", }) if (response === 1) { abortAllClaudeSessions() abortAllCodexStreams() + abortAllHermesStreams() setIsQuitting(true) app.quit() } @@ -793,7 +928,11 @@ if (gotTheLock) { click: () => { const win = BrowserWindow.getFocusedWindow() if (!win) return - if (hasActiveClaudeSessions() || hasActiveCodexStreams()) { + if ( + hasActiveClaudeSessions() || + hasActiveCodexStreams() || + hasActiveHermesStreams() + ) { dialog .showMessageBox(win, { type: "warning", @@ -809,6 +948,7 @@ if (gotTheLock) { if (response === 1) { abortAllClaudeSessions() abortAllCodexStreams() + abortAllHermesStreams() win.webContents.reloadIgnoringCache() } }) @@ -853,7 +993,7 @@ if (gotTheLock) { } // macOS: Set dock menu (right-click on dock icon) - if (process.platform === "darwin") { + if (process.platform === "darwin" && app.dock) { const dockMenu = Menu.buildFromTemplate([ { label: "New Window", @@ -939,6 +1079,33 @@ if (gotTheLock) { console.error("[App] Failed to initialize database:", error) } + try { + mobileGatewayServer = await startDesktopMobileGateway({ + enabled: IS_DEV || process.env.ONECODE_MOBILE_GATEWAY === "1", + }) + setDesktopMobileGateway(mobileGatewayServer) + if (mobileGatewayServer) { + console.log(`[Mobile Gateway] Listening on ${mobileGatewayServer.url}`) + if (IS_DEV) { + console.log( + `[Mobile Gateway] Dev pairing token: ${mobileGatewayServer.pairingToken}`, + ) + const pairingUrl = mobileGatewayServer.getPairingUrl() + if (pairingUrl) { + console.log(`[Mobile Gateway] Dev pairing URL: ${pairingUrl}`) + } + } + } + } catch (error) { + setDesktopMobileGateway(null) + console.warn("[Mobile Gateway] Failed to start:", error) + } + + // Dev builds still render update preferences, but should not initialize updater. + if (!app.isPackaged) { + registerAutoUpdaterIpcHandlers() + } + // Create main window createMainWindow() @@ -1003,6 +1170,12 @@ if (gotTheLock) { app.on("before-quit", async () => { console.log("[App] Shutting down...") cancelAllPendingOAuth() + if (mobileGatewayServer) { + await mobileGatewayServer.close() + mobileGatewayServer = null + setDesktopMobileGateway(null) + console.log("[Mobile Gateway] Closed") + } await cleanupGitWatchers() await shutdownAnalytics() await closeDatabase() diff --git a/src/main/lib/agent-runtime/adapters/claude-code.ts b/src/main/lib/agent-runtime/adapters/claude-code.ts index 7e8a2294f..6d2a1b6b6 100644 --- a/src/main/lib/agent-runtime/adapters/claude-code.ts +++ b/src/main/lib/agent-runtime/adapters/claude-code.ts @@ -8,11 +8,17 @@ import { getDatabase, } from "../../db" import { getAgentRuntimeManifest } from "../manifests" +import { + createUnsupportedAgentRuntimeControlResult, + createUnsupportedAgentRuntimeReceipt, + streamUnsupportedAgentRuntimeRun, +} from "../runtime-run-ledger" import type { AgentRuntimeAdapter, AgentRuntimeAvailability, AgentRuntimeHealth, AgentRuntimeSessionRef, + AgentRuntimeStartRequest, } from "../types" function getClaudeStoredAuthMethod(): "oauth" | null { @@ -107,6 +113,18 @@ async function inspectClaudeRuntime(): Promise { } } +function unsupportedClaudeRun( + action: "start" | "resume", + request: AgentRuntimeStartRequest, +) { + return createUnsupportedAgentRuntimeReceipt({ + action, + request, + reason: + "Claude Code lifecycle is still owned by the existing SDK transport; the unified Moss adapter bridge is not connected yet.", + }) +} + export const claudeCodeAdapter: AgentRuntimeAdapter = { manifest: getAgentRuntimeManifest("claude-code"), async inspect( @@ -119,4 +137,25 @@ export const claudeCodeAdapter: AgentRuntimeAdapter = { ): Promise { return (await inspectClaudeRuntime()).availability }, + async start(request) { + return unsupportedClaudeRun("start", request) + }, + async resume(request) { + return unsupportedClaudeRun("resume", request) + }, + stream(request) { + return streamUnsupportedAgentRuntimeRun(unsupportedClaudeRun("start", request)) + }, + async stop(request) { + return createUnsupportedAgentRuntimeControlResult( + request, + "Claude Code unified stop requires the SDK transport bridge slice.", + ) + }, + async submitToolResult(request) { + return createUnsupportedAgentRuntimeControlResult( + request, + "Claude Code unified tool result submission requires the SDK transport bridge slice.", + ) + }, } diff --git a/src/main/lib/agent-runtime/adapters/codex.ts b/src/main/lib/agent-runtime/adapters/codex.ts index 8eebcb2c8..359633aac 100644 --- a/src/main/lib/agent-runtime/adapters/codex.ts +++ b/src/main/lib/agent-runtime/adapters/codex.ts @@ -1,33 +1,175 @@ -import { getCodexIntegrationStatus } from "../../trpc/routers/codex" -import { getAgentRuntimeManifest } from "../manifests" +import { + batchWriteCodexAppServerConfig, + cancelCodexAppServerAccountLogin, + clearCodexAppServerThreadGoal, + controlCodexAppServerThread, + detectCodexAppServerExternalAgentConfig, + forkCodexAppServerThread, + getCodexAppServerThreadFullDiff, + getCodexAppServerThreadGoal, + getCodexAppServerThreadTurnDiff, + importCodexAppServerExternalAgentConfig, + installCodexAppServerPlugin, + listCodexAppServerApps, + listCodexAppServerHooks, + listCodexAppServerMcpServerStatuses, + listCodexAppServerModels, + listCodexAppServerPermissionProfiles, + listCodexAppServerPlugins, + listCodexAppServerSkills, + listInstalledCodexAppServerPlugins, + listLoadedCodexAppServerThreads, + listCodexAppServerThreads, + logoutCodexAppServerAccount, + readCodexAppServerAccount, + readCodexAppServerAccountRateLimits, + readCodexAppServerAccountUsage, + readCodexAppServerConfig, + readCodexAppServerConfigRequirements, + readCodexAppServerPlugin, + readCodexAppServerThread, + reloadCodexAppServerMcpServerConfig, + runCodexAppServerRuntimeRun, + rollbackCodexAppServerThread, + setCodexAppServerThreadGoal, + setCodexAppServerThreadName, + startCodexAppServerAccountLogin, + startCodexAppServerMcpServerOauthLogin, + streamCodexAppServerRuntimeRun, + updateCodexAppServerThreadMetadata, + writeCodexAppServerConfigValue, +} from "../codex-app-server-runtime"; +import { getAgentRuntimeManifest } from "../manifests"; +import { + stopAgentRuntimeProcess, + submitAgentRuntimeToolResult, +} from "../process-registry"; import type { AgentRuntimeAdapter, + AgentRuntimeAccountLoginCancelRequest, + AgentRuntimeAccountLoginCancelResult, + AgentRuntimeAccountLoginStartRequest, + AgentRuntimeAccountLoginStartResult, + AgentRuntimeAccountLogoutRequest, + AgentRuntimeAccountLogoutResult, + AgentRuntimeAccountRateLimitsReadRequest, + AgentRuntimeAccountRateLimitsReadResult, + AgentRuntimeAccountReadRequest, + AgentRuntimeAccountReadResult, + AgentRuntimeAccountUsageReadRequest, + AgentRuntimeAccountUsageReadResult, + AgentRuntimeAppListRequest, + AgentRuntimeAppListResult, + AgentRuntimeConfigBatchWriteRequest, AgentRuntimeAvailability, + AgentRuntimeConfigReadRequest, + AgentRuntimeConfigReadResult, + AgentRuntimeConfigRequirementsReadRequest, + AgentRuntimeConfigRequirementsReadResult, + AgentRuntimeConfigValueWriteRequest, + AgentRuntimeConfigWriteResult, + AgentRuntimeExternalAgentConfigDetectRequest, + AgentRuntimeExternalAgentConfigDetectResult, + AgentRuntimeExternalAgentConfigImportRequest, + AgentRuntimeExternalAgentConfigImportResult, + AgentRuntimeHookListRequest, + AgentRuntimeHookListResult, + AgentRuntimeMcpServerConfigReloadRequest, + AgentRuntimeMcpServerConfigReloadResult, + AgentRuntimeMcpServerStatusListRequest, + AgentRuntimeMcpServerStatusListResult, + AgentRuntimeMcpServerOauthLoginRequest, + AgentRuntimeMcpServerOauthLoginResult, + AgentRuntimeModelListRequest, + AgentRuntimeModelListResult, + AgentRuntimePermissionProfileListRequest, + AgentRuntimePermissionProfileListResult, + AgentRuntimePluginInstalledRequest, + AgentRuntimePluginInstalledResult, + AgentRuntimePluginInstallRequest, + AgentRuntimePluginInstallResult, + AgentRuntimePluginListRequest, + AgentRuntimePluginListResult, + AgentRuntimePluginReadRequest, + AgentRuntimePluginReadResult, + AgentRuntimeRunReceipt, AgentRuntimeHealth, AgentRuntimeSessionRef, -} from "../types" + AgentRuntimeSkillListRequest, + AgentRuntimeSkillListResult, + AgentRuntimeStartRequest, + AgentRuntimeStreamEvent, + AgentRuntimeThreadControlRequest, + AgentRuntimeThreadControlResult, + AgentRuntimeThreadDiffResult, + AgentRuntimeThreadFullDiffRequest, + AgentRuntimeThreadForkRequest, + AgentRuntimeThreadForkResult, + AgentRuntimeThreadGoalClearRequest, + AgentRuntimeThreadGoalClearResult, + AgentRuntimeThreadGoalGetRequest, + AgentRuntimeThreadGoalGetResult, + AgentRuntimeThreadGoalSetRequest, + AgentRuntimeThreadGoalSetResult, + AgentRuntimeThreadLoadedListRequest, + AgentRuntimeThreadLoadedListResult, + AgentRuntimeThreadListRequest, + AgentRuntimeThreadListResult, + AgentRuntimeThreadMetadataUpdateRequest, + AgentRuntimeThreadMetadataUpdateResult, + AgentRuntimeThreadNameSetRequest, + AgentRuntimeThreadNameSetResult, + AgentRuntimeThreadReadRequest, + AgentRuntimeThreadReadResult, + AgentRuntimeThreadRollbackRequest, + AgentRuntimeThreadRollbackResult, + AgentRuntimeThreadTurnDiffRequest, +} from "../types"; + +function createCodexRuntimeVersionAdvisory( + version: string | null, +): AgentRuntimeHealth["versionAdvisory"] { + return { + status: "unknown", + currentVersion: version, + latestVersion: null, + updateCommand: null, + canUpdate: false, + checkedAt: new Date().toISOString(), + message: version + ? "Codex CLI version detected; latest-version checks are not connected yet." + : "Codex CLI version could not be detected.", + }; +} async function inspectCodexRuntime(): Promise { - const manifest = getAgentRuntimeManifest("codex") + const manifest = getAgentRuntimeManifest("codex"); try { - const integration = await getCodexIntegrationStatus() - const authMethod = integration.state === "connected_api_key" - ? "api-key" - : integration.state === "connected_chatgpt" - ? "oauth" - : "not-authenticated" + const { getCodexIntegrationStatus } = + await import("../../trpc/routers/codex"); + const integration = await getCodexIntegrationStatus(); + const authMethod = + integration.state === "connected_api_key" + ? "api-key" + : integration.state === "connected_chatgpt" + ? "oauth" + : "not-authenticated"; if (integration.isConnected) { return { availability: "available", statusReason: `Codex auth detected via ${integration.state}.`, authMethod, + version: integration.version, + versionAdvisory: createCodexRuntimeVersionAdvisory( + integration.version, + ), models: manifest.models?.map((model) => ({ ...model, availability: "available", })), - } + }; } return { @@ -36,38 +178,405 @@ async function inspectCodexRuntime(): Promise { integration.rawOutput || "Codex CLI is installed but no login was found.", authMethod, + version: integration.version, + versionAdvisory: createCodexRuntimeVersionAdvisory(integration.version), models: manifest.models?.map((model) => ({ ...model, availability: "needs-auth", reason: "Codex authentication is required.", })), - } + }; } catch (error) { - const message = error instanceof Error ? error.message : String(error) - const missingBinary = message.includes("Bundled Codex CLI not found") + const message = error instanceof Error ? error.message : String(error); + const missingBinary = message.includes("Bundled Codex CLI not found"); return { availability: missingBinary ? "not-installed" : "error", statusReason: message, authMethod: "unknown", + version: null, + versionAdvisory: createCodexRuntimeVersionAdvisory(null), models: manifest.models?.map((model) => ({ ...model, availability: missingBinary ? "not-installed" : "error", reason: message, })), - } + }; } } +async function runCodexLifecycle( + action: "start" | "resume", + request: AgentRuntimeStartRequest, +): Promise { + return runCodexAppServerRuntimeRun(action, request); +} + +async function* streamCodexLifecycle( + request: AgentRuntimeStartRequest, +): AsyncIterable { + const action = + request.session.nativeSessionId && !request.forceNewSession + ? "resume" + : "start"; + yield* streamCodexAppServerRuntimeRun(action, request); +} + +async function readCodexThread( + request: AgentRuntimeThreadReadRequest, +): Promise { + return readCodexAppServerThread(request); +} + +async function readCodexConfig( + request: AgentRuntimeConfigReadRequest, +): Promise { + return readCodexAppServerConfig(request); +} + +async function writeCodexConfigValue( + request: AgentRuntimeConfigValueWriteRequest, +): Promise { + return writeCodexAppServerConfigValue(request); +} + +async function batchWriteCodexConfig( + request: AgentRuntimeConfigBatchWriteRequest, +): Promise { + return batchWriteCodexAppServerConfig(request); +} + +async function readCodexConfigRequirements( + request: AgentRuntimeConfigRequirementsReadRequest, +): Promise { + return readCodexAppServerConfigRequirements(request); +} + +async function listCodexPermissionProfiles( + request: AgentRuntimePermissionProfileListRequest, +): Promise { + return listCodexAppServerPermissionProfiles(request); +} + +async function listCodexMcpServerStatuses( + request: AgentRuntimeMcpServerStatusListRequest, +): Promise { + return listCodexAppServerMcpServerStatuses(request); +} + +async function reloadCodexMcpServerConfig( + request: AgentRuntimeMcpServerConfigReloadRequest, +): Promise { + return reloadCodexAppServerMcpServerConfig(request); +} + +async function listCodexSkills( + request: AgentRuntimeSkillListRequest, +): Promise { + return listCodexAppServerSkills(request); +} + +async function listCodexHooks( + request: AgentRuntimeHookListRequest, +): Promise { + return listCodexAppServerHooks(request); +} + +async function listCodexApps( + request: AgentRuntimeAppListRequest, +): Promise { + return listCodexAppServerApps(request); +} + +async function listCodexPlugins( + request: AgentRuntimePluginListRequest, +): Promise { + return listCodexAppServerPlugins(request); +} + +async function listInstalledCodexPlugins( + request: AgentRuntimePluginInstalledRequest, +): Promise { + return listInstalledCodexAppServerPlugins(request); +} + +async function readCodexPlugin( + request: AgentRuntimePluginReadRequest, +): Promise { + return readCodexAppServerPlugin(request); +} + +async function installCodexPlugin( + request: AgentRuntimePluginInstallRequest, +): Promise { + return installCodexAppServerPlugin(request); +} + +async function detectCodexExternalAgentConfig( + request: AgentRuntimeExternalAgentConfigDetectRequest, +): Promise { + return detectCodexAppServerExternalAgentConfig(request); +} + +async function importCodexExternalAgentConfig( + request: AgentRuntimeExternalAgentConfigImportRequest, +): Promise { + return importCodexAppServerExternalAgentConfig(request); +} + +async function startCodexMcpServerOauthLogin( + request: AgentRuntimeMcpServerOauthLoginRequest, +): Promise { + return startCodexAppServerMcpServerOauthLogin(request); +} + +async function listCodexModels( + request: AgentRuntimeModelListRequest, +): Promise { + return listCodexAppServerModels(request); +} + +async function readCodexAccount( + request: AgentRuntimeAccountReadRequest, +): Promise { + return readCodexAppServerAccount(request); +} + +async function startCodexAccountLogin( + request: AgentRuntimeAccountLoginStartRequest, +): Promise { + return startCodexAppServerAccountLogin(request); +} + +async function cancelCodexAccountLogin( + request: AgentRuntimeAccountLoginCancelRequest, +): Promise { + return cancelCodexAppServerAccountLogin(request); +} + +async function logoutCodexAccount( + request: AgentRuntimeAccountLogoutRequest, +): Promise { + return logoutCodexAppServerAccount(request); +} + +async function readCodexAccountRateLimits( + request: AgentRuntimeAccountRateLimitsReadRequest, +): Promise { + return readCodexAppServerAccountRateLimits(request); +} + +async function readCodexAccountUsage( + request: AgentRuntimeAccountUsageReadRequest, +): Promise { + return readCodexAppServerAccountUsage(request); +} + +async function forkCodexThread( + request: AgentRuntimeThreadForkRequest, +): Promise { + return forkCodexAppServerThread(request); +} + +async function getCodexThreadTurnDiff( + request: AgentRuntimeThreadTurnDiffRequest, +): Promise { + return getCodexAppServerThreadTurnDiff(request); +} + +async function getCodexThreadFullDiff( + request: AgentRuntimeThreadFullDiffRequest, +): Promise { + return getCodexAppServerThreadFullDiff(request); +} + +async function listCodexThreads( + request: AgentRuntimeThreadListRequest, +): Promise { + return listCodexAppServerThreads(request); +} + +async function listLoadedCodexThreads( + request: AgentRuntimeThreadLoadedListRequest, +): Promise { + return listLoadedCodexAppServerThreads(request); +} + +async function controlCodexThread( + request: AgentRuntimeThreadControlRequest, +): Promise { + return controlCodexAppServerThread(request); +} + +async function setCodexThreadName( + request: AgentRuntimeThreadNameSetRequest, +): Promise { + return setCodexAppServerThreadName(request); +} + +async function updateCodexThreadMetadata( + request: AgentRuntimeThreadMetadataUpdateRequest, +): Promise { + return updateCodexAppServerThreadMetadata(request); +} + +async function getCodexThreadGoal( + request: AgentRuntimeThreadGoalGetRequest, +): Promise { + return getCodexAppServerThreadGoal(request); +} + +async function setCodexThreadGoal( + request: AgentRuntimeThreadGoalSetRequest, +): Promise { + return setCodexAppServerThreadGoal(request); +} + +async function clearCodexThreadGoal( + request: AgentRuntimeThreadGoalClearRequest, +): Promise { + return clearCodexAppServerThreadGoal(request); +} + +async function rollbackCodexThread( + request: AgentRuntimeThreadRollbackRequest, +): Promise { + return rollbackCodexAppServerThread(request); +} + export const codexAdapter: AgentRuntimeAdapter = { manifest: getAgentRuntimeManifest("codex"), - async inspect( - _session: AgentRuntimeSessionRef, - ): Promise { - return inspectCodexRuntime() + async inspect(_session: AgentRuntimeSessionRef): Promise { + return inspectCodexRuntime(); }, async canStart( _session: AgentRuntimeSessionRef, ): Promise { - return (await inspectCodexRuntime()).availability + return (await inspectCodexRuntime()).availability; }, -} + async start(request) { + return runCodexLifecycle("start", request); + }, + async resume(request) { + return runCodexLifecycle("resume", request); + }, + stream(request) { + return streamCodexLifecycle(request); + }, + async stop(request) { + return stopAgentRuntimeProcess(request); + }, + async submitToolResult(request) { + return submitAgentRuntimeToolResult(request); + }, + async readConfig(request) { + return readCodexConfig(request); + }, + async writeConfigValue(request) { + return writeCodexConfigValue(request); + }, + async batchWriteConfig(request) { + return batchWriteCodexConfig(request); + }, + async readConfigRequirements(request) { + return readCodexConfigRequirements(request); + }, + async listPermissionProfiles(request) { + return listCodexPermissionProfiles(request); + }, + async listMcpServerStatuses(request) { + return listCodexMcpServerStatuses(request); + }, + async reloadMcpServerConfig(request) { + return reloadCodexMcpServerConfig(request); + }, + async listSkills(request) { + return listCodexSkills(request); + }, + async listHooks(request) { + return listCodexHooks(request); + }, + async listApps(request) { + return listCodexApps(request); + }, + async listPlugins(request) { + return listCodexPlugins(request); + }, + async listInstalledPlugins(request) { + return listInstalledCodexPlugins(request); + }, + async readPlugin(request) { + return readCodexPlugin(request); + }, + async installPlugin(request) { + return installCodexPlugin(request); + }, + async detectExternalAgentConfig(request) { + return detectCodexExternalAgentConfig(request); + }, + async importExternalAgentConfig(request) { + return importCodexExternalAgentConfig(request); + }, + async startMcpServerOauthLogin(request) { + return startCodexMcpServerOauthLogin(request); + }, + async listModels(request) { + return listCodexModels(request); + }, + async startAccountLogin(request) { + return startCodexAccountLogin(request); + }, + async cancelAccountLogin(request) { + return cancelCodexAccountLogin(request); + }, + async logoutAccount(request) { + return logoutCodexAccount(request); + }, + async readAccount(request) { + return readCodexAccount(request); + }, + async readAccountRateLimits(request) { + return readCodexAccountRateLimits(request); + }, + async readAccountUsage(request) { + return readCodexAccountUsage(request); + }, + async readThread(request) { + return readCodexThread(request); + }, + async forkThread(request) { + return forkCodexThread(request); + }, + async getThreadTurnDiff(request) { + return getCodexThreadTurnDiff(request); + }, + async getThreadFullDiff(request) { + return getCodexThreadFullDiff(request); + }, + async listThreads(request) { + return listCodexThreads(request); + }, + async listLoadedThreads(request) { + return listLoadedCodexThreads(request); + }, + async controlThread(request) { + return controlCodexThread(request); + }, + async setThreadName(request) { + return setCodexThreadName(request); + }, + async updateThreadMetadata(request) { + return updateCodexThreadMetadata(request); + }, + async getThreadGoal(request) { + return getCodexThreadGoal(request); + }, + async setThreadGoal(request) { + return setCodexThreadGoal(request); + }, + async clearThreadGoal(request) { + return clearCodexThreadGoal(request); + }, + async rollbackThread(request) { + return rollbackCodexThread(request); + }, +}; diff --git a/src/main/lib/agent-runtime/adapters/custom-acp.ts b/src/main/lib/agent-runtime/adapters/custom-acp.ts index 0192a1df8..cac970086 100644 --- a/src/main/lib/agent-runtime/adapters/custom-acp.ts +++ b/src/main/lib/agent-runtime/adapters/custom-acp.ts @@ -1,9 +1,15 @@ import { getAgentRuntimeManifest } from "../manifests" +import { + createUnsupportedAgentRuntimeControlResult, + createUnsupportedAgentRuntimeReceipt, + streamUnsupportedAgentRuntimeRun, +} from "../runtime-run-ledger" import type { AgentRuntimeAdapter, AgentRuntimeAvailability, AgentRuntimeHealth, AgentRuntimeSessionRef, + AgentRuntimeStartRequest, } from "../types" async function inspectCustomAcpRuntime(): Promise { @@ -22,6 +28,18 @@ async function inspectCustomAcpRuntime(): Promise { } } +function unsupportedCustomAcpRun( + action: "start" | "resume", + request: AgentRuntimeStartRequest, +) { + return createUnsupportedAgentRuntimeReceipt({ + action, + request, + reason: + "Configure a Moss Custom ACP endpoint or command adapter before starting sessions.", + }) +} + export const customAcpAdapter: AgentRuntimeAdapter = { manifest: getAgentRuntimeManifest("custom-acp"), async inspect( @@ -34,4 +52,25 @@ export const customAcpAdapter: AgentRuntimeAdapter = { ): Promise { return (await inspectCustomAcpRuntime()).availability }, + async start(request) { + return unsupportedCustomAcpRun("start", request) + }, + async resume(request) { + return unsupportedCustomAcpRun("resume", request) + }, + stream(request) { + return streamUnsupportedAgentRuntimeRun(unsupportedCustomAcpRun("start", request)) + }, + async stop(request) { + return createUnsupportedAgentRuntimeControlResult( + request, + "Custom ACP stop requires a configured endpoint or command adapter.", + ) + }, + async submitToolResult(request) { + return createUnsupportedAgentRuntimeControlResult( + request, + "Custom ACP tool result submission requires a configured endpoint or command adapter.", + ) + }, } diff --git a/src/main/lib/agent-runtime/adapters/hermes.ts b/src/main/lib/agent-runtime/adapters/hermes.ts index 956c4b792..824bfc375 100644 --- a/src/main/lib/agent-runtime/adapters/hermes.ts +++ b/src/main/lib/agent-runtime/adapters/hermes.ts @@ -1,41 +1,241 @@ +import * as fs from "fs/promises" +import * as os from "os" +import * as path from "path" +import { createRequire } from "node:module" import { getAgentRuntimeManifest } from "../manifests" +import { runHermesCliResumeBridge } from "../hermes-native-session" +import { streamHermesAcpRuntimeRun } from "../hermes-acp-runtime" +import { + createAgentRuntimeProcessHandle, + registerAgentRuntimeProcessHandle, + stopAgentRuntimeProcess, + unregisterAgentRuntimeProcessHandle, +} from "../process-registry" +import { + createAgentRuntimeRunReceipt, + createUnsupportedAgentRuntimeControlResult, + createUnsupportedAgentRuntimeReceipt, + streamUnsupportedAgentRuntimeRun, +} from "../runtime-run-ledger" import type { AgentRuntimeAdapter, AgentRuntimeAvailability, AgentRuntimeHealth, AgentRuntimeSessionRef, + AgentRuntimeStartRequest, + AgentRuntimeStreamEvent, } from "../types" -import { resolveHermesRuntime } from "../../hermes/runtime" +import { + resolveHermesRuntime, + type HermesRuntimeResolution, +} from "../../hermes/runtime" -async function inspectHermesRuntime(): Promise { +const require = createRequire(import.meta.url) +const yaml = require("js-yaml") as { + load(source: string): unknown +} + +export interface HermesRuntimeModelConfig { + provider?: string + defaultModel?: string + baseUrl?: string + apiKey?: string + apiMode?: string +} + +export type HermesModelCatalogResult = + | { status: "ok"; modelIds: string[] } + | { status: "auth-error"; message: string } + | { status: "error"; message: string } + +export interface HermesRuntimeHealthDeps { + resolveRuntime?: () => HermesRuntimeResolution + readModelConfig?: () => Promise + fetchModelCatalog?: (config: HermesRuntimeModelConfig) => Promise +} + +function asRecord(value: unknown): Record | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) return undefined + return value as Record +} + +function asCleanString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined +} + +function modelHealth( + availability: AgentRuntimeAvailability, + reason?: string, +): AgentRuntimeHealth["models"] { const manifest = getAgentRuntimeManifest("hermes") - const runtime = resolveHermesRuntime() + return manifest.models?.map((model) => ({ + ...model, + availability, + ...(reason ? { reason } : {}), + })) +} + +function sanitizeBaseUrl(baseUrl: string): string { + try { + const url = new URL(baseUrl) + url.username = "" + url.password = "" + return url.toString().replace(/\/$/, "") + } catch { + return baseUrl + } +} + +function isAuthLikeMessage(message: string): boolean { + return /auth_unavailable|no auth available|not authenticated|unauthorized|forbidden|authentication/i + .test(message) +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +export async function readHermesModelConfig(): Promise { + try { + const configPath = path.join(os.homedir(), ".hermes", "config.yaml") + const raw = await fs.readFile(configPath, "utf-8") + const parsed = asRecord(yaml.load(raw)) + const model = asRecord(parsed?.model) + if (!model) return null + + return { + provider: asCleanString(model.provider), + defaultModel: asCleanString(model.default) ?? asCleanString(model.defaultModel), + baseUrl: asCleanString(model.base_url) ?? asCleanString(model.baseUrl), + apiKey: asCleanString(model.api_key) ?? asCleanString(model.apiKey), + apiMode: asCleanString(model.api_mode) ?? asCleanString(model.apiMode), + } + } catch { + return null + } +} + +export async function fetchHermesModelCatalog( + config: HermesRuntimeModelConfig, +): Promise { + if (!config.baseUrl) { + return { status: "error", message: "Hermes custom endpoint base URL is missing." } + } + + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 2000) + + try { + const response = await fetch(`${config.baseUrl.replace(/\/+$/, "")}/models`, { + method: "GET", + headers: { + ...(config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {}), + }, + signal: controller.signal, + }) + const body = await response.text() + const authLike = isAuthLikeMessage(body) + + if (!response.ok) { + const message = body.trim() + ? `HTTP ${response.status}: ${body.trim()}` + : `HTTP ${response.status}` + if (response.status === 401 || response.status === 403 || authLike) { + return { status: "auth-error", message } + } + return { status: "error", message } + } + + const parsed = asRecord(body.trim() ? JSON.parse(body) : {}) + const data = Array.isArray(parsed?.data) ? parsed.data : [] + const modelIds = data + .map((item) => asCleanString(asRecord(item)?.id)) + .filter((id): id is string => Boolean(id)) + + return { status: "ok", modelIds } + } catch (error) { + const message = errorMessage(error) + return { + status: isAuthLikeMessage(message) ? "auth-error" : "error", + message, + } + } finally { + clearTimeout(timeout) + } +} + +export async function inspectHermesRuntime( + _session?: AgentRuntimeSessionRef, + deps: HermesRuntimeHealthDeps = {}, +): Promise { + const runtime = (deps.resolveRuntime ?? resolveHermesRuntime)() if (!runtime.executable && !runtime.sourceRoot) { return { availability: "not-installed", statusReason: "Hermes CLI and source root were not found.", authMethod: "unknown", - models: manifest.models?.map((model) => ({ - ...model, - availability: "not-installed", - reason: "Hermes is not installed.", - })), + models: modelHealth("not-installed", "Hermes is not installed."), } } if ((runtime.acpExecutable || runtime.executable) && runtime.acpAdapterPath) { const launchPath = runtime.acpExecutable || `${runtime.executable} acp` - return { + const availableHealth: AgentRuntimeHealth = { availability: "available", statusReason: `Hermes ACP transport is available via ${launchPath}.`, authMethod: "shell-config", - models: manifest.models?.map((model) => ({ - ...model, - availability: "available", - reason: "Hermes uses the current ACP runtime model unless a concrete ACP model is selected.", - })), + models: modelHealth( + "available", + "Hermes uses the current ACP runtime model unless a concrete ACP model is selected.", + ), + } + + const config = await (deps.readModelConfig ?? readHermesModelConfig)() + if ( + config?.provider !== "custom" || + !config.baseUrl || + !config.defaultModel + ) { + return availableHealth + } + + const safeBaseUrl = sanitizeBaseUrl(config.baseUrl) + const catalog = await (deps.fetchModelCatalog ?? fetchHermesModelCatalog)(config) + if (catalog.status === "auth-error") { + return { + availability: "needs-auth", + statusReason: `Hermes custom endpoint auth failed: ${catalog.message}`, + authMethod: "not-authenticated", + models: modelHealth("needs-auth", catalog.message), + } + } + + if (catalog.status === "error") { + return { + availability: "error", + statusReason: `Hermes custom endpoint probe failed: ${catalog.message}`, + authMethod: "unknown", + models: modelHealth("error", catalog.message), + } + } + + if (!catalog.modelIds.includes(config.defaultModel)) { + const reason = + `Hermes custom endpoint at ${safeBaseUrl} does not expose configured model ${config.defaultModel}.` + return { + availability: "needs-auth", + statusReason: reason, + authMethod: "not-authenticated", + models: modelHealth("needs-auth", reason), + } + } + + return { + ...availableHealth, + statusReason: + `Hermes custom endpoint exposes ${config.defaultModel} via ${safeBaseUrl}.`, } } @@ -44,24 +244,151 @@ async function inspectHermesRuntime(): Promise { statusReason: `Hermes source detected at ${runtime.sourceRoot || "unknown"}, but executable or ACP adapter is missing.`, authMethod: "unknown", - models: manifest.models?.map((model) => ({ - ...model, - availability: "unsupported", - reason: "Hermes executable or ACP adapter is missing.", - })), + models: modelHealth("unsupported", "Hermes executable or ACP adapter is missing."), + } +} + +function unsupportedHermesStart(request: AgentRuntimeStartRequest) { + return createUnsupportedAgentRuntimeReceipt({ + action: "start", + request, + reason: + "Hermes start requires the ACP session registry slice before Moss can own start/stream/stop.", + }) +} + +async function resumeHermesRuntime(request: AgentRuntimeStartRequest) { + const handle = registerAgentRuntimeProcessHandle( + createAgentRuntimeProcessHandle({ + action: "resume", + session: request.session, + runId: request.runId, + }), + ) + const nativeSessionId = request.session.nativeSessionId + if (!nativeSessionId) { + unregisterAgentRuntimeProcessHandle(handle.runId) + return createAgentRuntimeRunReceipt({ + runId: handle.runId, + action: "resume", + session: request.session, + status: "error", + resultSubtype: "error", + now: handle.createdAt, + completedAt: new Date(), + error: "Hermes resume requires a native session id.", + }) + } + + try { + const [{ getMossProviderSecret }, { resolveMossProviderForEngine }] = + await Promise.all([ + import("../../moss-source/provider-secrets"), + import("../../moss-source/provider-config"), + ]) + const mossProvider = await resolveMossProviderForEngine({ + projectPath: request.session.projectPath ?? request.session.cwd, + engineId: "hermes", + requestedModelId: request.session.modelId ?? undefined, + createIfMissing: true, + secretResolver: { getSecret: getMossProviderSecret }, + }) + if (mossProvider.warnings.length > 0) { + console.warn("[hermes-runtime] Moss provider warnings:", mossProvider.warnings) + } + const runtime = resolveHermesRuntime() + const result = await runHermesCliResumeBridge({ + sessionId: nativeSessionId, + cwd: request.session.cwd, + prompt: request.prompt, + modelId: mossProvider.status === "resolved" + ? mossProvider.model ?? request.session.modelId + : request.session.modelId, + permissionMode: request.session.permissionMode, + command: runtime.executable, + env: mossProvider.status === "resolved" ? mossProvider.env : {}, + abortSignal: handle.abortController.signal, + }) + const wasCancelled = handle.abortController.signal.aborted + + return createAgentRuntimeRunReceipt({ + runId: handle.runId, + action: "resume", + session: request.session, + status: wasCancelled ? "cancelled" : result.success ? "success" : "error", + nativeSessionId, + resultSubtype: wasCancelled ? "cancelled" : result.success ? "success" : "error", + now: handle.createdAt, + completedAt: new Date(), + error: wasCancelled ? "Hermes runtime run was cancelled." : result.error, + metadata: { + bridge: result.plan.bridge, + command: result.plan.command, + args: result.plan.args, + promptSource: result.plan.promptSource, + canRunHeadless: result.plan.canRunHeadless, + lastText: result.lastText, + exitCode: result.exitCode, + stderr: result.stderr, + stopReason: handle.stopReason, + }, + }) + } catch (error) { + const wasCancelled = handle.abortController.signal.aborted + return createAgentRuntimeRunReceipt({ + runId: handle.runId, + action: "resume", + session: request.session, + status: wasCancelled ? "cancelled" : "error", + resultSubtype: wasCancelled ? "cancelled" : "error", + now: handle.createdAt, + completedAt: new Date(), + error: wasCancelled ? "Hermes runtime run was cancelled." : error instanceof Error ? error.message : String(error), + }) + } finally { + unregisterAgentRuntimeProcessHandle(handle.runId) } } +async function* streamHermesRuntime( + request: AgentRuntimeStartRequest, +): AsyncIterable { + if (!request.session.nativeSessionId || request.forceNewSession) { + yield* streamUnsupportedAgentRuntimeRun(unsupportedHermesStart(request)) + return + } + + yield* streamHermesAcpRuntimeRun(request) +} + export const hermesAdapter: AgentRuntimeAdapter = { manifest: getAgentRuntimeManifest("hermes"), async inspect( - _session: AgentRuntimeSessionRef, + session: AgentRuntimeSessionRef, ): Promise { - return inspectHermesRuntime() + return inspectHermesRuntime(session) }, async canStart( - _session: AgentRuntimeSessionRef, + session: AgentRuntimeSessionRef, ): Promise { - return (await inspectHermesRuntime()).availability + return (await inspectHermesRuntime(session)).availability + }, + async start(request) { + return unsupportedHermesStart(request) + }, + async resume(request) { + return resumeHermesRuntime(request) + }, + stream(request) { + return streamHermesRuntime(request) + }, + async stop(request) { + return stopAgentRuntimeProcess(request) + }, + async submitToolResult(request) { + return createUnsupportedAgentRuntimeControlResult( + request, + "Hermes tool result submission requires the ACP session registry slice.", + ) }, } diff --git a/src/main/lib/agent-runtime/codex-app-server-client.ts b/src/main/lib/agent-runtime/codex-app-server-client.ts new file mode 100644 index 000000000..1945c9e50 --- /dev/null +++ b/src/main/lib/agent-runtime/codex-app-server-client.ts @@ -0,0 +1,285 @@ +import type { Readable, Writable } from "node:stream" + +export type CodexAppServerJsonRpcId = string | number + +export interface CodexAppServerRpcNotification { + method: string + params?: unknown +} + +export interface CodexAppServerRpcRequest + extends CodexAppServerRpcNotification { + id: CodexAppServerJsonRpcId +} + +export interface CodexAppServerRpcErrorShape { + code: number + message: string + data?: unknown +} + +export interface CodexAppServerRpcClientOptions { + stdin: Writable + stdout: Readable + stderr?: Readable | null + signal?: AbortSignal | null + onNotification?: (notification: CodexAppServerRpcNotification) => void + onRequest?: (request: CodexAppServerRpcRequest) => void + onStderrLine?: (line: string) => void + onProtocolError?: (error: Error) => void +} + +interface PendingRpcRequest { + method: string + resolve(value: unknown): void + reject(error: Error): void +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function isJsonRpcId(value: unknown): value is CodexAppServerJsonRpcId { + return typeof value === "string" || typeof value === "number" +} + +function toError(error: unknown): Error { + return error instanceof Error ? error : new Error(String(error)) +} + +function errorFromRpcResponse( + method: string, + requestId: string, + error: unknown, +): Error { + if (isRecord(error)) { + const message = + typeof error.message === "string" + ? error.message + : `Codex app-server request ${method} failed.` + const code = typeof error.code === "number" ? error.code : undefined + const details = code === undefined ? message : `${message} (${code})` + return new Error(`Codex app-server request ${method} failed for id ${requestId}: ${details}`) + } + + return new Error(`Codex app-server request ${method} failed for id ${requestId}.`) +} + +export function codexAppServerMethodNotFoundError( + method: string, +): CodexAppServerRpcErrorShape { + return { + code: -32601, + message: `Method not found: ${method}`, + } +} + +export function codexAppServerInvalidParamsError( + message: string, + data?: unknown, +): CodexAppServerRpcErrorShape { + return { + code: -32602, + message, + ...(data !== undefined ? { data } : {}), + } +} + +export class CodexAppServerRpcClient { + private nextRequestId = 1 + private stdoutRemainder = "" + private stderrRemainder = "" + private closed = false + private readonly pending = new Map() + private readonly abortListener: (() => void) | undefined + + constructor(private readonly options: CodexAppServerRpcClientOptions) { + this.options.stdout.on("data", this.handleStdoutData) + this.options.stdout.on("error", this.handleStreamError) + this.options.stdout.on("end", this.handleStreamEnd) + this.options.stdout.on("close", this.handleStreamEnd) + + this.options.stderr?.on("data", this.handleStderrData) + this.options.stderr?.on("error", this.handleProtocolError) + + if (this.options.signal) { + this.abortListener = () => { + this.close(new Error("Codex app-server request was aborted.")) + } + if (this.options.signal.aborted) { + this.abortListener() + } else { + this.options.signal.addEventListener("abort", this.abortListener, { once: true }) + } + } + } + + request(method: string, params?: unknown): Promise { + if (this.closed) { + return Promise.reject(new Error("Codex app-server RPC client is closed.")) + } + + const id = this.nextRequestId++ + const requestId = String(id) + const message: Record = { id, method } + if (params !== undefined) message.params = params + + return new Promise((resolve, reject) => { + this.pending.set(requestId, { method, resolve, reject }) + try { + this.writeMessage(message) + } catch (error) { + this.pending.delete(requestId) + reject(toError(error)) + } + }) + } + + notify(method: string, params?: unknown): void { + const message: Record = { method } + if (params !== undefined) message.params = params + this.writeMessage(message) + } + + respond(id: CodexAppServerJsonRpcId, result: unknown): void { + this.writeMessage({ + id, + ...(result !== undefined ? { result } : {}), + }) + } + + respondError( + id: CodexAppServerJsonRpcId, + error: CodexAppServerRpcErrorShape, + ): void { + this.writeMessage({ id, error }) + } + + close(error?: Error): void { + if (this.closed) return + this.closed = true + this.options.stdout.off("data", this.handleStdoutData) + this.options.stdout.off("error", this.handleStreamError) + this.options.stdout.off("end", this.handleStreamEnd) + this.options.stdout.off("close", this.handleStreamEnd) + this.options.stderr?.off("data", this.handleStderrData) + this.options.stderr?.off("error", this.handleProtocolError) + if (this.options.signal && this.abortListener) { + this.options.signal.removeEventListener("abort", this.abortListener) + } + + const closeError = error ?? new Error("Codex app-server RPC client closed.") + for (const pending of this.pending.values()) { + pending.reject(closeError) + } + this.pending.clear() + } + + private writeMessage(message: Record): void { + if (this.closed) { + throw new Error("Codex app-server RPC client is closed.") + } + + this.options.stdin.write(`${JSON.stringify(message)}\n`) + } + + private readonly handleStdoutData = (chunk: Buffer | string): void => { + this.stdoutRemainder = this.consumeJsonLines( + this.stdoutRemainder + chunk.toString(), + this.handleWireLine, + ) + } + + private readonly handleStderrData = (chunk: Buffer | string): void => { + this.stderrRemainder = this.consumeJsonLines( + this.stderrRemainder + chunk.toString(), + (line) => this.options.onStderrLine?.(line), + ) + } + + private consumeJsonLines( + value: string, + onLine: (line: string) => void, + ): string { + const lines = value.split("\n") + const remainder = lines.pop() ?? "" + for (const rawLine of lines) { + const line = rawLine.replace(/\r$/, "") + if (line.trim()) onLine(line) + } + return remainder + } + + private readonly handleWireLine = (line: string): void => { + let decoded: unknown + try { + decoded = JSON.parse(line) + } catch (error) { + this.handleProtocolError( + new Error(`Failed to parse Codex app-server JSON line: ${toError(error).message}`), + ) + return + } + + if (!isRecord(decoded)) { + this.handleProtocolError(new Error("Codex app-server message must be an object.")) + return + } + + if (isJsonRpcId(decoded.id) && ("result" in decoded || "error" in decoded)) { + this.handleResponse(decoded) + return + } + + if (typeof decoded.method === "string" && isJsonRpcId(decoded.id)) { + this.options.onRequest?.({ + id: decoded.id, + method: decoded.method, + ...(decoded.params !== undefined ? { params: decoded.params } : {}), + }) + return + } + + if (typeof decoded.method === "string" && !("id" in decoded)) { + this.options.onNotification?.({ + method: decoded.method, + ...(decoded.params !== undefined ? { params: decoded.params } : {}), + }) + return + } + + this.handleProtocolError( + new Error("Codex app-server message was not a response, request, or notification."), + ) + } + + private handleResponse(message: Record): void { + const id = String(message.id) + const pending = this.pending.get(id) + if (!pending) { + this.handleProtocolError( + new Error(`Codex app-server response had no pending request for id ${id}.`), + ) + return + } + + this.pending.delete(id) + if ("error" in message && message.error !== undefined) { + pending.reject(errorFromRpcResponse(pending.method, id, message.error)) + return + } + pending.resolve(message.result) + } + + private readonly handleStreamError = (error: Error): void => { + this.close(error) + } + + private readonly handleStreamEnd = (): void => { + this.close(new Error("Codex app-server stdout closed.")) + } + + private readonly handleProtocolError = (error: Error): void => { + this.options.onProtocolError?.(error) + } +} diff --git a/src/main/lib/agent-runtime/codex-app-server-events.ts b/src/main/lib/agent-runtime/codex-app-server-events.ts new file mode 100644 index 000000000..d9dc49995 --- /dev/null +++ b/src/main/lib/agent-runtime/codex-app-server-events.ts @@ -0,0 +1,788 @@ +import type { + AgentRuntimeConversationBlock, + AgentRuntimeStreamEvent, +} from "./types" +import { + providerRuntimeEventToStreamEvent, + type CanonicalRuntimeItemType, + type CanonicalRuntimeRequestType, + type ProviderRuntimeEvent, + type ProviderRuntimeEventType, +} from "./provider-runtime-contract" + +export interface CodexAppServerNotification { + method: string + params?: unknown +} + +export interface CodexAppServerServerRequest { + id: string | number + method: string + params?: unknown +} + +export interface CodexAppServerNotificationContext { + providerInstanceId?: string | null + nativeSessionId?: string | null + eventId?: string | null + createdAt?: string | null +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function readRecord(value: unknown): Record | null { + return isRecord(value) ? value : null +} + +function readString(value: unknown): string | undefined { + if (typeof value !== "string") return undefined + const trimmed = value.trim() + return trimmed ? trimmed : undefined +} + +function readNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined +} + +function readBoolean(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined +} + +function readArray(value: unknown): unknown[] { + return Array.isArray(value) ? value : [] +} + +export function codexAppServerServerRequestId( + value: CodexAppServerServerRequest, +): string { + const params = readRecord(value.params) ?? {} + return ( + readString(params.approvalId) ?? + readString(params.requestId) ?? + readString(params.itemId) ?? + readString(params.callId) ?? + String(value.id) + ) +} + +function readThreadId(method: string, params: Record): string | undefined { + if (method === "thread/started") { + return readString(readRecord(params.thread)?.id) + } + return readString(params.threadId) +} + +function readTurnRecord(params: Record): Record | null { + return readRecord(params.turn) +} + +function readTurnId(params: Record): string | undefined { + return readString(params.turnId) ?? readString(readTurnRecord(params)?.id) +} + +function readItemRecord(params: Record): Record | null { + return readRecord(params.item) +} + +function readItemId(params: Record): string | undefined { + return readString(params.itemId) ?? readString(readItemRecord(params)?.id) +} + +function mapCodexAppServerItemType(value: unknown): CanonicalRuntimeItemType { + switch (value) { + case "userMessage": + return "user_message" + case "agentMessage": + return "assistant_message" + case "reasoning": + return "reasoning" + case "plan": + return "plan" + case "commandExecution": + return "command_execution" + case "fileChange": + return "file_change" + case "mcpToolCall": + return "mcp_tool_call" + case "dynamicToolCall": + return "dynamic_tool_call" + case "collabAgentToolCall": + return "collab_agent_tool_call" + case "webSearch": + return "web_search" + case "imageView": + return "image_view" + case "enteredReviewMode": + return "review_entered" + case "exitedReviewMode": + return "review_exited" + case "contextCompaction": + return "context_compaction" + default: + return "unknown" + } +} + +export function codexAppServerRequestType( + method: string, +): CanonicalRuntimeRequestType { + switch (method) { + case "item/commandExecution/requestApproval": + return "command_execution_approval" + case "item/fileRead/requestApproval": + return "file_read_approval" + case "item/fileChange/requestApproval": + return "file_change_approval" + case "applyPatchApproval": + return "apply_patch_approval" + case "execCommandApproval": + return "exec_command_approval" + case "item/tool/requestUserInput": + return "tool_user_input" + case "item/tool/call": + return "dynamic_tool_call" + case "account/chatgptAuthTokens/refresh": + return "auth_tokens_refresh" + default: + return "unknown" + } +} + +function titleForItem(item: Record): string | undefined { + switch (item.type) { + case "commandExecution": + return readString(item.command) + case "mcpToolCall": { + const server = readString(item.server) + const tool = readString(item.tool) + return server && tool ? `${server}.${tool}` : tool ?? server + } + case "dynamicToolCall": + return readString(item.tool) + case "webSearch": + return readString(item.query) + case "imageView": + return readString(item.path) + case "imageGeneration": + return readString(item.revisedPrompt) ?? readString(item.result) + case "enteredReviewMode": + case "exitedReviewMode": + return readString(item.review) + default: + return undefined + } +} + +function detailForItem(item: Record): string | undefined { + return ( + readString(item.command) ?? + readString(item.title) ?? + readString(item.summary) ?? + readString(item.text) ?? + readString(item.aggregatedOutput) ?? + readString(item.path) ?? + readString(item.prompt) + ) +} + +function detailForRequest( + method: string, + params: Record, +): string | undefined { + switch (method) { + case "item/commandExecution/requestApproval": + return readString(params.command) ?? readString(params.reason) + case "item/fileChange/requestApproval": + case "applyPatchApproval": + return readString(params.reason) + case "execCommandApproval": { + const command = readArray(params.command) + .filter((entry): entry is string => typeof entry === "string") + .join(" ") + return readString(params.reason) ?? readString(command) + } + case "item/tool/call": + return readString(params.tool) + default: + return undefined + } +} + +function tokenUsagePayload(params: Record): Record { + const tokenUsage = readRecord(params.tokenUsage) + const total = readRecord(tokenUsage?.total) ?? tokenUsage ?? {} + const last = readRecord(tokenUsage?.last) + const totalTokens = readNumber(total.totalTokens) + const usedTokens = readNumber(total.usedTokens) ?? totalTokens + const maxTokens = + readNumber(tokenUsage?.modelContextWindow) ?? + readNumber(tokenUsage?.maxTokens) ?? + readNumber(total.modelContextWindow) ?? + readNumber(total.maxTokens) + return { + usage: { + inputTokens: readNumber(total.inputTokens), + outputTokens: readNumber(total.outputTokens), + totalTokens, + usedTokens, + totalProcessedTokens: + readNumber(tokenUsage?.totalProcessedTokens) ?? + readNumber(total.totalProcessedTokens), + reasoningOutputTokens: readNumber(total.reasoningOutputTokens), + cachedInputTokens: readNumber(total.cachedInputTokens), + maxTokens, + modelContextWindow: maxTokens, + lastUsedTokens: readNumber(last?.usedTokens) ?? readNumber(last?.totalTokens), + lastInputTokens: readNumber(last?.inputTokens), + lastCachedInputTokens: readNumber(last?.cachedInputTokens), + lastOutputTokens: readNumber(last?.outputTokens), + lastReasoningOutputTokens: readNumber(last?.reasoningOutputTokens), + toolUses: readNumber(tokenUsage?.toolUses) ?? readNumber(total.toolUses), + durationMs: readNumber(tokenUsage?.durationMs) ?? readNumber(total.durationMs), + compactsAutomatically: + readBoolean(tokenUsage?.compactsAutomatically) ?? + readBoolean(total.compactsAutomatically), + last, + }, + } +} + +function compactObject(value: Record): Record { + return Object.fromEntries( + Object.entries(value).filter(([, entry]) => entry !== undefined), + ) +} + +function makeProviderRuntimeEvent( + type: ProviderRuntimeEventType, + notification: CodexAppServerNotification, + context: CodexAppServerNotificationContext, + payload: Record, + fallbackEventId?: string, +): ProviderRuntimeEvent | null { + const params = readRecord(notification.params) ?? {} + const threadId = readThreadId(notification.method, params) + const turnId = readTurnId(params) + const itemId = readItemId(params) + const requestId = readString(params.requestId) + + return compactObject({ + type, + engineId: "codex", + providerInstanceId: readString(context.providerInstanceId), + nativeSessionId: readString(context.nativeSessionId) ?? null, + eventId: readString(context.eventId) ?? readString(fallbackEventId), + createdAt: readString(context.createdAt), + threadId, + turnId, + itemId, + requestId, + payload, + raw: notification, + }) as unknown as ProviderRuntimeEvent +} + +function itemPayload(params: Record): Record { + const item = readItemRecord(params) ?? {} + return { + itemType: mapCodexAppServerItemType(item.type), + title: titleForItem(item), + status: item.status, + detail: detailForItem(item), + data: item, + } +} + +function warningPayload(params: Record): Record { + return { + summary: + readString(params.summary) ?? + readString(params.title) ?? + readString(params.message) ?? + "Codex app-server warning", + message: readString(params.message) ?? readString(params.details), + details: readString(params.details), + data: params, + } +} + +function hookRunOutput(run: Record): string | undefined { + const entries = readArray(run.entries) + .map((entry) => readString(readRecord(entry)?.text)) + .filter((entry): entry is string => Boolean(entry)) + return readString(entries.join("\n")) ?? readString(run.statusMessage) +} + +function hookRunOutcome(run: Record): "success" | "error" | "cancelled" { + const status = readString(run.status) + if (status === "completed") return "success" + if (status === "stopped") return "cancelled" + return "error" +} + +function hookRunPayload( + params: Record, + phase: "started" | "completed", +): Record { + const run = readRecord(params.run) ?? {} + const hookId = readString(run.id) + const hookName = + readString(run.sourcePath) ?? + readString(run.handlerType) ?? + "hook" + const output = hookRunOutput(run) + + return compactObject({ + hookId, + hookName, + hookEvent: readString(run.eventName), + status: readString(run.status), + ...(phase === "completed" ? { outcome: hookRunOutcome(run) } : {}), + output, + durationMs: readNumber(run.durationMs), + run, + }) +} + +function directBlockPatch( + notification: CodexAppServerNotification, + context: CodexAppServerNotificationContext, + patch: Partial, +): AgentRuntimeStreamEvent | null { + const params = readRecord(notification.params) ?? {} + const id = readItemId(params) ?? readString(params.requestId) + if (!id) return null + const patchRecord = patch as Record + const metadata = isRecord(patchRecord.metadata) ? patchRecord.metadata : {} + + return { + type: "conversation-block-update", + id, + patch: { + ...patch, + metadata: { + ...metadata, + codexAppServerMethod: notification.method, + ...(readString(context.providerInstanceId) + ? { providerInstanceId: readString(context.providerInstanceId) } + : {}), + }, + } as Partial, + } +} + +export function codexAppServerServerRequestToProviderRuntimeEvent( + request: CodexAppServerServerRequest, + context: CodexAppServerNotificationContext = {}, +): ProviderRuntimeEvent | null { + const params = readRecord(request.params) ?? {} + const requestId = codexAppServerServerRequestId(request) + const base = compactObject({ + engineId: "codex", + providerInstanceId: readString(context.providerInstanceId), + nativeSessionId: readString(context.nativeSessionId) ?? null, + eventId: readString(context.eventId), + createdAt: readString(context.createdAt), + threadId: readString(params.threadId) ?? readString(context.nativeSessionId), + turnId: readString(params.turnId), + requestId, + raw: request, + }) + + if (request.method === "item/tool/requestUserInput") { + return { + ...base, + type: "user-input.requested", + payload: { + requestType: codexAppServerRequestType(request.method), + questions: params.questions, + args: params, + }, + } as ProviderRuntimeEvent + } + + return { + ...base, + type: "request.opened", + payload: compactObject({ + requestType: codexAppServerRequestType(request.method), + detail: detailForRequest(request.method, params), + args: params, + }), + } as ProviderRuntimeEvent +} + +export function codexAppServerServerRequestToStreamEvent( + request: CodexAppServerServerRequest, + context: CodexAppServerNotificationContext = {}, +): AgentRuntimeStreamEvent | null { + const event = codexAppServerServerRequestToProviderRuntimeEvent(request, context) + return event ? providerRuntimeEventToStreamEvent(event) : null +} + +export function codexAppServerNotificationToProviderRuntimeEvent( + notification: CodexAppServerNotification, + context: CodexAppServerNotificationContext = {}, +): ProviderRuntimeEvent | null { + const params = readRecord(notification.params) ?? {} + + switch (notification.method) { + case "session/connecting": + return makeProviderRuntimeEvent("session.state.changed", notification, context, { + state: "starting", + reason: + readString(params.message) ?? + "Starting Codex app-server session.", + }) + case "session/ready": + return makeProviderRuntimeEvent("session.state.changed", notification, context, { + state: "ready", + reason: + readString(params.message) ?? + "Codex app-server session ready.", + }) + case "session/error": + return makeProviderRuntimeEvent("session.state.changed", notification, context, { + state: "error", + reason: + readString(params.message) ?? + readString(readRecord(params.error)?.message) ?? + "Codex app-server session failed.", + error: params.error, + }) + case "thread/started": + return makeProviderRuntimeEvent("thread.started", notification, context, { + thread: params.thread, + }) + case "thread/status/changed": + return makeProviderRuntimeEvent("thread.state.changed", notification, context, { + status: params.status, + }) + case "thread/tokenUsage/updated": + return makeProviderRuntimeEvent( + "thread.token-usage.updated", + notification, + context, + tokenUsagePayload(params), + ) + case "turn/started": + return makeProviderRuntimeEvent("turn.started", notification, context, { + state: readString(readTurnRecord(params)?.status) ?? "inProgress", + turn: params.turn, + }) + case "turn/completed": { + const turn = readTurnRecord(params) + return makeProviderRuntimeEvent("turn.completed", notification, context, { + state: readString(turn?.status) ?? "failed", + error: turn?.error, + turn, + }) + } + case "turn/plan/updated": + return makeProviderRuntimeEvent("turn.plan.updated", notification, context, { + plan: params.plan, + explanation: params.explanation, + }) + case "item/plan/delta": + return makeProviderRuntimeEvent("turn.proposed.delta", notification, context, { + delta: readString(params.delta) ?? "", + }) + case "hook/started": { + const run = readRecord(params.run) ?? {} + return makeProviderRuntimeEvent( + "hook.started", + notification, + context, + hookRunPayload(params, "started"), + `codex-app-server:hook:${readString(run.id) ?? "unknown"}`, + ) + } + case "hook/completed": { + const run = readRecord(params.run) ?? {} + return makeProviderRuntimeEvent( + "hook.completed", + notification, + context, + hookRunPayload(params, "completed"), + `codex-app-server:hook:${readString(run.id) ?? "unknown"}`, + ) + } + case "item/started": + return makeProviderRuntimeEvent( + "item.started", + notification, + context, + itemPayload(params), + ) + case "item/completed": { + const item = readItemRecord(params) ?? {} + const planMarkdown = item.type === "plan" ? readString(item.text) : undefined + if (planMarkdown) { + return makeProviderRuntimeEvent( + "turn.proposed.completed", + notification, + context, + { + planMarkdown, + data: item, + }, + ) + } + return makeProviderRuntimeEvent( + "item.completed", + notification, + context, + itemPayload(params), + ) + } + case "serverRequest/resolved": + return makeProviderRuntimeEvent("request.resolved", notification, context, { + requestId: params.requestId, + requestType: readString(params.requestType), + rawRequestId: params.rawRequestId, + state: "resolved", + }) + case "item/mcpToolCall/progress": + return makeProviderRuntimeEvent("tool.progress", notification, context, { + toolUseId: readString(params.itemId), + summary: readString(params.message), + toolName: readString(params.toolName), + data: params, + }) + case "model/rerouted": + return makeProviderRuntimeEvent("model.rerouted", notification, context, { + fromModel: params.fromModel, + toModel: params.toModel, + reason: params.reason, + }) + case "thread/realtime/started": + return makeProviderRuntimeEvent( + "thread.realtime.started", + notification, + context, + { + threadId: readString(params.threadId), + realtimeSessionId: readString(params.realtimeSessionId), + version: readString(params.version), + }, + `codex-app-server:realtime:${readString(params.threadId) ?? "unknown"}`, + ) + case "thread/realtime/itemAdded": + return makeProviderRuntimeEvent( + "thread.realtime.item-added", + notification, + context, + { + threadId: readString(params.threadId), + item: params.item, + }, + `codex-app-server:realtime:${readString(params.threadId) ?? "unknown"}`, + ) + case "thread/realtime/outputAudio/delta": + return makeProviderRuntimeEvent( + "thread.realtime.audio.delta", + notification, + context, + { + threadId: readString(params.threadId), + audio: params.audio, + }, + `codex-app-server:realtime:${readString(params.threadId) ?? "unknown"}`, + ) + case "thread/realtime/error": + return makeProviderRuntimeEvent( + "thread.realtime.error", + notification, + context, + { + threadId: readString(params.threadId), + message: readString(params.message) ?? "Realtime error", + }, + `codex-app-server:realtime:${readString(params.threadId) ?? "unknown"}`, + ) + case "thread/realtime/closed": + return makeProviderRuntimeEvent( + "thread.realtime.closed", + notification, + context, + { + threadId: readString(params.threadId), + reason: readString(params.reason), + }, + `codex-app-server:realtime:${readString(params.threadId) ?? "unknown"}`, + ) + case "configWarning": + return makeProviderRuntimeEvent( + "config.warning", + notification, + context, + warningPayload(params), + ) + case "account/updated": + return makeProviderRuntimeEvent( + "account.updated", + notification, + context, + { + account: params, + }, + "codex-app-server:account/updated", + ) + case "account/rateLimits/updated": + return makeProviderRuntimeEvent( + "account.rate-limits.updated", + notification, + context, + { + rateLimits: readRecord(params.rateLimits) ?? params.rateLimits ?? params, + }, + "codex-app-server:account/rateLimits/updated", + ) + case "mcpServer/startupStatus/updated": + return makeProviderRuntimeEvent( + "mcp.status.updated", + notification, + context, + { + name: readString(params.name), + status: readString(params.status), + error: readString(params.error), + data: params, + }, + `codex-app-server:mcp:${readString(params.name) ?? "unknown"}`, + ) + case "mcpServer/oauthLogin/completed": + return makeProviderRuntimeEvent( + "mcp.oauth.completed", + notification, + context, + { + name: readString(params.name), + success: readBoolean(params.success) ?? false, + error: readString(params.error), + }, + `codex-app-server:mcp-oauth:${readString(params.name) ?? "unknown"}`, + ) + case "app/list/updated": + return makeProviderRuntimeEvent( + "app.list.updated", + notification, + context, + { + apps: readArray(params.data), + }, + "codex-app-server:app/list/updated", + ) + case "externalAgentConfig/import/completed": + return makeProviderRuntimeEvent( + "config.updated", + notification, + context, + { + source: "external-agent-config", + title: "External config imported", + message: "External agent configuration import completed.", + }, + "codex-app-server:externalAgentConfig/import/completed", + ) + case "deprecationNotice": + return makeProviderRuntimeEvent( + "deprecation.notice", + notification, + context, + { + summary: + readString(params.summary) ?? + "Codex app-server deprecation notice", + details: readString(params.details), + }, + "codex-app-server:deprecationNotice", + ) + case "warning": + case "guardianWarning": + return makeProviderRuntimeEvent( + "runtime.warning", + notification, + context, + warningPayload(params), + ) + case "error": { + const error = readRecord(params.error) + return makeProviderRuntimeEvent("runtime.error", notification, context, { + message: + readString(error?.message) ?? + readString(params.message) ?? + "Codex app-server error.", + error: params.error, + willRetry: params.willRetry, + }) + } + default: + return null + } +} + +export function codexAppServerNotificationToStreamEvent( + notification: CodexAppServerNotification, + context: CodexAppServerNotificationContext = {}, +): AgentRuntimeStreamEvent | null { + const params = readRecord(notification.params) ?? {} + + switch (notification.method) { + case "item/agentMessage/delta": { + const delta = readString(params.delta) + return delta ? { type: "text", text: delta } : null + } + case "item/plan/delta": { + const event = codexAppServerNotificationToProviderRuntimeEvent( + notification, + context, + ) + return event ? providerRuntimeEventToStreamEvent(event) : null + } + case "item/commandExecution/outputDelta": + case "item/fileChange/outputDelta": { + const delta = readString(params.delta) + return delta + ? directBlockPatch(notification, context, { + output: { delta }, + }) + : null + } + case "item/reasoning/summaryTextDelta": + case "item/reasoning/textDelta": { + const delta = readString(params.delta) + return delta + ? directBlockPatch(notification, context, { + summary: delta, + }) + : null + } + case "turn/diff/updated": + return directBlockPatch(notification, context, { + input: { + diff: params.diff, + }, + }) + default: { + const event = codexAppServerNotificationToProviderRuntimeEvent( + notification, + context, + ) + return event ? providerRuntimeEventToStreamEvent(event) : null + } + } +} + +export function codexAppServerNotificationsToStreamEvents( + notifications: readonly CodexAppServerNotification[], + context: CodexAppServerNotificationContext = {}, +): AgentRuntimeStreamEvent[] { + return notifications + .map((notification) => + codexAppServerNotificationToStreamEvent(notification, context), + ) + .filter((event): event is AgentRuntimeStreamEvent => Boolean(event)) +} diff --git a/src/main/lib/agent-runtime/codex-app-server-plan.ts b/src/main/lib/agent-runtime/codex-app-server-plan.ts new file mode 100644 index 000000000..f42175dae --- /dev/null +++ b/src/main/lib/agent-runtime/codex-app-server-plan.ts @@ -0,0 +1,339 @@ +import { homedir } from "node:os" +import path from "node:path" +import { resolveCodexNativeCommand } from "./codex-native-session" +import { + resolveCodexAppServerPolicies, + type CodexAppServerApprovalPolicy, + type CodexAppServerRuntimeMode, + type CodexAppServerSandboxMode, +} from "./codex-app-server-policy" +import { + resolveAgentRuntimeProviderInstanceId, + type AgentRuntimeProviderInstanceId, +} from "./provider-instances" +import type { AgentPermissionMode, AgentRuntimeSessionRef } from "./types" + +export type CodexAppServerSandboxPolicy = + | { type: "readOnly" } + | { type: "workspaceWrite"; writableRoots?: string[] } + | { type: "dangerFullAccess" } + +export type CodexAppServerInteractionMode = "default" | "plan" + +export interface CodexAppServerImageInput { + type: "image" + url: string +} + +export type CodexAppServerUserInput = + | { + type: "text" + text: string + } + | CodexAppServerImageInput + +export interface CodexAppServerCollaborationMode { + mode: CodexAppServerInteractionMode + settings: { + model: string + reasoning_effort: string + developer_instructions?: string + } +} + +export interface CodexAppServerThreadStartParams { + cwd: string + approvalPolicy?: CodexAppServerApprovalPolicy + sandbox?: CodexAppServerSandboxMode + model?: string + serviceTier?: string +} + +export interface CodexAppServerThreadResumeParams + extends CodexAppServerThreadStartParams { + threadId: string +} + +export interface CodexAppServerThreadForkParams + extends CodexAppServerThreadStartParams { + threadId: string +} + +export interface CodexAppServerTurnStartParams { + threadId: string + input: CodexAppServerUserInput[] + approvalPolicy?: CodexAppServerApprovalPolicy + sandboxPolicy?: CodexAppServerSandboxPolicy + model?: string + serviceTier?: string + effort?: string + collaborationMode?: CodexAppServerCollaborationMode +} + +export interface CodexAppServerLaunchPlan { + engine: "codex" + bridge: "codex-app-server" + command: string + args: string[] + cwd: string + env: Record + extendEnv: boolean + providerInstanceId: AgentRuntimeProviderInstanceId + modelId?: string + permissionMode: AgentPermissionMode + runtimeMode: CodexAppServerRuntimeMode + approvalPolicy?: CodexAppServerApprovalPolicy + sandboxMode?: CodexAppServerSandboxMode + usesCodexConfigDefaults: boolean + bypassApprovalsAndSandbox: boolean + notes: string[] +} + +export interface BuildCodexAppServerLaunchPlanInput { + session: AgentRuntimeSessionRef + command?: string | null + appServerArgs?: readonly string[] | null + env?: NodeJS.ProcessEnv | Record | null + codeHome?: string | null + homeDir?: string | null + providerInstanceId?: string | null +} + +export interface BuildCodexAppServerThreadParamsInput { + plan: Pick< + CodexAppServerLaunchPlan, + "cwd" | "modelId" | "approvalPolicy" | "sandboxMode" | "usesCodexConfigDefaults" + > + serviceTier?: string | null +} + +export interface BuildCodexAppServerTurnParamsInput { + plan: Pick< + CodexAppServerLaunchPlan, + "modelId" | "approvalPolicy" | "runtimeMode" | "usesCodexConfigDefaults" + > + threadId: string + prompt?: string | null + images?: readonly CodexAppServerImageInput[] | null + serviceTier?: string | null + effort?: string | null + interactionMode?: CodexAppServerInteractionMode | null + developerInstructions?: string | null +} + +function cleanString(value: string | null | undefined): string | undefined { + const trimmed = value?.trim() + return trimmed ? trimmed : undefined +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function readMetadataModelSelection( + metadata: Record | undefined, +): Record | null { + const modelSelection = metadata?.modelSelection + return isRecord(modelSelection) ? modelSelection : null +} + +function requireCleanString( + value: string | null | undefined, + label: string, +): string { + const cleaned = cleanString(value) + if (!cleaned) { + throw new Error(`Codex app-server ${label} is required.`) + } + return cleaned +} + +function cleanAppServerArgs(args: readonly string[] | null | undefined): string[] { + return (args ?? []) + .map((argument) => cleanString(argument)) + .filter((argument): argument is string => Boolean(argument)) +} + +function cleanEnv( + env: NodeJS.ProcessEnv | Record | null | undefined, +): Record { + const cleaned: Record = {} + for (const [key, value] of Object.entries(env ?? {})) { + if (value !== undefined) cleaned[key] = value + } + return cleaned +} + +function expandHomePath(value: string, homeDir: string): string { + if (value === "~") return homeDir + if (value.startsWith("~/")) return path.join(homeDir, value.slice(2)) + return value +} + +function resolveCodeHome(input: BuildCodexAppServerLaunchPlanInput): string | undefined { + const configured = cleanString(input.codeHome) ?? cleanString(input.env?.CODEX_HOME) + if (!configured) return undefined + return expandHomePath(configured, cleanString(input.homeDir) ?? homedir()) +} + +export function codexAppServerRuntimeModeToSandboxPolicy( + runtimeMode: CodexAppServerRuntimeMode, +): CodexAppServerSandboxPolicy { + if (runtimeMode === "approval-required") return { type: "readOnly" } + if (runtimeMode === "auto-accept-edits") return { type: "workspaceWrite" } + return { type: "dangerFullAccess" } +} + +export function hasConfiguredCodexAppServerMcpServer( + appServerArgs: readonly string[] | null | undefined, +): boolean { + return appServerArgs?.some((argument) => argument.includes("mcp_servers.")) === true +} + +export function isRecoverableCodexAppServerThreadResumeError( + error: unknown, +): boolean { + const message = (error instanceof Error ? error.message : String(error)).toLowerCase() + if (!message.includes("thread")) return false + return [ + "not found", + "missing thread", + "no such thread", + "unknown thread", + "does not exist", + ].some((snippet) => message.includes(snippet)) +} + +export function buildCodexAppServerLaunchPlan( + input: BuildCodexAppServerLaunchPlanInput, +): CodexAppServerLaunchPlan { + if (input.session.engineId !== "codex") { + throw new Error("Codex app-server launch plans require a codex session.") + } + + const cwd = requireCleanString(input.session.cwd, "working directory") + const permissionMode = input.session.permissionMode ?? "agent" + const policies = resolveCodexAppServerPolicies(permissionMode) + const env = cleanEnv(input.env) + const codeHome = resolveCodeHome(input) + if (codeHome) env.CODEX_HOME = codeHome + + const explicitProviderInstanceId = cleanString(input.providerInstanceId) + const providerInstanceId = explicitProviderInstanceId + ? (explicitProviderInstanceId as AgentRuntimeProviderInstanceId) + : resolveAgentRuntimeProviderInstanceId(input.session) + const metadataModelSelection = readMetadataModelSelection(input.session.metadata) + const modelId = + cleanString(input.session.modelSelection?.modelId) ?? + cleanString(metadataModelSelection?.modelId as string | undefined) ?? + cleanString(metadataModelSelection?.model as string | undefined) ?? + cleanString(input.session.modelId) + + return { + engine: "codex", + bridge: "codex-app-server", + command: resolveCodexNativeCommand({ + command: input.command, + env: input.env, + homeDir: input.homeDir, + }), + args: ["app-server", ...cleanAppServerArgs(input.appServerArgs)], + cwd, + env, + extendEnv: input.env === undefined || input.env === null, + providerInstanceId, + ...(modelId ? { modelId } : {}), + permissionMode, + runtimeMode: policies.runtimeMode, + ...(policies.approvalPolicy ? { approvalPolicy: policies.approvalPolicy } : {}), + ...(policies.sandboxMode ? { sandboxMode: policies.sandboxMode } : {}), + usesCodexConfigDefaults: policies.usesCodexConfigDefaults, + bypassApprovalsAndSandbox: policies.bypassApprovalsAndSandbox, + notes: [ + "Codex app-server is launched as a persistent stdio RPC process.", + ...policies.notes, + ...(hasConfiguredCodexAppServerMcpServer(input.appServerArgs) + ? ["Codex app-server args include inline MCP configuration."] + : []), + ], + } +} + +export function buildCodexAppServerThreadStartParams( + input: BuildCodexAppServerThreadParamsInput, +): CodexAppServerThreadStartParams { + return { + cwd: input.plan.cwd, + ...(!input.plan.usesCodexConfigDefaults && input.plan.approvalPolicy + ? { approvalPolicy: input.plan.approvalPolicy } + : {}), + ...(!input.plan.usesCodexConfigDefaults && input.plan.sandboxMode + ? { sandbox: input.plan.sandboxMode } + : {}), + ...(input.plan.modelId ? { model: input.plan.modelId } : {}), + ...(cleanString(input.serviceTier) ? { serviceTier: cleanString(input.serviceTier) } : {}), + } +} + +export function buildCodexAppServerThreadResumeParams( + input: BuildCodexAppServerThreadParamsInput & { threadId: string }, +): CodexAppServerThreadResumeParams { + return { + threadId: requireCleanString(input.threadId, "thread id"), + ...buildCodexAppServerThreadStartParams(input), + } +} + +export function buildCodexAppServerThreadForkParams( + input: BuildCodexAppServerThreadParamsInput & { threadId: string }, +): CodexAppServerThreadForkParams { + return { + threadId: requireCleanString(input.threadId, "thread id"), + ...buildCodexAppServerThreadStartParams(input), + } +} + +export function buildCodexAppServerTurnStartParams( + input: BuildCodexAppServerTurnParamsInput, +): CodexAppServerTurnStartParams { + const prompt = cleanString(input.prompt) + const serviceTier = cleanString(input.serviceTier) + const effort = cleanString(input.effort) + const developerInstructions = cleanString(input.developerInstructions) + const inputItems: CodexAppServerUserInput[] = [] + if (prompt) inputItems.push({ type: "text", text: prompt }) + for (const image of input.images ?? []) { + if (image.type === "image" && cleanString(image.url)) { + inputItems.push({ type: "image", url: image.url }) + } + } + + const collaborationMode = + input.interactionMode && input.plan.modelId + ? { + mode: input.interactionMode, + settings: { + model: input.plan.modelId, + reasoning_effort: effort ?? "medium", + ...(developerInstructions + ? { developer_instructions: developerInstructions } + : {}), + }, + } + : undefined + + return { + threadId: requireCleanString(input.threadId, "thread id"), + input: inputItems, + ...(!input.plan.usesCodexConfigDefaults && input.plan.approvalPolicy + ? { approvalPolicy: input.plan.approvalPolicy } + : {}), + ...(!input.plan.usesCodexConfigDefaults + ? { sandboxPolicy: codexAppServerRuntimeModeToSandboxPolicy(input.plan.runtimeMode) } + : {}), + ...(input.plan.modelId ? { model: input.plan.modelId } : {}), + ...(serviceTier ? { serviceTier } : {}), + ...(effort ? { effort } : {}), + ...(collaborationMode ? { collaborationMode } : {}), + } +} diff --git a/src/main/lib/agent-runtime/codex-app-server-policy.ts b/src/main/lib/agent-runtime/codex-app-server-policy.ts new file mode 100644 index 000000000..6872a9b82 --- /dev/null +++ b/src/main/lib/agent-runtime/codex-app-server-policy.ts @@ -0,0 +1,93 @@ +import type { AgentPermissionMode } from "./types" + +export type CodexAppServerRuntimeMode = + | "approval-required" + | "auto-accept-edits" + | "full-access" + +export type CodexAppServerApprovalPolicy = + | "untrusted" + | "on-failure" + | "on-request" + | "never" + +export type CodexAppServerSandboxMode = + | "read-only" + | "workspace-write" + | "danger-full-access" + +export interface CodexAppServerPolicyResolution { + runtimeMode: CodexAppServerRuntimeMode + approvalPolicy?: CodexAppServerApprovalPolicy + sandboxMode?: CodexAppServerSandboxMode + bypassApprovalsAndSandbox: boolean + usesCodexConfigDefaults: boolean + notes: string[] +} + +export function resolveCodexAppServerPolicies( + permissionMode: AgentPermissionMode, +): CodexAppServerPolicyResolution { + if (permissionMode === "bypass" || permissionMode === "full-access") { + return { + runtimeMode: "full-access", + approvalPolicy: "never", + sandboxMode: "danger-full-access", + bypassApprovalsAndSandbox: true, + usesCodexConfigDefaults: false, + notes: [ + permissionMode === "full-access" + ? "Moss full-access maps to Codex app-server full-access runtime." + : "Moss bypass maps to Codex app-server full-access runtime.", + ], + } + } + + if (permissionMode === "custom") { + return { + runtimeMode: "approval-required", + bypassApprovalsAndSandbox: false, + usesCodexConfigDefaults: true, + notes: [ + "Moss custom permissions defer app-server approval and sandbox overrides to Codex config.", + ], + } + } + + if (permissionMode === "plan" || permissionMode === "read-only") { + return { + runtimeMode: "approval-required", + approvalPolicy: "untrusted", + sandboxMode: "read-only", + bypassApprovalsAndSandbox: false, + usesCodexConfigDefaults: false, + notes: [ + "Moss read-only permissions map to Codex app-server approval-required runtime.", + ], + } + } + + if (permissionMode === "ask-approval") { + return { + runtimeMode: "auto-accept-edits", + approvalPolicy: "on-request", + sandboxMode: "workspace-write", + bypassApprovalsAndSandbox: false, + usesCodexConfigDefaults: false, + notes: [ + "Moss ask-approval maps to workspace-write with on-request approvals.", + ], + } + } + + return { + runtimeMode: "auto-accept-edits", + approvalPolicy: "never", + sandboxMode: "workspace-write", + bypassApprovalsAndSandbox: false, + usesCodexConfigDefaults: false, + notes: [ + "Legacy Moss agent permissions keep Codex app-server workspace writes with approvals disabled for compatibility.", + ], + } +} diff --git a/src/main/lib/agent-runtime/codex-app-server-runtime.ts b/src/main/lib/agent-runtime/codex-app-server-runtime.ts new file mode 100644 index 000000000..7246f9588 --- /dev/null +++ b/src/main/lib/agent-runtime/codex-app-server-runtime.ts @@ -0,0 +1,3824 @@ +import { spawn } from "node:child_process"; +import type { Readable, Writable } from "node:stream"; +import { + CodexAppServerRpcClient, + codexAppServerInvalidParamsError, + type CodexAppServerJsonRpcId, + type CodexAppServerRpcNotification, + type CodexAppServerRpcRequest, +} from "./codex-app-server-client"; +import { + codexAppServerNotificationToStreamEvent, + codexAppServerRequestType, + codexAppServerServerRequestId, + codexAppServerServerRequestToStreamEvent, +} from "./codex-app-server-events"; +import { + buildCodexAppServerLaunchPlan, + buildCodexAppServerThreadForkParams, + buildCodexAppServerThreadResumeParams, + buildCodexAppServerThreadStartParams, + buildCodexAppServerTurnStartParams, + hasConfiguredCodexAppServerMcpServer, + isRecoverableCodexAppServerThreadResumeError, + type CodexAppServerLaunchPlan, +} from "./codex-app-server-plan"; +import { + createAgentRuntimeProcessHandle, + registerAgentRuntimeProcessHandle, + unregisterAgentRuntimeProcessHandle, +} from "./process-registry"; +import { + createAgentRuntimeRunId, + createAgentRuntimeRunReceipt, + createInMemoryAgentRuntimeRunLedger, + projectAgentRuntimeRunLedgerSnapshot, + replayAgentRuntimeRunLedgerEvents, + type AgentRuntimeRunLedger, +} from "./runtime-run-ledger"; +import type { AgentRuntimeReceiptBus } from "./runtime-receipt-bus"; +import type { + AgentRuntimeAccountRateLimitsReadRequest, + AgentRuntimeAccountRateLimitsReadResult, + AgentRuntimeAccountLoginCancelRequest, + AgentRuntimeAccountLoginCancelResult, + AgentRuntimeAccountLoginStartRequest, + AgentRuntimeAccountLoginStartResult, + AgentRuntimeAccountLoginType, + AgentRuntimeAccountLogoutRequest, + AgentRuntimeAccountLogoutResult, + AgentRuntimeAccountReadRequest, + AgentRuntimeAccountReadResult, + AgentRuntimeAccountUsageReadRequest, + AgentRuntimeAccountUsageReadResult, + AgentRuntimeAppListRequest, + AgentRuntimeAppListResult, + AgentRuntimeConfigBatchWriteRequest, + AgentRuntimeConfigReadRequest, + AgentRuntimeConfigReadResult, + AgentRuntimeConfigRequirementsReadRequest, + AgentRuntimeConfigRequirementsReadResult, + AgentRuntimeConfigValueWriteRequest, + AgentRuntimeConfigWriteEdit, + AgentRuntimeConfigWriteMergeStrategy, + AgentRuntimeConfigWriteResult, + AgentRuntimeControlResult, + AgentRuntimeExternalAgentConfigDetectRequest, + AgentRuntimeExternalAgentConfigDetectResult, + AgentRuntimeExternalAgentConfigImportRequest, + AgentRuntimeExternalAgentConfigImportResult, + AgentRuntimeExternalAgentConfigMigrationDetails, + AgentRuntimeExternalAgentConfigMigrationItem, + AgentRuntimeExternalAgentConfigMigrationItemType, + AgentRuntimeHookListRequest, + AgentRuntimeHookListResult, + AgentRuntimeMcpServerConfigReloadRequest, + AgentRuntimeMcpServerConfigReloadResult, + AgentRuntimeMcpServerOauthLoginRequest, + AgentRuntimeMcpServerOauthLoginResult, + AgentRuntimeMcpServerStatusDetail, + AgentRuntimeMcpServerStatusListRequest, + AgentRuntimeMcpServerStatusListResult, + AgentRuntimeModelListRequest, + AgentRuntimeModelListResult, + AgentRuntimePermissionProfileListRequest, + AgentRuntimePermissionProfileListResult, + AgentRuntimePluginInstalledRequest, + AgentRuntimePluginInstalledResult, + AgentRuntimePluginInstallRequest, + AgentRuntimePluginInstallResult, + AgentRuntimePluginListRequest, + AgentRuntimePluginListResult, + AgentRuntimePluginMarketplaceKind, + AgentRuntimePluginReadRequest, + AgentRuntimePluginReadResult, + AgentRuntimeRunAction, + AgentRuntimeRunReceipt, + AgentRuntimeSessionRef, + AgentRuntimeSkillListRequest, + AgentRuntimeSkillListResult, + AgentRuntimeStartRequest, + AgentRuntimeStreamEvent, + AgentRuntimeThreadControlRequest, + AgentRuntimeThreadControlResult, + AgentRuntimeThreadDiffResult, + AgentRuntimeThreadFullDiffRequest, + AgentRuntimeThreadForkRequest, + AgentRuntimeThreadForkResult, + AgentRuntimeThreadGoal, + AgentRuntimeThreadGoalClearRequest, + AgentRuntimeThreadGoalClearResult, + AgentRuntimeThreadGoalGetRequest, + AgentRuntimeThreadGoalGetResult, + AgentRuntimeThreadGoalSetRequest, + AgentRuntimeThreadGoalSetResult, + AgentRuntimeThreadGoalStatus, + AgentRuntimeThreadLoadedListRequest, + AgentRuntimeThreadLoadedListResult, + AgentRuntimeThreadListRequest, + AgentRuntimeThreadListResult, + AgentRuntimeThreadMetadataUpdateRequest, + AgentRuntimeThreadMetadataUpdateResult, + AgentRuntimeThreadNameSetRequest, + AgentRuntimeThreadNameSetResult, + AgentRuntimeThreadReadRequest, + AgentRuntimeThreadReadResult, + AgentRuntimeThreadRollbackRequest, + AgentRuntimeThreadRollbackResult, + AgentRuntimeThreadTurnDiffRequest, + AgentRuntimeToolResultRequest, +} from "./types"; +import { + providerRuntimeEventToStreamEvent, + type CanonicalRuntimeRequestType, +} from "./provider-runtime-contract"; + +interface CodexAppServerProcess { + stdin: Writable; + stdout: Readable; + stderr?: Readable | null; + kill(signal?: NodeJS.Signals | number): boolean; + once( + event: "exit" | "close", + listener: (...args: unknown[]) => void, + ): unknown; +} + +interface PendingServerRequest { + jsonRpcId: CodexAppServerJsonRpcId; + method: string; + requestId: string; + requestType: CanonicalRuntimeRequestType; + turnId?: string; + itemId?: string; +} + +export interface CodexAppServerRuntimeOptions { + spawnProcess?: (plan: CodexAppServerLaunchPlan) => CodexAppServerProcess; + runLedger?: AgentRuntimeRunLedger | null; + recordRunLedger?: boolean; + runtimeReceipts?: AgentRuntimeReceiptBus; + appServerArgs?: readonly string[] | null; + command?: string | null; + env?: NodeJS.ProcessEnv | Record | null; + codeHome?: string | null; + homeDir?: string | null; +} + +let defaultCodexAppServerRuntimeRunLedger = + createInMemoryAgentRuntimeRunLedger(); + +export function getCodexAppServerRuntimeRunLedger(): AgentRuntimeRunLedger { + return defaultCodexAppServerRuntimeRunLedger; +} + +export function resetCodexAppServerRuntimeRunLedgerForTests(): void { + defaultCodexAppServerRuntimeRunLedger = + createInMemoryAgentRuntimeRunLedger(); +} + +const THREAD_GOAL_STATUSES = new Set([ + "active", + "paused", + "blocked", + "usageLimited", + "budgetLimited", + "complete", +]); + +const MCP_SERVER_STATUS_DETAILS = new Set([ + "full", + "toolsAndAuthOnly", +]); + +const CONFIG_WRITE_MERGE_STRATEGIES = + new Set(["replace", "upsert"]); + +const ACCOUNT_LOGIN_TYPES = new Set([ + "apiKey", + "chatgpt", + "chatgptDeviceCode", + "chatgptAuthTokens", +]); + +const PLUGIN_MARKETPLACE_KINDS = new Set([ + "local", + "vertical", + "workspace-directory", + "shared-with-me", +]); + +const EXTERNAL_AGENT_CONFIG_ITEM_TYPES = + new Set([ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "SESSIONS", + ]); + +class AsyncEventQueue implements AsyncIterable { + private readonly items: T[] = []; + private readonly waiters: Array<{ + resolve(value: IteratorResult): void; + reject(error: unknown): void; + }> = []; + private closed = false; + private error: unknown; + + push(item: T): void { + if (this.closed) return; + const waiter = this.waiters.shift(); + if (waiter) { + waiter.resolve({ value: item, done: false }); + return; + } + this.items.push(item); + } + + close(): void { + if (this.closed) return; + this.closed = true; + for (const waiter of this.waiters.splice(0)) { + waiter.resolve({ value: undefined as T, done: true }); + } + } + + fail(error: unknown): void { + if (this.closed) return; + this.closed = true; + this.error = error; + for (const waiter of this.waiters.splice(0)) { + waiter.reject(error); + } + } + + async next(): Promise> { + if (this.items.length > 0) { + return { value: this.items.shift() as T, done: false }; + } + if (this.error) throw this.error; + if (this.closed) return { value: undefined as T, done: true }; + + return new Promise>((resolve, reject) => { + this.waiters.push({ resolve, reject }); + }); + } + + [Symbol.asyncIterator](): AsyncIterator { + return this; + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function cleanString(value: unknown): string | undefined { + if (typeof value !== "string") return undefined; + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; +} + +function splitModelAndReasoning(modelId: string | null | undefined): { + modelId?: string; + reasoningEffort?: string; +} { + const cleaned = cleanString(modelId); + if (!cleaned) return {}; + const separatorIndex = cleaned.indexOf("/"); + if (separatorIndex === -1) return { modelId: cleaned }; + + const model = cleanString(cleaned.slice(0, separatorIndex)); + const reasoningEffort = cleanString(cleaned.slice(separatorIndex + 1)); + return { + ...(model ? { modelId: model } : {}), + ...(reasoningEffort ? { reasoningEffort } : {}), + }; +} + +function readOptionsRecord(value: unknown): Record | null { + return isRecord(value) ? value : null; +} + +function resolveSessionModelAndEffort(session: AgentRuntimeSessionRef): { + modelId?: string; + reasoningEffort?: string; + serviceTier?: string; + developerInstructions?: string; +} { + const metadataSelection = readOptionsRecord(session.metadata?.modelSelection); + const options = + readOptionsRecord(session.modelSelection?.options) ?? + readOptionsRecord(metadataSelection?.options); + const selectedModel = + cleanString(session.modelSelection?.modelId) ?? + cleanString(metadataSelection?.modelId) ?? + cleanString(metadataSelection?.model); + const legacy = splitModelAndReasoning(selectedModel ? null : session.modelId); + + return { + modelId: selectedModel ?? legacy.modelId, + reasoningEffort: + cleanString(options?.reasoningEffort) ?? + cleanString(options?.reasoning_effort) ?? + legacy.reasoningEffort, + serviceTier: + cleanString(options?.serviceTier) ?? cleanString(options?.service_tier), + developerInstructions: + cleanString(options?.developerInstructions) ?? + cleanString(options?.developer_instructions), + }; +} + +function resolveModelAndEffort(request: AgentRuntimeStartRequest): { + modelId?: string; + reasoningEffort?: string; + serviceTier?: string; + developerInstructions?: string; +} { + return resolveSessionModelAndEffort(request.session); +} + +function patchPlanModel( + plan: CodexAppServerLaunchPlan, + modelId: string | undefined, +): CodexAppServerLaunchPlan { + if (!modelId || plan.modelId === modelId) return plan; + return { ...plan, modelId }; +} + +function cleanStringArray( + value: string[] | null | undefined, +): string[] | undefined { + const cleaned = (value ?? []) + .map((entry) => cleanString(entry)) + .filter((entry): entry is string => Boolean(entry)); + return cleaned.length > 0 ? cleaned : undefined; +} + +function cleanCwdFilter( + value: string | string[] | null | undefined, +): string | string[] | undefined { + if (Array.isArray(value)) return cleanStringArray(value); + return cleanString(value); +} + +function buildThreadListParams( + request: AgentRuntimeThreadListRequest, +): Record { + const params: Record = {}; + if (typeof request.archived === "boolean") params.archived = request.archived; + const cursor = cleanString(request.cursor); + if (cursor) params.cursor = cursor; + const cwd = cleanCwdFilter(request.cwd); + if (cwd) params.cwd = cwd; + if (request.limit !== null && request.limit !== undefined) { + if (!Number.isInteger(request.limit) || request.limit < 0) { + throw new Error( + "Codex app-server thread/list limit must be an integer >= 0.", + ); + } + params.limit = request.limit; + } + const modelProviders = cleanStringArray(request.modelProviders); + if (modelProviders) params.modelProviders = modelProviders; + const searchTerm = cleanString(request.searchTerm); + if (searchTerm) params.searchTerm = searchTerm; + if (request.sortDirection) params.sortDirection = request.sortDirection; + if (request.sortKey) params.sortKey = request.sortKey; + const sourceKinds = cleanStringArray(request.sourceKinds); + if (sourceKinds) params.sourceKinds = sourceKinds; + if (typeof request.useStateDbOnly === "boolean") { + params.useStateDbOnly = request.useStateDbOnly; + } + return params; +} + +function buildThreadLoadedListParams( + request: AgentRuntimeThreadLoadedListRequest, +): Record { + const params: Record = {}; + const cursor = cleanString(request.cursor); + if (cursor) params.cursor = cursor; + if (request.limit !== null && request.limit !== undefined) { + if (!Number.isInteger(request.limit) || request.limit < 0) { + throw new Error( + "Codex app-server thread/loaded/list limit must be an integer >= 0.", + ); + } + params.limit = request.limit; + } + return params; +} + +function writeOptionalLimitParam( + params: Record, + value: number | null | undefined, + method: string, +): void { + if (value === null || value === undefined) return; + if (!Number.isInteger(value) || value < 0) { + throw new Error( + `Codex app-server ${method} limit must be an integer >= 0.`, + ); + } + params.limit = value; +} + +function buildConfigReadParams( + request: AgentRuntimeConfigReadRequest, +): Record { + const params: Record = {}; + const cwd = cleanString(request.cwd); + if (cwd) params.cwd = cwd; + if (typeof request.includeLayers === "boolean") { + params.includeLayers = request.includeLayers; + } + return params; +} + +function normalizeConfigWriteMergeStrategy( + value: AgentRuntimeConfigWriteMergeStrategy | undefined, +): AgentRuntimeConfigWriteMergeStrategy { + if (value === undefined) return "replace"; + if (!CONFIG_WRITE_MERGE_STRATEGIES.has(value)) { + throw new Error( + `Unsupported Codex app-server config merge strategy: ${value}`, + ); + } + return value; +} + +function buildConfigWriteEdit( + edit: AgentRuntimeConfigWriteEdit, + index: number, +): Record { + const keyPath = cleanString(edit.keyPath); + if (!keyPath) { + throw new Error( + `Codex app-server config/batchWrite edit ${index + 1} requires a keyPath.`, + ); + } + if (edit.value === undefined) { + throw new Error( + `Codex app-server config/batchWrite edit ${index + 1} requires a JSON value.`, + ); + } + return { + keyPath, + mergeStrategy: normalizeConfigWriteMergeStrategy(edit.mergeStrategy), + value: edit.value, + }; +} + +function writeConfigFileOptions( + params: Record, + request: { + filePath?: string | null; + expectedVersion?: string | null; + }, +): void { + const filePath = cleanString(request.filePath); + if (filePath) params.filePath = filePath; + const expectedVersion = cleanString(request.expectedVersion); + if (expectedVersion) params.expectedVersion = expectedVersion; +} + +function buildConfigValueWriteParams( + request: AgentRuntimeConfigValueWriteRequest, +): Record { + const keyPath = cleanString(request.keyPath); + if (!keyPath) { + throw new Error("Codex app-server config/value/write requires a keyPath."); + } + if (request.value === undefined) { + throw new Error( + "Codex app-server config/value/write requires a JSON value.", + ); + } + const params: Record = { + keyPath, + mergeStrategy: normalizeConfigWriteMergeStrategy(request.mergeStrategy), + value: request.value, + }; + writeConfigFileOptions(params, request); + return params; +} + +function buildConfigBatchWriteParams( + request: AgentRuntimeConfigBatchWriteRequest, +): Record { + if (!Array.isArray(request.edits) || request.edits.length === 0) { + throw new Error( + "Codex app-server config/batchWrite requires at least one edit.", + ); + } + const params: Record = { + edits: request.edits.map((edit, index) => + buildConfigWriteEdit(edit, index), + ), + }; + writeConfigFileOptions(params, request); + if (typeof request.reloadUserConfig === "boolean") { + params.reloadUserConfig = request.reloadUserConfig; + } + return params; +} + +function buildPermissionProfileListParams( + request: AgentRuntimePermissionProfileListRequest, +): Record { + const params: Record = {}; + const cursor = cleanString(request.cursor); + if (cursor) params.cursor = cursor; + const cwd = cleanString(request.cwd); + if (cwd) params.cwd = cwd; + writeOptionalLimitParam(params, request.limit, "permissionProfile/list"); + return params; +} + +function buildMcpServerStatusListParams( + request: AgentRuntimeMcpServerStatusListRequest, +): Record { + const params: Record = {}; + const cursor = cleanString(request.cursor); + if (cursor) params.cursor = cursor; + if (request.detail !== null && request.detail !== undefined) { + if (!MCP_SERVER_STATUS_DETAILS.has(request.detail)) { + throw new Error( + `Unsupported Codex app-server mcpServerStatus/list detail: ${request.detail}`, + ); + } + params.detail = request.detail; + } + writeOptionalLimitParam(params, request.limit, "mcpServerStatus/list"); + const threadId = cleanString(request.threadId); + if (threadId) params.threadId = threadId; + return params; +} + +function writeCwdsParam( + params: Record, + cwds: string[] | null | undefined, +): void { + const cleaned = cleanStringArray(cwds); + if (cleaned) params.cwds = cleaned; +} + +function buildSkillListParams( + request: AgentRuntimeSkillListRequest, +): Record { + const params: Record = {}; + writeCwdsParam(params, request.cwds); + if (typeof request.forceReload === "boolean") { + params.forceReload = request.forceReload; + } + return params; +} + +function buildHookListParams( + request: AgentRuntimeHookListRequest, +): Record { + const params: Record = {}; + writeCwdsParam(params, request.cwds); + return params; +} + +function buildAppListParams( + request: AgentRuntimeAppListRequest, +): Record { + const params: Record = {}; + const cursor = cleanString(request.cursor); + if (cursor) params.cursor = cursor; + if (typeof request.forceRefetch === "boolean") { + params.forceRefetch = request.forceRefetch; + } + writeOptionalLimitParam(params, request.limit, "app/list"); + const threadId = cleanString(request.threadId); + if (threadId) params.threadId = threadId; + return params; +} + +function cleanPluginMarketplaceKinds( + value: AgentRuntimePluginMarketplaceKind[] | null | undefined, +): AgentRuntimePluginMarketplaceKind[] | undefined { + const cleaned = (value ?? []) + .map((entry) => cleanString(entry)) + .filter((entry): entry is AgentRuntimePluginMarketplaceKind => + Boolean(entry), + ); + for (const kind of cleaned) { + if (!PLUGIN_MARKETPLACE_KINDS.has(kind)) { + throw new Error( + `Unsupported Codex app-server plugin marketplace kind: ${kind}`, + ); + } + } + return cleaned.length > 0 ? cleaned : undefined; +} + +function buildPluginListParams( + request: AgentRuntimePluginListRequest, +): Record { + const params: Record = {}; + writeCwdsParam(params, request.cwds); + const marketplaceKinds = cleanPluginMarketplaceKinds( + request.marketplaceKinds, + ); + if (marketplaceKinds) params.marketplaceKinds = marketplaceKinds; + return params; +} + +function buildPluginInstalledParams( + request: AgentRuntimePluginInstalledRequest, +): Record { + const params: Record = {}; + writeCwdsParam(params, request.cwds); + const installSuggestionPluginNames = cleanStringArray( + request.installSuggestionPluginNames, + ); + if (installSuggestionPluginNames) { + params.installSuggestionPluginNames = installSuggestionPluginNames; + } + return params; +} + +function writePluginTargetParams( + params: Record, + request: AgentRuntimePluginReadRequest | AgentRuntimePluginInstallRequest, + method: string, +): void { + const pluginName = cleanString(request.pluginName); + if (!pluginName) { + throw new Error(`Codex app-server ${method} requires a pluginName.`); + } + params.pluginName = pluginName; + const marketplacePath = cleanString(request.marketplacePath); + if (marketplacePath) params.marketplacePath = marketplacePath; + const remoteMarketplaceName = cleanString(request.remoteMarketplaceName); + if (remoteMarketplaceName) + params.remoteMarketplaceName = remoteMarketplaceName; +} + +function buildPluginReadParams( + request: AgentRuntimePluginReadRequest, +): Record { + const params: Record = {}; + writePluginTargetParams(params, request, "plugin/read"); + return params; +} + +function buildPluginInstallParams( + request: AgentRuntimePluginInstallRequest, +): Record { + const params: Record = {}; + writePluginTargetParams(params, request, "plugin/install"); + return params; +} + +function buildExternalAgentConfigDetectParams( + request: AgentRuntimeExternalAgentConfigDetectRequest, +): Record { + const params: Record = {}; + writeCwdsParam(params, request.cwds); + if (typeof request.includeHome === "boolean") { + params.includeHome = request.includeHome; + } + return params; +} + +function normalizeNamedMigrationDetails( + value: unknown, +): Array<{ name: string }> | undefined { + if (!Array.isArray(value)) return undefined; + const entries = value.flatMap((entry) => { + const name = cleanString(readOptionsRecord(entry)?.name); + return name ? [{ name }] : []; + }); + return entries.length > 0 ? entries : undefined; +} + +function normalizePluginMigrationDetails( + value: unknown, +): Array<{ marketplaceName: string; pluginNames: string[] }> | undefined { + if (!Array.isArray(value)) return undefined; + const entries = value.flatMap((entry) => { + const record = readOptionsRecord(entry); + const marketplaceName = cleanString(record?.marketplaceName); + const pluginNames = cleanStringArray( + Array.isArray(record?.pluginNames) ? record.pluginNames : undefined, + ); + if (!marketplaceName || !pluginNames) return []; + return [{ marketplaceName, pluginNames }]; + }); + return entries.length > 0 ? entries : undefined; +} + +function normalizeSessionMigrationDetails( + value: unknown, +): Array<{ cwd: string; path: string; title?: string | null }> | undefined { + if (!Array.isArray(value)) return undefined; + const entries = value.flatMap((entry) => { + const record = readOptionsRecord(entry); + const cwd = cleanString(record?.cwd); + const path = cleanString(record?.path); + if (!cwd || !path) return []; + const title = cleanString(record?.title); + return [ + { + cwd, + path, + ...(title ? { title } : record?.title === null ? { title: null } : {}), + }, + ]; + }); + return entries.length > 0 ? entries : undefined; +} + +function normalizeExternalAgentConfigMigrationDetails( + value: unknown, +): AgentRuntimeExternalAgentConfigMigrationDetails | null | undefined { + if (value === null) return null; + const record = readOptionsRecord(value); + if (!record) return undefined; + + const details: AgentRuntimeExternalAgentConfigMigrationDetails = {}; + const commands = normalizeNamedMigrationDetails(record.commands); + if (commands) details.commands = commands; + const hooks = normalizeNamedMigrationDetails(record.hooks); + if (hooks) details.hooks = hooks; + const mcpServers = normalizeNamedMigrationDetails(record.mcpServers); + if (mcpServers) details.mcpServers = mcpServers; + const plugins = normalizePluginMigrationDetails(record.plugins); + if (plugins) details.plugins = plugins; + const sessions = normalizeSessionMigrationDetails(record.sessions); + if (sessions) details.sessions = sessions; + const subagents = normalizeNamedMigrationDetails(record.subagents); + if (subagents) details.subagents = subagents; + + return Object.keys(details).length > 0 ? details : undefined; +} + +function normalizeExternalAgentConfigMigrationItem( + value: unknown, + index: number, +): AgentRuntimeExternalAgentConfigMigrationItem { + const record = readOptionsRecord(value); + if (!record) { + throw new Error( + `Codex app-server externalAgentConfig/import item ${index + 1} must be an object.`, + ); + } + const itemType = cleanString(record.itemType); + if ( + !itemType || + !EXTERNAL_AGENT_CONFIG_ITEM_TYPES.has( + itemType as AgentRuntimeExternalAgentConfigMigrationItemType, + ) + ) { + throw new Error( + `Codex app-server externalAgentConfig/import item ${index + 1} has an unsupported itemType.`, + ); + } + const description = cleanString(record.description); + if (!description) { + throw new Error( + `Codex app-server externalAgentConfig/import item ${index + 1} requires a description.`, + ); + } + const cwd = cleanString(record.cwd); + const details = normalizeExternalAgentConfigMigrationDetails(record.details); + + return { + cwd: cwd ?? null, + description, + ...(details !== undefined ? { details } : {}), + itemType: itemType as AgentRuntimeExternalAgentConfigMigrationItemType, + }; +} + +function readExternalAgentConfigMigrationItems( + response: unknown, +): AgentRuntimeExternalAgentConfigMigrationItem[] { + return readArrayProperty(response, "items").flatMap((item, index) => { + try { + return [normalizeExternalAgentConfigMigrationItem(item, index)]; + } catch { + return []; + } + }); +} + +function buildExternalAgentConfigImportParams( + request: AgentRuntimeExternalAgentConfigImportRequest, +): Record { + if ( + !Array.isArray(request.migrationItems) || + request.migrationItems.length === 0 + ) { + throw new Error( + "Codex app-server externalAgentConfig/import requires at least one migration item.", + ); + } + + return { + migrationItems: request.migrationItems.map((item, index) => + normalizeExternalAgentConfigMigrationItem(item, index), + ), + }; +} + +function buildMcpServerOauthLoginParams( + request: AgentRuntimeMcpServerOauthLoginRequest, +): Record { + const name = cleanString(request.name); + if (!name) { + throw new Error("Codex app-server mcpServer/oauth/login requires a name."); + } + const params: Record = { name }; + if (request.scopes !== null && request.scopes !== undefined) { + const scopes = request.scopes + .map((scope) => cleanString(scope)) + .filter((scope): scope is string => Boolean(scope)); + if (scopes.length > 0) params.scopes = scopes; + } + if (request.timeoutSecs !== null && request.timeoutSecs !== undefined) { + if (!Number.isInteger(request.timeoutSecs) || request.timeoutSecs < 0) { + throw new Error( + "Codex app-server mcpServer/oauth/login timeoutSecs must be an integer >= 0.", + ); + } + params.timeoutSecs = request.timeoutSecs; + } + return params; +} + +function buildModelListParams( + request: AgentRuntimeModelListRequest, +): Record { + const params: Record = {}; + const cursor = cleanString(request.cursor); + if (cursor) params.cursor = cursor; + if (request.includeHidden !== null && request.includeHidden !== undefined) { + params.includeHidden = request.includeHidden; + } + writeOptionalLimitParam(params, request.limit, "model/list"); + return params; +} + +function buildAccountReadParams( + request: AgentRuntimeAccountReadRequest, +): Record { + const params: Record = {}; + if (typeof request.refreshToken === "boolean") { + params.refreshToken = request.refreshToken; + } + return params; +} + +function buildAccountLoginStartParams( + request: AgentRuntimeAccountLoginStartRequest, +): Record { + if (!ACCOUNT_LOGIN_TYPES.has(request.type)) { + throw new Error( + `Unsupported Codex app-server account login type: ${request.type}`, + ); + } + + switch (request.type) { + case "apiKey": { + const apiKey = cleanString(request.apiKey); + if (!apiKey) { + throw new Error( + "Codex app-server account/login/start apiKey requires an API key.", + ); + } + return { + apiKey, + type: "apiKey", + }; + } + case "chatgpt": { + return { + ...(typeof request.codexStreamlinedLogin === "boolean" + ? { codexStreamlinedLogin: request.codexStreamlinedLogin } + : {}), + type: "chatgpt", + }; + } + case "chatgptDeviceCode": + return { + type: "chatgptDeviceCode", + }; + case "chatgptAuthTokens": { + const accessToken = cleanString(request.accessToken); + const chatgptAccountId = cleanString(request.chatgptAccountId); + if (!accessToken || !chatgptAccountId) { + throw new Error( + "Codex app-server account/login/start chatgptAuthTokens requires accessToken and chatgptAccountId.", + ); + } + return { + accessToken, + chatgptAccountId, + ...(request.chatgptPlanType !== undefined + ? { + chatgptPlanType: + request.chatgptPlanType === null + ? null + : (cleanString(request.chatgptPlanType) ?? null), + } + : {}), + type: "chatgptAuthTokens", + }; + } + } +} + +function buildAccountLoginCancelParams( + request: AgentRuntimeAccountLoginCancelRequest, +): Record { + const loginId = cleanString(request.loginId); + if (!loginId) { + throw new Error( + "Codex app-server account/login/cancel requires a loginId.", + ); + } + return { loginId }; +} + +function buildThreadNameSetParams( + request: AgentRuntimeThreadNameSetRequest, +): Record { + const name = cleanString(request.name); + if (!name) { + throw new Error( + "Codex app-server thread/name/set requires a non-empty name.", + ); + } + return { + threadId: requireThreadId(request.threadId), + name, + }; +} + +function buildThreadMetadataUpdateParams( + request: AgentRuntimeThreadMetadataUpdateRequest, +): Record { + const params: Record = { + threadId: requireThreadId(request.threadId), + }; + if (request.gitInfo !== undefined) { + const gitInfo = request.gitInfo; + params.gitInfo = + gitInfo === null + ? null + : { + ...(gitInfo.branch !== undefined + ? { + branch: + gitInfo.branch === null + ? null + : (cleanString(gitInfo.branch) ?? null), + } + : {}), + ...(gitInfo.originUrl !== undefined + ? { + originUrl: + gitInfo.originUrl === null + ? null + : (cleanString(gitInfo.originUrl) ?? null), + } + : {}), + ...(gitInfo.sha !== undefined + ? { + sha: + gitInfo.sha === null + ? null + : (cleanString(gitInfo.sha) ?? null), + } + : {}), + }; + } + return params; +} + +function cleanThreadGoalStatus( + status: AgentRuntimeThreadGoalStatus | null | undefined, +): AgentRuntimeThreadGoalStatus | null | undefined { + if (status === null || status === undefined) return status; + if (!THREAD_GOAL_STATUSES.has(status)) { + throw new Error( + `Unsupported Codex app-server thread goal status: ${status}`, + ); + } + return status; +} + +function buildThreadGoalSetParams( + request: AgentRuntimeThreadGoalSetRequest, +): Record { + const params: Record = { + threadId: requireThreadId(request.threadId), + }; + + if (request.objective !== undefined) { + params.objective = + request.objective === null + ? null + : (cleanString(request.objective) ?? null); + } + if (request.status !== undefined) { + params.status = cleanThreadGoalStatus(request.status); + } + if (request.tokenBudget !== undefined) { + if ( + request.tokenBudget !== null && + (!Number.isInteger(request.tokenBudget) || request.tokenBudget < 0) + ) { + throw new Error( + "Codex app-server thread/goal/set tokenBudget must be an integer >= 0.", + ); + } + params.tokenBudget = request.tokenBudget; + } + + return params; +} + +function imageToAppServerInput( + image: NonNullable[number], +): { type: "image"; url: string } | null { + const mediaType = cleanString(image.mediaType); + const base64Data = cleanString(image.base64Data); + if (!mediaType || !base64Data) return null; + return { + type: "image", + url: `data:${mediaType};base64,${base64Data}`, + }; +} + +function readNestedString(value: unknown, key: string): string | undefined { + return isRecord(value) ? cleanString(value[key]) : undefined; +} + +function readThreadId(response: unknown): string | undefined { + return ( + readNestedString(readOptionsRecord(response)?.thread, "id") ?? + readNestedString(response, "threadId") + ); +} + +function readThreadList(response: unknown): unknown[] { + return readDataList(response); +} + +function readDataList(response: unknown): unknown[] { + const data = readOptionsRecord(response)?.data; + return Array.isArray(data) ? data : []; +} + +function readArrayProperty(response: unknown, key: string): unknown[] { + const value = readOptionsRecord(response)?.[key]; + return Array.isArray(value) ? value : []; +} + +function readStringArrayProperty(response: unknown, key: string): string[] { + return readArrayProperty(response, key).filter( + (value): value is string => typeof value === "string", + ); +} + +function readThreadIdList(response: unknown): string[] { + const data = readOptionsRecord(response)?.data; + return Array.isArray(data) + ? data.filter((value): value is string => typeof value === "string") + : []; +} + +function readOptionalString(response: unknown, key: string): string | null { + const value = readOptionsRecord(response)?.[key]; + return typeof value === "string" && value.trim() ? value : null; +} + +function readBoolean(response: unknown, key: string): boolean | undefined { + const value = readOptionsRecord(response)?.[key]; + return typeof value === "boolean" ? value : undefined; +} + +function readNumber(response: unknown, key: string): number | undefined { + const value = readOptionsRecord(response)?.[key]; + return typeof value === "number" && Number.isFinite(value) + ? value + : undefined; +} + +function readThreadDiffString(response: unknown): string | undefined { + const record = readOptionsRecord(response); + if (typeof record?.diff === "string") return record.diff; + + const nested = readOptionsRecord(record?.threadDiff); + return typeof nested?.diff === "string" ? nested.diff : undefined; +} + +function readThreadDiffNumber( + response: unknown, + key: "fromTurnCount" | "toTurnCount", +): number | undefined { + return ( + readNumber(response, key) ?? + ((): number | undefined => { + const nested = readOptionsRecord(readOptionsRecord(response)?.threadDiff); + const value = nested?.[key]; + return typeof value === "number" && Number.isFinite(value) + ? value + : undefined; + })() + ); +} + +function readThreadGoal( + response: unknown, +): AgentRuntimeThreadGoal | null | undefined { + const goal = readOptionsRecord(response)?.goal; + if (goal === null) return null; + const goalRecord = readOptionsRecord(goal); + if (!goalRecord) return undefined; + + return { + createdAt: + typeof goalRecord.createdAt === "number" + ? goalRecord.createdAt + : undefined, + objective: + typeof goalRecord.objective === "string" + ? goalRecord.objective + : undefined, + status: + typeof goalRecord.status === "string" ? goalRecord.status : undefined, + threadId: + typeof goalRecord.threadId === "string" ? goalRecord.threadId : undefined, + timeUsedSeconds: + typeof goalRecord.timeUsedSeconds === "number" + ? goalRecord.timeUsedSeconds + : undefined, + tokenBudget: + goalRecord.tokenBudget === null || + typeof goalRecord.tokenBudget === "number" + ? goalRecord.tokenBudget + : undefined, + tokensUsed: + typeof goalRecord.tokensUsed === "number" + ? goalRecord.tokensUsed + : undefined, + updatedAt: + typeof goalRecord.updatedAt === "number" + ? goalRecord.updatedAt + : undefined, + }; +} + +function readTurnId(response: unknown): string | undefined { + return ( + readNestedString(readOptionsRecord(response)?.turn, "id") ?? + readNestedString(response, "turnId") + ); +} + +function makeInitializeParams(): Record { + return { + clientInfo: { + name: "1code_desktop", + title: "1code Desktop", + version: "0.0.0", + }, + capabilities: { + experimentalApi: true, + }, + }; +} + +function spawnCodexAppServer( + plan: CodexAppServerLaunchPlan, +): CodexAppServerProcess { + const child = spawn(plan.command, plan.args, { + cwd: plan.cwd, + env: plan.extendEnv ? { ...process.env, ...plan.env } : plan.env, + stdio: ["pipe", "pipe", "pipe"], + }); + return child; +} + +function nowIso(): string { + return new Date().toISOString(); +} + +function requireNativeThreadId(session: AgentRuntimeSessionRef): string { + const threadId = cleanString(session.nativeSessionId); + if (!threadId) { + throw new Error( + "Codex app-server thread control requires a native thread id.", + ); + } + return threadId; +} + +function requireThreadId(threadId: string | null | undefined): string { + const cleaned = cleanString(threadId); + if (!cleaned) { + throw new Error("Codex app-server thread control requires a thread id."); + } + return cleaned; +} + +async function withCodexAppServerControl( + session: AgentRuntimeSessionRef, + options: CodexAppServerRuntimeOptions, + runControl: ( + client: CodexAppServerRpcClient, + plan: CodexAppServerLaunchPlan, + ) => Promise, +): Promise { + const modelOptions = resolveModelAndEffort({ + session, + prompt: "", + }); + const launchPlan = patchPlanModel( + buildCodexAppServerLaunchPlan({ + session, + appServerArgs: options.appServerArgs, + command: options.command, + env: options.env, + codeHome: options.codeHome, + homeDir: options.homeDir, + }), + modelOptions.modelId, + ); + const child = (options.spawnProcess ?? spawnCodexAppServer)(launchPlan); + const client = new CodexAppServerRpcClient({ + stdin: child.stdin, + stdout: child.stdout, + stderr: child.stderr, + }); + + try { + await client.request("initialize", makeInitializeParams()); + client.notify("initialized"); + + if (hasConfiguredCodexAppServerMcpServer(launchPlan.args)) { + await client.request("config/mcpServer/reload"); + } + + return await runControl(client, launchPlan); + } finally { + client.close(); + child.kill("SIGTERM"); + } +} + +export async function readCodexAppServerThread( + request: AgentRuntimeThreadReadRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + const includeTurns = request.includeTurns ?? true; + let threadId: string | undefined; + + try { + const nativeThreadId = requireNativeThreadId(request.session); + threadId = nativeThreadId; + const response = await withCodexAppServerControl( + request.session, + options, + (client) => + client.request("thread/read", { + threadId: nativeThreadId, + includeTurns, + }), + ); + + return { + status: "success", + threadId: readThreadId(response) ?? nativeThreadId, + thread: response, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/read", + includeTurns, + }, + }; + } catch (error) { + return { + status: "error", + threadId, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/read", + includeTurns, + }, + }; + } +} + +export async function forkCodexAppServerThread( + request: AgentRuntimeThreadForkRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + let sourceThreadId: string | undefined; + + try { + const nativeThreadId = requireNativeThreadId(request.session); + sourceThreadId = nativeThreadId; + const modelOptions = resolveSessionModelAndEffort(request.session); + const response = await withCodexAppServerControl( + request.session, + options, + (client, plan) => + client.request( + "thread/fork", + buildCodexAppServerThreadForkParams({ + plan: patchPlanModel(plan, modelOptions.modelId), + threadId: nativeThreadId, + serviceTier: modelOptions.serviceTier, + }), + ), + ); + const forkedThreadId = readThreadId(response); + if (!forkedThreadId) { + throw new Error( + "Codex app-server thread/fork response did not include a forked thread id.", + ); + } + + return { + status: "success", + sourceThreadId: nativeThreadId, + threadId: forkedThreadId, + thread: response, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/fork", + }, + }; + } catch (error) { + return { + status: "error", + sourceThreadId, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/fork", + }, + }; + } +} + +function resolveThreadDiffThreadId( + request: + | AgentRuntimeThreadTurnDiffRequest + | AgentRuntimeThreadFullDiffRequest, +): string { + return requireThreadId(request.threadId ?? request.session.nativeSessionId); +} + +function makeThreadDiffMetadata( + method: "thread/diff/turn" | "thread/diff/full", + ignoreWhitespace: boolean | undefined, +): Record { + return { + bridge: "codex-app-server", + method, + ignoreWhitespace: ignoreWhitespace === true, + }; +} + +function validateTurnDiffRange( + fromTurnCount: number, + toTurnCount: number, +): string | null { + if ( + !Number.isInteger(fromTurnCount) || + !Number.isInteger(toTurnCount) || + fromTurnCount < 0 || + toTurnCount < 0 || + fromTurnCount > toTurnCount + ) { + return "Codex app-server thread/diff/turn requires fromTurnCount and toTurnCount to be integers >= 0 with fromTurnCount <= toTurnCount."; + } + + return null; +} + +function validateFullThreadDiffTarget(toTurnCount: number): string | null { + if (!Number.isInteger(toTurnCount) || toTurnCount < 0) { + return "Codex app-server thread/diff/full requires toTurnCount to be an integer >= 0."; + } + + return null; +} + +export async function getCodexAppServerThreadTurnDiff( + request: AgentRuntimeThreadTurnDiffRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + const method = "thread/diff/turn" as const; + const metadata = makeThreadDiffMetadata(method, request.ignoreWhitespace); + const validationError = validateTurnDiffRange( + request.fromTurnCount, + request.toTurnCount, + ); + + if (validationError) { + return { + status: "error", + threadId: request.threadId ?? request.session.nativeSessionId ?? null, + fromTurnCount: request.fromTurnCount, + toTurnCount: request.toTurnCount, + message: validationError, + updatedAt: nowIso(), + metadata, + }; + } + + let threadId: string | undefined; + + try { + threadId = resolveThreadDiffThreadId(request); + const params: Record = { + threadId, + fromTurnCount: request.fromTurnCount, + toTurnCount: request.toTurnCount, + }; + if (request.ignoreWhitespace === true) { + params.ignoreWhitespace = true; + } + + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request(method, params), + ); + + return { + status: "success", + threadId: readThreadId(response) ?? threadId, + fromTurnCount: + readThreadDiffNumber(response, "fromTurnCount") ?? + request.fromTurnCount, + toTurnCount: + readThreadDiffNumber(response, "toTurnCount") ?? request.toTurnCount, + diff: readThreadDiffString(response) ?? "", + updatedAt: nowIso(), + metadata, + }; + } catch (error) { + return { + status: "error", + threadId, + fromTurnCount: request.fromTurnCount, + toTurnCount: request.toTurnCount, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata, + }; + } +} + +export async function getCodexAppServerThreadFullDiff( + request: AgentRuntimeThreadFullDiffRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + const method = "thread/diff/full" as const; + const metadata = makeThreadDiffMetadata(method, request.ignoreWhitespace); + const validationError = validateFullThreadDiffTarget(request.toTurnCount); + + if (validationError) { + return { + status: "error", + threadId: request.threadId ?? request.session.nativeSessionId ?? null, + fromTurnCount: 0, + toTurnCount: request.toTurnCount, + message: validationError, + updatedAt: nowIso(), + metadata, + }; + } + + let threadId: string | undefined; + + try { + threadId = resolveThreadDiffThreadId(request); + const params: Record = { + threadId, + toTurnCount: request.toTurnCount, + }; + if (request.ignoreWhitespace === true) { + params.ignoreWhitespace = true; + } + + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request(method, params), + ); + + return { + status: "success", + threadId: readThreadId(response) ?? threadId, + fromTurnCount: readThreadDiffNumber(response, "fromTurnCount") ?? 0, + toTurnCount: + readThreadDiffNumber(response, "toTurnCount") ?? request.toTurnCount, + diff: readThreadDiffString(response) ?? "", + updatedAt: nowIso(), + metadata, + }; + } catch (error) { + return { + status: "error", + threadId, + fromTurnCount: 0, + toTurnCount: request.toTurnCount, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata, + }; + } +} + +export async function listCodexAppServerThreads( + request: AgentRuntimeThreadListRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildThreadListParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("thread/list", params), + ); + + return { + status: "success", + threads: readThreadList(response), + nextCursor: readOptionalString(response, "nextCursor"), + backwardsCursor: readOptionalString(response, "backwardsCursor"), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/list", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/list", + }, + }; + } +} + +export async function listLoadedCodexAppServerThreads( + request: AgentRuntimeThreadLoadedListRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildThreadLoadedListParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("thread/loaded/list", params), + ); + + return { + status: "success", + threadIds: readThreadIdList(response), + nextCursor: readOptionalString(response, "nextCursor"), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/loaded/list", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/loaded/list", + }, + }; + } +} + +export async function readCodexAppServerConfig( + request: AgentRuntimeConfigReadRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildConfigReadParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("config/read", params), + ); + const record = readOptionsRecord(response) ?? {}; + const layers = record.layers; + + return { + status: "success", + config: record.config, + layers: Array.isArray(layers) + ? layers + : layers === null + ? null + : undefined, + origins: readOptionsRecord(record.origins) ?? {}, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "config/read", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "config/read", + }, + }; + } +} + +function buildConfigWriteResult( + response: unknown, + method: "config/value/write" | "config/batchWrite", + params: Record, +): AgentRuntimeConfigWriteResult { + const record = readOptionsRecord(response) ?? {}; + return { + status: "success", + filePath: typeof record.filePath === "string" ? record.filePath : null, + writeStatus: typeof record.status === "string" ? record.status : undefined, + version: typeof record.version === "string" ? record.version : null, + overriddenMetadata: + record.overriddenMetadata === undefined + ? undefined + : record.overriddenMetadata, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method, + params, + }, + }; +} + +export async function writeCodexAppServerConfigValue( + request: AgentRuntimeConfigValueWriteRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildConfigValueWriteParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("config/value/write", params), + ); + return buildConfigWriteResult(response, "config/value/write", params); + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "config/value/write", + }, + }; + } +} + +export async function batchWriteCodexAppServerConfig( + request: AgentRuntimeConfigBatchWriteRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildConfigBatchWriteParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("config/batchWrite", params), + ); + return buildConfigWriteResult(response, "config/batchWrite", params); + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "config/batchWrite", + }, + }; + } +} + +export async function readCodexAppServerConfigRequirements( + request: AgentRuntimeConfigRequirementsReadRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("configRequirements/read"), + ); + const record = readOptionsRecord(response) ?? {}; + + return { + status: "success", + requirements: + record.requirements === undefined ? null : record.requirements, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "configRequirements/read", + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "configRequirements/read", + }, + }; + } +} + +export async function listCodexAppServerPermissionProfiles( + request: AgentRuntimePermissionProfileListRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildPermissionProfileListParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("permissionProfile/list", params), + ); + + return { + status: "success", + profiles: readDataList(response), + nextCursor: readOptionalString(response, "nextCursor"), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "permissionProfile/list", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "permissionProfile/list", + }, + }; + } +} + +export async function listCodexAppServerMcpServerStatuses( + request: AgentRuntimeMcpServerStatusListRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildMcpServerStatusListParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("mcpServerStatus/list", params), + ); + + return { + status: "success", + servers: readDataList(response), + nextCursor: readOptionalString(response, "nextCursor"), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "mcpServerStatus/list", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "mcpServerStatus/list", + }, + }; + } +} + +export async function reloadCodexAppServerMcpServerConfig( + request: AgentRuntimeMcpServerConfigReloadRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + await withCodexAppServerControl(request.session, options, (client) => + client.request("config/mcpServer/reload"), + ); + + return { + status: "success", + reloaded: true, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "config/mcpServer/reload", + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "config/mcpServer/reload", + }, + }; + } +} + +export async function listCodexAppServerSkills( + request: AgentRuntimeSkillListRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildSkillListParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("skills/list", params), + ); + + return { + status: "success", + entries: readDataList(response), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "skills/list", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "skills/list", + }, + }; + } +} + +export async function listCodexAppServerHooks( + request: AgentRuntimeHookListRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildHookListParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("hooks/list", params), + ); + + return { + status: "success", + entries: readDataList(response), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "hooks/list", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "hooks/list", + }, + }; + } +} + +export async function listCodexAppServerApps( + request: AgentRuntimeAppListRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildAppListParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("app/list", params), + ); + + return { + status: "success", + apps: readDataList(response), + nextCursor: readOptionalString(response, "nextCursor"), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "app/list", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "app/list", + }, + }; + } +} + +export async function listCodexAppServerPlugins( + request: AgentRuntimePluginListRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildPluginListParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("plugin/list", params), + ); + + return { + status: "success", + marketplaces: readArrayProperty(response, "marketplaces"), + featuredPluginIds: readStringArrayProperty(response, "featuredPluginIds"), + marketplaceLoadErrors: readArrayProperty( + response, + "marketplaceLoadErrors", + ), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "plugin/list", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "plugin/list", + }, + }; + } +} + +export async function listInstalledCodexAppServerPlugins( + request: AgentRuntimePluginInstalledRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildPluginInstalledParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("plugin/installed", params), + ); + + return { + status: "success", + marketplaces: readArrayProperty(response, "marketplaces"), + marketplaceLoadErrors: readArrayProperty( + response, + "marketplaceLoadErrors", + ), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "plugin/installed", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "plugin/installed", + }, + }; + } +} + +export async function readCodexAppServerPlugin( + request: AgentRuntimePluginReadRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildPluginReadParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("plugin/read", params), + ); + + return { + status: "success", + plugin: readOptionsRecord(response)?.plugin, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "plugin/read", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "plugin/read", + }, + }; + } +} + +export async function installCodexAppServerPlugin( + request: AgentRuntimePluginInstallRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildPluginInstallParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("plugin/install", params), + ); + + return { + status: "success", + appsNeedingAuth: readArrayProperty(response, "appsNeedingAuth"), + authPolicy: readOptionalString(response, "authPolicy"), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "plugin/install", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "plugin/install", + }, + }; + } +} + +export async function detectCodexAppServerExternalAgentConfig( + request: AgentRuntimeExternalAgentConfigDetectRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildExternalAgentConfigDetectParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("externalAgentConfig/detect", params), + ); + + return { + status: "success", + items: readExternalAgentConfigMigrationItems(response), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "externalAgentConfig/detect", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "externalAgentConfig/detect", + }, + }; + } +} + +export async function importCodexAppServerExternalAgentConfig( + request: AgentRuntimeExternalAgentConfigImportRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildExternalAgentConfigImportParams(request); + await withCodexAppServerControl(request.session, options, (client) => + client.request("externalAgentConfig/import", params), + ); + + return { + status: "success", + importedCount: request.migrationItems.length, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "externalAgentConfig/import", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "externalAgentConfig/import", + }, + }; + } +} + +export async function startCodexAppServerMcpServerOauthLogin( + request: AgentRuntimeMcpServerOauthLoginRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildMcpServerOauthLoginParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("mcpServer/oauth/login", params), + ); + + return { + status: "success", + authorizationUrl: + readOptionalString(response, "authorizationUrl") ?? null, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "mcpServer/oauth/login", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "mcpServer/oauth/login", + }, + }; + } +} + +export async function listCodexAppServerModels( + request: AgentRuntimeModelListRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildModelListParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("model/list", params), + ); + + return { + status: "success", + models: readDataList(response), + nextCursor: readOptionalString(response, "nextCursor"), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "model/list", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "model/list", + }, + }; + } +} + +export async function startCodexAppServerAccountLogin( + request: AgentRuntimeAccountLoginStartRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildAccountLoginStartParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("account/login/start", params), + ); + const record = readOptionsRecord(response) ?? {}; + + return { + status: "success", + type: + typeof record.type === "string" + ? (record.type as AgentRuntimeAccountLoginType | (string & {})) + : undefined, + authUrl: readOptionalString(record, "authUrl") ?? null, + loginId: readOptionalString(record, "loginId") ?? null, + verificationUrl: readOptionalString(record, "verificationUrl") ?? null, + userCode: readOptionalString(record, "userCode") ?? null, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/login/start", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/login/start", + }, + }; + } +} + +export async function cancelCodexAppServerAccountLogin( + request: AgentRuntimeAccountLoginCancelRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildAccountLoginCancelParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("account/login/cancel", params), + ); + const record = readOptionsRecord(response) ?? {}; + + return { + status: "success", + cancelStatus: + typeof record.status === "string" + ? (record.status as AgentRuntimeAccountLoginCancelResult["cancelStatus"]) + : undefined, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/login/cancel", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/login/cancel", + }, + }; + } +} + +export async function logoutCodexAppServerAccount( + request: AgentRuntimeAccountLogoutRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + await withCodexAppServerControl(request.session, options, (client) => + client.request("account/logout"), + ); + + return { + status: "success", + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/logout", + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/logout", + }, + }; + } +} + +export async function readCodexAppServerAccount( + request: AgentRuntimeAccountReadRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const params = buildAccountReadParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("account/read", params), + ); + const record = readOptionsRecord(response) ?? {}; + + return { + status: "success", + account: record.account ?? null, + requiresOpenaiAuth: + typeof record.requiresOpenaiAuth === "boolean" + ? record.requiresOpenaiAuth + : undefined, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/read", + params, + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/read", + }, + }; + } +} + +export async function readCodexAppServerAccountRateLimits( + request: AgentRuntimeAccountRateLimitsReadRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("account/rateLimits/read"), + ); + const record = readOptionsRecord(response) ?? {}; + + return { + status: "success", + rateLimits: record.rateLimits, + rateLimitsByLimitId: + readOptionsRecord(record.rateLimitsByLimitId) ?? + (record.rateLimitsByLimitId === null ? null : undefined), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/rateLimits/read", + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/rateLimits/read", + }, + }; + } +} + +export async function readCodexAppServerAccountUsage( + request: AgentRuntimeAccountUsageReadRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + try { + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("account/usage/read"), + ); + const record = readOptionsRecord(response) ?? {}; + const buckets = record.dailyUsageBuckets; + + return { + status: "success", + summary: record.summary, + dailyUsageBuckets: Array.isArray(buckets) + ? buckets + : buckets === null + ? null + : undefined, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/usage/read", + }, + }; + } catch (error) { + return { + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "account/usage/read", + }, + }; + } +} + +export async function controlCodexAppServerThread( + request: AgentRuntimeThreadControlRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + const methodByAction = { + archive: "thread/archive", + unarchive: "thread/unarchive", + delete: "thread/delete", + } as const; + let threadId: string | undefined; + + try { + threadId = requireThreadId(request.threadId); + const method = methodByAction[request.action]; + const response = await withCodexAppServerControl( + request.session, + options, + (client) => + client.request(method, { + threadId, + }), + ); + + return { + status: "success", + action: request.action, + threadId: readThreadId(response) ?? threadId, + thread: response, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method, + }, + }; + } catch (error) { + return { + status: "error", + action: request.action, + threadId, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: methodByAction[request.action], + }, + }; + } +} + +export async function setCodexAppServerThreadName( + request: AgentRuntimeThreadNameSetRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + let threadId: string | undefined; + let name: string | undefined; + + try { + const params = buildThreadNameSetParams(request); + threadId = params.threadId as string; + name = params.name as string; + await withCodexAppServerControl(request.session, options, (client) => + client.request("thread/name/set", params), + ); + + return { + status: "success", + threadId, + name, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/name/set", + }, + }; + } catch (error) { + return { + status: "error", + threadId, + name, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/name/set", + }, + }; + } +} + +export async function updateCodexAppServerThreadMetadata( + request: AgentRuntimeThreadMetadataUpdateRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + let threadId: string | undefined; + + try { + threadId = requireThreadId(request.threadId); + const params = buildThreadMetadataUpdateParams(request); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("thread/metadata/update", params), + ); + + return { + status: "success", + threadId: readThreadId(response) ?? threadId, + thread: response, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/metadata/update", + }, + }; + } catch (error) { + return { + status: "error", + threadId, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/metadata/update", + }, + }; + } +} + +export async function getCodexAppServerThreadGoal( + request: AgentRuntimeThreadGoalGetRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + let threadId: string | undefined; + + try { + threadId = requireThreadId(request.threadId); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("thread/goal/get", { threadId }), + ); + const goal = readThreadGoal(response); + + return { + status: "success", + threadId: goal?.threadId ?? threadId, + goal, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/goal/get", + }, + }; + } catch (error) { + return { + status: "error", + threadId, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/goal/get", + }, + }; + } +} + +export async function setCodexAppServerThreadGoal( + request: AgentRuntimeThreadGoalSetRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + let threadId: string | undefined; + + try { + const params = buildThreadGoalSetParams(request); + threadId = params.threadId as string; + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("thread/goal/set", params), + ); + const goal = readThreadGoal(response); + + return { + status: "success", + threadId: goal?.threadId ?? threadId, + goal, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/goal/set", + params, + }, + }; + } catch (error) { + return { + status: "error", + threadId, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/goal/set", + }, + }; + } +} + +export async function clearCodexAppServerThreadGoal( + request: AgentRuntimeThreadGoalClearRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + let threadId: string | undefined; + + try { + threadId = requireThreadId(request.threadId); + const response = await withCodexAppServerControl( + request.session, + options, + (client) => client.request("thread/goal/clear", { threadId }), + ); + + return { + status: "success", + threadId, + cleared: readBoolean(response, "cleared") ?? true, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/goal/clear", + }, + }; + } catch (error) { + return { + status: "error", + threadId, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/goal/clear", + }, + }; + } +} + +export async function rollbackCodexAppServerThread( + request: AgentRuntimeThreadRollbackRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + let threadId: string | undefined; + + if (!Number.isInteger(request.numTurns) || request.numTurns < 1) { + return { + status: "error", + threadId: request.session.nativeSessionId ?? null, + numTurns: request.numTurns, + message: + "Codex app-server rollback requires numTurns to be an integer >= 1.", + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/rollback", + }, + }; + } + + try { + const nativeThreadId = requireNativeThreadId(request.session); + threadId = nativeThreadId; + const response = await withCodexAppServerControl( + request.session, + options, + (client) => + client.request("thread/rollback", { + threadId: nativeThreadId, + numTurns: request.numTurns, + }), + ); + + return { + status: "success", + threadId: readThreadId(response) ?? nativeThreadId, + numTurns: request.numTurns, + thread: response, + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/rollback", + }, + }; + } catch (error) { + return { + status: "error", + threadId, + numTurns: request.numTurns, + message: error instanceof Error ? error.message : String(error), + updatedAt: nowIso(), + metadata: { + bridge: "codex-app-server", + method: "thread/rollback", + }, + }; + } +} + +function isAcceptLike(value: string | undefined): boolean { + return [ + "accept", + "accepted", + "approve", + "approved", + "allow", + "yes", + "true", + ].includes(value ?? ""); +} + +function isSessionAcceptLike(value: string | undefined): boolean { + return [ + "acceptforsession", + "accept_for_session", + "approved_for_session", + "approve_for_session", + ].includes(value ?? ""); +} + +function isCancelLike(value: string | undefined): boolean { + return ["cancel", "abort", "aborted"].includes(value ?? ""); +} + +function readDecisionValue( + result: unknown, + isError: boolean | undefined, +): string { + if (isError) return "decline"; + if (typeof result === "boolean") return result ? "accept" : "decline"; + if (typeof result === "string") return result.trim().toLowerCase(); + if (isRecord(result)) { + return ( + cleanString(result.decision) ?? + cleanString(result.approval) ?? + cleanString(result.status) ?? + (typeof result.approved === "boolean" + ? result.approved + ? "accept" + : "decline" + : undefined) ?? + "accept" + ).toLowerCase(); + } + return "accept"; +} + +function normalizeApprovalDecision( + method: string, + request: AgentRuntimeToolResultRequest, +): string { + const value = readDecisionValue(request.result, request.isError); + if (method === "applyPatchApproval" || method === "execCommandApproval") { + if (isSessionAcceptLike(value)) return "approved_for_session"; + if (isAcceptLike(value)) return "approved"; + if (isCancelLike(value)) return "abort"; + return "denied"; + } + + if (isSessionAcceptLike(value)) return "acceptForSession"; + if (isAcceptLike(value)) return "accept"; + if (isCancelLike(value)) return "cancel"; + return "decline"; +} + +function normalizeUserInputAnswers( + result: unknown, +): Record { + const answers = + isRecord(result) && isRecord(result.answers) ? result.answers : result; + if (!isRecord(answers)) return {}; + + const normalized: Record = {}; + for (const [questionId, value] of Object.entries(answers)) { + if (isRecord(value) && Array.isArray(value.answers)) { + normalized[questionId] = { + answers: value.answers.map((entry) => String(entry)), + }; + } else if (Array.isArray(value)) { + normalized[questionId] = { + answers: value.map((entry) => String(entry)), + }; + } else { + normalized[questionId] = { + answers: [String(value)], + }; + } + } + return normalized; +} + +function responseForToolResult( + pending: PendingServerRequest, + request: AgentRuntimeToolResultRequest, +): unknown { + if ( + isRecord(request.result) && + request.result.codexAppServerResponse !== undefined + ) { + return request.result.codexAppServerResponse; + } + + if (pending.method === "item/tool/requestUserInput") { + return { + answers: normalizeUserInputAnswers(request.result), + }; + } + + if (pending.method === "item/tool/call") { + if (isRecord(request.result) && "contentItems" in request.result) + return request.result; + return { + contentItems: [{ type: "text", text: String(request.result ?? "") }], + success: request.isError !== true, + }; + } + + if (pending.method === "item/permissions/requestApproval") { + if (isRecord(request.result) && "permissions" in request.result) + return request.result; + return { + permissions: {}, + }; + } + + return { + decision: normalizeApprovalDecision(pending.method, request), + }; +} + +function makeResolvedEvent( + pending: PendingServerRequest, + response: unknown, + failed: boolean, +): AgentRuntimeStreamEvent { + if (pending.method === "item/tool/requestUserInput") { + const event = providerRuntimeEventToStreamEvent({ + type: "user-input.resolved", + engineId: "codex", + requestId: pending.requestId, + ...(pending.turnId ? { turnId: pending.turnId } : {}), + ...(pending.itemId ? { itemId: pending.itemId } : {}), + payload: { + ...(isRecord(response) && response.answers !== undefined + ? { answers: response.answers } + : { response }), + ...(failed ? { error: true } : {}), + }, + }); + if (event) return event; + } + + return { + type: "conversation-block-update", + id: pending.requestId, + patch: { + status: failed ? "failed" : "completed", + output: { + method: pending.method, + response, + }, + }, + }; +} + +function createAcceptedControlResult( + request: AgentRuntimeToolResultRequest, + pending: PendingServerRequest, +): AgentRuntimeControlResult { + return { + runId: request.runId, + status: "accepted", + message: "Codex app-server tool result accepted.", + updatedAt: new Date().toISOString(), + metadata: { + engineId: request.session.engineId, + requestId: pending.requestId, + method: pending.method, + }, + }; +} + +function createErrorControlResult( + request: AgentRuntimeToolResultRequest, + message: string, +): AgentRuntimeControlResult { + return { + runId: request.runId, + status: "error", + message, + updatedAt: new Date().toISOString(), + }; +} + +function createNotFoundControlResult( + request: AgentRuntimeToolResultRequest, +): AgentRuntimeControlResult { + return { + runId: request.runId, + status: "not-found", + message: `No pending Codex app-server request matched ${request.toolCallId}.`, + updatedAt: new Date().toISOString(), + }; +} + +function registerPendingAliases( + pendingById: Map, + request: CodexAppServerRpcRequest, + pending: PendingServerRequest, +): void { + pendingById.set(pending.requestId, pending); + pendingById.set(String(request.id), pending); + const params = readOptionsRecord(request.params); + for (const key of ["approvalId", "requestId", "itemId", "callId"]) { + const alias = cleanString(params?.[key]); + if (alias) pendingById.set(alias, pending); + } +} + +function readRequestAlias(value: unknown): string | undefined { + if (typeof value === "number" && Number.isFinite(value)) return String(value); + return cleanString(value); +} + +function removePendingAliases( + pendingById: Map, + pending: PendingServerRequest, +): void { + for (const [key, value] of [...pendingById.entries()]) { + if (value === pending) pendingById.delete(key); + } +} + +function correlateResolvedServerRequestNotification( + notification: CodexAppServerRpcNotification, + correlationsById: Map, +): CodexAppServerRpcNotification { + if (notification.method !== "serverRequest/resolved") return notification; + const params = readOptionsRecord(notification.params) ?? {}; + const rawRequestId = readRequestAlias(params.requestId); + if (!rawRequestId) return notification; + + const correlation = correlationsById.get(rawRequestId); + if (!correlation) return notification; + + removePendingAliases(correlationsById, correlation); + return { + ...notification, + params: { + ...params, + rawRequestId, + requestId: correlation.requestId, + requestType: correlation.requestType, + ...(correlation.turnId ? { turnId: correlation.turnId } : {}), + ...(correlation.itemId ? { itemId: correlation.itemId } : {}), + }, + }; +} + +function shouldCompleteForNotification( + notification: CodexAppServerRpcNotification, + activeTurnId: string | null, +): boolean { + if (notification.method !== "turn/completed") return false; + if (!activeTurnId) return true; + const params = readOptionsRecord(notification.params); + const turn = readOptionsRecord(params?.turn); + return ( + cleanString(params?.turnId) === activeTurnId || + cleanString(turn?.id) === activeTurnId + ); +} + +function isRuntimeStreamEventPayload( + value: unknown, +): value is AgentRuntimeStreamEvent { + return isRecord(value) && typeof value.type === "string"; +} + +export async function* streamCodexAppServerRuntimeRun( + action: AgentRuntimeRunAction, + request: AgentRuntimeStartRequest, + options: CodexAppServerRuntimeOptions = {}, +): AsyncIterable { + const startedAt = new Date(); + const runId = + request.runId ?? + createAgentRuntimeRunId(request.session, action, startedAt.getTime()); + const runLedger = + options.recordRunLedger === false || options.runLedger === null + ? null + : (options.runLedger ?? getCodexAppServerRuntimeRunLedger()); + const commandId = runId; + + if (runLedger) { + const commandReceipt = runLedger.getCommandReceipt(commandId); + if (commandReceipt?.status === "accepted") { + const snapshot = runLedger.snapshot(commandReceipt.runId); + if (snapshot && snapshot.receipt.status !== "running") { + for (const event of replayAgentRuntimeRunLedgerEvents(snapshot)) { + if ( + event.kind === "stream-event" && + isRuntimeStreamEventPayload(event.payload) + ) { + yield event.payload; + } + } + return; + } + } + if (commandReceipt?.status === "rejected") { + yield { + type: "error", + message: + commandReceipt.error ?? + "Codex app-server command was previously rejected.", + }; + yield { + type: "finish", + nativeSessionId: request.session.nativeSessionId ?? null, + resultSubtype: "error", + }; + return; + } + } + + const queue = new AsyncEventQueue(); + const pendingById = new Map(); + const requestCorrelationsById = new Map(); + let client: CodexAppServerRpcClient | undefined; + let child: CodexAppServerProcess | undefined; + let nativeSessionId = request.session.nativeSessionId ?? null; + let ledgerStatus: Exclude = + "success"; + let ledgerResultSubtype: AgentRuntimeRunReceipt["resultSubtype"] = "success"; + let ledgerErrorMessage: string | undefined; + let latestLedgerEventId: string | null = null; + let recordedStreamEventCount = 0; + let ledgerFinished = false; + let activeTurnId: string | null = null; + let providerInstanceId = request.session.providerInstanceId ?? null; + let lifecycleSettled = false; + let unregisterRun = true; + let interruptSent = false; + + const context = () => ({ + providerInstanceId, + nativeSessionId, + }); + + if (runLedger) { + runLedger.upsertReceipt( + createAgentRuntimeRunReceipt({ + runId, + action, + session: request.session, + status: "running", + nativeSessionId, + now: startedAt, + }), + ); + latestLedgerEventId = runLedger.appendEvent({ + runId, + engineId: request.session.engineId, + kind: "run-started", + commandId, + correlationId: commandId, + payload: { + bridge: "codex-app-server", + forceNewSession: request.forceNewSession === true, + imageCount: request.images?.length ?? 0, + promptLength: request.prompt.length, + streaming: true, + }, + }).id; + } + options.runtimeReceipts?.publish({ + type: "runtime.run.started", + runId, + engineId: request.session.engineId, + chatId: request.session.chatId, + subChatId: request.session.subChatId, + createdAt: startedAt.toISOString(), + source: "codex-app-server", + metadata: { + action, + forceNewSession: request.forceNewSession === true, + imageCount: request.images?.length ?? 0, + promptLength: request.prompt.length, + streaming: true, + }, + }); + + const recordStreamEvent = (event: AgentRuntimeStreamEvent): void => { + recordedStreamEventCount += 1; + if (event.type === "error" || event.type === "auth-error") { + ledgerStatus = "error"; + ledgerResultSubtype = "error"; + ledgerErrorMessage = event.message; + } + if (event.type === "finish") { + nativeSessionId = event.nativeSessionId ?? nativeSessionId; + ledgerResultSubtype = event.resultSubtype ?? ledgerResultSubtype; + if (event.resultSubtype === "cancelled") ledgerStatus = "cancelled"; + else if (event.resultSubtype === "error") ledgerStatus = "error"; + } + if (!runLedger) return; + latestLedgerEventId = runLedger.appendEvent({ + runId, + engineId: request.session.engineId, + kind: "stream-event", + commandId, + correlationId: commandId, + causationEventId: latestLedgerEventId, + payload: event as unknown as Record, + }).id; + }; + + const finishStreamLedger = (): void => { + if (ledgerFinished) return; + ledgerFinished = true; + let latestSequence: number | null = null; + if (runLedger) { + runLedger.finishRun({ + runId, + status: ledgerStatus, + resultSubtype: ledgerResultSubtype, + commandId, + correlationId: commandId, + causationEventId: latestLedgerEventId, + error: ledgerErrorMessage, + metadata: { + bridge: "codex-app-server", + eventCount: recordedStreamEventCount, + streaming: true, + }, + }); + const snapshot = runLedger.snapshot(runId); + const projection = snapshot + ? projectAgentRuntimeRunLedgerSnapshot(snapshot) + : null; + latestSequence = projection?.latestEventSequence ?? null; + const commandReceipt = runLedger.getCommandReceipt(commandId); + runLedger.upsertReceipt( + createAgentRuntimeRunReceipt({ + runId, + action, + session: request.session, + status: ledgerStatus, + nativeSessionId, + resultSubtype: ledgerResultSubtype, + now: startedAt, + completedAt: new Date(), + error: ledgerErrorMessage, + metadata: { + bridge: "codex-app-server", + eventCount: recordedStreamEventCount, + streaming: true, + ...(projection + ? { + ledger: { + eventCount: projection.eventCount, + latestEventSequence: projection.latestEventSequence ?? null, + latestEventKind: projection.latestEventKind ?? null, + correlationIds: projection.correlationIds, + causationEventIds: projection.causationEventIds, + }, + } + : {}), + ...(commandReceipt + ? { + commandReceipt: { + status: commandReceipt.status, + resultSequence: commandReceipt.resultSequence, + }, + } + : {}), + }, + }), + ); + } + options.runtimeReceipts?.publish({ + type: "turn.processing.quiesced", + runId, + engineId: request.session.engineId, + chatId: request.session.chatId, + subChatId: request.session.subChatId, + status: ledgerStatus, + resultSubtype: ledgerResultSubtype, + latestSequence, + eventCount: recordedStreamEventCount, + createdAt: new Date().toISOString(), + error: ledgerErrorMessage ?? null, + source: "codex-app-server", + metadata: { + action, + streaming: true, + }, + }); + }; + + const pushSessionNotification = ( + method: "session/connecting" | "session/ready" | "session/error", + message: string, + error?: unknown, + ): void => { + const event = codexAppServerNotificationToStreamEvent( + { + method, + params: { + message, + ...(nativeSessionId ? { threadId: nativeSessionId } : {}), + ...(error !== undefined ? { error } : {}), + }, + }, + { + ...context(), + eventId: `codex-app-server:${method}`, + }, + ); + if (event) queue.push(event); + }; + + const interruptActiveTurn = async (): Promise => { + if (interruptSent || !client || !nativeSessionId || !activeTurnId) return; + interruptSent = true; + await client.request("turn/interrupt", { + threadId: nativeSessionId, + turnId: activeTurnId, + }); + }; + + const handle = registerAgentRuntimeProcessHandle( + createAgentRuntimeProcessHandle({ + action, + session: request.session, + runId, + onStop: interruptActiveTurn, + submitToolResult: async (toolResult) => { + const pending = pendingById.get(toolResult.toolCallId); + if (!pending) return createNotFoundControlResult(toolResult); + if (!client) { + return createErrorControlResult( + toolResult, + "Codex app-server RPC client is not ready.", + ); + } + + try { + const response = responseForToolResult(pending, toolResult); + if (toolResult.isError) { + client.respondError( + pending.jsonRpcId, + codexAppServerInvalidParamsError( + "Codex app-server request rejected.", + response, + ), + ); + } else { + client.respond(pending.jsonRpcId, response); + } + removePendingAliases(pendingById, pending); + queue.push( + makeResolvedEvent(pending, response, toolResult.isError === true), + ); + return createAcceptedControlResult(toolResult, pending); + } catch (error) { + return createErrorControlResult( + toolResult, + error instanceof Error ? error.message : String(error), + ); + } + }, + }), + ); + + const runLifecycle = async (): Promise => { + try { + if (action === "resume" && !nativeSessionId) { + throw new Error( + "Codex app-server resume requires a native session id.", + ); + } + + const modelOptions = resolveModelAndEffort(request); + const launchPlan = patchPlanModel( + buildCodexAppServerLaunchPlan({ + session: request.session, + appServerArgs: options.appServerArgs, + command: options.command, + env: options.env, + codeHome: options.codeHome, + homeDir: options.homeDir, + }), + modelOptions.modelId, + ); + providerInstanceId = launchPlan.providerInstanceId; + child = (options.spawnProcess ?? spawnCodexAppServer)(launchPlan); + const appServerChild = child; + + let completeAbortListener: (() => void) | undefined; + const complete = new Promise((resolve, reject) => { + const abort = () => resolve(); + completeAbortListener = abort; + const fail = (error: Error) => { + if (handle.abortController.signal.aborted) resolve(); + else reject(error); + }; + + if (handle.abortController.signal.aborted) { + resolve(); + } else { + handle.abortController.signal.addEventListener("abort", abort, { + once: true, + }); + } + + const rpcClient = new CodexAppServerRpcClient({ + stdin: appServerChild.stdin, + stdout: appServerChild.stdout, + stderr: appServerChild.stderr, + signal: handle.abortController.signal, + onNotification(notification) { + const routedNotification = + correlateResolvedServerRequestNotification( + notification, + requestCorrelationsById, + ); + const params = readOptionsRecord(notification.params); + if (routedNotification.method === "thread/started") { + nativeSessionId = + readNestedString(params?.thread, "id") ?? nativeSessionId; + } + const event = codexAppServerNotificationToStreamEvent( + routedNotification, + context(), + ); + if (event) queue.push(event); + if ( + shouldCompleteForNotification(routedNotification, activeTurnId) + ) { + resolve(); + } + }, + onRequest(serverRequest) { + const requestId = codexAppServerServerRequestId(serverRequest); + const params = readOptionsRecord(serverRequest.params) ?? {}; + const pending: PendingServerRequest = { + jsonRpcId: serverRequest.id, + method: serverRequest.method, + requestId, + requestType: codexAppServerRequestType(serverRequest.method), + turnId: cleanString(params.turnId), + itemId: cleanString(params.itemId), + }; + registerPendingAliases(pendingById, serverRequest, pending); + registerPendingAliases( + requestCorrelationsById, + serverRequest, + pending, + ); + const event = codexAppServerServerRequestToStreamEvent( + serverRequest, + context(), + ); + if (event) queue.push(event); + }, + onStderrLine(line) { + if (!line.trim()) return; + queue.push({ + type: "conversation-block", + block: { + id: `codex-stderr:${Date.now()}`, + type: "status", + status: "completed", + level: "warning", + title: "Codex app-server", + message: line, + data: { + bridge: "codex-app-server", + stream: "stderr", + }, + }, + }); + }, + onProtocolError(error) { + queue.push({ type: "error", message: error.message }); + }, + }); + client = rpcClient; + + appServerChild.once("exit", () => + fail(new Error("Codex app-server process exited.")), + ); + appServerChild.once("close", () => + fail(new Error("Codex app-server process closed.")), + ); + }).finally(() => { + if (completeAbortListener) { + handle.abortController.signal.removeEventListener( + "abort", + completeAbortListener, + ); + } + }); + + const rpcClient = client; + if (!rpcClient) + throw new Error("Codex app-server RPC client is not ready."); + + pushSessionNotification( + "session/connecting", + "Starting Codex app-server session.", + ); + await rpcClient.request("initialize", makeInitializeParams()); + rpcClient.notify("initialized"); + + if (hasConfiguredCodexAppServerMcpServer(launchPlan.args)) { + await rpcClient.request("config/mcpServer/reload"); + } + pushSessionNotification( + "session/ready", + "Codex app-server session ready.", + ); + + let threadResponse: unknown; + if (action === "resume" && nativeSessionId && !request.forceNewSession) { + try { + threadResponse = await rpcClient.request( + "thread/resume", + buildCodexAppServerThreadResumeParams({ + plan: launchPlan, + threadId: nativeSessionId, + serviceTier: modelOptions.serviceTier, + }), + ); + } catch (error) { + if (!isRecoverableCodexAppServerThreadResumeError(error)) throw error; + threadResponse = await rpcClient.request( + "thread/start", + buildCodexAppServerThreadStartParams({ + plan: launchPlan, + serviceTier: modelOptions.serviceTier, + }), + ); + } + } else { + threadResponse = await rpcClient.request( + "thread/start", + buildCodexAppServerThreadStartParams({ + plan: launchPlan, + serviceTier: modelOptions.serviceTier, + }), + ); + } + + nativeSessionId = readThreadId(threadResponse) ?? nativeSessionId; + if (!nativeSessionId) { + throw new Error("Codex app-server did not return a thread id."); + } + + const turnResponse = await rpcClient.request( + "turn/start", + buildCodexAppServerTurnStartParams({ + plan: launchPlan, + threadId: nativeSessionId, + prompt: request.prompt, + images: request.images + ?.map(imageToAppServerInput) + .filter((image): image is { type: "image"; url: string } => + Boolean(image), + ), + effort: modelOptions.reasoningEffort, + serviceTier: modelOptions.serviceTier, + interactionMode: + request.session.permissionMode === "plan" ? "plan" : "default", + developerInstructions: modelOptions.developerInstructions, + }), + ); + activeTurnId = readTurnId(turnResponse) ?? activeTurnId; + await complete; + if (handle.abortController.signal.aborted) { + queue.push({ + type: "finish", + nativeSessionId, + resultSubtype: "cancelled", + }); + } + } catch (error) { + const wasCancelled = handle.abortController.signal.aborted; + if (!wasCancelled) { + pushSessionNotification( + "session/error", + error instanceof Error ? error.message : String(error), + error, + ); + queue.push({ + type: "error", + message: error instanceof Error ? error.message : String(error), + }); + } + queue.push({ + type: "finish", + nativeSessionId, + resultSubtype: wasCancelled ? "cancelled" : "error", + }); + } finally { + lifecycleSettled = true; + client?.close(); + child?.kill("SIGTERM"); + pendingById.clear(); + requestCorrelationsById.clear(); + if (unregisterRun) unregisterAgentRuntimeProcessHandle(handle.runId); + queue.close(); + } + }; + + const lifecyclePromise = runLifecycle(); + + try { + for await (const event of queue) { + recordStreamEvent(event); + yield event; + } + await lifecyclePromise; + finishStreamLedger(); + } finally { + if (!lifecycleSettled && !handle.abortController.signal.aborted) { + unregisterRun = false; + ledgerStatus = "cancelled"; + ledgerResultSubtype = "cancelled"; + handle.abortController.abort("Codex app-server stream consumer stopped."); + client?.close(); + child?.kill("SIGTERM"); + unregisterAgentRuntimeProcessHandle(handle.runId); + } + finishStreamLedger(); + } +} + +export async function runCodexAppServerRuntimeRun( + action: AgentRuntimeRunAction, + request: AgentRuntimeStartRequest, + options: CodexAppServerRuntimeOptions = {}, +): Promise { + const startedAt = new Date(); + const runId = + request.runId ?? + createAgentRuntimeRunId(request.session, action, startedAt.getTime()); + const runRequest = + request.runId === runId ? request : { ...request, runId }; + const runLedger = options.runLedger ?? getCodexAppServerRuntimeRunLedger(); + const commandId = runId; + const existingCommandReceipt = runLedger.getCommandReceipt(commandId); + if (existingCommandReceipt?.status === "accepted") { + const existingSnapshot = runLedger.snapshot(existingCommandReceipt.runId); + if (existingSnapshot && existingSnapshot.receipt.status !== "running") { + return existingSnapshot.receipt; + } + } + if (existingCommandReceipt?.status === "rejected") { + return createAgentRuntimeRunReceipt({ + runId, + action, + session: request.session, + status: "error", + nativeSessionId: request.session.nativeSessionId ?? null, + resultSubtype: "error", + now: startedAt, + completedAt: new Date(), + error: + existingCommandReceipt.error ?? + "Codex app-server command was previously rejected.", + metadata: { + bridge: "codex-app-server", + commandReceipt: { + status: existingCommandReceipt.status, + resultSequence: existingCommandReceipt.resultSequence, + }, + }, + }); + } + let latestLedgerEventId: string | null = null; + let lastText = ""; + let usage: Record | undefined; + let status: Exclude = "success"; + let resultSubtype: AgentRuntimeRunReceipt["resultSubtype"] = "success"; + let errorMessage: string | undefined; + let nativeSessionId = request.session.nativeSessionId ?? null; + let eventCount = 0; + + runLedger.upsertReceipt( + createAgentRuntimeRunReceipt({ + runId, + action, + session: request.session, + status: "running", + nativeSessionId, + now: startedAt, + }), + ); + latestLedgerEventId = runLedger.appendEvent({ + runId, + engineId: request.session.engineId, + kind: "run-started", + commandId, + correlationId: commandId, + payload: { + bridge: "codex-app-server", + forceNewSession: request.forceNewSession === true, + imageCount: request.images?.length ?? 0, + promptLength: request.prompt.length, + }, + }).id; + + for await (const event of streamCodexAppServerRuntimeRun( + action, + runRequest, + { + ...options, + runLedger, + recordRunLedger: false, + }, + )) { + eventCount += 1; + latestLedgerEventId = runLedger.appendEvent({ + runId, + engineId: request.session.engineId, + kind: "stream-event", + commandId, + correlationId: commandId, + causationEventId: latestLedgerEventId, + payload: event as unknown as Record, + }).id; + if (event.type === "text") lastText += event.text; + if (event.type === "usage") + usage = event as unknown as Record; + if (event.type === "error" || event.type === "auth-error") { + status = "error"; + resultSubtype = "error"; + errorMessage = event.message; + } + if (event.type === "finish") { + nativeSessionId = event.nativeSessionId ?? nativeSessionId; + resultSubtype = event.resultSubtype ?? resultSubtype; + if (event.resultSubtype === "cancelled") status = "cancelled"; + else if (event.resultSubtype === "error") status = "error"; + } + } + + runLedger.finishRun({ + runId, + status, + resultSubtype, + commandId, + correlationId: commandId, + causationEventId: latestLedgerEventId, + error: errorMessage, + metadata: { + bridge: "codex-app-server", + eventCount, + }, + }); + const ledgerSnapshot = runLedger.snapshot(runId); + const ledgerProjection = ledgerSnapshot + ? projectAgentRuntimeRunLedgerSnapshot(ledgerSnapshot) + : null; + const commandReceipt = runLedger.getCommandReceipt(commandId); + + const finalReceipt = createAgentRuntimeRunReceipt({ + runId, + action, + session: request.session, + status, + nativeSessionId, + resultSubtype, + now: startedAt, + completedAt: new Date(), + error: errorMessage, + metadata: { + bridge: "codex-app-server", + eventCount, + lastText, + usage, + ...(ledgerProjection + ? { + ledger: { + eventCount: ledgerProjection.eventCount, + latestEventSequence: ledgerProjection.latestEventSequence ?? null, + latestEventKind: ledgerProjection.latestEventKind ?? null, + correlationIds: ledgerProjection.correlationIds, + causationEventIds: ledgerProjection.causationEventIds, + }, + } + : {}), + ...(commandReceipt + ? { + commandReceipt: { + status: commandReceipt.status, + resultSequence: commandReceipt.resultSequence, + }, + } + : {}), + }, + }); + runLedger.upsertReceipt(finalReceipt); + return finalReceipt; +} diff --git a/src/main/lib/agent-runtime/codex-native-session.ts b/src/main/lib/agent-runtime/codex-native-session.ts index 640da3163..8b7d1c753 100644 --- a/src/main/lib/agent-runtime/codex-native-session.ts +++ b/src/main/lib/agent-runtime/codex-native-session.ts @@ -1,6 +1,7 @@ import { spawn } from "node:child_process" +import { constants, statSync } from "node:fs" import { mkdtemp, rm, writeFile } from "node:fs/promises" -import { tmpdir } from "node:os" +import { homedir, tmpdir } from "node:os" import path from "node:path" import { StringDecoder } from "node:string_decoder" import type { AgentPermissionMode } from "./types" @@ -52,6 +53,16 @@ export interface BuildCodexNativeSessionBridgePlanInput { includeJson?: boolean skipGitRepoCheck?: boolean imagePaths?: string[] | null + env?: NodeJS.ProcessEnv | null +} + +export interface ResolveCodexNativeCommandInput { + command?: string | null + env?: NodeJS.ProcessEnv | Record | null + homeDir?: string | null + platform?: NodeJS.Platform + pathDelimiter?: string + isExecutableFile?: (filePath: string) => boolean } export interface CodexNativeCommandRunnerInput { @@ -94,6 +105,18 @@ export type CodexNativeToolEvent = isError?: boolean } +export type CodexNativeTextAppendKind = + | "fresh" + | "suffix" + | "overlap" + | "duplicate" + | "separate" + +export interface CodexNativeTextAppendResult { + appendText: string + kind: CodexNativeTextAppendKind +} + export interface CodexExecResumeEventSummary { nativeSessionId?: string lastText?: string @@ -147,11 +170,142 @@ export interface CodexExecResumeBridgeResult events: CodexJsonlEvent[] } +const MIN_CODEX_NATIVE_TEXT_OVERLAP = 8 +const MIN_CODEX_NATIVE_REPEATED_FINAL_TEXT_LENGTH = 80 + +function normalizeCodexNativeComparableText(value: string): string { + return value.replace(/\r\n/g, "\n") +} + +function codexNativeTextOverlapLength( + existingText: string, + nextText: string, +): number { + const maxLength = Math.min(existingText.length, nextText.length) + + for (let length = maxLength; length >= MIN_CODEX_NATIVE_TEXT_OVERLAP; length--) { + if (existingText.endsWith(nextText.slice(0, length))) { + return length + } + } + + return 0 +} + +export function reconcileCodexNativeTextAppend( + existingText: string, + nextText: string, +): CodexNativeTextAppendResult { + if (!nextText) return { appendText: "", kind: "duplicate" } + if (!existingText) return { appendText: nextText, kind: "fresh" } + if (nextText.startsWith(existingText)) { + const appendText = nextText.slice(existingText.length) + return { + appendText, + kind: appendText ? "suffix" : "duplicate", + } + } + if (existingText.includes(nextText)) { + return { appendText: "", kind: "duplicate" } + } + + const overlapLength = codexNativeTextOverlapLength(existingText, nextText) + if (overlapLength > 0) { + const appendText = nextText.slice(overlapLength) + return { + appendText, + kind: appendText ? "overlap" : "duplicate", + } + } + + return { appendText: nextText, kind: "separate" } +} + +export function isCodexNativeRepeatedFinalText( + existingText: string, + nextText: string, +): boolean { + const existing = normalizeCodexNativeComparableText(existingText).trim() + const next = normalizeCodexNativeComparableText(nextText).trim() + if ( + existing.length < MIN_CODEX_NATIVE_REPEATED_FINAL_TEXT_LENGTH || + next.length <= existing.length || + !next.startsWith(existing) + ) { + return false + } + + const repeatedSuffix = next.slice(existing.length) + if (repeatedSuffix.length % existing.length !== 0) return false + + return repeatedSuffix === existing.repeat(repeatedSuffix.length / existing.length) +} + function cleanString(value: string | null | undefined): string | undefined { const trimmed = value?.trim() return trimmed ? trimmed : undefined } +function defaultIsExecutableFile(filePath: string): boolean { + try { + const stats = statSync(filePath) + if (!stats.isFile()) return false + if (process.platform === "win32") return true + return (stats.mode & constants.X_OK) !== 0 + } catch { + return false + } +} + +function uniqueCleanStrings(values: Array): string[] { + return Array.from( + new Set(values.map((value) => cleanString(value)).filter(Boolean) as string[]), + ) +} + +export function resolveCodexNativeCommand( + input: ResolveCodexNativeCommandInput = {}, +): string { + const explicitCommand = cleanString(input.command) + if (explicitCommand) return explicitCommand + + const env = input.env ?? process.env + const platform = input.platform ?? process.platform + const binaryName = platform === "win32" ? "codex.exe" : "codex" + const configuredPath = + cleanString(env.MOSS_CODEX_CLI_PATH) ?? cleanString(env.CODEX_CLI_PATH) + const isExecutableFile = input.isExecutableFile ?? defaultIsExecutableFile + + if (configuredPath) { + if (!isExecutableFile(configuredPath)) { + throw new Error(`[codex] Configured Codex CLI not found at ${configuredPath}.`) + } + + return configuredPath + } + + const home = cleanString(input.homeDir) ?? homedir() + const delimiter = + input.pathDelimiter ?? (platform === "win32" ? ";" : path.delimiter) + const pathValue = platform === "win32" ? env.PATH ?? env.Path : env.PATH + const pathCandidates = + pathValue + ?.split(delimiter) + .map((pathEntry) => cleanString(pathEntry)) + .filter(Boolean) + .map((pathEntry) => path.join(pathEntry!, binaryName)) ?? [] + const candidates = uniqueCleanStrings([ + path.join(home, ".local", "bin", binaryName), + path.join(home, ".bun", "bin", binaryName), + path.join(home, "bin", binaryName), + platform === "darwin" ? path.join("/opt/homebrew/bin", binaryName) : undefined, + platform === "darwin" ? path.join("/usr/local/bin", binaryName) : undefined, + ...pathCandidates, + ]) + + return candidates.find((candidate) => isExecutableFile(candidate)) ?? binaryName +} + export function splitCodexTextForStreamingDeltas( text: string, maxChunkLength = 36, @@ -245,23 +399,39 @@ function appendCodexExecPermissionArgs( args: string[], permissionMode: AgentPermissionMode, ): string[] { - if (permissionMode === "bypass") { + if (permissionMode === "bypass" || permissionMode === "full-access") { args.push("--dangerously-bypass-approvals-and-sandbox") return [ - "Moss bypass maps to Codex dangerous approval and sandbox bypass.", + permissionMode === "full-access" + ? "Moss full-access maps to Codex dangerous approval and sandbox bypass." + : "Moss bypass maps to Codex dangerous approval and sandbox bypass.", + ] + } + + if (permissionMode === "custom") { + return [ + "Moss custom permissions defer sandbox and approval policy to Codex config.toml.", ] } const sandboxMode = - permissionMode === "plan" ? "read-only" : "workspace-write" + permissionMode === "plan" || permissionMode === "read-only" + ? "read-only" + : "workspace-write" + const approvalPolicy = + permissionMode === "ask-approval" || permissionMode === "read-only" + ? "on-request" + : "never" args.push("-c", `sandbox_mode=${tomlString(sandboxMode)}`) - args.push("-c", `approval_policy=${tomlString("never")}`) + args.push("-c", `approval_policy=${tomlString(approvalPolicy)}`) return [ - permissionMode === "plan" - ? "Moss plan mode maps to Codex read-only sandbox for non-interactive resume." - : "Moss agent mode maps to Codex workspace-write sandbox for non-interactive resume.", - "Codex exec resume is non-interactive, so approvals are set to never.", + permissionMode === "plan" || permissionMode === "read-only" + ? "Moss read-only permissions map to Codex read-only sandbox." + : "Moss workspace permissions map to Codex workspace-write sandbox.", + approvalPolicy === "on-request" + ? "Moss ask-approval permissions map to Codex on-request approvals." + : "Legacy Moss agent permissions keep Codex exec approvals disabled for compatibility.", ] } @@ -298,19 +468,32 @@ function appendCodexTuiPermissionArgs( args: string[], permissionMode: AgentPermissionMode, ): string[] { - if (permissionMode === "bypass") { + if (permissionMode === "bypass" || permissionMode === "full-access") { args.push("--dangerously-bypass-approvals-and-sandbox") return [ - "Moss bypass maps to Codex dangerous approval and sandbox bypass.", + permissionMode === "full-access" + ? "Moss full-access maps to Codex dangerous approval and sandbox bypass." + : "Moss bypass maps to Codex dangerous approval and sandbox bypass.", ] } - args.push("-s", permissionMode === "plan" ? "read-only" : "workspace-write") + if (permissionMode === "custom") { + return [ + "Moss custom permissions defer sandbox and approval policy to Codex config.toml.", + ] + } + + args.push( + "-s", + permissionMode === "plan" || permissionMode === "read-only" + ? "read-only" + : "workspace-write", + ) args.push("-a", "on-request") return [ - permissionMode === "plan" - ? "Moss plan mode maps to Codex read-only sandbox." - : "Moss agent mode maps to Codex workspace-write sandbox.", + permissionMode === "plan" || permissionMode === "read-only" + ? "Moss read-only permissions map to Codex read-only sandbox." + : "Moss workspace permissions map to Codex workspace-write sandbox.", "Codex native fork is TUI-backed, so interactive approvals remain available.", ] } @@ -348,7 +531,10 @@ function appendPromptArg( export function buildCodexNativeSessionBridgePlan( input: BuildCodexNativeSessionBridgePlanInput, ): CodexNativeSessionBridgePlan { - const command = cleanString(input.command) ?? "codex" + const command = resolveCodexNativeCommand({ + command: input.command, + env: input.env, + }) const cwd = requireCleanString(input.cwd, "working directory") const modelId = cleanString(input.modelId) const permissionMode = input.permissionMode ?? "agent" @@ -1169,8 +1355,24 @@ export function summarizeCodexExecResumeEvents( accumulatedDeltaText += text summary.lastText = accumulatedDeltaText } else { - accumulatedDeltaText = "" - summary.lastText = text + const existingText = accumulatedDeltaText || summary.lastText || "" + if (isCodexNativeRepeatedFinalText(existingText, text)) { + summary.lastText = existingText + } else if (accumulatedDeltaText) { + const textAppend = reconcileCodexNativeTextAppend( + accumulatedDeltaText, + text, + ) + if (textAppend.kind !== "separate") { + accumulatedDeltaText += textAppend.appendText + summary.lastText = accumulatedDeltaText + } else { + accumulatedDeltaText = "" + summary.lastText = text + } + } else { + summary.lastText = text + } } } const usage = extractUsage(event) @@ -1388,6 +1590,7 @@ export async function runCodexExecBridge( prompt, promptSource: "stdin", imagePaths: materializedImages.imagePaths, + env: input.env, }) const runner = input.runner ?? spawnCodexNativeCommand const forwardedEventKeys = new Set() diff --git a/src/main/lib/agent-runtime/control-plane.ts b/src/main/lib/agent-runtime/control-plane.ts index 883a83285..7c47f459c 100644 --- a/src/main/lib/agent-runtime/control-plane.ts +++ b/src/main/lib/agent-runtime/control-plane.ts @@ -18,6 +18,10 @@ import { type MossProviderSummary, } from "../moss-source" import { getAgentRuntimeManifest } from "./manifests" +import { + readNativeThreadReadSummaryFromMetadata, + type NativeThreadReadSummary, +} from "./native-thread-summary" import { buildMossSessionActionPlan, type MossSessionActionPlan, @@ -75,6 +79,7 @@ export interface MossSessionControlEntry { actions: MossSessionActionPlan["actions"] providerId?: string providerModel?: string + nativeThreadRead: NativeThreadReadSummary | null updatedAt: string | null runtimeUpdatedAt: string | null } @@ -309,6 +314,9 @@ function buildEntries(params: { actions: actionPlan.actions, providerId: params.providerRoutes[engine]?.providerId, providerModel: params.providerRoutes[engine]?.model, + nativeThreadRead: readNativeThreadReadSummaryFromMetadata( + subChat.runtimeMetadata, + ), updatedAt: toIsoString(subChat.updatedAt), runtimeUpdatedAt: typeof metadata.updatedAt === "string" ? metadata.updatedAt : null, diff --git a/src/main/lib/agent-runtime/events.ts b/src/main/lib/agent-runtime/events.ts index 77d3e520e..dfce8cfce 100644 --- a/src/main/lib/agent-runtime/events.ts +++ b/src/main/lib/agent-runtime/events.ts @@ -3,6 +3,7 @@ import type { AgentRuntimeConversationBlock, AgentRuntimeStreamEvent, } from "./types" +import { providerRuntimeEventToStreamEvent } from "./provider-runtime-contract" const CONVERSATION_BLOCK_TYPES = new Set([ "exec", @@ -101,6 +102,9 @@ function normalizeConversationBlockUpdate( export function normalizeRuntimeStreamEvent( chunk: Record, ): AgentRuntimeStreamEvent | null { + const providerRuntimeEvent = providerRuntimeEventToStreamEvent(chunk) + if (providerRuntimeEvent) return providerRuntimeEvent + if (chunk.type === "text" && typeof chunk.text === "string") { return { type: "text", text: chunk.text } } diff --git a/src/main/lib/agent-runtime/hermes-acp-runtime.ts b/src/main/lib/agent-runtime/hermes-acp-runtime.ts new file mode 100644 index 000000000..0015f4abe --- /dev/null +++ b/src/main/lib/agent-runtime/hermes-acp-runtime.ts @@ -0,0 +1,279 @@ +import { createACPProvider, type ACPProvider } from "@mcpc-tech/acp-ai-provider" +import { streamText } from "ai" +import { homedir } from "node:os" +import { join } from "node:path" +import { resolveHermesAcpLaunch } from "../hermes/runtime" +import { + resolveMossProviderForEngine, + type ResolvedMossProvider, +} from "../moss-source/provider-config" +import { + createAgentRuntimeProcessHandle, + registerAgentRuntimeProcessHandle, + unregisterAgentRuntimeProcessHandle, +} from "./process-registry" +import type { + AgentPermissionMode, + AgentRuntimeStartRequest, + AgentRuntimeStreamEvent, +} from "./types" + +type HermesAcpProvider = Pick< + ACPProvider, + "cleanup" | "getSessionId" | "initSession" | "languageModel" | "tools" +> + +export type HermesAcpTextStreamInput = { + model: unknown + messages: Array<{ role: "user"; content: unknown }> + tools: unknown + abortSignal?: AbortSignal +} + +export type HermesAcpRuntimeDeps = { + resolveProvider?: typeof resolveMossProviderForEngine + getProviderSecret?: MossProviderSecretGetter + resolveLaunch?: typeof resolveHermesAcpLaunch + createProvider?: (input: Parameters[0]) => HermesAcpProvider + textStream?: (input: HermesAcpTextStreamInput) => AsyncIterable + env?: NodeJS.ProcessEnv + shellEnv?: Record +} + +type MossProviderSecretGetter = (providerId: string) => Promise<{ + apiKey?: string +}> + +const DEFAULT_HERMES_MODEL = "moss-default" + +export function resolveHermesAcpModelForCall( + model: string | null | undefined, +): string | undefined { + const normalized = model?.trim() + if (!normalized || normalized === DEFAULT_HERMES_MODEL || normalized === "hermes") { + return undefined + } + return normalized +} + +export function resolveHermesAcpMode( + permissionMode: AgentPermissionMode | undefined, +): string { + return permissionMode === "plan" || permissionMode === "read-only" + ? "default" + : "accept_edits" +} + +export function detectHermesAcpErrorText(text: string): string | undefined { + const trimmed = text.trim() + if (/^API call failed after \d+ retries:/i.test(trimmed)) { + return trimmed + } + return undefined +} + +export function buildHermesAcpProviderEnv(params: { + mossProvider?: ResolvedMossProvider | null + baseEnv?: NodeJS.ProcessEnv + shellEnv?: Record +}): Record { + const env: Record = {} + + for (const [key, value] of Object.entries(params.baseEnv ?? process.env)) { + if (typeof value === "string") env[key] = value + } + + for (const [key, value] of Object.entries(params.shellEnv ?? {})) { + if (typeof value === "string") env[key] = value + } + + if (!env.HERMES_HOME) { + env.HERMES_HOME = join(homedir(), ".hermes") + } + + if (params.mossProvider?.status === "resolved") { + Object.assign(env, params.mossProvider.env) + } + + return env +} + +export async function* streamHermesAcpRuntimeRun( + request: AgentRuntimeStartRequest, + deps: HermesAcpRuntimeDeps = {}, +): AsyncIterable { + const handle = registerAgentRuntimeProcessHandle( + createAgentRuntimeProcessHandle({ + action: "resume", + session: request.session, + }), + ) + let provider: HermesAcpProvider | null = null + + try { + const nativeSessionId = request.session.nativeSessionId + if (!nativeSessionId) { + yield { + type: "error", + message: "Hermes ACP resume requires a native session id.", + } + yield { type: "finish", resultSubtype: "error", nativeSessionId: null } + return + } + + const resolveProvider = deps.resolveProvider ?? resolveMossProviderForEngine + const mossProvider = await resolveProvider({ + projectPath: request.session.projectPath ?? request.session.cwd, + engineId: "hermes", + requestedModelId: request.session.modelId ?? undefined, + createIfMissing: true, + secretResolver: { + getSecret: deps.getProviderSecret ?? getDefaultMossProviderSecret, + }, + }) + + if (mossProvider.warnings.length > 0) { + console.warn("[hermes-acp-runtime] Moss provider warnings:", mossProvider.warnings) + } + + const launch = (deps.resolveLaunch ?? resolveHermesAcpLaunch)() + const createProvider: (input: Parameters[0]) => HermesAcpProvider = + deps.createProvider ?? createACPProvider + const shellEnv = deps.shellEnv ?? await getDefaultClaudeShellEnvironment() + const activeProvider = createProvider({ + command: launch.command, + args: launch.args, + env: buildHermesAcpProviderEnv({ + mossProvider, + baseEnv: deps.env, + shellEnv, + }), + ...((deps.env ?? process.env).HERMES_ACP_AUTH_METHOD + ? { authMethodId: (deps.env ?? process.env).HERMES_ACP_AUTH_METHOD } + : {}), + session: { + cwd: request.session.cwd, + mcpServers: [], + }, + existingSessionId: nativeSessionId, + persistSession: true, + }) + provider = activeProvider + + const sessionInfo = await activeProvider.initSession() + const activeNativeSessionId = + activeProvider.getSessionId() || sessionInfo?.sessionId || nativeSessionId + const providerModel = + mossProvider.status === "resolved" + ? mossProvider.model ?? request.session.modelId + : request.session.modelId + const languageModel = activeProvider.languageModel( + resolveHermesAcpModelForCall(providerModel), + resolveHermesAcpMode(request.session.permissionMode), + ) + const textStream = deps.textStream ?? defaultHermesAcpTextStream + let accumulatedText = "" + + for await (const text of textStream({ + model: languageModel, + messages: [ + { + role: "user", + content: buildHermesAcpUserContent(request), + }, + ], + tools: activeProvider.tools, + abortSignal: handle.abortController.signal, + })) { + if (text) { + accumulatedText += text + yield { type: "text", text } + } + } + + const wasCancelled = handle.abortController.signal.aborted + const detectedError = wasCancelled + ? undefined + : detectHermesAcpErrorText(accumulatedText) + if (detectedError) { + yield { type: "error", message: detectedError } + yield { + type: "finish", + nativeSessionId: activeNativeSessionId, + resultSubtype: "error", + } + return + } + + yield { + type: "finish", + nativeSessionId: activeNativeSessionId, + resultSubtype: wasCancelled ? "cancelled" : "success", + } + } catch (error) { + const wasCancelled = handle.abortController.signal.aborted + yield { + type: "error", + message: wasCancelled + ? "Hermes ACP runtime run was cancelled." + : error instanceof Error ? error.message : String(error), + } + yield { + type: "finish", + nativeSessionId: request.session.nativeSessionId ?? null, + resultSubtype: wasCancelled ? "cancelled" : "error", + } + } finally { + provider?.cleanup() + unregisterAgentRuntimeProcessHandle(handle.runId) + } +} + +async function* defaultHermesAcpTextStream( + input: HermesAcpTextStreamInput, +): AsyncIterable { + const result = streamText({ + model: input.model as never, + messages: input.messages as never, + tools: input.tools as never, + abortSignal: input.abortSignal, + }) as unknown as { + text?: PromiseLike + textStream?: AsyncIterable + } + + if (result.textStream) { + for await (const text of result.textStream) { + yield text + } + return + } + + const text = await result.text + if (text) yield text +} + +async function getDefaultMossProviderSecret(providerId: string): Promise<{ + apiKey?: string +}> { + const { getMossProviderSecret } = await import("../moss-source/provider-secrets") + return getMossProviderSecret(providerId) +} + +async function getDefaultClaudeShellEnvironment(): Promise> { + const { getClaudeShellEnvironment } = await import("../claude/env") + return getClaudeShellEnvironment() +} + +function buildHermesAcpUserContent(request: AgentRuntimeStartRequest): unknown { + if (!request.images?.length) return request.prompt + + return [ + { type: "text", text: request.prompt }, + ...request.images.map((image) => ({ + type: "image", + image: `data:${image.mediaType};base64,${image.base64Data}`, + ...(image.filename ? { filename: image.filename } : {}), + })), + ] +} diff --git a/src/main/lib/agent-runtime/hermes-native-session.ts b/src/main/lib/agent-runtime/hermes-native-session.ts index 742f4608f..3ab6d9b2c 100644 --- a/src/main/lib/agent-runtime/hermes-native-session.ts +++ b/src/main/lib/agent-runtime/hermes-native-session.ts @@ -1,4 +1,5 @@ import { spawn } from "node:child_process" +import { resolveHermesRuntime } from "../hermes/runtime" import type { AgentPermissionMode } from "./types" export type HermesNativeBridgeAction = "resume" | "fork" | "rollback" @@ -114,7 +115,9 @@ function requireCleanString( export function buildHermesNativeSessionBridgePlan( input: BuildHermesNativeSessionBridgePlanInput, ): HermesNativeSessionBridgePlan { - const command = cleanString(input.command) ?? "hermes" + const command = cleanString(input.command) ?? + resolveHermesRuntime().executable ?? + "hermes" const cwd = requireCleanString(input.cwd, "working directory") const sessionId = requireCleanString(input.sessionId, "session id") const modelId = cleanString(input.modelId) @@ -123,6 +126,9 @@ export function buildHermesNativeSessionBridgePlan( if (input.action === "resume") { const args = ["--resume", sessionId] const prompt = cleanString(input.prompt) + if (modelId) { + args.push("-m", modelId) + } const promptSource = input.promptSource === "none" || !prompt ? "none" : "argument" if (promptSource === "argument" && prompt) { @@ -270,10 +276,11 @@ export async function runHermesCliResumeBridge( }) const stdout = cleanString(result.stdout) const stderr = cleanString(result.stderr) - const error = - result.exitCode === 0 - ? undefined - : stderr ?? stdout ?? `Hermes exited with ${result.exitCode}.` + const error = detectHermesCliError({ + exitCode: result.exitCode, + stdout, + stderr, + }) return { ...result, @@ -284,3 +291,20 @@ export async function runHermesCliResumeBridge( success: result.exitCode === 0 && !error, } } + +function detectHermesCliError(input: { + exitCode: number | null + stdout?: string + stderr?: string +}): string | undefined { + if (input.exitCode !== 0) { + return input.stderr ?? input.stdout ?? `Hermes exited with ${input.exitCode}.` + } + + const text = input.stderr ?? input.stdout + if (text && /^API call failed after \d+ retries:/i.test(text)) { + return text + } + + return undefined +} diff --git a/src/main/lib/agent-runtime/index.ts b/src/main/lib/agent-runtime/index.ts index 0dd6de18c..2b0fd1d9b 100644 --- a/src/main/lib/agent-runtime/index.ts +++ b/src/main/lib/agent-runtime/index.ts @@ -1,10 +1,22 @@ export * from "./types" export * from "./manifests" export * from "./session-store" +export * from "./launch-plan" export * from "./session-actions" export * from "./session-records" export * from "./codex-native-session" export * from "./hermes-native-session" export * from "./control-plane" +export * from "./native-thread-summary" export * from "./adapters" export * from "./events" +export * from "./provider-instances" +export * from "./provider-runtime-contract" +export * from "./codex-app-server-policy" +export * from "./codex-app-server-plan" +export * from "./codex-app-server-events" +export * from "./codex-app-server-client" +export * from "./codex-app-server-runtime" +export * from "./process-registry" +export * from "./runtime-run-ledger" +export * from "./runtime-receipt-bus" diff --git a/src/main/lib/agent-runtime/launch-plan.ts b/src/main/lib/agent-runtime/launch-plan.ts new file mode 100644 index 000000000..00f49e7fd --- /dev/null +++ b/src/main/lib/agent-runtime/launch-plan.ts @@ -0,0 +1,183 @@ +import type { + AgentPermissionMode, + AgentRuntimeFeature, + AgentRuntimeManifest, + AgentRuntimeSessionRef, +} from "./types" +import { getAgentRuntimeManifest } from "./manifests" + +export type AgentRuntimeLaunchPlanResultSubtype = + | "running" + | "success" + | "error" + | "cancelled" + +export type AgentRuntimeNativeSessionStrategy = + | "start" + | "resume" + | "continue" + | "not-supported" + | "unknown" + +export type AgentRuntimeProjectionStatus = + | "ready" + | "partial" + | "missing" + | "unsupported" + | "unknown" + +export type AgentRuntimeCapabilityStatus = + | "native" + | "moss-projected" + | "unsupported" + +export interface AgentRuntimeNegotiatedCapability { + feature: AgentRuntimeFeature + status: AgentRuntimeCapabilityStatus +} + +export interface AgentRuntimeLaunchPlan { + version: 1 + runId: string + subChatId: string + chatId: string + engineId: AgentRuntimeSessionRef["engineId"] + modelId: string | null + permissionMode: AgentPermissionMode + cwd: string + projectPath: string | null + runtimeConfigDir: string | null + nativeSessionId: string | null + nativeSessionStrategy: AgentRuntimeNativeSessionStrategy + transport: string | null + providerRoute: string | null + projectionStatus: AgentRuntimeProjectionStatus + runtimeContextFingerprint: string | null + mcpFingerprint: string | null + resultSubtype: AgentRuntimeLaunchPlanResultSubtype + negotiatedCapabilities: AgentRuntimeNegotiatedCapability[] + createdAt: string + metadata?: Record +} + +export interface BuildAgentRuntimeLaunchPlanInput { + runId: string + session: AgentRuntimeSessionRef + manifest?: AgentRuntimeManifest + modelId?: string | null + nativeSessionId?: string | null + nativeSessionStrategy?: AgentRuntimeNativeSessionStrategy + transport?: string | null + providerRoute?: string | null + projectionStatus?: AgentRuntimeProjectionStatus + runtimeContextFingerprint?: string | null + mcpFingerprint?: string | null + resultSubtype?: AgentRuntimeLaunchPlanResultSubtype + now?: Date | string + metadata?: Record +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value) +} + +function resolveCreatedAt(value: Date | string | undefined): string { + if (typeof value === "string") return value + return (value ?? new Date()).toISOString() +} + +function inferNativeSessionStrategy(params: { + manifest: AgentRuntimeManifest + nativeSessionId: string | null + explicit?: AgentRuntimeNativeSessionStrategy +}): AgentRuntimeNativeSessionStrategy { + if (params.explicit) return params.explicit + if (params.manifest.availability === "unsupported") return "not-supported" + if (params.nativeSessionId) return "resume" + if (params.manifest.features.includes("chat")) return "start" + return "unknown" +} + +function buildNegotiatedCapabilities( + manifest: AgentRuntimeManifest, +): AgentRuntimeNegotiatedCapability[] { + return manifest.features.map((feature) => ({ + feature, + status: + manifest.availability === "unsupported" ? "unsupported" : "native", + })) +} + +export function buildAgentRuntimeLaunchPlan( + input: BuildAgentRuntimeLaunchPlanInput, +): AgentRuntimeLaunchPlan { + const manifest = input.manifest ?? getAgentRuntimeManifest(input.session.engineId) + const nativeSessionId = + input.nativeSessionId ?? input.session.nativeSessionId ?? null + const modelId = + input.modelId ?? + input.session.modelId ?? + manifest.defaultModelId ?? + null + + return { + version: 1, + runId: input.runId, + subChatId: input.session.subChatId, + chatId: input.session.chatId, + engineId: input.session.engineId, + modelId, + permissionMode: input.session.permissionMode, + cwd: input.session.cwd, + projectPath: input.session.projectPath ?? null, + runtimeConfigDir: input.session.runtimeConfigDir ?? null, + nativeSessionId, + nativeSessionStrategy: inferNativeSessionStrategy({ + manifest, + nativeSessionId, + explicit: input.nativeSessionStrategy, + }), + transport: input.transport ?? null, + providerRoute: input.providerRoute ?? null, + projectionStatus: input.projectionStatus ?? "unknown", + runtimeContextFingerprint: input.runtimeContextFingerprint ?? null, + mcpFingerprint: input.mcpFingerprint ?? null, + resultSubtype: input.resultSubtype ?? "running", + negotiatedCapabilities: buildNegotiatedCapabilities(manifest), + createdAt: resolveCreatedAt(input.now), + ...(input.metadata ? { metadata: input.metadata } : {}), + } +} + +function parseMetadata( + value: Record | string | null | undefined, +): Record { + if (isRecord(value)) return value + if (typeof value !== "string" || value.trim().length === 0) return {} + + try { + const parsed = JSON.parse(value) + return isRecord(parsed) ? parsed : {} + } catch { + return {} + } +} + +export function mergeAgentRuntimeLaunchPlanMetadata( + runtimeMetadata: Record | string | null | undefined, + launchPlan: AgentRuntimeLaunchPlan, +): Record { + const parsed = parseMetadata(runtimeMetadata) + const mossSessionControl = isRecord(parsed.mossSessionControl) + ? parsed.mossSessionControl + : {} + + return { + ...parsed, + mossSessionControl: { + ...mossSessionControl, + launchPlan, + launchPlanUpdatedAt: launchPlan.createdAt, + }, + } +} diff --git a/src/main/lib/agent-runtime/manifests.ts b/src/main/lib/agent-runtime/manifests.ts index 9fa6645f2..4d1b7137f 100644 --- a/src/main/lib/agent-runtime/manifests.ts +++ b/src/main/lib/agent-runtime/manifests.ts @@ -1,13 +1,13 @@ -import * as os from "os" -import * as path from "path" -import type { - AgentEngineId, - AgentRuntimeManifest, -} from "./types" +import * as os from "os"; +import * as path from "path"; +import type { AgentEngineId, AgentRuntimeManifest } from "./types"; -const home = os.homedir() +const home = os.homedir(); -export const AGENT_RUNTIME_MANIFESTS: Record = { +export const AGENT_RUNTIME_MANIFESTS: Record< + AgentEngineId, + AgentRuntimeManifest +> = { "claude-code": { id: "claude-code", label: "Claude Code", @@ -50,10 +50,12 @@ export const AGENT_RUNTIME_MANIFESTS: Record", }, - models: [ - { id: "custom-acp", label: "Custom ACP Default" }, - ], + models: [{ id: "custom-acp", label: "Custom ACP Default" }], notes: [ "Custom ACP is a governed external engine slot under Moss Unified Source.", "Moss provider, resource, and projection settings can be prepared now; session start remains disabled until a custom ACP endpoint or command adapter is configured.", "Shared skills, MCP, plugins, hooks, memory, and subagents are projected from .moss instead of maintained as a second real copy.", ], }, -} +}; -export function getAgentRuntimeManifest(engineId: AgentEngineId): AgentRuntimeManifest { - return AGENT_RUNTIME_MANIFESTS[engineId] +export function getAgentRuntimeManifest( + engineId: AgentEngineId, +): AgentRuntimeManifest { + return AGENT_RUNTIME_MANIFESTS[engineId]; } export function listAgentRuntimeManifests(): AgentRuntimeManifest[] { - return Object.values(AGENT_RUNTIME_MANIFESTS) + return Object.values(AGENT_RUNTIME_MANIFESTS); } diff --git a/src/main/lib/agent-runtime/native-thread-summary.ts b/src/main/lib/agent-runtime/native-thread-summary.ts new file mode 100644 index 000000000..a824fcf70 --- /dev/null +++ b/src/main/lib/agent-runtime/native-thread-summary.ts @@ -0,0 +1,515 @@ +import type { AgentRuntimeThreadReadResult } from "./types" + +export type NativeThreadConsistencyStatus = + | "consistent" + | "diverged" + | "unknown" + +export interface NativeThreadReadSummary { + status: AgentRuntimeThreadReadResult["status"] + threadId: string | null + includeTurns: boolean + turnCount?: number + firstTurnId?: string + lastTurnId?: string + turnIds?: string[] + turnIdsTruncated?: boolean + itemCount?: number + checkpointCount?: number + readyCheckpointCount?: number + checkpoints?: NativeThreadCheckpointSummary[] + checkpointsTruncated?: boolean + latestReadyCheckpoint?: NativeThreadCheckpointSummary + localUserTurnCount?: number + consistencyStatus?: NativeThreadConsistencyStatus + consistencyReason?: string + message?: string + updatedAt: string + bridge?: string + method?: string +} + +export type NativeThreadCheckpointStatus = "ready" | "missing" | "error" + +export interface NativeThreadCheckpointFileSummary { + path: string + kind?: string + additions: number + deletions: number +} + +export interface NativeThreadCheckpointSummary { + turnId: string + checkpointTurnCount: number + checkpointRef: string + status: NativeThreadCheckpointStatus + fileCount: number + additions: number + deletions: number + assistantMessageId?: string | null + completedAt?: string + files?: NativeThreadCheckpointFileSummary[] + filesTruncated?: boolean +} + +const DEFAULT_MAX_TURN_IDS = 64 +const DEFAULT_MAX_CHECKPOINTS = 64 +const DEFAULT_MAX_CHECKPOINT_FILES = 24 + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value) +} + +function cleanString(value: unknown): string | undefined { + if (typeof value !== "string") return undefined + const trimmed = value.trim() + return trimmed ? trimmed : undefined +} + +function readRecord(value: unknown, key: string): Record | null { + if (!isRecord(value)) return null + const nested = value[key] + return isRecord(nested) ? nested : null +} + +function readArray(value: unknown, key: string): unknown[] | null { + if (!isRecord(value)) return null + const nested = value[key] + return Array.isArray(nested) ? nested : null +} + +function readStringArray(value: unknown, key: string): string[] | undefined { + if (!isRecord(value)) return undefined + const nested = value[key] + if (!Array.isArray(nested)) return undefined + return nested.filter((entry): entry is string => typeof entry === "string") +} + +function readNativeThreadCheckpointStatus( + value: unknown, +): NativeThreadCheckpointStatus | undefined { + if (value === "ready" || value === "missing" || value === "error") { + return value + } + return undefined +} + +function readNonNegativeNumber(value: unknown): number | undefined { + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + return undefined + } + return value +} + +function parseMessageArray(value: unknown): unknown[] { + if (Array.isArray(value)) return value + if (typeof value !== "string") return [] + try { + const parsed = JSON.parse(value) + return Array.isArray(parsed) ? parsed : [] + } catch { + return [] + } +} + +export function countLocalTranscriptUserTurns(messages: unknown): number { + return parseMessageArray(messages).filter( + (message) => isRecord(message) && message.role === "user", + ).length +} + +function readTurnId(turn: unknown): string | undefined { + if (!isRecord(turn)) return undefined + return cleanString(turn.id) ?? cleanString(turn.turnId) +} + +function compactSummary>(summary: T): T { + return Object.fromEntries( + Object.entries(summary).filter(([, value]) => value !== undefined), + ) as T +} + +function summarizeNativeCheckpointFile( + file: unknown, +): NativeThreadCheckpointFileSummary | null { + if (!isRecord(file)) return null + const path = cleanString(file.path) + if (!path) return null + const kind = cleanString(file.kind) + return compactSummary({ + path, + ...(kind ? { kind } : {}), + additions: readNonNegativeNumber(file.additions) ?? 0, + deletions: readNonNegativeNumber(file.deletions) ?? 0, + }) +} + +function readCheckpointTurnCount(checkpoint: Record): number | undefined { + return ( + readNonNegativeNumber(checkpoint.checkpointTurnCount) ?? + readNonNegativeNumber(checkpoint.turnCount) + ) +} + +function checkpointOrder( + left: NativeThreadCheckpointSummary, + right: NativeThreadCheckpointSummary, +): number { + return left.checkpointTurnCount - right.checkpointTurnCount +} + +function summarizeNativeCheckpoint( + checkpoint: unknown, + options: { maxCheckpointFiles: number }, +): NativeThreadCheckpointSummary | null { + if (!isRecord(checkpoint)) return null + const turnId = cleanString(checkpoint.turnId) ?? cleanString(checkpoint.id) + const checkpointTurnCount = readCheckpointTurnCount(checkpoint) + const checkpointRef = cleanString(checkpoint.checkpointRef) + const status = readNativeThreadCheckpointStatus(checkpoint.status) + if (!turnId || checkpointTurnCount === undefined || !checkpointRef || !status) { + return null + } + + const rawFiles: unknown[] = Array.isArray(checkpoint.files) + ? checkpoint.files + : [] + const hasRawFiles = Array.isArray(checkpoint.files) + const allFiles = rawFiles + .map(summarizeNativeCheckpointFile) + .filter((file): file is NativeThreadCheckpointFileSummary => Boolean(file)) + const files = allFiles.slice(0, Math.max(0, options.maxCheckpointFiles)) + const fileCount = hasRawFiles + ? allFiles.length + : readNonNegativeNumber(checkpoint.fileCount) ?? allFiles.length + const additions = hasRawFiles + ? allFiles.reduce((sum, file) => sum + file.additions, 0) + : readNonNegativeNumber(checkpoint.additions) ?? 0 + const deletions = hasRawFiles + ? allFiles.reduce((sum, file) => sum + file.deletions, 0) + : readNonNegativeNumber(checkpoint.deletions) ?? 0 + const assistantMessageId = cleanString(checkpoint.assistantMessageId) + const completedAt = cleanString(checkpoint.completedAt) + + return compactSummary({ + turnId, + checkpointTurnCount, + checkpointRef, + status, + fileCount, + additions, + deletions, + ...(assistantMessageId + ? { assistantMessageId } + : checkpoint.assistantMessageId === null + ? { assistantMessageId: null } + : {}), + ...(completedAt ? { completedAt } : {}), + ...(files.length > 0 ? { files } : {}), + ...(allFiles.length > files.length ? { filesTruncated: true } : {}), + }) +} + +function summarizeNativeCheckpoints( + payload: unknown, + options: { maxCheckpoints: number; maxCheckpointFiles: number }, +): Pick< + NativeThreadReadSummary, + | "checkpointCount" + | "readyCheckpointCount" + | "checkpoints" + | "checkpointsTruncated" + | "latestReadyCheckpoint" +> { + const root = isRecord(payload) ? payload : {} + const thread = readRecord(root, "thread") ?? root + const rawCheckpoints = + readArray(thread, "checkpoints") ?? readArray(root, "checkpoints") + if (!rawCheckpoints) return {} + + const allCheckpoints = rawCheckpoints + .map((checkpoint) => + summarizeNativeCheckpoint(checkpoint, { + maxCheckpointFiles: options.maxCheckpointFiles, + }), + ) + .filter((checkpoint): checkpoint is NativeThreadCheckpointSummary => + Boolean(checkpoint), + ) + .sort(checkpointOrder) + const readyCheckpoints = allCheckpoints.filter( + (checkpoint) => checkpoint.status === "ready", + ) + const maxCheckpoints = Math.max(0, options.maxCheckpoints) + const retainedCheckpoints = + maxCheckpoints === 0 + ? [] + : allCheckpoints.slice(Math.max(0, allCheckpoints.length - maxCheckpoints)) + + return compactSummary({ + checkpointCount: allCheckpoints.length, + readyCheckpointCount: readyCheckpoints.length, + ...(retainedCheckpoints.length > 0 ? { checkpoints: retainedCheckpoints } : {}), + ...(allCheckpoints.length > retainedCheckpoints.length + ? { checkpointsTruncated: true } + : {}), + ...(readyCheckpoints.at(-1) + ? { latestReadyCheckpoint: readyCheckpoints.at(-1) } + : {}), + }) +} + +function readNativeCheckpointSummary( + value: unknown, +): NativeThreadCheckpointSummary | null { + return summarizeNativeCheckpoint(value, { + maxCheckpointFiles: DEFAULT_MAX_CHECKPOINT_FILES, + }) +} + +function readNativeCheckpointSummaries(value: unknown): NativeThreadCheckpointSummary[] { + if (!Array.isArray(value)) return [] + return value + .map(readNativeCheckpointSummary) + .filter((checkpoint): checkpoint is NativeThreadCheckpointSummary => + Boolean(checkpoint), + ) +} + +function resolveConsistency(params: { + status: AgentRuntimeThreadReadResult["status"] + turnCount?: number + localUserTurnCount?: number +}): { + consistencyStatus?: NativeThreadConsistencyStatus + consistencyReason?: string +} { + if (params.status !== "success") { + return { + consistencyStatus: "unknown", + consistencyReason: "Native thread read did not succeed.", + } + } + if (params.turnCount === undefined) { + return { + consistencyStatus: "unknown", + consistencyReason: "Native thread read did not include turn history.", + } + } + if (params.localUserTurnCount === undefined) { + return { + consistencyStatus: "unknown", + consistencyReason: "Local transcript turn count was not provided.", + } + } + if (params.turnCount === params.localUserTurnCount) { + return { + consistencyStatus: "consistent", + consistencyReason: "Native turns match local user turns.", + } + } + return { + consistencyStatus: "diverged", + consistencyReason: `Native turns (${params.turnCount}) differ from local user turns (${params.localUserTurnCount}).`, + } +} + +export function summarizeNativeThreadPayload( + payload: unknown, + options: { + maxTurnIds?: number + maxCheckpoints?: number + maxCheckpointFiles?: number + } = {}, +): { + threadId: string | null + turnCount?: number + firstTurnId?: string + lastTurnId?: string + turnIds?: string[] + turnIdsTruncated?: boolean + itemCount?: number + checkpointCount?: number + readyCheckpointCount?: number + checkpoints?: NativeThreadCheckpointSummary[] + checkpointsTruncated?: boolean + latestReadyCheckpoint?: NativeThreadCheckpointSummary +} { + const root = isRecord(payload) ? payload : {} + const thread = readRecord(root, "thread") ?? root + const turns = readArray(thread, "turns") ?? readArray(root, "turns") + const rootItems = readArray(thread, "items") ?? readArray(root, "items") + const turnItemCount = turns?.reduce((count, turn) => { + const items = readArray(turn, "items") + return count + (items?.length ?? 0) + }, 0) + const itemCount = + rootItems?.length ?? (typeof turnItemCount === "number" ? turnItemCount : undefined) + const maxTurnIds = Math.max(0, options.maxTurnIds ?? DEFAULT_MAX_TURN_IDS) + const allTurnIds = + turns?.map(readTurnId).filter((turnId): turnId is string => Boolean(turnId)) ?? + [] + const turnIds = allTurnIds.slice(0, maxTurnIds) + const checkpointSummary = summarizeNativeCheckpoints(payload, { + maxCheckpoints: options.maxCheckpoints ?? DEFAULT_MAX_CHECKPOINTS, + maxCheckpointFiles: + options.maxCheckpointFiles ?? DEFAULT_MAX_CHECKPOINT_FILES, + }) + + return { + threadId: + cleanString(thread.id) ?? + cleanString(root.threadId) ?? + cleanString(root.id) ?? + null, + ...(turns ? { turnCount: turns.length } : {}), + ...(allTurnIds[0] ? { firstTurnId: allTurnIds[0] } : {}), + ...(allTurnIds.at(-1) ? { lastTurnId: allTurnIds.at(-1) } : {}), + ...(turns && turnIds.length > 0 ? { turnIds } : {}), + ...(turns && allTurnIds.length > maxTurnIds + ? { turnIdsTruncated: true } + : {}), + ...(typeof itemCount === "number" ? { itemCount } : {}), + ...checkpointSummary, + } +} + +export function summarizeNativeThreadReadResult( + result: AgentRuntimeThreadReadResult, + options: { + includeTurns?: boolean + localMessages?: unknown + localUserTurnCount?: number + } = {}, +): NativeThreadReadSummary { + const payload = summarizeNativeThreadPayload(result.thread) + const metadata = isRecord(result.metadata) ? result.metadata : {} + const localUserTurnCount = + options.localUserTurnCount ?? + (options.localMessages !== undefined + ? countLocalTranscriptUserTurns(options.localMessages) + : undefined) + const consistency = resolveConsistency({ + status: result.status, + turnCount: payload.turnCount, + localUserTurnCount, + }) + + return compactSummary({ + status: result.status, + threadId: result.threadId ?? payload.threadId, + includeTurns: options.includeTurns ?? true, + ...(payload.turnCount !== undefined ? { turnCount: payload.turnCount } : {}), + ...(payload.firstTurnId !== undefined ? { firstTurnId: payload.firstTurnId } : {}), + ...(payload.lastTurnId !== undefined ? { lastTurnId: payload.lastTurnId } : {}), + ...(payload.turnIds !== undefined ? { turnIds: payload.turnIds } : {}), + ...(payload.turnIdsTruncated !== undefined + ? { turnIdsTruncated: payload.turnIdsTruncated } + : {}), + ...(payload.itemCount !== undefined ? { itemCount: payload.itemCount } : {}), + ...(payload.checkpointCount !== undefined + ? { checkpointCount: payload.checkpointCount } + : {}), + ...(payload.readyCheckpointCount !== undefined + ? { readyCheckpointCount: payload.readyCheckpointCount } + : {}), + ...(payload.checkpoints !== undefined ? { checkpoints: payload.checkpoints } : {}), + ...(payload.checkpointsTruncated !== undefined + ? { checkpointsTruncated: payload.checkpointsTruncated } + : {}), + ...(payload.latestReadyCheckpoint !== undefined + ? { latestReadyCheckpoint: payload.latestReadyCheckpoint } + : {}), + ...(localUserTurnCount !== undefined ? { localUserTurnCount } : {}), + ...consistency, + ...(result.message ? { message: result.message } : {}), + updatedAt: result.updatedAt, + bridge: cleanString(metadata.bridge), + method: cleanString(metadata.method), + }) +} + +export function readNativeThreadReadSummaryFromMetadata( + runtimeMetadata: string | null | undefined, +): NativeThreadReadSummary | null { + if (!runtimeMetadata) return null + + let parsed: unknown + try { + parsed = JSON.parse(runtimeMetadata) + } catch { + return null + } + + const control = readRecord(parsed, "mossSessionControl") + const summary = readRecord(control, "nativeThreadRead") + if (!summary) return null + + const rawStatus = cleanString(summary.status) + const updatedAt = cleanString(summary.updatedAt) + if ( + rawStatus !== "success" && + rawStatus !== "unsupported" && + rawStatus !== "error" + ) { + return null + } + if (!updatedAt) return null + const status: NativeThreadReadSummary["status"] = rawStatus + const consistencyStatus: NativeThreadConsistencyStatus | undefined = + summary.consistencyStatus === "consistent" || + summary.consistencyStatus === "diverged" || + summary.consistencyStatus === "unknown" + ? summary.consistencyStatus + : undefined + const consistencyReason = cleanString(summary.consistencyReason) + const checkpoints = readNativeCheckpointSummaries(summary.checkpoints) + const latestReadyCheckpoint = readNativeCheckpointSummary( + summary.latestReadyCheckpoint, + ) + + return compactSummary({ + status, + threadId: cleanString(summary.threadId) ?? null, + includeTurns: + typeof summary.includeTurns === "boolean" ? summary.includeTurns : true, + ...(typeof summary.turnCount === "number" + ? { turnCount: summary.turnCount } + : {}), + ...(cleanString(summary.firstTurnId) + ? { firstTurnId: cleanString(summary.firstTurnId) } + : {}), + ...(cleanString(summary.lastTurnId) + ? { lastTurnId: cleanString(summary.lastTurnId) } + : {}), + ...(readStringArray(summary, "turnIds") + ? { turnIds: readStringArray(summary, "turnIds") } + : {}), + ...(typeof summary.turnIdsTruncated === "boolean" + ? { turnIdsTruncated: summary.turnIdsTruncated } + : {}), + ...(typeof summary.itemCount === "number" + ? { itemCount: summary.itemCount } + : {}), + ...(typeof summary.checkpointCount === "number" + ? { checkpointCount: summary.checkpointCount } + : {}), + ...(typeof summary.readyCheckpointCount === "number" + ? { readyCheckpointCount: summary.readyCheckpointCount } + : {}), + ...(checkpoints.length > 0 ? { checkpoints } : {}), + ...(typeof summary.checkpointsTruncated === "boolean" + ? { checkpointsTruncated: summary.checkpointsTruncated } + : {}), + ...(latestReadyCheckpoint ? { latestReadyCheckpoint } : {}), + ...(typeof summary.localUserTurnCount === "number" + ? { localUserTurnCount: summary.localUserTurnCount } + : {}), + ...(consistencyStatus ? { consistencyStatus } : {}), + ...(consistencyReason ? { consistencyReason } : {}), + ...(cleanString(summary.message) ? { message: cleanString(summary.message) } : {}), + updatedAt, + bridge: cleanString(summary.bridge), + method: cleanString(summary.method), + }) as NativeThreadReadSummary +} diff --git a/src/main/lib/agent-runtime/process-registry.ts b/src/main/lib/agent-runtime/process-registry.ts new file mode 100644 index 000000000..bc67f1d3d --- /dev/null +++ b/src/main/lib/agent-runtime/process-registry.ts @@ -0,0 +1,196 @@ +import { + createAgentRuntimeRunId, + createUnsupportedAgentRuntimeControlResult, +} from "./runtime-run-ledger" +import type { + AgentRuntimeControlResult, + AgentRuntimeRunAction, + AgentRuntimeSessionRef, + AgentRuntimeStopRequest, + AgentRuntimeToolResultRequest, +} from "./types" + +export type AgentRuntimeProcessState = "running" | "stopping" + +export type AgentRuntimeToolResultSink = ( + request: AgentRuntimeToolResultRequest, +) => AgentRuntimeControlResult | Promise + +export type AgentRuntimeStopSink = ( + request: AgentRuntimeStopRequest, +) => void | Promise + +export interface AgentRuntimeProcessHandle { + runId: string + action: AgentRuntimeRunAction + session: AgentRuntimeSessionRef + abortController: AbortController + state: AgentRuntimeProcessState + createdAt: string + updatedAt: string + stopReason?: string + submitToolResult?: AgentRuntimeToolResultSink + onStop?: AgentRuntimeStopSink +} + +const processHandles = new Map() + +export function createAgentRuntimeProcessHandle(params: { + action: AgentRuntimeRunAction + session: AgentRuntimeSessionRef + runId?: string + now?: Date | string + submitToolResult?: AgentRuntimeToolResultSink + onStop?: AgentRuntimeStopSink +}): AgentRuntimeProcessHandle { + const createdAt = isoString(params.now) + return { + runId: + params.runId ?? + createAgentRuntimeRunId(params.session, params.action, Date.parse(createdAt)), + action: params.action, + session: params.session, + abortController: new AbortController(), + state: "running", + createdAt, + updatedAt: createdAt, + submitToolResult: params.submitToolResult, + onStop: params.onStop, + } +} + +export function registerAgentRuntimeProcessHandle( + handle: AgentRuntimeProcessHandle, +): AgentRuntimeProcessHandle { + processHandles.set(handle.runId, handle) + return handle +} + +export function getAgentRuntimeProcessHandle( + runId: string, +): AgentRuntimeProcessHandle | null { + return processHandles.get(runId) ?? null +} + +export function unregisterAgentRuntimeProcessHandle( + runId: string, +): AgentRuntimeProcessHandle | null { + const handle = getAgentRuntimeProcessHandle(runId) + if (!handle) return null + processHandles.delete(runId) + return handle +} + +export function stopAgentRuntimeProcess( + request: AgentRuntimeStopRequest, +): AgentRuntimeControlResult { + const handle = getAgentRuntimeProcessHandle(request.runId) + if (!handle) { + return { + ...createUnsupportedAgentRuntimeControlResult( + request, + "No active runtime process is registered for this run id.", + ), + status: "not-found", + } + } + + if (handle.session.engineId !== request.session.engineId) { + return { + runId: request.runId, + status: "error", + message: `Run ${request.runId} belongs to ${handle.session.engineId}, not ${request.session.engineId}.`, + updatedAt: new Date().toISOString(), + } + } + + const updatedAt = new Date().toISOString() + handle.state = "stopping" + handle.updatedAt = updatedAt + handle.stopReason = request.reason + try { + const stopResult = handle.onStop?.(request) + if (stopResult) { + void Promise.resolve(stopResult).catch(() => undefined) + } + } catch { + // Stop is best-effort. The abort signal below remains the authoritative local stop. + } + if (!handle.abortController.signal.aborted) { + handle.abortController.abort(request.reason ?? "Runtime stop requested.") + } + + return { + runId: request.runId, + status: "accepted", + message: "Runtime stop signal accepted.", + updatedAt, + metadata: { + engineId: handle.session.engineId, + subChatId: handle.session.subChatId, + state: handle.state, + reason: request.reason ?? null, + }, + } +} + +export async function submitAgentRuntimeToolResult( + request: AgentRuntimeToolResultRequest, +): Promise { + const handle = getAgentRuntimeProcessHandle(request.runId) + if (!handle) { + return { + ...createUnsupportedAgentRuntimeControlResult( + request, + "No active runtime process is registered for this run id.", + ), + status: "not-found", + } + } + + if (handle.session.engineId !== request.session.engineId) { + return { + runId: request.runId, + status: "error", + message: `Run ${request.runId} belongs to ${handle.session.engineId}, not ${request.session.engineId}.`, + updatedAt: new Date().toISOString(), + } + } + + if (!handle.submitToolResult) { + return createUnsupportedAgentRuntimeControlResult( + request, + "Active runtime process does not accept out-of-band tool result submission.", + ) + } + + try { + const result = await handle.submitToolResult(request) + handle.updatedAt = result.updatedAt + return result + } catch (error) { + const updatedAt = new Date().toISOString() + handle.updatedAt = updatedAt + return { + runId: request.runId, + status: "error", + message: error instanceof Error ? error.message : String(error), + updatedAt, + } + } +} + +export function listAgentRuntimeProcessHandles(): AgentRuntimeProcessHandle[] { + return [...processHandles.values()].sort((left, right) => + left.runId.localeCompare(right.runId), + ) +} + +export function clearAgentRuntimeProcessHandlesForTests(): void { + processHandles.clear() +} + +function isoString(value: Date | string | undefined): string { + if (!value) return new Date().toISOString() + return typeof value === "string" ? value : value.toISOString() +} diff --git a/src/main/lib/agent-runtime/provider-instances.ts b/src/main/lib/agent-runtime/provider-instances.ts new file mode 100644 index 000000000..d57f97ce3 --- /dev/null +++ b/src/main/lib/agent-runtime/provider-instances.ts @@ -0,0 +1,114 @@ +import { z } from "zod" +import { + AGENT_ENGINE_IDS, + type AgentEngineId, + type AgentRuntimeSessionRef, +} from "./types" + +export type AgentRuntimeProviderInstanceId = string & { + readonly __agentRuntimeProviderInstanceId: unique symbol +} + +export interface AgentRuntimeModelSelection { + instanceId: AgentRuntimeProviderInstanceId + modelId: string + options?: Record +} + +export const DEFAULT_AGENT_RUNTIME_PROVIDER_INSTANCE_IDS: Record< + AgentEngineId, + AgentRuntimeProviderInstanceId +> = { + "claude-code": "claude-code" as AgentRuntimeProviderInstanceId, + codex: "codex" as AgentRuntimeProviderInstanceId, + hermes: "hermes" as AgentRuntimeProviderInstanceId, + "custom-acp": "custom-acp" as AgentRuntimeProviderInstanceId, +} + +const agentEngineIdSet = new Set(AGENT_ENGINE_IDS) + +const legacyModelSelectionSchema = z.object({ + provider: z.unknown().optional(), + engineId: z.unknown().optional(), + instanceId: z.unknown().optional(), + model: z.unknown().optional(), + modelId: z.unknown().optional(), + options: z.record(z.string(), z.unknown()).optional(), +}).passthrough() + +function cleanString(value: unknown): string | null { + if (typeof value !== "string") return null + const trimmed = value.trim() + return trimmed.length > 0 ? trimmed : null +} + +export function isAgentEngineId(value: unknown): value is AgentEngineId { + return typeof value === "string" && agentEngineIdSet.has(value) +} + +export function defaultProviderInstanceIdForEngine( + engineId: AgentEngineId, +): AgentRuntimeProviderInstanceId { + return DEFAULT_AGENT_RUNTIME_PROVIDER_INSTANCE_IDS[engineId] +} + +export function makeAgentRuntimeProviderInstanceId( + value: string, +): AgentRuntimeProviderInstanceId { + const cleaned = cleanString(value) + if (!cleaned) { + throw new Error("Agent runtime provider instance id must be a non-empty string.") + } + return cleaned as AgentRuntimeProviderInstanceId +} + +export function normalizeAgentRuntimeModelSelection( + value: unknown, + fallback?: { + engineId?: AgentEngineId | null + modelId?: string | null + }, +): AgentRuntimeModelSelection | null { + const parsed = legacyModelSelectionSchema.safeParse(value) + const source = parsed.success ? parsed.data : {} + const legacyEngine = + isAgentEngineId(source.engineId) + ? source.engineId + : isAgentEngineId(source.provider) + ? source.provider + : fallback?.engineId ?? null + const instanceId = + cleanString(source.instanceId) ?? + (legacyEngine ? defaultProviderInstanceIdForEngine(legacyEngine) : null) + const modelId = + cleanString(source.modelId) ?? + cleanString(source.model) ?? + cleanString(fallback?.modelId) + + if (!instanceId || !modelId) return null + + return { + instanceId: makeAgentRuntimeProviderInstanceId(instanceId), + modelId, + ...(source.options ? { options: source.options } : {}), + } +} + +export function resolveAgentRuntimeProviderInstanceId( + session: Pick< + AgentRuntimeSessionRef, + "engineId" | "modelId" | "metadata" | "providerInstanceId" | "modelSelection" + >, +): AgentRuntimeProviderInstanceId { + const explicit = cleanString(session.providerInstanceId) + if (explicit) return makeAgentRuntimeProviderInstanceId(explicit) + + const modelSelection = normalizeAgentRuntimeModelSelection( + session.modelSelection ?? session.metadata?.modelSelection, + { + engineId: session.engineId, + modelId: session.modelId, + }, + ) + return modelSelection?.instanceId ?? defaultProviderInstanceIdForEngine(session.engineId) +} diff --git a/src/main/lib/agent-runtime/provider-runtime-contract.ts b/src/main/lib/agent-runtime/provider-runtime-contract.ts new file mode 100644 index 000000000..74b52b251 --- /dev/null +++ b/src/main/lib/agent-runtime/provider-runtime-contract.ts @@ -0,0 +1,820 @@ +import { z } from "zod" +import type { + AgentEngineId, + AgentRuntimeConversationBlock, + AgentRuntimeStreamEvent, +} from "./types" +import { + isAgentEngineId, + makeAgentRuntimeProviderInstanceId, + type AgentRuntimeProviderInstanceId, +} from "./provider-instances" + +export const PROVIDER_RUNTIME_EVENT_TYPES = [ + "session.started", + "session.configured", + "session.state.changed", + "session.exited", + "thread.started", + "thread.state.changed", + "thread.metadata.updated", + "thread.token-usage.updated", + "thread.realtime.started", + "thread.realtime.item-added", + "thread.realtime.audio.delta", + "thread.realtime.error", + "thread.realtime.closed", + "turn.started", + "turn.completed", + "turn.aborted", + "turn.plan.updated", + "turn.proposed.delta", + "turn.proposed.completed", + "turn.diff.updated", + "item.started", + "item.updated", + "item.completed", + "content.delta", + "request.opened", + "request.resolved", + "user-input.requested", + "user-input.resolved", + "task.started", + "task.progress", + "task.completed", + "hook.started", + "hook.progress", + "hook.completed", + "tool.progress", + "tool.summary", + "tool.denied", + "auth.status", + "account.updated", + "account.rate-limits.updated", + "mcp.status.updated", + "mcp.oauth.completed", + "app.list.updated", + "model.rerouted", + "config.updated", + "config.warning", + "deprecation.notice", + "files.persisted", + "runtime.warning", + "runtime.error", +] as const + +export type ProviderRuntimeEventType = + (typeof PROVIDER_RUNTIME_EVENT_TYPES)[number] + +export const CANONICAL_RUNTIME_ITEM_TYPES = [ + "user_message", + "assistant_message", + "reasoning", + "plan", + "command_execution", + "file_change", + "mcp_tool_call", + "dynamic_tool_call", + "collab_agent_tool_call", + "web_search", + "image_view", + "review_entered", + "review_exited", + "context_compaction", + "error", + "unknown", +] as const + +export type CanonicalRuntimeItemType = + (typeof CANONICAL_RUNTIME_ITEM_TYPES)[number] + +export const CANONICAL_RUNTIME_REQUEST_TYPES = [ + "command_execution_approval", + "file_read_approval", + "file_change_approval", + "apply_patch_approval", + "exec_command_approval", + "tool_user_input", + "dynamic_tool_call", + "auth_tokens_refresh", + "unknown", +] as const + +export type CanonicalRuntimeRequestType = + (typeof CANONICAL_RUNTIME_REQUEST_TYPES)[number] + +const providerRuntimeEventSchema = z.object({ + type: z.enum(PROVIDER_RUNTIME_EVENT_TYPES), + eventId: z.string().trim().min(1).optional(), + provider: z.string().trim().min(1).optional(), + engineId: z.string().trim().min(1).optional(), + providerInstanceId: z.string().trim().min(1).optional(), + subChatId: z.string().trim().min(1).optional(), + chatId: z.string().trim().min(1).optional(), + threadId: z.string().trim().min(1).optional(), + nativeSessionId: z.string().trim().min(1).nullable().optional(), + turnId: z.string().trim().min(1).optional(), + itemId: z.string().trim().min(1).optional(), + requestId: z.string().trim().min(1).optional(), + createdAt: z.string().trim().min(1).optional(), + providerRefs: z.record(z.string(), z.unknown()).optional(), + payload: z.record(z.string(), z.unknown()).optional(), + raw: z.unknown().optional(), +}).passthrough() + +export interface ProviderRuntimeEvent { + type: ProviderRuntimeEventType + eventId?: string + provider?: string + engineId?: AgentEngineId + providerInstanceId?: AgentRuntimeProviderInstanceId + subChatId?: string + chatId?: string + threadId?: string + nativeSessionId?: string | null + turnId?: string + itemId?: string + requestId?: string + createdAt?: string + providerRefs?: Record + payload: Record + raw?: unknown +} + +function cleanString(value: unknown): string | undefined { + if (typeof value !== "string") return undefined + const trimmed = value.trim() + return trimmed.length > 0 ? trimmed : undefined +} + +function cleanNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined +} + +function cleanBoolean(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined +} + +function engineIdFromProvider(provider: unknown): AgentEngineId | undefined { + if (isAgentEngineId(provider)) return provider + if (provider === "claudeAgent" || provider === "claude-code") return "claude-code" + if (provider === "codex") return "codex" + if (provider === "hermes") return "hermes" + return undefined +} + +function compactRecord>(value: T): T { + return Object.fromEntries( + Object.entries(value).filter(([, entry]) => entry !== undefined), + ) as T +} + +function eventBlockId( + event: Pick< + ProviderRuntimeEvent, + "eventId" | "threadId" | "turnId" | "itemId" | "requestId" + >, +): string { + return ( + event.requestId ?? + event.itemId ?? + event.turnId ?? + event.threadId ?? + event.eventId ?? + "runtime-event" + ) +} + +function payloadBlockId( + event: ProviderRuntimeEvent, + keys: readonly string[], +): string { + for (const key of keys) { + const value = cleanString(event.payload[key]) + if (value) return value + } + return eventBlockId(event) +} + +function normalizeItemStatus(value: unknown): "running" | "completed" | "failed" | undefined { + if (value === "inProgress" || value === "running" || value === "queued") return "running" + if (value === "completed" || value === "declined") return "completed" + if (value === "failed" || value === "error") return "failed" + return undefined +} + +function runtimeItemTypeToBlock( + itemType: unknown, +): Pick & Record { + switch (itemType) { + case "command_execution": + return { + type: "exec", + command: "", + executionStatus: "running", + parsedCmd: { type: "unknown", isFinished: false }, + } + case "file_change": + return { type: "patch", toolName: "Edit" } + case "mcp_tool_call": + return { type: "mcp-tool-call", server: "mcp", tool: "tool", callId: "" } + case "dynamic_tool_call": + return { type: "dynamic-tool-call", toolName: "dynamic-tool" } + case "plan": + return { type: "proposed-plan" } + case "context_compaction": + return { type: "context-compaction" } + case "error": + return { type: "status", level: "error" } + default: + return { type: "status", level: "info" } + } +} + +function makeConversationBlock( + event: ProviderRuntimeEvent, + base: Record, +): AgentRuntimeConversationBlock { + return ({ + id: eventBlockId(event), + turnId: event.turnId, + status: normalizeItemStatus(base.status) ?? "running", + ...base, + metadata: { + providerRuntimeEventType: event.type, + ...(event.provider ? { provider: event.provider } : {}), + ...(event.providerInstanceId ? { providerInstanceId: event.providerInstanceId } : {}), + ...(event.engineId ? { engineId: event.engineId } : {}), + ...(event.providerRefs ? { providerRefs: event.providerRefs } : {}), + }, + } as unknown) as AgentRuntimeConversationBlock +} + +function turnStateToResultSubtype( + state: unknown, +): "success" | "error" | "cancelled" { + if (state === "completed") return "success" + if (state === "cancelled" || state === "interrupted") return "cancelled" + return "error" +} + +function hookOutcomeToStatus(value: unknown): "completed" | "failed" | "interrupted" { + if (value === "success" || value === "completed") return "completed" + if (value === "cancelled" || value === "canceled" || value === "stopped") { + return "interrupted" + } + return "failed" +} + +function realtimeBlockId(event: ProviderRuntimeEvent): string { + return `realtime:${ + event.threadId ?? + cleanString(event.payload.threadId) ?? + event.nativeSessionId ?? + event.chatId ?? + event.subChatId ?? + cleanString(event.payload.realtimeSessionId) ?? + eventBlockId(event) + }` +} + +export function parseProviderRuntimeEvent(value: unknown): ProviderRuntimeEvent | null { + const parsed = providerRuntimeEventSchema.safeParse(value) + if (!parsed.success) return null + const engineId = + engineIdFromProvider(parsed.data.engineId) ?? + engineIdFromProvider(parsed.data.provider) + const providerInstanceId = parsed.data.providerInstanceId + ? makeAgentRuntimeProviderInstanceId(parsed.data.providerInstanceId) + : undefined + + return { + type: parsed.data.type, + ...(parsed.data.eventId ? { eventId: parsed.data.eventId } : {}), + ...(parsed.data.provider ? { provider: parsed.data.provider } : {}), + ...(engineId ? { engineId } : {}), + ...(providerInstanceId ? { providerInstanceId } : {}), + ...(parsed.data.subChatId ? { subChatId: parsed.data.subChatId } : {}), + ...(parsed.data.chatId ? { chatId: parsed.data.chatId } : {}), + ...(parsed.data.threadId ? { threadId: parsed.data.threadId } : {}), + ...(parsed.data.nativeSessionId !== undefined + ? { nativeSessionId: parsed.data.nativeSessionId } + : {}), + ...(parsed.data.turnId ? { turnId: parsed.data.turnId } : {}), + ...(parsed.data.itemId ? { itemId: parsed.data.itemId } : {}), + ...(parsed.data.requestId ? { requestId: parsed.data.requestId } : {}), + ...(parsed.data.createdAt ? { createdAt: parsed.data.createdAt } : {}), + ...(parsed.data.providerRefs ? { providerRefs: parsed.data.providerRefs } : {}), + payload: parsed.data.payload ?? {}, + ...(parsed.data.raw !== undefined ? { raw: parsed.data.raw } : {}), + } +} + +export function providerRuntimeEventToStreamEvent( + value: unknown, +): AgentRuntimeStreamEvent | null { + const event = parseProviderRuntimeEvent(value) + if (!event) return null + + switch (event.type) { + case "content.delta": { + const delta = typeof event.payload.delta === "string" ? event.payload.delta : "" + return delta ? { type: "text", text: delta } : null + } + case "thread.token-usage.updated": { + const usage = event.payload.usage + if (!usage || typeof usage !== "object") return null + const record = usage as Record + const inputTokens = cleanNumber(record.inputTokens) + const outputTokens = cleanNumber(record.outputTokens) + const usedTokens = cleanNumber(record.usedTokens) + const totalTokens = usedTokens ?? cleanNumber(record.totalTokens) + const maxTokens = cleanNumber(record.maxTokens) + return compactRecord({ + type: "usage" as const, + inputTokens, + outputTokens, + totalTokens, + modelContextWindow: maxTokens ?? cleanNumber(record.modelContextWindow), + usedTokens, + totalProcessedTokens: cleanNumber(record.totalProcessedTokens), + maxTokens, + cachedInputTokens: cleanNumber(record.cachedInputTokens), + reasoningOutputTokens: cleanNumber(record.reasoningOutputTokens), + lastUsedTokens: cleanNumber(record.lastUsedTokens), + lastInputTokens: cleanNumber(record.lastInputTokens), + lastCachedInputTokens: cleanNumber(record.lastCachedInputTokens), + lastOutputTokens: cleanNumber(record.lastOutputTokens), + lastReasoningOutputTokens: cleanNumber(record.lastReasoningOutputTokens), + toolUses: cleanNumber(record.toolUses), + durationMs: cleanNumber(record.durationMs), + compactsAutomatically: cleanBoolean(record.compactsAutomatically), + }) + } + case "thread.state.changed": { + const state = cleanString(event.payload.state) ?? cleanString(event.payload.status) + if (state !== "compacted") return null + const detail = + event.payload.detail && typeof event.payload.detail === "object" + ? (event.payload.detail as Record) + : {} + return { + type: "conversation-block", + block: makeConversationBlock(event, { + id: + event.eventId ?? + `context-compaction:${event.threadId ?? event.turnId ?? "runtime-event"}`, + type: "context-compaction", + title: "Context compacted", + summary: cleanString(event.payload.reason) ?? "Context compacted", + input: event.payload, + previousInputTokens: cleanNumber(detail.previousInputTokens), + nextInputTokens: cleanNumber(detail.nextInputTokens), + droppedMessages: cleanNumber(detail.droppedMessages), + status: "completed", + }), + } + } + case "turn.completed": { + return { + type: "finish", + nativeSessionId: event.nativeSessionId ?? null, + resultSubtype: turnStateToResultSubtype(event.payload.state), + } + } + case "turn.aborted": { + return { + type: "finish", + nativeSessionId: event.nativeSessionId ?? null, + resultSubtype: "cancelled", + } + } + case "turn.proposed.delta": { + const delta = typeof event.payload.delta === "string" ? event.payload.delta : "" + return delta + ? { + type: "conversation-block-update", + id: eventBlockId(event), + patch: { + type: "proposed-plan", + status: "running", + input: { delta }, + } as Partial, + } + : null + } + case "turn.proposed.completed": { + const planMarkdown = cleanString(event.payload.planMarkdown) + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "proposed-plan", + plan: planMarkdown ? { markdown: planMarkdown } : event.payload, + output: event.payload, + status: "completed", + }), + } + } + case "session.state.changed": { + const state = cleanString(event.payload.state) ?? "unknown" + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: state === "error" ? "error" : "info", + title: "Codex app-server", + message: + cleanString(event.payload.reason) ?? + cleanString(event.payload.message) ?? + `Session ${state}.`, + data: event.payload, + status: state === "starting" ? "running" : "completed", + }), + } + } + case "thread.realtime.started": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + id: realtimeBlockId(event), + type: "realtime-state", + title: "Realtime voice", + mode: "voice-only", + input: event.payload, + status: "running", + }), + } + } + case "thread.realtime.item-added": + case "thread.realtime.audio.delta": { + return { + type: "conversation-block-update", + id: realtimeBlockId(event), + patch: { + status: "running", + output: event.payload, + } as Partial, + } + } + case "thread.realtime.error": { + return { + type: "conversation-block-update", + id: realtimeBlockId(event), + patch: { + status: "failed", + summary: cleanString(event.payload.message) ?? "Realtime error", + output: event.payload, + } as Partial, + } + } + case "thread.realtime.closed": { + return { + type: "conversation-block-update", + id: realtimeBlockId(event), + patch: { + status: "completed", + summary: cleanString(event.payload.reason), + output: event.payload, + } as Partial, + } + } + case "runtime.error": { + return { + type: "error", + message: + cleanString(event.payload.message) ?? + cleanString(event.payload.error) ?? + "Provider runtime error.", + } + } + case "auth.status": { + const error = cleanString(event.payload.error) + return error ? { type: "auth-error", message: error } : null + } + case "account.updated": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: "info", + title: "Account updated", + message: "Codex account state updated.", + data: event.payload, + status: "completed", + }), + } + } + case "account.rate-limits.updated": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: "info", + title: "Rate limits updated", + message: "Codex rate limit state updated.", + data: event.payload, + status: "completed", + }), + } + } + case "mcp.status.updated": { + const status = cleanString(event.payload.status) ?? "updated" + const serverName = cleanString(event.payload.name) + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: status === "failed" ? "error" : "info", + title: serverName ? `MCP ${serverName}` : "MCP server", + message: + cleanString(event.payload.error) ?? `MCP server status: ${status}.`, + data: event.payload, + status: status === "starting" ? "running" : "completed", + }), + } + } + case "mcp.oauth.completed": { + const serverName = cleanString(event.payload.name) + const success = event.payload.success === true + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: success ? "info" : "error", + title: serverName ? `MCP OAuth ${serverName}` : "MCP OAuth", + message: + cleanString(event.payload.error) ?? + (success ? "MCP OAuth completed." : "MCP OAuth failed."), + data: event.payload, + status: "completed", + }), + } + } + case "app.list.updated": { + const apps = Array.isArray(event.payload.apps) + ? event.payload.apps + : undefined + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: "info", + title: "Apps updated", + message: + typeof apps?.length === "number" + ? `${apps.length} app${apps.length === 1 ? "" : "s"} available.` + : "App list updated.", + data: event.payload, + status: "completed", + }), + } + } + case "config.updated": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: "info", + title: cleanString(event.payload.title) ?? "Config updated", + message: + cleanString(event.payload.message) ?? + "Codex configuration state updated.", + data: event.payload, + status: "completed", + }), + } + } + case "model.rerouted": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "model-reroute", + fromModelId: cleanString(event.payload.fromModel), + toModelId: cleanString(event.payload.toModel), + reason: cleanString(event.payload.reason), + status: "completed", + }), + } + } + case "turn.plan.updated": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "proposed-plan", + plan: event.payload.plan, + input: event.payload, + status: "running", + }), + } + } + case "request.opened": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "permission-request", + input: event.payload, + status: "running", + }), + } + } + case "request.resolved": { + return { + type: "conversation-block-update", + id: eventBlockId(event), + patch: { + status: event.payload.decision === "decline" ? "failed" : "completed", + output: event.payload, + } as Partial, + } + } + case "user-input.requested": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "user-input", + input: event.payload, + status: "running", + }), + } + } + case "user-input.resolved": { + return { + type: "conversation-block-update", + id: eventBlockId(event), + patch: { + status: "completed", + output: event.payload, + } as Partial, + } + } + case "item.started": + case "item.updated": + case "item.completed": { + const status = + event.type === "item.completed" + ? "completed" + : normalizeItemStatus(event.payload.status) ?? "running" + const blockBase = runtimeItemTypeToBlock(event.payload.itemType) + return { + type: "conversation-block", + block: makeConversationBlock(event, { + ...blockBase, + title: cleanString(event.payload.title), + summary: cleanString(event.payload.detail), + input: event.payload.data, + status, + }), + } + } + case "hook.started": { + const hookName = cleanString(event.payload.hookName) ?? "hook" + const hookEvent = cleanString(event.payload.hookEvent) + return { + type: "conversation-block", + block: makeConversationBlock(event, { + id: payloadBlockId(event, ["hookId"]), + type: "status", + level: "info", + title: `Hook ${hookName}`, + message: hookEvent ? `${hookEvent} started.` : "Hook started.", + data: event.payload, + status: "running", + }), + } + } + case "hook.progress": { + const output = + cleanString(event.payload.output) ?? + cleanString(event.payload.stdout) ?? + cleanString(event.payload.stderr) + return { + type: "conversation-block-update", + id: payloadBlockId(event, ["hookId"]), + patch: { + status: "running", + ...(output ? { summary: output } : {}), + output: event.payload, + } as Partial, + } + } + case "hook.completed": { + const output = + cleanString(event.payload.output) ?? + cleanString(event.payload.stdout) ?? + cleanString(event.payload.stderr) + return { + type: "conversation-block-update", + id: payloadBlockId(event, ["hookId"]), + patch: { + status: hookOutcomeToStatus(event.payload.outcome ?? event.payload.status), + ...(output ? { summary: output } : {}), + output: event.payload, + } as Partial, + } + } + case "tool.progress": { + const summary = cleanString(event.payload.summary) + return { + type: "conversation-block-update", + id: payloadBlockId(event, ["toolUseId"]), + patch: { + status: "running", + ...(summary ? { summary } : {}), + output: event.payload, + } as Partial, + } + } + case "tool.summary": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: "info", + title: "Tool summary", + message: cleanString(event.payload.summary), + data: event.payload, + status: "completed", + }), + } + } + case "deprecation.notice": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: "warning", + title: cleanString(event.payload.summary) ?? "Deprecation notice", + message: cleanString(event.payload.details), + data: event.payload, + status: "completed", + }), + } + } + case "tool.denied": { + const toolUseId = cleanString(event.payload.toolUseId) + const toolName = cleanString(event.payload.toolName) ?? "Tool" + const reason = cleanString(event.payload.reason) + if (toolUseId) { + return { + type: "conversation-block-update", + id: toolUseId, + patch: { + status: "failed", + summary: reason ?? `${toolName} denied.`, + output: event.payload, + } as Partial, + } + } + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: "warning", + title: `${toolName} denied`, + message: reason, + data: event.payload, + status: "completed", + }), + } + } + case "files.persisted": { + const files = Array.isArray(event.payload.files) + ? event.payload.files + : [] + const failed = Array.isArray(event.payload.failed) + ? event.payload.failed + : [] + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: failed.length > 0 ? "warning" : "info", + title: "Files persisted", + message: + failed.length > 0 + ? `${files.length} file${files.length === 1 ? "" : "s"} persisted, ${failed.length} failed.` + : `${files.length} file${files.length === 1 ? "" : "s"} persisted.`, + data: event.payload, + status: "completed", + }), + } + } + case "runtime.warning": + case "config.warning": { + return { + type: "conversation-block", + block: makeConversationBlock(event, { + type: "status", + level: "warning", + title: cleanString(event.payload.summary), + message: + cleanString(event.payload.message) ?? cleanString(event.payload.details), + data: event.payload, + status: "completed", + }), + } + } + default: + return null + } +} diff --git a/src/main/lib/agent-runtime/runtime-receipt-bus.ts b/src/main/lib/agent-runtime/runtime-receipt-bus.ts new file mode 100644 index 000000000..e18745333 --- /dev/null +++ b/src/main/lib/agent-runtime/runtime-receipt-bus.ts @@ -0,0 +1,110 @@ +import type { + AgentEngineId, + AgentRuntimeRunReceipt, + AgentRuntimeRunStatus, +} from "./types" + +export type AgentRuntimeReceiptSource = + | "agent-runtime" + | "mobile-gateway" + | "codex-app-server" + | string + +interface AgentRuntimeReceiptBase { + sequence: number + createdAt: string + runId: string + engineId: AgentEngineId + chatId: string + subChatId: string + source?: AgentRuntimeReceiptSource + metadata?: Record +} + +export interface AgentRuntimeRunStartedReceipt + extends AgentRuntimeReceiptBase { + type: "runtime.run.started" +} + +export interface AgentRuntimeTurnProcessingQuiescedReceipt + extends AgentRuntimeReceiptBase { + type: "turn.processing.quiesced" + status: Exclude + resultSubtype: AgentRuntimeRunReceipt["resultSubtype"] + latestSequence?: number | null + eventCount: number + error?: string | null +} + +export type AgentRuntimeReceipt = + | AgentRuntimeRunStartedReceipt + | AgentRuntimeTurnProcessingQuiescedReceipt + +export type AgentRuntimeReceiptInput = + | Omit + | Omit + +export interface AgentRuntimeReceiptBus { + publish(receipt: AgentRuntimeReceiptInput): AgentRuntimeReceipt +} + +export interface AgentRuntimeReceiptWaitOptions { + timeoutMs?: number +} + +export interface AgentRuntimeReceiptBusForTests + extends AgentRuntimeReceiptBus { + listReceipts(): AgentRuntimeReceipt[] + waitForReceipt( + predicate: (receipt: AgentRuntimeReceipt) => boolean, + options?: AgentRuntimeReceiptWaitOptions, + ): Promise +} + +export function createNoopAgentRuntimeReceiptBus(): AgentRuntimeReceiptBus { + return { + publish(receipt) { + return { ...receipt, sequence: 0 } as AgentRuntimeReceipt + }, + } +} + +export function createInMemoryAgentRuntimeReceiptBus(): AgentRuntimeReceiptBusForTests { + const receipts: AgentRuntimeReceipt[] = [] + const subscribers = new Set<(receipt: AgentRuntimeReceipt) => void>() + let nextSequence = 1 + + return { + publish(receipt) { + const stored = { + ...receipt, + sequence: nextSequence++, + } as AgentRuntimeReceipt + receipts.push(stored) + for (const subscriber of subscribers) subscriber(stored) + return stored + }, + listReceipts() { + return [...receipts] + }, + waitForReceipt(predicate, options = {}) { + const existing = receipts.find(predicate) + if (existing) return Promise.resolve(existing) + + const timeoutMs = options.timeoutMs ?? 1000 + return new Promise((resolve) => { + let timeout: ReturnType | null = null + const complete = (receipt: AgentRuntimeReceipt | null) => { + if (timeout) clearTimeout(timeout) + subscribers.delete(onReceipt) + resolve(receipt) + } + const onReceipt = (receipt: AgentRuntimeReceipt) => { + if (predicate(receipt)) complete(receipt) + } + subscribers.add(onReceipt) + timeout = setTimeout(() => complete(null), timeoutMs) + }) + }, + } +} diff --git a/src/main/lib/agent-runtime/runtime-run-ledger.ts b/src/main/lib/agent-runtime/runtime-run-ledger.ts new file mode 100644 index 000000000..c39788a0c --- /dev/null +++ b/src/main/lib/agent-runtime/runtime-run-ledger.ts @@ -0,0 +1,422 @@ +import type { + AgentRuntimeControlResult, + AgentRuntimeRunAction, + AgentRuntimeRunReceipt, + AgentRuntimeRunStatus, + AgentRuntimeSessionRef, + AgentRuntimeStartRequest, + AgentRuntimeStopRequest, + AgentRuntimeStreamEvent, + AgentRuntimeToolResultRequest, +} from "./types" + +export type AgentRuntimeLedgerEventKind = + | "run-started" + | "stream-event" + | "tool-result-submitted" + | "run-stopped" + | "run-finished" + +export interface AgentRuntimeLedgerEvent { + id: string + runId: string + engineId: AgentRuntimeSessionRef["engineId"] + kind: AgentRuntimeLedgerEventKind + sequence: number + createdAt: string + commandId?: string | null + causationEventId?: string | null + correlationId?: string | null + metadata?: Record + payload?: Record +} + +export type AgentRuntimeCommandReceiptStatus = "accepted" | "rejected" + +export interface AgentRuntimeCommandReceipt { + commandId: string + runId: string + engineId: AgentRuntimeSessionRef["engineId"] + acceptedAt: string + resultSequence: number + status: AgentRuntimeCommandReceiptStatus + error?: string | null +} + +export type AgentRuntimeLedgerAppendEvent = Omit< + AgentRuntimeLedgerEvent, + "id" | "createdAt" | "sequence" +> & + Partial> + +export interface AgentRuntimeRunLedgerSnapshot { + runId: string + receipt: AgentRuntimeRunReceipt + events: AgentRuntimeLedgerEvent[] +} + +export interface AgentRuntimeRunReplayOptions { + fromSequenceExclusive?: number + limit?: number +} + +export interface AgentRuntimeRunProjection { + runId: string + engineId: AgentRuntimeSessionRef["engineId"] + status: AgentRuntimeRunStatus + resultSubtype: AgentRuntimeRunReceipt["resultSubtype"] + startedAt: string + updatedAt: string + completedAt?: string | null + nativeSessionId?: string | null + eventCount: number + eventKinds: Partial> + latestEventSequence?: number + latestEventKind?: AgentRuntimeLedgerEventKind + latestEventAt?: string + commandIds: string[] + correlationIds: string[] + causationEventIds: string[] + errors: string[] +} + +export interface AgentRuntimeRunLedger { + upsertReceipt(receipt: AgentRuntimeRunReceipt): AgentRuntimeRunReceipt + upsertCommandReceipt( + receipt: AgentRuntimeCommandReceipt, + ): AgentRuntimeCommandReceipt + getCommandReceipt(commandId: string): AgentRuntimeCommandReceipt | null + listCommandReceipts(): AgentRuntimeCommandReceipt[] + appendEvent(event: AgentRuntimeLedgerAppendEvent): AgentRuntimeLedgerEvent + finishRun(params: { + runId: string + status: Exclude + resultSubtype?: AgentRuntimeRunReceipt["resultSubtype"] + commandId?: string | null + causationEventId?: string | null + correlationId?: string | null + error?: string + metadata?: Record + }): AgentRuntimeRunReceipt | null + snapshot(runId: string): AgentRuntimeRunLedgerSnapshot | null + replayEvents( + runId: string, + options?: AgentRuntimeRunReplayOptions, + ): AgentRuntimeLedgerEvent[] | null + list(): AgentRuntimeRunLedgerSnapshot[] +} + +export function createAgentRuntimeRunId( + session: Pick, + action: AgentRuntimeRunAction, + now = Date.now(), +): string { + return `${session.engineId}:${session.subChatId}:${action}:${now}` +} + +export function createAgentRuntimeRunReceipt(params: { + runId?: string + action: AgentRuntimeRunAction + session: AgentRuntimeSessionRef + status?: AgentRuntimeRunStatus + nativeSessionId?: string | null + resultSubtype?: AgentRuntimeRunReceipt["resultSubtype"] + now?: Date | string + completedAt?: Date | string | null + error?: string + metadata?: Record +}): AgentRuntimeRunReceipt { + const now = isoString(params.now) + return { + version: 1, + runId: params.runId ?? createAgentRuntimeRunId(params.session, params.action), + action: params.action, + engineId: params.session.engineId, + subChatId: params.session.subChatId, + chatId: params.session.chatId, + status: params.status ?? "running", + nativeSessionId: params.nativeSessionId ?? params.session.nativeSessionId ?? null, + resultSubtype: params.resultSubtype ?? null, + startedAt: now, + updatedAt: now, + completedAt: params.completedAt === undefined + ? null + : params.completedAt === null + ? null + : isoString(params.completedAt), + ...(params.error ? { error: params.error } : {}), + ...(params.metadata ? { metadata: params.metadata } : {}), + } +} + +export function createUnsupportedAgentRuntimeReceipt(params: { + action: AgentRuntimeRunAction + request: AgentRuntimeStartRequest + reason: string + metadata?: Record +}): AgentRuntimeRunReceipt { + const now = new Date() + return createAgentRuntimeRunReceipt({ + runId: params.request.runId, + action: params.action, + session: params.request.session, + status: "unsupported", + resultSubtype: "error", + now, + completedAt: now, + error: params.reason, + metadata: params.metadata, + }) +} + +export function createUnsupportedAgentRuntimeControlResult( + request: AgentRuntimeStopRequest | AgentRuntimeToolResultRequest, + message: string, +): AgentRuntimeControlResult { + return { + runId: request.runId, + status: "unsupported", + message, + updatedAt: new Date().toISOString(), + } +} + +export async function* streamUnsupportedAgentRuntimeRun( + receipt: AgentRuntimeRunReceipt, +): AsyncIterable { + yield { + type: "error", + message: receipt.error ?? "This runtime does not support streamed execution.", + } + yield { + type: "finish", + nativeSessionId: receipt.nativeSessionId, + resultSubtype: "error", + } +} + +export function createInMemoryAgentRuntimeRunLedger(): AgentRuntimeRunLedger { + const receipts = new Map() + const events = new Map() + const commandReceipts = new Map() + + return { + upsertReceipt(receipt) { + receipts.set(receipt.runId, receipt) + if (!events.has(receipt.runId)) events.set(receipt.runId, []) + return receipt + }, + upsertCommandReceipt(receipt) { + commandReceipts.set(receipt.commandId, receipt) + return receipt + }, + getCommandReceipt(commandId) { + return commandReceipts.get(commandId) ?? null + }, + listCommandReceipts() { + return [...commandReceipts.values()].sort((left, right) => + left.commandId.localeCompare(right.commandId), + ) + }, + appendEvent(event) { + const sequence = event.sequence ?? ((events.get(event.runId)?.length ?? 0) + 1) + const entry = { + ...event, + sequence, + id: event.id ?? `${event.runId}:${event.kind}:${sequence}`, + createdAt: event.createdAt ?? new Date().toISOString(), + } + const current = events.get(event.runId) ?? [] + current.push(entry) + events.set(event.runId, current) + return entry + }, + finishRun(params) { + const receipt = receipts.get(params.runId) + if (!receipt) return null + const now = new Date().toISOString() + const updated = { + ...receipt, + status: params.status, + resultSubtype: params.resultSubtype ?? receipt.resultSubtype, + updatedAt: now, + completedAt: now, + ...(params.error ? { error: params.error } : {}), + metadata: { + ...(receipt.metadata ?? {}), + ...(params.metadata ?? {}), + }, + } + receipts.set(params.runId, updated) + const finishedEvent = this.appendEvent({ + runId: params.runId, + engineId: receipt.engineId, + kind: "run-finished", + commandId: params.commandId, + causationEventId: params.causationEventId, + correlationId: params.correlationId, + payload: { + status: updated.status, + resultSubtype: updated.resultSubtype, + error: updated.error, + }, + }) + if (params.commandId) { + this.upsertCommandReceipt({ + commandId: params.commandId, + runId: params.runId, + engineId: receipt.engineId, + acceptedAt: finishedEvent.createdAt, + resultSequence: finishedEvent.sequence, + status: "accepted", + error: updated.error ?? null, + }) + } + return updated + }, + snapshot(runId) { + const receipt = receipts.get(runId) + if (!receipt) return null + return { + runId, + receipt, + events: [...(events.get(runId) ?? [])], + } + }, + replayEvents(runId, options) { + const snapshot = this.snapshot(runId) + if (!snapshot) return null + return replayAgentRuntimeRunLedgerEvents(snapshot, options) + }, + list() { + return [...receipts.keys()] + .sort() + .map((runId) => this.snapshot(runId)) + .filter(Boolean) as AgentRuntimeRunLedgerSnapshot[] + }, + } +} + +export function orderAgentRuntimeLedgerEvents( + events: ReadonlyArray, +): AgentRuntimeLedgerEvent[] { + return [...events].sort((left, right) => { + if (left.sequence !== right.sequence) return left.sequence - right.sequence + const createdAt = left.createdAt.localeCompare(right.createdAt) + if (createdAt !== 0) return createdAt + return left.id.localeCompare(right.id) + }) +} + +export function replayAgentRuntimeRunLedgerEvents( + snapshot: AgentRuntimeRunLedgerSnapshot, + options: AgentRuntimeRunReplayOptions = {}, +): AgentRuntimeLedgerEvent[] { + const fromSequenceExclusive = options.fromSequenceExclusive ?? 0 + const events = orderAgentRuntimeLedgerEvents(snapshot.events).filter( + (event) => event.sequence > fromSequenceExclusive, + ) + if (options.limit === undefined) return events + return events.slice(0, Math.max(0, options.limit)) +} + +export function projectAgentRuntimeRunLedgerSnapshot( + snapshot: AgentRuntimeRunLedgerSnapshot, +): AgentRuntimeRunProjection { + const orderedEvents = replayAgentRuntimeRunLedgerEvents(snapshot) + const eventKinds: Partial> = {} + const commandIds = new Set() + const correlationIds = new Set() + const causationEventIds = new Set() + const errors: string[] = [] + let status = snapshot.receipt.status + let resultSubtype = snapshot.receipt.resultSubtype + let updatedAt = snapshot.receipt.updatedAt + let completedAt = snapshot.receipt.completedAt + let nativeSessionId = snapshot.receipt.nativeSessionId + + for (const event of orderedEvents) { + eventKinds[event.kind] = (eventKinds[event.kind] ?? 0) + 1 + if (event.commandId) commandIds.add(event.commandId) + if (event.correlationId) correlationIds.add(event.correlationId) + if (event.causationEventId) causationEventIds.add(event.causationEventId) + updatedAt = event.createdAt + + const eventNativeSessionId = readString(event.payload?.nativeSessionId) + if (eventNativeSessionId) nativeSessionId = eventNativeSessionId + + if (event.kind === "stream-event" && event.payload?.type === "error") { + const message = readString(event.payload.message) + if (message) errors.push(message) + } + + if (event.kind === "run-finished") { + const finishedStatus = readRunStatus(event.payload?.status) + const finishedSubtype = readRunResultSubtype(event.payload?.resultSubtype) + status = finishedStatus ?? status + resultSubtype = finishedSubtype ?? resultSubtype + completedAt = event.createdAt + const error = readString(event.payload?.error) + if (error) errors.push(error) + } + } + + const latestEvent = orderedEvents.at(-1) + + return { + runId: snapshot.runId, + engineId: snapshot.receipt.engineId, + status, + resultSubtype, + startedAt: snapshot.receipt.startedAt, + updatedAt, + completedAt, + nativeSessionId, + eventCount: orderedEvents.length, + eventKinds, + ...(latestEvent + ? { + latestEventSequence: latestEvent.sequence, + latestEventKind: latestEvent.kind, + latestEventAt: latestEvent.createdAt, + } + : {}), + commandIds: [...commandIds], + correlationIds: [...correlationIds], + causationEventIds: [...causationEventIds], + errors, + } +} + +export function projectAgentRuntimeRunLedgerSnapshots( + snapshots: ReadonlyArray, +): AgentRuntimeRunProjection[] { + return snapshots.map(projectAgentRuntimeRunLedgerSnapshot) +} + +function isoString(value: Date | string | undefined): string { + if (!value) return new Date().toISOString() + return typeof value === "string" ? value : value.toISOString() +} + +function readString(value: unknown): string | null { + return typeof value === "string" && value.length > 0 ? value : null +} + +function readRunStatus(value: unknown): AgentRuntimeRunStatus | null { + return value === "running" || + value === "success" || + value === "error" || + value === "cancelled" || + value === "unsupported" + ? value + : null +} + +function readRunResultSubtype( + value: unknown, +): AgentRuntimeRunReceipt["resultSubtype"] | null { + return value === "success" || value === "error" || value === "cancelled" + ? value + : null +} diff --git a/src/main/lib/agent-runtime/session-actions.ts b/src/main/lib/agent-runtime/session-actions.ts index 19c804098..4f88b9580 100644 --- a/src/main/lib/agent-runtime/session-actions.ts +++ b/src/main/lib/agent-runtime/session-actions.ts @@ -1,108 +1,107 @@ -import type { AgentEngineId, AgentRuntimeFeature } from "./types" +import type { AgentEngineId, AgentRuntimeFeature } from "./types"; -export const MOSS_SESSION_ACTION_IDS = [ - "resume", - "fork", - "rollback", -] as const +export const MOSS_SESSION_ACTION_IDS = ["resume", "fork", "rollback"] as const; -export type MossSessionActionId = (typeof MOSS_SESSION_ACTION_IDS)[number] +export type MossSessionActionId = (typeof MOSS_SESSION_ACTION_IDS)[number]; export type MossSessionActionStatus = | "ready" | "unavailable" | "unsupported" | "needs-native-session" - | "needs-target" + | "needs-target"; export type MossSessionActionMode = | "native" | "moss-transcript" - | "message-history" + | "message-history"; export type MossSessionNativeBridge = | "claude-code-session" | "codex-exec-resume" + | "codex-app-server-thread" | "hermes-cli-resume" - | "hermes-acp-session-control" + | "hermes-acp-session-control"; export interface MossSessionMessage { - id?: string - role?: string - content?: unknown - parts?: unknown - metadata?: Record - [key: string]: unknown + id?: string; + role?: string; + content?: unknown; + parts?: unknown; + metadata?: Record; + [key: string]: unknown; } export interface MossSessionActionState { - status: MossSessionActionStatus - mode?: MossSessionActionMode - nativeBridge?: MossSessionNativeBridge - canRunHeadless?: boolean - reason?: string - targetMessageId?: string - targetSdkMessageUuid?: string - targetLabel?: string + status: MossSessionActionStatus; + mode?: MossSessionActionMode; + nativeBridge?: MossSessionNativeBridge; + canRunHeadless?: boolean; + reason?: string; + targetMessageId?: string; + targetSdkMessageUuid?: string; + targetLabel?: string; } export interface MossSessionActionPlan { - subChatId: string - engine: AgentEngineId - messageCount: number - latestMessageId?: string - latestAssistantMessageId?: string - latestAssistantSdkMessageUuid?: string - rollbackTargetMessageId?: string - rollbackTargetSdkMessageUuid?: string - actions: Record + subChatId: string; + engine: AgentEngineId; + messageCount: number; + latestMessageId?: string; + latestAssistantMessageId?: string; + latestAssistantSdkMessageUuid?: string; + rollbackTargetMessageId?: string; + rollbackTargetSdkMessageUuid?: string; + actions: Record; } export interface BuildMossSessionActionPlanInput { - subChatId: string - engine: AgentEngineId - nativeSessionId?: string | null - messages: string | MossSessionMessage[] | null | undefined - features?: AgentRuntimeFeature[] + subChatId: string; + engine: AgentEngineId; + nativeSessionId?: string | null; + messages: string | MossSessionMessage[] | null | undefined; + features?: AgentRuntimeFeature[]; } export interface MossForkSnapshot { - messages: MossSessionMessage[] - messageCount: number - forkAtSdkUuid: string | null - mode: MossSessionActionMode - nativeSessionLinked: boolean + messages: MossSessionMessage[]; + messageCount: number; + forkAtSdkUuid: string | null; + mode: MossSessionActionMode; + nativeSessionLinked: boolean; } export interface BuildMossForkSnapshotInput { - engine: AgentEngineId - nativeSessionId?: string | null - messages: string | MossSessionMessage[] | null | undefined - features?: AgentRuntimeFeature[] - targetMessageId?: string - targetMessageIndex?: number + engine: AgentEngineId; + nativeSessionId?: string | null; + messages: string | MossSessionMessage[] | null | undefined; + features?: AgentRuntimeFeature[]; + targetMessageId?: string; + targetMessageIndex?: number; } export interface MossRollbackSnapshot { - messages: MossSessionMessage[] - messageCount: number - targetMessageId: string | null - targetSdkMessageUuid: string | null - mode: MossSessionActionMode - nativeSessionLinked: boolean + messages: MossSessionMessage[]; + messageCount: number; + targetMessageId: string | null; + targetSdkMessageUuid: string | null; + nativeRollbackTurnCount: number; + mode: MossSessionActionMode; + nativeSessionLinked: boolean; } export interface BuildMossRollbackSnapshotInput { - engine: AgentEngineId - nativeSessionId?: string | null - messages: string | MossSessionMessage[] | null | undefined - features?: AgentRuntimeFeature[] - targetMessageId?: string - targetSdkMessageUuid?: string + engine: AgentEngineId; + nativeSessionId?: string | null; + messages: string | MossSessionMessage[] | null | undefined; + features?: AgentRuntimeFeature[]; + targetMessageId?: string; + targetSdkMessageUuid?: string; + strictTarget?: boolean; } function isRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value) + return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function cleanMetadata( @@ -110,55 +109,65 @@ function cleanMetadata( extra?: Record, options?: { clearNativeSession?: boolean }, ): Record { - const metadata = isRecord(value) ? { ...value } : {} - delete metadata.shouldResume - delete metadata.shouldForkResume + const metadata = isRecord(value) ? { ...value } : {}; + delete metadata.shouldResume; + delete metadata.shouldForkResume; if (options?.clearNativeSession) { - delete metadata.sessionId - delete metadata.sdkMessageUuid + delete metadata.sessionId; + delete metadata.sdkMessageUuid; } return { ...metadata, ...(extra ?? {}), - } + }; } function messageId(message: MossSessionMessage): string | undefined { - return typeof message.id === "string" ? message.id : undefined + return typeof message.id === "string" ? message.id : undefined; } function messageRole(message: MossSessionMessage): string | undefined { - return typeof message.role === "string" ? message.role : undefined + return typeof message.role === "string" ? message.role : undefined; +} + +function countUserTurnsAfterIndex( + messages: MossSessionMessage[], + targetIndex: number, +): number { + if (targetIndex < 0) return 0; + return messages + .slice(targetIndex + 1) + .filter((message) => messageRole(message) === "user").length; } function messageSdkUuid(message: MossSessionMessage): string | undefined { - const metadata = isRecord(message.metadata) ? message.metadata : {} - const value = metadata.sdkMessageUuid - return typeof value === "string" && value ? value : undefined + const metadata = isRecord(message.metadata) ? message.metadata : {}; + const value = metadata.sdkMessageUuid; + return typeof value === "string" && value ? value : undefined; } function messageLabel(message: MossSessionMessage, fallback: string): string { - const parts = Array.isArray(message.parts) ? message.parts : [] + const parts = Array.isArray(message.parts) ? message.parts : []; for (const part of parts) { - if (!isRecord(part)) continue - const text = part.text + if (!isRecord(part)) continue; + const text = part.text; if (typeof text === "string" && text.trim()) { - return text.trim().replace(/\s+/g, " ").slice(0, 80) + return text.trim().replace(/\s+/g, " ").slice(0, 80); } } if (typeof message.content === "string" && message.content.trim()) { - return message.content.trim().replace(/\s+/g, " ").slice(0, 80) + return message.content.trim().replace(/\s+/g, " ").slice(0, 80); } - return fallback + return fallback; } function hasFeature( features: AgentRuntimeFeature[] | undefined, feature: AgentRuntimeFeature, ): boolean { - return !features || features.includes(feature) + return !features || features.includes(feature); } function findLastIndex( @@ -166,9 +175,9 @@ function findLastIndex( predicate: (value: T, index: number) => boolean, ): number { for (let index = values.length - 1; index >= 0; index -= 1) { - if (predicate(values[index], index)) return index + if (predicate(values[index], index)) return index; } - return -1 + return -1; } function supportsNativeForkBridge( @@ -178,15 +187,19 @@ function supportsNativeForkBridge( forkAtSdkUuid: string | null, ): boolean { if (engine === "hermes") { - return Boolean(nativeSessionId && hasFeature(features, "fork")) + return Boolean(nativeSessionId && hasFeature(features, "fork")); + } + + if (engine === "codex") { + return Boolean(nativeSessionId && hasFeature(features, "fork")); } return Boolean( engine === "claude-code" && - nativeSessionId && - forkAtSdkUuid && - hasFeature(features, "fork"), - ) + nativeSessionId && + forkAtSdkUuid && + hasFeature(features, "fork"), + ); } function supportsNativeRollbackBridge( @@ -197,76 +210,98 @@ function supportsNativeRollbackBridge( hasTarget: boolean, ): boolean { if (engine === "hermes") { - return Boolean(nativeSessionId && hasTarget && hasFeature(features, "rollback")) + return Boolean( + nativeSessionId && hasTarget && hasFeature(features, "rollback"), + ); + } + + if (engine === "codex") { + return Boolean( + nativeSessionId && hasTarget && hasFeature(features, "rollback"), + ); } return Boolean( engine === "claude-code" && - nativeSessionId && - targetSdkUuid && - hasFeature(features, "rollback"), - ) + nativeSessionId && + targetSdkUuid && + hasFeature(features, "rollback"), + ); } export function parseMossSessionMessages( value: string | MossSessionMessage[] | null | undefined, ): MossSessionMessage[] { - if (!value) return [] - let parsed: unknown + if (!value) return []; + let parsed: unknown; try { - parsed = typeof value === "string" ? JSON.parse(value || "[]") : value + parsed = typeof value === "string" ? JSON.parse(value || "[]") : value; } catch { - return [] + return []; } - if (!Array.isArray(parsed)) return [] - return parsed.filter(isRecord) as MossSessionMessage[] + if (!Array.isArray(parsed)) return []; + return parsed.filter(isRecord) as MossSessionMessage[]; } export function buildMossSessionActionPlan( input: BuildMossSessionActionPlanInput, ): MossSessionActionPlan { - const messages = parseMossSessionMessages(input.messages) - const latestMessage = messages[messages.length - 1] + const messages = parseMossSessionMessages(input.messages); + const latestMessage = messages[messages.length - 1]; + const latestMessageIndex = messages.length - 1; const latestAssistantIndex = findLastIndex( messages, (message) => messageRole(message) === "assistant", - ) + ); const latestAssistant = - latestAssistantIndex >= 0 ? messages[latestAssistantIndex] : undefined + latestAssistantIndex >= 0 ? messages[latestAssistantIndex] : undefined; const latestAssistantWithSdkIndex = findLastIndex( messages, (message) => messageRole(message) === "assistant" && Boolean(messageSdkUuid(message)), - ) + ); const latestAssistantWithSdk = latestAssistantWithSdkIndex >= 0 ? messages[latestAssistantWithSdkIndex] - : undefined - const rollbackTarget = latestAssistantWithSdk ?? latestAssistant ?? latestMessage + : undefined; + const rollbackTargetIndex = latestAssistantWithSdk + ? latestAssistantWithSdkIndex + : latestAssistant + ? latestAssistantIndex + : latestMessageIndex; + const rollbackTarget = + rollbackTargetIndex >= 0 ? messages[rollbackTargetIndex] : undefined; + const rollbackTargetRole = rollbackTarget + ? messageRole(rollbackTarget) + : undefined; const rollbackTargetSdkUuid = rollbackTarget - ? messageSdkUuid(rollbackTarget) ?? null - : null + ? (messageSdkUuid(rollbackTarget) ?? null) + : null; const rollbackTargetMessageId = rollbackTarget - ? messageId(rollbackTarget) ?? null - : null - const nativeSessionLinked = Boolean(input.nativeSessionId) + ? (messageId(rollbackTarget) ?? null) + : null; + const nativeSessionLinked = Boolean(input.nativeSessionId); const latestAssistantSdkMessageUuid = latestAssistant ? messageSdkUuid(latestAssistant) - : undefined - const forkAtSdkUuid = latestAssistant ? messageSdkUuid(latestAssistant) ?? null : null + : undefined; + const forkAtSdkUuid = latestAssistant + ? (messageSdkUuid(latestAssistant) ?? null) + : null; const nativeFork = supportsNativeForkBridge( input.engine, input.nativeSessionId, input.features, forkAtSdkUuid, - ) - const nativeRollback = supportsNativeRollbackBridge( - input.engine, - input.nativeSessionId, - input.features, - rollbackTargetSdkUuid, - Boolean(rollbackTarget), - ) + ); + const nativeRollback = + supportsNativeRollbackBridge( + input.engine, + input.nativeSessionId, + input.features, + rollbackTargetSdkUuid, + Boolean(rollbackTarget), + ) && + (input.engine !== "codex" || rollbackTargetRole === "assistant"); const resume: MossSessionActionState = !hasFeature(input.features, "resume") ? { @@ -278,16 +313,16 @@ export function buildMossSessionActionPlan( status: "ready", mode: "native", ...(input.engine === "codex" - ? { nativeBridge: "codex-exec-resume" as const } + ? { nativeBridge: "codex-app-server-thread" as const } : input.engine === "claude-code" ? { nativeBridge: "claude-code-session" as const } : input.engine === "hermes" ? { nativeBridge: "hermes-cli-resume" as const } - : {}), + : {}), canRunHeadless: input.engine === "codex" || input.engine === "hermes", reason: input.engine === "codex" - ? "Codex native session id is linked through codex exec resume." + ? "Codex native session id is linked through app-server thread/resume." : input.engine === "hermes" ? "Hermes native session id is linked through hermes --resume." : "Native session id is linked.", @@ -296,86 +331,106 @@ export function buildMossSessionActionPlan( ? { status: "needs-native-session", mode: "message-history", - reason: "Transcript exists, but no native engine session has been linked yet.", + reason: + "Transcript exists, but no native engine session has been linked yet.", } : { status: "unavailable", reason: "No transcript is available to resume.", - } + }; - const fork: MossSessionActionState = messages.length === 0 - ? { - status: "unavailable", - reason: "No transcript is available to fork.", - } - : nativeFork + const fork: MossSessionActionState = + messages.length === 0 ? { - status: "ready", - mode: "native", - nativeBridge: - input.engine === "hermes" - ? "hermes-acp-session-control" - : "claude-code-session", - targetMessageId: latestAssistant ? messageId(latestAssistant) : undefined, - targetSdkMessageUuid: forkAtSdkUuid ?? undefined, - targetLabel: latestAssistant - ? messageLabel(latestAssistant, "Latest assistant message") - : undefined, - reason: - input.engine === "hermes" - ? "Hermes fork stays linked to the Moss-owned Hermes session and starts from the selected Moss transcript boundary." - : "Claude Code native fork bridge can resume at the selected assistant turn.", + status: "unavailable", + reason: "No transcript is available to fork.", } - : { - status: "ready", - mode: "moss-transcript", - targetMessageId: latestMessage ? messageId(latestMessage) : undefined, - targetLabel: latestMessage - ? messageLabel(latestMessage, "Latest message") - : undefined, - reason: "Moss will clone the transcript and start a fresh native engine session.", - } - - const rollback: MossSessionActionState = messages.length === 0 - ? { - status: "unavailable", - reason: "No transcript is available to roll back.", - } - : !rollbackTarget - ? { - status: "needs-target", - reason: "No rollback target message was found.", - } - : nativeRollback + : nativeFork ? { status: "ready", mode: "native", nativeBridge: - input.engine === "hermes" - ? "hermes-acp-session-control" - : "claude-code-session", - targetMessageId: rollbackTargetMessageId ?? undefined, - targetSdkMessageUuid: rollbackTargetSdkUuid ?? undefined, - targetLabel: messageLabel(rollbackTarget, "Rollback target"), + input.engine === "codex" + ? "codex-app-server-thread" + : input.engine === "hermes" + ? "hermes-acp-session-control" + : "claude-code-session", + canRunHeadless: + input.engine === "codex" || input.engine === "hermes", + targetMessageId: latestAssistant + ? messageId(latestAssistant) + : undefined, + targetSdkMessageUuid: forkAtSdkUuid ?? undefined, + targetLabel: latestAssistant + ? messageLabel(latestAssistant, "Latest assistant message") + : undefined, reason: - input.engine === "hermes" - ? "Hermes rollback stays linked to the Moss-owned Hermes session and truncates to the selected Moss transcript boundary." - : "Claude Code rollback can resume at the target assistant turn.", + input.engine === "codex" + ? "Codex fork uses the app-server thread/fork bridge and keeps the native thread linked." + : input.engine === "hermes" + ? "Hermes fork stays linked to the Moss-owned Hermes session and starts from the selected Moss transcript boundary." + : "Claude Code native fork bridge can resume at the selected assistant turn.", } - : messages.length > 1 + : { + status: "ready", + mode: "moss-transcript", + targetMessageId: latestMessage + ? messageId(latestMessage) + : undefined, + targetLabel: latestMessage + ? messageLabel(latestMessage, "Latest message") + : undefined, + reason: + "Moss will clone the transcript and start a fresh native engine session.", + }; + + const rollback: MossSessionActionState = + messages.length === 0 + ? { + status: "unavailable", + reason: "No transcript is available to roll back.", + } + : !rollbackTarget + ? { + status: "needs-target", + reason: "No rollback target message was found.", + } + : nativeRollback ? { status: "ready", - mode: "message-history", + mode: "native", + nativeBridge: + input.engine === "hermes" + ? "hermes-acp-session-control" + : input.engine === "codex" + ? "codex-app-server-thread" + : "claude-code-session", targetMessageId: rollbackTargetMessageId ?? undefined, targetSdkMessageUuid: rollbackTargetSdkUuid ?? undefined, targetLabel: messageLabel(rollbackTarget, "Rollback target"), - reason: "Moss will truncate the transcript and clear stale native session ids.", - } - : { - status: "needs-target", - mode: "message-history", - reason: "At least two messages are needed for message-history rollback.", + reason: + input.engine === "hermes" + ? "Hermes rollback stays linked to the Moss-owned Hermes session and truncates to the selected Moss transcript boundary." + : input.engine === "codex" + ? "Codex rollback uses the app-server thread/rollback bridge and keeps the native thread linked." + : "Claude Code rollback can resume at the target assistant turn.", } + : messages.length > 1 + ? { + status: "ready", + mode: "message-history", + targetMessageId: rollbackTargetMessageId ?? undefined, + targetSdkMessageUuid: rollbackTargetSdkUuid ?? undefined, + targetLabel: messageLabel(rollbackTarget, "Rollback target"), + reason: + "Moss will truncate the transcript and clear stale native session ids.", + } + : { + status: "needs-target", + mode: "message-history", + reason: + "At least two messages are needed for message-history rollback.", + }; return { subChatId: input.subChatId, @@ -393,7 +448,7 @@ export function buildMossSessionActionPlan( fork, rollback, }, - } + }; } function resolveForkCutoffIndex( @@ -401,52 +456,56 @@ function resolveForkCutoffIndex( targetMessageId: string | undefined, targetMessageIndex: number | undefined, ): number { - if (messages.length === 0) return -1 + if (messages.length === 0) return -1; if (targetMessageId) { - const byId = messages.findIndex((message) => messageId(message) === targetMessageId) - if (byId >= 0) return byId + const byId = messages.findIndex( + (message) => messageId(message) === targetMessageId, + ); + if (byId >= 0) return byId; } if ( typeof targetMessageIndex === "number" && targetMessageIndex >= 0 && targetMessageIndex < messages.length ) { - return targetMessageIndex + return targetMessageIndex; } - return messages.length - 1 + return messages.length - 1; } function createForkMessageId(index: number): string { - return `fork-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 8)}` + return `fork-${Date.now()}-${index}-${Math.random().toString(36).slice(2, 8)}`; } export function buildMossForkSnapshot( input: BuildMossForkSnapshotInput, ): MossForkSnapshot { - const messages = parseMossSessionMessages(input.messages) + const messages = parseMossSessionMessages(input.messages); const cutoffIndex = resolveForkCutoffIndex( messages, input.targetMessageId, input.targetMessageIndex, - ) + ); if (cutoffIndex < 0) { - throw new Error("No transcript is available to fork.") + throw new Error("No transcript is available to fork."); } - const messagesToFork = messages.slice(0, cutoffIndex + 1) + const messagesToFork = messages.slice(0, cutoffIndex + 1); const lastAssistantIndex = findLastIndex( messagesToFork, (message) => messageRole(message) === "assistant", - ) + ); const lastAssistant = - lastAssistantIndex >= 0 ? messagesToFork[lastAssistantIndex] : undefined - const forkAtSdkUuid = lastAssistant ? messageSdkUuid(lastAssistant) ?? null : null + lastAssistantIndex >= 0 ? messagesToFork[lastAssistantIndex] : undefined; + const forkAtSdkUuid = lastAssistant + ? (messageSdkUuid(lastAssistant) ?? null) + : null; const nativeFork = supportsNativeForkBridge( input.engine, input.nativeSessionId, input.features, forkAtSdkUuid, - ) + ); return { messages: messagesToFork.map((message, index) => ({ @@ -467,62 +526,81 @@ export function buildMossForkSnapshot( forkAtSdkUuid, mode: nativeFork ? "native" : "moss-transcript", nativeSessionLinked: nativeFork, - } + }; } function resolveRollbackTargetIndex( messages: MossSessionMessage[], targetMessageId: string | undefined, targetSdkMessageUuid: string | undefined, + strictTarget: boolean | undefined, ): number { - if (messages.length === 0) return -1 + if (messages.length === 0) return -1; if (targetMessageId) { - const byId = messages.findIndex((message) => messageId(message) === targetMessageId) - if (byId >= 0) return byId + const byId = messages.findIndex( + (message) => messageId(message) === targetMessageId, + ); + if (byId >= 0) return byId; } if (targetSdkMessageUuid) { const bySdkUuid = messages.findIndex( (message) => messageSdkUuid(message) === targetSdkMessageUuid, - ) - if (bySdkUuid >= 0) return bySdkUuid + ); + if (bySdkUuid >= 0) return bySdkUuid; } + if (strictTarget && (targetMessageId || targetSdkMessageUuid)) return -1; const lastAssistantWithSdk = findLastIndex( messages, (message) => messageRole(message) === "assistant" && Boolean(messageSdkUuid(message)), - ) - if (lastAssistantWithSdk >= 0) return lastAssistantWithSdk + ); + if (lastAssistantWithSdk >= 0) return lastAssistantWithSdk; const lastAssistant = findLastIndex( messages, (message) => messageRole(message) === "assistant", - ) - if (lastAssistant >= 0) return lastAssistant - return messages.length - 1 + ); + if (lastAssistant >= 0) return lastAssistant; + return messages.length - 1; } export function buildMossRollbackSnapshot( input: BuildMossRollbackSnapshotInput, ): MossRollbackSnapshot { - const messages = parseMossSessionMessages(input.messages) + const messages = parseMossSessionMessages(input.messages); const targetIndex = resolveRollbackTargetIndex( messages, input.targetMessageId, input.targetSdkMessageUuid, - ) + input.strictTarget, + ); if (targetIndex < 0) { - throw new Error("No transcript is available to roll back.") + if ( + input.strictTarget && + (input.targetMessageId || input.targetSdkMessageUuid) + ) { + throw new Error( + "Strict rollback target was not found in the Moss transcript.", + ); + } + throw new Error("No transcript is available to roll back."); } - const target = messages[targetIndex] - const targetSdkUuid = messageSdkUuid(target) ?? null - const nativeRollback = supportsNativeRollbackBridge( - input.engine, - input.nativeSessionId, - input.features, - targetSdkUuid, - true, - ) - const truncated = messages.slice(0, targetIndex + 1) + const target = messages[targetIndex]; + const targetSdkUuid = messageSdkUuid(target) ?? null; + const nativeRollbackTurnCount = countUserTurnsAfterIndex( + messages, + targetIndex, + ); + const nativeRollback = + supportsNativeRollbackBridge( + input.engine, + input.nativeSessionId, + input.features, + targetSdkUuid, + true, + ) && + (input.engine !== "codex" || messageRole(target) === "assistant"); + const truncated = messages.slice(0, targetIndex + 1); return { messages: truncated.map((message, index) => ({ @@ -540,59 +618,58 @@ export function buildMossRollbackSnapshot( messageCount: truncated.length, targetMessageId: messageId(target) ?? null, targetSdkMessageUuid: targetSdkUuid, + nativeRollbackTurnCount, mode: nativeRollback ? "native" : "message-history", nativeSessionLinked: nativeRollback, - } + }; } export function shouldIgnoreMossStoredMessageSessionIds( runtimeMetadata: string | null | undefined, ): boolean { - if (!runtimeMetadata) return false + if (!runtimeMetadata) return false; - let parsed: unknown + let parsed: unknown; try { - parsed = JSON.parse(runtimeMetadata) + parsed = JSON.parse(runtimeMetadata); } catch { - return false + return false; } if (!isRecord(parsed) || !isRecord(parsed.mossSessionControl)) { - return false + return false; } - const control = parsed.mossSessionControl - const action = control.action - const mode = control.mode - if (mode === "native") return false + const control = parsed.mossSessionControl; + const action = control.action; + const mode = control.mode; + if (mode === "native") return false; return ( (action === "fork" && mode === "moss-transcript") || (action === "rollback" && mode === "message-history") - ) + ); } export function mergeMossSessionControlMetadata( runtimeMetadata: string | null | undefined, controlMetadata: Record, ): string { - let parsed: Record = {} + let parsed: Record = {}; if (runtimeMetadata) { try { - const value = JSON.parse(runtimeMetadata) - if (isRecord(value)) parsed = value + const value = JSON.parse(runtimeMetadata); + if (isRecord(value)) parsed = value; } catch { - parsed = {} + parsed = {}; } } return JSON.stringify({ ...parsed, mossSessionControl: { - ...(isRecord(parsed.mossSessionControl) - ? parsed.mossSessionControl - : {}), + ...(isRecord(parsed.mossSessionControl) ? parsed.mossSessionControl : {}), ...controlMetadata, updatedAt: new Date().toISOString(), }, - }) + }); } diff --git a/src/main/lib/agent-runtime/session-records.ts b/src/main/lib/agent-runtime/session-records.ts index f4b249002..7175019c2 100644 --- a/src/main/lib/agent-runtime/session-records.ts +++ b/src/main/lib/agent-runtime/session-records.ts @@ -1,113 +1,121 @@ -import { getAgentRuntimeManifest } from "./manifests" +import { getAgentRuntimeManifest } from "./manifests"; import { buildMossForkSnapshot, buildMossRollbackSnapshot, mergeMossSessionControlMetadata, type MossForkSnapshot, type MossRollbackSnapshot, -} from "./session-actions" -import { AGENT_ENGINE_IDS, DEFAULT_AGENT_ENGINE_ID, type AgentEngineId } from "./types" +} from "./session-actions"; +import { + AGENT_ENGINE_IDS, + DEFAULT_AGENT_ENGINE_ID, + type AgentEngineId, +} from "./types"; export interface MossSessionSubChatRecord { - id: string - chatId: string - name: string | null - mode: string - messages: string - sessionId: string | null - engine: string | null - engineSessionId: string | null - engineConfigDir: string | null - modelId: string | null - runtimeMetadata: string | null + id: string; + chatId: string; + name: string | null; + mode: string; + messages: string; + sessionId: string | null; + engine: string | null; + engineSessionId: string | null; + engineConfigDir: string | null; + modelId: string | null; + runtimeMetadata: string | null; } export interface MossForkSubChatInsertValues { - id: string - chatId: string - name: string - mode: string - messages: string - sessionId: string | null - engine: AgentEngineId - engineSessionId: string | null - engineConfigDir: string | null - modelId: string | null - runtimeMetadata: string + id: string; + chatId: string; + name: string; + mode: string; + messages: string; + sessionId: string | null; + engine: AgentEngineId; + engineSessionId: string | null; + engineConfigDir: string | null; + modelId: string | null; + runtimeMetadata: string; } export interface MossRollbackSubChatUpdateValues { - messages: string - sessionId: string | null - engineSessionId: string | null - runtimeMetadata: string - updatedAt: Date + messages: string; + sessionId: string | null; + engineSessionId: string | null; + runtimeMetadata: string; + updatedAt: Date; } export interface BuildMossForkSubChatRecordInput { - sourceSubChat: MossSessionSubChatRecord - targetSubChatId: string - targetName: string - targetMessageId?: string - targetMessageIndex?: number - nativeBridgePlan?: unknown - forceTranscript?: boolean - fallbackReason?: string - metadata?: Record + sourceSubChat: MossSessionSubChatRecord; + targetSubChatId: string; + targetName: string; + targetMessageId?: string; + targetMessageIndex?: number; + nativeBridgePlan?: unknown; + forceTranscript?: boolean; + fallbackReason?: string; + metadata?: Record; } export interface BuildMossForkSubChatRecordResult { - engine: AgentEngineId - sourceNativeSessionId: string | null - snapshot: MossForkSnapshot - nativeSessionLinked: boolean - insertValues: MossForkSubChatInsertValues + engine: AgentEngineId; + sourceNativeSessionId: string | null; + snapshot: MossForkSnapshot; + nativeSessionLinked: boolean; + insertValues: MossForkSubChatInsertValues; } export interface BuildMossRollbackSubChatUpdateInput { - subChat: MossSessionSubChatRecord - targetMessageId?: string - targetSdkMessageUuid?: string - appliedGitCheckpoint?: boolean - nativeBridgePlan?: unknown - metadata?: Record + subChat: MossSessionSubChatRecord; + targetMessageId?: string; + targetSdkMessageUuid?: string; + strictTarget?: boolean; + appliedGitCheckpoint?: boolean; + nativeBridgePlan?: unknown; + metadata?: Record; } export interface BuildMossRollbackSubChatUpdateResult { - engine: AgentEngineId - sourceNativeSessionId: string | null - snapshot: MossRollbackSnapshot - nativeSessionLinked: boolean - updateValues: MossRollbackSubChatUpdateValues + engine: AgentEngineId; + sourceNativeSessionId: string | null; + snapshot: MossRollbackSnapshot; + nativeSessionLinked: boolean; + updateValues: MossRollbackSubChatUpdateValues; } export function normalizeMossSessionRecordEngine( value: string | null | undefined, ): AgentEngineId { return AGENT_ENGINE_IDS.includes(value as AgentEngineId) - ? value as AgentEngineId - : DEFAULT_AGENT_ENGINE_ID + ? (value as AgentEngineId) + : DEFAULT_AGENT_ENGINE_ID; } export function nativeSessionIdForMossSessionRecord( subChat: Pick, engine: AgentEngineId, ): string | null { - return subChat.engineSessionId ?? (engine === "claude-code" ? subChat.sessionId : null) + return ( + subChat.engineSessionId ?? + (engine === "claude-code" ? subChat.sessionId : null) + ); } export function buildMossForkSubChatRecord( input: BuildMossForkSubChatRecordInput, ): BuildMossForkSubChatRecordResult { - const engine = normalizeMossSessionRecordEngine(input.sourceSubChat.engine) - const manifest = getAgentRuntimeManifest(engine) + const engine = normalizeMossSessionRecordEngine(input.sourceSubChat.engine); + const manifest = getAgentRuntimeManifest(engine); const originalNativeSessionId = nativeSessionIdForMossSessionRecord( input.sourceSubChat, engine, - ) + ); const sourceNativeSessionId = input.forceTranscript ? null - : originalNativeSessionId + : originalNativeSessionId; const snapshot = buildMossForkSnapshot({ engine, nativeSessionId: sourceNativeSessionId, @@ -115,8 +123,8 @@ export function buildMossForkSubChatRecord( features: manifest.features, targetMessageId: input.targetMessageId, targetMessageIndex: input.targetMessageIndex, - }) - const nativeSessionLinked = snapshot.nativeSessionLinked + }); + const nativeSessionLinked = snapshot.nativeSessionLinked; const runtimeMetadata = mergeMossSessionControlMetadata( input.sourceSubChat.runtimeMetadata, { @@ -132,7 +140,7 @@ export function buildMossForkSubChatRecord( ...(input.fallbackReason ? { fallbackReason: input.fallbackReason } : {}), ...(input.metadata ?? {}), }, - ) + ); return { engine, @@ -155,18 +163,18 @@ export function buildMossForkSubChatRecord( modelId: input.sourceSubChat.modelId, runtimeMetadata, }, - } + }; } export function buildMossRollbackSubChatUpdate( input: BuildMossRollbackSubChatUpdateInput, ): BuildMossRollbackSubChatUpdateResult { - const engine = normalizeMossSessionRecordEngine(input.subChat.engine) - const manifest = getAgentRuntimeManifest(engine) + const engine = normalizeMossSessionRecordEngine(input.subChat.engine); + const manifest = getAgentRuntimeManifest(engine); const sourceNativeSessionId = nativeSessionIdForMossSessionRecord( input.subChat, engine, - ) + ); const snapshot = buildMossRollbackSnapshot({ engine, nativeSessionId: sourceNativeSessionId, @@ -174,8 +182,9 @@ export function buildMossRollbackSubChatUpdate( features: manifest.features, targetMessageId: input.targetMessageId, targetSdkMessageUuid: input.targetSdkMessageUuid, - }) - const nativeSessionLinked = snapshot.nativeSessionLinked + strictTarget: input.strictTarget, + }); + const nativeSessionLinked = snapshot.nativeSessionLinked; return { engine, @@ -197,6 +206,8 @@ export function buildMossRollbackSubChatUpdate( nativeSessionLinked, targetMessageId: snapshot.targetMessageId, targetSdkMessageUuid: snapshot.targetSdkMessageUuid, + nativeRollbackTurnCount: snapshot.nativeRollbackTurnCount, + ...(input.strictTarget ? { strictTarget: true } : {}), appliedGitCheckpoint: Boolean(input.appliedGitCheckpoint), nativeBridgePlan: input.nativeBridgePlan, ...(input.metadata ?? {}), @@ -204,5 +215,5 @@ export function buildMossRollbackSubChatUpdate( ), updatedAt: new Date(), }, - } + }; } diff --git a/src/main/lib/agent-runtime/session-store.ts b/src/main/lib/agent-runtime/session-store.ts index 1675fa703..bbb83e6a5 100644 --- a/src/main/lib/agent-runtime/session-store.ts +++ b/src/main/lib/agent-runtime/session-store.ts @@ -1,6 +1,10 @@ import { eq } from "drizzle-orm" import { getDatabase, subChats } from "../db" import type { AgentEngineId, AgentPermissionMode } from "./types" +import { + mergeAgentRuntimeLaunchPlanMetadata, + type AgentRuntimeLaunchPlan, +} from "./launch-plan" type RuntimeMetadata = Record @@ -9,20 +13,37 @@ export type PersistAgentRuntimeSessionInput = { engine: AgentEngineId nativeSessionId?: string | null configDir?: string | null + providerInstanceId?: string | null modelId?: string | null + modelSelection?: { + instanceId: string + modelId: string + options?: Record + } | null permissionMode?: AgentPermissionMode metadata?: RuntimeMetadata + launchPlan?: AgentRuntimeLaunchPlan updateLegacySessionId?: boolean } function serializeMetadata( input: PersistAgentRuntimeSessionInput, ): string { - return JSON.stringify({ + const metadata = { ...(input.metadata ?? {}), + ...(input.providerInstanceId + ? { providerInstanceId: input.providerInstanceId } + : {}), + ...(input.modelSelection ? { modelSelection: input.modelSelection } : {}), ...(input.permissionMode ? { permissionMode: input.permissionMode } : {}), updatedAt: new Date().toISOString(), - }) + } + + return JSON.stringify( + input.launchPlan + ? mergeAgentRuntimeLaunchPlanMetadata(metadata, input.launchPlan) + : metadata, + ) } export function persistAgentRuntimeSession( diff --git a/src/main/lib/agent-runtime/stale-stream-state.ts b/src/main/lib/agent-runtime/stale-stream-state.ts new file mode 100644 index 000000000..ca3436c86 --- /dev/null +++ b/src/main/lib/agent-runtime/stale-stream-state.ts @@ -0,0 +1,28 @@ +export type AgentStreamSubChatState = { + id: string + engine: string + streamId?: string | null +} + +export type AgentStreamActivityLookup = { + isActiveCodexStream: (subChatId: string, streamId: string) => boolean + isActiveHermesStream: (subChatId: string, streamId: string) => boolean +} + +export function shouldClearStaleAgentStreamId( + subChat: AgentStreamSubChatState, + activity: AgentStreamActivityLookup, +): boolean { + const streamId = subChat.streamId + if (!streamId) return false + + if (subChat.engine === "codex") { + return !activity.isActiveCodexStream(subChat.id, streamId) + } + + if (subChat.engine === "hermes") { + return !activity.isActiveHermesStream(subChat.id, streamId) + } + + return false +} diff --git a/src/main/lib/agent-runtime/types.ts b/src/main/lib/agent-runtime/types.ts index 8b0a19984..72fd81d6a 100644 --- a/src/main/lib/agent-runtime/types.ts +++ b/src/main/lib/agent-runtime/types.ts @@ -1,24 +1,24 @@ import type { CodexBlockStatus, CodexConversationBlock, -} from "../../../shared/codex-tool-normalizer" +} from "../../../shared/codex-tool-normalizer"; export const AGENT_ENGINE_IDS = [ "claude-code", "codex", "hermes", "custom-acp", -] as const +] as const; -export type AgentEngineId = (typeof AGENT_ENGINE_IDS)[number] -export const DEFAULT_AGENT_ENGINE_ID: AgentEngineId = "hermes" +export type AgentEngineId = (typeof AGENT_ENGINE_IDS)[number]; +export const DEFAULT_AGENT_ENGINE_ID: AgentEngineId = "hermes"; export type AgentRuntimeAvailability = | "available" | "needs-auth" | "not-installed" | "unsupported" - | "error" + | "error"; export type AgentRuntimeFeature = | "chat" @@ -42,8 +42,16 @@ export type AgentRuntimeFeature = | "realtime-voice" | "dictation" | "diagnostics" + | "thread-management"; -export type AgentPermissionMode = "plan" | "agent" | "bypass" +export type AgentPermissionMode = + | "plan" + | "agent" + | "bypass" + | "read-only" + | "ask-approval" + | "full-access" + | "custom"; export type AgentRuntimeAuthMethod = | "oauth" @@ -51,66 +59,807 @@ export type AgentRuntimeAuthMethod = | "shell-config" | "not-authenticated" | "unsupported" - | "unknown" + | "unknown"; export interface AgentRuntimeModel { - id: string - label: string + id: string; + label: string; } export interface AgentRuntimeModelHealth extends AgentRuntimeModel { - availability: AgentRuntimeAvailability - reason?: string + availability: AgentRuntimeAvailability; + reason?: string; +} + +export type AgentRuntimeProviderInstanceStatus = + | "ready" + | "warning" + | "error" + | "disabled"; + +export interface AgentRuntimeProviderInstance { + instanceId: string; + engineId: AgentEngineId; + displayName?: string; + enabled: boolean; + installed: boolean; + status: AgentRuntimeProviderInstanceStatus; + isDefault?: boolean; + modelIds?: string[]; + version?: string | null; + versionAdvisory?: AgentRuntimeVersionAdvisory | null; + updateState?: AgentRuntimeUpdateState | null; +} + +export type AgentRuntimeVersionAdvisoryStatus = + | "unknown" + | "current" + | "behind_latest"; + +export interface AgentRuntimeVersionAdvisory { + status: AgentRuntimeVersionAdvisoryStatus; + currentVersion: string | null; + latestVersion: string | null; + updateCommand: string | null; + canUpdate: boolean; + checkedAt: string | null; + message: string | null; +} + +export type AgentRuntimeUpdateStatus = + | "idle" + | "queued" + | "running" + | "succeeded" + | "failed" + | "unchanged"; + +export interface AgentRuntimeUpdateState { + status: AgentRuntimeUpdateStatus; + startedAt: string | null; + finishedAt: string | null; + message: string | null; + output: string | null; } export interface AgentRuntimeHealth { - availability: AgentRuntimeAvailability - statusReason?: string - authMethod?: AgentRuntimeAuthMethod - models?: AgentRuntimeModelHealth[] + availability: AgentRuntimeAvailability; + statusReason?: string; + authMethod?: AgentRuntimeAuthMethod; + models?: AgentRuntimeModelHealth[]; + providerInstances?: AgentRuntimeProviderInstance[]; + version?: string | null; + versionAdvisory?: AgentRuntimeVersionAdvisory | null; + updateState?: AgentRuntimeUpdateState | null; } export interface AgentRuntimeManifest { - id: AgentEngineId - label: string - vendor: string - availability: AgentRuntimeAvailability - features: AgentRuntimeFeature[] - defaultModelId?: string - models?: AgentRuntimeModel[] + id: AgentEngineId; + label: string; + vendor: string; + availability: AgentRuntimeAvailability; + features: AgentRuntimeFeature[]; + defaultModelId?: string; + models?: AgentRuntimeModel[]; + providerInstances?: AgentRuntimeProviderInstance[]; + version?: string | null; + versionAdvisory?: AgentRuntimeVersionAdvisory | null; + updateState?: AgentRuntimeUpdateState | null; configRoots: { - user?: string - project?: string - sessions?: string - } - notes?: string[] + user?: string; + project?: string; + sessions?: string; + }; + notes?: string[]; } export interface AgentRuntimeSessionRef { - subChatId: string - chatId: string - engineId: AgentEngineId - nativeSessionId?: string | null - modelId?: string | null - permissionMode: AgentPermissionMode - cwd: string - projectPath?: string | null - runtimeConfigDir?: string | null - metadata?: Record + subChatId: string; + chatId: string; + engineId: AgentEngineId; + providerInstanceId?: string | null; + nativeSessionId?: string | null; + modelId?: string | null; + modelSelection?: { + instanceId: string; + modelId: string; + options?: Record; + } | null; + permissionMode: AgentPermissionMode; + cwd: string; + projectPath?: string | null; + runtimeConfigDir?: string | null; + metadata?: Record; } export interface AgentRuntimeStartRequest { - session: AgentRuntimeSessionRef - prompt: string + runId?: string; + session: AgentRuntimeSessionRef; + prompt: string; images?: Array<{ - base64Data: string - mediaType: string - filename?: string - }> - forceNewSession?: boolean + base64Data: string; + mediaType: string; + filename?: string; + }>; + forceNewSession?: boolean; +} + +export type AgentRuntimeRunAction = "start" | "resume"; + +export type AgentRuntimeRunStatus = + | "running" + | "success" + | "error" + | "cancelled" + | "unsupported"; + +export interface AgentRuntimeRunReceipt { + version: 1; + runId: string; + action: AgentRuntimeRunAction; + engineId: AgentEngineId; + subChatId: string; + chatId: string; + status: AgentRuntimeRunStatus; + nativeSessionId?: string | null; + resultSubtype?: "success" | "error" | "cancelled" | null; + startedAt: string; + updatedAt: string; + completedAt?: string | null; + error?: string; + metadata?: Record; +} + +export interface AgentRuntimeStopRequest { + session: AgentRuntimeSessionRef; + runId: string; + reason?: string; +} + +export interface AgentRuntimeToolResultRequest { + session: AgentRuntimeSessionRef; + runId: string; + toolCallId: string; + result: unknown; + isError?: boolean; +} + +export interface AgentRuntimeControlResult { + runId: string; + status: "accepted" | "cancelled" | "unsupported" | "not-found" | "error"; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeThreadReadRequest { + session: AgentRuntimeSessionRef; + includeTurns?: boolean; +} + +export interface AgentRuntimeThreadReadResult { + status: "success" | "unsupported" | "error"; + threadId?: string | null; + thread?: unknown; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeThreadForkRequest { + session: AgentRuntimeSessionRef; +} + +export interface AgentRuntimeThreadForkResult { + status: "success" | "unsupported" | "error"; + sourceThreadId?: string | null; + threadId?: string | null; + thread?: unknown; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeThreadTurnDiffRequest { + session: AgentRuntimeSessionRef; + threadId?: string | null; + fromTurnCount: number; + toTurnCount: number; + ignoreWhitespace?: boolean; +} + +export interface AgentRuntimeThreadFullDiffRequest { + session: AgentRuntimeSessionRef; + threadId?: string | null; + toTurnCount: number; + ignoreWhitespace?: boolean; +} + +export interface AgentRuntimeThreadDiffResult { + status: "success" | "unsupported" | "error"; + threadId?: string | null; + fromTurnCount?: number; + toTurnCount?: number; + diff?: string; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeThreadListRequest { + session: AgentRuntimeSessionRef; + archived?: boolean | null; + cursor?: string | null; + cwd?: string | string[] | null; + limit?: number | null; + modelProviders?: string[] | null; + searchTerm?: string | null; + sortDirection?: "asc" | "desc" | null; + sortKey?: "created_at" | "updated_at" | null; + sourceKinds?: string[] | null; + useStateDbOnly?: boolean; +} + +export interface AgentRuntimeThreadListResult { + status: "success" | "unsupported" | "error"; + threads?: unknown[]; + nextCursor?: string | null; + backwardsCursor?: string | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeThreadLoadedListRequest { + session: AgentRuntimeSessionRef; + cursor?: string | null; + limit?: number | null; +} + +export interface AgentRuntimeThreadLoadedListResult { + status: "success" | "unsupported" | "error"; + threadIds?: string[]; + nextCursor?: string | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export type AgentRuntimeThreadControlAction = + | "archive" + | "unarchive" + | "delete"; + +export interface AgentRuntimeThreadControlRequest { + session: AgentRuntimeSessionRef; + action: AgentRuntimeThreadControlAction; + threadId: string; +} + +export interface AgentRuntimeThreadControlResult { + status: "success" | "unsupported" | "error"; + action: AgentRuntimeThreadControlAction; + threadId?: string | null; + thread?: unknown; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeThreadNameSetRequest { + session: AgentRuntimeSessionRef; + threadId: string; + name: string; +} + +export interface AgentRuntimeThreadNameSetResult { + status: "success" | "unsupported" | "error"; + threadId?: string | null; + name?: string | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeThreadMetadataUpdateRequest { + session: AgentRuntimeSessionRef; + threadId: string; + gitInfo?: { + branch?: string | null; + originUrl?: string | null; + sha?: string | null; + } | null; +} + +export interface AgentRuntimeThreadMetadataUpdateResult { + status: "success" | "unsupported" | "error"; + threadId?: string | null; + thread?: unknown; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export type AgentRuntimeThreadGoalStatus = + | "active" + | "paused" + | "blocked" + | "usageLimited" + | "budgetLimited" + | "complete"; + +export interface AgentRuntimeThreadGoal { + createdAt?: number; + objective?: string; + status?: AgentRuntimeThreadGoalStatus | (string & {}); + threadId?: string; + timeUsedSeconds?: number; + tokenBudget?: number | null; + tokensUsed?: number; + updatedAt?: number; +} + +export interface AgentRuntimeThreadGoalGetRequest { + session: AgentRuntimeSessionRef; + threadId: string; +} + +export interface AgentRuntimeThreadGoalGetResult { + status: "success" | "unsupported" | "error"; + threadId?: string | null; + goal?: AgentRuntimeThreadGoal | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeThreadGoalSetRequest { + session: AgentRuntimeSessionRef; + threadId: string; + objective?: string | null; + status?: AgentRuntimeThreadGoalStatus | null; + tokenBudget?: number | null; +} + +export interface AgentRuntimeThreadGoalSetResult { + status: "success" | "unsupported" | "error"; + threadId?: string | null; + goal?: AgentRuntimeThreadGoal | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeThreadGoalClearRequest { + session: AgentRuntimeSessionRef; + threadId: string; +} + +export interface AgentRuntimeThreadGoalClearResult { + status: "success" | "unsupported" | "error"; + threadId?: string | null; + cleared?: boolean; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeThreadRollbackRequest { + session: AgentRuntimeSessionRef; + numTurns: number; +} + +export interface AgentRuntimeThreadRollbackResult { + status: "success" | "unsupported" | "error"; + threadId?: string | null; + numTurns: number; + thread?: unknown; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeConfigReadRequest { + session: AgentRuntimeSessionRef; + cwd?: string | null; + includeLayers?: boolean; +} + +export interface AgentRuntimeConfigReadResult { + status: "success" | "unsupported" | "error"; + config?: unknown; + layers?: unknown[] | null; + origins?: Record; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export type AgentRuntimeConfigWriteMergeStrategy = "replace" | "upsert"; + +export interface AgentRuntimeConfigWriteEdit { + keyPath: string; + mergeStrategy?: AgentRuntimeConfigWriteMergeStrategy; + value: unknown; +} + +export interface AgentRuntimeConfigValueWriteRequest { + session: AgentRuntimeSessionRef; + keyPath: string; + value: unknown; + mergeStrategy?: AgentRuntimeConfigWriteMergeStrategy; + filePath?: string | null; + expectedVersion?: string | null; +} + +export interface AgentRuntimeConfigBatchWriteRequest { + session: AgentRuntimeSessionRef; + edits: AgentRuntimeConfigWriteEdit[]; + filePath?: string | null; + expectedVersion?: string | null; + reloadUserConfig?: boolean; +} + +export interface AgentRuntimeConfigWriteResult { + status: "success" | "unsupported" | "error"; + filePath?: string | null; + writeStatus?: "ok" | "okOverridden" | (string & {}); + version?: string | null; + overriddenMetadata?: unknown | null; + message?: string; + updatedAt: string; + metadata?: Record; } -export type AgentRuntimeBlockStatus = CodexBlockStatus | "blocked" +export interface AgentRuntimeConfigRequirementsReadRequest { + session: AgentRuntimeSessionRef; +} + +export interface AgentRuntimeConfigRequirementsReadResult { + status: "success" | "unsupported" | "error"; + requirements?: unknown | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimePermissionProfileListRequest { + session: AgentRuntimeSessionRef; + cwd?: string | null; + cursor?: string | null; + limit?: number | null; +} + +export interface AgentRuntimePermissionProfileListResult { + status: "success" | "unsupported" | "error"; + profiles?: unknown[]; + nextCursor?: string | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export type AgentRuntimeMcpServerStatusDetail = "full" | "toolsAndAuthOnly"; + +export interface AgentRuntimeMcpServerStatusListRequest { + session: AgentRuntimeSessionRef; + cursor?: string | null; + detail?: AgentRuntimeMcpServerStatusDetail | null; + limit?: number | null; + threadId?: string | null; +} + +export interface AgentRuntimeMcpServerStatusListResult { + status: "success" | "unsupported" | "error"; + servers?: unknown[]; + nextCursor?: string | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeMcpServerConfigReloadRequest { + session: AgentRuntimeSessionRef; +} + +export interface AgentRuntimeMcpServerConfigReloadResult { + status: "success" | "unsupported" | "error"; + reloaded?: boolean; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeSkillListRequest { + session: AgentRuntimeSessionRef; + cwds?: string[] | null; + forceReload?: boolean | null; +} + +export interface AgentRuntimeSkillListResult { + status: "success" | "unsupported" | "error"; + entries?: unknown[]; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeHookListRequest { + session: AgentRuntimeSessionRef; + cwds?: string[] | null; +} + +export interface AgentRuntimeHookListResult { + status: "success" | "unsupported" | "error"; + entries?: unknown[]; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeAppListRequest { + session: AgentRuntimeSessionRef; + cursor?: string | null; + forceRefetch?: boolean | null; + limit?: number | null; + threadId?: string | null; +} + +export interface AgentRuntimeAppListResult { + status: "success" | "unsupported" | "error"; + apps?: unknown[]; + nextCursor?: string | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export type AgentRuntimePluginMarketplaceKind = + | "local" + | "vertical" + | "workspace-directory" + | "shared-with-me"; + +export interface AgentRuntimePluginListRequest { + session: AgentRuntimeSessionRef; + cwds?: string[] | null; + marketplaceKinds?: AgentRuntimePluginMarketplaceKind[] | null; +} + +export interface AgentRuntimePluginListResult { + status: "success" | "unsupported" | "error"; + marketplaces?: unknown[]; + featuredPluginIds?: string[]; + marketplaceLoadErrors?: unknown[]; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimePluginInstalledRequest { + session: AgentRuntimeSessionRef; + cwds?: string[] | null; + installSuggestionPluginNames?: string[] | null; +} + +export interface AgentRuntimePluginInstalledResult { + status: "success" | "unsupported" | "error"; + marketplaces?: unknown[]; + marketplaceLoadErrors?: unknown[]; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimePluginReadRequest { + session: AgentRuntimeSessionRef; + pluginName: string; + marketplacePath?: string | null; + remoteMarketplaceName?: string | null; +} + +export interface AgentRuntimePluginReadResult { + status: "success" | "unsupported" | "error"; + plugin?: unknown; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimePluginInstallRequest { + session: AgentRuntimeSessionRef; + pluginName: string; + marketplacePath?: string | null; + remoteMarketplaceName?: string | null; +} + +export interface AgentRuntimePluginInstallResult { + status: "success" | "unsupported" | "error"; + appsNeedingAuth?: unknown[]; + authPolicy?: string | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export type AgentRuntimeExternalAgentConfigMigrationItemType = + | "AGENTS_MD" + | "CONFIG" + | "SKILLS" + | "PLUGINS" + | "MCP_SERVER_CONFIG" + | "SUBAGENTS" + | "HOOKS" + | "COMMANDS" + | "SESSIONS"; + +export interface AgentRuntimeExternalAgentConfigMigrationDetails { + commands?: Array<{ name: string }>; + hooks?: Array<{ name: string }>; + mcpServers?: Array<{ name: string }>; + plugins?: Array<{ marketplaceName: string; pluginNames: string[] }>; + sessions?: Array<{ cwd: string; path: string; title?: string | null }>; + subagents?: Array<{ name: string }>; +} + +export interface AgentRuntimeExternalAgentConfigMigrationItem { + cwd?: string | null; + description: string; + details?: AgentRuntimeExternalAgentConfigMigrationDetails | null; + itemType: AgentRuntimeExternalAgentConfigMigrationItemType; +} + +export interface AgentRuntimeExternalAgentConfigDetectRequest { + session: AgentRuntimeSessionRef; + cwds?: string[] | null; + includeHome?: boolean | null; +} + +export interface AgentRuntimeExternalAgentConfigDetectResult { + status: "success" | "unsupported" | "error"; + items?: AgentRuntimeExternalAgentConfigMigrationItem[]; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeExternalAgentConfigImportRequest { + session: AgentRuntimeSessionRef; + migrationItems: AgentRuntimeExternalAgentConfigMigrationItem[]; +} + +export interface AgentRuntimeExternalAgentConfigImportResult { + status: "success" | "unsupported" | "error"; + importedCount?: number; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeMcpServerOauthLoginRequest { + session: AgentRuntimeSessionRef; + name: string; + scopes?: string[] | null; + timeoutSecs?: number | null; +} + +export interface AgentRuntimeMcpServerOauthLoginResult { + status: "success" | "unsupported" | "error"; + authorizationUrl?: string | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeModelListRequest { + session: AgentRuntimeSessionRef; + cursor?: string | null; + includeHidden?: boolean | null; + limit?: number | null; +} + +export interface AgentRuntimeModelListResult { + status: "success" | "unsupported" | "error"; + models?: unknown[]; + nextCursor?: string | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export type AgentRuntimeAccountLoginType = + | "apiKey" + | "chatgpt" + | "chatgptDeviceCode" + | "chatgptAuthTokens"; + +export interface AgentRuntimeAccountLoginStartRequest { + session: AgentRuntimeSessionRef; + type: AgentRuntimeAccountLoginType; + apiKey?: string | null; + codexStreamlinedLogin?: boolean; + accessToken?: string | null; + chatgptAccountId?: string | null; + chatgptPlanType?: string | null; +} + +export interface AgentRuntimeAccountLoginStartResult { + status: "success" | "unsupported" | "error"; + type?: AgentRuntimeAccountLoginType | (string & {}); + authUrl?: string | null; + loginId?: string | null; + verificationUrl?: string | null; + userCode?: string | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeAccountLoginCancelRequest { + session: AgentRuntimeSessionRef; + loginId: string; +} + +export interface AgentRuntimeAccountLoginCancelResult { + status: "success" | "unsupported" | "error"; + cancelStatus?: "canceled" | "notFound" | (string & {}); + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeAccountLogoutRequest { + session: AgentRuntimeSessionRef; +} + +export interface AgentRuntimeAccountLogoutResult { + status: "success" | "unsupported" | "error"; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeAccountReadRequest { + session: AgentRuntimeSessionRef; + refreshToken?: boolean; +} + +export interface AgentRuntimeAccountReadResult { + status: "success" | "unsupported" | "error"; + account?: unknown | null; + requiresOpenaiAuth?: boolean; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeAccountRateLimitsReadRequest { + session: AgentRuntimeSessionRef; +} + +export interface AgentRuntimeAccountRateLimitsReadResult { + status: "success" | "unsupported" | "error"; + rateLimits?: unknown; + rateLimitsByLimitId?: Record | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export interface AgentRuntimeAccountUsageReadRequest { + session: AgentRuntimeSessionRef; +} + +export interface AgentRuntimeAccountUsageReadResult { + status: "success" | "unsupported" | "error"; + summary?: unknown; + dailyUsageBuckets?: unknown[] | null; + message?: string; + updatedAt: string; + metadata?: Record; +} + +export type AgentRuntimeBlockStatus = CodexBlockStatus | "blocked"; export type AgentRuntimeAutomationAction = | "created" @@ -122,111 +871,139 @@ export type AgentRuntimeAutomationAction = | "completed" | "failed" | "paused" - | "resumed" + | "resumed"; export interface AgentRuntimeBaseConversationBlock { - id: string - type: string - turnId?: string - status?: AgentRuntimeBlockStatus - title?: string - summary?: string - input?: unknown - output?: unknown - metadata?: Record + id: string; + type: string; + turnId?: string; + status?: AgentRuntimeBlockStatus; + title?: string; + summary?: string; + input?: unknown; + output?: unknown; + metadata?: Record; } -export interface AgentRuntimeAutomationUpdateBlock - extends AgentRuntimeBaseConversationBlock { - type: "automation-update" - automationId?: string - action?: AgentRuntimeAutomationAction | (string & {}) +export interface AgentRuntimeAutomationUpdateBlock extends AgentRuntimeBaseConversationBlock { + type: "automation-update"; + automationId?: string; + action?: AgentRuntimeAutomationAction | (string & {}); } -export interface AgentRuntimeMultiAgentActionBlock - extends AgentRuntimeBaseConversationBlock { - type: "multi-agent-action" - agentId?: string - agentLabel?: string - action?: "spawn" | "message" | "handoff" | "complete" | "failed" | (string & {}) +export interface AgentRuntimeMultiAgentActionBlock extends AgentRuntimeBaseConversationBlock { + type: "multi-agent-action"; + agentId?: string; + agentLabel?: string; + action?: + | "spawn" + | "message" + | "handoff" + | "complete" + | "failed" + | (string & {}); } -export interface AgentRuntimeContextCompactionBlock - extends AgentRuntimeBaseConversationBlock { - type: "context-compaction" - previousInputTokens?: number - nextInputTokens?: number - droppedMessages?: number +export interface AgentRuntimeContextCompactionBlock extends AgentRuntimeBaseConversationBlock { + type: "context-compaction"; + previousInputTokens?: number; + nextInputTokens?: number; + droppedMessages?: number; } -export interface AgentRuntimeModelChangeBlock - extends AgentRuntimeBaseConversationBlock { - type: "model-change" | "model-reroute" - fromModelId?: string - toModelId?: string - reason?: string +export interface AgentRuntimeModelChangeBlock extends AgentRuntimeBaseConversationBlock { + type: "model-change" | "model-reroute"; + fromModelId?: string; + toModelId?: string; + reason?: string; } -export interface AgentRuntimeGoalStatusBlock - extends AgentRuntimeBaseConversationBlock { - type: "goal-status" - goalId?: string +export interface AgentRuntimeGoalStatusBlock extends AgentRuntimeBaseConversationBlock { + type: "goal-status"; + goalId?: string; } -export interface AgentRuntimeRealtimeStateBlock - extends AgentRuntimeBaseConversationBlock { - type: "realtime-state" | "dictation-state" - mode?: "dictation" | "voice-only" | "voice_and_screen" | (string & {}) - microphoneDeviceId?: string +export interface AgentRuntimeRealtimeStateBlock extends AgentRuntimeBaseConversationBlock { + type: "realtime-state" | "dictation-state"; + mode?: "dictation" | "voice-only" | "voice_and_screen" | (string & {}); + microphoneDeviceId?: string; } -export interface AgentRuntimeQueuedFollowUpBlock - extends AgentRuntimeBaseConversationBlock { - type: "queued-follow-up" - followUpId?: string - queueState?: "queued" | "sending" | "sent" | "failed" | "cancelled" | (string & {}) +export interface AgentRuntimeQueuedFollowUpBlock extends AgentRuntimeBaseConversationBlock { + type: "queued-follow-up"; + followUpId?: string; + queueState?: + | "queued" + | "sending" + | "sent" + | "failed" + | "cancelled" + | (string & {}); } -export interface AgentRuntimeUsageStatusBlock - extends AgentRuntimeBaseConversationBlock { - type: "rate-limit-status" | "usage-status" - window?: "hourly" | "daily" | "weekly" | "monthly" | "annual" | (string & {}) - remaining?: number - limit?: number - resetAt?: string +export interface AgentRuntimeUsageStatusBlock extends AgentRuntimeBaseConversationBlock { + type: "rate-limit-status" | "usage-status"; + window?: "hourly" | "daily" | "weekly" | "monthly" | "annual" | (string & {}); + remaining?: number; + limit?: number; + resetAt?: string; } -export interface AgentRuntimeProjectEventBlock - extends AgentRuntimeBaseConversationBlock { - type: "project-event" - projectId?: string - projectName?: string - action?: "created" | "updated" | "pinned" | "unpinned" | "selected" | (string & {}) +export interface AgentRuntimeProjectEventBlock extends AgentRuntimeBaseConversationBlock { + type: "project-event"; + projectId?: string; + projectName?: string; + action?: + | "created" + | "updated" + | "pinned" + | "unpinned" + | "selected" + | (string & {}); } -export interface AgentRuntimeLibraryArtifactBlock - extends AgentRuntimeBaseConversationBlock { - type: "library-artifact" - artifactId?: string - artifactKind?: "file" | "image" | "site" | "document" | "spreadsheet" | (string & {}) - path?: string - url?: string +export interface AgentRuntimeLibraryArtifactBlock extends AgentRuntimeBaseConversationBlock { + type: "library-artifact"; + artifactId?: string; + artifactKind?: + | "file" + | "image" + | "site" + | "document" + | "spreadsheet" + | (string & {}); + path?: string; + url?: string; } -export interface AgentRuntimePullRequestStatusBlock - extends AgentRuntimeBaseConversationBlock { - type: "pull-request-status" - pullRequestId?: string - url?: string - reviewState?: "queued" | "running" | "changes-requested" | "approved" | "failed" | (string & {}) - checksState?: "pending" | "running" | "passing" | "failing" | "unknown" | (string & {}) +export interface AgentRuntimePullRequestStatusBlock extends AgentRuntimeBaseConversationBlock { + type: "pull-request-status"; + pullRequestId?: string; + url?: string; + reviewState?: + | "queued" + | "running" + | "changes-requested" + | "approved" + | "failed" + | (string & {}); + checksState?: + | "pending" + | "running" + | "passing" + | "failing" + | "unknown" + | (string & {}); } -export interface AgentRuntimeDiagnosticSnapshotBlock - extends AgentRuntimeBaseConversationBlock { - type: "diagnostic-snapshot" - snapshotKind?: "child-processes" | "renderer-memory" | "trace-recording" | (string & {}) - path?: string +export interface AgentRuntimeDiagnosticSnapshotBlock extends AgentRuntimeBaseConversationBlock { + type: "diagnostic-snapshot"; + snapshotKind?: + | "child-processes" + | "renderer-memory" + | "trace-recording" + | (string & {}); + path?: string; } export type AgentRuntimeConversationBlock = @@ -242,53 +1019,186 @@ export type AgentRuntimeConversationBlock = | AgentRuntimeProjectEventBlock | AgentRuntimeLibraryArtifactBlock | AgentRuntimePullRequestStatusBlock - | AgentRuntimeDiagnosticSnapshotBlock + | AgentRuntimeDiagnosticSnapshotBlock; export type AgentRuntimeStreamEvent = | { - type: "text" - text: string + type: "text"; + text: string; } | { - type: "tool-call" - id?: string - name: string - input?: unknown + type: "tool-call"; + id?: string; + name: string; + input?: unknown; } | { - type: "tool-result" - id?: string - name?: string - result?: unknown + type: "tool-result"; + id?: string; + name?: string; + result?: unknown; } | { - type: "usage" - inputTokens?: number - outputTokens?: number - totalTokens?: number - modelContextWindow?: number + type: "usage"; + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + modelContextWindow?: number; + usedTokens?: number; + totalProcessedTokens?: number; + maxTokens?: number; + cachedInputTokens?: number; + reasoningOutputTokens?: number; + lastUsedTokens?: number; + lastInputTokens?: number; + lastCachedInputTokens?: number; + lastOutputTokens?: number; + lastReasoningOutputTokens?: number; + toolUses?: number; + durationMs?: number; + compactsAutomatically?: boolean; } | { - type: "conversation-block" - block: AgentRuntimeConversationBlock + type: "conversation-block"; + block: AgentRuntimeConversationBlock; } | { - type: "conversation-block-update" - id: string - patch: Partial + type: "conversation-block-update"; + id: string; + patch: Partial; } | { - type: "auth-error" | "error" - message: string + type: "auth-error" | "error"; + message: string; } | { - type: "finish" - nativeSessionId?: string | null - resultSubtype?: "success" | "error" | "cancelled" - } + type: "finish"; + nativeSessionId?: string | null; + resultSubtype?: "success" | "error" | "cancelled"; + }; export interface AgentRuntimeAdapter { - manifest: AgentRuntimeManifest - inspect?(session: AgentRuntimeSessionRef): Promise - canStart(session: AgentRuntimeSessionRef): Promise + manifest: AgentRuntimeManifest; + inspect?(session: AgentRuntimeSessionRef): Promise; + canStart(session: AgentRuntimeSessionRef): Promise; + start(request: AgentRuntimeStartRequest): Promise; + resume(request: AgentRuntimeStartRequest): Promise; + stream( + request: AgentRuntimeStartRequest, + ): AsyncIterable; + stop(request: AgentRuntimeStopRequest): Promise; + submitToolResult( + request: AgentRuntimeToolResultRequest, + ): Promise; + readConfig?( + request: AgentRuntimeConfigReadRequest, + ): Promise; + writeConfigValue?( + request: AgentRuntimeConfigValueWriteRequest, + ): Promise; + batchWriteConfig?( + request: AgentRuntimeConfigBatchWriteRequest, + ): Promise; + readConfigRequirements?( + request: AgentRuntimeConfigRequirementsReadRequest, + ): Promise; + listPermissionProfiles?( + request: AgentRuntimePermissionProfileListRequest, + ): Promise; + listMcpServerStatuses?( + request: AgentRuntimeMcpServerStatusListRequest, + ): Promise; + reloadMcpServerConfig?( + request: AgentRuntimeMcpServerConfigReloadRequest, + ): Promise; + listSkills?( + request: AgentRuntimeSkillListRequest, + ): Promise; + listHooks?( + request: AgentRuntimeHookListRequest, + ): Promise; + listApps?( + request: AgentRuntimeAppListRequest, + ): Promise; + listPlugins?( + request: AgentRuntimePluginListRequest, + ): Promise; + listInstalledPlugins?( + request: AgentRuntimePluginInstalledRequest, + ): Promise; + readPlugin?( + request: AgentRuntimePluginReadRequest, + ): Promise; + installPlugin?( + request: AgentRuntimePluginInstallRequest, + ): Promise; + detectExternalAgentConfig?( + request: AgentRuntimeExternalAgentConfigDetectRequest, + ): Promise; + importExternalAgentConfig?( + request: AgentRuntimeExternalAgentConfigImportRequest, + ): Promise; + startMcpServerOauthLogin?( + request: AgentRuntimeMcpServerOauthLoginRequest, + ): Promise; + listModels?( + request: AgentRuntimeModelListRequest, + ): Promise; + startAccountLogin?( + request: AgentRuntimeAccountLoginStartRequest, + ): Promise; + cancelAccountLogin?( + request: AgentRuntimeAccountLoginCancelRequest, + ): Promise; + logoutAccount?( + request: AgentRuntimeAccountLogoutRequest, + ): Promise; + readAccount?( + request: AgentRuntimeAccountReadRequest, + ): Promise; + readAccountRateLimits?( + request: AgentRuntimeAccountRateLimitsReadRequest, + ): Promise; + readAccountUsage?( + request: AgentRuntimeAccountUsageReadRequest, + ): Promise; + readThread?( + request: AgentRuntimeThreadReadRequest, + ): Promise; + forkThread?( + request: AgentRuntimeThreadForkRequest, + ): Promise; + getThreadTurnDiff?( + request: AgentRuntimeThreadTurnDiffRequest, + ): Promise; + getThreadFullDiff?( + request: AgentRuntimeThreadFullDiffRequest, + ): Promise; + listThreads?( + request: AgentRuntimeThreadListRequest, + ): Promise; + listLoadedThreads?( + request: AgentRuntimeThreadLoadedListRequest, + ): Promise; + controlThread?( + request: AgentRuntimeThreadControlRequest, + ): Promise; + setThreadName?( + request: AgentRuntimeThreadNameSetRequest, + ): Promise; + updateThreadMetadata?( + request: AgentRuntimeThreadMetadataUpdateRequest, + ): Promise; + getThreadGoal?( + request: AgentRuntimeThreadGoalGetRequest, + ): Promise; + setThreadGoal?( + request: AgentRuntimeThreadGoalSetRequest, + ): Promise; + clearThreadGoal?( + request: AgentRuntimeThreadGoalClearRequest, + ): Promise; + rollbackThread?( + request: AgentRuntimeThreadRollbackRequest, + ): Promise; } diff --git a/src/main/lib/auto-updater.ts b/src/main/lib/auto-updater.ts index 59c2c0cdf..190f42235 100644 --- a/src/main/lib/auto-updater.ts +++ b/src/main/lib/auto-updater.ts @@ -1,6 +1,10 @@ import { BrowserWindow, ipcMain, app } from "electron" import log from "electron-log" -import { autoUpdater, type UpdateInfo, type ProgressInfo } from "electron-updater" +import { + autoUpdater, + type UpdateInfo, + type ProgressInfo, +} from "electron-updater" import { readFileSync, writeFileSync, existsSync } from "fs" import { join } from "path" @@ -65,6 +69,7 @@ function saveChannel(channel: UpdateChannel): void { } let getAllWindows: (() => BrowserWindow[]) | null = null +let updateIpcHandlersRegistered = false /** * Send update event to all renderer windows @@ -110,7 +115,7 @@ export async function initAutoUpdater(getWindows: () => BrowserWindow[]) { // Add cache-busting to update requests autoUpdater.requestHeaders = { "Cache-Control": "no-cache, no-store, must-revalidate", - "Pragma": "no-cache", + Pragma: "no-cache", } // Event: Checking for updates @@ -178,15 +183,22 @@ export async function initAutoUpdater(getWindows: () => BrowserWindow[]) { }) // Register IPC handlers - registerIpcHandlers() + registerAutoUpdaterIpcHandlers() log.info("[AutoUpdater] Initialized with feed URL:", CDN_BASE) } /** - * Register IPC handlers for update operations + * Register IPC handlers for update operations. + * + * The renderer asks for update state/channel in both packaged and dev builds. + * Dev still needs these handlers registered, but should not initialize + * electron-updater or perform network update checks. */ -function registerIpcHandlers() { +export function registerAutoUpdaterIpcHandlers() { + if (updateIpcHandlersRegistered) return + updateIpcHandlersRegistered = true + // Check for updates ipcMain.handle("update:check", async (_event, force?: boolean) => { if (!app.isPackaged) { @@ -201,7 +213,10 @@ function registerIpcHandlers() { provider: "generic", url: `${CDN_BASE}${cacheBuster}`, }) - log.info("[AutoUpdater] Force check with cache-busting:", `${CDN_BASE}${cacheBuster}`) + log.info( + "[AutoUpdater] Force check with cache-busting:", + `${CDN_BASE}${cacheBuster}`, + ) } const result = await autoUpdater.checkForUpdates() // Reset feed URL back to normal after force check diff --git a/src/main/lib/claude-plugin-settings.ts b/src/main/lib/claude-plugin-settings.ts new file mode 100644 index 000000000..8453dab23 --- /dev/null +++ b/src/main/lib/claude-plugin-settings.ts @@ -0,0 +1,77 @@ +import * as fs from "fs/promises" +import * as os from "os" +import * as path from "path" + +const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json") + +let enabledPluginsCache: { plugins: string[]; timestamp: number } | null = null +const ENABLED_PLUGINS_CACHE_TTL_MS = 5000 + +let approvedMcpCache: { servers: string[]; timestamp: number } | null = null +const APPROVED_MCP_CACHE_TTL_MS = 5000 + +export function invalidateEnabledPluginsCache(): void { + enabledPluginsCache = null +} + +export function invalidateApprovedMcpCache(): void { + approvedMcpCache = null +} + +export async function readClaudeSettings(): Promise> { + try { + const content = await fs.readFile(CLAUDE_SETTINGS_PATH, "utf-8") + return JSON.parse(content) + } catch { + return {} + } +} + +export async function writeClaudeSettings(settings: Record): Promise { + const dir = path.dirname(CLAUDE_SETTINGS_PATH) + await fs.mkdir(dir, { recursive: true }) + await fs.writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8") +} + +export async function getEnabledPlugins(): Promise { + if ( + enabledPluginsCache && + Date.now() - enabledPluginsCache.timestamp < ENABLED_PLUGINS_CACHE_TTL_MS + ) { + return enabledPluginsCache.plugins + } + + const settings = await readClaudeSettings() + const plugins = Array.isArray(settings.enabledPlugins) + ? settings.enabledPlugins as string[] + : [] + + enabledPluginsCache = { plugins, timestamp: Date.now() } + return plugins +} + +export async function getApprovedPluginMcpServers(): Promise { + if ( + approvedMcpCache && + Date.now() - approvedMcpCache.timestamp < APPROVED_MCP_CACHE_TTL_MS + ) { + return approvedMcpCache.servers + } + + const settings = await readClaudeSettings() + const servers = Array.isArray(settings.approvedPluginMcpServers) + ? settings.approvedPluginMcpServers as string[] + : [] + + approvedMcpCache = { servers, timestamp: Date.now() } + return servers +} + +export async function isPluginMcpApproved( + pluginSource: string, + serverName: string, +): Promise { + const approved = await getApprovedPluginMcpServers() + const identifier = `${pluginSource}:${serverName}` + return approved.includes(identifier) +} diff --git a/src/main/lib/claude/env.ts b/src/main/lib/claude/env.ts index 0ea2ab0cf..e95a8c81c 100644 --- a/src/main/lib/claude/env.ts +++ b/src/main/lib/claude/env.ts @@ -36,6 +36,65 @@ const STRIPPED_ENV_KEYS = !app.isPackaged // Cache the bundled binary path (only compute once) let cachedBinaryPath: string | null = null let binaryPathComputed = false +let cachedExecutableResolution: ClaudeExecutableResolution | null = null + +export type ClaudeExecutableSource = "bundled" | "system" + +export type ClaudeExecutableResolution = { + path: string + source: ClaudeExecutableSource + bundledPath: string + reason?: string +} + +function isExecutableFile(filePath: string): boolean { + try { + const stats = fs.statSync(filePath) + if (!stats.isFile()) return false + if (process.platform === "win32") return true + return (stats.mode & fs.constants.X_OK) !== 0 + } catch { + return false + } +} + +function findExecutableInPath( + binaryName: string, + pathValue: string | undefined, +): string | null { + if (!pathValue) return null + + for (const rawDir of pathValue.split(path.delimiter)) { + const dir = rawDir.trim() + if (!dir) continue + + const candidate = path.join(dir, binaryName) + if (isExecutableFile(candidate)) { + return candidate + } + } + + return null +} + +function getCommonUserBinPaths(): string[] { + const home = os.homedir() + const paths = [ + path.join(home, ".local/bin"), + path.join(home, "bin"), + ] + + if (process.platform === "darwin") { + paths.push("/opt/homebrew/bin", "/usr/local/bin") + } + + if (process.platform === "win32") { + const appData = process.env.APPDATA + if (appData) paths.push(path.join(appData, "npm")) + } + + return paths +} /** * Get path to the bundled Claude binary. @@ -104,6 +163,70 @@ export function getBundledClaudeBinaryPath(): string { return binaryPath } +/** + * Resolve the Claude Code executable used by the SDK. + * Prefer the app-bundled binary for packaged builds, but allow a system + * `claude` fallback in development so runtime switching can be tested without + * downloading the bundle artifact first. + */ +export function resolveClaudeCodeExecutable(): ClaudeExecutableResolution { + if (cachedExecutableResolution) { + return cachedExecutableResolution + } + + const bundledPath = getBundledClaudeBinaryPath() + if (isExecutableFile(bundledPath)) { + cachedExecutableResolution = { + path: bundledPath, + source: "bundled", + bundledPath, + } + return cachedExecutableResolution + } + + const binaryName = process.platform === "win32" ? "claude.exe" : "claude" + const pathValues = [process.env.PATH] + + try { + const shellEnv = getClaudeShellEnvironment() + pathValues.unshift(shellEnv.PATH) + } catch (error) { + console.warn("[claude-binary] Failed to inspect shell PATH:", error) + } + + pathValues.push(getCommonUserBinPaths().join(path.delimiter)) + const systemPath = findExecutableInPath( + binaryName, + pathValues.filter(Boolean).join(path.delimiter), + ) + if (systemPath) { + cachedExecutableResolution = { + path: systemPath, + source: "system", + bundledPath, + reason: "Bundled Claude Code binary is missing; using system claude.", + } + console.warn( + "[claude-binary] Bundled binary missing; using system Claude Code:", + systemPath, + ) + return cachedExecutableResolution + } + + cachedExecutableResolution = { + path: bundledPath, + source: "bundled", + bundledPath, + reason: + "Bundled Claude Code binary is missing and no system claude executable was found on PATH.", + } + return cachedExecutableResolution +} + +export function getClaudeCodeExecutablePath(): string { + return resolveClaudeCodeExecutable().path +} + /** * Parse environment variables from shell output */ diff --git a/src/main/lib/claude/index.ts b/src/main/lib/claude/index.ts index 9d42c7ead..e38a48bdc 100644 --- a/src/main/lib/claude/index.ts +++ b/src/main/lib/claude/index.ts @@ -11,6 +11,9 @@ export { clearClaudeEnvCache, logClaudeEnv, getBundledClaudeBinaryPath, + getClaudeCodeExecutablePath, + resolveClaudeCodeExecutable, } from "./env" +export type { ClaudeExecutableResolution } from "./env" export { checkOfflineFallback } from "./offline-handler" export type { OfflineCheckResult, CustomClaudeConfig } from "./offline-handler" diff --git a/src/main/lib/claude/transform.ts b/src/main/lib/claude/transform.ts index 0d1a1cec4..11af6ef57 100644 --- a/src/main/lib/claude/transform.ts +++ b/src/main/lib/claude/transform.ts @@ -1,6 +1,9 @@ import type { MCPServer, MCPServerStatus, MessageMetadata, UIMessageChunk } from "./types"; -export function createTransformer(options?: { isUsingOllama?: boolean }) { +export function createTransformer(options?: { + emitSdkMessageUuid?: boolean + isUsingOllama?: boolean +}) { const isUsingOllama = options?.isUsingOllama === true let textId: string | null = null let textStarted = false @@ -171,7 +174,7 @@ export function createTransformer(options?: { isUsingOllama?: boolean }) { yield { type: "tool-input-start", toolCallId: currentToolCallId, - toolName: currentToolName, + toolName: currentToolName ?? "unknown", } } diff --git a/src/main/lib/claude/types.ts b/src/main/lib/claude/types.ts index defd75512..ef16eaee6 100644 --- a/src/main/lib/claude/types.ts +++ b/src/main/lib/claude/types.ts @@ -20,6 +20,7 @@ export type UIMessageChunk = toolCallId: string toolName: string input: unknown + providerMetadata?: Record } | { type: "tool-output-available"; toolCallId: string; output: unknown } | { type: "tool-output-error"; toolCallId: string; errorText: string } @@ -38,6 +39,7 @@ export type UIMessageChunk = }> } | { type: "ask-user-question-timeout"; toolUseId: string } + | { type: "ask-user-question-result"; toolUseId: string; result: unknown } | { type: "message-metadata"; messageMetadata: MessageMetadata } // Session initialization (MCP servers, plugins, tools) | { diff --git a/src/main/lib/codex-automations.test.ts b/src/main/lib/codex-automations.test.ts index 576424e9c..541e39ab8 100644 --- a/src/main/lib/codex-automations.test.ts +++ b/src/main/lib/codex-automations.test.ts @@ -165,4 +165,37 @@ updated_at = 1781687761313 await rm(codexHome, { force: true, recursive: true }) } }) + + test("persists the selected Custom ACP engine for local automations", async () => { + const originalCodexHome = process.env.CODEX_HOME + const codexHome = await useTempCodexHome() + try { + const created = await createLocalCodexAutomation({ + name: "Custom ACP 自动化", + prompt: "用自定义 ACP 检查当前工作区。", + model: "custom-acp", + engine: "custom-acp", + }) + + expect(created).toMatchObject({ + model: "custom-acp", + engine: "custom-acp", + source: "codex-local", + }) + + const toml = await readFile( + join(codexHome, "automations", created.id, "automation.toml"), + "utf8", + ) + expect(toml).toContain('model = "custom-acp"') + expect(toml).toContain('engine = "custom-acp"') + } finally { + if (originalCodexHome === undefined) { + delete process.env.CODEX_HOME + } else { + process.env.CODEX_HOME = originalCodexHome + } + await rm(codexHome, { force: true, recursive: true }) + } + }) }) diff --git a/src/main/lib/codex-automations.ts b/src/main/lib/codex-automations.ts index 0fb67b954..cd0687487 100644 --- a/src/main/lib/codex-automations.ts +++ b/src/main/lib/codex-automations.ts @@ -14,7 +14,12 @@ type AutomationInput = Record const AUTOMATION_FILE_NAME = "automation.toml" const AUTOMATION_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/ const HERMES_AUTOMATION_MODEL_ID = "moss-default" -const AUTOMATION_ENGINE_IDS = ["hermes", "codex", "claude-code"] as const +const AUTOMATION_ENGINE_IDS = [ + "hermes", + "codex", + "claude-code", + "custom-acp", +] as const type LocalAutomationEngineId = (typeof AUTOMATION_ENGINE_IDS)[number] export function getCodexAutomationsRoot(): string { @@ -339,13 +344,16 @@ function normalizeAutomationEngine( if (normalizedModel.startsWith("gpt-") || normalizedModel.includes("codex")) { return "codex" } + if (normalizedModel.includes("custom-acp")) { + return "custom-acp" + } return "hermes" } function isLocalAutomationEngineId( value: string | null, ): value is LocalAutomationEngineId { - return value === "hermes" || value === "codex" || value === "claude-code" + return AUTOMATION_ENGINE_IDS.includes(value as LocalAutomationEngineId) } function copyStringField( diff --git a/src/main/lib/db/schema/index.ts b/src/main/lib/db/schema/index.ts index fe6aa3490..9b03969a1 100644 --- a/src/main/lib/db/schema/index.ts +++ b/src/main/lib/db/schema/index.ts @@ -73,6 +73,11 @@ export const subChats = sqliteTable("sub_chats", { .notNull() .references(() => chats.id, { onDelete: "cascade" }), sessionId: text("session_id"), // Claude SDK session ID for resume + engine: text("engine").notNull().default("claude-code"), // "claude-code" | "codex" | "hermes" | "custom-acp" + engineSessionId: text("engine_session_id"), // Native engine session ID for resume + engineConfigDir: text("engine_config_dir"), // Per-engine config/session projection dir + modelId: text("model_id"), // Last selected runtime model for this sub-chat + runtimeMetadata: text("runtime_metadata"), // JSON object for engine-specific metadata streamId: text("stream_id"), // Track in-progress streams mode: text("mode").notNull().default("agent"), // "plan" | "agent" messages: text("messages").notNull().default("[]"), // JSON array diff --git a/src/main/lib/git/status-fallback.ts b/src/main/lib/git/status-fallback.ts new file mode 100644 index 000000000..50fd6169c --- /dev/null +++ b/src/main/lib/git/status-fallback.ts @@ -0,0 +1,166 @@ +import type { ChangedFile, GitChangesStatus } from "../../../shared/changes-types"; +import fs from "node:fs"; +import * as isoGit from "isomorphic-git"; +import type { StatusRow } from "isomorphic-git"; + +const FALLBACK_SKIP_PREFIXES = [ + ".git/", + "node_modules/", + "dist/", + "out/", + "release/", + "release-", + "tmp/", + ".tmp/", + ".vite/", + "evidence/", + ".1code/", +]; + +const FALLBACK_MAX_FILES_PER_BUCKET = 500; + +interface FallbackStatusBuckets { + staged: ChangedFile[]; + unstaged: ChangedFile[]; + untracked: ChangedFile[]; +} + +export function isRecoverableGitStatusError(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error); + return /xcode.*license|xcodebuild -license|have not agreed|spawn git ENOENT|unable to find git|cannot find git|git.*not found/i.test( + message, + ); +} + +export async function getReadOnlyGitStatusFallback( + worktreePath: string, + defaultBranch: string, +): Promise { + const [branch, statusMatrix] = await Promise.all([ + getCurrentBranch(worktreePath), + isoGit.statusMatrix({ + fs, + dir: worktreePath, + filter: shouldIncludeFallbackPath, + ignored: false, + refresh: false, + }), + ]); + const parsed = parseIsomorphicStatusMatrix(statusMatrix); + + return { + branch, + defaultBranch, + againstBase: [], + commits: [], + staged: parsed.staged, + unstaged: parsed.unstaged, + untracked: parsed.untracked, + ahead: 0, + behind: 0, + pushCount: 0, + pullCount: 0, + hasUpstream: false, + }; +} + +export function parseIsomorphicStatusMatrix( + statusMatrix: StatusRow[], +): FallbackStatusBuckets { + const staged: ChangedFile[] = []; + const unstaged: ChangedFile[] = []; + const untracked: ChangedFile[] = []; + + for (const [path, head, workdir, stage] of statusMatrix) { + if ((head === 1 && workdir === 1 && stage === 1) || (head === 0 && workdir === 0 && stage === 0)) { + continue; + } + + if (head === 0 && workdir === 2 && stage === 0) { + untracked.push(toChangedFile(path, "untracked")); + continue; + } + + if (head !== stage) { + staged.push(toChangedFile(path, getStagedStatus(head, stage))); + } + + if (workdir !== stage) { + unstaged.push(toChangedFile(path, getUnstagedStatus(head, workdir, stage))); + } + } + + return { + staged: limitChangedFilesForStatus(staged), + unstaged: limitChangedFilesForStatus(unstaged), + untracked: limitChangedFilesForStatus(untracked), + }; +} + +export function limitChangedFilesForStatus(files: ChangedFile[]): ChangedFile[] { + return files + .sort((a, b) => getPathSortRank(a.path) - getPathSortRank(b.path) || a.path.localeCompare(b.path)) + .slice(0, FALLBACK_MAX_FILES_PER_BUCKET); +} + +function shouldIncludeFallbackPath(path: string): boolean { + return !FALLBACK_SKIP_PREFIXES.some((prefix) => path === prefix.slice(0, -1) || path.startsWith(prefix)); +} + +async function getCurrentBranch(worktreePath: string): Promise { + try { + return ( + (await isoGit.currentBranch({ + fs, + dir: worktreePath, + fullname: false, + })) || "HEAD" + ); + } catch { + return "HEAD"; + } +} + +function getStagedStatus( + head: StatusRow[1], + stage: StatusRow[3], +): ChangedFile["status"] { + if (stage === 0) return "deleted"; + if (head === 0) return "added"; + return "modified"; +} + +function getUnstagedStatus( + head: StatusRow[1], + workdir: StatusRow[2], + stage: StatusRow[3], +): ChangedFile["status"] { + if (workdir === 0) return "deleted"; + if (head === 0 && stage === 0) return "untracked"; + return "modified"; +} + +function toChangedFile( + path: string, + status: ChangedFile["status"], +): ChangedFile { + return { + path, + status, + additions: 0, + deletions: 0, + }; +} + +function getPathSortRank(path: string): number { + if ( + path === "package.json" || + path === "bun.lock" || + path.startsWith("src/") || + path.startsWith("scripts/") + ) { + return 0; + } + if (path.startsWith(".")) return 2; + return 1; +} diff --git a/src/main/lib/git/status.ts b/src/main/lib/git/status.ts index 333660304..db0ef3318 100644 --- a/src/main/lib/git/status.ts +++ b/src/main/lib/git/status.ts @@ -3,6 +3,11 @@ import simpleGit from "simple-git"; import { z } from "zod"; import { publicProcedure, router } from "../trpc"; import { assertRegisteredWorktree, secureFs } from "./security"; +import { + getReadOnlyGitStatusFallback, + isRecoverableGitStatusError, + limitChangedFilesForStatus, +} from "./status-fallback"; import { applyNumstatToFiles } from "./utils/apply-numstat"; import { parseGitLog, @@ -34,40 +39,59 @@ export const createStatusRouter = () => { const git = simpleGit(input.worktreePath); const defaultBranch = input.defaultBranch || "main"; - const status = await git.status(); - const parsed = parseGitStatus(status); + let result: GitChangesStatus; + try { + const status = await git.status(["--untracked-files=normal"]); + const parsed = parseGitStatus(status); + const staged = limitChangedFilesForStatus(parsed.staged); + const unstaged = limitChangedFilesForStatus(parsed.unstaged); + const untracked = limitChangedFilesForStatus(parsed.untracked); + + // Run independent git operations in parallel (VS Code style) + const [branchComparison, trackingStatus] = await Promise.all([ + getBranchComparison(git, defaultBranch), + getTrackingBranchStatus(git), + ]); - // Run independent git operations in parallel (VS Code style) - const [branchComparison, trackingStatus] = await Promise.all([ - getBranchComparison(git, defaultBranch), - getTrackingBranchStatus(git), - ]); + // Run numstat operations in parallel + await Promise.all([ + applyNumstatToFiles(git, staged, [ + "diff", + "--cached", + "--numstat", + ]), + applyNumstatToFiles(git, unstaged, ["diff", "--numstat"]), + applyUntrackedLineCount(input.worktreePath, untracked), + ]); - // Run numstat operations in parallel - await Promise.all([ - applyNumstatToFiles(git, parsed.staged, [ - "diff", - "--cached", - "--numstat", - ]), - applyNumstatToFiles(git, parsed.unstaged, ["diff", "--numstat"]), - applyUntrackedLineCount(input.worktreePath, parsed.untracked), - ]); + result = { + branch: parsed.branch, + defaultBranch, + againstBase: branchComparison.againstBase, + commits: branchComparison.commits, + staged, + unstaged, + untracked, + ahead: branchComparison.ahead, + behind: branchComparison.behind, + pushCount: trackingStatus.pushCount, + pullCount: trackingStatus.pullCount, + hasUpstream: trackingStatus.hasUpstream, + }; + } catch (error) { + if (!isRecoverableGitStatusError(error)) { + throw error; + } - const result: GitChangesStatus = { - branch: parsed.branch, - defaultBranch, - againstBase: branchComparison.againstBase, - commits: branchComparison.commits, - staged: parsed.staged, - unstaged: parsed.unstaged, - untracked: parsed.untracked, - ahead: branchComparison.ahead, - behind: branchComparison.behind, - pushCount: trackingStatus.pushCount, - pullCount: trackingStatus.pullCount, - hasUpstream: trackingStatus.hasUpstream, - }; + console.warn("[getStatus] Native git unavailable; using read-only fallback:", { + worktreePath: input.worktreePath, + error: error instanceof Error ? error.message : String(error), + }); + result = await getReadOnlyGitStatusFallback( + input.worktreePath, + defaultBranch, + ); + } // Store in cache gitCache.setStatus(input.worktreePath, result); diff --git a/src/main/lib/git/watcher/git-watcher.ts b/src/main/lib/git/watcher/git-watcher.ts index 141868db8..aa8da85b2 100644 --- a/src/main/lib/git/watcher/git-watcher.ts +++ b/src/main/lib/git/watcher/git-watcher.ts @@ -1,7 +1,7 @@ import { EventEmitter } from "events"; +import type { FSWatcher } from "chokidar"; -// Chokidar is ESM-only, so we need to dynamically import it -type FSWatcher = Awaited>["FSWatcher"] extends new () => infer T ? T : never; +// Chokidar is ESM-only at runtime, so loading still happens through dynamic import. // Simple debounce implementation to avoid lodash-es dependency in main process function debounce unknown>( @@ -160,7 +160,7 @@ export class GitWatcher extends EventEmitter { this.pendingChanges.set(path, "unlink"); flushChanges(); }) - .on("error", (error: Error) => { + .on("error", (error: unknown) => { console.error("[GitWatcher] Error:", error); this.emit("error", error); }); diff --git a/src/main/lib/git/worktree.ts b/src/main/lib/git/worktree.ts index 298c3de28..bc3f5908c 100644 --- a/src/main/lib/git/worktree.ts +++ b/src/main/lib/git/worktree.ts @@ -896,10 +896,29 @@ export interface WorktreeResult { error?: string; } +export interface WorktreeCreatedContext { + worktreePath: string; + branch?: string; + baseBranch?: string; +} + export interface CreateWorktreeForChatOptions { + onCreated?: (context: WorktreeCreatedContext) => Promise | void; onSetupComplete?: (result: WorktreeSetupResult) => void; } +async function notifyWorktreeCreated( + options: CreateWorktreeForChatOptions | undefined, + context: WorktreeCreatedContext, +): Promise { + try { + await options?.onCreated?.(context); + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.warn(`[worktree] onCreated hook failed: ${errorMsg}`); + } +} + /** * Create a git worktree for a chat (wrapper for chats.ts) * @param projectPath - Path to the main repository @@ -920,6 +939,7 @@ export async function createWorktreeForChat( const isRepo = await git.checkIsRepo(); if (!isRepo) { + await notifyWorktreeCreated(options, { worktreePath: projectPath }); return { success: true, worktreePath: projectPath }; } @@ -938,6 +958,7 @@ export async function createWorktreeForChat( const startPoint = branchType === "local" ? baseBranch : `origin/${baseBranch}`; await createWorktree(projectPath, branch, worktreePath, startPoint); + await notifyWorktreeCreated(options, { worktreePath, branch, baseBranch }); // Run worktree setup commands in BACKGROUND (don't block chat creation) // This allows the user to start chatting immediately while deps install diff --git a/src/main/lib/mcp-auth.ts b/src/main/lib/mcp-auth.ts index f24646865..94b77e143 100644 --- a/src/main/lib/mcp-auth.ts +++ b/src/main/lib/mcp-auth.ts @@ -2,6 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { BrowserWindow, shell } from 'electron'; +import net from 'node:net'; import { getMcpServerConfig, GLOBAL_MCP_PATH, @@ -13,6 +14,10 @@ import { getClaudeShellEnvironment } from './claude/env'; import { CraftOAuth, fetchOAuthMetadata, getMcpBaseUrl, type OAuthMetadata, type OAuthTokens } from './oauth'; import { discoverPluginMcpServers } from './plugins'; import { bringToFront } from './window'; +import { + extractLoopbackMcpBridgeEndpoint, + resolveHostCompatibleMcpStdioConfig, +} from './mcp-stdio-compat'; /** @@ -83,6 +88,83 @@ const BLOCKED_ENV_VARS = [ 'OPENAI_API_KEY', ]; +function isTcpPortListening(host: string, port: number, timeoutMs = 300): Promise { + return new Promise((resolve) => { + const socket = net.createConnection({ host, port }); + let settled = false; + + const finish = (value: boolean) => { + if (settled) return; + settled = true; + socket.removeAllListeners(); + socket.destroy(); + resolve(value); + }; + + socket.setTimeout(timeoutMs); + socket.once('connect', () => finish(true)); + socket.once('error', () => finish(false)); + socket.once('timeout', () => finish(false)); + }); +} + +function reserveFreeLoopbackPort(host: string): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer(); + + server.once('error', reject); + server.listen(0, host, () => { + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : null; + server.close(() => { + if (port) { + resolve(port); + } else { + reject(new Error(`Unable to reserve a free port on ${host}`)); + } + }); + }); + }); +} + +async function avoidLoopbackBridgePortCollision(config: { + command: string; + args?: string[]; + env?: Record; + cwd?: string; +}): Promise<{ + command: string; + args?: string[]; + env?: Record; + cwd?: string; +}> { + const endpoint = extractLoopbackMcpBridgeEndpoint(config.env); + if (!endpoint) return config; + + const isListening = await isTcpPortListening(endpoint.host, endpoint.port); + if (!isListening) return config; + + try { + const fallbackPort = await reserveFreeLoopbackPort(endpoint.host); + console.warn( + `[MCP] ${endpoint.host}:${endpoint.port} is already in use; probing stdio server with temporary ${endpoint.portEnvKey}=${fallbackPort}`, + ); + return { + ...config, + env: { + ...config.env, + [endpoint.portEnvKey]: String(fallbackPort), + }, + }; + } catch (error) { + console.warn( + `[MCP] ${endpoint.host}:${endpoint.port} is already in use and no temporary probe port could be reserved:`, + error, + ); + return config; + } +} + /** * Fetch tools from a stdio-based MCP server * Uses shell environment to ensure proper PATH (homebrew, nvm, etc.) in production @@ -91,6 +173,8 @@ export async function fetchMcpToolsStdio(config: { command: string; args?: string[]; env?: Record; + cwd?: string; + sourcePath?: string | null; }): Promise { let transport: StdioClientTransport | null = null; @@ -113,10 +197,26 @@ export async function fetchMcpToolsStdio(config: { } } + const launchConfig = resolveHostCompatibleMcpStdioConfig(config); + if (!launchConfig.ok) { + console.warn(`[MCP] Skipping stdio server probe: ${launchConfig.reason}`); + return []; + } + + if (launchConfig.rewrites.length > 0) { + const rewritten = launchConfig.rewrites + .map((rewrite) => `${rewrite.from} -> ${rewrite.to}`) + .join(', '); + console.log(`[MCP] Applied stdio path mapping: ${rewritten}`); + } + + const stdioConfig = await avoidLoopbackBridgePortCollision(launchConfig.config); + transport = new StdioClientTransport({ - command: config.command, - args: config.args, - env: { ...safeEnv, ...config.env }, + command: stdioConfig.command, + args: stdioConfig.args, + env: { ...safeEnv, ...stdioConfig.env }, + cwd: stdioConfig.cwd, }); await client.connect(transport); @@ -139,14 +239,14 @@ export async function fetchMcpToolsStdio(config: { } } -import { AUTH_SERVER_PORT, IS_DEV } from '../constants'; +import { getAuthServerPort, IS_DEV } from '../constants'; const OAUTH_TIMEOUT_MS = 5 * 60 * 1000; function getMcpOAuthRedirectUri(): string { return IS_DEV - ? `http://localhost:${AUTH_SERVER_PORT}/callback` - : `http://127.0.0.1:${AUTH_SERVER_PORT}/callback`; + ? `http://localhost:${getAuthServerPort()}/callback` + : `http://127.0.0.1:${getAuthServerPort()}/callback`; } interface PendingOAuth { diff --git a/src/main/lib/mcp-stdio-compat.test.ts b/src/main/lib/mcp-stdio-compat.test.ts index efbc65546..ccc1e4377 100644 --- a/src/main/lib/mcp-stdio-compat.test.ts +++ b/src/main/lib/mcp-stdio-compat.test.ts @@ -1,5 +1,42 @@ import { describe, expect, test } from "bun:test" -import { resolveHostCompatibleMcpStdioConfig } from "./mcp-stdio-compat" +import { + extractLoopbackMcpBridgeEndpoint, + resolveHostCompatibleMcpStdioConfig, +} from "./mcp-stdio-compat" + +describe("extractLoopbackMcpBridgeEndpoint", () => { + test("detects a loopback MCP bridge endpoint from env", () => { + expect( + extractLoopbackMcpBridgeEndpoint({ + HOOLA_CANVAS_MCP_BRIDGE_HOST: "127.0.0.1", + HOOLA_CANVAS_MCP_BRIDGE_PORT: "47841", + }), + ).toEqual({ + host: "127.0.0.1", + port: 47841, + hostEnvKey: "HOOLA_CANVAS_MCP_BRIDGE_HOST", + portEnvKey: "HOOLA_CANVAS_MCP_BRIDGE_PORT", + }) + }) + + test("ignores non-loopback bridge endpoints", () => { + expect( + extractLoopbackMcpBridgeEndpoint({ + HOOLA_CANVAS_MCP_BRIDGE_HOST: "0.0.0.0", + HOOLA_CANVAS_MCP_BRIDGE_PORT: "47841", + }), + ).toBeNull() + }) + + test("ignores invalid bridge ports", () => { + expect( + extractLoopbackMcpBridgeEndpoint({ + HOOLA_CANVAS_MCP_BRIDGE_HOST: "127.0.0.1", + HOOLA_CANVAS_MCP_BRIDGE_PORT: "70000", + }), + ).toBeNull() + }) +}) describe("resolveHostCompatibleMcpStdioConfig", () => { test("maps a Windows project root path through the source project path", () => { diff --git a/src/main/lib/mcp-stdio-compat.ts b/src/main/lib/mcp-stdio-compat.ts index 1f37de204..46f0fffbd 100644 --- a/src/main/lib/mcp-stdio-compat.ts +++ b/src/main/lib/mcp-stdio-compat.ts @@ -15,6 +15,13 @@ export interface McpPathMapping { to: string } +export interface McpLoopbackBridgeEndpoint { + host: string + port: number + hostEnvKey: string + portEnvKey: string +} + export type McpStdioCompatResult = | { ok: true @@ -53,6 +60,43 @@ const NODE_OPTIONS_WITH_VALUE = new Set([ "--import", ]) const LOCAL_SCRIPT_EXTENSIONS = /\.(?:[cm]?js|[cm]?ts|tsx)$/i +const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]) + +function isLoopbackHost(value: string): boolean { + return LOOPBACK_HOSTS.has(value.trim().toLowerCase()) +} + +function parseTcpPort(value: string): number | null { + const port = Number(value) + if (!Number.isInteger(port) || port <= 0 || port > 65535) return null + return port +} + +export function extractLoopbackMcpBridgeEndpoint( + env?: Record, +): McpLoopbackBridgeEndpoint | null { + if (!env) return null + + for (const [portEnvKey, portValue] of Object.entries(env)) { + if (!portEnvKey.endsWith("_MCP_BRIDGE_PORT")) continue + + const hostEnvKey = `${portEnvKey.slice(0, -"_PORT".length)}_HOST` + const host = env[hostEnvKey] + if (!host || !isLoopbackHost(host)) continue + + const port = parseTcpPort(portValue) + if (!port) continue + + return { + host, + port, + hostEnvKey, + portEnvKey, + } + } + + return null +} function parseWindowsPath(value: string): { root: string; rest: string } | null { const match = value.match(WINDOWS_ABSOLUTE_PATH) diff --git a/src/main/lib/mobile-gateway/database-sessions.ts b/src/main/lib/mobile-gateway/database-sessions.ts new file mode 100644 index 000000000..ba7ad053d --- /dev/null +++ b/src/main/lib/mobile-gateway/database-sessions.ts @@ -0,0 +1,36 @@ +import { desc, eq } from "drizzle-orm" +import { chats, getDatabase, projects, subChats } from "../db" +import { + mapMobileGatewaySessionRows, + type MobileGatewaySessionRow, +} from "./sessions" +import type { MobileGatewaySessionSummary } from "./facade" + +export function listMobileGatewaySessionsFromDatabase(): MobileGatewaySessionSummary[] { + const db = getDatabase() + const rows = db + .select({ + chatId: chats.id, + chatName: chats.name, + chatWorktreePath: chats.worktreePath, + projectName: projects.name, + projectPath: projects.path, + subChatId: subChats.id, + subChatName: subChats.name, + engine: subChats.engine, + modelId: subChats.modelId, + mode: subChats.mode, + runtimeMetadata: subChats.runtimeMetadata, + streamId: subChats.streamId, + engineSessionId: subChats.engineSessionId, + sessionId: subChats.sessionId, + updatedAt: subChats.updatedAt, + }) + .from(subChats) + .innerJoin(chats, eq(subChats.chatId, chats.id)) + .innerJoin(projects, eq(chats.projectId, projects.id)) + .orderBy(desc(subChats.updatedAt)) + .all() + + return mapMobileGatewaySessionRows(rows satisfies MobileGatewaySessionRow[]) +} diff --git a/src/main/lib/mobile-gateway/desktop-state.ts b/src/main/lib/mobile-gateway/desktop-state.ts new file mode 100644 index 000000000..72c671260 --- /dev/null +++ b/src/main/lib/mobile-gateway/desktop-state.ts @@ -0,0 +1,106 @@ +export interface DesktopMobileGatewayPairingStatus { + status: "available" | "unavailable" + isAvailable: boolean + url: string | null + pairingUrl: string | null + redactedPairingUrl: string | null + redactedToken: string | null + detail: string +} + +export interface DesktopMobileGatewayPairingSource { + url: string + getPairingToken(): string | undefined + getPairingUrl(): string | undefined +} + +let runningDesktopMobileGateway: DesktopMobileGatewayPairingSource | null = null + +export function setDesktopMobileGateway( + gateway: DesktopMobileGatewayPairingSource | null, +): void { + runningDesktopMobileGateway = gateway +} + +export function getDesktopMobileGateway(): DesktopMobileGatewayPairingSource | null { + return runningDesktopMobileGateway +} + +export function readDesktopMobileGatewayPairingStatus(): + DesktopMobileGatewayPairingStatus { + return buildDesktopMobileGatewayPairingStatus(runningDesktopMobileGateway) +} + +export function buildDesktopMobileGatewayPairingStatus( + gateway: DesktopMobileGatewayPairingSource | null, +): DesktopMobileGatewayPairingStatus { + if (!gateway) { + return { + status: "unavailable", + isAvailable: false, + url: null, + pairingUrl: null, + redactedPairingUrl: null, + redactedToken: null, + detail: "Mobile gateway is not running.", + } + } + + const token = cleanString(gateway.getPairingToken()) + const pairingUrl = cleanString(gateway.getPairingUrl()) + + if (!token || !pairingUrl) { + return { + status: "unavailable", + isAvailable: false, + url: gateway.url, + pairingUrl: null, + redactedPairingUrl: null, + redactedToken: null, + detail: "Mobile gateway is running without a pairing token.", + } + } + + const redactedToken = redactMobileGatewayToken(token) + + return { + status: "available", + isAvailable: true, + url: gateway.url, + pairingUrl, + redactedPairingUrl: redactPairingUrlToken(pairingUrl, token, redactedToken), + redactedToken, + detail: "Mobile gateway is ready for pairing.", + } +} + +export function redactMobileGatewayToken(token: string): string { + const trimmed = token.trim() + if (trimmed.length <= 8) { + return `${trimmed.slice(0, 2)}...${trimmed.slice(-2)}` + } + return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}` +} + +function redactPairingUrlToken( + pairingUrl: string, + token: string, + redactedToken: string, +): string { + try { + const parsed = new URL(pairingUrl) + if (parsed.searchParams.has("token")) { + parsed.searchParams.set("token", redactedToken) + return parsed.toString() + } + } catch { + // Fall through to a direct replacement for malformed debug strings. + } + + return pairingUrl.replace(token, redactedToken) +} + +function cleanString(value: string | null | undefined): string | null { + const trimmed = value?.trim() + return trimmed ? trimmed : null +} diff --git a/src/main/lib/mobile-gateway/desktop.ts b/src/main/lib/mobile-gateway/desktop.ts new file mode 100644 index 000000000..d1882bdf9 --- /dev/null +++ b/src/main/lib/mobile-gateway/desktop.ts @@ -0,0 +1,70 @@ +import { randomUUID } from "node:crypto" +import { agentRuntimeAdapters } from "../agent-runtime/adapters" +import { createMobileGatewayFacade } from "./facade" +import { listMobileGatewaySessionsFromDatabase } from "./database-sessions" +import { + startMobileGatewayServer, + type RunningMobileGatewayServer, +} from "./server" + +export interface DesktopMobileGatewayOptions { + enabled?: boolean + host?: string + port?: number + pairingToken?: string +} + +export interface RunningDesktopMobileGateway extends RunningMobileGatewayServer { + pairingToken: string +} + +const DEFAULT_MOBILE_GATEWAY_PORT = 4177 + +export async function startDesktopMobileGateway( + options: DesktopMobileGatewayOptions = {}, +): Promise { + const enabled = options.enabled ?? process.env.ONECODE_MOBILE_GATEWAY === "1" + if (!enabled) return null + + const pairingToken = + cleanString(options.pairingToken) ?? + cleanString(process.env.ONECODE_MOBILE_GATEWAY_TOKEN) ?? + randomUUID() + const host = + cleanString(options.host) ?? + cleanString(process.env.ONECODE_MOBILE_GATEWAY_HOST) ?? + "127.0.0.1" + const port = + options.port ?? + parsePort(process.env.ONECODE_MOBILE_GATEWAY_PORT) ?? + DEFAULT_MOBILE_GATEWAY_PORT + const facade = createMobileGatewayFacade({ + sessions: listMobileGatewaySessionsFromDatabase, + adapters: agentRuntimeAdapters, + }) + const server = await startMobileGatewayServer({ + facade, + host, + port, + pairingToken, + }) + + return { + ...server, + get pairingToken() { + return server.getPairingToken() ?? pairingToken + }, + } +} + +function cleanString(value: string | null | undefined): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined +} + +function parsePort(value: string | null | undefined): number | undefined { + if (!value?.trim()) return undefined + const port = Number.parseInt(value, 10) + return Number.isSafeInteger(port) && port >= 0 && port <= 65535 + ? port + : undefined +} diff --git a/src/main/lib/mobile-gateway/facade.ts b/src/main/lib/mobile-gateway/facade.ts new file mode 100644 index 000000000..876910949 --- /dev/null +++ b/src/main/lib/mobile-gateway/facade.ts @@ -0,0 +1,1031 @@ +import type { + AgentEngineId, + AgentPermissionMode, + AgentRuntimeAdapter, + AgentRuntimeAvailability, + AgentRuntimeControlResult, + AgentRuntimeReceiptBus, + AgentRuntimeRunReceipt, + AgentRuntimeRunStatus, + AgentRuntimeHealth, + AgentRuntimeSessionRef, + AgentRuntimeStartRequest, + AgentRuntimeStreamEvent, +} from "../agent-runtime" + +export type MobileGatewaySessionStatus = + | "idle" + | "running" + | "needs-approval" + | "needs-auth" + | "offline" + | "error" + +export interface MobileGatewaySessionSummary { + chatId: string + subChatId: string + title: string + projectLabel: string + projectPath: string + detail: string + engineId: AgentEngineId + modelId: string | null + nativeSessionId?: string | null + permissionMode: AgentPermissionMode | string + status: MobileGatewaySessionStatus + updatedAt: string + pendingApprovals: number + unreadEvents: number +} + +export type MobileGatewaySessionSource = + | readonly MobileGatewaySessionSummary[] + | (() => readonly MobileGatewaySessionSummary[]) + +export type MobileGatewayRuntimeHealthStatus = AgentRuntimeAvailability + +export interface MobileGatewayRuntimeHealth { + engineId: AgentEngineId + status: MobileGatewayRuntimeHealthStatus + detail: string + checkedAt: string + modelId?: string | null + authMethod?: string +} + +export type MobileGatewayRuntimeHealthSource = + | readonly MobileGatewayRuntimeHealth[] + | (() => readonly MobileGatewayRuntimeHealth[] | Promise) + +export type MobileGatewayEvent = + | { + type: "turn-start" + engineId: AgentEngineId + modelId?: string | null + permissionMode: string + } + | { + type: "text-delta" + text: string + } + | { + type: "tool-call" + toolCallId?: string + name: string + input?: unknown + } + | { + type: "tool-result" + toolCallId?: string + name?: string + result?: unknown + isError?: boolean + } + | { + type: "permission-request" + requestId: string + title?: string + details?: unknown + } + | { + type: "block" + block: Record + } + | { + type: "block-patch" + id: string + patch: Record + } + | { + type: "usage" + inputTokens?: number + outputTokens?: number + totalTokens?: number + modelContextWindow?: number + usedTokens?: number + totalProcessedTokens?: number + maxTokens?: number + cachedInputTokens?: number + reasoningOutputTokens?: number + lastUsedTokens?: number + lastInputTokens?: number + lastCachedInputTokens?: number + lastOutputTokens?: number + lastReasoningOutputTokens?: number + toolUses?: number + durationMs?: number + compactsAutomatically?: boolean + } + | { + type: "auth-error" + message: string + recoverable: true + } + | { + type: "error" + message: string + recoverable?: boolean + } + | { + type: "finish" + resultSubtype?: "success" | "error" | "cancelled" + nativeSessionId?: string | null + } + +export interface MobileGatewayEnvelope { + seq: number + time: string + chatId: string + subChatId: string + runId?: string + event: MobileGatewayEvent +} + +export interface MobileGatewayStartRunInput { + chatId: string + subChatId: string + prompt: string + engineId?: AgentEngineId + modelId?: string | null + permissionMode?: AgentPermissionMode | string + images?: AgentRuntimeStartRequest["images"] + forceNewSession?: boolean +} + +export interface MobileGatewayRunReceipt { + runId: string + status: "running" + engineId: AgentEngineId + chatId: string + subChatId: string + startedAt: string +} + +export interface MobileGatewayStopRunInput { + runId: string + reason?: string +} + +export interface MobileGatewaySubmitToolResultInput { + runId: string + toolCallId: string + result: unknown + isError?: boolean +} + +export interface ReadMobileGatewayEventsInput { + subChatId: string + afterSeq?: number +} + +export interface MobileGatewayFacade { + listSessions(): MobileGatewaySessionSummary[] + listRuntimeHealth(): Promise + startRun(input: MobileGatewayStartRunInput): Promise + stopRun(input: MobileGatewayStopRunInput): Promise + submitToolResult( + input: MobileGatewaySubmitToolResultInput, + ): Promise + readEvents( + input: ReadMobileGatewayEventsInput, + ): AsyncIterable +} + +export interface MobileGatewayHttpRequest { + method: string + url: string + headers?: Record + body?: string +} + +export interface MobileGatewayHttpResponse { + status: number + headers: Record + body: string +} + +export type MobileGatewayPairingTokenSource = + | string + | (() => string | undefined) + +export interface MobileGatewayPairingTokenRotation { + token: string + rotatedAt: string +} + +export interface MobileGatewayHttpRequestOptions { + pairingToken?: MobileGatewayPairingTokenSource + rotatePairingToken?: () => MobileGatewayPairingTokenRotation + pairingBaseUrl?: string | ((requestUrl: URL) => string) + pairingLabel?: string +} + +type MobileGatewayRunState = { + receipt: MobileGatewayRunReceipt + request: AgentRuntimeStartRequest + history: MobileGatewayEnvelope[] + completed: boolean + streaming: boolean + quiescedReceiptPublished: boolean +} + +type MobileGatewayFinalRunStatus = Exclude +type MobileGatewayResultSubtype = AgentRuntimeRunReceipt["resultSubtype"] + +export function createMobileGatewayFacade(input: { + sessions: MobileGatewaySessionSource + adapters: Partial> + runtimeHealth?: MobileGatewayRuntimeHealthSource + runtimeReceipts?: AgentRuntimeReceiptBus + now?: () => Date +}): MobileGatewayFacade { + const now = input.now ?? (() => new Date()) + const getSessions = createSessionReader(input.sessions) + const getRuntimeHealth = createRuntimeHealthReader(input.runtimeHealth) + const runsBySubChat = new Map() + const runsByRunId = new Map() + const runtimeHealthOverrides = new Map() + + return { + listSessions() { + return getSessions() + }, + + async listRuntimeHealth() { + const sessions = getSessions() + const baseHealth = getRuntimeHealth + ? await getRuntimeHealth() + : await inspectRuntimeHealth({ + sessions, + adapters: input.adapters, + checkedAt: now().toISOString(), + }) + return mergeRuntimeHealthOverrides(baseHealth, runtimeHealthOverrides) + }, + + async startRun(runInput) { + const session = findSessionOrThrow(getSessions(), runInput) + const prompt = runInput.prompt.trim() + if (!prompt) { + throw new Error("prompt must be a non-empty string") + } + if (!input.adapters[session.engineId]) { + throw new Error(`No mobile gateway adapter configured for ${session.engineId}`) + } + + const startedAt = now().toISOString() + const receipt = { + runId: `mobile:${session.engineId}:${session.subChatId}:${Date.parse(startedAt)}`, + status: "running" as const, + engineId: session.engineId, + chatId: session.chatId, + subChatId: session.subChatId, + startedAt, + } + const request: AgentRuntimeStartRequest = { + runId: receipt.runId, + session: buildRuntimeSession(session, runInput), + prompt, + images: runInput.images, + forceNewSession: runInput.forceNewSession, + } + + const state = { + receipt, + request, + history: [], + completed: false, + streaming: false, + quiescedReceiptPublished: false, + } + runsBySubChat.set(session.subChatId, state) + runsByRunId.set(receipt.runId, state) + input.runtimeReceipts?.publish({ + type: "runtime.run.started", + runId: receipt.runId, + engineId: receipt.engineId, + chatId: receipt.chatId, + subChatId: receipt.subChatId, + createdAt: startedAt, + source: "mobile-gateway", + }) + + return receipt + }, + + async stopRun(stopInput) { + const run = runsByRunId.get(stopInput.runId) + if (!run) { + throw new MobileGatewayHttpError( + 404, + `Mobile gateway run not found: ${stopInput.runId}`, + ) + } + + const adapter = input.adapters[run.receipt.engineId] + if (!adapter) { + throw new Error(`No mobile gateway adapter configured for ${run.receipt.engineId}`) + } + + return adapter.stop({ + session: run.request.session, + runId: stopInput.runId, + reason: stopInput.reason, + }) + }, + + async submitToolResult(toolInput) { + const run = runsByRunId.get(toolInput.runId) + if (!run) { + throw new MobileGatewayHttpError( + 404, + `Mobile gateway run not found: ${toolInput.runId}`, + ) + } + + const adapter = input.adapters[run.receipt.engineId] + if (!adapter) { + throw new Error(`No mobile gateway adapter configured for ${run.receipt.engineId}`) + } + + return adapter.submitToolResult({ + session: run.request.session, + runId: toolInput.runId, + toolCallId: toolInput.toolCallId, + result: toolInput.result, + isError: toolInput.isError, + }) + }, + + async *readEvents(eventsInput) { + const run = runsBySubChat.get(eventsInput.subChatId) + if (!run) { + throw new Error(`No mobile gateway run is registered for ${eventsInput.subChatId}`) + } + + if (run.completed) { + yield* filterAfterSeq(run.history, eventsInput.afterSeq) + return + } + if (run.streaming) { + throw new Error(`Mobile gateway run is already streaming for ${eventsInput.subChatId}`) + } + + run.streaming = true + let finalStatus: MobileGatewayFinalRunStatus = "success" + let finalResultSubtype: MobileGatewayResultSubtype = "success" + let finalError: string | undefined + let nextSeq = run.history.length > 0 + ? run.history[run.history.length - 1]!.seq + 1 + : (eventsInput.afterSeq ?? -1) + 1 + const append = (event: MobileGatewayEvent): MobileGatewayEnvelope => { + const envelope = { + seq: nextSeq++, + time: now().toISOString(), + chatId: run.receipt.chatId, + subChatId: run.receipt.subChatId, + runId: run.receipt.runId, + event, + } + run.history.push(envelope) + return envelope + } + const publishQuiescedReceipt = () => { + if (!input.runtimeReceipts || run.quiescedReceiptPublished) return + if (!run.completed) return + run.quiescedReceiptPublished = true + input.runtimeReceipts.publish({ + type: "turn.processing.quiesced", + runId: run.receipt.runId, + engineId: run.receipt.engineId, + chatId: run.receipt.chatId, + subChatId: run.receipt.subChatId, + status: finalStatus, + resultSubtype: finalResultSubtype, + latestSequence: run.history.at(-1)?.seq ?? null, + eventCount: run.history.length, + createdAt: now().toISOString(), + error: finalError ?? null, + source: "mobile-gateway", + }) + } + const rememberTerminalMobileEvent = (event: MobileGatewayEvent) => { + if (event.type === "error" || event.type === "auth-error") { + finalStatus = "error" + finalResultSubtype = "error" + finalError = event.message + return + } + if (event.type !== "finish") return + finalResultSubtype = event.resultSubtype ?? finalResultSubtype + if (event.resultSubtype === "cancelled") { + finalStatus = "cancelled" + } else if (event.resultSubtype === "error") { + finalStatus = "error" + } else if (finalStatus !== "error") { + finalStatus = "success" + } + } + + try { + const adapter = input.adapters[run.receipt.engineId] + if (!adapter) { + const errorEvent: MobileGatewayEvent = { + type: "error", + message: `No mobile gateway adapter configured for ${run.receipt.engineId}`, + recoverable: false, + } + rememberTerminalMobileEvent(errorEvent) + yield append(errorEvent) + const finishEvent: MobileGatewayEvent = { type: "finish", resultSubtype: "error" } + rememberTerminalMobileEvent(finishEvent) + yield append(finishEvent) + run.completed = true + return + } + + yield append({ + type: "turn-start", + engineId: run.request.session.engineId, + modelId: run.request.session.modelId, + permissionMode: run.request.session.permissionMode, + }) + + for await (const runtimeEvent of adapter.stream(run.request)) { + const mobileEvent = mapRuntimeEventToMobileEvent(runtimeEvent) + if (mobileEvent) { + rememberRuntimeHealthFailure({ + event: mobileEvent, + run, + checkedAt: now().toISOString(), + overrides: runtimeHealthOverrides, + }) + rememberTerminalMobileEvent(mobileEvent) + yield append(mobileEvent) + if (mobileEvent.type === "finish" || mobileEvent.type === "auth-error") { + run.completed = true + } + } + } + run.completed = true + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + const errorEvent: MobileGatewayEvent = { + type: "error", + message, + recoverable: false, + } + rememberRuntimeHealthFailure({ + event: errorEvent, + run, + checkedAt: now().toISOString(), + overrides: runtimeHealthOverrides, + }) + rememberTerminalMobileEvent(errorEvent) + yield append(errorEvent) + const finishEvent: MobileGatewayEvent = { + type: "finish", + nativeSessionId: run.request.session.nativeSessionId ?? null, + resultSubtype: "error", + } + rememberTerminalMobileEvent(finishEvent) + yield append(finishEvent) + run.completed = true + } finally { + publishQuiescedReceipt() + run.streaming = false + } + }, + } +} + +export async function collectMobileGatewayEvents( + events: AsyncIterable, +): Promise { + const collected: MobileGatewayEnvelope[] = [] + for await (const event of events) { + collected.push(event) + } + return collected +} + +export function serializeMobileGatewaySse( + envelopes: MobileGatewayEnvelope[], +): string { + return envelopes + .map((envelope) => `event: mobile-envelope\ndata: ${JSON.stringify(envelope)}\n\n`) + .join("") +} + +export async function handleMobileGatewayRequest( + facade: MobileGatewayFacade, + request: MobileGatewayHttpRequest, + options?: MobileGatewayHttpRequestOptions, +): Promise { + const url = new URL(request.url) + if (!isAuthorized(request.headers ?? {}, options?.pairingToken)) { + return jsonResponse(401, { error: "Unauthorized" }) + } + + try { + if ( + request.method === "POST" && + url.pathname === "/mobile/v1/pairing/token/rotate" + ) { + if (!options?.rotatePairingToken) { + return jsonResponse(404, { error: "Pairing token rotation is not configured" }) + } + + const rotation = options.rotatePairingToken() + const token = requireString(rotation.token, "token") + const baseUrl = resolvePairingBaseUrl(url, options.pairingBaseUrl) + return jsonResponse(200, { + token, + baseUrl, + pairingUrl: buildOneCodeMobilePairingUrl({ + baseUrl, + token, + label: options.pairingLabel ?? "Local 1code", + }), + rotatedAt: requireString(rotation.rotatedAt, "rotatedAt"), + expiresPreviousToken: true, + }) + } + + if (request.method === "GET" && url.pathname === "/mobile/v1/sessions") { + return jsonResponse(200, { sessions: facade.listSessions() }) + } + + if (request.method === "GET" && url.pathname === "/mobile/v1/runtime/health") { + return jsonResponse(200, { health: await facade.listRuntimeHealth() }) + } + + const stopMatch = url.pathname.match(/^\/mobile\/v1\/runs\/([^/]+)\/stop$/) + if (request.method === "POST" && stopMatch) { + const body = parseJsonBody(request.body) + const result = await facade.stopRun({ + runId: decodeURIComponent(stopMatch[1]!), + reason: optionalString(body.reason), + }) + return jsonResponse(200, result) + } + + const toolResultMatch = url.pathname.match( + /^\/mobile\/v1\/runs\/([^/]+)\/tool-results\/([^/]+)$/, + ) + if (request.method === "POST" && toolResultMatch) { + const body = parseJsonBody(request.body) + const result = await facade.submitToolResult({ + runId: decodeURIComponent(toolResultMatch[1]!), + toolCallId: decodeURIComponent(toolResultMatch[2]!), + result: body.result, + isError: optionalBoolean(body.isError), + }) + return jsonResponse(200, result) + } + + const runMatch = url.pathname.match(/^\/mobile\/v1\/sessions\/([^/]+)\/runs$/) + if (request.method === "POST" && runMatch) { + const body = parseJsonBody(request.body) + const receipt = await facade.startRun({ + chatId: requireString(body.chatId, "chatId"), + subChatId: decodeURIComponent(runMatch[1]!), + prompt: requireString(body.prompt, "prompt"), + engineId: optionalEngine(body.engineId), + modelId: optionalString(body.modelId), + permissionMode: optionalString(body.permissionMode), + forceNewSession: + typeof body.forceNewSession === "boolean" + ? body.forceNewSession + : undefined, + }) + return jsonResponse(200, receipt) + } + + const eventsMatch = url.pathname.match(/^\/mobile\/v1\/sessions\/([^/]+)\/events$/) + if (request.method === "GET" && eventsMatch) { + const afterSeq = parseOptionalSeq(url.searchParams.get("afterSeq")) + const envelopes = await collectMobileGatewayEvents( + facade.readEvents({ + subChatId: decodeURIComponent(eventsMatch[1]!), + afterSeq, + }), + ) + return { + status: 200, + headers: { + "content-type": "text/event-stream", + "cache-control": "no-cache", + }, + body: serializeMobileGatewaySse(envelopes), + } + } + + return jsonResponse(404, { error: "Not found" }) + } catch (error) { + if (error instanceof MobileGatewayHttpError) { + return jsonResponse(error.status, { error: error.message }) + } + return jsonResponse(400, { + error: error instanceof Error ? error.message : String(error), + }) + } +} + +class MobileGatewayHttpError extends Error { + constructor( + readonly status: number, + message: string, + ) { + super(message) + } +} + +function createSessionReader( + source: MobileGatewaySessionSource, +): () => MobileGatewaySessionSummary[] { + return () => [...(typeof source === "function" ? source() : source)] +} + +function createRuntimeHealthReader( + source: MobileGatewayRuntimeHealthSource | undefined, +): (() => Promise) | undefined { + if (!source) return undefined + return async () => [...(typeof source === "function" ? await source() : source)] +} + +async function inspectRuntimeHealth(params: { + sessions: readonly MobileGatewaySessionSummary[] + adapters: Partial> + checkedAt: string +}): Promise { + const sessionsByEngine = new Map() + for (const session of params.sessions) { + if (!sessionsByEngine.has(session.engineId)) { + sessionsByEngine.set(session.engineId, session) + } + } + + const health: MobileGatewayRuntimeHealth[] = [] + for (const [engineId, session] of sessionsByEngine) { + const adapter = params.adapters[engineId] + if (!adapter) { + health.push({ + engineId, + modelId: session.modelId, + status: "unsupported", + detail: `No mobile gateway adapter configured for ${engineId}.`, + checkedAt: params.checkedAt, + }) + continue + } + + try { + const runtimeSession = buildInspectRuntimeSession(session) + const runtimeHealth = adapter.inspect + ? await adapter.inspect(runtimeSession) + : { availability: await adapter.canStart(runtimeSession) } satisfies AgentRuntimeHealth + health.push(toMobileRuntimeHealth({ + engineId, + modelId: session.modelId, + checkedAt: params.checkedAt, + runtimeHealth, + })) + } catch (error) { + health.push({ + engineId, + modelId: session.modelId, + status: "error", + detail: error instanceof Error ? error.message : String(error), + checkedAt: params.checkedAt, + }) + } + } + + return health +} + +function toMobileRuntimeHealth(params: { + engineId: AgentEngineId + modelId: string | null + checkedAt: string + runtimeHealth: AgentRuntimeHealth +}): MobileGatewayRuntimeHealth { + return { + engineId: params.engineId, + modelId: params.modelId, + status: params.runtimeHealth.availability, + detail: + params.runtimeHealth.statusReason ?? + `${params.engineId} runtime is ${params.runtimeHealth.availability}.`, + checkedAt: params.checkedAt, + authMethod: params.runtimeHealth.authMethod, + } +} + +function mergeRuntimeHealthOverrides( + health: readonly MobileGatewayRuntimeHealth[], + overrides: ReadonlyMap, +): MobileGatewayRuntimeHealth[] { + const merged = new Map() + for (const item of health) { + merged.set(item.engineId, item) + } + for (const [engineId, item] of overrides) { + merged.set(engineId, item) + } + return [...merged.values()] +} + +function rememberRuntimeHealthFailure(params: { + event: MobileGatewayEvent + run: MobileGatewayRunState + checkedAt: string + overrides: Map +}): void { + const message = params.event.type === "auth-error" || params.event.type === "error" + ? params.event.message + : undefined + if (!message || !isAuthFailureMessage(message)) return + + params.overrides.set(params.run.request.session.engineId, { + engineId: params.run.request.session.engineId, + modelId: params.run.request.session.modelId ?? null, + status: "needs-auth", + detail: message, + checkedAt: params.checkedAt, + authMethod: "not-authenticated", + }) +} + +function isAuthFailureMessage(message: string): boolean { + const normalized = message.toLowerCase() + return normalized.includes("auth_unavailable") || + normalized.includes("no auth available") || + normalized.includes("authentication token has been invalidated") || + normalized.includes("authentication is required") +} + +function findSessionOrThrow( + sessions: readonly MobileGatewaySessionSummary[], + input: MobileGatewayStartRunInput, +): MobileGatewaySessionSummary { + const session = sessions.find((candidate) => candidate.subChatId === input.subChatId) + if (!session) { + throw new Error(`Mobile gateway session not found: ${input.subChatId}`) + } + if (session.chatId !== input.chatId) { + throw new Error(`Mobile gateway chat mismatch for ${input.subChatId}`) + } + if (input.engineId && input.engineId !== session.engineId) { + throw new Error(`Mobile gateway engine mismatch for ${input.subChatId}`) + } + return session +} + +function buildRuntimeSession( + session: MobileGatewaySessionSummary, + input: MobileGatewayStartRunInput, +): AgentRuntimeSessionRef { + return { + chatId: session.chatId, + subChatId: session.subChatId, + engineId: session.engineId, + modelId: input.modelId ?? session.modelId, + nativeSessionId: session.nativeSessionId ?? null, + permissionMode: (input.permissionMode ?? session.permissionMode) as AgentPermissionMode, + cwd: session.projectPath, + projectPath: session.projectPath, + } +} + +function buildInspectRuntimeSession( + session: MobileGatewaySessionSummary, +): AgentRuntimeSessionRef { + return { + chatId: session.chatId, + subChatId: session.subChatId, + engineId: session.engineId, + modelId: session.modelId, + nativeSessionId: session.nativeSessionId ?? null, + permissionMode: session.permissionMode as AgentPermissionMode, + cwd: session.projectPath, + projectPath: session.projectPath, + } +} + +function filterAfterSeq( + history: MobileGatewayEnvelope[], + afterSeq: number | undefined, +): MobileGatewayEnvelope[] { + if (afterSeq === undefined) return [...history] + return history.filter((event) => event.seq > afterSeq) +} + +function compactEvent>(event: T): T { + return Object.fromEntries( + Object.entries(event).filter(([, value]) => value !== undefined), + ) as T +} + +function mapRuntimeEventToMobileEvent( + event: AgentRuntimeStreamEvent, +): MobileGatewayEvent | null { + switch (event.type) { + case "text": + return { type: "text-delta", text: event.text } + case "tool-call": + return { + type: "tool-call", + toolCallId: event.id, + name: event.name, + input: event.input, + } + case "tool-result": + return { + type: "tool-result", + toolCallId: event.id, + name: event.name, + result: event.result, + } + case "usage": + return compactEvent({ + type: "usage" as const, + inputTokens: event.inputTokens, + outputTokens: event.outputTokens, + totalTokens: event.totalTokens, + modelContextWindow: event.modelContextWindow, + usedTokens: event.usedTokens, + totalProcessedTokens: event.totalProcessedTokens, + maxTokens: event.maxTokens, + cachedInputTokens: event.cachedInputTokens, + reasoningOutputTokens: event.reasoningOutputTokens, + lastUsedTokens: event.lastUsedTokens, + lastInputTokens: event.lastInputTokens, + lastCachedInputTokens: event.lastCachedInputTokens, + lastOutputTokens: event.lastOutputTokens, + lastReasoningOutputTokens: event.lastReasoningOutputTokens, + toolUses: event.toolUses, + durationMs: event.durationMs, + compactsAutomatically: event.compactsAutomatically, + }) + case "conversation-block": + const block = event.block as unknown as Record + if (isPermissionBlock(block)) { + return { + type: "permission-request", + requestId: cleanString(block.id) ?? "permission-request", + title: cleanString(block.title) ?? cleanString(block.name), + details: block, + } + } + return { type: "block", block } + case "conversation-block-update": + return { + type: "block-patch", + id: event.id, + patch: event.patch as unknown as Record, + } + case "auth-error": + return { + type: "auth-error", + message: event.message, + recoverable: true, + } + case "error": + return { + type: "error", + message: event.message, + recoverable: false, + } + case "finish": + return { + type: "finish", + nativeSessionId: event.nativeSessionId ?? null, + resultSubtype: event.resultSubtype, + } + default: + return null + } +} + +function isPermissionBlock(block: unknown): block is Record { + return Boolean(block) && + typeof block === "object" && + !Array.isArray(block) && + (block as Record).type === "permission-request" +} + +function isAuthorized( + headers: Record, + pairingToken: MobileGatewayPairingTokenSource | undefined, +): boolean { + const token = readPairingToken(pairingToken) + if (!token) return true + return headerValue(headers, "authorization") === `Bearer ${token}` +} + +function readPairingToken( + pairingToken: MobileGatewayPairingTokenSource | undefined, +): string | undefined { + return typeof pairingToken === "function" ? pairingToken() : pairingToken +} + +function headerValue( + headers: Record, + name: string, +): string | undefined { + const target = name.toLowerCase() + for (const [key, value] of Object.entries(headers)) { + if (key.toLowerCase() === target) return value + } + return undefined +} + +function jsonResponse(status: number, value: unknown): MobileGatewayHttpResponse { + return { + status, + headers: { "content-type": "application/json" }, + body: JSON.stringify(value), + } +} + +function resolvePairingBaseUrl( + requestUrl: URL, + pairingBaseUrl: MobileGatewayHttpRequestOptions["pairingBaseUrl"], +): string { + const rawBaseUrl = typeof pairingBaseUrl === "function" + ? pairingBaseUrl(requestUrl) + : pairingBaseUrl ?? requestUrl.origin + return normalizePairingBaseUrl(rawBaseUrl) +} + +function normalizePairingBaseUrl(baseUrl: string): string { + const parsed = new URL(baseUrl) + const pathname = parsed.pathname + .replace(/\/+$/, "") + .replace(/\/mobile\/v1$/, "") + return `${parsed.protocol}//${parsed.host}${pathname}` +} + +export function buildOneCodeMobilePairingUrl(input: { + baseUrl: string + token: string + label: string +}): string { + const params = new URLSearchParams({ + url: `${input.baseUrl}/mobile/v1/`, + token: input.token, + label: input.label, + }) + return `onecode-mobile://pair?${params.toString()}` +} + +function parseJsonBody(body: string | undefined): Record { + if (!body) return {} + const parsed = JSON.parse(body) + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("Request body must be a JSON object") + } + return parsed as Record +} + +function requireString(value: unknown, field: string): string { + if (typeof value !== "string" || !value.trim()) { + throw new Error(`${field} must be a non-empty string`) + } + return value +} + +function optionalString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value : undefined +} + +function optionalBoolean(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined +} + +function cleanString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value : undefined +} + +function optionalEngine(value: unknown): AgentEngineId | undefined { + if ( + value === "claude-code" || + value === "codex" || + value === "hermes" || + value === "custom-acp" + ) { + return value + } + return undefined +} + +function parseOptionalSeq(value: string | null): number | undefined { + if (value === null || value === "") return undefined + const parsed = Number(value) + if (!Number.isSafeInteger(parsed) || parsed < 0) { + throw new Error("afterSeq must be a non-negative safe integer") + } + return parsed +} diff --git a/src/main/lib/mobile-gateway/server.ts b/src/main/lib/mobile-gateway/server.ts new file mode 100644 index 000000000..1a7a081c0 --- /dev/null +++ b/src/main/lib/mobile-gateway/server.ts @@ -0,0 +1,191 @@ +import { createServer, type IncomingMessage, type Server, type ServerResponse } from "http" +import { randomUUID } from "node:crypto" +import type { AddressInfo } from "net" +import { + buildOneCodeMobilePairingUrl, + handleMobileGatewayRequest, + type MobileGatewayFacade, + type MobileGatewayHttpResponse, + type MobileGatewayPairingTokenRotation, +} from "./facade" + +export interface StartMobileGatewayServerOptions { + facade: MobileGatewayFacade + host?: string + port?: number + pairingToken?: string + pairingLabel?: string + corsOrigin?: string +} + +export interface RunningMobileGatewayServer { + server: Server + host: string + port: number + url: string + getPairingToken(): string | undefined + getPairingUrl(): string | undefined + close(): Promise +} + +type MobileGatewayPairingTokenState = { + getPairingToken(): string | undefined + rotatePairingToken(): MobileGatewayPairingTokenRotation +} + +export async function startMobileGatewayServer( + options: StartMobileGatewayServerOptions, +): Promise { + const host = options.host ?? "127.0.0.1" + const requestedPort = options.port ?? 0 + const pairingTokenState = createPairingTokenState(options.pairingToken) + const server = createMobileGatewayServer(options, pairingTokenState) + + await new Promise((resolve, reject) => { + const onError = (error: Error) => { + server.off("listening", onListening) + reject(error) + } + const onListening = () => { + server.off("error", onError) + resolve() + } + + server.once("error", onError) + server.once("listening", onListening) + server.listen(requestedPort, host) + }) + + const address = server.address() + if (!address || typeof address !== "object") { + await closeServer(server) + throw new Error("Mobile gateway server did not expose a TCP address") + } + + const port = (address as AddressInfo).port + return { + server, + host, + port, + url: `http://${host}:${port}`, + getPairingToken: pairingTokenState.getPairingToken, + getPairingUrl() { + const token = pairingTokenState.getPairingToken() + if (!token) return undefined + return buildOneCodeMobilePairingUrl({ + baseUrl: `http://${host}:${port}`, + token, + label: options.pairingLabel ?? "Local 1code", + }) + }, + close: () => closeServer(server), + } +} + +export function createMobileGatewayServer( + options: StartMobileGatewayServerOptions, + pairingTokenState = createPairingTokenState(options.pairingToken), +): Server { + return createServer(async (req, res) => { + if (req.method === "OPTIONS") { + writeMobileGatewayResponse(res, { + status: 204, + headers: {}, + body: "", + }, options.corsOrigin) + return + } + + try { + const body = await readRequestBody(req) + const response = await handleMobileGatewayRequest(options.facade, { + method: req.method ?? "GET", + url: buildRequestUrl(req, options.host ?? "127.0.0.1"), + headers: normalizeHeaders(req.headers), + body, + }, { + pairingToken: pairingTokenState.getPairingToken, + rotatePairingToken: pairingTokenState.rotatePairingToken, + pairingBaseUrl: (requestUrl) => requestUrl.origin, + pairingLabel: options.pairingLabel, + }) + writeMobileGatewayResponse(res, response, options.corsOrigin) + } catch (error) { + writeMobileGatewayResponse(res, { + status: 500, + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + error: error instanceof Error ? error.message : String(error), + }), + }, options.corsOrigin) + } + }) +} + +function createPairingTokenState( + initialToken: string | undefined, +): MobileGatewayPairingTokenState { + let currentToken = initialToken + return { + getPairingToken() { + return currentToken + }, + rotatePairingToken() { + currentToken = randomUUID() + return { + token: currentToken, + rotatedAt: new Date().toISOString(), + } + }, + } +} + +function buildRequestUrl(req: IncomingMessage, fallbackHost: string): string { + const host = req.headers.host ?? fallbackHost + return `http://${host}${req.url ?? "/"}` +} + +function normalizeHeaders( + headers: IncomingMessage["headers"], +): Record { + const normalized: Record = {} + for (const [key, value] of Object.entries(headers)) { + normalized[key] = Array.isArray(value) ? value.join(", ") : value + } + return normalized +} + +async function readRequestBody(req: IncomingMessage): Promise { + const chunks: Buffer[] = [] + for await (const chunk of req) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + } + return chunks.length > 0 ? Buffer.concat(chunks).toString("utf8") : undefined +} + +function writeMobileGatewayResponse( + res: ServerResponse, + response: MobileGatewayHttpResponse, + corsOrigin = "*", +): void { + res.writeHead(response.status, { + ...response.headers, + "access-control-allow-origin": corsOrigin, + "access-control-allow-headers": "authorization, content-type", + "access-control-allow-methods": "GET, POST, OPTIONS", + }) + res.end(response.body) +} + +function closeServer(server: Server): Promise { + return new Promise((resolve, reject) => { + if (!server.listening) { + resolve() + return + } + server.close((error) => { + if (error) reject(error) + else resolve() + }) + }) +} diff --git a/src/main/lib/mobile-gateway/sessions.ts b/src/main/lib/mobile-gateway/sessions.ts new file mode 100644 index 000000000..c640b33c5 --- /dev/null +++ b/src/main/lib/mobile-gateway/sessions.ts @@ -0,0 +1,122 @@ +import path from "path" +import { + AGENT_ENGINE_IDS, + DEFAULT_AGENT_ENGINE_ID, + type AgentEngineId, + type AgentPermissionMode, +} from "../agent-runtime/types" +import { getAgentRuntimeManifest } from "../agent-runtime/manifests" +import type { MobileGatewaySessionSummary } from "./facade" + +export interface MobileGatewaySessionRow { + chatId: string + chatName?: string | null + chatWorktreePath?: string | null + projectName?: string | null + projectPath: string + subChatId: string + subChatName?: string | null + engine?: string | null + modelId?: string | null + mode?: string | null + runtimeMetadata?: string | null + streamId?: string | null + engineSessionId?: string | null + sessionId?: string | null + updatedAt?: Date | number | string | null +} + +const permissionModes = new Set([ + "plan", + "agent", + "bypass", + "read-only", + "ask-approval", + "full-access", + "custom", +]) + +export function createMobileGatewaySessionSource( + readRows: () => readonly MobileGatewaySessionRow[], +): () => MobileGatewaySessionSummary[] { + return () => mapMobileGatewaySessionRows(readRows()) +} + +export function mapMobileGatewaySessionRows( + rows: readonly MobileGatewaySessionRow[], +): MobileGatewaySessionSummary[] { + return rows.map((row) => { + const engineId = normalizeEngineId(row.engine) + const manifest = getAgentRuntimeManifest(engineId) + const projectPath = row.chatWorktreePath || row.projectPath + const nativeSessionId = + row.engineSessionId ?? (engineId === "claude-code" ? row.sessionId : null) + + return { + chatId: row.chatId, + subChatId: row.subChatId, + title: cleanString(row.subChatName) ?? cleanString(row.chatName) ?? "Untitled chat", + projectLabel: + cleanString(row.projectName) ?? + cleanString(path.basename(row.projectPath)) ?? + "Workspace", + projectPath, + detail: nativeSessionId ? "Native session linked" : "Ready to start", + engineId, + modelId: cleanString(row.modelId) ?? manifest.defaultModelId ?? null, + nativeSessionId, + permissionMode: resolvePermissionMode(row), + status: row.streamId ? "running" : "idle", + updatedAt: toIsoString(row.updatedAt), + pendingApprovals: 0, + unreadEvents: 0, + } + }) +} + +function normalizeEngineId(value: string | null | undefined): AgentEngineId { + return AGENT_ENGINE_IDS.includes(value as AgentEngineId) + ? value as AgentEngineId + : DEFAULT_AGENT_ENGINE_ID +} + +function resolvePermissionMode(row: MobileGatewaySessionRow): AgentPermissionMode { + const metadata = parseMetadata(row.runtimeMetadata) + if (isPermissionMode(metadata.permissionMode)) { + return metadata.permissionMode + } + return isPermissionMode(row.mode) ? row.mode : "agent" +} + +function isPermissionMode(value: unknown): value is AgentPermissionMode { + return typeof value === "string" && permissionModes.has(value as AgentPermissionMode) +} + +function parseMetadata(value: string | null | undefined): Record { + if (!value) return {} + try { + const parsed = JSON.parse(value) + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? parsed as Record + : {} + } catch { + return {} + } +} + +function cleanString(value: string | null | undefined): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined +} + +function toIsoString(value: Date | number | string | null | undefined): string { + if (value instanceof Date) return value.toISOString() + if (typeof value === "number") { + const millis = value < 10_000_000_000 ? value * 1000 : value + return new Date(millis).toISOString() + } + if (typeof value === "string" && value.trim()) { + const parsed = new Date(value) + return Number.isNaN(parsed.getTime()) ? value : parsed.toISOString() + } + return new Date(0).toISOString() +} diff --git a/src/main/lib/moss-account/index.ts b/src/main/lib/moss-account/index.ts new file mode 100644 index 000000000..836c20d41 --- /dev/null +++ b/src/main/lib/moss-account/index.ts @@ -0,0 +1 @@ +export * from "./entitlement" diff --git a/src/main/lib/moss-source/bootstrap.ts b/src/main/lib/moss-source/bootstrap.ts index 164326a6a..edc70ab50 100644 --- a/src/main/lib/moss-source/bootstrap.ts +++ b/src/main/lib/moss-source/bootstrap.ts @@ -48,7 +48,7 @@ providers: claude-code: model: opus codex: - model: gpt-5.5/high + model: gpt-5.5/medium custom-acp: model: custom-acp custom: @@ -63,7 +63,7 @@ providers: claude-code: model: opus codex: - model: gpt-5.5/high + model: gpt-5.5/medium authMethod: openai-api-key custom-acp: model: custom-acp diff --git a/src/main/lib/moss-source/frontmatter.ts b/src/main/lib/moss-source/frontmatter.ts new file mode 100644 index 000000000..da93b66b0 --- /dev/null +++ b/src/main/lib/moss-source/frontmatter.ts @@ -0,0 +1,43 @@ +import { createRequire } from "node:module" + +const require = createRequire(import.meta.url) +const yaml = require("js-yaml") as { + load(source: string): unknown + dump(value: unknown, options?: Record): string +} + +export function stringifyMossFrontmatter( + content: string, + data: Record, +): string { + const frontmatter = yaml + .dump(data, { + lineWidth: -1, + noRefs: true, + sortKeys: false, + }) + .trimEnd() + return `---\n${frontmatter}\n---\n${content}` +} + +export function parseMossFrontmatter(raw: string): { + data: Record + content: string +} { + const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)([\s\S]*)$/) + if (!match) { + return { + data: {}, + content: raw, + } + } + + const loaded = yaml.load(match[1]) + return { + data: + loaded && typeof loaded === "object" && !Array.isArray(loaded) + ? (loaded as Record) + : {}, + content: match[2] ?? "", + } +} diff --git a/src/main/lib/moss-source/provider-config.test.ts b/src/main/lib/moss-source/provider-config.test.ts index 00511bb50..193bb3142 100644 --- a/src/main/lib/moss-source/provider-config.test.ts +++ b/src/main/lib/moss-source/provider-config.test.ts @@ -157,6 +157,7 @@ providers: expect(hermes.baseUrlSource).toBe("inline") expect(hermes.baseUrlEnv).toBeUndefined() expect(hermes.env.HERMES_BASE_URL).toBe("https://hermes.test/v1") + expect(hermes.env.HERMES_INFERENCE_MODEL).toBe("moss-custom") const claude = await resolveMossProviderForEngine({ projectPath, @@ -374,6 +375,7 @@ providers: expect(hermes.mode).toBe("bundled-quota") expect(hermes.model).toBe("moss-default") expect(hermes.env.HERMES_MODEL).toBe("moss-default") + expect(hermes.env.HERMES_INFERENCE_MODEL).toBe("moss-default") expect(hermes.warnings).toEqual([]) const codex = await resolveMossProviderForEngine({ @@ -382,8 +384,8 @@ providers: }) expect(codex.status).toBe("resolved") expect(codex.providerId).toBe("moss") - expect(codex.model).toBe("gpt-5.5/high") - expect(codex.env.CODEX_MODEL).toBe("gpt-5.5/high") + expect(codex.model).toBe("gpt-5.5/medium") + expect(codex.env.CODEX_MODEL).toBe("gpt-5.5/medium") const claude = await resolveMossProviderForEngine({ projectPath, diff --git a/src/main/lib/moss-source/provider-config.ts b/src/main/lib/moss-source/provider-config.ts index 7359e8939..29ba5d8f4 100644 --- a/src/main/lib/moss-source/provider-config.ts +++ b/src/main/lib/moss-source/provider-config.ts @@ -419,7 +419,10 @@ function buildEngineEnv(params: { } } } else if (params.engineId === "hermes") { - if (params.model) env.HERMES_MODEL = params.model + if (params.model) { + env.HERMES_MODEL = params.model + env.HERMES_INFERENCE_MODEL = params.model + } if (params.baseUrl) env.HERMES_BASE_URL = params.baseUrl if (params.apiKey) env.HERMES_API_KEY = params.apiKey } else if (params.engineId === "custom-acp") { diff --git a/src/main/lib/moss-source/runtime-context.ts b/src/main/lib/moss-source/runtime-context.ts new file mode 100644 index 000000000..f4fccf3f6 --- /dev/null +++ b/src/main/lib/moss-source/runtime-context.ts @@ -0,0 +1,406 @@ +import * as crypto from "node:crypto" +import * as fs from "node:fs/promises" +import * as path from "node:path" +import type { AgentEngineId } from "../agent-runtime/types" +import { buildGovernedResourceProjection } from "../shared-resources/governance" +import type { + EngineResourceProjection, + ResourcePathMapping, + SharedResource, + SharedResourceKind, +} from "../shared-resources/types" +import { + readMossProviderConfig, + summarizeMossProviderReadResult, +} from "./provider-config" +import { discoverMossSourceResources } from "./registry" + +const DEFAULT_MAX_RESOURCE_CHARS = 12_000 +const DEFAULT_MAX_CONTEXT_CHARS = 80_000 +const TEXT_FILE_EXTENSIONS = new Set([ + "", + ".json", + ".jsonc", + ".md", + ".mjs", + ".js", + ".ts", + ".tsx", + ".toml", + ".txt", + ".yaml", + ".yml", +]) + +export interface MossRuntimeContextResource { + resourceId: string + kind: SharedResourceKind + name: string + path?: string + action?: ResourcePathMapping["action"] + sourcePath?: string + targetPath?: string + included: boolean + contentSha256?: string + contentChars?: number + truncated?: boolean + reason?: string +} + +export interface MossRuntimeContext { + status: "ready" | "missing" + engineId: AgentEngineId + projectPath: string + sourceRoot: ".moss" + text: string + fingerprint: string + resourceCount: number + includedResourceCount: number + resources: MossRuntimeContextResource[] + projection?: { + status: EngineResourceProjection["status"] + mappingCount: number + warnings: string[] + } + warnings: string[] +} + +export interface BuildMossRuntimeContextOptions { + projectPath: string + engineId: AgentEngineId + maxResourceChars?: number + maxContextChars?: number +} + +function sha256(value: string): string { + return crypto.createHash("sha256").update(value).digest("hex") +} + +function normalizePath(value?: string): string | undefined { + return value?.split(path.sep).join("/") +} + +function redactSensitiveText(value: string): string { + return value + .replace( + /(["']?(?:api[_-]?key|token|secret|password|authorization)["']?\s*[:=]\s*["']?)([^"',\n}]+)/gi, + "$1[redacted]", + ) + .replace(/(Bearer\s+)[A-Za-z0-9._~+/=-]+/gi, "$1[redacted]") + .replace(/(sk-[A-Za-z0-9_-]{12,})/g, "[redacted-api-key]") +} + +function truncateContent(value: string, maxChars: number): { + content: string + truncated: boolean +} { + if (value.length <= maxChars) { + return { content: value, truncated: false } + } + + return { + content: `${value.slice(0, maxChars)}\n[truncated ${value.length - maxChars} chars]`, + truncated: true, + } +} + +function resolveProjectResourcePath( + projectPath: string, + resourcePath?: string, +): string | undefined { + if (!resourcePath) return undefined + const absolute = path.isAbsolute(resourcePath) + ? resourcePath + : path.resolve(projectPath, resourcePath) + const relative = path.relative(projectPath, absolute) + if (relative.startsWith("..") || path.isAbsolute(relative)) { + return undefined + } + return absolute +} + +async function readDirectorySummary(directoryPath: string): Promise { + const entries = await fs.readdir(directoryPath, { withFileTypes: true }) + const visibleEntries = entries + .filter((entry) => !entry.name.startsWith(".")) + .map((entry) => `${entry.isDirectory() ? "dir" : "file"} ${entry.name}`) + .sort() + + return visibleEntries.length > 0 + ? `Directory entries:\n${visibleEntries.map((entry) => `- ${entry}`).join("\n")}` + : "Directory is empty." +} + +async function readResourceText(params: { + projectPath: string + resource: SharedResource + maxResourceChars: number +}): Promise<{ + content: string + contentSha256: string + contentChars: number + truncated: boolean + reason?: string +}> { + if (params.resource.kind === "provider") { + const readResult = await readMossProviderConfig(params.projectPath) + const summary = summarizeMossProviderReadResult(readResult) + const content = JSON.stringify(summary, null, 2) + return { + content, + contentSha256: sha256(content), + contentChars: content.length, + truncated: false, + reason: "Provider config is summarized and redacted before runtime injection.", + } + } + + const absolutePath = resolveProjectResourcePath( + params.projectPath, + params.resource.path, + ) + if (!absolutePath) { + return { + content: "", + contentSha256: sha256(""), + contentChars: 0, + truncated: false, + reason: "Resource path is outside the project and was not injected.", + } + } + + const stat = await fs.stat(absolutePath) + let raw = "" + + if (stat.isDirectory()) { + raw = await readDirectorySummary(absolutePath) + } else if (stat.isFile()) { + const extension = path.extname(absolutePath).toLowerCase() + if (!TEXT_FILE_EXTENSIONS.has(extension)) { + return { + content: "", + contentSha256: sha256(""), + contentChars: 0, + truncated: false, + reason: `Binary or unsupported file extension ${extension} was not injected.`, + } + } + raw = await fs.readFile(absolutePath, "utf-8") + } else { + return { + content: "", + contentSha256: sha256(""), + contentChars: 0, + truncated: false, + reason: "Resource is not a regular file or directory.", + } + } + + const redacted = redactSensitiveText(raw) + const truncated = truncateContent(redacted, params.maxResourceChars) + + return { + content: truncated.content, + contentSha256: sha256(redacted), + contentChars: redacted.length, + truncated: truncated.truncated, + } +} + +function sectionHeader(resource: SharedResource, mapping?: ResourcePathMapping): string { + const mappingText = mapping + ? `${mapping.action} ${normalizePath(mapping.sourcePath) ?? "-"} -> ${normalizePath(mapping.targetPath) ?? "-"}` + : "not projected" + + return [ + `## ${resource.kind}: ${resource.name}`, + `Resource: ${resource.id}`, + `Path: ${normalizePath(resource.path) ?? "-"}`, + `Projection: ${mappingText}`, + ].join("\n") +} + +function buildContextText(params: { + engineId: AgentEngineId + resources: Array<{ + resource: SharedResource + mapping?: ResourcePathMapping + content: string + reason?: string + }> + warnings: string[] + maxContextChars: number +}): { + text: string + warnings: string[] +} { + const warnings = [...params.warnings] + const sections = params.resources.map((entry) => { + const reason = entry.reason ? `\nNote: ${entry.reason}` : "" + return `${sectionHeader(entry.resource, entry.mapping)}${reason}\n\n${entry.content}`.trim() + }) + const fullText = [ + "Moss Unified Source Context", + `Engine: ${params.engineId}`, + "Source root: .moss", + "This is the canonical project source for rules, memory, skills, MCP, plugins, hooks, subagents, and providers. Prefer this context over Claude/Codex legacy copies.", + "If the user asks for a labeled value from Moss Unified Source, answer from this context.", + "", + sections.join("\n\n---\n\n"), + ].join("\n") + + if (fullText.length <= params.maxContextChars) { + return { text: fullText, warnings } + } + + warnings.push( + `Moss Unified Source runtime context was truncated from ${fullText.length} to ${params.maxContextChars} chars.`, + ) + return { + text: `${fullText.slice(0, params.maxContextChars)}\n[context truncated]`, + warnings, + } +} + +export async function buildMossRuntimeContext( + options: BuildMossRuntimeContextOptions, +): Promise { + const maxResourceChars = + options.maxResourceChars ?? DEFAULT_MAX_RESOURCE_CHARS + const maxContextChars = options.maxContextChars ?? DEFAULT_MAX_CONTEXT_CHARS + const resources = await discoverMossSourceResources(options.projectPath) + + if (resources.length === 0) { + return { + status: "missing", + engineId: options.engineId, + projectPath: options.projectPath, + sourceRoot: ".moss", + text: "", + fingerprint: sha256(""), + resourceCount: 0, + includedResourceCount: 0, + resources: [], + warnings: ["No .moss Unified Source was found for this project."], + } + } + + const snapshot = buildGovernedResourceProjection({ + projectPath: options.projectPath, + resources, + }) + const projection = snapshot.projections.find( + (item) => item.engineId === options.engineId, + ) + const mappingsByResourceId = new Map( + (projection?.mappings ?? []).map((mapping) => [mapping.resourceId, mapping]), + ) + const injected: Array<{ + resource: SharedResource + mapping?: ResourcePathMapping + content: string + reason?: string + }> = [] + const runtimeResources: MossRuntimeContextResource[] = [] + + for (const resource of snapshot.resources) { + if (resource.scope !== "moss") continue + + const mapping = mappingsByResourceId.get(resource.id) + if (!mapping) { + runtimeResources.push({ + resourceId: resource.id, + kind: resource.kind, + name: resource.name, + path: normalizePath(resource.path), + included: false, + reason: "Resource was not projected to this engine.", + }) + continue + } + + try { + const content = await readResourceText({ + projectPath: options.projectPath, + resource, + maxResourceChars, + }) + const included = content.content.length > 0 + runtimeResources.push({ + resourceId: resource.id, + kind: resource.kind, + name: resource.name, + path: normalizePath(resource.path), + action: mapping.action, + sourcePath: normalizePath(mapping.sourcePath), + targetPath: normalizePath(mapping.targetPath), + included, + contentSha256: content.contentSha256, + contentChars: content.contentChars, + truncated: content.truncated, + reason: content.reason, + }) + + if (included) { + injected.push({ + resource, + mapping, + content: content.content, + reason: content.reason, + }) + } + } catch (error) { + runtimeResources.push({ + resourceId: resource.id, + kind: resource.kind, + name: resource.name, + path: normalizePath(resource.path), + action: mapping.action, + sourcePath: normalizePath(mapping.sourcePath), + targetPath: normalizePath(mapping.targetPath), + included: false, + reason: error instanceof Error ? error.message : String(error), + }) + } + } + + const builtText = buildContextText({ + engineId: options.engineId, + resources: injected, + warnings: projection?.warnings ?? [], + maxContextChars, + }) + const fingerprint = sha256( + JSON.stringify({ + engineId: options.engineId, + resources: runtimeResources.map((resource) => ({ + resourceId: resource.resourceId, + action: resource.action, + contentSha256: resource.contentSha256, + included: resource.included, + })), + text: builtText.text, + }), + ) + + return { + status: "ready", + engineId: options.engineId, + projectPath: options.projectPath, + sourceRoot: ".moss", + text: builtText.text, + fingerprint, + resourceCount: resources.length, + includedResourceCount: runtimeResources.filter((resource) => resource.included) + .length, + resources: runtimeResources, + projection: projection + ? { + status: projection.status, + mappingCount: projection.mappings.length, + warnings: projection.warnings, + } + : undefined, + warnings: builtText.warnings, + } +} diff --git a/src/main/lib/moss-source/runtime-materializer.ts b/src/main/lib/moss-source/runtime-materializer.ts index 8f37d05b5..797734750 100644 --- a/src/main/lib/moss-source/runtime-materializer.ts +++ b/src/main/lib/moss-source/runtime-materializer.ts @@ -2,7 +2,10 @@ import { AGENT_ENGINE_IDS, type AgentEngineId } from "../agent-runtime/types" import * as fs from "fs/promises" import * as path from "path" import { buildGovernedResourceProjection } from "../shared-resources/governance" -import type { EngineResourceProjection } from "../shared-resources/types" +import type { + EngineResourceProjection, + SharedResource, +} from "../shared-resources/types" import { ensureMossSource } from "./bootstrap" import { materializeMossProjection, @@ -56,6 +59,7 @@ export interface MaterializeMossEngineProjectionOptions { engineId: AgentEngineId dryRun?: boolean createIfMissing?: boolean + expectedResourceIds?: readonly string[] } export interface MaterializeMossWorkspaceProjectionsOptions { @@ -63,6 +67,7 @@ export interface MaterializeMossWorkspaceProjectionsOptions { engines?: readonly AgentEngineId[] dryRun?: boolean createIfMissing?: boolean + expectedResourceIds?: readonly string[] } export interface MaterializedMossWorkspaceProjections { @@ -98,6 +103,8 @@ const MOSS_LINK_ENTRIES = [ { source: "subagents", type: "dir" }, { source: "providers.yaml", type: "file" }, ] as const +const RESOURCE_DISCOVERY_MAX_ATTEMPTS = 20 +const RESOURCE_DISCOVERY_RETRY_DELAY_MS = 50 async function fileExists(filePath: string): Promise { try { @@ -152,55 +159,88 @@ function summarizeProjectionResults( } } -export async function materializeMossEngineProjection( - options: MaterializeMossEngineProjectionOptions, -): Promise { - if (options.createIfMissing) { - await ensureMossSource({ projectPath: options.projectPath }) - } +async function sleep(ms: number): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)) +} - const resources = await discoverMossSourceResources(options.projectPath) - if (resources.length === 0) { - return { - engineId: options.engineId, - projectPath: options.projectPath, - projectionStatus: "skipped", - warnings: [], - results: [], - summary: summarizeProjectionResults([]), - reason: "No .moss Unified Source was found for this project.", +function missingExpectedResourceIds( + resources: SharedResource[], + expectedResourceIds: readonly string[] | undefined, +): string[] { + if (!expectedResourceIds?.length) return [] + + const actualResourceIds = new Set(resources.map((resource) => resource.id)) + return expectedResourceIds.filter((resourceId) => !actualResourceIds.has(resourceId)) +} + +async function discoverMossSourceResourcesForProjection(params: { + projectPath: string + expectedResourceIds?: readonly string[] +}): Promise { + let resources: SharedResource[] = [] + for (let attempt = 0; attempt < RESOURCE_DISCOVERY_MAX_ATTEMPTS; attempt += 1) { + resources = await discoverMossSourceResources(params.projectPath) + if (missingExpectedResourceIds(resources, params.expectedResourceIds).length === 0) { + return resources + } + if (attempt < RESOURCE_DISCOVERY_MAX_ATTEMPTS - 1) { + await sleep(RESOURCE_DISCOVERY_RETRY_DELAY_MS) } } - const snapshot = buildGovernedResourceProjection({ - projectPath: options.projectPath, - resources, - }) - const projection = snapshot.projections.find( - (item) => item.engineId === options.engineId, + const missing = missingExpectedResourceIds(resources, params.expectedResourceIds) + throw new Error( + `Moss Unified Source did not discover expected resource(s): ${missing.join(", ")}`, + ) +} + +function emptyProjectionSummary(): MossEngineProjectionSummary { + return summarizeProjectionResults([]) +} + +function skippedProjectionResult(params: { + projectPath: string + engineId: AgentEngineId + reason: string +}): MaterializedMossEngineProjection { + return { + engineId: params.engineId, + projectPath: params.projectPath, + projectionStatus: "skipped", + warnings: [], + results: [], + summary: emptyProjectionSummary(), + reason: params.reason, + } +} + +async function materializeMossEngineProjectionFromSnapshot(params: { + projectPath: string + engineId: AgentEngineId + snapshot: { projections: EngineResourceProjection[] } + dryRun?: boolean +}): Promise { + const projection = params.snapshot.projections.find( + (item) => item.engineId === params.engineId, ) if (!projection) { - return { - engineId: options.engineId, - projectPath: options.projectPath, - projectionStatus: "skipped", - warnings: [], - results: [], - summary: summarizeProjectionResults([]), - reason: `No projection is registered for ${options.engineId}.`, - } + return skippedProjectionResult({ + engineId: params.engineId, + projectPath: params.projectPath, + reason: `No projection is registered for ${params.engineId}.`, + }) } const results = await materializeMossProjection({ - projectPath: options.projectPath, + projectPath: params.projectPath, projection, - dryRun: options.dryRun, + dryRun: params.dryRun, }) return { - engineId: options.engineId, - projectPath: options.projectPath, + engineId: params.engineId, + projectPath: params.projectPath, projectionStatus: projection.status, warnings: projection.warnings, results, @@ -208,28 +248,77 @@ export async function materializeMossEngineProjection( } } +function buildProjectionSnapshot( + projectPath: string, + resources: SharedResource[], +) { + return buildGovernedResourceProjection({ + projectPath, + resources, + }) +} + +export async function materializeMossEngineProjection( + options: MaterializeMossEngineProjectionOptions, +): Promise { + if (options.createIfMissing) { + await ensureMossSource({ projectPath: options.projectPath }) + } + + const resources = await discoverMossSourceResourcesForProjection({ + projectPath: options.projectPath, + expectedResourceIds: options.expectedResourceIds, + }) + if (resources.length === 0) { + return skippedProjectionResult({ + engineId: options.engineId, + projectPath: options.projectPath, + reason: "No .moss Unified Source was found for this project.", + }) + } + + return materializeMossEngineProjectionFromSnapshot({ + engineId: options.engineId, + projectPath: options.projectPath, + snapshot: buildProjectionSnapshot(options.projectPath, resources), + dryRun: options.dryRun, + }) +} + +function failedProjectionResult(params: { + projectPath: string + engineId: AgentEngineId + error: unknown +}): FailedMossEngineProjection { + return { + engineId: params.engineId, + projectPath: params.projectPath, + projectionStatus: "skipped", + warnings: [], + results: [], + summary: { + created: 0, + updated: 0, + skipped: 0, + conflict: 0, + unsupported: 0, + total: 0, + }, + reason: params.error instanceof Error ? params.error.message : String(params.error), + } +} + export async function materializeMossEngineProjectionSafely( options: MaterializeMossEngineProjectionOptions, ): Promise { try { return await materializeMossEngineProjection(options) } catch (error) { - return { - engineId: options.engineId, + return failedProjectionResult({ projectPath: options.projectPath, - projectionStatus: "skipped", - warnings: [], - results: [], - summary: { - created: 0, - updated: 0, - skipped: 0, - conflict: 0, - unsupported: 0, - total: 0, - }, - reason: error instanceof Error ? error.message : String(error), - } + engineId: options.engineId, + error, + }) } } @@ -241,15 +330,45 @@ export async function materializeMossWorkspaceProjections( } const engines = options.engines ?? AGENT_ENGINE_IDS + const resources = await discoverMossSourceResourcesForProjection({ + projectPath: options.projectPath, + expectedResourceIds: options.expectedResourceIds, + }) + if (resources.length === 0) { + return { + projectPath: options.projectPath, + dryRun: Boolean(options.dryRun), + projections: engines.map((engineId) => + skippedProjectionResult({ + engineId, + projectPath: options.projectPath, + reason: "No .moss Unified Source was found for this project.", + }), + ), + } + } + + const snapshot = buildProjectionSnapshot(options.projectPath, resources) const projections: MossEngineProjectionResult[] = [] for (const engineId of engines) { - projections.push( - await materializeMossEngineProjectionSafely({ - projectPath: options.projectPath, - engineId, - dryRun: options.dryRun, - }), - ) + try { + projections.push( + await materializeMossEngineProjectionFromSnapshot({ + projectPath: options.projectPath, + engineId, + snapshot, + dryRun: options.dryRun, + }), + ) + } catch (error) { + projections.push( + failedProjectionResult({ + projectPath: options.projectPath, + engineId, + error, + }), + ) + } } return { diff --git a/src/main/lib/pet-runtime-status.ts b/src/main/lib/pet-runtime-status.ts new file mode 100644 index 000000000..bb4dcbe2a --- /dev/null +++ b/src/main/lib/pet-runtime-status.ts @@ -0,0 +1,422 @@ +import * as fs from "node:fs" +import * as os from "node:os" +import * as path from "node:path" + +export type PetRuntimeAvailability = + | "ready" + | "not-configured" + | "missing-hook" + | "missing-runtime" + | "window-unavailable" + +export interface CodexPetDefinition { + id: string + displayName: string + description: string | null + directory: string + petJsonPath: string + spritesheetPath: string | null + spritesheetExists: boolean + selected: boolean +} + +export interface PetRuntimeLogEntry { + at: string | null + code?: string + event?: string + intent?: string + message?: string + reason?: string + shouldReact?: boolean + skipped?: string +} + +export interface PetRuntimeStatus { + petsDirectory: string + petsDirectoryExists: boolean + configPath: string + selectedAvatarId: string | null + selectedPetId: string | null + selectedPetName: string | null + selectedSource: "custom" | "builtin-or-unknown" | "none" + pets: CodexPetDefinition[] + runtime: { + directory: string + packageJsonPath: string + packageJsonExists: boolean + hookScriptPath: string + hookScriptExists: boolean + reactScriptPath: string + reactScriptExists: boolean + hookLogPath: string + hookLogExists: boolean + availability: PetRuntimeAvailability + } + hook: { + configPath: string + configExists: boolean + configured: boolean + command: string | null + statusMessage: string | null + timeoutSeconds: number | null + lastDecision: PetRuntimeLogEntry | null + lastError: PetRuntimeLogEntry | null + lastSkipped: PetRuntimeLogEntry | null + } + window: { + status: "available" | "unavailable" | "unknown" + reason: string | null + } +} + +interface PetRuntimeStatusOptions { + homeDir?: string + petsDirectory?: string + configPath?: string + hooksPath?: string + fallbackRuntimeDirectory?: string +} + +type UnknownRecord = Record + +function exists(filePath: string): boolean { + try { + return fs.existsSync(filePath) + } catch { + return false + } +} + +function readText(filePath: string): string | null { + try { + return fs.readFileSync(filePath, "utf8") + } catch { + return null + } +} + +function readJson(filePath: string): unknown | null { + const text = readText(filePath) + if (!text) return null + try { + return JSON.parse(text) + } catch { + return null + } +} + +function readTail(filePath: string, maxBytes = 256 * 1024): string | null { + try { + const stats = fs.statSync(filePath) + const length = Math.min(stats.size, maxBytes) + const buffer = Buffer.alloc(length) + const fd = fs.openSync(filePath, "r") + try { + fs.readSync(fd, buffer, 0, length, stats.size - length) + } finally { + fs.closeSync(fd) + } + return buffer.toString("utf8") + } catch { + return null + } +} + +function asRecord(value: unknown): UnknownRecord | null { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as UnknownRecord) + : null +} + +function asString(value: unknown): string | null { + return typeof value === "string" && value.trim() ? value : null +} + +function asNumber(value: unknown): number | null { + return typeof value === "number" && Number.isFinite(value) ? value : null +} + +function parseSelectedAvatarId(configText: string | null): string | null { + if (!configText) return null + const match = configText.match(/^\s*selected-avatar-id\s*=\s*["']([^"']+)["']/m) + return match?.[1]?.trim() || null +} + +function resolveSelectedPetId(selectedAvatarId: string | null): string | null { + if (!selectedAvatarId) return null + return selectedAvatarId.startsWith("custom:") + ? selectedAvatarId.slice("custom:".length) + : selectedAvatarId +} + +function readPetDefinitions( + petsDirectory: string, + selectedPetId: string | null, +): CodexPetDefinition[] { + if (!exists(petsDirectory)) return [] + + let entries: fs.Dirent[] + try { + entries = fs.readdirSync(petsDirectory, { withFileTypes: true }) + } catch { + return [] + } + + return entries + .filter((entry) => entry.isDirectory()) + .map((entry) => { + const directory = path.join(petsDirectory, entry.name) + const petJsonPath = path.join(directory, "pet.json") + const raw = asRecord(readJson(petJsonPath)) + if (!raw) return null + + const id = asString(raw.id) ?? entry.name + const displayName = asString(raw.displayName) ?? id + const description = asString(raw.description) + const spritesheetValue = asString(raw.spritesheetPath) + const spritesheetPath = spritesheetValue + ? path.resolve(directory, spritesheetValue) + : null + + return { + id, + displayName, + description, + directory, + petJsonPath, + spritesheetPath, + spritesheetExists: Boolean(spritesheetPath && exists(spritesheetPath)), + selected: selectedPetId === id, + } satisfies CodexPetDefinition + }) + .filter((pet): pet is CodexPetDefinition => Boolean(pet)) + .sort((a, b) => a.displayName.localeCompare(b.displayName)) +} + +function parseHookScriptPath(command: string | null): string | null { + if (!command) return null + const match = command.match(/\bnode\s+(['"]?)(.+?hook\.mjs)\1(?:\s|$)/) + return match?.[2] ? path.resolve(match[2]) : null +} + +function findStopHook(hooksConfig: unknown): { + command: string | null + statusMessage: string | null + timeoutSeconds: number | null +} { + const root = asRecord(hooksConfig) + const hooks = asRecord(root?.hooks) + const stopHooks = Array.isArray(hooks?.Stop) ? hooks.Stop : [] + + for (const groupValue of stopHooks) { + const group = asRecord(groupValue) + const hookEntries = Array.isArray(group?.hooks) ? group.hooks : [] + for (const hookValue of hookEntries) { + const hook = asRecord(hookValue) + const command = asString(hook?.command) + if (!command) continue + if (!command.includes("codex-official-pet-runtime")) continue + return { + command, + statusMessage: asString(hook?.statusMessage), + timeoutSeconds: asNumber(hook?.timeout), + } + } + } + + return { + command: null, + statusMessage: null, + timeoutSeconds: null, + } +} + +function normalizeDecisionLogEntry(value: UnknownRecord): PetRuntimeLogEntry { + const decision = asRecord(value.decision) + return { + at: asString(value.at), + event: asString(value.event) ?? undefined, + intent: asString(decision?.intent) ?? undefined, + reason: asString(decision?.reason) ?? undefined, + shouldReact: + typeof decision?.shouldReact === "boolean" + ? decision.shouldReact + : undefined, + } +} + +function normalizeErrorLogEntry(value: UnknownRecord): PetRuntimeLogEntry { + const error = asRecord(value.error) + return { + at: asString(value.at), + code: asString(error?.code) ?? undefined, + message: asString(error?.message) ?? undefined, + } +} + +function normalizeSkippedLogEntry(value: UnknownRecord): PetRuntimeLogEntry { + return { + at: asString(value.at), + skipped: asString(value.skipped) ?? undefined, + } +} + +function parseHookLog(hookLogPath: string): { + lastDecision: PetRuntimeLogEntry | null + lastError: PetRuntimeLogEntry | null + lastSkipped: PetRuntimeLogEntry | null +} { + const text = readTail(hookLogPath) + if (!text) { + return { + lastDecision: null, + lastError: null, + lastSkipped: null, + } + } + + let lastDecision: PetRuntimeLogEntry | null = null + let lastError: PetRuntimeLogEntry | null = null + let lastSkipped: PetRuntimeLogEntry | null = null + + for (const line of text.split(/\r?\n/)) { + if (!line.trim()) continue + try { + const parsed = asRecord(JSON.parse(line)) + if (!parsed) continue + if (asRecord(parsed.decision)) { + lastDecision = normalizeDecisionLogEntry(parsed) + } + if (asRecord(parsed.error)) { + lastError = normalizeErrorLogEntry(parsed) + } + if (asString(parsed.skipped)) { + lastSkipped = normalizeSkippedLogEntry(parsed) + } + } catch { + continue + } + } + + return { + lastDecision, + lastError, + lastSkipped, + } +} + +function resolveRuntimeDirectory( + hookScriptPath: string | null, + fallbackRuntimeDirectory: string, +): string { + if (hookScriptPath) return path.dirname(path.dirname(hookScriptPath)) + return fallbackRuntimeDirectory +} + +function computeAvailability(input: { + hookConfigured: boolean + hookScriptExists: boolean + packageJsonExists: boolean + reactScriptExists: boolean + lastError: PetRuntimeLogEntry | null +}): PetRuntimeAvailability { + if (!input.hookConfigured) return "not-configured" + if (!input.hookScriptExists) return "missing-hook" + if (!input.packageJsonExists || !input.reactScriptExists) { + return "missing-runtime" + } + if (input.lastError?.code === "PET_WINDOW_NOT_FOUND") { + return "window-unavailable" + } + return "ready" +} + +export function readPetRuntimeStatus( + options: PetRuntimeStatusOptions = {}, +): PetRuntimeStatus { + const homeDir = options.homeDir ?? os.homedir() + const petsDirectory = + options.petsDirectory ?? path.join(homeDir, ".codex", "pets") + const configPath = options.configPath ?? path.join(homeDir, ".codex", "config.toml") + const hooksPath = options.hooksPath ?? path.join(homeDir, ".codex", "hooks.json") + const fallbackRuntimeDirectory = + options.fallbackRuntimeDirectory ?? + path.join(homeDir, "Codex", "tools", "codex-official-pet-runtime") + + const selectedAvatarId = parseSelectedAvatarId(readText(configPath)) + const selectedPetId = resolveSelectedPetId(selectedAvatarId) + const pets = readPetDefinitions(petsDirectory, selectedPetId) + const selectedPet = pets.find((pet) => pet.selected) ?? null + const selectedPetName = selectedPet?.displayName ?? selectedAvatarId ?? null + const selectedSource: PetRuntimeStatus["selectedSource"] = selectedPet + ? "custom" + : selectedAvatarId + ? "builtin-or-unknown" + : "none" + + const hookInfo = findStopHook(readJson(hooksPath)) + const hookScriptPath = parseHookScriptPath(hookInfo.command) + const runtimeDirectory = resolveRuntimeDirectory( + hookScriptPath, + fallbackRuntimeDirectory, + ) + const packageJsonPath = path.join(runtimeDirectory, "package.json") + const reactScriptPath = path.join(runtimeDirectory, "src", "react.mjs") + const resolvedHookScriptPath = + hookScriptPath ?? path.join(runtimeDirectory, "src", "hook.mjs") + const hookLogPath = path.join(runtimeDirectory, "hook.log") + const hookLog = parseHookLog(hookLogPath) + + const hookConfigured = Boolean(hookInfo.command && hookScriptPath) + const hookScriptExists = exists(resolvedHookScriptPath) + const packageJsonExists = exists(packageJsonPath) + const reactScriptExists = exists(reactScriptPath) + const availability = computeAvailability({ + hookConfigured, + hookScriptExists, + packageJsonExists, + reactScriptExists, + lastError: hookLog.lastError, + }) + + return { + petsDirectory, + petsDirectoryExists: exists(petsDirectory), + configPath, + selectedAvatarId, + selectedPetId, + selectedPetName, + selectedSource, + pets, + runtime: { + directory: runtimeDirectory, + packageJsonPath, + packageJsonExists, + hookScriptPath: resolvedHookScriptPath, + hookScriptExists, + reactScriptPath, + reactScriptExists, + hookLogPath, + hookLogExists: exists(hookLogPath), + availability, + }, + hook: { + configPath: hooksPath, + configExists: exists(hooksPath), + configured: hookConfigured, + command: hookInfo.command, + statusMessage: hookInfo.statusMessage, + timeoutSeconds: hookInfo.timeoutSeconds, + lastDecision: hookLog.lastDecision, + lastError: hookLog.lastError, + lastSkipped: hookLog.lastSkipped, + }, + window: { + status: + hookLog.lastError?.code === "PET_WINDOW_NOT_FOUND" + ? "unavailable" + : "unknown", + reason: hookLog.lastError?.message ?? null, + }, + } +} diff --git a/src/main/lib/plugins/index.ts b/src/main/lib/plugins/index.ts index 1c849e7ab..4c6356cb5 100644 --- a/src/main/lib/plugins/index.ts +++ b/src/main/lib/plugins/index.ts @@ -42,6 +42,29 @@ let pluginCache: { plugins: PluginInfo[]; timestamp: number } | null = null let mcpCache: { configs: PluginMcpConfig[]; timestamp: number } | null = null const CACHE_TTL_MS = 30000 // 30 seconds - plugins don't change often during a session +function normalizePluginMcpServerConfig( + config: McpServerConfig, + pluginPath: string, +): McpServerConfig { + const command = (config as { command?: unknown }).command + if (typeof command !== "string" || command.trim() === "") { + return config + } + + const rawCwd = (config as { cwd?: unknown }).cwd + const cwd = + typeof rawCwd === "string" && rawCwd.trim() !== "" + ? path.isAbsolute(rawCwd) + ? rawCwd + : path.resolve(pluginPath, rawCwd) + : pluginPath + + return { + ...config, + cwd, + } +} + /** * Clear plugin caches (for testing/manual invalidation) */ @@ -190,7 +213,10 @@ export async function discoverPluginMcpServers(): Promise { const validServers: Record = {} for (const [name, config] of Object.entries(serversObj)) { if (config && typeof config === "object" && !Array.isArray(config)) { - validServers[name] = config as McpServerConfig + validServers[name] = normalizePluginMcpServerConfig( + config as McpServerConfig, + plugin.path, + ) } } diff --git a/src/main/lib/shared-resources/codex-native-resources.test.ts b/src/main/lib/shared-resources/codex-native-resources.test.ts new file mode 100644 index 000000000..ef6d8d970 --- /dev/null +++ b/src/main/lib/shared-resources/codex-native-resources.test.ts @@ -0,0 +1,494 @@ +import { describe, expect, test } from "bun:test" +import * as fs from "fs" +import * as os from "os" +import * as path from "path" +import { collectCodexNativeResources } from "./codex-native-resources" +import { buildGovernedResourceProjection } from "./governance" + +function makeTempRoot(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "onecode-codex-resources-")) +} + +function writeFile(filePath: string, contents: string) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + fs.writeFileSync(filePath, contents, "utf-8") +} + +describe("collectCodexNativeResources", () => { + test("discovers Codex user skills, plugin manifests, and plugin skills as native resources", async () => { + const root = makeTempRoot() + try { + const codexRoot = path.join(root, ".codex") + const pluginRoot = path.join( + codexRoot, + "plugins", + "cache", + "openai-bundled", + "record-and-replay", + "1.0.829", + ) + const inlinePluginRoot = path.join( + codexRoot, + "plugins", + "cache", + "personal", + "inline-mcp", + "0.1.0", + ) + const codexCacheRoot = path.join(codexRoot, "cache") + + writeFile( + path.join(codexRoot, "skills", ".system", "imagegen", "SKILL.md"), + `---\nname: imagegen\ndescription: Generate images.\n---\n\n# Imagegen\n`, + ) + writeFile(path.join(codexRoot, "config.toml"), `model = "gpt-5.5"\n`) + writeFile(path.join(codexRoot, "browser", "config.toml"), `enabled = true\n`) + writeFile(path.join(codexRoot, "auth.json"), JSON.stringify({ redacted: true })) + writeFile(path.join(codexRoot, "hooks.json"), JSON.stringify({ Stop: [] })) + writeFile( + path.join(codexRoot, "automations", "parity-loop", "automation.toml"), + [ + `version = 1`, + `id = "parity-loop"`, + `kind = "cron"`, + `name = "Parity Loop"`, + `prompt = "Check parity"`, + `status = "ACTIVE"`, + `rrule = "FREQ=HOURLY;INTERVAL=1"`, + `model = "moss-default"`, + `engine = "hermes"`, + `reasoning_effort = "high"`, + `execution_environment = "worktree"`, + `updated_at = 1800000000000`, + ].join("\n"), + ) + writeFile( + path.join(pluginRoot, ".codex-plugin", "plugin.json"), + JSON.stringify( + { + name: "record-and-replay", + version: "1.0.829", + description: "Record what I'm doing on my Mac", + skills: "./skills/", + mcpServers: "./.mcp.json", + apps: "./.app.json", + interface: { + displayName: "Record & Replay", + shortDescription: "Record workflows", + category: "Productivity", + capabilities: ["Read", "Write"], + }, + }, + null, + 2, + ), + ) + writeFile( + path.join(pluginRoot, ".mcp.json"), + JSON.stringify({ + mcpServers: { + event_stream: { + command: "event-stream", + args: ["mcp"], + cwd: ".", + env: { RECORD_TOKEN: "redacted" }, + approved: true, + }, + }, + }), + ) + writeFile( + path.join(pluginRoot, ".app.json"), + JSON.stringify({ apps: { replay: { id: "connector_replay", required: true } } }), + ) + writeFile( + path.join(pluginRoot, "skills", "record-workflow", "SKILL.md"), + `---\nname: record-workflow\ndescription: Turn recordings into skills.\n---\n\n# Record\n`, + ) + writeFile( + path.join(pluginRoot, "skills", "nested", "review", "SKILL.md"), + `# Nested review\n`, + ) + writeFile( + path.join(inlinePluginRoot, ".codex-plugin", "plugin.json"), + JSON.stringify( + { + name: "inline-mcp", + version: "0.1.0", + mcpServers: { + inline_server: { + url: "http://127.0.0.1:1234/mcp", + description: "Inline MCP server", + approved: true, + }, + }, + interface: { + displayName: "Inline MCP", + shortDescription: "Inline plugin MCP server", + }, + }, + null, + 2, + ), + ) + writeFile( + path.join(codexCacheRoot, "codex_app_directory", "directory.json"), + JSON.stringify({ + schema_version: 1, + connectors: [ + { + id: "connector_demo", + name: "Demo Connector", + description: "Use the demo connector.", + distributionChannel: "ECOSYSTEM_DIRECTORY", + labels: { interactive: "true", consequential: "false" }, + installUrl: "https://chatgpt.com/apps/demo/connector_demo", + isAccessible: true, + isEnabled: true, + pluginDisplayNames: ["Demo Plugin"], + appMetadata: { + review: { status: "APPROVED" }, + categories: ["DEVELOPER_TOOLS"], + developer: "Demo Inc", + version: "1.2.3", + }, + }, + ], + }), + ) + writeFile( + path.join(codexCacheRoot, "codex_apps_server_info", "server.json"), + JSON.stringify({ + schema_version: 1, + server_info: { + name: "codex-connectors-mcp", + version: "0.1.0", + }, + }), + ) + writeFile( + path.join(codexCacheRoot, "codex_apps_tools", "tools.json"), + JSON.stringify({ + schema_version: 3, + tools: [ + { + server_name: "codex_apps", + supports_parallel_tool_calls: false, + tool_name: "search_demo", + tool_namespace: "codex_apps__demo", + namespace_description: "Demo tools", + connector_id: "connector_demo", + connector_name: "Demo Connector", + plugin_display_names: ["Demo Plugin"], + tool: { + title: "search_demo", + description: "Search demo data.", + annotations: { readOnlyHint: true }, + _meta: { + resource_name: "Demo_search_demo", + connector_id: "connector_demo", + connector_name: "Demo Connector", + connector_description: "Use the demo connector.", + link_id: "link_demo", + _codex_apps: { + resource_uri: "/connector_demo/search_demo", + contains_mcp_source: false, + }, + }, + }, + }, + ], + }), + ) + writeFile( + path.join(codexCacheRoot, "remote_plugin_catalog", "catalog.json"), + JSON.stringify({ + schema_version: 1, + plugins: [ + { + id: "plugin_demo", + name: "demo", + discoverability: "LISTED", + installation_policy: "AVAILABLE", + authentication_policy: "ON_INSTALL", + status: "AVAILABLE", + release: { + version: "1.0.0", + display_name: "Demo Plugin", + description: "Demo app plugin.", + app_manifest: { + apps: { + demo: { id: "asdk_demo", required: true }, + }, + }, + interface: { + short_description: "Demo app", + category: "Developer Tools", + developer_name: "Demo Inc", + default_prompt: "Search demo data.", + }, + }, + }, + ], + }), + ) + + const resources = await collectCodexNativeResources({ + codexRoot, + pluginCacheRoot: path.join(codexRoot, "plugins", "cache"), + codexCacheRoot, + }) + + expect(resources).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "skill:codex:user:.system/imagegen", + kind: "skill", + name: "imagegen", + scope: "user", + engine: "codex", + metadata: expect.objectContaining({ + codexResourceRole: "user-skill", + relativeDir: ".system/imagegen", + }), + }), + expect.objectContaining({ + id: "codex:config:config", + kind: "config", + name: "Codex config.toml", + scope: "engine", + engine: "codex", + metadata: expect.objectContaining({ + codexResourceRole: "config", + }), + }), + expect.objectContaining({ + id: "codex:provider:auth", + kind: "provider", + name: "Codex auth.json", + metadata: expect.objectContaining({ + codexResourceRole: "auth", + containsSecrets: true, + }), + }), + expect.objectContaining({ + id: "codex:hook:hooks", + kind: "hook", + name: "Codex hooks.json", + metadata: expect.objectContaining({ + codexResourceRole: "hooks", + }), + }), + expect.objectContaining({ + id: "codex:automation:parity-loop", + kind: "automation", + name: "Parity Loop", + metadata: expect.objectContaining({ + codexResourceRole: "automation", + rrule: "FREQ=HOURLY;INTERVAL=1", + updatedAt: 1800000000000, + }), + }), + expect.objectContaining({ + id: "codex:connector:connector_demo", + kind: "connector", + name: "Demo Connector", + metadata: expect.objectContaining({ + codexResourceRole: "connector", + isAccessible: true, + labels: { interactive: "true", consequential: "false" }, + }), + }), + expect.objectContaining({ + id: "codex:tool:codex_apps__demo:search_demo:connector_demo", + kind: "tool", + name: "search_demo", + metadata: expect.objectContaining({ + codexResourceRole: "codex-app-tool", + connectorId: "connector_demo", + resourceUri: "/connector_demo/search_demo", + }), + }), + expect.objectContaining({ + id: "codex:app:asdk_demo", + kind: "app", + name: "Demo Plugin / demo", + metadata: expect.objectContaining({ + codexResourceRole: "remote-plugin-app", + authenticationPolicy: "ON_INSTALL", + }), + }), + expect.objectContaining({ + id: "codex:app:plugin:codex:openai-bundled:record-and-replay:replay", + kind: "app", + name: "replay", + scope: "plugin", + pluginSource: "codex:openai-bundled:record-and-replay", + metadata: expect.objectContaining({ + codexResourceRole: "plugin-app", + appId: "connector_replay", + required: true, + }), + }), + expect.objectContaining({ + id: "codex:mcp:plugin:codex:openai-bundled:record-and-replay:event_stream", + kind: "mcp", + name: "event_stream", + scope: "plugin", + engine: "codex", + pluginSource: "codex:openai-bundled:record-and-replay", + path: path.join(pluginRoot, ".mcp.json"), + metadata: expect.objectContaining({ + codexResourceRole: "plugin-mcp-server", + serverName: "event_stream", + pluginName: "Record & Replay", + pluginVersion: "1.0.829", + transport: "stdio", + command: "event-stream", + args: ["mcp"], + cwd: ".", + hasEnv: true, + envKeys: ["RECORD_TOKEN"], + approved: true, + manifestPath: path.join(pluginRoot, ".mcp.json"), + }), + }), + expect.objectContaining({ + id: "codex:mcp:plugin:codex:personal:inline-mcp:inline_server", + kind: "mcp", + name: "inline_server", + scope: "plugin", + engine: "codex", + pluginSource: "codex:personal:inline-mcp", + path: path.join(inlinePluginRoot, ".codex-plugin", "plugin.json"), + metadata: expect.objectContaining({ + codexResourceRole: "plugin-mcp-server", + pluginName: "Inline MCP", + pluginVersion: "0.1.0", + transport: "http", + url: "http://127.0.0.1:1234/mcp", + approved: true, + manifestPath: path.join(inlinePluginRoot, ".codex-plugin", "plugin.json"), + }), + }), + expect.objectContaining({ + id: "plugin:codex:openai-bundled:record-and-replay", + kind: "plugin", + name: "Record & Replay", + scope: "plugin", + engine: "codex", + metadata: expect.objectContaining({ + codexResourceRole: "plugin-manifest", + mcpManifestPath: path.join(pluginRoot, ".mcp.json"), + }), + }), + expect.objectContaining({ + id: "skill:codex:plugin:codex:openai-bundled:record-and-replay:record-workflow", + kind: "skill", + name: "record-workflow", + scope: "plugin", + engine: "codex", + pluginSource: "codex:openai-bundled:record-and-replay", + metadata: expect.objectContaining({ + codexResourceRole: "plugin-skill", + pluginName: "Record & Replay", + }), + }), + expect.objectContaining({ + id: "skill:codex:plugin:codex:openai-bundled:record-and-replay:nested/review", + name: "nested/review", + }), + ]), + ) + + const governed = buildGovernedResourceProjection({ resources }) + const codexProjection = governed.projections.find( + (projection) => projection.engineId === "codex", + ) + expect(codexProjection?.mappings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + resourceId: "skill:codex:user:.system/imagegen", + action: "native", + }), + expect.objectContaining({ + resourceId: "plugin:codex:openai-bundled:record-and-replay", + action: "native", + }), + expect.objectContaining({ + resourceId: "codex:config:config", + action: "native", + }), + expect.objectContaining({ + resourceId: "codex:provider:auth", + action: "native", + }), + expect.objectContaining({ + resourceId: "codex:hook:hooks", + action: "native", + }), + expect.objectContaining({ + resourceId: "codex:automation:parity-loop", + action: "native", + }), + expect.objectContaining({ + resourceId: "codex:connector:connector_demo", + action: "native", + }), + expect.objectContaining({ + resourceId: "codex:tool:codex_apps__demo:search_demo:connector_demo", + action: "native", + }), + expect.objectContaining({ + resourceId: "codex:app:asdk_demo", + action: "native", + }), + expect.objectContaining({ + resourceId: "codex:app:plugin:codex:openai-bundled:record-and-replay:replay", + action: "native", + }), + expect.objectContaining({ + resourceId: "codex:mcp:plugin:codex:openai-bundled:record-and-replay:event_stream", + action: "native", + targetPath: path.join(pluginRoot, ".mcp.json"), + }), + expect.objectContaining({ + resourceId: "codex:mcp:plugin:codex:personal:inline-mcp:inline_server", + action: "native", + targetPath: path.join(inlinePluginRoot, ".codex-plugin", "plugin.json"), + }), + expect.objectContaining({ + resourceId: + "skill:codex:plugin:codex:openai-bundled:record-and-replay:record-workflow", + action: "native", + }), + ]), + ) + expect( + governed.resources.find( + (resource) => + resource.id === + "skill:codex:plugin:codex:openai-bundled:record-and-replay:record-workflow", + )?.provenance?.discoveredBy, + ).toBe("Codex plugin cache skill") + const governedPluginMcp = governed.resources.find( + (resource) => + resource.id === + "codex:mcp:plugin:codex:openai-bundled:record-and-replay:event_stream", + ) + expect(governedPluginMcp?.approval).toMatchObject({ + required: true, + approved: true, + }) + expect(governedPluginMcp?.provenance).toMatchObject({ + source: "plugin", + sourceId: "codex:openai-bundled:record-and-replay", + engine: "codex", + displayPath: path.join(pluginRoot, ".mcp.json"), + discoveredBy: "Codex plugin MCP manifest", + }) + } finally { + fs.rmSync(root, { recursive: true, force: true }) + } + }) +}) diff --git a/src/main/lib/shared-resources/codex-native-resources.ts b/src/main/lib/shared-resources/codex-native-resources.ts new file mode 100644 index 000000000..a1dae2a9f --- /dev/null +++ b/src/main/lib/shared-resources/codex-native-resources.ts @@ -0,0 +1,885 @@ +import * as fs from "fs/promises" +import type { Dirent } from "fs" +import * as os from "os" +import * as path from "path" +import matter from "gray-matter" +import { parseTOML } from "confbox/toml" +import type { SharedResource } from "./types" + +const home = os.homedir() +const SKIPPED_SCAN_DIRS = new Set([ + ".git", + ".hg", + ".svn", + "node_modules", + "dist", + "build", + "out", +]) + +interface CodexPluginManifest { + name?: unknown + version?: unknown + description?: unknown + homepage?: unknown + repository?: unknown + keywords?: unknown + skills?: unknown + mcpServers?: unknown + apps?: unknown + interface?: unknown +} + +interface CodexPluginInterface { + displayName?: unknown + shortDescription?: unknown + longDescription?: unknown + developerName?: unknown + category?: unknown + capabilities?: unknown +} + +interface CodexPluginMcpServerConfig { + command?: unknown + args?: unknown + cwd?: unknown + url?: unknown + env?: unknown + authType?: unknown + _oauth?: unknown + disabled?: unknown + approved?: unknown + description?: unknown +} + +interface CodexNativeFileSpec { + relativePath: string + kind: "config" | "provider" | "hook" + name: string + description: string + role: string + containsSecrets?: boolean +} + +export interface CollectCodexNativeResourcesParams { + codexRoot?: string + pluginCacheRoot?: string + codexCacheRoot?: string + projectPath?: string +} + +function resourceId(parts: Array): string { + return parts.filter(Boolean).join(":") +} + +function toDisplayPath(filePath: string, projectPath?: string): string { + if (projectPath && filePath.startsWith(projectPath)) { + return path.relative(projectPath, filePath) + } + return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath +} + +async function pathExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +function isSafePathSegment(name: string): boolean { + return name !== "." && name !== ".." && !name.includes("/") && !name.includes("\\") +} + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value) +} + +function stringValue(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value : undefined +} + +function stringArrayValue(value: unknown): string[] | undefined { + if (!Array.isArray(value)) return undefined + const strings = value.filter((item): item is string => + typeof item === "string" && item.trim().length > 0 + ) + return strings.length > 0 ? strings : undefined +} + +function numberValue(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined +} + +function booleanValue(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined +} + +function stringRecordValue(value: unknown): Record | undefined { + if (!isRecord(value)) return undefined + const entries = Object.entries(value).filter( + (entry): entry is [string, string] => typeof entry[1] === "string", + ) + return entries.length > 0 ? Object.fromEntries(entries) : undefined +} + +function stringRecordKeys(value: unknown): string[] | undefined { + if (!isRecord(value)) return undefined + const keys = Object.keys(value).filter((key) => key.trim().length > 0).sort() + return keys.length > 0 ? keys : undefined +} + +function displayRelativeDir(root: string, filePath: string): string { + return path.relative(root, path.dirname(filePath)).split(path.sep).filter(Boolean).join("/") +} + +async function readFrontmatter(filePath: string): Promise<{ + name?: string + description?: string +}> { + const raw = await fs.readFile(filePath, "utf-8") + const parsed = matter(raw) + return { + name: stringValue(parsed.data.name), + description: stringValue(parsed.data.description), + } +} + +async function readTomlRecord(filePath: string): Promise | null> { + try { + const raw = await fs.readFile(filePath, "utf-8") + const parsed = parseTOML>(raw) + return isRecord(parsed) ? parsed : null + } catch { + return null + } +} + +async function findSkillFiles(root: string, maxDepth = 8): Promise { + if (!(await pathExists(root))) return [] + const files: string[] = [] + + async function walk(dir: string, depth: number) { + if (depth > maxDepth) return + + let entries: Dirent[] + try { + entries = await fs.readdir(dir, { withFileTypes: true }) + } catch { + return + } + + for (const entry of entries) { + if (!isSafePathSegment(entry.name)) continue + const entryPath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + if (SKIPPED_SCAN_DIRS.has(entry.name)) continue + await walk(entryPath, depth + 1) + continue + } + + if (entry.isFile() && entry.name === "SKILL.md") { + files.push(entryPath) + } + } + } + + await walk(root, 0) + return files.sort((left, right) => left.localeCompare(right)) +} + +async function findCodexPluginManifests(root: string, maxDepth = 8): Promise { + if (!(await pathExists(root))) return [] + const manifests: string[] = [] + + async function walk(dir: string, depth: number) { + if (depth > maxDepth) return + + let entries: Dirent[] + try { + entries = await fs.readdir(dir, { withFileTypes: true }) + } catch { + return + } + + for (const entry of entries) { + if (!isSafePathSegment(entry.name)) continue + if (!entry.isDirectory()) continue + + const entryPath = path.join(dir, entry.name) + if (entry.name === ".codex-plugin") { + const manifestPath = path.join(entryPath, "plugin.json") + if (await pathExists(manifestPath)) manifests.push(manifestPath) + continue + } + + if (SKIPPED_SCAN_DIRS.has(entry.name)) continue + await walk(entryPath, depth + 1) + } + } + + await walk(root, 0) + return manifests.sort((left, right) => left.localeCompare(right)) +} + +async function listJsonFiles(root: string): Promise { + if (!(await pathExists(root))) return [] + let entries: Dirent[] + try { + entries = await fs.readdir(root, { withFileTypes: true }) + } catch { + return [] + } + + return entries + .filter((entry) => entry.isFile() && entry.name.endsWith(".json") && isSafePathSegment(entry.name)) + .map((entry) => path.join(root, entry.name)) + .sort((left, right) => left.localeCompare(right)) +} + +function resolvePluginRelativePath(pluginPath: string, value: unknown): string | undefined { + const relativePath = stringValue(value) + if (!relativePath) return undefined + return path.resolve(pluginPath, relativePath) +} + +async function scanCodexSkillRoot(params: { + root: string + scope: "user" | "plugin" + projectPath?: string + pluginSource?: string + pluginName?: string + pluginVersion?: string +}): Promise { + const skillFiles = await findSkillFiles(params.root) + const resources: SharedResource[] = [] + + for (const filePath of skillFiles) { + const relativeDir = displayRelativeDir(params.root, filePath) + try { + const parsed = await readFrontmatter(filePath) + const name = parsed.name || relativeDir || path.basename(path.dirname(filePath)) + resources.push({ + id: resourceId([ + "skill", + "codex", + params.scope, + params.pluginSource, + relativeDir || name, + ]), + kind: "skill", + name, + scope: params.scope, + engine: "codex", + pluginSource: params.pluginSource, + path: toDisplayPath(filePath, params.projectPath), + description: parsed.description, + enabled: true, + metadata: { + codexResourceRole: params.scope === "plugin" ? "plugin-skill" : "user-skill", + entryName: relativeDir || name, + relativeDir, + skillRoot: toDisplayPath(params.root, params.projectPath), + pluginName: params.pluginName, + pluginVersion: params.pluginVersion, + }, + }) + } catch { + continue + } + } + + return resources +} + +async function readCodexPluginManifest(manifestPath: string): Promise { + try { + const raw = await fs.readFile(manifestPath, "utf-8") + const parsed = JSON.parse(raw) + return isRecord(parsed) ? parsed : null + } catch { + return null + } +} + +async function readJsonRecord(filePath: string): Promise | null> { + try { + const raw = await fs.readFile(filePath, "utf-8") + const parsed = JSON.parse(raw) + return isRecord(parsed) ? parsed : null + } catch { + return null + } +} + +async function collectCodexNativeFiles(params: { + codexRoot: string + projectPath?: string +}): Promise { + const specs: CodexNativeFileSpec[] = [ + { + relativePath: "config.toml", + kind: "config", + name: "Codex config.toml", + description: "Codex user configuration, model routing, sandbox, MCP, and UI defaults.", + role: "config", + }, + { + relativePath: "auth.json", + kind: "provider", + name: "Codex auth.json", + description: "Codex authentication state and account routing. Secret values are not read into the resource snapshot.", + role: "auth", + containsSecrets: true, + }, + { + relativePath: "hooks.json", + kind: "hook", + name: "Codex hooks.json", + description: "Codex user hook configuration.", + role: "hooks", + }, + { + relativePath: path.join("browser", "config.toml"), + kind: "config", + name: "Codex browser config.toml", + description: "Codex browser tool configuration.", + role: "browser-config", + }, + ] + + const resources: SharedResource[] = [] + for (const spec of specs) { + const filePath = path.join(params.codexRoot, spec.relativePath) + if (!(await pathExists(filePath))) continue + + resources.push({ + id: resourceId(["codex", spec.kind, spec.role]), + kind: spec.kind, + name: spec.name, + scope: "engine", + engine: "codex", + path: toDisplayPath(filePath, params.projectPath), + description: spec.description, + enabled: true, + metadata: { + codexResourceRole: spec.role, + relativePath: spec.relativePath.split(path.sep).join("/"), + containsSecrets: spec.containsSecrets === true, + }, + }) + } + + return resources +} + +async function collectCodexAutomations(params: { + codexRoot: string + projectPath?: string +}): Promise { + const automationsRoot = path.join(params.codexRoot, "automations") + if (!(await pathExists(automationsRoot))) return [] + + let entries: Dirent[] + try { + entries = await fs.readdir(automationsRoot, { withFileTypes: true }) + } catch { + return [] + } + + const resources: SharedResource[] = [] + for (const entry of entries) { + if (!entry.isDirectory() || !isSafePathSegment(entry.name)) continue + const filePath = path.join(automationsRoot, entry.name, "automation.toml") + if (!(await pathExists(filePath))) continue + + const parsed = await readTomlRecord(filePath) + const id = stringValue(parsed?.id) || entry.name + const name = stringValue(parsed?.name) || id + const status = stringValue(parsed?.status) + + resources.push({ + id: resourceId(["codex", "automation", id]), + kind: "automation", + name, + scope: "engine", + engine: "codex", + path: toDisplayPath(filePath, params.projectPath), + description: stringValue(parsed?.prompt), + enabled: status ? status.toLowerCase() !== "paused" : true, + metadata: { + codexResourceRole: "automation", + automationId: id, + kind: stringValue(parsed?.kind), + status, + rrule: stringValue(parsed?.rrule), + model: stringValue(parsed?.model), + engine: stringValue(parsed?.engine), + reasoningEffort: stringValue(parsed?.reasoning_effort), + executionEnvironment: stringValue(parsed?.execution_environment), + lastRunAt: numberValue(parsed?.last_run_at), + createdAt: numberValue(parsed?.created_at), + updatedAt: numberValue(parsed?.updated_at), + hasTargetThread: Boolean(stringValue(parsed?.target_thread_id)), + hasCwds: Array.isArray(parsed?.cwds) && parsed.cwds.length > 0, + isEnabled: booleanValue(parsed?.is_enabled), + }, + }) + } + + return resources.sort((left, right) => left.id.localeCompare(right.id)) +} + +async function collectCodexConnectorResources(params: { + cacheRoot: string + projectPath?: string +}): Promise { + const files = await listJsonFiles(path.join(params.cacheRoot, "codex_app_directory")) + const byId = new Map() + + for (const filePath of files) { + const parsed = await readJsonRecord(filePath) + const connectors = Array.isArray(parsed?.connectors) ? parsed.connectors : [] + for (const connector of connectors) { + if (!isRecord(connector)) continue + const id = stringValue(connector.id) + const name = stringValue(connector.name) + if (!id || !name) continue + + const appMetadata = isRecord(connector.appMetadata) ? connector.appMetadata : {} + const review = isRecord(appMetadata.review) ? appMetadata.review : {} + const categories = stringArrayValue(appMetadata.categories) + byId.set(id, { + id: resourceId(["codex", "connector", id]), + kind: "connector", + name, + scope: "engine", + engine: "codex", + path: toDisplayPath(filePath, params.projectPath), + description: stringValue(connector.description), + enabled: booleanValue(connector.isEnabled) ?? true, + metadata: { + codexResourceRole: "connector", + connectorId: id, + distributionChannel: stringValue(connector.distributionChannel), + installUrl: stringValue(connector.installUrl), + isAccessible: booleanValue(connector.isAccessible), + isEnabled: booleanValue(connector.isEnabled), + labels: stringRecordValue(connector.labels), + categories, + reviewStatus: stringValue(review.status), + developer: stringValue(appMetadata.developer), + version: stringValue(appMetadata.version), + pluginDisplayNames: stringArrayValue(connector.pluginDisplayNames), + logoUrl: stringValue(connector.logoUrl), + logoUrlDark: stringValue(connector.logoUrlDark), + cacheFile: toDisplayPath(filePath, params.projectPath), + }, + }) + } + } + + return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id)) +} + +async function collectCodexAppsToolsResources(params: { + cacheRoot: string + projectPath?: string +}): Promise { + const resources = new Map() + + for (const filePath of await listJsonFiles(path.join(params.cacheRoot, "codex_apps_server_info"))) { + const parsed = await readJsonRecord(filePath) + const serverInfo = isRecord(parsed?.server_info) ? parsed.server_info : null + const name = stringValue(serverInfo?.name) + if (!serverInfo || !name) continue + resources.set(`server:${name}:${path.basename(filePath)}`, { + id: resourceId(["codex", "mcp", "apps-server", name, path.basename(filePath, ".json")]), + kind: "mcp", + name, + scope: "engine", + engine: "codex", + path: toDisplayPath(filePath, params.projectPath), + description: stringValue(serverInfo.description), + enabled: true, + metadata: { + codexResourceRole: "codex-apps-server", + title: stringValue(serverInfo.title), + version: stringValue(serverInfo.version), + websiteUrl: stringValue(serverInfo.websiteUrl), + cacheFile: toDisplayPath(filePath, params.projectPath), + }, + }) + } + + for (const filePath of await listJsonFiles(path.join(params.cacheRoot, "codex_apps_tools"))) { + const parsed = await readJsonRecord(filePath) + const tools = Array.isArray(parsed?.tools) ? parsed.tools : [] + for (const item of tools) { + if (!isRecord(item)) continue + const tool = isRecord(item.tool) ? item.tool : {} + const meta = isRecord(tool._meta) ? tool._meta : {} + const codexAppsMeta = isRecord(meta._codex_apps) ? meta._codex_apps : {} + const namespace = stringValue(item.tool_namespace) + const toolName = stringValue(item.tool_name) || stringValue(tool.name) + if (!namespace || !toolName) continue + + const connectorId = + stringValue(item.connector_id) || + stringValue(meta.connector_id) + const id = resourceId(["codex", "tool", namespace, toolName, connectorId]) + resources.set(id, { + id, + kind: "tool", + name: stringValue(tool.title) || toolName, + scope: "engine", + engine: "codex", + path: toDisplayPath(filePath, params.projectPath), + description: stringValue(tool.description), + enabled: true, + metadata: { + codexResourceRole: "codex-app-tool", + serverName: stringValue(item.server_name), + serverOrigin: stringValue(item.server_origin), + supportsParallelToolCalls: booleanValue(item.supports_parallel_tool_calls), + toolNamespace: namespace, + toolName, + namespaceDescription: stringValue(item.namespace_description), + connectorId, + connectorName: + stringValue(item.connector_name) || + stringValue(meta.connector_name), + connectorDescription: stringValue(meta.connector_description), + linkId: stringValue(meta.link_id), + resourceName: stringValue(meta.resource_name), + resourceUri: stringValue(codexAppsMeta.resource_uri), + containsMcpSource: booleanValue(codexAppsMeta.contains_mcp_source), + annotations: isRecord(tool.annotations) ? tool.annotations : undefined, + pluginDisplayNames: stringArrayValue(item.plugin_display_names), + cacheFile: toDisplayPath(filePath, params.projectPath), + }, + }) + } + } + + return [...resources.values()].sort((left, right) => left.id.localeCompare(right.id)) +} + +async function collectCodexRemoteAppResources(params: { + cacheRoot: string + projectPath?: string +}): Promise { + const resources = new Map() + + for (const filePath of await listJsonFiles(path.join(params.cacheRoot, "remote_plugin_catalog"))) { + const parsed = await readJsonRecord(filePath) + const plugins = Array.isArray(parsed?.plugins) ? parsed.plugins : [] + for (const plugin of plugins) { + if (!isRecord(plugin)) continue + const release = isRecord(plugin.release) ? plugin.release : {} + const appManifest = isRecord(release.app_manifest) ? release.app_manifest : {} + const apps = isRecord(appManifest.apps) ? appManifest.apps : {} + const pluginId = stringValue(plugin.id) + const pluginName = stringValue(release.display_name) || stringValue(plugin.name) + const interfaceMeta = isRecord(release.interface) ? release.interface : {} + + for (const [appName, appValue] of Object.entries(apps)) { + if (!isRecord(appValue)) continue + const appId = stringValue(appValue.id) + if (!appId) continue + const id = resourceId(["codex", "app", appId]) + resources.set(id, { + id, + kind: "app", + name: pluginName ? `${pluginName} / ${appName}` : appName, + scope: "engine", + engine: "codex", + path: toDisplayPath(filePath, params.projectPath), + description: + stringValue(interfaceMeta.short_description) || + stringValue(release.description), + enabled: stringValue(plugin.status) !== "UNAVAILABLE", + metadata: { + codexResourceRole: "remote-plugin-app", + appId, + appName, + pluginId, + pluginName: stringValue(plugin.name), + displayName: pluginName, + pluginStatus: stringValue(plugin.status), + installationPolicy: stringValue(plugin.installation_policy), + authenticationPolicy: stringValue(plugin.authentication_policy), + discoverability: stringValue(plugin.discoverability), + releaseVersion: stringValue(release.version), + category: stringValue(interfaceMeta.category), + developerName: stringValue(interfaceMeta.developer_name), + defaultPrompt: stringValue(interfaceMeta.default_prompt), + defaultPrompts: stringArrayValue(interfaceMeta.default_prompts), + websiteUrl: stringValue(interfaceMeta.website_url), + privacyPolicyUrl: stringValue(interfaceMeta.privacy_policy_url), + termsOfServiceUrl: stringValue(interfaceMeta.terms_of_service_url), + cacheFile: toDisplayPath(filePath, params.projectPath), + }, + }) + } + } + } + + return [...resources.values()].sort((left, right) => left.id.localeCompare(right.id)) +} + +async function collectCodexPluginAppResources(params: { + appManifestPath?: string + pluginSource: string + pluginName: string + pluginVersion?: string + projectPath?: string +}): Promise { + if (!params.appManifestPath || !(await pathExists(params.appManifestPath))) return [] + const parsed = await readJsonRecord(params.appManifestPath) + const apps = isRecord(parsed?.apps) ? parsed.apps : {} + const resources: SharedResource[] = [] + + for (const [name, value] of Object.entries(apps)) { + if (!isRecord(value)) continue + const appId = stringValue(value.id) + if (!appId) continue + resources.push({ + id: resourceId(["codex", "app", "plugin", params.pluginSource, name]), + kind: "app", + name, + scope: "plugin", + engine: "codex", + pluginSource: params.pluginSource, + path: toDisplayPath(params.appManifestPath, params.projectPath), + enabled: true, + metadata: { + codexResourceRole: "plugin-app", + appId, + appName: name, + pluginName: params.pluginName, + pluginVersion: params.pluginVersion, + required: booleanValue(value.required), + manifestPath: toDisplayPath(params.appManifestPath, params.projectPath), + }, + }) + } + + return resources +} + +function mcpServersFromRecord(value: unknown): Record { + if (!isRecord(value)) return {} + if (isRecord(value.mcpServers)) return value.mcpServers + if (isRecord(value.servers)) return value.servers + return value +} + +async function collectCodexPluginMcpResources(params: { + mcpServers: unknown + mcpManifestPath?: string + manifestPath: string + pluginSource: string + pluginName: string + pluginVersion?: string + projectPath?: string +}): Promise { + const manifestFile = + params.mcpManifestPath && await pathExists(params.mcpManifestPath) + ? params.mcpManifestPath + : params.manifestPath + const parsed = + manifestFile === params.mcpManifestPath + ? await readJsonRecord(manifestFile) + : isRecord(params.mcpServers) + ? { mcpServers: params.mcpServers } + : null + const servers = mcpServersFromRecord(parsed) + const resources: SharedResource[] = [] + + for (const [serverName, value] of Object.entries(servers)) { + if (!isRecord(value)) continue + const config = value as CodexPluginMcpServerConfig + const command = stringValue(config.command) + const url = stringValue(config.url) + const transport = command ? "stdio" : url ? "http" : "unknown" + + resources.push({ + id: resourceId(["codex", "mcp", "plugin", params.pluginSource, serverName]), + kind: "mcp", + name: serverName, + scope: "plugin", + engine: "codex", + pluginSource: params.pluginSource, + path: toDisplayPath(manifestFile, params.projectPath), + description: stringValue(config.description), + enabled: booleanValue(config.disabled) === true ? false : true, + metadata: { + codexResourceRole: "plugin-mcp-server", + serverName, + pluginName: params.pluginName, + pluginVersion: params.pluginVersion, + transport, + command, + args: stringArrayValue(config.args), + cwd: stringValue(config.cwd), + url, + authType: stringValue(config.authType), + hasOAuth: Boolean(config._oauth), + hasEnv: isRecord(config.env) && Object.keys(config.env).length > 0, + envKeys: stringRecordKeys(config.env), + approved: booleanValue(config.approved), + manifestPath: toDisplayPath(manifestFile, params.projectPath), + }, + }) + } + + return resources +} + +function pluginCacheParts(pluginCacheRoot: string, pluginPath: string): { + marketplace?: string + slug?: string + cacheVersion?: string +} { + const relative = path.relative(pluginCacheRoot, pluginPath) + if (relative.startsWith("..")) return {} + const [marketplace, slug, cacheVersion] = relative.split(path.sep) + return { marketplace, slug, cacheVersion } +} + +export async function collectCodexNativeResources( + params: CollectCodexNativeResourcesParams = {}, +): Promise { + const codexRoot = params.codexRoot ?? path.join(home, ".codex") + const pluginCacheRoot = + params.pluginCacheRoot ?? path.join(codexRoot, "plugins", "cache") + const codexCacheRoot = params.codexCacheRoot ?? path.join(codexRoot, "cache") + const resources: SharedResource[] = [] + + resources.push( + ...(await collectCodexNativeFiles({ + codexRoot, + projectPath: params.projectPath, + })), + ...(await collectCodexAutomations({ + codexRoot, + projectPath: params.projectPath, + })), + ...(await collectCodexConnectorResources({ + cacheRoot: codexCacheRoot, + projectPath: params.projectPath, + })), + ...(await collectCodexAppsToolsResources({ + cacheRoot: codexCacheRoot, + projectPath: params.projectPath, + })), + ...(await collectCodexRemoteAppResources({ + cacheRoot: codexCacheRoot, + projectPath: params.projectPath, + })), + ...(await scanCodexSkillRoot({ + root: path.join(codexRoot, "skills"), + scope: "user", + projectPath: params.projectPath, + })), + ) + + const manifests = await findCodexPluginManifests(pluginCacheRoot) + for (const manifestPath of manifests) { + const pluginPath = path.dirname(path.dirname(manifestPath)) + const manifest = await readCodexPluginManifest(manifestPath) + if (!manifest) continue + + const interfaceMeta = isRecord(manifest.interface) + ? (manifest.interface as CodexPluginInterface) + : {} + const cacheParts = pluginCacheParts(pluginCacheRoot, pluginPath) + const manifestName = stringValue(manifest.name) || cacheParts.slug || path.basename(pluginPath) + const displayName = stringValue(interfaceMeta.displayName) || manifestName + const version = stringValue(manifest.version) || cacheParts.cacheVersion + const pluginSource = resourceId([ + "codex", + cacheParts.marketplace || "cache", + manifestName, + ]) + const mcpManifestPath = resolvePluginRelativePath(pluginPath, manifest.mcpServers) + const appManifestPath = resolvePluginRelativePath(pluginPath, manifest.apps) + const skillsRoot = + resolvePluginRelativePath(pluginPath, manifest.skills) ?? path.join(pluginPath, "skills") + + resources.push({ + id: resourceId(["plugin", pluginSource]), + kind: "plugin", + name: displayName, + scope: "plugin", + engine: "codex", + pluginSource, + path: toDisplayPath(pluginPath, params.projectPath), + description: + stringValue(interfaceMeta.shortDescription) || + stringValue(manifest.description) || + stringValue(interfaceMeta.longDescription), + enabled: true, + metadata: { + codexResourceRole: "plugin-manifest", + manifestName, + displayName, + version, + marketplace: cacheParts.marketplace, + cacheSlug: cacheParts.slug, + cacheVersion: cacheParts.cacheVersion, + category: stringValue(interfaceMeta.category), + developerName: stringValue(interfaceMeta.developerName), + homepage: stringValue(manifest.homepage), + repository: stringValue(manifest.repository), + keywords: stringArrayValue(manifest.keywords), + capabilities: stringArrayValue(interfaceMeta.capabilities), + manifestPath: toDisplayPath(manifestPath, params.projectPath), + mcpManifestPath: + mcpManifestPath && await pathExists(mcpManifestPath) + ? toDisplayPath(mcpManifestPath, params.projectPath) + : undefined, + appManifestPath: + appManifestPath && await pathExists(appManifestPath) + ? toDisplayPath(appManifestPath, params.projectPath) + : undefined, + skillsPath: + await pathExists(skillsRoot) + ? toDisplayPath(skillsRoot, params.projectPath) + : undefined, + }, + }) + + resources.push( + ...(await scanCodexSkillRoot({ + root: skillsRoot, + scope: "plugin", + projectPath: params.projectPath, + pluginSource, + pluginName: displayName, + pluginVersion: version, + })), + ...(await collectCodexPluginAppResources({ + appManifestPath, + pluginSource, + pluginName: displayName, + pluginVersion: version, + projectPath: params.projectPath, + })), + ...(await collectCodexPluginMcpResources({ + mcpServers: manifest.mcpServers, + mcpManifestPath, + manifestPath, + pluginSource, + pluginName: displayName, + pluginVersion: version, + projectPath: params.projectPath, + })), + ) + } + + return resources +} diff --git a/src/main/lib/shared-resources/governance.test.ts b/src/main/lib/shared-resources/governance.test.ts new file mode 100644 index 000000000..6d985a77c --- /dev/null +++ b/src/main/lib/shared-resources/governance.test.ts @@ -0,0 +1,356 @@ +import { describe, expect, test } from "bun:test" +import { buildGovernedResourceProjection } from "./governance" +import type { SharedResource } from "./types" + +function resource(overrides: Partial & Pick): SharedResource { + return { + enabled: true, + ...overrides, + } +} + +describe("buildGovernedResourceProjection", () => { + test("resolves same-name conflicts by scope precedence and projects only the winner", () => { + const snapshot = buildGovernedResourceProjection({ + projectPath: "/workspace/app", + resources: [ + resource({ + id: "agent:project:reviewer", + kind: "agent", + name: "Reviewer", + scope: "project", + path: ".claude/agents/reviewer.md", + }), + resource({ + id: "agent:user:reviewer", + kind: "agent", + name: "reviewer", + scope: "user", + path: "~/.claude/agents/reviewer.md", + }), + ], + }) + + expect(snapshot.conflicts).toHaveLength(1) + expect(snapshot.conflicts[0]).toMatchObject({ + key: "agent:shared:reviewer", + winnerResourceId: "agent:project:reviewer", + resolution: "winner-by-precedence", + }) + + const userAgent = snapshot.resources.find( + (item) => item.id === "agent:user:reviewer", + ) + expect(userAgent?.conflict?.winnerResourceId).toBe("agent:project:reviewer") + + const claudeProjection = snapshot.projections.find( + (projection) => projection.engineId === "claude-code", + ) + expect(claudeProjection?.mappings.map((mapping) => mapping.resourceId)).toEqual([ + "agent:project:reviewer", + ]) + + const codexProjection = snapshot.projections.find( + (projection) => projection.engineId === "codex", + ) + expect(codexProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "agent:project:reviewer", + action: "prompt-inject", + }), + ) + expect(codexProjection?.warnings).toContain( + "reviewer is shadowed by a higher precedence resource.", + ) + }) + + test("tracks plugin MCP approval state and keeps unapproved resources out of projections", () => { + const snapshot = buildGovernedResourceProjection({ + resources: [ + resource({ + id: "mcp:claude-code:plugin:demo:browser", + kind: "mcp", + name: "browser", + scope: "plugin", + engine: "claude-code", + pluginSource: "demo", + path: "plugins/demo/mcp/browser.json", + metadata: { approved: false }, + enabled: false, + }), + ], + }) + + const pluginMcp = snapshot.resources[0] + expect(pluginMcp?.approval).toMatchObject({ + required: true, + approved: false, + }) + expect(pluginMcp?.provenance).toMatchObject({ + source: "plugin", + sourceId: "demo", + discoveredBy: "plugin MCP manifest", + }) + + for (const projection of snapshot.projections) { + expect(projection.mappings).toHaveLength(0) + } + }) + + test("projects Codex-native MCP resources natively for Codex", () => { + const snapshot = buildGovernedResourceProjection({ + resources: [ + resource({ + id: "mcp:codex:global:node_repl", + kind: "mcp", + name: "node_repl", + scope: "engine", + engine: "codex", + path: "~/.codex/config.toml", + }), + ], + }) + + const codexProjection = snapshot.projections.find( + (projection) => projection.engineId === "codex", + ) + expect(codexProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "mcp:codex:global:node_repl", + action: "native", + targetPath: "~/.codex/config.toml", + }), + ) + }) + + test("treats Moss Unified Source as canonical over legacy project files", () => { + const snapshot = buildGovernedResourceProjection({ + projectPath: "/workspace/app", + resources: [ + resource({ + id: "moss:instruction:moss.md", + kind: "instruction", + name: "AGENTS.md", + scope: "moss", + path: ".moss/source/moss.md", + metadata: { + mossRole: "source-instruction", + }, + }), + resource({ + id: "instruction:project:AGENTS.md", + kind: "instruction", + name: "AGENTS.md", + scope: "project", + path: "AGENTS.md", + }), + ], + }) + + expect(snapshot.conflicts[0]).toMatchObject({ + key: "instruction:shared:agents.md", + winnerResourceId: "moss:instruction:moss.md", + resolution: "winner-by-precedence", + }) + + const mossInstruction = snapshot.resources.find( + (item) => item.id === "moss:instruction:moss.md", + ) + expect(mossInstruction?.provenance).toMatchObject({ + source: "moss", + discoveredBy: "Moss Unified Source", + precedenceLabel: "Moss Unified Source is canonical", + }) + }) + + test("projects Moss resources to Claude, Codex, Hermes, and Custom ACP without duplicating source data", () => { + const snapshot = buildGovernedResourceProjection({ + projectPath: "/workspace/app", + resources: [ + resource({ + id: "moss:instruction:moss.md", + kind: "instruction", + name: "moss.md", + scope: "moss", + path: ".moss/source/moss.md", + metadata: { + mossRole: "source-instruction", + }, + }), + resource({ + id: "moss:skill:review", + kind: "skill", + name: "review", + scope: "moss", + path: ".moss/skills/review/SKILL.md", + metadata: { + mossRole: "skill", + entryName: "review", + }, + }), + resource({ + id: "moss:mcp:browser", + kind: "mcp", + name: "browser", + scope: "moss", + path: ".moss/mcp/config.json", + metadata: { + mossRole: "mcp-config", + }, + }), + resource({ + id: "moss:provider:providers.yaml", + kind: "provider", + name: "providers.yaml", + scope: "moss", + path: ".moss/providers.yaml", + metadata: { + mossRole: "provider-config", + }, + }), + resource({ + id: "moss:hook:on-stop", + kind: "hook", + name: "on-stop", + scope: "moss", + path: ".moss/hooks/on-stop.md", + metadata: { + mossRole: "hook", + entryName: "on-stop.md", + }, + }), + resource({ + id: "moss:plugin:moss-starter", + kind: "plugin", + name: "moss-starter", + scope: "moss", + path: ".moss/plugins/moss-starter.md", + metadata: { + mossRole: "plugin", + entryName: "moss-starter.md", + }, + }), + resource({ + id: "moss:subagent:reviewer", + kind: "subagent", + name: "reviewer", + scope: "moss", + path: ".moss/subagents/reviewer.md", + metadata: { + mossRole: "subagent", + entryName: "reviewer.md", + }, + }), + ], + }) + + const claudeProjection = snapshot.projections.find( + (projection) => projection.engineId === "claude-code", + ) + expect(claudeProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:instruction:moss.md", + action: "symlink", + targetPath: "CLAUDE.md", + }), + ) + expect(claudeProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:mcp:browser", + action: "managed-bridge", + targetPath: ".mcp.json", + }), + ) + expect(claudeProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:hook:on-stop", + action: "adapter-inject", + sourcePath: ".moss/hooks/on-stop.md", + targetPath: ".claude/hooks", + }), + ) + + const codexProjection = snapshot.projections.find( + (projection) => projection.engineId === "codex", + ) + expect(codexProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:instruction:moss.md", + action: "symlink", + targetPath: "AGENTS.md", + }), + ) + expect(codexProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:provider:providers.yaml", + action: "managed-bridge", + targetPath: ".codex/config.toml", + }), + ) + expect(codexProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:hook:on-stop", + action: "adapter-inject", + sourcePath: ".moss/hooks/on-stop.md", + targetPath: ".codex/hooks", + }), + ) + expect(codexProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:plugin:moss-starter", + action: "adapter-inject", + sourcePath: ".moss/plugins/moss-starter.md", + targetPath: ".codex/plugins", + }), + ) + expect(codexProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:subagent:reviewer", + action: "symlink", + sourcePath: ".moss/subagents/reviewer.md", + targetPath: ".codex/agents/reviewer.md", + }), + ) + + const hermesProjection = snapshot.projections.find( + (projection) => projection.engineId === "hermes", + ) + expect(hermesProjection?.status).toBe("ready") + expect(hermesProjection?.warnings).toEqual([]) + expect(hermesProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:skill:review", + action: "native", + sourcePath: ".moss/skills/review", + targetPath: ".moss/skills/review", + }), + ) + expect(hermesProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:hook:on-stop", + action: "native", + sourcePath: ".moss/hooks/on-stop.md", + targetPath: ".moss/hooks/on-stop.md", + }), + ) + + const customAcpProjection = snapshot.projections.find( + (projection) => projection.engineId === "custom-acp", + ) + expect(customAcpProjection?.status).toBe("unsupported") + expect(customAcpProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:instruction:moss.md", + action: "prompt-inject", + sourcePath: ".moss/source/moss.md", + }), + ) + expect(customAcpProjection?.mappings).toContainEqual( + expect.objectContaining({ + resourceId: "moss:provider:providers.yaml", + action: "prompt-inject", + sourcePath: ".moss/providers.yaml", + }), + ) + }) +}) diff --git a/src/main/lib/shared-resources/governance.ts b/src/main/lib/shared-resources/governance.ts index 7d7d14a94..5b12316d1 100644 --- a/src/main/lib/shared-resources/governance.ts +++ b/src/main/lib/shared-resources/governance.ts @@ -8,6 +8,8 @@ import type { SharedResourceApproval, SharedResourceConflict, SharedResourceKind, + SharedResourceRuntimeGate, + SharedResourceRuntimeGateIssue, SharedResourceSnapshot, } from "./types" @@ -21,6 +23,11 @@ const GOVERNED_RESOURCE_KINDS = new Set([ "instruction", "hook", "provider", + "config", + "automation", + "connector", + "app", + "tool", ]) const SCOPE_PRECEDENCE: Record = { @@ -38,7 +45,7 @@ function normalizeResourceName(name: string): string { function buildConflictKey(resource: SharedResource): string | undefined { if (!GOVERNED_RESOURCE_KINDS.has(resource.kind)) return undefined - const engineKey = resource.kind === "mcp" || resource.kind === "memory" + const engineKey = ["mcp", "memory", "config", "automation", "connector", "app", "tool"].includes(resource.kind) ? resource.engine ?? "shared" : "shared" return `${resource.kind}:${engineKey}:${normalizeResourceName(resource.name)}` @@ -63,6 +70,50 @@ function getPrecedenceLabel(resource: SharedResource): string { function getDiscoverySource(resource: SharedResource): string { if (resource.scope === "moss") return "Moss Unified Source" + const codexResourceRole = resource.metadata?.codexResourceRole + if (resource.engine === "codex" && codexResourceRole === "plugin-manifest") { + return "Codex plugin manifest" + } + if (resource.engine === "codex" && codexResourceRole === "plugin-skill") { + return "Codex plugin cache skill" + } + if (resource.engine === "codex" && codexResourceRole === "user-skill") { + return "Codex skill directory" + } + if (resource.engine === "codex" && codexResourceRole === "config") { + return "Codex config.toml" + } + if (resource.engine === "codex" && codexResourceRole === "browser-config") { + return "Codex browser config.toml" + } + if (resource.engine === "codex" && codexResourceRole === "auth") { + return "Codex auth state" + } + if (resource.engine === "codex" && codexResourceRole === "hooks") { + return "Codex hooks.json" + } + if (resource.engine === "codex" && codexResourceRole === "automation") { + return "Codex automation.toml" + } + if (resource.engine === "codex" && codexResourceRole === "connector") { + return "Codex connector directory cache" + } + if (resource.engine === "codex" && codexResourceRole === "codex-apps-server") { + return "Codex apps MCP server cache" + } + if (resource.engine === "codex" && codexResourceRole === "codex-app-tool") { + return "Codex apps tool cache" + } + if (resource.engine === "codex" && codexResourceRole === "remote-plugin-app") { + return "Codex remote plugin app catalog" + } + if (resource.engine === "codex" && codexResourceRole === "plugin-app") { + return "Codex plugin app manifest" + } + if (resource.engine === "codex" && codexResourceRole === "plugin-mcp-server") { + return "Codex plugin MCP manifest" + } + if (resource.kind === "mcp") { if (resource.scope === "plugin") return "plugin MCP manifest" if (resource.engine === "codex") return "Codex MCP config" @@ -459,7 +510,17 @@ function buildEngineProjection( } if (engineId === "codex") { - if (resource.kind === "mcp" && resource.engine === "codex") { + if ( + resource.engine === "codex" && + ["skill", "plugin", "config", "provider", "hook", "automation", "connector", "app", "tool"].includes(resource.kind) + ) { + mappings.push({ + resourceId: resource.id, + action: "native", + sourcePath: resource.path, + targetPath: resource.path, + }) + } else if (resource.kind === "mcp" && resource.engine === "codex") { mappings.push({ resourceId: resource.id, action: "native", @@ -518,3 +579,144 @@ export function buildGovernedResourceProjection(params: { projections, } } + +function runtimeGateIssue( + issue: Omit, +): SharedResourceRuntimeGateIssue { + const anchor = + issue.resourceId ?? issue.conflictKey ?? issue.kind + return { + id: `${issue.kind}:${anchor}`, + ...issue, + } +} + +function resourceIsConflictLoser(resource: SharedResource): boolean { + return Boolean( + resource.conflict && + resource.conflict.resolution === "winner-by-precedence" && + resource.conflict.winnerResourceId !== resource.id, + ) +} + +function resourceRequiresPendingApproval(resource: SharedResource): boolean { + return Boolean(resource.approval?.required && !resource.approval.approved) +} + +function mappingForResource( + projection: EngineResourceProjection | undefined, + resource: SharedResource, +): ResourcePathMapping | undefined { + return projection?.mappings.find((mapping) => mapping.resourceId === resource.id) +} + +export function buildSharedResourceRuntimeGate(params: { + engineId: AgentEngineId + snapshot: Pick +}): SharedResourceRuntimeGate { + const projection = params.snapshot.projections.find( + (item) => item.engineId === params.engineId, + ) + const blockers: SharedResourceRuntimeGateIssue[] = [] + const warnings: SharedResourceRuntimeGateIssue[] = [] + + if (!projection) { + blockers.push(runtimeGateIssue({ + kind: "missing-projection", + severity: "blocker", + message: `${params.engineId} has no shared-resource projection.`, + })) + } else if (projection.status === "unsupported") { + blockers.push(runtimeGateIssue({ + kind: "unsupported-projection", + severity: "blocker", + message: `${params.engineId} cannot launch with shared-resource projection status unsupported.`, + })) + } else if (projection.status === "partial") { + warnings.push(runtimeGateIssue({ + kind: "partial-projection", + severity: "warning", + message: `${params.engineId} starts with explicit projection warnings: ${projection.warnings.join(" ")}`, + })) + } + + for (const conflict of params.snapshot.conflicts) { + if (conflict.resolution !== "manual-review") continue + blockers.push(runtimeGateIssue({ + kind: "manual-review-conflict", + severity: "blocker", + message: `${conflict.name} requires manual resource conflict review before launch.`, + conflictKey: conflict.key, + })) + } + + let pendingApprovalCount = 0 + let shadowedResourceCount = 0 + let unsafeProjectedResourceCount = 0 + + for (const resource of params.snapshot.resources) { + const mapping = mappingForResource(projection, resource) + const pendingApproval = resourceRequiresPendingApproval(resource) + const shadowed = resourceIsConflictLoser(resource) + + if (pendingApproval) { + pendingApprovalCount += 1 + if (mapping) { + unsafeProjectedResourceCount += 1 + blockers.push(runtimeGateIssue({ + kind: "unsafe-projected-resource", + severity: "blocker", + message: `${resource.name} is pending approval but is still projected to ${params.engineId}.`, + resourceId: resource.id, + })) + } else { + warnings.push(runtimeGateIssue({ + kind: "withheld-approval", + severity: "warning", + message: `${resource.name} is withheld until approval is granted.`, + resourceId: resource.id, + })) + } + } + + if (shadowed) { + shadowedResourceCount += 1 + if (mapping) { + unsafeProjectedResourceCount += 1 + blockers.push(runtimeGateIssue({ + kind: "unsafe-projected-resource", + severity: "blocker", + message: `${resource.name} is shadowed by a higher precedence resource but is still projected to ${params.engineId}.`, + resourceId: resource.id, + })) + } else { + warnings.push(runtimeGateIssue({ + kind: "shadowed-resource", + severity: "warning", + message: `${resource.name} is shadowed and withheld from ${params.engineId}.`, + resourceId: resource.id, + })) + } + } + } + + return { + version: 1, + engineId: params.engineId, + status: blockers.length === 0 ? "passed" : "blocked", + resourceCount: params.snapshot.resources.length, + projectionStatus: projection?.status, + mappingCount: projection?.mappings.length ?? 0, + blockers, + warnings, + summary: { + conflictCount: params.snapshot.conflicts.length, + manualReviewConflictCount: params.snapshot.conflicts.filter( + (conflict) => conflict.resolution === "manual-review", + ).length, + pendingApprovalCount, + shadowedResourceCount, + unsafeProjectedResourceCount, + }, + } +} diff --git a/src/main/lib/shared-resources/index.ts b/src/main/lib/shared-resources/index.ts new file mode 100644 index 000000000..0dd11e1ea --- /dev/null +++ b/src/main/lib/shared-resources/index.ts @@ -0,0 +1,3 @@ +export * from "./types" +export * from "./registry" +export * from "./governance" diff --git a/src/main/lib/shared-resources/registry.test.ts b/src/main/lib/shared-resources/registry.test.ts new file mode 100644 index 000000000..215d33878 --- /dev/null +++ b/src/main/lib/shared-resources/registry.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, test } from "bun:test" +import * as fs from "fs" +import * as os from "os" +import * as path from "path" +import { buildSharedResourceSnapshot } from "./registry" + +function makeTempRoot(): string { + return fs.mkdtempSync(path.join(os.tmpdir(), "onecode-shared-registry-")) +} + +function writeFile(filePath: string, contents: string) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + fs.writeFileSync(filePath, contents, "utf-8") +} + +describe("buildSharedResourceSnapshot", () => { + test("loads in non-Electron automation scripts and includes Codex-native resources", async () => { + const root = makeTempRoot() + try { + const codexRoot = path.join(root, ".codex") + const pluginRoot = path.join( + codexRoot, + "plugins", + "cache", + "openai-bundled", + "record-and-replay", + "1.0.829", + ) + + writeFile( + path.join(codexRoot, "skills", "parity-audit", "SKILL.md"), + `---\nname: parity-audit\ndescription: Compare local UI against Codex Desktop.\n---\n\n# Parity Audit\n`, + ) + writeFile( + path.join(pluginRoot, ".codex-plugin", "plugin.json"), + JSON.stringify( + { + name: "record-and-replay", + version: "1.0.829", + description: "Record what I'm doing on my Mac", + skills: "./skills/", + interface: { + displayName: "Record & Replay", + shortDescription: "Record workflows", + }, + }, + null, + 2, + ), + ) + writeFile( + path.join(pluginRoot, "skills", "record-and-replay", "SKILL.md"), + `---\nname: record-and-replay\ndescription: Convert recordings into reusable skills.\n---\n\n# Record\n`, + ) + + const snapshot = await buildSharedResourceSnapshot({ + codexRoot, + codexPluginCacheRoot: path.join(codexRoot, "plugins", "cache"), + }) + + expect(snapshot.resources).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "skill:codex:user:parity-audit", + kind: "skill", + name: "parity-audit", + engine: "codex", + provenance: expect.objectContaining({ + discoveredBy: "Codex skill directory", + }), + }), + expect.objectContaining({ + id: "plugin:codex:openai-bundled:record-and-replay", + kind: "plugin", + name: "Record & Replay", + engine: "codex", + provenance: expect.objectContaining({ + discoveredBy: "Codex plugin manifest", + }), + }), + expect.objectContaining({ + id: "skill:codex:plugin:codex:openai-bundled:record-and-replay:record-and-replay", + kind: "skill", + name: "record-and-replay", + engine: "codex", + provenance: expect.objectContaining({ + discoveredBy: "Codex plugin cache skill", + }), + }), + ]), + ) + + const codexProjection = snapshot.projections.find( + (projection) => projection.engineId === "codex", + ) + expect(codexProjection?.mappings).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + resourceId: "skill:codex:user:parity-audit", + action: "native", + }), + expect.objectContaining({ + resourceId: "plugin:codex:openai-bundled:record-and-replay", + action: "native", + }), + ]), + ) + } finally { + fs.rmSync(root, { recursive: true, force: true }) + } + }) +}) diff --git a/src/main/lib/shared-resources/registry.ts b/src/main/lib/shared-resources/registry.ts new file mode 100644 index 000000000..792510966 --- /dev/null +++ b/src/main/lib/shared-resources/registry.ts @@ -0,0 +1,591 @@ +import * as fs from "fs/promises" +import * as os from "os" +import * as path from "path" +import matter from "gray-matter" +import type { AgentEngineId } from "../agent-runtime" +import { ensureMossSource } from "../moss-source/bootstrap" +import { discoverMossSourceResources } from "../moss-source/registry" +import type { McpServerConfig } from "../claude-config" +import { + discoverInstalledPlugins, + discoverPluginMcpServers, + getPluginComponentPaths, +} from "../plugins" +import { + getApprovedPluginMcpServers, + getEnabledPlugins, +} from "../claude-plugin-settings" +import { + type SharedResource, + type SharedResourceSnapshot, +} from "./types" +import { buildGovernedResourceProjection } from "./governance" +import { collectCodexNativeResources } from "./codex-native-resources" + +const home = os.homedir() + +function toDisplayPath(filePath: string, projectPath?: string): string { + if (projectPath && filePath.startsWith(projectPath)) { + return path.relative(projectPath, filePath) + } + return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath +} + +function resourceId(parts: Array): string { + return parts.filter(Boolean).join(":") +} + +async function pathExists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +function isSafeEntryName(name: string): boolean { + return !name.includes("..") && !name.includes("/") && !name.includes("\\") +} + +async function readFrontmatter(filePath: string): Promise<{ + name?: string + description?: string + body: string +}> { + const raw = await fs.readFile(filePath, "utf-8") + const parsed = matter(raw) + return { + name: typeof parsed.data.name === "string" ? parsed.data.name : undefined, + description: + typeof parsed.data.description === "string" + ? parsed.data.description + : undefined, + body: parsed.content.trim(), + } +} + +async function scanAgentFiles( + dir: string, + scope: "project" | "user" | "plugin", + projectPath?: string, + pluginSource?: string, +): Promise { + if (!(await pathExists(dir))) return [] + const entries = await fs.readdir(dir, { withFileTypes: true }) + const resources: SharedResource[] = [] + + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith(".md") || !isSafeEntryName(entry.name)) { + continue + } + + const filePath = path.join(dir, entry.name) + try { + const parsed = await readFrontmatter(filePath) + const name = parsed.name || entry.name.replace(/\.md$/, "") + resources.push({ + id: resourceId(["agent", scope, pluginSource, name]), + kind: "agent", + name, + scope, + pluginSource, + path: toDisplayPath(filePath, projectPath), + description: parsed.description, + enabled: scope !== "plugin" || Boolean(pluginSource), + }) + } catch { + continue + } + } + + return resources +} + +async function scanSkillDirs( + dir: string, + scope: "project" | "user" | "plugin", + projectPath?: string, + pluginSource?: string, +): Promise { + if (!(await pathExists(dir))) return [] + const entries = await fs.readdir(dir, { withFileTypes: true }) + const resources: SharedResource[] = [] + + for (const entry of entries) { + if (!entry.isDirectory() || !isSafeEntryName(entry.name)) continue + const filePath = path.join(dir, entry.name, "SKILL.md") + if (!(await pathExists(filePath))) continue + + try { + const parsed = await readFrontmatter(filePath) + const name = parsed.name || entry.name + resources.push({ + id: resourceId(["skill", scope, pluginSource, name]), + kind: "skill", + name, + scope, + pluginSource, + path: toDisplayPath(filePath, projectPath), + description: parsed.description, + enabled: scope !== "plugin" || Boolean(pluginSource), + }) + } catch { + continue + } + } + + return resources +} + +async function scanCommandFiles( + dir: string, + scope: "project" | "user" | "plugin", + projectPath?: string, + pluginSource?: string, + prefix = "", +): Promise { + if (!(await pathExists(dir))) return [] + const entries = await fs.readdir(dir, { withFileTypes: true }) + const resources: SharedResource[] = [] + + for (const entry of entries) { + if (!isSafeEntryName(entry.name)) continue + const filePath = path.join(dir, entry.name) + + if (entry.isDirectory()) { + const nextPrefix = prefix ? `${prefix}:${entry.name}` : entry.name + resources.push( + ...(await scanCommandFiles( + filePath, + scope, + projectPath, + pluginSource, + nextPrefix, + )), + ) + continue + } + + if (!entry.isFile() || !entry.name.endsWith(".md")) continue + try { + const parsed = await readFrontmatter(filePath) + const fallback = entry.name.replace(/\.md$/, "") + const name = parsed.name || (prefix ? `${prefix}:${fallback}` : fallback) + resources.push({ + id: resourceId(["command", scope, pluginSource, name]), + kind: "command", + name, + scope, + pluginSource, + path: toDisplayPath(filePath, projectPath), + description: parsed.description, + enabled: scope !== "plugin" || Boolean(pluginSource), + }) + } catch { + continue + } + } + + return resources +} + +function mcpResourcesFromRecord(params: { + servers: Record + scope: "project" | "user" | "plugin" | "engine" + engine?: AgentEngineId + group: string + pluginSource?: string +}): SharedResource[] { + return Object.entries(params.servers).map(([name, config]) => ({ + id: resourceId(["mcp", params.engine, params.scope, params.pluginSource, params.group, name]), + kind: "mcp" as const, + name, + scope: params.scope, + engine: params.engine, + pluginSource: params.pluginSource, + enabled: true, + metadata: { + group: params.group, + transport: config.command ? "stdio" : config.url ? "http" : "unknown", + hasOAuth: Boolean(config._oauth), + authType: config.authType, + }, + })) +} + +async function collectClaudeMcpResources(projectPath?: string): Promise { + try { + const { + getMergedGlobalMcpServers, + getMergedLocalProjectMcpServers, + readClaudeConfig, + readClaudeDirConfig, + readProjectMcpJson, + resolveProjectPathFromWorktree, + } = await import("../claude-config") + const [claudeConfig, claudeDirConfig] = await Promise.all([ + readClaudeConfig(), + readClaudeDirConfig(), + ]) + const globalServers = await getMergedGlobalMcpServers( + claudeConfig, + claudeDirConfig, + ) + const resources = mcpResourcesFromRecord({ + servers: globalServers, + scope: "user", + engine: "claude-code", + group: "global", + }) + + if (projectPath) { + const resolvedProjectPath = resolveProjectPathFromWorktree(projectPath) || projectPath + const projectServers = await getMergedLocalProjectMcpServers( + resolvedProjectPath, + claudeConfig, + claudeDirConfig, + ) + const projectMcpJsonServers = await readProjectMcpJson(resolvedProjectPath) + resources.push( + ...mcpResourcesFromRecord({ + servers: { ...projectMcpJsonServers, ...projectServers }, + scope: "project", + engine: "claude-code", + group: resolvedProjectPath, + }), + ) + } + + return resources + } catch (error) { + return [{ + id: "mcp:claude-code:error", + kind: "mcp", + name: "Claude MCP discovery failed", + scope: "engine", + engine: "claude-code", + enabled: false, + metadata: { + error: error instanceof Error ? error.message : String(error), + }, + }] + } +} + +async function collectCodexMcpResources(): Promise { + try { + const { getAllCodexMcpConfigHandler } = await import("../trpc/routers/codex") + const snapshot = await getAllCodexMcpConfigHandler() + return snapshot.groups.flatMap((group) => + group.mcpServers.map((server) => ({ + id: resourceId(["mcp", "codex", group.projectPath || "global", server.name]), + kind: "mcp" as const, + name: server.name, + scope: group.projectPath ? "project" : "engine", + engine: "codex" as const, + enabled: server.status === "connected", + metadata: { + group: group.groupName, + projectPath: group.projectPath, + status: server.status, + needsAuth: server.needsAuth, + toolCount: server.tools.length, + config: server.config, + }, + })), + ) + } catch (error) { + return [{ + id: "mcp:codex:error", + kind: "mcp", + name: "Codex MCP discovery failed", + scope: "engine", + engine: "codex", + enabled: false, + metadata: { + error: error instanceof Error ? error.message : String(error), + }, + }] + } +} + +async function collectMemoryResources(projectPath?: string): Promise { + const resources: SharedResource[] = [] + + const addMemoryResource = async (params: { + filePath: string | null + name: string + scope: "project" | "user" + engine?: AgentEngineId + description: string + memoryRole: string + }) => { + if (!params.filePath || !(await pathExists(params.filePath))) return + + let entryType: "file" | "directory" = "file" + try { + const stat = await fs.stat(params.filePath) + entryType = stat.isDirectory() ? "directory" : "file" + } catch { + return + } + + resources.push({ + id: resourceId(["memory", params.engine, params.scope, params.filePath]), + kind: "memory", + name: params.name, + scope: params.scope, + engine: params.engine, + path: toDisplayPath(params.filePath, projectPath), + description: params.description, + enabled: true, + metadata: { + entryType, + memoryRole: params.memoryRole, + }, + }) + } + + const codexMemoryRoot = path.join(home, ".codex", "memories") + await addMemoryResource({ + filePath: codexMemoryRoot, + name: "Codex memories", + scope: "user", + engine: "codex", + description: "Codex user memory root.", + memoryRole: "codex-user-root", + }) + await addMemoryResource({ + filePath: path.join(codexMemoryRoot, "MEMORY.md"), + name: "MEMORY.md", + scope: "user", + engine: "codex", + description: "Codex memory registry.", + memoryRole: "codex-registry", + }) + await addMemoryResource({ + filePath: path.join(codexMemoryRoot, "memory_summary.md"), + name: "memory_summary.md", + scope: "user", + engine: "codex", + description: "Codex compact memory summary.", + memoryRole: "codex-summary", + }) + await addMemoryResource({ + filePath: path.join(codexMemoryRoot, "raw_memories.md"), + name: "raw_memories.md", + scope: "user", + engine: "codex", + description: "Codex raw memory notes.", + memoryRole: "codex-raw", + }) + await addMemoryResource({ + filePath: path.join(codexMemoryRoot, "extensions"), + name: "memory extensions", + scope: "user", + engine: "codex", + description: "Codex memory extension queue.", + memoryRole: "codex-extensions", + }) + await addMemoryResource({ + filePath: path.join(codexMemoryRoot, "rollout_summaries"), + name: "rollout_summaries", + scope: "user", + engine: "codex", + description: "Codex memory evidence summaries.", + memoryRole: "codex-rollouts", + }) + + await addMemoryResource({ + filePath: path.join(home, ".claude", "CLAUDE.md"), + name: "CLAUDE.md", + scope: "user", + engine: "claude-code", + description: "Claude Code user memory file.", + memoryRole: "claude-user-memory", + }) + await addMemoryResource({ + filePath: path.join(home, ".claude", "memory"), + name: "Claude memory", + scope: "user", + engine: "claude-code", + description: "Claude Code user memory directory.", + memoryRole: "claude-user-memory-dir", + }) + await addMemoryResource({ + filePath: path.join(home, ".claude", "memories"), + name: "Claude memories", + scope: "user", + engine: "claude-code", + description: "Claude Code user memories directory.", + memoryRole: "claude-user-memories-dir", + }) + + if (projectPath) { + await addMemoryResource({ + filePath: path.join(projectPath, "CLAUDE.md"), + name: "CLAUDE.md", + scope: "project", + engine: "claude-code", + description: "Claude Code project memory and instruction file.", + memoryRole: "claude-project-memory", + }) + await addMemoryResource({ + filePath: path.join(projectPath, ".claude", "memory"), + name: "Claude project memory", + scope: "project", + engine: "claude-code", + description: "Claude Code project memory directory.", + memoryRole: "claude-project-memory-dir", + }) + await addMemoryResource({ + filePath: path.join(projectPath, ".claude", "memories"), + name: "Claude project memories", + scope: "project", + engine: "claude-code", + description: "Claude Code project memories directory.", + memoryRole: "claude-project-memories-dir", + }) + await addMemoryResource({ + filePath: path.join(projectPath, ".codex", "memories"), + name: "Codex project memories", + scope: "project", + engine: "codex", + description: "Codex project memory directory.", + memoryRole: "codex-project-memory-dir", + }) + await addMemoryResource({ + filePath: path.join(projectPath, ".1code", "memory"), + name: "1Code shared memory", + scope: "project", + description: "1Code shared memory projection root.", + memoryRole: "onecode-shared-memory", + }) + + const agentsPath = path.join(projectPath, "AGENTS.md") + if (await pathExists(agentsPath)) { + resources.push({ + id: "instruction:project:AGENTS.md", + kind: "instruction", + name: "AGENTS.md", + scope: "project", + path: "AGENTS.md", + enabled: true, + }) + } + } + + return resources +} + +export async function buildSharedResourceSnapshot(params: { + projectPath?: string + codexRoot?: string + codexPluginCacheRoot?: string + codexCacheRoot?: string +} = {}): Promise { + const projectPath = params.projectPath + if (projectPath) { + await ensureMossSource({ projectPath }) + } + + const enabledPluginSources = await getEnabledPlugins() + const [installedPlugins, pluginMcpConfigs] = await Promise.all([ + discoverInstalledPlugins(), + discoverPluginMcpServers(), + ]) + const approvedPluginMcpServers = new Set(await getApprovedPluginMcpServers()) + const enabledPlugins = installedPlugins.filter((plugin) => + enabledPluginSources.includes(plugin.source), + ) + + const userClaudeRoot = path.join(home, ".claude") + const projectClaudeRoot = projectPath ? path.join(projectPath, ".claude") : null + + const resources: SharedResource[] = [ + ...(projectPath ? await discoverMossSourceResources(projectPath) : []), + ...(await scanAgentFiles(path.join(userClaudeRoot, "agents"), "user")), + ...(await scanSkillDirs(path.join(userClaudeRoot, "skills"), "user")), + ...(await scanCommandFiles(path.join(userClaudeRoot, "commands"), "user")), + ...(projectClaudeRoot + ? [ + ...(await scanAgentFiles(path.join(projectClaudeRoot, "agents"), "project", projectPath)), + ...(await scanSkillDirs(path.join(projectClaudeRoot, "skills"), "project", projectPath)), + ...(await scanCommandFiles(path.join(projectClaudeRoot, "commands"), "project", projectPath)), + ] + : []), + ...(await collectClaudeMcpResources(projectPath)), + ...(await collectCodexNativeResources({ + projectPath, + codexRoot: params.codexRoot, + pluginCacheRoot: params.codexPluginCacheRoot, + codexCacheRoot: params.codexCacheRoot, + })), + ...(await collectCodexMcpResources()), + ...(await collectMemoryResources(projectPath)), + ] + + for (const plugin of installedPlugins) { + const enabled = enabledPluginSources.includes(plugin.source) + resources.push({ + id: resourceId(["plugin", plugin.source]), + kind: "plugin", + name: plugin.name, + scope: "plugin", + pluginSource: plugin.source, + path: toDisplayPath(plugin.path), + description: plugin.description, + enabled, + metadata: { + marketplace: plugin.marketplace, + version: plugin.version, + category: plugin.category, + homepage: plugin.homepage, + tags: plugin.tags, + }, + }) + } + + for (const plugin of enabledPlugins) { + const paths = getPluginComponentPaths(plugin) + resources.push( + ...(await scanAgentFiles(paths.agents, "plugin", undefined, plugin.source)), + ...(await scanSkillDirs(paths.skills, "plugin", undefined, plugin.source)), + ...(await scanCommandFiles(paths.commands, "plugin", undefined, plugin.source)), + ) + } + + for (const pluginConfig of pluginMcpConfigs) { + for (const [serverName, serverConfig] of Object.entries(pluginConfig.mcpServers)) { + const approved = approvedPluginMcpServers.has(`${pluginConfig.pluginSource}:${serverName}`) + resources.push(...mcpResourcesFromRecord({ + servers: { [serverName]: serverConfig }, + scope: "plugin", + engine: "claude-code", + group: pluginConfig.pluginSource, + pluginSource: pluginConfig.pluginSource, + }).map((resource) => ({ + ...resource, + enabled: enabledPluginSources.includes(pluginConfig.pluginSource) && approved, + metadata: { + ...resource.metadata, + approved, + }, + }))) + } + } + + const governed = buildGovernedResourceProjection({ + resources, + projectPath, + }) + + return { + generatedAt: new Date().toISOString(), + projectPath, + resources: governed.resources, + conflicts: governed.conflicts, + projections: governed.projections, + } +} diff --git a/src/main/lib/shared-resources/types.ts b/src/main/lib/shared-resources/types.ts index 6958bbedc..fb5372d2a 100644 --- a/src/main/lib/shared-resources/types.ts +++ b/src/main/lib/shared-resources/types.ts @@ -11,6 +11,11 @@ export type SharedResourceKind = | "instruction" | "hook" | "provider" + | "config" + | "automation" + | "connector" + | "app" + | "tool" export type SharedResourceScope = "moss" | "project" | "user" | "plugin" | "engine" @@ -89,3 +94,41 @@ export interface SharedResourceSnapshot { conflicts: SharedResourceConflict[] projections: EngineResourceProjection[] } + +export type SharedResourceRuntimeGateStatus = "passed" | "blocked" + +export type SharedResourceRuntimeGateIssueKind = + | "missing-projection" + | "unsupported-projection" + | "manual-review-conflict" + | "unsafe-projected-resource" + | "withheld-approval" + | "shadowed-resource" + | "partial-projection" + +export interface SharedResourceRuntimeGateIssue { + id: string + kind: SharedResourceRuntimeGateIssueKind + severity: "blocker" | "warning" + message: string + resourceId?: string + conflictKey?: string +} + +export interface SharedResourceRuntimeGate { + version: 1 + engineId: AgentEngineId + status: SharedResourceRuntimeGateStatus + resourceCount: number + projectionStatus?: EngineResourceProjection["status"] + mappingCount: number + blockers: SharedResourceRuntimeGateIssue[] + warnings: SharedResourceRuntimeGateIssue[] + summary: { + conflictCount: number + manualReviewConflictCount: number + pendingApprovalCount: number + shadowedResourceCount: number + unsafeProjectedResourceCount: number + } +} diff --git a/src/main/lib/trpc/routers/agent-runtime.ts b/src/main/lib/trpc/routers/agent-runtime.ts new file mode 100644 index 000000000..6f2cd4633 --- /dev/null +++ b/src/main/lib/trpc/routers/agent-runtime.ts @@ -0,0 +1,2640 @@ +import { eq } from "drizzle-orm"; +import * as fs from "fs/promises"; +import * as path from "path"; +import { z } from "zod"; +import { + AGENT_ENGINE_IDS, + DEFAULT_AGENT_ENGINE_ID, + buildMossForkSubChatRecord, + buildMossRollbackSubChatUpdate, + buildMossSessionActionPlan, + buildAgentRuntimeLaunchPlan, + buildHermesNativeSessionBridgePlan, + getMossSessionControlPlane, + getAgentRuntimeAdapter, + getAgentRuntimeManifest, + listAgentRuntimeManifests, + mergeMossSessionControlMetadata, + persistAgentRuntimeSession, + countLocalTranscriptUserTurns, + summarizeNativeThreadReadResult, + type AgentEngineId, + type AgentPermissionMode, + type AgentRuntimeExternalAgentConfigMigrationItem, + type AgentRuntimeSessionRef, + type AgentRuntimeMcpServerStatusDetail, + type AgentRuntimePluginMarketplaceKind, + type AgentRuntimeThreadControlAction, + type AgentRuntimeThreadGoalStatus, + type NativeThreadReadSummary, +} from "../../agent-runtime"; +import { getDatabase, chats, projects, subChats } from "../../db"; +import { createId } from "../../db/utils"; +import { applyRollbackStash } from "../../git/stash"; +import { + materializeMossEngineProjectionSafely, + materializeMossWorkspaceProjections, + readMossProviderConfig, + setMossProviderSecret, + getMossProviderSecret, + hasMossProviderSecret, + summarizeMossProviderReadResult, + writeMossProviderConfig, + type MossProviderConfig, + type MossProviderDefinition, +} from "../../moss-source"; +import { publicProcedure, router } from "../index"; + +const agentEngineSchema = z.enum(AGENT_ENGINE_IDS); +const permissionModeValues = [ + "plan", + "agent", + "bypass", + "read-only", + "ask-approval", + "full-access", + "custom", +] as const; +const permissionModeSchema = z.enum(permissionModeValues); +const providerModelSettingsSchema = z.object({ + hermes: z.string().optional(), + claudeCode: z.string().optional(), + codex: z.string().optional(), + customAcp: z.string().optional(), +}); +const runtimeModelSelectionSchema = z.object({ + instanceId: z.string().min(1), + modelId: z.string().min(1), + options: z.record(z.string(), z.unknown()).optional(), +}); +const nativeThreadSortDirectionSchema = z.enum(["asc", "desc"]); +const nativeThreadSortKeySchema = z.enum(["created_at", "updated_at"]); +const nativeThreadControlActionSchema = z.enum([ + "archive", + "unarchive", + "delete", +]); +const nativeMcpServerStatusDetailSchema = z.enum(["full", "toolsAndAuthOnly"]); +const nativeConfigWriteMergeStrategySchema = z.enum(["replace", "upsert"]); +const nativeConfigWriteValueSchema = z + .unknown() + .refine( + (value) => value !== undefined, + "Native config write value is required.", + ); +const nativeAccountLoginTypeSchema = z.enum([ + "apiKey", + "chatgpt", + "chatgptDeviceCode", + "chatgptAuthTokens", +]); +const nativePluginMarketplaceKindSchema = z.enum([ + "local", + "vertical", + "workspace-directory", + "shared-with-me", +]); +const nativeExternalAgentConfigMigrationItemTypeSchema = z.enum([ + "AGENTS_MD", + "CONFIG", + "SKILLS", + "PLUGINS", + "MCP_SERVER_CONFIG", + "SUBAGENTS", + "HOOKS", + "COMMANDS", + "SESSIONS", +]); +const nativeExternalAgentConfigMigrationDetailsSchema = z.object({ + commands: z.array(z.object({ name: z.string().min(1) })).optional(), + hooks: z.array(z.object({ name: z.string().min(1) })).optional(), + mcpServers: z.array(z.object({ name: z.string().min(1) })).optional(), + plugins: z + .array( + z.object({ + marketplaceName: z.string().min(1), + pluginNames: z.array(z.string().min(1)), + }), + ) + .optional(), + sessions: z + .array( + z.object({ + cwd: z.string().min(1), + path: z.string().min(1), + title: z.string().nullable().optional(), + }), + ) + .optional(), + subagents: z.array(z.object({ name: z.string().min(1) })).optional(), +}); +const nativeExternalAgentConfigMigrationItemSchema = z.object({ + cwd: z.string().nullable().optional(), + description: z.string().min(1), + details: nativeExternalAgentConfigMigrationDetailsSchema + .nullable() + .optional(), + itemType: nativeExternalAgentConfigMigrationItemTypeSchema, +}); +const nativeThreadGoalStatusSchema = z.enum([ + "active", + "paused", + "blocked", + "usageLimited", + "budgetLimited", + "complete", +]); +const nativeThreadGitInfoPatchSchema = z.object({ + branch: z.string().nullable().optional(), + originUrl: z.string().nullable().optional(), + sha: z.string().nullable().optional(), +}); +const nativeThreadTurnDiffInputSchema = z + .object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + threadId: z.string().min(1), + fromTurnCount: z.number().int().nonnegative(), + toTurnCount: z.number().int().nonnegative(), + ignoreWhitespace: z.boolean().optional(), + }) + .refine((value) => value.fromTurnCount <= value.toTurnCount, { + message: "fromTurnCount must be less than or equal to toTurnCount.", + path: ["toTurnCount"], + }); +const nativeThreadFullDiffInputSchema = z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + threadId: z.string().min(1), + toTurnCount: z.number().int().nonnegative(), + ignoreWhitespace: z.boolean().optional(), +}); + +function cleanString(value: string | undefined | null): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +async function buildStoredSecretSummary( + config: MossProviderConfig | undefined, +): Promise> { + const providerIds = Object.keys(config?.providers ?? {}); + const entries = await Promise.all( + providerIds.map( + async (providerId) => + [ + providerId, + { hasApiKey: await hasMossProviderSecret(providerId) }, + ] as const, + ), + ); + return Object.fromEntries(entries); +} + +function getOrCreateCustomProvider( + config: MossProviderConfig, +): MossProviderDefinition { + const existing = config.providers.custom; + return { + ...existing, + id: "custom", + label: existing?.label ?? "Custom OpenAI-Compatible", + mode: existing?.mode ?? "custom-url-key", + runtime: existing?.runtime ?? "any", + apiKeyEnv: existing?.apiKeyEnv ?? "MOSS_CUSTOM_API_KEY", + baseUrlEnv: existing?.baseUrlEnv ?? "MOSS_CUSTOM_BASE_URL", + engines: { + ...existing?.engines, + hermes: { + ...existing?.engines?.hermes, + model: existing?.engines?.hermes?.model ?? "moss-custom", + }, + "claude-code": { + ...existing?.engines?.["claude-code"], + model: existing?.engines?.["claude-code"]?.model ?? "opus", + }, + codex: { + ...existing?.engines?.codex, + model: existing?.engines?.codex?.model ?? "gpt-5.5/medium", + authMethod: existing?.engines?.codex?.authMethod ?? "openai-api-key", + }, + "custom-acp": { + ...existing?.engines?.["custom-acp"], + model: existing?.engines?.["custom-acp"]?.model ?? "custom-acp", + }, + }, + }; +} + +function parseRuntimeMetadata(value: string | null): Record { + if (!value) return {}; + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === "object" ? parsed : {}; + } catch { + return {}; + } +} + +function isAgentPermissionMode(value: unknown): value is AgentPermissionMode { + return ( + typeof value === "string" && + permissionModeValues.includes(value as AgentPermissionMode) + ); +} + +function parseRuntimeModelSelection( + value: unknown, +): z.infer | null { + const parsed = runtimeModelSelectionSchema.safeParse(value); + return parsed.success ? parsed.data : null; +} + +function resolveSubChatPermissionMode(subChat: { + mode: string | null; + runtimeMetadata: string | null; +}): AgentPermissionMode { + const metadata = parseRuntimeMetadata(subChat.runtimeMetadata); + return isAgentPermissionMode(metadata.permissionMode) + ? metadata.permissionMode + : (subChat.mode as AgentPermissionMode); +} + +function normalizeEngineId(value: string | null | undefined): AgentEngineId { + return AGENT_ENGINE_IDS.includes(value as AgentEngineId) + ? (value as AgentEngineId) + : DEFAULT_AGENT_ENGINE_ID; +} + +function getProjectPathForSubChat(subChatId: string): string { + const db = getDatabase(); + const subChat = db + .select() + .from(subChats) + .where(eq(subChats.id, subChatId)) + .get(); + if (!subChat) throw new Error("Sub-chat not found"); + + const chat = db + .select() + .from(chats) + .where(eq(chats.id, subChat.chatId)) + .get(); + if (!chat) throw new Error("Chat not found"); + + const project = db + .select() + .from(projects) + .where(eq(projects.id, chat.projectId)) + .get(); + if (!project) throw new Error("Project not found"); + + return chat.worktreePath || project.path; +} + +function getSubChatOrThrow(subChatId: string) { + const subChat = getDatabase() + .select() + .from(subChats) + .where(eq(subChats.id, subChatId)) + .get(); + if (!subChat) throw new Error("Sub-chat not found"); + return subChat; +} + +function buildForkName(params: { + sourceSubChatId: string; + chatId: string; + sourceName: string | null; + requestedName?: string; +}): string { + if (params.requestedName?.trim()) return params.requestedName.trim(); + + const baseName = (params.sourceName || "Chat").replace(/^\[\d+\]\s*/, ""); + const siblings = getDatabase() + .select({ name: subChats.name }) + .from(subChats) + .where(eq(subChats.chatId, params.chatId)) + .all(); + + let maxN = 0; + for (const sibling of siblings) { + const match = sibling.name?.match(/^\[(\d+)\]/); + if (match) maxN = Math.max(maxN, Number.parseInt(match[1], 10)); + } + + return `[${maxN + 1}] ${baseName}`; +} + +async function copyClaudeForkSessionFiles(params: { + sourceSubChatId: string; + targetSubChatId: string; +}): Promise { + try { + const { app } = await import("electron"); + const userDataPath = app.getPath("userData"); + const sourceDir = path.join( + userDataPath, + "claude-sessions", + params.sourceSubChatId, + "projects", + ); + const targetDir = path.join( + userDataPath, + "claude-sessions", + params.targetSubChatId, + "projects", + ); + const sourceDirExists = await fs + .stat(sourceDir) + .then(() => true) + .catch(() => false); + if (!sourceDirExists) return false; + + await fs.cp(sourceDir, targetDir, { recursive: true }); + return true; + } catch (error) { + console.warn( + "[agentRuntime.forkSession] Failed to copy Claude session files:", + error, + ); + return false; + } +} + +function buildActionPlanForSubChat(subChatId: string) { + const subChat = getSubChatOrThrow(subChatId); + const engine = normalizeEngineId(subChat.engine); + const manifest = getAgentRuntimeManifest(engine); + const nativeSessionId = + subChat.engineSessionId ?? + (engine === "claude-code" ? subChat.sessionId : null); + return { + subChat, + engine, + manifest, + plan: buildMossSessionActionPlan({ + subChatId, + engine, + nativeSessionId, + messages: subChat.messages, + features: manifest.features, + }), + }; +} + +function updateSessionControlMetadata(params: { + subChatId: string; + runtimeMetadata: string | null; + metadata: Record; +}) { + return getDatabase() + .update(subChats) + .set({ + runtimeMetadata: mergeMossSessionControlMetadata( + params.runtimeMetadata, + params.metadata, + ), + updatedAt: new Date(), + }) + .where(eq(subChats.id, params.subChatId)) + .returning() + .get(); +} + +function summarizeNativeThreadControlResult(result: { + status: string; + sourceThreadId?: string | null; + threadId?: string | null; + numTurns?: number; + message?: string; + updatedAt: string; + metadata?: Record; +}): Record { + return { + status: result.status, + ...(result.sourceThreadId !== undefined + ? { sourceThreadId: result.sourceThreadId ?? null } + : {}), + threadId: result.threadId ?? null, + ...(typeof result.numTurns === "number" + ? { numTurns: result.numTurns } + : {}), + ...(result.message ? { message: result.message } : {}), + updatedAt: result.updatedAt, + ...(result.metadata ? { metadata: result.metadata } : {}), + }; +} + +function buildRuntimeSessionRefForSubChat(params: { + subChat: { + id: string; + chatId: string; + engineConfigDir: string | null; + modelId: string | null; + mode: string | null; + runtimeMetadata: string | null; + }; + engine: AgentEngineId; + nativeSessionId: string | null; + projectPath: string; +}): AgentRuntimeSessionRef { + const metadata = parseRuntimeMetadata(params.subChat.runtimeMetadata); + const modelSelection = parseRuntimeModelSelection(metadata.modelSelection); + const providerInstanceId = + typeof metadata.providerInstanceId === "string" + ? (cleanString(metadata.providerInstanceId) ?? null) + : (modelSelection?.instanceId ?? null); + + return { + subChatId: params.subChat.id, + chatId: params.subChat.chatId, + engineId: params.engine, + providerInstanceId, + nativeSessionId: params.nativeSessionId, + modelId: params.subChat.modelId, + modelSelection, + permissionMode: resolveSubChatPermissionMode(params.subChat), + cwd: params.projectPath, + projectPath: params.projectPath, + runtimeConfigDir: params.subChat.engineConfigDir, + metadata, + }; +} + +function buildRuntimeSessionRefForProject(params: { + engine: AgentEngineId; + projectPath: string; + permissionMode?: AgentPermissionMode; +}): AgentRuntimeSessionRef { + return { + subChatId: "", + chatId: "", + engineId: params.engine, + nativeSessionId: null, + permissionMode: params.permissionMode ?? "agent", + cwd: params.projectPath, + projectPath: params.projectPath, + metadata: {}, + }; +} + +export const agentRuntimeRouter = router({ + listEngines: publicProcedure.query(async () => { + const manifests = listAgentRuntimeManifests(); + const healthEntries = await Promise.all( + manifests.map(async (manifest) => { + const adapter = getAgentRuntimeAdapter(manifest.id); + const session = { + subChatId: "", + chatId: "", + engineId: manifest.id, + permissionMode: "agent", + cwd: "", + } as const; + const health = adapter.inspect + ? await adapter.inspect(session) + : { + availability: await adapter.canStart(session), + }; + return [manifest.id, health] as const; + }), + ); + + const healthByEngine = Object.fromEntries(healthEntries); + + return manifests.map((manifest) => ({ + ...manifest, + availability: + healthByEngine[manifest.id]?.availability ?? manifest.availability, + statusReason: healthByEngine[manifest.id]?.statusReason, + authMethod: healthByEngine[manifest.id]?.authMethod, + models: healthByEngine[manifest.id]?.models ?? manifest.models, + providerInstances: + healthByEngine[manifest.id]?.providerInstances ?? + manifest.providerInstances, + version: healthByEngine[manifest.id]?.version ?? manifest.version, + versionAdvisory: + healthByEngine[manifest.id]?.versionAdvisory ?? + manifest.versionAdvisory, + updateState: + healthByEngine[manifest.id]?.updateState ?? manifest.updateState, + })); + }), + + getSession: publicProcedure + .input(z.object({ subChatId: z.string() })) + .query(({ input }) => { + const db = getDatabase(); + const subChat = db + .select() + .from(subChats) + .where(eq(subChats.id, input.subChatId)) + .get(); + + if (!subChat) { + return null; + } + + const metadata = parseRuntimeMetadata(subChat.runtimeMetadata); + const modelSelection = parseRuntimeModelSelection( + metadata.modelSelection, + ); + const providerInstanceId = + typeof metadata.providerInstanceId === "string" + ? (cleanString(metadata.providerInstanceId) ?? null) + : (modelSelection?.instanceId ?? null); + + return { + subChatId: subChat.id, + chatId: subChat.chatId, + engine: subChat.engine as AgentEngineId, + legacySessionId: subChat.sessionId, + nativeSessionId: subChat.engineSessionId, + configDir: subChat.engineConfigDir, + modelId: subChat.modelId, + providerInstanceId, + modelSelection, + permissionMode: isAgentPermissionMode(metadata.permissionMode) + ? metadata.permissionMode + : (subChat.mode as AgentPermissionMode), + metadata, + updatedAt: subChat.updatedAt, + }; + }), + + getProviderConfig: publicProcedure + .input( + z.object({ + projectPath: z.string().optional(), + }), + ) + .query(async ({ input }) => { + if (!input.projectPath) { + return { + status: "missing" as const, + sourcePath: "", + providers: [], + }; + } + + const readResult = await readMossProviderConfig(input.projectPath, { + createIfMissing: true, + }); + const storedSecrets = await buildStoredSecretSummary(readResult.config); + return summarizeMossProviderReadResult(readResult, storedSecrets); + }), + + getControlPlane: publicProcedure + .input( + z.object({ + projectPath: z.string().optional(), + }), + ) + .query(({ input }) => + getMossSessionControlPlane({ + projectPath: input.projectPath, + secretResolver: { getSecret: getMossProviderSecret }, + }), + ), + + getSessionActionPlan: publicProcedure + .input(z.object({ subChatId: z.string() })) + .query(({ input }) => buildActionPlanForSubChat(input.subChatId).plan), + + refreshNativeThread: publicProcedure + .input( + z.object({ + subChatId: z.string(), + includeTurns: z.boolean().optional(), + }), + ) + .mutation(async ({ input }) => { + const { subChat, engine } = buildActionPlanForSubChat(input.subChatId); + const nativeSessionId = + subChat.engineSessionId ?? + (engine === "claude-code" ? subChat.sessionId : null); + if (!nativeSessionId) { + throw new Error( + "A native session id is required to refresh the thread.", + ); + } + + const adapter = getAgentRuntimeAdapter(engine); + if (!adapter.readThread) { + throw new Error(`${engine} does not support native thread refresh.`); + } + + const includeTurns = input.includeTurns ?? true; + const projectPath = getProjectPathForSubChat(input.subChatId); + const nativeResult = await adapter.readThread({ + includeTurns, + session: buildRuntimeSessionRefForSubChat({ + subChat, + engine, + nativeSessionId, + projectPath, + }), + }); + const nativeThreadRead = summarizeNativeThreadReadResult(nativeResult, { + includeTurns, + localMessages: subChat.messages, + }); + const nativeBridgePlan = { + engine, + action: "read", + bridge: nativeResult.metadata?.bridge ?? "native-thread", + method: nativeResult.metadata?.method ?? "thread/read", + sessionId: nativeSessionId, + cwd: projectPath, + includeTurns, + canRunHeadless: true, + }; + const updated = updateSessionControlMetadata({ + subChatId: input.subChatId, + runtimeMetadata: subChat.runtimeMetadata, + metadata: { + action: "refresh-native-thread", + mode: "native", + nativeSessionLinked: true, + nativeThreadRead, + nativeBridgePlan, + }, + }); + + return { + success: nativeResult.status === "success", + action: "refresh-native-thread" as const, + status: nativeResult.status, + nativeThreadRead, + nativeBridgePlan, + subChat: updated, + }; + }), + + listNativeThreads: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + archived: z.boolean().nullable().optional(), + cursor: z.string().nullable().optional(), + cwd: z + .union([z.string(), z.array(z.string())]) + .nullable() + .optional(), + limit: z.number().int().nonnegative().nullable().optional(), + modelProviders: z.array(z.string()).nullable().optional(), + searchTerm: z.string().nullable().optional(), + sortDirection: nativeThreadSortDirectionSchema.nullable().optional(), + sortKey: nativeThreadSortKeySchema.nullable().optional(), + sourceKinds: z.array(z.string()).nullable().optional(), + useStateDbOnly: z.boolean().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.listThreads) { + throw new Error( + `${input.engine} does not support native thread listing.`, + ); + } + + const result = await adapter.listThreads({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + archived: input.archived, + cursor: input.cursor, + cwd: input.cwd === undefined ? input.projectPath : input.cwd, + limit: input.limit, + modelProviders: input.modelProviders, + searchTerm: input.searchTerm, + sortDirection: input.sortDirection, + sortKey: input.sortKey, + sourceKinds: input.sourceKinds, + useStateDbOnly: input.useStateDbOnly, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native thread listing failed.`, + ); + } + return result; + }), + + listLoadedNativeThreads: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cursor: z.string().nullable().optional(), + limit: z.number().int().nonnegative().nullable().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.listLoadedThreads) { + throw new Error( + `${input.engine} does not support loaded native thread listing.`, + ); + } + + const result = await adapter.listLoadedThreads({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cursor: input.cursor, + limit: input.limit, + }); + if (result.status !== "success") { + throw new Error( + result.message || + `${input.engine} loaded native thread listing failed.`, + ); + } + return result; + }), + + getNativeThreadTurnDiff: publicProcedure + .input(nativeThreadTurnDiffInputSchema) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.getThreadTurnDiff) { + throw new Error( + `${input.engine} does not support native thread turn diffs.`, + ); + } + + const result = await adapter.getThreadTurnDiff({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + threadId: input.threadId, + fromTurnCount: input.fromTurnCount, + toTurnCount: input.toTurnCount, + ignoreWhitespace: input.ignoreWhitespace, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native thread turn diff failed.`, + ); + } + return result; + }), + + getNativeThreadFullDiff: publicProcedure + .input(nativeThreadFullDiffInputSchema) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.getThreadFullDiff) { + throw new Error( + `${input.engine} does not support native thread full diffs.`, + ); + } + + const result = await adapter.getThreadFullDiff({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + threadId: input.threadId, + toTurnCount: input.toTurnCount, + ignoreWhitespace: input.ignoreWhitespace, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native thread full diff failed.`, + ); + } + return result; + }), + + readNativeConfig: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cwd: z.string().nullable().optional(), + includeLayers: z.boolean().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.readConfig) { + throw new Error( + `${input.engine} does not support native config reads.`, + ); + } + + const result = await adapter.readConfig({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cwd: input.cwd === undefined ? input.projectPath : input.cwd, + includeLayers: input.includeLayers, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native config read failed.`, + ); + } + return result; + }), + + readNativeConfigRequirements: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.readConfigRequirements) { + throw new Error( + `${input.engine} does not support native config requirements.`, + ); + } + + const result = await adapter.readConfigRequirements({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + }); + if (result.status !== "success") { + throw new Error( + result.message || + `${input.engine} native config requirements read failed.`, + ); + } + return result; + }), + + writeNativeConfigValue: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + keyPath: z.string().min(1), + value: nativeConfigWriteValueSchema, + mergeStrategy: nativeConfigWriteMergeStrategySchema.default("replace"), + filePath: z.string().nullable().optional(), + expectedVersion: z.string().nullable().optional(), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.writeConfigValue) { + throw new Error( + `${input.engine} does not support native config writes.`, + ); + } + + const result = await adapter.writeConfigValue({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + keyPath: input.keyPath, + value: input.value, + mergeStrategy: input.mergeStrategy, + filePath: input.filePath, + expectedVersion: input.expectedVersion, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native config write failed.`, + ); + } + return result; + }), + + batchWriteNativeConfig: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + edits: z + .array( + z.object({ + keyPath: z.string().min(1), + value: nativeConfigWriteValueSchema, + mergeStrategy: + nativeConfigWriteMergeStrategySchema.default("replace"), + }), + ) + .min(1), + filePath: z.string().nullable().optional(), + expectedVersion: z.string().nullable().optional(), + reloadUserConfig: z.boolean().optional(), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.batchWriteConfig) { + throw new Error( + `${input.engine} does not support native config batch writes.`, + ); + } + + const result = await adapter.batchWriteConfig({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + edits: input.edits, + filePath: input.filePath, + expectedVersion: input.expectedVersion, + reloadUserConfig: input.reloadUserConfig, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native config batch write failed.`, + ); + } + return result; + }), + + listNativePermissionProfiles: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cwd: z.string().nullable().optional(), + cursor: z.string().nullable().optional(), + limit: z.number().int().nonnegative().nullable().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.listPermissionProfiles) { + throw new Error( + `${input.engine} does not support native permission profiles.`, + ); + } + + const result = await adapter.listPermissionProfiles({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cwd: input.cwd === undefined ? input.projectPath : input.cwd, + cursor: input.cursor, + limit: input.limit, + }); + if (result.status !== "success") { + throw new Error( + result.message || + `${input.engine} native permission profile listing failed.`, + ); + } + return result; + }), + + listNativeMcpServerStatuses: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cursor: z.string().nullable().optional(), + detail: nativeMcpServerStatusDetailSchema.nullable().optional(), + limit: z.number().int().nonnegative().nullable().optional(), + threadId: z.string().nullable().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.listMcpServerStatuses) { + throw new Error( + `${input.engine} does not support native MCP status reads.`, + ); + } + + const result = await adapter.listMcpServerStatuses({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cursor: input.cursor, + detail: input.detail as + | AgentRuntimeMcpServerStatusDetail + | null + | undefined, + limit: input.limit, + threadId: input.threadId, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native MCP status read failed.`, + ); + } + return result; + }), + + reloadNativeMcpServerConfig: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.reloadMcpServerConfig) { + throw new Error( + `${input.engine} does not support native MCP config reload.`, + ); + } + + const result = await adapter.reloadMcpServerConfig({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native MCP config reload failed.`, + ); + } + return result; + }), + + listNativeSkills: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cwds: z.array(z.string().min(1)).nullable().optional(), + forceReload: z.boolean().nullable().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.listSkills) { + throw new Error( + `${input.engine} does not support native skill listing.`, + ); + } + + const result = await adapter.listSkills({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cwds: input.cwds, + forceReload: input.forceReload, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native skill listing failed.`, + ); + } + return result; + }), + + listNativeHooks: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cwds: z.array(z.string().min(1)).nullable().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.listHooks) { + throw new Error( + `${input.engine} does not support native hook listing.`, + ); + } + + const result = await adapter.listHooks({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cwds: input.cwds, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native hook listing failed.`, + ); + } + return result; + }), + + listNativeApps: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cursor: z.string().nullable().optional(), + forceRefetch: z.boolean().nullable().optional(), + limit: z.number().int().nonnegative().nullable().optional(), + threadId: z.string().nullable().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.listApps) { + throw new Error(`${input.engine} does not support native app listing.`); + } + + const result = await adapter.listApps({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cursor: input.cursor, + forceRefetch: input.forceRefetch, + limit: input.limit, + threadId: input.threadId, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native app listing failed.`, + ); + } + return result; + }), + + listNativePlugins: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cwds: z.array(z.string().min(1)).nullable().optional(), + marketplaceKinds: z + .array(nativePluginMarketplaceKindSchema) + .nullable() + .optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.listPlugins) { + throw new Error( + `${input.engine} does not support native plugin listing.`, + ); + } + + const result = await adapter.listPlugins({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cwds: input.cwds, + marketplaceKinds: input.marketplaceKinds as + | AgentRuntimePluginMarketplaceKind[] + | null + | undefined, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native plugin listing failed.`, + ); + } + return result; + }), + + listNativeInstalledPlugins: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cwds: z.array(z.string().min(1)).nullable().optional(), + installSuggestionPluginNames: z + .array(z.string().min(1)) + .nullable() + .optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.listInstalledPlugins) { + throw new Error( + `${input.engine} does not support native installed plugin listing.`, + ); + } + + const result = await adapter.listInstalledPlugins({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cwds: input.cwds, + installSuggestionPluginNames: input.installSuggestionPluginNames, + }); + if (result.status !== "success") { + throw new Error( + result.message || + `${input.engine} native installed plugin listing failed.`, + ); + } + return result; + }), + + readNativePlugin: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + pluginName: z.string().min(1), + marketplacePath: z.string().nullable().optional(), + remoteMarketplaceName: z.string().nullable().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.readPlugin) { + throw new Error(`${input.engine} does not support native plugin read.`); + } + + const result = await adapter.readPlugin({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + pluginName: input.pluginName, + marketplacePath: input.marketplacePath, + remoteMarketplaceName: input.remoteMarketplaceName, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native plugin read failed.`, + ); + } + return result; + }), + + installNativePlugin: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + pluginName: z.string().min(1), + marketplacePath: z.string().nullable().optional(), + remoteMarketplaceName: z.string().nullable().optional(), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.installPlugin) { + throw new Error( + `${input.engine} does not support native plugin install.`, + ); + } + + const result = await adapter.installPlugin({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + pluginName: input.pluginName, + marketplacePath: input.marketplacePath, + remoteMarketplaceName: input.remoteMarketplaceName, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native plugin install failed.`, + ); + } + return result; + }), + + detectNativeExternalAgentConfig: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cwds: z.array(z.string().min(1)).nullable().optional(), + includeHome: z.boolean().nullable().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.detectExternalAgentConfig) { + throw new Error( + `${input.engine} does not support native external config detection.`, + ); + } + + const result = await adapter.detectExternalAgentConfig({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cwds: input.cwds, + includeHome: input.includeHome, + }); + if (result.status !== "success") { + throw new Error( + result.message || + `${input.engine} native external config detection failed.`, + ); + } + return result; + }), + + importNativeExternalAgentConfig: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + migrationItems: z + .array(nativeExternalAgentConfigMigrationItemSchema) + .min(1), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.importExternalAgentConfig) { + throw new Error( + `${input.engine} does not support native external config import.`, + ); + } + + const result = await adapter.importExternalAgentConfig({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + migrationItems: + input.migrationItems as AgentRuntimeExternalAgentConfigMigrationItem[], + }); + if (result.status !== "success") { + throw new Error( + result.message || + `${input.engine} native external config import failed.`, + ); + } + return result; + }), + + startNativeMcpServerOauthLogin: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + name: z.string().min(1), + scopes: z.array(z.string().min(1)).nullable().optional(), + timeoutSecs: z.number().int().nonnegative().nullable().optional(), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.startMcpServerOauthLogin) { + throw new Error( + `${input.engine} does not support native MCP OAuth login.`, + ); + } + + const result = await adapter.startMcpServerOauthLogin({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + name: input.name, + scopes: input.scopes, + timeoutSecs: input.timeoutSecs, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native MCP OAuth login failed.`, + ); + } + return result; + }), + + listNativeModels: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + cursor: z.string().nullable().optional(), + includeHidden: z.boolean().nullable().optional(), + limit: z.number().int().nonnegative().nullable().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.listModels) { + throw new Error( + `${input.engine} does not support native model listing.`, + ); + } + + const result = await adapter.listModels({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + cursor: input.cursor, + includeHidden: input.includeHidden, + limit: input.limit, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native model listing failed.`, + ); + } + return result; + }), + + startNativeAccountLogin: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + type: nativeAccountLoginTypeSchema.default("chatgpt"), + apiKey: z.string().nullable().optional(), + codexStreamlinedLogin: z.boolean().optional(), + accessToken: z.string().nullable().optional(), + chatgptAccountId: z.string().nullable().optional(), + chatgptPlanType: z.string().nullable().optional(), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.startAccountLogin) { + throw new Error( + `${input.engine} does not support native account login.`, + ); + } + + const result = await adapter.startAccountLogin({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + type: input.type, + apiKey: input.apiKey, + codexStreamlinedLogin: input.codexStreamlinedLogin, + accessToken: input.accessToken, + chatgptAccountId: input.chatgptAccountId, + chatgptPlanType: input.chatgptPlanType, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native account login failed.`, + ); + } + return result; + }), + + cancelNativeAccountLogin: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + loginId: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.cancelAccountLogin) { + throw new Error( + `${input.engine} does not support native account login cancel.`, + ); + } + + const result = await adapter.cancelAccountLogin({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + loginId: input.loginId, + }); + if (result.status !== "success") { + throw new Error( + result.message || + `${input.engine} native account login cancel failed.`, + ); + } + return result; + }), + + logoutNativeAccount: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.logoutAccount) { + throw new Error( + `${input.engine} does not support native account logout.`, + ); + } + + const result = await adapter.logoutAccount({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native account logout failed.`, + ); + } + return result; + }), + + readNativeAccount: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + refreshToken: z.boolean().optional(), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.readAccount) { + throw new Error( + `${input.engine} does not support native account reads.`, + ); + } + + const result = await adapter.readAccount({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + refreshToken: input.refreshToken, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native account read failed.`, + ); + } + return result; + }), + + readNativeAccountRateLimits: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.readAccountRateLimits) { + throw new Error( + `${input.engine} does not support native account rate limits.`, + ); + } + + const result = await adapter.readAccountRateLimits({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + }); + if (result.status !== "success") { + throw new Error( + result.message || + `${input.engine} native account rate-limit read failed.`, + ); + } + return result; + }), + + readNativeAccountUsage: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.readAccountUsage) { + throw new Error( + `${input.engine} does not support native account usage.`, + ); + } + + const result = await adapter.readAccountUsage({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native account usage read failed.`, + ); + } + return result; + }), + + controlNativeThread: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + action: nativeThreadControlActionSchema, + threadId: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.controlThread) { + throw new Error( + `${input.engine} does not support native thread control.`, + ); + } + + const result = await adapter.controlThread({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + action: input.action as AgentRuntimeThreadControlAction, + threadId: input.threadId, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native thread control failed.`, + ); + } + return result; + }), + + setNativeThreadName: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + threadId: z.string().min(1), + name: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.setThreadName) { + throw new Error( + `${input.engine} does not support native thread name updates.`, + ); + } + + const result = await adapter.setThreadName({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + threadId: input.threadId, + name: input.name, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native thread name update failed.`, + ); + } + return result; + }), + + updateNativeThreadMetadata: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + threadId: z.string().min(1), + gitInfo: nativeThreadGitInfoPatchSchema.nullable().optional(), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.updateThreadMetadata) { + throw new Error( + `${input.engine} does not support native thread metadata updates.`, + ); + } + + const result = await adapter.updateThreadMetadata({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + threadId: input.threadId, + gitInfo: input.gitInfo, + }); + if (result.status !== "success") { + throw new Error( + result.message || + `${input.engine} native thread metadata update failed.`, + ); + } + return result; + }), + + getNativeThreadGoal: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + threadId: z.string().min(1), + }), + ) + .query(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.getThreadGoal) { + throw new Error( + `${input.engine} does not support native thread goals.`, + ); + } + + const result = await adapter.getThreadGoal({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + threadId: input.threadId, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native thread goal read failed.`, + ); + } + return result; + }), + + setNativeThreadGoal: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + threadId: z.string().min(1), + objective: z.string().nullable().optional(), + status: nativeThreadGoalStatusSchema.nullable().optional(), + tokenBudget: z.number().int().nonnegative().nullable().optional(), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.setThreadGoal) { + throw new Error( + `${input.engine} does not support native thread goal updates.`, + ); + } + + const result = await adapter.setThreadGoal({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + threadId: input.threadId, + objective: input.objective, + status: input.status as AgentRuntimeThreadGoalStatus | null | undefined, + tokenBudget: input.tokenBudget, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native thread goal update failed.`, + ); + } + return result; + }), + + clearNativeThreadGoal: publicProcedure + .input( + z.object({ + engine: agentEngineSchema.default("codex"), + projectPath: z.string().min(1), + threadId: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + const adapter = getAgentRuntimeAdapter(input.engine); + if (!adapter.clearThreadGoal) { + throw new Error( + `${input.engine} does not support native thread goal clearing.`, + ); + } + + const result = await adapter.clearThreadGoal({ + session: buildRuntimeSessionRefForProject({ + engine: input.engine, + projectPath: input.projectPath, + }), + threadId: input.threadId, + }); + if (result.status !== "success") { + throw new Error( + result.message || `${input.engine} native thread goal clear failed.`, + ); + } + return result; + }), + + prepareSessionResume: publicProcedure + .input(z.object({ subChatId: z.string() })) + .mutation(({ input }) => { + const { subChat, plan } = buildActionPlanForSubChat(input.subChatId); + const action = plan.actions.resume; + if (action.status !== "ready") { + throw new Error(action.reason || "Session is not ready to resume."); + } + const engine = normalizeEngineId(subChat.engine); + const nativeSessionId = + subChat.engineSessionId ?? + (engine === "claude-code" ? subChat.sessionId : null); + const permissionMode = resolveSubChatPermissionMode(subChat); + const nativeBridgePlan = + engine === "codex" && nativeSessionId + ? ({ + engine: "codex", + action: "resume", + sessionId: nativeSessionId, + bridge: "codex-app-server-thread", + mode: "headless-app-server", + cwd: getProjectPathForSubChat(input.subChatId), + modelId: subChat.modelId, + permissionMode, + canRunHeadless: true, + notes: ["Codex native resume uses app-server thread/resume."], + } as const) + : engine === "hermes" && nativeSessionId + ? buildHermesNativeSessionBridgePlan({ + action: "resume", + sessionId: nativeSessionId, + cwd: getProjectPathForSubChat(input.subChatId), + modelId: subChat.modelId, + permissionMode, + }) + : undefined; + const nativeBridgeRunner = + nativeBridgePlan?.bridge === "codex-app-server-thread" + ? { + kind: "codex-app-server-thread", + runner: "streamCodexAppServerRuntimeRun", + promptSource: "stdin", + canRunHeadless: nativeBridgePlan.canRunHeadless, + } + : nativeBridgePlan?.bridge === "hermes-cli-resume" + ? { + kind: "hermes-cli-resume", + runner: "runHermesCliResumeBridge", + promptSource: nativeBridgePlan.promptSource, + canRunHeadless: nativeBridgePlan.canRunHeadless, + } + : undefined; + + const updated = updateSessionControlMetadata({ + subChatId: input.subChatId, + runtimeMetadata: subChat.runtimeMetadata, + metadata: { + action: "resume", + mode: action.mode, + status: action.status, + nativeSessionLinked: action.mode === "native", + nativeSessionId, + nativeBridgePlan, + nativeBridgeRunner, + }, + }); + + return { + success: true as const, + action: "resume" as const, + mode: action.mode, + nativeBridgePlan, + nativeBridgeRunner, + subChat: updated, + }; + }), + + forkSession: publicProcedure + .input( + z.object({ + subChatId: z.string(), + messageId: z.string().optional(), + messageIndex: z.number().int().nonnegative().optional(), + name: z.string().optional(), + }), + ) + .mutation(async ({ input }) => { + const { subChat: sourceSubChat, engine } = buildActionPlanForSubChat( + input.subChatId, + ); + const sourceNativeSessionId = + sourceSubChat.engineSessionId ?? + (engine === "claude-code" ? sourceSubChat.sessionId : null); + const permissionMode = resolveSubChatPermissionMode(sourceSubChat); + const projectPath = getProjectPathForSubChat(input.subChatId); + let nativeBridgePlan: unknown = + engine === "codex" && sourceNativeSessionId + ? { + engine: "codex", + action: "fork", + sessionId: sourceNativeSessionId, + bridge: "codex-app-server-thread", + mode: "headless-app-server", + cwd: projectPath, + modelId: sourceSubChat.modelId, + permissionMode, + targetMessageId: input.messageId ?? null, + targetMessageIndex: input.messageIndex ?? null, + canRunHeadless: true, + notes: [ + "Codex native fork uses app-server thread/fork.", + "When the requested fork point is not the latest turn, the forked thread is rolled back after creation.", + ], + } + : engine === "hermes" && sourceNativeSessionId + ? buildHermesNativeSessionBridgePlan({ + action: "fork", + sessionId: sourceNativeSessionId, + cwd: projectPath, + modelId: sourceSubChat.modelId, + permissionMode, + targetMessageId: input.messageId, + }) + : undefined; + const forkName = buildForkName({ + sourceSubChatId: input.subChatId, + chatId: sourceSubChat.chatId, + sourceName: sourceSubChat.name, + requestedName: input.name, + }); + const targetSubChatId = createId(); + let forkRecord = buildMossForkSubChatRecord({ + sourceSubChat, + targetSubChatId, + targetName: forkName, + targetMessageId: input.messageId, + targetMessageIndex: input.messageIndex, + nativeBridgePlan, + }); + let snapshot = forkRecord.snapshot; + let nativeSessionLinked = forkRecord.nativeSessionLinked; + let nativeThreadRead: NativeThreadReadSummary | null = null; + let forkedCodexThreadId: string | null = null; + + if (nativeSessionLinked && engine === "codex" && sourceNativeSessionId) { + const adapter = getAgentRuntimeAdapter("codex"); + if (!adapter.forkThread) { + throw new Error("Codex adapter does not support native thread fork."); + } + + const runtimeSession = buildRuntimeSessionRefForSubChat({ + subChat: sourceSubChat, + engine: "codex", + nativeSessionId: sourceNativeSessionId, + projectPath, + }); + const sourceUserTurnCount = countLocalTranscriptUserTurns( + sourceSubChat.messages, + ); + const forkUserTurnCount = countLocalTranscriptUserTurns( + JSON.stringify(snapshot.messages), + ); + const nativeForkRollbackTurns = Math.max( + 0, + sourceUserTurnCount - forkUserTurnCount, + ); + + if (typeof nativeBridgePlan === "object" && nativeBridgePlan !== null) { + nativeBridgePlan = { + ...nativeBridgePlan, + nativeForkRollbackTurns, + }; + } + + const nativeForkResult = await adapter.forkThread({ + session: runtimeSession, + }); + forkedCodexThreadId = cleanString(nativeForkResult.threadId) ?? null; + if (nativeForkResult.status !== "success" || !forkedCodexThreadId) { + throw new Error( + nativeForkResult.message || "Codex native thread fork failed.", + ); + } + + const targetRuntimeSession: AgentRuntimeSessionRef = { + ...runtimeSession, + subChatId: targetSubChatId, + nativeSessionId: forkedCodexThreadId, + }; + let nativeBridgeMetadata: Record = { + targetEngineSessionId: forkedCodexThreadId, + nativeForkRollbackTurns, + nativeBridgeResult: + summarizeNativeThreadControlResult(nativeForkResult), + }; + + if (nativeForkRollbackTurns > 0) { + if (!adapter.rollbackThread) { + throw new Error( + "Codex adapter does not support native thread rollback.", + ); + } + + const nativeRollbackResult = await adapter.rollbackThread({ + numTurns: nativeForkRollbackTurns, + session: targetRuntimeSession, + }); + if (nativeRollbackResult.status !== "success") { + throw new Error( + nativeRollbackResult.message || + "Codex native fork rollback failed.", + ); + } + + nativeBridgeMetadata = { + ...nativeBridgeMetadata, + nativeForkRollbackResult: + summarizeNativeThreadControlResult(nativeRollbackResult), + }; + } + + if (adapter.readThread) { + const nativeReadResult = await adapter.readThread({ + includeTurns: true, + session: targetRuntimeSession, + }); + nativeThreadRead = summarizeNativeThreadReadResult(nativeReadResult, { + includeTurns: true, + localMessages: JSON.stringify(snapshot.messages), + }); + nativeBridgeMetadata = { + ...nativeBridgeMetadata, + nativeThreadRead, + }; + } + + forkRecord = buildMossForkSubChatRecord({ + sourceSubChat, + targetSubChatId, + targetName: forkName, + targetMessageId: input.messageId, + targetMessageIndex: input.messageIndex, + nativeBridgePlan, + metadata: nativeBridgeMetadata, + }); + snapshot = forkRecord.snapshot; + nativeSessionLinked = forkRecord.nativeSessionLinked; + } + + let newSubChat = getDatabase() + .insert(subChats) + .values({ + ...forkRecord.insertValues, + ...(forkedCodexThreadId + ? { engineSessionId: forkedCodexThreadId } + : {}), + }) + .returning() + .get(); + + if (nativeSessionLinked && engine === "claude-code") { + const copied = sourceSubChat.sessionId + ? await copyClaudeForkSessionFiles({ + sourceSubChatId: input.subChatId, + targetSubChatId, + }) + : false; + + if (!copied) { + forkRecord = buildMossForkSubChatRecord({ + sourceSubChat, + targetSubChatId, + targetName: forkName, + targetMessageId: input.messageId, + targetMessageIndex: input.messageIndex, + nativeBridgePlan, + forceTranscript: true, + fallbackReason: "Claude session files were not available to copy.", + }); + snapshot = forkRecord.snapshot; + nativeSessionLinked = forkRecord.nativeSessionLinked; + newSubChat = getDatabase() + .update(subChats) + .set({ + messages: forkRecord.insertValues.messages, + sessionId: forkRecord.insertValues.sessionId, + engineSessionId: forkRecord.insertValues.engineSessionId, + runtimeMetadata: forkRecord.insertValues.runtimeMetadata, + updatedAt: new Date(), + }) + .where(eq(subChats.id, targetSubChatId)) + .returning() + .get(); + } + } + + return { + success: true as const, + action: "fork" as const, + mode: snapshot.mode, + nativeSessionLinked, + subChat: newSubChat, + messageCount: snapshot.messageCount, + forkAtSdkUuid: snapshot.forkAtSdkUuid, + nativeBridgePlan, + nativeThreadRead, + }; + }), + + rollbackSession: publicProcedure + .input( + z.object({ + subChatId: z.string(), + targetMessageId: z.string().optional(), + targetSdkMessageUuid: z.string().optional(), + strictTarget: z.boolean().optional(), + nativeCheckpointTurnCount: z.number().int().nonnegative().optional(), + nativeCheckpointRef: z.string().optional(), + applyGitCheckpoint: z.boolean().optional(), + }), + ) + .mutation(async ({ input }) => { + const { subChat, engine } = buildActionPlanForSubChat(input.subChatId); + const sourceNativeSessionId = + subChat.engineSessionId ?? + (engine === "claude-code" ? subChat.sessionId : null); + const permissionMode = resolveSubChatPermissionMode(subChat); + let nativeBridgePlan: unknown = + engine === "hermes" && sourceNativeSessionId + ? buildHermesNativeSessionBridgePlan({ + action: "rollback", + sessionId: sourceNativeSessionId, + cwd: getProjectPathForSubChat(input.subChatId), + modelId: subChat.modelId, + permissionMode, + targetMessageId: input.targetMessageId, + targetSdkMessageUuid: input.targetSdkMessageUuid, + }) + : undefined; + let rollbackRecord = buildMossRollbackSubChatUpdate({ + subChat, + targetMessageId: input.targetMessageId, + targetSdkMessageUuid: input.targetSdkMessageUuid, + strictTarget: input.strictTarget, + appliedGitCheckpoint: Boolean(input.applyGitCheckpoint), + nativeBridgePlan, + }); + let snapshot = rollbackRecord.snapshot; + let nativeThreadRead: NativeThreadReadSummary | null = null; + + if (input.applyGitCheckpoint) { + if (!snapshot.targetSdkMessageUuid) { + throw new Error( + "A target SDK message UUID is required for git rollback.", + ); + } + + const chat = getDatabase() + .select() + .from(chats) + .where(eq(chats.id, subChat.chatId)) + .get(); + if (!chat?.worktreePath) { + throw new Error("A worktree path is required for git rollback."); + } + + const rollback = await applyRollbackStash( + chat.worktreePath, + snapshot.targetSdkMessageUuid, + ); + if (!rollback.success) { + throw new Error(`Git rollback failed: ${rollback.error}`); + } + if (!rollback.checkpointFound) { + throw new Error("Checkpoint not found - cannot rollback git state."); + } + } + + if ( + engine === "codex" && + sourceNativeSessionId && + snapshot.nativeSessionLinked + ) { + const projectPath = getProjectPathForSubChat(input.subChatId); + const adapter = getAgentRuntimeAdapter("codex"); + const runtimeSession = buildRuntimeSessionRefForSubChat({ + subChat, + engine: "codex", + nativeSessionId: sourceNativeSessionId, + projectPath, + }); + const nativeCheckpointTurnCount = input.nativeCheckpointTurnCount; + const nativeCheckpointRef = input.nativeCheckpointRef?.trim() || null; + let numTurns = snapshot.nativeRollbackTurnCount; + let nativeThreadReadBeforeRollback: NativeThreadReadSummary | null = + null; + let nativeCheckpointCurrentTurnCount: number | null = null; + + if (nativeCheckpointTurnCount !== undefined) { + if (!adapter.readThread) { + throw new Error( + "Codex adapter does not support native thread reads for checkpoint rollback.", + ); + } + + const nativeReadResult = await adapter.readThread({ + includeTurns: true, + session: runtimeSession, + }); + nativeThreadReadBeforeRollback = summarizeNativeThreadReadResult( + nativeReadResult, + { + includeTurns: true, + localMessages: JSON.stringify(snapshot.messages), + }, + ); + + if (nativeReadResult.status !== "success") { + throw new Error( + nativeThreadReadBeforeRollback.message || + nativeReadResult.message || + "Codex native thread read failed before checkpoint rollback.", + ); + } + + if (typeof nativeThreadReadBeforeRollback.turnCount !== "number") { + throw new Error( + "Codex native thread read did not include a turn count for checkpoint rollback.", + ); + } + + nativeCheckpointCurrentTurnCount = + nativeThreadReadBeforeRollback.turnCount; + if (nativeCheckpointTurnCount > nativeCheckpointCurrentTurnCount) { + throw new Error( + `Checkpoint turn count (${nativeCheckpointTurnCount}) is newer than Codex native thread turn count (${nativeCheckpointCurrentTurnCount}).`, + ); + } + numTurns = + nativeCheckpointCurrentTurnCount - nativeCheckpointTurnCount; + } + + const nativeCheckpointRevertPlan = + nativeCheckpointTurnCount !== undefined + ? { + type: "thread.checkpoint.revert", + turnCount: nativeCheckpointTurnCount, + checkpointRef: nativeCheckpointRef, + currentTurnCount: nativeCheckpointCurrentTurnCount, + numTurns, + } + : null; + + nativeBridgePlan = { + engine: "codex", + action: "rollback", + bridge: "codex-app-server-thread", + mode: "headless-app-server", + sessionId: sourceNativeSessionId, + cwd: projectPath, + modelId: subChat.modelId, + permissionMode, + numTurns, + ...(nativeCheckpointRevertPlan + ? { + checkpointTurnCount: nativeCheckpointTurnCount, + checkpointRef: nativeCheckpointRef, + nativeCheckpointRevert: nativeCheckpointRevertPlan, + } + : {}), + canRunHeadless: true, + notes: [ + "Codex native rollback uses app-server thread/rollback.", + nativeCheckpointRevertPlan + ? "Rollback turns were computed from the selected native checkpoint turn count." + : "Rollback turns were computed from the Moss transcript boundary.", + numTurns > 0 + ? "The native thread will be rolled back before the Moss transcript is persisted." + : "No later Codex turns were found after the rollback target; the native thread call is skipped.", + ], + }; + + let nativeBridgeMetadata: Record = { + nativeBridgeSkipped: + numTurns <= 0 ? "No Codex turns after rollback target." : false, + ...(nativeCheckpointRevertPlan + ? { nativeCheckpointRevert: nativeCheckpointRevertPlan } + : {}), + ...(nativeThreadReadBeforeRollback + ? { nativeThreadReadBeforeRollback } + : {}), + }; + + if (numTurns > 0) { + if (!adapter.rollbackThread) { + throw new Error( + "Codex adapter does not support native thread rollback.", + ); + } + + const nativeResult = await adapter.rollbackThread({ + numTurns, + session: runtimeSession, + }); + + if (nativeResult.status !== "success") { + throw new Error( + nativeResult.message || "Codex native thread rollback failed.", + ); + } + + nativeBridgeMetadata = { + ...nativeBridgeMetadata, + nativeBridgeResult: + summarizeNativeThreadControlResult(nativeResult), + }; + + if (adapter.readThread) { + const nativeReadResult = await adapter.readThread({ + includeTurns: true, + session: runtimeSession, + }); + nativeThreadRead = summarizeNativeThreadReadResult( + nativeReadResult, + { + includeTurns: true, + localMessages: JSON.stringify(snapshot.messages), + }, + ); + nativeBridgeMetadata = { + ...nativeBridgeMetadata, + nativeThreadRead, + }; + } + } + + rollbackRecord = buildMossRollbackSubChatUpdate({ + subChat, + targetMessageId: input.targetMessageId, + targetSdkMessageUuid: input.targetSdkMessageUuid, + strictTarget: input.strictTarget, + appliedGitCheckpoint: Boolean(input.applyGitCheckpoint), + nativeBridgePlan, + metadata: nativeBridgeMetadata, + }); + snapshot = rollbackRecord.snapshot; + } + + const updated = getDatabase() + .update(subChats) + .set(rollbackRecord.updateValues) + .where(eq(subChats.id, input.subChatId)) + .returning() + .get(); + + return { + success: true as const, + action: "rollback" as const, + mode: snapshot.mode, + nativeSessionLinked: snapshot.nativeSessionLinked, + subChat: updated, + messageCount: snapshot.messageCount, + targetMessageId: snapshot.targetMessageId, + targetSdkMessageUuid: snapshot.targetSdkMessageUuid, + nativeBridgePlan, + nativeThreadRead, + }; + }), + + getProviderSettings: publicProcedure + .input( + z.object({ + projectPath: z.string().optional(), + }), + ) + .query(async ({ input }) => { + if (!input.projectPath) { + return { + status: "missing" as const, + sourcePath: "", + defaultProvider: "moss", + useCustomProvider: false, + customProvider: null, + }; + } + + const readResult = await readMossProviderConfig(input.projectPath, { + createIfMissing: true, + }); + if (readResult.status !== "found" || !readResult.config) { + return { + status: readResult.status, + sourcePath: readResult.sourcePath, + defaultProvider: "moss", + useCustomProvider: false, + customProvider: null, + error: readResult.error, + }; + } + + const custom = getOrCreateCustomProvider(readResult.config); + const hasApiKey = await hasMossProviderSecret(custom.id); + + return { + status: "found" as const, + sourcePath: readResult.sourcePath, + defaultProvider: readResult.config.defaultProvider ?? "moss", + useCustomProvider: readResult.config.defaultProvider === "custom", + customProvider: { + id: custom.id, + label: custom.label, + mode: custom.mode, + baseUrl: custom.baseUrl ?? "", + hasApiKey, + models: { + hermes: custom.engines?.hermes?.model ?? "", + claudeCode: custom.engines?.["claude-code"]?.model ?? "", + codex: custom.engines?.codex?.model ?? "", + customAcp: custom.engines?.["custom-acp"]?.model ?? "", + }, + }, + }; + }), + + saveProviderSettings: publicProcedure + .input( + z.object({ + projectPath: z.string(), + useCustomProvider: z.boolean(), + customProvider: z.object({ + apiKey: z.string().optional(), + clearApiKey: z.boolean().optional(), + baseUrl: z.string().optional(), + models: providerModelSettingsSchema.optional(), + }), + }), + ) + .mutation(async ({ input }) => { + const readResult = await readMossProviderConfig(input.projectPath, { + createIfMissing: true, + }); + if (readResult.status !== "found" || !readResult.config) { + throw new Error( + readResult.error || "Unable to read Moss provider config", + ); + } + + const config = readResult.config; + const custom = getOrCreateCustomProvider(config); + const models = input.customProvider.models; + + custom.baseUrl = cleanString(input.customProvider.baseUrl); + custom.engines = { + ...custom.engines, + hermes: { + ...custom.engines?.hermes, + model: cleanString(models?.hermes) ?? "moss-custom", + }, + "claude-code": { + ...custom.engines?.["claude-code"], + model: cleanString(models?.claudeCode) ?? "opus", + }, + codex: { + ...custom.engines?.codex, + model: cleanString(models?.codex) ?? "gpt-5.5/medium", + authMethod: custom.engines?.codex?.authMethod ?? "openai-api-key", + }, + "custom-acp": { + ...custom.engines?.["custom-acp"], + model: cleanString(models?.customAcp) ?? "custom-acp", + }, + }; + delete custom.apiKey; + + config.defaultProvider = input.useCustomProvider ? "custom" : "moss"; + config.credentialPolicy = { + ...config.credentialPolicy, + singleUserConfiguration: true, + allowCustomBaseUrl: true, + allowCustomApiKey: true, + shareAcrossEngines: true, + }; + config.providers.custom = custom; + + if (input.customProvider.clearApiKey) { + await setMossProviderSecret({ providerId: "custom", apiKey: null }); + } else if (typeof input.customProvider.apiKey === "string") { + await setMossProviderSecret({ + providerId: "custom", + apiKey: input.customProvider.apiKey, + }); + } + + const updated = await writeMossProviderConfig(input.projectPath, config); + const storedSecrets = await buildStoredSecretSummary(updated.config); + return summarizeMossProviderReadResult(updated, storedSecrets); + }), + + setSessionEngine: publicProcedure + .input( + z.object({ + subChatId: z.string(), + engine: agentEngineSchema, + nativeSessionId: z.string().nullable().optional(), + configDir: z.string().nullable().optional(), + providerInstanceId: z.string().nullable().optional(), + modelId: z.string().nullable().optional(), + modelSelection: runtimeModelSelectionSchema.nullable().optional(), + permissionMode: permissionModeSchema.optional(), + metadata: z.record(z.string(), z.unknown()).optional(), + }), + ) + .mutation(async ({ input }) => { + const existingSubChat = getSubChatOrThrow(input.subChatId); + const projectPath = getProjectPathForSubChat(input.subChatId); + const manifest = getAgentRuntimeManifest(input.engine); + const providerInstanceId = + input.providerInstanceId ?? input.modelSelection?.instanceId ?? null; + const metadata = { + ...(input.metadata ?? {}), + ...(providerInstanceId ? { providerInstanceId } : {}), + ...(input.modelSelection + ? { modelSelection: input.modelSelection } + : {}), + }; + const launchPlan = buildAgentRuntimeLaunchPlan({ + runId: `engine-selection:${input.subChatId}:${Date.now()}`, + session: { + subChatId: input.subChatId, + chatId: existingSubChat.chatId, + engineId: input.engine, + providerInstanceId, + nativeSessionId: input.nativeSessionId ?? null, + modelId: input.modelId ?? manifest.defaultModelId ?? null, + modelSelection: input.modelSelection ?? null, + permissionMode: + input.permissionMode ?? + resolveSubChatPermissionMode(existingSubChat), + cwd: projectPath, + projectPath, + runtimeConfigDir: + input.configDir ?? manifest.configRoots.user ?? null, + metadata, + }, + nativeSessionStrategy: input.nativeSessionId ? "resume" : "start", + transport: "settings-selection", + providerRoute: "settings-selection", + }); + + persistAgentRuntimeSession({ + subChatId: input.subChatId, + engine: input.engine, + nativeSessionId: input.nativeSessionId, + configDir: input.configDir, + providerInstanceId, + modelId: input.modelId, + modelSelection: input.modelSelection, + permissionMode: input.permissionMode, + metadata, + launchPlan, + updateLegacySessionId: input.engine === "claude-code", + }); + + const projection = await materializeMossEngineProjectionSafely({ + projectPath, + engineId: input.engine, + createIfMissing: true, + }); + + return { success: true as const, projection }; + }), + + materializeProjection: publicProcedure + .input( + z.object({ + subChatId: z.string().optional(), + projectPath: z.string().optional(), + engine: agentEngineSchema.optional(), + dryRun: z.boolean().optional(), + createIfMissing: z.boolean().optional(), + }), + ) + .mutation(async ({ input }) => { + const projectPath = + input.projectPath || + (input.subChatId ? getProjectPathForSubChat(input.subChatId) : null); + + if (!projectPath) { + throw new Error("projectPath or subChatId is required"); + } + + return materializeMossWorkspaceProjections({ + projectPath, + engines: input.engine ? [input.engine] : AGENT_ENGINE_IDS, + dryRun: input.dryRun, + createIfMissing: input.createIfMissing ?? true, + }); + }), +}); diff --git a/src/main/lib/trpc/routers/agent-utils.ts b/src/main/lib/trpc/routers/agent-utils.ts index ababf03b9..b79d23ab4 100644 --- a/src/main/lib/trpc/routers/agent-utils.ts +++ b/src/main/lib/trpc/routers/agent-utils.ts @@ -1,10 +1,13 @@ import * as fs from "fs/promises" import * as path from "path" import * as os from "os" -import matter from "gray-matter" import { discoverInstalledPlugins, getPluginComponentPaths } from "../../plugins" import { resolveDirentType } from "../../fs/dirent" import { getEnabledPlugins } from "./claude-settings" +import { + parseMossFrontmatter, + stringifyMossFrontmatter, +} from "../../moss-source/frontmatter" // Valid model values for agents export const VALID_AGENT_MODELS = ["sonnet", "opus", "haiku", "inherit"] as const @@ -22,7 +25,7 @@ export interface ParsedAgent { // Agent with source/path metadata export interface FileAgent extends ParsedAgent { - source: "user" | "project" | "plugin" + source: "moss" | "user" | "project" | "plugin" pluginName?: string path: string } @@ -44,7 +47,7 @@ export function parseAgentMd( filename: string ): Partial { try { - const { data, content: body } = matter(content) + const { data, content: body } = parseMossFrontmatter(content) // Parse tools - can be comma-separated string or array let tools: string[] | undefined @@ -70,7 +73,8 @@ export function parseAgentMd( // Validate model const model = - data.model && VALID_AGENT_MODELS.includes(data.model) + typeof data.model === "string" && + VALID_AGENT_MODELS.includes(data.model as AgentModel) ? (data.model as AgentModel) : undefined @@ -100,20 +104,21 @@ export function generateAgentMd(agent: { disallowedTools?: string[] model?: AgentModel }): string { - const frontmatter: string[] = [] - frontmatter.push(`name: ${agent.name}`) - frontmatter.push(`description: ${agent.description}`) + const frontmatter: Record = { + name: agent.name, + description: agent.description, + } if (agent.tools && agent.tools.length > 0) { - frontmatter.push(`tools: ${agent.tools.join(", ")}`) + frontmatter.tools = agent.tools.join(", ") } if (agent.disallowedTools && agent.disallowedTools.length > 0) { - frontmatter.push(`disallowedTools: ${agent.disallowedTools.join(", ")}`) + frontmatter.disallowedTools = agent.disallowedTools.join(", ") } if (agent.model && agent.model !== "inherit") { - frontmatter.push(`model: ${agent.model}`) + frontmatter.model = agent.model } - return `---\n${frontmatter.join("\n")}\n---\n\n${agent.prompt}` + return stringifyMossFrontmatter(`\n\n${agent.prompt}`, frontmatter) } /** @@ -125,6 +130,7 @@ export async function loadAgent( cwd?: string ): Promise { const locations = [ + ...(cwd ? [path.join(cwd, ".moss", "subagents")] : []), path.join(os.homedir(), ".claude", "agents"), ...(cwd ? [path.join(cwd, ".claude", "agents")] : []), ] @@ -188,7 +194,7 @@ export async function loadAgent( */ export async function scanAgentsDirectory( dir: string, - source: "user" | "project" | "plugin", + source: "moss" | "user" | "project" | "plugin", basePath?: string // For project agents, the cwd to make paths relative to ): Promise { const agents: FileAgent[] = [] @@ -217,9 +223,9 @@ export async function scanAgentsDirectory( const parsed = parseAgentMd(content, entry.name) if (parsed.description && parsed.prompt) { - // For project agents, show relative path; for user agents, show ~/.claude/... path + // For project and Moss agents, show relative path; for user agents, show ~/.claude/... path let displayPath: string - if (source === "project" && basePath) { + if ((source === "project" || source === "moss") && basePath) { displayPath = path.relative(basePath, agentPath) } else { // For user agents, show ~/.claude/agents/... format diff --git a/src/main/lib/trpc/routers/agents.ts b/src/main/lib/trpc/routers/agents.ts index a2b19cedf..f665f4407 100644 --- a/src/main/lib/trpc/routers/agents.ts +++ b/src/main/lib/trpc/routers/agents.ts @@ -12,6 +12,7 @@ import { } from "./agent-utils" import { discoverInstalledPlugins, getPluginComponentPaths } from "../../plugins" import { getEnabledPlugins } from "./claude-settings" +import { removeMossProjectionResource } from "../../moss-source" // Shared procedure for listing agents const listAgentsProcedure = publicProcedure @@ -26,9 +27,12 @@ const listAgentsProcedure = publicProcedure const userAgentsDir = path.join(os.homedir(), ".claude", "agents") const userAgentsPromise = scanAgentsDirectory(userAgentsDir, "user") + let mossAgentsPromise = Promise.resolve([]) let projectAgentsPromise = Promise.resolve([]) if (input?.cwd) { + const mossSubagentsDir = path.join(input.cwd, ".moss", "subagents") const projectAgentsDir = path.join(input.cwd, ".claude", "agents") + mossAgentsPromise = scanAgentsDirectory(mossSubagentsDir, "moss", input.cwd) projectAgentsPromise = scanAgentsDirectory(projectAgentsDir, "project", input.cwd) } @@ -51,17 +55,40 @@ const listAgentsProcedure = publicProcedure }) // Scan all directories in parallel - const [userAgents, projectAgents, ...pluginAgentsArrays] = + const [mossAgents, userAgents, projectAgents, ...pluginAgentsArrays] = await Promise.all([ + mossAgentsPromise, userAgentsPromise, projectAgentsPromise, ...pluginAgentsPromises, ]) const pluginAgents = pluginAgentsArrays.flat() - return [...projectAgents, ...userAgents, ...pluginAgents] + return [...mossAgents, ...projectAgents, ...userAgents, ...pluginAgents] }) +async function resolveMossSubagentSymlinkSource( + agentPath: string, + projectPath: string, +): Promise { + try { + const stat = await fs.lstat(agentPath) + if (!stat.isSymbolicLink()) return null + const linkTarget = await fs.readlink(agentPath) + const resolvedTarget = path.resolve(path.dirname(agentPath), linkTarget) + const mossSubagentsRoot = path.resolve(projectPath, ".moss", "subagents") + if ( + resolvedTarget === mossSubagentsRoot || + resolvedTarget.startsWith(`${mossSubagentsRoot}${path.sep}`) + ) { + return resolvedTarget + } + } catch { + return null + } + return null +} + export const agentsRouter = router({ /** * List all agents from filesystem @@ -82,6 +109,14 @@ export const agentsRouter = router({ .input(z.object({ name: z.string(), cwd: z.string().optional() })) .query(async ({ input }) => { const locations = [ + ...(input.cwd + ? [ + { + dir: path.join(input.cwd, ".moss", "subagents"), + source: "moss" as const, + }, + ] + : []), { dir: path.join(os.homedir(), ".claude", "agents"), source: "user" as const, @@ -306,10 +341,12 @@ export const agentsRouter = router({ } let targetDir: string + let projectPath: string | undefined if (input.source === "project") { if (!input.cwd) { throw new Error("Project path (cwd) required for project agents") } + projectPath = input.cwd targetDir = path.join(input.cwd, ".claude", "agents") } else { targetDir = path.join(os.homedir(), ".claude", "agents") @@ -317,6 +354,30 @@ export const agentsRouter = router({ const agentPath = path.join(targetDir, `${safeName}.md`) + const mossSourcePath = projectPath + ? await resolveMossSubagentSymlinkSource(agentPath, projectPath) + : null + + if (mossSourcePath && projectPath) { + try { + await fs.unlink(mossSourcePath) + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error + } + await removeMossProjectionResource({ + projectPath, + resourceId: `moss:subagent:${safeName}`, + sourcePath: path.relative(projectPath, mossSourcePath), + targetPaths: [ + path.join(".claude", "agents", `${safeName}.md`), + path.join(".codex", "agents", `${safeName}.md`), + ], + removeTargets: true, + }) + await fs.rm(agentPath, { force: true }) + return { deleted: true } + } + await fs.unlink(agentPath) return { deleted: true } diff --git a/src/main/lib/trpc/routers/chat-runtime-selection.ts b/src/main/lib/trpc/routers/chat-runtime-selection.ts index 8b5c2fab0..a572cdb23 100644 --- a/src/main/lib/trpc/routers/chat-runtime-selection.ts +++ b/src/main/lib/trpc/routers/chat-runtime-selection.ts @@ -25,12 +25,16 @@ export function buildInitialSubChatValues(input: { model?: string mode: ChatMode messages: string + runtimeMetadata?: string }) { return { chatId: input.chatId, ...resolveChatRuntimeSelection(input), mode: input.mode, messages: input.messages, + ...(input.runtimeMetadata + ? { runtimeMetadata: input.runtimeMetadata } + : {}), } } diff --git a/src/main/lib/trpc/routers/chats.ts b/src/main/lib/trpc/routers/chats.ts index a699b445d..935597109 100644 --- a/src/main/lib/trpc/routers/chats.ts +++ b/src/main/lib/trpc/routers/chats.ts @@ -4,6 +4,11 @@ import * as fs from "fs/promises" import * as path from "path" import simpleGit from "simple-git" import { z } from "zod" +import { + getCodexOutputArtifactsFromBlocks, + normalizeCodexConversationBlocksFromMessage, + type CodexOutputArtifact, +} from "../../../../shared/codex-tool-normalizer" import { getAuthManager } from "../../../index" import { trackPRCreated, @@ -11,6 +16,21 @@ import { trackWorkspaceCreated, trackWorkspaceDeleted, } from "../../analytics" +import { + AGENT_ENGINE_IDS, + DEFAULT_AGENT_ENGINE_ID, + buildMossForkSnapshot, + getAgentRuntimeManifest, + mergeMossSessionControlMetadata, + type AgentEngineId, + type AgentPermissionMode, +} from "../../agent-runtime" +import { + readCodexNativeSessionEventsById, + recoverCodexNativeMessagesFromSessionEvents, + type CodexNativeRecoveredMessage, +} from "../../agent-runtime/codex-native-recovery" +import { shouldClearStaleAgentStreamId } from "../../agent-runtime/stale-stream-state" import { chats, getDatabase, projects, subChats } from "../../db" import { createWorktreeForChat, @@ -24,9 +44,19 @@ import { computeContentHash, gitCache } from "../../git/cache" import { splitUnifiedDiffByFile } from "../../git/diff-parser" import { execWithShellEnv } from "../../git/shell-env" import { applyRollbackStash } from "../../git/stash" +import { + linkMossSourceIntoWorkspace, + materializeMossWorkspaceProjections, +} from "../../moss-source" import { checkInternetConnection, checkOllamaStatus } from "../../ollama" import { terminalManager } from "../../terminal/manager" import { publicProcedure, router } from "../index" +import { + buildEmptySubChatValues, + buildInitialSubChatValues, +} from "./chat-runtime-selection" +import { hasActiveCodexStreamForSubChat } from "./codex" +import { hasActiveHermesStreamForSubChat } from "./hermes" type WorktreeSetupFailurePayload = { kind: "create-failed" | "setup-failed" @@ -34,9 +64,153 @@ type WorktreeSetupFailurePayload = { projectId: string } +type WorktreeSetupEventPayload = { + kind: "create-started" | "created" | "setup-complete" | "setup-failed" | "create-failed" + chatId: string + projectId: string + clientRequestId?: string | null + projectName?: string | null + worktreePath?: string | null + branch?: string | null + baseBranch?: string | null + output?: string[] + errors?: string[] + message?: string +} + +const permissionModeSchema = z.enum([ + "plan", + "agent", + "bypass", + "read-only", + "ask-approval", + "full-access", + "custom", +]) + +function buildInitialRuntimeMetadata( + permissionMode: AgentPermissionMode | undefined, +): string | undefined { + return permissionMode + ? JSON.stringify({ permissionMode }) + : undefined +} + +type ChatOutputArtifactRow = { + id: string + artifact: CodexOutputArtifact + chatId: string + chatName: string | null + subChatId: string + subChatName: string | null + projectId: string | null + projectName: string | null + projectPath: string | null + engine: string | null + createdAt: Date | null + updatedAt: Date | null +} + +type ArtifactCandidateSubChat = { + chatId: string + chatName: string | null + chatCreatedAt: Date | null + chatUpdatedAt: Date | null + projectId: string | null + projectName: string | null + projectPath: string | null + subChatId: string + subChatName: string | null + subChatCreatedAt: Date | null + subChatUpdatedAt: Date | null + engine: string | null + messages: string | null +} + +function safeParseMessageList(value: string | null): unknown[] { + if (!value) return [] + try { + const parsed = JSON.parse(value) + return Array.isArray(parsed) ? parsed : [] + } catch { + return [] + } +} + +function extractLibraryArtifactsFromSubChat( + row: ArtifactCandidateSubChat, +): ChatOutputArtifactRow[] { + const messages = safeParseMessageList(row.messages) + const outputArtifacts: ChatOutputArtifactRow[] = [] + + messages.forEach((message, messageIndex) => { + const messageRecord = + typeof message === "object" && message !== null + ? (message as Record) + : null + if (!messageRecord || messageRecord.role !== "assistant") return + + const messageId = + typeof messageRecord.id === "string" && messageRecord.id.trim() + ? messageRecord.id + : `message-${messageIndex}` + const blocks = normalizeCodexConversationBlocksFromMessage(messageRecord, { + chatStatus: "idle", + turnId: messageId, + }) + + getCodexOutputArtifactsFromBlocks(blocks).forEach((artifact, artifactIndex) => { + outputArtifacts.push({ + id: [ + row.chatId, + row.subChatId, + messageId, + artifact.id || `artifact-${artifactIndex}`, + ].join(":"), + artifact: { + ...artifact, + id: [ + row.chatId, + row.subChatId, + messageId, + artifact.id || `artifact-${artifactIndex}`, + ].join(":"), + }, + chatId: row.chatId, + chatName: row.chatName, + subChatId: row.subChatId, + subChatName: row.subChatName, + projectId: row.projectId, + projectName: row.projectName, + projectPath: row.projectPath, + engine: row.engine, + createdAt: row.subChatCreatedAt ?? row.chatCreatedAt, + updatedAt: row.subChatUpdatedAt ?? row.chatUpdatedAt, + }) + }) + }) + + return outputArtifacts +} + function sendWorktreeSetupFailure( windowId: number | null, payload: WorktreeSetupFailurePayload, +): void { + sendWorktreeSetupPayload(windowId, "worktree:setup-failed", payload) +} + +function sendWorktreeSetupEvent( + windowId: number | null, + payload: WorktreeSetupEventPayload, +): void { + sendWorktreeSetupPayload(windowId, "worktree:setup-event", payload) +} + +function sendWorktreeSetupPayload( + windowId: number | null, + channel: "worktree:setup-failed" | "worktree:setup-event", + payload: WorktreeSetupFailurePayload | WorktreeSetupEventPayload, ): void { const targets: BrowserWindow[] = [] @@ -53,7 +227,164 @@ function sendWorktreeSetupFailure( for (const window of targets) { if (window.isDestroyed()) continue - window.webContents.send("worktree:setup-failed", payload) + window.webContents.send(channel, payload) + } +} + +async function recoverCodexNativeSubChatMessagesIfNeeded< + TSubChat extends { + id: string + engine: string + engineSessionId?: string | null + messages: string + }, +>(subChat: TSubChat): Promise { + if (subChat.engine !== "codex" || !subChat.engineSessionId) { + return subChat + } + + let messages: CodexNativeRecoveredMessage[] + try { + const parsedMessages = JSON.parse(subChat.messages || "[]") + if (!Array.isArray(parsedMessages)) return subChat + messages = parsedMessages + } catch { + return subChat + } + + if (!messages.some((message) => message.role === "assistant")) { + return subChat + } + + const sessionEvents = await readCodexNativeSessionEventsById( + subChat.engineSessionId, + ) + if (sessionEvents.length === 0) return subChat + + const recovery = recoverCodexNativeMessagesFromSessionEvents( + messages, + sessionEvents, + ) + if (!recovery.changed) return subChat + + const nextMessages = JSON.stringify(recovery.messages) + try { + getDatabase() + .update(subChats) + .set({ messages: nextMessages }) + .where(eq(subChats.id, subChat.id)) + .run() + } catch (error) { + console.warn("[chats] Failed to persist recovered Codex native messages:", error) + } + + return { + ...subChat, + messages: nextMessages, + } +} + +async function clearStaleAgentStreamIdIfNeeded< + TSubChat extends { + id: string + engine: string + streamId?: string | null + }, +>(subChat: TSubChat): Promise { + if ( + !shouldClearStaleAgentStreamId(subChat, { + isActiveCodexStream: hasActiveCodexStreamForSubChat, + isActiveHermesStream: hasActiveHermesStreamForSubChat, + }) + ) { + return subChat + } + + try { + getDatabase() + .update(subChats) + .set({ streamId: null }) + .where(eq(subChats.id, subChat.id)) + .run() + } catch (error) { + console.warn("[chats] Failed to clear stale agent stream id:", error) + } + + return { + ...subChat, + streamId: null, + } +} + +async function normalizeLoadedSubChatRuntimeState< + TSubChat extends { + id: string + engine: string + engineSessionId?: string | null + messages: string + streamId?: string | null + }, +>(subChat: TSubChat): Promise { + const recoveredSubChat = await recoverCodexNativeSubChatMessagesIfNeeded(subChat) + return clearStaleAgentStreamIdIfNeeded(recoveredSubChat) +} + +async function materializeMossWorkspaceForChat(params: { + sourceProjectPath: string + workspacePath: string + chatId: string +}): Promise { + try { + const sourceLink = await linkMossSourceIntoWorkspace({ + sourceProjectPath: params.sourceProjectPath, + workspacePath: params.workspacePath, + }) + const projections = await materializeMossWorkspaceProjections({ + projectPath: params.workspacePath, + createIfMissing: true, + }) + const summary = projections.projections.reduce( + (acc, projection) => ({ + created: acc.created + projection.summary.created, + updated: acc.updated + projection.summary.updated, + skipped: acc.skipped + projection.summary.skipped, + conflict: acc.conflict + projection.summary.conflict, + unsupported: acc.unsupported + projection.summary.unsupported, + total: acc.total + projection.summary.total, + }), + { + created: 0, + updated: 0, + skipped: 0, + conflict: 0, + unsupported: 0, + total: 0, + }, + ) + + if (sourceLink.status === "conflict" || summary.conflict > 0) { + console.warn("[moss-source] Workspace projection conflicts:", { + chatId: params.chatId, + workspacePath: params.workspacePath, + sourceLink, + summary, + }) + return + } + + console.log("[moss-source] Workspace projections refreshed:", { + chatId: params.chatId, + workspacePath: params.workspacePath, + sourceLinkStatus: sourceLink.status, + sourceLinkReason: sourceLink.reason, + summary, + }) + } catch (error) { + console.warn("[moss-source] Workspace projection refresh failed:", { + chatId: params.chatId, + workspacePath: params.workspacePath, + error: error instanceof Error ? error.message : String(error), + }) } } @@ -229,6 +560,65 @@ export const chatsRouter = router({ .all() }), + /** + * List local output artifacts across non-archived chats. + * This backs the desktop Library route without coupling it to a single agent engine. + */ + listOutputArtifacts: publicProcedure + .input( + z + .object({ + projectId: z.string().optional(), + limit: z.number().int().min(1).max(1000).optional(), + }) + .optional(), + ) + .query(({ input }): ChatOutputArtifactRow[] => { + const db = getDatabase() + const conditions = [ + isNull(chats.archivedAt), + sql`( + ${subChats.messages} LIKE '%generated-image%' OR + ${subChats.messages} LIKE '%data-output%' OR + ${subChats.messages} LIKE '%resource_link%' OR + ${subChats.messages} LIKE '%embedded_resource%' OR + ${subChats.messages} LIKE '%tool-Write%' OR + ${subChats.messages} LIKE '%tool-Edit%' OR + ${subChats.messages} LIKE '%tool-write:%' OR + ${subChats.messages} LIKE '%tool-edit:%' + )`, + ] + if (input?.projectId) { + conditions.push(eq(chats.projectId, input.projectId)) + } + + const rows = db + .select({ + chatId: chats.id, + chatName: chats.name, + chatCreatedAt: chats.createdAt, + chatUpdatedAt: chats.updatedAt, + projectId: chats.projectId, + projectName: projects.name, + projectPath: projects.path, + subChatId: subChats.id, + subChatName: subChats.name, + subChatCreatedAt: subChats.createdAt, + subChatUpdatedAt: subChats.updatedAt, + engine: subChats.engine, + messages: subChats.messages, + }) + .from(subChats) + .innerJoin(chats, eq(subChats.chatId, chats.id)) + .leftJoin(projects, eq(chats.projectId, projects.id)) + .where(and(...conditions)) + .orderBy(desc(subChats.updatedAt)) + .limit(input?.limit ?? 500) + .all() + + return rows.flatMap((row) => extractLibraryArtifactsFromSubChat(row)) + }), + /** * List archived chats (optionally filter by project) */ @@ -253,17 +643,22 @@ export const chatsRouter = router({ */ get: publicProcedure .input(z.object({ id: z.string() })) - .query(({ input }) => { + .query(async ({ input }) => { const db = getDatabase() const chat = db.select().from(chats).where(eq(chats.id, input.id)).get() if (!chat) return null - const chatSubChats = db + const loadedSubChats = db .select() .from(subChats) .where(eq(subChats.chatId, input.id)) .orderBy(subChats.createdAt) .all() + const chatSubChats = await Promise.all( + loadedSubChats.map((subChat) => + normalizeLoadedSubChatRuntimeState(subChat), + ), + ) const project = db .select() @@ -282,6 +677,7 @@ export const chatsRouter = router({ z.object({ projectId: z.string(), name: z.string().optional(), + engine: z.enum(AGENT_ENGINE_IDS).optional(), model: z.string().optional(), initialMessage: z.string().optional(), initialMessageParts: z @@ -309,7 +705,9 @@ export const chatsRouter = router({ baseBranch: z.string().optional(), // Branch to base the worktree off branchType: z.enum(["local", "remote"]).optional(), // Whether baseBranch is local or remote useWorktree: z.boolean().default(true), // If false, work directly in project dir + clientRequestId: z.string().optional(), mode: z.enum(["plan", "agent"]).default("agent"), + permissionMode: permissionModeSchema.optional(), }), ) .mutation(async ({ input, ctx }) => { @@ -364,11 +762,14 @@ export const chatsRouter = router({ const subChat = db .insert(subChats) - .values({ + .values(buildInitialSubChatValues({ chatId: chat.id, + engine: input.engine, + model: input.model, mode: input.mode, messages: initialMessages, - }) + runtimeMetadata: buildInitialRuntimeMetadata(input.permissionMode), + })) .returning() .get() console.log("[chats.create] created subChat:", subChat) @@ -388,6 +789,14 @@ export const chatsRouter = router({ "type:", input.branchType, ) + sendWorktreeSetupEvent(requestingWindowId, { + kind: "create-started", + chatId: chat.id, + projectId: project.id, + clientRequestId: input.clientRequestId ?? null, + projectName: project.name, + branch: input.baseBranch ?? null, + }) const result = await createWorktreeForChat( project.path, sanitizeProjectName(project.name), @@ -395,7 +804,38 @@ export const chatsRouter = router({ input.baseBranch, input.branchType, { + onCreated: ({ worktreePath, branch, baseBranch }) => { + sendWorktreeSetupEvent(requestingWindowId, { + kind: "created", + chatId: chat.id, + projectId: project.id, + clientRequestId: input.clientRequestId ?? null, + projectName: project.name, + worktreePath, + branch: branch ?? null, + baseBranch: baseBranch ?? input.baseBranch ?? null, + output: [`Created worktree at ${worktreePath}`], + }) + return materializeMossWorkspaceForChat({ + sourceProjectPath: project.path, + workspacePath: worktreePath, + chatId: chat.id, + }) + }, onSetupComplete: (setupResult: WorktreeSetupResult) => { + sendWorktreeSetupEvent(requestingWindowId, { + kind: setupResult.success ? "setup-complete" : "setup-failed", + chatId: chat.id, + projectId: project.id, + clientRequestId: input.clientRequestId ?? null, + projectName: project.name, + output: setupResult.output, + errors: setupResult.errors, + message: setupResult.success + ? "Worktree setup complete." + : setupResult.errors[0] || + "Worktree setup failed. Check your setup commands.", + }) if (setupResult.success) return const message = setupResult.errors[0] || @@ -426,6 +866,15 @@ export const chatsRouter = router({ } } else { console.warn(`[Worktree] Failed: ${result.error}`) + sendWorktreeSetupEvent(requestingWindowId, { + kind: "create-failed", + chatId: chat.id, + projectId: project.id, + clientRequestId: input.clientRequestId ?? null, + projectName: project.name, + message: result.error || "Worktree creation failed.", + errors: result.error ? [result.error] : [], + }) sendWorktreeSetupFailure(requestingWindowId, { kind: "create-failed", message: result.error || "Worktree creation failed.", @@ -437,6 +886,11 @@ export const chatsRouter = router({ .where(eq(chats.id, chat.id)) .run() worktreeResult = { worktreePath: project.path } + await materializeMossWorkspaceForChat({ + sourceProjectPath: project.path, + workspacePath: project.path, + chatId: chat.id, + }) } } else { // Local mode: use project path directly, no branch info @@ -446,10 +900,16 @@ export const chatsRouter = router({ .where(eq(chats.id, chat.id)) .run() worktreeResult = { worktreePath: project.path } + await materializeMossWorkspaceForChat({ + sourceProjectPath: project.path, + workspacePath: project.path, + chatId: chat.id, + }) } const response = { ...chat, + clientRequestId: input.clientRequestId ?? null, worktreePath: worktreeResult.worktreePath || project.path, branch: worktreeResult.branch, baseBranch: worktreeResult.baseBranch, @@ -683,15 +1143,17 @@ export const chatsRouter = router({ */ getSubChat: publicProcedure .input(z.object({ id: z.string() })) - .query(({ input }) => { + .query(async ({ input }) => { const db = getDatabase() - const subChat = db + const loadedSubChat = db .select() .from(subChats) .where(eq(subChats.id, input.id)) .get() - if (!subChat) return null + if (!loadedSubChat) return null + const subChat = + await normalizeLoadedSubChatRuntimeState(loadedSubChat) const chat = db .select() @@ -718,6 +1180,8 @@ export const chatsRouter = router({ z.object({ chatId: z.string(), name: z.string().optional(), + engine: z.enum(AGENT_ENGINE_IDS).optional(), + model: z.string().optional(), mode: z.enum(["plan", "agent"]).default("agent"), }), ) @@ -725,12 +1189,13 @@ export const chatsRouter = router({ const db = getDatabase() return db .insert(subChats) - .values({ + .values(buildEmptySubChatValues({ chatId: input.chatId, name: input.name, + engine: input.engine, + model: input.model, mode: input.mode, - messages: "[]", - }) + })) .returning() .get() }), @@ -773,28 +1238,22 @@ export const chatsRouter = router({ } if (cutoffIndex === -1) throw new Error("Message not found") - // 3. Slice messages up to and including the target - const messagesToFork = allMessages.slice(0, cutoffIndex + 1) - - // 4. Find sdkMessageUuid of last assistant message (for resumeSessionAt) - const lastAssistant = [...messagesToFork] - .reverse() - .find((m: any) => m.role === "assistant") - const forkAtSdkUuid = lastAssistant?.metadata?.sdkMessageUuid || null - - // 5. Generate new IDs for all messages + set shouldForkResume on last assistant - const forkedMessages = messagesToFork.map((msg: any, i: number) => ({ - ...msg, - id: `fork-${Date.now()}-${i}-${Math.random().toString(36).slice(2, 7)}`, - metadata: { - ...msg.metadata, - shouldResume: undefined, - ...(msg === lastAssistant && - forkAtSdkUuid && { - shouldForkResume: true, - }), - }, - })) + const sourceEngine = AGENT_ENGINE_IDS.includes( + sourceSubChat.engine as AgentEngineId, + ) + ? sourceSubChat.engine as AgentEngineId + : DEFAULT_AGENT_ENGINE_ID + const manifest = getAgentRuntimeManifest(sourceEngine) + const sourceNativeSessionId = + sourceSubChat.engineSessionId ?? + (sourceEngine === "claude-code" ? sourceSubChat.sessionId : null) + let snapshot = buildMossForkSnapshot({ + engine: sourceEngine, + nativeSessionId: sourceNativeSessionId, + messages: allMessages, + features: manifest.features, + targetMessageIndex: cutoffIndex, + }) // 6. Generate fork name: [N] originalName let forkName = input.name @@ -821,21 +1280,49 @@ export const chatsRouter = router({ forkName = `[${maxN + 1}] ${baseName}` } - // 7. Insert new sub-chat with sessionId from original (needed for resume) - const newSubChat = db + const buildForkRuntimeMetadata = ( + mode: typeof snapshot.mode, + extra?: Record, + ) => + mergeMossSessionControlMetadata(sourceSubChat.runtimeMetadata, { + action: "fork", + mode, + source: "fork-sub-chat", + sourceSubChatId: input.subChatId, + sourceEngineSessionId: + sourceSubChat.engineSessionId ?? sourceSubChat.sessionId ?? null, + nativeSessionLinked, + targetMessageId: input.messageId, + targetMessageIndex: cutoffIndex, + forkAtSdkUuid: snapshot.forkAtSdkUuid, + ...(extra ?? {}), + }) + + let nativeSessionLinked = snapshot.nativeSessionLinked + + // 7. Insert new sub-chat. Only engines with a verified native bridge keep + // native session identity; all others fork the transcript and start fresh. + let newSubChat = db .insert(subChats) .values({ chatId: sourceSubChat.chatId, name: forkName, mode: sourceSubChat.mode, - messages: JSON.stringify(forkedMessages), - sessionId: sourceSubChat.sessionId, + messages: JSON.stringify(snapshot.messages), + sessionId: nativeSessionLinked ? sourceSubChat.sessionId : null, + engine: sourceEngine, + engineSessionId: nativeSessionLinked + ? sourceSubChat.engineSessionId ?? sourceSubChat.sessionId + : null, + engineConfigDir: sourceSubChat.engineConfigDir, + modelId: sourceSubChat.modelId, + runtimeMetadata: buildForkRuntimeMetadata(snapshot.mode), }) .returning() .get() - // 8. Copy .jsonl session files to the new isolated config dir - if (sourceSubChat.sessionId) { + // 8. Copy Claude .jsonl session files to the new isolated config dir + if (nativeSessionLinked && sourceEngine === "claude-code") { try { const { app } = await import("electron") const userDataPath = app.getPath("userData") @@ -859,28 +1346,50 @@ export const chatsRouter = router({ if (sourceDirExists) { await fs.cp(sourceDir, targetDir, { recursive: true }) + } else { + throw new Error("Claude session files were not available to copy") } } catch (err) { console.warn("[forkSubChat] Failed to copy session files:", err) - // Clear shouldForkResume since there's no .jsonl to fork from - for (const m of forkedMessages) { - if (m.metadata?.shouldForkResume) { - delete m.metadata.shouldForkResume - } - } - db.update(subChats) - .set({ messages: JSON.stringify(forkedMessages) }) + snapshot = buildMossForkSnapshot({ + engine: sourceEngine, + nativeSessionId: null, + messages: allMessages, + features: manifest.features, + targetMessageIndex: cutoffIndex, + }) + nativeSessionLinked = false + newSubChat = db + .update(subChats) + .set({ + messages: JSON.stringify(snapshot.messages), + sessionId: null, + engineSessionId: null, + runtimeMetadata: buildForkRuntimeMetadata(snapshot.mode, { + nativeSessionLinked, + fallbackReason: "Claude session files were not available to copy.", + }), + updatedAt: new Date(), + }) .where(eq(subChats.id, newSubChat.id)) - .run() + .returning() + .get() } } - console.log("[forkSubChat] Created", { id: newSubChat.id, name: forkName, messages: forkedMessages.length }) + console.log("[forkSubChat] Created", { + id: newSubChat.id, + name: forkName, + messages: snapshot.messageCount, + mode: snapshot.mode, + }) return { subChat: newSubChat, - messageCount: forkedMessages.length, - forkAtSdkUuid, + messageCount: snapshot.messageCount, + forkAtSdkUuid: snapshot.forkAtSdkUuid, + mode: snapshot.mode, + nativeSessionLinked, } }), diff --git a/src/main/lib/trpc/routers/claude-settings.ts b/src/main/lib/trpc/routers/claude-settings.ts index 6ab649059..19cce290b 100644 --- a/src/main/lib/trpc/routers/claude-settings.ts +++ b/src/main/lib/trpc/routers/claude-settings.ts @@ -1,107 +1,20 @@ -import * as fs from "fs/promises" -import * as path from "path" -import * as os from "os" import { z } from "zod" import { router, publicProcedure } from "../index" - -const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json") - -// Cache for enabled plugins to avoid repeated filesystem reads -let enabledPluginsCache: { plugins: string[]; timestamp: number } | null = null -const ENABLED_PLUGINS_CACHE_TTL_MS = 5000 // 5 seconds - -// Cache for approved plugin MCP servers -let approvedMcpCache: { servers: string[]; timestamp: number } | null = null -const APPROVED_MCP_CACHE_TTL_MS = 5000 // 5 seconds - -/** - * Invalidate the enabled plugins cache - * Call this when enabledPlugins setting changes - */ -export function invalidateEnabledPluginsCache(): void { - enabledPluginsCache = null -} - -/** - * Invalidate the approved MCP servers cache - * Call this when approvedPluginMcpServers setting changes - */ -export function invalidateApprovedMcpCache(): void { - approvedMcpCache = null -} - -/** - * Read Claude settings.json file - * Returns empty object if file doesn't exist - */ -async function readClaudeSettings(): Promise> { - try { - const content = await fs.readFile(CLAUDE_SETTINGS_PATH, "utf-8") - return JSON.parse(content) - } catch (error) { - // File doesn't exist or is invalid JSON - return {} - } -} - -/** - * Get list of enabled plugin identifiers from settings.json - * Plugins are DISABLED by default — only plugins explicitly in this list are active. - * Returns empty array if no plugins have been enabled. - * Results are cached for 5 seconds to reduce filesystem reads. - */ -export async function getEnabledPlugins(): Promise { - // Return cached result if still valid - if (enabledPluginsCache && Date.now() - enabledPluginsCache.timestamp < ENABLED_PLUGINS_CACHE_TTL_MS) { - return enabledPluginsCache.plugins - } - - const settings = await readClaudeSettings() - const plugins = Array.isArray(settings.enabledPlugins) ? settings.enabledPlugins as string[] : [] - - enabledPluginsCache = { plugins, timestamp: Date.now() } - return plugins -} - -/** - * Get list of approved plugin MCP server identifiers from settings.json - * Format: "{pluginSource}:{serverName}" e.g., "ccsetup:ccsetup:context7" - * Returns empty array if no approved servers - * Results are cached for 5 seconds to reduce filesystem reads - */ -export async function getApprovedPluginMcpServers(): Promise { - // Return cached result if still valid - if (approvedMcpCache && Date.now() - approvedMcpCache.timestamp < APPROVED_MCP_CACHE_TTL_MS) { - return approvedMcpCache.servers - } - - const settings = await readClaudeSettings() - const servers = Array.isArray(settings.approvedPluginMcpServers) - ? settings.approvedPluginMcpServers as string[] - : [] - - approvedMcpCache = { servers, timestamp: Date.now() } - return servers -} - -/** - * Check if a plugin MCP server is approved - */ -export async function isPluginMcpApproved(pluginSource: string, serverName: string): Promise { - const approved = await getApprovedPluginMcpServers() - const identifier = `${pluginSource}:${serverName}` - return approved.includes(identifier) -} - -/** - * Write Claude settings.json file - * Creates the .claude directory if it doesn't exist - */ -async function writeClaudeSettings(settings: Record): Promise { - const dir = path.dirname(CLAUDE_SETTINGS_PATH) - await fs.mkdir(dir, { recursive: true }) - await fs.writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8") -} +export { + getApprovedPluginMcpServers, + getEnabledPlugins, + invalidateApprovedMcpCache, + invalidateEnabledPluginsCache, + isPluginMcpApproved, +} from "../../claude-plugin-settings" +import { + getApprovedPluginMcpServers, + getEnabledPlugins, + invalidateApprovedMcpCache, + invalidateEnabledPluginsCache, + readClaudeSettings, + writeClaudeSettings, +} from "../../claude-plugin-settings" export const claudeSettingsRouter = router({ /** diff --git a/src/main/lib/trpc/routers/claude.ts b/src/main/lib/trpc/routers/claude.ts index 9e5eadffe..4832bf9c2 100644 --- a/src/main/lib/trpc/routers/claude.ts +++ b/src/main/lib/trpc/routers/claude.ts @@ -10,7 +10,7 @@ import { buildClaudeEnv, checkOfflineFallback, createTransformer, - getBundledClaudeBinaryPath, + resolveClaudeCodeExecutable, logClaudeEnv, logRawClaudeMessage, type UIMessageChunk, @@ -41,6 +41,13 @@ import { } from "../../mcp-auth" import { fetchOAuthMetadata, getMcpBaseUrl } from "../../oauth" import { discoverPluginMcpServers } from "../../plugins" +import { buildAgentRuntimeLaunchPlan } from "../../agent-runtime/launch-plan" +import { persistAgentRuntimeSession } from "../../agent-runtime/session-store" +import { + getMossProviderSecret, + resolveMossProviderForEngine, + type ResolvedMossProvider, +} from "../../moss-source" import { publicProcedure, router } from "../index" import { buildAgentsOption } from "./agent-utils" import { @@ -146,6 +153,19 @@ function parseMentions(prompt: string): { } } +function buildClaudeCustomConfigFromMossProvider( + provider: ResolvedMossProvider, + fallbackModel?: string, +): { model: string; token: string; baseUrl: string } | undefined { + if (provider.status !== "resolved" || !provider.apiKey) return undefined + + return { + model: provider.model || fallbackModel || "opus", + token: provider.apiKey, + baseUrl: provider.baseUrl || "https://api.anthropic.com", + } +} + /** * Decrypt token using Electron's safeStorage */ @@ -424,6 +444,7 @@ const MCP_FETCH_TIMEOUT_MS = 40_000 */ async function fetchToolsForServer( serverConfig: McpServerConfig, + options: { sourcePath?: string | null } = {}, ): Promise { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), MCP_FETCH_TIMEOUT_MS), @@ -448,6 +469,8 @@ async function fetchToolsForServer( command, args: (serverConfig as any).args, env: (serverConfig as any).env, + cwd: (serverConfig as any).cwd, + sourcePath: options.sourcePath, }) } catch { return [] @@ -494,7 +517,7 @@ export async function getAllMcpConfigHandler() { let needsAuth = false try { - tools = await fetchToolsForServer(serverConfig) + tools = await fetchToolsForServer(serverConfig, { sourcePath: scope }) } catch (error) { console.error(`[MCP] Failed to fetch tools for ${name}:`, error) } @@ -980,6 +1003,20 @@ export const claudeRouter = router({ // 2.5. AUTO-FALLBACK: Check internet and switch to Ollama if offline // Only check if offline mode is enabled in settings const claudeCodeToken = getClaudeCodeToken() + const mossProviderLookupPath = + input.projectPath || + resolveProjectPathFromWorktree(input.cwd) || + input.cwd + const mossProvider = await resolveMossProviderForEngine({ + projectPath: mossProviderLookupPath, + engineId: "claude-code", + requestedModelId: input.model, + createIfMissing: true, + secretResolver: { getSecret: getMossProviderSecret }, + }) + if (mossProvider.warnings.length > 0) { + console.warn("[claude] Moss provider warnings:", mossProvider.warnings) + } const offlineResult = await checkOfflineFallback( input.customConfig, claudeCodeToken, @@ -998,13 +1035,25 @@ export const claudeRouter = router({ } // Use offline config if available - const finalCustomConfig = offlineResult.config || input.customConfig + const mossCustomConfig = + input.customConfig + ? undefined + : buildClaudeCustomConfigFromMossProvider( + mossProvider, + input.model, + ) + const finalCustomConfig = + offlineResult.config || input.customConfig || mossCustomConfig + const isUsingMossProviderConfig = + Boolean(mossCustomConfig) && finalCustomConfig === mossCustomConfig const isUsingOllama = offlineResult.isUsingOllama // Track connection method for analytics let connectionMethod = "claude-subscription" // default (Claude Code OAuth) if (isUsingOllama) { connectionMethod = "offline-ollama" + } else if (isUsingMossProviderConfig) { + connectionMethod = "moss-provider" } else if (finalCustomConfig) { // Has custom config = either API key or custom model const isDefaultAnthropicUrl = @@ -1126,13 +1175,22 @@ export const claudeRouter = router({ prompt = createPromptWithImages() } + const mossProviderEnv = + mossProvider.status === "resolved" ? mossProvider.env : {} + const customClaudeEnv = { + ...mossProviderEnv, + ...(finalCustomConfig + ? { + ANTHROPIC_AUTH_TOKEN: finalCustomConfig.token, + ANTHROPIC_BASE_URL: finalCustomConfig.baseUrl, + } + : {}), + } + // Build full environment for Claude SDK (includes HOME, PATH, etc.) const claudeEnv = buildClaudeEnv({ - ...(finalCustomConfig && { - customEnv: { - ANTHROPIC_AUTH_TOKEN: finalCustomConfig.token, - ANTHROPIC_BASE_URL: finalCustomConfig.baseUrl, - }, + ...(Object.keys(customClaudeEnv).length > 0 && { + customEnv: customClaudeEnv, }), enableTasks: input.enableTasks ?? true, }) @@ -1399,7 +1457,7 @@ export const claudeRouter = router({ // Build final env - only add OAuth token if we have one AND no existing API config // Existing CLI config takes precedence over OAuth - const finalEnv = { + const finalEnv: Record = { ...claudeEnv, ...(claudeCodeToken && !hasExistingApiConfig && { @@ -1439,8 +1497,11 @@ export const claudeRouter = router({ "[claude-auth] ============================================", ) - // Get bundled Claude binary path - const claudeBinaryPath = getBundledClaudeBinaryPath() + const claudeExecutable = resolveClaudeCodeExecutable() + const claudeBinaryPath = claudeExecutable.path + console.log( + `[claude-binary] Using ${claudeExecutable.source} Claude Code executable: ${claudeBinaryPath}`, + ) const resumeSessionId = input.sessionId || existingSessionId || undefined @@ -1864,19 +1925,19 @@ ${prompt} : "" if (!/\.md$/i.test(filePath)) { return { - behavior: "deny", + behavior: "deny" as const, message: 'Only ".md" files can be modified in plan mode.', } } } else if (toolName == "ExitPlanMode") { return { - behavior: "deny", + behavior: "deny" as const, message: `IMPORTANT: DONT IMPLEMENT THE PLAN UNTIL THE EXPLIT COMMAND. THE PLAN WAS **ONLY** PRESENTED TO USER, FINISH CURRENT MESSAGE AS SOON AS POSSIBLE`, } } else if (PLAN_MODE_BLOCKED_TOOLS.has(toolName)) { return { - behavior: "deny", + behavior: "deny" as const, message: `Tool "${toolName}" blocked in plan mode.`, } } @@ -1937,7 +1998,7 @@ ${prompt} result: errorMessage, } as UIMessageChunk) return { - behavior: "deny", + behavior: "deny" as const, message: errorMessage, } } @@ -1945,6 +2006,12 @@ ${prompt} // Update the tool part with answers result for approved const answers = (response.updatedInput as any)?.answers const answerResult = { answers } + const updatedInput = + response.updatedInput && + typeof response.updatedInput === "object" && + !Array.isArray(response.updatedInput) + ? (response.updatedInput as Record) + : toolInput if (askToolPart) { askToolPart.result = answerResult askToolPart.state = "result" @@ -1956,12 +2023,12 @@ ${prompt} result: answerResult, } as UIMessageChunk) return { - behavior: "allow", - updatedInput: response.updatedInput, + behavior: "allow" as const, + updatedInput, } } return { - behavior: "allow", + behavior: "allow" as const, updatedInput: toolInput, } }, @@ -2006,6 +2073,84 @@ ${prompt} let policyRetryNeeded = false let messageCount = 0 let pendingFinishChunk: UIMessageChunk | null = null + const runtimeMetadataBase = { + cwd: input.cwd, + projectPath: input.projectPath ?? null, + offlineMode: isUsingOllama, + sdkPermissionMode: + input.mode === "plan" ? "plan" : "bypassPermissions", + mcpServerCount: mcpServersFiltered + ? Object.keys(mcpServersFiltered).length + : 0, + historyEnabled, + hasImages: Boolean(input.images?.length), + mossProviderStatus: mossProvider.status, + mossProviderId: mossProvider.providerId ?? null, + mossProviderMode: mossProvider.mode ?? null, + usingMossProviderConfig: isUsingMossProviderConfig, + } + const launchRunId = `claude:${input.subChatId}:${Date.now()}` + const claudeProviderRoute = isUsingMossProviderConfig + ? "moss-provider" + : finalCustomConfig + ? isUsingOllama + ? "ollama-compatible" + : "custom-api" + : "claude-code" + const buildClaudeLaunchPlan = (params: { + nativeSessionId?: string | null + resultSubtype: "running" | "success" | "error" | "cancelled" + error?: string | null + }) => + buildAgentRuntimeLaunchPlan({ + runId: launchRunId, + session: { + subChatId: input.subChatId, + chatId: input.chatId, + engineId: "claude-code", + nativeSessionId: + params.nativeSessionId ?? resumeSessionId ?? null, + modelId: resolvedModel ?? null, + permissionMode: input.mode, + cwd: input.cwd, + projectPath: input.projectPath ?? null, + runtimeConfigDir: isolatedConfigDir, + }, + nativeSessionStrategy: resumeSessionId ? "resume" : "continue", + transport: "claude-code-sdk", + providerRoute: claudeProviderRoute, + projectionStatus: "ready", + resultSubtype: params.resultSubtype, + metadata: { + offlineMode: isUsingOllama, + historyEnabled, + mcpServerCount: mcpServersFiltered + ? Object.keys(mcpServersFiltered).length + : 0, + agentCount: Object.keys(agentsOption).length, + mossProviderStatus: mossProvider.status, + usingMossProviderConfig: isUsingMossProviderConfig, + ...(params.error ? { error: params.error } : {}), + }, + }) + + persistAgentRuntimeSession({ + subChatId: input.subChatId, + engine: "claude-code", + nativeSessionId: resumeSessionId ?? null, + configDir: isolatedConfigDir, + modelId: resolvedModel ?? null, + permissionMode: input.mode, + metadata: { + ...runtimeMetadataBase, + resultSubtype: "running", + }, + launchPlan: buildClaudeLaunchPlan({ + nativeSessionId: resumeSessionId ?? null, + resultSubtype: "running", + }), + updateLegacySessionId: true, + }) // eslint-disable-next-line no-constant-condition while (true) { @@ -2505,7 +2650,7 @@ ${prompt} `[claude] Session not found - clearing invalid sessionId from database`, ) db.update(subChats) - .set({ sessionId: null }) + .set({ sessionId: null, engineSessionId: null }) .where(eq(subChats.id, input.subChatId)) .run() @@ -2622,6 +2767,37 @@ ${prompt} } } + persistAgentRuntimeSession({ + subChatId: input.subChatId, + engine: "claude-code", + nativeSessionId: + metadata.sessionId ?? currentSessionId ?? resumeSessionId ?? null, + configDir: isolatedConfigDir, + modelId: resolvedModel ?? null, + permissionMode: input.mode, + metadata: { + ...runtimeMetadataBase, + resultSubtype: abortController.signal.aborted + ? "cancelled" + : "error", + sdkMessageUuid: metadata.sdkMessageUuid ?? null, + errorCategory, + error: errorContext, + }, + launchPlan: buildClaudeLaunchPlan({ + nativeSessionId: + metadata.sessionId ?? + currentSessionId ?? + resumeSessionId ?? + null, + resultSubtype: abortController.signal.aborted + ? "cancelled" + : "error", + error: errorContext, + }), + updateLegacySessionId: true, + }) + console.log( `[SD] M:END sub=${subId} reason=stream_error cat=${errorCategory} n=${chunkCount} last=${lastChunkType}`, ) @@ -2652,6 +2828,26 @@ ${prompt} console.log( `[SD] M:END sub=${subId} reason=no_response n=${chunkCount}`, ) + persistAgentRuntimeSession({ + subChatId: input.subChatId, + engine: "claude-code", + nativeSessionId: metadata.sessionId ?? resumeSessionId ?? null, + configDir: isolatedConfigDir, + modelId: resolvedModel ?? null, + permissionMode: input.mode, + metadata: { + ...runtimeMetadataBase, + resultSubtype: "error", + sdkMessageUuid: metadata.sdkMessageUuid ?? null, + error: "No response received from Claude", + }, + launchPlan: buildClaudeLaunchPlan({ + nativeSessionId: metadata.sessionId ?? resumeSessionId ?? null, + resultSubtype: "error", + error: "No response received from Claude", + }), + updateLegacySessionId: true, + }) safeEmit({ type: "finish" } as UIMessageChunk) safeComplete() return @@ -2669,6 +2865,28 @@ ${prompt} } const savedSessionId = metadata.sessionId + persistAgentRuntimeSession({ + subChatId: input.subChatId, + engine: "claude-code", + nativeSessionId: savedSessionId ?? null, + configDir: isolatedConfigDir, + modelId: resolvedModel ?? null, + permissionMode: input.mode, + metadata: { + ...runtimeMetadataBase, + resultSubtype: abortController.signal.aborted + ? "cancelled" + : "success", + sdkMessageUuid: metadata.sdkMessageUuid ?? null, + }, + launchPlan: buildClaudeLaunchPlan({ + nativeSessionId: savedSessionId ?? null, + resultSubtype: abortController.signal.aborted + ? "cancelled" + : "success", + }), + updateLegacySessionId: true, + }) if (parts.length > 0) { const assistantMessage = { @@ -2939,7 +3157,7 @@ ${prompt} transport: z.enum(["stdio", "http"]), command: z.string().optional(), args: z.array(z.string()).optional(), - env: z.record(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), url: z.string().url().optional(), authType: z.enum(["none", "oauth", "bearer"]).optional(), bearerToken: z.string().optional(), @@ -3017,7 +3235,7 @@ ${prompt} .optional(), command: z.string().optional(), args: z.array(z.string()).optional(), - env: z.record(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), url: z.string().url().optional(), authType: z.enum(["none", "oauth", "bearer"]).optional(), bearerToken: z.string().optional(), diff --git a/src/main/lib/trpc/routers/codex.ts b/src/main/lib/trpc/routers/codex.ts index 0bc355eb9..f85e0ce40 100644 --- a/src/main/lib/trpc/routers/codex.ts +++ b/src/main/lib/trpc/routers/codex.ts @@ -3,12 +3,12 @@ import { observable } from "@trpc/server/observable" import { streamText } from "ai" import { eq } from "drizzle-orm" import { app } from "electron" -import { spawn, type ChildProcess } from "node:child_process" +import { spawn, spawnSync, type ChildProcess } from "node:child_process" import { createHash } from "node:crypto" import { existsSync } from "node:fs" import { readdir, readFile } from "node:fs/promises" import { homedir } from "node:os" -import { basename, dirname, join, sep } from "node:path" +import { basename, delimiter, dirname, join, sep } from "node:path" import { z } from "zod" import { normalizeCodexAssistantMessage, @@ -16,13 +16,52 @@ import { } from "../../../../shared/codex-tool-normalizer" import { getClaudeShellEnvironment } from "../../claude/env" import { resolveProjectPathFromWorktree } from "../../claude-config" +import { shouldIgnoreMossStoredMessageSessionIds } from "../../agent-runtime/session-actions" +import { + codexJsonlEventToNativeToolEvent, + extractCodexJsonlEventSessionId, + extractCodexJsonlEventText, + isCodexJsonlCommentaryTextEvent, + isCodexJsonlDeltaTextEvent, + isCodexJsonlFinalTextEvent, + isCodexNativeRepeatedFinalText, + parseCodexJsonlEventLine, + reconcileCodexNativeTextAppend, + runCodexExecResumeBridge, + runCodexExecStartBridge, + splitCodexTextForStreamingDeltas, + type CodexExecResumeBridgeResult, + type CodexJsonlEvent, +} from "../../agent-runtime/codex-native-session" +import { + buildNativePartsFromCodexEvents, + getCodexNativePartsRichness, + isCodexJsonlUserEvent, +} from "../../agent-runtime/codex-native-recovery" +import { + createCodexNativeMessagePartsAccumulator, + isCodexNativeRuntimeNoticeText, +} from "../../agent-runtime/codex-native-message-parts" +import { + shouldStartFreshCodexNativeSession, + stripCodexNativeRuntimeNoticeMessages, +} from "../../agent-runtime/codex-native-resume" import { getDatabase, projects as projectsTable, subChats } from "../../db" +import { buildAgentRuntimeLaunchPlan } from "../../agent-runtime/launch-plan" +import { persistAgentRuntimeSession } from "../../agent-runtime/session-store" +import { + getMossProviderSecret, + getMossProviderFingerprint, + resolveMossProviderForEngine, + type ResolvedMossProvider, +} from "../../moss-source" import { fetchMcpTools, fetchMcpToolsStdio, type McpToolInfo, } from "../../mcp-auth" import { publicProcedure, router } from "../index" +import { shouldAttachCodexMcpServerToSession } from "./codex-mcp-session" const imageAttachmentSchema = z.object({ base64Data: z.string(), @@ -30,13 +69,62 @@ const imageAttachmentSchema = z.object({ filename: z.string().optional(), }) +const permissionModeSchema = z.enum([ + "plan", + "agent", + "bypass", + "read-only", + "ask-approval", + "full-access", + "custom", +]) + +const runtimeModelSelectionSchema = z.object({ + instanceId: z.string().min(1), + modelId: z.string().min(1), + options: z.record(z.string(), z.unknown()).optional(), +}) + +type RuntimeModelSelectionInput = z.infer + +function resolveRuntimeModelSelection(input: { + providerInstanceId?: string | null + modelSelection?: RuntimeModelSelectionInput | null + fallbackModelId: string +}): { + providerInstanceId: string | null + modelSelection: RuntimeModelSelectionInput | null +} { + const modelSelection = + input.modelSelection ?? + (input.providerInstanceId + ? { + instanceId: input.providerInstanceId, + modelId: input.fallbackModelId, + } + : null) + return { + providerInstanceId: + input.providerInstanceId ?? modelSelection?.instanceId ?? null, + modelSelection, + } +} + type CodexProviderSession = { provider: ACPProvider cwd: string authFingerprint: string | null + mossProviderFingerprint: string | null mcpFingerprint: string } +type CodexAuthConfig = { + apiKey: string + authMethodId?: "codex-api-key" | "openai-api-key" + baseUrl?: string + providerId?: string +} + type CodexLoginSessionState = | "running" | "success" @@ -53,7 +141,7 @@ type CodexLoginSession = { exitCode: number | null } -type CodexIntegrationState = +export type CodexIntegrationState = | "connected_chatgpt" | "connected_api_key" | "not_logged_in" @@ -66,6 +154,7 @@ type CodexMcpServerForSession = command: string args: string[] env: Array<{ name: string; value: string }> + cwd?: string } | { name: string @@ -80,6 +169,17 @@ type CodexMcpServerForSettings = { tools: McpToolInfo[] needsAuth: boolean config: Record + serverInfo?: { + name: string + version: string + icons?: Array<{ + src: string + mimeType?: string + sizes?: string[] + theme?: "light" | "dark" + }> + } + error?: string } type CodexMcpSnapshot = { @@ -108,6 +208,15 @@ export function hasActiveCodexStreams(): boolean { return activeStreams.size > 0 } +export function hasActiveCodexStreamForSubChat( + subChatId: string, + runId?: string | null, +): boolean { + const stream = activeStreams.get(subChatId) + if (!stream) return false + return !runId || stream.runId === runId +} + /** Abort all active Codex streams so their cleanup saves partial state */ export function abortAllCodexStreams(): void { for (const [subChatId, stream] of activeStreams) { @@ -136,10 +245,16 @@ const AUTH_HINTS = [ "401", "403", ] -const DEFAULT_CODEX_MODEL = "gpt-5.3-codex/high" +const DEFAULT_CODEX_MODEL = "gpt-5.5/medium" +const MIN_DEFAULT_CODEX_CLI_VERSION = "0.133.0" const CODEX_MCP_TOOLS_FETCH_TIMEOUT_MS = 40_000 const CODEX_USAGE_POLL_ATTEMPTS = 3 const CODEX_USAGE_POLL_INTERVAL_MS = 200 +const NATIVE_TEXT_REPLAY_CHUNK_LENGTH = 12 +const NATIVE_TEXT_REPLAY_MIN_INTERVAL_MS = 16 +const NATIVE_TEXT_REPLAY_MAX_INTERVAL_MS = 50 +const NATIVE_TEXT_REPLAY_MAX_DURATION_MS = 2400 +let loggedCodexCliPath: string | null = null type CodexTokenUsage = { input_tokens?: number @@ -170,13 +285,13 @@ const codexMcpListEntrySchema = z type: z.string(), command: z.string().nullable().optional(), args: z.array(z.string()).nullable().optional(), - env: z.record(z.string()).nullable().optional(), + env: z.record(z.string(), z.string()).nullable().optional(), env_vars: z.array(z.string()).nullable().optional(), cwd: z.string().nullable().optional(), url: z.string().nullable().optional(), bearer_token_env_var: z.string().nullable().optional(), - http_headers: z.record(z.string()).nullable().optional(), - env_http_headers: z.record(z.string()).nullable().optional(), + http_headers: z.record(z.string(), z.string()).nullable().optional(), + env_http_headers: z.record(z.string(), z.string()).nullable().optional(), }) .passthrough(), auth_status: z.string().nullable().optional(), @@ -234,7 +349,7 @@ function resolveCodexAcpBinaryPath(): string { return toUnpackedAsarPath(resolvedPath) } -function resolveBundledCodexCliPath(): string { +function getBundledCodexCliPath(): string { const binaryName = process.platform === "win32" ? "codex.exe" : "codex" const resourcesDir = app.isPackaged ? join(process.resourcesPath, "bin") @@ -245,20 +360,141 @@ function resolveBundledCodexCliPath(): string { `${process.platform}-${process.arch}`, ) - const binaryPath = join(resourcesDir, binaryName) - if (existsSync(binaryPath)) { - return binaryPath + return join(resourcesDir, binaryName) +} + +function parseCodexCliVersion(output: string): string | null { + const match = output.match(/\bcodex-cli\s+(\d+\.\d+\.\d+)\b/i) + return match?.[1] ?? null +} + +function compareSemver(a: string | null, b: string | null): number { + if (!a && !b) return 0 + if (!a) return -1 + if (!b) return 1 + + const aParts = a.split(".").map((part) => Number.parseInt(part, 10) || 0) + const bParts = b.split(".").map((part) => Number.parseInt(part, 10) || 0) + const length = Math.max(aParts.length, bParts.length) + + for (let index = 0; index < length; index += 1) { + const diff = (aParts[index] ?? 0) - (bParts[index] ?? 0) + if (diff !== 0) return diff + } + + return 0 +} + +function getCodexCliVersion(binaryPath: string): string | null { + const result = spawnSync(binaryPath, ["--version"], { + stdio: ["ignore", "pipe", "pipe"], + env: process.env, + encoding: "utf8", + timeout: 5_000, + windowsHide: true, + }) + + if (result.error || result.status !== 0) { + return null + } + + return parseCodexCliVersion(`${result.stdout}\n${result.stderr}`) +} + +function getSelectedCodexCliVersion(): string | null { + try { + return getCodexCliVersion(resolveCodexCliPath()) + } catch { + return null } +} - const hint = app.isPackaged - ? "Binary is missing from bundled resources." - : "Run `bun run codex:download` to download it for local dev." +function getSystemCodexCliCandidates(): string[] { + const binaryName = process.platform === "win32" ? "codex.exe" : "codex" + const candidates = [ + process.env.MOSS_CODEX_CLI_PATH, + process.env.CODEX_CLI_PATH, + join(homedir(), ".local", "bin", binaryName), + ] + + if (process.platform === "darwin") { + candidates.push( + join("/opt/homebrew/bin", binaryName), + join("/usr/local/bin", binaryName), + ) + } - throw new Error( - `[codex] Bundled Codex CLI not found at ${binaryPath}. ${hint}`, + for (const pathEntry of process.env.PATH?.split(delimiter) ?? []) { + if (pathEntry.trim().length > 0) { + candidates.push(join(pathEntry, binaryName)) + } + } + + return Array.from( + new Set(candidates.filter((candidate): candidate is string => Boolean(candidate))), ) } +function resolveCodexCliPath(): string { + const bundledPath = getBundledCodexCliPath() + const explicitPath = process.env.MOSS_CODEX_CLI_PATH || process.env.CODEX_CLI_PATH + + if (explicitPath) { + if (!existsSync(explicitPath)) { + throw new Error(`[codex] Configured Codex CLI not found at ${explicitPath}.`) + } + + return explicitPath + } + + const candidates = [ + bundledPath, + ...getSystemCodexCliCandidates().filter((candidate) => candidate !== bundledPath), + ] + .filter((candidate) => existsSync(candidate)) + .map((path) => ({ + path, + isBundled: path === bundledPath, + version: getCodexCliVersion(path), + })) + + if (candidates.length === 0) { + const hint = app.isPackaged + ? "Binary is missing from bundled resources." + : "Run `bun run codex:download` to download it for local dev or install a current `codex` CLI." + + throw new Error( + `[codex] Codex CLI not found at ${bundledPath} or on PATH. ${hint}`, + ) + } + + const selected = candidates.reduce((best, candidate) => { + const versionDiff = compareSemver(candidate.version, best.version) + if (versionDiff > 0) return candidate + if (versionDiff === 0 && best.isBundled && !candidate.isBundled) return candidate + return best + }) + + if ( + loggedCodexCliPath !== selected.path && + selected.version && + compareSemver(selected.version, MIN_DEFAULT_CODEX_CLI_VERSION) < 0 + ) { + console.warn( + `[codex] Using Codex CLI ${selected.version} at ${selected.path}; default model ${DEFAULT_CODEX_MODEL} expects ${MIN_DEFAULT_CODEX_CLI_VERSION} or newer.`, + ) + } + + if (loggedCodexCliPath !== selected.path && !selected.isBundled) { + console.info( + `[codex] Using Codex CLI ${selected.version ?? "unknown"} at ${selected.path}.`, + ) + } + loggedCodexCliPath = selected.path + + return selected.path +} + function stripAnsi(input: string): string { return input.replace(ANSI_OSC_REGEX, "").replace(ANSI_ESCAPE_REGEX, "") } @@ -360,7 +596,7 @@ async function runCodexCli( stderr: string exitCode: number | null }> { - const codexCliPath = resolveBundledCodexCliPath() + const codexCliPath = resolveCodexCliPath() const cwd = options?.cwd?.trim() return await new Promise((resolvePromise, rejectPromise) => { @@ -501,6 +737,155 @@ async function findSessionFileById(sessionId: string): Promise { return null } +async function readSessionEventsForCurrentRun( + sessionId: string, + options: { notBeforeTimestampMs: number }, +): Promise { + const sessionFile = await findSessionFileById(sessionId) + if (!sessionFile) return [] + + let rawContent = "" + try { + rawContent = await readFile(sessionFile, "utf8") + } catch { + return [] + } + + return parseSessionEventsForCurrentRun(rawContent, options) +} + +function parseSessionEventsForCurrentRun( + rawContent: string, + options: { notBeforeTimestampMs: number }, +): CodexJsonlEvent[] { + const events: CodexJsonlEvent[] = [] + const notBeforeTimestampMs = Math.max(0, options.notBeforeTimestampMs - 2_000) + for (const line of rawContent.split("\n")) { + const event = parseCodexJsonlEventLine(line) + if (!event) continue + + const timestampMs = toTimestampMs((event as any).timestamp) + if (timestampMs !== undefined && timestampMs < notBeforeTimestampMs) { + continue + } + events.push(event) + } + + return events +} + +type NativeSessionEventTailer = { + start: (sessionId: string | null | undefined) => void + stop: () => Promise + getForwardedCount: () => number +} + +function createNativeSessionEventTailer(options: { + notBeforeTimestampMs: number + onEvent: (event: CodexJsonlEvent) => void + intervalMs?: number +}): NativeSessionEventTailer { + const intervalMs = options.intervalMs ?? 250 + const forwardedEventHashes = new Set() + let currentSessionId: string | null = null + let currentSessionFile: string | null = null + let stopped = false + let polling = false + let pendingPoll = false + let timer: ReturnType | null = null + let forwardedCount = 0 + + const getEventHash = (event: CodexJsonlEvent): string => { + try { + return createHash("sha1").update(JSON.stringify(event)).digest("hex") + } catch { + return createHash("sha1").update(String(event)).digest("hex") + } + } + + const poll = async (): Promise => { + if (!currentSessionId) return 0 + if (polling) { + pendingPoll = true + return 0 + } + + polling = true + let forwardedInPoll = 0 + try { + currentSessionFile = + currentSessionFile ?? (await findSessionFileById(currentSessionId)) + if (!currentSessionFile) { + return 0 + } + + let rawContent = "" + try { + rawContent = await readFile(currentSessionFile, "utf8") + } catch { + return 0 + } + + const events = parseSessionEventsForCurrentRun(rawContent, { + notBeforeTimestampMs: options.notBeforeTimestampMs, + }) + for (const event of events) { + const eventHash = getEventHash(event) + if (forwardedEventHashes.has(eventHash)) continue + forwardedEventHashes.add(eventHash) + forwardedInPoll += 1 + forwardedCount += 1 + try { + options.onEvent(event) + } catch (error) { + console.warn("[codex] Ignoring tailed native JSONL event:", error) + } + } + } finally { + polling = false + } + + if (pendingPoll && !stopped) { + pendingPoll = false + forwardedInPoll += await poll() + } + return forwardedInPoll + } + + const schedule = () => { + if (stopped || !currentSessionId || timer) return + timer = setTimeout(() => { + timer = null + void poll().finally(schedule) + }, intervalMs) + } + + return { + start(sessionId) { + const cleanedSessionId = sessionId?.trim() + if (!cleanedSessionId || stopped) return + if (currentSessionId === cleanedSessionId && timer) return + if (currentSessionId !== cleanedSessionId) { + currentSessionFile = null + } + currentSessionId = cleanedSessionId + void poll() + schedule() + }, + async stop() { + stopped = true + if (timer) { + clearTimeout(timer) + timer = null + } + return poll() + }, + getForwardedCount() { + return forwardedCount + }, + } +} + async function readLatestTokenCountInfo( filePath: string, options?: { notBeforeTimestampMs?: number }, @@ -607,6 +992,39 @@ function mapToUsageMetadata(info: CodexTokenCountInfo): CodexUsageMetadata | nul return Object.keys(usageMetadata).length > 0 ? usageMetadata : null } +function mapNativeUsageMetadata( + usage: Record | undefined, +): CodexUsageMetadata | null { + if (!usage) return null + + const rawInputTokens = + toNonNegativeInt(usage.input_tokens) ?? toNonNegativeInt(usage.inputTokens) + const rawCachedInputTokens = + toNonNegativeInt(usage.cached_input_tokens) ?? + toNonNegativeInt(usage.cachedInputTokens) + const outputTokens = + toNonNegativeInt(usage.output_tokens) ?? toNonNegativeInt(usage.outputTokens) + const totalTokens = + toNonNegativeInt(usage.total_tokens) ?? toNonNegativeInt(usage.totalTokens) + const modelContextWindow = + toNonNegativeInt(usage.model_context_window) ?? + toNonNegativeInt(usage.modelContextWindow) + const inputTokens = + rawInputTokens !== undefined + ? Math.max(0, rawInputTokens - (rawCachedInputTokens ?? 0)) + : undefined + + const usageMetadata: CodexUsageMetadata = {} + if (inputTokens !== undefined) usageMetadata.inputTokens = inputTokens + if (outputTokens !== undefined) usageMetadata.outputTokens = outputTokens + if (totalTokens !== undefined) usageMetadata.totalTokens = totalTokens + if (modelContextWindow !== undefined) { + usageMetadata.modelContextWindow = modelContextWindow + } + + return Object.keys(usageMetadata).length > 0 ? usageMetadata : null +} + async function pollUsage( sessionId: string, options?: { notBeforeTimestampMs?: number }, @@ -745,7 +1163,10 @@ function normalizeCodexTools(tools: McpToolInfo[]): McpToolInfo[] { return [...unique.values()] } -async function fetchCodexMcpTools(entry: CodexMcpListEntry): Promise { +async function fetchCodexMcpTools( + entry: CodexMcpListEntry, + sourcePath?: string | null, +): Promise { const transportType = entry.transport.type.trim().toLowerCase() const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), CODEX_MCP_TOOLS_FETCH_TIMEOUT_MS), @@ -759,6 +1180,8 @@ async function fetchCodexMcpTools(entry: CodexMcpListEntry): Promise 0 ? trimmed : undefined +} + function getCodexMcpFingerprint(servers: CodexMcpServerForSession[]): string { return createHash("sha256").update(JSON.stringify(servers)).digest("hex") } @@ -829,6 +1259,7 @@ async function resolveCodexMcpSnapshot(params: { const includeInSession = entry.enabled const resolvedStdioEnv = resolveCodexStdioEnv(entry.transport) const resolvedHttpHeaders = resolveCodexHttpHeaders(entry.transport) + const transportCwd = normalizeCodexTransportCwd(entry.transport.cwd) let status: CodexMcpServerForSettings["status"] = !entry.enabled ? "failed" : authState.needsAuth @@ -854,11 +1285,13 @@ async function resolveCodexMcpSnapshot(params: { command, args: Array.isArray(args) ? args : [], env: envPairs, + ...(transportCwd ? { cwd: transportCwd } : {}), } } settingsConfig.command = command settingsConfig.args = args + settingsConfig.cwd = transportCwd settingsConfig.env = entry.transport.env || undefined settingsConfig.envVars = entry.transport.env_vars || undefined } else if ( @@ -895,7 +1328,12 @@ async function resolveCodexMcpSnapshot(params: { // For auth-capable HTTP, only probe if explicit auth header is available. Boolean(resolvedHttpHeaders?.Authorization) ) - const tools = shouldProbeTools ? await fetchCodexMcpTools(entry) : [] + const tools = shouldProbeTools + ? await fetchCodexMcpTools( + entry, + lookupPath === "__global__" ? undefined : lookupPath, + ) + : [] if (shouldProbeTools && tools.length === 0) { status = "failed" } @@ -914,8 +1352,16 @@ async function resolveCodexMcpSnapshot(params: { ) for (const converted of convertedEntries) { - if (converted.sessionServer) { - mcpServersForSession.push(converted.sessionServer) + const { sessionServer } = converted + if ( + sessionServer && + shouldAttachCodexMcpServerToSession({ + sessionServer, + settingsServer: converted.settingsServer, + toolsWereResolved: shouldIncludeTools, + }) + ) { + mcpServersForSession.push(sessionServer) } mcpServersForSettings.push(converted.settingsServer) } @@ -952,6 +1398,7 @@ function getCodexServerIdentity( transportType: config.transportType ?? null, command: config.command ?? null, args: config.args ?? null, + cwd: config.cwd ?? null, env: config.env ?? null, envVars: config.envVars ?? null, url: config.url ?? null, @@ -1047,6 +1494,30 @@ function normalizeCodexIntegrationState(rawOutput: string): CodexIntegrationStat return "unknown" } +export async function getCodexIntegrationStatus(): Promise<{ + state: CodexIntegrationState + isConnected: boolean + rawOutput: string + exitCode: number | null + version: string | null +}> { + const result = await runCodexCli(["login", "status"]) + const combinedOutput = [result.stdout, result.stderr] + .filter((chunk) => chunk.trim().length > 0) + .join("\n") + .trim() + const state = normalizeCodexIntegrationState(combinedOutput) + + return { + state, + isConnected: + state === "connected_chatgpt" || state === "connected_api_key", + rawOutput: combinedOutput, + exitCode: result.exitCode, + version: getSelectedCodexCliVersion(), + } +} + function parseStoredMessages(raw: string | null | undefined): any[] { if (!raw) return [] try { @@ -1100,7 +1571,7 @@ function extractCodexModelId(rawModel: unknown): string | undefined { function preprocessCodexModelName(params: { modelId: string - authConfig?: { apiKey: string } + authConfig?: CodexAuthConfig }): string { const hasAppManagedApiKey = Boolean(params.authConfig?.apiKey?.trim()) if (!hasAppManagedApiKey) { @@ -1111,13 +1582,25 @@ function preprocessCodexModelName(params: { return params.modelId } -function getAuthFingerprint(authConfig?: { apiKey: string }): string | null { +function getAuthFingerprint(authConfig?: CodexAuthConfig): string | null { const apiKey = authConfig?.apiKey?.trim() - if (!apiKey) return null - return createHash("sha256").update(apiKey).digest("hex") + if (!apiKey && !authConfig?.baseUrl && !authConfig?.authMethodId) return null + return createHash("sha256") + .update( + JSON.stringify({ + apiKey, + baseUrl: authConfig?.baseUrl, + authMethodId: authConfig?.authMethodId, + providerId: authConfig?.providerId, + }), + ) + .digest("hex") } -function buildCodexProviderEnv(authConfig?: { apiKey: string }): Record { +function buildCodexProviderEnv(params?: { + authConfig?: CodexAuthConfig + mossProvider?: ResolvedMossProvider | null +}): Record { // Prefer shell-derived values (notably PATH) so stdio MCP dependencies // like pipx/npx resolve the same way as in MCP tool probing. const env: Record = {} @@ -1135,25 +1618,42 @@ function buildCodexProviderEnv(authConfig?: { apiKey: string }): Record +}): any { + const parts = [...(params.parts ?? [])] + if (params.text) { + parts.push({ + type: "text", + text: params.text, + state: "done", + }) + } + + return { + id: params.id ?? crypto.randomUUID(), + role: "assistant", + parts, + metadata: params.metadata, + } +} + +function emitNativeCodexUiChunks(params: { + result: CodexExecResumeBridgeResult + metadata: Record + messageId: string + emit: (chunk: any) => void +}): void { + const textPartId = crypto.randomUUID() + const text = params.result.lastText || "" + + params.emit({ + type: "start", + messageId: params.messageId, + messageMetadata: params.metadata, + }) + params.emit({ type: "start-step" }) + + if (text) { + params.emit({ type: "text-start", id: textPartId }) + params.emit({ type: "text-delta", id: textPartId, delta: text }) + params.emit({ type: "text-end", id: textPartId }) + } + + params.emit({ type: "finish-step" }) + params.emit({ + type: "finish", + finishReason: params.result.success ? "stop" : "error", + messageMetadata: params.metadata, + }) +} + function getOrCreateProvider(params: { subChatId: string cwd: string mcpServers: CodexMcpServerForSession[] mcpFingerprint: string existingSessionId?: string - authConfig?: { - apiKey: string - } + authConfig?: CodexAuthConfig + mossProvider?: ResolvedMossProvider | null }): ACPProvider { const authFingerprint = getAuthFingerprint(params.authConfig) + const mossProviderFingerprint = getMossProviderFingerprint(params.mossProvider) const existing = providerSessions.get(params.subChatId) if ( existing && existing.cwd === params.cwd && existing.authFingerprint === authFingerprint && + existing.mossProviderFingerprint === mossProviderFingerprint && existing.mcpFingerprint === params.mcpFingerprint ) { return existing.provider @@ -1254,7 +1828,10 @@ function getOrCreateProvider(params: { const provider = createACPProvider({ command: resolveCodexAcpBinaryPath(), - env: buildCodexProviderEnv(params.authConfig), + env: buildCodexProviderEnv({ + authConfig: params.authConfig, + mossProvider: params.mossProvider, + }), authMethodId: getCodexAuthMethodId(params.authConfig), session: { cwd: params.cwd, @@ -1270,6 +1847,7 @@ function getOrCreateProvider(params: { provider, cwd: params.cwd, authFingerprint, + mossProviderFingerprint, mcpFingerprint: params.mcpFingerprint, }) @@ -1286,21 +1864,7 @@ function cleanupProvider(subChatId: string): void { export const codexRouter = router({ getIntegration: publicProcedure.query(async () => { - const result = await runCodexCli(["login", "status"]) - const combinedOutput = [result.stdout, result.stderr] - .filter((chunk) => chunk.trim().length > 0) - .join("\n") - .trim() - - const state = normalizeCodexIntegrationState(combinedOutput) - - return { - state, - isConnected: - state === "connected_chatgpt" || state === "connected_api_key", - rawOutput: combinedOutput, - exitCode: result.exitCode, - } + return getCodexIntegrationStatus() }), logout: publicProcedure.mutation(async () => { @@ -1341,7 +1905,7 @@ export const codexRouter = router({ return toLoginSessionResponse(existingSession) } - const codexCliPath = resolveBundledCodexCliPath() + const codexCliPath = resolveCodexCliPath() const sessionId = crypto.randomUUID() const child = spawn(codexCliPath, ["login"], { @@ -1563,21 +2127,28 @@ export const codexRouter = router({ runId: z.string(), prompt: z.string(), model: z.string().optional(), + providerInstanceId: z.string().min(1).optional(), + modelSelection: runtimeModelSelectionSchema.optional(), cwd: z.string(), projectPath: z.string().optional(), mode: z.enum(["plan", "agent"]).default("agent"), + permissionMode: permissionModeSchema.optional(), sessionId: z.string().optional(), forceNewSession: z.boolean().optional(), images: z.array(imageAttachmentSchema).optional(), authConfig: z .object({ apiKey: z.string().min(1), + authMethodId: z.enum(["codex-api-key", "openai-api-key"]).optional(), + baseUrl: z.string().min(1).optional(), + providerId: z.string().min(1).optional(), }) .optional(), }), ) .subscription(({ input }) => { return observable((emit) => { + const runtimePermissionMode = input.permissionMode ?? input.mode const existingStream = activeStreams.get(input.subChatId) if (existingStream) { existingStream.cancelRequested = true @@ -1594,6 +2165,8 @@ export const codexRouter = router({ }) let isActive = true + let clearActiveStreamId: (() => void) | null = null + let latestKnownCodexSessionId: string | null = input.sessionId ?? null const safeEmit = (chunk: any) => { if (!isActive) return @@ -1628,14 +2201,67 @@ export const codexRouter = router({ throw new Error("Sub-chat not found") } - const existingMessages = parseStoredMessages(existingSubChat.messages) + const parsedExistingMessages = parseStoredMessages(existingSubChat.messages) + const nativeMessageHygiene = + stripCodexNativeRuntimeNoticeMessages(parsedExistingMessages) + const existingMessages = nativeMessageHygiene.messages as any[] + const ignoreStoredSessionIds = shouldIgnoreMossStoredMessageSessionIds( + existingSubChat.runtimeMetadata, + ) + const existingRunSessionIdCandidate = input.forceNewSession + ? undefined + : (ignoreStoredSessionIds ? undefined : input.sessionId) ?? + existingSubChat.engineSessionId ?? + (ignoreStoredSessionIds + ? undefined + : getLastSessionId(existingMessages)) + const nativeResumeDecision = shouldStartFreshCodexNativeSession({ + storedMessagesByteLength: Buffer.byteLength( + existingSubChat.messages ?? "", + "utf8", + ), + runtimeMetadata: existingSubChat.runtimeMetadata, + candidateSessionId: existingRunSessionIdCandidate, + forceNewSession: input.forceNewSession, + messages: existingMessages, + }) + const existingRunSessionId = nativeResumeDecision.startFresh + ? undefined + : existingRunSessionIdCandidate + latestKnownCodexSessionId = existingRunSessionId ?? null + const requestedModelIdFromInput = extractCodexModelId(input.model) + const resolvedProjectPathFromCwd = resolveProjectPathFromWorktree( + input.cwd, + ) + const providerLookupPath = + input.projectPath || resolvedProjectPathFromCwd || input.cwd + const mossProvider = await resolveMossProviderForEngine({ + projectPath: providerLookupPath, + engineId: "codex", + requestedModelId: requestedModelIdFromInput, + createIfMissing: true, + secretResolver: { getSecret: getMossProviderSecret }, + }) + if (mossProvider.warnings.length > 0) { + console.warn("[codex] Moss provider warnings:", mossProvider.warnings) + } + const effectiveAuthConfig = + input.authConfig || buildCodexAuthConfigFromMossProvider(mossProvider) const requestedModelId = - extractCodexModelId(input.model) || DEFAULT_CODEX_MODEL + requestedModelIdFromInput || mossProvider.model || DEFAULT_CODEX_MODEL const selectedModelId = preprocessCodexModelName({ modelId: requestedModelId, - authConfig: input.authConfig, + authConfig: effectiveAuthConfig, }) const metadataModel = selectedModelId + const { + providerInstanceId, + modelSelection: runtimeModelSelection, + } = resolveRuntimeModelSelection({ + providerInstanceId: input.providerInstanceId ?? null, + modelSelection: input.modelSelection ?? null, + fallbackModelId: selectedModelId, + }) const lastMessage = existingMessages[existingMessages.length - 1] const isDuplicatePrompt = @@ -1663,15 +2289,66 @@ export const codexRouter = router({ return true } - const cleanAssistantMessageForPersistence = (message: any) => { - if (!message || message.role !== "assistant") return message - if (!Array.isArray(message.parts)) return message - - const cleanedParts = message.parts.filter( - (part: any) => part?.state !== "input-streaming", - ) + const persistSubChatStreamId = (streamId: string | null) => { + if (!isAuthoritativeRun()) { + return false + } - if (cleanedParts.length === 0) { + db.update(subChats) + .set({ + streamId, + updatedAt: new Date(), + }) + .where(eq(subChats.id, input.subChatId)) + .run() + return true + } + clearActiveStreamId = () => { + persistSubChatStreamId(null) + } + + const shouldDedupeCodexNativeTextParts = (message: any) => + message?.metadata?.transport === "codex-native-exec" || + typeof message?.metadata?.nativeBridge === "string" + + const getTextPartDedupeKey = (text: unknown) => { + if (typeof text !== "string") return null + const trimmed = text.trim() + return trimmed ? trimmed.replace(/\r\n/g, "\n") : null + } + + const dedupeCodexNativeTextParts = ( + message: any, + parts: any[], + ) => { + if (!shouldDedupeCodexNativeTextParts(message)) return parts + const seenTextParts = new Set() + const dedupedParts: any[] = [] + for (const part of parts) { + if (part?.type !== "text") { + dedupedParts.push(part) + continue + } + const textKey = getTextPartDedupeKey(part.text) + if (textKey && seenTextParts.has(textKey)) continue + if (textKey) seenTextParts.add(textKey) + dedupedParts.push(part) + } + return dedupedParts + } + + const cleanAssistantMessageForPersistence = (message: any) => { + if (!message || message.role !== "assistant") return message + if (!Array.isArray(message.parts)) return message + + const cleanedParts = dedupeCodexNativeTextParts( + message, + message.parts.filter( + (part: any) => part?.state !== "input-streaming", + ), + ) + + if (cleanedParts.length === 0) { return null } @@ -1698,10 +2375,19 @@ export const codexRouter = router({ db.update(subChats) .set({ messages: JSON.stringify(messagesForStream), + streamId: input.runId, updatedAt: new Date(), }) .where(eq(subChats.id, input.subChatId)) .run() + } else { + if ( + nativeMessageHygiene.removedCount > 0 || + nativeMessageHygiene.removedPartCount > 0 + ) { + persistSubChatMessages(messagesForStream) + } + persistSubChatStreamId(input.runId) } if (input.forceNewSession) { @@ -1716,36 +2402,905 @@ export const codexRouter = router({ toolsResolved: false, } try { - const resolvedProjectPathFromCwd = resolveProjectPathFromWorktree( - input.cwd, - ) const mcpLookupPath = input.projectPath || resolvedProjectPathFromCwd || input.cwd mcpSnapshot = await resolveCodexMcpSnapshot({ lookupPath: mcpLookupPath, + includeTools: true, }) } catch (mcpError) { console.error("[codex] Failed to resolve MCP servers:", mcpError) } + const providerRoute = input.authConfig + ? `api-key:${input.authConfig.authMethodId ?? "codex-api-key"}` + : effectiveAuthConfig + ? "moss-provider" + : "codex-cli" + const buildCodexLaunchPlan = (params: { + nativeSessionId?: string | null + nativeSessionStrategy?: "start" | "resume" + transport: string + resultSubtype: "running" | "success" | "error" | "cancelled" + nativeBridge?: string | null + error?: string | null + }) => + buildAgentRuntimeLaunchPlan({ + runId: input.runId, + session: { + subChatId: input.subChatId, + chatId: input.chatId, + engineId: "codex", + providerInstanceId, + nativeSessionId: params.nativeSessionId ?? null, + modelId: metadataModel, + modelSelection: runtimeModelSelection, + permissionMode: runtimePermissionMode, + cwd: input.cwd, + projectPath: input.projectPath ?? null, + runtimeConfigDir: join(homedir(), ".codex"), + }, + nativeSessionStrategy: params.nativeSessionStrategy, + transport: params.transport, + providerRoute, + mcpFingerprint: mcpSnapshot.fingerprint, + resultSubtype: params.resultSubtype, + metadata: { + mossProviderStatus: mossProvider.status, + mossProviderId: mossProvider.providerId ?? null, + forceNewSession: Boolean(input.forceNewSession), + ...(params.nativeBridge + ? { nativeBridge: params.nativeBridge } + : {}), + ...(params.error ? { error: params.error } : {}), + }, + }) + + if (shouldUseNativeCodexExec()) { + const startedAt = Date.now() + const responseMessageId = crypto.randomUUID() + let didEmitNativeStart = false + let didEmitNativeStartStep = false + let didEmitNativeFinishStep = false + let activeNativeTextPartId: string | null = null + let nativeTextSequence = 0 + let nativeFinalTextPartId: string | null = null + let didEmitNativeFinalTextStart = false + let emittedNativeText = "" + let emittedNativeFinalText = "" + let latestNativeSessionId = existingRunSessionId ?? null + let pendingNativeSnapshotTimer: ReturnType | null = + null + let lastNativeSnapshotPersistedAt = 0 + let lastNativeSnapshotKey = "" + let nativeVisualTextQueue: Promise = Promise.resolve() + const handledNativeEventHashes = new Set() + const nativeMessageParts = + createCodexNativeMessagePartsAccumulator() + const runningNativeBridge = existingRunSessionId + ? "codex-exec-resume" + : "codex-exec-start" + let nativeSessionEventTailer: NativeSessionEventTailer | null = null + let tailedNativeSessionEventCount = 0 + const nativeRuntimeNoticeCleanupMetadata: Record = { + ...(nativeMessageHygiene.removedCount > 0 + ? { + nativeRuntimeNoticeMessagesRemoved: + nativeMessageHygiene.removedCount, + } + : {}), + ...(nativeMessageHygiene.removedPartCount > 0 + ? { + nativeRuntimeNoticePartsRemoved: + nativeMessageHygiene.removedPartCount, + } + : {}), + } + + const buildNativeResponseMetadata = ( + resultSubtype: "running" | "success" | "error" | "cancelled", + options?: { + durationMs?: number + nativeBridge?: string + imageCount?: number + usageMetadata?: CodexUsageMetadata | null + error?: string | null + }, + ): Record => ({ + model: metadataModel, + sessionId: latestNativeSessionId ?? undefined, + durationMs: options?.durationMs ?? Date.now() - startedAt, + resultSubtype, + nativeBridge: options?.nativeBridge ?? runningNativeBridge, + transport: "codex-native-exec", + imageCount: options?.imageCount ?? input.images?.length ?? 0, + ...(nativeResumeDecision.reason + ? { nativeResumeSkipReason: nativeResumeDecision.reason } + : {}), + ...nativeRuntimeNoticeCleanupMetadata, + ...(options?.usageMetadata || {}), + ...(options?.error ? { error: options.error } : {}), + }) + + const persistNativeRuntimeSession = ( + resultSubtype: "running" | "success" | "error" | "cancelled", + options?: { + nativeBridge?: string + imageCount?: number + eventCount?: number + exitCode?: number | null + error?: string | null + }, + ) => { + if (!isAuthoritativeRun()) return false + + persistAgentRuntimeSession({ + subChatId: input.subChatId, + engine: "codex", + nativeSessionId: latestNativeSessionId, + configDir: join(homedir(), ".codex"), + providerInstanceId, + modelId: metadataModel, + modelSelection: runtimeModelSelection, + permissionMode: runtimePermissionMode, + metadata: { + cwd: input.cwd, + projectPath: input.projectPath ?? null, + transport: "codex-native-exec", + nativeBridge: options?.nativeBridge ?? runningNativeBridge, + authMode: input.authConfig + ? "api-key" + : effectiveAuthConfig + ? "moss-provider" + : "codex-cli", + mossProviderStatus: mossProvider.status, + mossProviderId: mossProvider.providerId ?? null, + mossProviderMode: mossProvider.mode ?? null, + forceNewSession: Boolean(input.forceNewSession), + ignoredStoredSessionIds: ignoreStoredSessionIds, + ...(nativeResumeDecision.reason + ? { nativeResumeSkipReason: nativeResumeDecision.reason } + : {}), + ...nativeRuntimeNoticeCleanupMetadata, + mcpFingerprint: mcpSnapshot.fingerprint, + mcpFetchedAt: mcpSnapshot.fetchedAt, + mcpToolsResolved: mcpSnapshot.toolsResolved, + mcpServerCount: mcpSnapshot.mcpServersForSession.length, + hasImages: Boolean(input.images?.length), + imageCount: options?.imageCount ?? input.images?.length ?? 0, + resultSubtype, + ...(typeof options?.eventCount === "number" + ? { eventCount: options.eventCount } + : {}), + ...(options?.exitCode !== undefined + ? { exitCode: options.exitCode } + : {}), + ...(options?.error ? { error: options.error } : {}), + }, + launchPlan: buildCodexLaunchPlan({ + nativeSessionId: latestNativeSessionId, + nativeSessionStrategy: existingRunSessionId ? "resume" : "start", + transport: "codex-native-exec", + nativeBridge: options?.nativeBridge ?? runningNativeBridge, + resultSubtype, + error: options?.error ?? null, + }), + }) + return true + } + + persistNativeRuntimeSession("running") + + const persistNativeSnapshot = ( + resultSubtype: "running" | "success" | "error" | "cancelled" = + "running", + options?: { + force?: boolean + nativeBridge?: string + imageCount?: number + usageMetadata?: CodexUsageMetadata | null + error?: string | null + }, + ) => { + if (nativeMessageParts.parts.length === 0) { + return false + } + + const snapshotKey = JSON.stringify({ + resultSubtype, + sessionId: latestNativeSessionId, + textLength: emittedNativeText.length, + parts: nativeMessageParts.parts.map((part) => ({ + type: part.type, + id: part.toolCallId, + state: part.state, + textLength: + typeof part.text === "string" ? part.text.length : undefined, + hasResult: part.result !== undefined || part.output !== undefined, + })), + tools: nativeMessageParts.toolParts.map((part) => ({ + type: part.type, + id: part.toolCallId, + state: part.state, + textLength: + typeof part.text === "string" ? part.text.length : undefined, + hasResult: part.result !== undefined || part.output !== undefined, + })), + }) + if (!options?.force && snapshotKey === lastNativeSnapshotKey) { + return false + } + + const responseMessage = buildNativeCodexAssistantMessage({ + id: responseMessageId, + parts: nativeMessageParts.parts, + metadata: buildNativeResponseMetadata(resultSubtype, options), + }) + const cleanedResponseMessage = + cleanAssistantMessageForPersistence(responseMessage) + const messagesToPersist = cleanedResponseMessage + ? [...messagesForStream, cleanedResponseMessage] + : messagesForStream + + if (persistSubChatMessages(messagesToPersist)) { + lastNativeSnapshotKey = snapshotKey + lastNativeSnapshotPersistedAt = Date.now() + return true + } + return false + } + + const flushNativeSnapshot = ( + resultSubtype: "running" | "success" | "error" | "cancelled" = + "running", + options?: Parameters[1], + ) => { + if (pendingNativeSnapshotTimer) { + clearTimeout(pendingNativeSnapshotTimer) + pendingNativeSnapshotTimer = null + } + return persistNativeSnapshot(resultSubtype, { + ...options, + force: true, + }) + } + + const scheduleNativeSnapshotPersist = () => { + const now = Date.now() + const elapsed = now - lastNativeSnapshotPersistedAt + if (elapsed >= 500) { + persistNativeSnapshot() + return + } + + if (pendingNativeSnapshotTimer) return + pendingNativeSnapshotTimer = setTimeout(() => { + pendingNativeSnapshotTimer = null + persistNativeSnapshot() + }, Math.max(50, 500 - elapsed)) + } + + const getNativeEventHash = (event: CodexJsonlEvent): string => { + try { + return createHash("sha1") + .update(JSON.stringify(event)) + .digest("hex") + } catch { + return createHash("sha1") + .update(String(event)) + .digest("hex") + } + } + + const emitNativeStart = () => { + if (didEmitNativeStart) return + didEmitNativeStart = true + safeEmit({ + type: "start", + messageId: responseMessageId, + messageMetadata: { + model: metadataModel, + sessionId: latestNativeSessionId ?? undefined, + transport: "codex-native-exec", + }, + }) + safeEmit({ type: "start-step" }) + didEmitNativeStartStep = true + } + + const enqueueNativeVisualTextDelta = ( + emitChunk: () => void, + delayAfterMs: number, + ) => { + nativeVisualTextQueue = nativeVisualTextQueue + .catch(() => undefined) + .then(async () => { + if (!isActive) return + emitChunk() + await sleep(delayAfterMs) + }) + return nativeVisualTextQueue + } + + const drainNativeVisualTextQueue = async () => { + try { + await nativeVisualTextQueue + } catch { + // Keep stream completion authoritative even if a late UI emit races + // with teardown. + } + } + + const closeNativeTextPart = () => { + if (!activeNativeTextPartId) { + nativeMessageParts.closeActiveTextPart() + return + } + safeEmit({ type: "text-end", id: activeNativeTextPartId }) + activeNativeTextPartId = null + nativeMessageParts.closeActiveTextPart() + } + + const closeNativeFinalTextPart = () => { + if (!didEmitNativeFinalTextStart || !nativeFinalTextPartId) { + nativeMessageParts.closeFinalTextPart() + return + } + safeEmit({ type: "text-end", id: nativeFinalTextPartId }) + didEmitNativeFinalTextStart = false + nativeFinalTextPartId = null + nativeMessageParts.closeFinalTextPart() + } + + const closeNativeTextParts = () => { + closeNativeTextPart() + closeNativeFinalTextPart() + } + + const emitNativeFinishStep = () => { + if (!didEmitNativeStartStep || didEmitNativeFinishStep) return + closeNativeTextParts() + safeEmit({ type: "finish-step" }) + didEmitNativeFinishStep = true + } + + const emitNativeTextDelta = (delta: string) => { + if (!delta) return + emitNativeStart() + const textChange = nativeMessageParts.appendTextDelta(delta) + if (!textChange) return + if (textChange.didStart || !activeNativeTextPartId) { + activeNativeTextPartId = `native-text-${responseMessageId}-${nativeTextSequence++}` + safeEmit({ type: "text-start", id: activeNativeTextPartId }) + } + safeEmit({ type: "text-delta", id: activeNativeTextPartId, delta }) + emittedNativeText += delta + scheduleNativeSnapshotPersist() + } + + const emitNativeTextDeltaChunks = (delta: string) => { + for (const chunk of splitCodexTextForStreamingDeltas(delta)) { + emitNativeTextDelta(chunk) + } + } + + const reconcileNativeText = (text: string) => { + if (!text) return + if (!emittedNativeText) { + emitNativeTextDeltaChunks(text) + return + } + const textAppend = reconcileCodexNativeTextAppend( + emittedNativeText, + text, + ) + if (textAppend.kind !== "separate") { + emitNativeTextDeltaChunks(textAppend.appendText) + } + } + + const emitNativeFinalText = (text: string) => { + if (!text) return + closeNativeTextPart() + if (!emittedNativeText) { + emitNativeFinalTextDeltaChunks(text, false) + return + } + if (emittedNativeFinalText.includes(text)) { + return + } + if ( + isCodexNativeRepeatedFinalText(emittedNativeFinalText, text) || + isCodexNativeRepeatedFinalText(emittedNativeText, text) + ) { + return + } + + const textAppend = reconcileCodexNativeTextAppend( + emittedNativeText, + text, + ) + if (!textAppend.appendText) return + + emitNativeFinalTextDeltaChunks( + textAppend.appendText, + textAppend.kind === "separate", + ) + } + + const emitNativeFinalTextDelta = ( + delta: string, + separateFromPreviousText: boolean, + ) => { + if (!delta) return + emitNativeStart() + const finalTextChange = nativeMessageParts.appendFinalTextDelta(delta) + if (!finalTextChange) return + if (finalTextChange.didStart || !nativeFinalTextPartId) { + nativeFinalTextPartId = `native-final-${responseMessageId}` + safeEmit({ type: "text-start", id: nativeFinalTextPartId }) + didEmitNativeFinalTextStart = true + } + safeEmit({ type: "text-delta", id: nativeFinalTextPartId, delta }) + emittedNativeFinalText += delta + emittedNativeText += + separateFromPreviousText && emittedNativeText + ? `\n\n${delta}` + : delta + scheduleNativeSnapshotPersist() + } + + const emitNativeFinalTextDeltaChunks = ( + delta: string, + separateFromPreviousText: boolean, + ) => { + const chunks = splitCodexTextForStreamingDeltas( + delta, + NATIVE_TEXT_REPLAY_CHUNK_LENGTH, + ) + const replayDelayMs = Math.max( + NATIVE_TEXT_REPLAY_MIN_INTERVAL_MS, + Math.min( + NATIVE_TEXT_REPLAY_MAX_INTERVAL_MS, + Math.floor( + NATIVE_TEXT_REPLAY_MAX_DURATION_MS / + Math.max(chunks.length, 1), + ), + ), + ) + chunks.forEach((chunk, index) => { + enqueueNativeVisualTextDelta( + () => { + emitNativeFinalTextDelta( + chunk, + separateFromPreviousText && index === 0, + ) + }, + replayDelayMs, + ) + }) + } + + const emitNativeCommentaryText = (text: string) => { + const commentaryText = text.trim() + if (!commentaryText) return + + emitNativeStart() + closeNativeTextPart() + + const commentaryPart = nativeMessageParts.appendCommentaryText(text) + if (!commentaryPart) return + const commentaryTextPartId = `native-commentary-${responseMessageId}-${nativeTextSequence++}` + + safeEmit({ type: "text-start", id: commentaryTextPartId }) + for (const chunk of splitCodexTextForStreamingDeltas(commentaryText)) { + safeEmit({ + type: "text-delta", + id: commentaryTextPartId, + delta: chunk, + }) + } + safeEmit({ type: "text-end", id: commentaryTextPartId }) + emittedNativeText += emittedNativeText + ? `\n\n${commentaryText}` + : commentaryText + scheduleNativeSnapshotPersist() + } + + const emitNativeToolInput = (params: { + callId: string + toolName: string + input: unknown + title?: string + }) => { + emitNativeStart() + closeNativeTextPart() + closeNativeFinalTextPart() + const toolChange = nativeMessageParts.startTool({ + callId: params.callId, + toolName: params.toolName, + input: params.input, + ...(params.title ? { title: params.title } : {}), + }) + + if (toolChange.didStart) { + safeEmit({ + type: "tool-input-available", + toolCallId: toolChange.part.toolCallId ?? params.callId, + toolName: params.toolName, + input: params.input, + ...(params.title ? { title: params.title } : {}), + }) + } + scheduleNativeSnapshotPersist() + return toolChange.part + } + + const emitNativeToolOutput = (params: { + callId: string + output: unknown + toolName?: string + input?: unknown + title?: string + isError?: boolean + }) => { + emitNativeStart() + let updatedToolPart = nativeMessageParts.updateToolResult( + params.callId, + { + output: params.output, + ...(params.input !== undefined ? { input: params.input } : {}), + ...(params.isError ? { isError: true } : {}), + }, + ) + if (!updatedToolPart && params.toolName) { + emitNativeToolInput({ + callId: params.callId, + toolName: params.toolName, + input: params.input ?? {}, + ...(params.title ? { title: params.title } : {}), + }) + updatedToolPart = nativeMessageParts.updateToolResult( + params.callId, + { + output: params.output, + ...(params.input !== undefined ? { input: params.input } : {}), + ...(params.isError ? { isError: true } : {}), + }, + ) + } + + safeEmit({ + type: params.isError ? "tool-output-error" : "tool-output-available", + toolCallId: updatedToolPart?.toolCallId ?? params.callId, + ...(params.isError + ? { errorText: String(params.output ?? "Codex tool failed") } + : { output: params.output }), + }) + scheduleNativeSnapshotPersist() + } + + const handleNativeJsonlEvent = (event: CodexJsonlEvent) => { + const eventHash = getNativeEventHash(event) + if (handledNativeEventHashes.has(eventHash)) return + handledNativeEventHashes.add(eventHash) + + const eventSessionId = extractCodexJsonlEventSessionId(event) + if (eventSessionId && eventSessionId !== latestNativeSessionId) { + latestNativeSessionId = eventSessionId + latestKnownCodexSessionId = eventSessionId + nativeSessionEventTailer?.start(eventSessionId) + persistNativeRuntimeSession("running") + safeEmit({ + type: "message-metadata", + messageMetadata: buildNativeResponseMetadata("running"), + }) + scheduleNativeSnapshotPersist() + } + + if (isCodexJsonlUserEvent(event)) return + + const text = extractCodexJsonlEventText(event) + if (text && isCodexNativeRuntimeNoticeText(text)) return + if (text && isCodexJsonlCommentaryTextEvent(event)) { + emitNativeCommentaryText(text) + return + } + + const toolEvent = codexJsonlEventToNativeToolEvent(event) + if (toolEvent?.kind === "tool-input") { + emitNativeToolInput({ + callId: toolEvent.callId, + toolName: toolEvent.toolName, + input: toolEvent.input, + ...(toolEvent.title ? { title: toolEvent.title } : {}), + }) + } else if (toolEvent?.kind === "tool-output") { + emitNativeToolOutput({ + callId: toolEvent.callId, + output: toolEvent.output, + ...(toolEvent.toolName ? { toolName: toolEvent.toolName } : {}), + ...(toolEvent.input !== undefined ? { input: toolEvent.input } : {}), + ...(toolEvent.title ? { title: toolEvent.title } : {}), + ...(toolEvent.isError ? { isError: true } : {}), + }) + } + + if (!text) return + + if (isCodexJsonlFinalTextEvent(event)) { + emitNativeFinalText(text) + return + } + + if (isCodexJsonlDeltaTextEvent(event)) { + emitNativeTextDelta(text) + return + } + + reconcileNativeText(text) + } + + nativeSessionEventTailer = createNativeSessionEventTailer({ + notBeforeTimestampMs: startedAt, + onEvent: handleNativeJsonlEvent, + }) + nativeSessionEventTailer.start(existingRunSessionId) + + try { + emitNativeStart() + const nativeEnv = buildCodexProviderEnv({ + authConfig: effectiveAuthConfig, + mossProvider, + }) + const runNativeCodex = existingRunSessionId + ? runCodexExecResumeBridge({ + sessionId: existingRunSessionId, + cwd: input.cwd, + prompt: input.prompt, + modelId: selectedModelId, + permissionMode: runtimePermissionMode, + command: resolveCodexCliPath(), + images: input.images, + env: nativeEnv, + abortSignal: abortController.signal, + onEvent: handleNativeJsonlEvent, + }) + : runCodexExecStartBridge({ + cwd: input.cwd, + prompt: input.prompt, + modelId: selectedModelId, + permissionMode: runtimePermissionMode, + command: resolveCodexCliPath(), + images: input.images, + env: nativeEnv, + abortSignal: abortController.signal, + onEvent: handleNativeJsonlEvent, + }) + + const nativeResult = await runNativeCodex + for (const event of nativeResult.events) { + handleNativeJsonlEvent(event) + await drainNativeVisualTextQueue() + } + tailedNativeSessionEventCount += await nativeSessionEventTailer.stop() + const replaySessionId = + nativeResult.nativeSessionId || + latestNativeSessionId || + existingRunSessionId + const replayedNativeSessionEvents = replaySessionId + ? await readSessionEventsForCurrentRun(replaySessionId, { + notBeforeTimestampMs: startedAt, + }) + : [] + if (replayedNativeSessionEvents.length > 0) { + const replaySnapshot = buildNativePartsFromCodexEvents( + replayedNativeSessionEvents, + ) + if ( + getCodexNativePartsRichness(replaySnapshot) > + getCodexNativePartsRichness(nativeMessageParts.snapshot()) + ) { + nativeMessageParts.replaceWith(replaySnapshot) + scheduleNativeSnapshotPersist() + } + } + await drainNativeVisualTextQueue() + const activeStream = activeStreams.get(input.subChatId) + const wasCancelled = + abortController.signal.aborted || activeStream?.cancelRequested + latestNativeSessionId = + nativeResult.nativeSessionId || + latestNativeSessionId || + existingRunSessionId || + null + latestKnownCodexSessionId = latestNativeSessionId + const usageMetadata = mapNativeUsageMetadata(nativeResult.usage) + const resultSubtype = wasCancelled + ? "cancelled" + : nativeResult.success + ? "success" + : "error" + const finalText = nativeResult.lastText || emittedNativeText + + const finalTextAlreadyPresent = nativeMessageParts.parts.some( + (part) => + part.type === "text" && + typeof part.text === "string" && + part.text.trim() === finalText.trim(), + ) + if (!finalTextAlreadyPresent) { + emitNativeFinalText(finalText) + } + await drainNativeVisualTextQueue() + + persistNativeRuntimeSession(resultSubtype, { + nativeBridge: nativeResult.plan.bridge, + imageCount: nativeResult.plan.imageCount, + eventCount: + nativeResult.events.length + + tailedNativeSessionEventCount + + replayedNativeSessionEvents.length, + exitCode: nativeResult.exitCode, + ...(nativeResult.error ? { error: nativeResult.error } : {}), + }) + + if (wasCancelled) { + const responseMetadata = buildNativeResponseMetadata("cancelled", { + nativeBridge: nativeResult.plan.bridge, + imageCount: nativeResult.plan.imageCount, + usageMetadata, + }) + flushNativeSnapshot("cancelled", { + nativeBridge: nativeResult.plan.bridge, + imageCount: nativeResult.plan.imageCount, + usageMetadata, + }) + emitNativeFinishStep() + persistSubChatStreamId(null) + safeEmit({ + type: "message-metadata", + messageMetadata: responseMetadata, + }) + safeEmit({ + type: "finish", + finishReason: "stop", + messageMetadata: responseMetadata, + }) + safeComplete() + return + } + + if (!nativeResult.success) { + flushNativeSnapshot("error", { + nativeBridge: nativeResult.plan.bridge, + imageCount: nativeResult.plan.imageCount, + usageMetadata, + error: nativeResult.error ?? null, + }) + emitNativeFinishStep() + throw new Error( + nativeResult.error || + `Codex native exec failed with exit code ${nativeResult.exitCode ?? "unknown"}.`, + ) + } + + const responseMetadata: Record = { + model: metadataModel, + sessionId: latestNativeSessionId ?? undefined, + durationMs: Date.now() - startedAt, + resultSubtype, + nativeBridge: nativeResult.plan.bridge, + transport: "codex-native-exec", + imageCount: nativeResult.plan.imageCount, + ...(nativeResumeDecision.reason + ? { nativeResumeSkipReason: nativeResumeDecision.reason } + : {}), + ...nativeRuntimeNoticeCleanupMetadata, + ...(usageMetadata || {}), + } + flushNativeSnapshot("success", { + nativeBridge: nativeResult.plan.bridge, + imageCount: nativeResult.plan.imageCount, + usageMetadata, + }) + persistSubChatStreamId(null) + emitNativeFinishStep() + safeEmit({ + type: "message-metadata", + messageMetadata: responseMetadata, + }) + safeEmit({ + type: "finish", + finishReason: "stop", + messageMetadata: responseMetadata, + }) + safeComplete() + return + } catch (nativeError) { + tailedNativeSessionEventCount += + (await nativeSessionEventTailer?.stop().catch((tailError) => { + console.warn("[codex] Failed to stop native session tailer:", tailError) + return 0 + })) ?? 0 + flushNativeSnapshot( + abortController.signal.aborted ? "cancelled" : "error", + { + error: + nativeError instanceof Error + ? nativeError.message + : String(nativeError), + }, + ) + emitNativeFinishStep() + throw nativeError + } + } + const provider = getOrCreateProvider({ subChatId: input.subChatId, cwd: input.cwd, mcpServers: mcpSnapshot.mcpServersForSession, mcpFingerprint: mcpSnapshot.fingerprint, - existingSessionId: - input.forceNewSession - ? undefined - : input.sessionId ?? getLastSessionId(existingMessages), - authConfig: input.authConfig, + existingSessionId: existingRunSessionId, + authConfig: effectiveAuthConfig, + mossProvider, }) const startedAt = Date.now() - let latestSessionId = - provider.getSessionId() || - input.sessionId || - getLastSessionId(existingMessages) + let latestSessionId = provider.getSessionId() || existingRunSessionId + if (latestSessionId) { + latestKnownCodexSessionId = latestSessionId + } let usagePromise: Promise | null = null + const persistCodexRuntimeSession = ( + resultSubtype: "success" | "error" | "cancelled" = "success", + ) => { + if (!isAuthoritativeRun()) return false + + persistAgentRuntimeSession({ + subChatId: input.subChatId, + engine: "codex", + nativeSessionId: + provider.getSessionId() || latestSessionId || null, + configDir: join(homedir(), ".codex"), + providerInstanceId, + modelId: metadataModel, + modelSelection: runtimeModelSelection, + permissionMode: runtimePermissionMode, + metadata: { + cwd: input.cwd, + projectPath: input.projectPath ?? null, + authMode: input.authConfig + ? "api-key" + : effectiveAuthConfig + ? "moss-provider" + : "codex-cli", + mossProviderStatus: mossProvider.status, + mossProviderId: mossProvider.providerId ?? null, + mossProviderMode: mossProvider.mode ?? null, + forceNewSession: Boolean(input.forceNewSession), + ignoredStoredSessionIds: ignoreStoredSessionIds, + mcpFingerprint: mcpSnapshot.fingerprint, + mcpFetchedAt: mcpSnapshot.fetchedAt, + mcpToolsResolved: mcpSnapshot.toolsResolved, + mcpServerCount: mcpSnapshot.mcpServersForSession.length, + hasImages: Boolean(input.images?.length), + resultSubtype, + }, + launchPlan: buildCodexLaunchPlan({ + nativeSessionId: + provider.getSessionId() || latestSessionId || null, + nativeSessionStrategy: existingRunSessionId ? "resume" : "start", + transport: "codex-acp", + resultSubtype, + }), + }) + + return true + } + + persistCodexRuntimeSession() const resolveUsageOnce = (): Promise => { if (usagePromise) return usagePromise @@ -1780,6 +3335,7 @@ export const codexRouter = router({ const sessionId = provider.getSessionId() || undefined if (sessionId) { latestSessionId = sessionId + latestKnownCodexSessionId = sessionId } if (part.type === "finish") { @@ -1816,7 +3372,9 @@ export const codexRouter = router({ cleanAssistantMessageForPersistence(responseWithUsage) if (!cleanedResponseMessage) { - persistSubChatMessages(messagesForStream) + if (persistSubChatMessages(messagesForStream)) { + persistCodexRuntimeSession() + } return } @@ -1827,9 +3385,12 @@ export const codexRouter = router({ cleanedResponseMessage, ] - persistSubChatMessages(messagesToPersist) + if (persistSubChatMessages(messagesToPersist)) { + persistCodexRuntimeSession() + } } catch (error) { console.error("[codex] Failed to persist messages:", error) + persistCodexRuntimeSession("error") } }, onError: (error) => extractCodexError(error).message, @@ -1873,11 +3434,63 @@ export const codexRouter = router({ safeEmit({ type: "finish" }) } + persistSubChatStreamId(null) safeComplete() } catch (error) { const normalized = extractCodexError(error) + const { + providerInstanceId: errorProviderInstanceId, + modelSelection: errorRuntimeModelSelection, + } = resolveRuntimeModelSelection({ + providerInstanceId: input.providerInstanceId ?? null, + modelSelection: input.modelSelection ?? null, + fallbackModelId: input.model ?? DEFAULT_CODEX_MODEL, + }) console.error("[codex] chat stream error:", error) + try { + persistAgentRuntimeSession({ + subChatId: input.subChatId, + engine: "codex", + nativeSessionId: latestKnownCodexSessionId, + configDir: join(homedir(), ".codex"), + providerInstanceId: errorProviderInstanceId, + modelId: input.model ?? DEFAULT_CODEX_MODEL, + modelSelection: errorRuntimeModelSelection, + permissionMode: runtimePermissionMode, + metadata: { + cwd: input.cwd, + projectPath: input.projectPath ?? null, + resultSubtype: "error", + error: normalized.message, + }, + launchPlan: buildAgentRuntimeLaunchPlan({ + runId: input.runId, + session: { + subChatId: input.subChatId, + chatId: input.chatId, + engineId: "codex", + providerInstanceId: errorProviderInstanceId, + nativeSessionId: latestKnownCodexSessionId, + modelId: input.model ?? DEFAULT_CODEX_MODEL, + modelSelection: errorRuntimeModelSelection, + permissionMode: runtimePermissionMode, + cwd: input.cwd, + projectPath: input.projectPath ?? null, + runtimeConfigDir: join(homedir(), ".codex"), + }, + nativeSessionStrategy: latestKnownCodexSessionId + ? "resume" + : "start", + transport: "codex-chat", + providerRoute: input.authConfig ? "api-key" : "unknown", + resultSubtype: "error", + metadata: { error: normalized.message }, + }), + }) + } catch { + // Best-effort runtime index update only. + } if (isCodexAuthError(normalized)) { safeEmit({ type: "auth-error", errorText: normalized.message }) } else { @@ -1893,6 +3506,7 @@ export const codexRouter = router({ if (shouldCleanupProvider) { cleanupProvider(input.subChatId) } + clearActiveStreamId?.() activeStreams.delete(input.subChatId) } } diff --git a/src/main/lib/trpc/routers/files.ts b/src/main/lib/trpc/routers/files.ts index d4081f9ae..18621531b 100644 --- a/src/main/lib/trpc/routers/files.ts +++ b/src/main/lib/trpc/routers/files.ts @@ -29,6 +29,15 @@ const IGNORED_DIRS = new Set([ ".astro", ]) +const ALLOWED_HIDDEN_DIRS = new Set([ + ".1code", + ".moss", + ".claude", + ".codex", + ".github", + ".vscode", +]) + // Files to ignore const IGNORED_FILES = new Set([ ".DS_Store", @@ -125,8 +134,8 @@ async function scanDirectory( if (entry.isDirectory()) { // Skip ignored directories if (IGNORED_DIRS.has(entry.name)) continue - // Skip hidden directories (except .github, .vscode, etc.) - if (entry.name.startsWith(".") && !entry.name.startsWith(".github") && !entry.name.startsWith(".vscode")) continue + // Keep product-owned agent/config directories visible in the right panel. + if (entry.name.startsWith(".") && !ALLOWED_HIDDEN_DIRS.has(entry.name)) continue // Add the folder itself to results entries.push({ path: relativePath, type: "folder" }) @@ -391,6 +400,13 @@ export const filesRouter = router({ ".webp": "image/webp", ".ico": "image/x-icon", ".bmp": "image/bmp", + ".pdf": "application/pdf", + ".mp4": "video/mp4", + ".m4v": "video/mp4", + ".mov": "video/quicktime", + ".webm": "video/webm", + ".ogv": "video/ogg", + ".ogg": "video/ogg", } const mimeType = mimeMap[ext] || "application/octet-stream" diff --git a/src/main/lib/trpc/routers/hermes.ts b/src/main/lib/trpc/routers/hermes.ts new file mode 100644 index 000000000..6ee11bd12 --- /dev/null +++ b/src/main/lib/trpc/routers/hermes.ts @@ -0,0 +1,917 @@ +import { createACPProvider, type ACPProvider } from "@mcpc-tech/acp-ai-provider" +import { observable } from "@trpc/server/observable" +import { streamText } from "ai" +import { eq } from "drizzle-orm" +import { createHash } from "node:crypto" +import { homedir } from "node:os" +import { join } from "node:path" +import { z } from "zod" +import { buildAgentRuntimeLaunchPlan } from "../../agent-runtime/launch-plan" +import { shouldIgnoreMossStoredMessageSessionIds } from "../../agent-runtime/session-actions" +import { persistAgentRuntimeSession } from "../../agent-runtime/session-store" +import { getClaudeShellEnvironment } from "../../claude/env" +import { resolveProjectPathFromWorktree } from "../../claude-config" +import { getDatabase, subChats } from "../../db" +import { resolveHermesAcpLaunch } from "../../hermes/runtime" +import { + getMossProviderSecret, + getMossProviderFingerprint, + resolveMossProviderForEngine, + runMossHooks, + type ResolvedMossProvider, +} from "../../moss-source" +import { buildMossRuntimeContext } from "../../moss-source/runtime-context" +import { publicProcedure, router } from "../index" + +const imageAttachmentSchema = z.object({ + base64Data: z.string(), + mediaType: z.string(), + filename: z.string().optional(), +}) + +const permissionModeSchema = z.enum([ + "plan", + "agent", + "bypass", + "read-only", + "ask-approval", + "full-access", + "custom", +]) + +const runtimeModelSelectionSchema = z.object({ + instanceId: z.string().min(1), + modelId: z.string().min(1), + options: z.record(z.string(), z.unknown()).optional(), +}) + +type RuntimeModelSelectionInput = z.infer + +function resolveRuntimeModelSelection(input: { + providerInstanceId?: string | null + modelSelection?: RuntimeModelSelectionInput | null + fallbackModelId: string +}): { + providerInstanceId: string | null + modelSelection: RuntimeModelSelectionInput | null +} { + const modelSelection = + input.modelSelection ?? + (input.providerInstanceId + ? { + instanceId: input.providerInstanceId, + modelId: input.fallbackModelId, + } + : null) + return { + providerInstanceId: + input.providerInstanceId ?? modelSelection?.instanceId ?? null, + modelSelection, + } +} + +type HermesProviderSession = { + provider: ACPProvider + cwd: string + mossProviderFingerprint: string | null + launchFingerprint: string +} + +type ActiveHermesStream = { + runId: string + controller: AbortController + cancelRequested: boolean +} + +const providerSessions = new Map() +const activeStreams = new Map() + +const DEFAULT_HERMES_MODEL = "moss-default" + +export function hasActiveHermesStreams(): boolean { + return activeStreams.size > 0 +} + +export function hasActiveHermesStreamForSubChat( + subChatId: string, + runId?: string | null, +): boolean { + const stream = activeStreams.get(subChatId) + if (!stream) return false + return !runId || stream.runId === runId +} + +export function abortAllHermesStreams(): void { + for (const [subChatId, stream] of activeStreams) { + console.log(`[hermes] Aborting stream ${subChatId} before reload`) + stream.cancelRequested = true + stream.controller.abort() + } + activeStreams.clear() +} + +function parseStoredMessages(raw: string | null | undefined): any[] { + if (!raw) return [] + try { + const parsed = JSON.parse(raw) + return Array.isArray(parsed) ? parsed : [] + } catch { + return [] + } +} + +function extractPromptFromStoredMessage(message: any): string { + if (!message || !Array.isArray(message.parts)) return "" + + const textParts: string[] = [] + const fileContents: string[] = [] + + for (const part of message.parts) { + if (part?.type === "text" && typeof part.text === "string") { + textParts.push(part.text) + } else if (part?.type === "file-content") { + const filePath = + typeof part.filePath === "string" ? part.filePath : undefined + const fileName = filePath?.split("/").pop() || filePath || "file" + const content = typeof part.content === "string" ? part.content : "" + fileContents.push(`\n--- ${fileName} ---\n${content}`) + } + } + + return textParts.join("\n") + fileContents.join("") +} + +function getLastSessionId(messages: any[]): string | undefined { + const lastAssistant = [...messages] + .reverse() + .find((message) => message?.role === "assistant") + const sessionId = lastAssistant?.metadata?.sessionId + return typeof sessionId === "string" ? sessionId : undefined +} + +function buildUserParts( + prompt: string, + images: + | Array<{ + base64Data?: string + mediaType?: string + filename?: string + }> + | undefined, +): any[] { + const parts: any[] = [{ type: "text", text: prompt }] + + if (images && images.length > 0) { + for (const image of images) { + if (!image.base64Data || !image.mediaType) continue + parts.push({ + type: "data-image", + data: { + base64Data: image.base64Data, + mediaType: image.mediaType, + filename: image.filename, + }, + }) + } + } + + return parts +} + +function buildModelMessageContent( + prompt: string, + images: + | Array<{ + base64Data?: string + mediaType?: string + filename?: string + }> + | undefined, +): any[] { + const content: any[] = [{ type: "text", text: prompt }] + + if (images && images.length > 0) { + for (const image of images) { + if (!image.base64Data || !image.mediaType) continue + content.push({ + type: "file", + mediaType: image.mediaType, + data: image.base64Data, + ...(image.filename ? { filename: image.filename } : {}), + }) + } + } + + return content +} + +function resourcesForSessionInfo( + resources: Awaited>["resources"], + kind: "mcp" | "plugin" | "skill", +): Array<{ name: string; path?: string; source: "moss" }> { + return resources + .filter((resource) => resource.kind === kind && resource.included) + .map((resource) => ({ + name: resource.name, + path: resource.path, + source: "moss" as const, + })) +} + +type MossHookRunSummaryForSession = Awaited> + +function summarizeMossHookRunForSession(run: MossHookRunSummaryForSession) { + return { + status: run.status, + event: run.event, + matchedCount: run.matchedCount, + executedCount: run.executedCount, + skippedCount: run.skippedCount, + failedCount: run.failedCount, + timedOutCount: run.timedOutCount, + warnings: run.warnings, + results: run.results.map((result) => ({ + resourceId: result.resourceId, + name: result.name, + status: result.status, + exitCode: result.exitCode ?? null, + elapsedMs: result.elapsedMs, + timedOut: Boolean(result.timedOut), + })), + } +} + +function extractHermesError(error: unknown): { message: string; code?: string } { + const anyError = error as any + const message = + anyError?.data?.message || + anyError?.errorText || + anyError?.message || + anyError?.error || + String(error) + const code = anyError?.data?.code || anyError?.code + + return { + message: typeof message === "string" ? message : String(message), + code: typeof code === "string" ? code : undefined, + } +} + +function buildHermesProviderEnv(params?: { + mossProvider?: ResolvedMossProvider | null +}): Record { + const env: Record = {} + + for (const [key, value] of Object.entries(process.env)) { + if (typeof value === "string") { + env[key] = value + } + } + + const shellEnv = getClaudeShellEnvironment() + for (const [key, value] of Object.entries(shellEnv)) { + if (typeof value === "string") { + env[key] = value + } + } + + if (!env.HERMES_HOME) { + env.HERMES_HOME = join(homedir(), ".hermes") + } + + if (params?.mossProvider?.status === "resolved") { + Object.assign(env, params.mossProvider.env) + } + + return env +} + +function getLaunchFingerprint(): { + launch: { command: string; args: string[] } + fingerprint: string +} { + const launch = resolveHermesAcpLaunch() + return { + launch, + fingerprint: createHash("sha256") + .update(JSON.stringify(launch)) + .digest("hex"), + } +} + +function getOrCreateProvider(params: { + subChatId: string + cwd: string + existingSessionId?: string + mossProvider?: ResolvedMossProvider | null +}): ACPProvider { + const mossProviderFingerprint = getMossProviderFingerprint(params.mossProvider) + const { launch, fingerprint: launchFingerprint } = getLaunchFingerprint() + const existing = providerSessions.get(params.subChatId) + + if ( + existing && + existing.cwd === params.cwd && + existing.mossProviderFingerprint === mossProviderFingerprint && + existing.launchFingerprint === launchFingerprint + ) { + return existing.provider + } + + if (existing) { + existing.provider.cleanup() + providerSessions.delete(params.subChatId) + } + + const provider = createACPProvider({ + command: launch.command, + args: launch.args, + env: buildHermesProviderEnv({ mossProvider: params.mossProvider }), + ...(process.env.HERMES_ACP_AUTH_METHOD + ? { authMethodId: process.env.HERMES_ACP_AUTH_METHOD } + : {}), + session: { + cwd: params.cwd, + mcpServers: [], + }, + ...(params.existingSessionId + ? { existingSessionId: params.existingSessionId } + : {}), + persistSession: true, + }) + + providerSessions.set(params.subChatId, { + provider, + cwd: params.cwd, + mossProviderFingerprint, + launchFingerprint, + }) + + return provider +} + +function cleanupProvider(subChatId: string): void { + const existing = providerSessions.get(subChatId) + if (!existing) return + + existing.provider.cleanup() + providerSessions.delete(subChatId) +} + +function resolveHermesMode(mode: "plan" | "agent"): string { + return mode === "agent" ? "accept_edits" : "default" +} + +function resolveHermesModelForCall(model: string | undefined): string | undefined { + const normalized = model?.trim() + if (!normalized || normalized === DEFAULT_HERMES_MODEL || normalized === "hermes") { + return undefined + } + + return normalized +} + +function cleanAssistantMessageForPersistence(message: any): any | null { + if (!message || message.role !== "assistant") return message + if (!Array.isArray(message.parts)) return message + + const cleanedParts = message.parts.filter( + (part: any) => part?.state !== "input-streaming", + ) + + if (cleanedParts.length === 0) { + return null + } + + return { + ...message, + parts: cleanedParts, + } +} + +export const hermesRouter = router({ + chat: publicProcedure + .input( + z.object({ + subChatId: z.string(), + chatId: z.string(), + runId: z.string(), + prompt: z.string(), + model: z.string().optional(), + providerInstanceId: z.string().min(1).optional(), + modelSelection: runtimeModelSelectionSchema.optional(), + cwd: z.string(), + projectPath: z.string().optional(), + mode: z.enum(["plan", "agent"]).default("agent"), + permissionMode: permissionModeSchema.optional(), + sessionId: z.string().optional(), + forceNewSession: z.boolean().optional(), + images: z.array(imageAttachmentSchema).optional(), + }), + ) + .subscription(({ input }) => { + return observable((emit) => { + const runtimePermissionMode = input.permissionMode ?? input.mode + const existingStream = activeStreams.get(input.subChatId) + if (existingStream) { + existingStream.cancelRequested = true + existingStream.controller.abort() + cleanupProvider(input.subChatId) + } + + const abortController = new AbortController() + activeStreams.set(input.subChatId, { + runId: input.runId, + controller: abortController, + cancelRequested: false, + }) + + let isActive = true + + const safeEmit = (chunk: any) => { + if (!isActive) return + try { + emit.next(chunk) + } catch { + isActive = false + } + } + + const safeComplete = () => { + if (!isActive) return + isActive = false + try { + emit.complete() + } catch { + // Ignore double completion + } + } + + ;(async () => { + try { + const db = getDatabase() + const existingSubChat = db + .select() + .from(subChats) + .where(eq(subChats.id, input.subChatId)) + .get() + + if (!existingSubChat) { + throw new Error("Sub-chat not found") + } + + const existingMessages = parseStoredMessages(existingSubChat.messages) + const ignoreStoredSessionIds = shouldIgnoreMossStoredMessageSessionIds( + existingSubChat.runtimeMetadata, + ) + const existingRunSessionId = input.forceNewSession + ? undefined + : (ignoreStoredSessionIds ? undefined : input.sessionId) ?? + existingSubChat.engineSessionId ?? + (ignoreStoredSessionIds + ? undefined + : getLastSessionId(existingMessages)) + const requestedModelId = input.model?.trim() || DEFAULT_HERMES_MODEL + const modelForCall = resolveHermesModelForCall(requestedModelId) + const { + providerInstanceId, + modelSelection: runtimeModelSelection, + } = resolveRuntimeModelSelection({ + providerInstanceId: input.providerInstanceId ?? null, + modelSelection: input.modelSelection ?? null, + fallbackModelId: requestedModelId, + }) + const resolvedProjectPathFromCwd = resolveProjectPathFromWorktree( + input.cwd, + ) + const providerLookupPath = + input.projectPath || resolvedProjectPathFromCwd || input.cwd + const mossProvider = await resolveMossProviderForEngine({ + projectPath: providerLookupPath, + engineId: "hermes", + requestedModelId, + createIfMissing: true, + secretResolver: { getSecret: getMossProviderSecret }, + }) + if (mossProvider.warnings.length > 0) { + console.warn("[hermes] Moss provider warnings:", mossProvider.warnings) + } + const mossRuntimeContext = await buildMossRuntimeContext({ + projectPath: providerLookupPath, + engineId: "hermes", + }) + if (mossRuntimeContext.warnings.length > 0) { + console.warn( + "[hermes] Moss runtime context warnings:", + mossRuntimeContext.warnings, + ) + } + const mossHookRun = await runMossHooks({ + projectPath: providerLookupPath, + cwd: input.cwd, + event: "HermesSessionStart", + engineId: "hermes", + payload: { + subChatId: input.subChatId, + chatId: input.chatId, + runId: input.runId, + mode: input.mode, + requestedModelId, + runtimeContextFingerprint: mossRuntimeContext.fingerprint, + promptSha256: createHash("sha256").update(input.prompt).digest("hex"), + }, + }) + const mossHookSummary = + summarizeMossHookRunForSession(mossHookRun) + if (mossHookRun.status === "failed") { + console.warn("[hermes] Moss hook run failed:", mossHookSummary) + } + + const metadataModel = + modelForCall || mossProvider.model || requestedModelId + const lastMessage = existingMessages[existingMessages.length - 1] + const isDuplicatePrompt = + lastMessage?.role === "user" && + extractPromptFromStoredMessage(lastMessage) === input.prompt + + let messagesForStream = existingMessages + const isAuthoritativeRun = () => { + const currentStream = activeStreams.get(input.subChatId) + return !currentStream || currentStream.runId === input.runId + } + + const persistSubChatMessages = (messages: any[]) => { + if (!isAuthoritativeRun()) { + return false + } + + db.update(subChats) + .set({ + messages: JSON.stringify(messages), + updatedAt: new Date(), + }) + .where(eq(subChats.id, input.subChatId)) + .run() + return true + } + + if (!isDuplicatePrompt) { + const userMessage = { + id: crypto.randomUUID(), + role: "user", + parts: buildUserParts(input.prompt, input.images), + metadata: { model: metadataModel }, + } + + messagesForStream = [...existingMessages, userMessage] + + db.update(subChats) + .set({ + messages: JSON.stringify(messagesForStream), + updatedAt: new Date(), + }) + .where(eq(subChats.id, input.subChatId)) + .run() + } + + if (input.forceNewSession) { + cleanupProvider(input.subChatId) + } + + const provider = getOrCreateProvider({ + subChatId: input.subChatId, + cwd: input.cwd, + existingSessionId: existingRunSessionId, + mossProvider, + }) + + const startedAt = Date.now() + let latestSessionId = provider.getSessionId() || existingRunSessionId + const modeForCall = resolveHermesMode(input.mode) + const languageModel = provider.languageModel(modelForCall, modeForCall) + const sessionInfo = await provider.initSession() + latestSessionId = + provider.getSessionId() || + latestSessionId || + sessionInfo?.sessionId || + undefined + + safeEmit({ + type: "session-init", + engine: "hermes", + sessionId: latestSessionId, + models: sessionInfo?.models, + modes: sessionInfo?.modes, + tools: [], + mcpServers: resourcesForSessionInfo( + mossRuntimeContext.resources, + "mcp", + ), + plugins: resourcesForSessionInfo( + mossRuntimeContext.resources, + "plugin", + ), + skills: resourcesForSessionInfo( + mossRuntimeContext.resources, + "skill", + ), + mossUnifiedSourceContext: { + status: mossRuntimeContext.status, + fingerprint: mossRuntimeContext.fingerprint, + resourceCount: mossRuntimeContext.resourceCount, + includedResourceCount: + mossRuntimeContext.includedResourceCount, + warnings: mossRuntimeContext.warnings, + }, + mossHooks: mossHookSummary, + }) + + const persistHermesRuntimeSession = ( + resultSubtype: "success" | "error" | "cancelled" = "success", + ) => { + if (!isAuthoritativeRun()) return false + + persistAgentRuntimeSession({ + subChatId: input.subChatId, + engine: "hermes", + nativeSessionId: + provider.getSessionId() || latestSessionId || null, + configDir: join(homedir(), ".hermes"), + providerInstanceId, + modelId: metadataModel, + modelSelection: runtimeModelSelection, + permissionMode: runtimePermissionMode, + metadata: { + cwd: input.cwd, + projectPath: input.projectPath ?? null, + mossProviderStatus: mossProvider.status, + mossProviderId: mossProvider.providerId ?? null, + mossProviderMode: mossProvider.mode ?? null, + mossUnifiedSourceContext: { + status: mossRuntimeContext.status, + fingerprint: mossRuntimeContext.fingerprint, + resourceCount: mossRuntimeContext.resourceCount, + includedResourceCount: + mossRuntimeContext.includedResourceCount, + }, + mossHooks: mossHookSummary, + forceNewSession: Boolean(input.forceNewSession), + ignoredStoredSessionIds: ignoreStoredSessionIds, + hasImages: Boolean(input.images?.length), + resultSubtype, + }, + launchPlan: buildAgentRuntimeLaunchPlan({ + runId: input.runId, + session: { + subChatId: input.subChatId, + chatId: input.chatId, + engineId: "hermes", + providerInstanceId, + nativeSessionId: + provider.getSessionId() || latestSessionId || null, + modelId: metadataModel, + modelSelection: runtimeModelSelection, + permissionMode: runtimePermissionMode, + cwd: input.cwd, + projectPath: input.projectPath ?? null, + runtimeConfigDir: join(homedir(), ".hermes"), + }, + nativeSessionStrategy: existingRunSessionId ? "resume" : "start", + transport: "hermes-acp", + providerRoute: + mossProvider.status === "resolved" + ? "moss-provider" + : "hermes-default", + projectionStatus: + mossRuntimeContext.status === "ready" ? "ready" : "partial", + runtimeContextFingerprint: mossRuntimeContext.fingerprint, + resultSubtype, + metadata: { + mossProviderStatus: mossProvider.status, + mossHookStatus: mossHookSummary.status, + }, + }), + }) + + return true + } + + persistHermesRuntimeSession() + + const result = streamText({ + model: languageModel, + messages: [ + ...(mossRuntimeContext.status === "ready" + ? ([ + { + role: "system" as const, + content: mossRuntimeContext.text, + }, + ] as const) + : []), + { + role: "user", + content: buildModelMessageContent(input.prompt, input.images), + }, + ], + tools: provider.tools, + abortSignal: abortController.signal, + }) + + const uiStream = result.toUIMessageStream({ + originalMessages: messagesForStream, + generateMessageId: () => crypto.randomUUID(), + messageMetadata: ({ part }) => { + const sessionId = provider.getSessionId() || undefined + if (sessionId) { + latestSessionId = sessionId + } + + if (part.type === "finish") { + return { + model: metadataModel, + sessionId, + durationMs: Date.now() - startedAt, + resultSubtype: part.finishReason === "error" ? "error" : "success", + } + } + + if (sessionId) { + return { + model: metadataModel, + sessionId, + } + } + + return { model: metadataModel } + }, + onFinish: async ({ responseMessage, isContinuation }) => { + try { + const cleanedResponseMessage = + cleanAssistantMessageForPersistence(responseMessage) + + if (!cleanedResponseMessage) { + if (persistSubChatMessages(messagesForStream)) { + persistHermesRuntimeSession() + } + return + } + + const messagesToPersist = [ + ...(isContinuation + ? messagesForStream.slice(0, -1) + : messagesForStream), + cleanedResponseMessage, + ] + + if (persistSubChatMessages(messagesToPersist)) { + persistHermesRuntimeSession() + } + } catch (error) { + console.error("[hermes] Failed to persist messages:", error) + persistHermesRuntimeSession("error") + } + }, + onError: (error) => extractHermesError(error).message, + }) + + const reader = uiStream.getReader() + let pendingFinishChunk: any | null = null + while (true) { + const { done, value } = await reader.read() + if (done) break + + if (value?.type === "error") { + const normalized = extractHermesError(value) + safeEmit({ ...value, errorText: normalized.message }) + continue + } + + if (value?.type === "finish") { + pendingFinishChunk = value + continue + } + + safeEmit(value) + } + + safeEmit(pendingFinishChunk || { type: "finish" }) + safeComplete() + } catch (error) { + const normalized = extractHermesError(error) + const { + providerInstanceId: errorProviderInstanceId, + modelSelection: errorRuntimeModelSelection, + } = resolveRuntimeModelSelection({ + providerInstanceId: input.providerInstanceId ?? null, + modelSelection: input.modelSelection ?? null, + fallbackModelId: input.model ?? DEFAULT_HERMES_MODEL, + }) + + console.error("[hermes] chat stream error:", error) + try { + persistAgentRuntimeSession({ + subChatId: input.subChatId, + engine: "hermes", + nativeSessionId: null, + configDir: join(homedir(), ".hermes"), + providerInstanceId: errorProviderInstanceId, + modelId: input.model ?? DEFAULT_HERMES_MODEL, + modelSelection: errorRuntimeModelSelection, + permissionMode: runtimePermissionMode, + metadata: { + cwd: input.cwd, + projectPath: input.projectPath ?? null, + resultSubtype: "error", + error: normalized.message, + }, + launchPlan: buildAgentRuntimeLaunchPlan({ + runId: input.runId, + session: { + subChatId: input.subChatId, + chatId: input.chatId, + engineId: "hermes", + providerInstanceId: errorProviderInstanceId, + nativeSessionId: null, + modelId: input.model ?? DEFAULT_HERMES_MODEL, + modelSelection: errorRuntimeModelSelection, + permissionMode: runtimePermissionMode, + cwd: input.cwd, + projectPath: input.projectPath ?? null, + runtimeConfigDir: join(homedir(), ".hermes"), + }, + nativeSessionStrategy: "start", + transport: "hermes-acp", + providerRoute: "hermes-error", + resultSubtype: "error", + metadata: { error: normalized.message }, + }), + }) + } catch { + // Best-effort runtime index update only. + } + safeEmit({ type: "error", errorText: normalized.message }) + safeEmit({ type: "finish" }) + safeComplete() + } finally { + const activeStream = activeStreams.get(input.subChatId) + if (activeStream?.runId === input.runId) { + const shouldCleanupProvider = + abortController.signal.aborted || activeStream.cancelRequested + if (shouldCleanupProvider) { + cleanupProvider(input.subChatId) + } + activeStreams.delete(input.subChatId) + } + } + })() + + return () => { + isActive = false + abortController.abort() + + const activeStream = activeStreams.get(input.subChatId) + if (activeStream?.runId === input.runId) { + activeStream.cancelRequested = true + } + } + }) + }), + + cancel: publicProcedure + .input( + z.object({ + subChatId: z.string(), + runId: z.string(), + }), + ) + .mutation(({ input }) => { + const activeStream = activeStreams.get(input.subChatId) + if (!activeStream) { + return { cancelled: false, ignoredStale: false } + } + + if (activeStream.runId !== input.runId) { + return { cancelled: false, ignoredStale: true } + } + + activeStream.cancelRequested = true + activeStream.controller.abort() + + return { cancelled: true, ignoredStale: false } + }), + + cleanup: publicProcedure + .input(z.object({ subChatId: z.string() })) + .mutation(({ input }) => { + cleanupProvider(input.subChatId) + + const activeStream = activeStreams.get(input.subChatId) + if (activeStream) { + activeStream.controller.abort() + activeStreams.delete(input.subChatId) + } + + return { success: true } + }), +}) diff --git a/src/main/lib/trpc/routers/index.ts b/src/main/lib/trpc/routers/index.ts index b98b18264..e8b6ed764 100644 --- a/src/main/lib/trpc/routers/index.ts +++ b/src/main/lib/trpc/routers/index.ts @@ -7,6 +7,7 @@ import { claudeSettingsRouter } from "./claude-settings" import { anthropicAccountsRouter } from "./anthropic-accounts" import { ollamaRouter } from "./ollama" import { codexRouter } from "./codex" +import { hermesRouter } from "./hermes" import { terminalRouter } from "./terminal" import { externalRouter } from "./external" import { filesRouter } from "./files" @@ -18,6 +19,12 @@ import { sandboxImportRouter } from "./sandbox-import" import { commandsRouter } from "./commands" import { voiceRouter } from "./voice" import { pluginsRouter } from "./plugins" +import { sharedResourcesRouter } from "./shared-resources" +import { agentRuntimeRouter } from "./agent-runtime" +import { petRuntimeRouter } from "./pet-runtime" +import { releaseReadinessRouter } from "./release-readiness" +import { mossAccountRouter } from "./moss-account" +import { mobileGatewayRouter } from "./mobile-gateway" import { createGitRouter } from "../../git" import { BrowserWindow } from "electron" @@ -35,6 +42,7 @@ export function createAppRouter(getWindow: () => BrowserWindow | null) { anthropicAccounts: anthropicAccountsRouter, ollama: ollamaRouter, codex: codexRouter, + hermes: hermesRouter, terminal: terminalRouter, external: externalRouter, files: filesRouter, @@ -46,6 +54,12 @@ export function createAppRouter(getWindow: () => BrowserWindow | null) { commands: commandsRouter, voice: voiceRouter, plugins: pluginsRouter, + sharedResources: sharedResourcesRouter, + agentRuntime: agentRuntimeRouter, + petRuntime: petRuntimeRouter, + releaseReadiness: releaseReadinessRouter, + mossAccount: mossAccountRouter, + mobileGateway: mobileGatewayRouter, // Git operations - named "changes" to match Superset API changes: createGitRouter(), }) diff --git a/src/main/lib/trpc/routers/mobile-gateway.ts b/src/main/lib/trpc/routers/mobile-gateway.ts new file mode 100644 index 000000000..c83f8d683 --- /dev/null +++ b/src/main/lib/trpc/routers/mobile-gateway.ts @@ -0,0 +1,8 @@ +import { readDesktopMobileGatewayPairingStatus } from "../../mobile-gateway/desktop-state" +import { publicProcedure, router } from "../index" + +export const mobileGatewayRouter = router({ + getPairingStatus: publicProcedure.query(() => + readDesktopMobileGatewayPairingStatus(), + ), +}) diff --git a/src/main/lib/trpc/routers/moss-account.ts b/src/main/lib/trpc/routers/moss-account.ts new file mode 100644 index 000000000..e747b20cf --- /dev/null +++ b/src/main/lib/trpc/routers/moss-account.ts @@ -0,0 +1,118 @@ +import { z } from "zod" +import { getAuthManager } from "../../../auth-manager" +import { + buildMossAccountEntitlement, + type MossAccountPlanQuota, +} from "../../moss-account" +import { + hasMossProviderSecret, + readMossProviderConfig, + type MossProviderConfig, +} from "../../moss-source" +import { publicProcedure, router } from "../index" + +const PLAN_FETCH_TIMEOUT_MS = 2500 + +async function buildStoredSecretSummary( + config: MossProviderConfig | undefined, +): Promise> { + const providerIds = Object.keys(config?.providers ?? {}) + const entries = await Promise.all( + providerIds.map(async (providerId) => [ + providerId, + { hasApiKey: await hasMossProviderSecret(providerId) }, + ] as const), + ) + return Object.fromEntries(entries) +} + +function normalizePlanQuota(planData: { + quota?: MossAccountPlanQuota | null + includedCredits?: number | null + usedCredits?: number | null + remainingCredits?: number | null + resetAt?: string | null + quotaUnit?: string | null +}): MossAccountPlanQuota | null { + const quota = planData.quota ?? null + if (quota) return { ...quota, source: "backend" } + + if ( + planData.includedCredits === undefined && + planData.usedCredits === undefined && + planData.remainingCredits === undefined && + planData.resetAt === undefined && + planData.quotaUnit === undefined + ) { + return null + } + + return { + includedCredits: planData.includedCredits, + usedCredits: planData.usedCredits, + remainingCredits: planData.remainingCredits, + resetAt: planData.resetAt, + unit: planData.quotaUnit, + source: "backend", + } +} + +async function fetchMossPlanSafely() { + const authManager = getAuthManager() + if (!authManager?.isAuthenticated()) return null + + let timeout: ReturnType | undefined + try { + return await Promise.race([ + authManager.fetchUserPlan().then((planData) => + planData + ? { + plan: planData.plan, + status: planData.status, + source: "backend" as const, + quota: normalizePlanQuota(planData), + } + : null, + ), + new Promise((resolve) => { + timeout = setTimeout(() => resolve(null), PLAN_FETCH_TIMEOUT_MS) + }), + ]) + } catch { + return null + } finally { + if (timeout) clearTimeout(timeout) + } +} + +export const mossAccountRouter = router({ + getEntitlement: publicProcedure + .input( + z + .object({ + projectPath: z.string().optional(), + }) + .optional(), + ) + .query(async ({ input }) => { + const authManager = getAuthManager() + const user = authManager?.getUser() ?? null + const plan = await fetchMossPlanSafely() + + const providerReadResult = input?.projectPath + ? await readMossProviderConfig(input.projectPath, { + createIfMissing: true, + }) + : null + const storedSecrets = await buildStoredSecretSummary( + providerReadResult?.config, + ) + + return buildMossAccountEntitlement({ + user, + plan, + providerReadResult, + storedSecrets, + }) + }), +}) diff --git a/src/main/lib/trpc/routers/pet-runtime.ts b/src/main/lib/trpc/routers/pet-runtime.ts new file mode 100644 index 000000000..ca7d4d1e8 --- /dev/null +++ b/src/main/lib/trpc/routers/pet-runtime.ts @@ -0,0 +1,35 @@ +import { publicProcedure, router } from "../index" +import { execFile } from "node:child_process" +import { promisify } from "node:util" +import { z } from "zod" +import { readPetRuntimeStatus } from "../../pet-runtime-status" + +const execFileAsync = promisify(execFile) + +export const petRuntimeRouter = router({ + getStatus: publicProcedure.query(() => readPetRuntimeStatus()), + + react: publicProcedure + .input(z.object({ intent: z.enum(["waving", "goodnight"]) })) + .mutation(async ({ input }) => { + const status = readPetRuntimeStatus() + if (!status.runtime.reactScriptExists) { + throw new Error("Codex official pet runtime is not installed") + } + + const result = await execFileAsync( + process.execPath, + [status.runtime.reactScriptPath, input.intent], + { + cwd: status.runtime.directory, + timeout: 15_000, + }, + ) + + return { + success: true, + stdout: result.stdout, + stderr: result.stderr, + } + }), +}) diff --git a/src/main/lib/trpc/routers/plugins.ts b/src/main/lib/trpc/routers/plugins.ts index 710cc0557..064866cad 100644 --- a/src/main/lib/trpc/routers/plugins.ts +++ b/src/main/lib/trpc/routers/plugins.ts @@ -11,12 +11,12 @@ import { } from "../../plugins" import { getEnabledPlugins } from "./claude-settings" -interface PluginComponent { +export interface PluginComponent { name: string description?: string } -interface PluginWithComponents { +export interface PluginWithComponents { name: string version: string description?: string diff --git a/src/main/lib/trpc/routers/release-readiness.ts b/src/main/lib/trpc/routers/release-readiness.ts new file mode 100644 index 000000000..9e99cad35 --- /dev/null +++ b/src/main/lib/trpc/routers/release-readiness.ts @@ -0,0 +1,435 @@ +import path from "node:path" +import { promises as fs } from "node:fs" +import { z } from "zod" +import { publicProcedure, router } from "../index" + +type GateStatus = "passed" | "pending" | "blocked" | "missing" | "failed" + +type ReleaseReport = { + status?: string + generatedAt?: string + scripts?: { + build?: string + packageMac?: string + distManifest?: string + distUpload?: string + distUploadDryRun?: string + releaseNotarize?: string + releaseEvidenceAudit?: string + } + mac?: { + targets?: Array<{ + target?: string + arch?: string[] + }> + hardenedRuntime?: boolean + entitlements?: string + entitlementsInherit?: string + publish?: { + provider?: string + url?: string + } + } + signing?: { + appleIdentityEnv?: string + electronBuilderIdentity?: string + notarizationMode?: string + electronBuilderNotarize?: boolean + releaseWorkflow?: { + workflowPath?: string + present?: boolean + uploadsEvidence?: boolean + } + credentialPreflight?: { + report?: string + status?: string + credentialsComplete?: boolean + missingCredentials?: string[] + toolsComplete?: boolean + missingTools?: string[] + blockers?: string[] + } | null + evidenceAudit?: { + report?: string + status?: string + requireNotarization?: boolean + distributable?: boolean + blockerCount?: number + validNotarizationReports?: string[] + acceptedSubmissions?: number + } | null + validNotarizationReports?: string[] + } + artifacts?: { + releaseDir?: string + files?: string[] + macArtifacts?: string[] + updateManifests?: string[] + notarizationEvidence?: string[] + } + distribution?: { + uploadScript?: string + uploadPlan?: { + manifest?: string + status?: string + dryRun?: boolean + provider?: string + channel?: string + artifactCount?: number + target?: { + baseUrl?: string + uploadPrefix?: string + } + } | null + } + warnings?: string[] + failures?: string[] +} + +const releaseLatestRelativePath = path.join( + ".1code", + "program", + "release-packaging", + "latest.json", +) + +async function readJson(filePath: string): Promise { + try { + const raw = await fs.readFile(filePath, "utf-8") + return JSON.parse(raw) as T + } catch { + return null + } +} + +async function exists(filePath: string): Promise { + try { + await fs.access(filePath) + return true + } catch { + return false + } +} + +async function listReleaseFiles(rootPath: string) { + const releaseDir = path.join(rootPath, "release") + try { + const entries = await fs.readdir(releaseDir, { withFileTypes: true }) + return entries + .filter((entry) => entry.isFile()) + .map((entry) => path.join("release", entry.name).split(path.sep).join("/")) + .sort() + } catch { + return [] + } +} + +function normalizeRootPath(projectPath?: string) { + if (projectPath && path.isAbsolute(projectPath)) return projectPath + return process.cwd() +} + +async function resolveReleaseRootPath(projectPath?: string) { + const candidateRootPath = normalizeRootPath(projectPath) + const cwdRootPath = process.cwd() + if (candidateRootPath === cwdRootPath) return candidateRootPath + + if (await exists(path.join(candidateRootPath, releaseLatestRelativePath))) { + return candidateRootPath + } + + if (await exists(path.join(cwdRootPath, releaseLatestRelativePath))) { + return cwdRootPath + } + + return candidateRootPath +} + +function gate( + id: string, + title: string, + status: GateStatus, + detail: string, + command: string, + evidence?: string | null, +) { + return { + id, + title, + status, + detail, + command, + evidence: evidence ?? null, + } +} + +export const releaseReadinessRouter = router({ + snapshot: publicProcedure + .input( + z + .object({ + projectPath: z.string().optional(), + }) + .optional(), + ) + .query(async ({ input }) => { + const rootPath = await resolveReleaseRootPath(input?.projectPath) + const latestPath = path.join(rootPath, releaseLatestRelativePath) + const latest = await readJson<{ + report?: string + generatedAt?: string + status?: string + }>(latestPath) + const reportPath = latest?.report + ? path.join(rootPath, latest.report) + : path.join( + rootPath, + ".1code", + "program", + "release-packaging", + "report.json", + ) + const report = await readJson(reportPath) + const releaseFiles = await listReleaseFiles(rootPath) + const reportArtifacts = report?.artifacts ?? {} + const macArtifacts = + reportArtifacts.macArtifacts?.length + ? reportArtifacts.macArtifacts + : releaseFiles.filter((filePath) => /\.(dmg|zip)$/i.test(filePath)) + const updateManifests = + reportArtifacts.updateManifests?.length + ? reportArtifacts.updateManifests + : releaseFiles.filter((filePath) => + /(?:latest|beta)-mac(?:-x64)?\.yml$/.test(path.basename(filePath)), + ) + const notarizationEvidence = + reportArtifacts.notarizationEvidence?.length + ? reportArtifacts.notarizationEvidence + : releaseFiles.filter((filePath) => + /notary|notar|codesign|staple|spctl/i.test(path.basename(filePath)), + ) + const validNotarizationReports = + report?.signing?.validNotarizationReports ?? [] + const credentialPreflight = report?.signing?.credentialPreflight ?? null + const evidenceAudit = report?.signing?.evidenceAudit ?? null + const uploadPlan = report?.distribution?.uploadPlan ?? null + const migrationEvidenceCandidates = [ + ".1code/program/migration-data-loss/latest.json", + ".1code/program/migration/latest.json", + ".1code/program/data-loss/latest.json", + ] + const migrationEvidence = ( + await Promise.all( + migrationEvidenceCandidates.map(async (candidate) => ({ + candidate, + present: await exists(path.join(rootPath, candidate)), + })), + ) + ).find((candidate) => candidate.present)?.candidate + + const preflightStatus: GateStatus = report + ? report.status === "passed" + ? "passed" + : "failed" + : "missing" + const artifactsStatus: GateStatus = + macArtifacts.length > 0 && updateManifests.length > 0 ? "passed" : "pending" + const signingStatus: GateStatus = + report?.signing?.appleIdentityEnv === "set" ? "passed" : "pending" + const credentialPreflightStatus: GateStatus = + credentialPreflight?.status === "passed" + ? "passed" + : credentialPreflight?.status === "blocked" + ? "blocked" + : credentialPreflight?.status === "failed" + ? "failed" + : report + ? "pending" + : "missing" + const releaseWorkflowStatus: GateStatus = + report?.signing?.releaseWorkflow?.present && report.signing.releaseWorkflow.uploadsEvidence + ? "passed" + : report + ? "pending" + : "missing" + const uploadPlanStatus: GateStatus = + uploadPlan?.status === "passed" && uploadPlan.dryRun && (uploadPlan.artifactCount ?? 0) >= 6 + ? "passed" + : report + ? "pending" + : "missing" + const notarizationStatus: GateStatus = + validNotarizationReports.length > 0 ? "passed" : "pending" + const evidenceAuditStatus: GateStatus = + evidenceAudit?.status === "passed" && evidenceAudit.distributable + ? "passed" + : evidenceAudit?.status === "blocked" + ? "blocked" + : evidenceAudit?.status === "failed" + ? "failed" + : report + ? "pending" + : "missing" + const migrationStatus: GateStatus = migrationEvidence ? "passed" : "pending" + + const gates = [ + gate( + "preflight", + "Preflight", + preflightStatus, + report + ? "Packaging configuration report is available." + : "No release packaging preflight report found.", + "bun run verify:packaging", + latest?.report, + ), + gate( + "artifacts", + "Artifacts", + artifactsStatus, + `${macArtifacts.length} macOS artifact(s), ${updateManifests.length} update manifest(s).`, + "bun run package:mac", + macArtifacts[0], + ), + gate( + "manifest", + "Update manifest", + updateManifests.length > 0 ? "passed" : "pending", + updateManifests.length > 0 + ? "macOS update manifest evidence is present." + : "Run manifest generation after packaging.", + "bun run dist:manifest", + updateManifests[0], + ), + gate( + "upload-plan", + "Upload plan", + uploadPlanStatus, + uploadPlan?.status === "passed" + ? `${uploadPlan.artifactCount ?? 0} CDN upload artifact(s) planned for ${uploadPlan.target?.baseUrl ?? "release CDN"}.` + : "No release CDN upload plan has been generated yet.", + "bun run dist:upload:dry-run", + uploadPlan?.manifest, + ), + gate( + "signing", + "Signing", + signingStatus, + report?.signing?.appleIdentityEnv === "set" + ? "APPLE_IDENTITY is available to electron-builder." + : "APPLE_IDENTITY is not set in this local environment.", + "APPLE_IDENTITY=... bun run package:mac", + report?.signing?.electronBuilderIdentity, + ), + gate( + "credential-preflight", + "Credential preflight", + credentialPreflightStatus, + credentialPreflight?.status === "passed" + ? "Apple signing/notarization credentials and local release tools are ready." + : credentialPreflight?.status === "blocked" + ? `Waiting for Apple credentials: ${(credentialPreflight.missingCredentials ?? []).join(", ") || "missing credentials"}.` + : credentialPreflight?.status === "failed" + ? "Apple credential preflight failed." + : "No Apple signing/notarization credential preflight report found.", + "bun run release:credentials", + credentialPreflight?.report, + ), + gate( + "ci-workflow", + "CI workflow", + releaseWorkflowStatus, + report?.signing?.releaseWorkflow?.present + ? "Release workflow declares signing credentials, notarization, stapling, verification, and evidence upload." + : "No Moss desktop release CI workflow evidence found.", + "workflow_dispatch: Moss Desktop Release", + report?.signing?.releaseWorkflow?.workflowPath, + ), + gate( + "notarization", + "Notarization", + notarizationStatus, + validNotarizationReports.length > 0 + ? "Passing notarytool, stapler, codesign, and spctl evidence was found." + : "No passing CI notarization report found yet.", + "bun run release:notarize", + validNotarizationReports[0] ?? notarizationEvidence[0], + ), + gate( + "evidence-audit", + "Evidence audit", + evidenceAuditStatus, + evidenceAudit?.status === "passed" && evidenceAudit.distributable + ? `${evidenceAudit.acceptedSubmissions ?? 0} accepted notarytool submission(s) and signed distribution evidence are audit-ready.` + : evidenceAudit?.status === "blocked" + ? `${evidenceAudit.blockerCount ?? 0} signed release evidence blocker(s) remain.` + : "No signed release evidence audit has been recorded yet.", + "bun run release:evidence:audit", + evidenceAudit?.report, + ), + gate( + "migration", + "Migration", + migrationStatus, + migrationEvidence + ? "Migration and data-loss evidence is present." + : "Migration and data-loss fixture evidence is still pending.", + "bun run verify:program --release", + migrationEvidence, + ), + ] + + return { + rootPath, + latestPath: latest ? ".1code/program/release-packaging/latest.json" : null, + reportPath: latest?.report ?? null, + generatedAt: report?.generatedAt ?? latest?.generatedAt ?? null, + status: report?.status ?? latest?.status ?? "missing", + scripts: { + build: report?.scripts?.build ?? "electron-vite build", + packageMac: report?.scripts?.packageMac ?? "electron-builder --mac", + distManifest: + report?.scripts?.distManifest ?? "node scripts/generate-update-manifest.mjs", + distUpload: + report?.scripts?.distUpload ?? "node scripts/upload-release.mjs", + distUploadDryRun: + report?.scripts?.distUploadDryRun ?? "node scripts/upload-release.mjs --dry-run", + releaseNotarize: + report?.scripts?.releaseNotarize ?? "node scripts/notarize-release-artifacts.mjs", + releaseEvidenceAudit: + report?.scripts?.releaseEvidenceAudit ?? "node scripts/audit-release-evidence.mjs", + verifyProgram: "node scripts/verify-program-ledger.mjs --release", + }, + mac: { + targets: report?.mac?.targets ?? [], + hardenedRuntime: report?.mac?.hardenedRuntime ?? false, + entitlements: report?.mac?.entitlements ?? null, + entitlementsInherit: report?.mac?.entitlementsInherit ?? null, + publish: report?.mac?.publish ?? null, + }, + signing: report?.signing ?? { + appleIdentityEnv: "missing", + electronBuilderIdentity: "missing", + notarizationMode: "external-ci", + electronBuilderNotarize: false, + validNotarizationReports: [], + evidenceAudit: null, + credentialPreflight: null, + }, + artifacts: { + releaseDir: reportArtifacts.releaseDir ?? "release", + files: releaseFiles, + macArtifacts, + updateManifests, + notarizationEvidence, + }, + distribution: report?.distribution ?? { + uploadScript: "scripts/upload-release.mjs", + uploadPlan: null, + }, + warnings: report?.warnings ?? [], + failures: report?.failures ?? [], + gates, + } + }), +}) diff --git a/src/main/lib/trpc/routers/shared-resources.ts b/src/main/lib/trpc/routers/shared-resources.ts new file mode 100644 index 000000000..925270bf8 --- /dev/null +++ b/src/main/lib/trpc/routers/shared-resources.ts @@ -0,0 +1,809 @@ +import { z } from "zod" +import * as fs from "fs/promises" +import * as path from "path" +import { buildSharedResourceSnapshot } from "../../shared-resources" +import { + ensureMossSource, + getMossSourceLayout, + materializeMossWorkspaceProjections, + removeMossProjectionResource, +} from "../../moss-source" +import { + parseMossFrontmatter, + stringifyMossFrontmatter, +} from "../../moss-source/frontmatter" +import { generateAgentMd, VALID_AGENT_MODELS } from "./agent-utils" +import { publicProcedure, router } from "../index" + +const mossMcpServerInput = z.object({ + projectPath: z.string().min(1), + name: z + .string() + .min(1) + .regex( + /^[a-zA-Z0-9_-]+$/, + "Name must contain only letters, numbers, underscores, and hyphens", + ), + transport: z.enum(["stdio", "http"]), + command: z.string().optional(), + args: z.array(z.string()).optional(), + env: z.record(z.string(), z.string()).optional(), + url: z.string().url().optional(), +}) + +const mossSubagentInput = z.object({ + projectPath: z.string().min(1), + name: z.string().min(1), + description: z.string().min(1), + prompt: z.string().min(1), + tools: z.array(z.string()).optional(), + disallowedTools: z.array(z.string()).optional(), + model: z.enum(VALID_AGENT_MODELS).optional(), +}) + +const mossMemoryEntryInput = z.object({ + projectPath: z.string().min(1), + name: z.string().min(1), + description: z.string().optional(), + content: z.string().optional(), +}) + +const mossHookInput = z.object({ + projectPath: z.string().min(1), + name: z.string().min(1), + description: z.string().optional(), + event: z.string().optional(), + command: z.string().optional(), + content: z.string().optional(), + enabled: z.boolean().optional(), +}) + +type MossMcpServerConfig = { + command?: string + args?: string[] + env?: Record + url?: string +} + +type MossMcpConfig = { + mcpServers?: Record + servers?: Record +} + +function safeSubagentName(name: string): string { + return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") +} + +function safeMemoryEntryName(name: string): string { + return name + .replace(/\.md$/i, "") + .toLowerCase() + .replace(/[^a-z0-9-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, "") +} + +function safeHookName(name: string): string { + return name + .replace(/\.md$/i, "") + .toLowerCase() + .replace(/[^a-z0-9-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, "") +} + +function assertSafeSubagentName(name: string): string { + const safeName = safeSubagentName(name) + if (!safeName || safeName.includes("..")) { + throw new Error("Invalid subagent name") + } + return safeName +} + +function assertSafeMemoryEntryName(name: string): string { + const safeName = safeMemoryEntryName(name) + if (!safeName || safeName.includes("..")) { + throw new Error("Invalid memory entry name") + } + return safeName +} + +function assertSafeHookName(name: string): string { + const safeName = safeHookName(name) + if (!safeName || safeName.includes("..")) { + throw new Error("Invalid hook name") + } + return safeName +} + +function buildMossMemoryEntryMd(input: { + name: string + description?: string + content?: string +}): string { + const content = input.content?.trimEnd() ?? "" + return stringifyMossFrontmatter(`${content}${content ? "\n" : ""}`, { + name: input.name, + ...(input.description?.trim() ? { description: input.description.trim() } : {}), + }) +} + +function buildMossHookMd(input: { + name: string + description?: string + event?: string + command?: string + content?: string + enabled?: boolean +}): string { + const content = input.content?.trimEnd() ?? "" + return stringifyMossFrontmatter(`${content}${content ? "\n" : ""}`, { + name: input.name, + enabled: input.enabled !== false, + event: input.event?.trim() || "Stop", + ...(input.command?.trim() ? { command: input.command.trim() } : {}), + ...(input.description?.trim() ? { description: input.description.trim() } : {}), + }) +} + +async function readMossMemoryEntryFile(sourcePath: string): Promise<{ + name: string + description: string + content: string +}> { + const raw = await fs.readFile(sourcePath, "utf-8") + const parsed = parseMossFrontmatter(raw) + return { + name: + typeof parsed.data.name === "string" && parsed.data.name.trim() + ? parsed.data.name + : path.basename(sourcePath, ".md"), + description: + typeof parsed.data.description === "string" + ? parsed.data.description + : "", + content: parsed.content.trimEnd(), + } +} + +async function readMossHookFile(sourcePath: string): Promise<{ + name: string + description: string + event: string + command: string + enabled: boolean + content: string +}> { + const raw = await fs.readFile(sourcePath, "utf-8") + const parsed = parseMossFrontmatter(raw) + return { + name: + typeof parsed.data.name === "string" && parsed.data.name.trim() + ? parsed.data.name + : path.basename(sourcePath, ".md"), + description: + typeof parsed.data.description === "string" + ? parsed.data.description + : "", + event: + typeof parsed.data.event === "string" && parsed.data.event.trim() + ? parsed.data.event + : "Stop", + command: + typeof parsed.data.command === "string" + ? parsed.data.command + : "", + enabled: parsed.data.enabled !== false, + content: parsed.content.trimEnd(), + } +} + +async function mossMemoryEntryPath(projectPath: string, name: string): Promise<{ + safeName: string + sourcePath: string +}> { + await ensureMossSource({ projectPath }) + const safeName = assertSafeMemoryEntryName(name) + return { + safeName, + sourcePath: path.join( + getMossSourceLayout(projectPath).memoryRoot, + `${safeName}.md`, + ), + } +} + +async function mossHookPath(projectPath: string, name: string): Promise<{ + safeName: string + sourcePath: string +}> { + await ensureMossSource({ projectPath }) + const safeName = assertSafeHookName(name) + return { + safeName, + sourcePath: path.join( + getMossSourceLayout(projectPath).hooksRoot, + `${safeName}.md`, + ), + } +} + +function buildMossMcpServerConfig(input: z.infer): MossMcpServerConfig { + if (input.transport === "stdio") { + const command = input.command?.trim() + if (!command) throw new Error("Command is required for stdio servers") + return { + command, + ...(input.args && input.args.length > 0 ? { args: input.args } : {}), + ...(input.env && Object.keys(input.env).length > 0 ? { env: input.env } : {}), + } + } + + const url = input.url?.trim() + if (!url) throw new Error("URL is required for HTTP servers") + return { url } +} + +async function readMossMcpConfig(projectPath: string): Promise<{ + sourcePath: string + config: MossMcpConfig +}> { + await ensureMossSource({ projectPath }) + const sourcePath = getMossSourceLayout(projectPath).mcpConfig + + try { + const raw = await fs.readFile(sourcePath, "utf-8") + const parsed = JSON.parse(raw) as MossMcpConfig + const mcpServers = parsed.mcpServers ?? parsed.servers ?? {} + return { + sourcePath, + config: { + ...parsed, + mcpServers, + }, + } + } catch (error) { + throw new Error( + `Unable to read Moss MCP source: ${ + error instanceof Error ? error.message : String(error) + }`, + ) + } +} + +async function writeMossMcpConfig( + projectPath: string, + config: MossMcpConfig, +): Promise { + const sourcePath = getMossSourceLayout(projectPath).mcpConfig + const nextConfig: MossMcpConfig = { + ...config, + mcpServers: config.mcpServers ?? {}, + } + delete nextConfig.servers + await fs.mkdir(path.dirname(sourcePath), { recursive: true }) + await fs.writeFile(sourcePath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf-8") + return sourcePath +} + +async function materializeProject( + projectPath: string, + options?: { expectedResourceIds?: readonly string[] }, +) { + return materializeMossWorkspaceProjections({ + projectPath, + createIfMissing: true, + expectedResourceIds: options?.expectedResourceIds, + }) +} + +async function unlinkProjectedSubagentIfManaged(params: { + projectPath: string + sourcePath: string + targetPath: string +}) { + try { + const stat = await fs.lstat(params.targetPath) + if (!stat.isSymbolicLink()) return + const linkTarget = await fs.readlink(params.targetPath) + const resolvedTarget = path.resolve(path.dirname(params.targetPath), linkTarget) + if (resolvedTarget !== path.resolve(params.sourcePath)) return + await fs.unlink(params.targetPath) + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error + } +} + +async function removeProjectedSubagentResource(params: { + projectPath: string + safeName: string + sourcePath: string +}) { + await removeMossProjectionResource({ + projectPath: params.projectPath, + resourceId: `moss:subagent:${params.safeName}`, + sourcePath: path.relative(params.projectPath, params.sourcePath), + targetPaths: [ + path.join(".claude", "agents", `${params.safeName}.md`), + path.join(".codex", "agents", `${params.safeName}.md`), + ], + removeTargets: true, + }) +} + +async function removeProjectedHookAdapters(params: { + projectPath: string + safeName: string + sourcePath: string +}) { + await removeMossProjectionResource({ + projectPath: params.projectPath, + resourceId: `moss:hook:${params.safeName}`, + sourcePath: path.relative(params.projectPath, params.sourcePath), + targetPaths: [ + path.join(".claude", "hooks", ".moss-adapter.json"), + path.join(".codex", "hooks", ".moss-adapter.json"), + ], + removeTargets: true, + }) +} + +export const sharedResourcesRouter = router({ + snapshot: publicProcedure + .input( + z + .object({ + projectPath: z.string().optional(), + }) + .optional(), + ) + .query(async ({ input }) => { + return buildSharedResourceSnapshot({ + projectPath: input?.projectPath, + }) + }), + + readMemoryEntry: publicProcedure + .input( + z.object({ + projectPath: z.string().min(1), + name: z.string().min(1), + }), + ) + .query(async ({ input }) => { + const { safeName, sourcePath } = await mossMemoryEntryPath( + input.projectPath, + input.name, + ) + const entry = await readMossMemoryEntryFile(sourcePath) + return { + ...entry, + safeName, + sourcePath, + } + }), + + createMemoryEntry: publicProcedure + .input(mossMemoryEntryInput) + .mutation(async ({ input }) => { + const { safeName, sourcePath } = await mossMemoryEntryPath( + input.projectPath, + input.name, + ) + + try { + await fs.access(sourcePath) + throw new Error(`Memory entry "${safeName}" already exists in Moss Unified Source`) + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error + } + + await fs.mkdir(path.dirname(sourcePath), { recursive: true }) + await fs.writeFile( + sourcePath, + buildMossMemoryEntryMd({ + name: safeName, + description: input.description, + content: input.content, + }), + "utf-8", + ) + + return { + success: true as const, + name: safeName, + sourcePath, + projections: await materializeProject(input.projectPath, { + expectedResourceIds: [`moss:memory:${safeName}`], + }), + } + }), + + updateMemoryEntry: publicProcedure + .input(mossMemoryEntryInput) + .mutation(async ({ input }) => { + const { safeName, sourcePath } = await mossMemoryEntryPath( + input.projectPath, + input.name, + ) + + await fs.writeFile( + sourcePath, + buildMossMemoryEntryMd({ + name: safeName, + description: input.description, + content: input.content, + }), + "utf-8", + ) + + return { + success: true as const, + name: safeName, + sourcePath, + projections: await materializeProject(input.projectPath, { + expectedResourceIds: [`moss:memory:${safeName}`], + }), + } + }), + + removeMemoryEntry: publicProcedure + .input( + z.object({ + projectPath: z.string().min(1), + name: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + const { safeName, sourcePath } = await mossMemoryEntryPath( + input.projectPath, + input.name, + ) + + let deleted = false + try { + await fs.unlink(sourcePath) + deleted = true + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error + } + + return { + success: true as const, + deleted, + name: safeName, + sourcePath, + projections: await materializeProject(input.projectPath), + } + }), + + readHook: publicProcedure + .input( + z.object({ + projectPath: z.string().min(1), + name: z.string().min(1), + }), + ) + .query(async ({ input }) => { + const { safeName, sourcePath } = await mossHookPath( + input.projectPath, + input.name, + ) + const hook = await readMossHookFile(sourcePath) + return { + ...hook, + safeName, + sourcePath, + } + }), + + createHook: publicProcedure + .input(mossHookInput) + .mutation(async ({ input }) => { + const { safeName, sourcePath } = await mossHookPath( + input.projectPath, + input.name, + ) + + try { + await fs.access(sourcePath) + throw new Error(`Hook "${safeName}" already exists in Moss Unified Source`) + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error + } + + await fs.mkdir(path.dirname(sourcePath), { recursive: true }) + await fs.writeFile( + sourcePath, + buildMossHookMd({ + name: safeName, + description: input.description, + event: input.event, + command: input.command, + content: input.content, + enabled: input.enabled, + }), + "utf-8", + ) + + return { + success: true as const, + name: safeName, + sourcePath, + projections: await materializeProject(input.projectPath, { + expectedResourceIds: [`moss:hook:${safeName}`], + }), + } + }), + + updateHook: publicProcedure + .input(mossHookInput) + .mutation(async ({ input }) => { + const { safeName, sourcePath } = await mossHookPath( + input.projectPath, + input.name, + ) + + await fs.writeFile( + sourcePath, + buildMossHookMd({ + name: safeName, + description: input.description, + event: input.event, + command: input.command, + content: input.content, + enabled: input.enabled, + }), + "utf-8", + ) + + return { + success: true as const, + name: safeName, + sourcePath, + projections: await materializeProject(input.projectPath, { + expectedResourceIds: [`moss:hook:${safeName}`], + }), + } + }), + + removeHook: publicProcedure + .input( + z.object({ + projectPath: z.string().min(1), + name: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + const { safeName, sourcePath } = await mossHookPath( + input.projectPath, + input.name, + ) + + let deleted = false + try { + await fs.unlink(sourcePath) + deleted = true + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error + } + + await removeProjectedHookAdapters({ + projectPath: input.projectPath, + safeName, + sourcePath, + }) + + return { + success: true as const, + deleted, + name: safeName, + sourcePath, + projections: await materializeProject(input.projectPath), + } + }), + + createMcpServer: publicProcedure + .input(mossMcpServerInput) + .mutation(async ({ input }) => { + const { sourcePath, config } = await readMossMcpConfig(input.projectPath) + const serverName = input.name.trim() + const mcpServers = config.mcpServers ?? {} + + if (mcpServers[serverName]) { + throw new Error(`MCP server "${serverName}" already exists in Moss Unified Source`) + } + + mcpServers[serverName] = buildMossMcpServerConfig(input) + await writeMossMcpConfig(input.projectPath, { + ...config, + mcpServers, + }) + + return { + success: true as const, + name: serverName, + sourcePath, + projections: await materializeProject(input.projectPath, { + expectedResourceIds: [`moss:mcp:${serverName}`], + }), + } + }), + + updateMcpServer: publicProcedure + .input(mossMcpServerInput) + .mutation(async ({ input }) => { + const { sourcePath, config } = await readMossMcpConfig(input.projectPath) + const serverName = input.name.trim() + const mcpServers = config.mcpServers ?? {} + + if (!mcpServers[serverName]) { + throw new Error(`MCP server "${serverName}" was not found in Moss Unified Source`) + } + + mcpServers[serverName] = buildMossMcpServerConfig(input) + await writeMossMcpConfig(input.projectPath, { + ...config, + mcpServers, + }) + + return { + success: true as const, + name: serverName, + sourcePath, + projections: await materializeProject(input.projectPath), + } + }), + + removeMcpServer: publicProcedure + .input( + z.object({ + projectPath: z.string().min(1), + name: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + const { sourcePath, config } = await readMossMcpConfig(input.projectPath) + const serverName = input.name.trim() + const mcpServers = config.mcpServers ?? {} + + if (!mcpServers[serverName]) { + return { + success: true as const, + deleted: false, + name: serverName, + sourcePath, + projections: await materializeProject(input.projectPath), + } + } + + delete mcpServers[serverName] + await writeMossMcpConfig(input.projectPath, { + ...config, + mcpServers, + }) + + return { + success: true as const, + deleted: true, + name: serverName, + sourcePath, + projections: await materializeProject(input.projectPath), + } + }), + + createSubagent: publicProcedure + .input(mossSubagentInput) + .mutation(async ({ input }) => { + await ensureMossSource({ projectPath: input.projectPath }) + const safeName = assertSafeSubagentName(input.name) + const sourcePath = path.join( + getMossSourceLayout(input.projectPath).subagentsRoot, + `${safeName}.md`, + ) + + try { + await fs.access(sourcePath) + throw new Error(`Subagent "${safeName}" already exists in Moss Unified Source`) + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error + } + + await fs.mkdir(path.dirname(sourcePath), { recursive: true }) + await fs.writeFile( + sourcePath, + generateAgentMd({ + name: safeName, + description: input.description, + prompt: input.prompt, + tools: input.tools, + disallowedTools: input.disallowedTools, + model: input.model, + }), + "utf-8", + ) + + return { + success: true as const, + name: safeName, + sourcePath, + projections: await materializeProject(input.projectPath, { + expectedResourceIds: [`moss:subagent:${safeName}`], + }), + } + }), + + updateSubagent: publicProcedure + .input(mossSubagentInput) + .mutation(async ({ input }) => { + await ensureMossSource({ projectPath: input.projectPath }) + const safeName = assertSafeSubagentName(input.name) + const sourcePath = path.join( + getMossSourceLayout(input.projectPath).subagentsRoot, + `${safeName}.md`, + ) + + await fs.writeFile( + sourcePath, + generateAgentMd({ + name: safeName, + description: input.description, + prompt: input.prompt, + tools: input.tools, + disallowedTools: input.disallowedTools, + model: input.model, + }), + "utf-8", + ) + + return { + success: true as const, + name: safeName, + sourcePath, + projections: await materializeProject(input.projectPath), + } + }), + + removeSubagent: publicProcedure + .input( + z.object({ + projectPath: z.string().min(1), + name: z.string().min(1), + }), + ) + .mutation(async ({ input }) => { + await ensureMossSource({ projectPath: input.projectPath }) + const safeName = assertSafeSubagentName(input.name) + const sourcePath = path.join( + getMossSourceLayout(input.projectPath).subagentsRoot, + `${safeName}.md`, + ) + + let deleted = false + try { + await fs.unlink(sourcePath) + deleted = true + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error + } + + await unlinkProjectedSubagentIfManaged({ + projectPath: input.projectPath, + sourcePath, + targetPath: path.join(input.projectPath, ".claude", "agents", `${safeName}.md`), + }) + await removeProjectedSubagentResource({ + projectPath: input.projectPath, + safeName, + sourcePath, + }) + + return { + success: true as const, + deleted, + name: safeName, + sourcePath, + projections: await materializeProject(input.projectPath), + } + }), +}) diff --git a/src/main/lib/trpc/routers/skill-md.ts b/src/main/lib/trpc/routers/skill-md.ts new file mode 100644 index 000000000..587abc5da --- /dev/null +++ b/src/main/lib/trpc/routers/skill-md.ts @@ -0,0 +1,37 @@ +import { + parseMossFrontmatter, + stringifyMossFrontmatter, +} from "../../moss-source/frontmatter" + +export function parseSkillMd(rawContent: string): { + name?: string + description?: string + content: string +} { + try { + const { data, content } = parseMossFrontmatter(rawContent) + return { + name: typeof data.name === "string" ? data.name : undefined, + description: typeof data.description === "string" ? data.description : undefined, + content: content.trim(), + } + } catch (err) { + console.error("[skills] Failed to parse frontmatter:", err) + return { content: rawContent.trim() } + } +} + +export function generateSkillMd(skill: { + name: string + description: string + content: string +}): string { + const frontmatter: Record = { + name: skill.name, + } + if (skill.description) { + frontmatter.description = skill.description + } + const body = skill.content ? `\n${skill.content}` : "" + return stringifyMossFrontmatter(body, frontmatter) +} diff --git a/src/main/lib/trpc/routers/skills.ts b/src/main/lib/trpc/routers/skills.ts index 87adebd9d..e9106274c 100644 --- a/src/main/lib/trpc/routers/skills.ts +++ b/src/main/lib/trpc/routers/skills.ts @@ -3,46 +3,43 @@ import { router, publicProcedure } from "../index" import * as fs from "fs/promises" import * as path from "path" import * as os from "os" -import matter from "gray-matter" import { discoverInstalledPlugins, getPluginComponentPaths } from "../../plugins" import { isDirentDirectory } from "../../fs/dirent" import { getEnabledPlugins } from "./claude-settings" +import { + ensureMossSource, + getMossSourceLayout, + materializeMossWorkspaceProjections, + removeMossProjectionResource, +} from "../../moss-source" +import { generateSkillMd, parseSkillMd } from "./skill-md" + +type SkillSource = "moss" | "user" | "project" | "plugin" export interface FileSkill { name: string description: string - source: "user" | "project" | "plugin" + source: SkillSource pluginName?: string path: string content: string } -/** - * Parse SKILL.md frontmatter to extract name and description - */ -function parseSkillMd(rawContent: string): { name?: string; description?: string; content: string } { - try { - const { data, content } = matter(rawContent) - return { - name: typeof data.name === "string" ? data.name : undefined, - description: typeof data.description === "string" ? data.description : undefined, - content: content.trim(), - } - } catch (err) { - console.error("[skills] Failed to parse frontmatter:", err) - return { content: rawContent.trim() } - } -} - /** * Scan a directory for SKILL.md files */ async function scanSkillsDirectory( dir: string, - source: "user" | "project" | "plugin", + source: SkillSource, basePath?: string, // For project skills, the cwd to make paths relative to + options?: { + skipResolvedUnder?: string + }, ): Promise { const skills: FileSkill[] = [] + const skipResolvedRoot = options?.skipResolvedUnder + ? path.resolve(options.skipResolvedUnder) + : undefined try { // Check if directory exists @@ -69,12 +66,22 @@ async function scanSkillsDirectory( try { await fs.access(skillMdPath) + if (skipResolvedRoot) { + const realSkillDir = await fs.realpath(path.dirname(skillMdPath)) + if ( + realSkillDir === skipResolvedRoot || + realSkillDir.startsWith(`${skipResolvedRoot}${path.sep}`) + ) { + continue + } + } + const content = await fs.readFile(skillMdPath, "utf-8") const parsed = parseSkillMd(content) - // For project skills, show relative path; for user skills, show ~/.claude/... path + // For project/Moss skills, show relative path; for user skills, show ~/.claude/... path let displayPath: string - if (source === "project" && basePath) { + if ((source === "project" || source === "moss") && basePath) { displayPath = path.relative(basePath, skillMdPath) } else { // For user skills, show ~/.claude/skills/... format @@ -115,10 +122,15 @@ const listSkillsProcedure = publicProcedure const userSkillsDir = path.join(os.homedir(), ".claude", "skills") const userSkillsPromise = scanSkillsDirectory(userSkillsDir, "user") + let mossSkillsPromise = Promise.resolve([]) let projectSkillsPromise = Promise.resolve([]) if (input?.cwd) { + const mossSkillsDir = path.join(input.cwd, ".moss", "skills") + mossSkillsPromise = scanSkillsDirectory(mossSkillsDir, "moss", input.cwd) const projectSkillsDir = path.join(input.cwd, ".claude", "skills") - projectSkillsPromise = scanSkillsDirectory(projectSkillsDir, "project", input.cwd) + projectSkillsPromise = scanSkillsDirectory(projectSkillsDir, "project", input.cwd, { + skipResolvedUnder: mossSkillsDir, + }) } // Discover plugin skills @@ -140,39 +152,89 @@ const listSkillsProcedure = publicProcedure }) // Scan all directories in parallel - const [userSkills, projectSkills, ...pluginSkillsArrays] = + const [userSkills, mossSkills, projectSkills, ...pluginSkillsArrays] = await Promise.all([ userSkillsPromise, + mossSkillsPromise, projectSkillsPromise, ...pluginSkillsPromises, ]) const pluginSkills = pluginSkillsArrays.flat() - return [...projectSkills, ...userSkills, ...pluginSkills] + return [...mossSkills, ...projectSkills, ...userSkills, ...pluginSkills] }) -/** - * Generate SKILL.md content from name, description, and body - */ -function generateSkillMd(skill: { name: string; description: string; content: string }): string { - const frontmatter: string[] = [] - frontmatter.push(`name: ${skill.name}`) - if (skill.description) { - frontmatter.push(`description: ${skill.description}`) - } - return `---\n${frontmatter.join("\n")}\n---\n\n${skill.content}` -} - /** * Resolve the absolute filesystem path of a skill given its display path */ -function resolveSkillPath(displayPath: string): string { +function resolveSkillPath(displayPath: string, cwd?: string): string { if (displayPath.startsWith("~")) { return path.join(os.homedir(), displayPath.slice(1)) } + if (!path.isAbsolute(displayPath)) { + if (!cwd) throw new Error("Project path (cwd) required for project-relative skills") + return path.join(cwd, displayPath) + } return displayPath } +function safeSkillName(name: string): string { + return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") +} + +function assertSafeSkillName(name: string): string { + const safeName = safeSkillName(name) + if (!safeName || safeName.includes("..")) { + throw new Error("Skill name must contain at least one alphanumeric character") + } + return safeName +} + +function isUnderPath(filePath: string, rootPath: string): boolean { + const resolvedFile = path.resolve(filePath) + const resolvedRoot = path.resolve(rootPath) + return resolvedFile === resolvedRoot || resolvedFile.startsWith(`${resolvedRoot}${path.sep}`) +} + +async function resolveWritableSkill( + displayPath: string, + cwd?: string, +): Promise<{ + absolutePath: string + skillDir: string + skillName: string + isMoss: boolean +}> { + const absolutePath = resolveSkillPath(displayPath, cwd) + let writablePath = absolutePath + let isMoss = false + + if (cwd) { + const mossSkillsRoot = getMossSourceLayout(cwd).skillsRoot + if (isUnderPath(absolutePath, mossSkillsRoot)) { + isMoss = true + } else { + try { + const realPath = await fs.realpath(absolutePath) + if (isUnderPath(realPath, mossSkillsRoot)) { + writablePath = realPath + isMoss = true + } + } catch { + // The caller will handle missing files through fs.access below. + } + } + } + + const skillDir = path.dirname(writablePath) + return { + absolutePath: writablePath, + skillDir, + skillName: path.basename(skillDir), + isMoss, + } +} + export const skillsRouter = router({ /** * List all skills from filesystem @@ -195,18 +257,21 @@ export const skillsRouter = router({ name: z.string(), description: z.string(), content: z.string(), - source: z.enum(["user", "project"]), + source: z.enum(["moss", "user", "project"]), cwd: z.string().optional(), }) ) .mutation(async ({ input }) => { - const safeName = input.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") - if (!safeName) { - throw new Error("Skill name must contain at least one alphanumeric character") - } + const safeName = assertSafeSkillName(input.name) let targetDir: string - if (input.source === "project") { + if (input.source === "moss") { + if (!input.cwd) { + throw new Error("Project path (cwd) required for Moss skills") + } + await ensureMossSource({ projectPath: input.cwd }) + targetDir = getMossSourceLayout(input.cwd).skillsRoot + } else if (input.source === "project") { if (!input.cwd) { throw new Error("Project path (cwd) required for project skills") } @@ -239,9 +304,18 @@ export const skillsRouter = router({ await fs.writeFile(skillMdPath, fileContent, "utf-8") + if (input.source === "moss" && input.cwd) { + await materializeMossWorkspaceProjections({ + projectPath: input.cwd, + expectedResourceIds: [`moss:skill:${safeName}`], + }) + } + return { name: safeName, - path: skillMdPath, + path: input.source === "moss" && input.cwd + ? path.relative(input.cwd, skillMdPath) + : skillMdPath, source: input.source, } }), @@ -260,12 +334,10 @@ export const skillsRouter = router({ }) ) .mutation(async ({ input }) => { - const absolutePath = input.cwd && !input.path.startsWith("~") && !input.path.startsWith("/") - ? path.join(input.cwd, input.path) - : resolveSkillPath(input.path) + const skill = await resolveWritableSkill(input.path, input.cwd) // Verify file exists before writing - await fs.access(absolutePath) + await fs.access(skill.absolutePath) const fileContent = generateSkillMd({ name: input.name, @@ -273,7 +345,14 @@ export const skillsRouter = router({ content: input.content, }) - await fs.writeFile(absolutePath, fileContent, "utf-8") + await fs.writeFile(skill.absolutePath, fileContent, "utf-8") + + if (skill.isMoss && input.cwd) { + await materializeMossWorkspaceProjections({ + projectPath: input.cwd, + expectedResourceIds: [`moss:skill:${skill.skillName}`], + }) + } return { success: true } }), @@ -293,14 +372,25 @@ export const skillsRouter = router({ throw new Error("Invalid path") } - const absolutePath = input.cwd && !input.path.startsWith("~") && !input.path.startsWith("/") - ? path.join(input.cwd, input.path) - : resolveSkillPath(input.path) - - // Skills are directories containing SKILL.md — delete the parent directory - const skillDir = path.dirname(absolutePath) - await fs.access(skillDir) - await fs.rm(skillDir, { recursive: true }) + const skill = await resolveWritableSkill(input.path, input.cwd) + + // Skills are directories containing SKILL.md - delete the parent directory. + await fs.access(skill.skillDir) + await fs.rm(skill.skillDir, { recursive: true, force: true }) + + if (skill.isMoss && input.cwd) { + await removeMossProjectionResource({ + projectPath: input.cwd, + resourceId: `moss:skill:${skill.skillName}`, + sourcePath: path.join(".moss", "skills", skill.skillName), + targetPaths: [ + path.join(".claude", "skills", skill.skillName), + path.join(".codex", "skills", skill.skillName), + ], + removeTargets: true, + }) + await materializeMossWorkspaceProjections({ projectPath: input.cwd }) + } return { success: true } }), diff --git a/src/main/lib/vscode-theme-scanner.ts b/src/main/lib/vscode-theme-scanner.ts index f2468e8f8..0f6d6f714 100644 --- a/src/main/lib/vscode-theme-scanner.ts +++ b/src/main/lib/vscode-theme-scanner.ts @@ -127,7 +127,7 @@ async function scanExtensionsDir(extensionsDir: string, source: EditorSource): P // Create Dirent-like objects from ls output const entries_final = await Promise.all( - lsEntries.map(async (name) => { + lsEntries.map(async (name: string) => { const fullPath = path.join(extensionsDir, name) try { const stat = await fs.stat(fullPath) diff --git a/src/main/windows/main.ts b/src/main/windows/main.ts index 15dcdd137..bf0a7c348 100644 --- a/src/main/windows/main.ts +++ b/src/main/windows/main.ts @@ -10,16 +10,26 @@ import { nativeImage, dialog, } from "electron" -import { join } from "path" +import { join, isAbsolute } from "path" import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs" +import { homedir } from "os" +import { fileURLToPath } from "url" import { createIPCHandler } from "trpc-electron/main" import { createAppRouter } from "../lib/trpc/routers" import { getAuthManager, handleAuthCode, getBaseUrl } from "../index" import { registerGitWatcherIPC } from "../lib/git/watcher" import { hasActiveClaudeSessions, abortAllClaudeSessions } from "../lib/trpc/routers/claude" import { hasActiveCodexStreams, abortAllCodexStreams } from "../lib/trpc/routers/codex" +import { hasActiveHermesStreams, abortAllHermesStreams } from "../lib/trpc/routers/hermes" import { registerThemeScannerIPC } from "../lib/vscode-theme-scanner" import { windowManager } from "./window-manager" +import { + createLocalCodexAutomation, + deleteLocalCodexAutomation, + listLocalCodexAutomations, + runLocalCodexAutomationNow, + updateLocalCodexAutomation, +} from "../lib/codex-automations" // Flag to bypass close confirmation when app.quit() has already been confirmed let isQuitting = false @@ -37,6 +47,27 @@ function getWindowFromEvent( return win && !win.isDestroyed() ? win : null } +function normalizeShellPath(input: string): string | null { + if (typeof input !== "string") return null + const trimmedPath = input.trim() + if (!trimmedPath) return null + + if (trimmedPath.startsWith("file://")) { + try { + return fileURLToPath(trimmedPath) + } catch { + return null + } + } + + if (trimmedPath === "~") return homedir() + if (trimmedPath.startsWith("~/")) { + return join(homedir(), trimmedPath.slice(2)) + } + + return isAbsolute(trimmedPath) ? trimmedPath : null +} + // Register IPC handlers for window operations (only once) let ipcHandlersRegistered = false @@ -80,7 +111,7 @@ function registerIpcHandlers(): void { // Note: Update checking is now handled by auto-updater module (lib/auto-updater.ts) ipcMain.handle("app:set-badge", (event, count: number | null) => { const win = getWindowFromEvent(event) - if (process.platform === "darwin") { + if (process.platform === "darwin" && app.dock) { app.dock.setBadge(count ? String(count) : "") } else if (process.platform === "win32" && win) { // Windows: Update title with count as fallback @@ -285,9 +316,40 @@ function registerIpcHandlers(): void { }) // Shell - ipcMain.handle("shell:open-external", (_event, url: string) => - shell.openExternal(url), - ) + ipcMain.handle("shell:open-external", async (_event, url: string) => { + if (typeof url !== "string") return { ok: false, reason: "invalid-url" } + const trimmedUrl = url.trim() + if (!trimmedUrl) return { ok: false, reason: "invalid-url" } + + try { + const parsedUrl = new URL(trimmedUrl) + if (!["http:", "https:", "mailto:"].includes(parsedUrl.protocol)) { + return { ok: false, reason: "unsupported-protocol" } + } + await shell.openExternal(parsedUrl.toString()) + return { ok: true } + } catch { + return { ok: false, reason: "invalid-url" } + } + }) + + ipcMain.handle("shell:open-path", async (_event, targetPath: string) => { + const localPath = normalizeShellPath(targetPath) + if (!localPath) return { ok: false, reason: "invalid-path" } + + const error = await shell.openPath(localPath) + if (error) return { ok: false, reason: error } + return { ok: true } + }) + + ipcMain.handle("shell:reveal-path", async (_event, targetPath: string) => { + const localPath = normalizeShellPath(targetPath) + if (!localPath) return { ok: false, reason: "invalid-path" } + if (!existsSync(localPath)) return { ok: false, reason: "path-not-found" } + + shell.showItemInFolder(localPath) + return { ok: true } + }) // Clipboard ipcMain.handle("clipboard:write", (_event, text: string) => @@ -346,6 +408,45 @@ function registerIpcHandlers(): void { } } + // Local Codex automations. These mirror the Codex Desktop + // ~/.codex/automations//automation.toml contract for offline parity. + ipcMain.handle("codex-automations:list", async (event) => { + if (!validateSender(event)) return [] + return listLocalCodexAutomations() + }) + + ipcMain.handle( + "codex-automations:create", + async (event, input: Record) => { + if (!validateSender(event)) return null + return createLocalCodexAutomation(input ?? {}) + }, + ) + + ipcMain.handle( + "codex-automations:update", + async (event, input: Record) => { + if (!validateSender(event)) return null + return updateLocalCodexAutomation(input ?? {}) + }, + ) + + ipcMain.handle( + "codex-automations:delete", + async (event, input: Record) => { + if (!validateSender(event)) return null + return deleteLocalCodexAutomation(input ?? {}) + }, + ) + + ipcMain.handle( + "codex-automations:run-now", + async (event, input: Record) => { + if (!validateSender(event)) return null + return runLocalCodexAutomationNow(input ?? {}) + }, + ) + ipcMain.handle("auth:get-user", (event) => { if (!validateSender(event)) return null return getAuthManager().getUser() @@ -642,10 +743,20 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): contextIsolation: true, sandbox: false, // Required for electron-trpc webSecurity: true, + webviewTag: true, partition: "persist:main", // Use persistent session for cookies }, }) + window.webContents.on("will-attach-webview", (event, webPreferences) => { + delete webPreferences.preload + webPreferences.nodeIntegration = false + webPreferences.contextIsolation = true + webPreferences.sandbox = true + webPreferences.webSecurity = true + webPreferences.allowRunningInsecureContent = false + }) + // Register window with manager and get stable ID for localStorage namespacing const stableWindowId = windowManager.register(window) console.log( @@ -710,7 +821,7 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): if (!input.shift) { // Block Cmd+R entirely event.preventDefault() - } else if (hasActiveClaudeSessions() || hasActiveCodexStreams()) { + } else if (hasActiveClaudeSessions() || hasActiveCodexStreams() || hasActiveHermesStreams()) { // Cmd+Shift+R with active streams — intercept and confirm event.preventDefault() dialog @@ -728,6 +839,7 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): if (response === 1) { abortAllClaudeSessions() abortAllCodexStreams() + abortAllHermesStreams() window.webContents.reloadIgnoringCache() } }) @@ -737,7 +849,14 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): // Handle external links window.webContents.setWindowOpenHandler(({ url }) => { - shell.openExternal(url) + try { + const parsedUrl = new URL(url) + if (["http:", "https:", "mailto:"].includes(parsedUrl.protocol)) { + shell.openExternal(parsedUrl.toString()).catch(() => undefined) + } + } catch { + // Ignore malformed window-open requests instead of surfacing a main-process error. + } return { action: "deny" } }) @@ -748,10 +867,11 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): // Still abort sessions gracefully so partial state is saved abortAllClaudeSessions() abortAllCodexStreams() + abortAllHermesStreams() return } - if (hasActiveClaudeSessions() || hasActiveCodexStreams()) { + if (hasActiveClaudeSessions() || hasActiveCodexStreams() || hasActiveHermesStreams()) { event.preventDefault() dialog .showMessageBox(window, { @@ -768,6 +888,7 @@ export function createWindow(options?: { chatId?: string; subChatId?: string }): if (response === 1) { abortAllClaudeSessions() abortAllCodexStreams() + abortAllHermesStreams() window.destroy() } }) diff --git a/src/renderer/assets/app-icons/computer-use-plugin-icon.png b/src/renderer/assets/app-icons/computer-use-plugin-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7a1db0454d7cf819bebf7a7b4d9c1693cc4c0de8 GIT binary patch literal 21091 zcmV)rK$*XZP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91fS>~a1ONa40RR91fB*mh07#AmcK`rD07*naRCocEz1y$t*LC0b+xzUp zb4ZF3bt5HHB1MUEWm&$caT42cQX_@i0xb%(eP~f2eW(lcseLHW|D(?V3iOf&2%OeU z6E~I|E4D1hce}DJ>uS-uIhL#o@8r4eem>tZ#$3O(&Y`SKp1H<-%rWN}bN+tox7OM` zJOAGO~5_p87Qy3qc>IURVdap5(1*Xe=K;`gOnSH1CZ?aFp}l~Bh2jOUosQLlI^ z-tC+fF;@Q79E1|WT_0L#bE*HTcG>H#UeW?>(ls@X4AW!D{%-Acb&u@7Tk`deaweyd zc!3H;`Crl0-#3@Qz8d2XFA#%8gE)C<;a%zOcbX0azXLyg_@k?=<$E89)4H-@7?mCxwFmkK<}67G$AHoc1DtjCwZ#~28X|jV04hBX!f$<30rS;)>y3r$k-DT z-t@Km(Mf^_qH{_o>B_YS#>c>&&&5YJC(eQ=JjhBh_WLF| z36p;!-m5O$<=MBP2PzZ4n2MC4M5jiXT-IDDY2Q}rd@ z36AvOk1L^z=g#EDaC0|?2Y-q{dLGxa=GuSv8dJpkC<7SbNr5GH$WhTer=hREmAmk@;LiA01c;G`P(bC9n z1ML^|x%Lm7LK&Y6qRYQJ6~sFxd3IkWUW1ee_%~?ra*w_~4wAb258X%2g*O2Qh3^e_ zlvA4w)DZ_qAy8$IdC;JpUD+IW<81E}@x4u>eH(*Q{!VAcOR=FRZBnKPhE6dkLuc?g z0YbxHN54AiVrA$(3gu>SgygeOe5t9M-OJz*;wv6pPF>E&k1#XMt81 zJRG?ss52ef_O8AgK2L+P+q6Y_>F@9s9nGkRs%u_Gz5h%6`Ldj}-q7RdKISu@QB`I9 z3GI{Yh08nb2v_38acGj$#an;rz?*`ZB8mT#@rIWT2ys*dO&hz5w4X~@ywrw5F-me@ zzp3JQ(}s2fdI|8-PznQEbBs=*$`jPU_!z~VA6fg|pIyJxo1juiLjYqZjjxG*%!Xc2 zypG*Up9j%(hj=S zD(2P?Zzt3D>$Ui3UrXo6X>%Q0JwW=N1`n_0Ng#TYupI3Wcd3&7rC?5g$gxcU;Arui zm(!*FO;N;m$09xj=(=RxjK#Qw{%3&71UtMqC$GwF(I4eumZ)CaVY4oLPCkp&yey3P znBNz|41b`0#eoTY>`&;}jI3?5L*cE^;I})t)wEl3S*=sjqEGQhUkSfcv&8h{I!CeT6RoFL+$^$a<7j(W5^QgJrYF$6g_Af}O%BZojIdLbsUg=nAlf z;|OvJbM%F-)!K>B`=sxaEB|z-rwFiFiQnN#J~sLu`9AS?2~s*S?4b~vxjMB=o0Zs5X~zp_;!U0k$ck_J9Ojs5sa^^M#BTCf6@1uuRYsuCX17FsCMaCDYS z81h25p1MmevExe)?bzz2?Qt2)X`zxPHpg&_C)OkjS(1({aA*wo*Z(PwN%~4SdPy>N zN4p=|u^np_fjsE_TCR9o*CHs(2BGEI^jPhPJ_^6yx`3k{y@}@T22gw^w|aa2@X#@amRU?h6lQKKaAiFl_!DH z;HwxK&+`C$C-Hlo_PGiIdVB?TJo>z+2q#u#pxv}8AK^(ZB%-`5_JP${Ni-=>#lE=e z=roxw{)`w}0c{z8xESh;d=eeK;UkQEphDUaZ)7_douOmVHms7i_iJlHBDTFEcVdZP$^BACSi7MIf?kj!Tj=26E%FNU{sTrIRdP?!YuzNigtr zliqf7b9ht;$mCA}ta-x=F2uLE?Q)JTaXR8dJ7SDMi(mEHy4T*)Y77T7^4 zk`9lTm1ucQtK;N`4wQ$Fa|C0b2OiQeqCYmm!Sy+KL+2G9P$aYrN#g_;hUTTwVnlhz z8(jm?)|X!z)702|t{BU&j8zuQAy&h=WSNn?y#|^5Lm-)V0ILiH5H|vpC`*aAL}5 zlF?O9J};998N1o=`I{_6fmU|n5F}kn(#G-XC{4U$rWg%K` z;q6zQ@TBbwa+Gj*xu&T%_#VPH@vvf-k7kv(er%wPJ^mZ+ldSE?Ko6}aJd31`Ei|&6 zs-q)6(!)#C2_*CD^_aODUy><=@UoB(gK&}$gD_U^DIh<^ARN24(U0pBDBc~_tV!Pb ziKCM_E_is~k64Y!odo~7>81RHC%KS_j$cZhLOQ!VoB-`W%?WCc4{t$`na~lh?PtR7 z!u@2$a|48tc1O6jtv`v5Z*^Ngw0c7`(ZOd$oVyEYTggY?f{4x@T9vf%d)M*d33FVF zBb=jsC0O|sgK~V>*QJ3mqT4WQg&t;W^UV6`U))eYqcga4IxmSII!z`v_0GSh)e=Az z3F2dcHoHdAon811*x{eT1dDU@`$OMvnkury=COBQ{C zQD-{5EKJ)?b-Ree*Y=*S&-N?yCNa=4SdhrzO)Rj&lb)ggt+*W9&PyP87EYf2n$BuT zz?-f-U)$aB#x~I87~3wUoB+6RPTA?m)+GRDE&<2_k==8d7+c2F$65#QiC?%s2_O%1 zH0GL>>@;`3>yqYNpl7PZ+`o?E@M5r40koB1)Ut>W;Td$}1}|?9Ga-2hQVcDZcCprk zm5(0q(wnMV+jiqGtKWP9`UZUTJnqh~u~Xhce+>M=j->}i~|i+-C+!IxxF^Ao}k3*lP=a`5apR@`1TGg74eG*$9Y; zuaaw^(q6Lt`R*>+i1V5!%IPrPT+TcNdaVGdL(ht1Op=O;!bel zs~bGTQ6a#SHsK~eiN}dYm5b_xU;fs%ef^KN?ZtO*+s*$TogYRq4@^R@Gr7`haSW{K;RUK|6QYw71vK-8Hc>O*%n{ih zCgJcbj5!Sk`kBileW1?+;+s`2xQm;y_?Nck0%PW9h|2SnFfgj*iIZXGQLNqTc$8I2 z>mH!#I91JV!fcs1&_D)Hz$mSqEv_y)Cc|BxT_KO|ntKd>=gZsn)h}+_OaBGC{%2tJ z7#T1Q+Q0ymu}YUu;QK$vo_^=^6vM}0f1G0YB>8_c{b~HawRDY7U$aogYzqV*I&9)m zFV|7K|u@w`{nVWIT=U;{U)(x5>QMHG zG&x5r6?XHB|06t(d}VRYeZa=Gf4fPaZRDUT7$3H0?DXvc)Ssch zAN*&Dhg0O%M^X6=Cc^#0)pr1&f~F|e4kjj#^|e^~9N)k*{liNvym->Z7MROHtIKx& z&eP;s_|7#)J21s=qLJC1lDl5$=PfzljJ45PI;$2VK=F(oH2> z6o!Bf%F8m0);%xxzX#ABTYU^PScAzTfJ+;6Ced^j1)d9F5oC7(UE>!x_P_EacK-i> z4GDx@#;*eHCg0VHV_+r#q$NqNx+_W{oKXlHH;CJZtKWTvF@Kl-5oEwC___LS)SqN* zE*F^xi=SpN4n7@W{HxTmRRpkN3rz7N&zxai!Qau`(V661@U#1co(dQ0V=wJAaxa0N zmy~sb=s44n&1G$hU_o?T+kNvEaacz<f>P zoC4Vm=qBmtWjtx{Tg>XKpWU|SIl^!4lm9Y8lke^~`K}<2eT(Mx*A_n_CWn6J>RAjF z!p4oFr@|RGh1=f&+P}jh{yI076vGC*g6`G#Fwu7*dk7vAjBg$5(->W(J(Qn?0H|+= zC(E$xNgxc@!LXB5d?ev0NINBA3Ils(IVt3$v(`{PJpJy+kluv?MPrP_6TFhHO)g`| z?%&W*={~J)0$VUTtI}|opb$&}Sg}q>{!HMuJai4#ov%EZ!_K4s8(-r1e~L~1?-V=1 zyoSyse~x|(&v2RqVH-UmK5eOb206p$>J|e1j7z{7r@|R;7tj8q>f4_a=dVx& zv*-__`d$|Mou%s}bRYk-(x(uy3+{Z6I(nHu{1iXXB${BMQxY0p3ZP*M1zUOQ9=%0` zcQgj`mNq0?5)UuSAt5hx;h9V-q2?96nqhSBec{KUeimSB7eT~zY|_OILfVOXTzNfH zmYW-7KGkxv7Q$TrO_umgX8QH7Z`&(Bivo0-t78GUMP}!_A|`)U75|wF(3a#k^vDj~ zE1hCsWj!6vI90qvq!`{ttp6oA{qhUArwCsA82BH;e+!4l`+#r9sh1RDH@uALnm8^{ zFuOkqfRW7roKS^s0K?gLr!=R4iO=5#z|M8@t9Us*h5AHp_cA)eXFMW6ob?YxY2s1f zFMOv(Gdwwae$B0T&hj8T6o9;ziP}>DAaglzqT=hV5P0r(;pe&seT(i}JriVMyvWXc zp2ha2FHOf5fW4ERU7uBimqD<1HPFRPnoyquMYOXR&RsDftAEDjB!$4%JAVm&7sHjO zLH}Fm|ArwxMVwqBUi}0#_YB@VKJZ@VLYyfs$EHof2TtNs{KV$4hMP`2PA3Hb?mdE2 zOyV0}WdgUDl*wO3fS+t6c-rQkfTSMo3(hEvI_g)l4S+H1o|djlodob~25eI&t?7Gk z;`6hBG@XFxC{QNIOj@234MZoCCTO9PHFzuBqqDFh+9j+3M@?im?4*EhkW2nKqIpcry8aKCWzL;ST5o;_UtcO>HX ziSHMQ^S6fBPJ%tVrryMielKEs!8_t?IcZSWiJ#T#(kALa%#oZA8mZ0tpNWS`Z z6mkN9dvlmg1=I%h(6iGWPk-`H^J9$i*Au@44TEOPt7wX~d#$26`Y-;Si)t1;`Ua|s7IyNimlX&FA^K?sZ+aw!25NA^eex~3l z5L&2T{fOHwK2}z^@s8oln@8^#D2A&)xor>8?>^7H=x-4(C&P=sSz}y(4~0VPzLPKx zcboHf9$60|ecccuGtr(z3|n{q!2L-u_XLEOBG}&yX7^*LEE@=4`q$TbG%QX}BPelW zD;im{UNJ^93N1fa4ucbbzh{HZSt`8Q4rn{Fc_(p& zo5u(Ky={Ahe)}5~!Y8;y{0sWWDH3E4Jxwt@fq!sgb-fHfuE8@`=)@%K?vU1xxbiGVO*Qs#7d01pJeA0AczYl?0TgbP}J_WJaA7@ z*Ue(m7O=XTP`~bZ`4&aiQd%5d5)JU!H*e0lJRCrbWT!E%UkTIUlt&USFU+cP2QUBw zcRh210+I(m&?y3R=0-3DpbsK`6r-ywE*ow&T_awxiBDmOA0;1U7sJUfC=W%GKX4TS zipz(7A@nUXV>khDFF_WFm!svJSTDI`U3xNfF+A{n?Ee3XIsZLs<+BvRuWZ|Mzd`?F z_`HL7lXvxfEor;p@YENQ zgPn_+$h`@;A5&j4fW@#;443S5?-{OtKMDT;Zhn@6rT||0mptS6{}>CsH&G~$K1DHb zLbyJ+EinA++JsjD)VlzW|M28@ck`Uy58PoVI4}KK6wb*tO*eLNWXV@AVr~XyHXViR z0D04*5S0u6MxJeKTqzwM6#;T9R2_Q)0?sk-N#IfMB*aaUv^(Mn7UZ2@-h3vL=B~@C zAg`=cQidGyKo`J13w4C!KkqzRXLNJfz~Njz*5$z9bv(_9&@x~2UF6((Qw(>>P>O)F z`;y~JeCxNBm)tx)#AE2U((h0pFMpD^x*y-RF9SVM-||HDE8IM~2Dex|%}x6Bo5oI` z`+}UN*xq4<4a-Yx9yc?Ia~8_bb3Xv$ys9;VT@-u)_iQldk%A=u!ynU_cnRPy6>z-k zVkJb>ovjC*7?4Sw!tiq7u1}H)G;e2+uj`jM-4o*RH*ecBf3j_#{ub9#kII!LoT9UK zWaHB7ILAH-hv#BBo(q{jytsq%rZD0jJx`(dsbT7}0zVT%mKjUaa~&7bottw{F<21d zIL)cxh1_EBWZ1o}eB>u7a{8_3_{@X1mEZovwtbW53lC8!kG-2UcqfIyW9TV{+z{f^ z<3F7-16fPwGDUEgOxNY0io)3Z!xDE%1gbR^vTI)E?Ox#sY3t6OIh0b^5`*vuBG5Bg z`=)qQvH&hb^MtD)u=8ZdW#FU`a^xYu$|8I}vwGrF+xGdtgR+r##^yB0F^%pdKM*zJ zNnTtnLW^?z8H#?FD^1=z=!jhWXQ9IT`_oL_}G7lKl)2w;c+;H@EI-?g@RrN%neRK>-o@qdI+LnmAn6R^ zI|2*A`L4M7BydcN(HqV>4N&UrWnG;FbP_H~>?05C_}4=tq!%vqUN1ginDA+$2}VAM zNtNfI=5en3@=adeBBGGX0P?r97$0Ktzy22-jU0#P&W?V10%&V;X4j?=P@T=DnF+T5 zxF;(dI)CF=nz>AoOfgI$Ac|kqs{ll3&SB=~9H*zSk-5;}0oBdi#qCQOLyBRZGh}WQ zLmopD>?2Q*^mn83AM=HnXV12O{Ri9jiBDf{Z~W73`vFdb?|;YH_V^n~8`rHAAbzQ_ zj4O2h^$wwre#dUP(-3#73+o#};glc>O%EdgI05iE4X%={_woSc0~En0C;(@aQ@|o{;VeQsHdePMhkY&qsTb%Z74sAUx)y=S z_j`>g0^rEho$dnaD}@Cl^=CQNw-ETk3{etPV2y=ghixdGc zrvQA1o7jaO^-~0eL!%M47=aQlI=+dI?-U zyqUy1cyRL9KD}+f`7`LL#De7UEIO+Iz>nU>;R9BD9u)(8ce=?o@zYLtQEw;T*Il9Fuh`Y?wx081{8T|VDU6V^OJjl)Cqd#)K-TDy@jpxs| zuYLaR_WY;raN~Sud+MEM+gl&!s*R7i4CMOHAZr}vJ@c?EyeU*T?+i+_Ba7&8Q&@i^ zNLf!zbl);jjhqC6EQ-1vkbGON{{$jw!V)BUbM$%yKKaIN`|Df=o_!iV3y}gK!=4nr zJ-osqCBe{nn8bkkN*C83^!SV}yeS0XTmk|yWSCU|qmNn@0?_8XY?)s}=V5`nE-7?S zTXc0mYp~#Qglv3f_d={|yX(dz?pP3<47Vu;i{Oskf_M`*f<$YdodG6UNjSDS5o*}XB}D=MIO05qbyZ zxrp+;<9s{cy7}9_AEp5KC^~m@zAf~N97#03CP@P=9?WZZIcPW0=?uT{;c26xa1yT1 z+!M$Hmv4;f>|1>BGQO0F+fHGaJ0Ce6prbdAhmLZMXF=yIZ{WR>PYHAHq>Li)ORX-` zYd?K`d+B-Z4Zd<~d)s?1>JrPOb2Er;N??xrTz_j`lj!5*qi?*}z5XSDLDNuj4jR}R zWOByn+Y)a8BS*L<4?e|^lL0+%gdXOh(BJ&iZTo+HhW;=*RJdn$P62dQ5#Zb#&Lj(8 zE`o(H0f?G(yE|#%;E4@>nnBYW92J8FA%6;?40zRnF$-x87Toiqpp#vj^^!ICf;+v$ zg!2@+lYo&^fDMt2LlOABP1o|JfA{+K!vFEo_Q*?jH$P{{ZpYR~!}Afag? zX7n?IFhcXx@Y3~t{k9YC(4}vo#VtZ7XU}eG+zEJ$naC6Ymggk>X-fRVpF7)r=ffNT zUa-05`IgWkm=A99c2HXe$Vs3rJ-bH#;3j7h?=uH?&~yu7dg5#w*lET&CxFlbo09+% zqI)JNquL31&$9N4FaAF1StH%xJMp*A8Or%NUq2P}a_D4Sm#b_N-Y9IhAK@Ls4?nQI z@aY@dgKyx0v3|x;0^=pT13bJFaIm^mIG*kD^6uHX2Pjs}d+wxUR4?VDb>XWZ+w_f9;Vm?zarP=N1$6ro^cCKSuPK82oqL!Y7 zb;1TYeM!8d7XmY2-uCri(l!?^Zl%TP*-a)KK~93^YySWbIezFfm)m=uJKLV)`GAj1 zQv_zvi6$e3>o_+JvP!(r`J z^FHJjQ%8`lRr_V7y!I{|WJoo;$dGz_E zpWrDMGrh%Sz#_@X;;?w1cyD6e0)UNxCZ2=g%&9xif4?cQj`zF<%OD}@h zB2Ru-a^txT1v>Db={`U|dUn{=jtID+eJQJBdm%UL@VXs=#yVCE_8HVGPf4-*HBNmfoo>E?ysj#B{O!raH~9f4orG3Q+$9&}O# zNxr_~kQaUFJ+gXd5JGlF4RU-5DAwUMK77$>x8a4=3z|U@xKrdC_)I9cs}@1aqN7Pa0=X{04#z$F~iUNfFckd9``25g3#q^5H70k0;Uj5v>6LTwwmL2 z5hVGIt1`P?73pOLGI$b%OoqHYzu5<-LAJt*7g{`hWWwE`83Ug2*!Rt^aQW6RH-8cM z+adhK>SqN#yusCuWK(A*CDCUEw zPW*iCV5s)U#Icn&?CFjn?!qEoO8k*mjx9lW|72YI_QGG> z<}IL#eG0@8rve;RCH)w|48G0(nM?Fc-NdRsviHDad z0(X#wV-ZZk1L5f>Ne7>=pGjL>xpBpx_7<59bTx}4YXo!g2ifr|31zm&HH!M!m^pE~ zINeP&9ZuREuKnNs%FXT3$FAhG^B%!j8%|~qQh0I8z4E!)8)@kuodAk77+5OB6HhCT zrDLEhmc*Le-an#Ac68dYg^?2iTQ37|{2UK8e&%d@kmUOXhzBUkS9xQ91OJ``o&dpx zbd&EROdt68{@G!H5s4U}0l-LCKn;k9Sk zfnVY$SI-cTMLf;fD`_EcC+ z8wmX=TzHY;TTm$iji-Z(Ptu2?<1C^z0f&!6OZB`8h6uacD0Zmx@94nx1wm73o0*(2G0b?fveVd_^uQ` zn()UKx4ytG{}9gy$f^&8d>7zHqpMh49Bkdu-mE40*i$d-N4~knLz_5V+Z@vMRL>*N z8>bWjv@nV)*`TwTFis%=laz-0MVP{gDL!>IxXHH&PmAC*`SGU_eUANETf?#a>t}Co zAN`+S*dF5%@WVU>%u~G#giR*E*o;+;qUczRXR#xcGhIe~7LW&XQ9Q9YA>(4IWn@Cz zz%`%<%qfDb%uXqKzBm!!l{fP&fOi8HfIIv;*?pC)d2?nWEP@n*KJA(UErhEi$71j@ zkV^r49~gNO*k%}$@3(?dIPfv8%YzEYq9~*X;5Xw!fL5_6pt;EFoq#l5fAJxtO|qv5 zvKA^~E1trF&PHIafw8O?6Z3n&^4j)W|Mb=EiT7OD-ueW|x3V+PPI1u8!xy79q6&7- z2_=VCvJ3p>e;qr5+41{{j7)!;=_q3@K{EicZ($8lN-o$eNI1}ohvGS(5Z?Mc?`2;+ z%U|TU#&*3%rf#vNy;)nQ0J!c4o=3a_E{d>2zU;l+yw->*8 zXZ!viyx88tWirVpSnuvJPcIt-8*8D`VwilXqNSim7sUIh#YlirMjfRjtmz}5gX0Kh zyu;W#dHi6be^-3}Xf{0E@jSqVxW$)7ul*P&0gL0?!K=Vr2HMuuS8s|y-;njd1er;l z>j(@vqM6#5Mf*ao%=xdJPL2Kdfv(ghCldYow7~M?hscL7JDijh#NauH}DNj2(emR5hjnbdxbz1zI4-4LS)m z;E`1^cml+sF)C9We|l)$ePPJ9#b1z+_0n;~ha8>iGtQd^pQAhr)yjf%s{H1kTv7n_ zZom`32SQ#3{KPQvxVR{;CF zlA#q$o57@a{8<>`ELQvtEx{K?Z-0iweVYk``{NOx30!4o8!d z^8_#{7PoHg83x2fH2G5mb6F5_g$-YS7DU!#pgzVCxUCA?B(Uc)!0d=T@fh9Px6#le6cVvimagcYFYfI1;i-O@`6RvS1938 zhlpSJs}Qh^zBaoSd5^JUFVsPj>>2rJp!SdK3YUS`_=NDmpX4$?pq>IZxx4cT-R%DT zeE{s70484@cdj>4xdBAxaCVzRj{YhFq;p>oh>|)M$moOCy2xmJ7%4yuJdI|zn|Np%C(_VItrmpQpXdHRPh zwzt2FpL&^b&UxqH&$O>Fkqaug3IM#5zYwiN&buvYt7>vp+4M8s5g%_SkD2JGiI6#GV_R&?pH)cfOHk@Gfr#w#9L9G-*O!c(X5;l&2N6Hfy-o;E8x zSg|jSdkSK9>p8z3dihT;w>RBjVK`#*KAOex?O>9kUj}lKa>FNdI9__#1v8XP2Ci6G zTuF%JSZK#03yLrcHxp7BK_1=q&cEKt|LQ+@Zu{*2^ez6URTtZnKlRXd^)2Ul2jZW4 z^IeNO-oO3hfPM;>GhKg^cmg=Xx0iuD>4SDoo(!l2PmxN~C&m;D{t_a3X-AN;LObz& zS5w-yy97w8pU#W1F1qD~EDlx!ml?9V{7ZO?F=tw)cREMze z?0#?f_%kFP`>#HGbNdR(f9Fp;xLtkcMg8Se0y3E<%Uy5sFD-&B%%K=ecKjkUnci2V z5U{Iv7xGb&CY}_7aS`a;2-aNS;mQ}h^CB&D9ZD9tEM!?7Vw`UbaSxzURPrkJx*b90 zp$@q2;&W#c9<(ho^j=RV$(g}4V6tXz3dH~W*9%+*EC3hPQ{WCTZwJwxr-KHfFB2#< z23^_2(;TM>QO7W@ET}X>?UM*$a0HG+&+a7o7bN}x{|*i98~^KfwugCc@$mOu;akk? zCLE+FN%~!I|InjO^Am0}$bfmIZE(yZMb=P!#>{DeZ6zNU$Y$rKIPgyH2bh(y5S-fUb>nuI3GtccqS>U!&UsH8y^*#0vQI3d40I7<&dY_D zfiLjavR>x8%ZEjIj?RL)Ko*LNt@47}Cdjbk)%hFPK=rfGve7jjyYrAAs1IcZFHZ8j z@yoTJ3Wih=QZ zeB+sz>LeifyO=GENqFHkpIQOqLAh2mXvo+jV^Q)hAaW$-s8oENfy7;$TmrDO2!@x1 zjl&_PjGnR43zSFmRM7YM z85p52jwjH6BUv~I7dVTpfW%~n_E`RY(Lb$z#V-`lp8JR2-mbs-e7p9R3*M5}j?B;6 z^y3a7PRC9mh_|1_9lUfr_T?R5*{>5pF>y_76<^Vbq-7~qMobRn4Q><$0i)| zLN3tI=2?)ngsE-GfIHfG^m-rUvDHJ_sAG$davk09^tBupuFCKf`F{lcHs2R~^ux9D zybSoqm9mhNYzK*<#?^bMp3OmQv8dup<}xNt=h!uY()x8*GUhpn_a|A}-2VEl?Ui48 zVSDIryn)lekvLw-j7jvkH%Y#~^#2H2JD$T=Uv%|^(+@?4A8hrJxO)8H`ow1OSxnuS z^h!K9E)TRZ*@RULA*Hhb#aDK60+gD`GGlSdMb&|gS=tsH!kwN#iXFM@5xs_SHF5>b z7tJ^=)5vk+Tpb7Dj{oXg&$j3JDfx}(xcTBS@mvPH56DTN?hF#AxHU$*~nr*%n1NG1(A8C z7zkc_WJ$g@&^Zy9SS1=-xjBg*I1{Tbzdq1GwD?(Q=rrwIh%_PHQ;Vao3P6GU3?dO! zGR^qbI{@_7B>?`+QG0`(}zjb@N@k=jm5B|u5 znS{ER-trqjC%Out{Ww_o-g!fGf~x?WpdH&}TASK%`b!~VGKqKmNqjx1(i}ZpIKw#& zq+*j9|85B_CbwonPEHT|SFvT#=z; z8(HElR&sfjo58RDAqDW`=%C~&;JX1I`zBfVS&mK|s=to?-5_f$O(AOGUBvRailC2w z{+3E2dino;p1*I%P2b!6Lrn}kH+U9;$s?I{+}9;5?@EZ(g~)RN7c5CvzmlBTcMFvm zjKd+jPZ3OFOQ(`PdgO_K^5zpm7Zh4@)F<<&hDB*a2ZZ};)Cg8M8-?6XHK?UF7P>a>e+@C4^M`AGrl^ll)og# zn*RkqC4Zj<0N)~*|HaGLM1KbGbkI3Xm-{yS8la0nJ9?h>4P*=pzz0CKSN_=xFj(xz z_&G%;IXj*rNK*B|XxN+lTnd0gn_PE&CalYr)q=s0Zofk3Q{xn@_2>d##%%hXB zcD}wj_ALP0UB0>V$}h6}KX^UkhdDdAZR5{Ng2_R)cJCAi{CPK!0A{gz0h0r54U}Q$ zxR0*%ir!oly>yTb&smo40Uj@stKeeXt#<)AMy2(j%qAQf80S=xRh$KDamU_+m)=?r zb?~!v2G=(|KHCo zE7y`4qhXTXK`-3h@7uoK{jYwMKWxayk5}HoOHWdJoQ!`Y zC&bZNi{pAvsIy?jbE1cxUwF0(KBiCtlQS1ZJ`2((L2avk8(3mbf;R0%teGBOiKPam zQYNt7+zyFGZ_N{5GtbyVm!A^|-_2|r`5@B%TEbqv z0(_k&I#GwFZCVesc5Cn2-0)cdedBkRFK@o|&tGBp`)$o0OkmH`zry1;$!8$P4$iP9 zMP0^olZnqMQMVp_eXwk7esq>RuLQ(03rmrhjj27 z3#6O0!3Q`H*FACg2G{e8{1jh$t)G)myY8^tZ$W9BW7&k-`2XqqGS37)^zU;Q&NF#W zfLycha~W918qPT5P@jz_G3pRxe7WQG>(7&V_rLrxzR|-6Q`gx2Xp%r3dODO{y@iwC zGAB%*n?T1((qrq3v@djRV7VBQa|&c}!f*XacrRMnR8lMXonUfU#bKm&`1%gsw0IXm zJ`VQnTP_1prwCG}rIYBiW5f&!UU63u$eZrG#Rm>Pbk=RBKX#c@^8q!^=(UnMSNVkS z#n1DW@QeH}SU$iwIL>7N=<|TtUHV9KVkb>^6R`6Tubs+#3^d6nxpVF3%bPEJdw|;Yu2q>S^1n4yMWGGN_-w-72*FS3nWu1i=7yiOF8bN| z6^{;l+a8zBhcUb*{M;w2093F5@(f--?MvMhxVucGW%`?zg+@L}O48WTIrdGyLDOFP z#EtD1U*o?1;rfDy)(b`#YYzGJ->{%4v&#GZCQ~q2^};+vuF)7U{!W2A=%_|K9=rr%c!RuxMQdoB$3`sWs79UXC`n3cAh01!9w( zD|F}XuKkz%=KMRq`093*pJe-)zQt5`@snhDOne>z=k{QN4^+;;nJjqAYJB+wG(C%3 z0Zx3{DLw@6B=)>auz7n~+0+)6j~=nlDUnT)@pu>DJ3^jlNy$#?fFqdosyH}e_9b$+-$uKmTV~@wM}@and?uz?=+C&SGS!^R^7|xU)T7G*O4sl}g%C*$40)5_Jq(UslC6$5ZJV_j zKO<94w?(hv&~C(sxD5Oe|LZjiz~(7nw+L1P*Ueft1^U*hjlqK(gbB;e2X_FPA2$8v zM{aFb`CB~xUY)`BIDlgC~L7Ol8CKom3Wm7Q#{2 zWaB>LCypkK=a8sj)=Un&x%dZKITP%pC-fwHCQV`;dsh^75+IMBZv*e<9cdEo;#vfL zj_oOM`tHi%Yy*3(DrqpY0gxx@z^sdKCksBjhNA~rzIJ?NtAl8Ng~!7F^{`KWivRh< zPih6v-1=G$Cs1s2^h?v+?0?RWHm|)02D|?*|9PK~BR-S#^M4e@ zNqiCsot2az$b2S{#6!-Y1`jc$V^z}S7+p8@Rx^p+!q>+zO0P)y($()h35@d~Pf;+h z+yt8Ru9cN|YbXVPcZ*?}imU({g_n6Fa$OW>eRjCX7=EBO%7NKD($nQ54BbAW+qkFn zoA~>}zxjLH_BZ&5xE7cqpa3j(V=w1G=;;&Vk`ZLy`}@GhQ=r?o|M1TC(qCL|kNtSx z`IG4U?hv)P&hxR$O1{M)ssBR}h|4no_93vwml;lAVlil2i3*>fLm@}zM2>fKtBx*y z4oPMe?%Dah7>UVMvH*0;OOZeZ@?*|B3{`l^oQ#EFYfwbL8lRnYSViDC>r?3fAk0XhoIifW#IQIfIs0<@Pj{4LAb#A;wW(anz(%>XdTez6SpMY#8Wua-2H#{ z&u?$npG3f;;C$rEB=X$@pbLBnQORX3oG^Z+=UKj!GL}rZ$1EJe2}=adsIpA%{hAZ- zpLV+zH_FW1*h24;ZszVe3C#3Pht0>4j|;DJf%q`dxdZ^cEcl(b{PitxzoRGaU)l68 zbBQmWe7b+wQRp|f2A{v@H*oasOJk?)>MTHTVdEb6k3F$%|LC9bDNd*ym&8 z+U2a3M|~&U|7W4{(?7-bm5<-qp8q<(PJTDLo;l(?Z+Z%2X!OI^j@hW6Vgvg310R1U9}c~| zZU6iK31in>I19kv?p-m_yPm{%^7qHS=HGeta{JUjxx@d`m9KhnS(pTyY?E&CS3yA6 z@iqx>=&DZ<1jiP*FA&F)mkbG4W1=>AM{Id5idgxa7$!FfW8)VbHkHO*(|o`+)tWU=3r0+5v16u+X)!YJ<0Ia1G{$Szh|^8b)Vi4YQ%dF$fMc zmLK5w|5v;ve1Oks{@#C5a@c9=X2%cXyTEUK2|s`T?)K=D{OE)4A-enhIo2fIIhp)i z0>BNz>$>YUlf@+QnOxaf9}v-+sI1R%ETdxbo?$ zft$-t?2;wF$9ytcy4alk{g2%6Z!~hW^4B`)bIf`Z=y5A_q5UOpiUB+)0?-Nj&oF{^ zkOBi<$p@drO=6>CT<9jbjYlp{jFAlvOrgNfpK}v7FL$y8`I*o2_l3D6{LD{o+fVcT z!N=cbVE1qS(DcjvMY2!+^5yp2S1z|Fzn?!^%~#M}oV+Pu@cMPG#o(`XEd<5{<~yLw z+8aY>u@H+pG^uWIP+hwKY>d+%^Ja(PE zBR=t@0PuZCK8Yu~Du~);;<*&KJjlco4zm=(J~VL2T7ZH{aH~bO$8j*cBv`t6M>KQs z(^uyrbRNpnc`xU6RJvk0N@llR6cUEhVUm*o<0IC45P7M6d94qMtGa$^Xc z;b2-23Y905AXxxmvrb_aWiKCmpmyLUB*h>O&oT8Stc4{R%3vlg1aIKU;D@;++~C8a z=fAUUzr$|}{a>~Fzh3h(|3lmyzKNfiy@ld<6kY!op_|g1T6ey05&bz=-u6LzjrS(d zIpvxUZi);nmxI_SD_ugXAO-SV8If)ab7J*xmMzd1*o6nvP|m(3g1ur3T^XDDI&bJ$ z!yDK#rhnR0Hq3%C(WNiYl>EWnD7>8?--Fqt6h!!272_O3-9Hz88 zUR{&ppNEl79K3h~ca^7w@8r|MiziBEp*Y!j=A*tvW8&S_#x;L%!XmhXb79>F;2S#c z{~0Cs1L#lEeG5I~Ptb_r>ezn_(ydbGs~WbHNKwOz{=ug7N>}}ALi$mXN9P#MWv6W0 zE*bQ+!-?Y>Pz5TbQbi+Xiy1E_UVM^2bmS_IxhQxNDnwDy(AnK1Z?nk39boW#l8;QD z{Q3okU zGA}hb$)gV8LZ%Re&{+hPY&{1T%kd;vhfZSUpUOtB`HnGm+N8s9Jn2sGaUk3t>wKs6#dixBXU+KU!mX&C%q7PU8wD^LE?l5-> zdw$`(jy%Q^!)O@L{23rR%^TdCq4wd4Sb-ZTc=IQuf~l^Wn1fo^<BxC8$teaL6>j1x z$$1;7e{`lOCh-{)y{w5oz;YruKFyUMp&O9YD{pln(4zwQRU? zD9<*s9kdAG_a≥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 literal 0 HcmV?d00001 diff --git a/src/renderer/assets/plugin-icons/codex-connected-cursor.png b/src/renderer/assets/plugin-icons/codex-connected-cursor.png new file mode 100644 index 0000000000000000000000000000000000000000..2f29882ce809d248637ee079fb45c6a723818f5a GIT binary patch literal 2082 zcmV+-2;KLIP)4Tx0C=2zkUL8QK@^3*l^7pUv9K^8vPiIsB2l!lNm7WQ5MmUyabM7W-bT1u#~EVfNITkvP&s*b8Fk| zc_K`NJ{E{tc3mYinH90{Ilouy7DLbLDgD;(^M9+qP^{ZBU!^B)<+xzPgfj=O(k#u0 zBTES%YcBbGsyXlTMcb3hriHJBhwZXe5WW$P=R5RL2UjZ%`(p>a{Xns{zNYt)9%Gd~ zOmb{UE`R&x+P*aVC=PMNU!sgFx00eLDinp6sbMosTsXmu^t{gZXKsP!reHBAU!4cq z&Ny7&K{tUm(SY72G>??!SYK%xtx$b;HsN#h0U}w?dmM!!$>MF$^PXd#_i_*6GdKyq z1DJeQilX_(_y7O~oJmAMRCt`#Sxrb(Ulc#{Qp-}289(x)w7?t{6f8p*8qsXBgh(jJ ztX0}B!Hq#Af+$oF8nnp3O|>#wWKa?#;>txKqLjE0g&}iv9B0h=aGd++ckm9+X&jw5 ze*F8vW!}7*chC8qbM86!+@}*l2#NwepO0KF7hkWfteU?j){nP5T@6OkUkV<50s%7=r);UJw($7H1Zh>19zPJVbR z7w;EbE*Bd$F)@+9E0I17-Um$rfDX__uFr_#0uSl+`jEfDI_vA}peU5~cs%s=>sJ~Y8X}9u!v6&+Eo5{XB%nOxWo2d2 zg$ozRWHQm=!-ts|eg{Af14i8O8j&tklfeYT!^70t+Da1>6HIC^pePCz6%|oiTN_nY zR&Mf+Og=j%f<>;*5=e(5BO`SC_HA}=N=gb978dfrwX~iV_&2<~u&_Wse*9n~;W{ZP ziQc|_OXts@=h~ts0@v>di;Ih!zK@TO3!~8}3eJa|AkIXS#I)uG|_dO7@}+yOB#H)J$5HL*;{h7M|ID$7uCc6OH6 zL4JPzrr)5r-EOCsFJDq-W+ty8#4|DtenU)ykO|qr`#McT{`~pF z7J;SYK1%|Nw6n95dU|@;dQj-*&6`XH!e705Mcv)qOlT`8ioynX`0ybsE&*AP3E9v= zUDGrXkrf^SLlr7yxKP)?{QNvC5-wFMib7Va^{@Moh2K!4WI2e6h*}9*z$D;-M~@z{ zz!N7<(49MX0%Y+CD>pPWY%3!E*4-k|8!EdNV z+1c4z#FAQ;S_fcH_TY$Pbr#zXDD;m(x8arGNZ9|bC<+`qc8o(61KX8O7->iG&(xUByw|e>Fn9FbougS zDkvxjoCk!;ilJ~IL{Ll@7Z>UK_wVc#RB05zk&zMV?d_%0r%%(xix;W1w3Pdh^z?K( za^y(ZOsI*7dO|ImLRcBf!4n{H?bp7y-Ot}CHxFX z$toXJ`c+S$wKtc0Fetmcyv)0S;o)KG@9(D*w|Qf{RrF*Yr!H|w9%wfY*(;e%w{v+Q=gz1 z3&g7OwB#zc2dw`ozM*LO`bGo;}jT2MFA19bs)06cmW8tSm7!G{pbI z|3Lx-K(f5NTs(gKSj^1K2)En)*IdvoCX#SMS0{UmGiT1w>({S2o1llTsj1l z9F&@xN>{F230b*uc3V+VK_5SU50~sZFq@|^aPoF-8oEO&Z?d@gHmXwt6oc;a%?4i=qQZY9-7gn#; z)m0ul_6V$r45i+6q6Q$hfByVA?2#$DJuJ>pK!$=RG5@b$zc>@2DZ`?^bm`KTp7;wQ z0w5NHy`uIG;uqFgSy|!r0xx3VUbtjnx7+!AOt06ATeohBl#~?q$iTqBwlzww-H3=l zq2=XevD-jGc=qgBAPn(oGMU8E(o)#xpbK;YBB~>BKSCk+1S47?fndKCA0N-P1K%4P z8@bMG1wOdmcx$DpAmTw}3RKqJ0dXzMcluuQh`4A4dQAYMLOmM*si- M07*qoM6N<$f}NuC;{X5v literal 0 HcmV?d00001 diff --git a/src/renderer/assets/plugin-icons/codex-connected-docs.png b/src/renderer/assets/plugin-icons/codex-connected-docs.png new file mode 100644 index 0000000000000000000000000000000000000000..371299502a45451d253bdb66ab37cbffb694423b GIT binary patch literal 2053 zcmV+g2>SPlP)4Tx0C=2zkUL8QK@^3*l^7pUv9K^8vPiIsB2l!lNm7WQ5MmUyabM7W-bT1u#~EVfNITkvP&s*b8Fk| zc_K`NJ{E{tc3mYinH90{Ilouy7DLbLDgD;(^M9+qP^{ZBU!^B)<+xzPgfj=O(k#u0 zBTES%YcBbGsyXlTMcb3hriHJBhwZXe5WW$P=R5RL2UjZ%`(p>a{Xns{zNYt)9%Gd~ zOmb{UE`R&x+P*aVC=PMNU!sgFx00eLDinp6sbMosTsXmu^t{gZXKsP!reHBAU!4cq z&Ny7&K{tUm(SY72G>??!SYK%xtx$b;HsN#h0U}w?dmM!!$>MF$^PXd#_i_*6GdKyq z1DJeQilX_(_y7O~e@R3^RCt`lSzT{bMHqf&&N@B}sxfj-_8L7XSA<>UM!pcwU?& zygW?S@0O-%OG%uQ^Q{HcGmyLm__D1MzEdleO1I44GZ4OnKVHwogKE`kRg^kDJ}zV3 z>yO#C8$@1)AXCpkf(E)Z+Bq|}<7KS--e$m5J&+9(JUCgmCPHSwjg=;5=j$*Au*N_s z1xv}vXZEed$kZ_I-93ciSV61afy8kv!G;v;tEXlL_~zUVeDK*U&R<$V*08YFK+q6` zvEUGJfcBrb504z%gBK4MaCA=$rEcFU8>SXf6aBiMXBHoSIfvKZ{1ZR^RzfqgFp458 z7^Ptp?+TXBh>4&wV76rN?u9z0=QF{2yKTcj7{@z>+P|*W@YbiZs5S{e0g(nG-W3#i zqcwy-{vy3}(*g~Pa>L^73pJdZ>m3YbqHH}md#Co^={YQ|W)1`e%?TaGN_%krS=_XU zD+a1&uv)Wtf4Yt{vnf*gRj+{h0iu~2d_BF4l)MREtK`4;1~-;xnTCxlpbS*qAXzn_ z+oTpK8;g?{>nKJFM|Nu@V!4gUuxX1>bCre_5S9~1S}p+;g)tF$dgZ7VH7$}#hNNN; zuNvr@MWt@=&V?GLuV=`5kfSfuA{fgzN{rW*$~ZJNfnh>q(lm4(_MLOpti`Ec>)2IP zI6M_~oW5^S%h4x9CFJ_vQmP$(36wt5Fb30e3nHH<{2bF0;!=zQ{66m%xf%K~^Z>+I$0xr6!C9jArn# z(!Vkip?vH?>>Q2Ojbi=t5lMc+9huA$m9cpC*e<;M)D+?tNZ;}u>q7X4U(Df?&ljW) zb7hT-B*h^G!}>=i*g_zpB?-4Wcr6PPQTpj`7xBYS%TQ8l`4A_TXVINf_ZZ7~oa?IKYC$RjH`;g6Xb{=QrVvf#iu zAIU{8LqD-hmJW#X6Q4*Wfer$QI6LzOB?y!-!gsALq9pi($}O-Qh-SkGt++mc)H!sE zU{zXdpcVW{FiUU8&-$vo~O~97r%?G0i)QI!g}- z2<2{1=*yA}xOzRsMOA|q!5O0336cLUU#q)XE8^-=IZ85OWpr|{l!01AoZYyB(HDU> z(O6i`@ZIl~o5L^t#e-tWXvJKrWd#?p7=B$}%(>N3i%Kjc3Q-b4rF`P5S<*;2KIDCe zati=jiarvk#8SrLQd0W)-+N!~%m=4Nzx2gK(?4;KlPw&M3i5MT9D`SD{iBR~3nyOiZo_uJF4!s6m0cI?>E^-LrH>TR~lUwslFVBu5Cd( j`IFavw{V=%>G1VG;ChWD#pFw100000NkvXXu0mjfVdv6a literal 0 HcmV?d00001 diff --git a/src/renderer/assets/plugin-icons/codex-connected-gmail.png b/src/renderer/assets/plugin-icons/codex-connected-gmail.png new file mode 100644 index 0000000000000000000000000000000000000000..7ca099ac10eec230de2a5b9d2f17955602a148c1 GIT binary patch literal 1427 zcmV;E1#J3>P)4Tx0C=2zkUL8QK@^3*l^7pUv9K^8vPiIsB2l!lNm7WQ5MmUyabM7W-bT1u#~EVfNITkvP&s*b8Fk| zc_K`NJ{E{tc3mYinH90{Ilouy7DLbLDgD;(^M9+qP^{ZBU!^B)<+xzPgfj=O(k#u0 zBTES%YcBbGsyXlTMcb3hriHJBhwZXe5WW$P=R5RL2UjZ%`(p>a{Xns{zNYt)9%Gd~ zOmb{UE`R&x+P*aVC=PMNU!sgFx00eLDinp6sbMosTsXmu^t{gZXKsP!reHBAU!4cq z&Ny7&K{tUm(SY72G>??!SYK%xtx$b;HsN#h0U}w?dmM!!$>MF$^PXd#_i_*6GdKyq z1DJeQilX_(_y7O|4M{{nRCt`#SQ88E3wrAhpa>e-p1n@^v0gVP8uhs zPGUaAo*8@Q&HLufn{nPSDW$|3z&VF)+bZuk4myEsHVe}(9akpUGwi?vI!9i6v;x;Dq znnX|sty63wdIZt{%d+AV(N2~on5L-~8=7LVERsf8zE#>LLad2>T4A(Xk=F56ORaqU zAqkyAkmUtLfpk*3ApyuYOOOwrK`g$9to(<}p?xrp99)x3{@^Sme+d*!utE-*9hblc zb~mk`h>ytEOAxP?C>`9Hy@Z?`Q+EY88XcpCCABB2EODFePPcAki^KLtil4Im_CXo^hFg5R8J zDTB}6LgkM$s+ISxp+7j(6Wn)(xn(EUTOm@1 zWSrgp4F2vuGMo1S%nVt?s{rtuo}%Wd@le~!nR5`O1t9O$m;R0U?Oz!ExcmPwY?pVQ zh=%*`Qy9N&0qa|bnkt@^=td(i=H{zp$Vy^vq2^!E7V)ewpB=z&Yy_y#8__)?s=F_L zgY^#sW?$HEAk}mer8!he^VPCv1D(RNqy%nk+=Od`n*i2+4iZ`xmO=Wjb@6|gzH4bB}sPZ(WRzpVgA?vJH z#-kai(@44K0h$re$hdjroOOYZoR-^2Z-}&lDe|zCMVML?bQ;+PDMZ%Iqu*H{aFNz? zE0si;$iQ%OfiGZ!?ELbpoi`~60dNO!xvXBt_7bt?Xgg$DE|*a#G~bkh|2K$OD~E1{ zna8!resRP=aU>I|TiZk^=G&{Fbvr@zpFTk6_+cvJN8+q$gwbwA9S9>+rU|wmla4jD z(q@%q-){-Ai4f92#;!Z7S<}&5FV0GMlcT>iABO4?37)(r4et9T^+$f1$YE$b^yman hUXzAC&WPH_z61E)^J9h-8Iu42002ovPDHLkV1k@aoiYFb literal 0 HcmV?d00001 diff --git a/src/renderer/assets/plugin-icons/codex-connected-huggingface.png b/src/renderer/assets/plugin-icons/codex-connected-huggingface.png new file mode 100644 index 0000000000000000000000000000000000000000..8f27e1dfdf8bee4eea3df91f44ba4ae6d322a02b GIT binary patch literal 2178 zcmV-|2z~d7P)4Tx0C=2zkUL8QK@^3*l^7pUv9K^8vPiIsB2l!lNm7WQ5MmUyabM7W-bT1u#~EVfNITkvP&s*b8Fk| zc_K`NJ{E{tc3mYinH90{Ilouy7DLbLDgD;(^M9+qP^{ZBU!^B)<+xzPgfj=O(k#u0 zBTES%YcBbGsyXlTMcb3hriHJBhwZXe5WW$P=R5RL2UjZ%`(p>a{Xns{zNYt)9%Gd~ zOmb{UE`R&x+P*aVC=PMNU!sgFx00eLDinp6sbMosTsXmu^t{gZXKsP!reHBAU!4cq z&Ny7&K{tUm(SY72G>??!SYK%xtx$b;HsN#h0U}w?dmM!!$>MF$^PXd#_i_*6GdKyq z1DJeQilX_(_y7O~{7FPXRCt`tnaginR~^Sc=RRiU&N#L^uBnsMaj2Chp&;72swz~Z zN-RRff)$S}SWzLwvWxx!P^oMRLP!X~0ttaC7HklxQdzJd4J0K+5mge}#CbV!9KUDA zbH{Vvs*LTCJvow^Q+cQ5c4)P!Ak9kp6bM9W1yXH3%W$L3nBTXV*ab`}_Hpd4FQUCA{yaVcVA58qRb#>`*B-2M7|6oH0_AeKGZ1kD{r^9s&;1dcDm+<$f&;FYkJ<#-Kx{0S zsa5cYa9(*G=kQD5=IE5S?u@b%ibC}N{(ye}w=lQ@$4b!Wf$xALK-gI1Th4(2AD%8k zejfeaPtcR^L9*KMvb_bOBlIVKMW6pY9CqMP5xk7~-AMZ695~VkRA4070Agk_W~r8J z2_{b9?p#C^wKX~lj+CH&m*hXc-dPI024X5xn3>BkoVQM$f%x~O2B+VwaQe+9E?(Pc z^HE~>={w7O>&I1IdwY>eU9UHaPYrpPYiBXx>^=gy`A^Ux^!uOJ65DH=tE->7H{pke$O^UbM5aiD3J+BG9aGAsTTs8 zQG>5dIo(hT%=Ke>!vd|kKaou{Y zFF>*!YA~NRiGrjlicR`in0Fm(If=G@BK-TUFkSN71SBC@!NSu105QVu4tqtluKD55 z7dbxa@y(a>d~qz#YGgR`QOIA;HVN|Z)@y?lTB*xs+MEqMEFj7c`x`hu8(IIEb2{Fa`JDK3A}F z$(#a6)>xU@(m(bNL^-JOS5ZSJ;Cg7K_=GrPW;!jBx@>N<4eJ+ktG4Luo3w52FC=q` zc5gJ&HPlmI-}X{s@8MSx#&8~e;WwBY7of2Kfq{P4UO0%vV-2$rGz+NG!F4-#bC}v) z$cON_pJvM`xJGun5-KQ^QAa0mCVqelhPDJEy!Gxn$QVpG2a7k*SKq}5jT# zOm#W;`ki!FQA-AhRz53KsZ`c3*1HBGFGVeNK1vG3!LwJZJwLrYino$D^VK~+y={IB zZAqm4<+axoXMQOFq~4Rq(9P5`v-X$QUXut(Xs7W1Dcn?6p<7Qbj{pDw07*qoM6N<$ Ef~MjYTmS$7 literal 0 HcmV?d00001 diff --git a/src/renderer/assets/plugin-icons/codex-connected-linear.png b/src/renderer/assets/plugin-icons/codex-connected-linear.png new file mode 100644 index 0000000000000000000000000000000000000000..729044515ebe5934adaf1eddbc2585a47e0221d7 GIT binary patch literal 2864 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 literal 0 HcmV?d00001 diff --git a/src/renderer/assets/plugin-icons/codex-connected-plugin-store.png b/src/renderer/assets/plugin-icons/codex-connected-plugin-store.png new file mode 100644 index 0000000000000000000000000000000000000000..bf10e99d35af8ff8752edc6c4b5830a700e4647e GIT binary patch literal 3502 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 literal 0 HcmV?d00001 diff --git a/src/renderer/assets/plugin-icons/codex-connected-record-replay.png b/src/renderer/assets/plugin-icons/codex-connected-record-replay.png new file mode 100644 index 0000000000000000000000000000000000000000..10a3ccac75a86e76edc87f542c9391ab520a67b7 GIT binary patch literal 1167902 zcmZ6zWn7fq_dPreA?*-DOAJGIgMf5*cXvrjBi$k0-AH$c(nt&-A`Q|hB3+{MfZq50 z`#%3ST-WddKKq=r*IsMweI{B}Sq2N81RVeXV9Ci!ssjK>53fi7RFsED;4`LB0M7J| zoTQkhH^R}c+Zm{y8yRZ0l(mu3tgWFCyKN#MvNXt+^=Vd8p3x#4UIAVV|zv$uCEEGxqI?Z4;N0^Qs(ntukqfnU!D z_>nCSY&wRteI(D=wGnhg9q_*8SUCE+O~wS*f7RDE%l0#O`R=!_;QO3!ceJ*6wWB`# zUf(=gI(qJRv6q{FPO~@FQsA7h@Vb!FS~>3T2@D1tS=Cpx*GRpUZ zgJT;4H;p2bJq{*|*v-vGL+M+n<(#J8PccFC<4-^L`%P{%$F2_RTGzE_RvSg9fe~Aa z-b26cEJqJ)TUrfYulTysaI_xD`#W`${(cINgb%OL2yfs2xR@!abRsU9*^|x$KaqR=*>o{yb1v9*@>|>tkoVyJMoV16 zRWye2$q6#YWF|Xneq+qh{t|C8%mI?=RvbEw)s(-0#eEu%YaO~+v>J_8Pdiw|^$D9f zx13TW;e#BsMFcDPCV7PcqFojDWqSo%{VOME7ZPJ+8}BZD3FIi5P*zIO(H%!ScS_@m zCfv-TV3dxLh(l*ncQk7G>Ne)S%Mr!+l4>YCx8V79sQH*T-wghDG zf?Fuu9$U^7#L{lw-SJvkyEyr|c}teI`Rk0Q(RpLxVv<@q^L|OJcHhfUB<|1yT{l^W zFiX&%o0@44^7p-+v)Ui98XGJKb{pn};P(j;^dkvPH@!iw-K_{g1oC+hL@UyV6Ip|} zmrq_=>qy^Z``!flVEtd<|E|MT`Wc*{+65&3e6NtW_AF%p2{pO&`sm|0GfbZcR{c^# znmQ~SN&ILx2F%6zB;!+9Sc+a5fJ)YeOgMa0yZEi>R(~Y=_=fb-GX{bp21@RWU+B0= zu{8!jE|&f>BIJk)uHOo#j5`&0*nxy3M$r0SOe^U`9e_!g;TiF!2Y>j>p|m6d)h8dL z;^MpXeFpd`E8NpgW1ltQh3e6LXv-3P(B@6V5QqjhC(tbCw9b ze+PN~C+~t{lH9aQ6yUQxs)+d%qpH#Gt#Fg-%3x9iY_ieHDQzGgt-WnIwtV`uSkx7d7aj!S;Y|wAFRLllcnCo8+Im zcD)}l8((=ih`@U2$yt1L6+bDRNsvKC&GZXBCbyDlv9Z@JrPBd_&Ojg$-~E)_Lly-7 z0}O$st~cG;DJ z=Wl-{OY!6}W;k64*G6py#~sD#OlEp|fq6lJmr02c3({b(4FeWgVNC2%rth=k{UGXX zCG$~*q)FRy#d6!vAZzG%@z)u&L`A#(lMnE>w&ZZ5a{dPZdb0SP|Lpx*!~--s3&Wkw zD#I)@sl9LGgU6}~gVMLY6v{%x4ZiR&!D_CFEkkkIXn_%0AXf}%J2wAD%Xd`E3-~g3U{sDs666`H^LQG9% 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�{cCAW;illW-q|M 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

kXe6LMF4X> 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

9ddRS8u$-{QuL7#d;2syNwJ*JwSO;fXwwwK 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 z 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�*(dVxNAC*_31*N{ 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 z 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

M? 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

(u zl5pl@vrheZxJ5tNySI}mTZUwc9BWsqBG;LZo!#Ty0zN+BwGfICDQ|cl%V3%7CuOeu z*Jc0tqO7l3>H8PT{nyw}7(2Xtm^X({^vrL|+`h*f%U;YoCCq!W1 zh}V?++`WhZFmv|?GvI$Fudt7ck^&G3@KM}3jO~H*v2oGQa>T2una|@VJ}> zk)m#Y-;{}Xsqs(y#mZ8{V%3Ge0Hp?i{*ik!B%Mi@>Nr`9_H6+{K6oL>jsNgKN)Fl@ zf;D{sn1?WrI)7KdI&DCaX$pmHKes3HT!4pxQ77X8G5<=@0K3 zLY89`NV5DZJ>nxf?>y539%iup<1!EUf8;UqNuGAQwjY;&Zx^dNb^F0nI5KZ8mGkz@ zGyEvL;QgP8$(aeVO%s&N(dmC=l>$fDq9#0?Y-rx$m{29H1U3E(i-STw)## zoRN$_vJm|;c$lE%i_&gF6an~Z^QjT)Nx_E9;+_GV*UF-R7fq4Vm~RVDx2Q}*3e`B) zxfH2FUZdYQ;4!7@waV1yo4wgLpj|88_n@-_{$3P2G=? z{NLb9g&IF5|EH8jUc3DZ8X&Be_*Ah`VBwB0t{qY#;}+OE)w))I7r?UveaRIGbu{B;)7?FV07QAI(>B!ijotQRD8`-IN@xx7 zdek4?V5hDrDR&}DG=JzOW;Y3A0(70(^-69X7@ZIUvoS5aVd0YB>Kw5m0pdqh?I5nt z=o(Vs2zWF`Ep9%=6I3&3e^`MHuH$s%746+orR zpBQ)n6dgO;vs~}QhHh5f5PZ%(t(&=*J@L#0YaM6+JM}|0LBrAyZc7m0kVfc~u#+vl zpLhdgRli{5JQcth7~K|O)mmh3%H@mOWy)#~T5tE4dp!TE@-C2Pf$~bv zJgb8pxLDN1!0*`)V(jG^f!m*~GZ&?x;Q*)K%p!1-VpGE!g`xmU1%Me#IF@yB58weY zo9cbM3jxL2uq9Hxu<}z5p#J4oUNeC`6f={FTz(}K%PrILV&5*-7h*;s1~b>viIf>w zQn-hCM86+dqaqMSlKJiYwA7vCP*Fww2R133J{x{+`H=pm&||59d7xm znM;;MVJXi6JVCH{@^_FF@*iVu0^TFl&F21zzQ?+=CsHU=NIZG;Fn#s;Zxzq~VY&BX zCj+FMBjy>d&Bda0LS4)CQ#>kAf)v4Pf{QUiWw+MD>buJAm<;PZF7mRjdmKKyP$l65 zJo+}ZW&5juF7;Rp&Ba}7+21j5d!Ve+^A4nIO(tjM&y7W&vao0agmIk@@E=NybAOI^ zhb5dk;RO*q1oB+)cML;QP(=JaWdZ&db|mSU-JCroo~Ox6%>%-lU@`d7Hh+KY^i#h- z2nK^7aW*8+gwQ+{f^&iT>}HSe_jb3t1uyj;{L9b(kU7BLW(iu|D;N|U1It57W zkCq~F)=5i*VJuOwl{NQCnLEEFRco2U*UPmR@dkp&qOAWLFwYk-bhE6IU0kAQjSq~5H|B!yTf3JYI@3d9{Q1T8a6N`Nw zAQ1}7iKjSY-+85gP;>JqP22afem_)f`GJVhP#UriqgYj#47?+*uzls#*VDW2eI)BW z3f-^1`b@wKu*yEcKBus+U?(TX)n9-0Ma^xG;)1f{tOGa#2wy4IRlFPkwz?OO zI#+TtFHl^v--yA#)Dyt|*a70MG9+Z}lQV&}>xs0dga+W9&@AzmDJR06@D8|s{pIxe zXTO!g1WyT+tDK9Rxty~oY_PU7whx@xFqA%87v+5@8%Sr!+5GVS57isXtn%iqcm2jo zvZ|YBzO-;tc1EWxn1nN@Wy4@+v83en^ni zk+M)?b6)O0d00szS+Uzn+4q`p>Xg1xDn~r&oIH_szJBK&)q}GG*+k)iE*l_cP&a;m zEYiy(&x&E00U#XQDdWewp ztOE!K`5kWu%HEdDLulbst+B10J@ExF2UhZCTvu5zch>)uo|Q+#7}a0vf5*Gv+6%9^ zfZk;KHQs!yMegKo-^@E4(Ut3&0ds(g;P5l$wt#?JOe=0`w!t_go+tl%3h@S)Dk%4x z8XT?Ye(0Z1so}f^JnK7p_~6B=HBF&V-{ANwHT>0qi|%y|e>qGKjYp3~b5kqSzG&bo zcgQ+-V7`IqsJw??)N(WuLua^c?H_6*qdyyvPb2lhxJ@Ht`jDEI!Fwa-TI%~Hr;yod zC;9!k{`7hd;~0-L-Mvsb09tRg(U02YC~6o+h;&FzKJ(YM88qs|uG#&RN~sOturVjo zXkxSlqnT=*+_WS^8a5OZ-U-o?*1nwDJgHa5@9rjty>Dm}Dc8x^&ChQB_7kK|Q#~+N z=k%!y`|)b!jQT3q3F3eBn?fE<)+Tq=Se91DO(jRuE5za3b$<%ITCZ=hK3Ai9mJu6{ z*4MethhAeA9m-)i->xm=Es6sznPQ=~yR9Ka$wKUIS;63pAcA0U zetzm{nJlNU+%`Q8UBr>00XM7au?|aWX)gIj1jk``>YN3nC=ysKhsRUJe;YC2!ddL9h6?g{37#a5Pdt8y}^&@_hBigqz&jup_H8F05-$=M%2{ zxb;P>BiHFqvuaP|PL6%MgTdNN5Vqc1J)kJz9J-An7~4Sfxa|FQ?6{|$0x@cJIVzhU>y^`fm_ZH*s!1g z6j)y<0fL+gf!IPp7U^&Q`Y+NFfKr}A0YF-|W$8PDa>`;)H&B95m-6Rv8Lt=0^&6~F z!beb?qz=?3C*F&7o-CHUx}9eO{{T_K4YpEPn+Nk0i0Axwtlr_OA7p!L{}-|L0gl6h zKKG5EcU#xN`$Y~!j^F~-_yIZ)99|0H zWsvFy>z)BAEatfp5c*yp7XAyl)jhHX#Kv#rfvKiK>_0nRwq-9Es%OB|YY-V1rg7f- zK{xT|0P?kj~Gz?F56 z^&AipmR-&bdV1;rNBcgO+;TPms98I#JCp`kh5=mRx@?yE9aBzs3QQExgh>JX0ma<^ z`RBj)bcANn1zcYAxbv`*(#a_70JMv;uJJGsYkmQfK^#i_bO5K>)VNPjPP|a|E$#<6 z;=<25W*=gYR!TW1(B*l6KWm>@f9?k;o++b=r45X;h&}12Kl>9YoWCgbCrup|WA+>N zNdwQ8l>l&u_W{ZgJOm(PJU>5C`v*T?s0RuWl+V;v@#9Y)3#jc$NicCTHj95pY5t9; zAUtygC3`dD#XX!YSi^ZI3Ifgtlnso<%tNH`_#hmC5D%>Oyps>Igr%A_PM){kdQb1M zOa^91m^!!2%3ae@*uzl(FphIF03vB{AzNfG#j?*i9`Zw+Yq;g3xF;+F#S!F+3Rxk2 zZ0Sd=-(u{SC&LfsedbFw+byCjNxwjLb%{+YT?gvu980-JTYN$|}+%o2G zy!8{EHz+4UX~u_hjroYoLCn6+1XtWolw$T=*>y0saBW}pKXzULBRhWc?e~@S1`muA zCvN70f(-=<>y)*}d_%crVQ(|-SGNj)pXr=@>>eXCC#T$&a*46#?7~wKZh(;Kko9i9 zWSMYKcCfxf5ojI;D7-IAJ|n~gImD#jb)9PwQkwj{*yO3|({ARCeU#75TW>j!Qtq$} za>9vO|6Ll-HPc>|w>L`4o&2x8vOZaHR)b6NZI5-rhC7n~`+8T>jrw2`Zo=(R=mxA#ZiXQ@rpRFyrcd$-qZ zquf-Nd5Q&4l`46o6aD{&crv$j5sS+k(9|408B*7%+&3{cnD&3E?lbn=h5|D788x74 z%Dk{q`*Jswqyg;OQgFgtn&2P={JTCeCxcy8%(GZB${_~8u|s~Y?aVnWrs17> z%zjwUFI>1Rt3d$j>C~>zl|pc$xXFbx{?iOBw+5864le+`*6fGNxySVLIrcbjtW$t|JGjmyoN<5w;KFs$Sb85+ zazn*~LpVdd_2LC6d1)^=RtIY^}zW7ZWO^-se)YK z;)90*-Wo=ZmrGl3DLf!@-EVJO94+G^R_szYlnJu*d5S<;KXO*te0Wx*fAA=fPg@M! zTjow$%A_w8QUcLnnRl13UQ<1|zVbL@WkMiLpFB~_0|Qr9TsmQRjxY&X_D{_-MX~&) zu7osj|Eb68?{&3GVlzwHt1!*Atijb=`fS$6DcT0fx^p_|C zP!J$?NO4m7jIaOz$>02&f0e%f;TwU&J7pfcTmS&s0M1KW4{0;xOQd)zRXEh9J4tW4 z#Y@27l}o$XK%o>BZEL8T7vPaygIb%~$$)N<-dViAl^IHcE#HB9B?IN1I&de3UTpQD z@lMVGkj?rZ*6>J)CtiXS1bW4?JxMjaU%1le)MELw*Ei~bt!vR8+5zlO91t(MdTOqB zoA=zWq3F1Tr%?)vxz#s31k9T5iV=T)n%g;R>G}+i+dT%><)Pr^aBFy8gd)VO__=xt zgp#EaA7-=i^V~8ur3P&R@SpjZTZV`1V;Eco7D{m9zpuRROEP7Y`SC+hD*jEGb7w`7 zwGBAO^3PsW_J>;l>|)g;?c{IDp7URp=O2_g_NV{k&(fd$Z~wCb)Zde9F`y2R4S)c! zTYU5n+OHN;bS|rMaL?nuEoJGR0D*`!-+CwAE^835iQ7HEQ7PU;r1sE5k0yE-vk9f= z#P>hSV}HSV&mM--;p?wH7sv)Mp}fPR=;EcTmeYK-_Ar5&)-o>k?CsQ@XT!qF2QY3V zoxAE&faIMw?+VNl5=1;YSqEslNb|~hHgz`lna74>QD;AYNSr*`21?)H$*|?&R#MPp zQ$0~1?gy*}=w<%_#9b+SUWBjFmp}XYKa|%6N&*xFva*)GCYu0zp;`AYi##x5=Q*b- zbIV!dnKv4nvKM{$@gJ+7BV{1xIotukccR?&#mY@x@F0lL0F=o!9WiY2K&)%4`@hiLu_%CCOf`hrjIcsEl7S;x6rY$HVAO;R4>Cz1b^e-{}#Kbdp z%HU9i_@uL({$&z+bBP0XMJBkPZvawTRD{~X&0CRf?KZ>U#kG;@SsC|~>;mW7kc%>ica!OWaa%bdGtV&=$BO%ZRF3-64 zVrqatcl&6MYvTJ&<-p#BD`~GN)q1uuoe9Wkt5p@RL%QxUN626~`u$y@8Ho<)+R=b-v}K6#PNSTVCd=eL zS$&->F%jF6O`SGqqm$cRLmq7jt>Z_X7Gsr1*#YVmuTz~6T}8SD5hastwhJHWz(V8-~EHO^AusZFIW;n{$|8sv}&oqXhB5CUYW3n{t~`o(}eF*jG1gDm$; z%lt6)R9+Desn}noFkEUJBb_0joY-M`vlNgco(dmfj9y|!I6tn;|6M_H59go}7r#KB8lk{fW}cA1It zc6{Fn7X8?VE=(Her+|s=^PM+C-HNLx0*Z&z!NB(_H8(p%!&BGbF8toJCNPYM_+kT( zx`sPFo&`phnCrD+<9_Z9qQ30Uh4Q73_2+tI8wkY(&x;d4gEZLKx%#jQKJz)x)OwXk+O`rS^j-m=HR{40>-hX(x>IxVOhr)${ux{)MQ0b_CtB+ zUzRezDr@t~Yj36h{eS*{PZtZQVjTi1#on*D>4yRe=TZPqw5GKtQgTieQwjsTP+$ru z7SPWg#UA#s0O0N0SbMXa%b zY*YM+XJk52y!YGXS?YzdfPLh#KgWIoI8u!Cw0RgnV)*WRA4|E2^_#ll-GBv@J&Mc& zD8?05uzNw)Q@}U(1NZ>Xb;Alfc^8(c>$CTj{TwRuZpcN;F7wd40$3K?|q$RI}p73c}y(*$3OcMxzNKO zAg%*k{W~IBo*{ezKubshuJ`~-6eNSYi!*Tn{&*zNUK9~j_)t8hMW<_9StUK4nTi1EM7`I$l<&)stWuFf`i$ z%XE>9O9=x&Q5-;y2Q$CY^|BYtwkkm;>yC5TvUeP--xd3hTvZe=gt!n60qZ|rAx79Z zm6s*P1mvaX{8+x@D%!{y zmF_*!gwcFY8_O__JRL?|H)>@R;P=Mjw5=xhPwB3BH1|t7rO@nEfB)LqdBlO>QIDD$U;BNpd;FvzJc=@H;wstpt=Es@W#X>q(nsWgB$V9*gr2^I>6e|&)!OXi_ zEP!wbP+qvoV>O?OS@=lhF{dHXTl?a$7~-|+wH6P>16e$HmY+N)GQv}91LKUqb_jvu z`I!leI~GEg14~C-39!Wl2uCTIibv zzhw#t=6y?NSN_gco`nl!C)pc}Yar_77WA3_9r5#gg6Te3^Jm?)u<&pBHkxxgoC2g% zpPG)a`_2>s=9142cjoU#(Gs2ur(Vv9D;LC3fGfElpY+G6n>x6z5R1Xe2(OB@`eYDT zH{}By4yJGkrNe1gFZyfcOnBXc)DVM`_EG-UouEb`_%mcmVRC8 zwNEHivUHLMXoAXI!|HihK+AVzq$pqgx&Vpez02wKkAInNVpS}2*{uFgW%*pWWu1LN zB%>%gNuMT{;z4od%s)JzUN4~OYFRU^wS%Wm<(7_h95BdwK^cY%w$2rvlQ-I=W`!Y4 z=A(~)QP%#eith((e*5j$=1N`wyrmwy;MoXvJyn{;_@i{db)V}$eD|%uKY&-()3R4w zaDrR0tNS^MwezC_&T;n!n16-Bpa9O`s3Dvzxv~Jo9^O9h6ss^6d#vD)?Ex^k9}fnU z17T(5T);vsB`XAiUwvjIaSy+j0(aNRNfmQl?!{XHZzU|+#LYid%EYD4TxD2U83;qc zfjd8Y+PB4mPI+evzuT=|Y$!x{_uX)Z@=@tyttw+oT~~e$;M^CJ_~6ciVp+tPingrno-*F(Z_!*?}6Y2*xEAUJDqFg zzdLu{(a(oPd9W`ywZNJ}8<|{!?b~?=W6BO4(uEC+sFqEMA}X-zOZ~gx%qwnxqS6zns4u=CefQhrOfYPjCLcAKlS2#1yvi^EDb#* z+<;^i$LHL!bLWZ&Op-W+By(T5~UId$C z*egf8MSYPeFqOA5=hpnO_emY2p?0ISy;l&mcXv23Dj(M%0sK{WYfhb}CZ*0*K6QiA z8qvRG781*3_To>}~DX7;Pr2fp(lNWG$@bzu8+<*6z) zY-00{t|?yK(U5F-QMB3DDY?{ZJ#%LFOns+iiq6vf*6zgWjVtxpi9p34+GGus56|4Q z5b@3h0Z+5$8>lxzo;?x3ad4bDh&M*{g#laA-d(t?+6F^2*F=<5o&hK#kRixKe0W&; z5x_W5I26qya#B-FNkIUM?X?%(dTw(GtIVVm7*PU5dKwdGEcG69dT;llG}4xe&Ein| z-4H>Dx|pl}Ob32257Q?XE}EBtrQ2CJky5cRY|SeRJ%`@0a|lp&FzGKnG(Zgfu;Y6= zK6AAeA_d#^&=3oWkTnH>qDBV{k_(yB!Lr-SvOhR4NH&0LO!)4ao*Iyj*!>@cz zZ+7dp@9FB3{;mg!KC3t^u9SnZ-1yHwG{zjI_JC28@REW#~ zA(4h>K`{Tvdm`0h^#8CS<#27g8xOi*?iZ(C*1Q`xF3i0j!c)QzINQ;-x20f28S_^K zjQ+YP-j3blEfhm9Vo5IG;Wzj1rN1rn=hM=N-U>MR<)d&Un&jpDdu9w3&cR);NIb zv(JAoEAs>8bAMoQ*Yj-wcqxRFk(FV|eC@S2(i?By6+q&BF!i$^@;sv`!m{mct#2t% zCI7Xbd;A6E2v-B;~wAgv*N z^rdGG0PwQ*IkRXdANrU*^@ET8NN2#8pMPT63?AAZ%RXWXLpx7McZin&iVB4hlzujk zIJ2AwnIu+rK>Vkl{94Kv-p@X?=YX8Eg1TWPXCF0Qg!`(GEdQ0weWrAQcn6>$c~JH% zV(rbMKd-=;tO8iAzx?V`>lacK4yHU{&qYB4sT|DmUx>TmzGXbvHexxL^4WJk_~;ip z2jzA0@P75`EiRC$m>8FKZ77p3R`KvRBX(=i3WMA>C%C)eb~CQ`s{53egq+@iq1 z>yq;UxhH@~WT7GDALBqj;%3j&$|w>^_3T1P7RY8m{^JfXdw`3uK0Bq?d1QVx`#*nd2j1;?gYm%2DJIE89Z}` zwQ@?m{O_fP{NL~4^@|=E45{LuoJN2;HQ3m^k%tXDm{R3!OJf`ut**vf*93GG&6Ux< zA@qv-8jg3&)4)r|fzVsY{f^v&o*rwaaVI@XH(WCx*s-E$qwno#U zXvef8_}A)M!F{SXReaj2+!QTC`_t4OA^~ijn*w0x&W*kY%|jlwp}sulG5QmvZ5=m) z`(49^zH(7y?ZgUBVWh21S^AR{12GkXvp^{k&(CVy=z*-{29&Rs!9EK0QQlSPeCRJn6nu3J!#Ytr7Oz!@Vpo3DuJORud6+y$- zlclNilzr5B>ERMeeaL~jUe&{#IxyY{y9j*89?MVv(Jz2`fk`|D$d66`AMTqlg$tzO zAkS4V-bXw69DCLQS>PoAWF6j=12gwj%9tne)G(5xJqx9n&vidQoBC2$DPz1J6<;Zf z;L1n_?^tZ405Wq1VS17!m<)@aV`=8BJV? z!5jjlBOIAW!$d#;!R6Qqz9Nm5h#QyIAI2>)DZt^5-Vs879Okr%J}}+`OYKLTFTyw} z%4CUiH(JIB1s4J~0xZwqiGV_bREZRN=86GRNEpWipakLV*T4JgbVgp|!ALCUrH?L@ zYs)hCWQ|T{iB?~fYj2dFxOC4c5AwvJk-~8o;4iG&L$}GHo|*~7XHUu3^2hTD5PCW^ zQa*kH#LrUzQ;R{L=0@~~OM1Lp*#>f|Bzv@VQTMJf;osmtSsN#AS*M-hdLLm8+3Udf zl!=$ZdN8HIFtnpcz^+{1o`iD1${4)vN(JbEeB1k`x2mtb5WWkJfjqRZgT9|u-Lok* zo`Gps@6}*+2p)yV+7M-JbPv#A&|IW)g`nJrC z{|-+QLRZQ&*YH3=!B*xC)_H)_NfF*3mA?W1Ka}!bFQDlk|9k%|y>R`8<|-g&-K9xg zIA07F?ia+G{WVf5q6ZA{FYiDp64|a{geIX3rshAL~5#vmc_|-0^HG+z%MVRi3^<5rV}$tiX)% zr7Ks}AL7a(fBVa~Wz7flqlBQ{)E#dEd166QS`@(Slka`-Go6`MUYKl_;fp@BZXNDM!EetO1-2TOy1-RhxJ>3J=;y zdPmKh6C=43-;NW`0sbM|Q;Ng_dMSsnhw$uRVJYSxqQY;!6tE@4U`J(`%R8%{czfFS zy{8mBJ~>pq0hsg^N?Q~g77Fr%XNZ|d5fQ-hC-41K`z+xrlo2vdggBz`p`7o&`&Kdg zBG&UWjO%!Kpvb^8504YhW#+ujrY)-j%4;n5k3??xNXi1v%QK~xy^&rn03EIyEbyf5 zLsrP1_V0z#2iya|raw0e7FngIQ3m{T#>#pR`SGx>G95|> z6adUiJYUY8Gk_0s`X*rmp0fGKGX|_Z?2Ht~cn+{GAwtCSBKV0;PLEVS&SvD0qatUX zD~+eS`@0+;;D1y8Oexjxd*wi{1mNfOm)=Mg?wHLD%J)U)V;W+IZiu4+aw}spaUhU- z8QATXn{c2r4yKWjZ3wdT@y{_(90Srk#xxz;%efjI1A`9T+be#_wbX_g19nxs8IA80 z2MyIbw(8#(;KDIeMTpj}6fmf1Kq`i9Al+0L8q{@^KhuoQj4Eht+M6Ck zdh2rxYrLojqXDd)9wDXFYeyCTN%dM{}`3jqU2yUAjZu}k-qW|06xKhpOEWr;!#bdBJ1OZYfM@R0SKj^wO+;UVA zmxonn@Iy(=$MY?B-QKl)@=J>u3j(;g69ZAU(%U&Wm!*6#OD^r(nr-P&L(tmIA_uIC)kxGc&weYvdR4@zt2X%P`_5Xi!9oR8w%otc_L8?Z`SA>H98 z0wQ%VChDX6<$&L7597r+9D7t72noeKdCRz zGX4moSdyN~^>XH^8wXQT_?)MIu}aDEH=nCsSbMmR1&O&3Spi7XgivVnaqUD-+Y;!- znXok1MnJeS0uYZ_zQc-P#8Kx0>bwgD1BwN#Y)Z+8vZ*{{@uBA`)B0vDr3cEDo8_Of z0-PF9rwN2qa2-KvA|8bU)%ki6u&%vmnIsUf zE>c!j3Qzy7PVe|(@U0AvXV-0k{FjQED7S|o4D1e^=ynU`@DMZtr@C|QiZ)^O@m z0X9p%DMA8F+ZGi>bpax`Q>q*4-0z+E`^7%*zb7x7{{@?UuR;!sy%Ys0vJ=?*P-yUe z#qs<5!UaAyTpV=qIl;CxanK&hi2&h|YA^P&oou`Xq13?RV3LBk&;BhP{71^aS!&@E z!PZZaGygmLd-!g9;vhSlk|C`6JE@a?QX}x|xfyb5nFO}H&bzr~9H8zKZ^O+0aj49! zaZ*-nxzTIdAKkfyP-w>>bsD6f<=?+3bL4(eT3su1=w`P4X#oH#e_t5;KrMSUo@1OjBB+HK-E)_tNC2b}BhKIV zfB4>?w>WRH$Cqb+gsWn*Pcd{J&1h{i#uKs zxcd`o_2}XE2E3dB-n_;vumjS!pS=5__V|0>e(4t6{pu|>cUNq@f3B1^{GtGL_H9A~ zj8N~fdB2tZoqIeuO3T}~OT7#Dhmi1FG58y?9(x@9LOoFS?(OX=^Xd^_5^a1h*9srg-$<{4$$sJGW#~1w-I>W{nWb&zq|2)j62cafNhpcXe z<`I;sXrQ65u^}#R*ei2a|0?Wzxw#tT47N7R*Pd?ZsKVBvJ{{X?a+Ex@M=F@@Yn@U9 z0r7R(FiLh+wM&6aI+&X)yh=SbYCtSE+*s=nO&`XMAL@l%{}_2M1Re>6MHf2V^;0u; z4|RStV!loZl51re@bY(e?Q0|0nvzDI0;yB>6uGZ@fEe_yeVxQ+~6mqgq{?k4@X zqgp-cWOK{C|KCvGSf_t(ld*l5`tHfOAE#8iGz}wC!A|=#npEAIscr23iyrM}Nd0cC zoM|yOsSSGWpK0}NI-%TEz3C(T(hyry%Kh~eQ+Vk6pgM7+l*hWQp>LYqK5<<~YMX_3 zhLMF$75ncqTZQ~j%>CK3Wl5Ii1*)EN@41uBlbgGn59Xu!9PSaBS(QK)O+j~6OLu`l z*Go%_fbfL$AAkr?S$`HH@BlneP!m~2CK*`~9^tdiztt+iBjAF~K4pvdBS zSlr?4ty^`eZq@az;ai)OnsaaV@7jptdv6qozsvGp6dwb=i&}zD7X1R|NC%h|Z@|5x z^Z}I3n{Q#r5x&M@apFX7kz-A1H~NX?o7_JBM*Aob{2hgHW7BdX7oMx8sD#C-hnZ)Q zwuxc-*xC?pgccsjoDTgzFmd_!$i<-6^ z`ix2UQ-gSB2P%g@b0h+*m4W=nML}a;PCYc8?@?8OyvI|G+lZkU%dw{$a4kj*6gZR) z#zeo44-eHx%Wupcwr(5<5uT^qN)`zKzj(^s0`#Fc>$AHh!hn6X8=!ygxu6RBmFMz|o_i#Q=7qosl?q;e<1MXQJVB8T zz&vhm?OMdh(P6DFKz~>_Qs9_Zk4Qn8#+!AwDB+vs-mgmk-YrIkjnXfL$zu%Im?;wF zd3jL|%3LjqEZ)L6hLI10%wj3RyoK)rfbDq)v<2J;XSmNzs|Z&60IUIeXTEK$Y>!eH zz0wL?zc}&T5~D<<7gP``z&;458Yqtlf);XCYuCqf2LsminZ8}qzmuVzzQn^=RDz8o z`WsvU{1OocV%pESvF$tf-*OMprf;PfOE$c(q*p|lLc2HDNC$qNWiv9+@0ONe*Khb| zQ$H1=6yZkK-iG&glC2^D5g9X&R*1BLr~+^j+HJB(f~dH#$`zm2NQ}YdJ}rj2r)BLv zDErk9R4Xa_0_M*%tpC}LnE&4_KyVvlRgvqR^6$Scjo&Zt1)=|c_{V>%@|m#iuN6Ze zb?R@v^S%tAW(<3hZaZ*huT-7*vPandeAeHuvzDIX{2;-Wk71Ggb~reAZvoWu*sHd> z)i)OpOc^67KiX%NuWq0KFFa92064<&6#`zsBz>|l>tpSIcnYIToVZ)cc|`>Qp3N5| zcf50esPJa<{_`#q8AN~O#eMa4MHlSv?-$SfcX|edb*D8_Zk|G^;vT&IkitPaXLau% zCG(uaL#`3Uwnk}5(!pZU2pB1qGMv+nMd0So%mxvMCfvAu9Bo|+#fQm-2XhIC}ybrfN z`ag51p744fJXs?tD*-Wy(?>M%bf_EeR#K#9nXpC5Xfla!9ev z?pGfvaa`6cq=2LWKw22{C>@IZuCzfU#et6zoy%VJL55$>e$oyi^k2@7Y*Bj3`!DY* zDKJkSJ+zX4L{y46Fze@6uD>C@OFSG0UM)yQfGo!WZDaUAwo7-?L>dTuNSvRlW&jx? z=XMAFwRf*xu5+$`_4;OfN8h=4^@b_B7ExQD@5Fz;>s6O|hYfuLY~ZvupE_?^3V2x? z8)`|#VMMJK4K^D?T-R_!s(3G@RIzmR^!xvf8qGaM$z!mZDryR7nH_xA&$>}%!%8iR zB8`j)Mjc+QAHygmAb`ety86&#C_k%nH$}a}JN5moUCT|7-NFb(Agp}DxQrot`SX+(RQ@gSu;|@m_z(6cWBJvVO<<>0Q^ZWly21pfPK-IpX#y; zyy+OcuABPD7XWDL1lG(isX_B0waINV9^G4?nX4ti4KqzpDfO$hX={DwQ)*=);(HJ4 zTYIg?xlLXg)Cy2{8I5E-csSFT*Qdf6*8ykImzQ53(| z$|^PN$(Ui}e8!5uDX$36!CQqK&g`9&>`)@!Nm+G%C4fw`dLoRz~_C(~x+;T56L(vGPY`hDQc;Q8%Pg-9^5ms(!nFmW}Sk>}N z)F+RUe8%}Osm6|}lanDt36!9{5C{p5pz;jy0W!}kaX5evFX1-L8 zaJ6(M=#?S`tXjYY84gf1k%3i6FsR2`!P7W*WlE6?7@W#{p@^zFK#?u$;$QsopNZA~ z)pF+-5Rb`Qn$lB3iV5A6Cv~py{6oqCmPLzqcS_U&ToS2m*8p9>;YMy-a)8OB!2tB! zKQRDlV0F@MTLCPm*-Hox&705H$`1n(0wx3agP)?7!|!{E?^->euHJP3c0h^0-~1?J z7!d#g-cxEv*UF!7q=dEsVi;g7K{%+K`)n-w+%xy*M$Z0H<SS-=N3`jnluginep` z5x7yr`w_+s`k=IeiSrChaxE!1NiC&<9nt=_37U zx%af}+kfZp{(sZwU;csKE3#s0)T$R5mf7)Pzzi5Y+i z6=uzIj@&HZ8^F7((weuGD)5s7@O>`{%AR1UO zB|i4@baqxPJzDEx`lxvQIU9fY?zd_)44dqATV6Vsy_0ANj20%Fdj{N%H*6sixH7K=QF^fEru0x)jwma~I23id8VIy}1X zrT(^EjOGl0@Q*NYi$=Jwvx7Y|O8#Q#APs>phzc1WMtkr{ZKZJ;Qwe{{nns@PvrH3LXT`0M_19FA>aqvah2{;Q43%NF5M}Mt4d^T8QjOUEs^27an=Z%uVC6Dd!38 zBWJ7n@U-s7(oLq6?Yp7=unvfhVBDOK^dG`NbQW|8SpCi5u;GNqRt}Rj1J@9S3snOm zx-eNf8peT$-ifG*$;2EX7~rrf3}6xRB-S*PG+>u=lZIpW9I^^b**m-W({$&I$^h2BrEUqjkaO)q;LJ`DOG5>{qi6uvr^0%a zM`$|NdW5N)DE%}E*gg&;Nfoq)lQ#t}PIXWnXl|3=$)_z;AFy?Lm~@tc&Sa2AQxKM+ z@kb+gpK~__G0qTv8%u5|zc*skdwPqMzXTKC2~u0~-I&_|a|O+LJe_H@SVt4wqwt~s zlTT6qmQ`5{;mOajgZM-0^qDNW!CH5R2JCAs$~r1lI`aq5Q(p!96b#d1)sA7yaCF@*@ti z4X+k(?9~GV?uwTxLZvs|yP>+$GCT|duU3A>i12jytV|O|Aq5-;=~VO`D?MLk$ej2IYAHXV@DSG*w#?Jsi2%R(PT$f&+~`9J>|vMiJ#fr9xwHDaBWlwy=^+Bx*x zLB^S6-Ee%9L~SI577s&-cS{fs&8M<%;e{~0f8}L_yBN3c|fj3QM3HzV<}R2 zD;YC}2Bi^PdrbxvKz~Gu%)DvK&!a@$Dh1%rqug^; z>=VCF_Y5sUK!Dsem5r_~vFT@v1laWNlo$+t0L^E1qeHT1ZA^?yfVrG?n$a}1XYmK| z3k>1mH}7~n|JZ*HkZgp529{$hM}k%%z)}qsKGq;ultEz#4h8!=d~Y z4pIz}Ka`t(iDBua>?7r~9U^VW`7LYsYI)zb4~<9UPFcJEUD+!hm38$u|JJ{s{_fxV zPvqqXC_f^69I$-y1Ca@w=}D>dXHwlZ&_7 zuho_j;q6yrrG-|T7t|`+WbNUA_jlj>Q0D=WJpjbV^1MD0h?=MpiIpS^P)-}{0T2`N zZt-`*>oGj?e(6j)IZ>O+iT5-$o{RUMGJx#EFy;d$9~Gd-UPp>i5YcT5oK9uv`{2XR zWQhFso8KrjpS~SCFF=$dwR7zxz542#>E^p1$is|bfoQRakf6t6cXMJ z*e2D1JbaM?5^X@KR^+{9^2RVh^&Fd%2QrTAi6G8&3S{*`X@@lmcK}9lO8EgcRrP@K z!Nm|jK0f>5eX;#N(fZDF zvDQnZ^5OX>@@uz@9a%AtoMRTLXb~50-~3R0eeB!_e6F*J6ucq_$^~*Z5xMZ>@qNu( z_=k9RUM=k_(#*LpB6^W~(OKv_;}>CMmmZ4e9a2Eff98pbRpnUA3fa;)7%ZF!p7X}8h+*son@tg?2u|2=S*1tN7Wf(y6oPL_pO2D zp@F-t$@opaJS`6}43| z5_@h#tNK_@W9yDjSdRdEsGBEw(iVlc6Bdg8t;p5-)C)UxW|bILZY$Ooi%oFn32$O< zVG=nOOq=mFYf7&2i#6KDK6DbUMu{};n9oOju~L-MOR@gqstWSkMAVnD_PcY9_cN8c zdCpCC>ldcL$^CFsOkUjvoSC#UBBs=>jmnde>pIQ3UlO@1H5t2BtlmmO;xC~0*#5?5 zyy2GciRa<#0NyTM_JhZxu&kGT@1p^NNJa#7?m-FVjsr#P>!+3*i4wTGW1f7~CN2lz ziIBx1UMDfz&y97O1D1o!asdZdV3X(b4^qxVScvzfJOfaS0w!& z3Wc8-aiB<*%ripCC_Tf$HG=|tPW^MdH$T2;huWFw8l;?9YY2U z!hVZPNG}i@zj6>KuF%Y+3@?^e8V?yEYtkBsSfL-rHoj@ZUs}u7HowzH*E%orNEk34H@UueDGC|ADCIxve~dWN^JtoNCR{mTeYWcwE3%@A~Y z6is-adAlR0pWh-3J@Wb^e;p69_yck)o44rphfUx3EWF*pXtt34hl^GL?UKFI-5!E$ewf1QrH~^6$8;&-3LSrf&GtqTt^t z>*>EQow;4Shi6`WD}D0U{$@HWV!(SMmb3if$9g}tmZnBh%6H>bnaJB}hGMHo1F;}r zU!~E?{t5@my#gX>>w&j>;;CO4fZ0EIpEwsV?1}@XoF}}?faGh%=*YfL2|?O<@W6=u z05OOO0|8!Hl?L??d)j^Xy<6fcz!S=IgG}&=%6l#ZoNaHK)bM3I-XDMbvl^jr*EtQI zsyr(t_|D3Z7kS{M{{Sf8e*69OyzBwL{?%V-|FnE=GhPh7*9Dirt&e^t5Kag@;9tgw z0u-Yx9D4-&FZ=$dpZ{Db248>uOJ}ivpnU@%$GbUkei=aJJ7v6pbPP7gh<%&9_)Sln zQ#3`93(rR;k;}hu4w;3Y9sI-Ki5L7%0ql6}*=M;QWBW1g-udEL zqy=!^5FPRG!R?yzv2vXa@Q*=-&v7cyK1L7DE!Nc)mHc}ya35#C8Sj&#UARW66{T-G z;&(9JfM=Y+(6Th5T{B=?N}&^9+n(AP6yhtdL5Sv{80zOH-_0_AuS!2*qYWJv{c3)4 z?0WPh(rkyo;J(|t3SIr|-n4n`hwA*UX}YyH{{N<4r{p?z7wVrXj*f?o^#6(w9gl?* zuwIiJ{789(@eE8j(97+UdS^orl^|?XMIgrP>C6ozR=qwfA8BMK2>{g}m401AElp6l z4YYydoGUhK#)8`K+@T*6VF)<&OTqzv$SjWy9u+snMQzNT?Ar`v?@JHdUKW*5?;5R4 zw}7N>Y$;6p`coxIan%Bh3HHUHdOjg>!Abh=fR zJ4=Ymb>d@-0eV1Udu7@L_A{O9A{{NyZldbKjCInj&z2)Ij0DrHU(Gx+L|d_cw8fsA z-K@7-q$Ur4ySt9o7pvKAyw5QQeH&ey?Zrz?5lh{kHbnS@FT4>j@0#-_cW&hnr)`$ziWQb769LH+$@q5tbffj)bJ18mnr81+Op zaIXswC`6%QNdcyU4SnHWd)hE!q7!48b#8!Q(&xT&UT#KtW~;$~JO%`qii>9oFAWL; zhq+4Wot{XsF^|LaWPkKc7iCrL-J%=L0NrzjE(Y z3do2MQC_2NJ)S1=_+uYBBL#FS@BO|LBb}aFnKy-go^b_tVXV@|#+w|t+E(z&_1C1l z@w33~Y$hef6yv#aR!>wneqZ5;$n?1?0SxL>0DOyvShVtj^V6!VG6yK;Per;n)w56z z2z|FOUCW8b=%F%wPK+1^2dx7K#c%?Iw;rRzY-V}3&!3yupL;J~x$4h*Y$RM88%7-H zX%{G-0CkieJ_ig3Atb0&hfr@j3t4$&AAwTZDu6XVb6S z$<26b?KSc}{ub>2)9$RC)cr2i-8487V*Gu%y}5rb@ISQDiF$u>BVC;O^EOhe1Q3w{ zlTMz=`?Hblx`h9Am_N z@g3aL^~l>Q6zpifM;1hm;eFqK_Nsmr?2IGD?GtJPusqc96*?ftHkUZ0?F*NL_z>E*;kbUm_P6)m|3ronA`(=luYl5B4ciak7RWVtn@Wu~!zd^ao^rv8;pl z-~U8w!72hg@Dhe+^lo5e!s8A&ze9Qfq=8h!!T7PcRnx@YdG{l=&055B&iQ4G{7Gj9 zsaU&ZofAC(_+L2Z5OdA=Sp%dO&^`tP`o=snR-)M8oM4WbCri>15ttfPcjts|ig(J*!Bn@&9&71a zAAitzfdEDI?YnSGn9O-n2dmnAk z0sOA5+^xM{eszw#y^b(3)%mCb$qhT_-dNBXH)4zy0g&N-@8$xqTFWR%D2CbZ3t~MEh+tOdP#C=YpYr=7C%ynn04j>()XAVCtUDBj zW0BfTbhsD*-qE3jw*k=q+{@M>hGS>u=Uxu(xtD2U4CVz-Rgr*Y8E}f1T2TaeZOJ=S znK|%tJ;8bGw{U$jAHiO34^faqYy+n zE2Ur*CG%?WQattYd10g>Q??R8{0)VlzA&e%3bC`J!<}*S zJd8nmMY+Kv5_ymqNP^=a(o0wy7z^-x9Exyp@N|*w8s;Cy6QVyzd9aj^r4bkTd~0nD z_J;5o0D=e*?l<=TxfCrFnnR^648Eog()lt!$d~bi;RAp{TlB?JGZtceMp=bO3}Z=L z9}L%opYj|OLXkYq=Q3b0&u_o;z9I=waznwwn1ok|cMGLTB?8NQ|5z0HCyz}5oR@xH z@LhvkbHF%2ev1;8rQPS{xo`tqg(G0%WPS=yx823^;>~NVocv^U({GOxK^7SJZepUT4 z4Nyo?`!x22JJx%cOP{I|rceqO0hQ{@y} zJ^4vB>{@QRfeNB>*aINEQ^eDJE^u3IO9QkQeyFPZX8I z`vhqJ{=47ElPy35%Q>Y7XWNyj-He!nr}~^ppMCao-47rqY#jj0ojUtNF@t?B zQsXcVV*n(|0AmNv`$3%6P5=myq-FJ9+V!3P|S}7z~Vz&e+m-ycF(qYQ}27 z^k<*{LVdh_+w$#${r>#<%i8CHUxf1pvO&V{0nISwL*z!^m>1@k2q^l_dHcbw&jc<3 z#zYiEpNVE-%#qWN=Np-S^UbdmNx-~_(A5p}@DOkxA@=NvJok~4)3N8$cZ@%*PGkic zAq+I>7?_=yxT9wNt^a;-SFTVIA8ANK;1CRcQ)G>_jAAb0` zjHJ2-E}J4-&QQL%jmCdjAvGQc;@LSS&oi3-+WixGJyNXN?Wlv zIr8&w76S!mCu1PQesAwwHO_E0a*hJf%^38c8sAuxfOm}Tqy->HE1NsWIRQ7+>qS0B z+_x|0{&23KBdAPo8QZa!3*-!5C=zmI5s^!)#KYP`w}@w&v&PazR!$K3GMzF&MgQ6Q zb$e$nq%-HP^e9n#D@Kk*b+(u@!hvT68|cY-gtqGBHl;i@0)Uh&p&wB?o&Mi&gzNAI z84Q?qdiU?s|5NI)U>;nfp7>(ReP7zy(qofUS@OHUeTPSBXwKmD0(h?X`-r2J`mBc3 zUbxmj4)<|IpzW*cV{TBrAyc0ZgWr>=2sc8)se|}cVm+uH86G-}88Y|ZT9ijkg^u<@ z#%=sDxTw6=t(3er?lhXa=vsrTX%x4vGJqXujV^SLjUmJ$>IBs$AU+>GIQ5{j3A=XK zTlekbWoW{!F)+2wZc*m;-bHJpMVWHqW1xV z)5~6{(V+Qp(!bY3w;=OqdFLjaG+un&Ti)b1=_QnNTkpg8g&oazlX-nBQx*1TsSkC- zNNrW66vTl;XB8OgJKJveDT&!hzQ2uMM75)w=IN|^vdL;jzWV-?+J5a^>_RWtgWBJ$ zJlwIu;ZbM~4vct4x$T~_j*_5II6$)Jt6{k{2K7y2Q7;F#g>G7oCmmRlB%-8W$NVZn zJq~_(q02asCXRyoV6FUtM&jItBl3A5Rz!qB7a4%b{S9zo^`Vjls5Eeq2%8QHoaEMDys-QjK1 z2b4#Ybr|=VM7u8HasvDQ(%QG_uHtUSd*Bvj>@$kd`21}Xa@ z@GC|GqEi4p2$=Omzs$Qi)QAk^75TUZn2aE5=H!y59PcW;AGx4t{^q-1t6Up-;sT3h z+r#sJ?7;uTHzI-M0?OWm=#A(ClujpwTz0#V<*J((8F0DjL1ALHb(1<)0U?{>(u z*+bs5h0I4;L50#+FgmEcNe5i5_PuRX>`KSpZg|1157y5}9{?aPhQ8nC;a+)Lr~Y@Y zJ55cy5V*^^_I;Myp)5e;ecLo0PC!1!7i|8~e*k%;JczTv55cLQ*Qn+jlU6#h8W>y_ zyB-#+6ouUW9%O?Xxs@ek{9AF2UpEL9*zn5=GfRE$_~~^824D&1jyk z@Q#*$3l#XGOrI}n>}+}Gu4Svv@IRF`e!F~s{heFs-}`_5Ju9Q>`zyx9(BS|xgg%qw zj-iZyyoc<60KD6(9skfnxF1wPPR0@?(Pm!ZgnfiPF5VwQ&Y#O_Q3svB3*WrYIE`8RT+!Kd+_j{KsBGSXF#%y z;Q_sXb-eEe7L|3RR48dL<-Ox)*3f~f?JTuFs6tb=RMDkJeZ;#GU+em0{H=F1u|_Xm zxl#XKvED$G;Bm4lRy7W)#nq%SnxBTV zDq0PJ#j=KKl-l*7{Vr z1De08<`U&DkH4<=)C@K$_tp%@_&N77&8f~{_CERtLuWxrzGTdL40ul!#G^u<9zP## z!Y|vntML4dk83S&bJtncmjd{z&3K6R+*GV2IWQ#6bdP^8yhlIFhJ#(~0nNWMHC zWg-uXQjMWPp71=85{dFb?mUM(;i-%-yx$lFP(T5oA{->N@Z3cM$Q<0$O(}ra@ubb? zp4(_1ZREFdxiS_~1_A)fVBy>imM?F)e|V!rqKNV-P(IYT6^UN?8RZEj5=D>Si8$bQ z6dth}10;&F^-A#^sRZ9-QbS33_H&PDKq2Fq7z_CQ$Qg*k$Rn~Oe%pp=r@AR@~GmIeB;R698#pM+g;VJPtD0}V=-%gfY&85l7&wp6dOX?yW5UFX z@3@AgPjk!BH{fgS#HJMt^`~uvlyM^R`2nacO29qs{(a;nPt7P090R(i+u0&&AfgTM zVz&~2c0-W*anF%I6T^gs(6+iWBB?8-um9s>mHx?K%>Vzlzx^L7Up^vbSnn7-A?hT@ zJ<@Nom*xa4_hY)f>tXVm@A3vo@^ z3ILx1mt`*{B`631%?rCN?=WCnM1OaF5D;U(Q>opuR|mh&p1|gNIwO86!2hn-u;?^fpMuPj=lh_AOJ!OgcgdgVeXTi#o*7J+`oHAQ4J!UC9PoRjLtd?%^#G$FwT2!Oo?<0g|9Cc%E!k5 zU|#pfMtVC8&Ua+q|M%bjRGxdrOg|V;gvN8u@HyWfY7O5X8#~H)ZkG2d_-d4fl8pE; zJHG*33-lG)%Q)bvF)9lv(!%$2{&MD1abR<{=~anNq%&dQAT5u%Wqp}0lq$ah#D2$z zRxyC^{fLfCZt&(EiO7ZJ%A*M>TkL6{=p~Budus1^IA5ew`&k}e>pJ8J)OVo2zwX?H zYh|1ku_9gBmH)-!*s2SdY8?ow+5c+fZ*ZnJY|!A6(0gpvxdS-uf61X=Xx3&8Xl=>K zTY>-Y)&Dy*HZ-_xhZTQN)v^wRh+K^a4I9>bhxmLQ5a$tAa?FuZST;J$ znW}fNj&>L~mm47Trd$rWA+IsqO6m7FVBNxzySs8H=lb}HbgBST>P#Bz?N%uay{y{T z(8itJh%t$KoJ^u())9QnoxEFH*;4@aL;~2s*L9FufmQ8fYDSFgt7NF*-H}_;rG%$y zippb)P-+9txr(Wp1KND#+Migk`2_*JILr`EYVS+^sQab=xqmrQ8ifP&OFoUl^Q%z8 zmgW;WT8DY`B>z4P^@7eSlZGzTXIQJGP`j4aYy{EnNOU7v48!P2UAH<((AH8 z^G*6?Mt~-V?BV_9L9vZLHtW-5to#>52q+Kg%E)^*orG<}YnY?x%srIeEW3k)c#9^8 zJay%^xk50wcX^`nd*>=qTmUQ%B7R4iG2nJwjUG~p$Wbexh-XJV+o|&joXT4>lZ<#a z%FmIP+oOM2y*Z#)&iuaRL^;Zf+%nQS$?%GNEfBVU#D2*$_XQ2gk3@E|~hg}k^g`i`=R;!l5QSjz3jv;tq=0G@s6(luub z9;_^*D|GXyY>*?> z0oFt;pv+m6iltHTeB}1zU|RQz zFa4fKBRF!C_mQ`A>hb`v2g5#ngM}X>h_U?}07n)lu{TEhfYEGY1r{z#(mEntfWI-0MJFo}4VJy}sLc0+GVi}EzdvGa zmG8Tx4;7GhP}beqqUi6HbzXL|bi1_kUzh8@DbK!fvv~ji&VQtLlaP4!9+=$?1WyF2 z0qGAP-IE6tFd$DPycigHCle!&o6Tf2wWz5{B{Wr3QZbNy@Zl$7swW?ZEv#ag%-B$x7L?DDB672;d~5 z0mH<@(pThg_S`w0Q@rCM(8Uw(jQq%0%=6ETM3nu9y+257#UXI(Q(b%a_KyzgA8Mav zu6W;(6AKz+4tvq}-+!Zd<2+@LLx$PQ+(yI|eN~CW0>&Rauyadg4ISJuw_knr$C3-t z1{et74`8nqVWBh3bMBFcPV@o>q9>A3Ib*lBs*!+m2<`w3A~9ZoIED`PYGk6Qj!(W+ z`al?T2Jzx-RZ>9e)MI!i4FMTPT9x(#A0PWFU#v6E!Q-O?D?#kl9?%sqrtn#W?L*wi zbB2|Ln}71z&($wWYq?*+wXLhEJntA(?%y|(OI5rPFO2ClfOm}eoMR%ab%(*$mPI>W zz4~fdV^^#3zzhSqZfF=NkjYax1t=#uKelMU!T5V_+_k?>BG1b4%|V zTpXO2MCHkVSk}{7rB1x1bKHzDhuSe`5xGazgxQYPn@Z#=7vI4DO8TXxY7Qxl^8I@~ zvj2IV{Esj6(eszDn=!s4-CHb9JdHtjrcmC88r5GTXZ35vkU0e*9veq!&H?;$IOkH0 zp0yKEj`)CT@dW?+-=hDg0M}!bo8!~dWRXqahjm*=;LIj#m%}7?z`EYAGJvUhiKCJDZv>z#plve{(r?YKEs(qpuj7(NauADS zFxgG&;B!CBT$k`@QP;`qS8}d*^%k&Bh@Cbxo{SFb+DaRSE*N7Cq^ItUP3;XB^-qlt z^$}~J{e_;7GgkT$sXvp^%b8zTRK1O)YrS338!2KOqeW5gtH0OH!~wk_{z=aLTU~`2 zKfn{vtfr|(_oTK#sRP<&tFvbk`nv=eAT8;E9J;#k1Fh*9$0*t+#V0>A^}b zk~t5F2h;$Z06efGqabQLUQ$k;escE9e*i1$>qfac85dAssQ+9*-MI@Ft>n^tUKP2O z8#SVyu?@22cW;SsXCYEKzI52)iS#If4KLFJ6D}SK0XNSNM?peqT#CHTz_%%QJ7&oz z+Ckx?ODoegxNg@#$#W)eE9=ubmhu{3>mm2ejWefRv@iOi)cjZ}6 z@eUPF<0TYUQYlJXhoz6C0i3}A!Wtzi$M-QLZ-jo$v(C5hQb(Bm+)pI{dVpi*J95!i z-Mv*q$!1XKOVp_hU+fotU6kMx8KHm1HCw(P&awrPoyq2LpJnsrb1&X|Lh5P1HwvWG zO5*W*@CfWA1I^tDRE+{r9x)6A0{+Rl2)w_`Fb4QFq$ZpWjj)gFNM$%l4TLKi!>{kl zD#q_B51OJZU?T|3{=8Lk!%S}d-s@zw(aCP+&sCy~Go2rnb@1!5e|(Fb?=E3@DhNr zA99xA6(&l6bRhsgx%)g18HoMA>|g8|sx~7)x2?z}#>zOUSb(v>$mULTE{aqZ!xKb) zThzS&Nap~C1;+H$iA6bwB8^}^H=Y#2_=%VR%+eRQWY}+H=+C+CtpI#dD%eAQqX}YFTeWh8n+oh?y7CP{UUTNGG&h> z$NaK55q_&E1`MRECDJB%9{Voy!+Au1RL!A0lQvZL5rh23RECESKl;Le{oUJo9{V%* zs@}YK8(vjA^aojs?caGM*ew+H|L#X>m*>#;LzUUxs+gb{^U$MUFMg`oI{>GqfH!N6Yui#h`Y*7;RXqk5!@PROg26M_FU# ztYF@sxN$%cY&zqtydeM`!-4o1o;|LS6nsv~2u9~8P8b-*gQcgmVccQNq}!m2Xurpy zGBG*7@~ZJ=(EjuNr~2EmbJx(uS+CAS+ee4{4g908P0Z}t6aTG$^}xUXJ-*8T^2*yb zXH?=ytH7Q`pF2wJ1kqh1JTi2t&f&L@v&cQGfqMhjna=D{1CvRAdgZ`E`XP)b0nbC9 z{+H4JJKWdsRr5+WAeo!~zm6W(z-wpcsF*4D83n1O2Q=I={EjeopOV~A>(Jq87nQYK zpY2!W9nf|V&UGW`SLjG0bsaK15BaYfm5VnFK$IbAMJ)U%WGDf_@*j&0kP8^HaxWHcjUq>%DL170?IP;4yl8$8R$n|- z#0hwAlfJI_3oq$t;?Q$P|0hBUtD@Aq;Qm*G>0-nkX zg|f<=ioB3;^qn&fYzoRQRN-W`Y&3_F3NlSoOFpItFu(#P#tOjDP(|? z^g8PuuPKV!$~FW%_S^mnZ|Ce~?sDDVR^2Aklla-D6JhjrkA=hM%Gkf>hgf*-<9R_C zSef#(fo}kJCI92u*{cEg9<%VLioH6!L2Y6He8b0pNl_6C$qWb4SEU@Ju9ROMm(|PO zm+aR@Y68!}yDnO;+&Bs98IYswpc@$m1M04ajd=jF?aq;#9|VX)Zib7HkNTI!h8eY= z8>wKcvS4#Lbs5e4o+X}7YIhd^^?d=*cMpxnWI@DAvi0)XwbzRPi$SWa^FwIw&aW+vhZ(}TlMmqc&71W^Em(+k3C=xZ@Q6{ z?F;x?KKIg7&y0WOY}tP)1Ni1!@5q2iB>})BV}e)^X^OP2y!Uu9H$3$VZ|%pQeyKK* z2Ot`NgjfFL^hh35-eY-rQ}S}6ZwUCaZ*#pF3ht+a@+^7Wb5gtPaoYui1LR*syuq75B^~Bg2HUc4T`8Vmcp1Xb0AVciMBl?n01+NsJCHBhygX6A zjfC=4x(!VIw2Ki2U|lQzxL!kFsfNJ(vj1WbLho33v;!a`2zmI697%OR zJ`a5E2(6b9CYvJz@Qi_lIY0;EY`|C$?~78vZhm0k{OLnQAuc^-kM%;T1v<(>IfLmV z2JbyDw}}x0;~LQc^4^o14{t@$jg&4|)+X-`XC$99Cm1Jkw#Wzr-mhsKmiPa}_eIOK zM_!~RqUX>qgrUiSKkF=X!KFK9)^*62aY@Bj|M&A3uge)_>S~KXU7Q|ExAHqu)p1jU ze*uA9Np=Ji>b+wAy((ADR;X3KZyY+?W?H*OdIYgRty~?^6f6940vL?UgT$atJN~)7n^wV+T$Ow_mxf&R1=)JG? zN#>^z2f*J?)wBMR1>Ze(^l5GeknaCSKsB!qui7d`%XAH#kAZLl_Bo}OflPmD9ve3D zOGYuoHT_cQEqVc;Us$xEBvlk2BO6i(f9S{#Q|c$Oy;zaQ-;bzwf_uMW%tIJsQYXFW z<}ft!!BoTT^9%CKu~D=gqDSM^O4Mx*F=l(ggL`A>Uw|gP!MCQ|byFVXNzB(cSHZe( z@9xOX6!#4E=^>IqHNmHwIcnj?`v))rxC7qSzkZhyAU@M0l>>b$7UNS7r;d8cI^c?O zvJ<3!hgOT0!wV}mhqH3-Dfd>Cgd;KkPpg+)fO9cdh+fow5B6XHsXPkhAiAP_`)8&U zZ&;cD3Z!!R%OS}9d=4<@neyN-m+tLZDGC9F%9!XUW?b;{eT+v{3!XlfqQj_Fb`W`% zzTC#yt)C343P%RC>|3Su(=O~go8ZF z3DS=Ze7{;`H>`c^s_f{c8nuay?*F z4E{xNoJ_2SG0iehX4p6{?;vx8LZ}plGzciOevva#7~%sU%-<_1(D!4lxkV+4yz%Bu zr9Gg8K=zp2vq;;)4S<{g_j^hmnS08Bl|aJ4W6>a4KpyWI%F%97;ElD~Hi`%fMHxd$ z6p^7v9$*MKU6{cH0fZtIhdA*iL6Z2aLSj+o%^OG+v8c8WFPxa2=?PH$Ui_FWtp>-7; z6;Y-X(99kRmk4G1Sg(L;_8CB^(gq$H=*P3IvZXo6aJ;2G)`(&w(uh5p6aj!c`v-sv z0Cq)$LumI@;PLjb12uFH8P!|0Zg-yV6G^g?IKb^+2q7YW;g`}Zu*{E<}yP+0!Xo<~&K z*%bT(JePCj-W~HSv&R7LBGn5aAz$Pw4=~<$f$RI~%S>mdr7BRik9odTyyl!GkTK#} zeKVY7|(VmgCZVDh@ziwoIcg6JoSbNu`rtKve z_CNjX7i#~fAHNkCjuMDbJ6?NyG2p&^^TYHb&x_KBO1U^!)3r!%VBXuaU1tY^Vk?NUI2?G`mieS|zRfYZE^8O#XezOv161vyTcW&u@uqYVQ zK{TfxB|=2ov6rSr_SrWN@Cb5aT^x#+z>Eu#g2UQD_d=fg%G)>Nft@v3b4UuJ^8U+Q zv8Fh~fqwYv7u@SuY?e(y`N=Uh1eD$;A9dqmIHsss4tEqWpSZ_xi!>T1?B z+JQ7eLZb(-$;yFfdf6vEz-|5Mnz1J1Q*SG!9)+~=Pez35sc(M$8SR2<%<$fOXvKJx zFIp0+uq#?j4Nq3&8aDuWK!(4V6ng%)if=vQz4jod-k8vkU5XJ5ooba}iA`aZR1_Ny|#aBb~L&Q;^7As?d&&c4i3S3k3RMy}SaoJW0YV@s)j zncG6lDW|m;vWEEt`)2>V2EhZk*M^5yrC>cy!k^b|P~G~Bo|{i7kBthWpTi*G?@!L% zYN?ZKyJp**+-T*7jL~Lc;v3qa%csh($LpKIAZC(h6Fz$W^!Hk9`>)Y^Q9ZO1BQiUD z?$&V&2gN#!Qic)~rCX$=c}XdS&g1DRe>)Mt%mPi3_l44#ReSdp56M1~R|t=W<;5GL zG@em=305{k1a>u2$jwX%pR75kkD-uPSv;d5I*{LNsP}H z_6aaN7J;96ipj;}a}Lsl69NUn;ilLB4Kg`-{}-pVPO<=YlHzpqJ^TfX& zs#}&Ca6#ej*_3=|Y>twBrob2>)LX@~CU0GCybv-%oLc@b0F{&sqCrrKcOo~xw7Fau zi67U~-)pKJeyu2)KUf&9d-9lbe6SU1-^ z%7gjfIt)oFEm*FVfka?tDFu=cR@MjCR<`ZUEh$RO1>Pv+c6t(Crndz`cn9b&vf#xE zMr_s@(GC2ON(D0~E@QolF&m>%nIn1dAfZGql)gl1EooRqwToY81KzUAO`%@a_9wY2lKKI{+tNKSEzo@Zj7+b zML7K|Tf2krQBls1l!=*-d%No&Ujh2#Xxs3%_Bed+U}vWnTG2+E$gS>Cxj<%a(TzLK)8 zsE&Sx@@O7$vIqA^d4~^A4%7WIU%xG%-zjaJD?s4qf9>BEuw^us1HV&q$yu8+oOMjnhtop!#_5-Dyl|BNNF(zW51i(XlXZJrSK<%;a!Jx?(@3!8d zjhWs__7s3I=LjDB2hPOKGijSXN0bw1D|T9m-l%D1w2 zzxVz}N@EbHEcZNkXq*SDr7_u0CQg1MxvbG$ zL@dw`OH;U8jo=$jG{~~yT!Nc`@qAnM6M!Se2h>oDOj=eiIo@xKs6YJhTSYSfr1`t} z4&3O17yV}OU~^^>_OFPH=LepLZ!a!;-dk_Ir!zny{r4^BpRw9HvvfAnL@pJNw;2)c zSUR2?@uyP@cfWk)dQF!AFmonb1jI~d6E6s90jx3j5m=8zVDRjzr=>8DH{bbC=e3dk zJ*nJ6{)#EO008_@JO@@W0fWob4f3Qm-YfcZO4t3?~Yl`f_Af_q~n`W{#=g73|2N$Ie@eW{M55vgH%Om%dF`7eY3|D`j zK8dWZTzkt>hf&32tI2Q=CGN4t2ft zHAnOD`7j2H|C0K@_q@aFeRmuwS!qDi_d5cD4m~@LU5{k$#xX)Mbp$>Yt&V;4e2ANd z4yzBT6G230=P?M2XE|z2O_p|a<-FctY7yTl818a~>R}t`10hn6Vry&7w$mP&94e*J zpZ)1;I660}j4m8~<+-(G59_5N^5fA?3O(1FEJmo*G#e zlFy+Je%#j_H>8k34~TOWLNA7q1;ValQffH9cPy^)>Fn|}x}K$V;pALDiTPCbn-w4y zL99R8Uk%klHj+si%~ZdO3~MVghxwxKRT?eC6nA82fm{orNp-1K;GNp5k{kH%5$WQe5oLsfk^_u`Ov;~I z^?53ui?I01d%mzB;82RN=+mA(+Zf*Iw>%-UZFzn75A2|pH~Yv+&qetuzEt{XYPpw$ za`L58Lmq;PM>}#l=l*;VH5H`?Zxo7&<)vz@0-g1enaJsl;=+S#Snq=$e}HQxgsNs4k*@^=elKp_r&-ZklBR-E?Fli zi`j{}kTXVrhmhN#Z#dx_eBa5nHOs5L(=e>xOfl#Ex z6#xk##ED9&*w^nr$(!{s%|+@rh!haOJS%NIDu3I?004PlGT=Nd9=z9yyub)he%^>O zhsgjYM0Av|d%kidDj5)s9ctnmU=4>)<~>)%fYeF=_P{te1ZW4NgxK@TeSp>=^FvJs z_*xo)m&40;UEAE8uOPQ@f1U@hS46;IAUxuhl&|m49{kxN^h-wICsMtXYq)&$Q{a6P zQ3eLWWvuY;IkyT1k?s)H0=Q=C*Tps2zcUKd?JfVC?n|w*f$s5fMM?nkvGVJpw4%rq zv(4w+PbS(71Bwg?c#Pp*DeL$*W$pY_*62=oN3NGE&XLc|zE{@on+5pn6fp5>S*Jgh zweU~N^Nz~8`Zxcb|9AT2)2}KKr^>J?OxPk}Y)x*-KuIV)AQ{ll`X`bLpsxINO0)^g zJd)AQs&-7Z|Ea8?Nagk_aM<(^W6pdtMtM?_}0z$bg$LD|F(LkF`Vx0?de>x zDm8O60GB=SodVvjU%#Oh6k+J>Ph#vZ*JB{bIy;sgWp*un`PE;O(a}l`{wUcgC8+d^ zD1xN}FW&!~#Z%6hkOlj%RkSfK9r~9DE;ow-_qebH4l5z&{|D^O3#=DIm|pTdxv?Kbj0UH#i3uPWH<= zuYIn_1CRba=NQ;kB;TOw2P`tyRFL_F;bq&M12VXm@o)_x@iLw~vNDZP(vg465x^e9 zjJP3+SDrmoTs;ul7TIIuf*0WV^XK|J3=vkgc2oO0&%nq4DW#rMK(d{EW++Fd@CI<+ z!A}6VMt?Ak*L0#=AOB2-o2V*b6^Jalk26~738kM(`>1*UTe^om^I9>=;h15rg19lB zPhSE5T#vqj&S4Rg6DRLo$tcCy3%`=_2ie!mxrHze(57)qUtHOe_4o68KHzb-^={e69$u#Y!LLgj6p$ zXDP9Rm(;;qHWko3@X8CP9_oKmLvoP~;g<50uPl`S1;|2RZLQOPyl3&MM+5|4C=bf3 z_cef`4X|H1W3)>A`7;+@o{-OB_@>>36Y$Y*lw|VbpL>}n+9YxVFF)W~91KgV(15{2 z?EhYp41zt3Kq%F;DS|>O0_>V1YWeqvRxZ%T_G;+^p0;4}7ZBRoss6H=6f- z=a^up*_|3>d`4tKGIn}i&7<;v#GPEh!|KKeK2wRgWFYg%-+#cbfoR((B6oh%H=hW> z=knsi5*Xjf_IVr=ksdJkHjkflHCSH%(zYotv*(5dS1@?yELCf!z5{%fir{CTN)ToP z{t>!=I>ec70D5lLRDI`f)XD&apZfhnYApY!{&UnXi86erzIzYeUQbjQ{o;p+M40;D zHYSZg5F^Eje;(Hz`9^-^eLM1bI7+SkFi3d9N;>xMr`_kz{p5==ilYIqWHB&4D{JX5 z3%Gw;j8MDf^9$wYqs{H|`*GSU@8f0ihKI85GR*(0^iA0}UMWE2Z~nV~SMNpu+=)u4 z?JD<}wfWAwA82nSx(I_9pnzO@z&`KQg96Cp<)^BKzvp<_C7N*Mqr*1vM9F?OkX%G^D|)AY)#uN&cB8G{iO5`KSItowlL z6S!hT?zg9Xf_dis+f;RxE9t{qpUPlp%4|(@~lF`Gw z(BW;zpvU=!p&`yI3>*;S-MxE9-fckNp&KPQ!)QO!#m+lOXAdNI9Y7o;vB4w2*x7Rc zop}8j*Kfc6OEK~XsqL;uIV6$2Z3tWk;jE=7Jdi<{c>;tRNv#=uBA^k?~fK=ML^V1IkhP5MGH3RgCm)raB*5_JB_q<$UcEVgM$Cs5_0NCe# z-VfG(=t8U;3>nD4I3FHpozk~Rt+@W`n=+!vfK?1pN9Enx@u*2;%Xx=)9>WA_0SBHU z$9hBW0Q|G&AA9P`>9G+XiwmeM3Dz(2plzPd*VNN3(N)fhYr!H5XkVnpoa0xwzw6TU4hfWh% z!+&-B!|=jqsiMiX2>C!^Ddw==S8-WtSj>9A!wnRiQa_E++0i@D z3n$fOQHR{HUTP~Q7JO}cSOfeokQ=0c)h*ZlMbUU;V~E}@#-Z9OC8uh2| z$mnm~q8Wkem-Q~!-`1C0N-r$7?vvb)yN$84{G|$nN0Zb4KFN7J&he6MyNEx=L!CN< zK%3uKe4UMLZDFKT9r4~&>Nd&L$s$HkfZedMX+g5_-m)2PZJ#NJ&Ex1~{l0G5?Kk=U zcM;P;5>=EK4tk3&NcA%St>xIQbU<%$~ayeTMXsta8XMaw6(zFa6?Ccf;@wmf#i;J6mbgo&!}(u&`5gYe5DEsW8hzPxh%RWqg>9|`mu`~RlaqC8uxKOo-hq7JZ9sUMhul2)Gv5oiCrSa*UgX5n z=CK3vzKB!Qt;A#>0P=*^M+(7~KQ}`Br=2RW^4~Z8+59Z_ ze+Tv9We31N8_ci}O#TMs!*F4n>0=C)gL&gcLYT4}3!M4C4D`f*r?lYUMusqoa8I!Q zt6D$@;1^+N@UtS0jE&TekYQ+*3-~>%Fs$$bV*n`Y^xN`&{E{>$_DSZx?6)Tvwu)iz zTKRcf6*Q*l9~WcUKPUq}DC_1A|E>Rf`sx?|rT{<0aTvY$&OZU}Vpt%5p9q%Q1?X8h zJ7eC5w2Uw9yJdZ13lz|I2r)4NvM-P?Z-j$J=0`sEffI!CE^j%T zJ8kiP@(#2A(>J{KQC9Z5Z@)I+NT{@zii1bQQmZEV9YDr=%D#$sn>_#nhLIgUsbSa% zT;cVl-#k-$bm>3bDU?La&PSl!a!Ln2eXLoQAu;F5uFiP^;07p@L#>Jj>0?3 zePZfk<+*sT$;&o^JS$m;OjK>ajpu-A4|$9(1Y!kP?As z9Z!A>dEcnfYs?YAm2!3_&-m^OL0y$#%2@|# zAtCOE#nZ0p0uSy~f?JF$p*MW+;iu9Ilx9%okZ^tadg>&(j2Xf|t1R%1mq&c8stgg* z&N_JS!%s8^7)*i}06?sCgqcMI{_M-YF2g_TO(FI0H|*><;a*Nx3$T7wZ2e}`fdj+N z1|!|WC_yC&`i2|{pU0SPIrmnvf^vL&W&I%Q2>s_=j5WME!By&6d(XMSz)(ONrcVvrq1tP*k(zSIEmsFAh)4XHz;shwoaJ}`_X z)&X{2lajDw&)rZ#Vv}R{%vx2=aaw1I81*~n7iOi&JR+$3`5F2j>oUqCXl?x>1>o=Z ze~rbG!$#5sRXgNY_ei;!0@6q*snfN(zWGNn+$Q>7INi8Y>Lg$7Y1NQbZ>s44X?;@q zw(Ei!MooKoMecf<#&(}Ji79vWK40?X{h!pFi`?|X9Lp!(hS8v^oZ zqZt&E>O^q1$F1ZEm=rJg$N@JFG*Vomp-@Liw2NHu>h1=j$4*D15h{@XdbFpV&+~9yzEcB?!74qxrXX< z$cR-JBB;Ug{s^^`r+R8d#9;Q_FNzf-M@iV(-jmW5h8oM2Z30IbsqFvdAO~D>$f}LO zmJNyr28-YZFmKEg_xxC?D~tq9#!1lxC?g?9Jl80MlROoeM2<3YLUsF1y zi$zfb{NptV5-U9YC~X)G0LOTN2+>4=u=0Z2Qc!tTko7SSDBe6v9&hA1vwTLB69XwL z!D5P5G$3|31gY=tqilQu%z*9?U;4R$r+}))}|DKz7loyX%nPk>e&0+0#@o3O>20`CAo;ykiZ74a4DcFTO_be3%6P;hCw@qg zS}@1}5TXA$1)#U^d)9Uk3?}a<&x{cJsrPTg#}eCA@C@{A0=j42-$dq@TD1a;M6hy* zP6S<39X4|V<+ZF}YS9Zz?_fk9M2W)S1n^YeT9qMrpJ%CU-Z39rIdWv}??FTe^Z#)H z>CekN|1aBH>1kO*JZFnCl4YNG4rng>CuER&#ryxfJo~>dgTwp({uh5O{ipxgf1b#_ z$Kwb{I(YWX^P2Cbd-o0OKXyRHe#JXM>;ms9>)yi4&0EO38JZq@B$Y|n3&?wiM}wZj z;!h|zAQu3IAxsSJ?&W9S+|;$^=>}jL@!RzTo4OGUZtMO#BD%XHZ~wF6X;pakmgVXj zA*e<6u+L#Ig&+@4Hoy$v4@d*d(*}`eFrLdRn45u{aPSWbxP-YKk2wFHI@n$=j5Lqv zBjwrqeSlT+>^YYONFP0vv4A~_z15-wjBS5Q#=R@)g90q^FykrbYytqX_n-4ze~ZXk zD53~&_}K!mk&WucKQ@9k_AdFO{E(SYXcXdj0w+YfC%q$Lx_9IWf#&-1z3V zztVh!A?1v?8YTkxc$Gi@*&pZ}r;H*1k`Q;57KG@~Wri`x*!6E~3`$AxC^vKrGdQf2 zBaf^}A5q!X`P{i&l9$+B6~O(eq7on~RD?r$=2@+~Q`M$NepD5p63tIPe5>@i;G5t} zM84(w`^C_JjDAve1}iCO1hKX+GY4kYntr38IatF6p7Yk}>`sjkeG782raeT+Jq*E@xSPM}O&sDWyDPsY$dgG0^r9=gx9?B`; ze(q%fl_sEy0Gk5dDA)TQIy-S51mqhf4+Zjb(zVD3Wgd`jBxP0tP~`;=4Fnk(K53<$^|QV>{&^y|ir zxAgM!nRqtJ8u-g!{&N}6UMu4%Ds;L^KPa8%sP%H``(EiY#C${qOvqWrkj6Vt>WznT zsw`jkJ_9Nv6#vwB6g<`8$xbPG>A#j95M=`K&_@nGe?J)-NL8dkcN&GEVbiyyW8V{3 zzKI^I&8*)U(#kjdVBz2J>tDz${1$lvk%_6m7Y=uzPtzju)I^hqVmgyE9npZ z#{VN-e&sa*NR_PfQfP$3KUB%E0|7kCo8NJAIIDt^{ZKL^>#Xc=JMw5_tOIlcD0$~u z%MkfJbB`sUguH2ajp0_=vb=I}1$b@qg{MLRSZ;c*{pA8G$;qalcb#+%mV51uiE1O) zDj_W3k^KO0$oq}gwl^9)c5u!3X61e0oi@@~%Ynvtf#J@))4Kv=-1jiNx};UPSDw8k zYPPfA6TZ(GNAA8EPaa5yd=4muEy8#}PGlIRf9C||?`S+K^>%0^dRL357;m|qd8evO zK$tsngq5Fp;tVvR+FgBTFJWI{4lslRRK?HmzzoR-uvcDbXCWEC_x{J)M*+VAr)7M| z%hJ*oX7n!S#Rs=O(|iJ=t>oPkPZ^nLT`Fa%j2%7)MRwe~W6^fJOJzPb{p?|WKmPP9 z?V;@bjQQEKN7bM_czP4^XKj7=?XOEaceS4ec`oM`_kZ;9&va(UK=A0E#vl7VZ$D)e zzx>6&p)>1lkpuG387u=sdEd+UiP-z#*5|c)gQDTG<;xH5V1VZa1IeA+-|Ji>Y@f9| z@yJB<38Lg!p8#y0vFqgzqe{cuWloSS>*=9NP-ah&F%F~m$~xt~M@mn)C%pxINcz*_ zRJ(G0Z7s&U{(lGlbMrt~P>}2MoLnpSj@9(9Qva_YvbvKSnrfd50yvh0aN< zKQ)-~vY&g~zzE-@c((Na7cq;C$y2y(T+!c(ZM(LjuMM8GXT*mAY(@*FVZYHt#<)jV ztrJKd{hrq6YZw_t#tSs{?f1WTV@so%?0TD0FGb@3$Y~rl-&=sXkp&*_rXY6$8$V|Z z>W4Z3IofE9n5jwjCA|%y)wsva&f;m6?_-do0Hzr202J&A3+>uw)`qxqt z!eO8L#nu3G6@*`#qOTLDTkc-k?q%#&&e$jt7R4d2?_jxWCGH!#ePa|2k@I+ot}}wx zar#Es_bErRP@91Ig4r*P3Dv@%1`qMiMrM)52BW^u9!@Yc+4;m)0YmmQnQqpNJeI`P zRspVu`<*qm%|S1!^+O;m!hGi-CY((KJ!vq2v|)LU+ba8aMv9a2pjC>=h{y*y$l zpNs+WBCP$9j@not{6i{;Tem!!} z#9R>s77B<5e(Bz0h_+A$F>XvJR>H~v@PUQsom%7p*CBgj1m2L0BQ+o*2FP_(RKcl% zZIxFnhrAg52MZlWiK8493WkyEt!kwO#z^D_@@H&BX56^(rWARrD{Xm?D06ts7Anzq zRFs*y3_yenhlhvHFqT-v!lBk9#u5}J<_M(~IS`ScGIk3KUuLafC{pExJgG*gR|_bi zKf%qQyj+N{%00{>ig^Hft-DN!>2>itSZcvi9zW(`+si&NpY$zKSm+-H5ai8Tjp!M2 zWp5WihpcZFDO`e08{_tO3D3TT`5Q2wd7rj& zW9V0DK))9G?B!+{iE@5Jiqe8jzA3fv{zxJIp^N{{Uu1KCh5#T;}^%MWO%m^7}t4_y1Jx-F@}V^nUSDi%?WGz@O^< z8x#R^;w4%)ja`4o*x^MEsPJ|Ry{0>_zwwqlpYObPOGZ)lSN2rGz5yx%Gv(a`=vgV( zQvrU~@s6T%77By6y!KNWTVaeRk6D#d%J{=TfhXCB>z)dr;O(ZLD%%Ozb0Rg~Z9JuK z-h5Bv0lc%%@$a!mLUG1$Z+PM3?82x?$UF@E^d0aFAjGqL>c&p?l>nLSbBsgb^TkN1 zQhj%RsN`$?N?0Jx-M-S={R!JvOeQ4ljL73*gcB#nSsQUjD*E~CP# zj(?)cH5RTPEdPYe({@BF#2LAR)h?57wA9v86A-ou{HzGu;KI|8;uUl28M z&kZ4b$?&0lf2GI`uEVHJH3Y5!{3BnUrIbC&*%m|S7hnF7I2%+A;L$y+T|b+-5!J@> z@h4yE4f*j0BNvP)5sb)^NxAOR&wj3X`2PFfTDpWdBdnhM%1hOK`uQ(p$h8uNPc(M6_0~o4Ur}P731%_Sm4!C~w z-UlD+Y%$)9htfxQuQ|t@2?(N}>>SD?6qiT(zoVy$!Hi=|_@P53 z9lrQis{dCk<``t?!BwhH$`N_rwH)6A_&WY-5Fj@jL`4`4SBwNV)F-(Z);>>tul@Fj zxJctjgArElNguk08UpDf%X1hNQbUbv0vfmJz%n%`)8-{+s!n&4S7#uIairQOYd5MB z_>=Abv`?x9LBTLi$L_&mnD5`7-J$;)d3O>3(oRYBEtJG@YHFy)%)?H#NL zFBZYjg|LslLjLV9<1(44S!n7X$MOp)1!V7pG@jJdz1|^-Ad7g}jxlhos)8la-{b1MDGCy|J-Y26Bf#N<4{&4!Ti+Os*UteWdffcI9qr2 zn)GvH2p)zSL7)Ri1K39S z$6Jp9fd~Y^4`BV=d1L=av5LCZdVXmGP|tdzL?w%Z`A>rxdQch}8%8MF5m!Lzhf)>F zcn7Zl!8;$Nw^&oEj8Nv9vG6|dj}3!&;c77;Tq@)Lvw!jr(qkeL%2VIW26h2{c+U?3 zYQ?a3epspwJkmHR?cV_SlcqvgtVaPviM~l}UD@C9c*IA8bg#U1lBHnlNBJ(rc_qqLcfNG8;ieJ0`TkOxRDVgH={#*r|I2^Z`*$V z_N0x0-ywqa$_aA+)`aMaVzhws1CMo4%Kuqer@t<1c*cHI*1&Z>!~0I86d}nE zZl|L%_g|NP-<4YwSoH}GZb_`FlGzp#JEE1R;D#0@eU0`!Q2Ar~1V zD}N`99_IZ3L4c{J$}v7sp0t7Onf8sjSKU~;(U5%!V=Ewuv>e_6QV`^I7a)2d&#kyx zW-}|xxnu9vs;u!1_bQX_L`ga-6%g(8?QegjKJZU$=7+|oVSECs8f^gBBLDl7&%To3 zl(Yam?3N2|5fSV|8(z_%a+j2TU@X8ZA4U$b`+Mob&Do~%+ap>?={;p1|Lluj==UFs zypWrS-khh#>dbEm=qhCV#!Z1k_P{&tEoZ-CpEYC0L^9-FnB{MM^tpgPAe?!KeJm>3 zaK^K5e_FOAHv*A&$8!X2xZ+79UvnTsR5-OIB78Uue3LA>K{=ZbU4_mD3NKf0<{D`BUehU|li>Qdckp zSvkNhjcvor>0uzy=Vh(X##~%4=c)sRXRzMIH?eO~6WR4Xyzitiu%3;K@}Zt_=FGVY z@Zlxck)CtkjTk2~hHQxUCyNV$`&sAvP6C-7FpI>uyi*t)0RNWqHds_o`5XN?dlrrY zi{iuh5Rq^)=sS>F4XgC~!usRqJ}n@Ak87^JaNsqO z+wo@qu-?%;vOqMfX5h+=4b^Uv^XS@o<(hmQG4_@28h&icqMqKh`ob8urgbdWgw!5B z?qTYBI(lATC35g*<~*8@kWp@9NTZK@;3oN;DrD->TLV530niiv)m53=N-{|gLnp}i z%vR;ltVh8HpI!vyZOnNDgKez7cDmbNw1p2r;FR_r9~>4BZ%W zHwo>7$)qiSRKdD>+fRNT%dbvcUDz+=hqQ6#Iwrd@k6*ylrfzh5|Mbzctr;|ieq61^ z@lV6}X)oz#3MoZfU2l9rPDVqlm)NGzOd@@|er2s6Xq_GU;nWwX@(v94WrG!QTnht& zp2NIB+p8SYDZx228_Hfh&1?gg*j~1-!H62wKkjhAi7U_*dpd zRVdwT%+ik+$k^k}6CtucFNwz?NH}NpY6xIkU3m_8fES^A3Hs8^0;iw__<8D7#c2;29`0DhCFcQ+YO^5e15`YorvAlHgvZ zx$@ywRe9uZc#rYKqf99@u#8U?6Ff2kB>`gsULq0oxB?OU3$%^m0NI@KM9UZzi6L*h zg%n0PJZfin#M~PR2s`2Qo5c`veQ9#t!brAUI_S0yBLH-iwp;pMRD4ik{^u(~ z@FTB1InZ8oyIB|mFo~!gKPv&nc+$he4NzvpQ7SK@1cubY-dCxS{RLT`WRz?-4zfQ! zTl(|G@Glt{kzZ_(^v8?^epqLV9GDN5W2Gi%&=#o{d=Jt_g5qJWEh`{>di$B!3?%e(<{rKaz+UsFYXJ24n-S+%!45U+!fWYHV2|Pd-;r>tD zgA5Q=Za5@=czLt!SpdQ}RmSd`o^w~=_Mz^x5_B8#7DKkixG=Qv4EFZhltnBadJGHV z4j|=Xcdse}$Q6DAj0eB|)i2ZIC*}oaPZKHQ%E}8ej`L-|{-k)$>9+v3Jj#!Zz_#>3 z2A0`Oh8@~uPy6PZUuz!Nzj+Uhqi3PTPxQlk4#vO#?zaMv7;BLAFc{Gw>x&2x&L(8_ z$e)QEBlW||GUECFO3_c27WTyVe=9$`^b&ZV|Ll(x83l&}h6F`nkc;o{7waz48sM9F z?jif(wPviy>SmGe6E|4N^PgK41Nuyl{vr}B*e{Qbw(*k%UwHzYUS2N7jHdt6K5a?O=1*X3kj zy)p)()p#eAjwsOmS~;Vynjy@21;PkUS|p_pd6ziLI1@$xtJdZOp;ofo8PMkkx&JssbT)+0Z;X+5y-2Ut&Xk(rj59-$;Rb2 zv~r8yuaS(J(V*{4PGc}$?~A^y&qk{FWi2w?mre!m(RCKaYCV;_^T;=uR>_5dBbH-3 zwY#2oUu$6B`;lIJp8q_m0+4!P*t0tb77Nn^S4SBx-!ux9$aQkY?WQ)cHq17t(NMaH zX~XN_b~g!KC`er{@!U3$l-8pZ>~8@m&496%u+w_e@fqaSmzTa)QBYyjqv3^DCib48 z%H$J+^qjt@UXL@-cD<^5S*mqx7&^)LyFc{T z`^z%8;B)_i=kY+>P*QtMd~1k3P#zjm0dieSS>TvM`BWwMT3~K${QUvYf30u+Dpw@0 z#;khpqht;smd_wqGEi%3w}CDz-v^K-2bA1+g>dv!}j5LpS@q}^M zt9({6Z#Rm$Y(MZj6eXNoUDcUinn&B_=d?y@;6cH=&F3Qc@=|;#et4`HgN!moS$wT{ zN#?2!;9i2M)glg=G8a>YA--F@El=dpLvF|r4+vf=JX6OGlHYv$T`9g&Or4m~@%fXw*A+2kA=i0J(p+*6qztgoH+aRwd=3Sn1iQ9#v6eg3-yfX3ff{0kR6QN!NkwC zk?)G~jdHgC>}fSBa6fHJW~+sBHJo;n=x~as4C1KGEjM6I zMermdt89iGj7Cx*9WX7*x}KEx_e|;E`Pex~r6C#yph$=*#tLLs+PPG|pTjeS0SQl- z?uanBR)bzZKQ;2b2=xbyPE*g|AF4;&u%eJChJr)CE=t=i{3z_fu$1;}o^n9&miJ}q z?N0sYFgoygB^k_(=x>@DQC_Ogv-_5|ZK`j8eExpo+t<{82H5j?ln7Kb#n6lzOaF|6 z0!qu{)L8xb8_6oCL%TN0`X&EN*A)5h4|oq)V=yDbgvgY#ww{%> z`Rk(WKR!5M;iofPN9}M7e&w#4#qhORKyV7?|I*k|nfJf(@BIhq)+ax6Ms_2lT&M)y zC6ROD^?dg|82jII0J+loe(>;~R{=0jrGbiv%4v?n1TexEXNL}~i4kQ2*j8Rq0}nlh zFV;B*JUrHS3P2((T-jIUzNTJvF^-l1Q{a)a` zyx-wXjTA9t`c~PC-hKa;)lx5G`0m@^$m0*tvl6Wq6=7vU?JQvL1hC>sJS=^+XodS$4M8ayr#iPu3$W@q_l-cec;!{)CYx~=#{>k0oE3_8Dg8yp+D!*D z%DnELdHSJfGfyjH9dfP!%t@c&eNvjkQv?6ID*0#8j~F4IdUXo`x*1jm=^#p%DRW96 zjimFbbfQT2AaadyaBmm~BGPk67Rg8y1n0EM33}OFs|YZ-K4?Umz30ECjJ_lqAinpJ zp_{XvqLaL?HL;QsI@nH)gW>r37#T-Ud>TRAz)glzXd*ZO!dExs(4X@d$mi4ti4PSL zwiEz+qJ7u6|4Q_KS3=vsU$m+;DBzCcoIBM%J`0ULS0L44ufSnp4#*<~{ROi9+Mh`^ zv@{r!`net(4N`k{{N{q}WZ!O%KB?^-Fh)B;mjhyhtVHfMmN16&j~;gwV?S-Pt|=C*A1tsD@z^_^4Z{Uj?}t1nNTSaOXpC_BPNecn*29P@W52?y_=qxL_T~ROZdz$mw%(syHI^Y-VTlL8m^bTo_puP^~ zrMGsAx%ewc^hTW&$2vVmcyP)d|&Q(C&xT{?1eEBmqFOA$W zMY+78<@YD=aK94U*qZ=iLWYqvbPA;rF(e(BIAKbDiDj=S3B$(5mfH{8%e zxfAn{Y#aN&4#>gSa|wyXs}qNxfyYBXl$Xkn2!EBwR&f}9>$vgw!*+cm){GBg6+G5?$ zMuHcFc5(m2!8c6?Xs(q3Sfj}z4d4!l(04Yf0Q-Klq`hoPq!IhIAg{?`VKk*I-O6`|a+r zmDGAz>c>%TAezxM9O>EHc3|DgaGUS-hE@qSBX3km)%o1 zysEZTzL7y*Ql+p=j5`DH&pHL*vsdtpcM4c%PYPovpb4O^a%bfqd!iAT?g$vpJQaa! zdCve+06;)LWOV#&VfVM4xbUG22fT}TTmfhTTRPCp6V2Et0Z8sU0F5zy`_0z|;C(-3 z9|sg6e;JWy7Vb^o$(QH+!C;}1gI*nheTVa5S6+EDLUQh4WW^JW0A4_$zrpYJkKbEL zf#+y*EdYVpC*G42FN=X&E^xnS!X@;*|Y%G?ow04P;uke|NS_>m!h+i;IQ z=LRW9gu}CUaUFXi9_uK}hkOGRLNth>mgoS=0MZBcv5OZh($4bm=SE2Q=DRY0C^sGt zdWt9uOUL7j=OR-);CRw8CU5Hew34*w0avfRCLS0|9r?-X+n0aH#7fa_l<{7WEHMC6 z_E6_vksHQ&48j)Suw(oJWqk6ud&Gos3>hx~a%2rK-;X^Xe_8Y42SsX-_D7nTA`ILh z1Yw@eIx4P|xdZ$YVe-IB3QDhWWs0@Nc?1X0gK~f5EdBw4G<(0SGfTrqGp3h&-n4^QT>^z>z zbktD0tKM4ydkW-b&Z)}hXBzlGb-;kgq3HMUrE0=?6w@7l3pM1gO#g3)GJKj<$FIU@ zn6D}N^^zWi4I`Kx&mGn$F5B7Jq5cjo1YYo71zv2ZuYNVG6@RT=PYq+$XLvYVf0D*? zhQ5(hpVzK!j`8#ozi8j3UZ$z0f>%RMTp#n0Vs6a1P!~cEjY6d@h@$^>?R&JLUJc_9_2*%%q6~Gia$P(_2tB<3-N(-wn$>3wP_F^` z?ltQcL%#regxUtO*8jfTV3f*ecjVD+T?ohM1!`|Z?u62jF{TzC(wjCy5GJ6`RMAa# zXDG1erqnOows_hW<;}Og=T7cbHr*VxvAg}x7fT(@WD;0D`DW`OPc{u6;WPBcmKbGh zBsH`nMqq{^yC~YTd~Y3Id6Gk&Mjl0xAcy_=v$PKW*Ymw9VCW8n2thX%IrIXIl4j>Mt_Q?|~OLX7_N+NT^n_AjL z$;Al3FGoiQihKafnK%1bK$pHsF`CUBz#djG8+qGD?k!fd03qJcS5z2bbl*Ek*#mOhF;vB(~vj-n$%#bP*_ZJDA(jxWFk(Vi4DFj`!PB0K4 zd&bWf>ytTVZ14jdikR<6%H*_*IH3>7fGP#_aV77{xngX1M|=p(v5Y~klrvu<7oj2-*yF<-~Zqv?fDosY5%r7)J_aYetKE|gf1gF`l3AwS;*r!)&8P#g2j9O z*3I{{7jSj}I0482a6lO6gjm^k_5`4ULq_?{1>|6e`0%681ju=R8KbfE-_bJ;JTIH` z#lDRByj3;$P@oSFJLU8M&;Vohb-*)#I|yzg)qs$9cuBtd?zidQou9;VuYMLeo$>H^ z_BqChfdnv*0YEwVMMi)&K(##Wc)^#8N(zT;-*&J6y?br|!Du|UeENl#Po-^ScB25= zheh5HGg=O71;j8>WQ4(=XLBuWz5VtFGT6$9 zP|gyn+i))1xw|Pt0FfD7!`uew#?W%?B?u{#M>)WU5AJIXagDejl%`@tlH}WyZo-@~ zF4ha4bOCRz??%`s-hh28#iuBXZF4>l!STxV8`1-wcq9XI!|1`7STApvHN`v=f$&TO zhg+7Ss5HHu1g8SgUdSalcCtGr=<_@8ek7R_g%h5CMMswJTOzf6U+;?2EsEUZOGbOn zKaVotGv-BH9mBGw9h!dE*NE}^dk1xDdSHP1{%=ary!5KZ6IG4^fzNf$FS5hCo;YtU z=iC(BN#MZh&rfzs<+Ta`kzHX<)u-;QR&CoA=9(&$=4V<>VRCH5q3=Q~u zKXT89*7X1SQwYQ&on0PjQn5$$apVb40WCZbd2D9rE=m=%wUv=l6Js58Am9cbMgnSo zGl}LfqVz_9dVkl=`m;T^k@j`P?%H)ZMwY4r)TkCG?wzsqWq|9G>3`Gh zU6BCBCob$7CO`C~d;wxd@*dKf32h|%Xuuj9ttx+JZi7s9xemif0LwdJr1dZ~$(4?qbwv4v!J{@^=(44%SE& zfD{NTKR}Y}RZI8GIa&2l&^D%#GGK;)sAo@@B}xbf;Zae#_zcAq!v-O)9Nc_{#Xr2` z3g62{v?i~v%JrSm-;KF>BE^b>UE|S#X*qJTlGjLpJt=bcm50A{Hr|Om;V3L9fby1? zXQF)5HxvjIKE5`b<^T0J-jjW$p?XMgf(ID*v6zGj&1X zz1)k^kMgQ~$YH4A`zTYSe&7{U8ieuyEpmZ&loz{U$`H@PBg4F)D20b2yHV&+1dl__ zuO~Vp$opi#I6bX7whNVfJgQX+kTuFDYYUGjE0ul(8?_WoCze#Ti1Hss28@`Ozd3nIB&&hG$&8-Yz zoG>SyF<|iaF%Z<>{aJYRE7@SOh=d@_TlwjZF(O#?*}ud$kQ&22f44M->}{BrKGGhN z_c{0lratyfzb{JjP5j@?EeBgJq8*pVmUnW)b+o9EuvMSi_+E0dssonlU}M6lAfrwB zw@FAfWjBlAaKEhkUzNH4U!FZnPmA#plhXhJB-K93yY}`+U#15?ev|H(IsVhKcl=pd zE6>hcNT2=j-%7VW`>L#;1A(6(fBfERo(pU~G=}r?J{_tWfaOZFp8>+A8yn6HzagMW zsPw{1f&sAc;PNg3e&DuXz23WfN1@>WSpe6*jAYOKon0DWFMB*7pSCRI`<%{&Gs=Pg zFuhs4pnxNB4HN)NANhG-#>Yb$;dsB|{29DPIqxGtjyCXs^FBiCr;u)dR{^&R?O|4$ z&dB2|T8x(16K~!6Tn1c1(r?Ri{-`qGvnu0^;jDmP17Hup#9)EJ5#Sp&ZdfpZI1WI2BokvEbCOnV@^74;!g?Wk!JIo1X4w03W47{y<-;4*_wG=H;bMGiu|B>3Eem#36&H|n5 zR0}HO{q)nXWZ1!ofL9+dYuIe_~SxOmm&Y-9|gB+qij~zIxoFwZanKOpD zVlCbGuz#WlG_DOxdwaDQ?=coz>cD*&m=uZO&)8OU-n$}(WgRMYpqy7+69E54G4ROH z<-7t>O9dQQeOtBr?QZG++?qO_GbU|?tV8~)O zG;+(OmmzL>|M9OU^b6f?Jq@(K?(F$1Y47Z%6#k^Dj8XxI^vH@;Du@Y;HHe~DD5RnHjW$~{M$+h44 zcVIJnrorxp5X>wSe;aoqcOeVopDN}`4O`@t#**zpc8oa%+DM&2#y`&EKCexb%X*Bl zYtuEYdTfC3=XHjd09rykm4mO%VVi5au$RkpcTXGuT@S4@0oHR}-@3OYaCZW#LdoNS z5B4yJV)enA8qdX#I0KWR^{-m5VjT}vSh@4O=R6W)>iXCQ&-$#O+s9Tr+1*p`Z?9Ax zL;uE(r*v(Bgz&2@hWbKzZO1-8W&EBFsY}`!H`^9lj;U`q4*i6VI$Iy@;t!eS+~%=2 zLgW`I`s$h){oFPZza#qId+D=JZnsW7m#AyKv%%!vf>WadtckStO0`!muCw)86tlds zx$STH5R0@NFx%<)@L8?FI}9xctUnM^o0yT2fbW2DtnlF#4zp~OgG2elGlj9h!Ya=i zC;_;NIBq3{1O*_xz9Lw2k~TZ&FUksTq<~8&f;+Bh1>zkLd;PvXgOuu6p4q7k1oVv@ za=cVa4~bLBIC-1RU}4_sg}@zcl0$yttoAC@-DTE9|ofX_mm!$1_mUf+#SIFz^(_wobpJx`BD?=8`w_t7kbe z;ia;!Fd!g@P{f!+#ve7C`8$Ra)*JU5cSBNC$i+*SwI*2C$MU#tiX`f{zx`EuQk2DW z<^A}eTzi9gCIX=-tOukjP`Zp-+(S~Nz+w6KxG2W&l=e5vyq(U97uWMXBmAB(Jk%<6 zm-6WN1;C#8b*PqrdO$D0oZSA%**C(#Y@~YG!1dIDaD>k*T;HDszz-0;*==S#Bd#ZZ z-rfS^BVa5m|6OS-zHt)G5JZC(O5f52khl>YTI2%1@A$O>wArI2l6_8lo4#>w_`irc z2=@M+Y=)K%Ki{(do>I%-*FNfPZFVw}@uK{WVdT_hyzBA|9)S(sGmLU&J^yI|HvhP+ zxqXaT7(UCj+jv&Xb#J`-W;!f${-*`V{YClg&&%JwW$(CjEq(OmAEn&_XaOGzyDtVV zl>hq$Nb;@&+yVRmP1b_3r5pE0-N#ja^@5C?c&*_H(Dg({l|BKC9xA`wyt(XYhfZ7x zh*5ZbwrUsb<-BWzwDT<952Cbq7tawPQ{DmEQ5it;oXZ|Tzc;+DxmAW)rZ`S{|9B_D z7=eL-v5L3go)NhLBxNl1_Z5xbIBuamRTFRN@o^j7LI! zP(0ko0{#HT1fVBPi9HxYL4@7oJtyxTP>E3iASlBH(Fx_8Id$fJ04?Vb9&P%J!2;t$ zgsUS1_9kRSl)^^^WD`{(u7Lu!*>|bJz}}1@lR1Sj5}pA(@v1~Xq=e*l)l%w+LM!ve z921UDngLO2%qzwq#=xG=d;rEVVBhs>5O}Iv6&wS!jP>33K637dAItOZNxoKkaIWX^ zGuP1P2VO#v)g0-BT!)eQYFT?KUx=})7@LWRLnlxbmeS69#rVRtN`-MkKjuggU!`B1 zBg`vjs2L(2)#@&M@$&~Cex~>2k#jX9AFHL5&9sJacmT|~--whqou_A05emFR7#JRw zXE0w;xdR;s{R!g=>l|arVtyh68afAh!ptj8aL)0J<0EsBL{+2E>7wLc^gWF3FVpj0 z_C++%#j9^5heNJMnmT{#m4aLsPbqwwp@HX?x*QlPJ9n;S9ZibDPK%FaNN1y&ET@LxFE|jdsZGtxw(S685It3%J?C8d9~t z*NI7~z17x%!TvlM(dnz;vV8%u6^-Fa_okyr8e-|6D}&CWox$_9M7Lr z@IMS;RvOE-zTYqTLJ5EmfUjUZ=PSy(q44mJFz(f`^4z{%9fmWJ+9fNSS3IN(&kau= z4vtp<0LACey>`1PT=QBFSNV)Z(HvG0WT6mnzv%Fcn?ypwM z{9j~p*NirUn3dF2HuiXL8| zI2=X(N4c$)5rrWSyzD3uiXtGlZ)M&yg{n(t7BK;NBlj!xT@@gVl)$q@Vx`+$RIj{D6E=M)?dt&2uO{h!VcxWfEmL zS(*_5z(XBx6lCTv)?1WMWLyW3{;g~-RCa1-S4Iiu$;vNI1=yLOt=W#0H^yV>8!L~T zIJI(qWj^SKq9T|J2M1OXa8nAgRhw97+{n<@`%E65z2Zq^4bex!tbh5J|8v?ezmqBa zRvAC0Gl6fSBpldZgbx4(j1&*Z*@|NLIuQ@0zf+V@j~oeL%J-%J92V<|ZzLpRC^aA( zc<)Q{^|_e*oB8$-WPBDfZyW+M2hmXppoYMwtnm40AJ9yc0A6+CL@+d**zQ%+4iqLo zI8%QzV9wadw~q)0+J}&jJ{+g0NYFR{xbDcU(3ITkqYb7$9t;(89|yqyB(*YzL6R8d z4*4T}4Zt7Q1h9{Eg^*7e7W`KAW5T{SSm^F=%bNM8A1}C59RaUvcFJQ{i{DtAAIz=gC|Q#0Z6cR@z_qB;F9+y!qfSRkYb`u0^Gr7 zf27ovBZZbf@S5+0jPE~trbr^d>7dB2Gu1N*Dc#(oiDq6tQ14J_6Cg^;i)x|c$$enu z|00Y!y!{vr0N^q#ZkqR=eUEqQz4wi2UKIuI{A6T>Pb{**-gVNMb}R?IfGoyfq=L5Z z5iP*}gr^@s9QEg|?An$9YD5cQwKtYmBRe z+GC`|+Ygu*q2iOrnsE%VtCfXvPd&m7&I3Hcg!_~FK-=sMq)}}<@Sl5X79|M*fNGmi zc+dadko9eoKl$|M`u)4_ep5>xvj1?c5zJaSNPsW<)o*_NOY`XCd4?3QfPS4v zGUzM?qB+y>vNJB^ckBjLo{bC{7f}S9N%sKuk1X9m=QwBSo<$t67r*w}Tbe_8{>89w z(H+4Hgn{7Q;tjuZ=LfY#JG>(n@ijFfz2f;NiUc`4@?3fhAMEMMi!XD65fy_q=^YpV z*r%B{&YdtGW2|8wRp!v6Kv+Zjn&(ZO8$>EF{|_HNkg>w#Gp&S;R7>Dz|6ga>I|!0h}7tN?p^r$}i40T+&uXLAF8e2?kwtZGS!XyWJ>>Fwerf;G3A&&%E z1>3zaDZK=Yw*@s`6y4wYY5?_h9U=9$-g;X>{U!H*ju*jze|veWOqhNbxum5MHW~Nd?=PE-9ab_p%)>M z7<7#N{3SEGC;oJrJYb3Sx3QBfMSnwOU^!q zD`dvlxK=rztUcJy6-8j_2jq4#pNxZ$Y1+mRaO9+8$bfaT>EX#dZ(n(&RtoU&`E#u; zys(spL=oOUI7ol_FaE56%~e8$zE&PgZuk}esjP#;!N^7lV?Rq21Z$&!k#hh%SmOr+ zdl@5xpDUJEpQ-|vQULe1MHAM);lBk~4kH1)0b;rzjO5S2dE10%lQ0mp-Eh;7nq!Y7 z&~^SSi~yLwj19kSWD%<0pMYV=h%bKcf%nw6LyQ5zIY5lhJXO6%1_vpvzKJe)5}~Qy2xxyT`bZacV}9WqrP-0nGimba>&4sFgW1dFuM|^Cp#1yk^#evfunr*71V^ zvZuWB%ufO5Pn1TrN^f0#HO=n*n9fj&tUTv`F7Lv3<@;M7f0_RM|IdG{y+%3J)G>$u zVQB=Fc(9*)N)SI^EI=Hf4fhgaP8rMbW{BzTW&6@W+bbn{+Vm5{mCEbEx{YtNfi zqaJ$>QCd{kP-(aVXaLU`4R)%peD?c@K){o& zvV>k|p4*ILX0sY=BOQ(Mf$R@l6aZeNc1{XtM26?|KJz@4cP&O%qN?65&t;FLf`FAQ zyeCgJU%~DlMr+d3h~j|+m3^LEcFW(tr+tk61P{uw?@s_~Jpag9Ir8@mq(_vLRY$N$ z1nz}EkUrlB$U4Z3J#^(nh~YI?<$yA0KYaI%$-Io$*ykrQibn+jUp4|NYw}Wzu{aA)u{8%Eh~5U_`c4inU~m$)I#)#>t+6H#7bE= zky2uYt7&?-fPcn}QG@kPUjgHI*O`9|L+=)F|G>*vK6`3WD&iJ8InsLtKf$z|aTre? zDgETctMm-&heZ#(ruBo2At;Wy6UlLrH-;vgtA`5v-`d)hVSRJ9EyEH#2|=oPBAy~E zRj9}{p2yl|zLAs3y{q1{T)v>c**5iJM5p%EHRAx^&tAADLynw89pBSjsY55V zN|^Amy9iUoAFlIuTDt#`LxFWT6$s8juW$3YA&Ks@q0`N)HlD+Q){paFhyFhT>GrLm z(;>iZ?TEKCI9PD7lWPYWmdia3^q%AfxN_}Q?r~lJ<(R4y&Cm~YWMF7x>TMds@Pa(6 zIEFR3kDomPJni{yLB$&I^_1JbjVfUJ|}B^3}a9+0G#{# zCWsdWn0jDb$J`dl_`=bPH*^=LNmykX$g9GibT2|oWOT&dhPJ3$KignhZOz^?kw=!0 zy7IaX{;o+2%Tq7EwMD-!pP`$S{6dqde@$aS<-UQgYxRAKwb4GQV(AxO8+%?~)H&Cm zb9-;RJ2qb-r;$`rjM;A-`u;`v!MGyzn`*pj-Cst`sCAo*4XK%Ws-W6iRqq?xu3+~E z*l%Vq-{Yzkc)1wLft8~|fgT9RDu)$efAf>0y78}ng_U?cO(3pacupEdfB>0%sqXv9 zkwQ25Y)?$j4|G6A9bkpXd+9xSIy|%;MbSvv21NpJkO7c5__LSyu~7c!7KUFy+NDd^ z9NZsi%orO62i*%1(^)!!)tKLus8K%UA@EXu7L9NyV7B1^G3sb9MclS4r5Z(4vdESu zaO|b!ID}F3$PdTUt9;w(rWl7+8j(XZB>_d+jl6v1LE3>r$=!p0y#QDGk5?k{{CEL4 zc*7HcazmIPiUhgyL^A|){?e-)ASY4_YAHVv9PMb{=3c`-$N(Y8Lg|T6-l)QW2QYH; z88==&yj)VMXGVC0a;?yM_fF6zb1EGOF8K)v= z^JY=#zb`=Kr@KE|E^5m1mQTtYoShW|1MQaeHu2DS!p#rn1wdT5kS<_&V;z+B!kaa5 z?f{rKi~}I2?TLl$&pgjR$^rsrool`3&l{m$u3q)I@9xL`JyHh@P`8s%q4haD{`QQi zZ`oVkUTkhe68@RV?rn%i8ZJadKPbm}h7ofS^-V;(uXwEh>7App2SE^6=Kmj*dH#pxvz;rirN8_4{!`_eAA72lr9&AAX6;wSY z7GMH1{(Z5iKUBUn&%&EK_0L6kh(Te)yvz%KmoYrPS&W4c;UP5oMjH=2bQ|K#C{b!> zheZkSJMS^Z-`8%unIKx@{Q*F7j_enMGmPl$@4Wx?0niD!ryqOe{f3+6W&xG#HGtiF zcPz)B_Z^Qcp5(}LXR&h+QAVvJt%DMSr`@1$_|F-lO~K zA0XFK+ziCBchE z{p^cB&@Wa6+o>}{32`45Biav zjE*vTta%Ixlscr(%p*F))T1_#Kj$0s$r=N^TZu@EK#0cyu@MWF^%I7c>4vp~ zL1UwwHY~)NM(>!@#ZZvc6h1-?li^9&m*t_8+gUuB)`O%c?jE1P9@evBCI4_=D z%PppMSs&(K$;hKitxr`vxwHP5?3?i+NDFhfXu4RBK*s&`UBr9t`kvd5Luzx}b)~<` z){+bWdE6;=bf`oPFF{G&0EZFHdx)(uD5f+vDFuGd-7L2@)4Bk2Z%F8IWF5??ox1kw zTha+7MW{wU&fJz_75ZefGt@R-2ITw2UYlr93HqC!H>ry;ugzHZt>I+ElLosWH`an4?6T zdSpUG0+8>x?>TrYSl1QOYap8Y0OjHB4tXP~!(*%a&S6b{Apns>9;u>S;n|o?1nKVQ|u{EdgTW zt2xd_jK{R@E4*?l1qa0wuQz=kmR89DaDkGhymlw+nu$c~nZ`j*e$;ZltOyPB)=Rm= z3-1aH09u?6Uf-JMM@dCwfM;*;v=DxmvgSGWkU#=(m}kOicU}38a*!zhGAIBz%3Kh= zfQL|A2vo30$ra6o*1~hW2P@+&DEFS8oN6vfxllxa*FWbAZU-ZDnrc3XypTblw9ER! zOUXZxmYrB>MvN*;&%b2tvTj*_c<)erx&NTZQ!LeGUY`{}wSsV~$oF~%><$99vm(3u zMZO!pwMAI}vogjrSFfho(Q(?r5C&;u`D}A#ku&T??PP2lOtAk4V?JQsz-Afv zBFpoeBTPQ>-KVao0hUh&D?=EYP?X3+fP|Jt)= z-e!dTt6X6=gG8|I^Bv;70QW)sSK0sl?^|h12pGHirvHv%p;ZBBeOaW~m|OY+hLs>- zq@7h+BU@DDfZw9r`_1Jm>DL7SJjGx{Tf7@(9X>DXgZ=nMd1p2$F;_nOWqI&_Rpj`z zfR4ZMxBsK`i$D4sYMAi(>(^gXS`qK|D=PhWF`X%f1!VT9fD4HG?#tW!lM~WCv2-fu z*%07J#+9Nx@;vbn=PemAnMdW~mv;m2HJ(q5iM$J}=OEJq_zRecmHw&L5Qba!0{~!v zM*}z$tE*lNa<_`78DK;u7Qh{0+X6L&`D>pa)Gs1v3{Wq%7ttp;4&HtLmXQvYvHVy7 zFvbmlo5px-WNN&Ftb5K9I>z4m{s$iml+&ij2)&X42If;4ZmpDPR1d*$%J^^H`b>bG zGwQJu@j+sUaR=iiU$*C4lmbx~oF|0u6Oka`y>HP^7{vkH>;>ANi$Mjx0}R4=)E_^x z9C!96D&+*@epILV=#wwC57Pe!ko_s-zZhG}zQWoF(lI6j5By$pomuQON+j9ENz;#zfoS7TRxt2a* zyumn51On9}Hr(K)h`g;?de6Zr*GYVJx?*1W$}}C!2nB?A0+i^XVUL z9D5EvMgWZT=42S^ITd=_%OW{nmf9KD@a|l?dc(s0mGW10`O=SlzqCdQRARBZZTBcL zKx%_JLl5Iqs&FuO;k^O%L*ox!*MMJa&<#cZUqt`U^&um;8h@%n?wsvS)lW@luOaSj z(ozSiT(WDQw{f(uicLF+9MiH+0H57}6W#6F8Y=F}lZMN3H5hn1^&y=BB#-b~uHEYT zH!K~5evxWl!T^v)Dv{c-6=-Zm=VkSbI); ziQ9eMWVP2b)z-EJX)3PZR&UI69=$vv ze6u7**^x9_EThny!H3hj6C-N|N&)&jdToNPFaDUia)MO^wL5hK>xA6(s}45%hBB4q zy_MhM;MwW;0KE13J^uZ^Q~=sTkx*!+Na;*1UU_Lg?&ULuIx77A*h@-Tn7#7nC=Iw{ zb(;^|<7#33%W5Pbv>Z^*p%-Q2%oA%70p`iK2m=fQ@`_CiOj~&UB3(FtNo|TvR|@V_ zZ2(wp=9835<+ZDXpaJUVUWyQ9LY|wVOmIjifw^t9ZKb4UY8P*l<;5S?vO>x?FW1l> z9)G~4O32Bof5uq3-5W=n&kI1ngT(U)6^|4Plq~=`&&CtQOmH2F53-0*Jw8WPCH&VBN3A6syAR6>}~jn$bsq@12o0*ID?q=<+B$?L#~ z7s=Hv{RMoZWIgrV&bhqd7xZ0u`)=Uica&|60n8ylen&OAQ9@So&P91gJm);`T2ZnY z7m6l;23alz{F9MPie1^b3NTg?X3rQ|8yGOQ!V6aV3SR^=gFFhbe(brW=gYbyM4xM! zA4NtKMVe>;k&qQd52Xx`DsrWthhADt;r&Gs5AFxnl9`&hRPI=b(0q3X>(_6j&1cWjcA1au(vIF#%1V`Q z8xzxS`dt5`q46E@4*dy#|D029q<2H(1c-=$Xlv=epSlj!Lg?-O3ZT2~ zF(L+Llg9X;{X46j?*Q|e?@j7oGUEf|+4fC|Xomlly+8S}EX&sXuzjwzZsu;@yN8EI z-$zEaFJE6(WQmekAV3L(2&w=Q5=5X%gOXG;36cPzIMBbKzd+zz6Ad&`Lka{R`HJ;2 z^D*1V=)LdV&8@e4xPISSYwvxoS!5MBs@L314>P;>+;jF`yIbFCzQr}VN16T{ihJ zKgyv2w(dXhyl%0z@9heR>?qabh4y~-yPWUUuM*=4fB^ggW?3J2D&KnR-OBj>3^F){ zPCu!@4!|ug62Q3(w`=umkT?9u)0kM3fH~E>H$w&M@=18ERSh6K#0L>ogcln_1P0_J zx)bpxK>4kAwQqX4!#mEf4(Z;Dr?Jmnj#LHr!YgFn`bAU(-dQ}=q&@%``5vD0ERD#0 zm8LOz=s)4yKm5@jiCY0rHXiSXwZz@Np5^=Njy%!q;jr+(XGRA0I(fe%936psDuu_i z&{ir50IKQRBjx1Z3&!|I4q~lM><>y6AVRp0rr)u#-^q=SBI?9J>pW5ie;2n?Ms)VNUjAg=T)pwOms5;Q zF_90t+x++_-%Lp8dHrSVG}AIs#%G?wstvctM)7MGzVEK7L_DdCyZ+j~G7QxaFjVVA z`PWPT57`g32@MZM)4jss(Bt2RryO%;$a^Uh{Xc(B@KIiNivB-a1f0Xr%c-_|=-}DZ z05A+ap5!;~IZhi^x;Eypl}R2*-f9ZhCUVPR8R{{dPUP%!StutsOrBesxkmD7BRlC* zXAme=W=rcB0P5{p*h$yDsZpu4+>>VF8irmN4p99Gvr_<71=f9A^7-9kr!tO{yOeHm zccBw)k`JXbf0WXvH;iL9iL!}JD3fkyBl_Ddw5vUWpY1=Tm&Jr2BAYn$BpY2gP+z8? zd)7GiV_MpZDQ#lpGlwqxWpT{CJuMm`O(`v^AeMc7@Guqna8hmbdC_Kcck5DW{z~b? zftt~;grRh7_x3$!=x6vWrI131E{|hi*fHj1KRt$V?uW$S8++`gA}jMULOmbAaALnW z89LTmkUbZCPlxj;Aa~$OHs0(=F@P)yfX^X~^06MZbn#5(VTHnlMs4pBnvXOR!6 zd}>ny4#RVyn#KaW&VI}5(&cONa`3`M%d{ zs08JTN4;ReTv0p#%gW6Ur2?fxhK*9EoB`)IbWPToPSPXK>0i}z`ANG`Ftf(CkJGXY zFN_D_?3ul{oBJnCb3-G__<@ZA4WnnA!r=zW))$9p-o zGyUMWwDA9>(%R}(3HBt84HT{P9~Ohv3s=g~o<|&gW@G!8_Svrz5n}vuiW|a z)AEh&kH4_@e`fRdp^ft^pZs3=@!$KmwNDco?;*_hYB>4h01W(n7=}ENm@co#c*eYv zVY0YE>?jm?8Tq0D2a!!TUw>0T6d<#%G@+vig?{Sg`V3$L+5z1vW4XK<-t4p5mkt%W z?I3tn;pb-*;{UeIc_MFAg@fAeB6+iWJ@rs+0epB`Yz=3rx~0gu27uwgX3gPwCY|cu z-8+gFAQT*Sbfp{}I**G=!iKSI9uY|Jpb)aobr>!(asdx4(Kg5@)13fQB@EU-_~9SQ z;Be3SOZh;_!aV6as(`a0FqV4&QDKToc<*EFeJaZsBzy9*4{n&81u@I9U@1BJ~4-ft;s2Lj?436x?I28XQj zz!}J#Lbe}5U}CW3;75ky#f!6k_|97x#>>E<0K9NIfXAVV~VAZrp_^mp8+T@S|Z-d`8ZQ*An^(*7l zsF3$)&+oom(-hhmq)ZkCwlEuF>?-;ihOT^I&X!^5rcM2?$^iPrOe;fcT(rZG)HI~M z7hF$5(0Y`UDd)>rG78%t&9pTOgir134^q&NOFA-5sQ2{^Wu6-I)Xvcq&7 zCWDo|8pf`<{Z`Y!d7W9M4pK>p=2$M62ZlOu-E2Q;Q0b*sszISAi3vK$A8KBUs_s~GC9oR?ZNo~j>QbWd#cqq-&?dQ_r`!EqB$=wyL(=E9fi_K z*+4wuD0=RZa*lAqMah9w&G*9tDWM!VD7DE1oOz%;Zzam)yr2wSgs5h?<`BRQyyVo9 zfCREQIQ0BO3>kQIBtA|?xR zsbvA14Jl;DK@ORv5FzHmo8^@W4kLwOqk4R36Kg>H0AYx^YykP%^;ZN69E_|xgLafB z<9&~4I1)LTD~?;{HTtgF0SL>GOFUQtPXX-Z$+7v6b+u7c5e^jxFvQSDkvD}$20)>D z=F1*-eZ&5J`OO#Q-XkZ^g2avxc*y21TfdG;JJ1O`D^!4{jNePD4Vav_Ob+X|2G%K^ zX>(_;QeVDV?8n}F?0aGbzzO_^xR*Tqr2%p&=%rNRKG`$KPef?SarG5YB_V3n`d1)z)_QO9a*KIF4IP}n9<&dkqufH2my zssY$DnYXMjaZH4Fko|$R%RY>EpR^W?t%RwQqH}iBL)bA`iHSX)BQO6WH#VrAyzOi3 zl}QYUL;xk+o4>Ps8HO%GyqVL0elfrYu^J!=FDAkR_#`n`u3Qo;%U-@VK|ON~**{ddx~Dq(Xus02RIc#@l>o|*$SMqa|(7Y2F_d42Q^ycEL=Xct=c&%)lyHqOhkBUFct%JRn9T+QR z3OA-e9@VgVenU|&Rb>n(Xjy1i^Gpg#4OMSXcu?}OVK>eD4*%Ne|HGth^_ihbmV~E6 z#YXknW%{Uk^Jsqvb!b?jl&(K%MMI2Pp8+Bdce0(I_gd;rGfo0q_$}gf^1Z3KChl0e5br9GzRW^;i^EPanY4Y! z%ECaFuC?u&+UG@i*PfQ~6c}G}LS(Lgw=HzvY3(o7veZC+$|u5aavqoILOad46|=B( z^6dwE!HP9ZHrPF!oBV?sLC)?Ew0d?5h!i)v0=T_4|jPg*s zRl&c9Kh0w7$12De*(#?dBpn8XyB~=UK=z2S)G)L)a`<<*D<43+Pnm%I(_VZg0Mjuw zXPBNJ+=n8O^03D9QXt6BPm&!6yxWyP2xW?h0+pR34=#)V0!racQ3WU6}h=DvWd$+dZFo5M4o+a}5@bV@Ic5nUQ47zqeUwh?M4HUl-xrv%0WQM5qs(q<>sX9wML{ zg-!W+lKYagNCQ5Cd=JHzdnuCQ*rOouqGLP|xtSR!;22;6c;y2R)yHUnvQxsKL;DGf zXMV_|j~t?+1ouOBZhV*v3`1h&oCTiij#mR^2jyr~NupU*NQqcLZZZ@NP7J2n)p^EIGkup)ByyTXthl_QaAUk?&kS%KMYVzedO(H0Bw12^uK|I* z0P`xlhEiZO;Kbmn`~UWh<>|AUXUKd>LM&Inn4pnB?0 zqs*Rze%42Z7_-lidbJs;HM#HcJNq?rA5SdCM?gRO7|}!zA{~Kpf{!CSn`k2E99WAe zgTu;RK92}6Li|}%MEE>0z=ii6APe|X2|%j0M1`3wug4lqrggXqRKMQ#;M?Yj2b4QO z-qUIb!7H4s?3@*Nq~9^1jnNHe{|6D~4?y2jWMhW)E8+JW=siy}#P`@}UxNa5|d$jh==<{xbh2OsYbyPLDr!mh|w=D6< zK0_Egdj~{#9$J52_t{Vi)u4N09C}+upX+-5{Yd5DT*TNQ9vO_r_P-l9om>%5{iCP| zzTggF~LjpCkwJpenoU6@lJef$F% zH16HIC89mN<;-sYbcu*H3?j@&!u55Zy_foYB`W$bznDWf9PZw|-IWZS)lCbJ{zu>W zfd~|3?6SV8YKwsNT+8gr)f?qi+k0y{NSlL-6e%9r$oivPApL`ffz%kjcYN&Bp{l}T zYl8LnSYG?ba(pm$uBy$Y3&Fjha(|HmHXFQ1DJcNx6qjtQ=oj;sXb8?pMb2^VhD^qA zxb%8j5B^Wf_s?tcC%iKk$@?FMB$f&ddNiHqh3{4#4UmwhdRa>X-N5!8oERIZ83UZ9 zhCi(sSK2SB7f0I~h{~I%Y8_7Re?9g8s;~F`N^q{v5}6I{#4VGw-bL&)Of4L}r6ECU zbgs&7XlO4-vvhgM-Ce5;D|YB!L&~xAShGUDif2j(BFAweq{+S7?>Q#D{Nmqf-|c0l zfpSAogQhK2Jtln{-;4rS;7*7Z+l`hsY88MsuLn)}Cg;hQ+5k0Ti>_lswfIaYWzO&v zX#OuOXny@r2c(;{)znh@b|zs(BaR4*P)KDqvHp}c2U7&Y;<<!#h6qmksr^e)pK% zW3oW%r_uh-h|oUD0jomq7OgjUoQz4COz=A4dkaA~EBuCS;@6%JRTk5AUVoXqU*DT7 zjzHPd5SO7&3~yJ?jiGKjxlt{YFX?~Fo_HHeze$u56DKVVwI>YrEN(xmH)yT){^`UaqcdHjD2?@D3N+S2gQv>)~kMK~!{Z^uc5c%1xogI0r zS1U+7ijsB+mgk7+_|S`paiG6Y+JZlazsbq<_q?PZVT`IhOqBxjMx(H+RNsNu&Q?8Y zd!Co`Y7GBC&{x^ zFSvMkY#IwtuQh12Q8eXgv0+aJDp|!|OP9a{@a3S(WBva7b)*b%*e zfq*sw7%+HYP(isz{?APr%zh|DI=n^5iZREqL71w>F$gKIUVTM#4a1Mv%42+Q8ko6c z@Bi#)|BZQ*_sg4hpZ81$J8Pc<^|sbl%ZnqQtL6t5P{xzKZSOtBlV?V&tE@>31-AAM zF~r#X%#BCv`GbeT1EvAs_#wGZlIta*UIp*5!ycCAQD<10fy>3C3H+a@1Y|@28?aXg z2=c$;>`orm?^_-B%252wtzU_KigwA{Kl*!P_k3P}@S|>f)^`SvEZ}}J48(@W__G=} zhGCL&z~=|!SR)SHSTS@)hlxd7Kvhu=$jSDEWdpH?M4&)=YIF9E86)KUPhYe3ex01=FMcI?Z;yEYhjQ2E%kTdi|4#Y${)7Lx0<)!vOkzGd;6ILi z%=482U94w%4dBOUMhG5BVB^AHx}IgLQDv02dx@K&=xGK9jdy)dZ}+MCXVME>0aUdwN4vPu_g%J*}a= zAO!@Rx!3iPbE!mx!-~pl9%~M$q@az$Wm_K~81Ps6*NYd`Gt52!cw>LSAeiTrdx^KA zeB5iQ+Oen7gLfqpMhUigl$k}YkO{JW?*+&N{AXnn3>o5j+22)vNjv-S z>|Q7!>b|#;*8*TJn%#&A)f~iJnWx&g)A_%<820r1J>E|zqb2n9Ei3HqTEuv z$`Ab`g@82x7=IjP?J$xrN9k0RzO{Mu9eXbQxGRFg2h|XhrDK`D5C?LuIIzE~y^`;` z;optN$erkx&jUb5Q&aHujtqH9EwMB`;MO<6cBRybP|3 zWf}1qFCqZNEdjBhUBg+NQHU73F*30acuxOAwK2oDBWLCm_u>3m3WGg! z`G$=h?GSn4izj|wuB_CmJsT03!*?G(xUIRxnW$fC@kFpz5af_^;IM>W75_^#oN z&apfTtW?Io68e9`QF#P5SlDn>Q}(;>ml7&|KDp}uksFcUA12>yB4~iM-M_=BW9!dQ zyEBnfyR?Fji2-3~k?$48P31VAl=V27BJKV|n=O-Tx_)$B?fN#9{zM-8J~p5^G^uHnn3Rw0ossTfY|vGKaT` zr`n3Tmi5An`O^uiMj$MZ{MCtF+AXSaZ?e$Z{T(HySGi0lK>kq|y9S_?F4?D>lx-kO z->wWD;+^??>~U?Z^R~cSS;V2A6t#cT6;Sc?P(STtRwnyK{aNqXtzvISHN;?~ouzb2 z={XC->eGL9GOle+EId5slV)$OfZQx~)g;uZw3kG!!=nsQlXKTy&4wJ8y zNg2R)vvG2`cjNm?O=XwOU0kz>fAjH1Bds}OjUOaqRGQRa`s2#*P#0Pz6; zrtMw~ISM7f@W##8<-tao0%W3eyo^xu4ClTWUIq?c84RM%brRu`hY5h*4ev4S2Mp3^ zPhHq_vO7D3hseJNv|-if#kK3E5Nw^hAjJ!%gmySDfJZ^hgB_XoP_&oi<#G1m^g??G z;ohyqVPcoqOf#uil`aKc770?2P=&N*q~QF+ab5*y}${K?OM zQg%qQvHM*%S)Z}{tz+z>#3hjlw$3(f{LfkXjB>}<`FTVssZu) zdR(i#|JbfYDrnDsNfb)A4`N^_wr?(-zf_Lkd@zN1=an1fxq18k{EN@ah;huWJFxHU z+PdAb>)$l5=-ZdBm1kf4MyVaQ?eqV?-R3v820#A8zhD0S|LA|MdCa^*;b)#{?+arT z=|x26VB`|Fgn?jyAnO3qJHAI=IYfJSRGG(*9ys7h5<3rnccQmV<)|-Jpq#n+7^~R3 z_?#3QJg}tvu+|ARf1t4MyTNR3&v@*>8+lfi`25yRH2GM*@dlnsZxwEPG9QnrXK@ z*JgZ#=rBXl9}D>37XUqb_FU|h?pZxPa;^l7DS%@<&|iP`xxR=#RB$-n!+afHE!zVIg}PdpAZM%Eki59|mD$_SHT1N7XRF+(;6>%#C+~Uyq9T z_~RdHQ;|UgUIQE3<*3fX-i&;{^U062u4y|dfG{uB8Uy;Y&3r%q<@=Yey{BhMc2>z7HCcCY*nOe9MhGK z?=DBY&V$zcviKV6_$I+(|KarK(0wPI*kznXEVOA@@y;-7;UvREEEWHZlNMrKPJyPnJaqFkyHu@RIeSW}B_%3&tut{Lrm}DD z=QbK)Eg2?@cp+_K=!UCJfgIah2-R3Kl~wLOlX30tIksnvtx=_KW}T#CjQ!w@=CRYp z9ABJgA<1q&RO3L7mv1h$3lUn8o3w_xPSxCiYS}=(bbNd`E-kMP@`?^9(lgI}hNYNr z{=Jvcme#JtznfuWtg9%mfpm@1fJ*`Ld8zc-u7g4>>Hs~yk=|<#dK4okN?Or<2!kf? zZE5D-p3JphTV0js8Kz*>O15hWaR;!g{NL(YiocxO+LHG<^XYjP5Swj9k#zFDgX)z? z-T}|oUsBGt14b|MmHFpcrZD*p9tHW)au63{C`b}64t|s__sn}}DZ_*UsXX-5 zk1U^;6%ly8hzJjl%u8`yULN;)AJtR}=W^KBGXUKvt@J&|AlE!f<8qXEl9wt{CU*Bc z7Zp(Ixw+27ja>Fsp71qAS3GZ26&H@tGVV81j`XhDzAA*D98)rkBk|VBAiEQ!w7lyGFH1$onc3cB!&iLA>+sK zaV_15*B@|>$M5LaJ)n5@Ffci>*GKZ`C6Gs6$>`5q!{|fkKB1c^rF(lGJpo8SR#ZTs zZM0Jn6R>uh{L-5XaM)2Kfg4e_x3{IB@{CMnAP*G};>&}h^7G&RvOKiwu9@7K$&{kp zvT@uX_2lqK`SbEh19-_Z1>jj9*RNeGbL+zfVX`(}1qN?Oyh?(9*V|TNH_u#p0sC3w z2jKI}ozD`2NzivVwtV{x^G~+_tQe5t_8x8E?~9ag2=@P>l^|4Uz1ZTGZRY~8V=P#S zaR8(r#zwgk-xJu6YnHnGD2)x7zn=yVg=r5_9U-2#9R198W_~~I1?;PhabG>#_OcZ` z=9JpQ@bcC><-V=wKl}W%Vn|fL9bSOl!3hv=+I)K3_O%C}|JuguxZJmU{TJ5gpV-=Y z^ZS2T{^5W8Pqk0%E23#dU|*&G%tH<%{j0COCa}r+=cV#@Hm@M8Q+;t;?-|aI;Sx_X zYnOcrkV&+W`fl@{J^+KlnwV1F-2IX9w=pqJ`A4xonN7_U7ZS`4xn zIM`ze2hWmoyvSqU4B|b;38MkV2qFbY6WHI|FW-Fgg}%pLi|3r{*e5crD)$K5`~HWY zD3SsLg_8o_tD)s*lS2P#3kUL!Q{z{91$T;-Za5g{S&2*xw2 z12IqDw`XFsc@Py4NEHNxtMZRs%e;TzKs$5dL5wkbIQwgQzLEcjAAMgH5Y$II|2Z>A zjbQq%21f~ zyE%1o()Od4i))xHq_LaW%^)-O;XkDd|L+#E_jl;J(oKp5K)v|;Fg2odi7aV@sBI}V zkfT(>{*=Q+=2gz!IOS0KBW4|WVtagy(a?-TrRMm@TL~XE zT0DI0aF{KvNV(10!SXT@@(4hS{OME_@}Nnu|82QtddxWLkV9#5g~~mkVz9SEo@a7! z^G`+rygPPCJEQb|-b6|R1VRrU-gSZ~DRi?+K!n!;fV3wM?2BDCF8B%aCOz!+!#F3*5dfoC6zFTp^EdF%1sDUu^dolq?C zW~*O}EuqRL8^)FJVR`uW_hN~RK~lCZunhpBe}_^!ivT(4B}AGi>>q{M6kVRJG!**| zV9LGpyOrdTCuT8Fk<2n=pWTk!`=!`DJg2_%(#QH^_2a&)L*3LJ$j84V!<}6bE}}*s4Ia7hp?O4a-h8V(_~p;ap3T8+yY|nAbAUJV|0u%qSp%%4`vLX=`cIWZ{6yZyXAyp{I`eV9us0}gTwe5}$~WSQ z#4JMm*98>cGlLho@Z`87A;S3aQhMcqhczVJ%WLih3;vczR22u>+CS%B-tvN?6mH5J zjE9*u4~xH25gy+c2*P`hK`qGuY42vF_ONGgMnGIg*fskJYe>K{O1rVIzX2*~cKrdyVQYd%Mcl2RMscfbyY|(Yh0z3dDOZH{%4@K4TF6N$yfmJ0rVJ&;04Hv5q_VuzUG3*fv5)3n2=}Y-y_4bDvMYY zNyjtEX(v1$@Q-(#s1No@#wvLPkR?$W;w~`He;P`7iHK50^A*i6#)P>FXvfH(5P^HJ zXEP5l*z87Hi1t@FC?c|u3Cwsy#D_jWg#>c#F@Rv~$<#pR6?_Wpr}U3@^L~;t0{&TJ zl>2)aIsfjp_ZTOP#Ecm{3#5%GGC+|WPogr96DOXx`OTbh@=U=0eLZ^?DUkeo`|S_4 zj+HtP5eV?)IDzr5&v$D(h8XA0cpm8|N43PV8=;*irq{{pcKj{BFaEuG?ov5t27rKZ z?Q=VG_xC*VNbmUOpLnE<^?}Vp9LQp9fdTp7MWuJBy6)I(txcmHrf_#WmBsL{ivB;8 zC{c`w)NB**)90myJ68#lS`%&iS%ZgY8~l}o1NJ(`qYnkl}I;sai7jW&=yvb zMC42vMdmwwMD%+YI;ou4GULiV>}VLHjNTHSR@I~RIfX*KvbIr40smzO%6Q5iore^l z1_i$etSD_~TDDAvFEOSYU@qRo@#BbqYKT1MB`w8e|Oq?KbwPZeLD&F)L zAc{9Ul{*yrHRXuDlsx6RG*C#0Fvfys5gO^lN~1slO2()|-@|Zb4ag9K3T<{(Wvtr)3O+JN;Uz+XXTk{}Pu}3h2Rwb;Bhwd9BA3Hk zxfBXNWQ)%9zN0n+!dM?^L`g0SO6+Z+$ntO$W0X)EtDQq@gGpxo?!1iHmhAZu0uH2mqG8I6Aaz`W!(e$Po;;dw-w z*mvJE<^S~`{b~8>PyXxj&CZMRE87QtZTswf*v8HC|9Aeuzh`S>y*x1x<3w~11&#ro z&qV_E$UTpbAIgi%+*C==;;A?6b%5P8Djmu2go?m@IhEsSr6j@+%(^Il` zJQUvy3=Xg(9G^U8_*3i}_7dd8d{1tT7>Cz6B9j2gR5}0@DS!Q;8w~)eyZ~kCedk{6!K^9v*bmHee?9W)A3XFB ze?rJ*FrZ|iz^$Jb;#E=J`XvGFtqT`y9h;Zkp2_-u^w4wDkrNRZ>GWZ}b1r;nfR?rc zBES6n)2IpWVfh$A70zz5QrSY2)Azpr``Qa0MZ36m-{eo^1D@{;?`B?7itjDL#_e-b z51uGBU^_SgHUuQ;Kka8+VeiLd?iCpBR`Np5THm^7x)Ut@$>)&7 zz>rIR^1a{FT=zWtM*_i&7cZXWW&FP5(JzW3@ybEy2r7w6I++=7-uuAA{tMSN-)RDA{b`>MDcqs-i!mN$cRiN7Yeo*7dK%TbXRx?atM*yx$Lchozi1%H z$N+v<*xKAn^Qa8o=;Tj)hQ%A@1q(Z%52cR$RSsPgAk}oA&!blL1?9g&=$2A)FzOx! zfHEP4JWsjz_M8O5u5>x+9ZHNy){Y#=%9Ah_XVpG9ltKouM3Fd-yg@PVq6`F4o`mPYjC4C3a6N8E;dB+rGg=0eOWPfLfkMdYwdrL+V`UqH%!enyV3t7u(Vf`i? z6$OO0i1cs8`3m+_4ZuqeVuXQ&kDN!3I`A;nwJ^X)feAVCJcSiopHVUoBl^NUAj^vS z*bOkwXDIY|#+FqRe9vo1i#X~?{cN6*0K+DSSImQnTxlQk!}F!j$b*HB*1*u$^b6MPBV+omP6@FGQXu^3f}onauM)Kqrh=)du0fcKz}8@X_6Gy$^?TO z5zvu{OHmoWF~|#}KPvxM8l!)!5-`-$Z^buj-9x+qGfyvwO}xyAwy}RpGf(HbWq|Xa zo6Ns@@uJ+eIbRgDaa^`U5NPYcu6fUZ(S;xWaryuL+5fNn3j-m)wK?{=$^0wsWq|0- zPkyhQzkE$==z(gtZ@afybDDVB~$A6-|jXvM{`pZgUw-dShUaoLO`zY5DJq51^2A(V%$aj(}MS1?k zBkox5zxR88qIK|0RS~|fVerU4qXT%cXTl*sIJ!Lfc2Dk~m1dGml6I5Q_SQS^>s%ou z9z!J8yJ4e9CiD%NKq%??{$F~SzU^tND{ibjKJqh|etu}rhPOe69y5LrA*JY(AfZLp zuiLngzD7S3QDJ+VO7Vu1K;Yk=`K~>aF?bk+i5O~0p*xO>Ld+8k1e~9=>$zPcg1Djm zOV2-*C?pF5)vElUC>3`Az?Xo5fX^{B@Vp!^RVA{q##n(d>7jTQ9(WoTMkgX3>>e1W z;8>9Hj0ldWZa@%^2GNvexPaFMV~tnFcWMAcg||@&qTUI zlK*NSJcXBOQ6D?)g`xTCD{qR(ICZ=H4(HX;@j)nZ^?s>~CbFrJB(}s73T$X*ig=Zt zTAZan0ZeC1$o6WhO4~yX3mD{)s`z1Sy7FIb{l5pa?Ovh(msHB@AHxLvR~%THt)L7G z%}U(VMBa5jrBedMPD#l9d+m0$4-J@<(s{;9nL5XtqqjG-l-l4Xw4s!#_+uW7(|e@| zeL(Zc4gF5=bhf8f?MvfG;~uEUv<>^vz&r+k|70qR(iXw=K|>paW)SGCM#H4L@x8Ix zn`)lSDI=K+Y?w5;G^oxG}nBd?k60{NIGzhU}uTeL$?$Ue#-C*DxAG8Wrd`&pwbhPrWdEzCcr{13eu zZgKH<@5QjET!&_5>LzjgK6cU=hR)8>Hr%1@2yN|-{Sda<s<$~vtar4k~{u8SAzdMg!1>y4Tn+Mneay)6NYuR%jzB@niP=6Ghz3@vLr z9?xT6_i}Qu_eQD%q5jC{xqxV{{6U`fBukP-U%9t)$uxNZXdh_&n&`J2-{XkxqmxR^J zBeY%#9z_%=PmzaIXQTjQg;Ja(ky+&cB@(bT&gWs|nO6e{-^GAHbP012pn~_FYqp~f zc!_nucT`1T7}P%$SJoTf$FoO!v-*MOoi0f!rnBiGMe)Q)v~FPYoQ*YX2zN}jUt?gg z``(Q?NXYUlD%*Bgc8O5Q^V2}fb3i2Ei#B7xfExp*bZh@WIs=Mkc#WGfT^f^rDFRO- z%PVbb60nX_hrgHNcv4Ei=tO*cs_^~TrjiISH&sCSQamHW?w5yQ^6Vkr&!5RJpl3Jr zMbA+hMEs87f(U`+G?>Nqc{ooafVZ`8Tjr0efAooPUjtGF10IGd3}-iQmVNW4-+uU@ z?Ausu*;s6t4A%jbvH5;rYkc#@8|5ee<^QMLCVbdFzhz@_Z!iF0`_Ui#{eP#t^MU6l zvnERzF_`P@X$kgKW>o*yY9*5_XQw_d%`{J79|t_cZV!-Rj!{~b?w1qj!zQ8&r`o`DJb*(`$>02KF-2Bcw&nPPQy3Q&aIm_sa`G+x+Yw_xGFV+`^HO>!_-I*-m%D^yyA%ie^`s*ds9;!4a z>p2Y&yqpK*Z27Kv!ifq3#D6WKyhlz*7e>L=)wK%1IM3h;z(9%718_`QgP8SgEwR37 zClN-9vRd*ow-`#v-3Nf*j;I1kyJ5^AB5U*PS*@R0R5+1*Uw=pKxGP?O2ijv82j&6u zLp%{d;75Ksdn({RL(DOd5P9_cc}6^}I{BjQAAoQo51u`Js6Lzx0!TM>k2>Q>U*bOS z7%(P8tU00MZY)Zw>AWW5Q_?EL!(jJgzhh5iZ)g25FERcCav7JWk*-pPs5l;rGO>L zVNC{D3|7q3N8y!M#GbX8XF+&NdIUz1hr#xr6=-B!-*x@qU0b`zo$-I7=Y*cXId9K= z_0>1U4dB%u9!WOK(FU)$b0Fge=fB4I$%Ak}9I3sWv#c8@{hRVF<*SdvOx>&3%lhlD zTz}K#?rD$}uBluzXYgLCLB11K7?9ww<^4AQUMZ#1r_%}2oS~@&=O5FyrKUaBZ0unu z%`5oVNB=KFnHp2_dkd-16(qFhErS0t#hj(PpZ-%VC#5U`g9V3VkG-nIhYn`fQ5?q( zHr@@weQSvMI(8tl8|2Ab>T{ie zb{4E}ZvC^qQT5)F2M@9Bot!Uvlpm?+uCp_`5;pRp*MZXynCB8-*MVOlz)bd8CY)^L@fNwBmI zPi4_{GzYh|5g5AnCjBal@8{z0`w;if&1vybRVF#-hJFE#z2LkV-431YYgQRelG^%S zM3XUz6&m+QE7j0~EK2*1zgNe!V{&bCYJPk)nqoAxurI%CCIxUs3=R> z$IHtDI@n8=dcZP=B|wT0XcRF*BLSBy;YFrz>1pBs$6~MieAw5mO(+@r5!Q`Ti86uW zlp%J@LBQvs-%B21ZpxDK(w{x6;i3wc1^9`X|H!<;uglYcVi5!3Z9NFWLX#70{3wVD z-oV!c;?(t0Fj#?@`qAb*VZ>A3lvxSMxV%UT{Q(zo)x@y#mkF552c$n6V}MR zFhn?s(^h5JKeB@xMFXV_g%ssyGEcD-;|#NfLnA#s@;io90<2k|UYR88@y?Wh&cC+sNYtG-N0|?=zO} z@!!>)$S4i2dERniFI)h;Ef`cK}H05_xBB8T`~pt4b3h3DPD!+W3L@;%JfQ< z-NY;AWg9&T1A`ET4)Ul02Qs?Y^I121hCGszKM@7XwgZHo7OYtz21 zi6{2Gr?AeOEH7fbuyNfzc5*L+Xg`c&3-az7P(-d$>&;Ui3uT!4FTtWMF zdrJTgC?z7m6M2RS(O;@vfc>Qa?S$>)^>;S?hzM}qt}*h@9C$0Af9CropJ%SWM;)|^ z1S`>|j38KxEfKI!UjI_;Y%(s)hMFG0-puFA(U#iYA=~d&2GlQZIh$2uM_~q_jZ-VJQz5L97qL}o|h-SbDl|-;f4wp6Ud3of@O@Tu8*fAF8Ac9ldXEIU%E&~AaJR&amH%|@%>l*->y#W$IQUo4GPCNbLOwgV| z8i(!GM4^53(f70mXX(5obL5=dh@5ox-}4up-vAH)Z5w0vKszxXXH=Fj%yuAa_S4L% zr`=iI#+msKb>4s1yz8`sz5&Lc2FPAjRRxbklz+?wFJ`AHPzrSz&Rn>_59@0-+F#Mtu zuveZpLniy2}4^?#qUeeBHUg_1G%{(0l!!)y)+0M=&%|bhN@kc zgfwHRWao(!hB9$JRlHFz?d=uuLkj3P_I|Lx4*LJZ3772}J0RBNJptc6FIobHQUljQ z&rg6~pM>dKhHjX~4!3rl^xKRbH$m6`9@Gq7N5`C7 zwLNWu4h!ItOxc`Jx@Gaird}AGX@Veg@}ryo}-4TQP_D zdy(IuiLC#uPO`Shb0W5~pt-}ub6Y=$9L+MF*D1;->S_rX^UvNQnA0Om5R}%Nq074R z{l^b4XR1{=9VBd~Xsw-b1?3>hIMp%mXP18&2~9Nu1lbe{F}a@Z33Boqz>rGi$q=wv z?wg`8i!w&M0B9cGx-Ua^8GO+m_i8Pdl3Xh zcwi2~`#X!0fhYhf2lVi+l;32d7MDRR06^xhv*p$>cPD!D{64@Xp{N|*0P9Ry$UX_+ zA0A54L{UO{!}~+OGJjoV0Hb6diVKPk&!XS718}(`@9^OF+VW}xlmR1$&8RZx!4qJINYDT(WwU4(MhOuthUXC_GDAyu0AQ2)tM*%aYUF%RwsN z)_};5|M2gYw?6to`T1{tVIcH=8D$KzwaUCPIi4e0fV2`q$W4}eHZD&ML_Rb5-?D3N znK9_t#&zTBwQ@}Ta8q<=L@sPy@@?kOBX(v(EB85%BP~hsYN@|i?+UFiExf+%y6tox zeIwLQHx9H-Ybio`I}JnzgS?bwhn(`-nZ}ngMYvLFaF8@LEh8*0i4B05$ga$=@|1GgAg8a3_k)% zW(*TKo$WP>5&*c_ItBdp`TI|!1R1|O$k`PDIt=a&zOQw>yyWZ7))j_H=IfrqtT(g= z-n2cA_5VslPwm?ta`(>H^720tn7tE(aK{4NxsS770CL1iUrMcffguxeH@HTe2jf|} zZTEXx2@M}>ZSXz%&3=dRK<&3@0Kx!OfJ)LDsQv5+YK}Pz{t=3 z^YvGs>AqA3a1NVgMLaO3^!fES-x2sHf`I-$iTr#|QE>12p*+#&MRvzJ`tpn42HX98 ztq(<`*!7%&w3T!N_VHSp55xV6bH*GUdFc8jGn&EwLLV}Ufru~+@6PnUs{ITj#+9o# z?3tem{QEg`P`&6x#<3qmM3~|Fq^j`3sKbjfzI^2s@em+y=FwwSSyfi7|Bx)cw0lBOObP{?*_j|hWPMS= z2%ZJnO20AK&<}xs(_=8`5E+1QC1<-2b3RFQlQdXqd68cCZLh@$fL{N~mq`CEUQfj^ z_ClgWiA;P^GDo-c`euVX@r7}Mw}(o1Hl$uzn^eJ0RYhlG-JJWI=T*97mWPFojfy5F zZThRN{|}`%P&EC&!+uphV+H9Y%>u(v@j>F{rvHznHzu^FH&i#IzF%=$Gk|nfnAwon z!DU*JS$lQH7u`AgRHF%ol3=GBzoAUbN%8eKzE3F)-^MJSF^O{T4L~_Tnn9&@p;Svo zDgCS)LuP7KkN}4DW?E;l&6j-Gkn^QX$EQbM-9VMn!1C0%knO9JG_Ot3On3m|yBSu| zl?Lp4*W16kVOvm!+}PKz*{QJWHBETBZ?kIU{2lrU&_=f`5)Wn<`%Pof05MK#*vF!% zjp*-F7f1JM^K_`2ijz-N%h=c?QQF>9rmwbWAKoee_JttY z;~*6v8|O8jUx%Sm4z{FBzYq!AAO!)SH3Z=y3YdHK1LVxSj2>Qm4tT&I`G152s%|zB z24^;;PypO!p68~}Rg^LmAMQ^d0PY7-i9lhJ*Kf+R;JNQk49EeGCr%!Pu}HDJaN(kq z#94Uj8T%~ZM`QvD)79&*NZAroKB3}-Z=0wwW~iz`L|EWq-xJ8(DQ7pgDw&}|Lj_{J zq!6Cx^jxS~!J!)s)Q1Q$1t3F~gmR0wCKXpFusbK9p?r9h2~%!SmU%ub*aBMNWs>Z6 zcjOu3Jwhi@LLo9#{{Mkz`2md528E>(@sWc0G-R=h0-~Y33C`gL_3Z%XV`$sRclAq7iSKk$a4?8dLMu1 z`{kVvzFThHxm8{onB1~!E)vFXh7G%@T()%p2_0=B#D5ttz5OH-VB5geQ%Y>v&u#N2 z4%V-AllKyyD)Q59Y@`gqZ&CLd2hMCW%ICMA%SsEtOYiT^>+a?<*b`G4LE#c<8f%~_|_M{vS-Z7 z*Y@wH=4tzddHDbCKm14KfAzopKWSgNBk*}&`wAiAfD;Tm>`jn>;&Eqx5sN>e_%=3> zp5aM$Qox5EcK_5p??l=FkN}PzMRTD2gZnOpu@vLil87>QwQklUCz{`R9snISbBvqH z`DeaSa_)`;IDej(81!_Qxt|$>r~RZ+^Ef<~?4^KU`bG2r>+Rtq2diE#?V+=(iww?< zWvnZKt)*q14W7z#MtjwFzyD(yOjSDW;R6SHK^8YAM?x4-JmtW1{4YeEd)6~+%#8*u z&z!arwom(JUHTd0^6@7>R6ia*xGy;nJ@HtCd2@x%1MEB{W2QNd48LRFp)?(P)bmJb zIE>0LNwAvp8?Z?od-fjMgE0ex@s2>XN5UoHD9-}Sld=HwJ%An~E@u!VjTo!NA)-BZ zx%}WqeLqU1bwA4UrHtu6K$h!P#C72J`~DCANbL}DAgKXnxaXXgGeF>ekf8^b ze&*k!NMXS+fB|eaI3X`GL@r*Y(kn!0&m$e@*mVo`OThm*NePd@!Riwq<-?bI65#MA06mE7!xo6b4F!(NhtAF*zfzu2CD`s@OeEki7uXGq-z%jEO1kFR;XG^`ZLLXP9 zODiAlS9B5HrJ~_7RGisIqsPagc{(a9W3PvnlAf&4u^r#!H~(7b|3hs+98!rBhu%a{ zO6eV+9ir=jdg=O6e;wt}^{|1lp~)c!rh)P_tPEi^T1Z#u{#)u; zMK2m=E@csIPK_&W5C|DW8@sl&kscQ2Ks)+ReRX4A+sN&f);Ayll+s-%&B^bzuVkZJ zXNP`1)ipMblSbu`P6o5LEOm%DIy4NE#TDhiY2L$BR71{4^X_mcI)#OF4x?Blr@wTJUSfIDpyB{ zta)-&i`QOf%!rXS?uV!jfYLzQl%HLkDDUNV5+C{rmB0}NRpf*_+X3oc>Z&B3IuL=M zgY{Id_QFs=`yiLf3Iix4+4jsU(3AxRU{8dGGb#3vdZ9p|BrdDE zf!BeiAIY6S`%%`86sGI-vE75?x#sgILkI}Z+RH=@c$CJPer9+)&)-$9>WaKtbbNQm z+r#r{3tkYwKO>!K8+Z>rpZ~e?`DGMYnn_LolOdFIkm%Jkz~z4jAwV@jaA9o+q+LO7ht_TsGyOF#F^1n4&-es#OpA@K9+S%$@PGOr4u= z9tShOX?rhr&*Qi*J^iVH()Q(9cRk)lTYszMLYwEmSpSO|QMS#i`M~DoO9O|WJ$zXH z?AERF1)fx!cW^B1jIMycYT)9>pZvh){JZ6wPk&J!8i;ynfapv6_vzNa{e7)@05)D$jA?kUpGV0#_9n)K zHOBsm{4|eO);t}Fs2$t?xG#|kc!1fP*vBBgBX66%BE!|`1BOM`Fyr85?9TZ-u`xkD zkn?H%*}Z8GIrM-<_7wmtGXl`5XeatjG?UF&+Q=SA{{VHW2B4hp`}!Ww78%>}vU`eS zk7Az#EaDAkT-0t=F@QVavE;}Z#dqf71#x3G`~>g6|8dzexv1VevQw$U$9l$ElmXQG zxAn>M0RH6ZGtL?EFXB&IlRSg|5WPUk8)f7^_~^SbG~W99^N8+v;Adj@V)oO9@BQEp zw7&!XGx`A8s$$IGNlG}2ym$6<&Zm1yWw@(7=y^n~F$YK+ z;OE=ssmJK_<(Hq@J$)Xr#Ka*}JQ@%KD1GNFAfkk}F}GCGR@#h5&0&OZKat_~?(J_p^}?RT z9LoMNH>eQ7ixdRO>Cmg^c>2;;HY*hYr`)&QE^LHdoN)_^qZbjDhKf ziI+SjIt-T2#tD@1DuD&*1Redjgb?2~+6eV+V2c8jN5 zBnwR$7N3;}av{W~ng_e@mB};5vHH%_#^@8dT8hTW{if|Nt^1LapRliM-z3VXitM!e zgao=)E^H(d<4Jdeew3=X+D*Hr$Aa(PYwd9n&ZnY|L#baE?H*oHAgp*pcdv16ZDSBo z3`T&VeFTBn{X4&{Uf0%x=>NAw1T4N!E;-?Wyq`pESu%=)91j!z_4?JvFuDEv|1&9zo_wMdWQB>GHBy)Bz4r!DWKp*$q zRvr486dLY}qRc&BL}=pb+Ir2;BL@;M5CHbeFMdjSyQnd>=^+V%iz`aoDLA-I4J6jj1IlT}MjC$r68)#=Tm20j*CWvR;+PWF#tWbnez^Gk~cWEUeMd%+g^7_oH8asFpkSDwl z7*+^_-BV6J90OYdBg}Z(BN9p*Bih0^iB!s-4#Nge5~X;ESd5Cm6F)Ui&P%CeVPd2~Zr83|vG$ho|M*Y;)AIlRzyIILWt*?x-+fW85;8pu zdiFBB4+bReym(R8ArrIr*8#$VmvDT=j1246-VNr6-FM3jLg#jzxU#@wfTxKPlcWnI zVblm2pBW8h1?V=YI5X>>ILzX`F+$_W#M$CeLCIeNP*P}f z5T7ykS;st!`_m6R;;d%}InGbvgqY5q!Cu3A%)z~wv$L2p%y*A6xT3v=HO4*I3&ek7 zpR=FxH~X|WKJ0VCvH@qyOCF-ndd}Q-_RGsxu4-K|&mTYb2m$ss?y+sx&^LwLNB%ec zlownn5r>Mt!2m(D48R|-Pdk({6?TvTJc{4_Ms=`ArE4t@t5gz5YDiF?oM#(@7i2!E+ z>x*YkJPO11Aw@IU^~eOU&YpNj#+S#+i|2(LNF#yNmi?Y*Ggt52@dyjAx^r01ca>V) zf*0Xq?RR&=n3Jg@EK}e2&M1eTar(xL9o&mC$iCA4bCk2RId|UH7|{^OgmR6XgDQzD z1M;!L@kyEBKJ2SX9~iw1uAt8bg-oCKwMM*?pd?I?bUwM-KKXNIwe3|K0#_ntt`WbS;&< zef3=#0oZ414Y)yj%6M?FGgRGuYyhVuW><9^MOb8tvwuJK&Tboxy}nd+Im$|>20FB| ztCiO*UyO`(FQR!(P)bR18_l z35?tN;a}*D7}I-=6BII)*SDp)C-RxJeynr4GfGVFJ5~>5S&&JGFB>Ku`yr2crax5$ zU}4Y_8&$cNu6<*baC_d+sU9&%oxn>DNEN`?4gP|Ll6|k>KR!JLh_}J3R+JNh>b{Wy z6;q>OeSO=+9jC;6i$=%#y~#vR!_dhG^*A;yP(!oK_%*&UELd+sVgX=XM%|F~ZS5^s z-6Zl!FbtDOk2d#)(k55w3~~L&)2B8J!@`0bhbrdDCOLH9E#88z5Y>={{9=5&j%8c= z=4peSHo)mGE9y~AG5oS-GDdDlmk@ldt*4+0UCrCu8h181A zP^ibKWi1bQkj&wUMu8-%z{5_xEMn&TyVAcej}DC3$R*>qvgDJ(Zi56bz2*SXc@XAt z9f~Tla1y-(&-XWPIbn)osV?qaL%}&baPqt(^?OvfCE=bBaHR(VqXYLuaUO%92#|px zMq?KYlGzI8olw4#C7(*0ssuvLA%Q%bW`oMwKER;uqqwo|S zMGAthF%VCPCt<-vAgEeI#IZAWv~5Lzv(_HHFrE<7d+A!4U3sO<&UooWr5l-S4sX0#US7Rc4w+8|`k#n3Z^8^B!CgXoJ{Ca{6{C1yK4I)<>@jai7;%H%6r^G{YgI{Pn$l8ALYo& zKxqs8Wj~~J9TVk_jm5(#?5UL~$Tv<^|u`*mT1M`Skpw z%@~ItD=p)(&d-%d2VR7dya2%%Y(Mzud)AjPgOA{W<}UK*ypjRb&Iii;acGUGT5Lq(UxCTQE#u|^PS(hwG17Qz$0!5Ej zVDHT|6O0FMs`9|oTHcX$vSs&`z7d47fPcpN(StiWW5xJCKdzN2=o@kvzfD2!bgo*d zIGt-_$#gh?^j+k4rdN%vW4xn8BJ#-S`%1+&Y}t&pQS!H0z%b-Sr82a_(eA#t`jj zK8WIF0RLHc<2oaA;M<_KzC|NlC@uG*3PBAjWzw2byB-ZMEl0k|AV;n1d?&PS0_MHP zZhmN<J|Pufkkt)HgG0T3lFKoDPB&FbK+0hs|Eww1_&gS9_4!}$}{1SPmfD_;8EInxkuW| z4a$o&_f!fg4K^rvlaP|ML|ij+3z zA8%=aTo$nI=QdX3W7`unKF}5gjCLu+Sc|^6m-Dqi!fMtKJv1ZL=-&n zHQ4Ldgl7JdC-YEV0_sY-`Fa@di1dDlXhVDnK8G>Kee&FP85MX=a6wEWSpAn_V>Q^dxtRe z?d`H@zwZB;~DTP zAAVHITW^&mn~&z+GeBV=*1#;FW5wq7#_CFx0b4F_+PwVX2j4048*i4M{>7h_U;g=j zZr468cWn$G+I^3&zg9l_(|@b{`0xGO`n^9oMQipLxyN#1O^a#vV!TGNJB(aR~c7f8I+@^2{U&1Wc3aBXDkOK@}#Bo!AtE zRstRXI?onKE9pvu8v?12!hUn}^*24LB#driWyh#M=<|~Y`kQ`mPvp!!vxMQ{k&~^m z*KrTh4Y+%!tJFyp1Q`-0LGzH|c+(F)dA%0<$ad612&+Q{1< zNFEA#56^!ZZdODZToi&&gaER85C#xMW2~;$C_fwuc;SieJyC_DjYa1&jFtgTAUpkRC(0exy?&N5&QoAJXL>J@DuT)(kp@ zm(lbT$2Z^lz$+P;afR{UR+|PrFWLGr@Zd~%7~r2hp0(%o?q}j8di$LZozueFBGSOB z6V1wV3<@hNIuBmPS|Kfj^fnpERkGNxmHfwVXZ+7slFR1YUVLp`yjC{PU2MPrZF%N= z36L;5%<7H{I4e$RBK$&3B{h+hN2YJ!o5dFUg$4w>o zRz>P=m0#kg)YQv3Ni#@qc6(L@=}k5r;F}Cy2kx7Da00$7W~f5y?pdK|HYd*L%nFpu zCzgtA%g{f*KB-~FFicDZSLM_UI;B(+{<6@&G;oAGhO&r+__#{}m|WC^F*Q*P9Z^M~ zuQ5Pa?Zsk;r!O~rwYJ2Fp7NcsPOtIQ`=tCcvtu%GyD*rs8HM_JP+Ex3go|M8g?)-e zXY%`Ga+coW*2pYccuART1|jsxrqS-#Gi25@fxhEfU$v3BuPn%_8d*x4L}SD$b&_11 zqP2J96ry+Xt8(>w3^mfKyLNidv6<;=H@dk~ds8%tgEJ*UvD)61&8T0Pw5i>u)aQ+3 z-|pVdsFS1%f$|HaG@Ut@)-ru1>)5}52iTvoyfvA&<%+A3dt@mmiyMR6P4OMtS(VSY90yb}h@;;~7E zKaCFL;iJG3)`^lp=(>A#pUX?*UR@9M=RVvIV2Lt>A%yqwmho(4z<1~)VUD+NyH`aa zx-+k$@ZyDs=%XakPo+gzn?C)`&m;eRUHwe21J7I!9tQ3!HtMJDaY|)2YZDV$fs%(( z>x$}cE1v?&#!TVMZlplr#HfI0lK$fb{l@yPd{juOmR2M~8EEXDDBS>SzDLvqiXMuy zawXw77>YbJu5izMF4!1hVDNDJ^YUyFYM=xbC-~{xZm& z+&eQ@uKs$>iQV7dkFTuKAY#vTSPcmZIWGY7nHgDg*U+x-^eMWT%j9%8> zLx7Am^0JN7WdoWwY^^_e{;YiY*{{m38I2wdOXV{IM8Dp;R6hQ9|L5g@`A`0L<;=Ou zwsu~WFFyZ`xEb(@GuOQA=z+$v=2**BV+)@B=-^XS(&4zi(Tv?4d!#sdBTn0gt zHS_u~%3ETLb27j~0sRBzE1y%nZ-C*U!jspW^e$3Y#EVha*dTBODWOWWs*I~wC)u`t z3N7D#srA5f*&o$}L4be=37s(gJB!hPhCG-jY<%{r&Tj!ljI z8vVw4bWVk389}MsK=cpi25Di)AkzdGEA}=|4_mHa6jpim78zb1d$@mwhqI4(M44A< z0I#(X9=7X|74ur$7tbEMky-2Pu$F4{(#Z3P4pnuz zVH)1w>s58HjL*-SQFiOX^=c$2o!+&(^E~y)is=&DEFhL%moKTKo~~(Dg?IrOG;k8_ zZ3=6s)n%fORwVH^NdI4u<5;~Y)`cH zq-=Zqv4CHz0b7L z!>0pZ5-?3>^7Qwy5+c-r$Y+GzXvl?;fVoV~Wi4g0wPo_?p$mKI7Rk_yGDN$OLeRH8 zX$wkScx?_;sdchzfWBKBi4#k0OLU|5dmMXfRu$JN16vjh5@l$6T$#Rq`nHs<7qR0w zF372Cz*rUyEIFCOax_d@7DHKu(l63!pJQzMnOMq#v7~jrlgWZ4O6hLD@O$0$)WyY7 z=x6gdQLb~NXjvVlZPc*|%}4Vn4`u}L5?M7T%nRI1kcx*Lt3Hes?)xrd_t~6A;8!`emiYNfs`_8zY})K=O`+G?Yw`Pdzm-xwPhaNjR4yy zslWQgPwSyBaz%3bO@Yleq0D*h>-)9lbmsWt(Gmv%z{}+Hr2%YtL89~@GC{G&@Br}5 z>Ih$a{%hUGJ@uYT=(V6P={lkv0NWTt_I7uJXm1t~5ZeJV&PkCc8UW8FdHHE{0HCWf zg3A1@3&?t1@(a#4V2W96>+1J$q*tIAyFtgf0qFZLfBq9i2+ZZ#Tq$Yvb}oSp&!aba zA_oi&C{TC@bNo>rlmB6mQR2M3JVX^>#9$8H3lG+EDDxhu?VpXk>77R4Wc75Xn>smDmbk zhdhatz{`vfi9|qYc5Vr;W6c=JQ>dy3$C?9xKJHEXkdsOl+6|70Jb;P%_Sa>!G2ApG zz$N>83*tos{WmGuW+4B_xO^__k0^6ycQBY%3T5?0o<$~I2nkJAdNs_-=Ub!W^JpQAGbX(Piu75rS8b;_rkiX z4*rkQn1FtGCh+#t2JU}EJNEq9)(=B*#gI!Wc=~zfu7P6%T2IW|e;=?z9y}Zs7=Ua( z?jPg*UoEfN`hVR3;L9Tee_#E!JS4o``f_V!y?kZkzyHDa%D?-M|7rPyzw^gs$)11f z*5mSvpZ`=q%gH?btU5|E#bF2nNH<6_&lp#Xbj)Ga*Crv`v!$5p`)-(`qKL_r`}iJo z5Gg>OXPjj`*^>YPn~@fP(SUseBLHAy7L`o^M_Lc0Ed|F1;F`}^&j1(FaajNC7kEHL zQW=I;@stGdsn+mN6p3`8s3#|sg%gAdCRu`&Jp~{PKq6X(3IL*^~eo0_Rre7A*>yq5DXGLV_V~WsQxlGer9>i`*+^`So@rVT+caY&q^??)Urqqc-{Jp zF_rs1ec}d%{U9?OM2>r)`(C|zqg=B#FeWOkXmi7v@K<#vs4@%@6xK(8ID`GoSD$N+ zi_Gr$uo}f@;?cRJ2oL6FR;ytDCFh>;5V@<(7v<^OIS(fQ(O?)c=r`x>;h}S7FsDBG z?vK?rsxdr~SAR!_bhZa%&T~He@Ox$1*6ZUa??~GS`&SB0R62s3lKGWY1CX1OE_xYY z`iIeiEyybmJkT7IAvhv&STm$OIFX?n2*hzSj{2}eO1y6Inf@OAFT zxX$O(2!LK+S_J)zIu~Tkkp4BTc&z)5dH%=w@!>F@&>@Gifa?OYcs-X+^FM(*yZcDp zt(y4s+Afm7H~YwUFGELYts`I@|HkS6|9xH!(@k^g>*vkuUAp%s4N1Guw1Y!;{k+Gb ziRH`mFdk0yzseyd-{k%YNlL}(aq~tTxY))HxYu#%j8iqjec`$?2JvKeVN9%9N{7G0 zDA6JS`bO0Wt~ZA=WjORlwCCoxJHbS)!M5aT$GQfTZcO|8FG#-hR+Q2`uNe>$#1E%f zh}vX6J@TLvR5c*f-DWHePS$RZ-N^TgHk#5I7Uo@_I{C&|hQ-aI4tW_CR(02k>16BW z*IhE4oUVQU$|RK{$7T2(3#nb(CjOWk=Db=I)5P-0fw?N8q@mk|-Bb1C{?{rgHR;LF+P(P7 zzAPF9Ao4*;bf)~7fZuX>yePXvm?7^G3du{1?*_b3AW%#kP@a{dBqn9^NT3LkHb4lg z!c|Ri@koF@fn?+-7T{q}J}6A<5s=)5!5e-{XfCxp!gNIC|e6M)TY&}`{gWKFdDJ7D^Lt0m**b_d=Sgo747^4CAMLrl4zBXfo zlRNE6kt0_h#eF{a)CFWJ27CyamY2$xpMP4OM#(4?_gw=J7;%yd0R?l-6luPT7n6D6 zrLb;Gsdmq3mOqM8{Uz?JPf|tfA6h&saQcmX1Pmw^Ld;2)J zHUV}( zCsX+byaLwJ9qaQ8Q$U}aC+pCZ&@-lFpSQ7l)7pRDzWlzm7oLD!N>bVVUjUlyeaHfd z3Roko)_z`Gx4D1Lyq1(PwD!pufU&~H%`{x{K8k774*XgEklwAHal=3b^>YX0Tw{Pi zh5?jKMGMUR^Tkav%BJbP1o`|7Ph2}PN;5KGfPm)!GCng59awvpZH^4#@m3`c`}v3z z4M@5muRHdd+au{PL_w^rmQ7QtHx0X#@q=nI_rS_;waLj zXxl+l4WZ49y*&0uP8=wsY((e)jxZho@R^rNV;P+XLV%peDG}uzX)Xfxnah-}V{c;J zG0)gzGBxNxUjLT@Kumgh)gw(oBz{LuuBy88LE6c_%FkELn1(@1ydvA~<)?4l7qHB{ zmq#@S1?i86+#f}m)_r{jsUIQka7Ex1M^>c1C`Dm!w^r0hr=AR^+A|Eyt_Pr#7K<=-2?jfetwFcw?i@RcT_fn?pd%L??5oKAjyBP$8&x3G| zpD!Z1VM&IyLGv}MsL)PQ3iyni_av$l>FKIhI6)SS^LkWoxM`kuudLv?`&^F^l<~}x zs!ETr^^b@8#ful}x2F+2_cDYmrP)%ARKWOvfhLIwMT~3xR2yU%S>+O(e5r@JJ4ud;B96;hq8MBw>}ows@5Z&NjFM z0P~)X@Wi<)Odc(=V_6Xi$cwZE`bw38BzI+QaShb~m{a0lurrr(hY?{Z9f$S!!H1tH zvc}Uw?rS}Hr3(k`oD&cpVwA%uO$C_aTAFasm~%hovJ6$Er95h+=%8e;d^qLILLBGYefyrYyV&lnx5LQb^L;lXY?a6-*3ol>8AyGj|3L$f@DBf3X8 z9gb@MjrNsx?|T2S1Nk!t6Acn|Dt9Wrf3x)eDv1-|dO;$SwuO$JZRotTWoQG`H@Sg_ z`gdO|jBNxz~K?S5?#CzGiG z$i?@{$#(P;s^WituMa(`Qe3b!ot&)wqFk6frNe}i^one9*QSZo`{$S8v~6*ES?)s9 zIn{<}JZV*IKRYqStt#6|Ht96OOzC8s8{)9A^>mAEV)<+L=w?c8U1R^%I^DaA7iV5g zbm?|i#r6-~h9yDfK3O_y2Y#8wdz1t$zOnK5z@bWIfUb*r02cp;cfP3Q+IrCcyZ-Kt z1UY0VAAm@KAqVYoCm3Gy)BgPP;XJrv$9@P47@svbpN zDoO^*BHm~>0$f)6lTF_hVvmYIfx_S*qltZwoOwJl^m87b*-QsOiA44(Gok_T?qh^W zZy|u;AVN54=cejBuZm0%00CeH=rChYfSGiRZ`jTqkbw-DA2P_Gz>9ouU;XzG z;Y+nT1>QPj?_^nC`l-2iUU<&22)9Vd2rn;&E55_rd*%8~&wI8>cmIxg z*vtrWo6vPrl4mBv^Y+Y}W>mRks~jBr;Ym!skbOnRcq+mA z8Be{?8VQ3ib4ffTQDSs15BzzpCjbijoA^wYAx=G%7yC4`H9?FBPyjBz;=vC0U0x`?d(_dfp${E zo<7hHyz5UNJG=jOkc=i#Xen-B;2xwjyboKytrZN7$~!mN@Z9BPk6g>NF`4h}9`H7> z*YIq-$R6(hT$=BKtH{t;%&*2b7C!04kUk* z2_PGw&;FcM3(~-YjAdZ9enT?IcOL|~A#Hb~$a<7pRS+nth+&_o4c{)7+%Pr_ zY9|~EspQE~|>op7t3c8yHiUoAUyQ@GnyD z@aRL%=xG#>bf(hptX8uf1`O63b5z8E*7o%JdqhKQbx{Q*b)(GZKh1g4>rso>u>P*T z@|J#ryK4A4g8r~aSee8gF=)^sOC}jaLBsvPsw7Y1c zn|JN_o2>tz-p_IB^iA2E_Yk-|u*e@H!w_7Rw)YhRQbC$5R(? z8-Vcy*mop4EXuoX6&;8iYGZp#L_@oEERqbO4RFb87Zy5o-cxuY$_NS&FF>T& z_yK%`eH%ywe4*5Q@%gVbm}y;C$)+<7q)>n}3_jcF<@;tHNdPIAJ(DnTfb@=PZ7)?Y z?Vh}|0>-?L$6aX$p=?CQ1a474QDOnNC`>3sD7Gk0D1ML_qNr^s4ELz~>K8xr@OmN~ zq~z{L8NN&6JOIF>piwF?OH!fC5durU0spj(y7$y0PGg9aL{lPBvXl~_`(21odG~a~ zu%G+^X$W8(GRnd|>X*yUe)`YV_B=dA01W*PDl&)g`W1e6j!5HK9 z&kc~nzhH_Ip`GNB=FtWLApJ#=Lm9=>#q*Faa^rVo&s=*b)@d){q#WaplpLWE! zEl_R%CsPt;0@3beqzoa~%5xuHQ?Xnl8v`0kLHsB-ag_mFmlq0oVeFz^G+g1O5AWA% z1pusLyANTo$O7I4$O_+m_qB54`l`*T(d6>5-1*{e`RSkiSLObvKQE&xyQ^jZxnS~p z4G*Ku#dT9!hcA9r#uw(rq}&@|7H=U0YVdW~9EBL~3D6?U|DAftT|fbMB7 zvO^z4)eP2khP69E;g41TO(!15K71wapvJokF3u=)cpp=W;+0mS|! zW0X>0j+<8%V7THX4)NkDGGXSGSOCxp^WWc%RG7oqm)0a_fGYcvWNUu|7}=LE+5U$y zmPi5iANE^v-dS_(WtmoxsWXfP=|AN8^ScuzKJqFY2A+97`*|}^I%fk$HumpxDhIl+ zyma=jr$OlHM4+uqBkLJs_SH zAd-VIX0JFT&B*5Px_u7-t_T6uqW3BaNo0#*GNXtHz2{tIPCR(v6$u!_&5d)d6zu3@ zg9o1dn|%v_t!f10Jj%{`r5MJMuz1c{Lf%D+7l1e;_E-l*Ykg!!7S2`nVg4~sSVly3 ziG#vk%qd6C$ij<5q};jCqsGn{g1WZfAh^3GH7R71#Mr7v+U#V{80NNM)9XI z26Q7|3qZS78kjPE!AUdMI-cuq)&%D<5f6C& z)0mNY|L*zsRGm~3Pji@5Wh`{7#rHJ7tPfYNziHd!vR6|{y>BGl$aIX|mrY*`EEUh{ z&`2mORr!N7cbF=*u8bE4U6FFyDa*r9y7u=vano{J^l{a77kR`!fVH{-gf zjZK0unfrAMGrY41O{_O1*IvuX=_dG47e+4>T{V`vTw>trv-IEA zx0>_cPk=GUp_@3mvEM!0($Iz48Q2`Q(7d^3K6a9zI|T@9VvL-?%3Zg~}8Hlo6CQK1WHUdtaFcKM!YK zJeOwywh3QFk*1D4-Y0;$&sX;Vp{$|UpqzdE)o-Opt%xk{Pzoo{n^Iat`htg_d#2Yx zb+rd4y#thZp6MWx#@(pZ?Ys!1z%GUXj0SuUS&}!)bz+MqoIHRI_ZtP2jy><5Q29uq z0AQo+6QRIzL0L!s7%xyd`G>&+AcgDzE{s2I#_)ks2*6RTY{KI0Gv)#95aal|m!*`K z&H4(cX6B-lQerB}q393t0Qhka6jr`}A<_`m4aBc0qT-U;ye(E}FPW90%*lV@d9)re zfj5>^3RvPdOu1HwwaqieHVNU9Ri62dCy#MliqKvF$y;x~Z@}RF^8R~fIPpRa!q~UJ zdQm?6i(lHmKPyko_%b7p*xCRA;H#vdK1Lz-!#rq{>ohHu%QZ}s3lrP)?{1dus<9p%a)iQhgf{KUUo2hs4)1w>cM9{vexr_lCclcw+#m+%gRS~ za!`O_me53&FSoZfZ<>eDx!p_cKF?NV2 z;y`OngMaEodF(yAK?=AM)mG9tk?Az7DfTAH zfwFGMU(dF%cDN_cX8m!dB=3kv?5rtlpM8XTvsNBFxTo{P`>-P;29g_7CG=v5{sdQeeCYtTeT|0kN?t@lCSnRW%)3quum%GAh-sUW(OIi%~{S5su9p13=~i7p2%Z4)(DXzjI~l8KK!2K zsvLaf*YDJ5jEus7>HoDi-YvJx;DFJb_HkBnme9t%xE~b=XpgEfP-b?1EF%Z=mFGI4 z@CTan7+WYMxf90nzU1*c?|r0_zZgQEN0i7~8m!TM>^|?9p#h^hGJvGj4SoBPr=mlw zeHU#WDeV6N)gGJ>7Mi5L-Q*|$$on?xzClH`iP4#c}|4z>zPPysW ztM}Q!q4jMj3z6LwUzPTRM3xCH`rD1^k?(Z=v{x)*(V;`D4FZ;lG2=u>dI3#Ad_2Bn zqg>vlELfkSEo}mI$S475gTskV(|}RN2Cs&Nw0k|{($&|;9u#$mx}PDFK$yB%GGD&C zdDp-I-_yF2eYeJ57X2j7RZ>odfQ)TYoUj5+p5K0UM0}+i-X?=?t(9)j#u`|@IA_KQ z7rGm#EmBm2cTE=M6XOz-UuCax;=z~P(Wy~C#x&JI3M?|(Ocfzg_Efk<%hkonD5pUm1N%d>Aj=Fxd z{Gqz;YX3{=9@!ZphGA(~J9D-Y@1*#7=FYZxS{~gl|FYNOVORICc#lK$swvtqDWi~} zI0E$e2_U63(KYkZp(F!zmLk0JAo9jh)&P5eX%y67{qkoKzP~1=n*$kTg?wvyj_fJdA;^07SQC@zHNtIj=Pfu^8kSRaZYikDdJ?YbK` zUzdWYkbAN4uhnpW+M?Szk*M$jyk#*brcVn<04(P|UK<^u8IYyn4cUUCVPY-Y~b2At?(yhdFf^#8@6G zdlVtpn>Bqwo-aN3yg)o&qoeSe%?1x)mZ9ayjX?(yCb|*jEEQh6ekLLr&dGp*f-R5f z!G1NUV6<9^@9hP!Vg&gvU@XI^Yudm@s|o->4Jfx}^_ zv`FuOxS_oYA`L_Qv~qnXIU$LhF3Sk`vvj={>+7AHzEn_ z2MOYM~rr&}i*AN3HJ-I1OyBt!-x)P@6yeSQ zspBxPuBde1rsfGzKkS{#m4EikyrwTSegMMf;aznyxr-vf9LM8IMn}Jq2Y?r28ta7q zCh4Nea;j>_o*R30zRm>ZWOuST_tsnQ3j||mqz#_3u@5(TMVdUY%d$o)% z@6mP0gSA1F9&O-k#xSQn#l{d&FXM(8f9{(Rf#?P=DZDEAQikvt!?B&C7@s+-IqR6G zBB4Ca${)@((1U;JW8>7jpHXs)5dn*afb*AMwKZ^|eTFVK^QgxI+oO-iS@#<^yZXN)EKz_gEkmNhf^B$0D!75TZ&^E@&S;uZDi->LL zzE!#gCm3o%>LylQq+^f8hpCWC_vYAUPo1!*2)+!xF`~6&LIhYBPx)OrEInBDTaV+U zjWyiow2AHaYX*ceoSZvjZb;+2iY_|PPyfX4Q(1pVP@b48!oPpO=2TPNp}6 z*cYu%i{D>ZpnVG$&xXa%TJp4C(f_5YyO&cXTtiHym_CG5tMDT13qVV4&cf zk3>XT&sOR@1t0AD{u7XThn&;WXn2Xg2G zD&Y>yW>U5`)ZblsFVcl`RXRLJYtY}fO~yv^5@>FVGTGaZj50qdrs*=i>R=* zL*G+5Kcn=84S#QCS<3E{r(R2(`HZ*p!}o8LtCyD*Wv}^ax7mGS9@Jl$BK)gglp`}# z9hl;O_SvH{+V@E-xM=r3vui$alhbgxD`iD_1aiy`fKZY}5@m285P!yVmu)R!BvVPZFgEc@0!ZdGL=!bzian?|DSQo=3*;JKoTFq`_ReYl~da8NAoIjLLG2ikHrQhFecKL?y1h{s^NM)S}nkI0w@pkLL# zbxqs?SM|O0=&GzC)m(y9E{!9e4mDS#1OS%YdcGq^Drh@uq*TZ(t-!hyHfI>~?Y;LuQJe4GH*Y;< z6U~UpDo$@cbMLx20K8=2JsDw0FL6-6ULz=|qJ!LNGo}78tR|1oVGyP+tBl|8i64RU zn7Ml(Zi-Ef34=&v5aZ66F&3PQ(r=>7qM~x(^MMz{=JNgfw-olznRpaLwps2Ld8Co} zwAZ3zB-1Dt^&?*A&R;3#E?jSplU6pq?7n=ay)j*%RU58qSW`9o%1+G^Fd;DSwdK@U zyS6NY7%D1tUNr@~O+U1No)@sFT}J>TRdbtF0-+FeT<2 ztHhg8piPR91K;VR16n-{XyL>U4u$1vo5h4OvMc2CVCgZar($ zsT zV+yo7FqYCw-~~p2XAdTaTz|0jwQd)U1O49|swk8^E9(wB8R3e;_}?`}0b)8V_4gq( zv%?b48Q=^6hPM_^y7Jsjk^arE{?ZwDm0x*Qid%ZrxF5>8AD3K%v6(3daKL5jG?X1?<1h&$fliqDcmG)&m zFKwhhj0Ft+C_m)h;(g;fg=)(4b15PhJW7I82{D4No{@5g5`O2-S5manzyQd>lZavu zcLK6?(8}!a0AXcf%@yA>|+dp1ssJ^CH6u|ayI?rYG^nYDISC>fA!_(^2i_$ zg(Qop>8XGhAZ%4&eyc_Vh!era2YC|KOG!wK7|c6)9j#3mW|HyzY!FY1H1OQHYK%a} zcW!?ZrP+=vx5C+TKCe8V`$(YU+}ZPP*a`Ok9NV*J&k88r4#Ge3N@t<4xq-pUCoV^o z2fe@M1aOTD1?G8W7+yn+{O{mg<}B+I-h zrABkwz1fN+iOL|!wZXn~5CojQzs|KMF>mK{_saTnm-U{A=*j!IRq&idyPjP9Muw`>`&*mwiGJv<^Oz*AFATP zw)WgnEb3=vNMVlmkvrT6850)Hx=`-8txej)I%QorPr-f#($$+jcz!m0BkY|hs%Mew z4(McEiR=)*29uF9$}j1AOEa&)0jVh2h)<1~sXeXX%KzHWkdqmMp=M3w3406DC-~Wz2r?eo3OYNnU zl+-nzhO!dXN#12nIP#~i;{X-T9AG7y$?Ro{C+4>K+>W-GAs=l+>?@q zoF$pd4`}yDts|ZJtR?328*ja*dtm%mx}bfRxrw}zM+W1C3@x^n>7&RfBMpr8=oK2i z8>wD*>v~*@a)C+QiZN+V?Y<*f97W29y#MnfrA<%VI38DUGmcYLm-k@$pH=T|gDb;CjZDRSHZ&|!dhlNVE@g6`s?N1O z)NOQVx?tT~|5pF++I8~Pz8eEn(m~h8xAT6NPX7-4hsbVVv@fDYspzKiZi0Umbf#V}Hvq6hH!)28OuP^d{5J zWjjuLf2dZQ(u6e?ir+pbd`y&TiR&=fM6jj5e>YaW;7gh2Ni=cQp{tgYZnEdX>F-m9 z6YIIuAuWrc9`Q*V+-S>CI^wwnoS8ygoODfCwOv(?6G4`-Gv^G`1zWd~I>9GavvNYV zeFsnBNvM4vdh1Sow>Gde;dK+OTex)}>18{fw0JIzM*nL2W4~4Zj@Lpr+9HeLsMD9f z`@^KaMV*r|5X&osM>>H{z88f*%oB09ynO!nceSY+1BTOgJ-x&Pef6fU$w+|o_@Wfz zEkH3tk-%F*JI_QNWI&Kg=*a+ZULok8efkS2XUcmcG*&?RtQ!I%FZ{FL{!+>}_W~dS z&NGyq`{8jXcM&#ulrXMC@#OO}p;!Q(A3bvK1Y^yCOb9Cq5y}E>N4dZtfMEiK_&CA% z+)1WX`jC=>0Eg(`^YE^v?C|~(c6df?`pzSO*PHxrsuXxRB5xmt2^1vmcf%CCypA!& zU!UoWw?FR%YjR1yG)i)fH&62kLMVZ`up$NHnFjxbvBl2L~;S=NTVJuelB z5rLny8^FU@(>@f(hc6@ne20vAFZFk1h5{+5gR}2zCz4ld)W$xK zeECp9q36JhKoB?%U^J^EH9znNkao<%s9m=8hti0A)57 zb5arv$?uzdc3Agz&9;621!HaV@6ZfQn+7Vj-hHiXy>_WAZS=dIQxbf2`=EUB#aEhl za8RK9Gbd%tw2PKkUB;AMR0@ub0fw=I1J4g$jj>h)350m8wa)X#J~YmqkHFJK_O)Lm zH%StPWobT-I5{sMNDeqtlIxhafV1QeK=#Tzwzb8)K8(_X?3pS%$vQYbuKWPB2IFax z5blRZo%tk^zSs*fYB5iBYRt!)dZf~TkVD!tF!DhrhoOUf{uiXSD1~Jwp7*jOu2_mv zmsz!Cru}X7z3SE1$d z>9;*EL&$#0i~Wnep6983LA=WMFdFb)650W>c|K7!ntQPyGgbh{*Is*D`|EZ@Ydnuq zfJ(oB{oeKzPcI@hV_%`{RAa#yL~6z3$M@?qW&t)e??*RwF=lxE*)P~v*q4Y9*i#h9 zvC^9uJK9CN;VIz$PYev><>tPCX3iqcGZ~6U9|Hg}@4>duewh^$IEU84s6@2HYp=gk zUNd711|3Kby$zm1N7PwHIl-BNkpknc0Q(d7=x6%e81s|J1X3OV;#r39ILH~@(C+D9 z;lk4g9+bdKnxC0&Kq*BGC#1S58o|a#>%o9J zdd;S)2E1l->7_~?BI`kO)hn@J;Kgtu@<*Fb4<6iBWrTfkx%k>Wj3^D)BaSBEK6Rkp zIr43Wf&M$2=Pt?f?}UG)mcGQJ{F6OvKTlzj#QFMj{&GAH={d~*In>omSr3_#+piZqt z;NCZ?n?Pd^hVzTxGB_u!J!2rN6igk!G8J+u&3H7Kd(+=f)O9Sq6|8+;!#n)F7G2Qw zZ!F!~7~6xo_ovYgrA&63Qfhcf?Lf0gPC`3czrJNds1qu$Xx6Z@5%+nY0^!z zp)3%2#$Hs~7xiENPc`IJYiM3pawBs<2bfqdy@i_WZxpyk56t_y{q%PN{9Yc5hhx4_ zWUgGfq2Jv?zaAd?^CJ9v>+KKaEheY)u6bO&oRk{^6e7OnK<;7yDVYCJBA0{F{5Znu zm3JQb{$dg)B-E5d6af?tfou^~ZRuY3#qfL2!}^X6gPdzmZBf*~)_L^_Zw?;U-6+q- z{XH-KoOy}PE4qNF0u&?>-Pw0`cAWGN;{{4|ma*fRGAcwaVS+-h4}IB7^#|_?O5uxO^*;_Gq--l=i*m|m4@~K#e<(I0?6Ui1xa`^Bb%0%Z!+hd(`%ud{u11LeT9kdnv(HQ6{Wf<{ z=yt<9B{uTnb=*;=xd%K6N=bmT!Q@BoJxV-)g;WiNaYM9a-vgNJN40~jqpcEdu>T8C zIeR#5mm$Pt&3r<^bb`hmdDgNvJM&AW8R1FTB}(GhYr`LTWC-6i3454%@CIbAGxMK0 zdHtHnY734GYs1=fJy2@pF~dTV8hA3jl;Eg8{SFU%Y2ev?a$Hts~1``&o<4H+ReKiqf=j}U1unBJPwR8Xqbxj!{<`|ZS-1W2xksr9oO?7B>zmh-(gQxw zd{wT$d9a_!6Mm@uO(eBc>WH)Vd*A;9?Mr}Ncmz264ugwiIk;eG6YYhA=aF*#A1LHs zXVH>ZMPNLh*k^d)UpGU+(@4c~rhoUotIVUd3*gND|IT|Kmv6rMLSUW92N_^&&(dBP z1erufFjxR~%J@Nk%e^=s#9d(H|B9Vcq#E!nRce8c2jk@WM%5)4pN~HNfnM;&%K|2@c` z=uN-F8Isp+oV}pDe~gFja2ca?bCE&pP?b8W))z=8evZxi-$l)bu2-ME0D0o8Ro_h& zxbMp%8X2cwZm-nc|5pG1SJ3~{%U`O|X3~!n12u+X-g(u>HcSrlhQ5Dg0^C!6Cn~-L zidd*D&_j$0=u6kGdefnkTOF&~+3D}Y!lW7dIoul#QWQ;)HTq*1nrUL}g_Ofl=dDj7 zF3vK56QQ;*O@V9N_2fV-Oy0>2JK!9R8dKyR&fS`wL!-Eul0tKN)`>6?$O#Ki=?A0= zy)1b8EAncSs%v`~CT7FfNpG^6yXVF{_;#X+4oOZ2axC?d&Qg;TvjNf48#Pi6v5`a< zP74{D8aq-bb@?Yco{Ul{iZ7dNM&3Slv)s9LGGVAd?Q zlY=9_BTszDdMH`LG@iNG!E$1PjHznpaJ7wOoQ&r5+2q{;NY0eb(yLoj((ci{FXK@9 z_SfQ}xcFJtoTo$}P0EmZ4(TM0d)p3c839a@q)z>^Ja89ly>+ofpY7+jjbBsKwMB3T4)>N7=^%#r+6Jq&;3od?{*+ z7kQ@7Z8?X)Wiu=g=1=-S=8pryB^y)J#0zltoPa&;2dJYA?*udVQW#QdS^_BnQHBdr zGS#*=;JY8;%9*2ycLzn3z5{MOEOVoL@%g7xEa^vbEQt6nJRKakj*RGe1 zb(^bamlY`hzF0AIK0|DVxeg!g+rQ`LI(||1Z2lfuf0k`bM#u}|tV4+9Ss zUU|Ufo|nv1Ln1ZEH?b%ID8~OaUC+-WligtJZ!km90iouEY+D--32(=c0^7LV;3y(b zASxX2yc?Q7IQEJGhdf~BDFUP?=;z!I4-67V2I%(<1iK>dKpzDj8S|d+gkwbN9Oty&F|0@brxod5vMTHzV{@m}&5Bw%r_i!(KvrXEnEr(0n2G5 zxKqPxF)J6ath6EyyFv>FL zOPoi!_W+cgSkOxV5+y;~*cTMmAKv>+C zjc}fjKd+p8a{U!i;iQKcERdUwem3rm4+h7}S6-3aZ`&Eb^%xc5WH}c+IBAS}_2!%M z{6k{O*`gGUm)kze0rc>$NJh)$yWjhL8KS=a`ZLLgeG(&)&zHU6ZCDcc2b6Q}F%EeC zRjzUfeh>Dys9L}|z`2)^63jEDbw!F7vU=y8k2Ln|^W^)xq09Gb8O3Z(aW7TBP-MXU zNG+RJE(hi_1{xd&ob4FCSzFG}vnj)nQm{x3uzN9g*ys7jzL@Kcb;L7SXUr>7#2y*= z_tJ{ZxR)|Mj*1GYV4vpfI8FcQ^&&Z2%+Ye?)wi`Zk|x8xk&6D|;hrin`MrAmf9L>B zPfQ=%SZSJS12gqM#r5wg$nCyX706Rk0!rzTSEUoEOZ9K{|MIV*|4;i|ri0f>y3cE` zMxC1X-jML(u$0~iF;tmN^#8H|%ukN**cvr18Y}wdW`AN{jh$FdVCgzj7VbIJi1}eM zK4seWNt8<)%SH;BGS|J)t7+OX`~X9 zg`9ccbx|~I81uW%s}t4~@3^ys^b@NW)Z(+VA2A6|3CHP)6{?8BqSv0DKy)lqM#^qq zX^V6`nMP11M0B0hhQ$Hz7jBbjDW@;u`T+l4E}!1|RJu7)y7ARJLqq2+;HR|jlrl*N zS&%`^t*UdUuHI-*DJM;d{a#Y%*r}hF4&P7i+r-+;1rkqb5 zbt&JQXE&u-QFQ_3idxkIQPy@8QIs>yCr5gpD{vAenGXOZ=EzK7*rU5qSqe@RIU0|h>#-VI0TH*Y8tyHdO z9En)q-hg2*E4F2SKbL`rb|j}jhJmBFDvv$NW^(VWRzMj0CgJrMUoZ%n$M?)KrO{T( z8NepWJ=ObOng^Wle|s+M{7XclFxLqsXHA-HXThB@VwA8R@;{|tRU+Gueix@Zfjznlo}h)nT_wU$zo+|ql}v-hoSgpIt(HzJhq2yZ||3#J!i~+ z0)d;&O`<4v_q@*hZV+7_2goL*7SQL0v00EXdNk5;cy@E`W%)GZ=Jmk`k@@jcV9ol& znqyuu)(&>pYR<68-Qe)x*?95~52D_E@u(Px?bbXEYDK&nSM}cEkrT2p|J+;tT@()VlEZpf&~+)FY|_S!6&>12Wa z92p2r7KG7juZy;5Zd1`B_Mlf^eO=K5$eiaA2G8|-`rR4!-8i-E>0M>;8t#z_#w)|M zRbc_p9sr*2rlHcwMoUQ+_NDO7VwB^%fNX(q`!3N-B002qOk@E2h)O$B3B;ZuLxi;j z`6ClN+)B$4OTL#M8UJimoPq(;m>9Wo*<`+>(0C3JSUhE~v z&LfISkiL4v79MlXMCJyc0pL9fY+dpt5LS8AG#sY+DIApy$ft2VTS-j_029DQf=cfWn#v6!Isu{pVCi&mZ!1JO;~iFf{l@Jj7( z7%wJ6+0Xv7QraFd0jzbO`dy5cxxlN4CwVgcoXWpTzloHEg;8D9C88UK zwm^r)QJ9Qh-Pr0x%w8-^tVQEW|J*j8{L-OWD*H2{wR0+qki(FBcgcpgF&VmXpPm3Q zHl@ASq6bP_W`X*r%e%CluDj`k$yKvv%?@r9guF=YlEQpg9^Cod4)nt+w7ippr70=S zY47EWa%Wer-q3TCRU0K`eQiSu-0N?=D}@t90xvmA8z2;A%xm!;Ip8#)uomDNrAH;^ zhEl!W08!(oue|DY zlrc^OctEK~c|eie*xXWRDIg6Vg1yK~UlqaFo|GE$Sv{rUqVn4Dh~TLJ;F8xZ4uNqL zFb1rnSR)q{>tFujC%WG(h@ZHhymHo8MFQAQ6eg5AyyM3aG7h-JtA&>g@RCG+c$!f5 z0j?-VPQK+NY=p`4T;%TI`xg~r{^b|HbwWN^%|};yM22|x8%1z9$UY+vJ%$4mF~T`( zc(f^E31*ZEB9F~xB_iAiHt8?F{H>RZ^6k*WUhUc>Z&Vb4JqOP#_W^{U5PlPV!E=_% zYTko!L&nGy8Jr0Zxza}Lo=Pn+V}QKGgNJA{ZmtB*Yt(~)*T!bB^n1y^^+*{xj@(j| zV!T`|?AhR9y_qwfA*c#BKRAe5^2?sGVPn1=6$Y3`>yeX<2QYKFF$%~?5)m8e-KG6_ zP-O^+eArAIpnrfvz$f#4Z8fS407S{Zw$EVxU$^_LtL)#boX3D=a)Z?$#(q))X8dFQ z1Yz6ElxWEpg@1R~s{up~>pU3r*#(Hk*PcNBZB#foCUxVS8Ee*-%IbLoO*X&EiYWkR z?Bb>JJ71W=UhEVb@0VuC`dsUX^?=OBsuB7gc@*U!#T%irbWYmGcxWCHX`y`mL+v;0 z2^qTXybQDIVK1>ih&0gT&-jz?&sg$3k0e>v_AfF^mBYML*P)_fBBY-013LK5OOvCg zBOGha!B~&n0JO*yPv@GVMJ~7pKA7R>$IWAz;{?ZiigFotOa-)YF~*UcMl~>#i_aOrS_G)#amM2=BUk83 zxgL2Y_aO|O_S`b#Dd`8SQT{#?MnsRGSW?OfMhsHrL|*#np033p!I(0Z^c{K9H%R&L z`qLKXzA7N>ZmW-3rZG7PXoIR+5bh5Mw>@-U(Pt|)^q%WD53ifCRvZmBhRD*-rPc6^ zkLn{)Y&@TR`hm#)9@a<{RceXIFATr0*uD&rA3OsXb`&8qb27v<4u0_Q_w~0@7ckh{ zPnGJ$xERJv$RkP1dK75^IlipJj1nZZfoKT$51u@IsFJy=RaNvX z$27Iplu4|uj}wbi1I;Fbk71QXQwQpUZiJj__YGLJi>O)9_N1{+aK8|mPrI5^X}=Nm zP}3RK=6AIH>t#LqgiqFfxz`p%We$yn-gT`#D&p;Dns)PjmDNPl?cb+q4RLrHq#cGf z@}l{@jMSPKhSVM$%}N`)G>B%dI)jRdb*+`Lvub+hyJK=PcQ+bX_<7dtmO7W-CB9it z;;Sfo!x0+3Ibm0>LQd{BUR0^V#CVnSw7;_%|4G@>|g zy}UJ9=?z|hM;=xfih#mUl?!SFgGXL25F-G})TT!hMA<6R1^|m(Z=UKunTO8-9IVtR zP=xT45`a>wN&?vfj-o}rBVGUj0fn+3s4WZ!gin$SANj=eg)m4ahA>dZ0E;#K`aFB> zBUF@rMfokjGvJ=Fs$^CE{_8J4lhR4ps*#ah8nd@j0*>T4!xN5zMdSc-F|L8DLM+Aj z$w5ZhM%jA#)z@UO`2O3kt+IfwoeP|wd(C9pH$%X-+6PQ2Eg;X+g|mE%WNL1q&?tfX z`&P{XMg5*CEm$5dsSW0xS;_;?dOGtmYAB_Ytf{P|CLnKca zE(Ee|-ieS1ZUwdmMiKED472J5^1Z;eq24(QaZeEed-_k_FE<82=!m>w9mn``qH=A) zzHXt*iwldoxKfSpu|$*@LFSPRF|2U_D*d1YqmevrNU28Y#oG!{5s;v-&MOgwcB&NN z#A8wZlD|uFUM_vzAILbM@Z)SCU|()&{xfJMN`ZWDK$L*krWpPq`Pvx za2m)^;P^n-WjW8AQ)hGX`+wtSk!^gBkFV1GU$W=!z8QNSxVMlwCR$1B@cdjJNs-(o zOIf+{5^@W#k8b(qqkx`y{w@TVI5VRB-qqD*FW9&p;l@TQ0mh433ELp&40 zJAr#rhdg42hz&RgI9o3q@LxF~zjPAF*ncxm3t-JF6^6KzmzL6u21#EMs$lFZBR_+_+NP~S}M1#(^ zqIrTSFP|68hY|OklyeeU(w0a8z0@FpGspm+IS5yk3op%v%y!&(Lw^D9lw##v6-dr~ z5nqA8wHaMyVDd7JGUi=gY5w1R?<1W}sy+Y9eHo0HC(ao5nXR)6oe3i1n{UeF|MJVP zs?^+5H#lqTS(X^y=1#oIa~T8YHKFRr3Pa$z1Ah!DtbN*Me8`LlwimM@0+5cJAqai& z@V@b&I7f>NF?yd~Yp;IvaWMCPQz|kLwQ%U9l`?jDH3TZ{pif}f?{xDAWi~go{ix;GepdKsr5xl1?!0v0?w*OD!n&4 zS4%1ev5!CbN6DMAt(;>gPK+u8)U}aj{_YR|S?^xPz?p7DtX7&gDoVWNsUKl*W}J%b z!QkqpA+5@cU5~y1&jEWNQfPE0I9|JoQjj<3H}TyR_)qU)jo!`RKR9Z_D4@0=kvw~r ze_a+J5YOm6t5sK&Zf9`R)x4DI&juq*&rB^=?hs9Qe@dSlM3wxhj$4{XHseaq_5c4? z{lDu|?OLFN(HkA=KjYLaW2#3ga`Hab^oe0;qSWi;reQTYpqb;;j-vFmH#XG{r!-C9 z=+{(p&zQ=H(uyG^#h@pKi@N4weR`eiX8BYc-lS09dGYmS>P-c$1*Zk?rIDC}mBBWX zI8MY<7EstY>UWJLqwIz9bKBOCN*HzseQY$Mw#9BkO%~0Hk-@>^&~ECK(yDc)o%8m( zl)9hSjJv&ROuZ$k{Jv%b)sauf(kziyC$z;;@#9z;YM|}VZ?N_dYe9FFm3`CNKE`(A zG=1nydTCYu{>(Ae9qc1qS#M2$hNj)IEOJO)X5E3ChGxSJ7QRzwAvR#_yLMNbaVbqr zN|tjon>8gmZ0w#!>@Oy>cFPDb%)G(av7=t@zx?=>UcT0kMn27paNX}=G>D%!754wC z+7=MnwlHXn4}g0j0ba?99$s8|S>$2c^H4vl7rcLXq?f!I0e%+PhT$8<_f$&4Wx6XO zszZTulnFx2jhT6-@uIY%5JJ=khz9uoNT|Onw)7J~VBzovYVmZ_HX-gPgQ^8ii6be} z8&ZBze(*{|H23*m|19tuEb{^Y`TqdmC@?6qD3d63!TN9P?-l|HuovN=3?{BE{*NMVS}G@d<{_MO%Wcm0W55uL zKgJ;EH-PL}`K2$+pLh?pEYEgVDGqnkUqZLZw`E)kpY>8%F;{%9JX9M% zO!%$x`$?C`h8fxbV1NJZm-3#*TW!Nx`Y|*C{8a`r_emagCtd`w;Ng2QE2U9+f$@Ep z+af)pI5UP+Le-ha0Ez}ej9wy}M~V{IH_|Kfpgz`rgfh!Wl4>1lDZqgB z-F+jW({wGJ^l+iEBm1bc&Y?n>_y(oR#w1xAX_Mcz&x^bK5)Qtuko^G@Bx#K?ORi#En zOlge$7>9xc5I{)ZSQD|Y09UGFa(Pig$e9b)7SAO_n*Lrn--@1>*Rnk2_g>Z*R924Y zr`)bc0o_1)?nXnEvn0=&y&xmSsX!@z$Dqra1wi7pX8-?`J+&8J2>^0A?&Hj&{cUHD zkF$z-;C%oe2kVx7`OF#pN&O)DC(bO47>p4)0ZciI-hJ;weFyjhyaMJ`6~&_;?A>FY zX~4IlC-Qsct+zkW{4j33=NKFCJ{uGLVF75!fxUyVfN=jXygc?*d@4r>_#+f4S4z&A8@1YHz$=NQ$ghx+o zh;QYz7~_~DQe1ei1Q&q_0Wsp__L&QD3f;04k)QH)KmGJ=Nv%ZBdkmlu~cND8TZ${-6G>^#AT}4S?>zc;~Ti4HB))T|WNEZgNUD z;-6y7nUV;OE3$AjYItOI+^L;!`?EiyZLi0u4m|5rx6$-9a%0k7%xP+;JixklYiv5* z830zTTJ7=}%I`z5jI5FkG=nUYGWL>cEiMi4Nxju1^^2lycd|$v5XQi(7k3=Ty5zgX zKiO!h58?0nGZ|+y5i3&e7e#B#=pWM>AOhTmjEB__pymwga#q^&Eo2%?G5M|>8#(qt zF5^0)h<*-+D&SV{?&l$`>ZW58%F}GnvVr~I?&)g|!jzhWGNpCLUGw$YFQ>;`{3$gr zo|GChfM;y^u6NsE%y)Nf-S&Q`SrvD751qw&=7Y6KyuP|9Mu2rMNr8eUI>25)%e5&1 z#tHCJ3NMu9hHn)zKOzAL>DyK~=3@b^edVKD>cFn&IP)FKFfV!ltV&){XWl*K04K4? z;~C4|55n^i0?MJ~zZ)<{xeV5LfGy1aORr%qMK>!Sq#qm!C~d0`Uw`#kDb;4>zKpS5 zkp%|^#E}8lSxF(Qg^ba|!bETD8j%uauQy(M0G!zT$59G9;|Z@iAY7s5`8gOMm=msH zJdDNa$6F10{==gQ2*JZ|U|?Aenqe@Oa4Q!`6vUCh1O^KLB+A*g6DS4&;B=k~VcqN6O*htEI%i?yb+`4T{(|P@T`^}e%QV@t5$HE%{kRYXl(ov8z zF+L+?ysdR3mU299S#h-5))`z~EVNyW@%^d#iIQ!yooOtQgU@^bCLu^sj3Fy zWR2W-?PHP`O}WZ@`uRA>E@y+3%3UWRRIagyRjV4n&W;%evf>;>lmHg;VA$-i!pO8 z|88ws#K89UEU&R;o@)#;8H{Wt84)s0O&9V~4_T%sQq=L%G8d66Px&(Ti$zUbd!6;U z02X7x$U@(PjL(?-uTxQxWh1yz(%6D+^|2Q_Xlp8?1-m zVBo{vW-SWnFBV$M=gtXY_0E?%hxiV|jzY}6R=T}PEz~x#0X0cSC*#W5Vmt zwF2l)02-A*;yr>v0K>uW{^36jl~12QDNqb-#Xb~(t`K>($&ZWpMkatz_etZ zFEIPHS(xR2dyC4L`YBD}Yc0cSf%`;Ir9-L9y(lssJ0X?!4diugOa)mRO2yF8e%CCG z>7w!$S1e2Y-^XI&Y!KPls+OQ z#8RI<_Md(FCjp6TulcOH*J27kQA&nb#Hp5$6L~>Jh2|Q7x@t8iPDEc$x@0uRi1@`z#<54Yd-MC zqqH+ldC$m$b#Ic@0k`sOQV27cILc$9jWAq96$d~ObH-XADupTntT)Ds;vXT~j4RSD zP)=0>43a%O$f{;>q`3xwp`3DU5T89!`AP%pb2k>D7@v8~b>*+=RuqqiMO5#tP_<_OJ0f) z{t{>VUwGK^uBV&GtJe^Ear)4&s6nqh;>BeFN=6>JFe1QY;FRx~dolA<-NHTTV~~Y$ zhHTh6ON>Iv3?f0}l;ZXVMr|&f1GJc#} ziBj5Lcp4M^CtV40I{M6cgv?GLK*07Q)iuwJ$ z6TJRep5hJ3`|u>+cicMDaq#}&9j6qBt)2AYN57SUfT%M1 z9|i)>9ArW?19Noi_KSMo1=~MugrSUQSlP>~QjKToz_>}Txk>k7uaWalWu;ipL+H@U zYjHJD+O<>dQq?P~TG&O5rc|`-PL~}0_D+Re)7PRyk40B*P-q1yt-ffNKG*-foBy`@ ze=n!FJ}fH@sBZcaK)4xWqyP7Se;8VV_%ZeCsqKBbaSq2mwV=7UUfVw}rPNT1rPP`^ z#tME@zoP3BOaW+g)v{fyEx1X|`r!K@0c<+#f4U*$qkNId28-1)v_=9q9!!K5p&3n2 zeD)^!EM68_x|Fr}66riV{^9SRsm02Iq$>9=x3Gbs@7zV}JC z^uH%Xn3p_Y2>_)qPxtho0Es!DQ2(r;egDlD0tDgRM;QV50-yoJc&W%2gq(;zDg0WA zbAuEVly^d3Q67Kz?rQ-{l)%WrMgfVERSLDw9(EZhU>Khnxfs{K=%ukxlwQw@BAyI@ z_~5za^45uQdSCrhL_i)hK;SR``dmnd6^3~E%H0eF~0 z=4Ers$_Pd&E@K^kBEA6D5DGlt4TY1CQBap0AxU$zoEMFau1d(99}c}y6fpUDq($ci(vCEVL{LdnSGryp|%Xcqo#b=ZUaPBrVrS8}_Q5mnhZ5ATnX zoL2(gW~f0f{5@)vKa&B^#(VCJ^-=HL%BEUf`7ll>@?y&XEN2!5xO!F?Xk|~Zcg#5S z)Xuw^5g>{q!Z|w>ZLzUYcoay*A{9kOHll5w9s9aJ);LdPXnLseaE`Kf0%*}Ep0(#Z z6r3k`(^<0-W^U;%RtAsfvi48Uo~h4bwkNtE?=jfli7;Yq11=#Yr0+r68pMJ8taY?) zgrpdI$=RnrC#nhmvkaf~S(P3j$lREhJwzk~?<{SX1gK|#jT}`?!z&|j z7SiWPHz1Us@o?6gQE|V3U)JS&AABO?2F7s=5f~C^pEj3%=F>k?2$*-;qH+LdQbaeg z=a~mm128Z{`1jz!PxfAdw?yx9Gl*Ggm!$(jQcKQ%koT%&rc!R^N+k;ZozPi2rKth; zQ{83yy$Fb3en)&DQ^2%Tgx*tEa(kt*!Fff`uey9yc%_3jDmC4!dZ{%uIN9u&3U;Y@ zV(h)VQF_|3L-9kkdg^oiKm8l(|6Mfj3ZU~Y21X5?-`epO+W8yn6>ZMOupS2%j)Qhx zSIf-Q3NXQxjS{fbc)Q5f zj5|iym@U)VkRX?9J>;t7hQ9Chz-i?XK%BocmdsSw&)9X%@2Md(Wqggio53}?B+2d@ znpH3kK}Rv98Y)wsKbwtvdq?(rLxRTbH4>l^MDz1aFULwGk=ek?%Ayn`r+ zC?=!>KvV>HL}5g6+1oX*@H_(tJi2$J{2gTl@~-;+&DVdm{8Wqt&bUnbumIzYIm${m zq4~-w9#T^^@i8756igyA-3DF1Pi6E4ipHZ|iI?yIz zqIf)%(?|{DF}Y%GWs~46Q22iC->nQ>zxd)WR>wHEha$J_N^1+&QEKTUGQq%rGJN2K zUgutm`S8dL3n;xr4-lG)hYMp0V~-GI05v(&01|oYb3af{zxwhsc_2|NH#~L13>XWm zYCtJJFTIG|0M?`i7(v#S`owa$bk8h`ANK(0k;$zLhA?mtB>`B$I02|29pcneB9If$ zGB5OgNuS_aj4#15&3NYyqO9h(c}tPgo=PVgnILiq4?5m_`Y6xxrIpgNFz1CCz6Rq% zpp2vP@nO$*W5AJ^*)0u0;km~~{>G{|!-+A*(*}9i0A1!3gGq2D$cVMG=Vb~HWIRD8 z08SKP<`G4ly%%6c-tjAA_?MSa-mICYLtMz)M(%f{u|&#)mBX8tY6Dwdp+ZI5DI!!C{QT!)xwM;msd)W zK;G%pPDp@mwnrm@>e9!PAk< zXr(BmJRcYJcjJS@u3)kU8TT=G>}bES!jOL+1zJ8iNPMhaie$ z?vR_(A5`^XU-M_AiPthB1N=GP0FqA}lmoV}-B=e~4l*{67CFGPV8SQu32=@9PR#lj zR!Wbz9tJq}F5@Q0pHOuoD=<0(%pZ9fKqhF@3HwyGHQx+CfA!V3bRH40#dGLWr2Akz zU@lo>7&kxo@HdKTV%)|L@S{c1{pk~(!FmU)TEe#0&zsrc z&UfF*K>FBA*_!dqq7;mK;*H!M_e2^PCG~8+t=y_rkJ!xK`rpgY#JNYgK-MX1($=)` zaljAp_PZY{l7cf20|9FR!^YCR^V~yJ1n+W;?lOvJx!=0;LNQ*^S5@4|^Nl?2=e|9B z_><0)sCp44K1b(;X*HBK0w7f0Fi3U^a-+k<&-Y$^H*Ie3>Rg-!>%KfX*-&?K91fEL za#O3bSDi7r0$RMFSXjLhtR$mN&TiR*2ypk+$58!!Ki7cUAG~Flqx)k*-7o3w=xJ^ z>uq)Ol+x-dAIk*B$m>|)02rqhuDEjufut_SCe?=8fyd;v6IK6IukE2+BQ~3r-2+)MQJHsba9YAi70T!_R%gqyjoqBlvDnG=`?sW6U~^rk#~d zG7Wh*!^RlfD>V`gfllt}Hg_-Qx{3OJbs-!}``vfvn!~7*ZM)FxxYA_a#~QLTtWLIF zF+DWYr8rTF5)|0&}Pw%H5mQ12_4Is)Aqe?tyIHqqy&d`SYdg4hbb(} zHoWY?&i{9v6~$jD5x6E2;HH;m?QhQuFjtuPi=~cxE{v&)-v83E+w%uaHmAQwBUoEqTIU@iL%HSpNKxv00C!JneW<0crrKZ@>QB zbKQ-UN;R{yf`x*2Akx1#L_+xKr~fRGqG%}O*UMMo-PluJ^(}#{@3PmMGy{~W$XP@w ze?iRomdlP|0Z&(WRw5jjd$^u)8vDP66iC`cZh&{Z`zWzd z8(W?|SdqPkv$4b<$%BMSl2@Le?l&Q=#axD1#>9%0fKuYuNuPO34SPCoy+%KI2#8QCf0)LiEZytBnh@}`=J4PnXR+2}5Tyj7m+I46^Qs=K3+CeEPyRtaa|TA$f#6f2Z+0%(`;U2hPKXO1;KRJ&;7$Gpu4bCHDQFI?`jmxY4&_e1Ley&Bu z2mYlG?_^{9iM-~<3!thw;mw~JDJUsofO?)yB*7C8`{z7t`Cn*{Qje??5f{qfWkx?m z8>CS(gX;$a1qP2ORT%Fs@!=#Rh<)eXkMt~}E-)q#;m3I%=cx?tRPD&?jj>V+@bROc zWmvQnBlG^xTt`vFQCbcApXw@8y(Ye|*AD5W*E0Cu+%2hseDofHBZ07gPZMhO%-O7} ze+_zc-PQHaM$s<%w_WdP&YezLRMylf^WzjdH<%?HQ4`O0danQfKdJx6@au2wLTkh} z)HPD@oc~Xi@g&8fG}n6Yi=m=G#Rd=8D2(`X`=B&*Shd(YCigWMx$Kt~#Sg8_+UKNZ zt#vn6VYltJve)056o7UJE!b@>AI;J%ooj^NrvVH#w4@d0>VU>h1krZ+VY+apBI^ta zhT!UAY9=jJcWo6+{j=}R&eD`td)?=Dbc<(V;23+OK{Ic*$GiirHLF{gRnCSUTiH%O z&3)5@u$7MsA%)Q6k~D3qMK0K>XBRdX$h)a~2%S?8%ew2q!A>3kg z-w==?8UO_gVE4^8pG)yzz$kPm5b|0(U?nX8#q5EHsRz(x=prh-Yk(Y(_q~@%in{Os z4HQt6Vq^ceY61`!q0EHGJ`^GpE&w;z2-eW zQvYr#Wcil7P23~z*qCGpLT)0IgTT?fjc_#(ZjSihn_ z<^&~Hbu1uWK5giXHSw!pSi-kAy4zA`hZdm5DoG$ zi{O}PjxqS4Tn2$2A4MR@D7GxQ*0Hu{ZorYTAwPT1%RDj`s~3Nfy;@&ueX?c&7kI8h z=@jV9fSLaD+zn?gugw3Jr+lEi{!xY=e&_EP1&*B%P2tw3=5-yD0ozb|FC4%csI|(m0rBW95G$ujR}Y*9%3&Bu|T& z)j611%83~qXKrknX&)d5m11}x$Tuk=aP2eP4ps3@Wz0L2(NFg8bIf}+Nh zuCiB*!<_BNjkAmO`IPd8A}zcwrDU})AhgYA=EuMJ2c3Z!j4^N$$v`f?=9I{z{0v1A z&CHPe!3V#w@_LU6rH9<`iPj9MMHpQ$m>?f28NC0&Z@p~aPl|Fd54jn8HkE<_56(vV z=#xLFZ&r=QNJ^1E=M6HA$`V8IM)k7O;p@#fpA==1R51FJ5O4Fs%N zMYBIA_X9DrO&*3IFpzg znEoH>oo+_3YbW$%jR#*$7(9z&HPMl#zfb>W-_0`khwyJF4HJOCwP$C?mKr#8x4914 za{x50y6%>$#$Eu>On-CDHXbh83`RDq7`hQsecc-*n5HsAg zUOSMQwjJM9{lCF+V*?Y0G>M$J0q1r4zdx!0|7nsoV9$4OkZ(O4#hw`3Rh-)HG+j;YM)1x~tzARE zz^A`=2f(DyKI^@OEA=Aog_k;^b>C=gvoZl?8d6#{9(4IRLz{-+LrOPoqBKk=)_15i z(8{|T^JENTyVK*Mp)uYyi@%Xi(_kNFwQg}X&9v>LGKOkcNp&7-hx&uB`K|q9&5=n} z(6hLpC9WJxIY*a^9e}GZia1@oKh2ZJ)NY}1TrXq;-ec)|H0wsvl$x^x0L6<|r^?Ja zBeIzM@9DEg>ESQmm9k^wy3;+af4Rn|kmcn6hy>X6vRAWE#5XOB^PV#(6aI)I%gY`B z7oK*$3toW;VgJ!d|1R7!sW5tKzh}^nhY$cy-wE|cnK7^Yh7=Nc!U^^FY6IsgC-}!g zyk{Of^UBlC-oc?1R6rj_1tgF1jiLu2qK_CIP-;-H0c_u9BLxa+^1N=^MX?7^0^U{X zD=SkdKLC4+0+?&kEPv9#k`W^Ai!%U)>remkU-UiC0$^aQh!9CaEB6%<@PeN6Bl)A` zvR_%bG{SEe3(MtJnLTpvl{#@t?UA?t&DWnh>%4{9iU3a~ueN1qILMxP<>2S*0RRwG zp{PN)W~B+wWL&_jjiRS242o(v(wrkl6mC*01aQwzO0)u=P{1Ytd&kKIQG7#Ql%zx9 z#P}d3|NLBijM8^Jf8osSDjT(Ts7M09JADCEaSb3tCAveV-Jd~*x*$nZbM@-!@;?QP`f9d9k(eNZ5Oh$;6)ItKc3?J zd*pD_-(!_ndn&_4tidy9pAYa$JAiF@%{^y4Ncvd+d@$6o4q8urfaG99N47+HD7wH? zHUI<${Mh8#KcNlf9mzGD&y5M|b z4v@jtw#kg>9Qw26IrvdhQTg~@A3O{)k?vy#!3(S2Ap_b;F}!h&N=pJJJha%z#jdP& zdrC?jC_o_Z{e3Un=)ritx3sL5}Q2419cKoSRw207{cWOv=7h8V=?29Kgcx4`}3B?EQG7 zL}W*hn~KvxUf|aA?UMB%+6;FUfCe}$#4Ei202o%xR%t5r(Q--oGTtOPn#QxblB6H`RL<6NUod# zL>5FTI*mQI%e=*Vin*k0B4P2Q%S0K#XI>_B!wD@haH@=Go|l)ip@0&Ffc5Xc{aSK0 zulunKs2ClPFMUEj6DX3-7I=j+*1kCBAPCZ#6i^`q#eyn&GqbIkMc`twePZw6q` zoNYSkrz(bwR!QXdfA~*2LzJVRuQ`)pB-wN47D+=i3(+3DBaHCxmy$LS5iLYK@ZKPO zkQ4~g+b|4V`<@iwzp}l!TpHPEtobRpk9|4)OdS?+&#k+!=CyD;@0;+9x-y56@BA6> z)6jm0#^XPR`Xlvgyjd?z0XB;xXmFiQe`vznL#Mj2>*W{ji_+5pt;?r2hwI zOcM}Y4KAg3@wAf0US&FlfnZ%9OXPPQS1M4ine$a+$&gZuDAHtZ8hLeSvoRJm`u;GH zkF`*Z3O->~*8cmD`sq)pH#oU5L1h3_wTh&65jPWCh1D*oCcL5C-;`c&O%cr?$8jzD zfAjY`L8&g4)HK=(pzB*|M(@J**r5gnrm^EYh-=ETyC?P=v+kraAt`kv^3$!+-q2Y4 zhW^jhmN3*Tlx8EPN$cJIZw6BSr>vkdP0gmLm&6dm;L;yP&68d1b-t`O(}kJ3=pYw- zMZ`7dr_tnw)E*S6L%U^Dq+TA=mrWy^_D|E<><;6}qf+hvI5hjlzU*c;hLGCl>%L(& ztO%O^2!QMy=x@Bl3A4X0Mu5lZ;m_X|5I;2njMKNvuYB}kJ-RF96i*CF_uN4TeW3gl zrGx;wT#F}SZ24CNuh zY>i3&P=FaA@#UAF8d;YElna%#IEM19v+yN|I<(ZMG7|x zwUnd`3{bdK#z}k##t(qP36TFPw@q7&358BXh52{nL7(xc0_XQ(iswF&FU)*>_URv`7}F;6xpbEH2$M#30QpU?OMmFzB@8N*tini<1lBfm z4So9V+b=ZND7~x`)(*xB3<&@r@g8Jl7+IoBF5O#aY~b646prYFJttXIi2sg77yvdQ z@ItXip=F+xr#{SzmmMW};lMGN`2h;Zrj!MQoX;8jBjj1N&^>oLqD6RS@_l3}3C6m8 z;i2)n9=%Y!)!P*;0u+(=+)L42sx+LDCMxZKI_^Hl0CPNpa5b<-S$k0%K1$$)7xLOm z=~)Q?pc#9l&5%T4Eo;$~a%I)pi{ELr6?ETNY%$*r}R0eV|PK6;D-Sb%k5P#|3t40{< zoC$YQZY86`&ybBx2%242xQ!1Q=!C32?YqVoG=zd@h9Z*iI>9pZv(nZk6`z&@m`-aV{g?Xo_5`P#Sb-bmFxdaB4tNs;c$is&Ji_lURu~n-juU|FV{MDwk6RxMj87F}$f z@=N_w^OV)@HtjcKpBP8{>ZZ48b+T867?K^R6h_b2jE$ubG zr>-Q+nARSGtx(($7mK}FkfV>Gn~!qHtpB|YwW$xN;~o97)XaJNY5L+zBbCt<;gyEF z7lySr`v26I%JfjO-vMk}+j~0uLP6mMl!}Yf@?vV%Dfd^sc#(ZvG9RQJ)1U!;LkLOVJobP|Hu-B!NJfiqwQ~(4l z+>5B{0nB|~0~;F_Mvt=l=Rf}+Qhc)1J~3Mq868%orGuqe`>+ydy`V0>bmU zbS?ziiV|g!*S#UnoTUludrAZm5*P*)y6&~ek;wzke?9lw>s%k@7b7fr%c}xbsRkqx`2vWtl8zqbgW-?- zfAK%u@hS&f0%~D2qYa)P;rBtrr-+7`k!0a@Mlly#KE^D-x!04A^Timz$CB3{pv{23 zortkMr{|3#i;_nedD%e>HHzfGpx{OmWK2JaATV#`OzY61S9VG@0U1q5GazNbpTWM2 zv>-)TYuZ4{5d-?Z07PGuQ08kZZ|ELQ z`otbqS;O$M=KmI@3}6K){2c=W*Kc_gN%Wicd2WPzv-dB(@&gB3$e8)qbQ$XBtu3u( za@22mMA0)(-(j5rT*KQR1eSB>V_>gfU?dtTqFo?zHJ$+@Vl>eIRO`=-w|3sL{vzz3 z{l;7Y;9r099rY7~mkb9O*&z43zS5owE`o?wV7!pfz46w&k`u;{ht5FHy5MJ#k3t}d zF?VZIYlCx^@O_n?Q$>W8KjD(LPl~+qT9)iU943NUsU`w=;I3^M?=V5Twdc5?ANWlZQHA zm;;Q*OW#YJc^`iCI~h=U-znV!qr4)Iwk&NU??ck^-gx6(uWn#r`ikK5RHRL%GgyiU z#u=3#%rbl6V4Xe&87}+ft#{s+k>!!gm9aB7bLTE$9Wl-q@_Zu?2ncyE5Q%W`w&s30rsmqDoPVoSQdOMPp~@bgOdj1S zpDhL+$JEB~7ynJD#%U!Kzd?Dae{gwN3wg%p`hWWGt^fN^jw4IEDZ)w0{|u?LJDr<0 zG{26gNU&Kmom_d|Y!>})!SgVg-}-al(U^~RQM7a9Z8nb`CNorx2u&-N`tA>ztf6*U zw0#(50BGkYI@ti-B9xe{$zq&5EVaOY4186ekcM6W6_mxij|Ip#vL902acQ0K+Kn_V ziO{Ui)K18BB33qiYT8Ww8PJ`49lLxpF@Do($VM->YHS(Do`7TedrV+z52gm!Hht|D zbKS#L?B(EUc66$%pvbJA0==bh<&LKDW+9K0rUd)lG{zVoL$hw$%~yt& zx{32K)uXjMqdq%z2jDpNy=`7MFmn@9t@|+FkHB&l5R(_=HWkUcVf+(Rs|MY_qP$^x( zyk;m^{5`z)w2hY=<(QtREKpV!RI?cJJq!UaSm-1KLU=||Bv9mt9p$-;0U)Z0>f0hXszXz-zADf3?1Z1T+{=pH!>ptc5kQ_l^Fc^wu+^h{8fl&ZbE}|m zD9_Y4-+UoO+sK+s8GNRC=hp?i0xV?I$bUngEF(ZTiZ^nyP;i#(Op3I;mV}AJ6Oe)N z?yiOP?yF?kT>%IBOE|t1?%YSfw@PAVLj~SWVTknkH~yVes)B_M5hQd*jtfK68r#)?6~l-eJ}YZU`V8fhy*W+ z!HVaI=^sN+8cN8$Nag(SW-X}V#cZy1g^`mK7nQ=J!a?vxM47?7-jzDw;p{^b z*_EO5O8arsIS20Cc|jgn$TLQ=}oSc&MB+{WUZZa=;_pP$P zg?sEDs|}HaUR-E@lsD99?EJ99v$i=;A3b{LWPoPa;+%^L7>Zcgm^;AU5@0sj9O{{z z3FmGQi}ITEm%c}u63_f7d#AYvJ`}+5aW*iJcE#9(Q-ryR5`vsxR1mFKk_YLmSuZ~C=W6`_676%YKh!xdE)S}oTvUU~@BiUH>l}&n0YEr&)}~uf*Pc?noXPanO8h-6gq*QA zks;-Lc?Unp?Jyq36)8xZ2f8+2M@j<94JmIj0Oaen=jLVzRPVDMzFh+ZJwK<|)6 z-BU*VX@=-^VgIpy)};d6WB}j{dEwrhd0*^LG;LGHE-;#1UKG8`^)uIFr7?G5^{EYa zcHMd=g|Gm8d40yQ?Z;{aX|I>t{YBdx0jo|>jGy|!KY-d;QO zV4>6Nd)cRzHB-kH4-EJ?1?mAZnnpTVhG51UaPiRL31(x~87oq1*7RyReq*%hrQ$Ls z1z;7x5B-KnYvyYeG{;jba&Nw_px$?k_sGh(4MPZ_SxM7%xYB`6khM#CPO)NnNKL!2 zqUmN5%P5;ksZ#0<6k|0Cq;AsP=+O2(CaKvXb>~$hMSXlXyiWK{chU;E$n@TJ;njm; z%_f`JG@4aeh2Acz<_2FuDHe6Ai+CKXEXQi8jIO1|vQ_UNS}8Ph)pTZkC{BA&3~i26 zm*+LEuIaS8ym*f)KY!@{Zg&Th>jp?LRo$A^L;54C_GUI+wy#^?za7V zEB*OT|5b`aFr8mJSrobJBJ|t0TwL-UQ6{5&nn=^|vL9KfKi*~PeUmzHsi*@h$;C72 z2gVEY^dISW@=)c$+1!*=QCPSZ1yO{CV#I$%$`6VhiZp-cS$G8TsNKGE&&lTY^*!Uj z5W#bK)}=F@)BhOnuJ?hqsCW@EsmtHWAI??fC}z0wsIp0Bt>FzaVB8G!~^J5 zhhEVHN9rqb?R6F#Ec|mEua)870mL4t| zI~#8jtCjmuK7*?Opta#dXN0N(+-cLuDUINcV-%1HAW+G&r;nYK@1PJFZbw8wUMGxo zCQ{5hT6bJ8hJA(apL@Q!k_%v~kg<6QJLJUQjzy+v;BD?D z3B^vIJ&1c=abRO()7Q`wzdS4 zqMTnuAutX_+zeJNfc?W5*qaPijG{9{vIX&o;fMr;9g741oi`K9Yeyg%a`>4@J5Ew4~sr9uReQB>ymye zTI8{X@n6=|0U0z-Ey72JE+SoatVE(JFF>rix!HkVBf2~&B&t-TU`=p;0RO8?BOYW< z=o@Xwh>7QY%Zygc8*LD7&s;vmc$N1b>mmpY`B`ZKN4HCEyUwaKDsqH%^zOSK$ry*h zFe;9SvmoCqUI;hT1jhk&-&yCJZ5SQcZ$w=ocgAhi0G?^BV3@!FB*y*=t9XII8OC}H zCnDm@fLDA3tT!ry#84l!Az}0w zNp_s%k~Udu7#OHv!0(pQV4yzsWTfICM^YKM7a2a1;o(>YD@*4vVoMA@ zicfy05P=iXbIcFx@1u|ZL2Kwo8C8GL`s8E$CMRwvJ6D-X&KlDH9;%9j5nS@INF&od zf~RAs$hN$fii)vD51C%WhjHG0`K`Q$cY9sN^_0c=$(YZn4$(#Rq_%zkQwkkx==E3W ze<)kVSl?rrvDd;IeZGukm>4+>P>fwpY0n+-T>t-frT-@fgv|{xSEa#WB9$J#4r|2F zU9ENlguA6_=o^@DK16?dxun#P4aT))4S6S@ae3P60p1LtpR<*8iDq%8DtY_M%7NZt_PR#>%hK<56HQy~<&=8MZZifC z;V-9lSbc^#j3Q;JT?NseEDh~y7`yrNWi^)B;m(cC?LrJ#WpDppF3!400RH=1B!M@5 z43tok@17JuK9Nf-X8jDZ-+A|AkuahB%42$IdEz21gODmKvnVA;8Q=$7|2JQKF6BPL zmaXjAAkci^02755kGz!tymCfyQ)&z>-;r{HSBmyRd5zlOD7{B+c;LB;F31WPA@)}8 zZ7^~%Cv#KIe;z9Yc=YYd&p%aIx_RWxLj;%+yZ;deh?h!^I_2|Ftdv_lm{9{|let1s z6hnXR13AAa%YbG;C2gUMi=4{yo5kcWqr@EnSu17rlMD+YloJ`>WeIavt?n#aKs~j+ z0g>NkQ2qI5f69vVekqR>GatFc7!WMy|CVHmjF1lmgQL$M3)r4@u4o73X1oFiA}c$( zbxZwWjtQ+rah9BXPUl*8gA;~X9eVCnz38muBgY`bLV;tV==h*;U&YZxI zwK;Ff0V-{V8sIRq1El4ZJyiHI z#=hYMld)!*-%~}LY>AZc)z{uopD@%t&i#J+#5~+)=-ASJX6*ER)6-HgRA6L=SW{*1 z1hyZRiU~GHyJqYgElmU<&z!JF33XRGK|T*S`|iH@k`ZqD`N>+txWL?E%n+a_g&_lK zQcCzl*-1b!6(C-Id~2I9uxd4dkh} zwBTqM);K+*X)l*rbiu0UKiB`;PW->9|5uzh#z_3WVS+7FE6>y-&xwG#^_QA1$6UKZ zuguL@M;tP0;X?awE0ETJP`b`PO)5s)_hDuIH9Z^}T)Zm#`Y6j6NJEeSrha0^ZmE_< z>a!99p8lIU99G6TH5`-~jSbbyw{Fq29`Ub)XZSTka{#r=+(Fs84LTH|_1~xRw}}*6 zV}@_WH1xvn|81`A4vNXJivc4Z7ZcPxx9rJ!W4*pTSo%XQHS^N;v&x_x1pbHi@4al+ z9Ufs-n^?~L>9f^?YiQR-(`pe$>Nd-Sm~!P{vQu)_9QORpH;*oj{%X^fgFI@#Fj;O8GcU;E;n ze%8HsQ_~0j1$2==8Ok8a%ZBIjqm;j$jR2G>LPX_p$Zh^P zd#k_s`mcpqm=`^Yn&st3sldDGtFJyYB0*T^RW8gzhmDQh!olg!ci(;~Psr$X-sxwQ z;zKD4o&l6RYSu?BZrY*Gu#cnAAGsHv@Ki$N@f<1dF)L#Lc$9mTUiyf#iD7|$0g%4< z{LfzMY;FJ;Pv|o%)kI$Xju`e|*K@!4;xAs>&_E@BL*!DvL}+*B0ja%b)0ztrWlpVB?U5g|^Syvvndv=jC&sO=dGs_+35LbP3ucCos zuhas7vV)RC)k(iBeM4E@bfta68OJ#VwmtmcA_+DN!~c#)>G1PRq3~N4%0DJ2C$#Wy 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 literal 0 HcmV?d00001 diff --git a/src/renderer/assets/plugin-icons/codex-connected-sheets.png b/src/renderer/assets/plugin-icons/codex-connected-sheets.png new file mode 100644 index 0000000000000000000000000000000000000000..18b39fd6f2cac4a81a8c666595cd30b4bd0a20ec GIT binary patch literal 2201 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 literal 0 HcmV?d00001 diff --git a/src/renderer/assets/plugin-icons/codex-marketplace-actively.png b/src/renderer/assets/plugin-icons/codex-marketplace-actively.png new file mode 100644 index 0000000000000000000000000000000000000000..1a1f423278636fa47101e7da730750cfa681296f GIT binary patch literal 1879 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 AaR2}S literal 0 HcmV?d00001 diff --git a/src/renderer/assets/plugin-icons/codex-record-replay-plugin-icon.png b/src/renderer/assets/plugin-icons/codex-record-replay-plugin-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..dd892d948c1bd011fa2cae3145d60b2d27ffd96d GIT binary patch literal 23100 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 bJ$?MQXcO_)nnpLP00000NkvXXu0mjfaBWH0 literal 0 HcmV?d00001 diff --git a/src/renderer/components/ui/icons.tsx b/src/renderer/components/ui/icons.tsx index 0b1a39da1..1c5edbec4 100644 --- a/src/renderer/components/ui/icons.tsx +++ b/src/renderer/components/ui/icons.tsx @@ -2573,7 +2573,10 @@ export function VolumeIcon({ className }: { className?: string }) { fill="none" className={className} > - + ( + "preferences:default-agent-permission-profile", + "ask-approval", + undefined, + { getOnInit: true }, + ) + +export const defaultAgentApprovalPolicyAtom = + atomWithStorage( + "preferences:default-agent-approval-policy", + "on-request", + undefined, + { getOnInit: true }, + ) + +export function workModeToDefaultPermissionProfile( + mode: AgentMode, + agentDefault: AgentPermissionProfile = "ask-approval", +): AgentPermissionProfile { + if (mode === "plan") return "read-only" + return agentDefault === "read-only" ? "ask-approval" : agentDefault +} + +export function permissionProfileToRuntimePermissionMode( + profile: AgentPermissionProfile, + fallbackMode: AgentMode = "agent", +): AgentRuntimePermissionMode { + if (profile === "approve-for-me") { + return fallbackMode === "plan" ? "read-only" : "ask-approval" + } + return profile +} + // Get next mode in cycle (for Shift+Tab toggle) export function getNextMode(current: AgentMode): AgentMode { const idx = AGENT_MODES.indexOf(current) @@ -206,7 +284,7 @@ export const selectedProjectAtom = atomWithWindowStorage( export const lastSelectedAgentIdAtom = atomWithStorage( "agents:lastSelectedAgentId", - "claude-code", + "hermes", undefined, { getOnInit: true }, ) @@ -220,20 +298,65 @@ export const lastSelectedModelIdAtom = atomWithStorage( export const lastSelectedCodexModelIdAtom = atomWithStorage( "agents:lastSelectedCodexModelId", - "gpt-5.3-codex", + "gpt-5.5", undefined, { getOnInit: true }, ) -export type CodexThinkingPreference = "low" | "medium" | "high" | "xhigh" +export type CodexThinkingPreference = CodexThinkingLevel export const lastSelectedCodexThinkingAtom = atomWithStorage( "agents:lastSelectedCodexThinking", - "high", + CODEX_DEFAULT_REASONING_EFFORT, + undefined, + { getOnInit: true }, +) + +export const lastSelectedProviderInstanceIdsAtom = atomWithStorage< + Partial> +>( + "agents:lastSelectedProviderInstanceIds", + {}, undefined, { getOnInit: true }, ) +// Storage for per-subChat provider instance selection. +// This keeps T3-style instance routing independent from the selected model slug. +const subChatProviderInstanceIdsStorageAtom = atomWithStorage< + Record +>( + "agents:subChatProviderInstanceIds", + {}, + undefined, + { getOnInit: true }, +) + +export const subChatProviderInstanceIdAtomFamily = atomFamily((subChatId: string) => + atom( + (get) => { + if (!subChatId) return null + return get(subChatProviderInstanceIdsStorageAtom)[subChatId] ?? null + }, + (get, set, newProviderInstanceId: string | null | undefined) => { + if (!subChatId) return + const current = get(subChatProviderInstanceIdsStorageAtom) + if (!newProviderInstanceId) { + if (!(subChatId in current)) return + const next = { ...current } + delete next[subChatId] + set(subChatProviderInstanceIdsStorageAtom, next) + return + } + if (current[subChatId] === newProviderInstanceId) return + set(subChatProviderInstanceIdsStorageAtom, { + ...current, + [subChatId]: newProviderInstanceId, + }) + }, + ), +) + // Storage for per-subChat Claude model selection. // Falls back to lastSelectedModelIdAtom when sub-chat has no explicit selection yet. const subChatModelIdsStorageAtom = atomWithStorage>( @@ -342,6 +465,31 @@ export const subChatModeAtomFamily = atomFamily((subChatId: string) => ), ) +const subChatPermissionProfilesStorageAtom = atomWithStorage< + Record +>("agents:subChatPermissionProfiles", {}, undefined, { getOnInit: true }) + +export const subChatPermissionProfileAtomFamily = atomFamily((subChatId: string) => + atom( + (get) => { + const stored = get(subChatPermissionProfilesStorageAtom)[subChatId] + if (isAgentPermissionProfile(stored)) return stored + return workModeToDefaultPermissionProfile( + get(subChatModesStorageAtom)[subChatId] ?? "agent", + get(defaultAgentPermissionProfileAtom), + ) + }, + (get, set, newProfile: AgentPermissionProfile) => { + const current = get(subChatPermissionProfilesStorageAtom) + if (current[subChatId] === newProfile) return + set(subChatPermissionProfilesStorageAtom, { + ...current, + [subChatId]: newProfile, + }) + }, + ), +) + // Model ID to full Claude model string mapping export const MODEL_ID_MAP: Record = { opus: "opus", @@ -409,7 +557,7 @@ export type DiffViewDisplayMode = "side-peek" | "center-peek" | "full-page" export const diffViewDisplayModeAtom = atomWithStorage( "agents:diffViewDisplayMode", - "center-peek", // default to dialog for new users + "side-peek", // Codex desktop defaults review/diff to a docked right pane undefined, { getOnInit: true }, ) @@ -661,6 +809,11 @@ export const subChatToChatMapAtom = atom>(new Map()) // When set, AgentDiffView will only show files matching these paths export const filteredDiffFilesAtom = atom(null) +// Filter files for the Changes/Commit panel (null = show all files) +// Keep this separate from filteredDiffFilesAtom because the diff viewer rewrites +// its own filter whenever the focused diff file changes. +export const filteredChangesFilePathsAtom = atom(null) + // Selected file path in diff sidebar (for highlighting in file list and showing in diff view) // Using atom instead of useState to prevent re-renders of unrelated components export const selectedDiffFilePathAtom = atom(null) @@ -705,7 +858,7 @@ export const pendingConflictResolutionMessageAtom = atom<{ message: string; subC // After successful OAuth flow, this triggers automatic retry of the message export type PendingAuthRetryMessage = { subChatId: string // Required: only retry in the correct chat - provider: "claude-code" | "codex" + provider: RunnableAgentEngineId prompt: string images?: Array<{ base64Data: string @@ -1019,8 +1172,32 @@ export const showMessageJsonAtom = atomWithStorage( // Desktop view mode - takes priority over chat-based rendering // null = default behavior (chat/new-chat/kanban) -export type DesktopView = "automations" | "automations-detail" | "inbox" | "settings" | null +export type DesktopView = + | "automations" + | "automations-detail" + | "inbox" + | "settings" + | "global-search" + | "plugins" + | "skills" + | "mcp-settings" + | "projects" + | "library" + | "pull-requests" + | null export const desktopViewAtom = atom(null) +export const pluginEntryNavigationNonceAtom = atom(0) +export type PendingPluginAction = PluginDeepLinkTarget +export const pendingPluginActionAtom = atom(null) +export const pendingPluginDetailIdAtom = atom( + (get) => get(pendingPluginActionAtom)?.pluginId ?? null, + (_get, set, pluginId: string | null) => { + set( + pendingPluginActionAtom, + pluginId ? { pluginId, action: "detail", source: "protocol" } : null, + ) + }, +) // Which automation is being viewed/edited (ID or "new" for creation) export const automationDetailIdAtom = atom(null) @@ -1055,6 +1232,7 @@ export const settingsMcpSidebarWidthAtom = atom(240) export const settingsSkillsSidebarWidthAtom = atom(240) export const settingsAgentsSidebarWidthAtom = atom(240) export const settingsPluginsSidebarWidthAtom = atom(240) +export const settingsMemorySidebarWidthAtom = atom(240) export const settingsKeyboardSidebarWidthAtom = atom(240) export const settingsProjectsSidebarWidthAtom = atom(240) @@ -1062,8 +1240,8 @@ export const settingsProjectsSidebarWidthAtom = atom(240) export type FileViewerDisplayMode = "side-peek" | "center-peek" | "full-page" export const fileViewerDisplayModeAtom = atomWithStorage( - "agents:fileViewerDisplayMode", - "side-peek", + "agents:fileViewerDisplayMode:v2", + "center-peek", undefined, { getOnInit: true }, ) diff --git a/src/renderer/features/agents/commands/slash-command-trigger.ts b/src/renderer/features/agents/commands/slash-command-trigger.ts new file mode 100644 index 000000000..69992e6e1 --- /dev/null +++ b/src/renderer/features/agents/commands/slash-command-trigger.ts @@ -0,0 +1,36 @@ +export interface SlashCommandTextTrigger { + query: string + rangeStart: number + rangeEnd: number +} + +function clampCursor(text: string, cursorInput: number): number { + if (!Number.isFinite(cursorInput)) { + return text.length + } + return Math.max(0, Math.min(text.length, Math.floor(cursorInput))) +} + +export function detectSlashCommandTextTrigger( + text: string, + cursorInput: number, +): SlashCommandTextTrigger | null { + const cursor = clampCursor(text, cursorInput) + const lineStart = text.lastIndexOf("\n", Math.max(0, cursor - 1)) + 1 + const linePrefix = text.slice(lineStart, cursor) + + if (!linePrefix.startsWith("/")) { + return null + } + + const commandMatch = /^\/(\S*)$/.exec(linePrefix) + if (!commandMatch) { + return null + } + + return { + query: commandMatch[1] ?? "", + rangeStart: lineStart, + rangeEnd: cursor, + } +} diff --git a/src/renderer/features/agents/lib/agent-runtime.test.ts b/src/renderer/features/agents/lib/agent-runtime.test.ts new file mode 100644 index 000000000..d41bc685f --- /dev/null +++ b/src/renderer/features/agents/lib/agent-runtime.test.ts @@ -0,0 +1,367 @@ +import { describe, expect, test } from "bun:test" +import { + AGENT_ENGINE_UI_DEFINITIONS, + CUSTOM_ACP_DEFAULT_MODEL_ID, + DEFAULT_RUNNABLE_AGENT_ENGINE_ID, + buildRuntimeEngineListState, + formatRuntimeModelLabel, + getAgentRuntimeProviderUpdateNotice, + isAgentRuntimeUpdateActive, + isAgentRuntimeUpdateCandidate, + isRunnableAgentEngineId, + mapRuntimeEnginesToUiDefinitions, +} from "./agent-runtime" +import { CODEX_DEFAULT_MODEL_ID } from "./models" + +describe("mapRuntimeEnginesToUiDefinitions", () => { + test("defaults new agent sessions to the Moss-native Hermes runtime", () => { + expect(DEFAULT_RUNNABLE_AGENT_ENGINE_ID).toBe("hermes") + }) + + test("returns static fallback definitions before runtime data loads", () => { + expect(mapRuntimeEnginesToUiDefinitions(undefined)).toEqual( + AGENT_ENGINE_UI_DEFINITIONS, + ) + }) + + test("marks unavailable engines as disabled with status labels", () => { + const engines = mapRuntimeEnginesToUiDefinitions([ + { + id: "claude-code", + label: "Claude Code", + availability: "needs-auth", + statusReason: "No token found.", + defaultModelId: "opus", + }, + { + id: "codex", + label: "OpenAI Codex", + availability: "available", + authMethod: "oauth", + defaultModelId: CODEX_DEFAULT_MODEL_ID, + }, + { + id: "hermes", + label: "Hermes", + availability: "unsupported", + statusReason: "No transport.", + }, + { + id: "custom-acp", + label: "Custom ACP", + availability: "unsupported", + statusReason: "No adapter.", + }, + ]) + + expect(engines).toMatchObject([ + { + id: "claude-code", + disabled: true, + statusLabel: "Needs auth", + statusReason: "No token found.", + }, + { + id: "codex", + disabled: false, + statusLabel: undefined, + authMethod: "oauth", + }, + { + id: "hermes", + disabled: true, + statusLabel: "Unsupported", + statusReason: "No transport.", + }, + { + id: "custom-acp", + disabled: true, + statusLabel: "Unsupported", + statusReason: "No adapter.", + }, + ]) + }) + + test("keeps Custom ACP visible but disabled until an adapter is configured", () => { + const customAcp = AGENT_ENGINE_UI_DEFINITIONS.find( + (engine) => engine.id === "custom-acp", + ) + + expect(customAcp).toMatchObject({ + id: "custom-acp", + name: "Custom ACP", + disabled: true, + availability: "unsupported", + statusLabel: "Unsupported", + defaultModelLabel: "custom-acp", + }) + }) + + test("treats Custom ACP as a valid renderer engine while runtime availability controls selection", () => { + expect(isRunnableAgentEngineId("custom-acp")).toBe(true) + expect(isRunnableAgentEngineId("unknown-engine")).toBe(false) + + const [customAcp] = mapRuntimeEnginesToUiDefinitions([ + { + id: "custom-acp", + label: "Custom ACP", + availability: "available", + defaultModelId: CUSTOM_ACP_DEFAULT_MODEL_ID, + }, + ]) + + expect(customAcp).toMatchObject({ + id: "custom-acp", + disabled: false, + availability: "available", + defaultModelLabel: CUSTOM_ACP_DEFAULT_MODEL_ID, + statusLabel: undefined, + }) + expect(formatRuntimeModelLabel(customAcp?.defaultModelLabel)).toBe( + "Custom ACP Default", + ) + }) + + test("falls back to static state for unknown availability values", () => { + const [hermes] = mapRuntimeEnginesToUiDefinitions([ + { + id: "hermes", + label: "Hermes", + availability: "experimental", + }, + ]) + + expect(hermes).toMatchObject({ + id: "hermes", + disabled: false, + availability: "available", + statusLabel: "Fallback", + fallback: true, + }) + }) + + test("maps Hermes default runtime model to Moss display label", () => { + const [, , hermes] = mapRuntimeEnginesToUiDefinitions([ + { + id: "claude-code", + availability: "available", + }, + { + id: "codex", + availability: "available", + }, + { + id: "hermes", + label: "Hermes", + availability: "available", + defaultModelId: "moss-default", + }, + ]) + + expect(hermes?.defaultModelLabel).toBe("moss-default") + expect(formatRuntimeModelLabel(hermes?.defaultModelLabel)).toBe("Moss Default") + }) + + test("preserves T3-style provider update advisory state from runtime health", () => { + const [codex] = mapRuntimeEnginesToUiDefinitions([ + { + id: "codex", + label: "OpenAI Codex", + availability: "available", + defaultModelId: CODEX_DEFAULT_MODEL_ID, + version: "0.139.0", + versionAdvisory: { + status: "behind_latest", + currentVersion: "0.139.0", + latestVersion: "0.140.0", + updateCommand: "npm i -g @openai/codex@latest", + canUpdate: true, + checkedAt: "2026-06-27T07:00:00.000Z", + message: "Update before long sessions.", + }, + updateState: { + status: "idle", + startedAt: null, + finishedAt: null, + message: null, + output: null, + }, + }, + ]) + + expect(codex).toMatchObject({ + id: "codex", + disabled: false, + version: "0.139.0", + versionAdvisory: { + status: "behind_latest", + latestVersion: "0.140.0", + canUpdate: true, + }, + updateState: { + status: "idle", + }, + }) + expect(isAgentRuntimeUpdateCandidate(codex!)).toBe(true) + expect(isAgentRuntimeUpdateActive(codex!)).toBe(false) + }) + + test("projects provider update advisory into a user-visible notice", () => { + const [codex] = mapRuntimeEnginesToUiDefinitions([ + { + id: "codex", + label: "OpenAI Codex", + availability: "available", + versionAdvisory: { + status: "behind_latest", + currentVersion: "0.139.0", + latestVersion: "0.140.0", + updateCommand: "npm i -g @openai/codex@latest", + canUpdate: true, + checkedAt: "2026-06-27T07:00:00.000Z", + message: null, + }, + }, + ]) + + expect(getAgentRuntimeProviderUpdateNotice(codex!)).toEqual({ + phase: "available", + tone: "warning", + icon: "warning", + titleKey: "runtime.update.available.title", + bodyKey: "runtime.update.available.body", + values: { + engine: "OpenAI Codex", + currentVersion: "0.139.0", + latestVersion: "0.140.0", + message: "", + }, + action: "runtime", + actionLabelKey: "runtime.openRuntime", + }) + }) + + test("prioritizes active and terminal provider update state over the stale-version advisory", () => { + const [running] = mapRuntimeEnginesToUiDefinitions([ + { + id: "codex", + label: "OpenAI Codex", + availability: "available", + versionAdvisory: { + status: "behind_latest", + currentVersion: "0.139.0", + latestVersion: "0.140.0", + updateCommand: "npm i -g @openai/codex@latest", + canUpdate: true, + checkedAt: "2026-06-27T07:00:00.000Z", + message: null, + }, + updateState: { + status: "running", + startedAt: "2026-06-27T07:01:00.000Z", + finishedAt: null, + message: "Updating provider.", + output: null, + }, + }, + ]) + const [failed] = mapRuntimeEnginesToUiDefinitions([ + { + id: "codex", + label: "OpenAI Codex", + availability: "available", + updateState: { + status: "failed", + startedAt: "2026-06-27T07:01:00.000Z", + finishedAt: "2026-06-27T07:02:00.000Z", + message: "permission denied", + output: "permission denied", + }, + }, + ]) + + expect(isAgentRuntimeUpdateActive(running!)).toBe(true) + expect(getAgentRuntimeProviderUpdateNotice(running!)).toMatchObject({ + phase: "running", + tone: "info", + icon: "loading", + titleKey: "runtime.update.running.title", + }) + expect(getAgentRuntimeProviderUpdateNotice(failed!)).toMatchObject({ + phase: "failed", + tone: "error", + bodyKey: "runtime.update.failed.bodyWithMessage", + values: { + engine: "OpenAI Codex", + currentVersion: "current version", + latestVersion: "latest version", + message: "permission denied", + }, + }) + }) + + test("builds a loading state for every engine before health resolves", () => { + const state = buildRuntimeEngineListState({ isLoading: true }) + + expect(state.kind).toBe("loading") + expect(state.engines).toEqual(AGENT_ENGINE_UI_DEFINITIONS) + expect( + state.engines + .filter((engine) => engine.id !== "custom-acp") + .every((engine) => engine.disabled !== true), + ).toBe(true) + }) + + test("builds an empty state instead of hiding all engines", () => { + const state = buildRuntimeEngineListState({ engines: [] }) + + expect(state.kind).toBe("empty") + expect(state.engines).toHaveLength(AGENT_ENGINE_UI_DEFINITIONS.length) + expect(state.engines.every((engine) => engine.disabled)).toBe(true) + expect(state.engines.map((engine) => engine.statusLabel)).toEqual([ + "No engines", + "No engines", + "No engines", + "No engines", + ]) + }) + + test("builds an error state for every engine when health query fails", () => { + const state = buildRuntimeEngineListState({ + isError: true, + errorMessage: "health endpoint failed", + }) + + expect(state.kind).toBe("error") + expect(state.isError).toBe(true) + expect(state.message).toBe("health endpoint failed") + expect(state.engines.every((engine) => engine.disabled)).toBe(true) + expect(state.engines.map((engine) => engine.statusLabel)).toEqual([ + "Runtime error", + "Runtime error", + "Runtime error", + "Runtime error", + ]) + }) + + test("surfaces fallback list state when any engine has unknown availability", () => { + const state = buildRuntimeEngineListState({ + engines: [ + { + id: "hermes", + label: "Hermes", + availability: "experimental", + }, + ], + }) + + expect(state.kind).toBe("fallback") + expect(state.isFallback).toBe(true) + expect(state.engines[0]).toMatchObject({ + id: "hermes", + disabled: false, + statusLabel: "Fallback", + fallback: true, + }) + }) +}) diff --git a/src/renderer/features/agents/lib/agent-runtime.ts b/src/renderer/features/agents/lib/agent-runtime.ts new file mode 100644 index 000000000..c3a33b6b4 --- /dev/null +++ b/src/renderer/features/agents/lib/agent-runtime.ts @@ -0,0 +1,492 @@ +import { CODEX_DEFAULT_MODEL_ID } from "./models" + +export const AGENT_ENGINE_IDS = [ + "claude-code", + "codex", + "hermes", + "custom-acp", +] as const +export const HERMES_DEFAULT_MODEL_ID = "moss-default" +export const CUSTOM_ACP_DEFAULT_MODEL_ID = "custom-acp" + +export type AgentEngineId = (typeof AGENT_ENGINE_IDS)[number] +export type RunnableAgentEngineId = AgentEngineId +export const DEFAULT_RUNNABLE_AGENT_ENGINE_ID: RunnableAgentEngineId = "hermes" + +export type AgentRuntimeVersionAdvisoryStatus = + | "unknown" + | "current" + | "behind_latest" + +export type AgentRuntimeUpdateStatus = + | "idle" + | "queued" + | "running" + | "succeeded" + | "failed" + | "unchanged" + +export type AgentRuntimeVersionAdvisory = { + status: AgentRuntimeVersionAdvisoryStatus + currentVersion: string | null + latestVersion: string | null + updateCommand: string | null + canUpdate: boolean + checkedAt: string | null + message: string | null +} + +export type AgentRuntimeUpdateState = { + status: AgentRuntimeUpdateStatus + startedAt: string | null + finishedAt: string | null + message: string | null + output: string | null +} + +export type AgentRuntimeProviderInstanceStatus = + | "ready" + | "warning" + | "error" + | "disabled" + +export type AgentRuntimeProviderInstanceLike = { + instanceId: string + engineId?: string + displayName?: string + enabled: boolean + installed: boolean + status: AgentRuntimeProviderInstanceStatus + isDefault?: boolean + modelIds?: string[] + version?: string | null + versionAdvisory?: AgentRuntimeVersionAdvisory | null + updateState?: AgentRuntimeUpdateState | null +} + +export type AgentRuntimeProviderUpdateNoticePhase = + | "available" + | "queued" + | "running" + | "succeeded" + | "failed" + | "unchanged" + +export type AgentRuntimeProviderUpdateNoticeTone = + | "info" + | "warning" + | "error" + | "muted" + +export type AgentRuntimeProviderUpdateNotice = { + phase: AgentRuntimeProviderUpdateNoticePhase + tone: AgentRuntimeProviderUpdateNoticeTone + icon: "loading" | "warning" | "settings" + titleKey: string + bodyKey: string + values?: Record + action: "models" | "runtime" + actionLabelKey: string +} + +export type AgentEngineUiDefinition = { + id: AgentEngineId + name: string + disabled?: boolean + availability?: string + statusLabel?: string + statusReason?: string + authMethod?: string + defaultModelLabel?: string + fallback?: boolean + version?: string | null + versionAdvisory?: AgentRuntimeVersionAdvisory | null + updateState?: AgentRuntimeUpdateState | null + providerInstances?: AgentRuntimeProviderInstanceLike[] +} + +export type RuntimeAvailability = + | "available" + | "needs-auth" + | "not-installed" + | "unsupported" + | "error" + +export type AgentRuntimeEngineListKind = + | "ready" + | "loading" + | "empty" + | "error" + | "fallback" + +export type AgentRuntimeEngineListState = { + kind: AgentRuntimeEngineListKind + engines: AgentEngineUiDefinition[] + message?: string + isLoading: boolean + isError: boolean + isFallback: boolean +} + +export type RuntimeEngineManifestLike = { + id: string + label?: string + availability?: unknown + statusReason?: unknown + authMethod?: unknown + defaultModelId?: string | null + version?: string | null + versionAdvisory?: AgentRuntimeVersionAdvisory | null + updateState?: AgentRuntimeUpdateState | null + providerInstances?: AgentRuntimeProviderInstanceLike[] +} + +const AVAILABILITY_LABELS: Record = { + available: "Available", + "needs-auth": "Needs auth", + "not-installed": "Not installed", + unsupported: "Unsupported", + error: "Error", +} + +export const AGENT_ENGINE_UI_DEFINITIONS: AgentEngineUiDefinition[] = [ + { + id: "claude-code", + name: "Claude Code", + defaultModelLabel: "opus", + }, + { + id: "codex", + name: "OpenAI Codex", + defaultModelLabel: CODEX_DEFAULT_MODEL_ID, + }, + { + id: "hermes", + name: "Hermes", + defaultModelLabel: HERMES_DEFAULT_MODEL_ID, + }, + { + id: "custom-acp", + name: "Custom ACP", + disabled: true, + availability: "unsupported", + statusLabel: "Unsupported", + statusReason: + "Configure a custom ACP adapter before using this engine.", + defaultModelLabel: CUSTOM_ACP_DEFAULT_MODEL_ID, + }, +] + +const RUNTIME_MODEL_LABELS: Record = { + [HERMES_DEFAULT_MODEL_ID]: "Moss Default", + [CUSTOM_ACP_DEFAULT_MODEL_ID]: "Custom ACP Default", +} + +function withRuntimeStatus( + engine: AgentEngineUiDefinition, + status: { + availability: string + statusLabel: string + statusReason: string + disabled?: boolean + fallback?: boolean + }, +): AgentEngineUiDefinition { + return { + ...engine, + availability: status.availability, + statusLabel: status.statusLabel, + statusReason: status.statusReason, + disabled: status.disabled ?? true, + fallback: status.fallback, + } +} + +export function buildStaticRuntimeStatusEngines( + status: { + availability: string + statusLabel: string + statusReason: string + disabled?: boolean + }, +): AgentEngineUiDefinition[] { + return AGENT_ENGINE_UI_DEFINITIONS.map((engine) => + withRuntimeStatus(engine, status), + ) +} + +export function isRunnableAgentEngineId( + engineId: unknown, +): engineId is RunnableAgentEngineId { + return AGENT_ENGINE_IDS.includes(engineId as AgentEngineId) +} + +export function isRuntimeAvailability( + value: unknown, +): value is RuntimeAvailability { + return ( + value === "available" || + value === "needs-auth" || + value === "not-installed" || + value === "unsupported" || + value === "error" + ) +} + +export function isEngineDisabled(availability: RuntimeAvailability): boolean { + return availability !== "available" +} + +export function mapRuntimeEnginesToUiDefinitions( + engines: RuntimeEngineManifestLike[] | undefined, +): AgentEngineUiDefinition[] { + if (!engines) return AGENT_ENGINE_UI_DEFINITIONS + + return engines.map((engine) => { + const fallback = AGENT_ENGINE_UI_DEFINITIONS.find( + (item) => item.id === engine.id, + ) + const runtimeAvailability: RuntimeAvailability | undefined = + isRuntimeAvailability(engine.availability) ? engine.availability : undefined + const fallbackAvailability: RuntimeAvailability = fallback?.disabled + ? "unsupported" + : "available" + const availability: RuntimeAvailability = + runtimeAvailability ?? fallbackAvailability + const isFallback = + runtimeAvailability === undefined && engine.availability !== undefined + + return { + id: engine.id as AgentEngineId, + name: engine.label || fallback?.name || engine.id, + disabled: isEngineDisabled(availability), + availability, + statusLabel: + isFallback + ? "Fallback" + : availability === "available" + ? undefined + : AVAILABILITY_LABELS[availability], + statusReason: + typeof engine.statusReason === "string" + ? engine.statusReason + : isFallback + ? "Moss runtime returned an unknown availability value; using the static engine fallback." + : undefined, + authMethod: + typeof engine.authMethod === "string" + ? engine.authMethod + : undefined, + defaultModelLabel: + engine.defaultModelId || fallback?.defaultModelLabel, + fallback: isFallback || undefined, + version: engine.version ?? undefined, + versionAdvisory: engine.versionAdvisory ?? undefined, + updateState: engine.updateState ?? undefined, + providerInstances: engine.providerInstances ?? undefined, + } + }) +} + +export function isAgentRuntimeUpdateActive( + engine: Pick, +): boolean { + return ( + engine.updateState?.status === "queued" || + engine.updateState?.status === "running" + ) +} + +export function isAgentRuntimeUpdateCandidate( + engine: Pick, +): boolean { + return ( + engine.disabled !== true && + engine.versionAdvisory?.status === "behind_latest" && + typeof engine.versionAdvisory.latestVersion === "string" && + engine.versionAdvisory.latestVersion.trim().length > 0 + ) +} + +function providerUpdateValues( + engine: Pick< + AgentEngineUiDefinition, + "name" | "versionAdvisory" | "updateState" + >, +): Record { + return { + engine: engine.name, + currentVersion: + engine.versionAdvisory?.currentVersion || "current version", + latestVersion: + engine.versionAdvisory?.latestVersion || "latest version", + message: + engine.updateState?.message || + engine.versionAdvisory?.message || + "", + } +} + +export function getAgentRuntimeProviderUpdateNotice( + engine: AgentEngineUiDefinition | undefined, +): AgentRuntimeProviderUpdateNotice | null { + if (!engine) return null + + const values = providerUpdateValues(engine) + switch (engine.updateState?.status) { + case "queued": + return { + phase: "queued", + tone: "info", + icon: "loading", + titleKey: "runtime.update.queued.title", + bodyKey: "runtime.update.queued.body", + values, + action: "runtime", + actionLabelKey: "runtime.openRuntime", + } + case "running": + return { + phase: "running", + tone: "info", + icon: "loading", + titleKey: "runtime.update.running.title", + bodyKey: "runtime.update.running.body", + values, + action: "runtime", + actionLabelKey: "runtime.openRuntime", + } + case "failed": + return { + phase: "failed", + tone: "error", + icon: "warning", + titleKey: "runtime.update.failed.title", + bodyKey: values.message + ? "runtime.update.failed.bodyWithMessage" + : "runtime.update.failed.body", + values, + action: "runtime", + actionLabelKey: "runtime.openRuntime", + } + case "unchanged": + return { + phase: "unchanged", + tone: "warning", + icon: "warning", + titleKey: "runtime.update.unchanged.title", + bodyKey: "runtime.update.unchanged.body", + values, + action: "runtime", + actionLabelKey: "runtime.openRuntime", + } + case "succeeded": + return { + phase: "succeeded", + tone: "muted", + icon: "settings", + titleKey: "runtime.update.succeeded.title", + bodyKey: "runtime.update.succeeded.body", + values, + action: "runtime", + actionLabelKey: "runtime.openRuntime", + } + default: + break + } + + if (!isAgentRuntimeUpdateCandidate(engine)) return null + + return { + phase: "available", + tone: "warning", + icon: "warning", + titleKey: engine.versionAdvisory?.canUpdate + ? "runtime.update.available.title" + : "runtime.update.manual.title", + bodyKey: engine.versionAdvisory?.canUpdate + ? "runtime.update.available.body" + : "runtime.update.manual.body", + values, + action: "runtime", + actionLabelKey: "runtime.openRuntime", + } +} + +export function buildRuntimeEngineListState(params: { + engines?: RuntimeEngineManifestLike[] + isLoading?: boolean + isError?: boolean + errorMessage?: string +}): AgentRuntimeEngineListState { + if (params.isLoading && !params.engines) { + return { + kind: "loading", + engines: mapRuntimeEnginesToUiDefinitions(undefined), + message: "Checking agent runtimes...", + isLoading: true, + isError: false, + isFallback: false, + } + } + + if (params.isError) { + return { + kind: "error", + engines: buildStaticRuntimeStatusEngines({ + availability: "error", + statusLabel: "Runtime error", + statusReason: + params.errorMessage || + "Moss could not read agent runtime health.", + }), + message: + params.errorMessage || + "Moss could not read agent runtime health.", + isLoading: false, + isError: true, + isFallback: false, + } + } + + if (params.engines && params.engines.length === 0) { + return { + kind: "empty", + engines: buildStaticRuntimeStatusEngines({ + availability: "empty", + statusLabel: "No engines", + statusReason: "Moss runtime returned no engine manifests.", + }), + message: "Moss runtime returned no engine manifests.", + isLoading: false, + isError: false, + isFallback: false, + } + } + + const engines = mapRuntimeEnginesToUiDefinitions(params.engines) + const hasFallback = engines.some((engine) => engine.fallback) + + return { + kind: hasFallback ? "fallback" : "ready", + engines, + message: hasFallback + ? "One or more engines are using static fallback metadata." + : undefined, + isLoading: false, + isError: false, + isFallback: hasFallback, + } +} + +export function getAgentEngineLabel(engineId: AgentEngineId): string { + return ( + AGENT_ENGINE_UI_DEFINITIONS.find((engine) => engine.id === engineId) + ?.name ?? engineId + ) +} + +export function formatRuntimeModelLabel(modelId?: string | null): string { + if (!modelId) return "Runtime Default" + return RUNTIME_MODEL_LABELS[modelId] ?? modelId +} diff --git a/src/renderer/features/agents/lib/models.test.ts b/src/renderer/features/agents/lib/models.test.ts new file mode 100644 index 000000000..f86815148 --- /dev/null +++ b/src/renderer/features/agents/lib/models.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "bun:test" + +import { + CODEX_MODELS, + CODEX_DEFAULT_REASONING_EFFORT, + CODEX_DEFAULT_MODEL_ID, + CODEX_REASONING_EFFORTS, + formatCodexThinkingLabel, + getCodexDefaultThinkingLevel, + isCodexThinkingLevel, +} from "./models" + +describe("Codex model reasoning contract", () => { + test("matches the Codex Desktop reasoning effort order exposed in the model picker", () => { + expect(CODEX_DEFAULT_REASONING_EFFORT).toBe("medium") + expect(CODEX_DEFAULT_MODEL_ID).toBe("gpt-5.5/medium") + expect([...CODEX_REASONING_EFFORTS]).toEqual([ + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", + ]) + + for (const model of CODEX_MODELS) { + expect(model.thinkings).toEqual([...CODEX_REASONING_EFFORTS]) + expect(getCodexDefaultThinkingLevel(model.thinkings)).toBe("medium") + } + }) + + test("formats reasoning efforts with Codex Desktop picker labels", () => { + expect(formatCodexThinkingLabel("minimal")).toBe("Minimal") + expect(formatCodexThinkingLabel("low")).toBe("Low") + expect(formatCodexThinkingLabel("medium")).toBe("Medium") + expect(formatCodexThinkingLabel("high")).toBe("High") + expect(formatCodexThinkingLabel("xhigh")).toBe("Extra High") + expect(formatCodexThinkingLabel("max")).toBe("Max") + }) + + test("guards persisted reasoning preferences before transport model resolution", () => { + for (const effort of CODEX_REASONING_EFFORTS) { + expect(isCodexThinkingLevel(effort)).toBe(true) + } + + expect(isCodexThinkingLevel("none")).toBe(false) + expect(isCodexThinkingLevel("extra-high")).toBe(false) + expect(isCodexThinkingLevel(null)).toBe(false) + }) +}) diff --git a/src/renderer/features/agents/lib/models.ts b/src/renderer/features/agents/lib/models.ts index 26fd580e4..783a2a0bc 100644 --- a/src/renderer/features/agents/lib/models.ts +++ b/src/renderer/features/agents/lib/models.ts @@ -4,32 +4,71 @@ export const CLAUDE_MODELS = [ { id: "haiku", name: "Haiku", version: "4.5" }, ] -export type CodexThinkingLevel = "low" | "medium" | "high" | "xhigh" +export const CODEX_REASONING_EFFORTS = [ + "minimal", + "low", + "medium", + "high", + "xhigh", + "max", +] as const + +export type CodexThinkingLevel = (typeof CODEX_REASONING_EFFORTS)[number] + +export const CODEX_DEFAULT_REASONING_EFFORT = "medium" satisfies CodexThinkingLevel +export const CODEX_DEFAULT_MODEL_ID = `gpt-5.5/${CODEX_DEFAULT_REASONING_EFFORT}` + +function createCodexThinkingLevels(): CodexThinkingLevel[] { + return [...CODEX_REASONING_EFFORTS] +} + +export function getCodexDefaultThinkingLevel( + thinkings: readonly CodexThinkingLevel[], +): CodexThinkingLevel { + return thinkings.includes(CODEX_DEFAULT_REASONING_EFFORT) + ? CODEX_DEFAULT_REASONING_EFFORT + : thinkings[0]! +} export const CODEX_MODELS = [ { - id: "gpt-5.3-codex", - name: "Codex 5.3", - thinkings: ["low", "medium", "high", "xhigh"] as CodexThinkingLevel[], + id: "gpt-5.5", + name: "GPT 5.5", + thinkings: createCodexThinkingLevels(), }, { - id: "gpt-5.2-codex", - name: "Codex 5.2", - thinkings: ["low", "medium", "high", "xhigh"] as CodexThinkingLevel[], + id: "gpt-5.4", + name: "GPT 5.4", + thinkings: createCodexThinkingLevels(), }, { - id: "gpt-5.1-codex-max", - name: "Codex 5.1 Max", - thinkings: ["low", "medium", "high", "xhigh"] as CodexThinkingLevel[], + id: "gpt-5.4-mini", + name: "GPT 5.4 Mini", + thinkings: createCodexThinkingLevels(), }, { - id: "gpt-5.1-codex-mini", - name: "Codex 5.1 Mini", - thinkings: ["medium", "high"] as CodexThinkingLevel[], + id: "gpt-5.2", + name: "GPT 5.2", + thinkings: createCodexThinkingLevels(), }, ] +export function isCodexThinkingLevel(value: unknown): value is CodexThinkingLevel { + return ( + typeof value === "string" && + CODEX_REASONING_EFFORTS.includes(value as CodexThinkingLevel) + ) +} + export function formatCodexThinkingLabel(thinking: CodexThinkingLevel): string { - if (thinking === "xhigh") return "Extra High" - return thinking.charAt(0).toUpperCase() + thinking.slice(1) + switch (thinking) { + case "minimal": + return "Minimal" + case "xhigh": + return "Extra High" + case "max": + return "Max" + default: + return thinking.charAt(0).toUpperCase() + thinking.slice(1) + } } diff --git a/src/renderer/features/agents/lib/shared-agent-support.ts b/src/renderer/features/agents/lib/shared-agent-support.ts new file mode 100644 index 000000000..a447b2867 --- /dev/null +++ b/src/renderer/features/agents/lib/shared-agent-support.ts @@ -0,0 +1,27 @@ +import { + AGENT_ENGINE_UI_DEFINITIONS, + isRunnableAgentEngineId, + type RunnableAgentEngineId, +} from "./agent-runtime" + +export const SHARED_RUNNABLE_AGENT_IDS = [ + "hermes", + "claude-code", + "codex", + "custom-acp", +] as const satisfies readonly RunnableAgentEngineId[] + +export const SHARED_RUNNABLE_AGENT_SUPPORT_KEY = + SHARED_RUNNABLE_AGENT_IDS.join(",") + +const SHARED_AGENT_LABELS_BY_ID = new Map( + AGENT_ENGINE_UI_DEFINITIONS + .filter((engine) => isRunnableAgentEngineId(engine.id)) + .map((engine) => [engine.id, engine.name]), +) + +export function getSharedRunnableAgentLabels(): string[] { + return SHARED_RUNNABLE_AGENT_IDS.map( + (engineId) => SHARED_AGENT_LABELS_BY_ID.get(engineId) ?? engineId, + ) +} diff --git a/src/renderer/features/agents/mentions/agents-mentions-editor.tsx b/src/renderer/features/agents/mentions/agents-mentions-editor.tsx index 548334e42..7895304b2 100644 --- a/src/renderer/features/agents/mentions/agents-mentions-editor.tsx +++ b/src/renderer/features/agents/mentions/agents-mentions-editor.tsx @@ -1,6 +1,7 @@ "use client" import { cn } from "../../../lib/utils" +import { detectSlashCommandTextTrigger } from "../commands/slash-command-trigger" import { forwardRef, useCallback, @@ -29,7 +30,7 @@ export interface FileMentionOption { description?: string // skill/agent/tool description tools?: string[] // agent allowed tools model?: string // agent model - source?: "user" | "project" // skill/agent source + source?: "moss" | "user" | "project" | "plugin" // skill/agent source mcpServer?: string // MCP server name for tools } @@ -471,10 +472,13 @@ function walkTreeOnce(root: HTMLElement, range: Range | null): TreeWalkResult { } } - // Validate / trigger - check if space/newline after it + // Validate / trigger using the same line-scoped parser as command search tests. if (slashIndex !== -1) { - const afterSlash = textBeforeCursor.slice(slashIndex + 1) - if (afterSlash.includes(" ") || afterSlash.includes("\n")) { + const slashTrigger = detectSlashCommandTextTrigger( + textBeforeCursor, + textBeforeCursor.length, + ) + if (!slashTrigger || slashTrigger.rangeStart !== slashIndex) { slashIndex = -1 slashPosition = null } @@ -1365,7 +1369,7 @@ export const AgentsMentionsEditor = memo( return (

{!hasContent && placeholder && ( -
+
{placeholder}
)} @@ -1383,8 +1387,9 @@ export const AgentsMentionsEditor = memo( }} onFocus={onFocus} onBlur={onBlur} + data-codex-composer-editor className={cn( - "min-h-[24px] outline-none whitespace-pre-wrap break-words text-sm relative", + "codex-composer-editor relative min-h-[24px] whitespace-pre-wrap break-words outline-none", disabled && "opacity-50 cursor-not-allowed", className, )} diff --git a/src/renderer/features/plugins/plugin-agent-support.tsx b/src/renderer/features/plugins/plugin-agent-support.tsx new file mode 100644 index 000000000..ff3f419e3 --- /dev/null +++ b/src/renderer/features/plugins/plugin-agent-support.tsx @@ -0,0 +1,52 @@ +import { + SHARED_RUNNABLE_AGENT_IDS, + SHARED_RUNNABLE_AGENT_SUPPORT_KEY, + getSharedRunnableAgentLabels, +} from "../agents/lib/shared-agent-support" + +export const PLUGIN_RUNNABLE_AGENT_IDS = SHARED_RUNNABLE_AGENT_IDS + +export const PLUGIN_RUNNABLE_AGENT_SUPPORT_KEY = + SHARED_RUNNABLE_AGENT_SUPPORT_KEY + +export function getPluginSupportedAgentLabels(): string[] { + return getSharedRunnableAgentLabels() +} + +export function PluginAgentSupportPills({ + className, + compact = false, + idPrefix, +}: { + className?: string + compact?: boolean + idPrefix: string +}) { + const classes = [ + "codex-plugin-agent-strip", + compact ? "codex-plugin-agent-strip-compact" : null, + className, + ] + .filter(Boolean) + .join(" ") + + return ( +
+ {PLUGIN_RUNNABLE_AGENT_IDS.map((engineId) => ( + + {getSharedRunnableAgentLabels()[ + PLUGIN_RUNNABLE_AGENT_IDS.indexOf(engineId) + ] ?? engineId} + + ))} +
+ ) +} diff --git a/src/renderer/features/plugins/plugin-entry-surfaces.test.ts b/src/renderer/features/plugins/plugin-entry-surfaces.test.ts new file mode 100644 index 000000000..291e7e1bd --- /dev/null +++ b/src/renderer/features/plugins/plugin-entry-surfaces.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, test } from "bun:test" +import { readFile } from "node:fs/promises" +import { + buildPluginEntrySurfaceRoute, + coercePluginManageTabForSurface, + getDesktopViewForSettingsIntegration, + getPluginEntrySurfaceDefaults, + getPluginEntrySurfaceForDesktopView, + getPluginEntrySurfaceManageTabIds, +} from "./plugin-entry-surfaces" + +describe("Codex plugin entry surfaces", () => { + test("keeps Settings integrations inside the Settings page", () => { + expect(getDesktopViewForSettingsIntegration("plugins")).toBeNull() + expect(getDesktopViewForSettingsIntegration("skills")).toBeNull() + expect(getDesktopViewForSettingsIntegration("mcp")).toBeNull() + expect(getDesktopViewForSettingsIntegration("preferences")).toBeNull() + }) + + test("keeps Plugins, Skills, and MCP as separate page contracts", () => { + expect(getPluginEntrySurfaceForDesktopView("plugins")).toBe("plugins") + expect(getPluginEntrySurfaceForDesktopView("skills")).toBe("skills") + expect(getPluginEntrySurfaceForDesktopView("mcp-settings")).toBe( + "mcp-settings", + ) + + expect(getPluginEntrySurfaceDefaults("plugins")).toEqual({ + pageTab: "plugins", + entryMode: "browse", + manageTab: "plugins", + }) + expect(getPluginEntrySurfaceDefaults("skills")).toEqual({ + pageTab: "skills", + entryMode: "browse", + manageTab: "skills", + }) + expect(getPluginEntrySurfaceDefaults("mcp-settings")).toEqual({ + pageTab: "plugins", + entryMode: "manage", + manageTab: "mcps", + }) + }) + + test("allows the full Plugins page to inspect all managed resource tabs", () => { + expect(getPluginEntrySurfaceManageTabIds("plugins")).toEqual([ + "plugins", + "apps", + "mcps", + "skills", + "marketplace", + ]) + expect(getPluginEntrySurfaceManageTabIds("skills")).toEqual(["skills"]) + expect(getPluginEntrySurfaceManageTabIds("mcp-settings")).toEqual(["mcps"]) + + expect(coercePluginManageTabForSurface("plugins", "mcps")).toBe("mcps") + expect(coercePluginManageTabForSurface("plugins", "skills")).toBe("skills") + expect(coercePluginManageTabForSurface("skills", "plugins")).toBe("skills") + expect(coercePluginManageTabForSurface("mcp-settings", "skills")).toBe( + "mcps", + ) + }) + + test("builds MCP as a manage route without reusing the Plugins surface", () => { + expect(buildPluginEntrySurfaceRoute("mcp-settings")).toMatchObject({ + surface: "manage", + topTab: "plugins", + entryMode: "manage", + manageTab: "mcps", + }) + expect(buildPluginEntrySurfaceRoute("skills")).toMatchObject({ + surface: "skills", + topTab: "skills", + entryMode: "browse", + }) + }) + + test("resets plugin entry state when the sidebar reopens the same surface", async () => { + const source = await readFile( + new URL("./plugin-entry-view.tsx", import.meta.url), + "utf8", + ) + + expect(source).toContain("pluginEntryNavigationNonceAtom") + expect(source).toContain("const resetPluginEntrySurface = useCallback") + expect(source).toContain("setSelectedPlugin(null)") + expect(source).toContain("setPluginRouteState(buildPluginEntrySurfaceRoute(surface))") + expect(source).toContain("[pluginEntryNavigationNonce, resetPluginEntrySurface]") + }) + + test("keeps plugin detail resource actions wired", async () => { + const source = await readFile( + new URL("./plugin-entry-view.tsx", import.meta.url), + "utf8", + ) + + expect(source).toContain('data-plugin-detail-resource-action="configure-mcp"') + expect(source).toContain('onConfigureMcp={() => openPluginManageTab("mcps")}') + expect(source).toContain('data-plugin-detail-resource-action="toggle-skill"') + expect(source).toContain('data-plugin-detail-copy-state={copiedLink ? "copied" : "idle"}') + expect(source).toContain("toggleCatalogPluginSkill(selectedPlugin, skillName, nextEnabled)") + expect(source).toContain("getDetailSkillKey(plugin.id, skillName)") + }) +}) diff --git a/src/renderer/features/plugins/plugin-entry-surfaces.ts b/src/renderer/features/plugins/plugin-entry-surfaces.ts new file mode 100644 index 000000000..b9ccb3845 --- /dev/null +++ b/src/renderer/features/plugins/plugin-entry-surfaces.ts @@ -0,0 +1,90 @@ +import type { SettingsTab } from "../../lib/atoms" +import type { DesktopView } from "../agents/atoms" +import { + buildPluginBrowseRoute, + type PluginEntryMode, + type PluginManageTabId, + type PluginRouteState, + type PluginTopTabId, +} from "./plugin-route-state" + +export type PluginEntrySurface = "plugins" | "skills" | "mcp-settings" + +export type PluginEntrySurfaceDefaults = { + pageTab: PluginTopTabId + entryMode: PluginEntryMode + manageTab: PluginManageTabId +} + +const PLUGIN_ENTRY_SURFACE_DEFAULTS: Record< + PluginEntrySurface, + PluginEntrySurfaceDefaults +> = { + plugins: { + pageTab: "plugins", + entryMode: "browse", + manageTab: "plugins", + }, + skills: { + pageTab: "skills", + entryMode: "browse", + manageTab: "skills", + }, + "mcp-settings": { + pageTab: "plugins", + entryMode: "manage", + manageTab: "mcps", + }, +} + +const PLUGIN_ENTRY_SURFACE_MANAGE_TABS: Record< + PluginEntrySurface, + PluginManageTabId[] +> = { + plugins: ["plugins", "apps", "mcps", "skills", "marketplace"], + skills: ["skills"], + "mcp-settings": ["mcps"], +} + +export function getPluginEntrySurfaceDefaults( + surface: PluginEntrySurface, +): PluginEntrySurfaceDefaults { + return PLUGIN_ENTRY_SURFACE_DEFAULTS[surface] +} + +export function buildPluginEntrySurfaceRoute( + surface: PluginEntrySurface, +): PluginRouteState { + return buildPluginBrowseRoute(getPluginEntrySurfaceDefaults(surface)) +} + +export function getPluginEntrySurfaceManageTabIds( + surface: PluginEntrySurface, +): PluginManageTabId[] { + return PLUGIN_ENTRY_SURFACE_MANAGE_TABS[surface] +} + +export function coercePluginManageTabForSurface( + surface: PluginEntrySurface, + tab: PluginManageTabId, +): PluginManageTabId { + const allowedTabs = getPluginEntrySurfaceManageTabIds(surface) + return allowedTabs.includes(tab) ? tab : allowedTabs[0] +} + +export function getPluginEntrySurfaceForDesktopView( + view: DesktopView, +): PluginEntrySurface | null { + if (view === "plugins") return "plugins" + if (view === "skills") return "skills" + if (view === "mcp-settings") return "mcp-settings" + return null +} + +export function getDesktopViewForSettingsIntegration( + _tab: SettingsTab, +): DesktopView { + // Settings integration tabs render inside SettingsContent. Standalone + // plugin/skill/MCP pages are opened from the main sidebar and deep links. + return null +} diff --git a/src/renderer/features/plugins/plugin-entry-view.tsx b/src/renderer/features/plugins/plugin-entry-view.tsx new file mode 100644 index 000000000..c9caa8476 --- /dev/null +++ b/src/renderer/features/plugins/plugin-entry-view.tsx @@ -0,0 +1,2741 @@ +"use client" + +import type { CSSProperties, KeyboardEvent, ReactNode } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { useAtom, useAtomValue, useSetAtom } from "jotai" +import { + ArrowRight, + Check, + ChevronLeft, + ChevronDown, + ChevronRight, + ExternalLink, + MessageCircle, + MoreHorizontal, + PanelLeftClose, + Plus, + RefreshCw, + Search, + Settings, +} from "lucide-react" +import computerUsePluginIcon from "../../assets/app-icons/computer-use-plugin-icon.png" +import activelyMarketplaceTile from "../../assets/plugin-icons/codex-marketplace-actively.png" +import connectedChromeTile from "../../assets/plugin-icons/codex-connected-chrome.png" +import connectedCodexLabsTile from "../../assets/plugin-icons/codex-connected-codex-labs.png" +import connectedCursorTile from "../../assets/plugin-icons/codex-connected-cursor.png" +import connectedDocsTile from "../../assets/plugin-icons/codex-connected-docs.png" +import connectedGmailTile from "../../assets/plugin-icons/codex-connected-gmail.png" +import connectedHuggingFaceTile from "../../assets/plugin-icons/codex-connected-huggingface.png" +import connectedLinearTile from "../../assets/plugin-icons/codex-connected-linear.png" +import connectedMoreTile from "../../assets/plugin-icons/codex-connected-more.png" +import connectedPluginGridTile from "../../assets/plugin-icons/codex-connected-plugin-grid.png" +import connectedPluginStoreTile from "../../assets/plugin-icons/codex-connected-plugin-store.png" +import connectedPurpleDotsTile from "../../assets/plugin-icons/codex-connected-purple-dots.png" +import connectedSheetsTile from "../../assets/plugin-icons/codex-connected-sheets.png" +import connectedSlidesTile from "../../assets/plugin-icons/codex-connected-slides.png" +import connectedStripedTile from "../../assets/plugin-icons/codex-connected-striped.png" +import pluginDetailGradient from "../../assets/plugin-icons/codex-plugin-detail-gradient.png" +import recordReplayPluginIcon from "../../assets/plugin-icons/codex-record-replay-plugin-icon.png" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "../../components/ui/dropdown-menu" +import { useI18n } from "../../lib/i18n" +import { trpc } from "../../lib/trpc" +import { cn } from "../../lib/utils" +import { PluginAgentSupportPills } from "./plugin-agent-support" +import { + agentsSidebarOpenAtom, + desktopViewAtom, + pendingPluginActionAtom, + pluginEntryNavigationNonceAtom, + selectedAgentChatIdAtom, + selectedDraftIdAtom, + selectedProjectAtom, + showNewChatFormAtom, +} from "../agents/atoms" +import { buildPluginDeepLinkUrl } from "../../../shared/plugin-deep-link" +import { generateDraftId, saveNewChatDraft } from "../agents/lib/drafts" +import { + buildPluginBrowseRoute, + cancelPluginUninstallRoute, + closePluginOverlayRoute, + finishPluginInstallRoute, + finishPluginUninstallRoute, + getPluginRouteDialogKind, + getPluginRouteOrigin, + getPluginRouteOriginFromState, + openPluginResourceRoute, + openPluginDetailRoute, + requestPluginUninstallRoute, + serializePluginRouteOrigin, + serializePluginRouteState, + startPluginInstallRoute, + startPluginTryInChatRoute, + startPluginUninstallRoute, + type PluginEntryMode, + type PluginManageTabId, + type PluginRouteState, + type PluginTopTabId, +} from "./plugin-route-state" +import { + BUSINESS_PLUGINS, + CATALOG_PLUGIN_DETAILS, + CONNECTED_PLUGINS, + FEATURED_PLUGINS, + MARKETPLACE_PLUGINS, + PRODUCTIVITY_PLUGINS, + RECOMMENDED_SKILLS, + buildPluginOnboardingSuggestions, + findCatalogPluginById, + findRuntimePluginForCatalog, + formatRuntimeLabel, + canRunManagedResourceAction, + getCatalogPluginDetail, + getManagedResourceRouteTarget, + getManagedResourcesForTab, + getPluginManageTabs, + getRuntimePluginResourceCount, + getRuntimeSkillLogo, + type CatalogPlugin, + type ConnectedPlugin, + type ManagedResource, + type PluginLogoId, + type PluginOnboardingRoleId, + type PluginOnboardingRoleSuggestion, + type RuntimePluginData, + type RuntimeSkillData, +} from "./plugin-resource-model" +import { + buildPluginEntrySurfaceRoute, + coercePluginManageTabForSurface, + getPluginEntrySurfaceDefaults, + getPluginEntrySurfaceManageTabIds, + type PluginEntrySurface, +} from "./plugin-entry-surfaces" + +const APP_PROTOCOL = import.meta.env.DEV + ? "twentyfirst-agents-dev" + : "twentyfirst-agents" + +function getCatalogPluginLink(plugin: CatalogPlugin) { + return buildPluginDeepLinkUrl({ + pluginId: plugin.id, + protocol: APP_PROTOCOL, + }) +} + +async function writeClipboardText(text: string) { + if (window.desktopApi?.clipboardWrite) { + await window.desktopApi.clipboardWrite(text) + return + } + + if (navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(text) + return + } catch { + // Electron CDP can reject Clipboard API writes when the document is not focused. + // Fall through to the selection-based copy path used by desktop shells. + } + } + + const textArea = document.createElement("textarea") + textArea.value = text + textArea.setAttribute("readonly", "true") + textArea.style.position = "fixed" + textArea.style.left = "-9999px" + textArea.style.top = "0" + document.body.append(textArea) + textArea.select() + + try { + if (!document.execCommand("copy")) { + throw new Error("Clipboard copy command failed") + } + } finally { + textArea.remove() + } +} + +function waitForPluginMutationFrame() { + return new Promise((resolve) => window.setTimeout(resolve, 450)) +} + +const CODEX_2026_GOOGLE_LOGO_SVGS: Partial> = { + docs: ``, + sheets: ``, + slides: ``, + gmail: ``, + calendar: ``, + drive: ``, +} + +const CODEX_2026_GOOGLE_LOGO_DATA_URIS = new Map() +const INLINE_GOOGLE_DOCUMENT_LOGOS = new Set(["docs", "sheets", "slides"]) +const CODEX_CONNECTED_TILE_IMAGES: Partial> = { + chrome: connectedChromeTile, + "codex-labs": connectedCodexLabsTile, + cursor: connectedCursorTile, + docs: connectedDocsTile, + gmail: connectedGmailTile, + huggingface: connectedHuggingFaceTile, + linear: connectedLinearTile, + "plugin-grid": connectedPluginGridTile, + "plugin-store": connectedPluginStoreTile, + "purple-dots": connectedPurpleDotsTile, + sheets: connectedSheetsTile, + slides: connectedSlidesTile, + striped: connectedStripedTile, +} + +function PluginFilterIcon() { + return ( + + ) +} + +function getCodex2026GoogleLogoDataUri(id: PluginLogoId) { + if (INLINE_GOOGLE_DOCUMENT_LOGOS.has(id)) { + return null + } + + const cachedLogo = CODEX_2026_GOOGLE_LOGO_DATA_URIS.get(id) + if (cachedLogo) { + return cachedLogo + } + + const source = CODEX_2026_GOOGLE_LOGO_SVGS[id] + if (!source) { + return null + } + + const frame = id === "docs" + ? { x: 0, y: 0, size: 192 } + : { x: 24, y: 24, size: 144 } + const innerLogo = source + .trim() + .replace( + /^]*>/u, + ``, + ) + const dataUri = `data:image/svg+xml,${encodeURIComponent( + `${innerLogo}`, + )}` + CODEX_2026_GOOGLE_LOGO_DATA_URIS.set(id, dataUri) + return dataUri +} + +function PluginLogo({ id }: { id: PluginLogoId }) { + const codex2026GoogleLogo = getCodex2026GoogleLogoDataUri(id) + if (codex2026GoogleLogo) { + return ( + + ) + } + + if (id === "github") { + return ( + + ) + } + + if (id === "slack") { + return ( + + ) + } + + if (id === "gmail") { + return ( + + ) + } + + if (id === "drive") { + return ( + + ) + } + + if (id === "docs") { + return ( + + ) + } + + if (id === "sheets") { + return ( + + ) + } + + if (id === "slides") { + return ( + + ) + } + + if (id === "chrome") { + return ( + + ) + } + + if (id === "cursor") { + return ( + + ) + } + + if (id === "linear") { + return ( + + ) + } + + if (id === "codex-labs") { + return ( + + ) + } + + if (id === "record-replay") { + return ( + + ) + } + + if (id === "skill") { + return ( + + ) + } + + if (id === "purple-dots") { + return ( + + ) + } + + if (id === "calendar") { + return ( + + ) + } + + if (id === "notion") { + return ( + + ) + } + + if (id === "actively") { + return ( + + ) + } + + if (id === "apollo") { + return ( + + ) + } + + if (id === "salesforce") { + return ( + + ) + } + + if (id === "hubspot") { + return ( + + ) + } + + if (id === "huggingface") { + return ( + + ) + } + + if (id === "plugin-store") { + return ( + + ) + } + + if (id === "striped") { + return ( + + ) + } + + if (id === "plugin-grid") { + return ( + + ) + } + + const label = { + "codex-labs": "", + figma: "F", + "purple-dots": "", + }[id] + + return ( + + ) +} + +function PluginConnectedLogo({ id }: { id: PluginLogoId }) { + const referenceTile = CODEX_CONNECTED_TILE_IMAGES[id] + + if (referenceTile) { + return ( + + ) + } + + return +} + +function CatalogPluginRow({ + isInstalled, + plugin, + onAdd, + onOpen, + onTryInChat, +}: { + isInstalled?: boolean + plugin: CatalogPlugin + onAdd: () => void + onOpen: () => void + onTryInChat?: () => void +}) { + const { t } = useI18n() + const actionLabel = isInstalled + ? t("pluginsPage.detail.tryInChat") + : t("pluginsPage.add") + + return ( +
{ + if (event.key === "Enter" || event.key === " ") { + event.preventDefault() + onOpen() + } + }} + > + +
+
{t(plugin.titleKey)}
+
+ {t(plugin.descriptionKey)} +
+ +
+ +
+ ) +} + +function ManagedResourceRow({ + resource, + onAction, +}: { + resource: ManagedResource + onAction?: (resource: ManagedResource) => void +}) { + const { t } = useI18n() + const actionTarget = getManagedResourceRouteTarget(resource) + const actionEnabled = canRunManagedResourceAction(resource) + + return ( +
+ +
+
{resource.title}
+
+ {t(resource.descriptionKey)} +
+ +
+ + {t(resource.statusKey)} + + +
+ ) +} + +function ConnectedPluginRow({ plugin }: { plugin: ConnectedPlugin }) { + const { t } = useI18n() + + return ( +
+ +
+
{plugin.label}
+
+ {t("pluginsPage.manage.connectedDescription")} +
+ +
+ + {t("pluginsPage.status.connected")} + + +
+ ) +} + +function RuntimePluginManageRow({ + catalogPlugin, + onOpenCatalogPlugin, + runtimePlugin, +}: { + catalogPlugin?: CatalogPlugin | null + onOpenCatalogPlugin?: (plugin: CatalogPlugin) => void + runtimePlugin: RuntimePluginData +}) { + const { t } = useI18n() + const title = catalogPlugin + ? t(catalogPlugin.titleKey) + : formatRuntimeLabel(runtimePlugin.name) + const statusKey = runtimePlugin.isDisabled + ? "pluginsPage.status.disabled" + : "pluginsPage.status.enabled" + const resourceCount = getRuntimePluginResourceCount(runtimePlugin) + + return ( +
+
+ ) +} + +function RuntimeSkillRow({ + catalogPlugin, + onTryInChat, + skill, +}: { + catalogPlugin?: CatalogPlugin | null + onTryInChat: (skill: RuntimeSkillData) => void + skill: RuntimeSkillData +}) { + const { t } = useI18n() + const title = formatRuntimeLabel(skill.name) + const description = + skill.description || skill.path || t("pluginsPage.skills.noDescription") + + return ( +
+ +
+
{title}
+
+ {description} +
+ +
+ + {t("pluginsPage.status.enabled")} + + +
+ ) +} + +function RuntimeSkillEmptyRow({ isLoading }: { isLoading: boolean }) { + const { t } = useI18n() + + return ( +
+ +
+
+ {isLoading + ? t("pluginsPage.skills.loading") + : t("pluginsPage.skills.empty.title")} +
+
+ {isLoading + ? t("pluginsPage.skills.loadingDescription") + : t("pluginsPage.skills.empty.description")} +
+
+
+ ) +} + +function PluginDetailResourceSection({ + disabledItems, + descriptions, + id, + items, + logo, + onMcpConfigure, + onResourceOpen, + onSkillToggle, + statusLabel, + title, + variant = "default", +}: { + disabledItems?: ReadonlySet + descriptions?: Record + id: string + items: string[] + logo: PluginLogoId + onMcpConfigure?: () => void + onResourceOpen?: (item: string) => void + onSkillToggle?: (item: string, nextEnabled: boolean) => void + statusLabel?: string + title: string + variant?: "apps" | "default" | "mcps" | "skills" +}) { + const { t } = useI18n() + + if (items.length === 0) return null + + return ( +
+
+

{title}

+ {items.length} +
+
+ {items.map((item) => { + const description = descriptions?.[item] + const skillEnabled = !disabledItems?.has(item) + const resourceCopyProps = + variant === "skills" && onResourceOpen + ? { + onClick: () => onResourceOpen(item), + onKeyDown: (event: KeyboardEvent) => { + if (event.key !== "Enter" && event.key !== " ") return + event.preventDefault() + onResourceOpen(item) + }, + role: "button", + tabIndex: 0, + } + : {} + + return ( +
+ +
+
{item}
+ {description ? ( +

{description}

+ ) : null} +
+ {statusLabel ? ( + {statusLabel} + ) : variant === "mcps" ? ( + + ) : variant === "skills" ? ( + + ) : null} +
+ ) + })} +
+
+ ) +} + +function PluginDetailInfoRow({ + children, + label, + value, +}: { + children?: ReactNode + label: string + value?: string | null +}) { + if (!children && !value) return null + + return ( +
+ {label} + {children ?? value} +
+ ) +} + +function PluginDetailInfoExternalLink({ + href, + label, +}: { + href?: string | null + label: string +}) { + if (!href) return null + + return ( +
+ + ) +} + +function PluginDetailPage({ + copiedLink, + disabledSkillNames, + installed, + installing, + mutationError, + onClose, + onConfigureMcp, + onCopyLink, + onOpenSkills, + onInstall, + onStartPrompt, + onToggleSkill, + onTryInChat, + onUninstall, + plugin, + runtimePlugin, + uninstalling, +}: { + copiedLink: boolean + disabledSkillNames?: ReadonlySet + installed: boolean + installing: boolean + mutationError?: string | null + onClose: () => void + onConfigureMcp: () => void + onCopyLink: () => void + onOpenSkills: () => void + onInstall: () => void + onStartPrompt: (promptKey: string) => void + onToggleSkill: (skillName: string, nextEnabled: boolean) => void + onTryInChat: () => void + onUninstall: () => void + plugin: CatalogPlugin + runtimePlugin?: RuntimePluginData | null + uninstalling: boolean +}) { + const { t } = useI18n() + const setSidebarOpen = useSetAtom(agentsSidebarOpenAtom) + const detail = getCatalogPluginDetail(plugin, runtimePlugin) + const title = t(plugin.titleKey) + const promptExampleKeys = + detail.promptExampleKeys && detail.promptExampleKeys.length > 0 + ? detail.promptExampleKeys + : [detail.promptKey] + const longDescription = detail.longDescriptionKey + ? t(detail.longDescriptionKey) + : t(plugin.descriptionKey) + + return ( +
+
+
+ + + +
+
+ +
+
+ +
+
+ + + + + + + {copiedLink + ? t("pluginsPage.detail.copiedLink") + : t("pluginsPage.detail.copyLink")} + + {installed ? ( + + {uninstalling + ? t("pluginsPage.detail.removingFromCodex") + : t("pluginsPage.detail.removeFromCodex")} + + ) : null} + + + +
+ +
+ +

{title}

+

{t(plugin.descriptionKey)}

+
+ +
+ {promptExampleKeys.map((promptKey) => ( + + ))} +
+ +

+ {longDescription} +

+ + {mutationError ? ( +
+ {mutationError} +
+ ) : null} + + + + + +
+

{t("pluginsPage.detail.info")}

+
+ + + + + + + + + + + + +
+
+ +
+
+ ) +} + +function PluginUninstallDialog({ + mutationError, + onCancel, + onConfirm, + plugin, + runtimePlugin, + uninstalling, +}: { + mutationError?: string | null + onCancel: () => void + onConfirm: () => void + plugin: CatalogPlugin + runtimePlugin?: RuntimePluginData | null + uninstalling: boolean +}) { + const { t } = useI18n() + const detail = getCatalogPluginDetail(plugin, runtimePlugin) + const title = t(plugin.titleKey) + const impactItems = [ + { + key: "apps", + label: t("pluginsPage.uninstall.apps"), + value: detail.apps.join(", "), + }, + { + key: "skills", + label: t("pluginsPage.uninstall.skills"), + value: detail.skills.join(", "), + }, + { + key: "mcps", + label: t("pluginsPage.uninstall.mcps"), + value: detail.mcps.join(", "), + }, + ].filter((item) => item.value.length > 0) + + return ( +
{ + if (!uninstalling) onCancel() + }} + > +
event.stopPropagation()} + role="dialog" + > +
+ +
+

+ {t("pluginsPage.uninstall.title", { plugin: title })} +

+

+ {t("pluginsPage.uninstall.body", { plugin: title })} +

+
+
+ +
+
+ {t("pluginsPage.uninstall.impact")} +
+
+ {impactItems.map((item) => ( +
+ {item.label} + {item.value} +
+ ))} +
+
+ + {mutationError ? ( +
+ {mutationError} +
+ ) : null} + +
+ + +
+
+
+ ) +} + +interface PluginEntryViewProps { + ariaLabel?: string + className?: string + surface?: PluginEntrySurface +} + +function getPluginSurfaceTitleKey(surface: PluginEntrySurface) { + if (surface === "skills") return "pluginsPage.skills" + if (surface === "mcp-settings") return "pluginsPage.manage.mcps" + return "pluginsPage.title" +} + +function getPluginSurfaceSubtitleKey( + surface: PluginEntrySurface, + entryMode: PluginEntryMode, +) { + if (surface === "skills") return "pluginsPage.skills.subtitle" + if (surface === "mcp-settings") return "pluginsPage.mcp.subtitle" + if (entryMode === "manage") return "pluginsPage.manage.subtitle" + return "pluginsPage.subtitle" +} + +function getPluginSurfaceSearchKey(surface: PluginEntrySurface) { + if (surface === "skills") return "pluginsPage.skills.search" + if (surface === "mcp-settings") return "pluginsPage.mcp.search" + return "pluginsPage.catalogSearch" +} + +function getDetailSkillKey(pluginId: string, skillName: string) { + return `${pluginId}::${skillName}` +} + +export function PluginEntryView({ + ariaLabel = "Plugin entry page", + className, + surface = "plugins", +}: PluginEntryViewProps) { + const surfaceDefaults = getPluginEntrySurfaceDefaults(surface) + const [pageTab, setPageTab] = useState( + () => surfaceDefaults.pageTab, + ) + const [entryMode, setEntryMode] = useState( + () => surfaceDefaults.entryMode, + ) + const [manageTab, setManageTab] = useState( + () => surfaceDefaults.manageTab, + ) + const [pluginRouteState, setPluginRouteState] = useState( + () => buildPluginEntrySurfaceRoute(surface), + ) + const [selectedPlugin, setSelectedPlugin] = useState(null) + const [installedPluginIds, setInstalledPluginIds] = useState>( + () => new Set(), + ) + const [installingPluginId, setInstallingPluginId] = useState( + null, + ) + const [uninstallCandidate, setUninstallCandidate] = + useState(null) + const [uninstallingPluginId, setUninstallingPluginId] = useState< + string | null + >(null) + const [pluginMutationError, setPluginMutationError] = useState( + null, + ) + const [copiedPluginLinkId, setCopiedPluginLinkId] = useState( + null, + ) + const [disabledDetailSkillKeys, setDisabledDetailSkillKeys] = useState< + Set + >(() => new Set()) + const catalogMainRef = useRef(null) + const [selectedOnboardingRoleId, setSelectedOnboardingRoleId] = + useState("developer") + const { t } = useI18n() + const selectedProject = useAtomValue(selectedProjectAtom) + const pluginEntryNavigationNonce = useAtomValue(pluginEntryNavigationNonceAtom) + const setDesktopView = useSetAtom(desktopViewAtom) + const setSelectedChatId = useSetAtom(selectedAgentChatIdAtom) + const setSelectedDraftId = useSetAtom(selectedDraftIdAtom) + const setShowNewChatForm = useSetAtom(showNewChatFormAtom) + const [pendingPluginAction, setPendingPluginAction] = useAtom( + pendingPluginActionAtom, + ) + const { + data: runtimePlugins = [], + isFetching: isFetchingRuntimePlugins, + refetch: refetchRuntimePlugins, + } = trpc.plugins.list.useQuery(undefined, { + staleTime: 5 * 60 * 1000, + }) + const { + data: runtimeSkills = [], + isFetching: isFetchingRuntimeSkills, + refetch: refetchRuntimeSkills, + } = trpc.skills.list.useQuery( + selectedProject?.path ? { cwd: selectedProject.path } : undefined, + { + staleTime: 5 * 60 * 1000, + }, + ) + const setPluginEnabledMutation = + trpc.claudeSettings.setPluginEnabled.useMutation() + const runtimePluginByCatalogId = useMemo(() => { + const next = new Map() + + for (const plugin of MARKETPLACE_PLUGINS) { + const runtimePlugin = findRuntimePluginForCatalog(plugin, runtimePlugins) + if (runtimePlugin) next.set(plugin.id, runtimePlugin) + } + + return next + }, [runtimePlugins]) + const catalogPluginByRuntimeSource = useMemo(() => { + const next = new Map() + + for (const [catalogPluginId, runtimePlugin] of runtimePluginByCatalogId) { + const catalogPlugin = findCatalogPluginById(catalogPluginId) + if (catalogPlugin) next.set(runtimePlugin.source, catalogPlugin) + } + + return next + }, [runtimePluginByCatalogId]) + const effectiveInstalledPluginIds = useMemo(() => { + const next = new Set(installedPluginIds) + + for (const plugin of CONNECTED_PLUGINS) { + next.add(plugin.id) + } + + for (const [pluginId, runtimePlugin] of runtimePluginByCatalogId) { + if (!runtimePlugin.isDisabled) next.add(pluginId) + } + + return next + }, [installedPluginIds, runtimePluginByCatalogId]) + const selectedPluginRuntimePlugin = selectedPlugin + ? runtimePluginByCatalogId.get(selectedPlugin.id) ?? null + : null + const selectedPluginDisabledSkillNames = useMemo(() => { + const next = new Set() + if (!selectedPlugin) return next + + const detail = getCatalogPluginDetail(selectedPlugin, selectedPluginRuntimePlugin) + for (const skillName of detail.skills) { + if (disabledDetailSkillKeys.has(getDetailSkillKey(selectedPlugin.id, skillName))) { + next.add(skillName) + } + } + return next + }, [disabledDetailSkillKeys, selectedPlugin, selectedPluginRuntimePlugin]) + const uninstallCandidateRuntimePlugin = uninstallCandidate + ? runtimePluginByCatalogId.get(uninstallCandidate.id) ?? null + : null + const pluginManageTabs = useMemo( + () => + getPluginManageTabs({ + runtimePluginCount: runtimePlugins.length, + runtimeSkillCount: runtimeSkills.length, + }), + [runtimePlugins.length, runtimeSkills.length], + ) + const allowedManageTabIds = useMemo( + () => getPluginEntrySurfaceManageTabIds(surface), + [surface], + ) + const visiblePluginManageTabs = useMemo( + () => pluginManageTabs.filter((tab) => allowedManageTabIds.includes(tab.id)), + [allowedManageTabIds, pluginManageTabs], + ) + const pluginOnboardingSuggestions = useMemo( + () => buildPluginOnboardingSuggestions(), + [], + ) + const selectedOnboardingRole = + pluginOnboardingSuggestions.find( + (suggestion) => suggestion.id === selectedOnboardingRoleId, + ) ?? pluginOnboardingSuggestions[0] + const isRefreshingRuntime = isFetchingRuntimePlugins || isFetchingRuntimeSkills + const showOnboardingSuggestions = false + + const resetPluginEntrySurface = useCallback(() => { + const nextDefaults = getPluginEntrySurfaceDefaults(surface) + setPageTab(nextDefaults.pageTab) + setEntryMode(nextDefaults.entryMode) + setManageTab(nextDefaults.manageTab) + setSelectedPlugin(null) + setUninstallCandidate(null) + setCopiedPluginLinkId(null) + setPluginMutationError(null) + setPluginRouteState(buildPluginEntrySurfaceRoute(surface)) + }, [surface]) + + useEffect(() => { + resetPluginEntrySurface() + }, [resetPluginEntrySurface]) + + useEffect(() => { + resetPluginEntrySurface() + }, [pluginEntryNavigationNonce, resetPluginEntrySurface]) + + useEffect(() => { + if ( + pluginRouteState.surface !== "detail" && + pluginRouteState.surface !== "install" && + pluginRouteState.surface !== "uninstall" + ) { + setSelectedPlugin(null) + return + } + + const plugin = findCatalogPluginById(pluginRouteState.pluginId) + if (!plugin) return + + setSelectedPlugin((current) => + current?.id === plugin.id ? current : plugin, + ) + + if (pluginRouteState.surface === "uninstall") { + setUninstallCandidate((current) => + current?.id === plugin.id ? current : plugin, + ) + } + }, [pluginRouteState]) + + useEffect(() => { + if (!selectedPlugin) return + catalogMainRef.current?.scrollTo({ top: 0 }) + }, [selectedPlugin]) + + const openPluginManage = () => { + const nextManageTab = coercePluginManageTabForSurface(surface, "plugins") + setPageTab("plugins") + setEntryMode("manage") + setManageTab(nextManageTab) + setPluginRouteState( + buildPluginBrowseRoute({ + pageTab: "plugins", + entryMode: "manage", + manageTab: nextManageTab, + }), + ) + } + + const openPluginManageTab = (tab: PluginManageTabId) => { + const nextManageTab = coercePluginManageTabForSurface(surface, tab) + setManageTab(nextManageTab) + setPluginRouteState( + buildPluginBrowseRoute({ + pageTab: "plugins", + entryMode: "manage", + manageTab: nextManageTab, + }), + ) + } + + const openPluginTopTab = (tab: PluginTopTabId) => { + if (tab === "skills") { + setPageTab("skills") + setEntryMode("browse") + setManageTab("skills") + setPluginRouteState( + buildPluginBrowseRoute({ + pageTab: "skills", + entryMode: "browse", + manageTab: "skills", + }), + ) + return + } + + setPageTab("plugins") + setEntryMode("browse") + setManageTab("plugins") + setPluginRouteState( + buildPluginBrowseRoute({ + pageTab: "plugins", + entryMode: "browse", + manageTab: "plugins", + }), + ) + } + + const getActivePluginRouteOrigin = () => + getPluginRouteOriginFromState(pluginRouteState) + + const closeCatalogPluginDetail = () => { + setSelectedPlugin(null) + setCopiedPluginLinkId(null) + setPluginMutationError(null) + setPluginRouteState(closePluginOverlayRoute(pluginRouteState)) + } + + const toggleCatalogPluginSkill = ( + plugin: CatalogPlugin, + skillName: string, + nextEnabled: boolean, + ) => { + setPluginMutationError(null) + setDisabledDetailSkillKeys((current) => { + const next = new Set(current) + const key = getDetailSkillKey(plugin.id, skillName) + if (nextEnabled) { + next.delete(key) + } else { + next.add(key) + } + return next + }) + } + + const startNewChatWithText = (prompt: string) => { + const draftId = generateDraftId() + saveNewChatDraft(draftId, prompt) + setSelectedChatId(null) + setSelectedDraftId(draftId) + setShowNewChatForm(false) + setDesktopView(null) + } + + const startNewChatWithPrompt = (promptKey: string) => { + startNewChatWithText(t(promptKey)) + } + + const startRuntimeSkillChat = (skill: RuntimeSkillData) => { + startNewChatWithText( + t("pluginsPage.skills.runtimePrompt", { + skill: formatRuntimeLabel(skill.name), + }), + ) + } + + const startNewSkillChat = () => { + startNewChatWithPrompt("pluginsPage.skills.createPrompt") + } + + const startOnboardingRoleChat = ( + role: PluginOnboardingRoleSuggestion, + ) => { + setPluginMutationError(null) + startNewChatWithPrompt(role.starterPromptKey) + } + + const handleManagedResourceAction = (resource: ManagedResource) => { + const targetId = getManagedResourceRouteTarget(resource) + setPluginMutationError(null) + setPluginRouteState( + openPluginResourceRoute({ + actionId: resource.actionId, + origin: getActivePluginRouteOrigin(), + promptKey: resource.promptKey, + resourceId: resource.id, + resourceKind: resource.kind, + targetId, + }), + ) + + if (resource.actionId === "try-in-chat" && resource.promptKey) { + startNewChatWithPrompt(resource.promptKey) + } + } + + const openCatalogPluginDetail = (plugin: CatalogPlugin) => { + setPluginMutationError(null) + setPluginRouteState(openPluginDetailRoute(plugin.id, getActivePluginRouteOrigin())) + setSelectedPlugin(plugin) + } + + useEffect(() => { + if (!pendingPluginAction) return + + const plugin = findCatalogPluginById(pendingPluginAction.pluginId) + setPendingPluginAction(null) + + if (!plugin) return + + const origin = getPluginRouteOrigin({ + pageTab: "plugins", + entryMode: "browse", + manageTab: "plugins", + }) + + setPluginMutationError(null) + setCopiedPluginLinkId(null) + setPageTab("plugins") + setEntryMode("browse") + setManageTab("plugins") + + if (pendingPluginAction.action === "try-in-chat") { + const detail = CATALOG_PLUGIN_DETAILS[plugin.id] + setSelectedPlugin(null) + setPluginRouteState( + startPluginTryInChatRoute(plugin.id, origin, detail.promptKey), + ) + startNewChatWithPrompt(detail.promptKey) + return + } + + setPluginRouteState( + openPluginDetailRoute(plugin.id, origin), + ) + setSelectedPlugin(plugin) + }, [pendingPluginAction, setPendingPluginAction]) + + const startCatalogPluginChat = (plugin: CatalogPlugin) => { + const detail = CATALOG_PLUGIN_DETAILS[plugin.id] + setPluginRouteState( + startPluginTryInChatRoute( + plugin.id, + getActivePluginRouteOrigin(), + detail.promptKey, + ), + ) + startNewChatWithPrompt(detail.promptKey) + } + + const installCatalogPlugin = async (plugin: CatalogPlugin) => { + const title = t(plugin.titleKey) + const runtimePlugin = runtimePluginByCatalogId.get(plugin.id) ?? null + const origin = getActivePluginRouteOrigin() + setPluginMutationError(null) + setInstallingPluginId(plugin.id) + setPluginRouteState(startPluginInstallRoute(plugin.id, origin)) + + try { + await waitForPluginMutationFrame() + + if (runtimePlugin) { + await setPluginEnabledMutation.mutateAsync({ + pluginSource: runtimePlugin.source, + enabled: true, + }) + await refetchRuntimePlugins() + } else { + setInstalledPluginIds((current) => { + const next = new Set(current) + next.add(plugin.id) + return next + }) + } + setPluginRouteState(finishPluginInstallRoute(plugin.id, origin, "success")) + } catch { + setPluginMutationError( + t("pluginsPage.detail.installFailed", { plugin: title }), + ) + setPluginRouteState(finishPluginInstallRoute(plugin.id, origin, "error")) + } finally { + setInstallingPluginId(null) + } + } + + const uninstallCatalogPlugin = (plugin: CatalogPlugin) => { + setPluginMutationError(null) + setPluginRouteState( + requestPluginUninstallRoute(plugin.id, getActivePluginRouteOrigin()), + ) + setUninstallCandidate(plugin) + } + + const confirmUninstallCatalogPlugin = async (plugin: CatalogPlugin) => { + const title = t(plugin.titleKey) + const runtimePlugin = runtimePluginByCatalogId.get(plugin.id) ?? null + const origin = getActivePluginRouteOrigin() + setPluginMutationError(null) + setUninstallingPluginId(plugin.id) + setPluginRouteState(startPluginUninstallRoute(plugin.id, origin)) + + try { + await waitForPluginMutationFrame() + + if (runtimePlugin) { + await setPluginEnabledMutation.mutateAsync({ + pluginSource: runtimePlugin.source, + enabled: false, + }) + await refetchRuntimePlugins() + } else { + setInstalledPluginIds((current) => { + const next = new Set(current) + next.delete(plugin.id) + return next + }) + } + + setUninstallCandidate(null) + setPluginRouteState( + finishPluginUninstallRoute(plugin.id, origin, "success"), + ) + } catch { + setPluginMutationError( + t("pluginsPage.detail.uninstallFailed", { plugin: title }), + ) + setPluginRouteState(finishPluginUninstallRoute(plugin.id, origin, "error")) + } finally { + setUninstallingPluginId(null) + } + } + + const copyCatalogPluginLink = async (plugin: CatalogPlugin) => { + const title = t(plugin.titleKey) + setPluginMutationError(null) + + try { + await writeClipboardText(getCatalogPluginLink(plugin)) + setCopiedPluginLinkId(plugin.id) + window.setTimeout(() => { + setCopiedPluginLinkId((current) => + current === plugin.id ? null : current, + ) + }, 1600) + } catch { + setPluginMutationError( + t("pluginsPage.detail.copyLinkFailed", { plugin: title }), + ) + } + } + + const cancelUninstallCatalogPlugin = () => { + if (uninstallingPluginId) return + if (uninstallCandidate) { + setPluginRouteState( + cancelPluginUninstallRoute( + uninstallCandidate.id, + getActivePluginRouteOrigin(), + ), + ) + } + setPluginMutationError(null) + setUninstallCandidate(null) + } + + const isSkillsPage = surface === "skills" || pageTab === "skills" + const isMcpSettingsPage = surface === "mcp-settings" + const surfaceTitleKey = getPluginSurfaceTitleKey(surface) + const surfaceSubtitleKey = getPluginSurfaceSubtitleKey(surface, entryMode) + const surfaceSearchKey = getPluginSurfaceSearchKey(surface) + + return ( +
+ {!selectedPlugin ? ( +
+
+ {t(surfaceTitleKey)} +
+ {!isMcpSettingsPage ? ( +
+ + +
+ ) : null} +
+ {!isMcpSettingsPage ? ( + + ) : null} + +
+
+ ) : null} + +
+
+ {selectedPlugin ? ( + openPluginManageTab("mcps")} + onCopyLink={() => copyCatalogPluginLink(selectedPlugin)} + onOpenSkills={() => openPluginManageTab("skills")} + onInstall={() => { + void installCatalogPlugin(selectedPlugin) + }} + onStartPrompt={startNewChatWithPrompt} + onToggleSkill={(skillName, nextEnabled) => + toggleCatalogPluginSkill(selectedPlugin, skillName, nextEnabled) + } + onTryInChat={() => startCatalogPluginChat(selectedPlugin)} + onUninstall={() => uninstallCatalogPlugin(selectedPlugin)} + runtimePlugin={selectedPluginRuntimePlugin} + uninstalling={uninstallingPluginId === selectedPlugin.id} + /> + ) : ( + <> +

+ {t(surfaceTitleKey)} +

+

+ {t(surfaceSubtitleKey)} +

+ +
+ + +
+ + {isSkillsPage ? ( + <> +
+

+ {t("pluginsPage.skills.installed")} +

+
+ {runtimeSkills.length > 0 ? ( + runtimeSkills.map((skill) => ( + + )) + ) : ( + + )} +
+
+ +
+
+ + +
+
+ {RECOMMENDED_SKILLS.map((resource) => ( + + ))} +
+
+ + ) : entryMode === "manage" ? ( + <> + {visiblePluginManageTabs.length > 1 ? ( +
+ {visiblePluginManageTabs.map((tab) => ( + + ))} +
+ ) : null} + + {manageTab === "plugins" && ( +
+
+

+ {t("pluginsPage.manage.installed")} +

+ +
+
+ {runtimePlugins.length > 0 + ? runtimePlugins.map((runtimePlugin) => ( + + )) + : CONNECTED_PLUGINS.map((plugin) => ( + + ))} +
+
+ )} + + {manageTab === "apps" && ( +
+

+ {t("pluginsPage.manage.apps")} +

+
+ {getManagedResourcesForTab("apps").map((resource) => ( + + ))} +
+
+ )} + + {manageTab === "mcps" && ( +
+

+ {t("pluginsPage.manage.mcps")} +

+
+ {getManagedResourcesForTab("mcps").map((resource) => ( + + ))} +
+
+ )} + + {manageTab === "skills" && ( +
+

+ {t("pluginsPage.manage.skills")} +

+
+ {runtimeSkills.length > 0 ? ( + runtimeSkills.map((skill) => ( + + )) + ) : !isFetchingRuntimeSkills ? ( + getManagedResourcesForTab("skills").map((resource) => ( + + )) + ) : ( + + )} +
+
+ )} + + {manageTab === "marketplace" && ( +
+

+ {t("pluginsPage.manage.marketplace")} +

+
+ {MARKETPLACE_PLUGINS.map((plugin) => ( + openCatalogPluginDetail(plugin)} + onOpen={() => openCatalogPluginDetail(plugin)} + onTryInChat={() => startCatalogPluginChat(plugin)} + /> + ))} +
+
+ )} + + ) : ( + <> + {showOnboardingSuggestions && selectedOnboardingRole ? ( +
+
+
+

+ {t("pluginsPage.onboarding.title")} +

+

+ {t("pluginsPage.onboarding.description")} +

+
+ +
+ +
+ {pluginOnboardingSuggestions.map((role) => ( + + ))} +
+ +

+ {t(selectedOnboardingRole.descriptionKey)} +

+
+ {selectedOnboardingRole.resources.map((resource) => ( + + ))} + {selectedOnboardingRole.catalogPlugins.map((plugin) => ( + openCatalogPluginDetail(plugin)} + onOpen={() => openCatalogPluginDetail(plugin)} + onTryInChat={() => startCatalogPluginChat(plugin)} + /> + ))} +
+
+ ) : null} + +
+ + + +
+ +
+
+

+ {t("pluginsPage.connected")} +

+ +
+
+ {CONNECTED_PLUGINS.map((plugin) => ( + + + + ))} + + + {t("pluginsPage.connectedMore")} + +
+
+ +
+ +
+ {FEATURED_PLUGINS.map((plugin) => ( + openCatalogPluginDetail(plugin)} + onOpen={() => openCatalogPluginDetail(plugin)} + onTryInChat={() => startCatalogPluginChat(plugin)} + /> + ))} +
+ +
+ +
+

+ {t("pluginsPage.productivity")} +

+
+ {PRODUCTIVITY_PLUGINS.map((plugin) => ( + openCatalogPluginDetail(plugin)} + onOpen={() => openCatalogPluginDetail(plugin)} + onTryInChat={() => startCatalogPluginChat(plugin)} + /> + ))} +
+
+ +
+

+ {t("pluginsPage.business")} +

+
+ {BUSINESS_PLUGINS.map((plugin) => ( + openCatalogPluginDetail(plugin)} + onOpen={() => openCatalogPluginDetail(plugin)} + onTryInChat={() => startCatalogPluginChat(plugin)} + /> + ))} +
+
+ + )} + + )} +
+
+ {uninstallCandidate ? ( + { + void confirmUninstallCatalogPlugin(uninstallCandidate) + }} + /> + ) : null} +
+ ) +} + +export function SkillsEntryView() { + return +} + +export function McpSettingsEntryView() { + return +} diff --git a/src/renderer/features/plugins/plugin-resource-model.ts b/src/renderer/features/plugins/plugin-resource-model.ts new file mode 100644 index 000000000..8c219fb2f --- /dev/null +++ b/src/renderer/features/plugins/plugin-resource-model.ts @@ -0,0 +1,794 @@ +import type { + PluginManageTabId, + PluginResourceRouteActionId, + PluginResourceRouteKind, +} from "./plugin-route-state" + +export type PluginLogoId = + | "actively" + | "apollo" + | "calendar" + | "chrome" + | "codex-labs" + | "cursor" + | "docs" + | "drive" + | "figma" + | "github" + | "gmail" + | "hubspot" + | "huggingface" + | "linear" + | "notion" + | "plugin-grid" + | "plugin-store" + | "purple-dots" + | "record-replay" + | "salesforce" + | "sheets" + | "skill" + | "slack" + | "slides" + | "striped" + +export type CatalogPlugin = { + id: string + logo: PluginLogoId + titleKey: string + descriptionKey: string +} + +export type CatalogPluginDetail = { + developer: string + apps: string[] + skills: string[] + mcps: string[] + capabilities: string[] + promptKey: string + category?: string + componentDescriptions?: { + apps?: Record + skills?: Record + mcps?: Record + } + longDescriptionKey?: string + promptExampleKeys?: string[] + termsOfService?: string + version?: string + website?: string + privacyPolicy?: string +} + +export type RuntimePluginComponent = { + name: string + description?: string +} + +export type RuntimePluginData = { + name: string + version: string + description?: string + path: string + source: string + marketplace: string + category?: string + homepage?: string + tags?: string[] + isDisabled: boolean + components: { + commands: RuntimePluginComponent[] + skills: RuntimePluginComponent[] + agents: RuntimePluginComponent[] + mcpServers: string[] + } +} + +export type RuntimeSkillData = { + name: string + description: string + source: "moss" | "user" | "project" | "plugin" + pluginName?: string + path: string + content: string +} + +export type ConnectedPlugin = { + id: string + logo: PluginLogoId + label: string +} + +export type ManagedResourceKind = PluginResourceRouteKind +export type ManagedResourceActionId = PluginResourceRouteActionId +export type ManagedResourceConnectionState = + | "connected" + | "enabled" + | "disabled" + | "needs-auth" + | "pending" + | "recommended" + | "unavailable" + +export type ManagedResource = { + id: string + kind: ManagedResourceKind + actionId: ManagedResourceActionId + connectionState: ManagedResourceConnectionState + logo: PluginLogoId + title: string + descriptionKey: string + statusKey: string + actionKey: string + capabilityIds?: string[] + connectorId?: string + mcpServerName?: string + promptKey?: string + targetId?: string +} + +export type PluginManageTab = { + id: PluginManageTabId + labelKey: string + count: number +} + +export type PluginOnboardingRoleId = "developer" | "operator" | "researcher" + +type PluginOnboardingRoleDefinition = { + id: PluginOnboardingRoleId + titleKey: string + descriptionKey: string + starterPromptKey: string + resourceIds: string[] + catalogPluginIds: string[] +} + +export type PluginOnboardingRoleSuggestion = PluginOnboardingRoleDefinition & { + resources: ManagedResource[] + catalogPlugins: CatalogPlugin[] +} + +export const CONNECTED_PLUGINS: ConnectedPlugin[] = [ + { id: "docs", logo: "docs", label: "Google Docs" }, + { id: "sheets", logo: "sheets", label: "Google Sheets" }, + { id: "slides", logo: "slides", label: "Google Slides" }, + { id: "cursor", logo: "cursor", label: "Cursor" }, + { id: "chrome", logo: "chrome", label: "Chrome" }, + { id: "codex-labs", logo: "codex-labs", label: "Codex Labs" }, + { id: "record-and-replay", logo: "record-replay", label: "Record & Replay" }, + { id: "purple-dots", logo: "purple-dots", label: "Research" }, + { id: "linear", logo: "linear", label: "Linear" }, + { id: "gmail", logo: "gmail", label: "Gmail" }, + { id: "huggingface", logo: "huggingface", label: "Hugging Face" }, + { id: "plugin-store", logo: "plugin-store", label: "Plugin Store" }, + { id: "striped", logo: "striped", label: "Raycast" }, + { id: "plugin-grid", logo: "plugin-grid", label: "Workflows" }, +] + +export const FEATURED_PLUGINS: CatalogPlugin[] = [ + { + id: "github", + logo: "github", + titleKey: "pluginsPage.catalog.github.title", + descriptionKey: "pluginsPage.catalog.github.description", + }, + { + id: "slack", + logo: "slack", + titleKey: "pluginsPage.catalog.slack.title", + descriptionKey: "pluginsPage.catalog.slack.description", + }, + { + id: "notion", + logo: "notion", + titleKey: "pluginsPage.catalog.notion.title", + descriptionKey: "pluginsPage.catalog.notion.description", + }, + { + id: "google-calendar", + logo: "calendar", + titleKey: "pluginsPage.catalog.googleCalendar.title", + descriptionKey: "pluginsPage.catalog.googleCalendar.description", + }, + { + id: "drive", + logo: "drive", + titleKey: "pluginsPage.catalog.googleDrive.title", + descriptionKey: "pluginsPage.catalog.googleDrive.description", + }, +] + +export const PRODUCTIVITY_PLUGINS: CatalogPlugin[] = [ + { + id: "record-and-replay", + logo: "record-replay", + titleKey: "pluginsPage.catalog.recordReplay.title", + descriptionKey: "pluginsPage.catalog.recordReplay.description", + }, +] + +export const BUSINESS_PLUGINS: CatalogPlugin[] = [ + { + id: "actively", + logo: "actively", + titleKey: "pluginsPage.catalog.actively.title", + descriptionKey: "pluginsPage.catalog.actively.description", + }, + { + id: "apollo", + logo: "apollo", + titleKey: "pluginsPage.catalog.apollo.title", + descriptionKey: "pluginsPage.catalog.apollo.description", + }, + { + id: "salesforce", + logo: "salesforce", + titleKey: "pluginsPage.catalog.salesforce.title", + descriptionKey: "pluginsPage.catalog.salesforce.description", + }, + { + id: "hubspot", + logo: "hubspot", + titleKey: "pluginsPage.catalog.hubspot.title", + descriptionKey: "pluginsPage.catalog.hubspot.description", + }, +] + +export const MARKETPLACE_PLUGINS = [ + ...FEATURED_PLUGINS, + ...PRODUCTIVITY_PLUGINS, + ...BUSINESS_PLUGINS, +] + +export const CATALOG_PLUGIN_DETAILS: Record = { + github: { + developer: "OpenAI", + apps: ["GitHub"], + skills: ["GitHub Fix CI", "GitHub", "Review Follow-up", "Publish Changes"], + mcps: ["GitHub"], + capabilities: ["Interactive", "Write"], + category: "Developer Tools", + componentDescriptions: { + apps: { + GitHub: + "Access repositories, issues, and pull requests. Required for some features such as Codex", + }, + skills: { + "GitHub Fix CI": "Debug failing GitHub Actions CI", + GitHub: + "Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.", + "Review Follow-up": "Address actionable PR feedback", + "Publish Changes": "Commit, push, and open a PR", + }, + }, + longDescriptionKey: "pluginsPage.catalog.github.longDescription", + promptKey: "pluginsPage.catalog.github.prompt", + promptExampleKeys: ["pluginsPage.catalog.github.promptExample"], + privacyPolicy: "https://openai.com/policies/privacy-policy", + termsOfService: "https://openai.com/policies/terms-of-use", + website: "https://github.com", + }, + slack: { + developer: "OpenAI", + apps: ["Slack"], + skills: ["Messages"], + mcps: ["Slack"], + capabilities: ["Channels", "Threads", "Team context"], + promptKey: "pluginsPage.catalog.slack.prompt", + }, + notion: { + developer: "OpenAI", + apps: ["Notion"], + skills: ["Knowledge capture"], + mcps: ["Notion"], + capabilities: ["Docs", "Databases", "Meeting notes"], + promptKey: "pluginsPage.catalog.notion.prompt", + }, + "google-calendar": { + developer: "OpenAI", + apps: ["Google Calendar"], + skills: ["Scheduling"], + mcps: ["Calendar"], + capabilities: ["Events", "Availability", "Meeting prep"], + promptKey: "pluginsPage.catalog.googleCalendar.prompt", + }, + drive: { + developer: "OpenAI", + apps: ["Google Drive", "Docs", "Sheets", "Slides"], + skills: ["Documents"], + mcps: ["Drive"], + capabilities: ["Files", "Docs", "Sheets", "Slides"], + promptKey: "pluginsPage.catalog.googleDrive.prompt", + }, + "record-and-replay": { + developer: "OpenAI", + apps: [], + skills: ["Record and Replay"], + mcps: ["Event-stream"], + capabilities: ["Interactive", "Write"], + category: "Productivity", + componentDescriptions: { + skills: { + "Record and Replay": + "Record the user's actions on their Mac with Record & Replay, and turn it into a reusable Codex skill from the captured event stream.", + }, + }, + longDescriptionKey: "pluginsPage.catalog.recordReplay.longDescription", + promptKey: "pluginsPage.catalog.recordReplay.prompt", + promptExampleKeys: [ + "pluginsPage.catalog.recordReplay.promptExample.workflow", + "pluginsPage.catalog.recordReplay.promptExample.task", + "pluginsPage.catalog.recordReplay.promptExample.fileExpense", + ], + privacyPolicy: "https://openai.com/policies/row-privacy-policy/", + termsOfService: "https://openai.com/policies/row-terms-of-use/", + version: "1.0.857", + website: "https://openai.com/", + }, + actively: { + developer: "OpenAI", + apps: ["Actively"], + skills: ["Account research"], + mcps: ["Actively"], + capabilities: ["Prospects", "Signals", "Account context"], + promptKey: "pluginsPage.catalog.actively.prompt", + }, + apollo: { + developer: "OpenAI", + apps: ["Apollo"], + skills: ["Sales research"], + mcps: ["Apollo"], + capabilities: ["Leads", "Companies", "Engagement"], + promptKey: "pluginsPage.catalog.apollo.prompt", + }, + salesforce: { + developer: "OpenAI", + apps: ["Salesforce"], + skills: ["CRM"], + mcps: ["Salesforce"], + capabilities: ["Accounts", "Opportunities", "Records"], + promptKey: "pluginsPage.catalog.salesforce.prompt", + }, + hubspot: { + developer: "OpenAI", + apps: ["HubSpot"], + skills: ["CRM"], + mcps: ["HubSpot"], + capabilities: ["Contacts", "Deals", "Notes"], + promptKey: "pluginsPage.catalog.hubspot.prompt", + }, +} + +export const MANAGED_APPS: ManagedResource[] = [ + { + id: "google-drive", + kind: "app", + actionId: "manage", + connectionState: "connected", + connectorId: "google-drive", + logo: "drive", + title: "Google Drive", + descriptionKey: "pluginsPage.manage.apps.drive.description", + statusKey: "pluginsPage.status.connected", + actionKey: "pluginsPage.manage", + }, + { + id: "gmail", + kind: "app", + actionId: "manage", + connectionState: "connected", + connectorId: "gmail", + logo: "gmail", + title: "Gmail", + descriptionKey: "pluginsPage.manage.apps.gmail.description", + statusKey: "pluginsPage.status.connected", + actionKey: "pluginsPage.manage", + }, + { + id: "google-calendar", + kind: "app", + actionId: "manage", + connectionState: "connected", + connectorId: "google-calendar", + logo: "calendar", + title: "Google Calendar", + descriptionKey: "pluginsPage.manage.apps.calendar.description", + statusKey: "pluginsPage.status.connected", + actionKey: "pluginsPage.manage", + }, + { + id: "chrome", + kind: "app", + actionId: "manage", + connectionState: "enabled", + connectorId: "chrome", + logo: "chrome", + title: "Chrome", + descriptionKey: "pluginsPage.manage.apps.chrome.description", + statusKey: "pluginsPage.status.enabled", + actionKey: "pluginsPage.manage", + }, +] + +export const MANAGED_MCPS: ManagedResource[] = [ + { + id: "github-mcp", + kind: "mcp", + actionId: "open-mcp-capability", + connectionState: "enabled", + capabilityIds: ["pull-requests", "issues", "ci", "releases"], + logo: "github", + mcpServerName: "GitHub", + title: "GitHub", + descriptionKey: "pluginsPage.manage.mcps.github.description", + statusKey: "pluginsPage.status.enabled", + actionKey: "pluginsPage.manage", + }, + { + id: "linear-mcp", + kind: "mcp", + actionId: "open-mcp-capability", + connectionState: "enabled", + capabilityIds: ["issues", "projects", "cycles"], + logo: "linear", + mcpServerName: "Linear", + title: "Linear", + descriptionKey: "pluginsPage.manage.mcps.linear.description", + statusKey: "pluginsPage.status.enabled", + actionKey: "pluginsPage.manage", + }, + { + id: "slack-mcp", + kind: "mcp", + actionId: "open-mcp-capability", + connectionState: "enabled", + capabilityIds: ["channels", "threads", "messages"], + logo: "slack", + mcpServerName: "Slack", + title: "Slack", + descriptionKey: "pluginsPage.manage.mcps.slack.description", + statusKey: "pluginsPage.status.enabled", + actionKey: "pluginsPage.manage", + }, + { + id: "notion-mcp", + kind: "mcp", + actionId: "connect-oauth", + connectionState: "needs-auth", + capabilityIds: ["docs", "databases"], + logo: "notion", + mcpServerName: "Notion", + title: "Notion", + descriptionKey: "pluginsPage.manage.mcps.notion.description", + statusKey: "pluginsPage.status.disabled", + actionKey: "pluginsPage.add", + }, +] + +export const MANAGED_SKILLS: ManagedResource[] = [ + { + id: "docs-skill", + kind: "managed-skill", + actionId: "try-in-chat", + connectionState: "enabled", + logo: "docs", + title: "Documents", + descriptionKey: "pluginsPage.manage.skills.documents.description", + statusKey: "pluginsPage.status.enabled", + actionKey: "pluginsPage.skills.try", + promptKey: "pluginsPage.skills.documents.prompt", + }, + { + id: "research-skill", + kind: "managed-skill", + actionId: "try-in-chat", + connectionState: "enabled", + logo: "purple-dots", + title: "Research", + descriptionKey: "pluginsPage.manage.skills.research.description", + statusKey: "pluginsPage.status.enabled", + actionKey: "pluginsPage.skills.try", + promptKey: "pluginsPage.skills.research.prompt", + }, + { + id: "workflow-skill", + kind: "managed-skill", + actionId: "try-in-chat", + connectionState: "enabled", + logo: "plugin-grid", + title: "Workflows", + descriptionKey: "pluginsPage.manage.skills.workflows.description", + statusKey: "pluginsPage.status.enabled", + actionKey: "pluginsPage.skills.try", + promptKey: "pluginsPage.skills.workflows.prompt", + }, +] + +export const RECOMMENDED_SKILLS: ManagedResource[] = [ + { + id: "browser-qa", + kind: "recommended-skill", + actionId: "install-skill", + connectionState: "recommended", + logo: "chrome", + title: "Browser QA", + descriptionKey: "pluginsPage.skills.recommended.browserQa.description", + statusKey: "pluginsPage.skills.status.recommended", + actionKey: "pluginsPage.skills.install", + }, + { + id: "github-ops", + kind: "recommended-skill", + actionId: "install-skill", + connectionState: "recommended", + logo: "github", + title: "GitHub Ops", + descriptionKey: "pluginsPage.skills.recommended.githubOps.description", + statusKey: "pluginsPage.skills.status.recommended", + actionKey: "pluginsPage.skills.install", + }, + { + id: "data-analytics", + kind: "recommended-skill", + actionId: "install-skill", + connectionState: "recommended", + logo: "plugin-store", + title: "Data Analytics", + descriptionKey: "pluginsPage.skills.recommended.dataAnalytics.description", + statusKey: "pluginsPage.skills.status.recommended", + actionKey: "pluginsPage.skills.install", + }, +] + +export const PLUGIN_ONBOARDING_ROLE_DEFINITIONS: PluginOnboardingRoleDefinition[] = [ + { + id: "developer", + titleKey: "pluginsPage.onboarding.roles.developer.title", + descriptionKey: "pluginsPage.onboarding.roles.developer.description", + starterPromptKey: "pluginsPage.onboarding.roles.developer.prompt", + resourceIds: ["github-mcp", "browser-qa", "github-ops", "chrome"], + catalogPluginIds: ["github"], + }, + { + id: "operator", + titleKey: "pluginsPage.onboarding.roles.operator.title", + descriptionKey: "pluginsPage.onboarding.roles.operator.description", + starterPromptKey: "pluginsPage.onboarding.roles.operator.prompt", + resourceIds: ["gmail", "google-calendar", "slack-mcp", "workflow-skill"], + catalogPluginIds: ["slack", "google-calendar"], + }, + { + id: "researcher", + titleKey: "pluginsPage.onboarding.roles.researcher.title", + descriptionKey: "pluginsPage.onboarding.roles.researcher.description", + starterPromptKey: "pluginsPage.onboarding.roles.researcher.prompt", + resourceIds: ["google-drive", "notion-mcp", "research-skill", "data-analytics"], + catalogPluginIds: ["drive", "notion"], + }, +] + +export const PLUGIN_MANAGE_TABS: PluginManageTab[] = [ + { + id: "plugins", + labelKey: "pluginsPage.manage.tabs.plugins", + count: CONNECTED_PLUGINS.length, + }, + { + id: "apps", + labelKey: "pluginsPage.manage.tabs.apps", + count: MANAGED_APPS.length, + }, + { + id: "mcps", + labelKey: "pluginsPage.manage.tabs.mcps", + count: MANAGED_MCPS.length, + }, + { + id: "skills", + labelKey: "pluginsPage.manage.tabs.skills", + count: MANAGED_SKILLS.length, + }, + { + id: "marketplace", + labelKey: "pluginsPage.manage.tabs.marketplace", + count: MARKETPLACE_PLUGINS.length, + }, +] + +export function findCatalogPluginById(pluginId: string) { + return MARKETPLACE_PLUGINS.find((plugin) => plugin.id === pluginId) +} + +export function normalizePluginHandle(value: string) { + return value + .toLowerCase() + .replace(/^@/, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") +} + +export function formatRuntimeLabel(value: string) { + return value + .replace(/[-_]+/g, " ") + .replace(/\b\w/g, (character) => character.toUpperCase()) +} + +export function uniqueNonEmpty(items: Array) { + const seen = new Set() + const result: string[] = [] + + for (const item of items) { + const value = item?.trim() + if (!value) continue + + const key = value.toLowerCase() + if (seen.has(key)) continue + + seen.add(key) + result.push(value) + } + + return result +} + +export function getCatalogPluginRuntimeAliases(plugin: CatalogPlugin) { + if (plugin.id === "drive") return ["drive", "google-drive"] + if (plugin.id === "google-calendar") return ["google-calendar", "calendar"] + if (plugin.id === "record-and-replay") { + return ["record-and-replay", "record-replay"] + } + + return [plugin.id] +} + +export function findRuntimePluginForCatalog( + plugin: CatalogPlugin, + runtimePlugins: RuntimePluginData[], +) { + const aliases = new Set( + getCatalogPluginRuntimeAliases(plugin).map(normalizePluginHandle), + ) + + return ( + runtimePlugins.find((runtimePlugin) => { + const sourceName = runtimePlugin.source.split(":").pop() ?? runtimePlugin.source + const pathName = runtimePlugin.path.split("/").pop() ?? runtimePlugin.path + const candidates = uniqueNonEmpty([ + runtimePlugin.name, + runtimePlugin.source, + sourceName, + pathName, + runtimePlugin.category, + runtimePlugin.homepage, + ...(runtimePlugin.tags ?? []), + ]).map(normalizePluginHandle) + + return candidates.some((candidate) => aliases.has(candidate)) + }) ?? null + ) +} + +export function getCatalogPluginDetail( + plugin: CatalogPlugin, + runtimePlugin?: RuntimePluginData | null, +) { + const detail = CATALOG_PLUGIN_DETAILS[plugin.id] + if (!runtimePlugin) return detail + const runtimeApps = + detail.apps.length > 0 ? [...detail.apps, runtimePlugin.name] : detail.apps + const runtimeCategoryCapability = + detail.category == null && runtimePlugin.category + ? formatRuntimeLabel(runtimePlugin.category) + : null + + return { + ...detail, + apps: uniqueNonEmpty(runtimeApps), + skills: uniqueNonEmpty([ + ...detail.skills, + ...runtimePlugin.components.skills.map((skill) => skill.name), + ...runtimePlugin.components.commands.map((command) => command.name), + ]), + mcps: uniqueNonEmpty([ + ...detail.mcps, + ...runtimePlugin.components.mcpServers, + ]), + capabilities: uniqueNonEmpty([ + ...detail.capabilities, + runtimeCategoryCapability, + ...runtimePlugin.components.agents.map((agent) => agent.name), + ]), + } +} + +export function getRuntimePluginResourceCount( + runtimePlugin: RuntimePluginData, +) { + return ( + runtimePlugin.components.commands.length + + runtimePlugin.components.skills.length + + runtimePlugin.components.agents.length + + runtimePlugin.components.mcpServers.length + ) +} + +export function getRuntimeSkillLogo( + skill: RuntimeSkillData, + catalogPlugin?: CatalogPlugin | null, +): PluginLogoId { + if (catalogPlugin) return catalogPlugin.logo + if (skill.source === "moss") return "codex-labs" + if (skill.source === "project") return "docs" + if (skill.source === "user") return "purple-dots" + return "plugin-store" +} + +export function getManagedResourcesForTab(tab: PluginManageTabId) { + if (tab === "apps") return MANAGED_APPS + if (tab === "mcps") return MANAGED_MCPS + if (tab === "skills") return MANAGED_SKILLS + return [] +} + +export function getManagedResourceRouteTarget(resource: ManagedResource) { + return ( + resource.targetId ?? + resource.connectorId ?? + resource.mcpServerName ?? + resource.promptKey ?? + resource.id + ) +} + +export function canRunManagedResourceAction(resource: ManagedResource) { + return resource.actionId !== "unavailable" +} + +export function buildPluginOnboardingSuggestions(): PluginOnboardingRoleSuggestion[] { + const resourceById = new Map( + [ + ...MANAGED_APPS, + ...MANAGED_MCPS, + ...MANAGED_SKILLS, + ...RECOMMENDED_SKILLS, + ].map((resource) => [resource.id, resource]), + ) + + return PLUGIN_ONBOARDING_ROLE_DEFINITIONS.map((definition) => ({ + ...definition, + resourceIds: [...definition.resourceIds], + catalogPluginIds: [...definition.catalogPluginIds], + resources: definition.resourceIds + .map((resourceId) => resourceById.get(resourceId)) + .filter((resource): resource is ManagedResource => Boolean(resource)), + catalogPlugins: definition.catalogPluginIds + .map(findCatalogPluginById) + .filter((plugin): plugin is CatalogPlugin => Boolean(plugin)), + })) +} + +export function getPluginManageTabs({ + runtimePluginCount, + runtimeSkillCount, +}: { + runtimePluginCount: number + runtimeSkillCount: number +}) { + return PLUGIN_MANAGE_TABS.map((tab) => { + if (tab.id === "plugins") { + return { + ...tab, + count: runtimePluginCount || CONNECTED_PLUGINS.length, + } + } + + if (tab.id === "skills") { + return { + ...tab, + count: runtimeSkillCount || MANAGED_SKILLS.length, + } + } + + return tab + }) +} diff --git a/src/renderer/features/plugins/plugin-route-state.ts b/src/renderer/features/plugins/plugin-route-state.ts new file mode 100644 index 000000000..d67ee77e7 --- /dev/null +++ b/src/renderer/features/plugins/plugin-route-state.ts @@ -0,0 +1,389 @@ +export type PluginEntryMode = "browse" | "manage" +export type PluginTopTabId = "plugins" | "skills" +export type PluginManageTabId = + | "plugins" + | "apps" + | "mcps" + | "skills" + | "marketplace" +export type PluginSourceTabId = "openai" | "mine" +export type PluginMutationStatus = "pending" | "success" | "error" +export type PluginResourceRouteKind = + | "app" + | "mcp" + | "managed-skill" + | "recommended-skill" +export type PluginResourceRouteActionId = + | "manage" + | "connect-oauth" + | "open-mcp-capability" + | "try-in-chat" + | "install-skill" + | "unavailable" + +export type PluginRouteOrigin = { + topTab: PluginTopTabId + entryMode: PluginEntryMode + manageTab: PluginManageTabId + sourceTab: PluginSourceTabId + searchQuery: string +} + +export type PluginBrowseRouteState = { + surface: "browse" + topTab: "plugins" + entryMode: "browse" + sourceTab: PluginSourceTabId + searchQuery: string +} + +export type PluginManageRouteState = { + surface: "manage" + topTab: "plugins" + entryMode: "manage" + manageTab: PluginManageTabId + sourceTab: PluginSourceTabId + searchQuery: string +} + +export type PluginSkillsRouteState = { + surface: "skills" + topTab: "skills" + entryMode: "browse" + sourceTab: PluginSourceTabId + searchQuery: string +} + +export type PluginDetailRouteState = { + surface: "detail" + pluginId: string + origin: PluginRouteOrigin + lastAction?: { + type: "install" | "uninstall" + status: "success" | "error" + } +} + +export type PluginInstallRouteState = { + surface: "install" + pluginId: string + origin: PluginRouteOrigin + status: Extract +} + +export type PluginUninstallRouteState = { + surface: "uninstall" + pluginId: string + origin: PluginRouteOrigin + status: "confirming" | Extract +} + +export type PluginTryInChatRouteState = { + surface: "try-in-chat" + pluginId: string + origin: PluginRouteOrigin + promptKey?: string +} + +export type PluginResourceRouteState = { + surface: "resource" + resourceId: string + resourceKind: PluginResourceRouteKind + actionId: PluginResourceRouteActionId + origin: PluginRouteOrigin + promptKey?: string + targetId?: string +} + +export type PluginRouteState = + | PluginBrowseRouteState + | PluginManageRouteState + | PluginSkillsRouteState + | PluginDetailRouteState + | PluginInstallRouteState + | PluginUninstallRouteState + | PluginTryInChatRouteState + | PluginResourceRouteState + +export type BuildPluginRouteOptions = { + pageTab: PluginTopTabId + entryMode: PluginEntryMode + manageTab?: PluginManageTabId + sourceTab?: PluginSourceTabId + searchQuery?: string +} + +const DEFAULT_MANAGE_TAB: PluginManageTabId = "plugins" +const DEFAULT_SOURCE_TAB: PluginSourceTabId = "openai" + +export function getPluginRouteOrigin( + options: BuildPluginRouteOptions, +): PluginRouteOrigin { + return { + topTab: options.pageTab, + entryMode: options.pageTab === "skills" ? "browse" : options.entryMode, + manageTab: options.manageTab ?? DEFAULT_MANAGE_TAB, + sourceTab: options.sourceTab ?? DEFAULT_SOURCE_TAB, + searchQuery: options.searchQuery ?? "", + } +} + +export function buildPluginBrowseRoute( + options: BuildPluginRouteOptions, +): PluginBrowseRouteState | PluginManageRouteState | PluginSkillsRouteState { + const origin = getPluginRouteOrigin(options) + + if (origin.topTab === "skills") { + return { + surface: "skills", + topTab: "skills", + entryMode: "browse", + sourceTab: origin.sourceTab, + searchQuery: origin.searchQuery, + } + } + + if (origin.entryMode === "manage") { + return { + surface: "manage", + topTab: "plugins", + entryMode: "manage", + manageTab: origin.manageTab, + sourceTab: origin.sourceTab, + searchQuery: origin.searchQuery, + } + } + + return { + surface: "browse", + topTab: "plugins", + entryMode: "browse", + sourceTab: origin.sourceTab, + searchQuery: origin.searchQuery, + } +} + +export function getPluginRouteOriginFromState( + route: PluginRouteState, +): PluginRouteOrigin { + if ("origin" in route) return route.origin + + return getPluginRouteOrigin({ + pageTab: route.topTab, + entryMode: route.entryMode, + manageTab: route.surface === "manage" ? route.manageTab : DEFAULT_MANAGE_TAB, + sourceTab: route.sourceTab, + searchQuery: route.searchQuery, + }) +} + +export function openPluginDetailRoute( + pluginId: string, + origin: PluginRouteOrigin, +): PluginDetailRouteState { + return { + surface: "detail", + pluginId, + origin, + } +} + +export function closePluginOverlayRoute( + route: PluginRouteState, +): PluginBrowseRouteState | PluginManageRouteState | PluginSkillsRouteState { + return buildPluginBrowseRoute(pluginRouteToLegacyTabs(route)) +} + +export function startPluginInstallRoute( + pluginId: string, + origin: PluginRouteOrigin, +): PluginInstallRouteState { + return { + surface: "install", + pluginId, + origin, + status: "pending", + } +} + +export function finishPluginInstallRoute( + pluginId: string, + origin: PluginRouteOrigin, + status: Extract, +): PluginDetailRouteState { + return { + surface: "detail", + pluginId, + origin, + lastAction: { + type: "install", + status, + }, + } +} + +export function requestPluginUninstallRoute( + pluginId: string, + origin: PluginRouteOrigin, +): PluginUninstallRouteState { + return { + surface: "uninstall", + pluginId, + origin, + status: "confirming", + } +} + +export function startPluginUninstallRoute( + pluginId: string, + origin: PluginRouteOrigin, +): PluginUninstallRouteState { + return { + surface: "uninstall", + pluginId, + origin, + status: "pending", + } +} + +export function finishPluginUninstallRoute( + pluginId: string, + origin: PluginRouteOrigin, + status: Extract, +): PluginDetailRouteState | PluginUninstallRouteState { + if (status === "error") { + return { + surface: "uninstall", + pluginId, + origin, + status: "error", + } + } + + return { + surface: "detail", + pluginId, + origin, + lastAction: { + type: "uninstall", + status, + }, + } +} + +export function cancelPluginUninstallRoute( + pluginId: string, + origin: PluginRouteOrigin, +): PluginDetailRouteState { + return openPluginDetailRoute(pluginId, origin) +} + +export function startPluginTryInChatRoute( + pluginId: string, + origin: PluginRouteOrigin, + promptKey?: string, +): PluginTryInChatRouteState { + return { + surface: "try-in-chat", + pluginId, + origin, + promptKey, + } +} + +export function openPluginResourceRoute({ + actionId, + origin, + promptKey, + resourceId, + resourceKind, + targetId, +}: { + actionId: PluginResourceRouteActionId + origin: PluginRouteOrigin + promptKey?: string + resourceId: string + resourceKind: PluginResourceRouteKind + targetId?: string +}): PluginResourceRouteState { + return { + surface: "resource", + resourceId, + resourceKind, + actionId, + origin, + promptKey, + targetId, + } +} + +export function pluginRouteToLegacyTabs( + route: PluginRouteState, +): Required { + const origin = getPluginRouteOriginFromState(route) + + return { + pageTab: origin.topTab, + entryMode: origin.entryMode, + manageTab: origin.manageTab, + sourceTab: origin.sourceTab, + searchQuery: origin.searchQuery, + } +} + +export function getPluginRouteDialogKind( + route: PluginRouteState, +): "detail" | "uninstall" | null { + if (route.surface === "detail" || route.surface === "install") { + return "detail" + } + + if (route.surface === "uninstall") return "uninstall" + + return null +} + +export function serializePluginRouteOrigin(origin: PluginRouteOrigin): string { + if (origin.topTab === "skills") { + return `skills:${origin.sourceTab}` + } + + if (origin.entryMode === "manage") { + return `plugins:manage:${origin.manageTab}:${origin.sourceTab}` + } + + return `plugins:browse:${origin.sourceTab}` +} + +export function serializePluginRouteState(route: PluginRouteState): string { + if (route.surface === "resource") { + const origin = serializePluginRouteOrigin(route.origin) + const target = route.targetId ? `:${route.targetId}` : "" + return `resource:${route.resourceKind}:${route.resourceId}:${route.actionId}:${origin}${target}` + } + + if (!("pluginId" in route)) { + return serializePluginRouteOrigin(getPluginRouteOriginFromState(route)) + } + + const origin = serializePluginRouteOrigin(route.origin) + + if (route.surface === "detail") { + const action = route.lastAction + ? `:${route.lastAction.type}-${route.lastAction.status}` + : "" + return `detail:${route.pluginId}:${origin}${action}` + } + + if (route.surface === "install") { + return `install:${route.pluginId}:${route.status}:${origin}` + } + + if (route.surface === "uninstall") { + return `uninstall:${route.pluginId}:${route.status}:${origin}` + } + + return route.promptKey + ? `try-in-chat:${route.pluginId}:${route.promptKey}:${origin}` + : `try-in-chat:${route.pluginId}:${origin}` +} diff --git a/src/renderer/lib/atoms/index.ts b/src/renderer/lib/atoms/index.ts index e28999953..cce663cb3 100644 --- a/src/renderer/lib/atoms/index.ts +++ b/src/renderer/lib/atoms/index.ts @@ -13,6 +13,8 @@ export { lastSelectedModelIdAtom, lastSelectedAgentIdAtom, lastSelectedRepoAtom, + defaultAgentApprovalPolicyAtom, + defaultAgentPermissionProfileAtom, selectedProjectAtom, agentsUnseenChangesAtom, agentsSubChatUnseenChangesAtom, @@ -68,6 +70,8 @@ export { type AgentsDebugMode, type SubChatFileChange, type AgentMode, + type AgentApprovalPolicy, + type AgentPermissionProfile, // Mode utilities AGENT_MODES, @@ -185,17 +189,39 @@ export type SettingsTab = | "profile" | "appearance" | "preferences" + | "capabilities" + | "agent" + | "billing" + | "code-review" | "models" + | "runtime" + | "desktop" + | "release" + | "configuration" + | "personalization" + | "pets" + | "environment" + | "hooks" + | "memory" | "skills" | "agents" | "mcp" | "plugins" + | "archived-conversations" | "worktrees" | "projects" | "debug" | "beta" | "keyboard" export const agentsSettingsDialogActiveTabAtom = atom("preferences") + +export type AppLanguage = "zh-CN" | "en-US" + +export const appLanguageAtom = atomWithStorage( + "app:language", + "zh-CN", +) + // Derived atom: maps settings open/close to desktopView navigation export const agentsSettingsDialogOpenAtom = atom( (get) => get(_desktopViewAtom) === "settings", @@ -210,6 +236,126 @@ export type CustomClaudeConfig = { baseUrl: string } +export type AgentEnvironmentSettings = { + inheritShellEnvironment: boolean + redactSensitiveValues: boolean + requireHookConfirmation: boolean + hookRunnerEnabled: boolean + allowedVariables: string[] +} + +export const DEFAULT_AGENT_ENVIRONMENT_SETTINGS: AgentEnvironmentSettings = { + inheritShellEnvironment: true, + redactSensitiveValues: true, + requireHookConfirmation: true, + hookRunnerEnabled: false, + allowedVariables: [ + "PATH", + "HOME", + "SHELL", + "LANG", + "NODE_ENV", + "GIT_AUTHOR_NAME", + "GIT_AUTHOR_EMAIL", + ], +} + +export const agentEnvironmentSettingsAtom = + atomWithStorage( + "agents:environment-settings", + DEFAULT_AGENT_ENVIRONMENT_SETTINGS, + ) + +export type AgentCapabilitiesSettings = { + unifiedSourceEnabled: boolean + memoryEnabled: boolean + skillsEnabled: boolean + mcpEnabled: boolean + pluginsEnabled: boolean + hooksEnabled: boolean + requireMcpApproval: boolean +} + +export const DEFAULT_AGENT_CAPABILITIES_SETTINGS: AgentCapabilitiesSettings = { + unifiedSourceEnabled: true, + memoryEnabled: true, + skillsEnabled: true, + mcpEnabled: true, + pluginsEnabled: true, + hooksEnabled: false, + requireMcpApproval: true, +} + +export const agentCapabilitiesSettingsAtom = + atomWithStorage( + "agents:capabilities-settings", + DEFAULT_AGENT_CAPABILITIES_SETTINGS, + ) + +export type AgentCodeReviewTriggerPolicy = + | "manual" + | "pull-request" + | "push-and-pull-request" + +export type AgentCodeReviewRateLimitPolicy = + | "warn-before-expensive" + | "respect-provider-limit" + | "disable-when-low" + +export type AgentCodeReviewSettings = { + automaticReviewEnabled: boolean + exhaustiveReviewEnabled: boolean + triggerPolicy: AgentCodeReviewTriggerPolicy + rateLimitCreditPolicy: AgentCodeReviewRateLimitPolicy + requireProviderReadiness: boolean +} + +export const DEFAULT_AGENT_CODE_REVIEW_SETTINGS: AgentCodeReviewSettings = { + automaticReviewEnabled: false, + exhaustiveReviewEnabled: false, + triggerPolicy: "manual", + rateLimitCreditPolicy: "warn-before-expensive", + requireProviderReadiness: true, +} + +export const agentCodeReviewSettingsAtom = + atomWithStorage( + "agents:code-review-settings", + DEFAULT_AGENT_CODE_REVIEW_SETTINGS, + ) + +export type AgentDesktopParitySettings = { + globalSearchEnabled: boolean + automationMonitorEnabled: boolean + browserToolsEnabled: boolean + requireComputerControlApproval: boolean + shareConnectionsAcrossEngines: boolean + includeArchivedConversations: boolean + appSnapshotsEnabled: boolean + gitBranchPrefix: string + worktreeRoot: string + openTargetLabel: string +} + +export const DEFAULT_AGENT_DESKTOP_PARITY_SETTINGS: AgentDesktopParitySettings = { + globalSearchEnabled: true, + automationMonitorEnabled: true, + browserToolsEnabled: true, + requireComputerControlApproval: true, + shareConnectionsAcrossEngines: true, + includeArchivedConversations: true, + appSnapshotsEnabled: true, + gitBranchPrefix: "codex/", + worktreeRoot: ".1code/worktrees", + openTargetLabel: "Ghostty", +} + +export const agentDesktopParitySettingsAtom = + atomWithStorage( + "agents:desktop-parity-settings", + DEFAULT_AGENT_DESKTOP_PARITY_SETTINGS, + ) + // Model profile system - support multiple configs export type ModelProfile = { id: string @@ -665,6 +811,14 @@ export const recordingHotkeyForActionAtom = atom(null) export const agentsLoginModalOpenAtom = atom(false) export const codexLoginModalOpenAtom = atom(false) +export type CodexLoginModalConfig = { + autoStart: boolean +} + +export const codexLoginModalConfigAtom = atom({ + autoStart: true, +}) + export type ClaudeLoginModalConfig = { hideCustomModelSettingsLink: boolean autoStartAuth: boolean diff --git a/src/renderer/lib/i18n.ts b/src/renderer/lib/i18n.ts new file mode 100644 index 000000000..c139dc80f --- /dev/null +++ b/src/renderer/lib/i18n.ts @@ -0,0 +1,1620 @@ +import { useAtomValue } from "jotai"; +import { appLanguageAtom, type AppLanguage } from "./atoms"; + +type Dictionary = Record; + +const zhCN: Dictionary = { + "common.back": "返回", + "common.forward": "前进", + "common.refresh": "刷新", + "common.ready": "就绪", + "common.off": "关闭", + "common.enabled": "已启用", + "common.details": "详情", + "common.default": "默认", + + "settings.tabs.preferences": "通用", + "settings.tabs.capabilities": "能力", + "settings.tabs.profile": "个人资料", + "settings.tabs.account": "账户", + "settings.tabs.models": "模型", + "settings.tabs.usage": "用量", + "settings.tabs.billing": "用量和计费", + "settings.tabs.appearance": "外观", + "settings.tabs.agent": "Agent", + "settings.tabs.pets": "宠物", + "settings.tabs.keyboard": "键盘快捷键", + "settings.tabs.beta": "Beta", + "settings.tabs.projects": "项目", + "settings.tabs.runtime": "运行时", + "settings.tabs.desktop": "桌面", + "settings.tabs.release": "发布", + "settings.tabs.configuration": "配置", + "settings.tabs.personalization": "个性化", + "settings.tabs.environment": "环境", + "settings.tabs.memory": "记忆", + "settings.tabs.skills": "技能", + "settings.tabs.agents": "自定义智能体", + "settings.tabs.mcp": "MCP 服务器", + "settings.tabs.hooks": "钩子", + "settings.tabs.codeReview": "代码审查", + "settings.tabs.plugins": "插件", + "settings.tabs.archived": "已归档对话", + "settings.tabs.debug": "调试", + "settings.groups.personal": "个人", + "settings.groups.integrations": "集成", + "settings.groups.coding": "编码", + "settings.groups.advanced": "开发者", + + "preferences.title": "通用", + "preferences.description": "配置 Moss 的基础行为、语言和桌面体验", + "preferences.language.title": "语言", + "preferences.language.description": "切换应用界面语言", + "preferences.language.zh": "中文", + "preferences.language.en": "English", + "preferences.extendedThinking.title": "深度思考", + "preferences.extendedThinking.description": + "启用更深入的推理,会使用更多额度。", + "preferences.extendedThinking.note": "会关闭响应流式输出。", + "preferences.defaultMode.title": "默认模式", + "preferences.defaultMode.description": + "新对话的默认工作方式:计划只读,Agent 可修改文件", + "preferences.defaultMode.agent": "Agent", + "preferences.defaultMode.plan": "计划", + "preferences.coAuthored.title": "包含 Co-Authored-By", + "preferences.coAuthored.description": "Claude 提交代码时附加共同作者信息", + "preferences.desktopNotifications.title": "桌面通知", + "preferences.desktopNotifications.description": + "智能体需要输入或完成工作时显示系统通知", + "preferences.soundNotifications.title": "声音提醒", + "preferences.soundNotifications.description": + "你离开时智能体完成工作后播放提示音", + "preferences.notifyFocused.title": "窗口聚焦时也通知", + "preferences.notifyFocused.description": "应用窗口处于激活状态时仍显示通知", + "preferences.quickSwitch.title": "快速切换", + "preferences.quickSwitch.description": "设置 Ctrl+Tab 在哪些内容之间切换", + "preferences.quickSwitch.workspaces": "工作区", + "preferences.quickSwitch.agents": "智能体", + "preferences.autoAdvance.title": "归档后跳转", + "preferences.autoAdvance.description": "归档工作区之后跳转到哪里", + "preferences.autoAdvance.next": "下一个工作区", + "preferences.autoAdvance.previous": "上一个工作区", + "preferences.autoAdvance.close": "关闭工作区", + "preferences.preferredEditor.title": "默认打开应用", + "preferences.preferredEditor.description": "打开工作区时默认使用的应用", + "preferences.analytics.title": "共享使用分析", + "preferences.analytics.description": + "共享匿名使用数据以帮助改进 Moss。只记录功能使用和应用性能,不记录代码、提示词或消息,也不会用于训练。", + + "agentSettings.title": "Agent", + "agentSettings.description": + "设置新对话默认使用的审批策略、沙盒范围和本地依赖来源。", + "agentSettings.config.title": "Custom config.toml settings", + "agentSettings.config.description": + "自定义权限仍由 Codex 配置文件统一接管。", + "agentSettings.config.project.title": "Project config", + "agentSettings.config.project.description": + "当前工作区的项目级 Codex 配置。", + "agentSettings.config.global.title": "Global config", + "agentSettings.config.global.description": + "用户和管理级 Codex 配置的全局范围。", + "agentSettings.config.user.title": "User config", + "agentSettings.config.user.description": + "当前用户的 Codex config.toml 设置。", + "agentSettings.config.admin.title": "Admin config", + "agentSettings.config.admin.description": + "由管理员或托管策略提供的 Codex 设置。", + "agentSettings.config.admin.value": "Managed", + "agentSettings.defaultMode.title": "默认模式", + "agentSettings.defaultMode.description": + "新对话进入 Agent 或 Plan 的默认工作方式。", + "agentSettings.approval.title": "Approval policy", + "agentSettings.approval.description": + "新 Agent 对话默认如何请求操作确认。", + "agentSettings.approval.onRequest": "On request", + "agentSettings.approval.onFailure": "On failure", + "agentSettings.approval.untrusted": "Untrusted", + "agentSettings.approval.never": "Never", + "agentSettings.sandbox.title": "Sandbox settings", + "agentSettings.sandbox.description": + "新 Agent 对话默认可访问的本地范围。", + "agentSettings.sandbox.readOnly": "Read only", + "agentSettings.sandbox.workspaceWrite": "Workspace write", + "agentSettings.sandbox.fullAccess": "Full access", + "agentSettings.network.title": "Allow network access", + "agentSettings.network.description": + "当前运行时只能在 Full access 下同时放开网络。", + "agentSettings.dependencies.title": "Workspace Dependencies", + "agentSettings.dependencies.description": + "Codex 会话使用的本地 Workspace 运行时依赖。", + "agentSettings.dependencies.version.title": "Current version", + "agentSettings.dependencies.version.description": + "使用 1code 打包和运行时检测到的本地依赖。", + "agentSettings.dependencies.version.value": "Bundled", + "agentSettings.dependencies.codex.title": "Codex dependencies", + "agentSettings.dependencies.codex.description": + "新 Codex 会话使用应用内管理的运行时依赖。", + "agentSettings.dependencies.diagnose.title": + "Diagnose issues in Codex Workspace", + "agentSettings.dependencies.diagnose.description": + "本地诊断 API 尚未接入;此按钮保留 Codex Desktop 的入口位置。", + "agentSettings.dependencies.diagnose.label": + "Diagnose issues in Codex Workspace", + "agentSettings.dependencies.reset.title": "Reset and install Workspace", + "agentSettings.dependencies.reset.description": + "本地重装 API 尚未接入;避免执行没有后端保证的假重置。", + "agentSettings.dependencies.reset.label": "Reset and install Workspace", + "agentSettings.experimental.title": "Experimental features (Beta)", + "agentSettings.experimental.description": + "实验功能入口保持为状态展示,具体开关由 Beta 页面承接。", + + "settings.personalization.pets.title": "宠物", + "settings.personalization.pets.current": "已选择 {petName}", + "settings.personalization.pets.openPet": "唤醒宠物", + "settings.personalization.pets.tuckAwayPet": "收起宠物", + "settings.pets.custom.title": "自定义宠物", + "settings.pets.custom.openFolder": "打开文件夹", + "settings.pets.custom.openFolderError": "无法打开宠物文件夹", + "settings.pets.loadCustomError": "无法加载自定义宠物", + "petSettings.description": + "读取本机 Codex 宠物目录、Stop hook 和官方 pointer runtime 的当前状态。", + "petSettings.selectedAvatar.description": "Codex 配置中的当前头像:", + "petSettings.loading": "加载中", + "petSettings.none": "无", + "petSettings.noPets": "没有发现自定义宠物", + "petSettings.custom.count": "发现 {count} 个", + "petSettings.custom.loading": "正在读取自定义宠物目录。", + "petSettings.custom.selected": "已选择", + "petSettings.custom.available": "可用", + "petSettings.stopHook.title": "Stop hook", + "petSettings.runtime.title": "Official runtime", + "petSettings.runtime.description": "官方宠物 pointer runtime。", + "petSettings.window.title": "Official pet window", + "petSettings.window.description": "最近的 hook 日志没有窗口错误。", + "petSettings.lastDecision.title": "Last hook decision", + "petSettings.action.success": "宠物动作已发送", + "petSettings.status.ready": "就绪", + "petSettings.status.configured": "已配置", + "petSettings.status.available": "可用", + "petSettings.status.unknown": "未知", + "petSettings.status.missing": "缺失", + "petSettings.status.notConfigured": "未配置", + "petSettings.status.missingHook": "缺少 hook", + "petSettings.status.missingRuntime": "缺少 runtime", + "petSettings.status.windowUnavailable": "窗口不可用", + + "capabilities.title": "能力", + "capabilities.description": + "一套 Moss 配置同时服务 Hermes、Claude Code、Codex 和自定义智能体。", + "capabilities.unifiedSource.title": "Moss 统一源", + "capabilities.unifiedSource.description": + "安装一次,所有智能体都获得同一套工作区能力。", + "capabilities.memory.title": "记忆", + "capabilities.memory.description": "个人记忆和项目记忆在所有引擎间共享。", + "capabilities.skills.title": "技能和命令", + "capabilities.skills.description": "已安装技能可以从同一个输入框使用。", + "capabilities.mcp.title": "MCP 服务器", + "capabilities.mcp.description": "工具和连接器通过同一套审批策略路由。", + "capabilities.plugins.title": "插件", + "capabilities.plugins.description": "插件包可以同时提供技能、工具和智能体。", + "capabilities.hooks.title": "钩子", + "capabilities.hooks.description": "本地自动化只通过共享安全策略运行。", + "capabilities.subagents.title": "子智能体", + "capabilities.subagents.description": "需要时自动选择专门的智能体。", + "capabilities.providers.title": "提供商路由", + "capabilities.providers.description": "一份 provider 配置驱动所有引擎。", + "capabilities.projections.title": "引擎映射", + "capabilities.projections.description": + "Moss 在启动时把统一源映射给每个引擎。", + "capabilities.approval.title": "新工具访问前询问", + "capabilities.approval.description": "新的 MCP 或钩子能力在使用前需要确认。", + "capabilities.nativeScope.label": "Codex 原生和云端能力范围", + "capabilities.native.globalDictation.title": "全局听写", + "capabilities.native.globalDictation.description": + "麦克风采集保持门控,直到 Moss 拥有跨引擎的原生听写能力。", + "capabilities.native.realtime.title": "实时语音", + "capabilities.native.realtime.description": + "Realtime 会话需要 Moss 麦克风权限、传输层和引擎支持后才启用输入框控制。", + "capabilities.native.cloudEnvironments.title": "云环境", + "capabilities.native.cloudEnvironments.description": + "Codex 云端 setup script、secrets 和网络规则映射到 Moss 环境配置档。", + "capabilities.native.avatarOverlay.title": "头像浮层", + "capabilities.native.avatarOverlay.description": + "官方宠物保持 hook 驱动;1Code 不会从这个设置页启动第二个浮层。", + "capabilities.native.status.gated": "门控", + "capabilities.native.status.scoped": "已限定", + "capabilities.native.status.deferred": "延后", + "capabilities.status.enabled": "{count} 个已启用", + "capabilities.status.readyCount": "{ready}/{total} 就绪", + + "hotkeyWindow.ariaLabel": "Codex 快捷键窗口", + "hotkeyWindow.title": "键盘快捷键", + "hotkeyWindow.description": "Codex 风格的独立快捷键窗口。", + "hotkeyWindow.closeHint": "关闭窗口", + "hotkeyWindow.keyboardSettings": "键盘设置", + "hotkeyWindow.groups.general": "通用", + "hotkeyWindow.groups.start": "开始", + "hotkeyWindow.groups.navigation": "导航", + "hotkeyWindow.groups.view": "视图", + "hotkeyWindow.groups.workspace": "工作区", + "hotkeyWindow.actions.showShortcuts.title": "显示快捷键", + "hotkeyWindow.actions.showShortcuts.description": "打开这个独立窗口。", + "hotkeyWindow.actions.newConversation.title": "新对话", + "hotkeyWindow.actions.newConversation.description": "从当前工作区开始新的任务。", + "hotkeyWindow.actions.goToFile.title": "转到文件", + "hotkeyWindow.actions.goToFile.description": "搜索并打开工作区文件。", + "hotkeyWindow.actions.searchChat.title": "搜索当前对话", + "hotkeyWindow.actions.searchChat.description": "在当前消息流里查找文本。", + "hotkeyWindow.actions.toggleSidebar.title": "切换侧边栏", + "hotkeyWindow.actions.toggleSidebar.description": "显示或隐藏 Codex 风格主侧边栏。", + "hotkeyWindow.actions.settings.title": "设置", + "hotkeyWindow.actions.settings.description": "打开桌面设置。", + "hotkeyWindow.actions.openEditor.title": "在编辑器中打开", + "hotkeyWindow.actions.openEditor.description": "用首选编辑器打开工作区。", + "hotkeyWindow.actions.openPreviewFile.title": "打开预览文件", + "hotkeyWindow.actions.openPreviewFile.description": "在编辑器中打开当前预览文件。", + "hotkeyWindow.pages.back": "返回", + "hotkeyWindow.pages.newThread.title": "新对话", + "hotkeyWindow.pages.newThread.description": + "从当前工作区打开一个新的 Codex 任务,而不离开快捷键窗口的上下文。", + "hotkeyWindow.pages.newThread.primaryAction": "开始新对话", + "hotkeyWindow.pages.newThread.composerHint": "输入任务,提交后会在这里显示 worktree 状态。", + "hotkeyWindow.pages.worktreeInit.title": "正在创建 worktree", + "hotkeyWindow.pages.worktreeInit.description": + "创建隔离工作区并运行环境设置。", + "hotkeyWindow.pages.worktreeInit.primaryAction": "在编辑器中打开", + "hotkeyWindow.pages.worktreeInit.phase.idle": "等待任务", + "hotkeyWindow.pages.worktreeInit.phase.creating": "创建中", + "hotkeyWindow.pages.worktreeInit.phase.created": "已创建", + "hotkeyWindow.pages.worktreeInit.phase.setup": "运行设置", + "hotkeyWindow.pages.worktreeInit.phase.ready": "就绪", + "hotkeyWindow.pages.worktreeInit.phase.failed": "需要处理", + "hotkeyWindow.pages.worktreeInit.projectLabel": "项目", + "hotkeyWindow.pages.worktreeInit.branchLabel": "分支", + "hotkeyWindow.pages.worktreeInit.pathLabel": "路径", + "hotkeyWindow.pages.worktreeInit.createStep": "创建 worktree", + "hotkeyWindow.pages.worktreeInit.setupStep": "运行环境设置", + "hotkeyWindow.pages.worktreeInit.waitingOutput": "等待输出...", + "hotkeyWindow.pages.worktreeInit.editEnvironment": "编辑环境", + "hotkeyWindow.pages.worktreeInit.autoFix": "自动修复", + "hotkeyWindow.pages.worktreeInit.retry": "重试", + "hotkeyWindow.pages.worktreeInit.workLocally": "改用本地工作区", + "hotkeyWindow.pages.worktreeInit.cancel": "取消", + "hotkeyWindow.pages.worktreeInit.continueAnyway": "继续", + "hotkeyWindow.pages.worktreeInit.openWorktree": "在编辑器中打开", + "hotkeyWindow.pages.worktreeInit.creatingOutput": "正在基于 {branch} 创建 worktree...", + "hotkeyWindow.pages.worktreeInit.readyOutput": "worktree 就绪: {path}", + + "pluginsPage.title": "插件", + "pluginsPage.subtitle": "在你常用的工具中使用 Codex", + "pluginsPage.modeTabs": "插件页面类型", + "pluginsPage.mcp.subtitle": "配置 Codex 可调用的 MCP 服务器", + "pluginsPage.mcp.search": "搜索 MCP 服务器", + "pluginsPage.skills": "技能", + "pluginsPage.skills.subtitle": "给 Codex 添加可复用能力", + "pluginsPage.skills.search": "搜索技能", + "pluginsPage.skills.installed": "已安装", + "pluginsPage.skills.recommended": "推荐", + "pluginsPage.skills.createSkill": "新技能", + "pluginsPage.skills.try": "在聊天中试用", + "pluginsPage.skills.install": "安装", + "pluginsPage.skills.loading": "正在加载技能", + "pluginsPage.skills.loadingDescription": "正在读取本地 Codex 技能源。", + "pluginsPage.skills.empty.title": "还没有已安装技能", + "pluginsPage.skills.empty.description": + "启用带技能的插件,或在当前项目中创建一个技能。", + "pluginsPage.skills.noDescription": "本地技能", + "pluginsPage.skills.createPrompt": + "帮我创建一个适合当前项目的 Codex 技能,并列出触发词、工作流和文件结构。", + "pluginsPage.skills.runtimePrompt": + "使用 {skill} 技能处理当前项目,先说明它能做什么、需要哪些文件或授权,然后给出第一步。", + "pluginsPage.skills.documents.prompt": + "使用 Documents 技能帮我处理当前项目里的文档,先列出可执行的编辑、导出和校验步骤。", + "pluginsPage.skills.research.prompt": + "使用 Research 技能帮我围绕当前项目做调研,先列出要查证的问题、来源和结论格式。", + "pluginsPage.skills.workflows.prompt": + "使用 Workflows 技能帮我把当前项目的重复工作整理成可复用流程,并给出执行步骤。", + "pluginsPage.skills.status.recommended": "推荐", + "pluginsPage.skills.recommended.browserQa.description": + "检查本地网页体验、截图和可访问性", + "pluginsPage.skills.recommended.githubOps.description": + "处理 issue、PR、CI 和发布流程", + "pluginsPage.skills.recommended.dataAnalytics.description": + "分析数据、生成图表和报告", + "pluginsPage.onboarding.title": "按角色开始", + "pluginsPage.onboarding.description": + "从现有插件、连接器和技能中选择一组推荐入口。", + "pluginsPage.onboarding.rolesLabel": "插件入门角色", + "pluginsPage.onboarding.start": "用这个角色开始", + "pluginsPage.onboarding.roles.developer.title": "开发者", + "pluginsPage.onboarding.roles.developer.description": + "GitHub、浏览器 QA 和本地 Chrome 上下文,适合代码、PR、CI 和页面验证。", + "pluginsPage.onboarding.roles.developer.prompt": + "为当前项目设置 GitHub 和浏览器 QA 工作流。先检查哪些连接器或技能可用,再建议第一个安全动作。", + "pluginsPage.onboarding.roles.operator.title": "运营", + "pluginsPage.onboarding.roles.operator.description": + "邮件、日历、Slack 和工作流技能,适合会议、团队上下文和重复流程。", + "pluginsPage.onboarding.roles.operator.prompt": + "为当前项目设置运营工作流,优先使用日历、邮件和团队上下文。先列出需要连接的工具和本地可替代方案。", + "pluginsPage.onboarding.roles.researcher.title": "研究", + "pluginsPage.onboarding.roles.researcher.description": + "Drive、Notion、Research 和数据分析,适合资料检索、知识库和报告。", + "pluginsPage.onboarding.roles.researcher.prompt": + "为当前项目设置研究工作流,优先使用文档、Drive 或 Notion 上下文以及数据分析。先列出来源和第一步检索动作。", + "pluginsPage.add": "添加", + "pluginsPage.refresh": "刷新", + "pluginsPage.prompt": "我们应该在当前项目中做些什么?", + "pluginsPage.promptWithProject": "我们应该在{project}中做些什么?", + "pluginsPage.composerPlaceholder": "随心输入", + "pluginsPage.fullAccess": "完全访问", + "pluginsPage.model": "5.5 超高", + "pluginsPage.newChatWithTool": "使用工具开始新对话", + "pluginsPage.sharedAcross": + "由 Hermes、Claude Code 和 Codex 共享", + "pluginsPage.openSettings": "管理插件", + "pluginsPage.installed": "已安装", + "pluginsPage.installedPlugins": "已安装插件", + "pluginsPage.enabled": "已启用", + "pluginsPage.mcpServers": "MCP 服务器", + "pluginsPage.availableTools": "可用工具", + "pluginsPage.search": "搜索插件...", + "pluginsPage.catalogSearch": "搜索插件", + "pluginsPage.filter": "筛选", + "pluginsPage.sourceTabs": "插件来源", + "pluginsPage.source.openai": "由 OpenAI 提供", + "pluginsPage.source.workspace": "由你的工作空间提供", + "pluginsPage.source.personal": "个人", + "pluginsPage.source.mine": "个人", + "pluginsPage.connected": "已连接", + "pluginsPage.manage": "管理", + "pluginsPage.connectedMore": "另有 14 个", + "pluginsPage.manage.subtitle": "管理已连接的工具、应用、MCP 和技能", + "pluginsPage.manage.tabsLabel": "插件管理类型", + "pluginsPage.manage.tabs.plugins": "插件", + "pluginsPage.manage.tabs.apps": "应用", + "pluginsPage.manage.tabs.mcps": "MCP", + "pluginsPage.manage.tabs.skills": "技能", + "pluginsPage.manage.tabs.marketplace": "市场", + "pluginsPage.manage.installed": "已安装", + "pluginsPage.manage.marketplace": "市场", + "pluginsPage.manage.browseMarketplace": "浏览市场", + "pluginsPage.manage.connectedDescription": "可在 Codex 中使用", + "pluginsPage.manage.apps": "应用", + "pluginsPage.manage.mcps": "MCP 服务器", + "pluginsPage.manage.skills": "技能", + "pluginsPage.manage.apps.drive.description": "连接文件、文档、表格和幻灯片", + "pluginsPage.manage.apps.gmail.description": "把邮件上下文带入 Codex", + "pluginsPage.manage.apps.calendar.description": "管理日程、会议和可用时间", + "pluginsPage.manage.apps.chrome.description": "使用浏览器页面和会话上下文", + "pluginsPage.manage.mcps.github.description": "提供 PR、issue、CI 和发布工具", + "pluginsPage.manage.mcps.linear.description": "读取 issue、项目和路线图状态", + "pluginsPage.manage.mcps.slack.description": "从团队讨论中读取上下文", + "pluginsPage.manage.mcps.notion.description": "连接规格、研究和知识库页面", + "pluginsPage.manage.skills.documents.description": "处理文档、编辑和导出工作流", + "pluginsPage.manage.skills.research.description": "组织调研、来源和结论", + "pluginsPage.manage.skills.workflows.description": "运行项目内的可复用工作流", + "pluginsPage.catalog": "插件目录", + "pluginsPage.catalogDescription": + "浏览已安装插件、连接器、技能、命令和 MCP 服务器。", + "pluginsPage.featured": "Featured", + "pluginsPage.featuredDescription": + "连接器从同一套 Moss 记忆、钩子、技能和 MCP 审批读取能力。", + "pluginsPage.viewMoreFeatured": "查看 Teams, SharePoint 和另外 4 个", + "pluginsPage.productivity": "Productivity", + "pluginsPage.business": "Business & Operations", + "pluginsPage.catalog.github.title": "GitHub", + "pluginsPage.catalog.github.description": + "Triage PRs, issues, CI, and publish flows", + "pluginsPage.catalog.slack.title": "Slack", + "pluginsPage.catalog.slack.description": "Read and manage Slack", + "pluginsPage.catalog.notion.title": "Notion", + "pluginsPage.catalog.notion.description": + "Notion workflows for specs, research, meetings, and knowledge capture", + "pluginsPage.catalog.googleCalendar.title": "Google Calendar", + "pluginsPage.catalog.googleCalendar.description": + "Manage Google Calendar events and schedules", + "pluginsPage.catalog.googleDrive.title": "Google Drive", + "pluginsPage.catalog.googleDrive.description": + "Work across Drive, Docs, Sheets, and Slides", + "pluginsPage.catalog.recordReplay.title": "Record & Replay", + "pluginsPage.catalog.recordReplay.description": + "Record what I'm doing on my Mac and turn it into a Skill", + "pluginsPage.catalog.actively.title": "Actively", + "pluginsPage.catalog.actively.description": + "Account intelligence and prospecting signals from Actively", + "pluginsPage.catalog.apollo.title": "Apollo", + "pluginsPage.catalog.apollo.description": + "B2B sales intelligence and engagement from Apollo", + "pluginsPage.catalog.salesforce.title": "Salesforce", + "pluginsPage.catalog.salesforce.description": + "Work with Salesforce accounts, opportunities, and records", + "pluginsPage.catalog.hubspot.title": "HubSpot", + "pluginsPage.catalog.hubspot.description": + "Manage CRM contacts, companies, deals, and notes", + "pluginsPage.catalog.github.longDescription": + "Use GitHub to inspect repositories, review pull requests, address feedback, debug failing Actions checks, and prepare code changes for review through a connector-first workflow with targeted CLI fallbacks.", + "pluginsPage.catalog.recordReplay.longDescription": + "Record & Replay lets Codex record your actions on your Mac to create skills for more automated workflows. When you choose to start a recording, Codex will record your mouse clicks, text you type, and the content in windows you interact with until you stop it (up to 30 minutes). You can stop or cancel recording at any time, and cancelling will discard any existing recording. Avoid recording sensitive workflows.", + "pluginsPage.catalog.github.prompt": + "使用 GitHub 插件帮我检查当前项目的 PR、issues、CI 和发布风险,先列出需要连接或授权的步骤。", + "pluginsPage.catalog.github.promptExample": + "Inspect PRs, triage issues, debug failing checks, and prepare code changes for review", + "pluginsPage.catalog.slack.prompt": + "使用 Slack 插件帮我整理近期团队讨论、决策和未解决问题,先列出需要连接或授权的步骤。", + "pluginsPage.catalog.notion.prompt": + "使用 Notion 插件帮我整理规格、调研和会议记录,先列出需要连接或授权的步骤。", + "pluginsPage.catalog.googleCalendar.prompt": + "使用 Google Calendar 插件帮我检查日程、会议准备和可用时间,先列出需要连接或授权的步骤。", + "pluginsPage.catalog.googleDrive.prompt": + "使用 Google Drive 插件帮我审查 Drive、Docs、Sheets 和 Slides 里的项目资料,先列出需要连接或授权的步骤。", + "pluginsPage.catalog.recordReplay.prompt": + "Record my workflow and turn it into a reusable skill", + "pluginsPage.catalog.recordReplay.promptExample.workflow": + "Record my workflow and turn it into a reusable skill", + "pluginsPage.catalog.recordReplay.promptExample.task": + "Watch me do this task and create a skill from it", + "pluginsPage.catalog.recordReplay.promptExample.fileExpense": + "Record what I'm doing and make a skill called 'File Expense'", + "pluginsPage.catalog.actively.prompt": + "使用 Actively 插件帮我整理账号情报、潜在客户信号和下一步动作,先列出需要连接或授权的步骤。", + "pluginsPage.catalog.apollo.prompt": + "使用 Apollo 插件帮我分析销售线索、公司信息和触达计划,先列出需要连接或授权的步骤。", + "pluginsPage.catalog.salesforce.prompt": + "使用 Salesforce 插件帮我检查账号、机会和记录,先列出需要连接或授权的步骤。", + "pluginsPage.catalog.hubspot.prompt": + "使用 HubSpot 插件帮我整理联系人、公司、交易和备注,先列出需要连接或授权的步骤。", + "pluginsPage.detail.installTitle": "安装 {plugin}", + "pluginsPage.detail.installedTitle": "{plugin} 已添加", + "pluginsPage.detail.developedBy": "由 {developer} 开发", + "pluginsPage.detail.about": "关于", + "pluginsPage.detail.localStatus": "本地状态", + "pluginsPage.detail.includes": "包括", + "pluginsPage.detail.includes.apps": "应用", + "pluginsPage.detail.includes.skills": "技能", + "pluginsPage.detail.includes.mcps": "MCP 服务器", + "pluginsPage.detail.availableAgents": "可用智能体", + "pluginsPage.detail.availableAgentsDescription": + "安装一次后,Hermes、Claude Code 和 Codex 都能识别并使用这套插件资源。", + "pluginsPage.detail.capabilities": "能力", + "pluginsPage.detail.info": "信息", + "pluginsPage.detail.features": "功能", + "pluginsPage.detail.developer": "开发者", + "pluginsPage.detail.category": "类别", + "pluginsPage.detail.website": "网站", + "pluginsPage.detail.policies": "", + "pluginsPage.detail.privacyPolicy": "隐私政策", + "pluginsPage.detail.termsOfService": "服务条款", + "pluginsPage.detail.addToCodex": "添加到 Codex", + "pluginsPage.detail.addingToCodex": "正在添加到 Codex", + "pluginsPage.detail.tryInChat": "在对话中试用", + "pluginsPage.detail.copyLink": "复制链接", + "pluginsPage.detail.copiedLink": "已复制", + "pluginsPage.detail.breadcrumbNavigation": "面包屑导航", + "pluginsPage.detail.configureMcp": "在 MCP 设置中配置", + "pluginsPage.detail.disableSkill": "禁用技能", + "pluginsPage.detail.enableSkill": "启用技能", + "pluginsPage.detail.moreActions": "更多操作", + "pluginsPage.detail.removeFromCodex": "从 Codex 移除", + "pluginsPage.detail.removingFromCodex": "正在从 Codex 移除", + "pluginsPage.detail.installFailed": "无法添加 {plugin},请稍后再试。", + "pluginsPage.detail.uninstallFailed": "无法移除 {plugin},请稍后再试。", + "pluginsPage.detail.copyLinkFailed": "无法复制 {plugin} 链接。", + "pluginsPage.detail.close": "关闭插件详情", + "pluginsPage.uninstall.title": "移除 {plugin}?", + "pluginsPage.uninstall.body": + "Codex 将停止使用此插件提供的应用、技能和 MCP 服务器。", + "pluginsPage.uninstall.impact": "将停用", + "pluginsPage.uninstall.apps": "应用", + "pluginsPage.uninstall.skills": "技能", + "pluginsPage.uninstall.mcps": "MCP 服务器", + "pluginsPage.uninstall.cancel": "取消", + "pluginsPage.uninstall.confirm": "移除插件", + "pluginsPage.uninstall.removing": "正在移除", + "pluginsPage.tool.slack.title": "连接消息传送", + "pluginsPage.tool.slack.description": "从近期团队讨论中获取背景信息", + "pluginsPage.tool.slack.prompt": + "使用 Slack 帮我跟进近期决策和待解决问题", + "pluginsPage.tool.gmail.title": "连接电子邮件", + "pluginsPage.tool.gmail.description": "总结电子邮件中利益相关方的请求", + "pluginsPage.tool.gmail.prompt": + "使用 Gmail 总结高管和利益相关方的请求", + "pluginsPage.tool.drive.title": "连接文件", + "pluginsPage.tool.drive.description": "审查结果、研究资料和计划", + "pluginsPage.tool.drive.prompt": + "使用 Google Drive 审查最新结果和研究资料,并标出机会", + "pluginsPage.tool.connected": "已连接", + "pluginsPage.tool.request": "请求", + "pluginsPage.noResults": "没有匹配的插件。", + "pluginsPage.loading": "正在加载插件...", + "pluginsPage.resources": "{count} 个资源", + "pluginsPage.status.enabled": "已启用", + "pluginsPage.status.disabled": "已关闭", + "pluginsPage.status.connected": "已连接", + "pluginsPage.action.signIn": "登录", + "pluginsPage.detail.version": "版本", + "pluginsPage.detail.source": "来源", + "pluginsPage.detail.homepage": "主页", + "pluginsPage.detail.tags": "标签", + "pluginsPage.detail.commands": "命令", + "pluginsPage.detail.skills": "技能", + "pluginsPage.detail.agents": "智能体", + "pluginsPage.detail.mcpServers": "MCP 服务器", + "pluginsPage.detail.select": "选择一个插件查看详情", + "pluginsPage.empty.title": "还没有插件", + "pluginsPage.empty.description": "安装插件后会显示在这里。", + "pluginsPage.empty.installPath": "安装插件到 ~/.claude/plugins/marketplaces/", + "pluginsPage.toast.authenticated": "{name} 已认证", + "pluginsPage.toast.authFailed": "认证失败", + "pluginsPage.toast.enabled": "插件已启用", + "pluginsPage.toast.disabled": "插件已关闭", + "pluginsPage.toast.updateFailed": "更新插件失败", + + "settingsResources.fromConfig": "来自配置", + "settingsResources.userConfig": "用户配置", + "settingsResources.count": "{count} 个", + "settingsResources.status.on": "已启用", + "settingsResources.status.off": "已关闭", + "settingsResources.status.review": "待确认", + + "mcpSettings.title": "MCP 服务器", + "mcpSettings.description": + "通过一套 Moss 配置为 Hermes、Claude Code、Codex 提供工具。", + "mcpSettings.enable.title": "启用 MCP", + "mcpSettings.enable.description": "让所有智能体从 Moss 统一源读取可用工具。", + "mcpSettings.approval.title": "新工具访问前询问", + "mcpSettings.approval.description": "首次连接或新增工具时先让你确认。", + "mcpSettings.connected.title": "已配置服务器", + "mcpSettings.connected.description": "来自 .moss/mcp 和引擎映射的服务器。", + "mcpSettings.approved.title": "已可用", + "mcpSettings.approved.description": "无需额外确认即可被智能体使用。", + "mcpSettings.projections.title": "引擎映射", + "mcpSettings.projections.description": + "Moss 映射到 Claude Code、Codex 和 Hermes 的状态。", + "mcpSettings.source.description": "{count} 个 MCP 服务器", + "mcpSettings.modal.title": "用户配置", + "mcpSettings.modal.description": + "这些 MCP 服务器来自 Moss 统一源,并映射给各智能体。", + "mcpSettings.empty": "还没有配置 MCP 服务器。", + + "hooksSettings.title": "钩子", + "hooksSettings.description": "通过配置和已启用的钩子管理生命周期自动化。", + "hooksSettings.runner.title": "启用钩子", + "hooksSettings.runner.description": "允许 Moss 运行已批准的本地自动化。", + "hooksSettings.confirmation.title": "执行前确认", + "hooksSettings.confirmation.description": + "钩子执行命令或修改文件前需要确认。", + "hooksSettings.enabled.title": "已启用钩子", + "hooksSettings.enabled.description": "当前可被 Moss 运行的钩子数量。", + "hooksSettings.environment.title": "环境变量", + "hooksSettings.environment.description": "允许传递给本地自动化的变量数量。", + "hooksSettings.source.description": "{count} 个钩子", + "hooksSettings.modal.title": "用户配置", + "hooksSettings.modal.description": + "这些钩子来自 .moss/hooks,并由 Moss 适配给不同智能体。", + "hooksSettings.empty": "还没有配置钩子。", + + "memorySettings.title": "个性化", + "memorySettings.description": + "把规则、记忆和项目指令收口为一套 Moss 统一源。", + "memorySettings.enable.title": "启用记忆", + "memorySettings.enable.description": "从聊天和项目上下文保留可复用记忆。", + "memorySettings.unified.title": "共享给所有智能体", + "memorySettings.unified.description": + "Claude Code、Codex 和 Hermes 使用同一套真实记忆。", + "memorySettings.memory.title": "记忆", + "memorySettings.memory.description": "来自 Moss 统一源的用户和项目记忆。", + "memorySettings.instructions.title": "自定义指令", + "memorySettings.instructions.description": + "会映射为 CLAUDE.md、AGENTS.md 或运行时注入。", + "memorySettings.projections.title": "引擎映射", + "memorySettings.projections.description": "Moss 将记忆投影给各智能体的状态。", + "memorySettings.source.description": "{count} 条记忆和指令", + "memorySettings.modal.title": "用户配置", + "memorySettings.modal.description": + "这里展示 Moss 统一源里的记忆和指令,不维护第二份数据。", + "memorySettings.empty": "还没有记忆或指令。", + + "configurationSettings.title": "配置", + "configurationSettings.description": + "项目指令和智能体运行开关来自 Moss,然后映射到 Hermes、Claude Code、Codex 和自定义 ACP。", + "configurationSettings.refresh": "刷新映射", + "configurationSettings.instructionFiles.title": "指令文件", + "configurationSettings.instructionFiles.description": + "使用 .moss/source/moss.md 作为工作区规则的唯一来源。", + "configurationSettings.agentsMd.description": + "项目 AGENTS.md 解析到 Moss 指令源。", + "configurationSettings.claudeMd.description": + "Claude Code 通过受管映射读取同一份 Moss 源。", + "configurationSettings.webSearch.title": "网页搜索", + "configurationSettings.webSearch.description": + "通过共享 provider 策略暴露浏览权限。", + "configurationSettings.transcriptExport.title": "转录导出", + "configurationSettings.transcriptExport.description": + "允许会话导出可回放日志,但不复制记忆。", + "configurationSettings.slashArguments.title": "斜杠命令参数", + "configurationSettings.slashArguments.description": + "在 Claude Code、Codex 或 Hermes 接收前规范化命令占位符。", + "configurationSettings.mossSource": "Moss 源", + "configurationSettings.slashArgumentToken": "斜杠参数令牌", + "configurationSettings.projections.title": "指令映射", + "configurationSettings.projections.description": + "只有在每个引擎需要时才投影协议专用路径,真实源只有一份。", + "configurationSettings.projections.empty": "这个引擎没有返回指令映射。", + "configurationSettings.projections.loading": "正在加载映射...", + "configurationSettings.projections.none": "没有找到映射。", + "configurationSettings.runtimeInjection": "运行时注入", + + "archivedSettings.title": "已归档对话", + "archivedSettings.description": + "已归档会话保留自己的转录,但仍由共享 Moss 源治理。", + "archivedSettings.refresh": "刷新已归档对话", + "archivedSettings.search": "搜索已归档对话...", + "archivedSettings.localArchive": "本地归档", + "archivedSettings.empty.title": "没有已归档对话", + "archivedSettings.empty.description": + "恢复后或活跃会话会显示在工作区侧边栏。", + "archivedSettings.restore": "恢复", + "archivedSettings.restored": "对话已恢复", + "archivedSettings.restoreFailed": "恢复失败", + "archivedSettings.newWorkspace": "新工作区", + "archivedSettings.noBranch": "无分支", + "archivedSettings.archived": "已归档", + + "sidebar.search": "搜索", + "sidebar.quickChat": "快速对话", + "sidebar.plugins": "插件", + "sidebar.skills": "技能", + "sidebar.mcpServers": "MCP 服务器", + "sidebar.inbox": "收件箱", + "sidebar.automations": "已安排", + "sidebar.projects": "项目", + "sidebar.library": "资料库", + "sidebar.pullRequests": "拉取请求", + "sidebar.kanban": "看板", + "sidebar.workspaces": "对话", + "sidebar.pinned": "置顶", + "sidebar.recentWorkspaces": "最近对话", + "sidebar.pinnedWorkspaces": "置顶对话", + "sidebar.drafts": "草稿", + "sidebar.newWorkspace": "新对话", + "sidebar.startNewWorkspace": "开始一个新的对话", + "sidebar.searchWorkspaces": "搜索对话...", + "sidebar.expandShow": "展开显示", + "sidebar.collapseShow": "收起显示", + "sidebar.collapseAll": "全部收起", + "sidebar.projectActions": "{project} 的项目操作", + "sidebar.projectNewChat": "在 {project} 中开始新对话", + "sidebar.projectTasks": "{project}中的已安排任务", + "sidebar.projectsSidebarOptions": "项目侧边栏选项", + "sidebar.addProject": "添加新项目", + "sidebar.expandProject": "展开项目", + "sidebar.collapseProject": "折叠项目", + "sidebar.pinnedConversations": "置顶对话", + "sidebar.scheduledTaskFolder": "已安排任务文件夹", + "sidebar.filterConversations": "筛选侧边栏对话", + "sidebar.noChats": "暂无聊天", + "sidebar.noConversations": "暂无对话", + "sidebar.settings": "设置", + "sidebar.openSettings": "打开Settings", + "sidebar.hideSidebar": "隐藏边栏", + "sidebar.update": "更新", + "sidebar.help": "帮助", + "sidebar.archive": "归档", + "sidebar.feedback": "反馈", + "sidebar.selected": "{count} 个已选择", + "sidebar.cancel": "取消", + "sidebar.archiving": "归档中...", + "sidebar.archiveAction": "归档", + + "chat.newChatOrPlan": "新建对话或计划", + "chat.newChat": "新建对话", + "chat.newPlan": "新建计划", + "chat.fullAccess": "完全访问", + "chat.permissions.change": "更改权限", + "chat.permissions.title": "如何批准 Codex 操作?", + "chat.permissions.learnMore": "了解更多", + "chat.permissions.readOnly.label": "只读", + "chat.permissions.readOnly.description": "读取文件并检查工作区,不编辑", + "chat.permissions.askApproval.label": "请求批准", + "chat.permissions.askApproval.description": "编辑外部文件和使用网络前始终询问", + "chat.permissions.approveForMe.label": "替我审批", + "chat.permissions.approveForMe.description": "仅在检测到潜在不安全操作时询问", + "chat.permissions.approveForMe.disabled": "此工作区需要默认沙盒权限", + "chat.permissions.fullAccess.label": "完全访问", + "chat.permissions.fullAccess.description": "不受限制地访问网络和电脑上的任何文件", + "chat.permissions.custom.label": "自定义 (config.toml)", + "chat.permissions.custom.description": "使用 config.toml 中定义的权限", + "chat.permissions.fullAccessConfirm.title": "确定吗?", + "chat.permissions.fullAccessConfirm.body": + "完全访问允许 Codex 在不询问的情况下访问网络并编辑电脑上的任何文件。这可能带来数据丢失或提示注入风险。", + "chat.permissions.fullAccessConfirm.cancel": "取消", + "chat.permissions.fullAccessConfirm.confirm": "开启完全访问", + "chat.newWorkspacePrompt": "你想完成什么?", + "chat.inputPlaceholder": "输入计划,@ 添加上下文,/ 使用命令", + "chat.queuePlaceholder": "添加到队列", + "chat.selectRepo": "选择仓库", + "chat.opening": "打开中...", + + "runtime.checking.title": "正在检查智能体运行时", + "runtime.checking.body": + "Moss 正在检查 Hermes、Claude Code、Codex 和 Custom ACP,确认后即可运行此对话。", + "runtime.empty.title": "没有可用智能体引擎", + "runtime.empty.body": "Moss 运行时没有返回引擎清单。", + "runtime.healthUnavailable.title": "运行时健康状态不可用", + "runtime.healthUnavailable.body": "Moss 无法读取智能体运行时健康状态。", + "runtime.engineMissing.title": "引擎不存在", + "runtime.engineMissing.body": "当前选择的引擎不在 Moss 运行时清单中。", + "runtime.fallback.title": "{engine} 正在使用备用元数据", + "runtime.fallback.body": "Moss 没有收到这个引擎的完整运行状态。", + "runtime.needsAuth.title": "{engine} 需要登录", + "runtime.needsAuth.body": "发送前请连接账号,或配置共享 Moss provider。", + "runtime.notInstalled.title": "{engine} 未安装", + "runtime.notInstalled.body": "Moss 找不到这个引擎需要的本地运行时。", + "runtime.unsupported.title": "{engine} 当前不可用", + "runtime.unsupported.body": "这个运行时暂不支持当前工作区。", + "runtime.error.title": "{engine} 运行时错误", + "runtime.error.body": "Moss 无法验证这个运行时。", + "runtime.checkingEngine.title": "正在检查 {engine}", + "runtime.checkingEngine.body": "Moss 正在检查这个运行时,确认后即可发送。", + "runtime.manifestMissing.title": "{engine} 清单缺失", + "runtime.manifestMissing.body": "Moss 运行时没有返回这个引擎的清单。", + "runtime.update.available.title": "{engine} 有可用更新", + "runtime.update.available.body": + "当前版本 {currentVersion},最新版本 {latestVersion}。新会话可以继续运行;建议更新运行时后再做长任务。", + "runtime.update.manual.title": "{engine} 有可用更新", + "runtime.update.manual.body": + "当前版本 {currentVersion},最新版本 {latestVersion}。请到运行时设置中查看更新方式。", + "runtime.update.queued.title": "{engine} 更新已排队", + "runtime.update.queued.body": "Moss 正在等待前一个 provider 更新完成。", + "runtime.update.running.title": "正在更新 {engine}", + "runtime.update.running.body": "更新命令正在运行,新会话会在更新完成后使用新版本。", + "runtime.update.failed.title": "{engine} 更新失败", + "runtime.update.failed.body": "Provider 更新命令未完成。请到运行时设置中查看详情。", + "runtime.update.failed.bodyWithMessage": "{message}", + "runtime.update.unchanged.title": "{engine} 仍需更新", + "runtime.update.unchanged.body": + "更新后仍检测到旧版本 {currentVersion}。请到运行时设置中查看详情。", + "runtime.update.succeeded.title": "{engine} 已更新", + "runtime.update.succeeded.body": "新会话会使用更新后的 provider。", + "runtime.openRuntime": "打开运行时", + "runtime.openModels": "打开模型设置", +}; + +const enUS: Dictionary = { + "common.back": "Back", + "common.forward": "Forward", + "common.refresh": "Refresh", + "common.ready": "Ready", + "common.off": "Off", + "common.enabled": "enabled", + "common.details": "Details", + "common.default": "Default", + + "settings.tabs.preferences": "General", + "settings.tabs.capabilities": "Capabilities", + "settings.tabs.profile": "Profile", + "settings.tabs.account": "Account", + "settings.tabs.models": "Models", + "settings.tabs.usage": "Usage", + "settings.tabs.billing": "Billing", + "settings.tabs.appearance": "Appearance", + "settings.tabs.agent": "Agent", + "settings.tabs.pets": "Pets", + "settings.tabs.keyboard": "Keyboard", + "settings.tabs.beta": "Beta", + "settings.tabs.projects": "Projects", + "settings.tabs.runtime": "Runtime", + "settings.tabs.desktop": "Desktop", + "settings.tabs.release": "Release", + "settings.tabs.configuration": "Configuration", + "settings.tabs.personalization": "Personalization", + "settings.tabs.environment": "Environment", + "settings.tabs.memory": "Memory", + "settings.tabs.skills": "Skills", + "settings.tabs.agents": "Custom Agents", + "settings.tabs.mcp": "MCP Servers", + "settings.tabs.hooks": "Hooks", + "settings.tabs.codeReview": "Code Review", + "settings.tabs.plugins": "Plugins", + "settings.tabs.archived": "Archived Conversations", + "settings.tabs.debug": "Debug", + "settings.groups.personal": "Personal", + "settings.groups.integrations": "Integrations", + "settings.groups.coding": "Coding", + "settings.groups.advanced": "Developer", + + "preferences.title": "General", + "preferences.description": + "Configure Moss core behavior, language, and desktop features", + "preferences.language.title": "Language", + "preferences.language.description": "Switch the app display language", + "preferences.language.zh": "中文", + "preferences.language.en": "English", + "preferences.extendedThinking.title": "Extended Thinking", + "preferences.extendedThinking.description": + "Enable deeper reasoning with more thinking tokens.", + "preferences.extendedThinking.note": "Disables response streaming.", + "preferences.defaultMode.title": "Default Mode", + "preferences.defaultMode.description": + "Mode for new agents: Plan is read-only, Agent can edit files", + "preferences.defaultMode.agent": "Agent", + "preferences.defaultMode.plan": "Plan", + "preferences.coAuthored.title": "Include Co-Authored-By", + "preferences.coAuthored.description": + "Add co-author metadata to git commits made by Claude", + "preferences.desktopNotifications.title": "Desktop Notifications", + "preferences.desktopNotifications.description": + "Show system notifications when an agent needs input or completes work", + "preferences.soundNotifications.title": "Sound Notifications", + "preferences.soundNotifications.description": + "Play a sound when an agent completes work while you're away", + "preferences.notifyFocused.title": "Notify When Focused", + "preferences.notifyFocused.description": + "Show notifications even when the app window is active", + "preferences.quickSwitch.title": "Quick Switch", + "preferences.quickSwitch.description": "What Ctrl+Tab switches between", + "preferences.quickSwitch.workspaces": "Workspaces", + "preferences.quickSwitch.agents": "Agents", + "preferences.autoAdvance.title": "Auto-advance", + "preferences.autoAdvance.description": + "Where to go after archiving a workspace", + "preferences.autoAdvance.next": "Go to next workspace", + "preferences.autoAdvance.previous": "Go to previous workspace", + "preferences.autoAdvance.close": "Close workspace", + "preferences.preferredEditor.title": "Preferred Editor", + "preferences.preferredEditor.description": + "Default app for opening workspaces", + "preferences.analytics.title": "Share Usage Analytics", + "preferences.analytics.description": + "Help improve Moss by sharing anonymous usage data. We only track feature usage and app performance, never your code, prompts, or messages. No AI training on your data.", + + "agentSettings.title": "Agent", + "agentSettings.description": + "Configure default approval, sandbox, and local dependency behavior for new conversations.", + "agentSettings.config.title": "Custom config.toml settings", + "agentSettings.config.description": + "Custom permission overrides still defer to Codex config files.", + "agentSettings.config.project.title": "Project config", + "agentSettings.config.project.description": + "Project-level Codex configuration for the current workspace.", + "agentSettings.config.global.title": "Global config", + "agentSettings.config.global.description": + "Global scope for user and admin Codex configuration.", + "agentSettings.config.user.title": "User config", + "agentSettings.config.user.description": + "Codex config.toml settings for the current user.", + "agentSettings.config.admin.title": "Admin config", + "agentSettings.config.admin.description": + "Codex settings supplied by admin or managed policy.", + "agentSettings.config.admin.value": "Managed", + "agentSettings.defaultMode.title": "Default mode", + "agentSettings.defaultMode.description": + "Choose whether new conversations start in Agent or Plan mode.", + "agentSettings.approval.title": "Approval policy", + "agentSettings.approval.description": + "How new Agent conversations request confirmation by default.", + "agentSettings.approval.onRequest": "On request", + "agentSettings.approval.onFailure": "On failure", + "agentSettings.approval.untrusted": "Untrusted", + "agentSettings.approval.never": "Never", + "agentSettings.sandbox.title": "Sandbox settings", + "agentSettings.sandbox.description": + "Default local access scope for new Agent conversations.", + "agentSettings.sandbox.readOnly": "Read only", + "agentSettings.sandbox.workspaceWrite": "Workspace write", + "agentSettings.sandbox.fullAccess": "Full access", + "agentSettings.network.title": "Allow network access", + "agentSettings.network.description": + "The current runtime only enables network access together with Full access.", + "agentSettings.dependencies.title": "Workspace Dependencies", + "agentSettings.dependencies.description": + "Local Workspace runtime dependencies used by Codex sessions.", + "agentSettings.dependencies.version.title": "Current version", + "agentSettings.dependencies.version.description": + "Uses the local dependencies detected by 1code packaging and runtime setup.", + "agentSettings.dependencies.version.value": "Bundled", + "agentSettings.dependencies.codex.title": "Codex dependencies", + "agentSettings.dependencies.codex.description": + "New Codex sessions use the runtime dependencies managed by the app.", + "agentSettings.dependencies.diagnose.title": + "Diagnose issues in Codex Workspace", + "agentSettings.dependencies.diagnose.description": + "The local diagnostics API is not wired yet; this preserves the Codex Desktop entry point.", + "agentSettings.dependencies.diagnose.label": + "Diagnose issues in Codex Workspace", + "agentSettings.dependencies.reset.title": "Reset and install Workspace", + "agentSettings.dependencies.reset.description": + "The local reinstall API is not wired yet, so the reset action is intentionally disabled.", + "agentSettings.dependencies.reset.label": "Reset and install Workspace", + "agentSettings.experimental.title": "Experimental features (Beta)", + "agentSettings.experimental.description": + "Experimental feature status is shown here; detailed toggles stay in the Beta page.", + + "settings.personalization.pets.title": "Pets", + "settings.personalization.pets.current": "{petName} selected", + "settings.personalization.pets.openPet": "Wake pet", + "settings.personalization.pets.tuckAwayPet": "Tuck away pet", + "settings.pets.custom.title": "Custom pets", + "settings.pets.custom.openFolder": "Open folder", + "settings.pets.custom.openFolderError": "Unable to open pet folder", + "settings.pets.loadCustomError": "Unable to load custom pets", + "petSettings.description": + "Reads the current local Codex pets directory, Stop hook, and official pointer runtime state.", + "petSettings.selectedAvatar.description": "Current avatar from Codex config:", + "petSettings.loading": "Loading", + "petSettings.none": "None", + "petSettings.noPets": "No custom pets found", + "petSettings.custom.count": "{count} found", + "petSettings.custom.loading": "Reading the custom pets directory.", + "petSettings.custom.selected": "Selected", + "petSettings.custom.available": "Available", + "petSettings.stopHook.title": "Stop hook", + "petSettings.runtime.title": "Official runtime", + "petSettings.runtime.description": "Official pet pointer runtime.", + "petSettings.window.title": "Official pet window", + "petSettings.window.description": "The latest hook log has no window error.", + "petSettings.lastDecision.title": "Last hook decision", + "petSettings.action.success": "Pet action sent", + "petSettings.status.ready": "Ready", + "petSettings.status.configured": "Configured", + "petSettings.status.available": "Available", + "petSettings.status.unknown": "Unknown", + "petSettings.status.missing": "Missing", + "petSettings.status.notConfigured": "Not configured", + "petSettings.status.missingHook": "Missing hook", + "petSettings.status.missingRuntime": "Missing runtime", + "petSettings.status.windowUnavailable": "Window unavailable", + + "capabilities.title": "Capabilities", + "capabilities.description": + "One Moss setup is shared by Hermes, Claude Code, Codex, and custom agents.", + "capabilities.unifiedSource.title": "Moss Unified Source", + "capabilities.unifiedSource.description": + "Install once, then every agent receives the same workspace abilities.", + "capabilities.memory.title": "Memory", + "capabilities.memory.description": + "Personal and project memory stay shared across engines.", + "capabilities.skills.title": "Skills and commands", + "capabilities.skills.description": + "Installed skills are available from the same composer.", + "capabilities.mcp.title": "MCP servers", + "capabilities.mcp.description": + "Tools and connectors are routed through one approval policy.", + "capabilities.plugins.title": "Plugins", + "capabilities.plugins.description": + "Plugin bundles can add skills, tools, and agents together.", + "capabilities.hooks.title": "Hooks", + "capabilities.hooks.description": + "Local automation runs only through the shared safety policy.", + "capabilities.subagents.title": "Subagents", + "capabilities.subagents.description": + "Specialized agents are selected automatically when useful.", + "capabilities.providers.title": "Provider routing", + "capabilities.providers.description": + "One provider configuration can power every engine.", + "capabilities.projections.title": "Engine projections", + "capabilities.projections.description": + "Moss maps the shared source into each engine at launch time.", + "capabilities.approval.title": "Ask before new tool access", + "capabilities.approval.description": + "New MCP or hook capabilities require approval before use.", + "capabilities.nativeScope.label": "Codex native and cloud capability scope", + "capabilities.native.globalDictation.title": "Global dictation", + "capabilities.native.globalDictation.description": + "Microphone capture stays gated until Moss owns a native dictation capability across engines.", + "capabilities.native.realtime.title": "Realtime voice", + "capabilities.native.realtime.description": + "Realtime sessions require Moss microphone permission, transport, and engine support before composer controls are enabled.", + "capabilities.native.cloudEnvironments.title": "Cloud environments", + "capabilities.native.cloudEnvironments.description": + "Codex cloud setup scripts, secrets, and network rules map to Moss environment profiles.", + "capabilities.native.avatarOverlay.title": "Avatar overlay", + "capabilities.native.avatarOverlay.description": + "The official pet remains hook-driven; 1Code will not start a second overlay from this settings surface.", + "capabilities.native.status.gated": "Gated", + "capabilities.native.status.scoped": "Scoped", + "capabilities.native.status.deferred": "Deferred", + "capabilities.status.enabled": "{count} enabled", + "capabilities.status.readyCount": "{ready}/{total} ready", + + "hotkeyWindow.ariaLabel": "Codex Hotkey Window", + "hotkeyWindow.title": "Keyboard shortcuts", + "hotkeyWindow.description": "A Codex-style standalone hotkey window.", + "hotkeyWindow.closeHint": "Close window", + "hotkeyWindow.keyboardSettings": "Keyboard settings", + "hotkeyWindow.groups.general": "General", + "hotkeyWindow.groups.start": "Start", + "hotkeyWindow.groups.navigation": "Navigation", + "hotkeyWindow.groups.view": "View", + "hotkeyWindow.groups.workspace": "Workspace", + "hotkeyWindow.actions.showShortcuts.title": "Show shortcuts", + "hotkeyWindow.actions.showShortcuts.description": "Open this standalone window.", + "hotkeyWindow.actions.newConversation.title": "New conversation", + "hotkeyWindow.actions.newConversation.description": "Start a new task from the current workspace.", + "hotkeyWindow.actions.goToFile.title": "Go to file", + "hotkeyWindow.actions.goToFile.description": "Search and open a workspace file.", + "hotkeyWindow.actions.searchChat.title": "Search current chat", + "hotkeyWindow.actions.searchChat.description": "Find text in the current message stream.", + "hotkeyWindow.actions.toggleSidebar.title": "Toggle sidebar", + "hotkeyWindow.actions.toggleSidebar.description": "Show or hide the Codex-style primary sidebar.", + "hotkeyWindow.actions.settings.title": "Settings", + "hotkeyWindow.actions.settings.description": "Open desktop settings.", + "hotkeyWindow.actions.openEditor.title": "Open in editor", + "hotkeyWindow.actions.openEditor.description": "Open the workspace in the preferred editor.", + "hotkeyWindow.actions.openPreviewFile.title": "Open preview file", + "hotkeyWindow.actions.openPreviewFile.description": "Open the current previewed file in the editor.", + "hotkeyWindow.pages.back": "Back", + "hotkeyWindow.pages.newThread.title": "New conversation", + "hotkeyWindow.pages.newThread.description": + "Open a new Codex task from the current workspace without leaving the hotkey window context.", + "hotkeyWindow.pages.newThread.primaryAction": "Start new conversation", + "hotkeyWindow.pages.newThread.composerHint": "Enter a task; worktree status appears here after submit.", + "hotkeyWindow.pages.worktreeInit.title": "Creating worktree", + "hotkeyWindow.pages.worktreeInit.description": + "Create an isolated workspace and run environment setup.", + "hotkeyWindow.pages.worktreeInit.primaryAction": "Open in editor", + "hotkeyWindow.pages.worktreeInit.phase.idle": "Waiting", + "hotkeyWindow.pages.worktreeInit.phase.creating": "Creating", + "hotkeyWindow.pages.worktreeInit.phase.created": "Created", + "hotkeyWindow.pages.worktreeInit.phase.setup": "Running setup", + "hotkeyWindow.pages.worktreeInit.phase.ready": "Ready", + "hotkeyWindow.pages.worktreeInit.phase.failed": "Needs attention", + "hotkeyWindow.pages.worktreeInit.projectLabel": "Project", + "hotkeyWindow.pages.worktreeInit.branchLabel": "Branch", + "hotkeyWindow.pages.worktreeInit.pathLabel": "Path", + "hotkeyWindow.pages.worktreeInit.createStep": "Create worktree", + "hotkeyWindow.pages.worktreeInit.setupStep": "Run environment setup", + "hotkeyWindow.pages.worktreeInit.waitingOutput": "Waiting for output...", + "hotkeyWindow.pages.worktreeInit.editEnvironment": "Edit environment", + "hotkeyWindow.pages.worktreeInit.autoFix": "Auto-fix", + "hotkeyWindow.pages.worktreeInit.retry": "Retry", + "hotkeyWindow.pages.worktreeInit.workLocally": "Work locally instead", + "hotkeyWindow.pages.worktreeInit.cancel": "Cancel", + "hotkeyWindow.pages.worktreeInit.continueAnyway": "Continue anyway", + "hotkeyWindow.pages.worktreeInit.openWorktree": "Open in editor", + "hotkeyWindow.pages.worktreeInit.creatingOutput": "Creating worktree from {branch}...", + "hotkeyWindow.pages.worktreeInit.readyOutput": "Worktree ready: {path}", + + "pluginsPage.title": "Plugins", + "pluginsPage.subtitle": "Use Codex in your everyday tools", + "pluginsPage.modeTabs": "Plugin page mode", + "pluginsPage.mcp.subtitle": "Configure MCP servers Codex can use", + "pluginsPage.mcp.search": "Search MCP servers", + "pluginsPage.skills": "Skills", + "pluginsPage.skills.subtitle": "Give Codex reusable abilities", + "pluginsPage.skills.search": "Search skills", + "pluginsPage.skills.installed": "Installed", + "pluginsPage.skills.recommended": "Recommended", + "pluginsPage.skills.createSkill": "New skill", + "pluginsPage.skills.try": "Try in Chat", + "pluginsPage.skills.install": "Install", + "pluginsPage.skills.loading": "Loading skills", + "pluginsPage.skills.loadingDescription": "Reading local Codex skill sources.", + "pluginsPage.skills.empty.title": "No installed skills", + "pluginsPage.skills.empty.description": + "Enable a plugin that provides skills, or create one in the current project.", + "pluginsPage.skills.noDescription": "Local skill", + "pluginsPage.skills.createPrompt": + "Help me create a Codex skill for this project, including triggers, workflow, and file structure.", + "pluginsPage.skills.runtimePrompt": + "Use the {skill} skill in this project. Start by explaining what it can do, any files or access it needs, and the first step.", + "pluginsPage.skills.documents.prompt": + "Use the Documents skill to handle documents in this project. Start by listing the editing, export, and validation steps.", + "pluginsPage.skills.research.prompt": + "Use the Research skill to investigate this project. Start with the questions to verify, sources, and conclusion format.", + "pluginsPage.skills.workflows.prompt": + "Use the Workflows skill to turn repeated project work into a reusable workflow with execution steps.", + "pluginsPage.skills.status.recommended": "Recommended", + "pluginsPage.skills.recommended.browserQa.description": + "Inspect local web experiences, screenshots, and accessibility", + "pluginsPage.skills.recommended.githubOps.description": + "Handle issues, PRs, CI, and publishing flows", + "pluginsPage.skills.recommended.dataAnalytics.description": + "Analyze data and generate charts and reports", + "pluginsPage.onboarding.title": "Start by role", + "pluginsPage.onboarding.description": + "Choose a recommended set from the existing plugins, connectors, and skills.", + "pluginsPage.onboarding.rolesLabel": "Plugin starter roles", + "pluginsPage.onboarding.start": "Start with this role", + "pluginsPage.onboarding.roles.developer.title": "Developer", + "pluginsPage.onboarding.roles.developer.description": + "GitHub, browser QA, and local Chrome context for code, PRs, CI, and page checks.", + "pluginsPage.onboarding.roles.developer.prompt": + "Set up the GitHub and browser QA workflow for this project. Start by checking which connector or skill is available, then suggest the first safe action.", + "pluginsPage.onboarding.roles.operator.title": "Operator", + "pluginsPage.onboarding.roles.operator.description": + "Email, calendar, Slack, and workflow skills for meetings, team context, and repeated processes.", + "pluginsPage.onboarding.roles.operator.prompt": + "Set up the operations workflow for this project using calendar, email, and team context where available. Start by listing required connections and local-only fallbacks.", + "pluginsPage.onboarding.roles.researcher.title": "Researcher", + "pluginsPage.onboarding.roles.researcher.description": + "Drive, Notion, Research, and data analysis for source retrieval, knowledge bases, and reports.", + "pluginsPage.onboarding.roles.researcher.prompt": + "Set up the research workflow for this project using docs, Drive or Notion context, and data analysis where available. Start by listing sources and the first retrieval step.", + "pluginsPage.add": "Add", + "pluginsPage.refresh": "Refresh", + "pluginsPage.prompt": "What should we do in this project?", + "pluginsPage.promptWithProject": "What should we do in {project}?", + "pluginsPage.composerPlaceholder": "Ask anything", + "pluginsPage.fullAccess": "Full access", + "pluginsPage.model": "5.5 high", + "pluginsPage.newChatWithTool": "New chat with a tool", + "pluginsPage.sharedAcross": + "Shared across Hermes, Claude Code, and Codex", + "pluginsPage.openSettings": "Manage plugins", + "pluginsPage.installed": "Installed", + "pluginsPage.installedPlugins": "Installed plugins", + "pluginsPage.enabled": "Enabled", + "pluginsPage.mcpServers": "MCP servers", + "pluginsPage.availableTools": "Available tools", + "pluginsPage.search": "Search plugins...", + "pluginsPage.catalogSearch": "Search plugins", + "pluginsPage.filter": "Filter", + "pluginsPage.sourceTabs": "Plugin source", + "pluginsPage.source.openai": "Built by OpenAI", + "pluginsPage.source.workspace": "Built by your workspace", + "pluginsPage.source.personal": "Personal", + "pluginsPage.source.mine": "Personal", + "pluginsPage.connected": "Connected", + "pluginsPage.manage": "Manage", + "pluginsPage.connectedMore": "14 more", + "pluginsPage.manage.subtitle": "Manage connected tools, apps, MCPs, and skills", + "pluginsPage.manage.tabsLabel": "Plugin management type", + "pluginsPage.manage.tabs.plugins": "Plugins", + "pluginsPage.manage.tabs.apps": "Apps", + "pluginsPage.manage.tabs.mcps": "MCPs", + "pluginsPage.manage.tabs.skills": "Skills", + "pluginsPage.manage.tabs.marketplace": "Marketplace", + "pluginsPage.manage.installed": "Installed", + "pluginsPage.manage.marketplace": "Marketplace", + "pluginsPage.manage.browseMarketplace": "Browse marketplace", + "pluginsPage.manage.connectedDescription": "Available in Codex", + "pluginsPage.manage.apps": "Apps", + "pluginsPage.manage.mcps": "MCP servers", + "pluginsPage.manage.skills": "Skills", + "pluginsPage.manage.apps.drive.description": "Connect files, docs, sheets, and slides", + "pluginsPage.manage.apps.gmail.description": "Bring email context into Codex", + "pluginsPage.manage.apps.calendar.description": "Manage schedules, meetings, and availability", + "pluginsPage.manage.apps.chrome.description": "Use browser pages and session context", + "pluginsPage.manage.mcps.github.description": "Provide PR, issue, CI, and publishing tools", + "pluginsPage.manage.mcps.linear.description": "Read issue, project, and roadmap state", + "pluginsPage.manage.mcps.slack.description": "Read context from team discussions", + "pluginsPage.manage.mcps.notion.description": "Connect specs, research, and knowledge pages", + "pluginsPage.manage.skills.documents.description": "Handle document editing and export workflows", + "pluginsPage.manage.skills.research.description": "Organize research, sources, and conclusions", + "pluginsPage.manage.skills.workflows.description": "Run reusable project workflows", + "pluginsPage.catalog": "Plugin catalog", + "pluginsPage.catalogDescription": + "Browse installed plugins, connectors, skills, commands, and MCP servers.", + "pluginsPage.featured": "Featured", + "pluginsPage.featuredDescription": + "Connectors inherit the same Moss memory, hooks, skills, and MCP approvals.", + "pluginsPage.viewMoreFeatured": "View Teams, SharePoint, and 4 more", + "pluginsPage.productivity": "Productivity", + "pluginsPage.business": "Business & Operations", + "pluginsPage.catalog.github.title": "GitHub", + "pluginsPage.catalog.github.description": + "Triage PRs, issues, CI, and publish flows", + "pluginsPage.catalog.slack.title": "Slack", + "pluginsPage.catalog.slack.description": "Read and manage Slack", + "pluginsPage.catalog.notion.title": "Notion", + "pluginsPage.catalog.notion.description": + "Notion workflows for specs, research, meetings, and knowledge capture", + "pluginsPage.catalog.googleCalendar.title": "Google Calendar", + "pluginsPage.catalog.googleCalendar.description": + "Manage Google Calendar events and schedules", + "pluginsPage.catalog.googleDrive.title": "Google Drive", + "pluginsPage.catalog.googleDrive.description": + "Work across Drive, Docs, Sheets, and Slides", + "pluginsPage.catalog.recordReplay.title": "Record & Replay", + "pluginsPage.catalog.recordReplay.description": + "Record what I'm doing on my Mac and turn it into a Skill", + "pluginsPage.catalog.actively.title": "Actively", + "pluginsPage.catalog.actively.description": + "Account intelligence and prospecting signals from Actively", + "pluginsPage.catalog.apollo.title": "Apollo", + "pluginsPage.catalog.apollo.description": + "B2B sales intelligence and engagement from Apollo", + "pluginsPage.catalog.salesforce.title": "Salesforce", + "pluginsPage.catalog.salesforce.description": + "Work with Salesforce accounts, opportunities, and records", + "pluginsPage.catalog.hubspot.title": "HubSpot", + "pluginsPage.catalog.hubspot.description": + "Manage CRM contacts, companies, deals, and notes", + "pluginsPage.catalog.github.longDescription": + "Use GitHub to inspect repositories, review pull requests, address feedback, debug failing Actions checks, and prepare code changes for review through a connector-first workflow with targeted CLI fallbacks.", + "pluginsPage.catalog.recordReplay.longDescription": + "Record & Replay lets Codex record your actions on your Mac to create skills for more automated workflows. When you choose to start a recording, Codex will record your mouse clicks, text you type, and the content in windows you interact with until you stop it (up to 30 minutes). You can stop or cancel recording at any time, and cancelling will discard any existing recording. Avoid recording sensitive workflows.", + "pluginsPage.catalog.github.prompt": + "Use the GitHub plugin to review this project's PRs, issues, CI, and release risks. Start by listing any connection or authorization steps.", + "pluginsPage.catalog.github.promptExample": + "Inspect PRs, triage issues, debug failing checks, and prepare code changes for review", + "pluginsPage.catalog.slack.prompt": + "Use the Slack plugin to summarize recent team discussions, decisions, and open questions. Start by listing any connection or authorization steps.", + "pluginsPage.catalog.notion.prompt": + "Use the Notion plugin to organize specs, research, and meeting notes. Start by listing any connection or authorization steps.", + "pluginsPage.catalog.googleCalendar.prompt": + "Use the Google Calendar plugin to review schedule, meeting prep, and availability. Start by listing any connection or authorization steps.", + "pluginsPage.catalog.googleDrive.prompt": + "Use the Google Drive plugin to review project materials across Drive, Docs, Sheets, and Slides. Start by listing any connection or authorization steps.", + "pluginsPage.catalog.recordReplay.prompt": + "Record my workflow and turn it into a reusable skill", + "pluginsPage.catalog.recordReplay.promptExample.workflow": + "Record my workflow and turn it into a reusable skill", + "pluginsPage.catalog.recordReplay.promptExample.task": + "Watch me do this task and create a skill from it", + "pluginsPage.catalog.recordReplay.promptExample.fileExpense": + "Record what I'm doing and make a skill called 'File Expense'", + "pluginsPage.catalog.actively.prompt": + "Use the Actively plugin to organize account intelligence, prospecting signals, and next steps. Start by listing any connection or authorization steps.", + "pluginsPage.catalog.apollo.prompt": + "Use the Apollo plugin to analyze leads, company data, and engagement plans. Start by listing any connection or authorization steps.", + "pluginsPage.catalog.salesforce.prompt": + "Use the Salesforce plugin to review accounts, opportunities, and records. Start by listing any connection or authorization steps.", + "pluginsPage.catalog.hubspot.prompt": + "Use the HubSpot plugin to organize contacts, companies, deals, and notes. Start by listing any connection or authorization steps.", + "pluginsPage.detail.installTitle": "Install {plugin}", + "pluginsPage.detail.installedTitle": "{plugin} added", + "pluginsPage.detail.developedBy": "Developed by {developer}", + "pluginsPage.detail.about": "About", + "pluginsPage.detail.localStatus": "Local status", + "pluginsPage.detail.includes": "Includes", + "pluginsPage.detail.includes.apps": "Apps", + "pluginsPage.detail.includes.skills": "Skills", + "pluginsPage.detail.includes.mcps": "MCP servers", + "pluginsPage.detail.availableAgents": "Available agents", + "pluginsPage.detail.availableAgentsDescription": + "Install once; Hermes, Claude Code, and Codex can all recognize and use these plugin resources.", + "pluginsPage.detail.capabilities": "Capabilities", + "pluginsPage.detail.info": "Info", + "pluginsPage.detail.features": "Features", + "pluginsPage.detail.developer": "Developer", + "pluginsPage.detail.category": "Category", + "pluginsPage.detail.website": "Website", + "pluginsPage.detail.policies": "", + "pluginsPage.detail.privacyPolicy": "Privacy Policy", + "pluginsPage.detail.termsOfService": "Terms of Service", + "pluginsPage.detail.addToCodex": "Add to Codex", + "pluginsPage.detail.addingToCodex": "Adding to Codex", + "pluginsPage.detail.tryInChat": "Try in Chat", + "pluginsPage.detail.copyLink": "Copy link", + "pluginsPage.detail.copiedLink": "Copied", + "pluginsPage.detail.breadcrumbNavigation": "Breadcrumb navigation", + "pluginsPage.detail.configureMcp": "Configure in MCP Settings", + "pluginsPage.detail.disableSkill": "Disable skill", + "pluginsPage.detail.enableSkill": "Enable skill", + "pluginsPage.detail.moreActions": "More actions", + "pluginsPage.detail.removeFromCodex": "Remove from Codex", + "pluginsPage.detail.removingFromCodex": "Removing from Codex", + "pluginsPage.detail.installFailed": "Could not add {plugin}. Try again.", + "pluginsPage.detail.uninstallFailed": "Could not remove {plugin}. Try again.", + "pluginsPage.detail.copyLinkFailed": "Could not copy {plugin} link.", + "pluginsPage.detail.close": "Close plugin detail", + "pluginsPage.uninstall.title": "Remove {plugin}?", + "pluginsPage.uninstall.body": + "Codex will stop using the apps, skills, and MCP servers provided by this plugin.", + "pluginsPage.uninstall.impact": "Will disable", + "pluginsPage.uninstall.apps": "Apps", + "pluginsPage.uninstall.skills": "Skills", + "pluginsPage.uninstall.mcps": "MCP servers", + "pluginsPage.uninstall.cancel": "Cancel", + "pluginsPage.uninstall.confirm": "Remove plugin", + "pluginsPage.uninstall.removing": "Removing", + "pluginsPage.tool.slack.title": "Connect messages", + "pluginsPage.tool.slack.description": "Get context from recent team discussions", + "pluginsPage.tool.slack.prompt": + "Use Slack to catch me up on recent decisions and open questions", + "pluginsPage.tool.gmail.title": "Connect email", + "pluginsPage.tool.gmail.description": "Summarize stakeholder asks from email", + "pluginsPage.tool.gmail.prompt": + "Use Gmail to summarize exec and stakeholder requests", + "pluginsPage.tool.drive.title": "Connect files", + "pluginsPage.tool.drive.description": "Review results, research, and plans", + "pluginsPage.tool.drive.prompt": + "Use Google Drive to review the latest results and research, and flag opportunities", + "pluginsPage.tool.connected": "Connected", + "pluginsPage.tool.request": "Request", + "pluginsPage.noResults": "No plugins match this search.", + "pluginsPage.loading": "Loading plugins...", + "pluginsPage.resources": "{count} resources", + "pluginsPage.status.enabled": "Enabled", + "pluginsPage.status.disabled": "Disabled", + "pluginsPage.status.connected": "Connected", + "pluginsPage.action.signIn": "Sign in", + "pluginsPage.detail.version": "Version", + "pluginsPage.detail.source": "Source", + "pluginsPage.detail.homepage": "Homepage", + "pluginsPage.detail.tags": "Tags", + "pluginsPage.detail.commands": "Commands", + "pluginsPage.detail.skills": "Skills", + "pluginsPage.detail.agents": "Agents", + "pluginsPage.detail.mcpServers": "MCP Servers", + "pluginsPage.detail.select": "Select a plugin to view details", + "pluginsPage.empty.title": "No plugins", + "pluginsPage.empty.description": "Installed plugins will appear here.", + "pluginsPage.empty.installPath": "Install plugins to ~/.claude/plugins/marketplaces/", + "pluginsPage.toast.authenticated": "{name} authenticated", + "pluginsPage.toast.authFailed": "Authentication failed", + "pluginsPage.toast.enabled": "Plugin enabled", + "pluginsPage.toast.disabled": "Plugin disabled", + "pluginsPage.toast.updateFailed": "Failed to update plugin", + + "settingsResources.fromConfig": "From configuration", + "settingsResources.userConfig": "User configuration", + "settingsResources.count": "{count} items", + "settingsResources.status.on": "Enabled", + "settingsResources.status.off": "Off", + "settingsResources.status.review": "Needs review", + + "mcpSettings.title": "MCP Servers", + "mcpSettings.description": + "Use one Moss configuration for tools across Hermes, Claude Code, and Codex.", + "mcpSettings.enable.title": "Enable MCP", + "mcpSettings.enable.description": + "Let every agent read available tools from the Moss unified source.", + "mcpSettings.approval.title": "Ask before new tool access", + "mcpSettings.approval.description": + "Confirm first connections and newly added tools before use.", + "mcpSettings.connected.title": "Configured servers", + "mcpSettings.connected.description": + "Servers from .moss/mcp and engine mappings.", + "mcpSettings.approved.title": "Available", + "mcpSettings.approved.description": + "Servers agents can use without another confirmation.", + "mcpSettings.projections.title": "Engine projections", + "mcpSettings.projections.description": + "Mapping status for Claude Code, Codex, and Hermes.", + "mcpSettings.source.description": "{count} MCP servers", + "mcpSettings.modal.title": "User configuration", + "mcpSettings.modal.description": + "These MCP servers come from the Moss unified source and are mapped into each agent.", + "mcpSettings.empty": "No MCP servers configured yet.", + + "hooksSettings.title": "Hooks", + "hooksSettings.description": + "Manage lifecycle automation through configured and enabled hooks.", + "hooksSettings.runner.title": "Enable hooks", + "hooksSettings.runner.description": + "Allow Moss to run approved local automation.", + "hooksSettings.confirmation.title": "Confirm before running", + "hooksSettings.confirmation.description": + "Ask before hooks run commands or modify files.", + "hooksSettings.enabled.title": "Enabled hooks", + "hooksSettings.enabled.description": + "Hooks currently available to the Moss runner.", + "hooksSettings.environment.title": "Environment variables", + "hooksSettings.environment.description": + "Variables allowed to pass into local automation.", + "hooksSettings.source.description": "{count} hooks", + "hooksSettings.modal.title": "User configuration", + "hooksSettings.modal.description": + "These hooks come from .moss/hooks and are adapted for each agent.", + "hooksSettings.empty": "No hooks configured yet.", + + "memorySettings.title": "Personalization", + "memorySettings.description": + "Keep rules, memory, and project instructions in one Moss unified source.", + "memorySettings.enable.title": "Enable memory", + "memorySettings.enable.description": + "Keep reusable memory from chats and project context.", + "memorySettings.unified.title": "Share with every agent", + "memorySettings.unified.description": + "Claude Code, Codex, and Hermes use the same real memory source.", + "memorySettings.memory.title": "Memory", + "memorySettings.memory.description": + "User and project memory from the Moss unified source.", + "memorySettings.instructions.title": "Custom instructions", + "memorySettings.instructions.description": + "Mapped as CLAUDE.md, AGENTS.md, or runtime injection.", + "memorySettings.projections.title": "Engine projections", + "memorySettings.projections.description": + "How Moss projects memory into each agent.", + "memorySettings.source.description": "{count} memories and instructions", + "memorySettings.modal.title": "User configuration", + "memorySettings.modal.description": + "This shows memory and instructions from the Moss unified source without a second data copy.", + "memorySettings.empty": "No memory or instructions yet.", + + "configurationSettings.title": "Configuration", + "configurationSettings.description": + "Project instructions and agent runtime switches are sourced from Moss, then projected into Hermes, Claude Code, Codex, and Custom ACP.", + "configurationSettings.refresh": "Refresh projections", + "configurationSettings.instructionFiles.title": "Instruction files", + "configurationSettings.instructionFiles.description": + "Use .moss/source/moss.md as the single source for workspace rules.", + "configurationSettings.agentsMd.description": + "Project AGENTS.md resolves to the Moss instruction source.", + "configurationSettings.claudeMd.description": + "Claude Code reads the same Moss source through a managed projection.", + "configurationSettings.webSearch.title": "Web search", + "configurationSettings.webSearch.description": + "Expose browsing permissions through the shared provider policy.", + "configurationSettings.transcriptExport.title": "Transcript export", + "configurationSettings.transcriptExport.description": + "Allow sessions to export replayable logs without copying memory.", + "configurationSettings.slashArguments.title": "Slash command arguments", + "configurationSettings.slashArguments.description": + "Normalize command placeholders before Claude Code, Codex, or Hermes receives them.", + "configurationSettings.mossSource": "Moss source", + "configurationSettings.slashArgumentToken": "Slash argument token", + "configurationSettings.projections.title": "Instruction projections", + "configurationSettings.projections.description": + "One real source, protocol-specific paths only where each engine needs them.", + "configurationSettings.projections.empty": + "No instruction mappings returned for this engine.", + "configurationSettings.projections.loading": "Loading projections...", + "configurationSettings.projections.none": "No projections found.", + "configurationSettings.runtimeInjection": "runtime injection", + + "archivedSettings.title": "Archived Conversations", + "archivedSettings.description": + "Archived sessions keep their own transcript while remaining governed by the shared Moss source.", + "archivedSettings.refresh": "Refresh archived conversations", + "archivedSettings.search": "Search archived conversations...", + "archivedSettings.localArchive": "Local archive", + "archivedSettings.empty.title": "No archived conversations", + "archivedSettings.empty.description": + "Restored and active sessions will appear in the workspace sidebar.", + "archivedSettings.restore": "Restore", + "archivedSettings.restored": "Conversation restored", + "archivedSettings.restoreFailed": "Restore failed", + "archivedSettings.newWorkspace": "New workspace", + "archivedSettings.noBranch": "No branch", + "archivedSettings.archived": "Archived", + + "sidebar.search": "Search", + "sidebar.quickChat": "Quick Chat", + "sidebar.plugins": "Plugins", + "sidebar.skills": "Skills", + "sidebar.mcpServers": "MCP Servers", + "sidebar.inbox": "Inbox", + "sidebar.automations": "Scheduled", + "sidebar.projects": "Projects", + "sidebar.library": "Library", + "sidebar.pullRequests": "Pull Requests", + "sidebar.kanban": "Kanban", + "sidebar.workspaces": "Conversations", + "sidebar.pinned": "Pinned", + "sidebar.recentWorkspaces": "Recent conversations", + "sidebar.pinnedWorkspaces": "Pinned conversations", + "sidebar.drafts": "Drafts", + "sidebar.newWorkspace": "New chat", + "sidebar.startNewWorkspace": "Start a new conversation", + "sidebar.searchWorkspaces": "Search conversations...", + "sidebar.expandShow": "Show more", + "sidebar.collapseShow": "Show less", + "sidebar.collapseAll": "Collapse all", + "sidebar.projectActions": "{project} project actions", + "sidebar.projectNewChat": "Start new chat in {project}", + "sidebar.projectTasks": "Scheduled tasks in {project}", + "sidebar.projectsSidebarOptions": "Projects sidebar options", + "sidebar.addProject": "Add new project", + "sidebar.expandProject": "Expand project", + "sidebar.collapseProject": "Collapse project", + "sidebar.pinnedConversations": "Pinned conversations", + "sidebar.scheduledTaskFolder": "Scheduled task folder", + "sidebar.filterConversations": "Filter sidebar conversations", + "sidebar.noChats": "No chats", + "sidebar.noConversations": "No conversations", + "sidebar.settings": "Settings", + "sidebar.openSettings": "Open Settings", + "sidebar.hideSidebar": "Hide sidebar", + "sidebar.update": "Update", + "sidebar.help": "Help", + "sidebar.archive": "Archive", + "sidebar.feedback": "Feedback", + "sidebar.selected": "{count} selected", + "sidebar.cancel": "Cancel", + "sidebar.archiving": "Archiving...", + "sidebar.archiveAction": "Archive", + + "chat.newChatOrPlan": "New chat or plan", + "chat.newChat": "New chat", + "chat.newPlan": "New plan", + "chat.fullAccess": "Full access", + "chat.permissions.change": "Change permissions", + "chat.permissions.title": "How should Codex actions be approved?", + "chat.permissions.learnMore": "Learn more", + "chat.permissions.readOnly.label": "Read only", + "chat.permissions.readOnly.description": "Read files and inspect the workspace without editing", + "chat.permissions.askApproval.label": "Ask for approval", + "chat.permissions.askApproval.description": "Always ask to edit external files and use the internet", + "chat.permissions.approveForMe.label": "Approve for me", + "chat.permissions.approveForMe.description": "Only ask for actions detected as potentially unsafe", + "chat.permissions.approveForMe.disabled": "Requires default sandboxed permissions in this workspace", + "chat.permissions.fullAccess.label": "Full access", + "chat.permissions.fullAccess.description": "Unrestricted access to the internet and any file on your computer", + "chat.permissions.custom.label": "Custom (config.toml)", + "chat.permissions.custom.description": "Uses permissions defined in config.toml", + "chat.permissions.fullAccessConfirm.title": "Are you sure?", + "chat.permissions.fullAccessConfirm.body": + "Full access lets Codex access the internet and edit any file on your computer without asking. This can risk data loss or prompt injection.", + "chat.permissions.fullAccessConfirm.cancel": "Cancel", + "chat.permissions.fullAccessConfirm.confirm": "Turn on full access", + "chat.newWorkspacePrompt": "What do you want to get done?", + "chat.inputPlaceholder": "Plan, @ for context, / for commands", + "chat.queuePlaceholder": "Add to the queue", + "chat.selectRepo": "Select repo", + "chat.opening": "Opening...", + + "runtime.checking.title": "Checking agent runtimes", + "runtime.checking.body": + "Moss is checking Hermes, Claude Code, Codex, and Custom ACP before this chat can run.", + "runtime.empty.title": "No agent engines", + "runtime.empty.body": "Moss runtime returned no engine manifests.", + "runtime.healthUnavailable.title": "Runtime health unavailable", + "runtime.healthUnavailable.body": "Moss could not read agent runtime health.", + "runtime.engineMissing.title": "Engine missing", + "runtime.engineMissing.body": + "The selected engine is not present in the Moss runtime manifest.", + "runtime.fallback.title": "{engine} is using fallback metadata", + "runtime.fallback.body": + "Moss did not receive a complete runtime status for this engine.", + "runtime.needsAuth.title": "{engine} needs authentication", + "runtime.needsAuth.body": + "Connect an account or configure the shared Moss provider before sending.", + "runtime.notInstalled.title": "{engine} is not installed", + "runtime.notInstalled.body": + "Moss cannot find the local runtime needed for this engine.", + "runtime.unsupported.title": "{engine} is unavailable", + "runtime.unsupported.body": + "This runtime is currently unsupported on this workspace.", + "runtime.error.title": "{engine} runtime error", + "runtime.error.body": "Moss could not verify this runtime.", + "runtime.checkingEngine.title": "Checking {engine}", + "runtime.checkingEngine.body": + "Moss is checking this runtime before sending.", + "runtime.manifestMissing.title": "{engine} is missing", + "runtime.manifestMissing.body": + "Moss runtime returned no manifest for this engine.", + "runtime.update.available.title": "{engine} update available", + "runtime.update.available.body": + "Current version {currentVersion}; latest version {latestVersion}. New sessions can still run, but update before long tasks.", + "runtime.update.manual.title": "{engine} update available", + "runtime.update.manual.body": + "Current version {currentVersion}; latest version {latestVersion}. Review runtime settings for the update path.", + "runtime.update.queued.title": "{engine} update queued", + "runtime.update.queued.body": + "Moss is waiting for the previous provider update to finish.", + "runtime.update.running.title": "Updating {engine}", + "runtime.update.running.body": + "The provider update command is running. New sessions will use the new version after it finishes.", + "runtime.update.failed.title": "{engine} update failed", + "runtime.update.failed.body": + "The provider update command did not finish. Check runtime settings for details.", + "runtime.update.failed.bodyWithMessage": "{message}", + "runtime.update.unchanged.title": "{engine} still needs an update", + "runtime.update.unchanged.body": + "The runtime still reports {currentVersion} after updating. Check runtime settings for details.", + "runtime.update.succeeded.title": "{engine} updated", + "runtime.update.succeeded.body": + "New sessions will use the updated provider.", + "runtime.openRuntime": "Open Runtime", + "runtime.openModels": "Open Models", +}; + +const dictionaries: Record = { + "zh-CN": zhCN, + "en-US": enUS, +}; + +export function translate( + language: AppLanguage, + key: string, + values?: Record, +) { + const template = dictionaries[language][key] ?? enUS[key] ?? key; + if (!values) return template; + return template.replace(/\{(\w+)\}/g, (_match, name) => + String(values[name] ?? ""), + ); +} + +export function useI18n() { + const language = useAtomValue(appLanguageAtom); + return { + language, + t: (key: string, values?: Record) => + translate(language, key, values), + }; +} diff --git a/src/renderer/lib/mock-api.ts b/src/renderer/lib/mock-api.ts index c96bf1023..ca5d04357 100644 --- a/src/renderer/lib/mock-api.ts +++ b/src/renderer/lib/mock-api.ts @@ -379,7 +379,7 @@ export const api = { }), }, }, - useUtils: () => { + useUtils: (): AnyObj => { const utils = trpc.useUtils() return { agents: { @@ -455,8 +455,8 @@ export const api = { }, // Stubs for features not needed in desktop teams: { - getUserTeams: { useQuery: () => ({ data: [], isLoading: false }) }, - getTeam: { useQuery: () => ({ data: null, isLoading: false }) }, + getUserTeams: { useQuery: (_args?: AnyObj, _opts?: AnyObj) => ({ data: [], isLoading: false }) }, + getTeam: { useQuery: (_args?: AnyObj, _opts?: AnyObj) => ({ data: null, isLoading: false }) }, updateTeam: { useMutation: () => ({ mutate: () => {}, @@ -467,7 +467,7 @@ export const api = { }, repositorySandboxes: { getRepositoriesWithStatus: { - useQuery: () => ({ + useQuery: (_args?: AnyObj, _opts?: AnyObj) => ({ data: { repositories: [] }, isLoading: false, refetch: async () => ({ data: { repositories: [] } }), diff --git a/src/renderer/lib/trpc.ts b/src/renderer/lib/trpc.ts index 684d64c03..83090088a 100644 --- a/src/renderer/lib/trpc.ts +++ b/src/renderer/lib/trpc.ts @@ -1,4 +1,4 @@ -import { createTRPCReact } from "@trpc/react-query" +import { createTRPCReact, type CreateTRPCReact } from "@trpc/react-query" import { createTRPCProxyClient } from "@trpc/client" import { ipcLink } from "trpc-electron/renderer" import type { AppRouter } from "../../main/lib/trpc/routers" @@ -7,11 +7,45 @@ import superjson from "superjson" /** * React hooks for tRPC */ -export const trpc = createTRPCReact() +export const trpc: CreateTRPCReact = + createTRPCReact() + +type TrpcProxyClient = ReturnType> + +const MISSING_ELECTRON_BRIDGE_MESSAGE = + "1Code desktop IPC bridge is unavailable. Open this screen from the 1Code desktop app instead of a plain browser tab." + +export function hasElectronTrpcBridge(): boolean { + return ( + typeof window !== "undefined" && + Boolean((window as Window & { electronTRPC?: unknown }).electronTRPC) + ) +} + +export function createElectronIpcLink() { + if (!hasElectronTrpcBridge()) { + throw new Error(MISSING_ELECTRON_BRIDGE_MESSAGE) + } + + return ipcLink({ transformer: superjson }) +} + +function createUnavailableTrpcClient(): TrpcProxyClient { + return new Proxy( + {}, + { + get() { + throw new Error(MISSING_ELECTRON_BRIDGE_MESSAGE) + }, + }, + ) as TrpcProxyClient +} /** * Vanilla client for use outside React components (stores, utilities) */ -export const trpcClient = createTRPCProxyClient({ - links: [ipcLink({ transformer: superjson })], -}) +export const trpcClient = hasElectronTrpcBridge() + ? createTRPCProxyClient({ + links: [createElectronIpcLink()], + }) + : createUnavailableTrpcClient() diff --git a/src/shared/codex-tool-normalizer.ts b/src/shared/codex-tool-normalizer.ts index ff6df8df6..2ccf5a8b6 100644 --- a/src/shared/codex-tool-normalizer.ts +++ b/src/shared/codex-tool-normalizer.ts @@ -11,6 +11,27 @@ const CODEX_VERB_TO_TOOL_TYPE: Record = { Write: "Write", Thought: "Thinking", Fetch: "WebFetch", + TodoWrite: "TodoWrite", + PlanWrite: "PlanWrite", + ExitPlanMode: "ExitPlanMode", + AskUserQuestion: "AskUserQuestion", + PermissionRequest: "PermissionRequest", + ApprovalRequest: "ApprovalRequest", +} + +const ACP_VERB_ALIAS_TO_TOOL_TYPE: Record = { + terminal: "Bash", + run: "Bash", + write: "Write", + patch: "Edit", + todo: "TodoWrite", +} + +const BUILTIN_MCP_TOOL_NAMES: Record = { + ListMcpResources: { server: "mcp", tool: "list_resources" }, + ListMcpResourcesTool: { server: "mcp", tool: "list_resources" }, + ReadMcpResource: { server: "mcp", tool: "read_resource" }, + ReadMcpResourceTool: { server: "mcp", tool: "read_resource" }, } type CodexToolDescriptor = { @@ -19,10 +40,254 @@ type CodexToolDescriptor = { isMcp: boolean } -type NormalizeCodexToolPartOptions = { +export type NormalizeCodexToolPartOptions = { normalizeState?: boolean } +export type CodexBlockStatus = + | "queued" + | "running" + | "completed" + | "failed" + | "interrupted" + +export type CodexParsedCommandType = + | "read" + | "search" + | "list_files" + | "format" + | "test" + | "lint" + | "noop" + | "unknown" + | (string & {}) + +export type CodexParsedCommand = { + type: CodexParsedCommandType + isFinished: boolean + fileName?: string + skillName?: string + query?: string + path?: string +} + +export type CodexBaseConversationBlock = { + id: string + type: string + turnId?: string + status?: CodexBlockStatus + sourcePart?: unknown +} + +export type CodexExecBlock = CodexBaseConversationBlock & { + type: "exec" + command: string + cwd?: string + processId?: string | number | null + executionStatus: "running" | "completed" | "interrupted" | "failed" + parsedCmd: CodexParsedCommand + output?: { + stdout?: string + stderr?: string + combined?: string + exitCode?: number | null + } + status: CodexBlockStatus +} + +export type CodexMcpToolBlock = CodexBaseConversationBlock & { + type: "mcp-tool-call" + server: string + tool: string + callId: string + input?: unknown + result?: unknown + rawOutput?: unknown + appResourceUri?: string + status: CodexBlockStatus +} + +export type CodexPatchBlock = CodexBaseConversationBlock & { + type: "patch" + toolName: string + input?: unknown + output?: unknown + status: CodexBlockStatus +} + +export type CodexPatchSummaryStatus = + | "applied" + | "pending" + | "streaming" + | "rejected" + | "stopped" + +export type CodexPatchSummaryFile = { + path: string + added?: number + removed?: number + status?: CodexPatchSummaryStatus +} + +export type CodexPatchSummaryOptions = { + chatStatus?: string + displayPath?: (path: string) => string + excludePath?: (path: string) => boolean +} + +export type CodexGeneratedImageBlock = CodexBaseConversationBlock & { + type: "generated-image" + data?: unknown + url?: string + mimeType?: string + prompt?: string + status: CodexBlockStatus +} + +export type CodexTextOutputBlock = CodexBaseConversationBlock & { + type: "text-output" + title?: string + content: string + mimeType?: string + status: CodexBlockStatus +} + +export type CodexTodoListBlock = CodexBaseConversationBlock & { + type: "todo-list" + todos: unknown[] + previousTodos?: unknown[] + input?: unknown + output?: unknown + status: CodexBlockStatus +} + +export type CodexProposedPlanBlock = CodexBaseConversationBlock & { + type: "proposed-plan" + action?: string + plan?: unknown + input?: unknown + output?: unknown + status: CodexBlockStatus +} + +export type CodexActiveGoalBlock = CodexBaseConversationBlock & { + type: "active-goal" + title: string + prompt?: string + elapsed?: string + agentLabel?: string + changedFiles?: number + addedLines?: number + removedLines?: number + status: CodexBlockStatus +} + +export type CodexPermissionRequestBlock = CodexBaseConversationBlock & { + type: "permission-request" + input?: unknown + result?: unknown + status: CodexBlockStatus +} + +export type CodexUserInputAutoResolutionStatus = + | "scheduled" + | "snoozed" + | "resolved" + | "removed" + | "expired" + | (string & {}) + +export type CodexUserInputAutoResolutionState = { + requestId?: string + status?: CodexUserInputAutoResolutionStatus + deadlineMs?: number + durationMs?: number + remainingMs?: number + defaultResponseLabel?: string + reason?: string +} + +export type CodexUserInputBlock = CodexBaseConversationBlock & { + type: "user-input" + prompt?: string + input?: unknown + result?: unknown + autoResolution?: CodexUserInputAutoResolutionState + status: CodexBlockStatus +} + +export type CodexStatusBlock = CodexBaseConversationBlock & { + type: "status" + level: "info" | "warning" | "error" + title?: string + message?: string + data?: unknown + status: CodexBlockStatus +} + +export type CodexDynamicToolBlock = CodexBaseConversationBlock & { + type: "dynamic-tool-call" + toolName: string + input?: unknown + output?: unknown + status: CodexBlockStatus +} + +export type CodexConversationBlock = + | CodexExecBlock + | CodexMcpToolBlock + | CodexPatchBlock + | CodexGeneratedImageBlock + | CodexTextOutputBlock + | CodexTodoListBlock + | CodexProposedPlanBlock + | CodexActiveGoalBlock + | CodexPermissionRequestBlock + | CodexUserInputBlock + | CodexStatusBlock + | CodexDynamicToolBlock + +export type CodexOutputArtifactKind = + | "image" + | "file" + | "text" + | "resource" + | "website" + +export type CodexOutputArtifact = { + id: string + kind: CodexOutputArtifactKind + label: string + sourceBlockId: string + turnId?: string + status: CodexBlockStatus + path?: string + url?: string + mimeType?: string + prompt?: string + content?: string +} + +export function hasPrimaryCodexOutputArtifact( + artifacts: readonly CodexOutputArtifact[], +): boolean { + return artifacts.some((artifact) => + artifact.kind === "image" || + artifact.kind === "text" || + artifact.kind === "resource" || + artifact.kind === "website" + ) +} + +export type NormalizeCodexConversationBlockOptions = + NormalizeCodexToolPartOptions & { + chatStatus?: string + fallbackId?: string + messageRole?: string + partIndex?: number + turnId?: string + } + function isRecord(value: unknown): value is AnyRecord { return typeof value === "object" && value !== null } @@ -138,10 +403,23 @@ function parseCodexToolDescriptor(rawToolName: string): CodexToolDescriptor | nu } } - const spaceIndex = normalizedName.indexOf(" ") - const verb = spaceIndex === -1 ? normalizedName : normalizedName.slice(0, spaceIndex) - const detail = spaceIndex === -1 ? "" : normalizedName.slice(spaceIndex + 1).trim() - const canonicalToolName = CODEX_VERB_TO_TOOL_TYPE[verb] + const colonIndex = normalizedName.indexOf(":") + const hasToolDetailSeparator = + colonIndex > 0 && !/^[a-z][a-z0-9+.-]*:\/\//i.test(normalizedName) + const label = hasToolDetailSeparator + ? normalizedName.slice(0, colonIndex).trim() + : normalizedName + const detail = hasToolDetailSeparator + ? normalizedName.slice(colonIndex + 1).trim() + : (() => { + const spaceIndex = normalizedName.indexOf(" ") + return spaceIndex === -1 ? "" : normalizedName.slice(spaceIndex + 1).trim() + })() + const spaceIndex = label.indexOf(" ") + const verb = spaceIndex === -1 ? label : label.slice(0, spaceIndex) + const canonicalToolName = + CODEX_VERB_TO_TOOL_TYPE[verb] ?? + ACP_VERB_ALIAS_TO_TOOL_TYPE[verb.toLowerCase()] if (!canonicalToolName) return null return { @@ -244,6 +522,12 @@ function normalizeCodexToolInput( } } + if (descriptor.canonicalToolName === "Write" || descriptor.canonicalToolName === "Edit") { + if (!normalizedInput.file_path && descriptor.detail) { + normalizedInput.file_path = descriptor.detail + } + } + if (descriptor.canonicalToolName === "Bash") { if (Array.isArray(normalizedInput.command)) { normalizedInput.command = @@ -297,76 +581,2140 @@ function getPartToolName(part: AnyRecord): string | null { return null } -export function normalizeCodexToolPart( - part: unknown, - options?: NormalizeCodexToolPartOptions, -): unknown { - if (!isRecord(part)) return part - if (typeof part.type !== "string" || !part.type.startsWith("tool-")) return part +function getNonEmptyString(value: unknown): string | undefined { + if (typeof value !== "string") return undefined + const trimmed = value.trim() + return trimmed.length > 0 ? trimmed : undefined +} - const rawToolName = getPartToolName(part) - const descriptor = rawToolName ? parseCodexToolDescriptor(rawToolName) : null - const shouldNormalizeState = - options?.normalizeState === true && - (part.state === "input-available" || part.state === "output-available") +const RUNTIME_STATUS_BLOCK_TITLES: Record = { + "realtime-state": "Realtime voice", + "dictation-state": "Global dictation", + "queued-follow-up": "Queued follow-up", + "rate-limit-status": "Rate limit", + "usage-status": "Usage", + "project-event": "Project", + "library-artifact": "Library artifact", + "pull-request-status": "Pull request", + "diagnostic-snapshot": "Diagnostics", +} - const hasCodexArgsWrapper = - isRecord(part.input) && - (isRecord(part.input.args) || typeof part.input.toolName === "string") +function normalizeExitCode(value: unknown): number | null | undefined { + if (typeof value === "number" && Number.isFinite(value)) { + return value + } + if (typeof value === "string") { + const trimmed = value.trim() + if (!trimmed) return undefined + const parsed = Number(trimmed) + return Number.isFinite(parsed) ? parsed : undefined + } + if (value === null) return null + return undefined +} - if (!descriptor && !hasCodexArgsWrapper && !shouldNormalizeState) { - return part +function getNestedRecord(source: AnyRecord, key: string): AnyRecord | undefined { + return isRecord(source[key]) ? source[key] : undefined +} + +function getFiniteNumber(value: unknown): number | undefined { + if (typeof value === "number" && Number.isFinite(value)) return value + if (typeof value === "string") { + const parsed = Number(value.trim()) + return Number.isFinite(parsed) ? parsed : undefined } + return undefined +} - const normalizedType = descriptor ? `tool-${descriptor.canonicalToolName}` : part.type - const fallbackDescriptor: CodexToolDescriptor = { - canonicalToolName: normalizedType.startsWith("tool-") - ? normalizedType.slice("tool-".length) - : normalizedType, - detail: "", - isMcp: normalizedType.startsWith("tool-mcp__"), +function getFirstFiniteNumber( + source: AnyRecord | undefined, + keys: string[], +): number | undefined { + if (!source) return undefined + for (const key of keys) { + const value = getFiniteNumber(source[key]) + if (value !== undefined) return value } - const normalizedInput = - descriptor - ? normalizeCodexToolInput(part.input, descriptor) - : hasCodexArgsWrapper - ? normalizeCodexToolInput(part.input, fallbackDescriptor) - : part.input - const normalizedOutput = part.output !== undefined ? part.output : part.result - const normalizedResult = part.result !== undefined ? part.result : part.output - const outputPayload = - normalizedOutput !== undefined ? normalizedOutput : normalizedResult - const outputEnrichedInput = - fallbackDescriptor.canonicalToolName === "Read" - ? normalizeReadInputFromPayload(normalizedInput, outputPayload) - : normalizedInput - const finalInput = - outputEnrichedInput !== part.input && isShallowEqual(outputEnrichedInput, part.input) - ? part.input - : outputEnrichedInput + return undefined +} - const normalizedState = shouldNormalizeState - ? toCanonicalToolState(part.state) - : part.state +function getFirstNonEmptyStringFromRecord( + source: AnyRecord | undefined, + keys: string[], +): string | undefined { + if (!source) return undefined + for (const key of keys) { + const value = getNonEmptyString(source[key]) + if (value) return value + } + return undefined +} - const typeChanged = normalizedType !== part.type - const inputChanged = finalInput !== part.input - const stateChanged = normalizedState !== part.state - const outputChanged = normalizedOutput !== part.output - const resultChanged = normalizedResult !== part.result +function getUserInputAutoResolutionCandidate( + source: AnyRecord | undefined, +): AnyRecord | undefined { + if (!source) return undefined + return ( + getNestedRecord(source, "autoResolution") ?? + getNestedRecord(source, "auto_resolution") ?? + getNestedRecord(source, "resolutionState") ?? + getNestedRecord(source, "resolution_state") + ) +} - if (!typeChanged && !inputChanged && !stateChanged && !outputChanged && !resultChanged) { - return part +function normalizeUserInputAutoResolutionState( + part: AnyRecord, + payloadRecord: AnyRecord, +): CodexUserInputAutoResolutionState | undefined { + const inputRecord = isRecord(part.input) ? part.input : undefined + const payloadInputRecord = isRecord(payloadRecord.input) + ? payloadRecord.input + : undefined + const stateRecord = + getUserInputAutoResolutionCandidate(payloadRecord) ?? + getUserInputAutoResolutionCandidate(payloadInputRecord) ?? + getUserInputAutoResolutionCandidate(part) ?? + getUserInputAutoResolutionCandidate(inputRecord) + + if (!stateRecord) return undefined + + const status = + getFirstNonEmptyStringFromRecord(stateRecord, ["status", "state"]) ?? + getFirstNonEmptyStringFromRecord(payloadRecord, [ + "autoResolutionStatus", + "auto_resolution_status", + ]) + const deadlineMs = getFirstFiniteNumber(stateRecord, [ + "deadlineMs", + "deadline_ms", + "expiresAtMs", + "expires_at_ms", + "resolveAtMs", + "resolve_at_ms", + ]) + const durationMs = getFirstFiniteNumber(stateRecord, [ + "durationMs", + "duration_ms", + "timeoutMs", + "timeout_ms", + ]) + const remainingMs = getFirstFiniteNumber(stateRecord, [ + "remainingMs", + "remaining_ms", + ]) + + if (!status && deadlineMs === undefined && durationMs === undefined && remainingMs === undefined) { + return undefined } - const normalizedPart: AnyRecord = { ...part } - if (typeChanged) normalizedPart.type = normalizedType - if (inputChanged) normalizedPart.input = finalInput - if (stateChanged) normalizedPart.state = normalizedState - if (normalizedOutput !== undefined) normalizedPart.output = normalizedOutput - if (normalizedResult !== undefined) normalizedPart.result = normalizedResult + const normalized: CodexUserInputAutoResolutionState = {} + const requestId = + getFirstNonEmptyStringFromRecord(stateRecord, ["requestId", "request_id"]) ?? + getFirstNonEmptyStringFromRecord(payloadRecord, ["requestId", "request_id", "id"]) ?? + getFirstNonEmptyStringFromRecord(part, ["requestId", "request_id", "id"]) + if (requestId) normalized.requestId = requestId + if (status) normalized.status = status as CodexUserInputAutoResolutionStatus + if (deadlineMs !== undefined) normalized.deadlineMs = deadlineMs + if (durationMs !== undefined) normalized.durationMs = durationMs + if (remainingMs !== undefined) normalized.remainingMs = remainingMs - return normalizedPart + const defaultResponseLabel = getFirstNonEmptyStringFromRecord(stateRecord, [ + "defaultResponseLabel", + "default_response_label", + "defaultLabel", + "default_label", + "label", + ]) + if (defaultResponseLabel) normalized.defaultResponseLabel = defaultResponseLabel + + const reason = getFirstNonEmptyStringFromRecord(stateRecord, [ + "reason", + "message", + ]) + if (reason) normalized.reason = reason + + return normalized +} + +function parseJsonLikeOutput(value: unknown): unknown | undefined { + if (typeof value !== "string") return undefined + const trimmed = value.trim() + if (!trimmed) return undefined + if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return undefined + try { + return JSON.parse(trimmed) + } catch { + return undefined + } +} + +function getOutputPayload(part: AnyRecord): unknown { + if (part.output !== undefined) return part.output + if (part.result !== undefined) return part.result + const parsedErrorText = parseJsonLikeOutput(part.errorText) + if (parsedErrorText !== undefined) return parsedErrorText + const errorText = getNonEmptyString(part.errorText) + if (errorText) { + return { + stderr: errorText, + combined: errorText, + success: false, + status: "failed", + } + } + return undefined +} + +function getExitCodeFromPayload(payload: unknown): number | null | undefined { + if (!isRecord(payload)) return undefined + return ( + normalizeExitCode(payload.exitCode) ?? + normalizeExitCode(payload.exit_code) ?? + normalizeExitCode(payload.code) + ) +} + +function getTextFromContentPayload(payload: unknown): string | undefined { + if (typeof payload === "string") { + const trimmed = payload.trim() + return trimmed.length > 0 ? payload : undefined + } + + if (Array.isArray(payload)) { + const parts = payload + .map((entry) => getTextFromContentPayload(entry)) + .filter((entry): entry is string => Boolean(entry?.trim())) + return parts.length > 0 ? parts.join("\n") : undefined + } + + if (!isRecord(payload)) return undefined + + const direct = + getNonEmptyString(payload.text) ?? + getNonEmptyString(payload.output) ?? + getNonEmptyString(payload.stdout) ?? + getNonEmptyString(payload.stderr) ?? + getNonEmptyString(payload.result) ?? + getNonEmptyString(payload.value) + if (direct) return direct + + if (payload.content !== undefined) { + const nestedContent = getTextFromContentPayload(payload.content) + if (nestedContent) return nestedContent + } + if (payload.data !== undefined) { + const nestedData = getTextFromContentPayload(payload.data) + if (nestedData) return nestedData + } + if (payload.result !== undefined) { + const nestedResult = getTextFromContentPayload(payload.result) + if (nestedResult) return nestedResult + } + + return undefined +} + +function getExitCodeFromText(text: string | undefined): number | undefined { + if (!text) return undefined + const match = + text.match(/(?:exit[_\s-]?code|code)\s*\*{0,2}\s*[:=]\s*\*{0,2}\s*(-?\d+)/i) ?? + text.match(/退出码\s*(-?\d+)/) + if (!match) return undefined + const parsed = Number(match[1]) + return Number.isFinite(parsed) ? parsed : undefined +} + +function normalizeParsedTodoStatus(marker: string | undefined, text: string): string { + if (marker === "✅" || /^\s*(?:done|completed|finished)\b/i.test(text)) { + return "completed" + } + if (marker === "🔄" || /^\s*(?:active|started|in[_\s-]?progress)\b/i.test(text)) { + return "in_progress" + } + return "pending" +} + +function stripTodoStatusPrefix(text: string): string { + return text + .replace(/^\s*(?:done|completed|finished|active|started|in[_\s-]?progress|pending)\s*[::-]\s*/i, "") + .replace(/\*\*/g, "") + .trim() +} + +function getTodosFromText(text: string | undefined): AnyRecord[] { + if (!text) return [] + const todos: AnyRecord[] = [] + for (const line of text.split("\n")) { + const match = line.match(/^\s*(?:-\s*|\*(?!\*)\s*)(?:(✅|🔄|⏳)\s*)?(.*?)\s*$/u) + if (!match) continue + const rawContent = stripTodoStatusPrefix(match[2] ?? "") + if (!rawContent || /^progress\s*:/i.test(rawContent)) continue + todos.push({ + content: rawContent, + status: normalizeParsedTodoStatus(match[1], rawContent), + }) + } + return todos +} + +function normalizeExecutionStatus( + value: unknown, +): "running" | "completed" | "interrupted" | "failed" | undefined { + const status = getNonEmptyString(value)?.toLowerCase() + if (!status) return undefined + if (status === "running" || status === "in_progress" || status === "started") { + return "running" + } + if (status === "completed" || status === "complete" || status === "success") { + return "completed" + } + if ( + status === "interrupted" || + status === "stopped" || + status === "cancelled" || + status === "canceled" + ) { + return "interrupted" + } + if (status === "failed" || status === "error" || status === "errored") { + return "failed" + } + return undefined +} + +function getExplicitExecutionStatus( + part: AnyRecord, +): "running" | "completed" | "interrupted" | "failed" | undefined { + const output = getOutputPayload(part) + const input = getNestedRecord(part, "input") + return ( + normalizeExecutionStatus(part.executionStatus) ?? + normalizeExecutionStatus(part.status) ?? + (isRecord(output) ? normalizeExecutionStatus(output.executionStatus) : undefined) ?? + (isRecord(output) ? normalizeExecutionStatus(output.status) : undefined) ?? + (input ? normalizeExecutionStatus(input.executionStatus) : undefined) ?? + (input ? normalizeExecutionStatus(input.execution_status) : undefined) + ) +} + +function isActiveChatStatus(chatStatus: string | undefined): boolean { + return chatStatus === "streaming" || chatStatus === "submitted" +} + +function getCodexBlockStatus( + part: AnyRecord, + options: NormalizeCodexConversationBlockOptions | undefined, +): CodexBlockStatus { + const explicitExecutionStatus = getExplicitExecutionStatus(part) + if (explicitExecutionStatus === "interrupted") return "interrupted" + + const output = getOutputPayload(part) + const state = getNonEmptyString(part.state) + const exitCode = getExitCodeFromPayload(output) + const hasOutput = output !== undefined + const outputRecord = isRecord(output) ? output : undefined + + if ( + explicitExecutionStatus === "failed" || + state === "output-error" || + outputRecord?.success === false || + outputRecord?.isError === true || + outputRecord?.error !== undefined + ) { + return "failed" + } + + if (typeof exitCode === "number" && exitCode !== 0) { + return "failed" + } + + if ( + explicitExecutionStatus === "completed" || + state === "result" || + state === "output-available" || + hasOutput + ) { + return "completed" + } + + if (explicitExecutionStatus === "running") return "running" + + if (state === "input-streaming" || state === "input-available" || state === "call") { + if (isActiveChatStatus(options?.chatStatus) || !options?.chatStatus) { + return "running" + } + return "interrupted" + } + + return "queued" +} + +function getBlockId( + part: AnyRecord, + options: NormalizeCodexConversationBlockOptions | undefined, +): string { + const input = getNestedRecord(part, "input") + const rawId = + getNonEmptyString(part.id) ?? + getNonEmptyString(part.toolCallId) ?? + getNonEmptyString(part.tool_call_id) ?? + (input ? getNonEmptyString(input.call_id) : undefined) ?? + (input ? getNonEmptyString(input.callId) : undefined) ?? + options?.fallbackId + + if (rawId) return rawId + + const index = options?.partIndex ?? 0 + return options?.turnId ? `${options.turnId}:tool:${index}` : `tool:${index}` +} + +function getPreservedToolCallId(part: AnyRecord): string | undefined { + const input = getNestedRecord(part, "input") + return ( + getNonEmptyString(part.toolCallId) ?? + getNonEmptyString(part.tool_call_id) ?? + (input ? getNonEmptyString(input.toolCallId) : undefined) ?? + (input ? getNonEmptyString(input.tool_call_id) : undefined) + ) +} + +function hasPatchTarget(input: unknown, output: unknown): boolean { + const inputRecord = isRecord(input) ? input : undefined + const outputRecord = isRecord(output) ? output : undefined + return Boolean( + (inputRecord && + (getNonEmptyString(inputRecord.file_path) ?? + getNonEmptyString(inputRecord.filePath) ?? + getNonEmptyString(inputRecord.path))) || + (outputRecord && + (getNonEmptyString(outputRecord.file_path) ?? + getNonEmptyString(outputRecord.filePath) ?? + getNonEmptyString(outputRecord.path))), + ) +} + +const CODEX_PATCH_SUMMARY_TOOL_TYPES = new Set([ + "tool-Edit", + "tool-Write", + "tool-MultiEdit", +]) + +function getNormalizedPatchSummaryPart(part: unknown): AnyRecord | undefined { + const normalized = normalizeCodexToolPart(part, { normalizeState: true }) + return isRecord(normalized) ? normalized : undefined +} + +export function isCodexPatchSummaryToolPart(part: unknown): part is AnyRecord { + const normalized = getNormalizedPatchSummaryPart(part) + return Boolean( + normalized?.type && CODEX_PATCH_SUMMARY_TOOL_TYPES.has(normalized.type), + ) +} + +export function getCodexPatchSummaryPath(part: unknown): string | undefined { + const normalized = getNormalizedPatchSummaryPart(part) + if (!normalized) return undefined + + const input = isRecord(normalized.input) ? normalized.input : undefined + const outputPayload = getOutputPayload(normalized) + const output = isRecord(outputPayload) ? outputPayload : undefined + + return ( + (input ? getNonEmptyString(input.file_path) : undefined) ?? + (input ? getNonEmptyString(input.filePath) : undefined) ?? + (input ? getNonEmptyString(input.path) : undefined) ?? + (output ? getNonEmptyString(output.file_path) : undefined) ?? + (output ? getNonEmptyString(output.filePath) : undefined) ?? + (output ? getNonEmptyString(output.path) : undefined) + ) +} + +function getPatchSummaryNumber( + source: AnyRecord | undefined, + keys: string[], +): number | undefined { + if (!source) return undefined + for (const key of keys) { + const value = source[key] + if (typeof value === "number" && Number.isFinite(value)) { + return Math.max(0, value) + } + } + return undefined +} + +function countPatchSummaryTextLines(value: unknown): number { + if (typeof value !== "string" || value.length === 0) return 0 + return value.split("\n").length +} + +function getUnifiedPatchStats(value: unknown): { + added: number + removed: number +} { + if (typeof value !== "string") return { added: 0, removed: 0 } + let added = 0 + let removed = 0 + for (const line of value.split("\n")) { + if (line.startsWith("+") && !line.startsWith("+++")) added += 1 + if (line.startsWith("-") && !line.startsWith("---")) removed += 1 + } + return { added, removed } +} + +function getStructuredPatchStats(value: unknown): { + added: number + removed: number +} { + if (typeof value === "string") return getUnifiedPatchStats(value) + if (Array.isArray(value)) { + return value.reduce( + (total, entry) => { + const stats = getStructuredPatchStats(entry) + return { + added: total.added + stats.added, + removed: total.removed + stats.removed, + } + }, + { added: 0, removed: 0 }, + ) + } + if (!isRecord(value)) return { added: 0, removed: 0 } + + const directLines = Array.isArray(value.lines) + ? getStructuredPatchStats(value.lines) + : { added: 0, removed: 0 } + const hunks = Array.isArray(value.hunks) + ? getStructuredPatchStats(value.hunks) + : { added: 0, removed: 0 } + const patchText = + getNonEmptyString(value.patch) ?? + getNonEmptyString(value.diff) ?? + getNonEmptyString(value.udiff) ?? + getNonEmptyString(value.text) + const patchTextStats = getUnifiedPatchStats(patchText) + + return { + added: directLines.added + hunks.added + patchTextStats.added, + removed: directLines.removed + hunks.removed + patchTextStats.removed, + } +} + +export function getCodexPatchSummaryStats(part: unknown): { + added: number + removed: number +} { + const normalized = getNormalizedPatchSummaryPart(part) + if (!normalized) return { added: 0, removed: 0 } + + const input = isRecord(normalized.input) ? normalized.input : undefined + const outputPayload = getOutputPayload(normalized) + const output = isRecord(outputPayload) ? outputPayload : undefined + + const explicitAdded = getPatchSummaryNumber(output, [ + "addedLines", + "added_lines", + "added", + "insertions", + ]) + const explicitRemoved = getPatchSummaryNumber(output, [ + "removedLines", + "removed_lines", + "removed", + "deletions", + ]) + if (explicitAdded !== undefined || explicitRemoved !== undefined) { + return { + added: explicitAdded ?? 0, + removed: explicitRemoved ?? 0, + } + } + + const structuredStats = getStructuredPatchStats( + output?.structuredPatch ?? + output?.structured_patch ?? + output?.patch ?? + output?.diff ?? + input?.structuredPatch ?? + input?.structured_patch ?? + input?.patch ?? + input?.diff, + ) + if (structuredStats.added > 0 || structuredStats.removed > 0) { + return structuredStats + } + + if (normalized.type === "tool-Write") { + return { + added: countPatchSummaryTextLines(input?.content ?? output?.content), + removed: 0, + } + } + + if (Array.isArray(input?.edits)) { + return input.edits.reduce( + (total: { added: number; removed: number }, edit: unknown) => { + if (!isRecord(edit)) return total + return { + added: total.added + countPatchSummaryTextLines(edit.new_string), + removed: total.removed + countPatchSummaryTextLines(edit.old_string), + } + }, + { added: 0, removed: 0 }, + ) + } + + return { + added: countPatchSummaryTextLines(input?.new_string), + removed: countPatchSummaryTextLines(input?.old_string), + } +} + +function getExplicitPatchSummaryStatus(part: AnyRecord): string | undefined { + const input = isRecord(part.input) ? part.input : undefined + const outputPayload = getOutputPayload(part) + const output = isRecord(outputPayload) ? outputPayload : undefined + return ( + getNonEmptyString(part.status) ?? + (input ? getNonEmptyString(input.status) : undefined) ?? + (output ? getNonEmptyString(output.status) : undefined) + )?.toLowerCase() +} + +function hasPatchSummaryApplicationEvidence(part: AnyRecord): boolean { + if (part.state === "output-available" || part.state === "result") { + return true + } + + const outputPayload = getOutputPayload(part) + if (outputPayload !== undefined && outputPayload !== null) return true + + const explicitStatus = getExplicitPatchSummaryStatus(part) + return ( + explicitStatus === "applied" || + explicitStatus === "completed" || + explicitStatus === "done" || + explicitStatus === "success" || + explicitStatus === "succeeded" + ) +} + +function isTargetOnlyPatchSummaryPlaceholder(part: AnyRecord): boolean { + const input = isRecord(part.input) ? part.input : undefined + const outputPayload = getOutputPayload(part) + if (!hasPatchTarget(input, outputPayload)) return false + + const state = getNonEmptyString(part.state) + if (state !== undefined && state !== "input-available" && state !== "call") { + return false + } + + if (outputPayload !== undefined && outputPayload !== null) return false + if (getExplicitPatchSummaryStatus(part)) return false + + return true +} + +export function getCodexPatchPartSummaryStatus( + part: unknown, + chatStatus?: string, +): CodexPatchSummaryStatus { + const normalized = getNormalizedPatchSummaryPart(part) + if (!normalized) return "applied" + + const explicitStatus = getExplicitPatchSummaryStatus(normalized) + if ( + explicitStatus === "rejected" || + explicitStatus === "denied" || + explicitStatus === "declined" + ) { + return "rejected" + } + if ( + explicitStatus === "stopped" || + explicitStatus === "interrupted" || + explicitStatus === "cancelled" || + explicitStatus === "canceled" + ) { + return "stopped" + } + if (hasPatchSummaryApplicationEvidence(normalized)) return "applied" + + const blockStatus = getCodexBlockStatus(normalized, { chatStatus }) + if (blockStatus === "running" || blockStatus === "queued") { + return normalized.state === "input-streaming" ? "streaming" : "pending" + } + if (blockStatus === "failed") return "rejected" + if (getCodexPatchSummaryPath(normalized)) return "applied" + if (blockStatus === "interrupted") return "stopped" + return "applied" +} + +export function shouldShowCodexPatchSummaryPart( + part: unknown, + chatStatus: string | undefined, + stats: { added: number; removed: number }, +): boolean { + const normalized = getNormalizedPatchSummaryPart(part) + if (!normalized) return false + + const status = getCodexPatchPartSummaryStatus(normalized, chatStatus) + + if ( + status === "pending" || + status === "streaming" || + status === "rejected" || + status === "stopped" + ) { + return true + } + + if (stats.added > 0 || stats.removed > 0) return true + + return isTargetOnlyPatchSummaryPlaceholder(normalized) +} + +export function getCodexPatchSummaryStatusFromParts( + parts: readonly unknown[], + options?: Pick, +): CodexPatchSummaryStatus | undefined { + return getCodexPatchSummaryStatusFromStatuses(parts + .filter(isCodexPatchSummaryToolPart) + .map((part) => getCodexPatchPartSummaryStatus(part, options?.chatStatus))) +} + +function getCodexPatchSummaryStatusFromStatuses( + statuses: readonly (CodexPatchSummaryStatus | undefined)[], +): CodexPatchSummaryStatus | undefined { + if (statuses.length === 0) return undefined + if (statuses.includes("streaming")) return "streaming" + if (statuses.includes("pending")) return "pending" + if (statuses.includes("rejected")) return "rejected" + if (statuses.includes("stopped")) return "stopped" + return "applied" +} + +export function getCodexPatchSummaryFilesFromParts( + parts: readonly unknown[], + options?: CodexPatchSummaryOptions, +): CodexPatchSummaryFile[] { + const files = new Map() + + for (const part of parts) { + if (!isCodexPatchSummaryToolPart(part)) continue + + const filePath = getCodexPatchSummaryPath(part) + if (!filePath || options?.excludePath?.(filePath)) continue + + const stats = getCodexPatchSummaryStats(part) + if (!shouldShowCodexPatchSummaryPart(part, options?.chatStatus, stats)) { + continue + } + + const displayPath = options?.displayPath?.(filePath) ?? filePath + const partStatus = getCodexPatchPartSummaryStatus(part, options?.chatStatus) + const current = files.get(displayPath) + + if (current) { + current.added = (current.added ?? 0) + stats.added + current.removed = (current.removed ?? 0) + stats.removed + current.status = getCodexPatchSummaryStatusFromStatuses([ + current.status ?? "applied", + partStatus, + ]) + if (current.status === "applied") current.status = undefined + continue + } + + files.set(displayPath, { + path: displayPath, + added: stats.added, + removed: stats.removed, + status: partStatus === "applied" ? undefined : partStatus, + }) + } + + return [...files.values()] +} + +function getResolvedToolBlockStatus( + toolName: string, + part: AnyRecord, + input: unknown, + output: unknown, + status: CodexBlockStatus, + options: NormalizeCodexConversationBlockOptions | undefined, +): CodexBlockStatus { + if (status !== "interrupted") return status + if (isActiveChatStatus(options?.chatStatus)) return status + + const isPatchLikeTool = + toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit" + if ( + isPatchLikeTool && + output === undefined && + hasPatchTarget(input, output) && + (part.state === "call" || part.state === "input-available") + ) { + return "completed" + } + + return status +} + +function getTurnId( + part: AnyRecord, + options: NormalizeCodexConversationBlockOptions | undefined, +): string | undefined { + const input = getNestedRecord(part, "input") + return ( + options?.turnId ?? + getNonEmptyString(part.turnId) ?? + getNonEmptyString(part.turn_id) ?? + (input ? getNonEmptyString(input.turn_id) : undefined) ?? + (input ? getNonEmptyString(input.turnId) : undefined) + ) +} + +function getToolNameFromNormalizedPart(part: AnyRecord): string { + const toolName = getPartToolName(part) + if (toolName) return toolName + return "unknown" +} + +function getCommandFromInput(input: unknown, fallback?: string): string { + if (typeof input === "string") { + return input.trim() || fallback || "" + } + if (!isRecord(input)) return fallback || "" + + const command = input.command + if (Array.isArray(command)) { + const lastCommand = [...command].reverse().find((entry) => typeof entry === "string") + if (typeof lastCommand === "string" && lastCommand.trim().length > 0) { + return lastCommand.trim() + } + } + + return ( + getNonEmptyString(input.command) ?? + getNonEmptyString(input.cmd) ?? + getNonEmptyString(input.shellCommand) ?? + (isRecord(input.args) ? getCommandFromInput(input.args, fallback) : fallback || "") + ) +} + +function getStringFromRecord( + source: AnyRecord | undefined, + keys: string[], +): string | undefined { + if (!source) return undefined + for (const key of keys) { + const value = source[key] + if (typeof value === "string") return value + } + return undefined +} + +function getExecOutput(part: AnyRecord): + | { + stdout?: string + stderr?: string + combined?: string + exitCode?: number | null + } + | undefined { + const payload = getOutputPayload(part) + if (payload === undefined) return undefined + + if (typeof payload === "string") { + return { + stdout: payload, + combined: payload, + exitCode: getExitCodeFromText(payload), + } + } + + if (!isRecord(payload)) { + const payloadText = getTextFromContentPayload(payload) + return payloadText + ? { + stdout: payloadText, + combined: payloadText, + exitCode: getExitCodeFromText(payloadText), + } + : undefined + } + + const stderr = getStringFromRecord(payload, ["stderr", "errorOutput"]) + const payloadText = getTextFromContentPayload(payload) + const stdout = + getStringFromRecord(payload, ["stdout", "output", "text"]) ?? + (stderr ? undefined : payloadText) + const combined = getStringFromRecord(payload, ["combined", "combinedOutput"]) + const exitCode = getExitCodeFromPayload(payload) ?? getExitCodeFromText(payloadText) + const output: { + stdout?: string + stderr?: string + combined?: string + exitCode?: number | null + } = {} + + if (stdout !== undefined) output.stdout = stdout + if (stderr !== undefined) output.stderr = stderr + if (combined !== undefined) output.combined = combined + if (exitCode !== undefined) output.exitCode = exitCode + + return Object.keys(output).length > 0 ? output : undefined +} + +function inferParsedCommandType(command: string): CodexParsedCommandType { + const firstToken = command.trim().split(/\s+/)[0] || "" + if (!firstToken) return "unknown" + if (firstToken === "rg" || firstToken === "grep") return "search" + if (firstToken === "ls" || firstToken === "find") return "list_files" + if (firstToken === "cat" || firstToken === "sed" || firstToken === "nl") return "read" + if (firstToken === "prettier" || firstToken === "eslint" || firstToken === "biome") { + return "format" + } + if (firstToken === "bun" || firstToken === "npm" || firstToken === "pnpm") { + return command.includes(" test") ? "test" : "unknown" + } + return "unknown" +} + +function getParsedCommand( + input: unknown, + command: string, + status: CodexBlockStatus, +): CodexParsedCommand { + const entries = getParsedCmdEntriesFromPayload(input) + const firstEntry = entries[0] + const explicitType = + firstEntry && getNonEmptyString(firstEntry.type) + ? getNonEmptyString(firstEntry.type) + : undefined + + return { + type: explicitType || inferParsedCommandType(command), + isFinished: status !== "queued" && status !== "running", + fileName: + firstEntry && + (getNonEmptyString(firstEntry.fileName) ?? + getNonEmptyString(firstEntry.file_name) ?? + getNonEmptyString(firstEntry.name)), + skillName: + firstEntry && + (getNonEmptyString(firstEntry.skillName) ?? + getNonEmptyString(firstEntry.skill_name)), + query: + firstEntry && + (getNonEmptyString(firstEntry.query) ?? getNonEmptyString(firstEntry.pattern)), + path: + firstEntry && + (getNonEmptyString(firstEntry.path) ?? + getNonEmptyString(firstEntry.file_path) ?? + getNonEmptyString(firstEntry.target_directory)), + } +} + +function getProcessId(input: unknown): string | number | null | undefined { + if (!isRecord(input)) return undefined + const processId = input.process_id ?? input.processId + if (typeof processId === "string" || typeof processId === "number" || processId === null) { + return processId + } + return undefined +} + +function getExecExecutionStatus( + status: CodexBlockStatus, +): "running" | "completed" | "interrupted" | "failed" { + if (status === "completed") return "completed" + if (status === "failed") return "failed" + if (status === "interrupted") return "interrupted" + return "running" +} + +function parseMcpToolName(toolName: string, input: unknown): { server: string; tool: string } { + const rawToolName = toolName.startsWith("tool-") ? toolName.slice("tool-".length) : toolName + const builtinMcpTool = BUILTIN_MCP_TOOL_NAMES[rawToolName] + if (builtinMcpTool) return builtinMcpTool + + if (rawToolName.startsWith("mcp__")) { + const [server = "", ...toolParts] = rawToolName.slice("mcp__".length).split("__") + return { + server, + tool: toolParts.join("__"), + } + } + + if (isRecord(input)) { + return { + server: getNonEmptyString(input.server) || "", + tool: getNonEmptyString(input.tool) || rawToolName, + } + } + + return { server: "", tool: rawToolName } +} + +function getMcpCallId( + part: AnyRecord, + blockId: string, +): string { + const input = getNestedRecord(part, "input") + return ( + getNonEmptyString(part.toolCallId) ?? + getNonEmptyString(part.tool_call_id) ?? + (input ? getNonEmptyString(input.call_id) : undefined) ?? + (input ? getNonEmptyString(input.callId) : undefined) ?? + blockId + ) +} + +function getAppResourceUri(payload: unknown): string | undefined { + if (!isRecord(payload)) return undefined + const meta = isRecord(payload._meta) ? payload._meta : undefined + return ( + getNonEmptyString(payload.appResourceUri) ?? + getNonEmptyString(payload.app_resource_uri) ?? + getNonEmptyString(payload.resourceUri) ?? + getNonEmptyString(payload.resource_uri) ?? + (meta ? getNonEmptyString(meta["openai/outputTemplate"]) : undefined) ?? + (meta ? getNonEmptyString(meta.appResourceUri) : undefined) + ) +} + +function getArrayFromRecord( + source: AnyRecord | undefined, + keys: string[], +): unknown[] | undefined { + if (!source) return undefined + for (const key of keys) { + const value = source[key] + if (Array.isArray(value)) return value + } + return undefined +} + +function getImageData(part: AnyRecord): unknown { + if (part.data !== undefined) return part.data + if (part.image !== undefined) return part.image + if (part.output !== undefined) return part.output + if (part.result !== undefined) return part.result + return undefined +} + +function getImageUrl(data: unknown, part: AnyRecord): string | undefined { + return ( + getNonEmptyString(part.url) ?? + getNonEmptyString(part.src) ?? + (isRecord(data) + ? getNonEmptyString(data.url) ?? + getNonEmptyString(data.src) ?? + getNonEmptyString(data.imageUrl) ?? + getNonEmptyString(data.image_url) + : undefined) + ) +} + +function getImageMimeType(data: unknown, part: AnyRecord): string | undefined { + return ( + getNonEmptyString(part.mimeType) ?? + getNonEmptyString(part.mime_type) ?? + (isRecord(data) + ? getNonEmptyString(data.mimeType) ?? getNonEmptyString(data.mime_type) + : undefined) + ) +} + +function getImagePrompt(data: unknown, part: AnyRecord): string | undefined { + return ( + getNonEmptyString(part.prompt) ?? + (isRecord(data) ? getNonEmptyString(data.prompt) : undefined) + ) +} + +function getStatusMessage(part: AnyRecord): string | undefined { + return ( + getNonEmptyString(part.message) ?? + getNonEmptyString(part.text) ?? + getNonEmptyString(part.errorText) ?? + getNonEmptyString(part.error) ?? + (isRecord(part.data) ? getNonEmptyString(part.data.message) : undefined) ?? + (isRecord(part.data) ? getNonEmptyString(part.data.error) : undefined) + ) +} + +function joinRuntimeStatusSegments( + segments: Array, +): string | undefined { + const filtered = segments.filter((segment): segment is string => + Boolean(segment && segment.trim()), + ) + return filtered.length > 0 ? filtered.join(" ") : undefined +} + +function getRuntimeStatusMessage(part: AnyRecord, payload: AnyRecord): string | undefined { + const direct = + getStatusMessage(part) ?? + getNonEmptyString(payload.summary) ?? + getNonEmptyString(payload.title) ?? + getNonEmptyString(payload.description) + if (direct) return direct + + const mode = + getNonEmptyString(payload.mode) ?? + getNonEmptyString(part.mode) + const queueState = + getNonEmptyString(payload.queueState) ?? + getNonEmptyString(payload.queue_state) + const windowLabel = + getNonEmptyString(payload.window) ?? + getNonEmptyString(payload.period) + const remaining = + typeof payload.remaining === "number" ? `${payload.remaining} remaining` : undefined + const limit = + typeof payload.limit === "number" ? `of ${payload.limit}` : undefined + const projectName = + getNonEmptyString(payload.projectName) ?? + getNonEmptyString(payload.project_name) + const action = getNonEmptyString(payload.action) + const artifactKind = + getNonEmptyString(payload.artifactKind) ?? + getNonEmptyString(payload.artifact_kind) + const artifactTarget = + getNonEmptyString(payload.path) ?? + getNonEmptyString(payload.url) + const prState = + getNonEmptyString(payload.reviewState) ?? + getNonEmptyString(payload.review_state) ?? + getNonEmptyString(payload.checksState) ?? + getNonEmptyString(payload.checks_state) + const snapshotKind = + getNonEmptyString(payload.snapshotKind) ?? + getNonEmptyString(payload.snapshot_kind) + + return joinRuntimeStatusSegments([ + mode, + queueState, + windowLabel, + remaining, + limit, + projectName, + action, + artifactKind, + artifactTarget, + prState, + snapshotKind, + ]) +} + +function getTextOutputContent(part: AnyRecord, payload: AnyRecord): string | undefined { + const direct = + getNonEmptyString(part.content) ?? + getNonEmptyString(part.text) ?? + getNonEmptyString(part.output) ?? + getNonEmptyString(part.result) ?? + getNonEmptyString(payload.content) ?? + getNonEmptyString(payload.text) ?? + getNonEmptyString(payload.output) ?? + getNonEmptyString(payload.result) + if (direct) return direct + + const data = part.data ?? payload.data + if (typeof data === "string" && data.trim()) return data + if (data !== undefined) { + try { + return JSON.stringify(data, null, 2) + } catch { + return String(data) + } + } + + return undefined +} + +function getTextOutputTitle(part: AnyRecord, payload: AnyRecord): string | undefined { + return ( + getNonEmptyString(part.title) ?? + getNonEmptyString(part.label) ?? + getNonEmptyString(part.name) ?? + getNonEmptyString(part.filename) ?? + getNonEmptyString(payload.title) ?? + getNonEmptyString(payload.label) ?? + getNonEmptyString(payload.name) ?? + getNonEmptyString(payload.filename) + ) +} + +function getTextOutputMimeType(part: AnyRecord, payload: AnyRecord): string | undefined { + return ( + getNonEmptyString(part.mimeType) ?? + getNonEmptyString(part.mime_type) ?? + getNonEmptyString(payload.mimeType) ?? + getNonEmptyString(payload.mime_type) + ) +} + +function getPayloadRecord(part: AnyRecord): AnyRecord { + if (isRecord(part.data)) return part.data + if (isRecord(part.output)) return part.output + if (isRecord(part.result)) return part.result + return part +} + +function isPermissionToolName(toolName: string): boolean { + const normalized = toolName.toLowerCase() + return normalized.includes("permission") || normalized.includes("approval") +} + +function isGeneratedImagePart( + part: AnyRecord, + options: NormalizeCodexConversationBlockOptions | undefined, +): boolean { + if (part.type === "generated-image" || part.type === "generated_image") return true + if (part.type !== "data-image") return false + return options?.messageRole === "assistant" +} + +function normalizeCodexNonToolPartToConversationBlock( + part: AnyRecord, + options?: NormalizeCodexConversationBlockOptions, +): CodexConversationBlock | null { + const id = getBlockId(part, options) + const turnId = getTurnId(part, options) + const status = getCodexBlockStatus(part, options) + const normalizedType = part.type.replaceAll("_", "-") + const payloadRecord = getPayloadRecord(part) + const base = { + id, + turnId, + sourcePart: part, + } + + if (normalizedType === "conversation-block" && isRecord(part.block)) { + return normalizeCodexNonToolPartToConversationBlock( + { + ...part.block, + sourcePart: part, + }, + options, + ) + } + + if (isGeneratedImagePart(part, options)) { + const data = getImageData(part) + const resolvedStatus = status === "queued" && data !== undefined ? "completed" : status + const imageBlock: CodexGeneratedImageBlock = { + ...base, + type: "generated-image", + data, + url: getImageUrl(data, part), + mimeType: getImageMimeType(data, part), + prompt: getImagePrompt(data, part), + status: resolvedStatus, + } + return imageBlock + } + + if ( + normalizedType === "data-text" || + normalizedType === "data-output" || + normalizedType === "text-output" || + normalizedType === "output-text" + ) { + const content = getTextOutputContent(part, payloadRecord) + if (!content) return null + const textOutputBlock: CodexTextOutputBlock = { + ...base, + type: "text-output", + title: getTextOutputTitle(part, payloadRecord), + content, + mimeType: getTextOutputMimeType(part, payloadRecord), + status: status === "queued" ? "completed" : status, + } + return textOutputBlock + } + + if (normalizedType === "todo-list" || normalizedType === "data-todo-list") { + const todoBlock: CodexTodoListBlock = { + ...base, + type: "todo-list", + todos: + getArrayFromRecord(payloadRecord, ["newTodos", "todos", "items"]) ?? + [], + previousTodos: getArrayFromRecord(payloadRecord, ["oldTodos", "previousTodos"]), + input: part.input ?? payloadRecord.input ?? payloadRecord, + output: part.output ?? payloadRecord.output ?? part.result, + status, + } + return todoBlock + } + + if ( + normalizedType === "proposed-plan" || + normalizedType === "plan" || + normalizedType === "data-plan" + ) { + const planBlock: CodexProposedPlanBlock = { + ...base, + type: "proposed-plan", + action: getNonEmptyString(payloadRecord.action), + plan: payloadRecord.plan ?? payloadRecord, + input: part.input ?? payloadRecord.input ?? payloadRecord, + output: part.output ?? payloadRecord.output ?? part.result, + status, + } + return planBlock + } + + if ( + normalizedType === "permission-request" || + normalizedType === "approval-request" || + normalizedType === "data-permission-request" + ) { + const permissionBlock: CodexPermissionRequestBlock = { + ...base, + type: "permission-request", + input: part.input ?? payloadRecord.input ?? payloadRecord, + result: part.result ?? payloadRecord.result ?? part.output, + status, + } + return permissionBlock + } + + if ( + normalizedType === "user-input" || + normalizedType === "ask-user-question" || + normalizedType === "data-user-input" + ) { + const userInputBlock: CodexUserInputBlock = { + ...base, + type: "user-input", + prompt: + getNonEmptyString(payloadRecord.question) ?? + getNonEmptyString(payloadRecord.prompt) ?? + getNonEmptyString(payloadRecord.message) ?? + getStatusMessage(part), + input: part.input ?? payloadRecord.input ?? payloadRecord, + result: part.result ?? payloadRecord.result ?? part.output, + autoResolution: normalizeUserInputAutoResolutionState(part, payloadRecord), + status, + } + return userInputBlock + } + + if ( + normalizedType === "dynamic-tool-call" || + normalizedType === "data-dynamic-tool-call" + ) { + const dynamicBlock: CodexDynamicToolBlock = { + ...base, + type: "dynamic-tool-call", + toolName: + getNonEmptyString(payloadRecord.toolName) ?? + getNonEmptyString(payloadRecord.tool_name) ?? + getNonEmptyString(part.toolName) ?? + "dynamic-tool", + input: part.input ?? payloadRecord.input ?? payloadRecord, + output: part.output ?? payloadRecord.output ?? part.result, + status, + } + return dynamicBlock + } + + if (normalizedType === "active-goal" || normalizedType === "data-active-goal") { + const goalBlock: CodexActiveGoalBlock = { + ...base, + type: "active-goal", + title: + getNonEmptyString(payloadRecord.title) ?? + getNonEmptyString(payloadRecord.goal) ?? + "Active goal", + prompt: + getNonEmptyString(payloadRecord.prompt) ?? + getNonEmptyString(payloadRecord.description), + elapsed: getNonEmptyString(payloadRecord.elapsed), + agentLabel: + getNonEmptyString(payloadRecord.agentLabel) ?? + getNonEmptyString(payloadRecord.agent_label), + changedFiles: getPatchSummaryNumber(payloadRecord, [ + "changedFiles", + "changed_files", + ]), + addedLines: getPatchSummaryNumber(payloadRecord, [ + "addedLines", + "added_lines", + ]), + removedLines: getPatchSummaryNumber(payloadRecord, [ + "removedLines", + "removed_lines", + ]), + status, + } + return goalBlock + } + + const runtimeStatusTitle = RUNTIME_STATUS_BLOCK_TITLES[normalizedType] + if (runtimeStatusTitle) { + const statusBlock: CodexStatusBlock = { + ...base, + type: "status", + level: part.status === "failed" ? "error" : "info", + title: + getNonEmptyString(payloadRecord.title) ?? + getNonEmptyString(part.title) ?? + runtimeStatusTitle, + message: getRuntimeStatusMessage(part, payloadRecord), + data: part.data ?? payloadRecord, + status, + } + return statusBlock + } + + if (part.type === "stream-error" || part.type === "error" || part.type === "data-error") { + const statusBlock: CodexStatusBlock = { + ...base, + type: "status", + level: "error", + message: getStatusMessage(part), + data: part.data ?? part.error ?? part, + status: "failed", + } + return statusBlock + } + + if (part.type === "status") { + const level = + part.level === "warning" || part.level === "error" || part.level === "info" + ? part.level + : "info" + const statusBlock: CodexStatusBlock = { + ...base, + type: "status", + level, + message: getStatusMessage(part), + data: part.data, + status, + } + return statusBlock + } + + return null +} + +export function normalizeCodexToolPart( + part: unknown, + options?: NormalizeCodexToolPartOptions, +): unknown { + if (!isRecord(part)) return part + if (typeof part.type !== "string" || !part.type.startsWith("tool-")) return part + + const rawToolName = getPartToolName(part) + const descriptor = rawToolName ? parseCodexToolDescriptor(rawToolName) : null + const shouldNormalizeState = + options?.normalizeState === true && + (part.state === "input-available" || part.state === "output-available") + + const hasCodexArgsWrapper = + isRecord(part.input) && + (isRecord(part.input.args) || typeof part.input.toolName === "string") + + if (!descriptor && !hasCodexArgsWrapper && !shouldNormalizeState) { + return part + } + + const normalizedType = descriptor ? `tool-${descriptor.canonicalToolName}` : part.type + const fallbackDescriptor: CodexToolDescriptor = { + canonicalToolName: normalizedType.startsWith("tool-") + ? normalizedType.slice("tool-".length) + : normalizedType, + detail: "", + isMcp: normalizedType.startsWith("tool-mcp__"), + } + const normalizedInput = + descriptor + ? normalizeCodexToolInput(part.input, descriptor) + : hasCodexArgsWrapper + ? normalizeCodexToolInput(part.input, fallbackDescriptor) + : part.input + let normalizedOutput = part.output !== undefined ? part.output : part.result + const normalizedResult = part.result !== undefined ? part.result : part.output + const outputPayload = + normalizedOutput !== undefined ? normalizedOutput : normalizedResult + let outputEnrichedInput = + fallbackDescriptor.canonicalToolName === "Read" + ? normalizeReadInputFromPayload(normalizedInput, outputPayload) + : normalizedInput + if (fallbackDescriptor.canonicalToolName === "TodoWrite") { + const inputRecord = isRecord(outputEnrichedInput) ? outputEnrichedInput : undefined + const outputRecord = + isRecord(normalizedOutput) && !Array.isArray(normalizedOutput) + ? normalizedOutput + : undefined + const existingTodos = + getArrayFromRecord(inputRecord, ["todos"]) ?? + getArrayFromRecord(outputRecord, ["newTodos", "todos"]) + if (!existingTodos) { + const outputText = getTextFromContentPayload(outputPayload) + const todos = getTodosFromText(outputText) + if (todos.length > 0) { + outputEnrichedInput = { + ...(inputRecord ?? {}), + todos, + } + normalizedOutput = { + ...(outputRecord ?? {}), + oldTodos: getArrayFromRecord(outputRecord, ["oldTodos", "previousTodos"]) ?? [], + newTodos: todos, + text: outputText, + } + } + } + } + const finalInput = + outputEnrichedInput !== part.input && isShallowEqual(outputEnrichedInput, part.input) + ? part.input + : outputEnrichedInput + + const normalizedState = shouldNormalizeState + ? toCanonicalToolState(part.state) + : part.state + + const typeChanged = normalizedType !== part.type + const inputChanged = finalInput !== part.input + const stateChanged = normalizedState !== part.state + const outputChanged = normalizedOutput !== part.output + const resultChanged = normalizedResult !== part.result + + if (!typeChanged && !inputChanged && !stateChanged && !outputChanged && !resultChanged) { + return part + } + + const normalizedPart: AnyRecord = { ...part } + if (typeChanged) normalizedPart.type = normalizedType + if (inputChanged) normalizedPart.input = finalInput + if (stateChanged) normalizedPart.state = normalizedState + if (normalizedOutput !== undefined) normalizedPart.output = normalizedOutput + if (normalizedResult !== undefined) normalizedPart.result = normalizedResult + + const preservedToolCallId = getPreservedToolCallId(part) + if ( + preservedToolCallId && + normalizedPart.toolCallId === undefined && + normalizedPart.tool_call_id === undefined + ) { + normalizedPart.toolCallId = preservedToolCallId + } + + return normalizedPart +} + +export function normalizeCodexToolPartToConversationBlock( + part: unknown, + options?: NormalizeCodexConversationBlockOptions, +): CodexConversationBlock | null { + const normalizedPart = normalizeCodexToolPart(part, { + normalizeState: options?.normalizeState ?? true, + }) + if (!isRecord(normalizedPart)) return null + if ( + typeof normalizedPart.type !== "string" || + !normalizedPart.type.startsWith("tool-") + ) { + return null + } + + const toolName = getToolNameFromNormalizedPart(normalizedPart) + const id = getBlockId(normalizedPart, options) + const turnId = getTurnId(normalizedPart, options) + const input = normalizedPart.input + const output = getOutputPayload(normalizedPart) + const status = getResolvedToolBlockStatus( + toolName, + normalizedPart, + input, + output, + getCodexBlockStatus(normalizedPart, options), + options, + ) + const base = { + id, + turnId, + status, + sourcePart: normalizedPart, + } + + if (toolName === "Bash" || toolName === "Run") { + const command = getCommandFromInput(input) + const execOutput = getExecOutput(normalizedPart) + const execBlock: CodexExecBlock = { + ...base, + type: "exec", + command, + cwd: isRecord(input) ? getNonEmptyString(input.cwd) : undefined, + processId: getProcessId(input), + executionStatus: getExecExecutionStatus(status), + parsedCmd: getParsedCommand(input, command, status), + output: execOutput, + } + return execBlock + } + + if (toolName.startsWith("mcp__") || BUILTIN_MCP_TOOL_NAMES[toolName]) { + const { server, tool } = parseMcpToolName(toolName, input) + const mcpBlock: CodexMcpToolBlock = { + ...base, + type: "mcp-tool-call", + server, + tool, + callId: getMcpCallId(normalizedPart, id), + input, + result: output, + rawOutput: output, + appResourceUri: getAppResourceUri(output), + } + return mcpBlock + } + + if (toolName === "TodoWrite") { + const inputRecord = isRecord(input) ? input : undefined + const outputRecord = isRecord(output) ? output : undefined + const todoBlock: CodexTodoListBlock = { + ...base, + type: "todo-list", + todos: + getArrayFromRecord(outputRecord, ["newTodos", "todos"]) ?? + getArrayFromRecord(inputRecord, ["todos"]) ?? + [], + previousTodos: getArrayFromRecord(outputRecord, ["oldTodos", "previousTodos"]), + input, + output, + } + return todoBlock + } + + if (toolName === "PlanWrite" || toolName === "ExitPlanMode") { + const inputRecord = isRecord(input) ? input : undefined + const outputRecord = isRecord(output) ? output : undefined + const planBlock: CodexProposedPlanBlock = { + ...base, + type: "proposed-plan", + action: inputRecord ? getNonEmptyString(inputRecord.action) : undefined, + plan: inputRecord?.plan ?? outputRecord?.plan, + input, + output, + } + return planBlock + } + + if (toolName === "AskUserQuestion") { + const inputRecord = isRecord(input) ? input : undefined + const userInputBlock: CodexUserInputBlock = { + ...base, + type: "user-input", + prompt: + (inputRecord + ? getNonEmptyString(inputRecord.question) ?? + getNonEmptyString(inputRecord.prompt) ?? + getNonEmptyString(inputRecord.message) + : undefined) ?? getStatusMessage(normalizedPart), + input, + result: output, + autoResolution: normalizeUserInputAutoResolutionState( + normalizedPart, + inputRecord ?? {}, + ), + } + return userInputBlock + } + + if (isPermissionToolName(toolName)) { + const permissionBlock: CodexPermissionRequestBlock = { + ...base, + type: "permission-request", + input, + result: output, + } + return permissionBlock + } + + if (toolName === "Edit" || toolName === "Write" || toolName === "MultiEdit") { + const patchBlock: CodexPatchBlock = { + ...base, + type: "patch", + toolName, + input, + output, + } + return patchBlock + } + + const dynamicBlock: CodexDynamicToolBlock = { + ...base, + type: "dynamic-tool-call", + toolName, + input, + output, + } + return dynamicBlock +} + +export function normalizeCodexPartToConversationBlock( + part: unknown, + options?: NormalizeCodexConversationBlockOptions, +): CodexConversationBlock | null { + if (isRecord(part)) { + const nonToolBlock = normalizeCodexNonToolPartToConversationBlock(part, options) + if (nonToolBlock) return nonToolBlock + } + + return normalizeCodexToolPartToConversationBlock(part, options) +} + +export function normalizeCodexConversationBlocksFromMessage( + message: unknown, + options?: NormalizeCodexConversationBlockOptions, +): CodexConversationBlock[] { + if (!isRecord(message) || !Array.isArray(message.parts)) return [] + const messageRole = getNonEmptyString(message.role) + const turnId = + options?.turnId ?? + getNonEmptyString(message.id) ?? + getNonEmptyString(message.turnId) ?? + getNonEmptyString(message.turn_id) + + return message.parts.flatMap((part, partIndex) => { + const block = normalizeCodexPartToConversationBlock(part, { + ...options, + messageRole, + partIndex, + turnId, + }) + return block ? [block] : [] + }) +} + +function getBaseName(pathLike: string): string { + const cleaned = pathLike.trim().replace(/\/+$/, "") + if (!cleaned) return pathLike + return cleaned.split(/[\\/]/).filter(Boolean).pop() || cleaned +} + +function getDirectoryName(pathLike: string): string { + const cleaned = pathLike.trim().replace(/\/+$/, "") + const parts = cleaned.split(/[\\/]/).filter(Boolean) + if (parts.length <= 1) return "" + const prefix = cleaned.startsWith("/") ? "/" : "" + return `${prefix}${parts.slice(0, -1).join("/")}` +} + +function normalizeFilePathForArtifact(pathLike: string): string { + return pathLike.trim().replace(/\\/g, "/").replace(/\/+$/, "") +} + +function isHtmlFilePath(pathLike: string | undefined): boolean { + if (!pathLike) return false + return /\.x?html?$/i.test(pathLike.split(/[?#]/, 1)[0] ?? pathLike) +} + +function encodeUrlPath(pathLike: string): string { + return pathLike + .split(/[\\/]/) + .filter(Boolean) + .map((segment) => encodeURIComponent(segment)) + .join("/") +} + +function appendPathToBaseUrl(baseUrl: string, pathLike: string): string { + const encodedPath = encodeUrlPath(pathLike) + if (!encodedPath) return baseUrl + return `${baseUrl.replace(/\/+$/, "")}/${encodedPath}` +} + +function displayUrlHostAndPath(url: string): string { + try { + const parsed = new URL(url) + return `${parsed.host}${parsed.pathname === "/" ? "" : parsed.pathname}` + } catch { + return url + } +} + +type LocalWebsiteCandidate = { + baseUrl: string + root?: string + explicitTarget?: string +} + +function normalizeLocalPreviewUrl(url: string): string { + try { + const parsed = new URL(url) + if (parsed.hostname === "0.0.0.0" || parsed.hostname === "::") { + parsed.hostname = "127.0.0.1" + } + return parsed.toString() + } catch { + return url + } +} + +function getLocalUrlCandidates(text: string): string[] { + const matches = text.matchAll( + /\bhttps?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(?::\d+)?(?:\/[^\s'"<>)\]]*)?/gi, + ) + return Array.from(matches, (match) => normalizeLocalPreviewUrl(match[0])) +} + +function getHttpServerDirectory(command: string): string | undefined { + const directoryMatch = + command.match(/--directory(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s]+))/i) ?? + command.match(/-d(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s]+))/i) + return directoryMatch?.[1] ?? directoryMatch?.[2] ?? directoryMatch?.[3] +} + +function getLocalWebsiteCandidatesFromExec(block: CodexExecBlock): LocalWebsiteCandidate[] { + const command = block.command || "" + const outputText = [ + block.output?.combined, + block.output?.stdout, + block.output?.stderr, + ].filter(Boolean).join("\n") + const text = [command, outputText].filter(Boolean).join("\n") + const candidates: LocalWebsiteCandidate[] = [] + const seen = new Set() + + for (const url of getLocalUrlCandidates(text)) { + try { + const parsed = new URL(url) + const pathname = decodeURIComponent(parsed.pathname || "/") + const explicitTarget = isHtmlFilePath(pathname) ? url : undefined + if (!explicitTarget) { + parsed.pathname = "/" + parsed.search = "" + parsed.hash = "" + } + const baseUrl = normalizeLocalPreviewUrl(parsed.toString()) + if (seen.has(`${baseUrl}|${explicitTarget ?? ""}`)) continue + seen.add(`${baseUrl}|${explicitTarget ?? ""}`) + candidates.push({ baseUrl, root: block.cwd, explicitTarget }) + } catch { + // Ignore malformed local URLs surfaced in terminal logs. + } + } + + const pythonServerMatch = command.match( + /\bpython(?:3(?:\.\d+)?)?\s+-m\s+http\.server(?:\s+(\d+))?/i, + ) + if (pythonServerMatch) { + const port = pythonServerMatch[1] || "8000" + const root = getHttpServerDirectory(command) || block.cwd + const baseUrl = `http://127.0.0.1:${port}/` + if (!seen.has(`${baseUrl}|`)) { + seen.add(`${baseUrl}|`) + candidates.push({ baseUrl, root }) + } + } + + return candidates +} + +function getRelativeWebsitePath( + filePath: string, + candidate: LocalWebsiteCandidate, +): string | undefined { + if (candidate.explicitTarget) return "" + + const normalizedFilePath = normalizeFilePathForArtifact(filePath) + const normalizedRoot = candidate.root + ? normalizeFilePathForArtifact(candidate.root) + : "" + + if (!normalizedRoot) return getBaseName(filePath) + if (normalizedFilePath === normalizedRoot) return getBaseName(filePath) + if (normalizedFilePath.startsWith(`${normalizedRoot}/`)) { + return normalizedFilePath.slice(normalizedRoot.length + 1) + } + + const fileDirectory = getDirectoryName(filePath) + if (fileDirectory && normalizeFilePathForArtifact(fileDirectory) === normalizedRoot) { + return getBaseName(filePath) + } + + return undefined +} + +function getWebsitePreviewUrlForFile( + filePath: string, + candidates: LocalWebsiteCandidate[], +): string | undefined { + for (const candidate of candidates) { + if (candidate.explicitTarget) return candidate.explicitTarget + const relativePath = getRelativeWebsitePath(filePath, candidate) + if (relativePath === undefined) continue + return appendPathToBaseUrl(candidate.baseUrl, relativePath) + } + return undefined +} + +function promoteHtmlFileArtifactsToWebsites( + artifacts: CodexOutputArtifact[], + blocks: CodexConversationBlock[], +): CodexOutputArtifact[] { + const candidates = blocks.flatMap((block) => + block.type === "exec" ? getLocalWebsiteCandidatesFromExec(block) : [], + ) + if (!candidates.length) return artifacts + + return artifacts.map((artifact) => { + const filePath = artifact.path || artifact.url + if (!filePath || artifact.kind !== "file" || !isHtmlFilePath(filePath)) { + return artifact + } + const previewUrl = getWebsitePreviewUrlForFile(filePath, candidates) + if (!previewUrl) return artifact + + return { + ...artifact, + id: `${artifact.id}:website`, + kind: "website", + label: displayUrlHostAndPath(previewUrl), + url: previewUrl, + mimeType: "text/html", + } + }) +} + +function getMcpResultContentBlocks(result: unknown): AnyRecord[] { + const content = isRecord(result) && Array.isArray(result.content) + ? result.content + : Array.isArray(result) + ? result + : [] + return content.filter(isRecord) +} + +function getMcpResourceObject(block: AnyRecord): AnyRecord { + return isRecord(block.resource) ? block.resource : block +} + +function getMcpResourceUri(block: AnyRecord): string | undefined { + const resource = getMcpResourceObject(block) + return ( + getNonEmptyString(resource.uri) ?? + getNonEmptyString(resource.url) ?? + getNonEmptyString(block.uri) ?? + getNonEmptyString(block.url) + ) +} + +function getMcpResourceMimeType(block: AnyRecord): string | undefined { + const resource = getMcpResourceObject(block) + return ( + getNonEmptyString(resource.mimeType) ?? + getNonEmptyString(resource.mime_type) ?? + getNonEmptyString(block.mimeType) ?? + getNonEmptyString(block.mime_type) + ) +} + +function getMcpResourceText(block: AnyRecord): string | undefined { + const resource = getMcpResourceObject(block) + return ( + getNonEmptyString(resource.text) ?? + getNonEmptyString(block.text) ?? + getNonEmptyString(resource.content) ?? + getNonEmptyString(block.content) + ) +} + +function getMcpResourceLabel(block: AnyRecord, uri: string, fallback: string): string { + const resource = getMcpResourceObject(block) + return ( + getNonEmptyString(block.name) ?? + getNonEmptyString(block.title) ?? + getNonEmptyString(resource.name) ?? + getNonEmptyString(resource.title) ?? + getBaseName(uri) ?? + fallback + ) +} + +function getMcpImageDataUrl(block: AnyRecord): string | undefined { + const uri = getNonEmptyString(block.uri) ?? getNonEmptyString(block.url) + if (uri) return uri + const data = getNonEmptyString(block.data) ?? getNonEmptyString(block.blob) + if (!data) return undefined + if (data.startsWith("data:")) return data + const mimeType = getMcpResourceMimeType(block) ?? "image/png" + return `data:${mimeType};base64,${data}` +} + +function getMcpStructuredContentText(result: unknown): string | undefined { + if (!isRecord(result)) return undefined + const structuredContent = result.structuredContent ?? result.structured_content + if (structuredContent === undefined) return undefined + if (typeof structuredContent === "string") return structuredContent + try { + return JSON.stringify(structuredContent, null, 2) + } catch { + return undefined + } +} + +function getPatchFilePath(block: CodexPatchBlock): string | undefined { + const input = isRecord(block.input) ? block.input : undefined + const output = isRecord(block.output) ? block.output : undefined + + return ( + (input ? getNonEmptyString(input.file_path) : undefined) ?? + (input ? getNonEmptyString(input.filePath) : undefined) ?? + (input ? getNonEmptyString(input.path) : undefined) ?? + (output ? getNonEmptyString(output.file_path) : undefined) ?? + (output ? getNonEmptyString(output.filePath) : undefined) ?? + (output ? getNonEmptyString(output.path) : undefined) + ) +} + +function codexOutputArtifactsFromBlock( + block: CodexConversationBlock, + index: number, +): CodexOutputArtifact[] { + if (block.type === "generated-image") { + const imageIndex = index + 1 + const imageFileName = block.url && !block.url.startsWith("data:") + ? getBaseName(block.url.split("?")[0] || "") + : "" + const label = imageFileName || `Generated image ${imageIndex}` + + return [{ + id: `${block.id}:artifact:image`, + kind: "image", + label, + sourceBlockId: block.id, + turnId: block.turnId, + status: block.status, + url: block.url, + mimeType: block.mimeType, + prompt: block.prompt, + }] + } + + if (block.type === "patch") { + const path = getPatchFilePath(block) + if (!path) return [] + return [{ + id: `${block.id}:artifact:file`, + kind: "file", + label: getBaseName(path), + sourceBlockId: block.id, + turnId: block.turnId, + status: block.status, + path, + }] + } + + if (block.type === "text-output") { + return [{ + id: `${block.id}:artifact:text`, + kind: "text", + label: block.title || `Output ${index + 1}`, + sourceBlockId: block.id, + turnId: block.turnId, + status: block.status, + mimeType: block.mimeType, + content: block.content, + }] + } + + if (block.type === "mcp-tool-call") { + const artifacts: CodexOutputArtifact[] = [] + const seenResources = new Set() + const contentBlocks = getMcpResultContentBlocks(block.result) + + contentBlocks.forEach((contentBlock, contentIndex) => { + const contentType = getNonEmptyString(contentBlock.type) + if (contentType === "image") { + const url = getMcpImageDataUrl(contentBlock) + if (!url) return + artifacts.push({ + id: `${block.id}:artifact:image:${contentIndex}`, + kind: "image", + label: + getNonEmptyString(contentBlock.name) ?? + getNonEmptyString(contentBlock.title) ?? + `Image output ${index + artifacts.length + 1}`, + sourceBlockId: block.id, + turnId: block.turnId, + status: block.status, + url, + mimeType: getMcpResourceMimeType(contentBlock), + prompt: getNonEmptyString(contentBlock.alt), + }) + return + } + + if ( + contentType !== "resource_link" && + contentType !== "embedded_resource" && + contentType !== "resource" && + !isRecord(contentBlock.resource) + ) { + return + } + + const uri = getMcpResourceUri(contentBlock) + if (!uri) return + seenResources.add(uri) + artifacts.push({ + id: `${block.id}:artifact:resource:${contentIndex}`, + kind: "resource", + label: getMcpResourceLabel( + contentBlock, + uri, + `Resource ${index + artifacts.length + 1}`, + ), + sourceBlockId: block.id, + turnId: block.turnId, + status: block.status, + url: uri, + mimeType: getMcpResourceMimeType(contentBlock), + content: getMcpResourceText(contentBlock), + }) + }) + + if (block.appResourceUri && !seenResources.has(block.appResourceUri)) { + artifacts.push({ + id: `${block.id}:artifact:resource:app`, + kind: "resource", + label: getBaseName(block.appResourceUri) || `${block.server}.${block.tool}`, + sourceBlockId: block.id, + turnId: block.turnId, + status: block.status, + url: block.appResourceUri, + mimeType: "application/vnd.openai.app+html", + content: getMcpStructuredContentText(block.result), + }) + } + + return artifacts + } + + return [] +} + +export function getCodexOutputArtifactsFromBlocks( + blocks: CodexConversationBlock[], +): CodexOutputArtifact[] { + const artifacts: CodexOutputArtifact[] = [] + + for (const block of blocks) { + artifacts.push(...codexOutputArtifactsFromBlock(block, artifacts.length)) + } + + return promoteHtmlFileArtifactsToWebsites(artifacts, blocks) } export function normalizeCodexAssistantMessage( diff --git a/src/shared/plugin-deep-link.ts b/src/shared/plugin-deep-link.ts new file mode 100644 index 000000000..22beabdde --- /dev/null +++ b/src/shared/plugin-deep-link.ts @@ -0,0 +1,79 @@ +export type PluginDeepLinkAction = "detail" | "try-in-chat" + +export type PluginDeepLinkSource = "protocol" | "mention" | "catalog" + +export type PluginDeepLinkTarget = { + pluginId: string + action: PluginDeepLinkAction + source?: PluginDeepLinkSource +} + +const TRY_IN_CHAT_ACTIONS = new Set(["try", "try-in-chat", "tryInChat"]) +const PLUGIN_DEEP_LINK_PROTOCOLS = new Set([ + "twentyfirst-agents:", + "twentyfirst-agents-dev:", +]) + +function normalizePluginDeepLinkAction(value: string | null): PluginDeepLinkAction { + if (!value) return "detail" + return TRY_IN_CHAT_ACTIONS.has(value) ? "try-in-chat" : "detail" +} + +export function buildPluginDeepLinkUrl({ + action = "detail", + pluginId, + protocol, +}: { + action?: PluginDeepLinkAction + pluginId: string + protocol: string +}): string { + const encodedPluginId = encodeURIComponent(pluginId) + + if (action === "try-in-chat") { + return `${protocol}://plugins/${encodedPluginId}/try-in-chat` + } + + return `${protocol}://plugins/${encodedPluginId}` +} + +export function parsePluginDeepLink(url: string): PluginDeepLinkTarget | null { + const trimmed = url.trim() + if (!trimmed) return null + + let parsed: URL + const hasProtocol = /^[a-z][a-z0-9+.-]*:/i.test(trimmed) + + try { + parsed = hasProtocol + ? new URL(trimmed) + : new URL(trimmed, "twentyfirst-agents://local") + } catch { + return null + } + + if (hasProtocol && !PLUGIN_DEEP_LINK_PROTOCOLS.has(parsed.protocol)) { + return null + } + + if (parsed.host !== "plugins" && !parsed.pathname.startsWith("/plugins/")) { + return null + } + + const pathParts = parsed.pathname.split("/").filter(Boolean) + const rawPluginId = parsed.host === "plugins" ? pathParts[0] : pathParts[1] + + if (!rawPluginId) return null + + const pathAction = parsed.host === "plugins" ? pathParts[1] : pathParts[2] + const queryAction = + parsed.searchParams.get("action") ?? + parsed.searchParams.get("intent") ?? + (parsed.searchParams.get("tryInChat") === "true" ? "try-in-chat" : null) + + return { + pluginId: decodeURIComponent(rawPluginId), + action: normalizePluginDeepLinkAction(pathAction ?? queryAction), + source: "protocol", + } +}