approval-box 0.1.19 → 0.1.21

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.mjs +101 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -50,3 +50,7 @@ Requires Node.js 20 or later. License: MIT.
50
50
 
51
51
  この機能はコネクタ0.1.13以降と対応サーバーを使います。会話を開き直しても、
52
52
  再開を指定するまでは元の会話が配送先です。答えは `get_decision` でも取得できます。
53
+
54
+ ### 申請に画像を添える
55
+
56
+ `request_decision` に `image_paths: ["/absolute/path/screenshot.png"]` を指定すると、AIの画像を本文下に添えて申請します。10件、単体20MiB、合計50MiBまで。全画像を上げてから申請と通知を確定します。最初の確認の札を付けた出し直しでも同じ画像パスを渡してください。`amend_decision.changes.image_paths` は全置換、空配列で削除です。回答添付とは別の欄です。リモートMCPは `upload_request_image` のIDを `request_attachment_ids` に渡します。
package/dist/cli.mjs CHANGED
@@ -1336,11 +1336,11 @@ var require_dijkstra = __commonJS({
1336
1336
  var predecessors = {};
1337
1337
  var costs = {};
1338
1338
  costs[s] = 0;
1339
- var open = dijkstra.PriorityQueue.make();
1340
- open.push(s, 0);
1339
+ var open2 = dijkstra.PriorityQueue.make();
1340
+ open2.push(s, 0);
1341
1341
  var closest, u, v, cost_of_s_to_u, adjacent_nodes, cost_of_e, cost_of_s_to_u_plus_cost_of_e, cost_of_s_to_v, first_visit;
1342
- while (!open.empty()) {
1343
- closest = open.pop();
1342
+ while (!open2.empty()) {
1343
+ closest = open2.pop();
1344
1344
  u = closest.value;
1345
1345
  cost_of_s_to_u = closest.cost;
1346
1346
  adjacent_nodes = graph[u] || {};
@@ -1352,7 +1352,7 @@ var require_dijkstra = __commonJS({
1352
1352
  first_visit = typeof costs[v] === "undefined";
1353
1353
  if (first_visit || cost_of_s_to_v > cost_of_s_to_u_plus_cost_of_e) {
1354
1354
  costs[v] = cost_of_s_to_u_plus_cost_of_e;
1355
- open.push(v, cost_of_s_to_u_plus_cost_of_e);
1355
+ open2.push(v, cost_of_s_to_u_plus_cost_of_e);
1356
1356
  predecessors[v] = u;
1357
1357
  }
1358
1358
  }
@@ -11761,7 +11761,7 @@ var require_dist = __commonJS({
11761
11761
  var import_qrcode = __toESM(require_lib(), 1);
11762
11762
  import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync5, rmSync as rmSync3 } from "node:fs";
11763
11763
  import { hostname } from "node:os";
11764
- import { join as join9 } from "node:path";
11764
+ import { join as join10 } from "node:path";
11765
11765
  import { createInterface } from "node:readline/promises";
11766
11766
 
11767
11767
  // src/api.ts
@@ -11814,6 +11814,17 @@ var Api = class {
11814
11814
  }
11815
11815
  return json;
11816
11816
  }
11817
+ /** 申請前画像をバイナリで上げる。 */
11818
+ async uploadRequestImage(data, name, contentType) {
11819
+ const response = await fetch(this.url(`/request-images?name=${encodeURIComponent(name)}`), {
11820
+ method: "POST",
11821
+ headers: { authorization: `Bearer ${this.token}`, "content-type": contentType },
11822
+ body: new Uint8Array(data)
11823
+ });
11824
+ const json = await response.json();
11825
+ if (!response.ok) throw new ServerError(response.status, json.error?.code ?? "internal", json.error?.message ?? `HTTP ${response.status}`, json);
11826
+ return json;
11827
+ }
11817
11828
  /** ファイルを取る(添付)。誤りはJSONの本文から読む。 */
11818
11829
  async download(path) {
11819
11830
  let response;
@@ -12192,8 +12203,8 @@ async function runDaemon() {
12192
12203
  await sync();
12193
12204
  try {
12194
12205
  const { items } = await api.call("GET", "/decisions");
12195
- const open = items.some((d) => d.status === "pending" || d.status === "held");
12196
- if (open || Object.keys(journal).length) idleSince = Date.now();
12206
+ const open2 = items.some((d) => d.status === "pending" || d.status === "held");
12207
+ if (open2 || Object.keys(journal).length) idleSince = Date.now();
12197
12208
  else if (Date.now() - idleSince > IDLE_EXIT_MS) stop();
12198
12209
  } catch {
12199
12210
  idleSince = Date.now();
@@ -15825,11 +15836,67 @@ var StdioServerTransport = class {
15825
15836
  };
15826
15837
 
15827
15838
  // src/mcp.ts
15828
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
15829
- import { basename as basename2, join as join8 } from "node:path";
15839
+ import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
15840
+ import { basename as basename3, join as join9 } from "node:path";
15841
+
15842
+ // src/request-images.ts
15843
+ import { open } from "node:fs/promises";
15844
+ import { basename as basename2 } from "node:path";
15845
+ var MAX_FILE = 20 * 1024 * 1024;
15846
+ var MAX_TOTAL = 50 * 1024 * 1024;
15847
+ function imageType(b) {
15848
+ if (b.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) return "image/png";
15849
+ if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255) return "image/jpeg";
15850
+ if (b.subarray(0, 4).toString() === "GIF8") return "image/gif";
15851
+ if (b.subarray(0, 4).toString() === "RIFF" && b.subarray(8, 12).toString() === "WEBP") return "image/webp";
15852
+ if (b.subarray(4, 8).toString() === "ftyp" && ["heic", "heix", "hevc", "hevx", "heim", "heis", "mif1", "msf1", "heif"].includes(b.subarray(8, 12).toString())) return "image/heic";
15853
+ throw new Error("\u7533\u8ACB\u306B\u4ED8\u3051\u3089\u308C\u308B\u306E\u306FJPEG\u30FBPNG\u30FBGIF\u30FBWebP\u30FBHEIC\u753B\u50CF\u3067\u3059\u3002");
15854
+ }
15855
+ async function uploadImagePaths(api, paths) {
15856
+ if (!Array.isArray(paths) || paths.length > 10 || paths.some((p) => typeof p !== "string" || !p.trim())) throw new Error("image_paths\u306F10\u4EF6\u307E\u3067\u306E\u753B\u50CF\u30D5\u30A1\u30A4\u30EB\u30D1\u30B9\u914D\u5217\u3067\u3059\u3002");
15857
+ const images = [];
15858
+ let total = 0;
15859
+ for (const path of paths) {
15860
+ const file = await open(path, "r");
15861
+ try {
15862
+ const stat = await file.stat();
15863
+ if (!stat.isFile() || !stat.size || stat.size > MAX_FILE) throw new Error(`${basename2(path)}: \u7A7A\u3067\u306A\u3044\u901A\u5E38\u30D5\u30A1\u30A4\u30EB\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\uFF0820MiB\u307E\u3067\uFF09\u3002`);
15864
+ total += stat.size;
15865
+ if (total > MAX_TOTAL) throw new Error("\u7533\u8ACB\u753B\u50CF\u306F\u5408\u8A0850MiB\u307E\u3067\u3067\u3059\u3002");
15866
+ const data = Buffer.alloc(stat.size + 1);
15867
+ let n = 0;
15868
+ while (n < data.length) {
15869
+ const r = await file.read(data, n, data.length - n, n);
15870
+ if (!r.bytesRead) break;
15871
+ n += r.bytesRead;
15872
+ }
15873
+ if (n !== stat.size) throw new Error(`${basename2(path)}: \u8AAD\u307F\u53D6\u308A\u4E2D\u306B\u753B\u50CF\u304C\u5909\u308F\u308A\u307E\u3057\u305F\u3002`);
15874
+ const bytes = data.subarray(0, n);
15875
+ images.push({ name: basename2(path), data: bytes, type: imageType(bytes) });
15876
+ } finally {
15877
+ await file.close();
15878
+ }
15879
+ }
15880
+ const ids = [];
15881
+ for (const image of images) ids.push((await api.uploadRequestImage(image.data, image.name, image.type)).id);
15882
+ return ids;
15883
+ }
15884
+
15885
+ // src/claude-channel.ts
15886
+ import { readFileSync as readFileSync6 } from "node:fs";
15887
+ import { join as join8 } from "node:path";
15888
+ function claudeChannelIsCurrent(profile, channelId, parent) {
15889
+ try {
15890
+ const bound = readChannel(profile, channelId).claude;
15891
+ const now = JSON.parse(readFileSync6(join8(parent.hook_root, parent.request_id, "request.json"), "utf8"));
15892
+ return !!bound && bound.session_id === parent.session_id && bound.parent_pid === now.parent_pid && bound.parent_started_identity === now.parent_started_identity;
15893
+ } catch {
15894
+ return false;
15895
+ }
15896
+ }
15830
15897
 
15831
15898
  // src/version.ts
15832
- var VERSION = true ? "0.1.19" : "0.0.0-dev";
15899
+ var VERSION = true ? "0.1.21" : "0.0.0-dev";
15833
15900
  function osName() {
15834
15901
  return process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : process.platform;
15835
15902
  }
@@ -15872,6 +15939,8 @@ var TOOLS = [
15872
15939
  deadline: { type: "string", description: "\u671F\u9650\uFF08ISO 8601\uFF09" },
15873
15940
  session_label: { type: "string", description: "\u3069\u306E\u4F5C\u696D\u306E\u8A71\u304B\uFF08\u4F8B: \u30EA\u30DD\u30B8\u30C8\u30EA\u540D / \u4F5C\u696D\u540D\uFF09\u3002\u7701\u7565\u3059\u308B\u3068\u4F5C\u696D\u30D5\u30A9\u30EB\u30C0\u540D" },
15874
15941
  distinct_reason: { type: "string", description: "\u540C\u3058\u4EF6\u540D\u306E\u672A\u6C7A\u304C\u3042\u308B\u306E\u306B\u5225\u4EF6\u3068\u3057\u3066\u51FA\u3059\u6642\u3060\u3051\u3001\u305D\u306E\u7406\u7531" },
15942
+ image_paths: { type: "array", items: { type: "string" }, maxItems: 10, description: "\u7533\u8ACB\u306B\u6DFB\u3048\u308B\u30ED\u30FC\u30AB\u30EB\u753B\u50CF\u30D1\u30B9\u300210\u4EF6/\u5358\u4F5320MiB/\u5408\u8A0850MiB\u307E\u3067\u3002\u30D1\u30B9\u81EA\u4F53\u306F\u4EBA\u3078\u9001\u3089\u306A\u3044" },
15943
+ request_attachment_ids: { type: "array", items: { type: "string" }, maxItems: 10 },
15875
15944
  check_token: { type: "string", description: "1\u56DE\u76EE\u306E request_decision \u3067\u8FD4\u3063\u305F\u78BA\u8A8D\u306E\u672D\u3002\u4E00\u89A7\u3092\u78BA\u304B\u3081\u3001\u76F4\u3059\u30FB\u53D6\u308A\u4E0B\u3052\u308B\u7533\u8ACB\u304C\u7121\u3044\u3068\u5224\u65AD\u3057\u305F\u6642\u306B\u4ED8\u3051\u308B" }
15876
15945
  },
15877
15946
  required: ["title", "options"],
@@ -15890,6 +15959,8 @@ var TOOLS = [
15890
15959
  changes: {
15891
15960
  type: "object",
15892
15961
  properties: {
15962
+ image_paths: { type: "array", items: { type: "string" }, maxItems: 10 },
15963
+ request_attachment_ids: { type: "array", items: { type: "string" }, maxItems: 10 },
15893
15964
  title: { type: "string" },
15894
15965
  context: { type: "string" },
15895
15966
  options: { type: "array", items: optionSchema, minItems: 2, maxItems: 6 },
@@ -15937,12 +16008,12 @@ var TOOLS = [
15937
16008
  ];
15938
16009
  var VIEWABLE_IMAGES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
15939
16010
  var MAX_INLINE_IMAGE = 5 * 1024 * 1024;
15940
- var parentsFile = () => join8(stateRoot(), "parents.json");
16011
+ var parentsFile = () => join9(stateRoot(), "parents.json");
15941
16012
  var processChannels = /* @__PURE__ */ new Map();
15942
16013
  var lastChannel;
15943
16014
  function savedParents() {
15944
16015
  try {
15945
- return existsSync6(parentsFile()) ? JSON.parse(readFileSync6(parentsFile(), "utf8")) : {};
16016
+ return existsSync6(parentsFile()) ? JSON.parse(readFileSync7(parentsFile(), "utf8")) : {};
15946
16017
  } catch {
15947
16018
  return {};
15948
16019
  }
@@ -15979,7 +16050,7 @@ async function channelFor(harness, clientName, meta) {
15979
16050
  if (!parent) throw new Error("Claude Code\u306Ehook\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002npx approval-box setup \u3092\u3084\u308A\u76F4\u3057\u3001Claude Code\u3092\u518D\u8D77\u52D5\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
15980
16051
  const key2 = `claude:${parent.session_id}`;
15981
16052
  const existing2 = reuse(key2, true);
15982
- if (existing2) return existing2;
16053
+ if (existing2 && claudeChannelIsCurrent(PROFILE, existing2, parent)) return existing2;
15983
16054
  const channel2 = openChannel(PROFILE, parent);
15984
16055
  remember(key2, channel2.channel_id, true);
15985
16056
  return channel2.channel_id;
@@ -16046,10 +16117,14 @@ ${JSON.stringify({ items })}`, { items });
16046
16117
  if (name === "request_decision") {
16047
16118
  const channelId = await channelFor(harness, clientName, request.params._meta);
16048
16119
  lastChannel = channelId;
16120
+ const { image_paths, ...rest } = args;
16121
+ if (image_paths !== void 0 && rest.request_attachment_ids !== void 0) throw new Error("image_paths\u3068request_attachment_ids\u306F\u7247\u65B9\u3060\u3051\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
16122
+ const request_attachment_ids = image_paths !== void 0 ? await uploadImagePaths(api, image_paths) : rest.request_attachment_ids;
16049
16123
  const body = {
16050
- ...args,
16124
+ ...rest,
16125
+ ...request_attachment_ids !== void 0 ? { request_attachment_ids } : {},
16051
16126
  client: CLIENT_OF[harness],
16052
- session_label: args.session_label || `${basename2(process.cwd())} (${CLIENT_OF[harness]})`,
16127
+ session_label: args.session_label || `${basename3(process.cwd())} (${CLIENT_OF[harness]})`,
16053
16128
  route: { channel_id: channelId, harness }
16054
16129
  };
16055
16130
  try {
@@ -16088,6 +16163,10 @@ ${JSON.stringify({ error: "duplicate_suspected", existing })}`, { error: "duplic
16088
16163
  }
16089
16164
  if (name === "amend_decision") {
16090
16165
  const { decision_id, ...rest } = args;
16166
+ const { image_paths, ...changes } = rest.changes ?? {};
16167
+ if (image_paths !== void 0 && changes.request_attachment_ids !== void 0) throw new Error("image_paths\u3068request_attachment_ids\u306F\u7247\u65B9\u3060\u3051\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
16168
+ if (image_paths !== void 0) changes.request_attachment_ids = await uploadImagePaths(api, image_paths);
16169
+ rest.changes = changes;
16091
16170
  const channelId = await channelFor(harness, clientName, request.params._meta);
16092
16171
  const decision = await api.call("POST", `/decisions/${encodeURIComponent(String(decision_id))}/amend`, { ...rest, route: { channel_id: channelId, harness } });
16093
16172
  lastChannel = channelId;
@@ -16122,7 +16201,7 @@ ${JSON.stringify(decision)}`, decision);
16122
16201
  if (name === "get_attachment") {
16123
16202
  const decisionId = String(args.decision_id);
16124
16203
  const decision = await api.call("GET", `/decisions/${encodeURIComponent(decisionId)}`);
16125
- const list = decision.answer?.attachments ?? [];
16204
+ const list = [...decision.request_attachments ?? [], ...decision.answer?.attachments ?? []];
16126
16205
  const index = list.findIndex((a) => a.id === args.attachment_id);
16127
16206
  if (index < 0) return text(`Approval Box: ${decisionId} \u306E\u7B54\u3048\u306B\u3001\u305D\u306E\u6DFB\u4ED8\u306F\u3042\u308A\u307E\u305B\u3093\u3002get_decision \u306E answer.attachments \u3092\u78BA\u304B\u3081\u3066\u304F\u3060\u3055\u3044\u3002`, { error: "not_found" }, true);
16128
16207
  const meta = list[index];
@@ -16130,7 +16209,7 @@ ${JSON.stringify(decision)}`, decision);
16130
16209
  const head = `${decisionId} \u306E\u6DFB\u4ED8 ${index + 1}: ${meta.name}\uFF08${meta.content_type}\u3001${meta.size}\u30D0\u30A4\u30C8\uFF09\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F: ${path}`;
16131
16210
  const structured = { decision_id: decisionId, attachment: meta, path };
16132
16211
  if (VIEWABLE_IMAGES.includes(meta.content_type) && meta.size <= MAX_INLINE_IMAGE) {
16133
- return { content: [{ type: "text", text: head }, { type: "image", data: readFileSync6(path).toString("base64"), mimeType: meta.content_type }], structuredContent: structured };
16212
+ return { content: [{ type: "text", text: head }, { type: "image", data: readFileSync7(path).toString("base64"), mimeType: meta.content_type }], structuredContent: structured };
16134
16213
  }
16135
16214
  return text(head, structured);
16136
16215
  }
@@ -16140,7 +16219,7 @@ ${JSON.stringify(decision)}`, decision);
16140
16219
  const decision = await api.call("POST", "/setup-test", {
16141
16220
  client: CLIENT_OF[harness],
16142
16221
  os: osName(),
16143
- session_label: `\u63A5\u7D9A\u30C6\u30B9\u30C8 ${basename2(process.cwd())} (${CLIENT_OF[harness]})`,
16222
+ session_label: `\u63A5\u7D9A\u30C6\u30B9\u30C8 ${basename3(process.cwd())} (${CLIENT_OF[harness]})`,
16144
16223
  route: { channel_id: channelId, harness }
16145
16224
  });
16146
16225
  const message = `\u63A5\u7D9A\u30C6\u30B9\u30C8\u306E\u7533\u8ACB\u3092\u9001\u308A\u307E\u3057\u305F\uFF08${decision.decision_id}\uFF09\u3002\u5229\u7528\u8005\u306B\u30A2\u30D7\u30EA\u304BWeb\u7248\u3067\u7B54\u3048\u308B\u3088\u3046\u4F1D\u3048\u3066\u304F\u3060\u3055\u3044\u3002\u7B54\u3048\u304C\u3053\u306E\u4F1A\u8A71\u3078\u5C4A\u3044\u305F\u3089\u3001\u305D\u3053\u306B\u66F8\u304B\u308C\u305F\u78BA\u8A8D\u30B3\u30FC\u30C9\u3067 confirm_setup_test \u3092\u547C\u3093\u3067\u304F\u3060\u3055\u3044\u3002`;
@@ -16196,13 +16275,13 @@ async function ask(question) {
16196
16275
  }
16197
16276
  }
16198
16277
  function installRuntime() {
16199
- const target = join9(home(), "runtime", VERSION);
16278
+ const target = join10(home(), "runtime", VERSION);
16200
16279
  if (runtimeDir() !== target) {
16201
16280
  rmSync3(target, { recursive: true, force: true });
16202
16281
  mkdirSync5(target, { recursive: true });
16203
16282
  cpSync(runtimeDir(), target, { recursive: true });
16204
16283
  }
16205
- return join9(target, "cli.mjs");
16284
+ return join10(target, "cli.mjs");
16206
16285
  }
16207
16286
  function selectedTargets() {
16208
16287
  const only = option("only");
@@ -16347,7 +16426,7 @@ async function uninstall() {
16347
16426
  out(` \u30B5\u30FC\u30D0\u30FC: \u63A5\u7D9A\u306E\u89E3\u9664\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF08\u30A2\u30D7\u30EA\u306E\u300C\u63A5\u7D9A\u300D\u304B\u3089\u5916\u3057\u3066\u304F\u3060\u3055\u3044\uFF09: ${error.message}`);
16348
16427
  }
16349
16428
  }
16350
- rmSync3(join9(home(), "config.json"), { force: true });
16429
+ rmSync3(join10(home(), "config.json"), { force: true });
16351
16430
  out("\nApproval Box\u3092\u5916\u3057\u307E\u3057\u305F\u3002npm install -g \u3067\u5165\u308C\u305F\u5834\u5408\u306F\u3001npm uninstall -g approval-box \u3067\u672C\u4F53\u3082\u6D88\u305B\u307E\u3059\u3002");
16352
16431
  }
16353
16432
  async function status() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "approval-box",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
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",