Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
30713c8
fix(providers): raise default model call timeout to 300s
xuzhiqin1990 Sep 19, 2026
4e4c5fc
feat: package agent runtime for npm
ZongYue99 Oct 9, 2026
2f934f5
chore: relocate agent npm package output
ZongYue99 Oct 9, 2026
019cce2
fix: sync generated agent npm package version
ZongYue99 Oct 9, 2026
8f9c428
fix(chat): keep copy actions on completed historical replies
Oct 9, 2026
a69fe87
fix: reuse degraded development memory service
Oct 9, 2026
d862a1c
Merge remote-tracking branch 'upstream/v1.2.2' into fix-bug-476-memor…
Oct 10, 2026
37598d5
fix: forward native progress events over SSE
ZongYue99 Oct 11, 2026
256b369
Merge pull request #4 from ZongYue99/main
ZongYue99 Oct 12, 2026
3a6adec
Merge branch 'zy_npm_1008' of https://github.com/ZongYue99/memmy-agen…
ZongYue99 Oct 12, 2026
c3b8162
fix: 支持 apply_patch 工作区内绝对路径
Oct 12, 2026
02e0bad
Merge pull request #613 from ZongYue99/zy_npm_1008
ZongYue99 Oct 12, 2026
22f542f
fix: add correlated API and model latency diagnostics
ZongYue99 Oct 12, 2026
3691ca6
Merge pull request #490 from xuzhiqin1990/fix/model-call-timeout-300s
syzsunshine219 Oct 12, 2026
8f1b045
Merge pull request #596 from zxr19980213/codex/fix-historical-reply-c…
syzsunshine219 Oct 12, 2026
27e0418
fix: wake exec waits on process completion and buffered output
yxyi123 Oct 12, 2026
e48fe20
Merge remote-tracking branch 'upstream/v1.2.3' into fix/memmy-675-exe…
yxyi123 Oct 12, 2026
ca38419
fix: locate bundled Codex CLI for hook trust installation
Orsted290 Oct 10, 2026
f0bd53b
fix: preserve hook removals and roll back failed Codex trust
Orsted290 Oct 12, 2026
516ba15
fix: allow models to share identical endpoints
Orsted290 Oct 12, 2026
b5ac066
fix: reuse saved BYOK endpoint in setup connection tests
Orsted290 Oct 13, 2026
b273fc5
Merge pull request #620 from Orsted290/fix-codex-hook-repeat-install-…
origami-chen Oct 13, 2026
b433ef3
fix: apply explicit API model selection to resumed sessions
ZongYue99 Oct 13, 2026
1d09b69
fix: 拒绝绝对路径中的父级跳转
Oct 13, 2026
440c289
Merge remote-tracking branch 'upstream/v1.2.3' into fix-671-apply-pat…
Oct 13, 2026
980ef05
Merge pull request #600 from jia030928/fix-bug-476-memory-port-conflict
origami-chen Oct 13, 2026
94e3eb9
fix: 恢复登录账号的内置模型
Oct 10, 2026
11c5ef9
Merge pull request #619 from MemTensor/main
syzsunshine219 Oct 13, 2026
0b83e1f
Merge remote-tracking branch 'upstream/v1.2.4' into fix-builtin-model…
Oct 13, 2026
3ba189a
fix: 修复登录账号内置模型配置恢复
Oct 13, 2026
516e2df
fix: 保留账号模型恢复结果并校验账号一致性
Oct 14, 2026
679b0ec
fix: 保留补丁规范化路径中的文件名空格
Oct 14, 2026
4557c6e
Merge remote-tracking branch 'upstream/v1.2.4' into fix-671-apply-pat…
Oct 14, 2026
daf6bc7
Merge pull request #612 from jia030928/fix-671-apply-patch-absolute-path
origami-chen Oct 14, 2026
43ead00
fix: trim API keys when matching pending presets
Orsted290 Oct 14, 2026
f83cdf1
fix: build bundled agent and memory runtimes in Memmy
ZongYue99 Oct 14, 2026
248bbaf
fix: deduplicate native source captures across account changes
Orsted290 Oct 10, 2026
4d38f01
test: clarify native conversation scope regression name
Orsted290 Oct 10, 2026
0e48c98
fix: collapse duplicate cross-account L1 recall candidates
Orsted290 Oct 14, 2026
a892b26
Merge pull request #622 from Orsted290/fix-duplicate-openai-endpoint-…
origami-chen Oct 14, 2026
15164f9
style(desktop): place social sign-in below email login
dolnna Oct 14, 2026
f2e4f07
Let external plugins participate in the agent lifecycle
penggaolai Oct 14, 2026
4753a6b
feat: build and verify runtime artifacts for all platforms
ZongYue99 Oct 14, 2026
6cb5ca4
chore: merge upstream v1.2.4
ZongYue99 Oct 14, 2026
1559374
Merge pull request #631 from ZongYue99/zy_runtime_artifacts
ZongYue99 Oct 14, 2026
7ec2e5e
Keep installed Memmy CLI directly executable
penggaolai Oct 14, 2026
f030dba
feat(auth): enable Google and GitHub social login
Oct 14, 2026
3512c27
Integrate upstream v1.2.4 updates into the customized agent branch
penggaolai Oct 14, 2026
374a4e5
feat: build all Memory runtime platforms with one command
ZongYue99 Oct 14, 2026
7db6ad6
fix(runtime): skip third-party install scripts when packaging the age…
Oct 14, 2026
fff7913
Merge pull request #635 from EF1874/fix/windows-runtime-package-posti…
ZongYue99 Oct 14, 2026
5a756c1
Merge pull request #634 from whipser030/v1.2.4-niu
ZongYue99 Oct 14, 2026
5dcbd11
Merge pull request #637 from antalike/codex/intl-social-login-style
dolnna Oct 14, 2026
195a8fa
fix: 保留迁移后的模型模式并刷新记忆配置
Oct 15, 2026
b9871f4
Merge remote-tracking branch 'upstream/v1.2.4' into fix-builtin-model…
Oct 15, 2026
0192ab8
fix: guard embedding inputs and isolate rejected batches (MEMMY-488)
whqf1123 Oct 10, 2026
ecb2c74
Merge pull request #624 from jia030928/fix-builtin-models-login-v1.2.3
origami-chen Oct 15, 2026
b972ec1
fix: delete generated spans with task memory
Orsted290 Oct 15, 2026
2b8f186
fix: emit SHA-256 checksum files for Agent runtime packages
ZongYue99 Oct 15, 2026
a8dbb8a
Merge remote-tracking branch 'upstream/v1.2.4' into fix-memory-task-s…
Orsted290 Oct 15, 2026
d0cea1f
fix: avoid failure alerts during background memory processing
Orsted290 Oct 15, 2026
d698dd4
fix: clarify missing ASR model error
Oct 15, 2026
ff70711
Merge pull request #643 from memory-lee/codex/fix-asr-model-not-confi…
ZongYue99 Oct 15, 2026
bb21412
fix: remove whole-turn deadlines from Memmy API
ZongYue99 Oct 15, 2026
b596123
fix: cancel Memmy turns only through explicit stop requests
ZongYue99 Oct 15, 2026
30a1199
fix(desktop): render skill markdown and restore slash command suggest…
Oct 15, 2026
16920de
fix(desktop): limit command palette height and surface stop in search
Oct 15, 2026
1755344
fix(desktop): match slash suggestions by command name prefix
Oct 15, 2026
7de08d5
feat(desktop): complete slash subcommands and model presets
Oct 15, 2026
117f7b2
fix(desktop): expand exact commands and place closest suggestions last
Oct 15, 2026
1cebfe0
fix(desktop): match commands and subcommands as flat candidates
Oct 15, 2026
5140cdd
Merge remote-tracking branch 'origin/v1.2.4' into codex/skill-markdow…
Oct 15, 2026
afece27
Merge pull request #636 from ZongYue99/zy_runtime_artifacts
ZongYue99 Oct 15, 2026
7a121e3
Merge pull request #618 from yxyi123/fix/memmy-675-exec-wakeup
origami-chen Oct 15, 2026
4d3228f
test: distinguish unexpected memory endpoint fixtures
Oct 15, 2026
896ca85
test(windows): model non-Memory health responses
Orsted290 Oct 16, 2026
e0f27e8
test: distinguish degraded Memory health from unrelated services
Orsted290 Oct 16, 2026
533d91e
Merge pull request #647 from GUANGDIJI/codex/skill-markdown-display
ZongYue99 Oct 16, 2026
083e5e1
fix: 统一 BYOK 显示、测试与保存的凭据来源
Orsted290 Oct 16, 2026
19ad8c9
Merge remote-tracking branch 'upstream/v1.2.4' into fix/memmy-488-emb…
whqf1123 Oct 16, 2026
3a0d9a4
fix: refine embedding failure isolation and empty chunk handling (MEM…
whqf1123 Oct 16, 2026
2184ff4
fix: 统一设置页凭据校验并保留连续复用关系
Orsted290 Oct 16, 2026
c6061ee
Merge pull request #639 from Orsted290/fix-cross-account-recall-dedup…
origami-chen Oct 16, 2026
58543d4
fix: 同步 v1.2.4 并解决 BYOK 凭据映射冲突
Orsted290 Oct 16, 2026
356a779
fix: find task spans by their own ownership links
Orsted290 Oct 16, 2026
b633dfa
Merge remote-tracking branch 'upstream/v1.2.4' into fix-memory-task-s…
Orsted290 Oct 16, 2026
7e2607d
Merge pull request #641 from whqf1123/fix/memmy-488-embedding-guard-v124
origami-chen Oct 16, 2026
e1683a6
Merge remote branch origin/fix-memory-task-span-delete
Orsted290 Oct 16, 2026
5a0951f
fix: 恢复记忆模型编辑后的连接重测并隔离卡片请求
Orsted290 Oct 17, 2026
4febd21
Merge branch 'v1.2.4' of github.com:MemTensor/memmy-agent into fix-by…
Orsted290 Oct 17, 2026
1fe1a6f
fix: isolate onboarding connection test responses
Orsted290 Oct 17, 2026
e00425f
test: cover BYOK requests completing after page unmount
Orsted290 Oct 17, 2026
4dd3d72
Merge pull request #623 from Orsted290/fix-byok-onboarding-key-test
origami-chen Oct 18, 2026
439ec88
Merge pull request #642 from Orsted290/fix-memory-task-span-delete
origami-chen Oct 18, 2026
946a752
Merge remote-tracking branch 'upstream/v1.2.4' into fix-memory-proces…
Orsted290 Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .github/workflows/runtime-artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Agent and Memory runtime artifacts
run-name: runtime-artifacts-${{ github.ref_name }}

on:
workflow_dispatch:
inputs:
runtime:
description: Runtime packages to build
type: choice
options: [both, memory]
default: both
push:
branches: [zy_memory_runtime_*]
permissions:
contents: read

jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- target: darwin-arm64
runner: macos-14
arch: arm64
- target: darwin-x64
runner: macos-15-intel
arch: x64
- target: linux-arm64
runner: ubuntu-24.04-arm
arch: arm64
- target: linux-x64
runner: ubuntu-24.04
arch: x64
- target: windows-arm64
runner: windows-11-arm
arch: arm64
- target: windows-x64
runner: windows-2025
arch: x64
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
defaults:
run:
shell: bash
env:
COPYFILE_DISABLE: '1'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 26.0.0
architecture: ${{ matrix.arch }}
cache: npm
- name: Install build dependencies
run: npm ci --no-audit --no-fund
- name: Install Agent build dependencies
if: github.event_name == 'workflow_dispatch' && inputs.runtime != 'memory'
run: npm --prefix App/memmy-agent install --ignore-scripts --no-audit --no-fund
- name: Build Agent runtime
if: github.event_name == 'workflow_dispatch' && inputs.runtime != 'memory'
run: npm run agent:runtime:package -- --target "${{ matrix.target }}" --output "$RUNNER_TEMP/runtime-artifacts"
- name: Build Memory runtime
run: npm run runtime:package -w @memmy/memory -- --target "${{ matrix.target }}" --output "$RUNNER_TEMP/runtime-artifacts"
- name: Verify unpacked runtimes and native dependencies
run: node scripts/internal/shared/verify-runtime-artifacts.mjs "$RUNNER_TEMP/runtime-artifacts" "${{ matrix.target }}" "${{ inputs.runtime || 'memory' }}"
- uses: actions/upload-artifact@v4
with:
name: runtimes-${{ matrix.target }}
path: ${{ runner.temp }}/runtime-artifacts/*
if-no-files-found: error
compression-level: 0
retention-days: 14

collect:
needs: build
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 26.0.0
- uses: actions/download-artifact@v4
with:
pattern: runtimes-*
path: runtime-input
- name: Collect runtime archives with checksums
run: node scripts/internal/shared/collect-runtime-artifacts.mjs runtime-input runtime-output "${{ inputs.runtime || 'memory' }}"
- uses: actions/upload-artifact@v4
with:
name: all-platform-runtimes
path: runtime-output/*
if-no-files-found: error
compression-level: 0
retention-days: 14
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules/
dist/
App/memmy-agent/npm-packages/
release/
App/shell/desktop/release/
Mac软件打包/
Expand Down
2 changes: 2 additions & 0 deletions App/backend/local-api-contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1127,6 +1127,8 @@ export type MemoryRuntimeModelSettings = z.infer<typeof MemoryRuntimeModelSettin

/** Schema for model config view. */
export const ModelConfigViewSchema = z.object({
/** Write-response-only aliases, resolved by the server using real credentials. */
endpointAliases: z.record(z.string(), z.record(z.string(), z.string())).optional(),
configRevision: z.string().min(1),
providers: z.array(TextModelProviderViewSchema),
modelAssignments: ModelAssignmentsSchema,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import type {
} from "./types.js";

const DEFAULT_TIMEOUT_MS = 5_000;
const CLOUD_SOCIAL_LOGIN_TIMEOUT_MS = 20_000;
const CLOUD_COMPOSIO_ROUTER_TIMEOUT_MS = 60_000;
const CLOUD_COMPOSIO_UNAVAILABLE_MESSAGE = "工具连接服务暂时不可用";
const CLOUD_COMPOSIO_SERVICE_UNAVAILABLE_CODE = 60020;
Expand Down Expand Up @@ -147,7 +148,7 @@ export function createHttpCloudClient(options: CreateHttpCloudClientOptions = {}
const data = await requestCloudData<Record<string, unknown>>(
fetchImpl,
baseUrl,
timeoutMs,
socialLoginTimeoutMs(timeoutMs),
"/api/agentUser/oauth/start",
{
body: {
Expand All @@ -169,7 +170,7 @@ export function createHttpCloudClient(options: CreateHttpCloudClientOptions = {}
const data = await requestCloudData<Record<string, unknown>>(
fetchImpl,
baseUrl,
timeoutMs,
socialLoginTimeoutMs(timeoutMs),
"/api/agentUser/oauth/status",
{
body: { flowId: input.flowId, pollToken: input.pollToken },
Expand Down Expand Up @@ -532,6 +533,10 @@ async function requestCloudData<T>(
return envelope.data as T;
}

function socialLoginTimeoutMs(timeoutMs: number): number {
return Math.max(timeoutMs, CLOUD_SOCIAL_LOGIN_TIMEOUT_MS);
}

function normalizeAgentRegion(value: string | undefined): "cn" | "intl" {
return value?.trim().toLowerCase() === "intl" ? "intl" : "cn";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,39 @@ describe("cloud client", () => {
]);
});

it("keeps waiting for Google login after the default cloud timeout", async () => {
server = createServer((_request, response) => {
setTimeout(() => {
sendJson(response, {
code: 0,
message: "ok",
data: {
flowId: "social-flow-id-0001",
pollToken: "social-poll-token-0000000000000001",
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth?state=opaque-state",
expiresInSec: 600,
pollIntervalSec: 2
}
});
}, 1_500);
});
await listen(server);
const address = server.address();
if (!address || typeof address === "string") throw new Error("Mock cloud server did not bind");
const client = createHttpCloudClient({
baseUrl: `http://127.0.0.1:${address.port}`,
timeoutMs: 200
});

const start = await client.startSocialLogin({
provider: "google",
locale: "en",
loginSource: "Memmy"
});

expect(start.authorizationUrl).toContain("https://accounts.google.com/");
});

it("rejects a social-login authorization URL outside the provider allowlist", async () => {
server = createServer((_request, response) => {
sendJson(response, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Codex hook trust persistence through the Codex app-server protocol. */
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { accessSync, constants, statSync } from "node:fs";
import { homedir } from "node:os";
import { basename, join, normalize } from "node:path";

const APP_SERVER_REQUEST_TIMEOUT_MS = 10_000;
Expand Down Expand Up @@ -283,8 +284,17 @@ function handleResponseLine(line: string, pending: Map<number, PendingRequest>):

function resolveCodexExecutable(codexHomeDirectory: string): string {
const executableName = process.platform === "win32" ? "codex.exe" : "codex";
const bundledExecutable = join(codexHomeDirectory, "plugins", ".plugin-appserver", executableName);
return isExecutableFile(bundledExecutable) ? bundledExecutable : executableName;
const candidates = [join(codexHomeDirectory, "plugins", ".plugin-appserver", executableName)];
if (process.platform === "darwin") {
// The desktop process may not inherit the shell PATH where ChatGPT exposes Codex CLI.
const codexCliPath = join("Contents", "Resources", "codex-cli", "CodexCLI.app", "Contents", "MacOS", "codex");
candidates.push(
join("/Applications", "ChatGPT.app", codexCliPath),
join(homedir(), "Applications", "ChatGPT.app", codexCliPath)
);
}

return candidates.find(isExecutableFile) ?? executableName;
}

function isExecutableFile(filePath: string): boolean {
Expand Down
105 changes: 83 additions & 22 deletions App/backend/src/adapters/outbound/skill-writer/codex/target.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Target module. */
import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
import { lstat, mkdir, readFile, readlink, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { basename, dirname, join } from "node:path";
import { resolveCodexHomeDirectory } from "../../agent-paths.js";
Expand Down Expand Up @@ -118,18 +118,26 @@ export function createCodexSkillTarget(deps: CreateCodexSkillTargetDeps = {}): S
memmy_config_path: memmyConfigPath,
...(await readMemmyMemoryServiceConfig(memmyConfigPath))
};
await mkdir(hookDirectory, { recursive: true });
await writeFileAtomically(hookConfigPath, `${JSON.stringify(hookConfig, null, 2)}\n`);
await writeFileAtomically(hookScriptPath, renderMemmyResumeHookScript({ source: CODEX_TARGET_ID, mode: "codex" }));
await writeFileAtomically(
join(hookDirectory, WORKSPACE_BRIDGE_FILE_NAME),
await loadMemmyWorkspaceBridgeRuntimeAsset()
);
const hooksFilePath = join(root, HOOKS_FILE_NAME);
const bridgePath = join(hookDirectory, WORKSPACE_BRIDGE_FILE_NAME);
const hookCommand = createNodeHookCommand(hookScriptPath);
await upsertCodexHookConfig(hooksFilePath, hookCommand);
await rm(join(root, HOOK_DIRECTORY_NAME, LEGACY_HOOK_SCRIPT_FILE_NAME), { force: true });
await withCodexHookFileRollback(
[hookConfigPath, hookScriptPath, bridgePath, hooksFilePath],
async () => {
await mkdir(hookDirectory, { recursive: true });
await writeFileAtomically(hookConfigPath, `${JSON.stringify(hookConfig, null, 2)}\n`);
await writeFileAtomically(hookScriptPath, renderMemmyResumeHookScript({ source: CODEX_TARGET_ID, mode: "codex" }));
await writeFileAtomically(bridgePath, await loadMemmyWorkspaceBridgeRuntimeAsset());
await upsertCodexHookConfig(hooksFilePath, hookCommand);
await trustHooks({ codexHomeDirectory: root, hooksFilePath, hookCommand });
await writeFileAtomically(
hookConfigPath,
`${JSON.stringify({ ...hookConfig, ...(await memmyHookRevisionField(CODEX_TARGET_ID, "codex")) }, null, 2)}\n`
);
}
);

await rm(join(hookDirectory, LEGACY_HOOK_SCRIPT_FILE_NAME), { force: true });
const manifest = renderMemmyPluginSkillManifest(_targetId);
const filePath = join(root, TARGET_FILE_NAME);
await writeFileAtomically(
Expand All @@ -138,15 +146,6 @@ export function createCodexSkillTarget(deps: CreateCodexSkillTargetDeps = {}): S
);
await replaceMemmySkillDirectory(root, manifest);
await replaceMemmyResumeSkillDirectory(root, CODEX_TARGET_ID);
await trustHooks({
codexHomeDirectory: root,
hooksFilePath,
hookCommand
});
await writeFileAtomically(
hookConfigPath,
`${JSON.stringify({ ...hookConfig, ...(await memmyHookRevisionField(CODEX_TARGET_ID, "codex")) }, null, 2)}\n`
);
},

async uninstallPlugin(_targetId) {
Expand Down Expand Up @@ -335,11 +334,73 @@ function createMarkerBlockPattern(startMarker: string, endMarker = END_MARKER):
return new RegExp(`${escapeRegExp(startMarker)}\\n[\\s\\S]*?${escapeRegExp(endMarker)}\\n?`, "m");
}

async function writeFileAtomically(filePath: string, content: string): Promise<void> {
async function writeFileAtomically(filePath: string, content: string | Buffer, mode?: number): Promise<void> {
await mkdir(dirname(filePath), { recursive: true });
const tempPath = join(dirname(filePath), `.${basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
await writeFile(tempPath, content, "utf8");
await rename(tempPath, filePath);
try {
await writeFile(tempPath, content, { encoding: "utf8", ...(mode === undefined ? {} : { mode }) });
await rename(tempPath, filePath);
} finally {
await rm(tempPath, { force: true });
}
}

type CodexHookFileSnapshot =
| { path: string; kind: "missing" }
| { path: string; kind: "file"; content: Buffer; mode: number }
| { path: string; kind: "symlink"; target: string };

async function withCodexHookFileRollback<T>(paths: string[], operation: () => Promise<T>): Promise<T> {
const snapshots = await Promise.all(paths.map(snapshotCodexHookFile));
try {
return await operation();
} catch (error) {
const rollbackResults = await Promise.allSettled(snapshots.map(restoreCodexHookFile));
const rollbackErrors = rollbackResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
if (rollbackErrors.length > 0) {
throw new AggregateError(
[error, ...rollbackErrors],
"Codex hook installation failed and its files could not be fully rolled back"
);
}

throw error;
}
}

async function snapshotCodexHookFile(filePath: string): Promise<CodexHookFileSnapshot> {
try {
const metadata = await lstat(filePath);
if (metadata.isSymbolicLink()) {
return { path: filePath, kind: "symlink", target: await readlink(filePath) };
}
if (!metadata.isFile()) {
throw new Error(`Codex hook path is not a regular file: ${filePath}`);
}

return { path: filePath, kind: "file", content: await readFile(filePath), mode: metadata.mode & 0o777 };
} catch (error) {
if (isNodeError(error) && error.code === "ENOENT") {
return { path: filePath, kind: "missing" };
}

throw error;
}
}

async function restoreCodexHookFile(snapshot: CodexHookFileSnapshot): Promise<void> {
if (snapshot.kind === "missing") {
await rm(snapshot.path, { force: true });
return;
}

if (snapshot.kind === "symlink") {
await rm(snapshot.path, { force: true });
await symlink(snapshot.target, snapshot.path);
return;
}

await writeFileAtomically(snapshot.path, snapshot.content, snapshot.mode);
}

function escapeRegExp(input: string): string {
Expand Down
Loading
Loading