opencode-immune 1.0.49 → 1.0.51

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.
Files changed (2) hide show
  1. package/dist/plugin.js +67 -67
  2. package/package.json +2 -1
package/dist/plugin.js CHANGED
@@ -1,19 +1,19 @@
1
- "use strict";
2
1
  // .opencode/plugin.ts — opencode-immune plugin
3
2
  // Hybrid single-file architecture with factory functions, explicit state, error boundaries
4
3
  // See: memory-bank/creative/creative-plugin-architecture.md (Option C)
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const client_1 = require("@opencode-ai/sdk/v2/client");
7
- const promises_1 = require("fs/promises");
8
- const path_1 = require("path");
9
- const crypto_1 = require("crypto");
10
- const os_1 = require("os");
11
- const child_process_1 = require("child_process");
4
+ import { createOpencodeClient as createOpencodeClientV2 } from "@opencode-ai/sdk/v2/client";
5
+ import { appendFile, mkdir, readFile, unlink, writeFile, stat, rm, rename, readdir, copyFile } from "fs/promises";
6
+ import { join, dirname } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { createHash } from "crypto";
9
+ import { tmpdir } from "os";
10
+ import { execFile } from "child_process";
12
11
  // ═══════════════════════════════════════════════════════════════════════════════
13
12
  // PLUGIN VERSION CHECK
14
13
  // ═══════════════════════════════════════════════════════════════════════════════
15
- const PLUGIN_VERSION = "1.0.49";
14
+ const PLUGIN_VERSION = "1.0.51";
16
15
  const PLUGIN_PACKAGE_NAME = "opencode-immune";
16
+ const PLUGIN_DIRNAME = dirname(fileURLToPath(import.meta.url));
17
17
  /**
18
18
  * Read plugin version from package.json at runtime.
19
19
  * Falls back to PLUGIN_VERSION constant if read fails.
@@ -24,12 +24,12 @@ async function getPluginVersion() {
24
24
  // dist/plugin.js → ../package.json
25
25
  // Also try direct path for when loaded from npm cache.
26
26
  const candidates = [
27
- (0, path_1.join)(__dirname, "..", "package.json"),
28
- (0, path_1.join)(__dirname, "package.json"),
27
+ join(PLUGIN_DIRNAME, "..", "package.json"),
28
+ join(PLUGIN_DIRNAME, "package.json"),
29
29
  ];
30
30
  for (const pkgPath of candidates) {
31
31
  try {
32
- const content = await (0, promises_1.readFile)(pkgPath, "utf-8");
32
+ const content = await readFile(pkgPath, "utf-8");
33
33
  const pkg = JSON.parse(content);
34
34
  if (pkg.version)
35
35
  return pkg.version;
@@ -77,7 +77,7 @@ function createState(input) {
77
77
  const { client: _client, ...runtimeInput } = input;
78
78
  return {
79
79
  input: runtimeInput,
80
- client: (0, client_1.createOpencodeClient)({
80
+ client: createOpencodeClientV2({
81
81
  baseUrl: input.serverUrl.toString(),
82
82
  directory: input.directory,
83
83
  }),
@@ -87,8 +87,8 @@ function createState(input) {
87
87
  providerRetryWatchdogs: new Map(),
88
88
  childFallbackRequests: new Map(),
89
89
  sessionErrorRetryCount: new Map(),
90
- ultraworkMarkerPath: (0, path_1.join)(input.directory, ".opencode", "state", "ultrawork-active.json"),
91
- diagnosticsLogPath: (0, path_1.join)(input.directory, ".opencode", "state", "opencode-immune-debug.log"),
90
+ ultraworkMarkerPath: join(input.directory, ".opencode", "state", "ultrawork-active.json"),
91
+ diagnosticsLogPath: join(input.directory, ".opencode", "state", "opencode-immune-debug.log"),
92
92
  lastEditAttempt: null,
93
93
  toolCallCount: 0,
94
94
  todoWriteUsed: false,
@@ -188,11 +188,11 @@ function pruneExpiredManagedSessions(state, now = Date.now()) {
188
188
  }
189
189
  async function writeDiagnosticLog(state, event, data = {}) {
190
190
  try {
191
- const cacheDir = (0, path_1.join)(state.input.directory, ".opencode", "state");
192
- await (0, promises_1.mkdir)(cacheDir, { recursive: true });
191
+ const cacheDir = join(state.input.directory, ".opencode", "state");
192
+ await mkdir(cacheDir, { recursive: true });
193
193
  await rotateDiagnosticLogIfNeeded(state.diagnosticsLogPath);
194
194
  const line = JSON.stringify({ ts: new Date().toISOString(), event, ...data });
195
- await (0, promises_1.appendFile)(state.diagnosticsLogPath, `${line}\n`, "utf-8");
195
+ await appendFile(state.diagnosticsLogPath, `${line}\n`, "utf-8");
196
196
  }
197
197
  catch {
198
198
  // diagnostics must never affect runtime behavior
@@ -200,12 +200,12 @@ async function writeDiagnosticLog(state, event, data = {}) {
200
200
  }
201
201
  async function rotateDiagnosticLogIfNeeded(logPath) {
202
202
  try {
203
- const current = await (0, promises_1.stat)(logPath);
203
+ const current = await stat(logPath);
204
204
  if (current.size < DIAGNOSTIC_LOG_MAX_BYTES)
205
205
  return;
206
206
  const rotatedPath = `${logPath}.1`;
207
- await (0, promises_1.rm)(rotatedPath, { force: true });
208
- await (0, promises_1.rename)(logPath, rotatedPath);
207
+ await rm(rotatedPath, { force: true });
208
+ await rename(logPath, rotatedPath);
209
209
  }
210
210
  catch {
211
211
  // missing log or rotation failure must never affect runtime behavior
@@ -228,10 +228,10 @@ function writePluginLog(state, level, message, extra = {}) {
228
228
  });
229
229
  }
230
230
  function writePluginLogForDirectory(directory, level, message, extra = {}) {
231
- const diagnosticsLogPath = (0, path_1.join)(directory, ".opencode", "state", "opencode-immune-debug.log");
231
+ const diagnosticsLogPath = join(directory, ".opencode", "state", "opencode-immune-debug.log");
232
232
  void (async () => {
233
233
  try {
234
- await (0, promises_1.mkdir)((0, path_1.dirname)(diagnosticsLogPath), { recursive: true });
234
+ await mkdir(dirname(diagnosticsLogPath), { recursive: true });
235
235
  await rotateDiagnosticLogIfNeeded(diagnosticsLogPath);
236
236
  const line = JSON.stringify({
237
237
  ts: new Date().toISOString(),
@@ -239,7 +239,7 @@ function writePluginLogForDirectory(directory, level, message, extra = {}) {
239
239
  message,
240
240
  ...extra,
241
241
  });
242
- await (0, promises_1.appendFile)(diagnosticsLogPath, `${line}\n`, "utf-8");
242
+ await appendFile(diagnosticsLogPath, `${line}\n`, "utf-8");
243
243
  }
244
244
  catch {
245
245
  // file logging must never affect runtime behavior
@@ -263,13 +263,13 @@ const pluginLog = {
263
263
  // ── Ultrawork Marker File ──
264
264
  async function writeUltraworkMarker(state) {
265
265
  try {
266
- const dir = (0, path_1.join)(state.input.directory, ".opencode", "state");
267
- await (0, promises_1.mkdir)(dir, { recursive: true });
266
+ const dir = join(state.input.directory, ".opencode", "state");
267
+ await mkdir(dir, { recursive: true });
268
268
  const payload = JSON.stringify({
269
269
  active: true,
270
270
  updatedAt: new Date().toISOString(),
271
271
  });
272
- await (0, promises_1.writeFile)(state.ultraworkMarkerPath, payload, "utf-8");
272
+ await writeFile(state.ultraworkMarkerPath, payload, "utf-8");
273
273
  }
274
274
  catch {
275
275
  // marker write must never affect runtime
@@ -277,7 +277,7 @@ async function writeUltraworkMarker(state) {
277
277
  }
278
278
  async function clearUltraworkMarker(state) {
279
279
  try {
280
- await (0, promises_1.unlink)(state.ultraworkMarkerPath);
280
+ await unlink(state.ultraworkMarkerPath);
281
281
  }
282
282
  catch {
283
283
  // file may not exist — that's fine
@@ -285,7 +285,7 @@ async function clearUltraworkMarker(state) {
285
285
  }
286
286
  async function isUltraworkMarkerActive(state) {
287
287
  try {
288
- const raw = await (0, promises_1.readFile)(state.ultraworkMarkerPath, "utf-8");
288
+ const raw = await readFile(state.ultraworkMarkerPath, "utf-8");
289
289
  const parsed = JSON.parse(raw);
290
290
  return parsed?.active === true;
291
291
  }
@@ -742,8 +742,8 @@ function compositeChatMessage(handlers) {
742
742
  */
743
743
  async function parseTasksFile(directory) {
744
744
  try {
745
- const tasksPath = (0, path_1.join)(directory, "memory-bank", "tasks.md");
746
- const content = await (0, promises_1.readFile)(tasksPath, "utf-8");
745
+ const tasksPath = join(directory, "memory-bank", "tasks.md");
746
+ const content = await readFile(tasksPath, "utf-8");
747
747
  // Check for active task
748
748
  if (!content.includes("## Active Task") ||
749
749
  content.includes("No active tasks")) {
@@ -815,7 +815,7 @@ const DEFAULT_HARNESS_REPO = "gendoor/opencode-immune-harness";
815
815
  async function parseDotEnv(filePath) {
816
816
  const result = {};
817
817
  try {
818
- const content = await (0, promises_1.readFile)(filePath, "utf-8");
818
+ const content = await readFile(filePath, "utf-8");
819
819
  for (const line of content.split("\n")) {
820
820
  const trimmed = line.trim();
821
821
  if (!trimmed || trimmed.startsWith("#"))
@@ -849,13 +849,13 @@ async function resolveEnvValue(directory, key) {
849
849
  if (process.env[key])
850
850
  return process.env[key];
851
851
  // 2. Per-project .env
852
- const projectEnv = await parseDotEnv((0, path_1.join)(directory, ".env"));
852
+ const projectEnv = await parseDotEnv(join(directory, ".env"));
853
853
  if (projectEnv[key])
854
854
  return projectEnv[key];
855
855
  // 3. Global config
856
856
  const home = process.env.HOME || process.env.USERPROFILE || "";
857
857
  if (home) {
858
- const globalEnv = await parseDotEnv((0, path_1.join)(home, ".config", "opencode-immune", ".env"));
858
+ const globalEnv = await parseDotEnv(join(home, ".config", "opencode-immune", ".env"));
859
859
  if (globalEnv[key])
860
860
  return globalEnv[key];
861
861
  }
@@ -913,8 +913,8 @@ async function fetchLatestHarnessRelease(directory, repo, token) {
913
913
  */
914
914
  async function readLocalHarnessVersion(directory) {
915
915
  try {
916
- const versionPath = (0, path_1.join)(directory, ".opencode", HARNESS_VERSION_FILE);
917
- const content = await (0, promises_1.readFile)(versionPath, "utf-8");
916
+ const versionPath = join(directory, ".opencode", HARNESS_VERSION_FILE);
917
+ const content = await readFile(versionPath, "utf-8");
918
918
  return content.trim() || null;
919
919
  }
920
920
  catch {
@@ -938,8 +938,8 @@ async function downloadHarnessAsset(assetUrl, token) {
938
938
  throw new Error(`Download failed: ${resp.status} ${resp.statusText}`);
939
939
  }
940
940
  const buffer = Buffer.from(await resp.arrayBuffer());
941
- const tempPath = (0, path_1.join)((0, os_1.tmpdir)(), `harness-${Date.now()}.tar.gz`);
942
- await (0, promises_1.writeFile)(tempPath, buffer);
941
+ const tempPath = join(tmpdir(), `harness-${Date.now()}.tar.gz`);
942
+ await writeFile(tempPath, buffer);
943
943
  return tempPath;
944
944
  }
945
945
  /**
@@ -948,7 +948,7 @@ async function downloadHarnessAsset(assetUrl, token) {
948
948
  */
949
949
  function extractTarGz(archivePath, destDir) {
950
950
  return new Promise((resolve, reject) => {
951
- (0, child_process_1.execFile)("tar", ["xzf", archivePath, "-C", destDir], (err) => {
951
+ execFile("tar", ["xzf", archivePath, "-C", destDir], (err) => {
952
952
  if (err)
953
953
  reject(err);
954
954
  else
@@ -963,22 +963,22 @@ function extractTarGz(archivePath, destDir) {
963
963
  */
964
964
  async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
965
965
  const effectiveRoot = rootDest ?? dest;
966
- const entries = await (0, promises_1.readdir)(src, { withFileTypes: true });
967
- await (0, promises_1.mkdir)(dest, { recursive: true });
966
+ const entries = await readdir(src, { withFileTypes: true });
967
+ await mkdir(dest, { recursive: true });
968
968
  for (const entry of entries) {
969
969
  // Skip files only at the root destination level
970
970
  if (skipRootFiles && dest === effectiveRoot && entry.name === ".gitignore") {
971
971
  pluginLog.info(`[opencode-immune] Harness sync: skipping root .gitignore`);
972
972
  continue;
973
973
  }
974
- const srcPath = (0, path_1.join)(src, entry.name);
975
- const destPath = (0, path_1.join)(dest, entry.name);
974
+ const srcPath = join(src, entry.name);
975
+ const destPath = join(dest, entry.name);
976
976
  if (entry.isDirectory()) {
977
977
  await copyDirRecursive(srcPath, destPath, skipRootFiles, effectiveRoot);
978
978
  }
979
979
  else {
980
- await (0, promises_1.mkdir)((0, path_1.dirname)(destPath), { recursive: true });
981
- await (0, promises_1.copyFile)(srcPath, destPath);
980
+ await mkdir(dirname(destPath), { recursive: true });
981
+ await copyFile(srcPath, destPath);
982
982
  }
983
983
  }
984
984
  }
@@ -987,8 +987,8 @@ async function copyDirRecursive(src, dest, skipRootFiles, rootDest) {
987
987
  */
988
988
  async function fileHash(filePath) {
989
989
  try {
990
- const content = await (0, promises_1.readFile)(filePath);
991
- return (0, crypto_1.createHash)("sha256").update(content).digest("hex");
990
+ const content = await readFile(filePath);
991
+ return createHash("sha256").update(content).digest("hex");
992
992
  }
993
993
  catch {
994
994
  return "";
@@ -1030,13 +1030,13 @@ async function syncHarness(state) {
1030
1030
  }
1031
1031
  pluginLog.info(`[opencode-immune] Harness sync: updating ${localVersion ?? "(none)"} → ${release.tagName}`);
1032
1032
  // 3. Hash opencode.json before update
1033
- const configPath = (0, path_1.join)(state.input.directory, "opencode.json");
1033
+ const configPath = join(state.input.directory, "opencode.json");
1034
1034
  const hashBefore = await fileHash(configPath);
1035
1035
  // 4. Download asset
1036
1036
  const archivePath = await downloadHarnessAsset(release.assetUrl, token);
1037
1037
  // 5. Extract to temp dir
1038
- const extractDir = (0, path_1.join)((0, os_1.tmpdir)(), `harness-extract-${Date.now()}`);
1039
- await (0, promises_1.mkdir)(extractDir, { recursive: true });
1038
+ const extractDir = join(tmpdir(), `harness-extract-${Date.now()}`);
1039
+ await mkdir(extractDir, { recursive: true });
1040
1040
  try {
1041
1041
  await extractTarGz(archivePath, extractDir);
1042
1042
  // 6. Copy extracted files to project root (skip .gitignore — project owns its own)
@@ -1044,9 +1044,9 @@ async function syncHarness(state) {
1044
1044
  const SKIP_ROOT_FILES = new Set([".gitignore"]);
1045
1045
  await copyDirRecursive(extractDir, state.input.directory, SKIP_ROOT_FILES);
1046
1046
  // 7. Write version marker
1047
- const versionDir = (0, path_1.join)(state.input.directory, ".opencode");
1048
- await (0, promises_1.mkdir)(versionDir, { recursive: true });
1049
- await (0, promises_1.writeFile)((0, path_1.join)(versionDir, HARNESS_VERSION_FILE), release.tagName + "\n", "utf-8");
1047
+ const versionDir = join(state.input.directory, ".opencode");
1048
+ await mkdir(versionDir, { recursive: true });
1049
+ await writeFile(join(versionDir, HARNESS_VERSION_FILE), release.tagName + "\n", "utf-8");
1050
1050
  // 8. Check if opencode.json changed
1051
1051
  const hashAfter = await fileHash(configPath);
1052
1052
  if (hashBefore && hashAfter && hashBefore !== hashAfter) {
@@ -1063,11 +1063,11 @@ async function syncHarness(state) {
1063
1063
  finally {
1064
1064
  // 9. Cleanup temp files
1065
1065
  try {
1066
- await (0, promises_1.unlink)(archivePath);
1066
+ await unlink(archivePath);
1067
1067
  }
1068
1068
  catch { /* ignore */ }
1069
1069
  try {
1070
- await (0, promises_1.rm)(extractDir, { recursive: true, force: true });
1070
+ await rm(extractDir, { recursive: true, force: true });
1071
1071
  }
1072
1072
  catch { /* ignore */ }
1073
1073
  }
@@ -1598,22 +1598,22 @@ const ALL_CYCLES_COMPLETE_MARKER = "0-ULTRAWORK: ALL_CYCLES_COMPLETE";
1598
1598
  * Skips silently if progress.md doesn't exist or is trivially empty.
1599
1599
  */
1600
1600
  async function archiveProgress(directory) {
1601
- const progressPath = (0, path_1.join)(directory, "memory-bank", "progress.md");
1601
+ const progressPath = join(directory, "memory-bank", "progress.md");
1602
1602
  try {
1603
- const content = await (0, promises_1.readFile)(progressPath, "utf-8");
1603
+ const content = await readFile(progressPath, "utf-8");
1604
1604
  // Skip if empty or trivially empty
1605
1605
  if (!content.trim() || content.trim() === "# Progress") {
1606
1606
  pluginLog.info("[opencode-immune] Archive progress: nothing to archive (empty).");
1607
1607
  return;
1608
1608
  }
1609
- const archiveDir = (0, path_1.join)(directory, "memory-bank", "archive");
1610
- await (0, promises_1.mkdir)(archiveDir, { recursive: true });
1609
+ const archiveDir = join(directory, "memory-bank", "archive");
1610
+ await mkdir(archiveDir, { recursive: true });
1611
1611
  const now = new Date();
1612
1612
  const dateStr = now.toISOString().slice(0, 10); // YYYY-MM-DD
1613
1613
  const ts = Math.floor(now.getTime() / 1000);
1614
1614
  const archiveName = `progress-${dateStr}-${ts}.md`;
1615
- const archivePath = (0, path_1.join)(archiveDir, archiveName);
1616
- await (0, promises_1.rename)(progressPath, archivePath);
1615
+ const archivePath = join(archiveDir, archiveName);
1616
+ await rename(progressPath, archivePath);
1617
1617
  pluginLog.info(`[opencode-immune] Archive progress: moved to ${archiveName}`);
1618
1618
  }
1619
1619
  catch (err) {
@@ -1664,17 +1664,17 @@ async function buildCommitMessage(directory, diffStat) {
1664
1664
  function runGitCommit(directory) {
1665
1665
  return new Promise((resolve) => {
1666
1666
  // Stage all changes
1667
- (0, child_process_1.execFile)("git", ["add", "-A"], { cwd: directory }, (addErr) => {
1667
+ execFile("git", ["add", "-A"], { cwd: directory }, (addErr) => {
1668
1668
  if (addErr) {
1669
1669
  pluginLog.error("[opencode-immune] git add failed:", addErr.message);
1670
1670
  resolve(false);
1671
1671
  return;
1672
1672
  }
1673
1673
  // Get diff stat for commit body
1674
- (0, child_process_1.execFile)("git", ["diff", "--cached", "--stat"], { cwd: directory }, async (_diffErr, diffOut) => {
1674
+ execFile("git", ["diff", "--cached", "--stat"], { cwd: directory }, async (_diffErr, diffOut) => {
1675
1675
  const stat = (diffOut ?? "").trim();
1676
1676
  const message = await buildCommitMessage(directory, stat);
1677
- (0, child_process_1.execFile)("git", ["commit", "-m", message], { cwd: directory }, (commitErr, stdout, stderr) => {
1677
+ execFile("git", ["commit", "-m", message], { cwd: directory }, (commitErr, stdout, stderr) => {
1678
1678
  if (commitErr) {
1679
1679
  if (stderr?.includes("nothing to commit") || stdout?.includes("nothing to commit")) {
1680
1680
  pluginLog.info("[opencode-immune] git commit: nothing to commit (clean tree).");
@@ -1897,8 +1897,8 @@ async function server(input) {
1897
1897
  else {
1898
1898
  // No active task — check if backlog has pending work to start a new cycle
1899
1899
  try {
1900
- const backlogPath = (0, path_1.join)(state.input.directory, "memory-bank", "backlog.md");
1901
- const backlogContent = await (0, promises_1.readFile)(backlogPath, "utf-8");
1900
+ const backlogPath = join(state.input.directory, "memory-bank", "backlog.md");
1901
+ const backlogContent = await readFile(backlogPath, "utf-8");
1902
1902
  const hasPendingTasks = /- \[ \]/.test(backlogContent);
1903
1903
  if (hasPendingTasks) {
1904
1904
  state.autoResumeAttempted = true;
@@ -1960,7 +1960,7 @@ async function server(input) {
1960
1960
  "permission.ask": withErrorBoundary(state, "permission.ask", createPermissionAskHandler(state)),
1961
1961
  };
1962
1962
  }
1963
- exports.default = {
1963
+ export default {
1964
1964
  id: "opencode-immune",
1965
1965
  server,
1966
1966
  };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "opencode-immune",
3
- "version": "1.0.49",
3
+ "version": "1.0.51",
4
+ "type": "module",
4
5
  "description": "OpenCode plugin: session recovery, auto-retry, multi-cycle automation, context monitoring",
5
6
  "exports": {
6
7
  "./server": "./dist/plugin.js"