@semalt-ai/code 1.8.5 → 1.19.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/.claude/settings.local.json +6 -1
- package/.github/workflows/ci.yml +69 -0
- package/CLAUDE.md +1584 -26
- package/README.md +147 -3
- package/examples/embed.js +74 -0
- package/index.js +251 -10
- package/lib/agent.js +711 -104
- package/lib/api.js +213 -49
- package/lib/args.js +74 -2
- package/lib/audit.js +23 -1
- package/lib/background.js +584 -0
- package/lib/checkpoints.js +757 -0
- package/lib/commands/auth.js +94 -0
- package/lib/commands/chat-session.js +306 -0
- package/lib/commands/chat-slash.js +399 -0
- package/lib/commands/chat-turn.js +446 -0
- package/lib/commands/chat.js +403 -0
- package/lib/commands/custom.js +157 -0
- package/lib/commands/history-utils.js +66 -0
- package/lib/commands/index.js +268 -0
- package/lib/commands/mcp.js +113 -0
- package/lib/commands/oneshot.js +193 -0
- package/lib/commands/registry.js +269 -0
- package/lib/commands/tasks.js +89 -0
- package/lib/compact.js +87 -0
- package/lib/config.js +333 -11
- package/lib/constants.js +372 -3
- package/lib/deny.js +199 -0
- package/lib/doctor.js +160 -0
- package/lib/headless.js +167 -0
- package/lib/hooks.js +286 -0
- package/lib/images.js +264 -0
- package/lib/internals.js +49 -0
- package/lib/mcp/boundary.js +131 -0
- package/lib/mcp/client.js +270 -0
- package/lib/mcp/oauth.js +134 -0
- package/lib/memory.js +209 -0
- package/lib/metrics.js +37 -2
- package/lib/payload.js +54 -0
- package/lib/permission-rules.js +401 -0
- package/lib/permissions.js +100 -10
- package/lib/pricing.js +67 -0
- package/lib/proc.js +62 -0
- package/lib/prompts.js +84 -5
- package/lib/sandbox.js +568 -0
- package/lib/sdk.js +328 -0
- package/lib/secrets.js +211 -0
- package/lib/skills.js +223 -0
- package/lib/subagents.js +516 -0
- package/lib/tool_registry.js +2558 -0
- package/lib/tool_specs.js +222 -2
- package/lib/tools.js +272 -1020
- package/lib/ui/format.js +22 -1
- package/lib/ui/input-field.js +16 -7
- package/lib/ui/status-bar.js +79 -11
- package/lib/ui/theme.js +1 -0
- package/lib/ui/web-activity.js +218 -0
- package/lib/verify.js +229 -0
- package/lib/web-extract.js +213 -0
- package/lib/web-summarize.js +68 -0
- package/package.json +19 -4
- package/scripts/lint.js +57 -0
- package/test/agent-loop.test.js +389 -0
- package/test/background.test.js +414 -0
- package/test/chat.test.js +114 -0
- package/test/checkpoints-agent.test.js +181 -0
- package/test/checkpoints.test.js +650 -0
- package/test/command-registry.test.js +160 -0
- package/test/compact.test.js +116 -0
- package/test/completion-lazy.test.js +52 -0
- package/test/config-merge.test.js +324 -0
- package/test/config-quarantine.test.js +128 -0
- package/test/config-write-guard-allow-anywhere.test.js +56 -0
- package/test/config-write-guard-skip.test.js +46 -0
- package/test/config-write-guard.test.js +153 -0
- package/test/context-split.test.js +215 -0
- package/test/cost-doctor.test.js +142 -0
- package/test/custom-commands-chat.test.js +106 -0
- package/test/custom-commands.test.js +230 -0
- package/test/deny-windows.test.js +120 -0
- package/test/deny.test.js +83 -0
- package/test/download-allow-anywhere.test.js +66 -0
- package/test/download-confine.test.js +153 -0
- package/test/executors.test.js +362 -0
- package/test/extract-tool-calls.test.js +315 -0
- package/test/fetch-url-validation.test.js +219 -0
- package/test/fixtures/tool-calls.js +57 -0
- package/test/fixtures/web-page.js +91 -0
- package/test/git-tools.test.js +384 -0
- package/test/grep-glob-serialize.test.js +242 -0
- package/test/grep-glob.test.js +268 -0
- package/test/harness/README.md +57 -0
- package/test/harness/chat-harness.js +142 -0
- package/test/harness/memwarn-headless-child.js +65 -0
- package/test/harness/mock-llm.js +120 -0
- package/test/harness/mock-mcp-server.js +142 -0
- package/test/harness/sse-server.js +69 -0
- package/test/headless.test.js +203 -0
- package/test/history-utils.test.js +88 -0
- package/test/hooks-agent.test.js +238 -0
- package/test/hooks-verify-sandbox.test.js +232 -0
- package/test/hooks.test.js +216 -0
- package/test/http-get-user-agent.test.js +142 -0
- package/test/images-api.test.js +208 -0
- package/test/images.test.js +238 -0
- package/test/max-iterations.test.js +216 -0
- package/test/mcp-boundary.test.js +57 -0
- package/test/mcp-client.test.js +267 -0
- package/test/mcp-oauth.test.js +86 -0
- package/test/memory-truncation-warning.test.js +222 -0
- package/test/memory.test.js +198 -0
- package/test/native-dispatch.test.js +356 -0
- package/test/output-chokepoint.test.js +188 -0
- package/test/path-guards.test.js +134 -0
- package/test/payload.test.js +99 -0
- package/test/permission-rules-agent.test.js +210 -0
- package/test/permission-rules.test.js +297 -0
- package/test/permissions.test.js +163 -0
- package/test/plan-mode.test.js +167 -0
- package/test/read-paginate.test.js +275 -0
- package/test/readonly-tools.test.js +177 -0
- package/test/result-cap.test.js +233 -0
- package/test/sandbox-agent.test.js +147 -0
- package/test/sandbox-integration.test.js +216 -0
- package/test/sandbox.test.js +408 -0
- package/test/sdk.test.js +234 -0
- package/test/shell-output-cap.test.js +181 -0
- package/test/skills-chat.test.js +110 -0
- package/test/skills.test.js +295 -0
- package/test/smoke.test.js +68 -0
- package/test/status-bar-pause.test.js +164 -0
- package/test/stream-parser.test.js +147 -0
- package/test/subagents-agent.test.js +178 -0
- package/test/subagents.test.js +222 -0
- package/test/tool-registry.test.js +85 -0
- package/test/trim-budget.test.js +101 -0
- package/test/verify-agent.test.js +317 -0
- package/test/verify.test.js +141 -0
- package/test/web-activity-ordering.test.js +194 -0
- package/test/web-activity.test.js +207 -0
- package/test/web-data-extraction-guidance.test.js +71 -0
- package/test/web-extract.test.js +185 -0
- package/test/web-fetch-agent.test.js +291 -0
- package/test/web-fetch-mode.test.js +193 -0
- package/test/web-search.test.js +380 -0
- package/lib/commands.js +0 -1438
package/lib/images.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Multimodal image input (Task 5.4)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
//
|
|
7
|
+
// Accept image input (screenshots, mockups, diagrams) so the agent can SEE.
|
|
8
|
+
// This module owns the pure, testable parts: reading an image file through the
|
|
9
|
+
// same `isPathSafe` guard every file read uses, enforcing a size cap, detecting
|
|
10
|
+
// the media type, base64-encoding, and building the PROVIDER-SPECIFIC content
|
|
11
|
+
// part the endpoint expects. The api client (lib/api.js) consumes these to
|
|
12
|
+
// transform a user turn's content into a multimodal `content[]` array.
|
|
13
|
+
//
|
|
14
|
+
// Scope (decided): input formats PNG, JPEG, WebP, GIF. PDF is DEFERRED and image
|
|
15
|
+
// GENERATION is out of scope entirely — this is multimodal *input* only.
|
|
16
|
+
//
|
|
17
|
+
// Provider-format selection (constraint #1). Endpoints encode image input two
|
|
18
|
+
// ways:
|
|
19
|
+
// * Anthropic-style: { type: 'image', source: { type: 'base64', media_type,
|
|
20
|
+
// data } }
|
|
21
|
+
// * OpenAI-style: { type: 'image_url', image_url: { url:
|
|
22
|
+
// 'data:<media_type>;base64,<data>' } }
|
|
23
|
+
// The shape is chosen per model/profile by `selectImageFormat`, precedence:
|
|
24
|
+
// 1. the matching models[] profile's `image_format`
|
|
25
|
+
// 2. top-level `config.image_format`
|
|
26
|
+
// 3. heuristic: an Anthropic-native api_base → 'anthropic', else 'openai'
|
|
27
|
+
// (the project's OpenAI-compatible lingua franca is the default).
|
|
28
|
+
//
|
|
29
|
+
// Vision capability (constraint #2) — FAIL LOUD, never silently drop the image.
|
|
30
|
+
// `resolveVisionCapability` returns true | false | null. `false` (a profile or
|
|
31
|
+
// config marked non-vision, or a well-known text-only model) → the caller
|
|
32
|
+
// raises a clear error before sending. `null` (unknown) → proceed and let the
|
|
33
|
+
// endpoint reject cleanly. We NEVER strip the image from the payload.
|
|
34
|
+
|
|
35
|
+
const fs = require('fs');
|
|
36
|
+
const path = require('path');
|
|
37
|
+
|
|
38
|
+
const { DEFAULT_IMAGE_MAX_BYTES } = require('./constants');
|
|
39
|
+
|
|
40
|
+
// Supported input formats. Extension → media type for the magic-byte fallback.
|
|
41
|
+
const EXT_MEDIA_TYPES = {
|
|
42
|
+
'.png': 'image/png',
|
|
43
|
+
'.jpg': 'image/jpeg',
|
|
44
|
+
'.jpeg': 'image/jpeg',
|
|
45
|
+
'.webp': 'image/webp',
|
|
46
|
+
'.gif': 'image/gif',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const SUPPORTED_MEDIA_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
|
|
50
|
+
|
|
51
|
+
const VALID_FORMATS = new Set(['anthropic', 'openai']);
|
|
52
|
+
|
|
53
|
+
// Detect the media type from the file's MAGIC BYTES first (authoritative — a
|
|
54
|
+
// .png that is really a JPEG is classified as JPEG), falling back to the file
|
|
55
|
+
// extension when the header is inconclusive. Returns a supported media type
|
|
56
|
+
// string or null (caller errors on null).
|
|
57
|
+
function detectMediaType(buf, filePath) {
|
|
58
|
+
if (Buffer.isBuffer(buf) && buf.length >= 12) {
|
|
59
|
+
// PNG: 89 50 4E 47 0D 0A 1A 0A
|
|
60
|
+
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return 'image/png';
|
|
61
|
+
// JPEG: FF D8 FF
|
|
62
|
+
if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return 'image/jpeg';
|
|
63
|
+
// GIF: 47 49 46 38 ("GIF8" — GIF87a / GIF89a)
|
|
64
|
+
if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x38) return 'image/gif';
|
|
65
|
+
// WebP: "RIFF" <4-byte size> "WEBP"
|
|
66
|
+
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 &&
|
|
67
|
+
buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50) return 'image/webp';
|
|
68
|
+
}
|
|
69
|
+
const ext = path.extname(filePath || '').toLowerCase();
|
|
70
|
+
return EXT_MEDIA_TYPES[ext] || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Read an image from disk for attachment to a user turn. It is a file read, so
|
|
74
|
+
// it goes through the SAME `isPathSafe` guard (out-of-CWD / sensitive dirs
|
|
75
|
+
// refused) every other file read uses. Enforces the raw-byte size cap (base64
|
|
76
|
+
// inflates ~33%; a clear pre-send error beats an opaque endpoint rejection),
|
|
77
|
+
// detects the media type, and base64-encodes. Throws a clear Error on any
|
|
78
|
+
// failure (unsafe path, missing/unreadable, oversize, unsupported format).
|
|
79
|
+
//
|
|
80
|
+
// Returns { path, media_type, data (base64), bytes }.
|
|
81
|
+
function readImage(filePath, { maxBytes = DEFAULT_IMAGE_MAX_BYTES, isPathSafe, fsImpl = fs } = {}) {
|
|
82
|
+
if (typeof filePath !== 'string' || !filePath.trim()) {
|
|
83
|
+
throw new Error('Image path is empty.');
|
|
84
|
+
}
|
|
85
|
+
// Same confinement as every file read: refuse out-of-CWD / sensitive dirs.
|
|
86
|
+
if (typeof isPathSafe === 'function' && !isPathSafe(filePath)) {
|
|
87
|
+
throw new Error(`Image path outside allowed area: ${filePath}. Use --allow-anywhere to override.`);
|
|
88
|
+
}
|
|
89
|
+
let stat;
|
|
90
|
+
try { stat = fsImpl.statSync(filePath); }
|
|
91
|
+
catch { throw new Error(`Image not found or unreadable: ${filePath}`); }
|
|
92
|
+
if (!stat.isFile()) throw new Error(`Not a file: ${filePath}`);
|
|
93
|
+
// Cap on the RAW bytes (before base64). A clear error here, not an opaque
|
|
94
|
+
// endpoint failure on an oversized payload.
|
|
95
|
+
if (Number.isFinite(maxBytes) && maxBytes > 0 && stat.size > maxBytes) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Image too large: ${filePath} is ${stat.size} bytes, exceeds the ${maxBytes}-byte cap ` +
|
|
98
|
+
`(image_max_bytes). Base64 inflates the payload ~33%; resize the image or raise the cap.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
let buf;
|
|
102
|
+
try { buf = fsImpl.readFileSync(filePath); }
|
|
103
|
+
catch { throw new Error(`Image not found or unreadable: ${filePath}`); }
|
|
104
|
+
const mediaType = detectMediaType(buf, filePath);
|
|
105
|
+
if (!mediaType) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Unsupported image format: ${filePath}. Supported: PNG, JPEG, WebP, GIF ` +
|
|
108
|
+
`(PDF and image generation are out of scope).`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return { path: filePath, media_type: mediaType, data: buf.toString('base64'), bytes: stat.size };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Read a list of image paths, preserving order. Throws on the FIRST failure so
|
|
115
|
+
// the user gets a clear, specific error rather than a partial attach.
|
|
116
|
+
function readImages(paths, opts = {}) {
|
|
117
|
+
return (paths || []).map((p) => readImage(p, opts));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Normalize a list of mixed image inputs to encoded image records. Accepts a
|
|
121
|
+
// file-path string, a { path } object (both read via readImage through the size
|
|
122
|
+
// + path guards), or an already-encoded { media_type, data } object (so an SDK
|
|
123
|
+
// host can pass bytes it produced itself). Used by the SDK `images` option.
|
|
124
|
+
function resolveImageInputs(images, opts = {}) {
|
|
125
|
+
return (images || []).map((img) => {
|
|
126
|
+
if (typeof img === 'string') return readImage(img, opts);
|
|
127
|
+
if (img && typeof img === 'object' && typeof img.data === 'string' && typeof img.media_type === 'string') {
|
|
128
|
+
if (!SUPPORTED_MEDIA_TYPES.has(img.media_type)) {
|
|
129
|
+
throw new Error(`Unsupported image media type: ${img.media_type}. Supported: PNG, JPEG, WebP, GIF.`);
|
|
130
|
+
}
|
|
131
|
+
return { path: img.path || '(inline)', media_type: img.media_type, data: img.data, bytes: img.bytes || 0 };
|
|
132
|
+
}
|
|
133
|
+
if (img && typeof img === 'object' && typeof img.path === 'string') return readImage(img.path, opts);
|
|
134
|
+
throw new Error('Invalid image input: expected a file path or { media_type, data } object.');
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Find the models[] profile backing the active model. Prefers an api_base +
|
|
139
|
+
// model match (the exact active profile), then any profile with that model name.
|
|
140
|
+
function activeProfile(config, model) {
|
|
141
|
+
if (!config || !Array.isArray(config.models)) return null;
|
|
142
|
+
return (
|
|
143
|
+
config.models.find((p) => p && p.model === model && p.api_base === config.api_base) ||
|
|
144
|
+
config.models.find((p) => p && p.model === model) ||
|
|
145
|
+
null
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Choose the provider-specific content-part shape. See the header for the
|
|
150
|
+
// precedence: profile → config → heuristic (Anthropic-native base → 'anthropic',
|
|
151
|
+
// else the OpenAI-compatible default).
|
|
152
|
+
function selectImageFormat(config = {}, model = '') {
|
|
153
|
+
const profile = activeProfile(config, model);
|
|
154
|
+
if (profile && VALID_FORMATS.has(profile.image_format)) return profile.image_format;
|
|
155
|
+
if (VALID_FORMATS.has(config.image_format)) return config.image_format;
|
|
156
|
+
const base = String(config.api_base || '');
|
|
157
|
+
if (/(^|\.)anthropic\.com/i.test(base) || /anthropic/i.test(base)) return 'anthropic';
|
|
158
|
+
return 'openai';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Well-known NON-vision model families (embeddings, audio, moderation): images
|
|
162
|
+
// to these can never work, so we fail loud rather than send a doomed payload.
|
|
163
|
+
const KNOWN_TEXT_ONLY = /(?:^|[-/_])(?:text-embedding|embedding|embed|whisper|tts|moderation|rerank|reranker)/i;
|
|
164
|
+
// Well-known vision-capable families: a positive signal so an attach proceeds
|
|
165
|
+
// without needing per-profile config.
|
|
166
|
+
const KNOWN_VISION = /(gpt-4o|gpt-4\.1|gpt-4-vision|gpt-4-turbo|claude-3|claude-opus|claude-sonnet|claude-haiku|claude-fable|claude-4|gemini|llava|qwen[\d.]*-?vl|pixtral|llama[-\d.]*(?:-)?vision|internvl|minicpm-v|-vl\b|vision|multimodal)/i;
|
|
167
|
+
|
|
168
|
+
// Determine vision capability from config/model metadata where available.
|
|
169
|
+
// true — accept the image
|
|
170
|
+
// false — a CLEAR pre-send error (profile/config marked non-vision, or a
|
|
171
|
+
// well-known text-only model)
|
|
172
|
+
// null — unknown; proceed and surface the endpoint's rejection cleanly
|
|
173
|
+
function resolveVisionCapability(config = {}, model = '') {
|
|
174
|
+
const profile = activeProfile(config, model);
|
|
175
|
+
if (profile && typeof profile.vision === 'boolean') return profile.vision;
|
|
176
|
+
if (typeof config.vision === 'boolean') return config.vision;
|
|
177
|
+
const m = String(model || '');
|
|
178
|
+
if (KNOWN_TEXT_ONLY.test(m)) return false;
|
|
179
|
+
if (KNOWN_VISION.test(m)) return true;
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Build a single provider-specific image content part.
|
|
184
|
+
function buildImagePart(image, format) {
|
|
185
|
+
if (format === 'anthropic') {
|
|
186
|
+
return { type: 'image', source: { type: 'base64', media_type: image.media_type, data: image.data } };
|
|
187
|
+
}
|
|
188
|
+
// OpenAI-style data URL is the default for any OpenAI-compatible endpoint.
|
|
189
|
+
return { type: 'image_url', image_url: { url: `data:${image.media_type};base64,${image.data}` } };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Build a multimodal user-message content array: the text part (when non-empty)
|
|
193
|
+
// followed by one image part per attached image.
|
|
194
|
+
function buildMultimodalContent(text, images, format) {
|
|
195
|
+
const parts = [];
|
|
196
|
+
const t = text == null ? '' : String(text);
|
|
197
|
+
if (t) parts.push({ type: 'text', text: t });
|
|
198
|
+
for (const img of (images || [])) parts.push(buildImagePart(img, format));
|
|
199
|
+
return parts;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// True when any message carries attached images.
|
|
203
|
+
function messagesHaveImages(messages) {
|
|
204
|
+
return Array.isArray(messages) && messages.some((m) => m && Array.isArray(m.images) && m.images.length);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Count all attached images across the message list (for error messages).
|
|
208
|
+
function countImages(messages) {
|
|
209
|
+
let n = 0;
|
|
210
|
+
for (const m of (messages || [])) {
|
|
211
|
+
if (m && Array.isArray(m.images)) n += m.images.length;
|
|
212
|
+
}
|
|
213
|
+
return n;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Transform messages for the wire: any message with attached `images` becomes a
|
|
217
|
+
// provider-specific multimodal `content[]` array; the internal `images` field is
|
|
218
|
+
// stripped from every message. Messages without images pass through unchanged.
|
|
219
|
+
// Pure — returns a new array, leaving the caller's messages intact.
|
|
220
|
+
function buildProviderMessages(messages, format) {
|
|
221
|
+
return (messages || []).map((m) => {
|
|
222
|
+
if (m && Array.isArray(m.images) && m.images.length) {
|
|
223
|
+
const { images, ...rest } = m;
|
|
224
|
+
return { ...rest, content: buildMultimodalContent(m.content, images, format) };
|
|
225
|
+
}
|
|
226
|
+
if (m && typeof m === 'object' && 'images' in m) {
|
|
227
|
+
const { images, ...rest } = m;
|
|
228
|
+
return rest;
|
|
229
|
+
}
|
|
230
|
+
return m;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Attach images to the most recent user message (mutating the array in place by
|
|
235
|
+
// replacing that entry). No-op when there are no images. Used by entry points
|
|
236
|
+
// after they read/encode the images.
|
|
237
|
+
function attachImagesToLastUser(messages, images) {
|
|
238
|
+
if (!Array.isArray(messages) || !images || !images.length) return messages;
|
|
239
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
240
|
+
if (messages[i] && messages[i].role === 'user') {
|
|
241
|
+
const prior = Array.isArray(messages[i].images) ? messages[i].images : [];
|
|
242
|
+
messages[i] = { ...messages[i], images: prior.concat(images) };
|
|
243
|
+
return messages;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return messages;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
module.exports = {
|
|
250
|
+
EXT_MEDIA_TYPES,
|
|
251
|
+
SUPPORTED_MEDIA_TYPES,
|
|
252
|
+
detectMediaType,
|
|
253
|
+
readImage,
|
|
254
|
+
readImages,
|
|
255
|
+
resolveImageInputs,
|
|
256
|
+
selectImageFormat,
|
|
257
|
+
resolveVisionCapability,
|
|
258
|
+
buildImagePart,
|
|
259
|
+
buildMultimodalContent,
|
|
260
|
+
messagesHaveImages,
|
|
261
|
+
countImages,
|
|
262
|
+
buildProviderMessages,
|
|
263
|
+
attachImagesToLastUser,
|
|
264
|
+
};
|
package/lib/internals.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Building blocks — the UNSTABLE internals subpath (Task 5.2)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
//
|
|
7
|
+
// ⚠ NO STABILITY GUARANTEE ⚠
|
|
8
|
+
//
|
|
9
|
+
// Everything exported here is an internal building block of @semalt-ai/code.
|
|
10
|
+
// It is exposed via the SEPARATE `@semalt-ai/code/internals` subpath precisely
|
|
11
|
+
// so that the stable facade (`require('@semalt-ai/code')` → createAgent) can be
|
|
12
|
+
// kept narrow and intentional while these factories remain free to change.
|
|
13
|
+
//
|
|
14
|
+
// These names, their signatures, and their behaviour MAY CHANGE OR BE REMOVED
|
|
15
|
+
// IN ANY RELEASE, including patch releases. They are NOT covered by semver. If
|
|
16
|
+
// you build on them you own the breakage. For supported embedding use the
|
|
17
|
+
// stable facade:
|
|
18
|
+
//
|
|
19
|
+
// const { createAgent } = require('@semalt-ai/code');
|
|
20
|
+
//
|
|
21
|
+
// Reach for /internals only when the facade genuinely cannot express what you
|
|
22
|
+
// need — and pin an exact version if you do.
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
// The agent loop factory.
|
|
26
|
+
createAgentRunner: require('./agent').createAgentRunner,
|
|
27
|
+
// OpenAI-compatible + dashboard HTTP client.
|
|
28
|
+
createApiClient: require('./api').createApiClient,
|
|
29
|
+
// Tool execution + XML tool-call extraction.
|
|
30
|
+
createToolExecutor: require('./tools').createToolExecutor,
|
|
31
|
+
extractToolCalls: require('./tools').extractToolCalls,
|
|
32
|
+
// Permission perimeter.
|
|
33
|
+
createPermissionManager: require('./permissions').createPermissionManager,
|
|
34
|
+
// Per-pattern rule engine (Task 4.1).
|
|
35
|
+
loadRuleLayers: require('./permission-rules').loadRuleLayers,
|
|
36
|
+
resolvePermission: require('./permission-rules').resolvePermission,
|
|
37
|
+
// Tool registry (static + dynamic).
|
|
38
|
+
toolRegistry: require('./tool_registry'),
|
|
39
|
+
// Config layering.
|
|
40
|
+
config: require('./config'),
|
|
41
|
+
// Headless output envelope helpers.
|
|
42
|
+
headless: require('./headless'),
|
|
43
|
+
// MCP client manager.
|
|
44
|
+
createMcpManager: require('./mcp/client').createMcpManager,
|
|
45
|
+
// The shared UI surface (no-op in non-TTY).
|
|
46
|
+
ui: require('./ui'),
|
|
47
|
+
// An explicit, machine-readable marker that this is the unstable surface.
|
|
48
|
+
__unstable__: true,
|
|
49
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// MCP boundary module (Task 3.2)
|
|
4
|
+
// -----------------------------------------------------------------------------
|
|
5
|
+
// The official MCP SDK (`@modelcontextprotocol/sdk`) is the project's first — and
|
|
6
|
+
// so far only — runtime dependency. It is **ESM-only** (`"type": "module"`, no
|
|
7
|
+
// CommonJS entry points), but this project is CommonJS: a CJS module cannot
|
|
8
|
+
// `require()` an ESM-only package.
|
|
9
|
+
//
|
|
10
|
+
// This module is the SINGLE place that bridges the gap. It loads the SDK via
|
|
11
|
+
// dynamic `import()` (the one mechanism CJS has for pulling in ESM) and re-exposes
|
|
12
|
+
// a small, CommonJS-friendly **async** surface. Every other file in the codebase
|
|
13
|
+
// stays plain CommonJS and talks to MCP through this boundary — no other module
|
|
14
|
+
// imports the SDK directly. That keeps the ESM/CJS friction contained to one file
|
|
15
|
+
// and means the rest of the project never has to change its module system.
|
|
16
|
+
//
|
|
17
|
+
// The dynamic import is memoized: the SDK's ESM graph is evaluated at most once
|
|
18
|
+
// per process, on first use (lazy — importing this module costs nothing until a
|
|
19
|
+
// boundary function is actually called). Task 3.3 builds the MCP client on top of
|
|
20
|
+
// the helpers here.
|
|
21
|
+
|
|
22
|
+
const { PACKAGE_JSON } = require('../constants');
|
|
23
|
+
|
|
24
|
+
// The SDK subpaths we consume. The SDK exposes deep subpath exports
|
|
25
|
+
// (`./client/index.js`, `./client/stdio.js`, …) rather than a single barrel, so
|
|
26
|
+
// we import exactly what we use.
|
|
27
|
+
const CLIENT_SUBPATH = '@modelcontextprotocol/sdk/client/index.js';
|
|
28
|
+
const STDIO_SUBPATH = '@modelcontextprotocol/sdk/client/stdio.js';
|
|
29
|
+
const HTTP_SUBPATH = '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
30
|
+
const SSE_SUBPATH = '@modelcontextprotocol/sdk/client/sse.js';
|
|
31
|
+
|
|
32
|
+
let _sdkPromise = null;
|
|
33
|
+
|
|
34
|
+
// Lazily load + memoize the SDK's client surface. `import()` is the ONLY bridge
|
|
35
|
+
// from this CommonJS module to the ESM-only package — do not try to `require()`
|
|
36
|
+
// the SDK anywhere. Returns just the named exports the rest of the code needs.
|
|
37
|
+
async function loadSdk() {
|
|
38
|
+
if (!_sdkPromise) {
|
|
39
|
+
_sdkPromise = (async () => {
|
|
40
|
+
const [clientMod, stdioMod] = await Promise.all([
|
|
41
|
+
import(CLIENT_SUBPATH),
|
|
42
|
+
import(STDIO_SUBPATH),
|
|
43
|
+
]);
|
|
44
|
+
return {
|
|
45
|
+
Client: clientMod.Client,
|
|
46
|
+
StdioClientTransport: stdioMod.StdioClientTransport,
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
// If the import rejects (e.g. the dependency is not installed), clear the
|
|
50
|
+
// cache so a later call can retry rather than re-throwing a stale rejection.
|
|
51
|
+
_sdkPromise.catch(() => { _sdkPromise = null; });
|
|
52
|
+
}
|
|
53
|
+
return _sdkPromise;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Whether the SDK is resolvable in this environment (installed in node_modules).
|
|
57
|
+
// Synchronous and side-effect-free: used by the smoke test to skip gracefully
|
|
58
|
+
// when the dependency could not be installed (e.g. an offline CI runner) instead
|
|
59
|
+
// of failing the suite.
|
|
60
|
+
function isSdkAvailable() {
|
|
61
|
+
try {
|
|
62
|
+
require.resolve(CLIENT_SUBPATH);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Default identity advertised to MCP servers during the connect handshake.
|
|
70
|
+
const DEFAULT_CLIENT_INFO = Object.freeze({
|
|
71
|
+
name: PACKAGE_JSON.name,
|
|
72
|
+
version: PACKAGE_JSON.version,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Instantiate an MCP `Client`. Does NOT connect — Task 3.3 owns transport wiring
|
|
76
|
+
// and the `connect()` handshake. `clientInfo` defaults to this CLI's identity;
|
|
77
|
+
// `options` defaults to declaring no client capabilities.
|
|
78
|
+
async function createClient(clientInfo, options) {
|
|
79
|
+
const { Client } = await loadSdk();
|
|
80
|
+
return new Client(
|
|
81
|
+
clientInfo || { ...DEFAULT_CLIENT_INFO },
|
|
82
|
+
options || { capabilities: {} },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Construct a stdio transport for launching a local MCP server subprocess.
|
|
87
|
+
// `params` is the SDK's `StdioServerParameters` ({ command, args, env, … }).
|
|
88
|
+
// When the caller supplies `env`, it is MERGED over the SDK's default safe
|
|
89
|
+
// environment (getDefaultEnvironment) rather than replacing it — otherwise the
|
|
90
|
+
// child would lose PATH/HOME and fail to launch most real servers.
|
|
91
|
+
async function createStdioTransport(params) {
|
|
92
|
+
const stdioMod = await import(STDIO_SUBPATH);
|
|
93
|
+
const { StdioClientTransport, getDefaultEnvironment } = stdioMod;
|
|
94
|
+
const merged = { ...params };
|
|
95
|
+
if (params && params.env && typeof getDefaultEnvironment === 'function') {
|
|
96
|
+
merged.env = { ...getDefaultEnvironment(), ...params.env };
|
|
97
|
+
}
|
|
98
|
+
return new StdioClientTransport(merged);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Construct a Streamable-HTTP transport for a remote MCP server. `url` is a URL
|
|
102
|
+
// (or string); `opts` is the SDK's `StreamableHTTPClientTransportOptions`
|
|
103
|
+
// ({ authProvider, requestInit, … }). OAuth is wired through `opts.authProvider`.
|
|
104
|
+
async function createStreamableHttpTransport(url, opts) {
|
|
105
|
+
const mod = await import(HTTP_SUBPATH);
|
|
106
|
+
return new mod.StreamableHTTPClientTransport(url instanceof URL ? url : new URL(url), opts);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Construct a legacy HTTP+SSE transport for a remote MCP server. Same shape as
|
|
110
|
+
// the streamable-HTTP transport; used for servers that only speak the older
|
|
111
|
+
// SSE protocol.
|
|
112
|
+
async function createSseTransport(url, opts) {
|
|
113
|
+
const mod = await import(SSE_SUBPATH);
|
|
114
|
+
return new mod.SSEClientTransport(url instanceof URL ? url : new URL(url), opts);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Test seam: drop the memoized import so a fresh load can be exercised.
|
|
118
|
+
function _reset() {
|
|
119
|
+
_sdkPromise = null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
loadSdk,
|
|
124
|
+
isSdkAvailable,
|
|
125
|
+
createClient,
|
|
126
|
+
createStdioTransport,
|
|
127
|
+
createStreamableHttpTransport,
|
|
128
|
+
createSseTransport,
|
|
129
|
+
DEFAULT_CLIENT_INFO,
|
|
130
|
+
_reset,
|
|
131
|
+
};
|