opencode-heartbeat-approval 0.3.0 → 0.3.1

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/dist/index.js CHANGED
@@ -13,6 +13,7 @@ var __export = (target, all) => {
13
13
  // src/plugin.ts
14
14
  import { readFile } from "fs/promises";
15
15
  import { join } from "path";
16
+ import { homedir } from "os";
16
17
 
17
18
  // node_modules/zod/v4/classic/external.js
18
19
  var exports_external = {};
@@ -12337,23 +12338,17 @@ tool.schema = exports_external;
12337
12338
  // src/plugin.ts
12338
12339
  var POLL_INTERVAL_MS = 1e4;
12339
12340
  var JSON_HEADERS = { "Content-Type": "application/json" };
12340
- async function discoverRunnerUrl(directory) {
12341
+ var SESSION_MAP_PATH = join(homedir(), ".config", "opencode", "heartbeat", "session-map.json");
12342
+ async function lookupSessionMapping(sessionId) {
12341
12343
  try {
12342
- const configPath = join(directory, "heartbeat.json");
12343
- const text = await readFile(configPath, "utf-8");
12344
- const config2 = JSON.parse(text);
12345
- const web = config2?.web;
12346
- const port = web?.port;
12347
- if (typeof port === "number" && Number.isInteger(port) && port > 0 && port < 65536) {
12348
- return `http://127.0.0.1:${port}`;
12349
- }
12350
- console.warn(`[heartbeat-approval] heartbeat.json found but web.port invalid: ${JSON.stringify(port)} in ${directory}`);
12351
- return null;
12352
- } catch (err) {
12353
- if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
12354
- return null;
12344
+ const text = await readFile(SESSION_MAP_PATH, "utf-8");
12345
+ const mapping = JSON.parse(text);
12346
+ const entry = mapping[sessionId];
12347
+ if (entry && typeof entry.port === "number" && entry.port > 0 && entry.port < 65536) {
12348
+ return `http://127.0.0.1:${entry.port}`;
12355
12349
  }
12356
- console.warn(`[heartbeat-approval] Failed to read heartbeat.json in ${directory}:`, err);
12350
+ return null;
12351
+ } catch {
12357
12352
  return null;
12358
12353
  }
12359
12354
  }
@@ -12425,12 +12420,18 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12425
12420
  });
12426
12421
  }
12427
12422
  const requestType = args.type === "assistance" ? "assistance" : "approval";
12428
- const resolvedUrl = await discoverRunnerUrl(ctx.directory);
12423
+ const envPort = process.env.APPROVAL_PORT;
12424
+ let resolvedUrl = null;
12425
+ if (envPort && /^\d+$/.test(envPort)) {
12426
+ resolvedUrl = `http://127.0.0.1:${envPort}`;
12427
+ } else {
12428
+ resolvedUrl = await lookupSessionMapping(ctx.sessionID);
12429
+ }
12429
12430
  if (!resolvedUrl) {
12430
12431
  return JSON.stringify({
12431
12432
  status: "unavailable",
12432
12433
  type: requestType,
12433
- error: `Cannot discover runner: no heartbeat.json with valid web.port in ${ctx.directory}`,
12434
+ error: `No approval server found for session ${ctx.sessionID}. Ensure heartbeat runner is running for this role.`,
12434
12435
  approval_id: null
12435
12436
  });
12436
12437
  }
package/dist/plugin.js CHANGED
@@ -13,6 +13,7 @@ var __export = (target, all) => {
13
13
  // src/plugin.ts
14
14
  import { readFile } from "fs/promises";
15
15
  import { join } from "path";
16
+ import { homedir } from "os";
16
17
 
17
18
  // node_modules/zod/v4/classic/external.js
18
19
  var exports_external = {};
@@ -12337,23 +12338,17 @@ tool.schema = exports_external;
12337
12338
  // src/plugin.ts
12338
12339
  var POLL_INTERVAL_MS = 1e4;
12339
12340
  var JSON_HEADERS = { "Content-Type": "application/json" };
12340
- async function discoverRunnerUrl(directory) {
12341
+ var SESSION_MAP_PATH = join(homedir(), ".config", "opencode", "heartbeat", "session-map.json");
12342
+ async function lookupSessionMapping(sessionId) {
12341
12343
  try {
12342
- const configPath = join(directory, "heartbeat.json");
12343
- const text = await readFile(configPath, "utf-8");
12344
- const config2 = JSON.parse(text);
12345
- const web = config2?.web;
12346
- const port = web?.port;
12347
- if (typeof port === "number" && Number.isInteger(port) && port > 0 && port < 65536) {
12348
- return `http://127.0.0.1:${port}`;
12349
- }
12350
- console.warn(`[heartbeat-approval] heartbeat.json found but web.port invalid: ${JSON.stringify(port)} in ${directory}`);
12351
- return null;
12352
- } catch (err) {
12353
- if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
12354
- return null;
12344
+ const text = await readFile(SESSION_MAP_PATH, "utf-8");
12345
+ const mapping = JSON.parse(text);
12346
+ const entry = mapping[sessionId];
12347
+ if (entry && typeof entry.port === "number" && entry.port > 0 && entry.port < 65536) {
12348
+ return `http://127.0.0.1:${entry.port}`;
12355
12349
  }
12356
- console.warn(`[heartbeat-approval] Failed to read heartbeat.json in ${directory}:`, err);
12350
+ return null;
12351
+ } catch {
12357
12352
  return null;
12358
12353
  }
12359
12354
  }
@@ -12425,12 +12420,18 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12425
12420
  });
12426
12421
  }
12427
12422
  const requestType = args.type === "assistance" ? "assistance" : "approval";
12428
- const resolvedUrl = await discoverRunnerUrl(ctx.directory);
12423
+ const envPort = process.env.APPROVAL_PORT;
12424
+ let resolvedUrl = null;
12425
+ if (envPort && /^\d+$/.test(envPort)) {
12426
+ resolvedUrl = `http://127.0.0.1:${envPort}`;
12427
+ } else {
12428
+ resolvedUrl = await lookupSessionMapping(ctx.sessionID);
12429
+ }
12429
12430
  if (!resolvedUrl) {
12430
12431
  return JSON.stringify({
12431
12432
  status: "unavailable",
12432
12433
  type: requestType,
12433
- error: `Cannot discover runner: no heartbeat.json with valid web.port in ${ctx.directory}`,
12434
+ error: `No approval server found for session ${ctx.sessionID}. Ensure heartbeat runner is running for this role.`,
12434
12435
  approval_id: null
12435
12436
  });
12436
12437
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-heartbeat-approval",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "OpenCode plugin providing request_human_input MCP tool for Heartbeat pipeline (approval + assistance)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/plugin.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
+ import { homedir } from "node:os";
3
4
  import { tool } from "@opencode-ai/plugin";
4
5
  import type { PluginInput } from "@opencode-ai/plugin";
5
6
 
@@ -26,28 +27,18 @@ interface ConflictResponse {
26
27
  existing_approval_id: string;
27
28
  }
28
29
 
29
- /**
30
- * Auto-discover runner URL from the session's directory.
31
- * Reads heartbeat.json in ctx.directory to find web.port.
32
- * Returns null if not found (ENOENT is expected — not a heartbeat dir).
33
- */
34
- async function discoverRunnerUrl(directory: string): Promise<string | null> {
30
+ const SESSION_MAP_PATH = join(homedir(), ".config", "opencode", "heartbeat", "session-map.json");
31
+
32
+ async function lookupSessionMapping(sessionId: string): Promise<string | null> {
35
33
  try {
36
- const configPath = join(directory, "heartbeat.json");
37
- const text = await readFile(configPath, "utf-8");
38
- const config = JSON.parse(text) as Record<string, unknown>;
39
- const web = config?.web as Record<string, unknown> | undefined;
40
- const port = web?.port;
41
- if (typeof port === "number" && Number.isInteger(port) && port > 0 && port < 65536) {
42
- return `http://127.0.0.1:${port}`;
34
+ const text = await readFile(SESSION_MAP_PATH, "utf-8");
35
+ const mapping = JSON.parse(text) as Record<string, { port: number; role: string; created_at: string }>;
36
+ const entry = mapping[sessionId];
37
+ if (entry && typeof entry.port === "number" && entry.port > 0 && entry.port < 65536) {
38
+ return `http://127.0.0.1:${entry.port}`;
43
39
  }
44
- console.warn(`[heartbeat-approval] heartbeat.json found but web.port invalid: ${JSON.stringify(port)} in ${directory}`);
45
40
  return null;
46
- } catch (err: unknown) {
47
- if (err && typeof err === "object" && "code" in err && (err as { code: string }).code === "ENOENT") {
48
- return null;
49
- }
50
- console.warn(`[heartbeat-approval] Failed to read heartbeat.json in ${directory}:`, err);
41
+ } catch {
51
42
  return null;
52
43
  }
53
44
  }
@@ -137,12 +128,18 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
137
128
  }
138
129
  const requestType = (args.type === "assistance" ? "assistance" : "approval") as "approval" | "assistance";
139
130
 
140
- const resolvedUrl = await discoverRunnerUrl(ctx.directory);
131
+ const envPort = process.env.APPROVAL_PORT;
132
+ let resolvedUrl: string | null = null;
133
+ if (envPort && /^\d+$/.test(envPort)) {
134
+ resolvedUrl = `http://127.0.0.1:${envPort}`;
135
+ } else {
136
+ resolvedUrl = await lookupSessionMapping(ctx.sessionID);
137
+ }
141
138
  if (!resolvedUrl) {
142
139
  return JSON.stringify({
143
140
  status: "unavailable",
144
141
  type: requestType,
145
- error: `Cannot discover runner: no heartbeat.json with valid web.port in ${ctx.directory}`,
142
+ error: `No approval server found for session ${ctx.sessionID}. Ensure heartbeat runner is running for this role.`,
146
143
  approval_id: null,
147
144
  });
148
145
  }