opencode-heartbeat-approval 0.2.0 → 0.3.0
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 +42 -20
- package/dist/plugin.js +42 -20
- package/package.json +1 -1
- package/src/plugin.ts +47 -22
package/dist/index.js
CHANGED
|
@@ -10,6 +10,10 @@ 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
|
+
|
|
13
17
|
// node_modules/zod/v4/classic/external.js
|
|
14
18
|
var exports_external = {};
|
|
15
19
|
__export(exports_external, {
|
|
@@ -12331,23 +12335,32 @@ function tool(input) {
|
|
|
12331
12335
|
}
|
|
12332
12336
|
tool.schema = exports_external;
|
|
12333
12337
|
// src/plugin.ts
|
|
12334
|
-
var DEFAULT_RUNNER_URL = "http://127.0.0.1:3210";
|
|
12335
12338
|
var POLL_INTERVAL_MS = 1e4;
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
}
|
|
12347
|
-
|
|
12348
|
-
|
|
12339
|
+
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
12340
|
+
async function discoverRunnerUrl(directory) {
|
|
12341
|
+
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;
|
|
12355
|
+
}
|
|
12356
|
+
console.warn(`[heartbeat-approval] Failed to read heartbeat.json in ${directory}:`, err);
|
|
12357
|
+
return null;
|
|
12358
|
+
}
|
|
12359
|
+
}
|
|
12360
|
+
async function createGate(runnerUrl, params) {
|
|
12361
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12349
12362
|
method: "POST",
|
|
12350
|
-
headers:
|
|
12363
|
+
headers: JSON_HEADERS,
|
|
12351
12364
|
body: JSON.stringify(params)
|
|
12352
12365
|
});
|
|
12353
12366
|
if (resp.status === 409) {
|
|
@@ -12359,10 +12372,10 @@ async function createGate(params) {
|
|
|
12359
12372
|
}
|
|
12360
12373
|
return await resp.json();
|
|
12361
12374
|
}
|
|
12362
|
-
async function pollGate() {
|
|
12363
|
-
const resp = await fetch(`${
|
|
12375
|
+
async function pollGate(runnerUrl) {
|
|
12376
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12364
12377
|
method: "GET",
|
|
12365
|
-
headers:
|
|
12378
|
+
headers: JSON_HEADERS
|
|
12366
12379
|
});
|
|
12367
12380
|
if (!resp.ok) {
|
|
12368
12381
|
const text = await resp.text().catch(() => "");
|
|
@@ -12412,9 +12425,18 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12412
12425
|
});
|
|
12413
12426
|
}
|
|
12414
12427
|
const requestType = args.type === "assistance" ? "assistance" : "approval";
|
|
12428
|
+
const resolvedUrl = await discoverRunnerUrl(ctx.directory);
|
|
12429
|
+
if (!resolvedUrl) {
|
|
12430
|
+
return JSON.stringify({
|
|
12431
|
+
status: "unavailable",
|
|
12432
|
+
type: requestType,
|
|
12433
|
+
error: `Cannot discover runner: no heartbeat.json with valid web.port in ${ctx.directory}`,
|
|
12434
|
+
approval_id: null
|
|
12435
|
+
});
|
|
12436
|
+
}
|
|
12415
12437
|
let gateResult;
|
|
12416
12438
|
try {
|
|
12417
|
-
gateResult = await createGate({
|
|
12439
|
+
gateResult = await createGate(resolvedUrl, {
|
|
12418
12440
|
prompt: args.prompt,
|
|
12419
12441
|
artifacts,
|
|
12420
12442
|
deadline_hours: args.deadline_hours,
|
|
@@ -12450,7 +12472,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12450
12472
|
await sleep(POLL_INTERVAL_MS, ctx.abort);
|
|
12451
12473
|
let poll;
|
|
12452
12474
|
try {
|
|
12453
|
-
poll = await pollGate();
|
|
12475
|
+
poll = await pollGate(resolvedUrl);
|
|
12454
12476
|
} catch {
|
|
12455
12477
|
await sleep(POLL_INTERVAL_MS, ctx.abort);
|
|
12456
12478
|
continue;
|
package/dist/plugin.js
CHANGED
|
@@ -10,6 +10,10 @@ 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
|
+
|
|
13
17
|
// node_modules/zod/v4/classic/external.js
|
|
14
18
|
var exports_external = {};
|
|
15
19
|
__export(exports_external, {
|
|
@@ -12331,23 +12335,32 @@ function tool(input) {
|
|
|
12331
12335
|
}
|
|
12332
12336
|
tool.schema = exports_external;
|
|
12333
12337
|
// src/plugin.ts
|
|
12334
|
-
var DEFAULT_RUNNER_URL = "http://127.0.0.1:3210";
|
|
12335
12338
|
var POLL_INTERVAL_MS = 1e4;
|
|
12336
|
-
|
|
12337
|
-
|
|
12338
|
-
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
}
|
|
12347
|
-
|
|
12348
|
-
|
|
12339
|
+
var JSON_HEADERS = { "Content-Type": "application/json" };
|
|
12340
|
+
async function discoverRunnerUrl(directory) {
|
|
12341
|
+
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;
|
|
12355
|
+
}
|
|
12356
|
+
console.warn(`[heartbeat-approval] Failed to read heartbeat.json in ${directory}:`, err);
|
|
12357
|
+
return null;
|
|
12358
|
+
}
|
|
12359
|
+
}
|
|
12360
|
+
async function createGate(runnerUrl, params) {
|
|
12361
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12349
12362
|
method: "POST",
|
|
12350
|
-
headers:
|
|
12363
|
+
headers: JSON_HEADERS,
|
|
12351
12364
|
body: JSON.stringify(params)
|
|
12352
12365
|
});
|
|
12353
12366
|
if (resp.status === 409) {
|
|
@@ -12359,10 +12372,10 @@ async function createGate(params) {
|
|
|
12359
12372
|
}
|
|
12360
12373
|
return await resp.json();
|
|
12361
12374
|
}
|
|
12362
|
-
async function pollGate() {
|
|
12363
|
-
const resp = await fetch(`${
|
|
12375
|
+
async function pollGate(runnerUrl) {
|
|
12376
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
12364
12377
|
method: "GET",
|
|
12365
|
-
headers:
|
|
12378
|
+
headers: JSON_HEADERS
|
|
12366
12379
|
});
|
|
12367
12380
|
if (!resp.ok) {
|
|
12368
12381
|
const text = await resp.text().catch(() => "");
|
|
@@ -12412,9 +12425,18 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12412
12425
|
});
|
|
12413
12426
|
}
|
|
12414
12427
|
const requestType = args.type === "assistance" ? "assistance" : "approval";
|
|
12428
|
+
const resolvedUrl = await discoverRunnerUrl(ctx.directory);
|
|
12429
|
+
if (!resolvedUrl) {
|
|
12430
|
+
return JSON.stringify({
|
|
12431
|
+
status: "unavailable",
|
|
12432
|
+
type: requestType,
|
|
12433
|
+
error: `Cannot discover runner: no heartbeat.json with valid web.port in ${ctx.directory}`,
|
|
12434
|
+
approval_id: null
|
|
12435
|
+
});
|
|
12436
|
+
}
|
|
12415
12437
|
let gateResult;
|
|
12416
12438
|
try {
|
|
12417
|
-
gateResult = await createGate({
|
|
12439
|
+
gateResult = await createGate(resolvedUrl, {
|
|
12418
12440
|
prompt: args.prompt,
|
|
12419
12441
|
artifacts,
|
|
12420
12442
|
deadline_hours: args.deadline_hours,
|
|
@@ -12450,7 +12472,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12450
12472
|
await sleep(POLL_INTERVAL_MS, ctx.abort);
|
|
12451
12473
|
let poll;
|
|
12452
12474
|
try {
|
|
12453
|
-
poll = await pollGate();
|
|
12475
|
+
poll = await pollGate(resolvedUrl);
|
|
12454
12476
|
} catch {
|
|
12455
12477
|
await sleep(POLL_INTERVAL_MS, ctx.abort);
|
|
12456
12478
|
continue;
|
package/package.json
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { tool } from "@opencode-ai/plugin";
|
|
2
4
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
3
5
|
|
|
4
|
-
const DEFAULT_RUNNER_URL = "http://127.0.0.1:3210";
|
|
5
6
|
const POLL_INTERVAL_MS = 10_000;
|
|
7
|
+
const JSON_HEADERS: Record<string, string> = { "Content-Type": "application/json" };
|
|
6
8
|
|
|
7
9
|
interface CreateResponse {
|
|
8
10
|
approval_id: string;
|
|
@@ -18,35 +20,48 @@ interface PollResponse {
|
|
|
18
20
|
type?: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
function getRunnerUrl(): string {
|
|
22
|
-
return 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
23
|
interface ConflictResponse {
|
|
35
24
|
error: string;
|
|
36
25
|
message: string;
|
|
37
26
|
existing_approval_id: string;
|
|
38
27
|
}
|
|
39
28
|
|
|
40
|
-
|
|
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> {
|
|
35
|
+
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}`;
|
|
43
|
+
}
|
|
44
|
+
console.warn(`[heartbeat-approval] heartbeat.json found but web.port invalid: ${JSON.stringify(port)} in ${directory}`);
|
|
45
|
+
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);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function createGate(runnerUrl: string, params: {
|
|
41
56
|
prompt: string;
|
|
42
57
|
artifacts?: string[];
|
|
43
58
|
deadline_hours?: number;
|
|
44
59
|
phase?: string;
|
|
45
60
|
type?: string;
|
|
46
61
|
}): Promise<CreateResponse | ConflictResponse> {
|
|
47
|
-
const resp = await fetch(`${
|
|
62
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
48
63
|
method: "POST",
|
|
49
|
-
headers:
|
|
64
|
+
headers: JSON_HEADERS,
|
|
50
65
|
body: JSON.stringify(params),
|
|
51
66
|
});
|
|
52
67
|
if (resp.status === 409) {
|
|
@@ -59,10 +74,10 @@ async function createGate(params: {
|
|
|
59
74
|
return (await resp.json()) as CreateResponse;
|
|
60
75
|
}
|
|
61
76
|
|
|
62
|
-
async function pollGate(): Promise<PollResponse> {
|
|
63
|
-
const resp = await fetch(`${
|
|
77
|
+
async function pollGate(runnerUrl: string): Promise<PollResponse> {
|
|
78
|
+
const resp = await fetch(`${runnerUrl}/api/approval`, {
|
|
64
79
|
method: "GET",
|
|
65
|
-
headers:
|
|
80
|
+
headers: JSON_HEADERS,
|
|
66
81
|
});
|
|
67
82
|
if (!resp.ok) {
|
|
68
83
|
const text = await resp.text().catch(() => "");
|
|
@@ -122,9 +137,19 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
122
137
|
}
|
|
123
138
|
const requestType = (args.type === "assistance" ? "assistance" : "approval") as "approval" | "assistance";
|
|
124
139
|
|
|
140
|
+
const resolvedUrl = await discoverRunnerUrl(ctx.directory);
|
|
141
|
+
if (!resolvedUrl) {
|
|
142
|
+
return JSON.stringify({
|
|
143
|
+
status: "unavailable",
|
|
144
|
+
type: requestType,
|
|
145
|
+
error: `Cannot discover runner: no heartbeat.json with valid web.port in ${ctx.directory}`,
|
|
146
|
+
approval_id: null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
125
150
|
let gateResult: CreateResponse | ConflictResponse;
|
|
126
151
|
try {
|
|
127
|
-
gateResult = await createGate({
|
|
152
|
+
gateResult = await createGate(resolvedUrl, {
|
|
128
153
|
prompt: args.prompt,
|
|
129
154
|
artifacts,
|
|
130
155
|
deadline_hours: args.deadline_hours,
|
|
@@ -165,7 +190,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
165
190
|
|
|
166
191
|
let poll: PollResponse;
|
|
167
192
|
try {
|
|
168
|
-
poll = await pollGate();
|
|
193
|
+
poll = await pollGate(resolvedUrl);
|
|
169
194
|
} catch {
|
|
170
195
|
await sleep(POLL_INTERVAL_MS, ctx.abort);
|
|
171
196
|
continue;
|