opencode-heartbeat-approval 0.1.0 → 0.2.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
@@ -12333,8 +12333,8 @@ tool.schema = exports_external;
12333
12333
  // src/plugin.ts
12334
12334
  var DEFAULT_RUNNER_URL = "http://127.0.0.1:3210";
12335
12335
  var POLL_INTERVAL_MS = 1e4;
12336
- function getRunnerUrl() {
12337
- return process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
12336
+ function getRunnerUrl(override) {
12337
+ return override ?? process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
12338
12338
  }
12339
12339
  function buildHeaders() {
12340
12340
  const headers = { "Content-Type": "application/json" };
@@ -12344,8 +12344,8 @@ function buildHeaders() {
12344
12344
  }
12345
12345
  return headers;
12346
12346
  }
12347
- async function createGate(params) {
12348
- const resp = await fetch(`${getRunnerUrl()}/api/approval`, {
12347
+ async function createGate(runnerUrl, params) {
12348
+ const resp = await fetch(`${runnerUrl}/api/approval`, {
12349
12349
  method: "POST",
12350
12350
  headers: buildHeaders(),
12351
12351
  body: JSON.stringify(params)
@@ -12359,8 +12359,8 @@ async function createGate(params) {
12359
12359
  }
12360
12360
  return await resp.json();
12361
12361
  }
12362
- async function pollGate() {
12363
- const resp = await fetch(`${getRunnerUrl()}/api/approval`, {
12362
+ async function pollGate(runnerUrl) {
12363
+ const resp = await fetch(`${runnerUrl}/api/approval`, {
12364
12364
  method: "GET",
12365
12365
  headers: buildHeaders()
12366
12366
  });
@@ -12386,31 +12386,46 @@ function sleep(ms, signal) {
12386
12386
  var HeartbeatApprovalPlugin = async (_input) => {
12387
12387
  return {
12388
12388
  tool: {
12389
- request_human_approval: tool({
12390
- description: `Request human approval before proceeding with a significant action.
12389
+ request_human_input: tool({
12390
+ description: `Request human input before proceeding \u2014 either approval (permission to act) or assistance (human help needed).
12391
12391
 
12392
- Call this tool when you need explicit human confirmation \u2014 for example before publishing content, executing a trade, or making an irreversible change. The tool sends a Feishu interactive card with approve/reject buttons and blocks until the human responds (or the deadline expires).
12392
+ Call this tool when you need explicit human confirmation or help. Use type "approval" for actions needing permission (publishing, trading, irreversible changes). Use type "assistance" when you need the human to do something (enter a captcha, provide credentials, perform a physical action).
12393
12393
 
12394
- Only call this when genuinely necessary. Do not use it as a routine step.
12394
+ The tool sends a Feishu interactive card and blocks until the human responds (or the deadline expires). Only call this when genuinely necessary.
12395
12395
 
12396
- Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", response?: string, approval_id: string | null }`,
12396
+ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", type: "approval" | "assistance", response?: string, approval_id: string | null }`,
12397
12397
  args: {
12398
- prompt: tool.schema.string().describe("What you need human approval for \u2014 be specific and concise"),
12398
+ prompt: tool.schema.string().describe("What you need from the human \u2014 be specific and concise"),
12399
+ type: tool.schema.string().optional().describe("Type of request: 'approval' (need permission) or 'assistance' (need human help). Default: 'approval'"),
12399
12400
  artifacts: tool.schema.string().optional().describe("Comma-separated list of relevant file paths or artifact names"),
12400
- deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)")
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.")
12401
12403
  },
12402
12404
  async execute(args, ctx) {
12403
12405
  const artifacts = args.artifacts ? args.artifacts.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
12406
+ const validTypes = ["approval", "assistance"];
12407
+ if (args.type !== undefined && !validTypes.includes(args.type)) {
12408
+ return JSON.stringify({
12409
+ status: "error",
12410
+ type: null,
12411
+ error: `Invalid type "${args.type}". Must be "approval" or "assistance".`,
12412
+ approval_id: null
12413
+ });
12414
+ }
12415
+ const requestType = args.type === "assistance" ? "assistance" : "approval";
12416
+ const resolvedUrl = getRunnerUrl(args.runner_url);
12404
12417
  let gateResult;
12405
12418
  try {
12406
- gateResult = await createGate({
12419
+ gateResult = await createGate(resolvedUrl, {
12407
12420
  prompt: args.prompt,
12408
12421
  artifacts,
12409
- deadline_hours: args.deadline_hours
12422
+ deadline_hours: args.deadline_hours,
12423
+ type: requestType
12410
12424
  });
12411
12425
  } catch (err) {
12412
12426
  return JSON.stringify({
12413
12427
  status: "unavailable",
12428
+ type: requestType,
12414
12429
  error: err instanceof Error ? err.message : String(err),
12415
12430
  approval_id: null
12416
12431
  });
@@ -12418,6 +12433,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12418
12433
  if ("error" in gateResult) {
12419
12434
  return JSON.stringify({
12420
12435
  status: "conflict",
12436
+ type: requestType,
12421
12437
  error: gateResult.message,
12422
12438
  existing_approval_id: gateResult.existing_approval_id,
12423
12439
  approval_id: null
@@ -12428,6 +12444,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12428
12444
  if (ctx.abort?.aborted) {
12429
12445
  return JSON.stringify({
12430
12446
  status: "cancelled",
12447
+ type: requestType,
12431
12448
  response: null,
12432
12449
  approval_id: approvalId
12433
12450
  });
@@ -12435,7 +12452,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12435
12452
  await sleep(POLL_INTERVAL_MS, ctx.abort);
12436
12453
  let poll;
12437
12454
  try {
12438
- poll = await pollGate();
12455
+ poll = await pollGate(resolvedUrl);
12439
12456
  } catch {
12440
12457
  await sleep(POLL_INTERVAL_MS, ctx.abort);
12441
12458
  continue;
@@ -12443,6 +12460,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12443
12460
  if (poll.status === "approved") {
12444
12461
  return JSON.stringify({
12445
12462
  status: "approved",
12463
+ type: requestType,
12446
12464
  response: poll.response ?? null,
12447
12465
  approval_id: approvalId
12448
12466
  });
@@ -12450,6 +12468,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12450
12468
  if (poll.status === "rejected") {
12451
12469
  return JSON.stringify({
12452
12470
  status: "rejected",
12471
+ type: requestType,
12453
12472
  response: poll.response ?? null,
12454
12473
  approval_id: approvalId
12455
12474
  });
@@ -12457,6 +12476,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12457
12476
  if (poll.status === "expired") {
12458
12477
  return JSON.stringify({
12459
12478
  status: "expired",
12479
+ type: requestType,
12460
12480
  response: null,
12461
12481
  approval_id: approvalId
12462
12482
  });
package/dist/plugin.js CHANGED
@@ -12333,8 +12333,8 @@ tool.schema = exports_external;
12333
12333
  // src/plugin.ts
12334
12334
  var DEFAULT_RUNNER_URL = "http://127.0.0.1:3210";
12335
12335
  var POLL_INTERVAL_MS = 1e4;
12336
- function getRunnerUrl() {
12337
- return process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
12336
+ function getRunnerUrl(override) {
12337
+ return override ?? process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
12338
12338
  }
12339
12339
  function buildHeaders() {
12340
12340
  const headers = { "Content-Type": "application/json" };
@@ -12344,8 +12344,8 @@ function buildHeaders() {
12344
12344
  }
12345
12345
  return headers;
12346
12346
  }
12347
- async function createGate(params) {
12348
- const resp = await fetch(`${getRunnerUrl()}/api/approval`, {
12347
+ async function createGate(runnerUrl, params) {
12348
+ const resp = await fetch(`${runnerUrl}/api/approval`, {
12349
12349
  method: "POST",
12350
12350
  headers: buildHeaders(),
12351
12351
  body: JSON.stringify(params)
@@ -12359,8 +12359,8 @@ async function createGate(params) {
12359
12359
  }
12360
12360
  return await resp.json();
12361
12361
  }
12362
- async function pollGate() {
12363
- const resp = await fetch(`${getRunnerUrl()}/api/approval`, {
12362
+ async function pollGate(runnerUrl) {
12363
+ const resp = await fetch(`${runnerUrl}/api/approval`, {
12364
12364
  method: "GET",
12365
12365
  headers: buildHeaders()
12366
12366
  });
@@ -12386,31 +12386,46 @@ function sleep(ms, signal) {
12386
12386
  var HeartbeatApprovalPlugin = async (_input) => {
12387
12387
  return {
12388
12388
  tool: {
12389
- request_human_approval: tool({
12390
- description: `Request human approval before proceeding with a significant action.
12389
+ request_human_input: tool({
12390
+ description: `Request human input before proceeding \u2014 either approval (permission to act) or assistance (human help needed).
12391
12391
 
12392
- Call this tool when you need explicit human confirmation \u2014 for example before publishing content, executing a trade, or making an irreversible change. The tool sends a Feishu interactive card with approve/reject buttons and blocks until the human responds (or the deadline expires).
12392
+ Call this tool when you need explicit human confirmation or help. Use type "approval" for actions needing permission (publishing, trading, irreversible changes). Use type "assistance" when you need the human to do something (enter a captcha, provide credentials, perform a physical action).
12393
12393
 
12394
- Only call this when genuinely necessary. Do not use it as a routine step.
12394
+ The tool sends a Feishu interactive card and blocks until the human responds (or the deadline expires). Only call this when genuinely necessary.
12395
12395
 
12396
- Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", response?: string, approval_id: string | null }`,
12396
+ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", type: "approval" | "assistance", response?: string, approval_id: string | null }`,
12397
12397
  args: {
12398
- prompt: tool.schema.string().describe("What you need human approval for \u2014 be specific and concise"),
12398
+ prompt: tool.schema.string().describe("What you need from the human \u2014 be specific and concise"),
12399
+ type: tool.schema.string().optional().describe("Type of request: 'approval' (need permission) or 'assistance' (need human help). Default: 'approval'"),
12399
12400
  artifacts: tool.schema.string().optional().describe("Comma-separated list of relevant file paths or artifact names"),
12400
- deadline_hours: tool.schema.number().optional().describe("Hours before the request expires (default: 24, max: 168)")
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.")
12401
12403
  },
12402
12404
  async execute(args, ctx) {
12403
12405
  const artifacts = args.artifacts ? args.artifacts.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
12406
+ const validTypes = ["approval", "assistance"];
12407
+ if (args.type !== undefined && !validTypes.includes(args.type)) {
12408
+ return JSON.stringify({
12409
+ status: "error",
12410
+ type: null,
12411
+ error: `Invalid type "${args.type}". Must be "approval" or "assistance".`,
12412
+ approval_id: null
12413
+ });
12414
+ }
12415
+ const requestType = args.type === "assistance" ? "assistance" : "approval";
12416
+ const resolvedUrl = getRunnerUrl(args.runner_url);
12404
12417
  let gateResult;
12405
12418
  try {
12406
- gateResult = await createGate({
12419
+ gateResult = await createGate(resolvedUrl, {
12407
12420
  prompt: args.prompt,
12408
12421
  artifacts,
12409
- deadline_hours: args.deadline_hours
12422
+ deadline_hours: args.deadline_hours,
12423
+ type: requestType
12410
12424
  });
12411
12425
  } catch (err) {
12412
12426
  return JSON.stringify({
12413
12427
  status: "unavailable",
12428
+ type: requestType,
12414
12429
  error: err instanceof Error ? err.message : String(err),
12415
12430
  approval_id: null
12416
12431
  });
@@ -12418,6 +12433,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12418
12433
  if ("error" in gateResult) {
12419
12434
  return JSON.stringify({
12420
12435
  status: "conflict",
12436
+ type: requestType,
12421
12437
  error: gateResult.message,
12422
12438
  existing_approval_id: gateResult.existing_approval_id,
12423
12439
  approval_id: null
@@ -12428,6 +12444,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12428
12444
  if (ctx.abort?.aborted) {
12429
12445
  return JSON.stringify({
12430
12446
  status: "cancelled",
12447
+ type: requestType,
12431
12448
  response: null,
12432
12449
  approval_id: approvalId
12433
12450
  });
@@ -12435,7 +12452,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12435
12452
  await sleep(POLL_INTERVAL_MS, ctx.abort);
12436
12453
  let poll;
12437
12454
  try {
12438
- poll = await pollGate();
12455
+ poll = await pollGate(resolvedUrl);
12439
12456
  } catch {
12440
12457
  await sleep(POLL_INTERVAL_MS, ctx.abort);
12441
12458
  continue;
@@ -12443,6 +12460,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12443
12460
  if (poll.status === "approved") {
12444
12461
  return JSON.stringify({
12445
12462
  status: "approved",
12463
+ type: requestType,
12446
12464
  response: poll.response ?? null,
12447
12465
  approval_id: approvalId
12448
12466
  });
@@ -12450,6 +12468,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12450
12468
  if (poll.status === "rejected") {
12451
12469
  return JSON.stringify({
12452
12470
  status: "rejected",
12471
+ type: requestType,
12453
12472
  response: poll.response ?? null,
12454
12473
  approval_id: approvalId
12455
12474
  });
@@ -12457,6 +12476,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
12457
12476
  if (poll.status === "expired") {
12458
12477
  return JSON.stringify({
12459
12478
  status: "expired",
12479
+ type: requestType,
12460
12480
  response: null,
12461
12481
  approval_id: approvalId
12462
12482
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-heartbeat-approval",
3
- "version": "0.1.0",
4
- "description": "OpenCode plugin providing request_human_approval MCP tool for Heartbeat pipeline",
3
+ "version": "0.2.1",
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",
7
7
  "module": "dist/index.js",
package/src/plugin.ts CHANGED
@@ -8,16 +8,18 @@ interface CreateResponse {
8
8
  approval_id: string;
9
9
  status: string;
10
10
  created: boolean;
11
+ type?: string;
11
12
  }
12
13
 
13
14
  interface PollResponse {
14
15
  approval_id: string | null;
15
16
  status: string;
16
17
  response?: string;
18
+ type?: string;
17
19
  }
18
20
 
19
- function getRunnerUrl(): string {
20
- return process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
21
+ function getRunnerUrl(override?: string): string {
22
+ return override ?? process.env.HEARTBEAT_RUNNER_URL ?? DEFAULT_RUNNER_URL;
21
23
  }
22
24
 
23
25
  function buildHeaders(): Record<string, string> {
@@ -35,13 +37,14 @@ interface ConflictResponse {
35
37
  existing_approval_id: string;
36
38
  }
37
39
 
38
- async function createGate(params: {
40
+ async function createGate(runnerUrl: string, params: {
39
41
  prompt: string;
40
42
  artifacts?: string[];
41
43
  deadline_hours?: number;
42
44
  phase?: string;
45
+ type?: string;
43
46
  }): Promise<CreateResponse | ConflictResponse> {
44
- const resp = await fetch(`${getRunnerUrl()}/api/approval`, {
47
+ const resp = await fetch(`${runnerUrl}/api/approval`, {
45
48
  method: "POST",
46
49
  headers: buildHeaders(),
47
50
  body: JSON.stringify(params),
@@ -56,8 +59,8 @@ async function createGate(params: {
56
59
  return (await resp.json()) as CreateResponse;
57
60
  }
58
61
 
59
- async function pollGate(): Promise<PollResponse> {
60
- const resp = await fetch(`${getRunnerUrl()}/api/approval`, {
62
+ async function pollGate(runnerUrl: string): Promise<PollResponse> {
63
+ const resp = await fetch(`${runnerUrl}/api/approval`, {
61
64
  method: "GET",
62
65
  headers: buildHeaders(),
63
66
  });
@@ -79,18 +82,22 @@ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
79
82
  export const HeartbeatApprovalPlugin = async (_input: PluginInput) => {
80
83
  return {
81
84
  tool: {
82
- request_human_approval: tool({
83
- description: `Request human approval before proceeding with a significant action.
85
+ request_human_input: tool({
86
+ description: `Request human input before proceeding either approval (permission to act) or assistance (human help needed).
84
87
 
85
- Call this tool when you need explicit human confirmation for example before publishing content, executing a trade, or making an irreversible change. The tool sends a Feishu interactive card with approve/reject buttons and blocks until the human responds (or the deadline expires).
88
+ Call this tool when you need explicit human confirmation or help. Use type "approval" for actions needing permission (publishing, trading, irreversible changes). Use type "assistance" when you need the human to do something (enter a captcha, provide credentials, perform a physical action).
86
89
 
87
- Only call this when genuinely necessary. Do not use it as a routine step.
90
+ The tool sends a Feishu interactive card and blocks until the human responds (or the deadline expires). Only call this when genuinely necessary.
88
91
 
89
- Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", response?: string, approval_id: string | null }`,
92
+ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancelled" | "conflict", type: "approval" | "assistance", response?: string, approval_id: string | null }`,
90
93
  args: {
91
94
  prompt: tool.schema
92
95
  .string()
93
- .describe("What you need human approval for — be specific and concise"),
96
+ .describe("What you need from the human — be specific and concise"),
97
+ type: tool.schema
98
+ .string()
99
+ .optional()
100
+ .describe("Type of request: 'approval' (need permission) or 'assistance' (need human help). Default: 'approval'"),
94
101
  artifacts: tool.schema
95
102
  .string()
96
103
  .optional()
@@ -99,22 +106,39 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
99
106
  .number()
100
107
  .optional()
101
108
  .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."),
102
113
  },
103
114
  async execute(args, ctx) {
104
115
  const artifacts = args.artifacts
105
116
  ? args.artifacts.split(",").map((s: string) => s.trim()).filter(Boolean)
106
117
  : undefined;
118
+ const validTypes = ["approval", "assistance"];
119
+ if (args.type !== undefined && !validTypes.includes(args.type)) {
120
+ return JSON.stringify({
121
+ status: "error",
122
+ type: null,
123
+ error: `Invalid type "${args.type}". Must be "approval" or "assistance".`,
124
+ approval_id: null,
125
+ });
126
+ }
127
+ const requestType = (args.type === "assistance" ? "assistance" : "approval") as "approval" | "assistance";
128
+ const resolvedUrl = getRunnerUrl(args.runner_url);
107
129
 
108
130
  let gateResult: CreateResponse | ConflictResponse;
109
131
  try {
110
- gateResult = await createGate({
132
+ gateResult = await createGate(resolvedUrl, {
111
133
  prompt: args.prompt,
112
134
  artifacts,
113
135
  deadline_hours: args.deadline_hours,
136
+ type: requestType,
114
137
  });
115
138
  } catch (err) {
116
139
  return JSON.stringify({
117
140
  status: "unavailable",
141
+ type: requestType,
118
142
  error: err instanceof Error ? err.message : String(err),
119
143
  approval_id: null,
120
144
  });
@@ -123,6 +147,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
123
147
  if ("error" in gateResult) {
124
148
  return JSON.stringify({
125
149
  status: "conflict",
150
+ type: requestType,
126
151
  error: gateResult.message,
127
152
  existing_approval_id: gateResult.existing_approval_id,
128
153
  approval_id: null,
@@ -135,6 +160,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
135
160
  if (ctx.abort?.aborted) {
136
161
  return JSON.stringify({
137
162
  status: "cancelled",
163
+ type: requestType,
138
164
  response: null,
139
165
  approval_id: approvalId,
140
166
  });
@@ -144,7 +170,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
144
170
 
145
171
  let poll: PollResponse;
146
172
  try {
147
- poll = await pollGate();
173
+ poll = await pollGate(resolvedUrl);
148
174
  } catch {
149
175
  await sleep(POLL_INTERVAL_MS, ctx.abort);
150
176
  continue;
@@ -153,6 +179,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
153
179
  if (poll.status === "approved") {
154
180
  return JSON.stringify({
155
181
  status: "approved",
182
+ type: requestType,
156
183
  response: poll.response ?? null,
157
184
  approval_id: approvalId,
158
185
  });
@@ -161,6 +188,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
161
188
  if (poll.status === "rejected") {
162
189
  return JSON.stringify({
163
190
  status: "rejected",
191
+ type: requestType,
164
192
  response: poll.response ?? null,
165
193
  approval_id: approvalId,
166
194
  });
@@ -169,6 +197,7 @@ Returns: { status: "approved" | "rejected" | "expired" | "unavailable" | "cancel
169
197
  if (poll.status === "expired") {
170
198
  return JSON.stringify({
171
199
  status: "expired",
200
+ type: requestType,
172
201
  response: null,
173
202
  approval_id: approvalId,
174
203
  });