opencode-heartbeat-approval 0.2.1 → 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 +36 -15
- package/dist/plugin.js +36 -15
- package/package.json +1 -1
- package/src/plugin.ts +38 -21
package/dist/index.js
CHANGED
|
@@ -10,6 +10,11 @@ var __export = (target, all) => {
|
|
|
10
10
|
});
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// src/plugin.ts
|
|
14
|
+
import { readFile } from "fs/promises";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
|
|
13
18
|
// node_modules/zod/v4/classic/external.js
|
|
14
19
|
var exports_external = {};
|
|
15
20
|
__export(exports_external, {
|
|
@@ -12331,23 +12336,26 @@ function tool(input) {
|
|
|
12331
12336
|
}
|
|
12332
12337
|
tool.schema = exports_external;
|
|
12333
12338
|
// src/plugin.ts
|
|
12334
|
-
var DEFAULT_RUNNER_URL = "http://127.0.0.1:3210";
|
|
12335
12339
|
var POLL_INTERVAL_MS = 1e4;
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12340
|
+
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
12341
|
+
var SESSION_MAP_PATH = join(homedir(), ".config", "opencode", "heartbeat", "session-map.json");
|
|
12342
|
+
async function lookupSessionMapping(sessionId) {
|
|
12343
|
+
try {
|
|
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}`;
|
|
12349
|
+
}
|
|
12350
|
+
return null;
|
|
12351
|
+
} catch {
|
|
12352
|
+
return null;
|
|
12344
12353
|
}
|
|
12345
|
-
return headers;
|
|
12346
12354
|
}
|
|
12347
12355
|
async function createGate(runnerUrl, params) {
|
|
12348
12356
|
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12349
12357
|
method: "POST",
|
|
12350
|
-
headers:
|
|
12358
|
+
headers: JSON_HEADERS,
|
|
12351
12359
|
body: JSON.stringify(params)
|
|
12352
12360
|
});
|
|
12353
12361
|
if (resp.status === 409) {
|
|
@@ -12362,7 +12370,7 @@ async function createGate(runnerUrl, params) {
|
|
|
12362
12370
|
async function pollGate(runnerUrl) {
|
|
12363
12371
|
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12364
12372
|
method: "GET",
|
|
12365
|
-
headers:
|
|
12373
|
+
headers: JSON_HEADERS
|
|
12366
12374
|
});
|
|
12367
12375
|
if (!resp.ok) {
|
|
12368
12376
|
const text = await resp.text().catch(() => "");
|
|
@@ -12398,8 +12406,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12398
12406
|
prompt: tool.schema.string().describe("What you need from the human \u2014 be specific and concise"),
|
|
12399
12407
|
type: tool.schema.string().optional().describe("Type of request: 'approval' (need permission) or 'assistance' (need human help). Default: 'approval'"),
|
|
12400
12408
|
artifacts: tool.schema.string().optional().describe("Comma-separated list of relevant file paths or artifact names"),
|
|
12401
|
-
deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)")
|
|
12402
|
-
runner_url: tool.schema.string().optional().describe("URL of this role's heartbeat runner (e.g., http://127.0.0.1:3211). Required for multi-role setups.")
|
|
12409
|
+
deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)")
|
|
12403
12410
|
},
|
|
12404
12411
|
async execute(args, ctx) {
|
|
12405
12412
|
const artifacts = args.artifacts ? args.artifacts.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
@@ -12413,7 +12420,21 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12413
12420
|
});
|
|
12414
12421
|
}
|
|
12415
12422
|
const requestType = args.type === "assistance" ? "assistance" : "approval";
|
|
12416
|
-
const
|
|
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
|
+
}
|
|
12430
|
+
if (!resolvedUrl) {
|
|
12431
|
+
return JSON.stringify({
|
|
12432
|
+
status: "unavailable",
|
|
12433
|
+
type: requestType,
|
|
12434
|
+
error: `No approval server found for session ${ctx.sessionID}. Ensure heartbeat runner is running for this role.`,
|
|
12435
|
+
approval_id: null
|
|
12436
|
+
});
|
|
12437
|
+
}
|
|
12417
12438
|
let gateResult;
|
|
12418
12439
|
try {
|
|
12419
12440
|
gateResult = await createGate(resolvedUrl, {
|
package/dist/plugin.js
CHANGED
|
@@ -10,6 +10,11 @@ var __export = (target, all) => {
|
|
|
10
10
|
});
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// src/plugin.ts
|
|
14
|
+
import { readFile } from "fs/promises";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
|
|
13
18
|
// node_modules/zod/v4/classic/external.js
|
|
14
19
|
var exports_external = {};
|
|
15
20
|
__export(exports_external, {
|
|
@@ -12331,23 +12336,26 @@ function tool(input) {
|
|
|
12331
12336
|
}
|
|
12332
12337
|
tool.schema = exports_external;
|
|
12333
12338
|
// src/plugin.ts
|
|
12334
|
-
var DEFAULT_RUNNER_URL = "http://127.0.0.1:3210";
|
|
12335
12339
|
var POLL_INTERVAL_MS = 1e4;
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12340
|
+
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
12341
|
+
var SESSION_MAP_PATH = join(homedir(), ".config", "opencode", "heartbeat", "session-map.json");
|
|
12342
|
+
async function lookupSessionMapping(sessionId) {
|
|
12343
|
+
try {
|
|
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}`;
|
|
12349
|
+
}
|
|
12350
|
+
return null;
|
|
12351
|
+
} catch {
|
|
12352
|
+
return null;
|
|
12344
12353
|
}
|
|
12345
|
-
return headers;
|
|
12346
12354
|
}
|
|
12347
12355
|
async function createGate(runnerUrl, params) {
|
|
12348
12356
|
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12349
12357
|
method: "POST",
|
|
12350
|
-
headers:
|
|
12358
|
+
headers: JSON_HEADERS,
|
|
12351
12359
|
body: JSON.stringify(params)
|
|
12352
12360
|
});
|
|
12353
12361
|
if (resp.status === 409) {
|
|
@@ -12362,7 +12370,7 @@ async function createGate(runnerUrl, params) {
|
|
|
12362
12370
|
async function pollGate(runnerUrl) {
|
|
12363
12371
|
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12364
12372
|
method: "GET",
|
|
12365
|
-
headers:
|
|
12373
|
+
headers: JSON_HEADERS
|
|
12366
12374
|
});
|
|
12367
12375
|
if (!resp.ok) {
|
|
12368
12376
|
const text = await resp.text().catch(() => "");
|
|
@@ -12398,8 +12406,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12398
12406
|
prompt: tool.schema.string().describe("What you need from the human \u2014 be specific and concise"),
|
|
12399
12407
|
type: tool.schema.string().optional().describe("Type of request: 'approval' (need permission) or 'assistance' (need human help). Default: 'approval'"),
|
|
12400
12408
|
artifacts: tool.schema.string().optional().describe("Comma-separated list of relevant file paths or artifact names"),
|
|
12401
|
-
deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)")
|
|
12402
|
-
runner_url: tool.schema.string().optional().describe("URL of this role's heartbeat runner (e.g., http://127.0.0.1:3211). Required for multi-role setups.")
|
|
12409
|
+
deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)")
|
|
12403
12410
|
},
|
|
12404
12411
|
async execute(args, ctx) {
|
|
12405
12412
|
const artifacts = args.artifacts ? args.artifacts.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
@@ -12413,7 +12420,21 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12413
12420
|
});
|
|
12414
12421
|
}
|
|
12415
12422
|
const requestType = args.type === "assistance" ? "assistance" : "approval";
|
|
12416
|
-
const
|
|
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
|
+
}
|
|
12430
|
+
if (!resolvedUrl) {
|
|
12431
|
+
return JSON.stringify({
|
|
12432
|
+
status: "unavailable",
|
|
12433
|
+
type: requestType,
|
|
12434
|
+
error: `No approval server found for session ${ctx.sessionID}. Ensure heartbeat runner is running for this role.`,
|
|
12435
|
+
approval_id: null
|
|
12436
|
+
});
|
|
12437
|
+
}
|
|
12417
12438
|
let gateResult;
|
|
12418
12439
|
try {
|
|
12419
12440
|
gateResult = await createGate(resolvedUrl, {
|
package/package.json
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
1
4
|
import { tool } from "@opencode-ai/plugin";
|
|
2
5
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
3
6
|
|
|
4
|
-
const DEFAULT_RUNNER_URL = "http://127.0.0.1:3210";
|
|
5
7
|
const POLL_INTERVAL_MS = 10_000;
|
|
8
|
+
const JSON_HEADERS: Record<string, string> = { "Content-Type": "application/json" };
|
|
6
9
|
|
|
7
10
|
interface CreateResponse {
|
|
8
11
|
approval_id: string;
|
|
@@ -18,25 +21,28 @@ interface PollResponse {
|
|
|
18
21
|
type?: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
|
-
function getRunnerUrl(override?: string): string {
|
|
22
|
-
return override ?? process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function buildHeaders(): Record<string, string> {
|
|
26
|
-
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
27
|
-
const token = process.env.HEARTBEAT_AUTH_TOKEN;
|
|
28
|
-
if (token) {
|
|
29
|
-
headers["Authorization"] = `Bearer ${token}`;
|
|
30
|
-
}
|
|
31
|
-
return headers;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
24
|
interface ConflictResponse {
|
|
35
25
|
error: string;
|
|
36
26
|
message: string;
|
|
37
27
|
existing_approval_id: string;
|
|
38
28
|
}
|
|
39
29
|
|
|
30
|
+
const SESSION_MAP_PATH = join(homedir(), ".config", "opencode", "heartbeat", "session-map.json");
|
|
31
|
+
|
|
32
|
+
async function lookupSessionMapping(sessionId: string): Promise<string | null> {
|
|
33
|
+
try {
|
|
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}`;
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
async function createGate(runnerUrl: string, params: {
|
|
41
47
|
prompt: string;
|
|
42
48
|
artifacts?: string[];
|
|
@@ -46,7 +52,7 @@ async function createGate(runnerUrl: string, params: {
|
|
|
46
52
|
}): Promise<CreateResponse | ConflictResponse> {
|
|
47
53
|
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
48
54
|
method: "POST",
|
|
49
|
-
headers:
|
|
55
|
+
headers: JSON_HEADERS,
|
|
50
56
|
body: JSON.stringify(params),
|
|
51
57
|
});
|
|
52
58
|
if (resp.status === 409) {
|
|
@@ -62,7 +68,7 @@ async function createGate(runnerUrl: string, params: {
|
|
|
62
68
|
async function pollGate(runnerUrl: string): Promise<PollResponse> {
|
|
63
69
|
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
64
70
|
method: "GET",
|
|
65
|
-
headers:
|
|
71
|
+
headers: JSON_HEADERS,
|
|
66
72
|
});
|
|
67
73
|
if (!resp.ok) {
|
|
68
74
|
const text = await resp.text().catch(() => "");
|
|
@@ -106,10 +112,6 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
106
112
|
.number()
|
|
107
113
|
.optional()
|
|
108
114
|
.describe("Hours before the request expires (default: 24, max: 168)"),
|
|
109
|
-
runner_url: tool.schema
|
|
110
|
-
.string()
|
|
111
|
-
.optional()
|
|
112
|
-
.describe("URL of this role's heartbeat runner (e.g., http://127.0.0.1:3211). Required for multi-role setups."),
|
|
113
115
|
},
|
|
114
116
|
async execute(args, ctx) {
|
|
115
117
|
const artifacts = args.artifacts
|
|
@@ -125,7 +127,22 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
125
127
|
});
|
|
126
128
|
}
|
|
127
129
|
const requestType = (args.type === "assistance" ? "assistance" : "approval") as "approval" | "assistance";
|
|
128
|
-
|
|
130
|
+
|
|
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
|
+
}
|
|
138
|
+
if (!resolvedUrl) {
|
|
139
|
+
return JSON.stringify({
|
|
140
|
+
status: "unavailable",
|
|
141
|
+
type: requestType,
|
|
142
|
+
error: `No approval server found for session ${ctx.sessionID}. Ensure heartbeat runner is running for this role.`,
|
|
143
|
+
approval_id: null,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
129
146
|
|
|
130
147
|
let gateResult: CreateResponse | ConflictResponse;
|
|
131
148
|
try {
|