kojee-mcp 0.7.5-staging.16 → 0.7.5-staging.18

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.
@@ -22,6 +22,10 @@ import os from "os";
22
22
  import path from "path";
23
23
 
24
24
  // src/tool-registry.ts
25
+ import { readFileSync, statSync } from "fs";
26
+ import { basename } from "path";
27
+ var UPLOAD_TOOL = "tandem_upload_attachment";
28
+ var MAX_UPLOAD_FILE_BYTES = 25 * 1024 * 1024;
25
29
  var ToolRegistry = class {
26
30
  constructor(gateway) {
27
31
  this.gateway = gateway;
@@ -68,8 +72,87 @@ var ToolRegistry = class {
68
72
  }
69
73
  this.tools.set(tool.name, tool);
70
74
  }
75
+ this.augmentUploadTool();
71
76
  console.error(`[tools] Registered ${this.tools.size} tools from gateway`);
72
77
  }
78
+ /**
79
+ * Teach the gateway's `tandem_upload_attachment` tool about a `file_path` arg
80
+ * that THIS proxy resolves in-process (see {@link resolveUploadFilePath}): the
81
+ * model emits a local path, the proxy reads the bytes and forwards them as
82
+ * `data_base64`, so a multi-MB image never has to be generated into the model's
83
+ * tool call. Purely additive — the original `data_base64` shape still works. A
84
+ * no-op if the gateway doesn't expose the tool (older backend).
85
+ */
86
+ augmentUploadTool() {
87
+ const tool = this.tools.get(UPLOAD_TOOL);
88
+ if (!tool) return;
89
+ const schema = { ...tool.inputSchema ?? {} };
90
+ const properties = { ...schema.properties ?? {} };
91
+ if ("file_path" in properties) return;
92
+ properties.file_path = {
93
+ type: "string",
94
+ description: "Absolute path to a local image file. Your local kojee-mcp-server reads the bytes and uploads them for you (downscaled server-side), so the image is NEVER placed in your prompt/tool-call. Prefer this over data_base64 for photos. Provide file_path OR data_base64, not both; filename is derived from the path when omitted."
95
+ };
96
+ schema.properties = properties;
97
+ if (Array.isArray(schema.required)) {
98
+ schema.required = schema.required.filter(
99
+ (k) => k !== "data_base64" && k !== "filename"
100
+ );
101
+ }
102
+ const dependent = {
103
+ ...schema.dependentRequired ?? {},
104
+ data_base64: ["filename"]
105
+ };
106
+ schema.dependentRequired = dependent;
107
+ this.tools.set(UPLOAD_TOOL, { ...tool, inputSchema: schema });
108
+ }
109
+ /**
110
+ * Resolve a `tandem_upload_attachment` call that used `file_path`: read the file
111
+ * locally, base64-encode the RAW bytes (Buffer→base64 is byte-exact — no text
112
+ * round-trip, so no U+FFFD corruption), and rewrite the args to the `data_base64`
113
+ * shape the gateway expects. Returns the args to forward, or throws with a clear
114
+ * message the caller surfaces as an isError result.
115
+ */
116
+ resolveUploadFilePath(args) {
117
+ const filePath = args.file_path;
118
+ const hasFilePath = typeof filePath === "string" && filePath.length > 0;
119
+ const hasBase64 = typeof args.data_base64 === "string" && args.data_base64.length > 0;
120
+ const hasFilename = typeof args.filename === "string" && args.filename.trim().length > 0;
121
+ if (!hasFilePath) {
122
+ if (hasBase64 && !hasFilename) {
123
+ throw new Error("filename is required when uploading with data_base64");
124
+ }
125
+ return args;
126
+ }
127
+ if (hasBase64) {
128
+ throw new Error("provide file_path OR data_base64, not both");
129
+ }
130
+ let size;
131
+ try {
132
+ const st = statSync(filePath);
133
+ if (!st.isFile()) throw new Error("not a regular file");
134
+ size = st.size;
135
+ } catch (err) {
136
+ throw new Error(`cannot read file_path '${filePath}': ${err?.message ?? String(err)}`);
137
+ }
138
+ if (size > MAX_UPLOAD_FILE_BYTES) {
139
+ throw new Error(
140
+ `file_path is ${size} bytes; exceeds the ${MAX_UPLOAD_FILE_BYTES}-byte upload cap`
141
+ );
142
+ }
143
+ const buf = readFileSync(filePath);
144
+ if (buf.length > MAX_UPLOAD_FILE_BYTES) {
145
+ throw new Error(
146
+ `file_path is ${buf.length} bytes; exceeds the ${MAX_UPLOAD_FILE_BYTES}-byte upload cap`
147
+ );
148
+ }
149
+ const { file_path: _drop, ...rest } = args;
150
+ return {
151
+ ...rest,
152
+ data_base64: buf.toString("base64"),
153
+ filename: hasFilename ? args.filename : basename(filePath)
154
+ };
155
+ }
73
156
  /**
74
157
  * Return all registered tools for the MCP ListTools response — gateway tools
75
158
  * first (minus any a local tool shadows), then local tools.
@@ -114,9 +197,20 @@ var ToolRegistry = class {
114
197
  isError: true
115
198
  };
116
199
  }
200
+ let outgoing = args;
201
+ if (name === UPLOAD_TOOL) {
202
+ try {
203
+ outgoing = this.resolveUploadFilePath(args);
204
+ } catch (err) {
205
+ return {
206
+ content: [{ type: "text", text: err?.message ?? String(err) }],
207
+ isError: true
208
+ };
209
+ }
210
+ }
117
211
  return this.gateway.sendRpc("tools/call", {
118
212
  name,
119
- arguments: args
213
+ arguments: outgoing
120
214
  });
121
215
  }
122
216
  /** Total number of registered tools (gateway + local, shadowed names once). */
@@ -296,7 +390,7 @@ async function startProxy(config) {
296
390
  console.error(`[kojee-mcp] Tandem memberships: ${tandemMembershipCount === -1 ? "unknown" : tandemMembershipCount}`);
297
391
  let server;
298
392
  const isHeadlessDaemon = !!process.env["KOJEE_HEADLESS"] || !!process.env["KOJEE_WEBHOOK_EXPECTED"];
299
- const { selectDelivery } = await import("./registry-BP5V3W5Y.js");
393
+ const { selectDelivery } = await import("./registry-MCJAVXJW.js");
300
394
  const delivery = selectDelivery(adapter.runtime, {
301
395
  supportsChannels: adapter.supportsChannels,
302
396
  headless: isHeadlessDaemon,
@@ -205,7 +205,7 @@ function buildStreamUrl(brokerUrl, sinceValue, seatName) {
205
205
  if (sinceValue !== null) params.set("since", sinceValue);
206
206
  if (seatName) params.set("seat_name", seatName);
207
207
  const qs = params.toString();
208
- return `${brokerUrl}/api/v2/tandems/stream${qs ? "?" + qs : ""}`;
208
+ return `${brokerUrl}/api/v2/tandems/stream/${qs ? "?" + qs : ""}`;
209
209
  }
210
210
  async function openStream(opts, controller, sinceValue, dispatcher) {
211
211
  const url = buildStreamUrl(opts.brokerUrl, sinceValue, opts.seatName);
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-EIAUW6KO.js";
5
5
  import {
6
6
  startProxy
7
- } from "./chunk-LY4DBSBS.js";
7
+ } from "./chunk-Q2UVK7VA.js";
8
8
  import "./chunk-CCVRXJJ2.js";
9
9
  import {
10
10
  pairedConfigPath
@@ -134,7 +134,7 @@ program.command("tail <path>").description("Stream a file's appends and follow (
134
134
  "--replay",
135
135
  "Replay the whole file from the start before following (default: follow from EOF, no replay)"
136
136
  ).action(async (filePath, opts) => {
137
- const { runTail } = await import("./tail-stream-MODWTGVA.js");
137
+ const { runTail } = await import("./tail-stream-PQXEY3TI.js");
138
138
  try {
139
139
  await runTail(filePath, { wakeOnly: opts.wakeOnly === true, replay: opts.replay === true });
140
140
  } catch (err) {
@@ -12,7 +12,7 @@ import {
12
12
  startEventStream,
13
13
  statusReason,
14
14
  undiciFetch
15
- } from "./chunk-XXHYGH7G.js";
15
+ } from "./chunk-UGEBGCYU.js";
16
16
  import "./chunk-Z5LPNJQ6.js";
17
17
  import "./chunk-MIEI4PLB.js";
18
18
  export {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  listTandemIds,
3
3
  startProxy
4
- } from "./chunk-LY4DBSBS.js";
4
+ } from "./chunk-Q2UVK7VA.js";
5
5
  import "./chunk-CCVRXJJ2.js";
6
6
  import "./chunk-PKELUA3T.js";
7
7
  import "./chunk-Z5LPNJQ6.js";
package/dist/lib.d.ts CHANGED
@@ -521,6 +521,23 @@ declare class ToolRegistry {
521
521
  * Fetch all tools with full schemas from the gateway in a single RPC call.
522
522
  */
523
523
  discoverTools(): Promise<void>;
524
+ /**
525
+ * Teach the gateway's `tandem_upload_attachment` tool about a `file_path` arg
526
+ * that THIS proxy resolves in-process (see {@link resolveUploadFilePath}): the
527
+ * model emits a local path, the proxy reads the bytes and forwards them as
528
+ * `data_base64`, so a multi-MB image never has to be generated into the model's
529
+ * tool call. Purely additive — the original `data_base64` shape still works. A
530
+ * no-op if the gateway doesn't expose the tool (older backend).
531
+ */
532
+ private augmentUploadTool;
533
+ /**
534
+ * Resolve a `tandem_upload_attachment` call that used `file_path`: read the file
535
+ * locally, base64-encode the RAW bytes (Buffer→base64 is byte-exact — no text
536
+ * round-trip, so no U+FFFD corruption), and rewrite the args to the `data_base64`
537
+ * shape the gateway expects. Returns the args to forward, or throws with a clear
538
+ * message the caller surfaces as an isError result.
539
+ */
540
+ private resolveUploadFilePath;
524
541
  /**
525
542
  * Return all registered tools for the MCP ListTools response — gateway tools
526
543
  * first (minus any a local tool shadows), then local tools.
package/dist/lib.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  normalizeBackendEvent,
3
3
  sanitizeDisplayname,
4
4
  startEventStream
5
- } from "./chunk-XXHYGH7G.js";
5
+ } from "./chunk-UGEBGCYU.js";
6
6
  import {
7
7
  resubscribeMemberships
8
8
  } from "./chunk-OT2GILXC.js";
@@ -125,7 +125,7 @@ function createClaudeCodeDelivery() {
125
125
  const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
126
126
  const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
127
127
  const { createWebhookSink } = await import("./webhook-sink-N6AUTFL3.js");
128
- const { startEventStream } = await import("./event-stream-K6G7B2KY.js");
128
+ const { startEventStream } = await import("./event-stream-5ZEL7FXN.js");
129
129
  const { createMcpServer } = await import("./server-VM3ZIGSZ.js");
130
130
  const { deriveDiscoveryKey } = await import("./ancestry-A2F5KQ6A.js");
131
131
  const { resolveSharedSessionId } = await import("./cc-session-id-5HHQJ7VC.js");
@@ -325,7 +325,7 @@ function createWebhookDelivery(name, pendingLedger) {
325
325
  const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
326
326
  const { createWebhookSink } = await import("./webhook-sink-N6AUTFL3.js");
327
327
  const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
328
- const { startEventStream } = await import("./event-stream-K6G7B2KY.js");
328
+ const { startEventStream } = await import("./event-stream-5ZEL7FXN.js");
329
329
  const { createMcpServer } = await import("./server-VM3ZIGSZ.js");
330
330
  sweepStaleEventLogs();
331
331
  eventLog = startEventLog({
@@ -521,7 +521,7 @@ function createOpenclawDelivery(deps = {}) {
521
521
  async start(ctx) {
522
522
  const { startEventLog, sweepStaleEventLogs } = await import("./event-log-M22X3FAI.js");
523
523
  const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
524
- const { startEventStream } = await import("./event-stream-K6G7B2KY.js");
524
+ const { startEventStream } = await import("./event-stream-5ZEL7FXN.js");
525
525
  const { createMcpServer } = await import("./server-VM3ZIGSZ.js");
526
526
  sweepStaleEventLogs();
527
527
  eventLog = startEventLog({
@@ -9,7 +9,7 @@ import {
9
9
  LOG_HEARTBEAT_INTERVAL_MS,
10
10
  UNARMED_FALLBACK_MS,
11
11
  createAdaptiveWatchdog
12
- } from "./chunk-XXHYGH7G.js";
12
+ } from "./chunk-UGEBGCYU.js";
13
13
  import "./chunk-Z5LPNJQ6.js";
14
14
  import "./chunk-MIEI4PLB.js";
15
15
  import "./chunk-U5HHHRXA.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kojee-mcp",
3
- "version": "0.7.5-staging.16",
3
+ "version": "0.7.5-staging.18",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {