@openagents-org/agent-launcher 0.2.115 → 0.2.118
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 +1 -1
- package/src/adapters/base.js +47 -0
- package/src/adapters/claude.js +159 -34
- package/src/adapters/workspace-prompt.js +97 -1
- package/src/cli.js +45 -0
- package/src/daemon.js +1 -0
- package/src/mcp-server.js +116 -0
- package/src/workspace-client.js +120 -0
package/package.json
CHANGED
package/src/adapters/base.js
CHANGED
|
@@ -391,6 +391,53 @@ class BaseAdapter {
|
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
393
|
|
|
394
|
+
async cleanupTodos(channel) {
|
|
395
|
+
try {
|
|
396
|
+
const result = await this.client.getTodos(this.workspaceId, channel, this.token, {
|
|
397
|
+
all: false,
|
|
398
|
+
});
|
|
399
|
+
const todos = (result && result.todos) || [];
|
|
400
|
+
const hasActive = todos.some((t) => t.status === 'pending' || t.status === 'in_progress');
|
|
401
|
+
if (!hasActive) return;
|
|
402
|
+
const updated = todos.map((t) => ({
|
|
403
|
+
content: t.content,
|
|
404
|
+
status: (t.status === 'pending' || t.status === 'in_progress') ? 'cancelled' : t.status,
|
|
405
|
+
assignee: t.assignee,
|
|
406
|
+
}));
|
|
407
|
+
await this.client.putTodos(this.workspaceId, channel, this.token, updated, {
|
|
408
|
+
source: `openagents:${this.agentName}`,
|
|
409
|
+
});
|
|
410
|
+
} catch {
|
|
411
|
+
// Best-effort cleanup
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async sendTodos(channel, todos) {
|
|
416
|
+
try {
|
|
417
|
+
await this.client.putTodos(this.workspaceId, channel, this.token, todos, {
|
|
418
|
+
source: `openagents:${this.agentName}`,
|
|
419
|
+
});
|
|
420
|
+
} catch (e) {
|
|
421
|
+
if (e instanceof SessionRevokedError) { this._onSessionRevoked(); return; }
|
|
422
|
+
// Fallback to event-based approach for older backends
|
|
423
|
+
const lines = todos.map((t) => {
|
|
424
|
+
const icon = t.status === 'completed' ? '✅' : t.status === 'in_progress' ? '🔄' : '⬜';
|
|
425
|
+
return `${icon} ${t.content}`;
|
|
426
|
+
});
|
|
427
|
+
try {
|
|
428
|
+
await this.client.sendMessage(this.workspaceId, channel, this.token, lines.join('\n'), {
|
|
429
|
+
senderType: 'agent',
|
|
430
|
+
senderName: this.agentName,
|
|
431
|
+
messageType: 'todos',
|
|
432
|
+
metadata: { agent_mode: this._mode, todos },
|
|
433
|
+
sessionId: this._sessionId,
|
|
434
|
+
});
|
|
435
|
+
} catch (e2) {
|
|
436
|
+
if (e2 instanceof SessionRevokedError) this._onSessionRevoked();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
394
441
|
async sendError(channel, error) {
|
|
395
442
|
try {
|
|
396
443
|
await this.client.sendMessage(this.workspaceId, channel, this.token, error, {
|
package/src/adapters/claude.js
CHANGED
|
@@ -18,7 +18,7 @@ const { execSync, spawn } = require('child_process');
|
|
|
18
18
|
|
|
19
19
|
const BaseAdapter = require('./base');
|
|
20
20
|
const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
|
|
21
|
-
const { buildClaudeSystemPrompt } = require('./workspace-prompt');
|
|
21
|
+
const { buildClaudeSystemPrompt, buildClaudeSkillMd } = require('./workspace-prompt');
|
|
22
22
|
|
|
23
23
|
const IS_WINDOWS = process.platform === 'win32';
|
|
24
24
|
|
|
@@ -31,6 +31,8 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
31
31
|
constructor(opts) {
|
|
32
32
|
super(opts);
|
|
33
33
|
this.disabledModules = opts.disabledModules || new Set();
|
|
34
|
+
/** @type {'mcp' | 'skills'} Tool integration mode */
|
|
35
|
+
this.toolMode = opts.toolMode || 'mcp';
|
|
34
36
|
this._channelSessions = {}; // channel → Claude CLI session_id
|
|
35
37
|
this._channelProcesses = {}; // channel → child process
|
|
36
38
|
this._stoppingChannels = new Set();
|
|
@@ -128,6 +130,48 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
128
130
|
} catch {}
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Build a short transcript of the channel's last chat exchanges, used to
|
|
135
|
+
* re-seed context when --resume fails and we have to start a fresh
|
|
136
|
+
* Claude Code session. Returns null when there's nothing useful to add.
|
|
137
|
+
*
|
|
138
|
+
* Excludes the user's current message (the for-loop will append it
|
|
139
|
+
* normally) and any status/thinking events, which are mostly tool-call
|
|
140
|
+
* noise and inflate the prompt without adding signal.
|
|
141
|
+
*/
|
|
142
|
+
async _buildChannelRecap(channelName, currentMessage) {
|
|
143
|
+
const messages = await this.client.getRecentMessages(
|
|
144
|
+
this.workspaceId, channelName, this.token, 30
|
|
145
|
+
);
|
|
146
|
+
if (!messages || messages.length === 0) return null;
|
|
147
|
+
|
|
148
|
+
const lines = [];
|
|
149
|
+
for (const m of messages) {
|
|
150
|
+
const mt = m.messageType || 'chat';
|
|
151
|
+
if (mt === 'status' || mt === 'thinking' || mt === 'loading') continue;
|
|
152
|
+
const text = (m.content || '').trim();
|
|
153
|
+
if (!text) continue;
|
|
154
|
+
// Don't echo the user's current message back at them.
|
|
155
|
+
if (text === currentMessage) continue;
|
|
156
|
+
const who = m.senderType === 'human'
|
|
157
|
+
? (m.senderName || 'user')
|
|
158
|
+
: (m.senderName || 'agent');
|
|
159
|
+
// Cap each line so a single huge paste doesn't blow up the prompt.
|
|
160
|
+
const truncated = text.length > 800 ? text.slice(0, 800) + '…' : text;
|
|
161
|
+
lines.push(`[${who}] ${truncated}`);
|
|
162
|
+
}
|
|
163
|
+
if (lines.length === 0) return null;
|
|
164
|
+
|
|
165
|
+
// Keep only the tail; older context has diminishing value and we
|
|
166
|
+
// don't want to balloon the system prompt.
|
|
167
|
+
const tail = lines.slice(-15).join('\n');
|
|
168
|
+
return (
|
|
169
|
+
'You previously worked in this channel but your prior session is no ' +
|
|
170
|
+
'longer available, so here is the recent conversation for context:\n\n' +
|
|
171
|
+
tail
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
131
175
|
async _stopAllProcesses(completionMessage = 'Execution stopped.') {
|
|
132
176
|
const entries = Object.entries(this._channelProcesses);
|
|
133
177
|
if (!entries.length) return;
|
|
@@ -243,15 +287,81 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
243
287
|
throw new Error('claude CLI not found. Install with: curl -fsSL https://claude.ai/install.sh | bash');
|
|
244
288
|
}
|
|
245
289
|
|
|
246
|
-
|
|
290
|
+
let systemPrompt = '\n' + buildClaudeSystemPrompt({
|
|
247
291
|
agentName: this.agentName,
|
|
248
292
|
workspaceId: this.workspaceId,
|
|
249
293
|
channelName,
|
|
250
294
|
mode: this._mode,
|
|
251
295
|
});
|
|
252
296
|
|
|
297
|
+
// In skills mode, replace MCP tool references with curl-based instructions
|
|
298
|
+
if (this.toolMode === 'skills') {
|
|
299
|
+
systemPrompt = systemPrompt
|
|
300
|
+
.replace(
|
|
301
|
+
'Use workspace_get_history to read previous messages.\n' +
|
|
302
|
+
'Use workspace_get_agents to see other agents.\n',
|
|
303
|
+
'Use the openagents-workspace skill (Bash + curl) for workspace operations:\n' +
|
|
304
|
+
'reading message history, discovering agents, sharing files, and browsing.\n' +
|
|
305
|
+
'Refer to the skill instructions for the exact curl commands.\n'
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
253
309
|
const cmd = [claudeBin, '-p', prompt, '--output-format', 'stream-json', '--verbose'];
|
|
254
310
|
|
|
311
|
+
cmd.push('--append-system-prompt', systemPrompt);
|
|
312
|
+
cmd.push('--disallowedTools', 'AskUserQuestion');
|
|
313
|
+
|
|
314
|
+
// Resume existing conversation (skipped on retry after stale session)
|
|
315
|
+
const sessionId = this._channelSessions[channelName];
|
|
316
|
+
if (sessionId && !skipResume) {
|
|
317
|
+
cmd.push('--resume', sessionId);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Skills mode: write SKILL.md, no MCP server ──
|
|
321
|
+
if (this.toolMode === 'skills') {
|
|
322
|
+
return this._buildSkillsCmd(cmd, channelName);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── MCP mode (default): spawn MCP server ──
|
|
326
|
+
return this._buildMcpCmd(cmd, channelName);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Skills mode: write a SKILL.md file and allow Bash + curl for workspace ops.
|
|
331
|
+
*/
|
|
332
|
+
_buildSkillsCmd(cmd, channelName) {
|
|
333
|
+
if (this._mode === 'plan') {
|
|
334
|
+
cmd.push('--permission-mode', 'plan');
|
|
335
|
+
cmd.push('--allowedTools', 'Read', 'Glob', 'Grep', 'Bash');
|
|
336
|
+
} else {
|
|
337
|
+
cmd.push('--dangerously-skip-permissions');
|
|
338
|
+
cmd.push('--allowedTools', 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Write SKILL.md to .claude/skills/ in the working directory
|
|
342
|
+
const workDir = this.workingDir || process.cwd();
|
|
343
|
+
const skillDir = path.join(workDir, '.claude', 'skills');
|
|
344
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
345
|
+
const skillFile = path.join(skillDir, 'openagents-workspace.md');
|
|
346
|
+
|
|
347
|
+
const skillContent = buildClaudeSkillMd({
|
|
348
|
+
endpoint: this.endpoint,
|
|
349
|
+
workspaceId: this.workspaceId,
|
|
350
|
+
token: this.token,
|
|
351
|
+
agentName: this.agentName,
|
|
352
|
+
channelName,
|
|
353
|
+
disabledModules: this.disabledModules,
|
|
354
|
+
});
|
|
355
|
+
fs.writeFileSync(skillFile, skillContent, 'utf-8');
|
|
356
|
+
this._log(`Wrote workspace skill to ${skillFile}`);
|
|
357
|
+
|
|
358
|
+
return { cmd, skillFile };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* MCP mode (default): spawn MCP server subprocess for workspace tools.
|
|
363
|
+
*/
|
|
364
|
+
_buildMcpCmd(cmd, channelName) {
|
|
255
365
|
// Mode-dependent permission and tool flags
|
|
256
366
|
const pfx = 'mcp__openagents-workspace__';
|
|
257
367
|
const mcpTools = [
|
|
@@ -284,23 +394,16 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
284
394
|
mcpWriteTools.push(`${pfx}tunnel_expose`, `${pfx}tunnel_close`);
|
|
285
395
|
}
|
|
286
396
|
|
|
287
|
-
|
|
397
|
+
// Todos & Timers (always enabled)
|
|
398
|
+
mcpTools.push(`${pfx}workspace_get_todos`, `${pfx}workspace_list_timers`);
|
|
399
|
+
mcpWriteTools.push(`${pfx}workspace_put_todos`, `${pfx}workspace_create_timer`, `${pfx}workspace_cancel_timer`);
|
|
400
|
+
|
|
288
401
|
if (this._mode === 'plan') {
|
|
289
402
|
cmd.push('--permission-mode', 'plan');
|
|
290
|
-
|
|
403
|
+
cmd.push('--allowedTools', ...mcpTools, 'Read', 'Glob', 'Grep');
|
|
291
404
|
} else {
|
|
292
405
|
cmd.push('--dangerously-skip-permissions');
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
cmd.push('--append-system-prompt', systemPrompt);
|
|
297
|
-
cmd.push('--allowedTools', ...allowed);
|
|
298
|
-
cmd.push('--disallowedTools', 'AskUserQuestion');
|
|
299
|
-
|
|
300
|
-
// Resume existing conversation (skipped on retry after stale session)
|
|
301
|
-
const sessionId = this._channelSessions[channelName];
|
|
302
|
-
if (sessionId && !skipResume) {
|
|
303
|
-
cmd.push('--resume', sessionId);
|
|
406
|
+
cmd.push('--allowedTools', ...mcpTools, ...mcpWriteTools, 'Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep');
|
|
304
407
|
}
|
|
305
408
|
|
|
306
409
|
// MCP config for workspace tools
|
|
@@ -314,18 +417,13 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
314
417
|
if (this.disabledModules.has('files')) mcpArgs.push('--disable-files');
|
|
315
418
|
if (this.disabledModules.has('browser')) mcpArgs.push('--disable-browser');
|
|
316
419
|
|
|
317
|
-
// Resolve the MCP server entry point
|
|
318
|
-
// very package — it's guaranteed to exist whenever claude.js is executing,
|
|
319
|
-
// so it never falls through to a broken PATH lookup. If Claude Code can't
|
|
320
|
-
// spawn the MCP server, it silently hides every workspace tool and the
|
|
321
|
-
// agent reports "workspace_read_file isn't in my tool set".
|
|
420
|
+
// Resolve the MCP server entry point
|
|
322
421
|
let mcpCommand = this._findNodeBin();
|
|
323
422
|
let mcpFinalArgs = mcpArgs;
|
|
324
423
|
const siblingBin = path.resolve(__dirname, '..', '..', 'bin', 'agent-connector.js');
|
|
325
424
|
if (fs.existsSync(siblingBin)) {
|
|
326
425
|
mcpFinalArgs = [siblingBin, ...mcpArgs];
|
|
327
426
|
} else {
|
|
328
|
-
// Fallback: search installed locations (older layouts, global installs)
|
|
329
427
|
let oaBin = null;
|
|
330
428
|
const home3 = os.homedir();
|
|
331
429
|
const oaExt = IS_WINDOWS ? '.cmd' : '';
|
|
@@ -433,26 +531,43 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
433
531
|
let mcpConfigFile = null;
|
|
434
532
|
let cmd;
|
|
435
533
|
|
|
436
|
-
// Clean env: strip
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
534
|
+
// Clean env: strip CLAUDE_* / AI_AGENT variables that make the spawned
|
|
535
|
+
// `claude` think it's running under an SDK harness (org-scoped auth
|
|
536
|
+
// path → 403). But preserve config vars the child needs for cloud
|
|
537
|
+
// provider auth (Vertex, Bedrock) and model selection.
|
|
538
|
+
const CLAUDE_ENV_KEEP = new Set([
|
|
539
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
540
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
541
|
+
'CLAUDE_MODEL',
|
|
542
|
+
'CLAUDE_API_KEY',
|
|
543
|
+
'CLAUDE_CODE_MAX_TURNS',
|
|
544
|
+
]);
|
|
443
545
|
const cleanEnv = { ...(this.agentEnv || process.env) };
|
|
444
546
|
for (const k of Object.keys(cleanEnv)) {
|
|
445
|
-
if (k.startsWith('CLAUDE_') || k === 'CLAUDECODE' || k === 'AI_AGENT') {
|
|
547
|
+
if ((k.startsWith('CLAUDE_') && !CLAUDE_ENV_KEEP.has(k)) || k === 'CLAUDECODE' || k === 'AI_AGENT') {
|
|
446
548
|
delete cleanEnv[k];
|
|
447
549
|
}
|
|
448
550
|
}
|
|
449
551
|
|
|
450
552
|
// Run up to 2 attempts: first with session resume, then fresh if stale session detected
|
|
451
553
|
let _shouldRetry = false;
|
|
554
|
+
let effectiveContent = content;
|
|
452
555
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
453
556
|
if (mcpConfigFile) { try { fs.unlinkSync(mcpConfigFile); } catch {} mcpConfigFile = null; }
|
|
557
|
+
|
|
558
|
+
// On the retry pass after a stale --resume, the spawned `claude`
|
|
559
|
+
// starts a brand-new session with no memory of prior turns. Replay
|
|
560
|
+
// the channel's recent chat history so the agent at least has a
|
|
561
|
+
// recap instead of saying "I don't see any previous messages."
|
|
562
|
+
if (attempt > 0) {
|
|
563
|
+
try {
|
|
564
|
+
const recap = await this._buildChannelRecap(msgChannel, content);
|
|
565
|
+
if (recap) effectiveContent = `${recap}\n\n---\n\n${content}`;
|
|
566
|
+
} catch {}
|
|
567
|
+
}
|
|
568
|
+
|
|
454
569
|
try {
|
|
455
|
-
const built = this._buildClaudeCmd(
|
|
570
|
+
const built = this._buildClaudeCmd(effectiveContent, msgChannel, { skipResume: attempt > 0 });
|
|
456
571
|
cmd = built.cmd;
|
|
457
572
|
mcpConfigFile = built.mcpConfigFile;
|
|
458
573
|
} catch (e) {
|
|
@@ -483,6 +598,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
483
598
|
const lastResponseText = [];
|
|
484
599
|
let hasToolUseSinceLastText = false;
|
|
485
600
|
let postedThinking = false;
|
|
601
|
+
let everPostedAnything = false;
|
|
486
602
|
let stderrBuf = '';
|
|
487
603
|
let lineBuffer = '';
|
|
488
604
|
let _pendingLines = Promise.resolve(); // chain of in-flight processLine calls
|
|
@@ -542,6 +658,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
542
658
|
}
|
|
543
659
|
lastResponseText.push(block.text.trim());
|
|
544
660
|
postedThinking = true;
|
|
661
|
+
everPostedAnything = true;
|
|
545
662
|
// Stream text in real-time as "thinking" (same as Python adapter)
|
|
546
663
|
try { await this.sendThinking(msgChannel, block.text.trim()); } catch {}
|
|
547
664
|
} else if (block.type === 'tool_use') {
|
|
@@ -549,6 +666,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
549
666
|
postedThinking = false;
|
|
550
667
|
lastResponseText.length = 0;
|
|
551
668
|
const toolName = block.name || '';
|
|
669
|
+
|
|
552
670
|
// Format tool input as readable text
|
|
553
671
|
let inputPreview = '';
|
|
554
672
|
if (block.input && typeof block.input === 'object') {
|
|
@@ -565,6 +683,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
565
683
|
inputPreview = String(block.input || '').slice(0, 150);
|
|
566
684
|
}
|
|
567
685
|
await this.sendStatus(msgChannel, `${toolName} › ${inputPreview}`);
|
|
686
|
+
everPostedAnything = true;
|
|
568
687
|
}
|
|
569
688
|
}
|
|
570
689
|
} else if (eventType === 'result') {
|
|
@@ -600,6 +719,10 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
600
719
|
}
|
|
601
720
|
|
|
602
721
|
delete this._channelProcesses[msgChannel];
|
|
722
|
+
|
|
723
|
+
// Clean up stale todos: mark pending/in_progress as cancelled
|
|
724
|
+
try { await this.cleanupTodos(msgChannel); } catch {}
|
|
725
|
+
|
|
603
726
|
const stoppedByUser = this._stoppingChannels.has(msgChannel);
|
|
604
727
|
if (stoppedByUser) {
|
|
605
728
|
this._stoppingChannels.delete(msgChannel);
|
|
@@ -607,8 +730,8 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
607
730
|
return;
|
|
608
731
|
}
|
|
609
732
|
|
|
733
|
+
this._log(`CLI exited: code=${code}, lastResponseText=${lastResponseText.length} items, everPosted=${everPostedAnything}, hasSession=${!!this._channelSessions[msgChannel]}`);
|
|
610
734
|
if (code !== 0) {
|
|
611
|
-
this._log(`CLI exited with code ${code}`);
|
|
612
735
|
if (stderrBuf.trim()) {
|
|
613
736
|
this._log(`stderr: ${stderrBuf.trim().slice(0, 500)}`);
|
|
614
737
|
}
|
|
@@ -620,14 +743,16 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
620
743
|
try { await this.sendResponse(msgChannel, fullResponse); } catch {}
|
|
621
744
|
}
|
|
622
745
|
resolve(false); // done, no retry
|
|
623
|
-
} else if (
|
|
624
|
-
// No output +
|
|
746
|
+
} else if (this._channelSessions[msgChannel] && !everPostedAnything) {
|
|
747
|
+
// No output + had a resume session → likely stale session, retry fresh.
|
|
748
|
+
// Covers both error exits (code !== 0) and silent success (code === 0)
|
|
749
|
+
// where Claude produced no text output on a resumed conversation.
|
|
625
750
|
this._log(`Stale session detected for ${msgChannel}, clearing and retrying without resume`);
|
|
626
751
|
delete this._channelSessions[msgChannel];
|
|
627
752
|
this._saveSessions();
|
|
628
753
|
resolve(true); // retry=true
|
|
629
754
|
} else {
|
|
630
|
-
if (!
|
|
755
|
+
if (!everPostedAnything) {
|
|
631
756
|
try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
|
|
632
757
|
}
|
|
633
758
|
resolve(false);
|
|
@@ -205,9 +205,73 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
205
205
|
sections.push(s);
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
// Message history
|
|
209
|
+
sections.push(
|
|
210
|
+
'\n### Message History\n\n' +
|
|
211
|
+
'**Get recent messages in the current channel:**\n' +
|
|
212
|
+
`\`curl -s -H "${h}" "${baseUrl}/v1/events?network=${workspaceId}&channel=${channelName}&type=workspace.message&limit=20"\`\n\n` +
|
|
213
|
+
'**Get messages from a specific channel:**\n' +
|
|
214
|
+
`\`curl -s -H "${h}" "${baseUrl}/v1/events?network=${workspaceId}&channel=CHANNEL_NAME&type=workspace.message&limit=20"\`\n`
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// Post status update
|
|
218
|
+
if (!isPlan) {
|
|
219
|
+
sections.push(
|
|
220
|
+
'\n### Post Status Update\n\n' +
|
|
221
|
+
'Post a status/thinking message (visible in the workspace UI as an intermediate step):\n' +
|
|
222
|
+
`\`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
|
|
223
|
+
`${baseUrl}/v1/events -d '{"type":"workspace.message.posted",` +
|
|
224
|
+
`"source":"openagents:${agentName}","target":"channel/${channelName}",` +
|
|
225
|
+
`"payload":{"content":"YOUR_STATUS","message_type":"status"}}'\`\n`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// To-Dos (planning)
|
|
230
|
+
if (!isPlan) {
|
|
231
|
+
sections.push(
|
|
232
|
+
'\n### To-Do List (Planning)\n\n' +
|
|
233
|
+
'Create or update your to-do list to track progress. The entire list ' +
|
|
234
|
+
'is replaced each time (send the full list with current statuses).\n\n' +
|
|
235
|
+
'**Status values:** `pending`, `in_progress`, `completed`\n\n' +
|
|
236
|
+
'**Update your to-do list:**\n' +
|
|
237
|
+
`\`curl -s -X PUT -H "${h}" -H "Content-Type: application/json" ` +
|
|
238
|
+
`${baseUrl}/v1/todos -d '{"todos":[` +
|
|
239
|
+
`{"content":"First task","status":"in_progress"},` +
|
|
240
|
+
`{"content":"Second task","status":"pending"}` +
|
|
241
|
+
`],"network":"${workspaceId}","channel":"${channelName}",` +
|
|
242
|
+
`"source":"openagents:${agentName}"}'\`\n\n` +
|
|
243
|
+
'**Get your to-do list:**\n' +
|
|
244
|
+
`\`curl -s -H "${h}" "${baseUrl}/v1/todos?network=${workspaceId}&channel=${channelName}"\`\n\n` +
|
|
245
|
+
'Use to-dos to plan multi-step work. Update the list as you complete each step.\n' +
|
|
246
|
+
'You can assign items to other agents: `"assignee": "other-agent-name"`\n'
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Timers
|
|
251
|
+
if (!isPlan) {
|
|
252
|
+
sections.push(
|
|
253
|
+
'\n### Timers\n\n' +
|
|
254
|
+
'Set a timer that will send you a message after a delay, waking you up ' +
|
|
255
|
+
'to continue work. Use this instead of `sleep` — timers let you release ' +
|
|
256
|
+
'the session and get called back later.\n\n' +
|
|
257
|
+
'Use cases: check back on a deploy, retry after a rate limit, remind ' +
|
|
258
|
+
'yourself to follow up.\n\n' +
|
|
259
|
+
'**Create a timer:**\n' +
|
|
260
|
+
`\`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
|
|
261
|
+
`${baseUrl}/v1/timers -d '{"delay":300,"message":"Check the build",` +
|
|
262
|
+
`"network":"${workspaceId}","channel":"${channelName}",` +
|
|
263
|
+
`"source":"openagents:${agentName}"}'\`\n\n` +
|
|
264
|
+
'**List active timers:**\n' +
|
|
265
|
+
`\`curl -s -H "${h}" "${baseUrl}/v1/timers?network=${workspaceId}&channel=${channelName}"\`\n\n` +
|
|
266
|
+
'**Cancel a timer:**\n' +
|
|
267
|
+
`\`curl -s -X DELETE -H "${h}" ${baseUrl}/v1/timers/TIMER_ID\`\n`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
208
271
|
// Discovery
|
|
209
272
|
sections.push(
|
|
210
|
-
'\n### Discover Agents\n' +
|
|
273
|
+
'\n### Discover Agents\n\n' +
|
|
274
|
+
'**List all agents in the workspace (with status and role):**\n' +
|
|
211
275
|
`\`curl -s -H "${h}" ${baseUrl}/v1/discover?network=${workspaceId}\`\n`
|
|
212
276
|
);
|
|
213
277
|
|
|
@@ -321,6 +385,37 @@ function buildOpenCodeSkillMd({ endpoint, workspaceId, token, agentName, channel
|
|
|
321
385
|
return frontmatter + identity + api;
|
|
322
386
|
}
|
|
323
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Build a SKILL.md file for Claude Code's skill auto-discovery.
|
|
390
|
+
*
|
|
391
|
+
* When tool_mode is 'skills', the Claude adapter writes this file instead
|
|
392
|
+
* of spawning an MCP server. Claude Code discovers the skill via its
|
|
393
|
+
* .claude/skills/ directory and uses Bash + curl to call workspace APIs.
|
|
394
|
+
*/
|
|
395
|
+
function buildClaudeSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules }) {
|
|
396
|
+
const api = buildApiSkillsPrompt({
|
|
397
|
+
endpoint, workspaceId, token, agentName,
|
|
398
|
+
channelName: channelName || 'general',
|
|
399
|
+
disabledModules,
|
|
400
|
+
mode: 'execute',
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute');
|
|
404
|
+
const collab = buildCollaborationPrompt();
|
|
405
|
+
|
|
406
|
+
const frontmatter =
|
|
407
|
+
'---\n' +
|
|
408
|
+
'name: openagents-workspace\n' +
|
|
409
|
+
'description: |\n' +
|
|
410
|
+
' OpenAgents Workspace collaboration tools — shared files, browser,\n' +
|
|
411
|
+
' and multi-agent coordination. Use when: sharing files or reports,\n' +
|
|
412
|
+
' browsing websites, reading shared files, checking workspace agents,\n' +
|
|
413
|
+
' or collaborating with other agents via @mentions.\n' +
|
|
414
|
+
'---\n\n';
|
|
415
|
+
|
|
416
|
+
return frontmatter + identity + '\n' + collab + '\n' + api;
|
|
417
|
+
}
|
|
418
|
+
|
|
324
419
|
module.exports = {
|
|
325
420
|
buildWorkspaceIdentity,
|
|
326
421
|
buildCollaborationPrompt,
|
|
@@ -331,4 +426,5 @@ module.exports = {
|
|
|
331
426
|
buildOpenclawSkillMd,
|
|
332
427
|
buildOpenCodeSystemPrompt,
|
|
333
428
|
buildOpenCodeSkillMd,
|
|
429
|
+
buildClaudeSkillMd,
|
|
334
430
|
};
|
package/src/cli.js
CHANGED
|
@@ -440,6 +440,49 @@ async function cmdEnv(connector, flags, positional) {
|
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
async function cmdToolMode(connector, _flags, positional) {
|
|
444
|
+
const agentName = positional[0];
|
|
445
|
+
const mode = positional[1];
|
|
446
|
+
|
|
447
|
+
if (!agentName) {
|
|
448
|
+
// Show tool mode for all agents
|
|
449
|
+
const agents = connector.config.getAgents();
|
|
450
|
+
if (agents.length === 0) {
|
|
451
|
+
print('No agents configured');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
for (const a of agents) {
|
|
455
|
+
print(` ${a.name}: ${a.tool_mode || 'mcp'}`);
|
|
456
|
+
}
|
|
457
|
+
print('\nUsage: agn tool-mode <agent> <mcp|skills>');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!mode) {
|
|
462
|
+
// Show tool mode for specific agent
|
|
463
|
+
const agent = connector.config.getAgent(agentName);
|
|
464
|
+
if (!agent) { print(`Agent '${agentName}' not found`); process.exitCode = 1; return; }
|
|
465
|
+
print(`${agentName}: ${agent.tool_mode || 'mcp'}`);
|
|
466
|
+
print('\nUsage: agn tool-mode <agent> <mcp|skills>');
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (mode !== 'mcp' && mode !== 'skills') {
|
|
471
|
+
print(`Invalid mode: ${mode}. Must be 'mcp' or 'skills'.`);
|
|
472
|
+
process.exitCode = 1;
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
connector.config.updateAgent(agentName, { tool_mode: mode });
|
|
477
|
+
try { connector.sendDaemonCommand('reload'); } catch {}
|
|
478
|
+
print(`Set tool mode for ${agentName} to '${mode}'`);
|
|
479
|
+
if (mode === 'skills') {
|
|
480
|
+
print('Agent will use SKILL.md (Bash + curl) instead of MCP server for workspace tools.');
|
|
481
|
+
} else {
|
|
482
|
+
print('Agent will use MCP server for workspace tools (default).');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
443
486
|
async function cmdTestLLM(connector, _flags, positional) {
|
|
444
487
|
const type = positional[0];
|
|
445
488
|
if (!type) { print('Usage: agn test-llm <type>'); return; }
|
|
@@ -504,6 +547,7 @@ Commands:
|
|
|
504
547
|
connect <agent> <token> Connect agent to workspace
|
|
505
548
|
disconnect <agent> Disconnect agent from workspace
|
|
506
549
|
env <type> [--set K=V] View/set env vars for agent type
|
|
550
|
+
tool-mode [agent] [mode] View/set tool mode (mcp or skills)
|
|
507
551
|
autostart [--disable] Enable/disable auto-start on login
|
|
508
552
|
test-llm <type> Test LLM connection
|
|
509
553
|
logs [agent] [--lines N] View daemon logs
|
|
@@ -589,6 +633,7 @@ async function main() {
|
|
|
589
633
|
autostart: () => cmdAutostart(connector, flags),
|
|
590
634
|
workspace: () => cmdWorkspace(connector, flags, positional),
|
|
591
635
|
env: () => cmdEnv(connector, flags, positional),
|
|
636
|
+
'tool-mode': () => cmdToolMode(connector, flags, positional),
|
|
592
637
|
'test-llm': () => cmdTestLLM(connector, flags, positional),
|
|
593
638
|
update: () => cmdUpdate(),
|
|
594
639
|
'mcp-server': () => {
|
package/src/daemon.js
CHANGED
|
@@ -433,6 +433,7 @@ class Daemon {
|
|
|
433
433
|
disabledModules: new Set(),
|
|
434
434
|
agentEnv: this._buildAgentEnv(agentCfg),
|
|
435
435
|
workingDir: agentCfg.path || undefined,
|
|
436
|
+
toolMode: agentCfg.tool_mode || 'mcp',
|
|
436
437
|
});
|
|
437
438
|
} catch (e) {
|
|
438
439
|
this._log(`${name} failed to create ${agentType} adapter: ${e.message}`);
|
package/src/mcp-server.js
CHANGED
|
@@ -235,6 +235,72 @@ function buildToolDefs(disabledModules) {
|
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// -- Todos & Timers (always enabled) --
|
|
239
|
+
tools.push(
|
|
240
|
+
{
|
|
241
|
+
name: 'workspace_put_todos',
|
|
242
|
+
description: 'Update your to-do list. Replaces the entire list each time (send full list with current statuses). Channel is auto-resolved.',
|
|
243
|
+
inputSchema: {
|
|
244
|
+
type: 'object',
|
|
245
|
+
properties: {
|
|
246
|
+
todos: {
|
|
247
|
+
type: 'array',
|
|
248
|
+
items: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
content: { type: 'string', description: 'Task description' },
|
|
252
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'completed'], description: 'Task status' },
|
|
253
|
+
assignee: { type: 'string', description: 'Agent name to assign to (defaults to self)' },
|
|
254
|
+
},
|
|
255
|
+
required: ['content', 'status'],
|
|
256
|
+
},
|
|
257
|
+
description: 'Full to-do list with current statuses',
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
required: ['todos'],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'workspace_get_todos',
|
|
265
|
+
description: 'Get to-do items for the current channel. Use all=true to see all agents\' todos.',
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: 'object',
|
|
268
|
+
properties: {
|
|
269
|
+
agent: { type: 'string', description: 'Filter by agent name' },
|
|
270
|
+
all: { type: 'boolean', description: 'Get all agents\' todos (default: own only)' },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'workspace_create_timer',
|
|
276
|
+
description: 'Set a timer that posts a message to the channel after the specified delay.',
|
|
277
|
+
inputSchema: {
|
|
278
|
+
type: 'object',
|
|
279
|
+
properties: {
|
|
280
|
+
delay: { type: 'integer', description: 'Seconds until the timer fires (1-86400)' },
|
|
281
|
+
message: { type: 'string', description: 'Message to post when the timer fires' },
|
|
282
|
+
},
|
|
283
|
+
required: ['delay', 'message'],
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'workspace_list_timers',
|
|
288
|
+
description: 'List active timers in the current channel.',
|
|
289
|
+
inputSchema: { type: 'object', properties: {} },
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'workspace_cancel_timer',
|
|
293
|
+
description: 'Cancel an active timer by its ID.',
|
|
294
|
+
inputSchema: {
|
|
295
|
+
type: 'object',
|
|
296
|
+
properties: {
|
|
297
|
+
timer_id: { type: 'string', description: 'Timer ID to cancel' },
|
|
298
|
+
},
|
|
299
|
+
required: ['timer_id'],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
|
|
238
304
|
return tools;
|
|
239
305
|
}
|
|
240
306
|
|
|
@@ -596,6 +662,56 @@ class McpServer {
|
|
|
596
662
|
return text(lines.join('\n'));
|
|
597
663
|
}
|
|
598
664
|
|
|
665
|
+
// ── Todos & Timers ──
|
|
666
|
+
|
|
667
|
+
case 'workspace_put_todos': {
|
|
668
|
+
await this.ws.putTodos(this.workspaceId, this.channelName, this.token, args.todos, {
|
|
669
|
+
source: `openagents:${this.agentName}`,
|
|
670
|
+
});
|
|
671
|
+
const summary = (args.todos || []).map((t) => {
|
|
672
|
+
const icon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[~]' : '[ ]';
|
|
673
|
+
return `${icon} ${t.content}${t.assignee ? ` → ${t.assignee}` : ''}`;
|
|
674
|
+
}).join('\n');
|
|
675
|
+
return text(`Todos updated:\n${summary}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
case 'workspace_get_todos': {
|
|
679
|
+
const data = await this.ws.getTodos(this.workspaceId, this.channelName, this.token, {
|
|
680
|
+
agent: args.agent, all: args.all,
|
|
681
|
+
});
|
|
682
|
+
const todos = (data && data.todos) || [];
|
|
683
|
+
if (!todos.length) return text('No todos.');
|
|
684
|
+
const lines = todos.map((t) => {
|
|
685
|
+
const icon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[~]' : '[ ]';
|
|
686
|
+
return `${icon} ${t.content} (${t.assignee || 'unassigned'})`;
|
|
687
|
+
});
|
|
688
|
+
return text(lines.join('\n'));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
case 'workspace_create_timer': {
|
|
692
|
+
const result = await this.ws.createTimer(
|
|
693
|
+
this.workspaceId, this.channelName, this.token,
|
|
694
|
+
args.delay, args.message,
|
|
695
|
+
{ source: `openagents:${this.agentName}` },
|
|
696
|
+
);
|
|
697
|
+
return text(`Timer set: "${args.message}" fires in ${args.delay}s (id: ${result.id})`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
case 'workspace_list_timers': {
|
|
701
|
+
const data = await this.ws.listTimers(this.workspaceId, this.channelName, this.token);
|
|
702
|
+
const timers = (data && data.timers) || [];
|
|
703
|
+
if (!timers.length) return text('No active timers.');
|
|
704
|
+
const lines = timers.map((t) =>
|
|
705
|
+
`- ${t.id}: "${t.message}" fires at ${t.fires_at} (by ${t.created_by})`
|
|
706
|
+
);
|
|
707
|
+
return text(lines.join('\n'));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
case 'workspace_cancel_timer': {
|
|
711
|
+
await this.ws.cancelTimer(this.workspaceId, this.token, args.timer_id);
|
|
712
|
+
return text(`Timer cancelled: ${args.timer_id}`);
|
|
713
|
+
}
|
|
714
|
+
|
|
599
715
|
default:
|
|
600
716
|
throw new Error(`Unknown tool: ${name}`);
|
|
601
717
|
}
|
package/src/workspace-client.js
CHANGED
|
@@ -191,6 +191,32 @@ class WorkspaceClient {
|
|
|
191
191
|
return events.map((e) => this._eventToMessage(e));
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Fetch the most recent N messages in a channel, returned oldest-to-newest.
|
|
196
|
+
* Used by adapters to rebuild context for a fresh Claude Code session
|
|
197
|
+
* when --resume of the previous session fails (the channel's chat history
|
|
198
|
+
* is the only thing that survives a session-storage rotation).
|
|
199
|
+
*/
|
|
200
|
+
async getRecentMessages(workspaceId, channelName, token, limit = 30) {
|
|
201
|
+
try {
|
|
202
|
+
const params = new URLSearchParams({
|
|
203
|
+
network: workspaceId,
|
|
204
|
+
channel: channelName,
|
|
205
|
+
type: 'workspace.message',
|
|
206
|
+
sort: 'desc',
|
|
207
|
+
limit: String(limit),
|
|
208
|
+
});
|
|
209
|
+
const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
|
|
210
|
+
const result = data.data || data;
|
|
211
|
+
const events = (result && result.events) || [];
|
|
212
|
+
// Server returned newest-first; reverse so the caller can present them
|
|
213
|
+
// in chronological order without further fiddling.
|
|
214
|
+
return events.slice().reverse().map((e) => this._eventToMessage(e));
|
|
215
|
+
} catch {
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
194
220
|
/**
|
|
195
221
|
* Fetch the latest workspace.message.posted event id (head cursor).
|
|
196
222
|
* Used by adapters to skip past existing events on join in O(1) instead
|
|
@@ -267,6 +293,11 @@ class WorkspaceClient {
|
|
|
267
293
|
// Legacy server (no target_agents): broadcast for compat
|
|
268
294
|
messages.push(this._eventToMessage(e));
|
|
269
295
|
}
|
|
296
|
+
} else if (source.startsWith('system:')) {
|
|
297
|
+
// System messages (timers, notifications): pick up if targeted
|
|
298
|
+
if (hasTargetList && targetAgents.includes(agentName)) {
|
|
299
|
+
messages.push(this._eventToMessage(e));
|
|
300
|
+
}
|
|
270
301
|
} else if (source.startsWith('openagents:')) {
|
|
271
302
|
// Agent messages: only pick up if explicitly listed
|
|
272
303
|
if (hasTargetList && targetAgents.includes(agentName)) {
|
|
@@ -498,6 +529,53 @@ class WorkspaceClient {
|
|
|
498
529
|
return data.data || data;
|
|
499
530
|
}
|
|
500
531
|
|
|
532
|
+
// ── Todos & Timers ──
|
|
533
|
+
|
|
534
|
+
async putTodos(workspaceId, channelName, token, todos, { source } = {}) {
|
|
535
|
+
const body = {
|
|
536
|
+
todos,
|
|
537
|
+
network: workspaceId,
|
|
538
|
+
channel: channelName,
|
|
539
|
+
source: source || 'openagents:unknown',
|
|
540
|
+
};
|
|
541
|
+
const data = await this._put('/v1/todos', body, this._wsHeaders(token));
|
|
542
|
+
return data.data || data;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
async getTodos(workspaceId, channelName, token, { agent, all } = {}) {
|
|
546
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
547
|
+
if (channelName) params.set('channel', channelName);
|
|
548
|
+
if (agent) params.set('agent', agent);
|
|
549
|
+
if (all) params.set('all', 'true');
|
|
550
|
+
const data = await this._get(`/v1/todos?${params}`, this._wsHeaders(token));
|
|
551
|
+
return data.data || data;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async createTimer(workspaceId, channelName, token, delay, message, { source } = {}) {
|
|
555
|
+
const body = {
|
|
556
|
+
delay,
|
|
557
|
+
message,
|
|
558
|
+
network: workspaceId,
|
|
559
|
+
channel: channelName,
|
|
560
|
+
source: source || 'openagents:unknown',
|
|
561
|
+
};
|
|
562
|
+
const data = await this._post('/v1/timers', body, this._wsHeaders(token));
|
|
563
|
+
return data.data || data;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async listTimers(workspaceId, channelName, token) {
|
|
567
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
568
|
+
if (channelName) params.set('channel', channelName);
|
|
569
|
+
const data = await this._get(`/v1/timers?${params}`, this._wsHeaders(token));
|
|
570
|
+
return data.data || data;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async cancelTimer(workspaceId, token, timerId, network) {
|
|
574
|
+
const params = network ? `?network=${network}` : '';
|
|
575
|
+
const data = await this._delete(`/v1/timers/${timerId}`, this._wsHeaders(token));
|
|
576
|
+
return data.data || data;
|
|
577
|
+
}
|
|
578
|
+
|
|
501
579
|
// ── Internal helpers ──
|
|
502
580
|
|
|
503
581
|
/**
|
|
@@ -643,6 +721,48 @@ class WorkspaceClient {
|
|
|
643
721
|
});
|
|
644
722
|
}
|
|
645
723
|
|
|
724
|
+
_put(urlPath, body, headers = {}, timeout = 30000) {
|
|
725
|
+
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
726
|
+
const jsonBody = JSON.stringify(body);
|
|
727
|
+
const fullUrl = this.endpoint + urlPath;
|
|
728
|
+
|
|
729
|
+
return new Promise((resolve, reject) => {
|
|
730
|
+
const parsedUrl = new URL(fullUrl);
|
|
731
|
+
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
732
|
+
|
|
733
|
+
const req = transport.request(fullUrl, {
|
|
734
|
+
method: 'PUT',
|
|
735
|
+
headers: { ...headers, 'Content-Length': Buffer.byteLength(jsonBody) },
|
|
736
|
+
timeout,
|
|
737
|
+
}, (res) => {
|
|
738
|
+
let data = '';
|
|
739
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
740
|
+
res.on('end', () => {
|
|
741
|
+
try {
|
|
742
|
+
const parsed = JSON.parse(data);
|
|
743
|
+
if (res.statusCode >= 400) {
|
|
744
|
+
const msg = parsed.message || `HTTP ${res.statusCode}`;
|
|
745
|
+
if (typeof msg === 'string' && msg.toLowerCase().includes('session_revoked')) {
|
|
746
|
+
reject(new SessionRevokedError(msg));
|
|
747
|
+
} else {
|
|
748
|
+
reject(new Error(msg));
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
resolve(parsed);
|
|
752
|
+
}
|
|
753
|
+
} catch {
|
|
754
|
+
reject(new Error(`Invalid response: ${data.slice(0, 200)}`));
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
req.on('error', reject);
|
|
760
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
761
|
+
req.write(jsonBody);
|
|
762
|
+
req.end();
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
646
766
|
_patch(urlPath, body, headers = {}, timeout = 15000) {
|
|
647
767
|
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
648
768
|
const jsonBody = JSON.stringify(body);
|