@0xmaxma/claude-gateway 1.5.7 → 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.
- package/README.md +22 -0
- package/dist/agent/runner.d.ts +31 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +224 -16
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/apps-router.d.ts.map +1 -1
- package/dist/api/apps-router.js +7 -2
- package/dist/api/apps-router.js.map +1 -1
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +27 -0
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/line-webhook-router.d.ts.map +1 -1
- package/dist/api/line-webhook-router.js +68 -0
- package/dist/api/line-webhook-router.js.map +1 -1
- package/dist/api/router.d.ts +2 -0
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +60 -4
- package/dist/api/router.js.map +1 -1
- package/dist/api/share-router.d.ts +11 -0
- package/dist/api/share-router.d.ts.map +1 -0
- package/dist/api/share-router.js +402 -0
- package/dist/api/share-router.js.map +1 -0
- package/dist/apps/installer.d.ts +55 -0
- package/dist/apps/installer.d.ts.map +1 -1
- package/dist/apps/installer.js +136 -1
- package/dist/apps/installer.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +9 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/public-url.d.ts +11 -0
- package/dist/config/public-url.d.ts.map +1 -0
- package/dist/config/public-url.js +41 -0
- package/dist/config/public-url.js.map +1 -0
- package/dist/config/watcher.d.ts.map +1 -1
- package/dist/config/watcher.js +1 -0
- package/dist/config/watcher.js.map +1 -1
- package/dist/history/db.d.ts +14 -0
- package/dist/history/db.d.ts.map +1 -1
- package/dist/history/db.js +48 -4
- package/dist/history/db.js.map +1 -1
- package/dist/history/types.d.ts +5 -0
- package/dist/history/types.d.ts.map +1 -1
- package/dist/session/process.d.ts +3 -0
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +27 -1
- package/dist/session/process.js.map +1 -1
- package/dist/session/store.d.ts +2 -1
- package/dist/session/store.d.ts.map +1 -1
- package/dist/session/store.js +10 -2
- package/dist/session/store.js.map +1 -1
- package/dist/share/session-image-catalog.d.ts +22 -0
- package/dist/share/session-image-catalog.d.ts.map +1 -0
- package/dist/share/session-image-catalog.js +151 -0
- package/dist/share/session-image-catalog.js.map +1 -0
- package/dist/share/share-store.d.ts +137 -0
- package/dist/share/share-store.d.ts.map +1 -0
- package/dist/share/share-store.js +340 -0
- package/dist/share/share-store.js.map +1 -0
- package/dist/types.d.ts +19 -7
- package/dist/types.d.ts.map +1 -1
- package/mcp/instructions.ts +1 -0
- package/mcp/server.ts +2 -0
- package/mcp/tools/agent/handlers.ts +55 -4
- package/mcp/tools/image/module.ts +240 -26
- package/mcp/tools/line/module.ts +186 -0
- package/mcp/tools/share-image/module.ts +120 -0
- package/mcp/tools/shared/share-client.ts +197 -0
- package/package.json +3 -1
|
@@ -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.
|
|
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
|
],
|