mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-22 05:25:08 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7766fa0868 | ||
|
|
70faeff067 | ||
|
|
7a69d72886 |
@@ -1,5 +1,60 @@
|
||||
# Changelog
|
||||
|
||||
## v3.16.2 — 2026-05-12
|
||||
|
||||
### Fixes — corrects v3.16.1
|
||||
|
||||
The v3.16.1 fix was directionally correct (plugin now reads env first, falls back to a hardcoded default) but **the narrative and the hardcoded default were both wrong**.
|
||||
|
||||
What v3.16.1 said: "OCP server moved to 3478 default in v3.14+; plugin lagged at 3456."
|
||||
What is actually true:
|
||||
- **OCP server source default has been `3456` since `593d0dc` (initial release) and has never changed.** Every line in `server.mjs`, `setup.mjs`, and the `ocp` CLI still uses `3456` as the documented and code-level default.
|
||||
- The single OCP installation observed on `3478` is the maintainer's Mac mini, whose plist was rewritten with `--port 3478` during a PR #71 dogfood smoke-test accident on 2026-05-08 (see `~/.cc-rules/memory/learnings/subagent_setup_mjs_prod_host_collision.md`). The plist drift was never reconciled back to source default, and v3.16.1 incorrectly canonised the post-accident value as if it had been a release decision.
|
||||
|
||||
This release:
|
||||
- Restores the plugin fallback to `http://127.0.0.1:3456` to match server source default.
|
||||
- Updates `openclaw.plugin.json` `configSchema.proxyUrl.default` back to `3456`.
|
||||
- Restores README §"Environment Variables" `CLAUDE_PROXY_PORT` default to `3456`.
|
||||
- Plugin reads `OCP_PROXY_URL` env (full URL) first, then `CLAUDE_PROXY_PORT` env (port only), then falls back to `3456`. Hosts whose OCP plist injects a non-default port must also inject the same `CLAUDE_PROXY_PORT` into the OpenClaw plist for the plugin to follow.
|
||||
- Maintainer's Mac mini plist was reverted from `3478` to `3456` as part of this release deploy (no source change reflects this; it was a one-host correction).
|
||||
|
||||
### Governance
|
||||
|
||||
- No `cli.js` citation needed (no `server.mjs` change). ALIGNMENT.md Rule 2 not engaged.
|
||||
|
||||
## v3.16.1 — 2026-05-12 (superseded — narrative incorrect; see v3.16.2 erratum)
|
||||
|
||||
### Fixes (as shipped — note erratum above)
|
||||
|
||||
- **OCP plugin port lag** — `ocp-plugin/index.js` hard-coded `http://127.0.0.1:3456`. ~~While OCP server moved to 3478 in v3.14+,~~ **(corrected v3.16.2: no such move ever happened.)** The Mac mini's plist was on `3478` only as residue from a dogfood accident. Result: `/ocp` slash commands from the home Telegram bot returned "OCP error: fetch failed". v3.16.1 changed the plugin default to `3478` (wrong direction; v3.16.2 reverts to `3456`).
|
||||
|
||||
### Governance
|
||||
|
||||
- No `cli.js` citation needed (no `server.mjs` change). ALIGNMENT.md Rule 2 not engaged.
|
||||
|
||||
## v3.16.0 — 2026-05-10
|
||||
|
||||
### Features
|
||||
|
||||
- **`ocp doctor --check oauth`** (PR #93) — fast path that runs only the OAuth check, skipping
|
||||
version detection / from-version / git operations / models endpoint. ~50ms vs. full doctor's
|
||||
~200-500ms. Use cases: AI agent repair loops, post-`claude auth login` verify, quick health
|
||||
gates. Help text in `cmd_doctor_help` now reflects working behaviour.
|
||||
- **`ocp update --rollback --gc`** — manually garbage-collect old upgrade snapshots.
|
||||
Retention policy: keep last 5 snapshots OR snapshots newer than 30 days OR the single most
|
||||
recent (always-keep safety net). `--dry-run` previews. Successful `ocp update` runs auto-GC
|
||||
at the end of the full path; light path does not (no snapshot created there).
|
||||
|
||||
### Behavior changes
|
||||
|
||||
- After a successful cross-minor `ocp update`, the auto-GC emits `[gc] removed N old snapshots`
|
||||
to stderr if any were collected. Safe to ignore; manual gc is `ocp update --rollback --gc`.
|
||||
|
||||
### Governance
|
||||
|
||||
- No `cli.js` citation needed (no `server.mjs` change). ALIGNMENT.md Rule 2 not engaged.
|
||||
- PR #93 (--check oauth) merged separately; this release bundles it with the GC feature.
|
||||
|
||||
## v3.15.1 — 2026-05-10
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -855,7 +855,8 @@ Future `ocp update` invocations sync automatically.
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CLAUDE_PROXY_PORT` | `3456` | Listen port |
|
||||
| `CLAUDE_PROXY_PORT` | `3456` | Listen port (server-side). Also consumed by the OpenClaw `ocp-plugin` to dial the local proxy. |
|
||||
| `OCP_PROXY_URL` | *(unset)* | Plugin-side full URL override (e.g. `http://10.0.0.5:3456`). Wins over `CLAUDE_PROXY_PORT` when both are set. Read by `ocp-plugin/index.js` only — server ignores it. |
|
||||
| `CLAUDE_BIND` | `127.0.0.1` | Bind address (`0.0.0.0` for LAN access) |
|
||||
| `CLAUDE_AUTH_MODE` | `none` | Auth mode: `none`, `shared`, or `multi` |
|
||||
| `OCP_ADMIN_KEY` | *(unset)* | Admin key for key management (multi mode) |
|
||||
|
||||
@@ -709,6 +709,8 @@ Usage:
|
||||
ocp update --rollback --list List available snapshots
|
||||
ocp update --rollback <path> Restore a specific snapshot
|
||||
ocp update --rollback --dry-run Preview rollback plan
|
||||
ocp update --rollback --gc Delete old snapshots (keep last 5, or <30 days)
|
||||
ocp update --rollback --gc --dry-run Preview what would be deleted
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
+13
-3
@@ -1,9 +1,19 @@
|
||||
/**
|
||||
* OCP Plugin — registers /ocp as a native slash command in OpenClaw gateway.
|
||||
* Calls the local claude-proxy at http://127.0.0.1:3456 and formats the response.
|
||||
* Calls the local claude-proxy and formats the response.
|
||||
*
|
||||
* Port resolution (in priority order):
|
||||
* 1. OCP_PROXY_URL env (full URL, e.g. http://10.0.0.5:3456)
|
||||
* 2. CLAUDE_PROXY_PORT env (port only; localhost assumed)
|
||||
* 3. Fallback: http://127.0.0.1:3456 (OCP server source default since v1.0)
|
||||
*
|
||||
* If a particular host's OCP plist injects a non-default CLAUDE_PROXY_PORT,
|
||||
* the OpenClaw launchd plist for that host must also inject the same
|
||||
* CLAUDE_PROXY_PORT into the plugin's env, or the plugin will fall back to
|
||||
* 3456 and miss the server.
|
||||
*/
|
||||
|
||||
const PROXY = "http://127.0.0.1:3456";
|
||||
const PROXY = process.env.OCP_PROXY_URL
|
||||
|| (process.env.CLAUDE_PROXY_PORT ? `http://127.0.0.1:${process.env.CLAUDE_PROXY_PORT}` : "http://127.0.0.1:3456");
|
||||
|
||||
// Wrap output in monospace code block for Telegram/Discord alignment
|
||||
function mono(text) { return "```\n" + text + "\n```"; }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "ocp",
|
||||
"name": "OCP Commands",
|
||||
"description": "Slash commands for the OpenClaw Proxy — /ocp usage, /ocp settings, /ocp health, etc.",
|
||||
"version": "3.12.0",
|
||||
"version": "3.16.2",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -10,7 +10,7 @@
|
||||
"proxyUrl": {
|
||||
"type": "string",
|
||||
"default": "http://127.0.0.1:3456",
|
||||
"description": "URL of the Claude proxy"
|
||||
"description": "URL of the Claude proxy. Overridable via OCP_PROXY_URL or CLAUDE_PROXY_PORT env."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "open-claude-proxy",
|
||||
"version": "3.15.1",
|
||||
"version": "3.16.2",
|
||||
"description": "OCP (Open Claude Proxy) — use your Claude Pro/Max subscription as an OpenAI-compatible API for any IDE. Works with Cline, OpenCode, Aider, Continue.dev, OpenClaw, and more.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdirSync, writeFileSync, readFileSync, copyFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
||||
import { mkdirSync, writeFileSync, readFileSync, copyFileSync, existsSync, readdirSync, statSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
export function writeSnapshot({ homeDir, fromCommit, fromVersion, toVersion, extraFiles = [] }) {
|
||||
@@ -50,3 +50,66 @@ export function listSnapshots(homeDir) {
|
||||
.map(name => ({ name, path: join(root, name), mtime: statSync(join(root, name)).mtimeMs }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage-collect old upgrade snapshots.
|
||||
*
|
||||
* Retention rule (a snapshot is KEPT if any of these is true):
|
||||
* - It is among the last `keepCount` snapshots (sorted oldest→newest)
|
||||
* - Its timestamp is within `keepDays` of `now`
|
||||
* - It is the single most-recent snapshot (always-keep safety net)
|
||||
*
|
||||
* @param {string} homeDir - Root containing ~/.ocp/
|
||||
* @param {object} opts
|
||||
* @param {number} [opts.keepCount=5] - Minimum count to keep
|
||||
* @param {number} [opts.keepDays=30] - Keep snapshots newer than N days
|
||||
* @param {boolean} [opts.dryRun=false] - If true, report plan but don't delete
|
||||
* @param {Date} [opts.now=new Date()] - Override clock for testing
|
||||
* @returns {{kept: Array, removed: Array, dryRun: boolean}}
|
||||
*/
|
||||
export function gcSnapshots(homeDir, opts = {}) {
|
||||
const keepCount = opts.keepCount ?? 5;
|
||||
const keepDays = opts.keepDays ?? 30;
|
||||
const dryRun = !!opts.dryRun;
|
||||
const now = opts.now || new Date();
|
||||
|
||||
const all = listSnapshots(homeDir); // sorted oldest→newest
|
||||
if (all.length === 0) return { kept: [], removed: [], dryRun };
|
||||
if (all.length === 1) return { kept: all, removed: [], dryRun }; // always keep most recent
|
||||
|
||||
const cutoffMs = now.getTime() - keepDays * 24 * 60 * 60 * 1000;
|
||||
const lastN = new Set(all.slice(-keepCount).map(s => s.path));
|
||||
|
||||
const kept = [], removed = [];
|
||||
for (let i = 0; i < all.length; i++) {
|
||||
const s = all[i];
|
||||
const isMostRecent = i === all.length - 1;
|
||||
const isInLastN = lastN.has(s.path);
|
||||
const isWithinDays = parseSnapshotTimestamp(s.name) >= cutoffMs;
|
||||
if (isMostRecent || isInLastN || isWithinDays) {
|
||||
kept.push(s);
|
||||
} else {
|
||||
removed.push(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
for (const s of removed) {
|
||||
try {
|
||||
rmSync(s.path, { recursive: true, force: true });
|
||||
} catch (err) {
|
||||
console.error(`[snapshot] warn: could not remove ${s.path} (${err.code || err.message})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { kept, removed, dryRun };
|
||||
}
|
||||
|
||||
function parseSnapshotTimestamp(name) {
|
||||
// upgrade-snapshot-2026-05-11T08:30:00Z → epoch ms
|
||||
const m = name.match(/upgrade-snapshot-(.+)$/);
|
||||
if (!m) return 0;
|
||||
const t = Date.parse(m[1]);
|
||||
return Number.isFinite(t) ? t : 0;
|
||||
}
|
||||
|
||||
+22
-2
@@ -14,7 +14,7 @@ import { execSync } from "node:child_process";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { existsSync, copyFileSync } from "node:fs";
|
||||
import { writeSnapshot, listSnapshots, readSnapshot } from "./lib/snapshot.mjs";
|
||||
import { writeSnapshot, listSnapshots, readSnapshot, gcSnapshots } from "./lib/snapshot.mjs";
|
||||
|
||||
export async function runUpgrade(opts = {}) {
|
||||
const dryRun = !!opts.dryRun;
|
||||
@@ -154,6 +154,16 @@ async function runFullUpgrade({ doctor, opts }) {
|
||||
phases.push({ name: "post-flight", status: "skipped-mock" });
|
||||
}
|
||||
|
||||
// Auto-GC old snapshots after successful upgrade (best-effort, never throws).
|
||||
try {
|
||||
const gc = gcSnapshots(homedir(), { keepCount: 5, keepDays: 30 });
|
||||
if (gc.removed.length > 0) {
|
||||
console.error(`[gc] removed ${gc.removed.length} old snapshots; kept ${gc.kept.length}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[gc] warn: snapshot GC failed: ${e.message}`);
|
||||
}
|
||||
|
||||
return { path: "upgrade", executed: true, changed: true, snapshotPath, phases };
|
||||
} catch (err) {
|
||||
if (snapshotPath && !err.snapshotPath) {
|
||||
@@ -193,6 +203,11 @@ async function runRollback(opts) {
|
||||
const homeDir = opts.homeDir || homedir();
|
||||
const snapshots = opts.mockSnapshots ?? listSnapshots(homeDir);
|
||||
|
||||
if (opts.gc) {
|
||||
const result = gcSnapshots(homeDir, { dryRun: opts.dryRun });
|
||||
return { path: opts.dryRun ? "rollback-gc-dry-run" : "rollback-gc", ...result };
|
||||
}
|
||||
|
||||
if (opts.list) {
|
||||
return { path: "rollback-list", snapshots };
|
||||
}
|
||||
@@ -295,6 +310,7 @@ if (_isMain()) {
|
||||
const yes = args.includes("--yes");
|
||||
const rollback = args.includes("--rollback");
|
||||
const list = args.includes("--list");
|
||||
const gc = args.includes("--gc");
|
||||
const targetIdx = args.indexOf("--target");
|
||||
const target = targetIdx !== -1 ? args[targetIdx + 1] : undefined;
|
||||
// First non-flag positional after --rollback is the snapshot path
|
||||
@@ -305,7 +321,7 @@ if (_isMain()) {
|
||||
if (cand && !cand.startsWith("--")) snapshotPath = cand;
|
||||
}
|
||||
try {
|
||||
const result = await runUpgrade({ dryRun, yes, rollback, list, snapshotPath, target });
|
||||
const result = await runUpgrade({ dryRun, yes, rollback, list, gc, snapshotPath, target });
|
||||
if (result.plan) for (const line of result.plan) console.log(line);
|
||||
if (result.phases) for (const p of result.phases) console.log(`[${p.name}] ${p.status}${p.cmd ? `: ${p.cmd}` : ""}`);
|
||||
if (result.steps) for (const s of result.steps) console.log(` ${s.status === "ok" ? "✓" : s.status === "skipped-mock" ? "·" : "✗"} ${s.cmd}`);
|
||||
@@ -313,6 +329,10 @@ if (_isMain()) {
|
||||
console.log(`Found ${result.snapshots.length} snapshots:`);
|
||||
for (const s of result.snapshots) console.log(` ${s.name}`);
|
||||
}
|
||||
if (result.removed && result.kept) {
|
||||
console.log(`Snapshots: kept ${result.kept.length}, ${result.dryRun ? "would remove" : "removed"} ${result.removed.length}`);
|
||||
for (const s of result.removed) console.log(` - ${s.name}`);
|
||||
}
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
console.error(`✗ ${e.message}`);
|
||||
|
||||
+59
-2
@@ -719,8 +719,8 @@ test("upgrade full path executes 5 phases", async () => {
|
||||
});
|
||||
|
||||
// ── Snapshot Tests ──
|
||||
import { writeSnapshot, readSnapshot, listSnapshots } from "./scripts/lib/snapshot.mjs";
|
||||
import { mkdtempSync, rmSync, mkdirSync as tMkdirSync, writeFileSync as testWriteFile } from "node:fs";
|
||||
import { writeSnapshot, readSnapshot, listSnapshots, gcSnapshots } from "./scripts/lib/snapshot.mjs";
|
||||
import { mkdtempSync, rmSync, mkdirSync as tMkdirSync, writeFileSync as testWriteFile, existsSync as testExistsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join as testJoin } from "node:path";
|
||||
|
||||
@@ -843,6 +843,63 @@ test("rollback latest snapshot restores files (mockExec)", async () => {
|
||||
assert.ok(result.phases.some(p => p.name === "git-checkout"));
|
||||
});
|
||||
|
||||
test("gcSnapshots keeps last N regardless of age", () => {
|
||||
const root = mkdtempSync(testJoin(tmpdir(), "ocp-gc-test-"));
|
||||
const dotOcp = testJoin(root, ".ocp");
|
||||
tMkdirSync(dotOcp, { recursive: true });
|
||||
for (const ts of ["2026-04-01T10:00:00Z", "2026-04-15T10:00:00Z", "2026-04-30T10:00:00Z", "2026-05-01T10:00:00Z", "2026-05-10T10:00:00Z"]) {
|
||||
tMkdirSync(testJoin(dotOcp, `upgrade-snapshot-${ts}`));
|
||||
}
|
||||
const result = gcSnapshots(root, { keepCount: 3, keepDays: 0, now: new Date("2026-05-11T00:00:00Z") });
|
||||
assert.equal(result.kept.length, 3);
|
||||
assert.equal(result.removed.length, 2);
|
||||
assert.ok(result.kept[0].name.includes("2026-04-30"));
|
||||
assert.ok(result.kept[2].name.includes("2026-05-10"));
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("gcSnapshots keeps snapshots newer than keepDays regardless of count", () => {
|
||||
const root = mkdtempSync(testJoin(tmpdir(), "ocp-gc-days-"));
|
||||
const dotOcp = testJoin(root, ".ocp");
|
||||
tMkdirSync(dotOcp, { recursive: true });
|
||||
for (const ts of ["2026-04-01T10:00:00Z", "2026-04-15T10:00:00Z", "2026-04-30T10:00:00Z", "2026-05-01T10:00:00Z", "2026-05-10T10:00:00Z"]) {
|
||||
tMkdirSync(testJoin(dotOcp, `upgrade-snapshot-${ts}`));
|
||||
}
|
||||
// keepCount=1 but keepDays=15 means anything from after 2026-04-26 is kept too
|
||||
const result = gcSnapshots(root, { keepCount: 1, keepDays: 15, now: new Date("2026-05-11T00:00:00Z") });
|
||||
// Kept: 2026-04-30 (within 15 days), 2026-05-01 (within 15 days), 2026-05-10 (within 15 days)
|
||||
assert.ok(result.kept.length >= 3);
|
||||
// Removed: 2026-04-01, 2026-04-15
|
||||
assert.ok(result.removed.some(s => s.name.includes("2026-04-01")));
|
||||
});
|
||||
|
||||
test("gcSnapshots never deletes the most recent snapshot", () => {
|
||||
const root = mkdtempSync(testJoin(tmpdir(), "ocp-gc-recent-"));
|
||||
const dotOcp = testJoin(root, ".ocp");
|
||||
tMkdirSync(dotOcp, { recursive: true });
|
||||
tMkdirSync(testJoin(dotOcp, "upgrade-snapshot-2026-01-01T10:00:00Z"));
|
||||
// Even with keepCount=0 and keepDays=0, the most recent must survive
|
||||
const result = gcSnapshots(root, { keepCount: 0, keepDays: 0, now: new Date("2026-05-11T00:00:00Z") });
|
||||
assert.equal(result.kept.length, 1);
|
||||
assert.equal(result.removed.length, 0);
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("gcSnapshots --dry-run reports plan without deleting", () => {
|
||||
const root = mkdtempSync(testJoin(tmpdir(), "ocp-gc-dryrun-"));
|
||||
const dotOcp = testJoin(root, ".ocp");
|
||||
tMkdirSync(dotOcp, { recursive: true });
|
||||
for (const ts of ["2026-04-01T10:00:00Z", "2026-04-15T10:00:00Z", "2026-05-10T10:00:00Z"]) {
|
||||
tMkdirSync(testJoin(dotOcp, `upgrade-snapshot-${ts}`));
|
||||
}
|
||||
const result = gcSnapshots(root, { keepCount: 1, keepDays: 0, dryRun: true, now: new Date("2026-05-11T00:00:00Z") });
|
||||
assert.equal(result.dryRun, true);
|
||||
assert.equal(result.removed.length, 2);
|
||||
// Files still exist
|
||||
assert.ok(testExistsSync(testJoin(dotOcp, "upgrade-snapshot-2026-04-01T10:00:00Z")));
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
// ── Doctor --check oauth fast path tests ──
|
||||
console.log("\nDoctor --check oauth:");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user