sloppydisk 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +52 -0
- package/bin/sloppydisk.js +55 -0
- package/lib/patcher.js +448 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lmtlssss
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# sloppydisk
|
|
2
|
+
|
|
3
|
+
`sloppydisk` is a small reset-based continuity patcher for `@openai/codex`.
|
|
4
|
+
|
|
5
|
+
It replaces summary-style compaction with reset-style continuity so long chats can clear live context and recover continuity from an Obsidian-style graph instead of stuffing an ever-growing summary capsule back into the model.
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
- auto-compaction resets live history instead of summarizing it
|
|
10
|
+
- `/compact` follows the same reset flow
|
|
11
|
+
- continuity is written under `~/.codex/obsidian_graph`
|
|
12
|
+
- `stock` restores the original Codex binary when a safe backup exists, or repairs Codex from the official npm package if it does not, then removes the managed config block
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Codex must already be installed:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g @openai/codex
|
|
20
|
+
npm install -g sloppydisk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Package install patches Codex automatically. The explicit helper commands are only for repatching, restoring stock behavior, and inspection:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
sloppydisk patch
|
|
27
|
+
sloppydisk stock
|
|
28
|
+
sloppydisk status
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The installed package does not build Codex from source. It only does this:
|
|
32
|
+
|
|
33
|
+
1. find a matching patched binary
|
|
34
|
+
2. back up the current Codex binary once
|
|
35
|
+
3. swap in the patched binary
|
|
36
|
+
4. write the managed config block
|
|
37
|
+
|
|
38
|
+
Patched binaries are resolved in this order:
|
|
39
|
+
|
|
40
|
+
1. bundled artifact in the package
|
|
41
|
+
2. cached artifact under `~/.slopex/artifacts`
|
|
42
|
+
3. matching GitHub release asset for the current package version
|
|
43
|
+
|
|
44
|
+
## Maintainers
|
|
45
|
+
|
|
46
|
+
Release artifact builds are kept out of the shipped runtime. If you are working in the repo itself, use:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm run build-release-artifact
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The GitHub Actions release workflow can call that script and publish the resulting artifact without making normal installs compile Codex locally.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const { installSlopex, printStatus, uninstallSlopex } = require("../lib/patcher");
|
|
5
|
+
|
|
6
|
+
const TOOL_NAME = "sloppydisk";
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const [command, ...args] = process.argv.slice(2);
|
|
10
|
+
const postinstall = args.includes("--postinstall");
|
|
11
|
+
const normalized = command || "help";
|
|
12
|
+
|
|
13
|
+
switch (normalized) {
|
|
14
|
+
case "patch":
|
|
15
|
+
case "install":
|
|
16
|
+
await installSlopex({ postinstall });
|
|
17
|
+
break;
|
|
18
|
+
case "stock":
|
|
19
|
+
case "uninstall":
|
|
20
|
+
await uninstallSlopex();
|
|
21
|
+
break;
|
|
22
|
+
case "status":
|
|
23
|
+
await printStatus();
|
|
24
|
+
break;
|
|
25
|
+
case "help":
|
|
26
|
+
case "--help":
|
|
27
|
+
case "-h":
|
|
28
|
+
printHelp();
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
console.error(`Unknown ${TOOL_NAME} command: ${normalized}`);
|
|
32
|
+
printHelp();
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function printHelp() {
|
|
38
|
+
const invokedAs = path.basename(process.argv[1] || TOOL_NAME);
|
|
39
|
+
console.log(`${invokedAs}
|
|
40
|
+
|
|
41
|
+
Commands:
|
|
42
|
+
${invokedAs} patch Patch the installed Codex runtime in place.
|
|
43
|
+
${invokedAs} stock Restore stock Codex behavior.
|
|
44
|
+
${invokedAs} status Show patch status.
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { main };
|
|
49
|
+
|
|
50
|
+
if (require.main === module) {
|
|
51
|
+
main().catch((error) => {
|
|
52
|
+
console.error(error.message || String(error));
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
});
|
|
55
|
+
}
|
package/lib/patcher.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { Readable } = require("node:stream");
|
|
6
|
+
const { pipeline } = require("node:stream/promises");
|
|
7
|
+
const { spawnSync } = require("node:child_process");
|
|
8
|
+
|
|
9
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
10
|
+
const PACKAGE_JSON = require(path.join(PACKAGE_ROOT, "package.json"));
|
|
11
|
+
const WORK_ROOT = path.join(os.homedir(), ".slopex");
|
|
12
|
+
const BACKUP_ROOT = path.join(WORK_ROOT, "backups");
|
|
13
|
+
const ARTIFACT_ROOT = path.join(WORK_ROOT, "artifacts");
|
|
14
|
+
const BUNDLED_ARTIFACT_ROOT = path.join(PACKAGE_ROOT, "artifacts");
|
|
15
|
+
const INSTALL_STATE_PATH = path.join(WORK_ROOT, "install-state.json");
|
|
16
|
+
const CONFIG_PATH = path.join(os.homedir(), ".codex", "config.toml");
|
|
17
|
+
const CONTINUITY_ROOT = path.join(os.homedir(), ".codex", "obsidian_graph");
|
|
18
|
+
const CHILD_ENV = createChildEnv();
|
|
19
|
+
|
|
20
|
+
const SUPPORTED_CODEX_VERSIONS = ["0.117.0"];
|
|
21
|
+
|
|
22
|
+
async function installSlopex({ postinstall = false } = {}) {
|
|
23
|
+
ensureDir(WORK_ROOT);
|
|
24
|
+
|
|
25
|
+
const codexInstall = resolveCodexInstall();
|
|
26
|
+
if (!codexInstall) {
|
|
27
|
+
const message =
|
|
28
|
+
"Codex is not installed globally. Install it first with: npm install -g @openai/codex";
|
|
29
|
+
if (postinstall) {
|
|
30
|
+
console.warn(`[sloppydisk] ${message}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(message);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
assertSupportedCodexVersion(codexInstall.version);
|
|
37
|
+
|
|
38
|
+
const patchedBinary = await resolvePatchedBinary(codexInstall.version);
|
|
39
|
+
if (!patchedBinary) {
|
|
40
|
+
const message = [
|
|
41
|
+
`No prebuilt sloppydisk binary is available for Codex ${codexInstall.version} on ${artifactPlatformKey()}.`,
|
|
42
|
+
"Install a slopex release that publishes this artifact, or place a matching binary under ~/.slopex/artifacts and rerun `sloppydisk patch`."
|
|
43
|
+
].join(" ");
|
|
44
|
+
if (postinstall) {
|
|
45
|
+
console.warn(`[sloppydisk] ${message}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ensureDir(BACKUP_ROOT);
|
|
52
|
+
const backupBinary = path.join(BACKUP_ROOT, `codex-${codexInstall.version}.original`);
|
|
53
|
+
const originalBinarySha256 = sha256File(codexInstall.vendorBinary);
|
|
54
|
+
const patchedBinarySha256 = sha256File(patchedBinary);
|
|
55
|
+
if (!fs.existsSync(backupBinary)) {
|
|
56
|
+
if (originalBinarySha256 === patchedBinarySha256) {
|
|
57
|
+
const message = [
|
|
58
|
+
"Codex already appears to be patched, but slopex could not find an original backup to restore later.",
|
|
59
|
+
"Refusing to overwrite the only copy. Reinstall @openai/codex first, then rerun `sloppydisk patch`."
|
|
60
|
+
].join(" ");
|
|
61
|
+
if (postinstall) {
|
|
62
|
+
console.warn(`[sloppydisk] ${message}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
throw new Error(message);
|
|
66
|
+
}
|
|
67
|
+
fs.copyFileSync(codexInstall.vendorBinary, backupBinary);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
replaceBinaryAtomically(patchedBinary, codexInstall.vendorBinary);
|
|
71
|
+
writeManagedConfigBlock();
|
|
72
|
+
|
|
73
|
+
const state = {
|
|
74
|
+
installedAt: new Date().toISOString(),
|
|
75
|
+
slopexVersion: PACKAGE_JSON.version,
|
|
76
|
+
codexVersion: codexInstall.version,
|
|
77
|
+
codexDir: codexInstall.codexDir,
|
|
78
|
+
vendorBinary: codexInstall.vendorBinary,
|
|
79
|
+
backupBinary,
|
|
80
|
+
artifactBinary: patchedBinary,
|
|
81
|
+
originalBinarySha256,
|
|
82
|
+
patchedBinarySha256: sha256File(codexInstall.vendorBinary),
|
|
83
|
+
binarySha256: sha256File(codexInstall.vendorBinary)
|
|
84
|
+
};
|
|
85
|
+
fs.writeFileSync(INSTALL_STATE_PATH, `${JSON.stringify(state, null, 2)}\n`);
|
|
86
|
+
|
|
87
|
+
console.log(`sloppydisk patched Codex ${codexInstall.version}`);
|
|
88
|
+
console.log(`patched binary: ${codexInstall.vendorBinary}`);
|
|
89
|
+
console.log(`artifact source: ${patchedBinary}`);
|
|
90
|
+
console.log(`continuity root: ${CONTINUITY_ROOT}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function printStatus() {
|
|
94
|
+
const codexInstall = resolveCodexInstall();
|
|
95
|
+
if (!codexInstall) {
|
|
96
|
+
console.log("Codex is not installed globally.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const state = readInstallState();
|
|
101
|
+
const configText = fs.existsSync(CONFIG_PATH) ? fs.readFileSync(CONFIG_PATH, "utf8") : "";
|
|
102
|
+
const managedConfigPresent = configText.includes("# BEGIN slopex");
|
|
103
|
+
|
|
104
|
+
console.log(`codex version: ${codexInstall.version}`);
|
|
105
|
+
console.log(`codex dir: ${codexInstall.codexDir}`);
|
|
106
|
+
console.log(`codex vendor binary: ${codexInstall.vendorBinary}`);
|
|
107
|
+
console.log(`supported by sloppydisk: ${SUPPORTED_CODEX_VERSIONS.includes(codexInstall.version)}`);
|
|
108
|
+
console.log(`sloppydisk config block: ${managedConfigPresent ? "present" : "missing"}`);
|
|
109
|
+
console.log(
|
|
110
|
+
`bundled artifact: ${resolveArtifactCandidate(codexInstall.version, BUNDLED_ARTIFACT_ROOT) || "missing"}`
|
|
111
|
+
);
|
|
112
|
+
console.log(
|
|
113
|
+
`cached artifact: ${resolveArtifactCandidate(codexInstall.version, ARTIFACT_ROOT) || "missing"}`
|
|
114
|
+
);
|
|
115
|
+
console.log(`release asset url: ${releaseArtifactUrl(codexInstall.version)}`);
|
|
116
|
+
|
|
117
|
+
if (!state) {
|
|
118
|
+
console.log("sloppydisk install state: not found");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(`sloppydisk version: ${state.slopexVersion}`);
|
|
123
|
+
console.log(`artifact binary: ${state.artifactBinary || "unknown"}`);
|
|
124
|
+
console.log(`backup binary: ${state.backupBinary}`);
|
|
125
|
+
console.log(
|
|
126
|
+
`patched binary hash matches state: ${sha256File(codexInstall.vendorBinary) === state.binarySha256}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function uninstallSlopex() {
|
|
131
|
+
const state = readInstallState();
|
|
132
|
+
const codexInstall = resolveCodexInstall();
|
|
133
|
+
const targetVersion = codexInstall?.version || state?.codexVersion;
|
|
134
|
+
if (!state && !targetVersion) {
|
|
135
|
+
throw new Error("No sloppydisk install state was found and Codex is not installed globally. Nothing to uninstall.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (canRestoreFromBackup({ state, codexInstall })) {
|
|
139
|
+
replaceBinaryAtomically(state.backupBinary, state.vendorBinary);
|
|
140
|
+
removeManagedConfigBlock();
|
|
141
|
+
fs.rmSync(INSTALL_STATE_PATH, { force: true });
|
|
142
|
+
console.log(`Restored original Codex binary from ${state.backupBinary}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
reinstallOfficialCodex(targetVersion);
|
|
147
|
+
removeManagedConfigBlock();
|
|
148
|
+
fs.rmSync(INSTALL_STATE_PATH, { force: true });
|
|
149
|
+
console.log(`Reinstalled official Codex ${targetVersion} from npm.`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function assertSupportedCodexVersion(version) {
|
|
153
|
+
if (SUPPORTED_CODEX_VERSIONS.includes(version)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Codex ${version} is not supported by this sloppydisk release. Supported versions: ${SUPPORTED_CODEX_VERSIONS.join(", ")}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveCodexInstall() {
|
|
163
|
+
const npmRoot = runChecked("npm", ["root", "-g"]).stdout.trim();
|
|
164
|
+
const codexDir = path.join(npmRoot, "@openai", "codex");
|
|
165
|
+
if (!fs.existsSync(codexDir)) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const packageJsonPath = path.join(codexDir, "package.json");
|
|
170
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
171
|
+
throw new Error(`Could not find Codex package.json at ${packageJsonPath}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
175
|
+
const vendorBinary = findVendorBinary(codexDir);
|
|
176
|
+
if (!vendorBinary) {
|
|
177
|
+
throw new Error(`Could not locate the Codex vendor binary inside ${codexDir}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
npmRoot,
|
|
182
|
+
codexDir,
|
|
183
|
+
version: packageJson.version,
|
|
184
|
+
vendorBinary
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function findVendorBinary(codexDir) {
|
|
189
|
+
const root = path.join(codexDir, "node_modules");
|
|
190
|
+
if (!fs.existsSync(root)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const queue = [root];
|
|
195
|
+
while (queue.length > 0) {
|
|
196
|
+
const current = queue.shift();
|
|
197
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
198
|
+
const fullPath = path.join(current, entry.name);
|
|
199
|
+
if (entry.isDirectory()) {
|
|
200
|
+
queue.push(fullPath);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
entry.isFile() &&
|
|
205
|
+
entry.name === "codex" &&
|
|
206
|
+
fullPath.includes(`${path.sep}vendor${path.sep}`) &&
|
|
207
|
+
fullPath.includes(`${path.sep}codex${path.sep}codex`)
|
|
208
|
+
) {
|
|
209
|
+
return fullPath;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function replaceBinaryAtomically(sourceBinary, targetBinary) {
|
|
218
|
+
const tempTarget = `${targetBinary}.slopex.tmp`;
|
|
219
|
+
fs.copyFileSync(sourceBinary, tempTarget);
|
|
220
|
+
fs.chmodSync(tempTarget, 0o755);
|
|
221
|
+
fs.renameSync(tempTarget, targetBinary);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function resolvePatchedBinary(codexVersion) {
|
|
225
|
+
const localArtifact =
|
|
226
|
+
resolveArtifactCandidate(codexVersion, BUNDLED_ARTIFACT_ROOT) ||
|
|
227
|
+
resolveArtifactCandidate(codexVersion, ARTIFACT_ROOT);
|
|
228
|
+
if (localArtifact) {
|
|
229
|
+
return localArtifact;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return downloadReleaseArtifact(codexVersion, { allowFailure: true });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveArtifactCandidate(codexVersion, rootDir) {
|
|
236
|
+
const candidate = artifactBinaryPath(rootDir, codexVersion);
|
|
237
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function cachedArtifactBinaryPath(codexVersion) {
|
|
241
|
+
return artifactBinaryPath(ARTIFACT_ROOT, codexVersion);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function artifactBinaryPath(rootDir, codexVersion) {
|
|
245
|
+
return path.join(rootDir, codexVersion, artifactPlatformKey(), executableName("codex"));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function releaseArtifactName(codexVersion) {
|
|
249
|
+
return executableName(`sloppydisk-codex-${codexVersion}-${artifactPlatformKey()}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function releaseArtifactUrl(codexVersion) {
|
|
253
|
+
return `https://github.com/${repositorySlug()}/releases/download/v${PACKAGE_JSON.version}/${releaseArtifactName(codexVersion)}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function repositorySlug() {
|
|
257
|
+
const repositoryUrl =
|
|
258
|
+
typeof PACKAGE_JSON.repository === "string"
|
|
259
|
+
? PACKAGE_JSON.repository
|
|
260
|
+
: PACKAGE_JSON.repository?.url;
|
|
261
|
+
const match = repositoryUrl && repositoryUrl.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
262
|
+
if (!match) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Unable to derive GitHub repository slug from package.json repository: ${repositoryUrl}`
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return match[1];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function downloadReleaseArtifact(codexVersion, { allowFailure = false } = {}) {
|
|
271
|
+
const targetBinary = cachedArtifactBinaryPath(codexVersion);
|
|
272
|
+
const tempTarget = `${targetBinary}.download`;
|
|
273
|
+
const url = releaseArtifactUrl(codexVersion);
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
ensureDir(path.dirname(targetBinary));
|
|
277
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
278
|
+
if (!response.ok || !response.body) {
|
|
279
|
+
throw new Error(`download failed with HTTP ${response.status} for ${url}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(tempTarget));
|
|
283
|
+
fs.chmodSync(tempTarget, 0o755);
|
|
284
|
+
fs.renameSync(tempTarget, targetBinary);
|
|
285
|
+
return targetBinary;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
fs.rmSync(tempTarget, { force: true });
|
|
288
|
+
if (allowFailure) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function artifactPlatformKey() {
|
|
296
|
+
return `${process.platform}-${process.arch}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function executableName(baseName) {
|
|
300
|
+
return process.platform === "win32" ? `${baseName}.exe` : baseName;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function writeManagedConfigBlock() {
|
|
304
|
+
ensureDir(path.dirname(CONFIG_PATH));
|
|
305
|
+
const managedBlock = [
|
|
306
|
+
"# BEGIN slopex",
|
|
307
|
+
'experimental_auto_compact_mode = "reset"',
|
|
308
|
+
'experimental_obsidian_graph_root = "~/.codex/obsidian_graph"',
|
|
309
|
+
"experimental_obsidian_graph_background_agent = true",
|
|
310
|
+
"# END slopex"
|
|
311
|
+
].join("\n");
|
|
312
|
+
|
|
313
|
+
const existing = fs.existsSync(CONFIG_PATH) ? fs.readFileSync(CONFIG_PATH, "utf8") : "";
|
|
314
|
+
const next = upsertManagedBlock(existing, managedBlock);
|
|
315
|
+
fs.writeFileSync(CONFIG_PATH, `${next.trimEnd()}\n`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function removeManagedConfigBlock() {
|
|
319
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const existing = fs.readFileSync(CONFIG_PATH, "utf8");
|
|
324
|
+
const next = existing.replace(/\n?# BEGIN slopex[\s\S]*?# END slopex\n?/g, "\n");
|
|
325
|
+
fs.writeFileSync(CONFIG_PATH, `${next.trimEnd()}\n`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function upsertManagedBlock(existing, managedBlock) {
|
|
329
|
+
const stripped = existing
|
|
330
|
+
.replace(/\n?# BEGIN slopex[\s\S]*?# END slopex\n?/g, "\n")
|
|
331
|
+
.trim();
|
|
332
|
+
|
|
333
|
+
if (!stripped) {
|
|
334
|
+
return managedBlock;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const firstTableMatch = stripped.match(/^\s*\[/m);
|
|
338
|
+
if (!firstTableMatch || firstTableMatch.index === undefined) {
|
|
339
|
+
return `${stripped}\n\n${managedBlock}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const firstTableIndex = firstTableMatch.index;
|
|
343
|
+
const prefix = stripped.slice(0, firstTableIndex).trimEnd();
|
|
344
|
+
const suffix = stripped.slice(firstTableIndex).replace(/^\n+/, "");
|
|
345
|
+
|
|
346
|
+
if (!prefix) {
|
|
347
|
+
return `${managedBlock}\n\n${suffix}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return `${prefix}\n\n${managedBlock}\n\n${suffix}`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function readInstallState() {
|
|
354
|
+
if (!fs.existsSync(INSTALL_STATE_PATH)) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
return JSON.parse(fs.readFileSync(INSTALL_STATE_PATH, "utf8"));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function canRestoreFromBackup({ state, codexInstall }) {
|
|
361
|
+
if (!state || !codexInstall) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
if (codexInstall.version !== state.codexVersion) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
if (codexInstall.vendorBinary !== state.vendorBinary) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
if (!fs.existsSync(state.backupBinary)) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const expectedPatchedSha256 = state.patchedBinarySha256 || state.binarySha256;
|
|
375
|
+
const currentVendorSha256 = sha256File(codexInstall.vendorBinary);
|
|
376
|
+
if (expectedPatchedSha256 && currentVendorSha256 !== expectedPatchedSha256) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (
|
|
381
|
+
state.originalBinarySha256 &&
|
|
382
|
+
sha256File(state.backupBinary) !== state.originalBinarySha256
|
|
383
|
+
) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function reinstallOfficialCodex(version) {
|
|
391
|
+
runChecked("npm", ["install", "-g", `@openai/codex@${version}`]);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function sha256File(filePath) {
|
|
395
|
+
const hash = crypto.createHash("sha256");
|
|
396
|
+
hash.update(fs.readFileSync(filePath));
|
|
397
|
+
return hash.digest("hex");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function ensureDir(dirPath) {
|
|
401
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function createChildEnv() {
|
|
405
|
+
const cargoBin = path.join(os.homedir(), ".cargo", "bin");
|
|
406
|
+
const pathEntries = (process.env.PATH || "")
|
|
407
|
+
.split(path.delimiter)
|
|
408
|
+
.filter(Boolean);
|
|
409
|
+
if (!pathEntries.includes(cargoBin)) {
|
|
410
|
+
pathEntries.unshift(cargoBin);
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
...process.env,
|
|
414
|
+
PATH: pathEntries.join(path.delimiter)
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function runChecked(command, args, options = {}) {
|
|
419
|
+
const result = spawnSync(command, args, {
|
|
420
|
+
cwd: options.cwd,
|
|
421
|
+
env: {
|
|
422
|
+
...CHILD_ENV,
|
|
423
|
+
...(options.env || {})
|
|
424
|
+
},
|
|
425
|
+
encoding: "utf8",
|
|
426
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
427
|
+
});
|
|
428
|
+
if (result.error && !options.allowFailure) {
|
|
429
|
+
throw result.error;
|
|
430
|
+
}
|
|
431
|
+
if (!options.allowFailure && result.status !== 0) {
|
|
432
|
+
const stderr = (result.stderr || "").trim();
|
|
433
|
+
throw new Error(
|
|
434
|
+
`${command} ${args.join(" ")} failed with exit code ${result.status}${stderr ? `\n${stderr}` : ""}`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
module.exports = {
|
|
441
|
+
SUPPORTED_CODEX_VERSIONS,
|
|
442
|
+
artifactPlatformKey,
|
|
443
|
+
installSlopex,
|
|
444
|
+
printStatus,
|
|
445
|
+
releaseArtifactName,
|
|
446
|
+
releaseArtifactUrl,
|
|
447
|
+
uninstallSlopex
|
|
448
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sloppydisk",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Minimal reset-based continuity patcher for OpenAI Codex.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sloppydisk": "bin/sloppydisk.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"lib",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"postinstall": "node bin/sloppydisk.js patch --postinstall",
|
|
18
|
+
"build-release-artifact": "node scripts/build-release-artifact.cjs",
|
|
19
|
+
"smoke:packaged": "node scripts/smoke-packaged-install.cjs"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/lmtlssss/slopex.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"codex",
|
|
30
|
+
"openai",
|
|
31
|
+
"patcher",
|
|
32
|
+
"obsidian",
|
|
33
|
+
"continuity"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
}
|
|
38
|
+
}
|