@0xmaxma/claude-gateway 1.5.8 → 1.6.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.
Files changed (65) hide show
  1. package/README.md +22 -0
  2. package/dist/agent/runner.d.ts +30 -0
  3. package/dist/agent/runner.d.ts.map +1 -1
  4. package/dist/agent/runner.js +223 -16
  5. package/dist/agent/runner.js.map +1 -1
  6. package/dist/api/apps-router.d.ts.map +1 -1
  7. package/dist/api/apps-router.js +7 -2
  8. package/dist/api/apps-router.js.map +1 -1
  9. package/dist/api/gateway-router.d.ts.map +1 -1
  10. package/dist/api/gateway-router.js +27 -0
  11. package/dist/api/gateway-router.js.map +1 -1
  12. package/dist/api/line-webhook-router.d.ts.map +1 -1
  13. package/dist/api/line-webhook-router.js +68 -0
  14. package/dist/api/line-webhook-router.js.map +1 -1
  15. package/dist/api/router.d.ts +2 -0
  16. package/dist/api/router.d.ts.map +1 -1
  17. package/dist/api/router.js +59 -3
  18. package/dist/api/router.js.map +1 -1
  19. package/dist/api/share-router.d.ts +11 -0
  20. package/dist/api/share-router.d.ts.map +1 -0
  21. package/dist/api/share-router.js +402 -0
  22. package/dist/api/share-router.js.map +1 -0
  23. package/dist/apps/installer.d.ts +55 -0
  24. package/dist/apps/installer.d.ts.map +1 -1
  25. package/dist/apps/installer.js +130 -0
  26. package/dist/apps/installer.js.map +1 -1
  27. package/dist/config/loader.d.ts.map +1 -1
  28. package/dist/config/loader.js +9 -0
  29. package/dist/config/loader.js.map +1 -1
  30. package/dist/config/public-url.d.ts +11 -0
  31. package/dist/config/public-url.d.ts.map +1 -0
  32. package/dist/config/public-url.js +41 -0
  33. package/dist/config/public-url.js.map +1 -0
  34. package/dist/config/watcher.d.ts.map +1 -1
  35. package/dist/config/watcher.js +1 -0
  36. package/dist/config/watcher.js.map +1 -1
  37. package/dist/history/db.d.ts +14 -0
  38. package/dist/history/db.d.ts.map +1 -1
  39. package/dist/history/db.js +48 -4
  40. package/dist/history/db.js.map +1 -1
  41. package/dist/history/types.d.ts +5 -0
  42. package/dist/history/types.d.ts.map +1 -1
  43. package/dist/session/process.d.ts.map +1 -1
  44. package/dist/session/process.js +17 -1
  45. package/dist/session/process.js.map +1 -1
  46. package/dist/session/store.d.ts.map +1 -1
  47. package/dist/session/store.js +9 -1
  48. package/dist/session/store.js.map +1 -1
  49. package/dist/share/session-image-catalog.d.ts +22 -0
  50. package/dist/share/session-image-catalog.d.ts.map +1 -0
  51. package/dist/share/session-image-catalog.js +151 -0
  52. package/dist/share/session-image-catalog.js.map +1 -0
  53. package/dist/share/share-store.d.ts +137 -0
  54. package/dist/share/share-store.d.ts.map +1 -0
  55. package/dist/share/share-store.js +340 -0
  56. package/dist/share/share-store.js.map +1 -0
  57. package/dist/types.d.ts +17 -7
  58. package/dist/types.d.ts.map +1 -1
  59. package/mcp/instructions.ts +1 -0
  60. package/mcp/server.ts +2 -0
  61. package/mcp/tools/image/module.ts +240 -26
  62. package/mcp/tools/line/module.ts +186 -0
  63. package/mcp/tools/share-image/module.ts +120 -0
  64. package/mcp/tools/shared/share-client.ts +197 -0
  65. package/package.json +3 -1
@@ -10,9 +10,34 @@
10
10
  * is passed, and fall back to push if it's absent or expired/used (the single-use
11
11
  * token has no guaranteed lifetime — ~1 min — so push remains the safety net).
12
12
  */
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
13
15
  import type { ToolModule, McpToolDefinition, McpToolResult, ToolVisibility } from '../../types';
14
16
  import { messagingApi } from '@line/bot-sdk';
15
17
  import { chunkText, planLineSend, LINE_TEXT_LIMIT } from './pure';
18
+ import { createShares, ShareClientError, shareBridgeEnabled } from '../shared/share-client';
19
+
20
+ /** Default lifetime of a share URL handed to LINE (1 hour). */
21
+ const DEFAULT_MEDIA_URL_TTL_MS = 60 * 60 * 1000;
22
+
23
+ /**
24
+ * Convert a media path (absolute under the agent media root, or already relative
25
+ * like "media/<chat>/<file>.png") to the "media/…"-relative form the gateway's
26
+ * signed-URL route expects. Returns null if it cannot be resolved to the media root.
27
+ */
28
+ function toRelativeMediaPath(p: string): string | null {
29
+ if (!p) return null;
30
+ const normalized = p.replace(/\\/g, '/');
31
+ if (!path.isAbsolute(normalized)) {
32
+ return normalized.startsWith('media/') ? normalized : `media/${normalized.replace(/^\/+/, '')}`;
33
+ }
34
+ const workspace = process.env.GATEWAY_WORKSPACE_DIR;
35
+ if (!workspace) return null;
36
+ const mediaRoot = path.resolve(workspace, '..', 'media');
37
+ const rel = path.relative(mediaRoot, path.resolve(normalized));
38
+ if (rel.startsWith('..') || path.isAbsolute(rel)) return null;
39
+ return `media/${rel.split(path.sep).join('/')}`;
40
+ }
16
41
 
17
42
  export class LineModule implements ToolModule {
18
43
  id = 'line';
@@ -54,11 +79,43 @@ export class LineModule implements ToolModule {
54
79
  required: ['chat_id', 'text'],
55
80
  },
56
81
  },
82
+ {
83
+ name: 'line_image',
84
+ description:
85
+ 'Send an image to the current LINE user. Pass chat_id (the LINE userId from the ' +
86
+ '<channel> tag) and image (a media path saved by the gateway, e.g. a generate_image ' +
87
+ 'result path or a "media/…" path). LINE can only fetch public HTTPS URLs, so the tool ' +
88
+ 'mints a signed, short-lived public URL for the file and sends it as an image message. ' +
89
+ 'Pass reply_token from the <channel> tag when present to send for FREE (falls back to push).',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ chat_id: {
94
+ type: 'string',
95
+ description: 'LINE userId to send to (the chat_id from the channel turn).',
96
+ },
97
+ image: {
98
+ type: 'string',
99
+ description: 'Media path of the image (absolute path under the agent media dir, or a "media/…" relative path).',
100
+ },
101
+ preview: {
102
+ type: 'string',
103
+ description: 'Optional separate preview-image media path. Defaults to the same image.',
104
+ },
105
+ reply_token: {
106
+ type: 'string',
107
+ description: 'Optional single-use reply token from the <channel> tag (sends free; falls back to push).',
108
+ },
109
+ },
110
+ required: ['chat_id', 'image'],
111
+ },
112
+ },
57
113
  ];
58
114
  }
59
115
 
60
116
  async handleTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
61
117
  if (name === 'line_reply') return this.handleReply(args);
118
+ if (name === 'line_image') return this.handleImage(args);
62
119
  return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
63
120
  }
64
121
 
@@ -124,4 +181,133 @@ export class LineModule implements ToolModule {
124
181
  };
125
182
  }
126
183
  }
184
+
185
+ /**
186
+ * Send an image to LINE. LINE's servers fetch the image over public HTTPS (the
187
+ * normal media route is API-key-gated), so we mint a short-lived share token via
188
+ * the gateway share bridge — the SAME `/shared/:token` primitive the image tools
189
+ * use — and hand LINE the resulting public URL. The host comes from `.public-base`
190
+ * (derived by the gateway from the inbound webhook); the token is host-agnostic,
191
+ * so we simply prepend that base to `/shared/<token>`.
192
+ */
193
+ private async handleImage(args: Record<string, unknown>): Promise<McpToolResult> {
194
+ const chatId = typeof args.chat_id === 'string' ? args.chat_id : '';
195
+ const image = typeof args.image === 'string' ? args.image : '';
196
+ const preview = typeof args.preview === 'string' && args.preview ? args.preview : image;
197
+ const replyToken = typeof args.reply_token === 'string' ? args.reply_token : '';
198
+ const token = process.env.LINE_CHANNEL_ACCESS_TOKEN ?? '';
199
+
200
+ if (!chatId) {
201
+ return { content: [{ type: 'text', text: 'line_image: missing chat_id' }], isError: true };
202
+ }
203
+ if (!image) {
204
+ return { content: [{ type: 'text', text: 'line_image: image path is required' }], isError: true };
205
+ }
206
+ // Fail fast on a missing LINE token BEFORE any file IO or share mint, so a
207
+ // misconfigured pod never mints a throwaway share it can't deliver.
208
+ if (!token) {
209
+ return { content: [{ type: 'text', text: 'line_image: missing LINE_CHANNEL_ACCESS_TOKEN' }], isError: true };
210
+ }
211
+
212
+ // Public base URL is derived by the gateway from the inbound LINE webhook and
213
+ // written to `<workspace>/../.public-base` (no public-base-URL env var). Read it
214
+ // here at call-time; if it isn't there yet the pod hasn't seen a webhook, so
215
+ // ask the user to send another message rather than pushing a broken URL.
216
+ const workspace = process.env.GATEWAY_WORKSPACE_DIR;
217
+ if (!workspace) {
218
+ return {
219
+ content: [{ type: 'text', text: 'line_image: GATEWAY_WORKSPACE_DIR not set' }],
220
+ isError: true,
221
+ };
222
+ }
223
+ let publicBase = '';
224
+ try {
225
+ publicBase = fs
226
+ .readFileSync(path.resolve(workspace, '..', '.public-base'), 'utf8')
227
+ .trim()
228
+ .replace(/\/+$/, '');
229
+ } catch {
230
+ publicBase = '';
231
+ }
232
+ if (!publicBase) {
233
+ return {
234
+ content: [{ type: 'text', text: 'line_image: public base not resolved yet (send a message again)' }],
235
+ isError: true,
236
+ };
237
+ }
238
+ // The share bridge (the same authenticated local gateway API the image tools
239
+ // use) needs the session identity injected into this subprocess. Missing it =
240
+ // image delivery isn't wired up on this pod.
241
+ if (!shareBridgeEnabled()) {
242
+ return {
243
+ content: [{ type: 'text', text: 'line_image: image delivery not configured (share bridge unavailable)' }],
244
+ isError: true,
245
+ };
246
+ }
247
+
248
+ const relImage = toRelativeMediaPath(image);
249
+ const relPreview = toRelativeMediaPath(preview);
250
+ if (!relImage || !relPreview) {
251
+ return {
252
+ content: [{ type: 'text', text: `line_image: could not resolve media path to the agent media root: ${image}` }],
253
+ isError: true,
254
+ };
255
+ }
256
+
257
+ const ttlMs = Number(process.env.GATEWAY_MEDIA_URL_TTL_MS) > 0
258
+ ? Number(process.env.GATEWAY_MEDIA_URL_TTL_MS)
259
+ : DEFAULT_MEDIA_URL_TTL_MS;
260
+ const ttlSeconds = Math.max(10, Math.ceil(ttlMs / 1000));
261
+
262
+ // Mint one share per ref (order preserved). Identical refs dedupe to the same
263
+ // token within the store's idempotency window — fine, LINE gets a valid URL.
264
+ let originalContentUrl: string;
265
+ let previewImageUrl: string;
266
+ try {
267
+ const items = await createShares(
268
+ [{ path: relImage }, { path: relPreview }],
269
+ { purpose: 'line', ttlSeconds },
270
+ );
271
+ if (!items[0]?.token || !items[1]?.token) {
272
+ throw new ShareClientError('share_failed', 'share response missing token', 500);
273
+ }
274
+ originalContentUrl = `${publicBase}/shared/${items[0].token}`;
275
+ previewImageUrl = `${publicBase}/shared/${items[1].token}`;
276
+ } catch (err) {
277
+ const msg = err instanceof ShareClientError ? `${err.code}: ${err.message}` : String(err);
278
+ return {
279
+ content: [{ type: 'text', text: `line_image: failed to mint share URL — ${msg}` }],
280
+ isError: true,
281
+ };
282
+ }
283
+
284
+ const message = {
285
+ type: 'image' as const,
286
+ originalContentUrl,
287
+ previewImageUrl,
288
+ };
289
+
290
+ const client = new messagingApi.MessagingApiClient({ channelAccessToken: token });
291
+
292
+ let via = 'push';
293
+ try {
294
+ if (replyToken) {
295
+ try {
296
+ await client.replyMessage({ replyToken, messages: [message] });
297
+ via = 'reply';
298
+ } catch {
299
+ await client.pushMessage({ to: chatId, messages: [message] });
300
+ via = 'push (reply fallback)';
301
+ }
302
+ } else {
303
+ await client.pushMessage({ to: chatId, messages: [message] });
304
+ }
305
+ return { content: [{ type: 'text', text: `Sent image to LINE (${via}).` }] };
306
+ } catch (err) {
307
+ return {
308
+ content: [{ type: 'text', text: `line_image failed: ${(err as Error).message}` }],
309
+ isError: true,
310
+ };
311
+ }
312
+ }
127
313
  }
@@ -0,0 +1,120 @@
1
+ import type { ToolModule, McpToolDefinition, McpToolResult, ToolVisibility } from '../../types';
2
+ import {
3
+ ShareClientError,
4
+ createShares,
5
+ revokeShare,
6
+ shareBridgeEnabled,
7
+ type ShareRef,
8
+ } from '../shared/share-client';
9
+
10
+ /**
11
+ * Standalone share_image MCP tool (#70, plan §15) — explicit create/revoke of
12
+ * short-lived public image URLs. There is deliberately NO list action (shares
13
+ * are unenumerable, §10). generate_image auto-normalizes refs through the same
14
+ * gateway API, so normal image editing never needs this tool; it exists for
15
+ * explicit workflows (e.g. handing a temporary URL to an external service).
16
+ */
17
+ export class ShareImageModule implements ToolModule {
18
+ id = 'share-image';
19
+ toolVisibility: ToolVisibility = 'all-configured';
20
+
21
+ isEnabled(): boolean {
22
+ return shareBridgeEnabled();
23
+ }
24
+
25
+ getTools(): McpToolDefinition[] {
26
+ return shareImageToolDefs;
27
+ }
28
+
29
+ async handleTool(name: string, args: Record<string, unknown>): Promise<McpToolResult> {
30
+ if (name !== 'share_image') {
31
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
32
+ }
33
+ const action = typeof args.action === 'string' ? args.action : 'create';
34
+ switch (action) {
35
+ case 'create':
36
+ return this.handleCreate(args);
37
+ case 'revoke':
38
+ return this.handleRevoke(args);
39
+ default:
40
+ return {
41
+ content: [{ type: 'text', text: `share_image: unknown action "${action}" (expected create | revoke)` }],
42
+ isError: true,
43
+ };
44
+ }
45
+ }
46
+
47
+ private async handleCreate(args: Record<string, unknown>): Promise<McpToolResult> {
48
+ const single = typeof args.path === 'string' && args.path.trim() ? [args.path.trim()] : [];
49
+ const many = Array.isArray(args.paths)
50
+ ? (args.paths.filter((p) => typeof p === 'string' && p.trim()) as string[])
51
+ : [];
52
+ if (single.length && many.length) {
53
+ return { content: [{ type: 'text', text: 'share_image: pass either "path" or "paths", not both.' }], isError: true };
54
+ }
55
+ const paths = single.length ? single : many;
56
+ if (!paths.length) {
57
+ return { content: [{ type: 'text', text: 'share_image: action="create" requires "path" or "paths".' }], isError: true };
58
+ }
59
+ const refs: ShareRef[] = paths.map((p) =>
60
+ p.startsWith('artifact:') ? { artifact_id: p.slice('artifact:'.length) } : { path: p },
61
+ );
62
+ const opts: { purpose?: string; ttlSeconds?: number } = {};
63
+ if (typeof args.purpose === 'string' && args.purpose.trim()) opts.purpose = args.purpose.trim();
64
+ if (typeof args.ttl_seconds === 'number' && args.ttl_seconds > 0) opts.ttlSeconds = args.ttl_seconds;
65
+ try {
66
+ const items = await createShares(refs, opts);
67
+ return { content: [{ type: 'text', text: JSON.stringify({ items }, null, 2) }] };
68
+ } catch (err) {
69
+ return this.mapError(err);
70
+ }
71
+ }
72
+
73
+ private async handleRevoke(args: Record<string, unknown>): Promise<McpToolResult> {
74
+ const shareId = typeof args.share_id === 'string' ? args.share_id.trim() : '';
75
+ if (!shareId) {
76
+ return { content: [{ type: 'text', text: 'share_image: action="revoke" requires "share_id".' }], isError: true };
77
+ }
78
+ try {
79
+ await revokeShare(shareId);
80
+ return { content: [{ type: 'text', text: JSON.stringify({ revoked: true, share_id: shareId }) }] };
81
+ } catch (err) {
82
+ return this.mapError(err);
83
+ }
84
+ }
85
+
86
+ private mapError(err: unknown): McpToolResult {
87
+ if (err instanceof ShareClientError) {
88
+ return { content: [{ type: 'text', text: `share_image: ${err.code}: ${err.message}` }], isError: true };
89
+ }
90
+ return {
91
+ content: [{ type: 'text', text: `share_image: gateway unavailable: ${(err as Error).message}` }],
92
+ isError: true,
93
+ };
94
+ }
95
+ }
96
+
97
+ const shareImageToolDefs: McpToolDefinition[] = [
98
+ {
99
+ name: 'share_image',
100
+ description:
101
+ 'Create or revoke a SHORT-LIVED public URL for an image in this agent\'s media directory. ' +
102
+ 'action="create" takes "path" (one media path or artifact:<id>) or "paths" (up to 5) and returns ' +
103
+ '{ share_id, url, expires_at } per image — the URL needs no auth and expires automatically (default 30 min). ' +
104
+ 'action="revoke" takes "share_id" and invalidates the URL immediately. ' +
105
+ 'You normally do NOT need this for image editing: generate_image converts local/artifact references ' +
106
+ 'to share URLs automatically. There is no list action.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ action: { type: 'string', enum: ['create', 'revoke'], description: 'create (default) | revoke' },
111
+ path: { type: 'string', description: 'One media path (e.g. "media/session-1/image.png") or "artifact:<id>" to share.' },
112
+ paths: { type: 'array', items: { type: 'string' }, description: 'Multiple media paths / artifact refs (max 5).' },
113
+ ttl_seconds: { type: 'number', description: 'Optional lifetime in seconds (default 1800).' },
114
+ purpose: { type: 'string', description: 'Optional purpose tag (default "codex_ref").' },
115
+ share_id: { type: 'string', description: 'Share id to revoke (required for action="revoke").' },
116
+ },
117
+ required: [],
118
+ },
119
+ },
120
+ ];
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Image share bridge client (#70) — the ONLY way MCP subprocesses touch the
3
+ * share/artifact store. They call the authenticated local Gateway HTTP API
4
+ * (GATEWAY_API_URL + GATEWAY_API_KEY, both already injected into the session
5
+ * subprocess env) and NEVER open the SQLite DB themselves (plan §9/§10).
6
+ *
7
+ * Self-contained on purpose: mcp/** ships as source without src/**, so this
8
+ * module must not import from src/ (see tests/unit/mcp-no-src-imports.test.ts).
9
+ */
10
+
11
+ const REQUEST_TIMEOUT_MS = 15_000;
12
+
13
+ export type ShareRef = { artifact_id?: string; path?: string };
14
+
15
+ export type ShareItem = { share_id: string; token: string; url?: string; expires_at: string };
16
+
17
+ export type ArtifactItem = { artifact_id: string; artifact_ref: string; index: number; path: string };
18
+
19
+ /**
20
+ * One entry of the session image catalog (#72). `index` is 1-based and stable —
21
+ * it is the order of FIRST appearance of that file in the session, so "image 1"
22
+ * / "the first image" maps to index 1 deterministically. `ref` is what to feed
23
+ * back into generate_image's image/images argument.
24
+ */
25
+ export type CatalogItem = {
26
+ index: number;
27
+ ref: string;
28
+ relative_path: string;
29
+ origin: string;
30
+ ts: number;
31
+ available: boolean;
32
+ /** What the image is: the generation prompt (origin=generated) or the user
33
+ * text that accompanied the upload. Absent when neither exists. */
34
+ desc?: string;
35
+ };
36
+
37
+ /** Error carrying the gateway's stable machine-readable code (e.g. image_ref_not_found). */
38
+ export class ShareClientError extends Error {
39
+ readonly code: string;
40
+ readonly status: number;
41
+ constructor(code: string, message: string, status: number) {
42
+ super(message);
43
+ this.code = code;
44
+ this.status = status;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * The bridge client is usable when the gateway API identity reached this
50
+ * subprocess. gateway.publicUrl is the sole server-side enable switch: when it
51
+ * is absent the private mint endpoint is not mounted and calls fail closed.
52
+ */
53
+ export function shareBridgeEnabled(): boolean {
54
+ return (
55
+ !!process.env.GATEWAY_API_URL &&
56
+ !!process.env.GATEWAY_API_KEY &&
57
+ !!process.env.GATEWAY_AGENT_ID &&
58
+ !!process.env.GATEWAY_SESSION_ID
59
+ );
60
+ }
61
+
62
+ function apiBase(): string {
63
+ return (process.env.GATEWAY_API_URL ?? '').replace(/\/+$/, '');
64
+ }
65
+
66
+ async function callGateway(
67
+ method: 'GET' | 'POST' | 'DELETE',
68
+ pathname: string,
69
+ body?: Record<string, unknown>,
70
+ ): Promise<{ status: number; json: Record<string, unknown> }> {
71
+ const res = await fetch(`${apiBase()}${pathname}`, {
72
+ method,
73
+ headers: {
74
+ 'Content-Type': 'application/json',
75
+ Authorization: `Bearer ${process.env.GATEWAY_API_KEY}`,
76
+ },
77
+ body: body ? JSON.stringify(body) : undefined,
78
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
79
+ });
80
+ const text = await res.text().catch(() => '');
81
+ let json: Record<string, unknown> = {};
82
+ try {
83
+ json = JSON.parse(text) as Record<string, unknown>;
84
+ } catch {
85
+ /* non-JSON body */
86
+ }
87
+ return { status: res.status, json };
88
+ }
89
+
90
+ /** Mint short-lived share URLs for local/artifact refs — order preserved. */
91
+ export async function createShares(
92
+ refs: ShareRef[],
93
+ opts: { purpose?: string; ttlSeconds?: number } = {},
94
+ ): Promise<ShareItem[]> {
95
+ const body: Record<string, unknown> = {
96
+ agent_id: process.env.GATEWAY_AGENT_ID,
97
+ session_id: process.env.GATEWAY_SESSION_ID,
98
+ purpose: opts.purpose ?? 'codex_ref',
99
+ refs,
100
+ };
101
+ if (opts.ttlSeconds !== undefined) body.ttl_seconds = opts.ttlSeconds;
102
+ const { status, json } = await callGateway('POST', '/api/v1/shares', body);
103
+ if (status !== 201) {
104
+ const code = typeof json.code === 'string' ? json.code : 'share_failed';
105
+ const message = typeof json.error === 'string' ? json.error : `share request failed (HTTP ${status})`;
106
+ throw new ShareClientError(code, message, status);
107
+ }
108
+ const items = json.items;
109
+ if (!Array.isArray(items)) throw new ShareClientError('share_failed', 'invalid share response', status);
110
+ return items as ShareItem[];
111
+ }
112
+
113
+ /**
114
+ * Read the ground-truth catalog of every image in this session (#72), numbered
115
+ * by first appearance. Read-only: no token is minted and nothing is persisted.
116
+ * Callers must check shareBridgeEnabled() first (same contract as createShares).
117
+ */
118
+ export async function listSessionImages(): Promise<CatalogItem[]> {
119
+ const query =
120
+ `agent_id=${encodeURIComponent(process.env.GATEWAY_AGENT_ID ?? '')}` +
121
+ `&session_id=${encodeURIComponent(process.env.GATEWAY_SESSION_ID ?? '')}`;
122
+ const { status, json } = await callGateway('GET', `/api/v1/image-catalog?${query}`);
123
+ if (status !== 200) {
124
+ const { code, message } = extractError(json, `image catalog request failed (HTTP ${status})`, 'catalog_failed');
125
+ throw new ShareClientError(code, message, status);
126
+ }
127
+ const items = json.items;
128
+ if (!Array.isArray(items)) throw new ShareClientError('catalog_failed', 'invalid catalog response', status);
129
+ return items as CatalogItem[];
130
+ }
131
+
132
+ /**
133
+ * Pull a { code, message } out of a gateway error body. The private routes are
134
+ * not uniform: some return { error: "text", code: "x" } (share mint) and some
135
+ * return { error: { code, message } } (image service style), so accept both and
136
+ * fall back to the supplied defaults.
137
+ */
138
+ function extractError(
139
+ json: Record<string, unknown>,
140
+ fallbackMessage: string,
141
+ fallbackCode: string,
142
+ ): { code: string; message: string } {
143
+ let code = typeof json.code === 'string' ? json.code : '';
144
+ let message = typeof json.error === 'string' ? json.error : '';
145
+ const err = json.error;
146
+ if (err && typeof err === 'object') {
147
+ const nested = err as { code?: unknown; message?: unknown };
148
+ if (!code && typeof nested.code === 'string') code = nested.code;
149
+ if (!message && typeof nested.message === 'string') message = nested.message;
150
+ }
151
+ return { code: code || fallbackCode, message: message || fallbackMessage };
152
+ }
153
+
154
+ /** Revoke a share by id. Throws ShareClientError on failure. */
155
+ export async function revokeShare(shareId: string): Promise<void> {
156
+ const { status, json } = await callGateway('DELETE', `/api/v1/shares/${encodeURIComponent(shareId)}`);
157
+ if (status !== 200) {
158
+ const message = typeof json.error === 'string' ? json.error : `revoke failed (HTTP ${status})`;
159
+ throw new ShareClientError('revoke_failed', message, status);
160
+ }
161
+ }
162
+
163
+ /** Best-effort revoke of freshly minted shares (used when submit fails, §18). */
164
+ export async function revokeSharesBestEffort(shareIds: string[]): Promise<void> {
165
+ for (const id of shareIds) {
166
+ try {
167
+ await revokeShare(id);
168
+ } catch {
169
+ /* best-effort — TTL still bounds exposure */
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Register generated image files as private artifacts (§8). Best-effort: on
176
+ * any failure returns null so image delivery itself never breaks.
177
+ */
178
+ export async function registerArtifacts(
179
+ files: string[],
180
+ meta: { provider: string; model: string; taskId?: string; prompt?: string },
181
+ ): Promise<ArtifactItem[] | null> {
182
+ try {
183
+ const { status, json } = await callGateway('POST', '/api/v1/image-artifacts', {
184
+ agent_id: process.env.GATEWAY_AGENT_ID,
185
+ session_id: process.env.GATEWAY_SESSION_ID,
186
+ provider: meta.provider,
187
+ model: meta.model,
188
+ ...(meta.taskId ? { task_id: meta.taskId } : {}),
189
+ ...(meta.prompt ? { prompt: meta.prompt } : {}),
190
+ files,
191
+ });
192
+ if (status !== 201 || !Array.isArray(json.items)) return null;
193
+ return json.items as ArtifactItem[];
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xmaxma/claude-gateway",
3
- "version": "1.5.8",
3
+ "version": "1.6.0",
4
4
  "description": "Multi-agent gateway for Claude",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,6 +18,8 @@
18
18
  "lib/",
19
19
  "resource/",
20
20
  "mcp/",
21
+ "!mcp/node_modules",
22
+ "!mcp/node_modules/**",
21
23
  "README.md",
22
24
  "config.template.json"
23
25
  ],