@openagents-org/agent-launcher 0.2.150 → 0.2.152
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/package.json +2 -2
- package/registry.json +95 -0
- package/src/adapters/aider.js +5 -3
- package/src/adapters/base.js +34 -6
- package/src/adapters/claude.js +358 -158
- package/src/adapters/codex.js +58 -4
- package/src/adapters/cursor.js +21 -3
- package/src/adapters/decision-log.js +181 -0
- package/src/adapters/index.js +4 -1
- package/src/adapters/mini.js +572 -0
- package/src/adapters/openclaw.js +39 -2
- package/src/adapters/opencode.js +58 -10
- package/src/adapters/workspace-prompt.js +255 -29
- package/src/mcp-server.js +112 -0
- package/src/skill-installer.js +218 -0
- package/src/workspace-client.js +59 -14
package/src/mcp-server.js
CHANGED
|
@@ -100,9 +100,66 @@ function buildToolDefs(disabledModules) {
|
|
|
100
100
|
);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// -- Search module --
|
|
104
|
+
if (!disabledModules.has('search')) {
|
|
105
|
+
tools.push(
|
|
106
|
+
{
|
|
107
|
+
name: 'workspace_image_search',
|
|
108
|
+
description:
|
|
109
|
+
'Search the web for images. Returns image URLs you can show in chat directly by embedding ' +
|
|
110
|
+
'markdown —  — in your reply, or persist with workspace_image_save.',
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: 'object',
|
|
113
|
+
properties: {
|
|
114
|
+
query: { type: 'string', description: 'Image search query' },
|
|
115
|
+
count: { type: 'number', description: 'Number of results (default 10, max 20)' },
|
|
116
|
+
},
|
|
117
|
+
required: ['query'],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'workspace_image_save',
|
|
122
|
+
description:
|
|
123
|
+
'Download an image (or any file) URL into workspace storage. By default it is also posted ' +
|
|
124
|
+
'into the current channel as an inline image attachment (set post_to_channel=false to only save).',
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
url: { type: 'string', description: 'Direct image/file URL (e.g. image_url from workspace_image_search)' },
|
|
129
|
+
filename: { type: 'string', description: 'Filename to store as (derived from URL if omitted)' },
|
|
130
|
+
caption: { type: 'string', description: 'Message text to accompany the posted image' },
|
|
131
|
+
post_to_channel: { type: 'boolean', description: 'Post into the chat as an attachment (default true)' },
|
|
132
|
+
},
|
|
133
|
+
required: ['url'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
// -- Browser module --
|
|
104
140
|
if (!disabledModules.has('browser')) {
|
|
105
141
|
tools.push(
|
|
142
|
+
{
|
|
143
|
+
name: 'workspace_fetch_url',
|
|
144
|
+
description:
|
|
145
|
+
'Read a web page as text WITHOUT opening a shared browser tab. Preferred way to read any URL: ' +
|
|
146
|
+
'tries a fast static fetch first, and automatically renders JavaScript-heavy pages (Notion, SPAs) ' +
|
|
147
|
+
'in a temporary browser session. Only open a shared browser tab (workspace_browser_open) when you ' +
|
|
148
|
+
'need to interact with the page (click/type/login) or the result says AUTH_REQUIRED.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
url: { type: 'string', description: 'URL to fetch (http/https)' },
|
|
153
|
+
mode: {
|
|
154
|
+
type: 'string',
|
|
155
|
+
enum: ['auto', 'static', 'render'],
|
|
156
|
+
description: 'auto (default): static first, browser render if needed. static: never use a browser. render: force browser rendering.',
|
|
157
|
+
},
|
|
158
|
+
max_chars: { type: 'number', description: 'Max characters of content to return (default 20000)' },
|
|
159
|
+
},
|
|
160
|
+
required: ['url'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
106
163
|
{
|
|
107
164
|
name: 'workspace_browser_open',
|
|
108
165
|
description:
|
|
@@ -629,6 +686,61 @@ class McpServer {
|
|
|
629
686
|
|
|
630
687
|
// ── Browser ──
|
|
631
688
|
|
|
689
|
+
case 'workspace_fetch_url': {
|
|
690
|
+
try {
|
|
691
|
+
const result = await this.ws.fetchUrl(this.workspaceId, this.token, args.url, {
|
|
692
|
+
mode: args.mode || 'auto',
|
|
693
|
+
maxChars: args.max_chars,
|
|
694
|
+
source: `openagents:${this.agentName}`,
|
|
695
|
+
});
|
|
696
|
+
const header = [
|
|
697
|
+
`URL: ${result.url}`,
|
|
698
|
+
result.title ? `Title: ${result.title}` : null,
|
|
699
|
+
`Source: ${result.content_source}${result.truncated ? ' (truncated)' : ''}`,
|
|
700
|
+
].filter(Boolean).join('\n');
|
|
701
|
+
return text(`${header}\n\n${result.content || '(empty page)'}`);
|
|
702
|
+
} catch (e) {
|
|
703
|
+
const detail = e.data || {};
|
|
704
|
+
const parts = [`Fetch failed: ${e.message}`];
|
|
705
|
+
if (detail.error_code) parts.push(`Code: ${detail.error_code}`);
|
|
706
|
+
if (detail.hint) parts.push(`Hint: ${detail.hint}`);
|
|
707
|
+
return text(parts.join('\n'));
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
case 'workspace_image_search': {
|
|
712
|
+
const data = await this.ws.searchImages(this.workspaceId, this.token, args.query, {
|
|
713
|
+
count: args.count || 10,
|
|
714
|
+
});
|
|
715
|
+
const results = (data && data.results) || [];
|
|
716
|
+
if (!results.length) return text(`No image results for "${args.query}".`);
|
|
717
|
+
const lines = results.map((r, i) => {
|
|
718
|
+
const dims = r.width && r.height ? ` (${r.width}x${r.height})` : '';
|
|
719
|
+
return `${i + 1}. ${r.title || 'untitled'}${dims}\n` +
|
|
720
|
+
` image_url: ${r.image_url}\n` +
|
|
721
|
+
` source: ${r.page_url || r.source || 'unknown'}`;
|
|
722
|
+
});
|
|
723
|
+
return text(
|
|
724
|
+
`Found ${results.length} images for "${args.query}":\n\n${lines.join('\n')}\n\n` +
|
|
725
|
+
'To show one in chat, embed it in your reply as markdown: . ' +
|
|
726
|
+
'To keep a copy in the workspace (and post it as an attachment), call workspace_image_save with the image_url.'
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
case 'workspace_image_save': {
|
|
731
|
+
const result = await this.ws.uploadFileFromUrl(this.workspaceId, this.token, args.url, {
|
|
732
|
+
filename: args.filename,
|
|
733
|
+
channelName: this.channelName,
|
|
734
|
+
source: `openagents:${this.agentName}`,
|
|
735
|
+
postToChannel: args.post_to_channel !== false,
|
|
736
|
+
caption: args.caption,
|
|
737
|
+
});
|
|
738
|
+
const posted = result.posted_to_channel
|
|
739
|
+
? ' and posted to the channel'
|
|
740
|
+
: '';
|
|
741
|
+
return text(`Saved ${result.filename} (${result.content_type}, ${result.size} bytes, id: ${result.id})${posted}.`);
|
|
742
|
+
}
|
|
743
|
+
|
|
632
744
|
case 'workspace_browser_open': {
|
|
633
745
|
const opts = {
|
|
634
746
|
url: args.url || 'about:blank',
|
package/src/skill-installer.js
CHANGED
|
@@ -24,9 +24,16 @@
|
|
|
24
24
|
const fs = require('fs');
|
|
25
25
|
const os = require('os');
|
|
26
26
|
const path = require('path');
|
|
27
|
+
const zlib = require('zlib');
|
|
27
28
|
const { execFileSync } = require('child_process');
|
|
28
29
|
const { defaultAgentWorkdir } = require('./paths');
|
|
29
30
|
|
|
31
|
+
// Uploaded-skill (.zip) extraction guards. Mirrors the server-side limits in
|
|
32
|
+
// workspace/backend/app/custom_skills.py so both ends agree.
|
|
33
|
+
const MAX_ZIP_ENTRIES = 2000;
|
|
34
|
+
const MAX_ZIP_UNCOMPRESSED_BYTES = 50 * 1024 * 1024; // 50 MB expanded total
|
|
35
|
+
const MAX_ZIP_COMPRESSION_RATIO = 200; // expanded / stored
|
|
36
|
+
|
|
30
37
|
/**
|
|
31
38
|
* Resolve the skills directory for a given agent type, rooted at the agent's
|
|
32
39
|
* working directory. Different runtimes look in different places:
|
|
@@ -257,6 +264,47 @@ function installSkill({ skill, agentType, workingDir, fetcher, log }) {
|
|
|
257
264
|
return { path: destDir, skillId: norm.id, partial };
|
|
258
265
|
}
|
|
259
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Install a user-uploaded custom skill (source_type=workspace_file) from an
|
|
269
|
+
* already-downloaded Buffer. Unlike catalog skills, the bytes are provided by
|
|
270
|
+
* the caller (the adapter downloads them via WorkspaceClient.readFile) — the
|
|
271
|
+
* installer never touches the network here.
|
|
272
|
+
*
|
|
273
|
+
* - `.md` → written verbatim as <destDir>/SKILL.md
|
|
274
|
+
* - `.zip` → extracted with full path/symlink/zip-bomb validation; a single
|
|
275
|
+
* top-level directory is stripped so SKILL.md ends up at the root.
|
|
276
|
+
*
|
|
277
|
+
* Reuses installSkill's dest-dir resolution, clean-on-reinstall, and final
|
|
278
|
+
* SKILL.md verification by passing a synchronous fetcher that writes the Buffer.
|
|
279
|
+
*
|
|
280
|
+
* @param {object} args
|
|
281
|
+
* @param {object} args.skill metadata (id, package_type, filename, …)
|
|
282
|
+
* @param {Buffer} args.buffer the uploaded file bytes
|
|
283
|
+
* @param {string} args.agentType
|
|
284
|
+
* @param {string} [args.workingDir]
|
|
285
|
+
* @param {function} [args.log]
|
|
286
|
+
* @returns {{path: string, skillId: string, partial: boolean}}
|
|
287
|
+
*/
|
|
288
|
+
function installUploadedSkill({ skill, buffer, agentType, workingDir, log }) {
|
|
289
|
+
const _log = log || (() => {});
|
|
290
|
+
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
291
|
+
const id = (skill && (skill.id || skill.skill_id)) || 'unknown';
|
|
292
|
+
throw new Error(`uploaded skill "${id}" has no file content`);
|
|
293
|
+
}
|
|
294
|
+
const packageType = _resolvePackageType(skill, buffer);
|
|
295
|
+
const fetcher = ({ destDir }) => {
|
|
296
|
+
if (packageType === 'zip') {
|
|
297
|
+
_installUploadedZip(buffer, destDir, _log);
|
|
298
|
+
} else {
|
|
299
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
300
|
+
fs.writeFileSync(path.join(destDir, 'SKILL.md'), buffer);
|
|
301
|
+
_log('wrote uploaded SKILL.md');
|
|
302
|
+
}
|
|
303
|
+
return { partial: false };
|
|
304
|
+
};
|
|
305
|
+
return installSkill({ skill, agentType, workingDir, fetcher, log: _log });
|
|
306
|
+
}
|
|
307
|
+
|
|
260
308
|
/**
|
|
261
309
|
* Remove an installed skill directory. Idempotent.
|
|
262
310
|
*
|
|
@@ -363,6 +411,175 @@ function _copyDir(src, dest) {
|
|
|
363
411
|
}
|
|
364
412
|
}
|
|
365
413
|
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Uploaded-skill (workspace_file) install internals
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
/** Decide md vs zip: declared package_type → zip magic sniff → filename hint. */
|
|
419
|
+
function _resolvePackageType(skill, buffer) {
|
|
420
|
+
const declared = String((skill && (skill.package_type || skill.packageType)) || '').toLowerCase();
|
|
421
|
+
if (declared === 'zip' || declared === 'md') return declared;
|
|
422
|
+
if (Buffer.isBuffer(buffer) && buffer.length >= 2 && buffer[0] === 0x50 && buffer[1] === 0x4b) {
|
|
423
|
+
return 'zip'; // "PK"
|
|
424
|
+
}
|
|
425
|
+
const fn = String((skill && (skill.filename || skill.file_name)) || '').toLowerCase();
|
|
426
|
+
if (fn.endsWith('.zip')) return 'zip';
|
|
427
|
+
return 'md';
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Only a real SKILL.md counts (uploaded zips must ship one, not just any *.md). */
|
|
431
|
+
function _hasSkillMd(dir) {
|
|
432
|
+
for (const c of ['SKILL.md', 'skill.md', 'Skill.md']) {
|
|
433
|
+
if (fs.existsSync(path.join(dir, c))) return true;
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Path safety for a zip entry name. Mirrors _is_unsafe_zip_name in the backend:
|
|
439
|
+
// reject absolute paths, Windows drive paths, and any ".." segment.
|
|
440
|
+
function _isUnsafeZipName(name) {
|
|
441
|
+
if (!name) return true;
|
|
442
|
+
if (name.startsWith('/') || name.startsWith('\\')) return true;
|
|
443
|
+
if (/^[A-Za-z]:/.test(name)) return true;
|
|
444
|
+
return name.split(/[\\/]/).some((seg) => seg === '..');
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function _isSymlinkMode(unixMode) {
|
|
448
|
+
return (unixMode & 0o170000) === 0o120000; // S_IFLNK
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Parse a zip's central directory into entry descriptors. We trust the central
|
|
453
|
+
* directory (not local headers) for sizes/method, and reject zip64 (skills are
|
|
454
|
+
* small). Returns [{name, method, compSize, uncompSize, unixMode, localOffset}].
|
|
455
|
+
*/
|
|
456
|
+
function _readZipEntries(buf) {
|
|
457
|
+
const EOCD_SIG = 0x06054b50;
|
|
458
|
+
const maxBack = Math.min(buf.length, 22 + 0xffff);
|
|
459
|
+
let eocd = -1;
|
|
460
|
+
for (let i = buf.length - 22; i >= buf.length - maxBack && i >= 0; i--) {
|
|
461
|
+
if (buf.readUInt32LE(i) === EOCD_SIG) { eocd = i; break; }
|
|
462
|
+
}
|
|
463
|
+
if (eocd < 0) throw new Error('invalid zip: end-of-central-directory not found');
|
|
464
|
+
|
|
465
|
+
const totalEntries = buf.readUInt16LE(eocd + 10);
|
|
466
|
+
const cdOffset = buf.readUInt32LE(eocd + 16);
|
|
467
|
+
if (totalEntries === 0xffff || cdOffset === 0xffffffff) {
|
|
468
|
+
throw new Error('zip64 archives are not supported');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const entries = [];
|
|
472
|
+
let p = cdOffset;
|
|
473
|
+
for (let n = 0; n < totalEntries; n++) {
|
|
474
|
+
if (p + 46 > buf.length || buf.readUInt32LE(p) !== 0x02014b50) {
|
|
475
|
+
throw new Error('invalid zip: corrupt central directory');
|
|
476
|
+
}
|
|
477
|
+
const method = buf.readUInt16LE(p + 10);
|
|
478
|
+
const compSize = buf.readUInt32LE(p + 20);
|
|
479
|
+
const uncompSize = buf.readUInt32LE(p + 24);
|
|
480
|
+
const nameLen = buf.readUInt16LE(p + 28);
|
|
481
|
+
const extraLen = buf.readUInt16LE(p + 30);
|
|
482
|
+
const commentLen = buf.readUInt16LE(p + 32);
|
|
483
|
+
const externalAttr = buf.readUInt32LE(p + 38);
|
|
484
|
+
const localOffset = buf.readUInt32LE(p + 42);
|
|
485
|
+
if (compSize === 0xffffffff || uncompSize === 0xffffffff || localOffset === 0xffffffff) {
|
|
486
|
+
throw new Error('zip64 archives are not supported');
|
|
487
|
+
}
|
|
488
|
+
const name = buf.toString('utf8', p + 46, p + 46 + nameLen);
|
|
489
|
+
entries.push({
|
|
490
|
+
name, method, compSize, uncompSize, localOffset,
|
|
491
|
+
unixMode: (externalAttr >>> 16) & 0xffff,
|
|
492
|
+
});
|
|
493
|
+
p += 46 + nameLen + extraLen + commentLen;
|
|
494
|
+
}
|
|
495
|
+
return entries;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Read + decompress one entry's bytes (store or deflate only). */
|
|
499
|
+
function _zipEntryData(buf, e) {
|
|
500
|
+
if (e.localOffset + 30 > buf.length || buf.readUInt32LE(e.localOffset) !== 0x04034b50) {
|
|
501
|
+
throw new Error(`invalid zip: bad local header for "${e.name}"`);
|
|
502
|
+
}
|
|
503
|
+
const nameLen = buf.readUInt16LE(e.localOffset + 26);
|
|
504
|
+
const extraLen = buf.readUInt16LE(e.localOffset + 28);
|
|
505
|
+
const start = e.localOffset + 30 + nameLen + extraLen;
|
|
506
|
+
const end = start + e.compSize;
|
|
507
|
+
if (end > buf.length) throw new Error(`invalid zip: truncated data for "${e.name}"`);
|
|
508
|
+
const raw = buf.subarray(start, end);
|
|
509
|
+
if (e.method === 0) return Buffer.from(raw); // stored
|
|
510
|
+
if (e.method === 8) return zlib.inflateRawSync(raw); // deflate
|
|
511
|
+
throw new Error(`unsupported zip compression method ${e.method} for "${e.name}"`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Extract a validated zip Buffer into `outDir`. Enforces entry-count, total-
|
|
516
|
+
* size and compression-ratio caps, rejects unsafe paths / symlinks, and
|
|
517
|
+
* verifies every written path stays inside `outDir`.
|
|
518
|
+
*/
|
|
519
|
+
function _extractZipToDir(buffer, outDir, log) {
|
|
520
|
+
const entries = _readZipEntries(buffer);
|
|
521
|
+
if (entries.length > MAX_ZIP_ENTRIES) {
|
|
522
|
+
throw new Error(`zip has too many entries (> ${MAX_ZIP_ENTRIES})`);
|
|
523
|
+
}
|
|
524
|
+
let totalUncomp = 0;
|
|
525
|
+
let totalComp = 0;
|
|
526
|
+
for (const e of entries) { totalUncomp += e.uncompSize; totalComp += e.compSize; }
|
|
527
|
+
if (totalUncomp > MAX_ZIP_UNCOMPRESSED_BYTES) {
|
|
528
|
+
throw new Error('zip expands too large');
|
|
529
|
+
}
|
|
530
|
+
if (totalComp > 0 && totalUncomp / totalComp > MAX_ZIP_COMPRESSION_RATIO) {
|
|
531
|
+
throw new Error('zip compression ratio looks abusive (possible zip bomb)');
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const root = path.resolve(outDir);
|
|
535
|
+
fs.mkdirSync(root, { recursive: true });
|
|
536
|
+
for (const e of entries) {
|
|
537
|
+
if (_isSymlinkMode(e.unixMode)) throw new Error(`zip contains a symlink entry: "${e.name}"`);
|
|
538
|
+
if (_isUnsafeZipName(e.name)) throw new Error(`zip contains an unsafe path: "${e.name}"`);
|
|
539
|
+
const isDir = e.name.endsWith('/');
|
|
540
|
+
const dest = path.resolve(root, e.name.replace(/\\/g, '/'));
|
|
541
|
+
if (dest !== root && !dest.startsWith(root + path.sep)) {
|
|
542
|
+
throw new Error(`zip entry escapes target dir: "${e.name}"`);
|
|
543
|
+
}
|
|
544
|
+
if (isDir) { fs.mkdirSync(dest, { recursive: true }); continue; }
|
|
545
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
546
|
+
fs.writeFileSync(dest, _zipEntryData(buffer, e));
|
|
547
|
+
}
|
|
548
|
+
if (log) log(`extracted ${entries.length} zip entr${entries.length === 1 ? 'y' : 'ies'}`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Install an uploaded `.zip` into `destDir`: extract to a temp dir, verify a
|
|
553
|
+
* SKILL.md is present (stripping a single wrapper directory if needed), then
|
|
554
|
+
* copy the validated tree in. On any failure the temp dir is cleaned up and
|
|
555
|
+
* nothing partial is written into destDir (installSkill emptied it first).
|
|
556
|
+
*/
|
|
557
|
+
function _installUploadedZip(buffer, destDir, log) {
|
|
558
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'oa-skillzip-'));
|
|
559
|
+
try {
|
|
560
|
+
_extractZipToDir(buffer, tmp, log);
|
|
561
|
+
|
|
562
|
+
// Strip a single top-level wrapper dir (my-skill/SKILL.md → SKILL.md).
|
|
563
|
+
let srcRoot = tmp;
|
|
564
|
+
if (!_hasSkillMd(tmp)) {
|
|
565
|
+
const ents = fs.readdirSync(tmp, { withFileTypes: true });
|
|
566
|
+
const dirs = ents.filter((e) => e.isDirectory());
|
|
567
|
+
const files = ents.filter((e) => !e.isDirectory());
|
|
568
|
+
if (dirs.length === 1 && files.length === 0 && _hasSkillMd(path.join(tmp, dirs[0].name))) {
|
|
569
|
+
srcRoot = path.join(tmp, dirs[0].name);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (!_hasSkillMd(srcRoot)) {
|
|
573
|
+
throw new Error('uploaded skill .zip has no SKILL.md');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
577
|
+
_copyDir(srcRoot, destDir);
|
|
578
|
+
} finally {
|
|
579
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch {}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
366
583
|
// Blocking GET used only by defaultFetcher's HTTPS fallback (which runs off
|
|
367
584
|
// the hot path inside a try/catch). Uses curl for redirect handling; returns
|
|
368
585
|
// the body string or throws. git sparse-checkout is the primary strategy, so
|
|
@@ -379,6 +596,7 @@ function _httpGetSync(url) {
|
|
|
379
596
|
module.exports = {
|
|
380
597
|
skillsDirForAgentType,
|
|
381
598
|
installSkill,
|
|
599
|
+
installUploadedSkill,
|
|
382
600
|
uninstallSkill,
|
|
383
601
|
listInstalledSkills,
|
|
384
602
|
normalizeSkill,
|
package/src/workspace-client.js
CHANGED
|
@@ -192,26 +192,29 @@ class WorkspaceClient {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
* Fetch
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
195
|
+
* Fetch N messages in a channel, returned oldest-to-newest. `sort` picks
|
|
196
|
+
* which end of the channel the window comes from: 'desc' (default) takes
|
|
197
|
+
* the most recent N, 'asc' takes the channel's opening N. Used by adapters
|
|
198
|
+
* to rebuild context for a fresh Claude Code session when --resume of the
|
|
199
|
+
* previous session fails (the channel's chat history is the only thing
|
|
200
|
+
* that survives a session-storage rotation).
|
|
199
201
|
*/
|
|
200
|
-
async getRecentMessages(workspaceId, channelName, token, limit = 30) {
|
|
202
|
+
async getRecentMessages(workspaceId, channelName, token, limit = 30, { sort = 'desc' } = {}) {
|
|
201
203
|
try {
|
|
202
204
|
const params = new URLSearchParams({
|
|
203
205
|
network: workspaceId,
|
|
204
206
|
channel: channelName,
|
|
205
207
|
type: 'workspace.message',
|
|
206
|
-
sort
|
|
208
|
+
sort,
|
|
207
209
|
limit: String(limit),
|
|
208
210
|
});
|
|
209
211
|
const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
|
|
210
212
|
const result = data.data || data;
|
|
211
213
|
const events = (result && result.events) || [];
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
214
|
+
// A desc window arrives newest-first; reverse so the caller always gets
|
|
215
|
+
// chronological order. An asc window is already chronological.
|
|
216
|
+
const ordered = sort === 'asc' ? events.slice() : events.slice().reverse();
|
|
217
|
+
return ordered.map((e) => this._eventToMessage(e));
|
|
215
218
|
} catch {
|
|
216
219
|
return [];
|
|
217
220
|
}
|
|
@@ -471,6 +474,31 @@ class WorkspaceClient {
|
|
|
471
474
|
return data.data || data;
|
|
472
475
|
}
|
|
473
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Search the web for images via POST /v1/search/images.
|
|
479
|
+
*/
|
|
480
|
+
async searchImages(workspaceId, token, query, { count = 10 } = {}) {
|
|
481
|
+
const body = { query, network: workspaceId, count };
|
|
482
|
+
const data = await this._post('/v1/search/images', body, this._wsHeaders(token), 30000);
|
|
483
|
+
return data.data || data;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Download a URL into workspace storage via POST /v1/files/from_url.
|
|
488
|
+
* With postToChannel, the file is also posted into the chat as an
|
|
489
|
+
* inline attachment.
|
|
490
|
+
*/
|
|
491
|
+
async uploadFileFromUrl(workspaceId, token, url, {
|
|
492
|
+
filename, channelName, source = 'human:user', postToChannel = false, caption,
|
|
493
|
+
} = {}) {
|
|
494
|
+
const body = { url, network: workspaceId, source, post_to_channel: postToChannel };
|
|
495
|
+
if (filename) body.filename = filename;
|
|
496
|
+
if (channelName) body.channel_name = channelName;
|
|
497
|
+
if (caption) body.caption = caption;
|
|
498
|
+
const data = await this._post('/v1/files/from_url', body, this._wsHeaders(token), 90000);
|
|
499
|
+
return data.data || data;
|
|
500
|
+
}
|
|
501
|
+
|
|
474
502
|
/**
|
|
475
503
|
* List files via GET /v1/files.
|
|
476
504
|
*/
|
|
@@ -583,6 +611,19 @@ class WorkspaceClient {
|
|
|
583
611
|
return data.data || data;
|
|
584
612
|
}
|
|
585
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Server-side fetch chain via POST /v1/fetch — reads a page WITHOUT
|
|
616
|
+
* holding a shared browser tab. Static HTTP first; JS-heavy pages are
|
|
617
|
+
* rendered in an ephemeral browser session that is closed immediately.
|
|
618
|
+
*/
|
|
619
|
+
async fetchUrl(workspaceId, token, url, { mode = 'auto', maxChars, source } = {}) {
|
|
620
|
+
const body = { url, network: workspaceId, mode };
|
|
621
|
+
if (maxChars) body.max_chars = maxChars;
|
|
622
|
+
if (source) body.source = source;
|
|
623
|
+
const data = await this._post('/v1/fetch', body, this._wsHeaders(token), 90000);
|
|
624
|
+
return data.data || data;
|
|
625
|
+
}
|
|
626
|
+
|
|
586
627
|
/**
|
|
587
628
|
* List persistent browser contexts via GET /v1/browser/contexts.
|
|
588
629
|
*/
|
|
@@ -699,18 +740,18 @@ class WorkspaceClient {
|
|
|
699
740
|
|
|
700
741
|
// ── Knowledge Base ──
|
|
701
742
|
|
|
702
|
-
async listKnowledge(workspaceId, token, { limit = 100 } = {}) {
|
|
743
|
+
async listKnowledge(workspaceId, token, { limit = 100, timeout } = {}) {
|
|
703
744
|
const params = new URLSearchParams({
|
|
704
745
|
network: workspaceId,
|
|
705
746
|
limit: String(limit),
|
|
706
747
|
});
|
|
707
|
-
const data = await this._get(`/v1/knowledge?${params}`, this._wsHeaders(token));
|
|
748
|
+
const data = await this._get(`/v1/knowledge?${params}`, this._wsHeaders(token), timeout);
|
|
708
749
|
return data.data || data;
|
|
709
750
|
}
|
|
710
751
|
|
|
711
|
-
async getKnowledge(workspaceId, token, entryId) {
|
|
752
|
+
async getKnowledge(workspaceId, token, entryId, { timeout } = {}) {
|
|
712
753
|
const params = new URLSearchParams({ network: workspaceId });
|
|
713
|
-
const data = await this._get(`/v1/knowledge/${entryId}?${params}`, this._wsHeaders(token));
|
|
754
|
+
const data = await this._get(`/v1/knowledge/${entryId}?${params}`, this._wsHeaders(token), timeout);
|
|
714
755
|
return data.data || data;
|
|
715
756
|
}
|
|
716
757
|
|
|
@@ -878,7 +919,11 @@ class WorkspaceClient {
|
|
|
878
919
|
if (typeof msg === 'string' && msg.toLowerCase().includes('session_revoked')) {
|
|
879
920
|
reject(new SessionRevokedError(msg));
|
|
880
921
|
} else {
|
|
881
|
-
|
|
922
|
+
const err = new Error(msg);
|
|
923
|
+
// Preserve structured error details (error_code, hint,
|
|
924
|
+
// quota occupancy, ...) so tool handlers can surface them.
|
|
925
|
+
if (parsed.data && typeof parsed.data === 'object') err.data = parsed.data;
|
|
926
|
+
reject(err);
|
|
882
927
|
}
|
|
883
928
|
} else {
|
|
884
929
|
resolve(parsed);
|