approval-box 0.1.11 → 0.1.13

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/README.md CHANGED
@@ -4,6 +4,8 @@ Approval Box connector. When an AI coding agent needs a human decision, approval
4
4
 
5
5
  Supported agents: Claude Code, Codex, Cursor, Grok — on macOS, Windows and Linux.
6
6
 
7
+ Cursor and Grok receive answers through the background receiver command returned by the request tool. Cursor hooks bind that receiver to the conversation; they do not consume answers. Keep the receiver running until it returns the answer. Its output includes a command to wait for the next answer.
8
+
7
9
  AIが人間の判断・承認・操作を求める時に、Approval Boxへも申請します。アプリかWeb版で答えると、その答えが申請したAIの会話へそのまま届きます。
8
10
 
9
11
  ## Setup / 使い方
@@ -33,3 +35,14 @@ npx approval-box uninstall # remove everything and unlink this device / 全部
33
35
  Self-hosted server: `npx -y approval-box@latest setup --server https://your-server`.
34
36
 
35
37
  Requires Node.js 20 or later. License: MIT.
38
+
39
+ ### 会話を開き直した時
40
+
41
+ `list_my_decisions` で、自分が引き継ぐ申請を確かめ、対象ごとに
42
+ `resume_decision({decision_id: "K-…"})` を呼びます。未決・保留なら今の会話へ
43
+ 配送先を移し、回答済みなら答えを直接返します。同じ端末にある他の作業の申請は、
44
+ 引き継ぐ対象に含めないでください。`amend_decision` も、修正が成功した時に配送先を
45
+ 呼び出した会話へ更新します。Cursor・Grokでは、返る案内に従って背景の受信を起動します。
46
+
47
+ この機能はコネクタ0.1.13以降と対応サーバーを使います。会話を開き直しても、
48
+ 再開を指定するまでは元の会話が配送先です。答えは `get_decision` でも取得できます。
@@ -2,7 +2,7 @@ import { createRequire as __kbCreateRequire } from 'node:module'; const require
2
2
  import {
3
3
  PROFILE,
4
4
  runClaudeHookMain
5
- } from "./chunks/chunk-LEWADASG.mjs";
5
+ } from "./chunks/chunk-YBOJCW6Y.mjs";
6
6
 
7
7
  // src/hooks/claude.ts
8
8
  await runClaudeHookMain(PROFILE);
@@ -2,7 +2,7 @@ import { createRequire as __kbCreateRequire } from 'node:module'; const require
2
2
  import {
3
3
  PROFILE,
4
4
  runCodexHookMain
5
- } from "./chunks/chunk-LEWADASG.mjs";
5
+ } from "./chunks/chunk-YBOJCW6Y.mjs";
6
6
 
7
7
  // src/hooks/codex.ts
8
8
  await runCodexHookMain(PROFILE, process.argv[2]);
@@ -1,8 +1,44 @@
1
1
  import { createRequire as __kbCreateRequire } from 'node:module'; const require = __kbCreateRequire(import.meta.url);
2
2
  import {
3
3
  PROFILE,
4
- runCursorHookMain
5
- } from "./chunks/chunk-LEWADASG.mjs";
4
+ cursorHookRoot,
5
+ handleCursorChannelHook,
6
+ handleCursorHook,
7
+ withoutBom
8
+ } from "./chunks/chunk-YBOJCW6Y.mjs";
9
+
10
+ // src/cursor-binding.ts
11
+ async function bindCursorResult(profile, raw) {
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(withoutBom(raw));
15
+ } catch {
16
+ return;
17
+ }
18
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return;
19
+ const event = parsed;
20
+ const name = event.hook_event_name;
21
+ if (name !== "afterMCPExecution" && name !== "postToolUse" && name !== "postToolUseFailure") return;
22
+ const binding = {
23
+ ...event,
24
+ hook_event_name: "afterMCPExecution",
25
+ result_json: name === "afterMCPExecution" ? event.result_json : event.tool_output
26
+ };
27
+ await handleCursorHook(profile, JSON.stringify(binding), cursorHookRoot(profile));
28
+ if (profile.channels) await handleCursorChannelHook(profile, binding, () => {
29
+ throw new Error("CURSOR_BIND_HOOK_MUST_NOT_CONSUME_ANSWERS");
30
+ });
31
+ }
6
32
 
7
33
  // src/hooks/cursor.ts
8
- await runCursorHookMain(PROFILE);
34
+ try {
35
+ let raw = "";
36
+ process.stdin.setEncoding("utf8");
37
+ for await (const chunk of process.stdin) raw += chunk;
38
+ await bindCursorResult(PROFILE, raw);
39
+ process.stdout.write("{}\n");
40
+ } catch (error) {
41
+ process.stderr.write(`${error instanceof Error ? error.message : "CURSOR_PARENT_HOOK_FAILED"}
42
+ `);
43
+ process.exitCode = 2;
44
+ }
@@ -2,7 +2,7 @@ import { createRequire as __kbCreateRequire } from 'node:module'; const require
2
2
  import {
3
3
  PROFILE,
4
4
  runChannelReceiveMain
5
- } from "./chunks/chunk-LEWADASG.mjs";
5
+ } from "./chunks/chunk-YBOJCW6Y.mjs";
6
6
 
7
7
  // src/hooks/receive.ts
8
8
  await runChannelReceiveMain(PROFILE);
@@ -43,7 +43,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
43
43
  import { homedir } from "node:os";
44
44
  import { join } from "node:path";
45
45
  var MCP_SERVER = "approval-box";
46
- var DISPATCH_TOOLS = ["request_decision", "setup_test"];
46
+ var DISPATCH_TOOLS = ["request_decision", "setup_test", "amend_decision", "resume_decision"];
47
47
  var home = () => process.env.APPROVAL_BOX_HOME ?? join(homedir(), ".approval-box");
48
48
  var stateRoot = () => join(home(), "state");
49
49
  var HOOK_FILES = {
@@ -21019,6 +21019,25 @@ function channelDeliveryState(profile, channelId, deliveryId) {
21019
21019
  }
21020
21020
  return fs7.readdirSync(path10.join(dir, "inbox")).some((name) => name.endsWith(`-${deliveryId}.json`)) ? "queued" : null;
21021
21021
  }
21022
+ function withdrawFromChannel(profile, channelId, deliveryId) {
21023
+ uuid3.parse(deliveryId);
21024
+ const dir = channelDir(profile, channelId);
21025
+ const inbox = path10.join(dir, "inbox");
21026
+ const name = fs7.existsSync(inbox) ? fs7.readdirSync(inbox).find((entry) => entry.endsWith(`-${deliveryId}.json`)) : void 0;
21027
+ if (!name)
21028
+ return false;
21029
+ const claim3 = path10.join(dir, "claims", `${deliveryId}.json`);
21030
+ try {
21031
+ fs7.linkSync(path10.join(inbox, name), claim3);
21032
+ fs7.unlinkSync(path10.join(inbox, name));
21033
+ } catch (error62) {
21034
+ if (["ENOENT", "EEXIST"].includes(error62.code ?? ""))
21035
+ return false;
21036
+ throw error62;
21037
+ }
21038
+ writeJson0600(claim3, { delivery_id: deliveryId, state: "withdrawn", at: (/* @__PURE__ */ new Date()).toISOString() });
21039
+ return true;
21040
+ }
21022
21041
  function claimPending(profile, channelId, by, limit = Infinity) {
21023
21042
  const dir = channelDir(profile, channelId);
21024
21043
  const inbox = path10.join(dir, "inbox");
@@ -21344,40 +21363,6 @@ async function runClaudeHookMain(profile, root = claudeHookRoot(profile)) {
21344
21363
  process.exitCode = 2;
21345
21364
  }
21346
21365
  }
21347
- async function runCursorHookMain(profile, root = cursorHookRoot(profile)) {
21348
- try {
21349
- const input2 = await readStdin();
21350
- const result = {};
21351
- for (const each of typeof root === "string" ? [root] : [...new Set(root)]) {
21352
- const found = await handleCursorHook(profile, input2.length > 0 ? input2 : "{}", each);
21353
- if (typeof found.additional_context === "string") {
21354
- result.additional_context = typeof result.additional_context === "string" ? `${result.additional_context}
21355
-
21356
- ${found.additional_context}` : found.additional_context;
21357
- }
21358
- }
21359
- if (profile.channels) {
21360
- let event = null;
21361
- try {
21362
- event = JSON.parse(input2);
21363
- } catch {
21364
- }
21365
- if (event !== null && typeof event === "object") {
21366
- await handleCursorChannelHook(profile, event, (text) => {
21367
- result.additional_context = typeof result.additional_context === "string" ? `${result.additional_context}
21368
-
21369
- ${text}` : text;
21370
- });
21371
- }
21372
- }
21373
- process.stdout.write(`${JSON.stringify(result)}
21374
- `);
21375
- } catch (error62) {
21376
- process.stderr.write(`${error62 instanceof Error ? error62.message : "CURSOR_PARENT_HOOK_FAILED"}
21377
- `);
21378
- process.exitCode = 2;
21379
- }
21380
- }
21381
21366
  async function runChannelReceiveMain(profile, argv = process.argv.slice(2), script = process.argv[1]) {
21382
21367
  const emit = (value) => new Promise((resolve2, reject) => {
21383
21368
  process.stdout.write(JSON.stringify(value) + "\n", (error62) => error62 ? reject(error62) : resolve2());
@@ -21918,6 +21903,7 @@ export {
21918
21903
  PROFILE,
21919
21904
  claudeHookRoot,
21920
21905
  cursorHookRoot,
21906
+ withoutBom,
21921
21907
  setupNodeExecutable,
21922
21908
  safeParse,
21923
21909
  string2 as string,
@@ -21946,6 +21932,7 @@ export {
21946
21932
  isCursorMcpClient,
21947
21933
  cursorParentHooksRegistered,
21948
21934
  verifyCursorParent,
21935
+ handleCursorHook,
21949
21936
  waitProcessCommandLine,
21950
21937
  mergeClaudeParentHooks,
21951
21938
  removeClaudeParentHooks,
@@ -21957,10 +21944,11 @@ export {
21957
21944
  channelClosed,
21958
21945
  sendToChannel,
21959
21946
  channelDeliveryState,
21947
+ withdrawFromChannel,
21960
21948
  channelMarker,
21949
+ handleCursorChannelHook,
21961
21950
  channelReceiveProcess,
21962
21951
  runCodexHookMain,
21963
21952
  runClaudeHookMain,
21964
- runCursorHookMain,
21965
21953
  runChannelReceiveMain
21966
21954
  };
package/dist/cli.mjs CHANGED
@@ -51,8 +51,9 @@ import {
51
51
  verifyCodexParent,
52
52
  verifyCursorParent,
53
53
  waitProcessCommandLine,
54
- withCodexReceiver
55
- } from "./chunks/chunk-LEWADASG.mjs";
54
+ withCodexReceiver,
55
+ withdrawFromChannel
56
+ } from "./chunks/chunk-YBOJCW6Y.mjs";
56
57
 
57
58
  // ../../node_modules/qrcode/lib/can-promise.js
58
59
  var require_can_promise = __commonJS({
@@ -12074,7 +12075,24 @@ async function runDaemon() {
12074
12075
  again = false;
12075
12076
  try {
12076
12077
  const { items } = await api.call("GET", "/deliveries");
12077
- for (const item of items) await deliver(item);
12078
+ const waiting = new Map(items.map((item) => [item.decision_id, item]));
12079
+ for (const [id, entry] of Object.entries(journal)) {
12080
+ if (entry.state !== "queued") continue;
12081
+ const item = waiting.get(id);
12082
+ if (item && item.delivery_id === entry.delivery_id && item.route.channel_id === entry.channel_id) continue;
12083
+ const withdrawn = withdrawFromChannel(PROFILE, entry.channel_id, entry.delivery_id);
12084
+ if (withdrawn || !item) {
12085
+ delete journal[id];
12086
+ save();
12087
+ log(`\u914D\u9001 ${id} \u2192 ${withdrawn ? "withdrawn" : "resolved"}`);
12088
+ } else {
12089
+ Object.assign(entry, safeState(entry) === "emitted" ? { state: "report", result: "delivered" } : { state: "report", result: "unknown", detail: "\u914D\u9001\u5148\u306E\u5909\u66F4\u524D\u306B\u53D7\u3051\u53E3\u304C\u672C\u6587\u3092\u53D6\u5F97\u3057\u307E\u3057\u305F\u3002\u518D\u9001\u3057\u307E\u305B\u3093" });
12090
+ save();
12091
+ await report(id);
12092
+ waiting.delete(id);
12093
+ }
12094
+ }
12095
+ for (const item of waiting.values()) await deliver(item);
12078
12096
  } catch (error) {
12079
12097
  log(`\u914D\u9001\u5F85\u3061\u306E\u53D6\u5F97\u306B\u5931\u6557: ${error.message}`);
12080
12098
  }
@@ -12247,7 +12265,7 @@ function stableNode() {
12247
12265
  return process.execPath;
12248
12266
  }
12249
12267
  }
12250
- var CURSOR_TOOLS = ["list_my_decisions", "request_decision", "amend_decision", "cancel_decision", "get_decision", "get_attachment", "setup_test", "confirm_setup_test"];
12268
+ var CURSOR_TOOLS = ["list_my_decisions", "request_decision", "amend_decision", "resume_decision", "cancel_decision", "get_decision", "get_attachment", "setup_test", "confirm_setup_test"];
12251
12269
  var cursorCliConfig = () => join6(cursorDir(), "cli-config.json");
12252
12270
  function setCursorPermissions(enable) {
12253
12271
  const file = cursorCliConfig();
@@ -15770,7 +15788,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:f
15770
15788
  import { basename as basename2, join as join7 } from "node:path";
15771
15789
 
15772
15790
  // src/version.ts
15773
- var VERSION = true ? "0.1.11" : "0.0.0-dev";
15791
+ var VERSION = true ? "0.1.13" : "0.0.0-dev";
15774
15792
  function osName() {
15775
15793
  return process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : process.platform;
15776
15794
  }
@@ -15786,6 +15804,7 @@ function harnessOf(clientName) {
15786
15804
  return "other";
15787
15805
  }
15788
15806
  var INSTRUCTIONS = [
15807
+ "\u4F1A\u8A71\u304C\u66FF\u308F\u3063\u305F\u3089 list_my_decisions \u3067\u81EA\u5206\u304C\u5F15\u304D\u7D99\u3050\u7533\u8ACB\u3092\u78BA\u304B\u3081\u3001\u7533\u8ACB\u3054\u3068\u306B resume_decision \u3092\u547C\u3076\u3002\u672A\u6C7A\u30FB\u4FDD\u7559\u306E\u7B54\u3048\u306F\u65B0\u3057\u3044\u4F1A\u8A71\u3078\u5C4A\u304D\u3001\u56DE\u7B54\u6E08\u307F\u306A\u3089\u305D\u306E\u5834\u3067\u7B54\u3048\u304C\u8FD4\u308B\u3002",
15789
15808
  "Approval Box\u306F\u3001\u5229\u7528\u8005\uFF08\u4EBA\u9593\uFF09\u306E\u5224\u65AD\u30FB\u627F\u8A8D\u30FB\u64CD\u4F5C\u304C\u8981\u308B\u6642\u306B\u4F7F\u3046\u3002URL\u3092\u958B\u3044\u3066\u306E\u627F\u8A8D\u3084\u3001\u5229\u7528\u8005\u306E\u624B\u3067\u884C\u3046\u4F5C\u696D\u3092\u983C\u3080\u6642\u3082\u542B\u3080\u3002\u305D\u306E\u6642\u306F request_decision \u3067\u3082\u7533\u8ACB\u3059\u308B\u3002\u5229\u7528\u8005\u304C\u7533\u8ACB\u306B\u6C17\u3065\u304D\u3084\u3059\u3044\u306E\u3067\u3001\u5F37\u304F\u63A8\u5968\u3059\u308B\u3002\u7B54\u3048\u306F\u5229\u7528\u8005\u304C\u30B9\u30DE\u30DB\u3084Web\u3067\u51FA\u3057\u3001\u3053\u306E\u4F1A\u8A71\u3078\u81EA\u52D5\u3067\u5C4A\u304F\u3002",
15790
15809
  "request_decision \u3092\u547C\u3076\u3068\u3001\u7533\u8ACB\u306F\u3059\u3050\u306B\u306F\u53D7\u3051\u4ED8\u3051\u3089\u308C\u305A\u3001\u81EA\u5206\u306E\u7533\u8ACB\u306E\u4E00\u89A7\u3068 check_token \u304C\u8FD4\u308B\u3002\u4E00\u89A7\u3092\u898B\u3066\u3001\u5185\u5BB9\u304C\u5909\u308F\u3063\u305F\u7533\u8ACB\u306F amend_decision\u3001\u8981\u3089\u306A\u304F\u306A\u3063\u305F\u7533\u8ACB\u306F cancel_decision \u3067\u5148\u306B\u6E08\u307E\u305B\u308B\u3002\u7B54\u3048\u304C\u51FA\u3066\u3044\u308C\u3070\u3001\u305D\u306E\u7B54\u3048\u306B\u5F93\u3046\u3002\u554F\u984C\u304C\u7121\u3051\u308C\u3070\u3001\u540C\u3058\u7533\u8ACB\u306B check_token \u3092\u4ED8\u3051\u3066\u51FA\u3057\u76F4\u3059\u3068\u53D7\u3051\u4ED8\u3051\u3089\u308C\u308B\u3002",
15791
15810
  "\u7533\u8ACB\u3057\u305F\u3089\u7B54\u3048\u3092\u5F85\u3063\u3066\u4F55\u5EA6\u3082\u547C\u3070\u306A\u3044\u3002\u7B54\u3048\u306F\u5C4A\u304F\u306E\u3067\u3001\u4ED6\u306E\u4F5C\u696D\u3092\u7D9A\u3051\u308B\u304B\u30BF\u30FC\u30F3\u3092\u7D42\u3048\u3066\u3088\u3044\u3002",
@@ -15844,6 +15863,11 @@ var TOOLS = [
15844
15863
  additionalProperties: false
15845
15864
  }
15846
15865
  },
15866
+ {
15867
+ name: "resume_decision",
15868
+ description: "\u524D\u306E\u4F1A\u8A71\u304B\u3089\u5F15\u304D\u7D99\u3044\u3060\u81EA\u5206\u306E\u7533\u8ACB\u3092\u3001\u3053\u306E\u4F1A\u8A71\u3067\u518D\u958B\u3059\u308B\u3002\u672A\u6C7A\u30FB\u4FDD\u7559\u306A\u3089\u7B54\u3048\u306E\u914D\u9001\u5148\u3092\u3053\u306E\u4F1A\u8A71\u3078\u5909\u66F4\u3057\u3001\u56DE\u7B54\u6E08\u307F\u306A\u3089\u7B54\u3048\u3092\u76F4\u63A5\u8FD4\u3059\u3002list_my_decisions\u3067\u5BFE\u8C61\u3092\u78BA\u304B\u3081\u3001\u5F15\u304D\u7D99\u3050\u7533\u8ACB\u3054\u3068\u306B\u547C\u3076\u3002",
15869
+ inputSchema: { type: "object", properties: { decision_id: { type: "string" } }, required: ["decision_id"], additionalProperties: false }
15870
+ },
15847
15871
  {
15848
15872
  name: "cancel_decision",
15849
15873
  description: "\u672A\u6C7A\u30FB\u4FDD\u7559\u306E\u81EA\u5206\u306E\u7533\u8ACB\u3092\u53D6\u308A\u4E0B\u3052\u308B\u3002reason \u306B\u7406\u7531\u3092\u66F8\u304F\u3002",
@@ -16028,10 +16052,21 @@ ${JSON.stringify({ error: "duplicate_suspected", existing })}`, { error: "duplic
16028
16052
  }
16029
16053
  if (name === "amend_decision") {
16030
16054
  const { decision_id, ...rest } = args;
16031
- const decision = await api.call("POST", `/decisions/${encodeURIComponent(String(decision_id))}/amend`, rest);
16032
- return text(`\u76F4\u3057\u307E\u3057\u305F: ${decision.decision_id} version=${decision.version}
16055
+ const channelId = await channelFor(harness, clientName, request.params._meta);
16056
+ const decision = await api.call("POST", `/decisions/${encodeURIComponent(String(decision_id))}/amend`, { ...rest, route: { channel_id: channelId, harness } });
16057
+ lastChannel = channelId;
16058
+ return text(`\u76F4\u3057\u307E\u3057\u305F: ${decision.decision_id} version=${decision.version}${receiveGuide(harness, channelId)}
16033
16059
 
16034
- ${JSON.stringify(decision)}`, decision);
16060
+ ${JSON.stringify(decision)}`, { ...decision, steer_channel: { channel_id: channelId } });
16061
+ }
16062
+ if (name === "resume_decision") {
16063
+ const channelId = await channelFor(harness, clientName, request.params._meta);
16064
+ const decision = await api.call("POST", `/decisions/${encodeURIComponent(String(args.decision_id))}/resume`, { channel_id: channelId, harness });
16065
+ lastChannel = channelId;
16066
+ const head = decision.answer_text ? String(decision.answer_text) : `\u518D\u958B\u3057\u307E\u3057\u305F: ${decision.decision_id}\u3002\u7B54\u3048\u306F\u3053\u306E\u4F1A\u8A71\u3078\u5C4A\u304D\u307E\u3059\u3002`;
16067
+ return text(`${head}${decision.answer_text ? "" : receiveGuide(harness, channelId)}
16068
+
16069
+ ${JSON.stringify(decision)}`, { ...decision, steer_channel: { channel_id: channelId } });
16035
16070
  }
16036
16071
  if (name === "cancel_decision") {
16037
16072
  const decision = await api.call("POST", `/decisions/${encodeURIComponent(String(args.decision_id))}/cancel`, { reason: args.reason });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "approval-box",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Approval Box connector: lets Claude Code, Codex, Cursor and Grok ask you for a decision, and steers your answer back into the same session.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://approval-box.kitepon.dev",