@empirical-gauzza/cli 1.0.37 → 1.0.39
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/lib/attachmentCommands.js +388 -0
- package/lib/constants.js +65 -0
- package/lib/empiricalTokensFormat.js +6 -4
- package/lib/help.js +15 -0
- package/lib/http.js +160 -0
- package/lib/installers/hooks.js +1312 -1312
- package/lib/installers/skill-variants/SKILL.both.md +20 -0
- package/lib/installers/skill-variants/SKILL.cli.md +10 -0
- package/lib/installers/skill-variants/SKILL.mcp.md +18 -0
- package/lib/memoryCommands.js +142 -97
- package/lib/memoryResolve.js +112 -0
- package/lib/setupCommands.js +191 -14
- package/lib/shapeCommands.js +313 -0
- package/lib/workspaceCommands.js +80 -5
- package/lib/workspaceContext.js +176 -176
- package/package.json +33 -33
package/lib/http.js
CHANGED
|
@@ -138,6 +138,163 @@ function httpJson(urlString, method, payload, headers = {}, timeoutMs = 0) {
|
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
// A filename travels to the server inside a Content-Disposition header we build by hand. Local
|
|
142
|
+
// filenames are arbitrary bytes: a quote ends the parameter early and a CR/LF splits the part
|
|
143
|
+
// header, so either one lets a filename forge headers or a whole extra part. multer/busboy also
|
|
144
|
+
// has no portable unescaping here, so these characters are replaced rather than escaped.
|
|
145
|
+
function sanitizeMultipartFilename(value) {
|
|
146
|
+
const cleaned = String(value || '')
|
|
147
|
+
.replace(/[\r\n"\\]/g, '_')
|
|
148
|
+
// C0 controls would be sent verbatim into a header line.
|
|
149
|
+
.replace(/[\x00-\x1f\x7f]/g, '_')
|
|
150
|
+
.trim();
|
|
151
|
+
return cleaned || 'attachment';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildMultipartBody(boundary, fields, file) {
|
|
155
|
+
const parts = [];
|
|
156
|
+
for (const [name, value] of Object.entries(fields || {})) {
|
|
157
|
+
if (value === undefined || value === null || value === '') continue;
|
|
158
|
+
parts.push(Buffer.from(
|
|
159
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="${sanitizeMultipartFilename(name)}"\r\n\r\n${String(value)}\r\n`,
|
|
160
|
+
'utf8'
|
|
161
|
+
));
|
|
162
|
+
}
|
|
163
|
+
parts.push(Buffer.from(
|
|
164
|
+
`--${boundary}\r\n`
|
|
165
|
+
+ `Content-Disposition: form-data; name="${sanitizeMultipartFilename(file.field || 'file')}";`
|
|
166
|
+
+ ` filename="${sanitizeMultipartFilename(file.filename)}"\r\n`
|
|
167
|
+
+ `Content-Type: ${sanitizeMultipartFilename(file.contentType || 'application/octet-stream')}\r\n\r\n`,
|
|
168
|
+
'utf8'
|
|
169
|
+
));
|
|
170
|
+
parts.push(file.buffer);
|
|
171
|
+
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8'));
|
|
172
|
+
return Buffer.concat(parts);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* multipart/form-data with exactly one file part, for attachment uploads. The CLI ships with one
|
|
177
|
+
* dependency and no form-data library, and the body is already fully buffered (the per-file cap
|
|
178
|
+
* is 25MB), so the body is assembled here rather than streamed.
|
|
179
|
+
*
|
|
180
|
+
* Resolves/rejects on the same contract as httpJson, so callers can use printApiError unchanged.
|
|
181
|
+
*/
|
|
182
|
+
function httpMultipart(urlString, method, { fields = {}, file }, headers = {}, timeoutMs = 0) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
if (!file || !Buffer.isBuffer(file.buffer)) {
|
|
185
|
+
reject(new Error('httpMultipart requires file.buffer'));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const parsed = new URL(urlString);
|
|
189
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
190
|
+
const boundary = `----empiricalcli${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`;
|
|
191
|
+
const body = buildMultipartBody(boundary, fields, file);
|
|
192
|
+
|
|
193
|
+
const req = lib.request(
|
|
194
|
+
parsed,
|
|
195
|
+
{
|
|
196
|
+
method,
|
|
197
|
+
headers: {
|
|
198
|
+
Accept: 'application/json',
|
|
199
|
+
// The server infers source 'cli' from this (see @empirical/core memoryAuthorship).
|
|
200
|
+
'User-Agent': 'empirical-cli/1.0',
|
|
201
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
202
|
+
'Content-Length': body.length,
|
|
203
|
+
...headers
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
(res) => {
|
|
207
|
+
let raw = '';
|
|
208
|
+
res.setEncoding('utf8');
|
|
209
|
+
res.on('data', (chunk) => {
|
|
210
|
+
raw += chunk;
|
|
211
|
+
});
|
|
212
|
+
res.on('end', () => {
|
|
213
|
+
let data = {};
|
|
214
|
+
try {
|
|
215
|
+
data = raw ? JSON.parse(raw) : {};
|
|
216
|
+
} catch (_error) {
|
|
217
|
+
data = { raw };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (data && Array.isArray(data.alerts) && data.alerts.length) {
|
|
221
|
+
process.stderr.write(renderCliAlertBanner(data.alerts));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const statusCode = res.statusCode || 0;
|
|
225
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
226
|
+
reject(httpErrorFromResponse(method, urlString, statusCode, data));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
resolve(data);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
req.on('error', reject);
|
|
235
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
236
|
+
req.setTimeout(timeoutMs, () => {
|
|
237
|
+
req.destroy(new Error(`request timeout after ${timeoutMs}ms`));
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
req.write(body);
|
|
241
|
+
req.end();
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* GET that keeps the response as BYTES. httpJson would decode the body as utf8 and corrupt any
|
|
247
|
+
* binary, so downloads cannot reuse it. A non-2xx body is still JSON here (the attachment
|
|
248
|
+
* download route answers 404/410 as JSON), so failures are parsed and rejected on the same
|
|
249
|
+
* contract as httpJson.
|
|
250
|
+
*
|
|
251
|
+
* @returns {Promise<{ statusCode: number, headers: object, buffer: Buffer }>}
|
|
252
|
+
*/
|
|
253
|
+
function httpDownload(urlString, headers = {}, timeoutMs = 0) {
|
|
254
|
+
return new Promise((resolve, reject) => {
|
|
255
|
+
const parsed = new URL(urlString);
|
|
256
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
257
|
+
|
|
258
|
+
const req = lib.request(
|
|
259
|
+
parsed,
|
|
260
|
+
{
|
|
261
|
+
method: 'GET',
|
|
262
|
+
headers: {
|
|
263
|
+
'User-Agent': 'empirical-cli/1.0',
|
|
264
|
+
...headers
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
(res) => {
|
|
268
|
+
const chunks = [];
|
|
269
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
270
|
+
res.on('end', () => {
|
|
271
|
+
const buffer = Buffer.concat(chunks);
|
|
272
|
+
const statusCode = res.statusCode || 0;
|
|
273
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
274
|
+
let data = {};
|
|
275
|
+
try {
|
|
276
|
+
data = JSON.parse(buffer.toString('utf8'));
|
|
277
|
+
} catch (_error) {
|
|
278
|
+
data = { raw: buffer.toString('utf8').slice(0, 500) };
|
|
279
|
+
}
|
|
280
|
+
reject(httpErrorFromResponse('GET', urlString, statusCode, data));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
resolve({ statusCode, headers: res.headers || {}, buffer });
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
req.on('error', reject);
|
|
289
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
290
|
+
req.setTimeout(timeoutMs, () => {
|
|
291
|
+
req.destroy(new Error(`request timeout after ${timeoutMs}ms`));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
req.end();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
141
298
|
function httpForm(urlString, method, params, headers = {}) {
|
|
142
299
|
return new Promise((resolve, reject) => {
|
|
143
300
|
const parsed = new URL(urlString);
|
|
@@ -196,5 +353,8 @@ module.exports = {
|
|
|
196
353
|
formatApiError,
|
|
197
354
|
printApiError,
|
|
198
355
|
httpJson,
|
|
356
|
+
httpMultipart,
|
|
357
|
+
httpDownload,
|
|
358
|
+
sanitizeMultipartFilename,
|
|
199
359
|
httpForm
|
|
200
360
|
};
|