opencode-heartbeat-approval 0.2.1 → 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 +35 -15
- package/dist/plugin.js +35 -15
- package/package.json +1 -1
- package/src/plugin.ts +41 -21
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
|
-
|
|
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;
|
|
12344
12358
|
}
|
|
12345
|
-
return headers;
|
|
12346
12359
|
}
|
|
12347
12360
|
async function createGate(runnerUrl, params) {
|
|
12348
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) {
|
|
@@ -12362,7 +12375,7 @@ async function createGate(runnerUrl, params) {
|
|
|
12362
12375
|
async function pollGate(runnerUrl) {
|
|
12363
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(() => "");
|
|
@@ -12398,8 +12411,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12398
12411
|
prompt: tool.schema.string().describe("What you need from the human \u2014 be specific and concise"),
|
|
12399
12412
|
type: tool.schema.string().optional().describe("Type of request: 'approval' (need permission) or 'assistance' (need human help). Default: 'approval'"),
|
|
12400
12413
|
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.")
|
|
12414
|
+
deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)")
|
|
12403
12415
|
},
|
|
12404
12416
|
async execute(args, ctx) {
|
|
12405
12417
|
const artifacts = args.artifacts ? args.artifacts.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
@@ -12413,7 +12425,15 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12413
12425
|
});
|
|
12414
12426
|
}
|
|
12415
12427
|
const requestType = args.type === "assistance" ? "assistance" : "approval";
|
|
12416
|
-
const resolvedUrl =
|
|
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
|
+
}
|
|
12417
12437
|
let gateResult;
|
|
12418
12438
|
try {
|
|
12419
12439
|
gateResult = await createGate(resolvedUrl, {
|
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
|
-
|
|
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;
|
|
12344
12358
|
}
|
|
12345
|
-
return headers;
|
|
12346
12359
|
}
|
|
12347
12360
|
async function createGate(runnerUrl, params) {
|
|
12348
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) {
|
|
@@ -12362,7 +12375,7 @@ async function createGate(runnerUrl, params) {
|
|
|
12362
12375
|
async function pollGate(runnerUrl) {
|
|
12363
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(() => "");
|
|
@@ -12398,8 +12411,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12398
12411
|
prompt: tool.schema.string().describe("What you need from the human \u2014 be specific and concise"),
|
|
12399
12412
|
type: tool.schema.string().optional().describe("Type of request: 'approval' (need permission) or 'assistance' (need human help). Default: 'approval'"),
|
|
12400
12413
|
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.")
|
|
12414
|
+
deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)")
|
|
12403
12415
|
},
|
|
12404
12416
|
async execute(args, ctx) {
|
|
12405
12417
|
const artifacts = args.artifacts ? args.artifacts.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
@@ -12413,7 +12425,15 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
12413
12425
|
});
|
|
12414
12426
|
}
|
|
12415
12427
|
const requestType = args.type === "assistance" ? "assistance" : "approval";
|
|
12416
|
-
const resolvedUrl =
|
|
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
|
+
}
|
|
12417
12437
|
let gateResult;
|
|
12418
12438
|
try {
|
|
12419
12439
|
gateResult = await createGate(resolvedUrl, {
|
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,25 +20,38 @@ interface PollResponse {
|
|
|
18
20
|
type?: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
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
23
|
interface ConflictResponse {
|
|
35
24
|
error: string;
|
|
36
25
|
message: string;
|
|
37
26
|
existing_approval_id: string;
|
|
38
27
|
}
|
|
39
28
|
|
|
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
|
+
|
|
40
55
|
async function createGate(runnerUrl: string, params: {
|
|
41
56
|
prompt: string;
|
|
42
57
|
artifacts?: string[];
|
|
@@ -46,7 +61,7 @@ async function createGate(runnerUrl: string, params: {
|
|
|
46
61
|
}): Promise<CreateResponse | ConflictResponse> {
|
|
47
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) {
|
|
@@ -62,7 +77,7 @@ async function createGate(runnerUrl: string, params: {
|
|
|
62
77
|
async function pollGate(runnerUrl: string): Promise<PollResponse> {
|
|
63
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(() => "");
|
|
@@ -106,10 +121,6 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
106
121
|
.number()
|
|
107
122
|
.optional()
|
|
108
123
|
.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
124
|
},
|
|
114
125
|
async execute(args, ctx) {
|
|
115
126
|
const artifacts = args.artifacts
|
|
@@ -125,7 +136,16 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
|
|
|
125
136
|
});
|
|
126
137
|
}
|
|
127
138
|
const requestType = (args.type === "assistance" ? "assistance" : "approval") as "approval" | "assistance";
|
|
128
|
-
|
|
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
|
+
}
|
|
129
149
|
|
|
130
150
|
let gateResult: CreateResponse | ConflictResponse;
|
|
131
151
|
try {
|