Files
openclaw/extensions/codex/src/migration/source-files.ts
Peter Steinberger 4319ddbe8c feat(control-ui): import Codex and Claude Code memory (#106406)
* feat(control-ui): import coding assistant memory

* test(migrate): clean memory import temp dirs

* chore: move memory import note to PR body

* build: refresh memory import generated artifacts

* fix(control-ui): complete memory import checks

* build(control-ui): localize memory recovery labels

* fix(control-ui): harden memory import recovery

* refactor(migrate): keep memory import surfaces bounded

* fix(control-ui): use canonical agent row type

* fix(codex): drop dead migration type exports

* fix(migrate): restrict imported memory permissions

* fix(control-ui): preserve memory import recovery state

* fix(control-ui): retain memory import results

* build(control-ui): preserve translation memory history

* fix(control-ui): bind memory import state to agent

* fix(control-ui): unlock memory import after refresh

* test(gateway): preserve memory method suffix

* fix(migrate): bind reviewed memory imports

* fix(migrate): make memory imports retry-safe

* perf(ui): keep memory import lazy

* build: refresh generated artifacts after rebase

* chore: keep migration runtime within LOC guard

* build: refresh Swift protocol model
2026-07-13 15:30:06 -07:00

187 lines
5.4 KiB
TypeScript

// Codex migration source file discovery stays filesystem-only and bounded.
import type { Dirent } from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { CODEX_PLUGINS_MARKETPLACE_NAME } from "../app-server/config.js";
import { exists, isDirectory, readJsonObject } from "./helpers.js";
const SKILL_FILENAME = "SKILL.md";
const MAX_SCAN_DEPTH = 6;
const MAX_DISCOVERED_DIRS = 2000;
export type CodexSkillSource = {
name: string;
source: string;
sourceLabel: string;
};
export type CodexPluginMigrationBlockCode =
| "plugin_disabled"
| "codex_subscription_required"
| "codex_account_unavailable"
| "plugin_read_unavailable"
| "app_inventory_unavailable"
| "app_inaccessible"
| "app_disabled"
| "app_missing";
export type CodexPluginMigrationAppFact = {
id: string;
name: string;
needsAuth?: boolean;
isAccessible?: boolean;
isEnabled?: boolean;
};
type CodexPluginMigrationBlock = {
code: CodexPluginMigrationBlockCode;
apps?: CodexPluginMigrationAppFact[];
error?: string;
};
export type CodexPluginSource = {
name: string;
source: string;
sourceKind: "app-server" | "cache";
migratable: boolean;
manifestPath?: string;
marketplaceName?: typeof CODEX_PLUGINS_MARKETPLACE_NAME;
pluginName?: string;
installed?: boolean;
enabled?: boolean;
apps?: CodexPluginMigrationAppFact[];
migrationBlock?: CodexPluginMigrationBlock;
message?: string;
};
export type CodexMemorySource = {
id: string;
label: string;
path: string;
};
async function safeReadDir(dir: string): Promise<Dirent[]> {
return await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
}
export async function discoverSkillDirs(params: {
root: string | undefined;
sourceLabel: string;
excludeSystem?: boolean;
}): Promise<CodexSkillSource[]> {
if (!params.root || !(await isDirectory(params.root))) {
return [];
}
const discovered: CodexSkillSource[] = [];
async function visit(dir: string, depth: number): Promise<void> {
if (discovered.length >= MAX_DISCOVERED_DIRS || depth > MAX_SCAN_DEPTH) {
return;
}
const name = path.basename(dir);
if (params.excludeSystem && depth === 1 && name === ".system") {
return;
}
if (await exists(path.join(dir, SKILL_FILENAME))) {
discovered.push({ name, source: dir, sourceLabel: params.sourceLabel });
return;
}
for (const entry of await safeReadDir(dir)) {
if (entry.isDirectory()) {
await visit(path.join(dir, entry.name), depth + 1);
}
}
}
await visit(params.root, 0);
return discovered;
}
export async function discoverPluginDirs(codexHome: string): Promise<CodexPluginSource[]> {
const root = path.join(codexHome, "plugins", "cache");
if (!(await isDirectory(root))) {
return [];
}
const discovered = new Map<string, CodexPluginSource>();
async function visit(dir: string, depth: number): Promise<void> {
if (discovered.size >= MAX_DISCOVERED_DIRS || depth > MAX_SCAN_DEPTH) {
return;
}
const manifestPath = path.join(dir, ".codex-plugin", "plugin.json");
if (await exists(manifestPath)) {
const manifest = await readJsonObject(manifestPath);
const manifestName = typeof manifest.name === "string" ? manifest.name.trim() : "";
discovered.set(dir, {
name: manifestName || path.basename(dir),
source: dir,
manifestPath,
sourceKind: "cache",
migratable: false,
message:
"Cached Codex plugin bundle found. Review manually unless the plugin is also installed in the source Codex app-server inventory",
});
return;
}
for (const entry of await safeReadDir(dir)) {
if (entry.isDirectory()) {
await visit(path.join(dir, entry.name), depth + 1);
}
}
}
await visit(root, 0);
return [...discovered.values()].toSorted((a, b) => a.source.localeCompare(b.source));
}
async function discoverCodexMemoryFile(
candidate: CodexMemorySource,
): Promise<CodexMemorySource | undefined> {
try {
const stat = await fs.lstat(candidate.path);
if (stat.isSymbolicLink()) {
throw new Error(`Codex memory source must not be a symbolic link: ${candidate.path}`);
}
if (!stat.isFile()) {
throw new Error(`Codex memory source must be a regular file: ${candidate.path}`);
}
return candidate;
} catch (error) {
if (
error &&
typeof error === "object" &&
"code" in error &&
(error as { code?: unknown }).code === "ENOENT"
) {
return undefined;
}
throw error;
}
}
export async function discoverCodexMemorySources(codexHome: string): Promise<{
memoriesDir?: string;
memoryFiles: CodexMemorySource[];
}> {
const memoriesDir = path.join(codexHome, "memories");
const memoryFiles = (
await Promise.all(
[
{ id: "memory:codex:MEMORY.md", label: "Codex consolidated memory", name: "MEMORY.md" },
{
id: "memory:codex:memory_summary.md",
label: "Codex memory summary",
name: "memory_summary.md",
},
].map(
async (candidate) =>
await discoverCodexMemoryFile({
id: candidate.id,
label: candidate.label,
path: path.join(memoriesDir, candidate.name),
}),
),
)
).filter((entry): entry is CodexMemorySource => entry !== undefined);
return {
...((await isDirectory(memoriesDir)) ? { memoriesDir } : {}),
memoryFiles,
};
}