From 568c1274f3a517cededa5053ac4033a5dc56188d Mon Sep 17 00:00:00 2001 From: ToussaintKnight <272199827+ToussaintKnight@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:36:09 +0800 Subject: [PATCH] feat: complete Codex Sync identity migration --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .gitignore | 6 +- CHANGELOG.md | 2 + CHANGELOG.zh-CN.md | 2 + README.md | 54 +++-- README.zh-CN.md | 56 +++-- SECURITY.md | 8 +- SECURITY.zh-CN.md | 8 +- SKILL.md | 54 ++--- agents/openai.yaml | 6 +- docs/architecture.md | 6 +- docs/architecture.zh-CN.md | 6 +- docs/demo.md | 6 +- docs/demo.zh-CN.md | 6 +- package.json | 11 +- references/deployment.md | 129 ++++++----- references/protocol.md | 28 +-- references/usage.md | 84 +++---- ...rap-sync2.ps1 => bootstrap-codex-sync.ps1} | 16 +- ...strap-sync2.sh => bootstrap-codex-sync.sh} | 18 +- scripts/check-repo-hygiene.mjs | 2 +- scripts/{sync2.mjs => codexsync.mjs} | 209 +++++++++--------- scripts/repair-git-https.ps1 | 16 +- .../{test-sync2.mjs => test-codexsync.mjs} | 38 ++-- 26 files changed, 412 insertions(+), 365 deletions(-) rename scripts/{bootstrap-sync2.ps1 => bootstrap-codex-sync.ps1} (73%) rename scripts/{bootstrap-sync2.sh => bootstrap-codex-sync.sh} (65%) rename scripts/{sync2.mjs => codexsync.mjs} (88%) rename scripts/{test-sync2.mjs => test-codexsync.mjs} (93%) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 8c403da..281f247 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug report -description: Report a reproducible Sync2 failure using synthetic or redacted data. +description: Report a reproducible Codex Sync failure using synthetic or redacted data. title: "[Bug]: " labels: [bug] body: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e98a619..ce24d0c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: false contact_links: - name: Security vulnerability - url: https://github.com/ToussaintKnight/codex-sync2/security/advisories/new + url: https://github.com/ToussaintKnight/codex-sync/security/advisories/new about: Report vulnerabilities or possible private-data exposure confidentially. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 7319ea4..84dccdd 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,5 +1,5 @@ name: Feature request -description: Propose a scoped improvement to Sync2. +description: Propose a scoped improvement to Codex Sync. title: "[Feature]: " labels: [enhancement] body: diff --git a/.gitignore b/.gitignore index 882efe5..822e218 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,10 @@ dist/ tmp/ *.log -# Local Sync2 and Codex state — never commit -.sync2/ +# Local Codex Sync and Codex state — never commit +.codex-sync/ .codex/ -Sync2Vault/ +CodexSyncVault/ vault/ conversations/ heads/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0b2e6..65f9d94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes are documented here. ## Unreleased +- Complete the breaking identity migration to **Codex Sync**: `$codex-sync` for the Skill, `codexsync` for the CLI, `.codex-sync/` for state, and matching scheduler/service identifiers. Mixed-version fleet writes are unsupported during this upgrade. +- Rewrite the README motivation around the real cross-device continuity gap between local Codex projects and trusted computers. - Fix project synchronization so selecting a project also selects its non-archived Codex tasks instead of producing a project with `No tasks` on the target device. - Add project discovery, per-device portable project catalogs, target-side folder acceptance, Codex desktop registration, and explicit path-mapping commands. - Make Windows/macOS project path mapping separator-aware and path-boundary-aware. diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index d7bc3ee..9dc0571 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -6,6 +6,8 @@ ## 未发布 +- 完成到 **Codex Sync** 的破坏性标识迁移:Skill 使用 `$codex-sync`,CLI 使用 `codexsync`,状态目录使用 `.codex-sync/`,调度器与服务标识同步更新。升级期间不支持新旧版本设备同时写入。 +- 围绕本地 Codex 项目在可信设备之间的真实连续工作缺口,重写 README 项目动机。 - 修复项目同步:选择项目时会一并选择其非归档 Codex 任务,避免目标设备只出现项目却显示 `No tasks`。 - 新增项目发现、每设备可移植项目目录、目标端文件夹接收、Codex 桌面注册与显式路径映射命令。 - Windows/macOS 项目路径映射现在可识别不同分隔符,并严格检查路径边界。 diff --git a/README.md b/README.md index dbef942..a5a083c 100644 --- a/README.md +++ b/README.md @@ -2,61 +2,72 @@ **English** | [简体中文](README.zh-CN.md) -Codex task history lives on one machine. **Codex Sync2 gives multi-device Codex users a recoverable synchronization layer for selected tasks and personal skills—without copying credentials, live databases, or unfinished turns.** +**Continue the same local Codex project on your trusted computers—without copying credentials, live databases, or unfinished turns.** -Built for developers who move between Windows and macOS and need the same trusted Codex context on every device. +Codex Sync is a local-first continuity layer for developers who move between Windows and macOS and want their selected projects, task histories, and personal skills to travel safely with them. + +> The product and Codex Skill are **Codex Sync** / `$codex-sync`. The shell CLI is `codexsync`—not `sync`—to avoid colliding with operating-system utilities. Local protocol state lives under `.codex-sync/`. ```text -$ sync2 conversation select current +$ codexsync conversation select current Selected: Example task -$ sync2 sync +$ codexsync sync conversationsPushed: 1 conversationConflicts: 0 -$ sync2 doctor +$ codexsync doctor ok: true ``` -## Why Sync2 exists +## Why Codex Sync exists + +Local Codex work is rooted in the computer where it runs: a task records its transcript and working directory, while a local project connects to folders on that machine. That is a useful safety boundary, but signing into the same account on another trusted computer does not by itself recreate the same local workspace and task history. + +Imagine a normal day. You start a project on an office workstation in the morning. In the afternoon, you open a MacBook beside a river or at the beach and want to continue. After dinner, while the family settles into a long TV episode, you open a Windows laptop on the couch—and expect the morning's Codex project, tasks, and context to be there. Your Git repository may already have the code, but the local Codex continuity can still be stranded on another device. -Copying `~/.codex` is unsafe: it mixes credentials, device identity, live SQLite state, caches, and partially written turns. Generic file synchronization does not understand which Codex events form a complete conversation. +**That continuity gap—not generic backup—is why Codex Sync exists.** -Sync2 adds that missing semantic layer: +Blindly copying `~/.codex` is not the answer: it mixes credentials, device identity, live SQLite state, caches, and partially written turns. Generic file synchronization also cannot tell which Codex events form a complete, recoverable conversation. + +Codex Sync adds that missing semantic layer: - select only the tasks worth carrying between devices; - publish only complete JSONL and stable, closed turns; - keep independent device heads and promote a canonical history only when they are byte-prefix compatible; - quarantine incomplete or divergent history instead of inventing a merge; - synchronize personal skill collections with three-way hashes and preserved conflict copies; -- update local Codex indexes without copying another device's database. +- update local Codex indexes without copying another device's database; - discover Codex Projects, select their tasks as a group, and map project roots across Windows and macOS. ## Quick start Requirements: Node.js 22+, Codex initialized once on each device, and a trusted shared folder or checked-out private Git vault. +> **Upgrading an existing fleet to v0.3:** pause vault transport and stop the scheduler installed by the earlier release on every device first. Back up the shared vault and each device's local state, then rebootstrap all devices as one coordinated upgrade. The Skill, state directory, and scheduler identities changed; pre-v0.3 and v0.3 writers must never share a live vault. + Install as a Codex skill: ```sh -git clone codex-sync2 -mkdir -p "$HOME/.codex/skills/sync2" -cp -R codex-sync2/. "$HOME/.codex/skills/sync2/" +git clone https://github.com/ToussaintKnight/codex-sync.git +npm install --global ./codex-sync +mkdir -p "$HOME/.codex/skills/codex-sync" +cp -R codex-sync/. "$HOME/.codex/skills/codex-sync/" ``` Initialize one device: ```sh -node "$HOME/.codex/skills/sync2/scripts/sync2.mjs" init \ - --vault "$HOME/Sync2Vault" \ +codexsync init \ + --vault "$HOME/CodexSyncVault" \ --transport folder \ --device mac-main ``` -Then invoke `$sync2 current` inside the Codex task you want to preserve. Run `$sync2 doctor` before enabling automatic scheduling. +Then invoke `$codex-sync current` inside the Codex task you want to preserve. Run `$codex-sync doctor` before enabling automatic scheduling. Windows and macOS cold-start commands are in the [deployment guide](references/deployment.md). -> **Security note:** the operational vault stores selected conversation text and user skill source in plaintext. Sync2 never copies credentials or complete Codex databases. Use only storage and peers you trust; see [Security and privacy](SECURITY.md). +> **Security note:** the operational vault stores selected conversation text and user skill source in plaintext. Codex Sync never copies credentials or complete Codex databases. Use only storage and peers you trust; see [Security and privacy](SECURITY.md). ## How it works @@ -70,7 +81,7 @@ flowchart LR K --> I["Import JSONL + update local indexes"] ``` -Syncthing or private Git transports bytes. Sync2 decides which bytes are a safe Codex conversation. Read the [architecture](docs/architecture.md) and [protocol](references/protocol.md) for the invariants. +Syncthing or private Git transports bytes. Codex Sync decides which bytes are a safe Codex conversation. Read the [architecture](docs/architecture.md) and [protocol](references/protocol.md) for the invariants. ## Evidence that it works @@ -89,7 +100,7 @@ Included: - explicitly selected rollout JSONL; - portable task metadata; - user-defined skill files; -- device selection events and health reports. +- device selection events and health reports; - per-device project catalogs and explicit source-to-local path mappings. Excluded: @@ -104,8 +115,9 @@ Excluded: - Conversation attachments are not copied. - Independently continuing the same task while devices are disconnected creates an explicit conflict requiring a human choice. -- Sync2 does not encrypt the vault itself. -- Older Sync2 clients do not understand maintenance mode; disable their schedulers before a fleet upgrade. +- Codex Sync does not encrypt the vault itself. +- Older clients do not understand maintenance mode; disable their schedulers before a fleet upgrade. +- Windows and macOS are supported today. iPhone and Android continuity is future scope and depends on compatible Codex storage and execution surfaces. - Codex storage formats may evolve; run `doctor` and the test suite after Codex upgrades. ## Documentation @@ -121,7 +133,7 @@ Excluded: ## Project status -Sync2 is an early, tested engineering project. The current release is intended for users comfortable inspecting local files, backups, and synchronization health. Safety and recoverability take priority over automatic conflict resolution. +Codex Sync is an early, tested engineering project. The current release is intended for users comfortable inspecting local files, backups, and synchronization health. Safety and recoverability take priority over automatic conflict resolution. ## License diff --git a/README.zh-CN.md b/README.zh-CN.md index 422678a..5f46a7b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,62 +1,73 @@ -# Codex Sync2 +# Codex Sync [English](README.md) | **简体中文** -Codex 的任务历史通常只保存在当前电脑。**Codex Sync2 为跨 Windows 与 macOS 使用 Codex 的开发者提供可恢复的选定任务与个人 Skill 同步层,同时避免复制凭据、运行中的数据库或尚未完成的回合。** +**在自己信任的不同电脑上继续同一个本地 Codex 项目,同时不复制凭据、运行中的数据库或尚未完成的回合。** -它适合需要在多台设备上继续使用同一份可信 Codex 上下文的开发者。 +Codex Sync 是一个本地优先的连续工作层,面向需要在 Windows 与 macOS 之间切换,并希望安全携带已选择项目、任务历史和个人 Skill 的开发者。 + +> 产品与 Codex Skill 的正式名称是 **Codex Sync** / `$codex-sync`。Shell CLI 使用 `codexsync`,而不是容易与操作系统工具冲突的 `sync`;本地协议状态存放在 `.codex-sync/`。 ```text -$ sync2 conversation select current +$ codexsync conversation select current Selected: Example task -$ sync2 sync +$ codexsync sync conversationsPushed: 1 conversationConflicts: 0 -$ sync2 doctor +$ codexsync doctor ok: true ``` -## 为什么需要 Sync2 +## 为什么需要 Codex Sync + +本地 Codex 工作扎根于实际运行它的电脑:任务会记录对话和工作目录,本地项目则连接到这台机器上的文件夹。这是一条有价值的安全边界,但即使在另一台可信电脑上登录同一个账号,也不会自动重建完全相同的本地工作区与任务历史。 + +想象普通的一天:早晨你在办公室工作站新建一个项目;下午带着 MacBook 到河边或海边,想接着做;晚饭后,全家在沙发上看一集很长的电视剧,你又打开另一台 Windows 笔记本,希望继续早晨那条 Codex 任务。代码也许已经通过 Git 到了新设备,但本地 Codex 项目、任务和上下文仍可能留在另一台电脑上。 -直接复制 `~/.codex` 并不安全:其中混合了凭据、设备身份、正在写入的 SQLite 状态、缓存和未完成回合。普通文件同步工具也不知道哪些 Codex 事件才能构成完整会话。 +**Codex Sync 正是为这个“连续工作缺口”而生,而不只是为了普通备份。** -Sync2 补上了这一层语义安全能力: +直接复制 `~/.codex` 并不是答案:其中混合了凭据、设备身份、正在写入的 SQLite 状态、缓存和未完成回合。普通文件同步工具也无法判断哪些 Codex 事件才能构成完整、可恢复的会话。 + +Codex Sync 补上了这一层语义安全能力: - 只选择真正需要跨设备保留的任务; - 只发布完整 JSONL 和稳定、已关闭的回合; - 为每台设备保留独立 head,仅在历史满足字节前缀关系时提升 canonical; - 隔离不完整或分叉历史,而不是虚构合并结果; - 通过三方哈希同步个人 Skill,并保留冲突副本; -- 更新本地 Codex 索引,而不是复制其他设备的数据库。 +- 更新本地 Codex 索引,而不是复制其他设备的数据库; - 发现 Codex Projects、按项目批量选择任务,并映射 Windows 与 macOS 的不同项目路径。 ## 快速开始 要求:Node.js 22+、每台设备至少启动过一次 Codex,以及一个可信共享文件夹或已检出的私有 Git vault。 +> **将现有设备集升级到 v0.3:**先在每台设备上暂停 vault 传输,并停止旧版本安装的调度器。备份共享 vault 与每台设备的本地状态,再把所有设备作为一次受控升级重新 bootstrap。Skill、状态目录和调度器标识都已变化;绝不能让 v0.3 之前的客户端与 v0.3 客户端同时写入在线 vault。 + 安装为 Codex Skill: ```sh -git clone codex-sync2 -mkdir -p "$HOME/.codex/skills/sync2" -cp -R codex-sync2/. "$HOME/.codex/skills/sync2/" +git clone https://github.com/ToussaintKnight/codex-sync.git +npm install --global ./codex-sync +mkdir -p "$HOME/.codex/skills/codex-sync" +cp -R codex-sync/. "$HOME/.codex/skills/codex-sync/" ``` 初始化一台设备: ```sh -node "$HOME/.codex/skills/sync2/scripts/sync2.mjs" init \ - --vault "$HOME/Sync2Vault" \ +codexsync init \ + --vault "$HOME/CodexSyncVault" \ --transport folder \ --device mac-main ``` -随后在需要保留的 Codex 任务中调用 `$sync2 current`。启用自动调度前先运行 `$sync2 doctor`。 +随后在需要保留的 Codex 任务中调用 `$codex-sync current`。启用自动调度前先运行 `$codex-sync doctor`。 Windows 与 macOS 的冷启动命令见[部署指南](references/deployment.md)。 -> **安全提示:**实际运行的 vault 会以明文保存已选择的会话文本与用户 Skill 源码。Sync2 不会复制凭据或完整 Codex 数据库。请只使用你信任的存储与对等设备;详见[安全与隐私](SECURITY.zh-CN.md)。 +> **安全提示:**实际运行的 vault 会以明文保存已选择的会话文本与用户 Skill 源码。Codex Sync 不会复制凭据或完整 Codex 数据库。请只使用你信任的存储与对等设备;详见[安全与隐私](SECURITY.zh-CN.md)。 ## 工作原理 @@ -70,7 +81,7 @@ flowchart LR K --> I["导入 JSONL 并更新本地索引"] ``` -Syncthing 或私有 Git 负责传输字节;Sync2 负责判断哪些字节构成安全的 Codex 会话。关键约束见[架构说明](docs/architecture.zh-CN.md)与[协议文档](references/protocol.md)。 +Syncthing 或私有 Git 负责传输字节;Codex Sync 负责判断哪些字节构成安全的 Codex 会话。关键约束见[架构说明](docs/architecture.zh-CN.md)与[协议文档](references/protocol.md)。 ## 可验证的工程证据 @@ -89,7 +100,7 @@ GitHub Actions 在 Windows、macOS 和 Linux 的 Node.js 22 环境中运行同 - 明确选择的 rollout JSONL; - 可移植任务元数据; - 用户自定义 Skill 文件; -- 设备选择事件与健康报告。 +- 设备选择事件与健康报告; - 每设备项目目录与显式的源路径到本地路径映射。 排除: @@ -104,8 +115,9 @@ GitHub Actions 在 Windows、macOS 和 Linux 的 Node.js 22 环境中运行同 - 不复制会话附件。 - 设备离线期间若在多端独立继续同一任务,会产生需要人工选择的显式冲突。 -- Sync2 本身不加密 vault。 -- 旧版 Sync2 不理解维护模式;升级整个设备集前必须禁用旧调度器。 +- Codex Sync 本身不加密 vault。 +- 旧版客户端不理解维护模式;升级整个设备集前必须禁用旧调度器。 +- 当前支持 Windows 与 macOS。iPhone 和 Android 连续工作属于未来范围,并依赖兼容的 Codex 存储与执行界面。 - Codex 存储格式可能变化;Codex 升级后应运行 `doctor` 与测试套件。 ## 文档 @@ -121,7 +133,7 @@ GitHub Actions 在 Windows、macOS 和 Linux 的 Node.js 22 环境中运行同 ## 项目状态 -Sync2 是一个处于早期阶段、但已有完整测试的工程项目。目前版本面向愿意检查本地文件、备份与同步健康状态的用户。安全与可恢复性优先于自动冲突合并。 +Codex Sync 是一个处于早期阶段、但已有完整测试的工程项目。目前版本面向愿意检查本地文件、备份与同步健康状态的用户。安全与可恢复性优先于自动冲突合并。 ## 许可证 diff --git a/SECURITY.md b/SECURITY.md index ffe43f7..799ea74 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,17 +6,17 @@ Use GitHub's private security-advisory flow for vulnerabilities or suspected data exposure. Do not open a public issue containing conversation text, local paths, device identifiers, credentials, or vault contents. -Include the affected Sync2 version, platform, reproduction steps using synthetic data, and the expected security boundary. Remove secrets before attaching logs. +Include the affected Codex Sync version, platform, reproduction steps using synthetic data, and the expected security boundary. Remove secrets before attaching logs. Before every push or release, run `npm run privacy:scan`. If a credential has already reached Git history or a remote, revoke or rotate it; deleting it in a later commit is not sufficient. ## Data model -Sync2 is a local-first synchronization tool. It reads selected Codex rollout files and user-configured skill roots, then writes sanitized protocol state into a user-controlled vault. +Codex Sync is a local-first synchronization tool. It reads selected Codex rollout files and user-configured skill roots, then writes sanitized protocol state into a user-controlled vault. -The vault may contain plaintext conversation bodies and source code. Sync2 does not encrypt the vault. Use encrypted disks, trusted Syncthing peers, or a private Git repository with appropriate access controls. +The vault may contain plaintext conversation bodies and source code. Codex Sync does not encrypt the vault. Use encrypted disks, trusted Syncthing peers, or a private Git repository with appropriate access controls. -Sync2 must not copy: +Codex Sync must not copy: - Codex authentication or API credentials; - complete Codex databases or WAL files; diff --git a/SECURITY.zh-CN.md b/SECURITY.zh-CN.md index 211b938..4579d47 100644 --- a/SECURITY.zh-CN.md +++ b/SECURITY.zh-CN.md @@ -6,17 +6,17 @@ 对于漏洞或疑似数据暴露,请使用 GitHub 私有安全公告流程。不要在公开 Issue 中包含会话文本、本地路径、设备标识、凭据或 vault 内容。 -请提供受影响的 Sync2 版本、平台、使用合成数据的复现步骤,以及预期的安全边界。附加日志前先删除敏感信息。 +请提供受影响的 Codex Sync 版本、平台、使用合成数据的复现步骤,以及预期的安全边界。附加日志前先删除敏感信息。 每次 push 或 release 前运行 `npm run privacy:scan`。如果凭据已经进入 Git 历史或远端,必须撤销或轮换;仅在后续提交中删除并不足够。 ## 数据模型 -Sync2 是本地优先的同步工具。它读取已选择的 Codex rollout 文件与用户配置的 Skill 根目录,然后将经过协议约束的状态写入用户控制的 vault。 +Codex Sync 是本地优先的同步工具。它读取已选择的 Codex rollout 文件与用户配置的 Skill 根目录,然后将经过协议约束的状态写入用户控制的 vault。 -Vault 可能包含明文会话正文与源码。Sync2 不加密 vault。请使用加密磁盘、可信 Syncthing 对等设备或具备适当访问控制的私有 Git 仓库。 +Vault 可能包含明文会话正文与源码。Codex Sync 不加密 vault。请使用加密磁盘、可信 Syncthing 对等设备或具备适当访问控制的私有 Git 仓库。 -Sync2 不得复制: +Codex Sync 不得复制: - Codex 身份验证信息或 API 凭据; - 完整 Codex 数据库或 WAL 文件; diff --git a/SKILL.md b/SKILL.md index c159824..fc37754 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,41 +1,43 @@ --- -name: sync2 -description: Safely synchronize selected local Codex conversations, user-defined skills, and explicitly selected project directories across Windows and macOS through Syncthing, a shared folder, cloud-drive folder, or private Git repository. Use when the user invokes $sync2 or writes /sync2, starts a new project that should become its own Syncthing folder, asks to sync/export/import a current or named Codex task, wants personal Codex/Hermes/agent skills mirrored between devices, needs sync status or conflict recovery, or wants automatic scheduled synchronization. +name: codex-sync +description: Run Codex Sync to safely synchronize selected local Codex conversations, user-defined skills, and explicitly selected project directories across Windows and macOS through Syncthing, a shared folder, cloud-drive folder, or private Git repository. Use when the user invokes $codex-sync or writes /codex-sync, starts a new project that should become its own Syncthing folder, asks to sync/export/import a current or named Codex task, wants personal Codex/Hermes/agent skills mirrored between devices, needs sync status or conflict recovery, or wants automatic scheduled synchronization. --- -# Sync2 +# Codex Sync + +Use `$codex-sync` to invoke the Codex Skill. The bundled shell CLI and Node entry point are named `codexsync` to avoid colliding with an operating-system `sync` command. Store local protocol state under `.codex-sync/`. Use the bundled cross-platform Node script for all state changes. Keep credentials, `auth.json`, device IDs, SQLite databases, logs, caches, system skills, and plugin caches out of the vault. ## Command routing -Interpret `/sync2` as an alias for this skill. Prefer `$sync2` when explaining native Codex invocation because skills are natively invoked with `$`. +Interpret `/codex-sync` as an alias for this skill. Prefer `$codex-sync` when explaining native Codex invocation because skills are natively invoked with `$`. Run: ```text -node /scripts/sync2.mjs +node /scripts/codexsync.mjs ``` Map requests as follows: -- `/sync2 setup ` -> `init --vault ` -- `/sync2 current` -> `conversation select current`, then `sync` -- `/sync2 ` -> `conversation select `, then `sync` -- `/sync2` or `/sync2 now` -> `sync` -- `/sync2 status` -> `status` -- `/sync2 skills` -> `skills discover`, show findings, then `sync` -- `/sync2 auto` -> `daemon install` -- `/sync2 doctor` -> `doctor` -- `/sync2 audit ` -> `conversation audit ` -- `/sync2 repair ` -> enable maintenance, back up, then `conversation repair ` -- `/sync2 maintenance` -> `maintenance status` -- `/sync2 move ` -> `vault use --vault --transport ` -- `/sync2 project current` -> run `project add current` from the project root -- `/sync2 project ` -> `project add ` -- `/sync2 projects` -> `project list` -- `/sync2 project discover` -> `project discover` to list Codex Projects and their local tasks -- `/sync2 devices` -> `device list` +- `/codex-sync setup ` -> `init --vault ` +- `/codex-sync current` -> `conversation select current`, then `sync` +- `/codex-sync ` -> `conversation select `, then `sync` +- `/codex-sync` or `/codex-sync now` -> `sync` +- `/codex-sync status` -> `status` +- `/codex-sync skills` -> `skills discover`, show findings, then `sync` +- `/codex-sync auto` -> `daemon install` +- `/codex-sync doctor` -> `doctor` +- `/codex-sync audit ` -> `conversation audit ` +- `/codex-sync repair ` -> enable maintenance, back up, then `conversation repair ` +- `/codex-sync maintenance` -> `maintenance status` +- `/codex-sync move ` -> `vault use --vault --transport ` +- `/codex-sync project current` -> run `project add current` from the project root +- `/codex-sync project ` -> `project add ` +- `/codex-sync projects` -> `project list` +- `/codex-sync project discover` -> `project discover` to list Codex Projects and their local tasks +- `/codex-sync devices` -> `device list` Always quote paths. Use `--config ` when operating a test or non-default profile. @@ -58,8 +60,8 @@ Manage project folders through the background CLI/API; do not open the Syncthing 2. Run `project discover` to show the project roots currently known to the Codex desktop app and the non-archived local tasks whose `cwd` belongs to each root. 3. From an originating project root, run `project add current`. The command creates one independent Syncthing folder entry pointing directly at that project and selects every non-archived Codex task under that root. It never copies the project into another Syncthing directory. Use `--no-tasks` only when project files should be shared without task history. 4. Share with all paired remote devices by default. Use `--share-with ` or `--share-with none` for a narrower set. Add repeated `--ignore ` arguments when generated caches or large local-only paths must stay out. -5. Reject parent/child overlap with an existing Syncthing folder. Keep each selected project and the Sync2 conversation vault as sibling, independent folder entries. -6. Run `sync` on the source so `.sync2/project-catalogs/.json` advertises the portable folder ID, source path, and selected tasks. +5. Reject parent/child overlap with an existing Syncthing folder. Keep each selected project and the Codex Sync conversation vault as sibling, independent folder entries. +6. Run `sync` on the source so `.codex-sync/project-catalogs/.json` advertises the portable folder ID, source path, and selected tasks. 7. On a target device run `project catalogs`, then `project accept --path `. This preserves the source folder ID, adds a source-to-local path map, and registers the local root in Codex via its supported `codex://new?path=...` deep link. Use `--no-register` only when UI registration is intentionally deferred. Do not independently generate a second folder ID. 8. Verify with `project status ` and use `project rescan ` only when the filesystem watcher has not noticed a change. If only task selection needs repair, run `project tasks `; if only paths need repair, run `project map --from --to `. 9. Pair a new machine with `syncthing add-device --device-id --name `. Projects registered with the default `sharePolicy=all` are shared to newly paired remote devices by the next scheduled `sync` run. @@ -69,7 +71,7 @@ Manage project folders through the background CLI/API; do not open the Syncthing - Treat conversation JSONL as append-only. Publish a per-device head and advance the canonical copy only when all heads have a prefix relationship. - Require semantic and desktop-projection completeness in addition to valid JSON: every tool call needs a matching output, every older turn needs `task_complete` or `turn_aborted`, and repaired aborts need Codex-compatible completion fields. -- Never export the active turn or the tool call currently running Sync2. When possible, publish the semantically complete prefix before the active turn as a stable checkpoint; otherwise skip the task until a healthy checkpoint exists. +- Never export the active turn or the tool call currently running Codex Sync. When possible, publish the semantically complete prefix before the active turn as a stable checkpoint; otherwise skip the task until a healthy checkpoint exists. - Quarantine incomplete local snapshots, heads, and canonical files from import or promotion while continuing independent skill/project work. - On divergent conversation history, preserve every head, emit a conflict, and do not invent a merged history. - Use three-way hashes for skills. On simultaneous edits, preserve local and remote versions as conflict copies; never silently choose one. @@ -79,7 +81,7 @@ Manage project folders through the background CLI/API; do not open the Syncthing - Stop if Git pull/rebase reports a conflict. Do not force-push or reset. - Store portable core metadata once and device-specific metadata under `metadata/.json`; never let several devices rewrite one mutable metadata record. - Use `maintenance on` before repair or migration. Normal sync must stop while the marker exists unless a controlled run explicitly passes `--force`. -- Record the last automatic run result under local `~/.sync2/runs`; an installed scheduler is not evidence of success. +- Record the last automatic run result under local `~/.codex-sync/runs`; an installed scheduler is not evidence of success. ## Verification and recovery diff --git a/agents/openai.yaml b/agents/openai.yaml index 6d35ede..cd6b50e 100644 --- a/agents/openai.yaml +++ b/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "Sync2" - short_description: "Safely sync selected Codex chats and personal skills" - default_prompt: "Use $sync2 to sync my selected Codex conversations and personal skills now." + display_name: "Codex Sync" + short_description: "Continue selected Codex projects safely across devices" + default_prompt: "Use Codex Sync via $codex-sync to synchronize my selected Codex tasks and personal skills now." diff --git a/docs/architecture.md b/docs/architecture.md index fcc288b..1e74071 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,7 +4,7 @@ ## Responsibility split -Sync2 deliberately separates transport from conversation correctness. +Codex Sync deliberately separates transport from conversation correctness. ```mermaid flowchart TB @@ -30,8 +30,8 @@ flowchart TB ``` - **Syncthing/shared folders/private Git** move files between machines. -- **Sync2** validates event completeness, reconciles histories, and updates local Codex indexes. -- **Codex** remains the only writer of live task activity; Sync2 never exports the active turn. +- **Codex Sync** validates event completeness, reconciles histories, and updates local Codex indexes. +- **Codex** remains the only writer of live task activity; Codex Sync never exports the active turn. ## Conversation invariants diff --git a/docs/architecture.zh-CN.md b/docs/architecture.zh-CN.md index 80f6ea6..980ce2b 100644 --- a/docs/architecture.zh-CN.md +++ b/docs/architecture.zh-CN.md @@ -4,7 +4,7 @@ ## 职责拆分 -Sync2 明确区分“文件传输”和“会话正确性”。 +Codex Sync 明确区分“文件传输”和“会话正确性”。 ```mermaid flowchart TB @@ -30,8 +30,8 @@ flowchart TB ``` - **Syncthing、共享文件夹或私有 Git** 负责在机器间移动文件。 -- **Sync2** 负责校验事件完整性、协调历史并更新本地 Codex 索引。 -- **Codex** 仍是实时任务活动的唯一写入方;Sync2 永远不导出活动回合。 +- **Codex Sync** 负责校验事件完整性、协调历史并更新本地 Codex 索引。 +- **Codex** 仍是实时任务活动的唯一写入方;Codex Sync 永远不导出活动回合。 ## 会话约束 diff --git a/docs/demo.md b/docs/demo.md index ef0a222..6c8b963 100644 --- a/docs/demo.md +++ b/docs/demo.md @@ -5,14 +5,14 @@ This demonstration uses synthetic paths and an empty test task. ```text -$ sync2 conversation select current +$ codexsync conversation select current Selected: Example task -$ sync2 sync +$ codexsync sync conversationsPushed: 1 conversationConflicts: 0 -$ sync2 doctor +$ codexsync doctor ok: true ``` diff --git a/docs/demo.zh-CN.md b/docs/demo.zh-CN.md index c8cfe78..2b60680 100644 --- a/docs/demo.zh-CN.md +++ b/docs/demo.zh-CN.md @@ -5,14 +5,14 @@ 此演示使用合成路径与空白测试任务。 ```text -$ sync2 conversation select current +$ codexsync conversation select current Selected: Example task -$ sync2 sync +$ codexsync sync conversationsPushed: 1 conversationConflicts: 0 -$ sync2 doctor +$ codexsync doctor ok: true ``` diff --git a/package.json b/package.json index 625bf7d..0eb4a1c 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,17 @@ { - "name": "codex-sync2", - "version": "0.2.0", + "name": "codex-sync", + "version": "0.3.0", "private": true, - "description": "Safely synchronize selected local Codex conversations and personal skills across Windows and macOS.", + "description": "Codex Sync safely carries selected local Codex projects, task histories, and personal skills across Windows and macOS.", "type": "module", + "bin": { + "codexsync": "scripts/codexsync.mjs" + }, "engines": { "node": ">=22" }, "scripts": { - "test": "node --no-warnings scripts/test-sync2.mjs", + "test": "node --no-warnings scripts/test-codexsync.mjs", "privacy:scan": "node scripts/check-repo-hygiene.mjs", "docs:check": "node scripts/check-docs.mjs", "check": "npm run privacy:scan && npm run docs:check && npm test" diff --git a/references/deployment.md b/references/deployment.md index d739b4d..5ededec 100644 --- a/references/deployment.md +++ b/references/deployment.md @@ -1,4 +1,4 @@ -# Sync2 deployment and cold-start guide +# Codex Sync deployment and cold-start guide ## Contents @@ -20,15 +20,28 @@ Meet these conditions before bootstrap: - Install Node.js 22 or newer and make `node` available in the interactive shell. -- Launch Codex at least once when possible so `~/.codex` and `state_5.sqlite` exist. Sync2 can still copy a session without the database, but Codex may need one restart to backfill its sidebar index. +- Launch Codex at least once when possible so `~/.codex` and `state_5.sqlite` exist. Codex Sync can still copy a session without the database, but Codex may need one restart to backfill its sidebar index. - Create or accept one shared vault folder. Different local paths are normal; the Syncthing folder ID must be the same. -- Wait until the vault reports fully synchronized before running a new-device bootstrap. The file `skills/codex/sync2/SKILL.md` must exist locally. +- Wait until the vault reports fully synchronized before running a new-device bootstrap. The file `skills/codex/codex-sync/SKILL.md` must exist locally. - Give every machine a unique stable device name such as `win-a`, `win-b`, or `macos`. - Keep system clocks synchronized because selection events use last-writer-wins timestamps. - Use a private Git repository or trusted encrypted storage. Vault content is plaintext at rest. - Keep the vault mounted and writable whenever an automatic task runs. -Never copy another machine's `~/.sync2/config.json`, Codex SQLite files, `auth.json`, or whole `~/.codex` directory. +Never copy another machine's `~/.codex-sync/config.json`, Codex SQLite files, `auth.json`, or whole `~/.codex` directory. + +### v0.3 coordinated identity migration + +Version 0.3 changes the Skill directory, local state directory, CLI entry point, and scheduler/service identifiers. It is not a rolling upgrade. + +1. Pause the shared-vault transport on every device. +2. Stop and remove the scheduler installed by the earlier release on every device. Installing the v0.3 scheduler does not replace a differently named earlier scheduler. +3. Back up the shared vault and every device's local synchronization state. +4. Place the v0.3 Skill at `skills/codex/codex-sync` in the vault and bootstrap it to `~/.codex/skills/codex-sync` on each device. +5. Initialize each device's new `~/.codex-sync/config.json`, keeping the same vault but a unique device ID. Re-select required tasks and verify their existing canonical histories before enabling writes. +6. Require `doctor`, the acceptance test, protocol revision 3, and matching script hashes on every expected device before resuming transport and automatic scheduling. + +Keep the transport paused if any earlier client or scheduler may still write. Retain the backups until every device is healthy. ## 2. Cold-start matrix @@ -37,7 +50,7 @@ Never copy another machine's `~/.sync2/config.json`, Codex SQLite files, `auth.j | Empty vault, first device | Install the skill locally, then run `init` | Personal skills push into the new vault; no tasks move until selected. | | Populated vault, completely new device | Run the vault bootstrap script | Skill installs locally, config initializes, selected tasks and skills pull, scheduler optionally installs. | | Populated vault, Codex never launched | Prefer launching Codex once, then bootstrap | Session files import; if no state DB existed, restart Codex once for indexing. | -| Existing Sync2 config with a wrong or old vault path | Run the current bootstrap or `vault use` | Config changes to the supplied local path without copying another device's config. | +| Existing Codex Sync config with a wrong or old vault path | Run the current bootstrap or `vault use` | Config changes to the supplied local path without copying another device's config. | | Reinstalled OS with an existing synchronized vault | Treat as a new device; choose a new unique device ID | Vault content pulls; old device report remains historical until manually retired. | | Vault path is offline, unmapped, or partially synchronized | Stop and restore/mount/synchronize it first | Do not initialize against an empty placeholder directory. | | Existing local skills differ from the vault | Back up if desired, then bootstrap | Three-way state is new; differing same-path files are preserved as conflicts, not overwritten. | @@ -45,19 +58,19 @@ Never copy another machine's `~/.sync2/config.json`, Codex SQLite files, `auth.j ## 3. First device and empty vault -Install `sync2` into the first device's Codex skill root, then run: +Install **Codex Sync** into the first device's Codex Skill root, then run its `codexsync` entry point: ```text -node /scripts/sync2.mjs init --vault "" --transport folder --device -node /scripts/sync2.mjs skills discover -node /scripts/sync2.mjs sync --dry-run -node /scripts/sync2.mjs sync -node /scripts/sync2.mjs daemon install --minutes 5 -node /scripts/sync2.mjs device report -node /scripts/sync2.mjs doctor +node /scripts/codexsync.mjs init --vault "" --transport folder --device +node /scripts/codexsync.mjs skills discover +node /scripts/codexsync.mjs sync --dry-run +node /scripts/codexsync.mjs sync +node /scripts/codexsync.mjs daemon install --minutes 5 +node /scripts/codexsync.mjs device report +node /scripts/codexsync.mjs doctor ``` -Select a harmless task with `$sync2 current`, run another sync, and confirm that `vault/conversations//canonical.jsonl` exists. +Select a harmless task with `$codex-sync current`, run another sync, and confirm that `vault/conversations//canonical.jsonl` exists. For Syncthing, register the vault once as an independent folder and share that same folder ID with every target device. Do not nest separately registered projects inside the vault. @@ -69,15 +82,15 @@ Confirm all of the following: ```powershell node --version -Test-Path "\skills\codex\sync2\SKILL.md" -Test-Path "\skills\codex\sync2\scripts\bootstrap-sync2.ps1" +Test-Path "\skills\codex\codex-sync\SKILL.md" +Test-Path "\skills\codex\codex-sync\scripts\bootstrap-codex-sync.ps1" ``` Require Node 22+ and two `True` results. If the path is uncertain, search the actual Syncthing folder root rather than guessing: ```powershell Get-ChildItem -LiteralPath "" ` - -Filter "bootstrap-sync2.ps1" ` + -Filter "bootstrap-codex-sync.ps1" ` -File -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName ``` @@ -86,7 +99,7 @@ Get-ChildItem -LiteralPath "" ` ```powershell $vault = "" -$bootstrap = Join-Path $vault "skills\codex\sync2\scripts\bootstrap-sync2.ps1" +$bootstrap = Join-Path $vault "skills\codex\codex-sync\scripts\bootstrap-codex-sync.ps1" powershell -ExecutionPolicy Bypass -File $bootstrap ` -Vault $vault ` @@ -100,7 +113,7 @@ The script installs the skill, initializes a missing config or safely points an For example: ```powershell -$vault = "D:\Sync2Vault" +$vault = "D:\CodexSyncVault" ``` Use the exact local path shown by Syncthing on that device; local drive letters may differ. @@ -108,10 +121,10 @@ Use the exact local path shown by Syncthing on that device; local drive letters ### Windows post-check ```powershell -Test-Path "$HOME\.sync2\config.json" -schtasks.exe /Query /TN "Sync2-AutoSync" /FO LIST /V -node --no-warnings "$HOME\.codex\skills\sync2\scripts\sync2.mjs" status --json -node --no-warnings "$HOME\.codex\skills\sync2\scripts\sync2.mjs" doctor --json +Test-Path "$HOME\.codex-sync\config.json" +schtasks.exe /Query /TN "CodexSync-AutoSync" /FO LIST /V +node --no-warnings "$HOME\.codex\skills\codex-sync\scripts\codexsync.mjs" status --json +node --no-warnings "$HOME\.codex\skills\codex-sync\scripts\codexsync.mjs" doctor --json ``` If Task Scheduler cannot see a mapped or removable drive, use a stable local Syncthing path or ensure the drive mounts before login. Reinstall the daemon after changing the Node executable location. @@ -122,14 +135,14 @@ If Task Scheduler cannot see a mapped or removable drive, use a stable local Syn ```sh node --version -test -f "/skills/codex/sync2/SKILL.md" -test -f "/skills/codex/sync2/scripts/bootstrap-sync2.sh" +test -f "/skills/codex/codex-sync/SKILL.md" +test -f "/skills/codex/codex-sync/scripts/bootstrap-codex-sync.sh" ``` ### Bootstrap ```sh -sh "/skills/codex/sync2/scripts/bootstrap-sync2.sh" \ +sh "/skills/codex/codex-sync/scripts/bootstrap-codex-sync.sh" \ "" macos folder --daemon ``` @@ -138,17 +151,17 @@ The bootstrap now handles both a missing config and an existing config whose vau Register Hermes after the first bootstrap when that root exists: ```sh -node --no-warnings "$HOME/.codex/skills/sync2/scripts/sync2.mjs" \ +node --no-warnings "$HOME/.codex/skills/codex-sync/scripts/codexsync.mjs" \ skills add --name hermes --path "$HOME/.hermes/skills" -node --no-warnings "$HOME/.codex/skills/sync2/scripts/sync2.mjs" sync -node --no-warnings "$HOME/.codex/skills/sync2/scripts/sync2.mjs" device report +node --no-warnings "$HOME/.codex/skills/codex-sync/scripts/codexsync.mjs" sync +node --no-warnings "$HOME/.codex/skills/codex-sync/scripts/codexsync.mjs" device report ``` Verify the LaunchAgent: ```sh -launchctl print "gui/$(id -u)/com.openai.sync2" -node --no-warnings "$HOME/.codex/skills/sync2/scripts/sync2.mjs" doctor --json +launchctl print "gui/$(id -u)/com.toussaintknight.codexsync" +node --no-warnings "$HOME/.codex/skills/codex-sync/scripts/codexsync.mjs" doctor --json ``` If the vault is on an external volume, keep the volume mounted before the LaunchAgent runs. macOS privacy controls may require granting the shell or Node access to protected folders. @@ -158,13 +171,13 @@ If the vault is on an external volume, keep the volume mounted before the Launch Inspect the local config rather than deleting it: ```text -node /scripts/sync2.mjs status --json +node /scripts/codexsync.mjs status --json ``` Change only the local vault path: ```text -node /scripts/sync2.mjs vault use --vault "" --transport folder +node /scripts/codexsync.mjs vault use --vault "" --transport folder ``` The automatic task reads the same config path and follows the new vault. Do not use `init --force` as routine recovery; it can replace selections, sources, device identity, and project registrations. @@ -176,14 +189,14 @@ When the old and new vault directories are not the same synchronized dataset, mo Use only a private repository: ```text -node /scripts/sync2.mjs init \ +node /scripts/codexsync.mjs init \ --vault "" \ --transport git \ --repo "" \ --device ``` -Configure Git user identity and an upstream before expecting automatic pushes. Sync2 stops on pull/rebase, commit, or push failures and never force-pushes or resets. +Configure Git user identity and an upstream before expecting automatic pushes. Codex Sync stops on pull/rebase, commit, or push failures and never force-pushes or resets. Do not combine a Git working tree and another bidirectional file synchronizer on the same vault unless the operational conflicts are understood. @@ -191,10 +204,10 @@ Do not combine a Git working tree and another bidirectional file synchronizer on Do not call `git-remote-https.exe` directly. It is an internal helper launched by `git` for HTTPS remotes. -First confirm that Sync2 does not need Git: +First confirm that Codex Sync does not need Git: ```powershell -Get-Content -Raw "$HOME\.sync2\config.json" | ConvertFrom-Json | +Get-Content -Raw "$HOME\.codex-sync\config.json" | ConvertFrom-Json | Select-Object deviceId, transport, vault ``` @@ -212,7 +225,7 @@ Back up user Git configuration before repair: ```powershell if (Test-Path "$HOME\.gitconfig") { - Copy-Item "$HOME\.gitconfig" "$HOME\.gitconfig.sync2-backup-$(Get-Date -Format yyyyMMddHHmmss)" + Copy-Item "$HOME\.gitconfig" "$HOME\.gitconfig.codex-sync-backup-$(Get-Date -Format yyyyMMddHHmmss)" } ``` @@ -228,7 +241,7 @@ The bundled equivalent performs diagnosis, backup, repair, and the isolated test ```powershell powershell -ExecutionPolicy Bypass -File ` - "\skills\codex\sync2\scripts\repair-git-https.ps1" ` + "\skills\codex\codex-sync\scripts\repair-git-https.ps1" ` -Repair -TestHttps ``` @@ -260,18 +273,18 @@ Get-WinEvent -FilterHashtable @{ Format-List ``` -Keep Sync2 on folder transport regardless; Git repair is required only for other Git workflows or a future deliberate Git transport migration. +Keep Codex Sync on folder transport regardless; Git repair is required only for other Git workflows or a future deliberate Git transport migration. ## 8. Automatic scheduling Install or refresh after changing Node, the skill location, the config location, or the desired interval: ```text -node /scripts/sync2.mjs daemon install --minutes -node /scripts/sync2.mjs daemon status +node /scripts/codexsync.mjs daemon install --minutes +node /scripts/codexsync.mjs daemon status ``` -Windows uses Task Scheduler with an interactive user token. macOS uses `~/Library/LaunchAgents/com.openai.sync2.plist`. Multiple overlapping runs are suppressed by a local operation lock. +Windows uses Task Scheduler with an interactive user token. macOS uses `~/Library/LaunchAgents/com.toussaintknight.codexsync.plist`. Multiple overlapping runs are suppressed by a local operation lock. An installed scheduler is not proof of a successful run. Check `status.lastRun.ok`, the OS scheduler result, and a later propagated device report. A nonzero OS result with a missing/failed local run record is unhealthy. @@ -280,7 +293,7 @@ An installed scheduler is not proof of a successful run. Check `status.lastRun.o Complete every item: 1. Run `doctor` on every device and require `ok: true` for core vault health. -2. Run `device report` on every device, then `device list` elsewhere; require all expected IDs, the same current protocol revision, the same Sync2 script SHA-256, and a successful last-run record. +2. Run `device report` on every device, then `device list` elsewhere; require all expected IDs, the same current protocol revision, the same Codex Sync script SHA-256, and a successful last-run record. 3. Select one harmless task on device A and sync after its final answer. On device B, require `rolloutExists: true`, `indexedInStateDb: true`, and `semanticOk: true`. 4. Compare SHA-256 hashes of every device head and `canonical.jsonl`; require equality before concurrent continuation tests. 5. Continue the task on one device only, sync twice across the fleet, and verify the appended record arrives. @@ -299,23 +312,23 @@ Complete every item: | Bootstrap file is missing | Wrong local vault path or incomplete Syncthing download | Read Syncthing's actual Folder Path, wait for 100%, then search for the script. | | Config is missing | Normal cold start | Run the bootstrap; do not copy another device's config. | | Config points at an old path | Drive letter or mount point changed | Rerun the current bootstrap or use `vault use`. | -| `Syncthing executable was not found` but doctor is otherwise OK | The vault is already managed externally, but Sync2 cannot call Syncthing CLI | Core conversation/skill sync still works. Configure the executable only for project/device automation. | +| `Syncthing executable was not found` but doctor is otherwise OK | The vault is already managed externally, but Codex Sync cannot call Syncthing CLI | Core conversation/skill sync still works. Configure the executable only for project/device automation. | | First sync pulls but does not push a conversation | New device had no local rollout at export time | Run sync again or wait for the scheduler; the second run publishes its head. | | Task is on disk but absent from sidebar | Codex was open during import, state DB was absent, or UI has not refreshed | Run doctor, verify `indexedInStateDb`, then restart Codex once. | | Scheduler is installed but updates stop | Node moved, vault was unmounted, user was logged out, or task last result is nonzero | Restore prerequisites and rerun `daemon install`. | | Conversation conflict appears | Two devices continued the same task from the same base while disconnected | Stop editing, inspect every head, then resolve explicitly from one device. | | Task reopens with missing progress or stays `inProgress` | A tool call lost its output event, an older turn lacks a close marker, or only commentary was persisted before interruption | Enter maintenance, back up, run `conversation audit`, then `conversation repair`; require zero persistent dangling calls and stale open turns. | -| `Path escapes root` contains `\\?\C:\...` | Windows returned an extended-length path | Upgrade Sync2 to protocol revision 2; do not rewrite the whole Codex home or database. | +| `Path escapes root` contains `\\?\C:\...` | Windows returned an extended-length path | Upgrade Codex Sync to protocol revision 2; do not rewrite the whole Codex home or database. | | Sync reports an active task as skipped | Its current turn has not emitted a final answer and the prefix before it is not independently healthy | This is expected protection. Repair any older semantic gap or let the turn finish; healthy long-running Goals publish completed-prefix checkpoints. | | Metadata `.sync-conflict-*` files appear repeatedly | Multiple devices are rewriting legacy shared `metadata.json` | Upgrade all clients; keep stable core metadata and write device rows under `metadata/.json`, then archive old conflicts after backup. | | Skill conflict appears | Both sides changed the same relative file | Compare local, vault, and conflict copies; choose intentionally and sync again. | -| Files deleted locally reappear | Deletion propagation is intentionally disabled | Remove unwanted data deliberately from both sides after backup; do not expect Sync2 to propagate deletion. | -| Attachments or project files are missing | Sync2 does not copy Codex attachments or arbitrary project content | Register the project separately or transfer the files through approved storage. | +| Files deleted locally reappear | Deletion propagation is intentionally disabled | Remove unwanted data deliberately from both sides after backup; do not expect Codex Sync to propagate deletion. | +| Attachments or project files are missing | Codex Sync does not copy Codex attachments or arbitrary project content | Register the project separately or transfer the files through approved storage. | | Same skill name differs only by case | Windows and default macOS filesystems are case-insensitive | Rename collections/files to unique case-insensitive names before sync. | | Selection unexpectedly flips | Device clocks differ or a later unselect/select event won | Synchronize clocks and issue the desired selection again. | | Git sync stops | Pull/rebase/push failure or missing upstream | Resolve Git normally; never force-reset the vault. | -| `git-remote-https.exe` references null memory | Damaged/mixed Git DLLs, injected security software, or HTTPS/proxy configuration | Keep Sync2 on folder transport, repair Git for Windows in place, then run the isolated `schannel`/no-proxy test above. | -| Syncthing creates `.sync-conflict-*` files | Transport-level concurrent edits bypassed Sync2's serialized view | Stop automatic sync, preserve both copies, reconcile manually, then resume. | +| `git-remote-https.exe` references null memory | Damaged/mixed Git DLLs, injected security software, or HTTPS/proxy configuration | Keep Codex Sync on folder transport, repair Git for Windows in place, then run the isolated `schannel`/no-proxy test above. | +| Syncthing creates `.sync-conflict-*` files | Transport-level concurrent edits bypassed Codex Sync's serialized view | Stop automatic sync, preserve both copies, reconcile manually, then resume. | | Project registration is rejected | It overlaps a parent or child Syncthing folder | Use sibling independent folder roots; do not nest registrations. | ## 11. Interrupted-rollout fleet recovery @@ -324,10 +337,10 @@ Use this order when other machines are asleep or powered off: 1. On the active machine, disable/stop the scheduler, pause the Syncthing vault folder, and enable `maintenance on`. 2. Back up the local rollout, `state_5.sqlite` through SQLite backup, `session_index.jsonl`, the desktop catalog database, local config, scheduler spec, skill, and vault task directory. -3. Upgrade Sync2 and pass its full tests. Run `conversation repair --title ` and require `semanticOk: true`, zero persistent dangling calls, zero stale open turns, and consistent indexes. +3. Upgrade Codex Sync and pass its full tests. Run `conversation repair --title ` and require `semanticOk: true`, zero persistent dangling calls, zero stale open turns, and consistent indexes. 4. Run `sync --dry-run`. Active local history and invalid old heads must be skipped, not imported. Use one controlled `sync --force` only to publish the upgraded skill/device report while maintenance is active. 5. Let the active Codex turn finish. The next healthy run publishes the stable full head and replaces the quarantined canonical. -6. Before reconnecting an old device, disable its Sync2 scheduler. Prefer booting Win B offline long enough to disable Task Scheduler; on macOS unload the LaunchAgent or pause its Syncthing folder before allowing exchange. +6. Before reconnecting an old device, disable its Codex Sync scheduler. Prefer booting Win B offline long enough to disable Task Scheduler; on macOS unload the LaunchAgent or pause its Syncthing folder before allowing exchange. 7. Resume transport, wait for the upgraded skill to arrive, run `conversation audit`, then reinstall the daemon from the upgraded skill. Do not continue the selected task until its local canonical is semantically healthy. 8. After all devices report the new protocol and healthy history, remove maintenance, resume all vault folders, enable schedulers, and complete the acceptance test. @@ -335,25 +348,25 @@ Older clients do not understand the maintenance marker. Invalid old heads are re ## 12. Recovery and removal -Before recovery, preserve the vault, `~/.sync2`, and any reported conflict copies. +Before recovery, preserve the vault, `~/.codex-sync`, and any reported conflict copies. Remove only automatic scheduling: ```text -node /scripts/sync2.mjs daemon uninstall +node /scripts/codexsync.mjs daemon uninstall ``` Unregister a project without deleting local files: ```text -node /scripts/sync2.mjs project remove +node /scripts/codexsync.mjs project remove ``` Unselect a task without deleting historical vault data: ```text -node /scripts/sync2.mjs conversation unselect -node /scripts/sync2.mjs sync +node /scripts/codexsync.mjs conversation unselect +node /scripts/codexsync.mjs sync ``` -Keep exported data until every device is healthy and a backup exists. Sync2 intentionally avoids destructive cleanup. +Keep exported data until every device is healthy and a backup exists. Codex Sync intentionally avoids destructive cleanup. diff --git a/references/protocol.md b/references/protocol.md index c6862fd..d6b785e 100644 --- a/references/protocol.md +++ b/references/protocol.md @@ -1,4 +1,4 @@ -# Sync2 protocol and edge cases +# Codex Sync protocol and edge cases ## Contents @@ -17,7 +17,7 @@ ```text vault/ - .sync2/ + .codex-sync/ vault.json maintenance.json selections/.json @@ -33,7 +33,7 @@ vault/ conflicts/skills//... ``` -Local-only state lives under `~/.sync2`: +Local-only state lives under `~/.codex-sync`: - `config.json`: device ID, local vault path, Codex home, sources, projects, and schedule settings; - `state/`: last synchronized skill hashes for three-way comparison; @@ -64,7 +64,7 @@ The imported `cwd` can reference a path that does not exist on the destination. ## 3. Selection events -Store per-device selection events in `.sync2/selections`. Merge by `updatedAt`, with device ID as a deterministic tie-breaker. Therefore: +Store per-device selection events in `.codex-sync/selections`. Merge by `updatedAt`, with device ID as a deterministic tie-breaker. Therefore: - keep device clocks synchronized; - use a unique device ID per installation; @@ -80,7 +80,7 @@ Compare local hash, vault hash, and the last successful local base hash for each - one side is absent -> restore/copy the existing side because deletion is non-propagating; - both sides changed differently -> preserve originals and write conflict copies. -Skip symlinks because targets and permissions are not portable. Exclude `.system`, plugin caches, `.git`, `node_modules`, `__pycache__`, and generated Sync2 conflict copies. +Skip symlinks because targets and permissions are not portable. Exclude `.system`, plugin caches, `.git`, `node_modules`, `__pycache__`, and generated Codex Sync conflict copies. Keep ecosystems in separate collections. `skills/hermes/foo` can be centrally stored without installing it into Codex on every device. @@ -88,7 +88,7 @@ Case-only file or directory names are unsafe across Windows and default macOS fi ## 5. Device reports and cold start -`device report` publishes content-stable evidence about platform, Node version, protocol revision, Sync2 script SHA-256, vault, selected-task local/vault health, skill counts, scheduler state, and last-run result. A normal sync embeds that same run's result in the report before transport commit; if the transport commit fails, it rewrites the local/vault report as failed instead of leaving a false success. `device list` reads all propagated reports. Do not declare a fleet upgraded until every expected device reports the same current protocol revision and script hash. +`device report` publishes content-stable evidence about platform, Node version, protocol revision, Codex Sync script SHA-256, vault, selected-task local/vault health, skill counts, scheduler state, and last-run result. A normal sync embeds that same run's result in the report before transport commit; if the transport commit fails, it rewrites the local/vault report as failed instead of leaving a false success. `device list` reads all propagated reports. Do not declare a fleet upgraded until every expected device reports the same current protocol revision and script hash. On a new device, the first sync usually performs this order: @@ -104,29 +104,29 @@ A report proves the explicit bootstrap reached the reporting step. To prove auto ## 6. Syncthing project folders -Conversation/skill vault synchronization works whenever an external tool keeps the vault directory synchronized; Sync2 does not require the Syncthing executable for this core path. +Conversation/skill vault synchronization works whenever an external tool keeps the vault directory synchronized; Codex Sync does not require the Syncthing executable for this core path. The executable is required for `project`, `syncthing add-device`, rescan, and automatic project sharing. Store its device-specific executable path only in local config. Register each project as a sibling independent Syncthing folder pointing at its real path. Reject parent/child overlaps. Generate the folder ID once on the originating device; other devices accept the advertised ID and choose local paths. -Project selection is also a conversation-selection boundary: `project add` selects each non-archived local thread whose `cwd` is the project root or a descendant. Per-device files under `.sync2/project-catalogs/` advertise the portable folder ID, local source path, Syncthing device ID, and task list without creating a shared mutable manifest. A target acceptance records an explicit source-to-local path map before importing tasks and registers the local root with Codex. Path matching is separator-aware and boundary-aware so `F:\Projects\MUON` maps safely to `/Volumes/Lab/MUON` without also matching a sibling such as `MUON-old`. +Project selection is also a conversation-selection boundary: `project add` selects each non-archived local thread whose `cwd` is the project root or a descendant. Per-device files under `.codex-sync/project-catalogs/` advertise the portable folder ID, local source path, Syncthing device ID, and task list without creating a shared mutable manifest. A target acceptance records an explicit source-to-local path map before importing tasks and registers the local root with Codex. Path matching is separator-aware and boundary-aware so `F:\Projects\MUON` maps safely to `/Volumes/Lab/MUON` without also matching a sibling such as `MUON-old`. Transport-level `.sync-conflict-*` files indicate Syncthing observed concurrent changes outside a single reconciled view. Preserve them and resolve manually before resuming automatic sync. ## 7. Git transport -Before reconciliation, run `git pull --rebase --autostash` when an upstream exists. After reconciliation, stage only Sync2 vault paths, commit when changed, and push only with an upstream. +Before reconciliation, run `git pull --rebase --autostash` when an upstream exists. After reconciliation, stage only Codex Sync vault paths, commit when changed, and push only with an upstream. Stop on any Git error. Never reset, force-push, or auto-resolve a Git conflict. Do not expose conversation data through a public repository. ## 8. Scheduler and locking -Use Task Scheduler on Windows and a per-user LaunchAgent on macOS. Store the exact Node executable, skill script, and config path in the scheduler definition. Record every non-dry automatic result under local `~/.sync2/runs/-last.json`. +Use Task Scheduler on Windows and a per-user LaunchAgent on macOS. Store the exact Node executable, skill script, and config path in the scheduler definition. Record every non-dry automatic result under local `~/.codex-sync/runs/-last.json`. Use a local operation lock to suppress overlapping runs on one device. The lock cannot serialize disconnected devices; protocol-level heads and three-way hashes provide cross-device safety. -Create `.sync2/maintenance.json` before repairs, protocol migrations, or fleet upgrades. New clients refuse normal writes while it exists; use `--force` only for the one controlled maintenance write. Older clients do not understand this marker, so disable their schedulers before reconnecting them. +Create `.codex-sync/maintenance.json` before repairs, protocol migrations, or fleet upgrades. New clients refuse normal writes while it exists; use `--force` only for the one controlled maintenance write. Older clients do not understand this marker, so disable their schedulers before reconnecting them. Report local rollout health separately from vault health. During maintenance, show unsafe heads without failing the local repair gate. Outside maintenance, make `doctor` fail until the canonical and every device head are stable; this prevents a healthy Win A from masking stale corrupted heads elsewhere. @@ -156,12 +156,12 @@ Conversation bodies and skill contents remain plaintext in the vault. Use truste 4. On each affected device run: ```text -node /scripts/sync2.mjs conversation resolve --from-device +node /scripts/codexsync.mjs conversation resolve --from-device ``` 5. Sync again on each device and require `conflict.json` to disappear. -Resolution archives nonchosen vault heads and backs up a differing local history under `~/.sync2/conflicts/conversations` before replacement. +Resolution archives nonchosen vault heads and backs up a differing local history under `~/.codex-sync/conflicts/conversations` before replacement. ### Interrupted tool call or stale turn @@ -182,4 +182,4 @@ Resolution archives nonchosen vault heads and backs up a differing local history ### Stale operation lock -Sync2 considers a local operation lock stale after its safety interval. Before removing a lock manually, confirm no scheduler or interactive Sync2 process is still running. +Codex Sync considers a local operation lock stale after its safety interval. Before removing a lock manually, confirm no scheduler or interactive Codex Sync process is still running. diff --git a/references/usage.md b/references/usage.md index 8edca81..8f95339 100644 --- a/references/usage.md +++ b/references/usage.md @@ -1,45 +1,45 @@ -# Sync2 usage guide +# Codex Sync usage guide ## Example three-device topology | Device | Device ID | Local vault | Personal skill roots | Schedule | | --- | --- | --- | --- | --- | -| Windows desktop | `win-main` | `D:\Sync2Vault` | `~/.codex/skills` | every 5 minutes | -| Windows laptop | `win-travel` | `E:\Sync2Vault` | `~/.codex/skills` | every 15 minutes | -| macOS | `mac-main` | `~/Sync2Vault` | `~/.codex/skills`, `~/.hermes/skills` | LaunchAgent installed | +| Windows desktop | `win-main` | `D:\CodexSyncVault` | `~/.codex/skills` | every 5 minutes | +| Windows laptop | `win-travel` | `E:\CodexSyncVault` | `~/.codex/skills` | every 15 minutes | +| macOS | `mac-main` | `~/CodexSyncVault` | `~/.codex/skills`, `~/.hermes/skills` | LaunchAgent installed | -Different local paths are expected. In this example, all paths represent the same Syncthing folder ID, `sync2-vault`. +Different local paths are expected. In this example, all paths represent the same Syncthing folder ID, `codex-sync-vault`. ## Invocation -Use `$sync2` for native Codex skill invocation. `/sync2` is a conversational alias interpreted by the skill. +Use `$codex-sync` for native Codex skill invocation. `/codex-sync` is a conversational alias interpreted by the skill. | Request | Result | | --- | --- | -| `$sync2 current` | Select the current Codex task, then synchronize it. | -| `$sync2` | Synchronize all selected tasks and configured skill roots now. | -| `$sync2 status` | Show local configuration, selected tasks, sources, conflicts, and scheduler state. | -| `$sync2 devices` | Show reports received from every bootstrapped device. | -| `$sync2 skills` | Discover common personal skill roots and synchronize them. | -| `$sync2 doctor` | Check Node, Codex storage, vault, SQLite index, Syncthing access, and forbidden files. | -| `$sync2 audit ` | Check JSONL syntax, tool-call/output pairs, turn closure, and local index agreement. | -| `$sync2 repair ` | Back up and repair interrupted tool events and stale older turns without inventing output. | -| `$sync2 auto` | Install or refresh the local automatic task. | -| `$sync2 project discover` | List Codex desktop projects and the local tasks grouped under each root. | -| `$sync2 project current` | Register the current project and select every non-archived task under it. | -| `$sync2 projects` | List project folder IDs and their real local paths. | +| `$codex-sync current` | Select the current Codex task, then synchronize it. | +| `$codex-sync` | Synchronize all selected tasks and configured skill roots now. | +| `$codex-sync status` | Show local configuration, selected tasks, sources, conflicts, and scheduler state. | +| `$codex-sync devices` | Show reports received from every bootstrapped device. | +| `$codex-sync skills` | Discover common personal skill roots and synchronize them. | +| `$codex-sync doctor` | Check Node, Codex storage, vault, SQLite index, Syncthing access, and forbidden files. | +| `$codex-sync audit ` | Check JSONL syntax, tool-call/output pairs, turn closure, and local index agreement. | +| `$codex-sync repair ` | Back up and repair interrupted tool events and stale older turns without inventing output. | +| `$codex-sync auto` | Install or refresh the local automatic task. | +| `$codex-sync project discover` | List Codex desktop projects and the local tasks grouped under each root. | +| `$codex-sync project current` | Register the current project and select every non-archived task under it. | +| `$codex-sync projects` | List project folder IDs and their real local paths. | ## Daily workflows ### Select a task once -From the task to preserve, invoke `$sync2 current`. Selection propagates to the other devices. Only explicitly selected tasks are synchronized. +From the task to preserve, invoke `$codex-sync current`. Selection propagates to the other devices. Only explicitly selected tasks are synchronized. To select by title or ID from another task, run: ```text -node /scripts/sync2.mjs conversation select "" -node /scripts/sync2.mjs sync +node /scripts/codexsync.mjs conversation select "" +node /scripts/codexsync.mjs sync ``` If a title matches more than one task, use the full thread ID. @@ -53,8 +53,8 @@ Run `conversation unselect `, then `sync`. Existing vault data is r Configured roots synchronize automatically. Add a nonstandard root once: ```text -node /scripts/sync2.mjs skills add --name --path "" --exclude .system -node /scripts/sync2.mjs sync +node /scripts/codexsync.mjs skills add --name --path "" --exclude .system +node /scripts/codexsync.mjs sync ``` Use stable collection names across devices. Keep incompatible agent ecosystems in separate collections such as `codex` and `hermes`; central storage does not require installing every collection into every agent. @@ -64,13 +64,13 @@ Use stable collection names across devices. Keep incompatible agent ecosystems i First inspect the roots and task grouping known to Codex: ```text -node /scripts/sync2.mjs project discover +node /scripts/codexsync.mjs project discover ``` Run this once on the originating device from the actual project root: ```text -$sync2 project current +$codex-sync project current ``` `project add` also selects every non-archived task whose `cwd` belongs to the project root. Run `sync` so the portable project catalog and those stable task histories reach the vault. @@ -78,9 +78,9 @@ $sync2 project current On another device, inspect and accept the advertised folder ID: ```text -node /scripts/sync2.mjs project catalogs -node /scripts/sync2.mjs project accept --path "" -node /scripts/sync2.mjs sync +node /scripts/codexsync.mjs project catalogs +node /scripts/codexsync.mjs project accept --path "" +node /scripts/codexsync.mjs sync ``` `project accept` preserves the source Syncthing folder ID, maps the source path to the target path for imported task metadata, and registers the local root in the Codex desktop Projects list. Do not independently create a second folder ID for the same project. @@ -90,14 +90,14 @@ node /scripts/sync2.mjs sync Run: ```text -$sync2 doctor -$sync2 status -$sync2 devices +$codex-sync doctor +$codex-sync status +$codex-sync devices ``` Healthy output has `ok: true`, no conversation or skill conflicts, `rolloutExists: true`, `indexedInStateDb: true`, and a device report for each expected device. Require the same current `protocolRevision` and `scriptSha256` across the fleet, plus `lastRunOk: true` after automatic scheduling is restored. -For an actively running task, `stable: false` is expected until its final answer. Require `semanticOk: true`, zero persistent dangling calls, and zero stale open turns. Sync2 publishes only the independently stable prefix before the active turn; if no healthy prefix exists, it skips the task while continuing skills and projects. +For an actively running task, `stable: false` is expected until its final answer. Require `semanticOk: true`, zero persistent dangling calls, and zero stale open turns. Codex Sync publishes only the independently stable prefix before the active turn; if no healthy prefix exists, it skips the task while continuing skills and projects. ## Interpreting sync results @@ -114,16 +114,16 @@ A new device commonly reports `conversationsPushed: 0` and `conversationsPulled: ## Manual CLI locations -- Windows: `%USERPROFILE%\.codex\skills\sync2\scripts\sync2.mjs` -- macOS: `~/.codex/skills/sync2/scripts/sync2.mjs` -- Local configuration: `~/.sync2/config.json` -- Local three-way state and recovery copies: `~/.sync2/state` and `~/.sync2/conflicts` +- Windows: `%USERPROFILE%\.codex\skills\codex-sync\scripts\codexsync.mjs` +- macOS: `~/.codex/skills/codex-sync/scripts/codexsync.mjs` +- Local configuration: `~/.codex-sync/config.json` +- Local three-way state and recovery copies: `~/.codex-sync/state` and `~/.codex-sync/conflicts` -Always pass `--config ` for alternate profiles. Never copy `~/.sync2/config.json` between devices because device IDs and local paths must remain unique. +Always pass `--config ` for alternate profiles. Never copy `~/.codex-sync/config.json` between devices because device IDs and local paths must remain unique. ## Data boundary -Sync2 includes selected conversation JSONL, portable thread metadata, user-defined skill files, selections, and device reports. It excludes authentication, Codex configuration, full SQLite/WAL databases, logs, caches, generated attachments, system skills, and plugin caches. +Codex Sync includes selected conversation JSONL, portable thread metadata, user-defined skill files, selections, and device reports. It excludes authentication, Codex configuration, full SQLite/WAL databases, logs, caches, generated attachments, system skills, and plugin caches. Conversation messages that reference local attachments or project files still synchronize as text, but the referenced files do not. Register the containing project separately or transfer those files through an approved storage path. @@ -132,13 +132,13 @@ Conversation messages that reference local attachments or project files still sy Before a repair or fleet upgrade: ```text -node /scripts/sync2.mjs maintenance on --reason "repair" -node /scripts/sync2.mjs conversation audit -node /scripts/sync2.mjs conversation repair --title "" +node /scripts/codexsync.mjs maintenance on --reason "repair" +node /scripts/codexsync.mjs conversation audit +node /scripts/codexsync.mjs conversation repair --title "" ``` Disable each device scheduler and pause the Syncthing folder separately; old clients do not understand the maintenance marker. After validation, publish the updated skill with one controlled `sync --force`, remove maintenance with `maintenance off`, resume the transport, and reinstall/re-enable schedulers. ## Git helper crash note -Folder-transport deployments do not invoke `git-remote-https.exe` during routine Sync2 runs. A `git-remote-https.exe` application error therefore comes from another Git operation or a damaged/misconfigured Git installation, not from conversation or skill reconciliation. Keep folder transport active while repairing Git. Follow the Git helper crash runbook in [deployment.md](deployment.md). +Folder-transport deployments do not invoke `git-remote-https.exe` during routine Codex Sync runs. A `git-remote-https.exe` application error therefore comes from another Git operation or a damaged/misconfigured Git installation, not from conversation or skill reconciliation. Keep folder transport active while repairing Git. Follow the Git helper crash runbook in [deployment.md](deployment.md). diff --git a/scripts/bootstrap-sync2.ps1 b/scripts/bootstrap-codex-sync.ps1 similarity index 73% rename from scripts/bootstrap-sync2.ps1 rename to scripts/bootstrap-codex-sync.ps1 index 95e8cdd..6708791 100644 --- a/scripts/bootstrap-sync2.ps1 +++ b/scripts/bootstrap-codex-sync.ps1 @@ -3,36 +3,36 @@ param( [string]$Device = $env:COMPUTERNAME, [ValidateSet('folder', 'git')][string]$Transport = 'folder', [string]$CodexHome = $(if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $HOME '.codex' }), - [string]$Config = $(Join-Path $HOME '.sync2\config.json'), + [string]$Config = $(Join-Path $HOME '.codex-sync\config.json'), [switch]$InstallDaemon, [int]$Minutes = 5 ) $ErrorActionPreference = 'Stop' $Vault = [IO.Path]::GetFullPath((Resolve-Path -LiteralPath $Vault)) -$source = Join-Path $Vault 'skills\codex\sync2' -$destination = Join-Path $CodexHome 'skills\sync2' +$source = Join-Path $Vault 'skills\codex\codex-sync' +$destination = Join-Path $CodexHome 'skills\codex-sync' if (-not (Test-Path -LiteralPath (Join-Path $source 'SKILL.md'))) { - throw "Sync2 skill was not found in vault: $source" + throw "Codex Sync skill was not found in vault: $source" } $node = Get-Command node -ErrorAction Stop $major = [int]((& $node.Source --version).TrimStart('v').Split('.')[0]) -if ($major -lt 22) { throw 'Sync2 requires Node.js 22 or newer.' } +if ($major -lt 22) { throw 'Codex Sync requires Node.js 22 or newer.' } New-Item -ItemType Directory -Force -Path $destination | Out-Null Copy-Item -Path (Join-Path $source '*') -Destination $destination -Recurse -Force -$cli = Join-Path $destination 'scripts\sync2.mjs' +$cli = Join-Path $destination 'scripts\codexsync.mjs' if (-not (Test-Path -LiteralPath $Config)) { & $node.Source --no-warnings $cli init --vault $Vault --transport $Transport --device $Device --codex-home $CodexHome --config $Config } else { & $node.Source --no-warnings $cli vault use --vault $Vault --transport $Transport --config $Config --no-sync } -$maintenance = Test-Path -LiteralPath (Join-Path $Vault '.sync2\maintenance.json') +$maintenance = Test-Path -LiteralPath (Join-Path $Vault '.codex-sync\maintenance.json') if ($maintenance) { - Write-Output 'Sync2 maintenance mode detected; performing controlled pull and head refresh without enabling the scheduler.' + Write-Output 'Codex Sync maintenance mode detected; performing controlled pull and head refresh without enabling the scheduler.' & $node.Source --no-warnings $cli pull --force --config $Config & $node.Source --no-warnings $cli sync --force --config $Config } else { diff --git a/scripts/bootstrap-sync2.sh b/scripts/bootstrap-codex-sync.sh similarity index 65% rename from scripts/bootstrap-sync2.sh rename to scripts/bootstrap-codex-sync.sh index ec3fdd4..783b312 100644 --- a/scripts/bootstrap-sync2.sh +++ b/scripts/bootstrap-codex-sync.sh @@ -2,7 +2,7 @@ set -eu usage() { - echo "Usage: bootstrap-sync2.sh VAULT [DEVICE] [folder|git] [--daemon]" >&2 + echo "Usage: bootstrap-codex-sync.sh VAULT [DEVICE] [folder|git] [--daemon]" >&2 exit 2 } @@ -12,26 +12,26 @@ DEVICE=${2:-"$(hostname)"} TRANSPORT=${3:-folder} DAEMON=${4:-} CODEX_HOME=${CODEX_HOME:-"$HOME/.codex"} -SOURCE="$VAULT/skills/codex/sync2" -DESTINATION="$CODEX_HOME/skills/sync2" +SOURCE="$VAULT/skills/codex/codex-sync" +DESTINATION="$CODEX_HOME/skills/codex-sync" -[ -f "$SOURCE/SKILL.md" ] || { echo "Sync2 skill was not found in vault: $SOURCE" >&2; exit 1; } +[ -f "$SOURCE/SKILL.md" ] || { echo "Codex Sync skill was not found in vault: $SOURCE" >&2; exit 1; } command -v node >/dev/null 2>&1 || { echo "Node.js 22 or newer is required." >&2; exit 1; } MAJOR=$(node --version | sed 's/^v//' | cut -d. -f1) -[ "$MAJOR" -ge 22 ] || { echo "Sync2 requires Node.js 22 or newer." >&2; exit 1; } +[ "$MAJOR" -ge 22 ] || { echo "Codex Sync requires Node.js 22 or newer." >&2; exit 1; } mkdir -p "$DESTINATION" cp -R "$SOURCE/." "$DESTINATION/" -CLI="$DESTINATION/scripts/sync2.mjs" -CONFIG=${SYNC2_CONFIG:-"$HOME/.sync2/config.json"} +CLI="$DESTINATION/scripts/codexsync.mjs" +CONFIG=${CODEX_SYNC_CONFIG:-"$HOME/.codex-sync/config.json"} if [ ! -f "$CONFIG" ]; then node --no-warnings "$CLI" init --vault "$VAULT" --transport "$TRANSPORT" --device "$DEVICE" --codex-home "$CODEX_HOME" --config "$CONFIG" else node --no-warnings "$CLI" vault use --vault "$VAULT" --transport "$TRANSPORT" --config "$CONFIG" --no-sync fi -if [ -f "$VAULT/.sync2/maintenance.json" ]; then - echo "Sync2 maintenance mode detected; performing controlled pull and head refresh without enabling the scheduler." +if [ -f "$VAULT/.codex-sync/maintenance.json" ]; then + echo "Codex Sync maintenance mode detected; performing controlled pull and head refresh without enabling the scheduler." node --no-warnings "$CLI" pull --force --config "$CONFIG" node --no-warnings "$CLI" sync --force --config "$CONFIG" if [ "$DAEMON" = "--daemon" ]; then echo "Scheduler installation deferred until fleet maintenance is disabled." >&2; fi diff --git a/scripts/check-repo-hygiene.mjs b/scripts/check-repo-hygiene.mjs index 373c650..e623fa3 100644 --- a/scripts/check-repo-hygiene.mjs +++ b/scripts/check-repo-hygiene.mjs @@ -10,7 +10,7 @@ const ignoredDirectories = new Set([".git", "node_modules", "coverage", "dist", const textExtensions = new Set(["", ".md", ".mjs", ".js", ".json", ".yaml", ".yml", ".ps1", ".sh", ".txt"]); const forbiddenFileRules = [ - { name: "local state directory", test: (value) => value.split("/").some((part) => [".sync2", ".codex", "Sync2Vault", "recovery", "device-reports"].includes(part)) }, + { name: "local state directory", test: (value) => value.split("/").some((part) => [".codex-sync", ".codex", "CodexSyncVault", "recovery", "device-reports"].includes(part)) }, { name: "credential/config file", test: (value) => /(^|\/)(auth\.json|config\.toml|\.env(?:\..*)?)$/i.test(value) && !value.endsWith("/.env.example") }, { name: "database or conversation artifact", test: (value) => /\.(?:sqlite|sqlite3|db|jsonl)(?:-wal|-shm)?$/i.test(value) }, { name: "private key material", test: (value) => /(^|\/)(?:id_rsa.*|id_ed25519.*|.*\.(?:pem|key))$/i.test(value) }, diff --git a/scripts/sync2.mjs b/scripts/codexsync.mjs similarity index 88% rename from scripts/sync2.mjs rename to scripts/codexsync.mjs index 0409888..c71a69c 100644 --- a/scripts/sync2.mjs +++ b/scripts/codexsync.mjs @@ -9,14 +9,15 @@ import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const SCRIPT_PATH = path.resolve(process.argv[1]); -const VERSION = 1; -const PROTOCOL_REVISION = 2; +const PACKAGE_VERSION = require("../package.json").version; +const FORMAT_VERSION = 1; +const PROTOCOL_REVISION = 3; const DEFAULT_EXCLUDES = [".system", ".git", "node_modules", "__pycache__", ".DS_Store"]; -const CONFIG_ROOT = process.env.SYNC2_HOME - ? expandPath(process.env.SYNC2_HOME) - : path.join(os.homedir(), ".sync2"); +const CONFIG_ROOT = process.env.CODEX_SYNC_HOME + ? expandPath(process.env.CODEX_SYNC_HOME) + : path.join(os.homedir(), ".codex-sync"); -class Sync2Error extends Error {} +class CodexSyncError extends Error {} function stripWindowsExtendedPrefix(input) { const value = String(input); @@ -55,7 +56,7 @@ function parseArgs(argv) { continue; } const next = inline !== undefined ? inline : argv[++i]; - if (next === undefined || next.startsWith("--")) throw new Sync2Error(`Missing value for --${rawKey}`); + if (next === undefined || next.startsWith("--")) throw new CodexSyncError(`Missing value for --${rawKey}`); if (["exclude", "ignore", "share-with"].includes(rawKey)) options[rawKey] = [...(options[rawKey] ?? []), next]; else options[rawKey] = next; } @@ -68,7 +69,7 @@ function nowIso() { function slug(value) { const result = String(value).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, ""); - if (!result) throw new Sync2Error(`Invalid portable name: ${value}`); + if (!result) throw new CodexSyncError(`Invalid portable name: ${value}`); return result.slice(0, 64); } @@ -81,7 +82,7 @@ function readJson(file, fallback = undefined) { return JSON.parse(fs.readFileSync(file, "utf8")); } catch (error) { if (error.code === "ENOENT" && fallback !== undefined) return fallback; - throw new Sync2Error(`Cannot read JSON ${file}: ${error.message}`); + throw new CodexSyncError(`Cannot read JSON ${file}: ${error.message}`); } } @@ -128,7 +129,7 @@ function portableRelative(root, file) { const normalizedFile = normalizeFsPath(file); const relative = path.relative(normalizedRoot, normalizedFile).split(path.sep).join("/"); if (!relative || relative.startsWith("../") || path.isAbsolute(relative)) { - throw new Sync2Error(`Path escapes root: ${file}`); + throw new CodexSyncError(`Path escapes root: ${file}`); } return relative; } @@ -145,7 +146,7 @@ function safeJoin(root, relative) { ? result.toLowerCase().startsWith(expected.toLowerCase()) : result.startsWith(expected); if (!sameRoot && !insideRoot) { - throw new Sync2Error(`Unsafe relative path: ${relative}`); + throw new CodexSyncError(`Unsafe relative path: ${relative}`); } return result; } @@ -163,10 +164,10 @@ function completeJsonlSnapshot(file) { complete.push(line); } catch (error) { const isLast = i === parts.length - 1; - if (!isLast) throw new Sync2Error(`Invalid JSONL before final line in ${file}: ${error.message}`); + if (!isLast) throw new CodexSyncError(`Invalid JSONL before final line in ${file}: ${error.message}`); } } - if (!complete.length) throw new Sync2Error(`No complete JSONL records in ${file}`); + if (!complete.length) throw new CodexSyncError(`No complete JSONL records in ${file}`); return Buffer.from(`${complete.join("\n")}\n`, "utf8"); } @@ -267,7 +268,7 @@ function walkFiles(root, excludes = []) { const result = new Map(); const visit = (dir) => { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - if (excluded.has(entry.name) || entry.name.includes(".sync2-conflict-")) continue; + if (excluded.has(entry.name) || entry.name.includes(".codex-sync-conflict-")) continue; const full = path.join(dir, entry.name); if (entry.isSymbolicLink()) continue; if (entry.isDirectory()) visit(full); @@ -284,7 +285,7 @@ function assertPortableCaseUnique(files, label) { const portableKey = relative.normalize("NFC").toLocaleLowerCase("en-US"); const previous = seen.get(portableKey); if (previous && previous !== relative) { - throw new Sync2Error(`Case/Unicode path collision in ${label}: ${previous} <> ${relative}`); + throw new CodexSyncError(`Case/Unicode path collision in ${label}: ${previous} <> ${relative}`); } seen.set(portableKey, relative); } @@ -299,13 +300,13 @@ function detectCodexHome(options = {}) { } function configPath(options = {}) { - return expandPath(options.config ?? process.env.SYNC2_CONFIG ?? path.join(CONFIG_ROOT, "config.json")); + return expandPath(options.config ?? process.env.CODEX_SYNC_CONFIG ?? path.join(CONFIG_ROOT, "config.json")); } function loadConfig(options = {}) { const file = configPath(options); const config = readJson(file); - if (config.version !== VERSION) throw new Sync2Error(`Unsupported config version ${config.version}`); + if (config.version !== FORMAT_VERSION) throw new CodexSyncError(`Unsupported config version ${config.version}`); config.vault = expandPath(config.vault); config.codexHome = expandPath(config.codexHome); config.skillSources = (config.skillSources ?? []).map((item) => ({ ...item, path: expandPath(item.path) })); @@ -357,7 +358,7 @@ function openDatabase(file, readOnly = false) { db.exec("PRAGMA busy_timeout=5000"); return db; } catch (error) { - throw new Sync2Error(`Cannot open SQLite ${file}: ${error.message}`); + throw new CodexSyncError(`Cannot open SQLite ${file}: ${error.message}`); } } @@ -414,7 +415,7 @@ function firstEventMetadata(snapshot) { function resolveThread(config, selector) { const candidate = selector === "current" ? process.env.CODEX_THREAD_ID : selector; - if (!candidate) throw new Sync2Error("Current thread ID is unavailable; pass a title fragment or thread ID."); + if (!candidate) throw new CodexSyncError("Current thread ID is unavailable; pass a title fragment or thread ID."); const threads = listThreads(config.codexHome); const exact = threads.find((item) => item.id === candidate); if (exact) return exact; @@ -424,8 +425,8 @@ function resolveThread(config, selector) { if (!matches.length && /^[0-9a-f-]{20,}$/i.test(candidate) && findRollout(config.codexHome, candidate)) { return { id: candidate, title: candidate }; } - if (!matches.length) throw new Sync2Error(`No local thread matches: ${selector}`); - throw new Sync2Error(`Ambiguous thread selector (${matches.length} matches): ${matches.slice(0, 5).map((x) => `${x.id} ${x.title ?? x.thread_name}`).join(" | ")}`); + if (!matches.length) throw new CodexSyncError(`No local thread matches: ${selector}`); + throw new CodexSyncError(`Ambiguous thread selector (${matches.length} matches): ${matches.slice(0, 5).map((x) => `${x.id} ${x.title ?? x.thread_name}`).join(" | ")}`); } function applyPathMaps(value, maps = []) { @@ -536,9 +537,9 @@ function run(command, args, options = {}) { windowsHide: true, env: { ...process.env, ...(options.env ?? {}) }, }); - if (result.error) throw new Sync2Error(`${command} failed to start: ${result.error.message}`); + if (result.error) throw new CodexSyncError(`${command} failed to start: ${result.error.message}`); if (result.status !== 0 && !options.allowFailure) { - throw new Sync2Error(`${command} ${args.join(" ")} failed (${result.status}): ${(result.stderr || result.stdout).trim()}`); + throw new CodexSyncError(`${command} ${args.join(" ")} failed (${result.status}): ${(result.stderr || result.stdout).trim()}`); } return result; } @@ -574,7 +575,7 @@ function findSyncthing(config, options = {}) { const check = run(candidate, ["--version"], { allowFailure: true }); if (check.status === 0) return candidate; } - throw new Sync2Error("Syncthing executable was not found. Run: sync2 syncthing configure --exe "); + throw new CodexSyncError("Syncthing executable was not found. Run: codexsync syncthing configure --exe "); } function stCli(executable, args, options = {}) { @@ -589,7 +590,7 @@ function stLines(executable, args) { function stJson(executable, args) { const output = stCli(executable, args); try { return JSON.parse(output); } - catch (error) { throw new Sync2Error(`Syncthing returned invalid JSON for ${args.join(" ")}: ${error.message}`); } + catch (error) { throw new CodexSyncError(`Syncthing returned invalid JSON for ${args.join(" ")}: ${error.message}`); } } function syncthingInventory(executable) { @@ -630,7 +631,7 @@ function selectSyncthingDevices(inventory, requested = []) { const lower = selector.toLowerCase(); const matches = remotes.filter((device) => device.id.toLowerCase() === lower || device.name.toLowerCase() === lower || device.id.toLowerCase().startsWith(lower) || device.name.toLowerCase().includes(lower)); - if (matches.length !== 1) throw new Sync2Error(`Syncthing device selector '${selector}' matched ${matches.length} devices.`); + if (matches.length !== 1) throw new CodexSyncError(`Syncthing device selector '${selector}' matched ${matches.length} devices.`); if (!result.some((item) => item.id === matches[0].id)) result.push(matches[0]); } return result; @@ -655,11 +656,11 @@ function decodeXml(value) { function syncthingRestCredentials(config) { const file = syncthingConfigFile(config); - if (!file) throw new Sync2Error("Syncthing config.xml was not found; REST status/rescan is unavailable."); + if (!file) throw new CodexSyncError("Syncthing config.xml was not found; REST status/rescan is unavailable."); const xml = fs.readFileSync(file, "utf8"); const apiKey = xml.match(/([^<]+)<\/apikey>/)?.[1]; const address = decodeXml(xml.match(/]*>[\s\S]*?
([^<]+)<\/address>/)?.[1] ?? "127.0.0.1:8384"); - if (!apiKey) throw new Sync2Error(`Syncthing API key is missing in ${file}`); + if (!apiKey) throw new CodexSyncError(`Syncthing API key is missing in ${file}`); const host = address.startsWith("http") ? address : `http://${address.replace(/^0\.0\.0\.0:/, "127.0.0.1:")}`; return { file, apiKey, base: `${host.replace(/\/$/, "")}/rest` }; } @@ -671,16 +672,16 @@ async function syncthingRest(config, endpoint, options = {}) { headers: { "X-API-Key": credentials.apiKey, "Content-Type": "application/json" }, body: options.body ? JSON.stringify(options.body) : undefined, }); - if (!response.ok) throw new Sync2Error(`Syncthing REST ${endpoint} failed: ${response.status} ${await response.text()}`); + if (!response.ok) throw new CodexSyncError(`Syncthing REST ${endpoint} failed: ${response.status} ${await response.text()}`); const text = await response.text(); return text ? JSON.parse(text) : null; } function resolveProjectFolder(inventory, selector) { - if (!selector) throw new Sync2Error("A project folder ID or path is required."); + if (!selector) throw new CodexSyncError("A project folder ID or path is required."); const normalized = normalizedPath(selector); const matches = inventory.folders.filter((folder) => folder.id === selector || normalizedPath(folder.path) === normalized || folder.label === selector); - if (matches.length !== 1) throw new Sync2Error(`Project selector '${selector}' matched ${matches.length} Syncthing folders.`); + if (matches.length !== 1) throw new CodexSyncError(`Project selector '${selector}' matched ${matches.length} Syncthing folders.`); return matches[0]; } @@ -744,12 +745,12 @@ function registerCodexProject(projectPath, dryRun = false) { if (process.platform === "darwin") result = run("open", [url], { allowFailure: true }); else if (process.platform === "win32") result = run("cmd.exe", ["/d", "/s", "/c", "start", "", url], { allowFailure: true }); else return { registered: false, unsupported: true, url }; - if (result.status !== 0) throw new Sync2Error(`Codex project registration failed: ${(result.stderr || result.stdout).trim()}`); + if (result.status !== 0) throw new CodexSyncError(`Codex project registration failed: ${(result.stderr || result.stdout).trim()}`); return { registered: true, url }; } function projectCatalogDir(config) { - return path.join(config.vault, ".sync2", "project-catalogs"); + return path.join(config.vault, ".codex-sync", "project-catalogs"); } function publishProjectCatalog(config, dryRun = false) { @@ -764,7 +765,7 @@ function publishProjectCatalog(config, dryRun = false) { }); const configuredPaths = new Set(configured.map((item) => normalizedPath(item.path))); const projects = [...configured, ...discovered.filter((item) => !configuredPaths.has(normalizedPath(item.path))).map((item) => ({ ...item, configured: false }))]; - const catalog = { version: VERSION, protocolRevision: PROTOCOL_REVISION, deviceId: config.deviceId, syncthingDeviceId, updatedAt: nowIso(), projects }; + const catalog = { version: FORMAT_VERSION, protocolRevision: PROTOCOL_REVISION, deviceId: config.deviceId, syncthingDeviceId, updatedAt: nowIso(), projects }; writeJson(path.join(projectCatalogDir(config), `${config.deviceId}.json`), catalog, dryRun); return catalog; } @@ -797,13 +798,13 @@ async function projectCommand(options, action, rest) { if (action === "tasks") { const selector = rest.join(" ") || options.path || "current"; const projectPath = selector === "current" ? process.cwd() : expandPath(selector); - if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) throw new Sync2Error(`Project directory does not exist: ${projectPath}`); + if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) throw new CodexSyncError(`Project directory does not exist: ${projectPath}`); const selectedTasks = selectProjectThreads(config, projectPath, true); saveConfig(file, config, Boolean(options["dry-run"])); return { action: "project-tasks-selected", path: path.resolve(projectPath), selectedTasks, count: selectedTasks.length }; } if (action === "map") { - if (!options.from || !options.to) throw new Sync2Error("project map requires --from --to "); + if (!options.from || !options.to) throw new CodexSyncError("project map requires --from --to "); addPathMap(config, options.from, expandPath(options.to)); saveConfig(file, config, Boolean(options["dry-run"])); return { action: "project-path-mapped", pathMaps: config.pathMaps }; @@ -811,7 +812,7 @@ async function projectCommand(options, action, rest) { if (action === "register") { const selector = rest.join(" ") || options.path || "current"; const projectPath = selector === "current" ? process.cwd() : expandPath(selector); - if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) throw new Sync2Error(`Project directory does not exist: ${projectPath}`); + if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) throw new CodexSyncError(`Project directory does not exist: ${projectPath}`); return { action: "registered-in-codex", path: path.resolve(projectPath), ...registerCodexProject(projectPath, Boolean(options["dry-run"])) }; } const executable = findSyncthing(config, options); @@ -824,20 +825,20 @@ async function projectCommand(options, action, rest) { if (action === "add" || action === "select") { const selector = rest.join(" ") || options.path || "current"; const projectPath = selector === "current" ? process.cwd() : expandPath(selector); - if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) throw new Sync2Error(`Project directory does not exist: ${projectPath}`); + if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) throw new CodexSyncError(`Project directory does not exist: ${projectPath}`); const desiredId = projectFolderId(projectPath, options.id); const label = options.label ?? path.basename(projectPath); const samePath = inventory.folders.find((folder) => normalizedPath(folder.path) === normalizedPath(projectPath)); for (const folder of inventory.folders) { if (normalizedPath(folder.path) === normalizedPath(projectPath)) continue; if (pathContains(folder.path, projectPath) || pathContains(projectPath, folder.path)) { - throw new Sync2Error(`Syncthing folder paths must stay independent: '${projectPath}' overlaps '${folder.path}' (${folder.id}).`); + throw new CodexSyncError(`Syncthing folder paths must stay independent: '${projectPath}' overlaps '${folder.path}' (${folder.id}).`); } } let folder = samePath; let created = false; if (!folder) { - if (inventory.folders.some((item) => item.id === desiredId)) throw new Sync2Error(`Syncthing folder ID already exists for another path: ${desiredId}`); + if (inventory.folders.some((item) => item.id === desiredId)) throw new CodexSyncError(`Syncthing folder ID already exists for another path: ${desiredId}`); if (!options["dry-run"]) stCli(executable, ["config", "folders", "add", "--id", desiredId, "--label", label, "--path", path.resolve(projectPath), "--type", "sendreceive"]); folder = { id: desiredId, label, path: path.resolve(projectPath), type: "sendreceive", devices: [{ deviceID: inventory.myID }] }; created = true; @@ -865,14 +866,14 @@ async function projectCommand(options, action, rest) { if (action === "accept") { const id = rest[0] || options.id; const localPath = expandPath(options.path ?? rest.slice(1).join(" ")); - if (!id || !localPath) throw new Sync2Error("project accept requires --path "); + if (!id || !localPath) throw new CodexSyncError("project accept requires --path "); const catalogs = listProjectCatalogs(config); const sources = catalogs.flatMap((catalog) => (catalog.projects ?? []).filter((project) => project.id === id).map((project) => ({ catalog, project }))); - if (!sources.length) throw new Sync2Error(`No portable project catalog advertises folder ID: ${id}`); + if (!sources.length) throw new CodexSyncError(`No portable project catalog advertises folder ID: ${id}`); const source = sources.sort((a, b) => String(b.catalog.updatedAt).localeCompare(String(a.catalog.updatedAt)))[0]; mkdir(localPath, Boolean(options["dry-run"])); let folder = inventory.folders.find((item) => item.id === id); - if (folder && normalizedPath(folder.path) !== normalizedPath(localPath)) throw new Sync2Error(`Syncthing folder ${id} already uses another path: ${folder.path}`); + if (folder && normalizedPath(folder.path) !== normalizedPath(localPath)) throw new CodexSyncError(`Syncthing folder ${id} already uses another path: ${folder.path}`); if (!folder) { if (!options["dry-run"]) stCli(executable, ["config", "folders", "add", "--id", id, "--label", source.project.label ?? id, "--path", path.resolve(localPath), "--type", "sendreceive"]); folder = { id, label: source.project.label ?? id, path: path.resolve(localPath), type: "sendreceive", devices: [{ deviceID: inventory.myID }] }; @@ -893,11 +894,11 @@ async function projectCommand(options, action, rest) { } const selector = rest.join(" ") || options.id || options.path; if (action === "remove") { - if (!selector) throw new Sync2Error("project remove requires a folder ID or path"); + if (!selector) throw new CodexSyncError("project remove requires a folder ID or path"); const configured = Object.values(config.projects).find((item) => item.id === selector || normalizedPath(item.path) === normalizedPath(selector) || item.label === selector); const registered = inventory.folders.find((item) => item.id === selector || normalizedPath(item.path) === normalizedPath(selector) || item.label === selector); const id = registered?.id ?? configured?.id; - if (!id) throw new Sync2Error(`Project is not registered: ${selector}`); + if (!id) throw new CodexSyncError(`Project is not registered: ${selector}`); if (registered && !options["dry-run"]) stCli(executable, ["config", "folders", id, "delete"]); if (!options["dry-run"]) delete config.projects[id]; saveConfig(file, config, Boolean(options["dry-run"])); @@ -921,7 +922,7 @@ async function projectCommand(options, action, rest) { saveConfig(file, config); return { action: "rescan-requested", folder: folder.id }; } - throw new Sync2Error(`Unknown project action: ${action}`); + throw new CodexSyncError(`Unknown project action: ${action}`); } function syncthingCommand(options, action) { @@ -936,7 +937,7 @@ function syncthingCommand(options, action) { const inventory = syncthingInventory(executable); if (action === "add-device") { const id = options["device-id"]; - if (!id) throw new Sync2Error("syncthing add-device requires --device-id "); + if (!id) throw new CodexSyncError("syncthing add-device requires --device-id "); const existing = inventory.devices.find((device) => device.id === id); if (!existing) stCli(executable, ["config", "devices", "add", "--device-id", id, "--name", options.name ?? id.slice(0, 7)]); config.syncthing = { ...(config.syncthing ?? {}), executable }; @@ -952,26 +953,26 @@ function gitHasUpstream(vault) { function gitPre(config, dryRun) { if (config.transport !== "git" || dryRun) return; - if (!fs.existsSync(path.join(config.vault, ".git"))) throw new Sync2Error(`Git transport requires a repository: ${config.vault}`); + if (!fs.existsSync(path.join(config.vault, ".git"))) throw new CodexSyncError(`Git transport requires a repository: ${config.vault}`); if (gitHasUpstream(config.vault)) run("git", ["pull", "--rebase", "--autostash"], { cwd: config.vault }); } function gitPost(config, dryRun) { if (config.transport !== "git" || dryRun) return; - run("git", ["add", "--", ".sync2", "conversations", "skills", "conflicts"], { cwd: config.vault }); + run("git", ["add", "--", ".codex-sync", "conversations", "skills", "conflicts"], { cwd: config.vault }); const changed = run("git", ["diff", "--cached", "--quiet"], { cwd: config.vault, allowFailure: true }).status !== 0; if (!changed) return; - run("git", ["commit", "-m", `sync2: ${config.deviceId} ${nowIso()}`], { cwd: config.vault }); + run("git", ["commit", "-m", `codex-sync: ${config.deviceId} ${nowIso()}`], { cwd: config.vault }); if (gitHasUpstream(config.vault)) run("git", ["push"], { cwd: config.vault }); } function ensureVault(config, dryRun = false) { mkdir(config.vault, dryRun); - for (const relative of [".sync2/selections", ".sync2/project-catalogs", "conversations", "skills", "conflicts/skills"]) mkdir(path.join(config.vault, relative), dryRun); - const marker = path.join(config.vault, ".sync2", "vault.json"); + for (const relative of [".codex-sync/selections", ".codex-sync/project-catalogs", "conversations", "skills", "conflicts/skills"]) mkdir(path.join(config.vault, relative), dryRun); + const marker = path.join(config.vault, ".codex-sync", "vault.json"); const existing = fs.existsSync(marker) ? readJson(marker) : null; - if (existing && existing.version !== VERSION) throw new Sync2Error(`Vault protocol version ${existing.version} is unsupported.`); - if (!existing) writeJson(marker, { version: VERSION, protocolRevision: PROTOCOL_REVISION, createdAt: nowIso(), format: "sync2-portable-v1" }, dryRun); + if (existing && existing.version !== FORMAT_VERSION) throw new CodexSyncError(`Vault protocol version ${existing.version} is unsupported.`); + if (!existing) writeJson(marker, { version: FORMAT_VERSION, protocolRevision: PROTOCOL_REVISION, createdAt: nowIso(), format: "codex-sync-portable-v1" }, dryRun); else if (Number(existing.protocolRevision ?? 1) < PROTOCOL_REVISION) writeJson(marker, { ...existing, protocolRevision: PROTOCOL_REVISION, upgradedAt: nowIso() }, dryRun); } @@ -986,7 +987,7 @@ function withVaultLock(config, dryRun, callback) { } catch (error) { if (error.code !== "EEXIST") throw error; const age = Date.now() - fs.statSync(lock).mtimeMs; - if (age < 10 * 60 * 1000) throw new Sync2Error(`Vault is locked by another Sync2 run: ${lock}`); + if (age < 10 * 60 * 1000) throw new CodexSyncError(`Vault is locked by another Codex Sync run: ${lock}`); fs.unlinkSync(lock); fd = fs.openSync(lock, "wx"); } @@ -999,18 +1000,18 @@ function withVaultLock(config, dryRun, callback) { function initCommand(options) { const file = configPath(options); - if (fs.existsSync(file) && !options.force) throw new Sync2Error(`Config already exists: ${file} (use --force to replace)`); + if (fs.existsSync(file) && !options.force) throw new CodexSyncError(`Config already exists: ${file} (use --force to replace)`); const codexHome = detectCodexHome(options); let vault = expandPath(options.vault); - if (!vault) throw new Sync2Error("init requires --vault "); + if (!vault) throw new CodexSyncError("init requires --vault "); const transport = options.transport ?? "folder"; - if (!['folder', 'git'].includes(transport)) throw new Sync2Error("--transport must be folder or git"); + if (!['folder', 'git'].includes(transport)) throw new CodexSyncError("--transport must be folder or git"); if (transport === "git" && options.repo && !fs.existsSync(path.join(vault, ".git"))) { mkdir(path.dirname(vault)); run("git", ["clone", options.repo, vault]); } const config = { - version: VERSION, + version: FORMAT_VERSION, deviceId: deviceId(options.device), vault, transport, @@ -1030,13 +1031,13 @@ function initCommand(options) { } function publishSelections(config, dryRun = false) { - const file = path.join(config.vault, ".sync2", "selections", `${config.deviceId}.json`); + const file = path.join(config.vault, ".codex-sync", "selections", `${config.deviceId}.json`); const eventTimes = Object.values(config.conversations ?? {}).map((event) => event.updatedAt).filter(Boolean).sort(); - writeJson(file, { version: VERSION, deviceId: config.deviceId, updatedAt: eventTimes.at(-1) ?? config.createdAt, conversations: config.conversations }, dryRun); + writeJson(file, { version: FORMAT_VERSION, deviceId: config.deviceId, updatedAt: eventTimes.at(-1) ?? config.createdAt, conversations: config.conversations }, dryRun); } function mergeSelections(config) { - const dir = path.join(config.vault, ".sync2", "selections"); + const dir = path.join(config.vault, ".codex-sync", "selections"); const merged = { ...config.conversations }; if (!fs.existsSync(dir)) return merged; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { @@ -1057,9 +1058,9 @@ function conversationMetadata(config, id, rollout, snapshot) { const row = threadRow(config.codexHome, id); const first = firstEventMetadata(snapshot); const relative = portableRelative(config.codexHome, rollout); - if (!relative.startsWith("sessions/")) throw new Sync2Error(`Rollout is outside Codex sessions: ${rollout}`); + if (!relative.startsWith("sessions/")) throw new CodexSyncError(`Rollout is outside Codex sessions: ${rollout}`); return { - version: VERSION, + version: FORMAT_VERSION, id, title: row?.title ?? config.conversations[id]?.title ?? id, createdAt: row?.created_at ? new Date(Number(row.created_at) * 1000).toISOString() : first.timestamp, @@ -1075,7 +1076,7 @@ function conversationMetadata(config, id, rollout, snapshot) { function portableConversationMetadata(metadata) { return { - version: VERSION, + version: FORMAT_VERSION, id: metadata.id, title: metadata.title, createdAt: metadata.createdAt, @@ -1088,7 +1089,7 @@ function portableConversationMetadata(metadata) { function readConversationMetadata(root) { const coreFile = path.join(root, "metadata.json"); - if (!fs.existsSync(coreFile)) throw new Sync2Error(`Conversation metadata missing: ${root}`); + if (!fs.existsSync(coreFile)) throw new CodexSyncError(`Conversation metadata missing: ${root}`); const core = readJson(coreFile); const deviceDir = path.join(root, "metadata"); if (!fs.existsSync(deviceDir)) return core; @@ -1174,7 +1175,7 @@ function reconcileConversation(config, id, dryRun, summary) { const conflictFile = path.join(root, "conflict.json"); if (!compatible) { writeJson(conflictFile, { - version: VERSION, + version: FORMAT_VERSION, threadId: id, detectedAt: nowIso(), heads: validHeads.map((item) => ({ device: item.device, bytes: item.data.length, sha256: hashBuffer(item.data) })), @@ -1228,7 +1229,7 @@ function importConversation(config, id, canonical, dryRun, summary) { const metadata = readConversationMetadata(root); metadata.pathMaps = config.pathMaps; const relative = metadata.relativePath; - if (!String(relative).startsWith("sessions/")) throw new Sync2Error(`Unsafe conversation relative path: ${relative}`); + if (!String(relative).startsWith("sessions/")) throw new CodexSyncError(`Unsafe conversation relative path: ${relative}`); const destination = safeJoin(config.codexHome, relative); const local = fs.existsSync(destination) ? completeJsonlSnapshot(destination) : null; if (local && !isPrefix(local, canonical) && !isPrefix(canonical, local)) { @@ -1277,13 +1278,13 @@ function syncSkillSource(config, source, direction, dryRun, summary) { mkdir(localRoot, dryRun); mkdir(remoteRoot, dryRun); const stateFile = skillStateFile(config, collection); - const state = readJson(stateFile, { version: VERSION, files: {} }); + const state = readJson(stateFile, { version: FORMAT_VERSION, files: {} }); const local = walkFiles(localRoot, source.excludes ?? []); const remote = walkFiles(remoteRoot, []); assertPortableCaseUnique(local, `local skill collection ${collection}`); assertPortableCaseUnique(remote, `vault skill collection ${collection}`); const paths = new Set([...local.keys(), ...remote.keys(), ...Object.keys(state.files ?? {})]); - const next = { version: VERSION, collection, updatedAt: nowIso(), files: { ...state.files } }; + const next = { version: FORMAT_VERSION, collection, updatedAt: nowIso(), files: { ...state.files } }; for (const relative of [...paths].sort()) { const l = local.get(relative); const r = remote.get(relative); @@ -1323,7 +1324,7 @@ function syncSkillSource(config, source, direction, dryRun, summary) { if (l) copyAtomic(l.file, `${conflictRoot}.${config.deviceId}.local`, dryRun); if (r) { copyAtomic(r.file, `${conflictRoot}.vault`, dryRun); - copyAtomic(r.file, `${safeJoin(localRoot, relative)}.sync2-conflict-${config.deviceId}-${stamp}`, dryRun); + copyAtomic(r.file, `${safeJoin(localRoot, relative)}.codex-sync-conflict-${config.deviceId}-${stamp}`, dryRun); } summary.skillConflicts += 1; summary.warnings.push(`Skill conflict in ${collection}/${relative}; originals were preserved.`); @@ -1407,7 +1408,7 @@ function buildDeviceReport(config, lastRunOverride = undefined) { return { name: source.name, path: source.path, exists: fs.existsSync(source.path), skillDirectories, files: files.size }; }); return { - version: VERSION, + version: FORMAT_VERSION, protocolRevision: PROTOCOL_REVISION, scriptSha256: hashFile(SCRIPT_PATH), deviceId: config.deviceId, @@ -1427,19 +1428,19 @@ function buildDeviceReport(config, lastRunOverride = undefined) { function publishDeviceReport(config, dryRun = false, lastRunOverride = undefined) { const report = buildDeviceReport(config, lastRunOverride); - writeJson(path.join(config.vault, ".sync2", "device-reports", `${config.deviceId}.json`), report, dryRun); + writeJson(path.join(config.vault, ".codex-sync", "device-reports", `${config.deviceId}.json`), report, dryRun); return report; } function listDeviceReports(config) { - const dir = path.join(config.vault, ".sync2", "device-reports"); + const dir = path.join(config.vault, ".codex-sync", "device-reports"); if (!fs.existsSync(dir)) return []; return fs.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".json")) .map((entry) => readJson(path.join(dir, entry.name))); } function maintenanceFile(config) { - return path.join(config.vault, ".sync2", "maintenance.json"); + return path.join(config.vault, ".codex-sync", "maintenance.json"); } function lastRunFile(config) { @@ -1463,7 +1464,7 @@ function maintenanceCommand(options, action) { try { fs.unlinkSync(file); } catch (error) { if (error.code !== "ENOENT") throw error; } return { action: "maintenance-disabled", file }; } - throw new Sync2Error(`Unknown maintenance action: ${action}`); + throw new CodexSyncError(`Unknown maintenance action: ${action}`); } function syncCommand(options, direction = "sync") { @@ -1475,7 +1476,7 @@ function syncCommand(options, direction = "sync") { try { ensureVault(config, dryRun); if (!dryRun && fs.existsSync(maintenanceFile(config)) && !options.force) { - throw new Sync2Error(`Vault maintenance mode is enabled: ${maintenanceFile(config)} (use --force only for a controlled repair run)`); + throw new CodexSyncError(`Vault maintenance mode is enabled: ${maintenanceFile(config)} (use --force only for a controlled repair run)`); } gitPre(config, dryRun); withVaultLock(config, dryRun, () => { @@ -1541,7 +1542,7 @@ function localCatalogEntry(codexHome, id) { function conversationAudit(config, selector) { const row = resolveThread(config, selector); const rollout = findRollout(config.codexHome, row.id); - if (!rollout) throw new Sync2Error(`Rollout not found for ${row.id}`); + if (!rollout) throw new CodexSyncError(`Rollout not found for ${row.id}`); const audit = auditJsonlFile(rollout); const state = threadRow(config.codexHome, row.id); const index = sessionIndexEntry(config.codexHome, row.id); @@ -1583,7 +1584,7 @@ function repairConversationCommand(options, selector) { payload: { type: "custom_tool_call_output", call_id: item.callId, - output: [{ type: "input_text", text: "Sync2 recovery: the original tool execution was interrupted before its output event was persisted. Partial side effects were inspected separately; no output is being reconstructed." }], + output: [{ type: "input_text", text: "Codex Sync recovery: the original tool execution was interrupted before its output event was persisted. Partial side effects were inspected separately; no output is being reconstructed." }], internal_chat_message_metadata_passthrough: item.turnId ? { turn_id: item.turnId } : undefined, }, }); @@ -1594,7 +1595,7 @@ function repairConversationCommand(options, selector) { payload: { type: "function_call_output", call_id: item.callId, - output: "Sync2 recovery: interrupted before the original function output event was persisted.", + output: "Codex Sync recovery: interrupted before the original function output event was persisted.", internal_chat_message_metadata_passthrough: item.turnId ? { turn_id: item.turnId } : undefined, }, }); @@ -1616,7 +1617,7 @@ function repairConversationCommand(options, selector) { role: "developer", content: [{ type: "input_text", - text: "\nThe previous turn was interrupted before its completion record was persisted. Sync2 recovered the stale turn without reconstructing missing execution output.\n", + text: "\nThe previous turn was interrupted before its completion record was persisted. Codex Sync recovered the stale turn without reconstructing missing execution output.\n", }], internal_chat_message_metadata_passthrough: { turn_id: turnId }, }, @@ -1638,7 +1639,7 @@ function repairConversationCommand(options, selector) { const state = threadRow(config.codexHome, before.id) ?? {}; const title = String(options.title ?? config.conversations?.[before.id]?.title ?? before.indexes.localCatalog?.title ?? state.title ?? before.id).trim() || before.id; const metadata = { - version: VERSION, + version: FORMAT_VERSION, id: before.id, title, createdAt: state.created_at ? new Date(Number(state.created_at) * 1000).toISOString() : nowIso(), @@ -1676,11 +1677,11 @@ function repairConversationCommand(options, selector) { function resolveConversationConflict(options, id) { const from = options["from-device"]; - if (!from) throw new Sync2Error("conversation resolve requires --from-device "); + if (!from) throw new CodexSyncError("conversation resolve requires --from-device "); const { config } = loadConfig(options); const root = path.join(config.vault, "conversations", id); const chosen = path.join(root, "heads", `${slug(from)}.jsonl`); - if (!fs.existsSync(chosen)) throw new Sync2Error(`Head not found: ${chosen}`); + if (!fs.existsSync(chosen)) throw new CodexSyncError(`Head not found: ${chosen}`); const data = completeJsonlSnapshot(chosen); const archive = path.join(root, "archived-heads", conflictStamp()); mkdir(archive); @@ -1733,7 +1734,7 @@ function skillsCommand(options, action, rest) { if (action === "add" || action === "install") { const sourcePath = expandPath(options.path ?? options.to); const name = slug(options.name ?? rest[0]); - if (!sourcePath) throw new Sync2Error(`skills ${action} requires --path/--to `); + if (!sourcePath) throw new CodexSyncError(`skills ${action} requires --path/--to `); const existing = config.skillSources.find((source) => source.name === name); const item = { name, path: sourcePath, excludes: options.exclude ?? [] }; if (existing) Object.assign(existing, item); @@ -1746,20 +1747,20 @@ function skillsCommand(options, action, rest) { const name = slug(options.name ?? rest[0]); const before = config.skillSources.length; config.skillSources = config.skillSources.filter((source) => source.name !== name); - if (config.skillSources.length === before) throw new Sync2Error(`Skill collection not configured: ${name}`); + if (config.skillSources.length === before) throw new CodexSyncError(`Skill collection not configured: ${name}`); saveConfig(file, config); return { action: "removed", name }; } - throw new Sync2Error(`Unknown skills action: ${action}`); + throw new CodexSyncError(`Unknown skills action: ${action}`); } function vaultCommand(options, action) { - if (action !== "use") throw new Sync2Error(`Unknown vault action: ${action}`); + if (action !== "use") throw new CodexSyncError(`Unknown vault action: ${action}`); const { config, file } = loadConfig(options); const nextVault = expandPath(options.vault ?? options.path); - if (!nextVault) throw new Sync2Error("vault use requires --vault "); + if (!nextVault) throw new CodexSyncError("vault use requires --vault "); const transport = options.transport ?? config.transport; - if (!["folder", "git"].includes(transport)) throw new Sync2Error("--transport must be folder or git"); + if (!["folder", "git"].includes(transport)) throw new CodexSyncError("--transport must be folder or git"); if (transport === "git" && options.repo && !fs.existsSync(path.join(nextVault, ".git"))) { mkdir(path.dirname(nextVault)); run("git", ["clone", options.repo, nextVault]); @@ -1871,7 +1872,7 @@ function statusCommand(options) { const skillConflictRoot = path.join(config.vault, "conflicts", "skills"); const vaultConversationHealthById = Object.fromEntries(selected.map((item) => [item.id, vaultConversationHealth(config, item.id)])); return { - version: VERSION, + version: FORMAT_VERSION, config: file, deviceId: config.deviceId, transport: config.transport, @@ -1895,7 +1896,7 @@ function deviceCommand(options, action) { const { config } = loadConfig(options); if (action === "report") return publishDeviceReport(config, Boolean(options["dry-run"])); if (action === "list") return listDeviceReports(config); - throw new Sync2Error(`Unknown device action: ${action}`); + throw new CodexSyncError(`Unknown device action: ${action}`); } function xmlEscape(value) { @@ -1904,9 +1905,9 @@ function xmlEscape(value) { function daemonPaths(config) { const localRoot = path.dirname(config._configFile ?? path.join(CONFIG_ROOT, "config.json")); - if (process.platform === "win32") return { task: "Sync2-AutoSync", spec: path.join(localRoot, "sync2-task.xml") }; - if (process.platform === "darwin") return { task: "com.openai.sync2", spec: path.join(os.homedir(), "Library", "LaunchAgents", "com.openai.sync2.plist") }; - return { task: "sync2-autosync", spec: path.join(localRoot, "sync2-autosync.service") }; + if (process.platform === "win32") return { task: "CodexSync-AutoSync", spec: path.join(localRoot, "codexsync-task.xml") }; + if (process.platform === "darwin") return { task: "com.toussaintknight.codexsync", spec: path.join(os.homedir(), "Library", "LaunchAgents", "com.toussaintknight.codexsync.plist") }; + return { task: "codexsync-autosync", spec: path.join(localRoot, "codexsync-autosync.service") }; } function daemonStatus(config, file) { @@ -1932,10 +1933,10 @@ function daemonCommand(options, action) { else if (process.platform === "darwin") { run("launchctl", ["unload", target.spec], { allowFailure: true }); try { fs.unlinkSync(target.spec); } catch (error) { if (error.code !== "ENOENT") throw error; } - } else throw new Sync2Error("Automatic install currently supports Windows Task Scheduler and macOS LaunchAgents."); + } else throw new CodexSyncError("Automatic install currently supports Windows Task Scheduler and macOS LaunchAgents."); return { action: "uninstalled", ...target }; } - if (action !== "install") throw new Sync2Error(`Unknown daemon action: ${action}`); + if (action !== "install") throw new CodexSyncError(`Unknown daemon action: ${action}`); const dryRun = Boolean(options["dry-run"]); if (process.platform === "win32") { const start = new Date(Date.now() + 60_000).toTimeString().slice(0, 5); @@ -1946,12 +1947,12 @@ function daemonCommand(options, action) { const plist = `\nLabel${target.task}ProgramArguments${xmlEscape(process.execPath)}--no-warnings${xmlEscape(SCRIPT_PATH)}sync--config${xmlEscape(file)}--quietStartInterval${minutes * 60}RunAtLoad`; atomicWrite(target.spec, plist, dryRun); if (!dryRun) run("launchctl", ["load", "-w", target.spec]); - } else throw new Sync2Error("Automatic install currently supports Windows Task Scheduler and macOS LaunchAgents."); + } else throw new CodexSyncError("Automatic install currently supports Windows Task Scheduler and macOS LaunchAgents."); return { action: dryRun ? "install-preview" : "installed", minutes, ...target, executable: process.execPath, script: SCRIPT_PATH, config: file }; } function helpText() { - return `Sync2 v${VERSION}\n\nCommands:\n init --vault PATH [--transport folder|git] [--repo URL]\n vault use --vault PATH [--transport folder|git] [--repo URL]\n sync|push|pull [--dry-run]\n status | doctor\n maintenance on|off|status [--reason TEXT]\n conversation select \n conversation unselect \n conversation list\n conversation audit \n conversation repair [--title TITLE]\n conversation resolve --from-device DEVICE\n skills list|discover\n skills add --name NAME --path PATH [--exclude NAME]\n skills remove NAME\n skills install NAME --to PATH\n project discover | project catalogs\n project add|select [--id ID] [--label LABEL] [--share-with all|DEVICE] [--ignore PATTERN] [--no-tasks]\n project tasks \n project accept --path PATH [--no-register]\n project map --from SOURCE_PATH --to LOCAL_PATH | project register \n project list | project status | project rescan \n project remove (registration only; keeps local files)\n syncthing configure --syncthing-exe PATH | syncthing status\n syncthing add-device --device-id ID [--name NAME]\n device report | device list\n daemon install|uninstall|status [--minutes 5] [--dry-run]\n\nGlobal options: --config PATH --json --quiet`; + return `Codex Sync v${PACKAGE_VERSION}\n\nCLI command: codexsync\n\nCommands:\n init --vault PATH [--transport folder|git] [--repo URL]\n vault use --vault PATH [--transport folder|git] [--repo URL]\n sync|push|pull [--dry-run]\n status | doctor\n maintenance on|off|status [--reason TEXT]\n conversation select \n conversation unselect \n conversation list\n conversation audit \n conversation repair [--title TITLE]\n conversation resolve --from-device DEVICE\n skills list|discover\n skills add --name NAME --path PATH [--exclude NAME]\n skills remove NAME\n skills install NAME --to PATH\n project discover | project catalogs\n project add|select [--id ID] [--label LABEL] [--share-with all|DEVICE] [--ignore PATTERN] [--no-tasks]\n project tasks \n project accept --path PATH [--no-register]\n project map --from SOURCE_PATH --to LOCAL_PATH | project register \n project list | project status | project rescan \n project remove (registration only; keeps local files)\n syncthing configure --syncthing-exe PATH | syncthing status\n syncthing add-device --device-id ID [--name NAME]\n device report | device list\n daemon install|uninstall|status [--minutes 5] [--dry-run]\n\nGlobal options: --config PATH --json --quiet`; } function printResult(result, options) { @@ -1982,16 +1983,16 @@ async function main() { else if (subcommand === "audit") result = auditConversationCommand(options, rest.join(" ") || "current"); else if (subcommand === "repair") result = repairConversationCommand(options, rest.join(" ") || "current"); else if (subcommand === "resolve") result = resolveConversationConflict(options, rest[0]); - else throw new Sync2Error(`Unknown conversation action: ${subcommand}`); + else throw new CodexSyncError(`Unknown conversation action: ${subcommand}`); } else if (command === "skills") result = skillsCommand(options, subcommand ?? "list", rest); else if (command === "daemon") result = daemonCommand(options, subcommand ?? "status"); - else throw new Sync2Error(`Unknown command: ${command}`); + else throw new CodexSyncError(`Unknown command: ${command}`); printResult(result, options); } main().catch((error) => { - const message = error instanceof Sync2Error ? error.message : `${error.name}: ${error.message}`; - console.error(`sync2: ${message}`); - if (process.env.SYNC2_DEBUG) console.error(error.stack); + const message = error instanceof CodexSyncError ? error.message : `${error.name}: ${error.message}`; + console.error(`codex-sync: ${message}`); + if (process.env.CODEX_SYNC_DEBUG) console.error(error.stack); process.exitCode = 1; }); diff --git a/scripts/repair-git-https.ps1 b/scripts/repair-git-https.ps1 index dcd51be..d308ceb 100644 --- a/scripts/repair-git-https.ps1 +++ b/scripts/repair-git-https.ps1 @@ -11,7 +11,7 @@ $result = [ordered]@{ computer = $env:COMPUTERNAME repairRequested = [bool]$Repair httpsTestRequested = [bool]$TestHttps - sync2 = $null + codexSync = $null gitBefore = @() gitAfter = @() backup = $null @@ -21,16 +21,16 @@ $result = [ordered]@{ warnings = @() } -$sync2Config = Join-Path $HOME '.sync2\config.json' -if (Test-Path -LiteralPath $sync2Config) { +$codexSyncConfig = Join-Path $HOME '.codex-sync\config.json' +if (Test-Path -LiteralPath $codexSyncConfig) { try { - $config = Get-Content -Raw -LiteralPath $sync2Config | ConvertFrom-Json - $result.sync2 = [ordered]@{ deviceId = $config.deviceId; transport = $config.transport; vault = $config.vault } + $config = Get-Content -Raw -LiteralPath $codexSyncConfig | ConvertFrom-Json + $result.codexSync = [ordered]@{ deviceId = $config.deviceId; transport = $config.transport; vault = $config.vault } if ($config.transport -ne 'git') { - $result.warnings += 'Sync2 does not use Git in the current transport mode; this repair targets another Git workflow.' + $result.warnings += 'Codex Sync does not use Git in the current transport mode; this repair targets another Git workflow.' } } catch { - $result.warnings += "Could not parse Sync2 config: $($_.Exception.Message)" + $result.warnings += "Could not parse Codex Sync config: $($_.Exception.Message)" } } @@ -59,7 +59,7 @@ if ($Repair) { $gitConfig = Join-Path $HOME '.gitconfig' if (Test-Path -LiteralPath $gitConfig) { - $backup = "$gitConfig.sync2-backup-$(Get-Date -Format yyyyMMddHHmmss)" + $backup = "$gitConfig.codex-sync-backup-$(Get-Date -Format yyyyMMddHHmmss)" Copy-Item -LiteralPath $gitConfig -Destination $backup $result.backup = $backup } diff --git a/scripts/test-sync2.mjs b/scripts/test-codexsync.mjs similarity index 93% rename from scripts/test-sync2.mjs rename to scripts/test-codexsync.mjs index 371af13..75ca85b 100644 --- a/scripts/test-sync2.mjs +++ b/scripts/test-codexsync.mjs @@ -9,9 +9,9 @@ import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const { DatabaseSync } = require("node:sqlite"); -const CLI = path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/(?:[A-Za-z]:)/, (m) => m.slice(1))), "sync2.mjs"); +const CLI = path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/(?:[A-Za-z]:)/, (m) => m.slice(1))), "codexsync.mjs"); const SKILL_ROOT = path.dirname(path.dirname(CLI)); -const root = fs.mkdtempSync(path.join(os.tmpdir(), "sync2-e2e-")); +const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-sync-e2e-")); const vault = path.join(root, "vault"); const id = "019f0000-0000-7000-8000-000000000001"; const damagedId = "019f0000-0000-7000-8000-000000000002"; @@ -83,9 +83,9 @@ function createThread(homeInfo) { fs.writeFileSync(rollout, `${lines.join("\n")}\n`); const db = new DatabaseSync(path.join(homeInfo.home, "state_5.sqlite")); db.prepare("INSERT INTO threads (id,rollout_path,created_at,updated_at,source,model_provider,cwd,title,sandbox_policy,approval_mode,tokens_used,has_user_event,archived,cli_version,first_user_message,preview,created_at_ms,updated_at_ms,recency_at,recency_at_ms) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)") - .run(id, rollout, 1767225600, 1767225600, "cli", "openai", path.join(root, "project"), "Sync2 E2E Thread", "{}", "never", 1, 1, 0, "1.0", "portable hello", "portable hello", 1767225600000, 1767225600000, 1767225600, 1767225600000); + .run(id, rollout, 1767225600, 1767225600, "cli", "openai", path.join(root, "project"), "Codex Sync E2E Thread", "{}", "never", 1, 1, 0, "1.0", "portable hello", "portable hello", 1767225600000, 1767225600000, 1767225600, 1767225600000); db.close(); - fs.writeFileSync(path.join(homeInfo.home, "session_index.jsonl"), `${JSON.stringify({ id, thread_name: "Sync2 E2E Thread", updated_at: "2026-01-01T00:00:00.000Z" })}\n`); + fs.writeFileSync(path.join(homeInfo.home, "session_index.jsonl"), `${JSON.stringify({ id, thread_name: "Codex Sync E2E Thread", updated_at: "2026-01-01T00:00:00.000Z" })}\n`); return rollout; } @@ -131,9 +131,9 @@ try { const first = run(["sync", "--config", configA]); assert.equal(first.conversationsPushed, 1); assert.ok(first.skillFilesPushed >= 1); - const projectCatalog = JSON.parse(fs.readFileSync(path.join(vault, ".sync2", "project-catalogs", "win-a.json"), "utf8")); + const projectCatalog = JSON.parse(fs.readFileSync(path.join(vault, ".codex-sync", "project-catalogs", "win-a.json"), "utf8")); assert.equal(projectCatalog.projects.find((project) => project.path === path.join(root, "project")).tasks.length, 1); - const selectionFile = path.join(vault, ".sync2", "selections", "win-a.json"); + const selectionFile = path.join(vault, ".codex-sync", "selections", "win-a.json"); const headFile = path.join(vault, "conversations", id, "heads", "win-a.jsonl"); const canonicalFile = path.join(vault, "conversations", id, "canonical.jsonl"); const unchangedBefore = [selectionFile, headFile, canonicalFile].map((file) => fs.statSync(file).mtimeMs); @@ -157,7 +157,7 @@ try { assert.ok(rolloutB); assert.equal(fs.readFileSync(rolloutB, "utf8"), fs.readFileSync(rolloutA, "utf8")); assert.equal(fs.readFileSync(path.join(b.home, "skills", "portable-skill", "SKILL.md"), "utf8"), "version a1\n"); - const reportB = JSON.parse(fs.readFileSync(path.join(vault, ".sync2", "device-reports", "mac-b.json"), "utf8")); + const reportB = JSON.parse(fs.readFileSync(path.join(vault, ".codex-sync", "device-reports", "mac-b.json"), "utf8")); assert.equal(reportB.selectedConversations[0].rolloutExists, true); assert.equal(reportB.selectedConversations[0].indexedInStateDb, true); assert.ok(reportB.skillSources[0].files >= 1); @@ -165,11 +165,11 @@ try { assert.equal(reportB.lastRun.summary.conversationsPulled, pullB.conversationsPulled); assert.equal(reportB.lastRun.summary.skillFilesPulled, pullB.skillFilesPulled); const dbB = new DatabaseSync(path.join(b.home, "state_5.sqlite")); - assert.equal(dbB.prepare("SELECT title FROM threads WHERE id=?").get(id).title, "Sync2 E2E Thread"); + assert.equal(dbB.prepare("SELECT title FROM threads WHERE id=?").get(id).title, "Codex Sync E2E Thread"); assert.equal(dbB.prepare("SELECT cwd FROM threads WHERE id=?").get(id).cwd, mappedProject); dbB.close(); const appDbB = new DatabaseSync(path.join(b.home, "sqlite", "codex-dev.db")); - assert.equal(appDbB.prepare("SELECT display_title FROM local_thread_catalog WHERE thread_id=?").get(id).display_title, "Sync2 E2E Thread"); + assert.equal(appDbB.prepare("SELECT display_title FROM local_thread_catalog WHERE thread_id=?").get(id).display_title, "Codex Sync E2E Thread"); assert.equal(appDbB.prepare("SELECT catalog_revision FROM local_thread_catalog_metadata WHERE id=1").get().catalog_revision, 1); appDbB.close(); @@ -310,22 +310,22 @@ try { if (process.platform === "win32") { const bootstrapVault = path.join(root, "bootstrap-maintenance-vault"); - const bootstrapSkill = path.join(bootstrapVault, "skills", "codex", "sync2"); + const bootstrapSkill = path.join(bootstrapVault, "skills", "codex", "codex-sync"); fs.mkdirSync(path.dirname(bootstrapSkill), { recursive: true }); fs.cpSync(SKILL_ROOT, bootstrapSkill, { recursive: true }); - fs.mkdirSync(path.join(bootstrapVault, ".sync2"), { recursive: true }); - fs.writeFileSync(path.join(bootstrapVault, ".sync2", "maintenance.json"), `${JSON.stringify({ enabled: true, reason: "bootstrap test" })}\n`); + fs.mkdirSync(path.join(bootstrapVault, ".codex-sync"), { recursive: true }); + fs.writeFileSync(path.join(bootstrapVault, ".codex-sync", "maintenance.json"), `${JSON.stringify({ enabled: true, reason: "bootstrap test" })}\n`); const bootstrapHome = makeHome("home-bootstrap-win"); const bootstrapConfig = path.join(root, "profile-bootstrap-win", "config.json"); execFileSync("powershell.exe", [ - "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path.join(SKILL_ROOT, "scripts", "bootstrap-sync2.ps1"), + "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", path.join(SKILL_ROOT, "scripts", "bootstrap-codex-sync.ps1"), "-Vault", bootstrapVault, "-Device", "bootstrap-win", "-CodexHome", bootstrapHome.home, "-Config", bootstrapConfig, ], { stdio: "ignore", env: { ...process.env, NODE_NO_WARNINGS: "1" } }); assert.ok(fs.existsSync(bootstrapConfig)); - assert.ok(fs.existsSync(path.join(bootstrapHome.home, "skills", "sync2", "scripts", "sync2.mjs"))); + assert.ok(fs.existsSync(path.join(bootstrapHome.home, "skills", "codex-sync", "scripts", "codexsync.mjs"))); const bootstrapLastRun = JSON.parse(fs.readFileSync(path.join(path.dirname(bootstrapConfig), "runs", "bootstrap-win-last.json"), "utf8")); assert.equal(bootstrapLastRun.ok, true); - assert.ok(fs.existsSync(path.join(bootstrapVault, ".sync2", "maintenance.json"))); + assert.ok(fs.existsSync(path.join(bootstrapVault, ".codex-sync", "maintenance.json"))); } const gitHome = makeHome("home-git"); @@ -335,14 +335,14 @@ try { const gitVault = path.join(root, "git-vault"); fs.mkdirSync(gitVault, { recursive: true }); execFileSync("git", ["init", "-b", "main"], { cwd: gitVault, stdio: "ignore" }); - execFileSync("git", ["config", "user.name", "Sync2 Test"], { cwd: gitVault }); - execFileSync("git", ["config", "user.email", "sync2-test@example.invalid"], { cwd: gitVault }); + execFileSync("git", ["config", "user.name", "Codex Sync Test"], { cwd: gitVault }); + execFileSync("git", ["config", "user.email", "codex-sync-test@example.invalid"], { cwd: gitVault }); const gitConfig = path.join(root, "profile-git", "config.json"); run(["init", "--vault", gitVault, "--transport", "git", "--device", "git-device", "--codex-home", gitHome.home, "--config", gitConfig]); const gitSync = run(["sync", "--config", gitConfig]); assert.ok(gitSync.skillFilesPushed >= 1); const gitLog = execFileSync("git", ["log", "-1", "--pretty=%s"], { cwd: gitVault, encoding: "utf8" }); - assert.match(gitLog, /^sync2: git-device/); + assert.match(gitLog, /^codex-sync: git-device/); assert.ok(fs.existsSync(path.join(gitVault, "skills", "codex", "git-skill", "SKILL.md"))); const relocatedVault = path.join(root, "relocated-vault"); @@ -355,5 +355,5 @@ try { console.error(`E2E FAILED; artifacts kept at ${root}`); throw error; } finally { - if (process.exitCode !== 1 && !process.env.SYNC2_KEEP_TEST) fs.rmSync(root, { recursive: true, force: true }); + if (process.exitCode !== 1 && !process.env.CODEX_SYNC_KEEP_TEST) fs.rmSync(root, { recursive: true, force: true }); }