@openagents-org/agent-launcher 0.2.119 → 0.2.120
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/icons/kimi.svg +11 -0
- package/package.json +1 -1
- package/registry.json +59 -0
- package/src/adapters/base.js +92 -4
- package/src/adapters/claude.js +96 -8
- package/src/adapters/index.js +4 -1
- package/src/adapters/kimi.js +66 -0
- package/src/adapters/llm-direct.js +44 -11
- package/src/adapters/workspace-prompt.js +32 -1
- package/src/daemon.js +12 -0
- package/src/installer.js +55 -1
- package/src/mcp-server.js +68 -0
- package/src/utils.js +10 -7
- package/src/workspace-client.js +34 -4
package/icons/kimi.svg
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="kimi-g" x1="4" y1="4" x2="20" y2="20" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop stop-color="#141821"/>
|
|
5
|
+
<stop offset="0.52" stop-color="#2f6df6"/>
|
|
6
|
+
<stop offset="1" stop-color="#49d2ff"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
<rect x="2" y="2" width="20" height="20" rx="6" fill="url(#kimi-g)"/>
|
|
10
|
+
<path d="M7 6.5h2.6v4.1l4.2-4.1H17l-4.7 4.7 5 6.3h-3.2l-3.6-4.7-.9.9v3.8H7z" fill="#fff"/>
|
|
11
|
+
</svg>
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -557,6 +557,65 @@
|
|
|
557
557
|
"windows": "pip install sweagent"
|
|
558
558
|
}
|
|
559
559
|
},
|
|
560
|
+
{
|
|
561
|
+
"name": "kimi",
|
|
562
|
+
"label": "Kimi",
|
|
563
|
+
"description": "Kimi agent powered by Moonshot AI, OpenAI-compatible API.",
|
|
564
|
+
"homepage": "https://platform.moonshot.ai",
|
|
565
|
+
"tags": [
|
|
566
|
+
"coding",
|
|
567
|
+
"moonshot",
|
|
568
|
+
"open-source"
|
|
569
|
+
],
|
|
570
|
+
"featured": true,
|
|
571
|
+
"order": 7,
|
|
572
|
+
"support": { "install": true, "workspace": true, "collaboration": true },
|
|
573
|
+
"builtin": true,
|
|
574
|
+
"install": {
|
|
575
|
+
"binary": "kimi",
|
|
576
|
+
"api_only": true,
|
|
577
|
+
"macos": "echo 'Kimi uses direct API mode — no binary install needed'",
|
|
578
|
+
"linux": "echo 'Kimi uses direct API mode — no binary install needed'",
|
|
579
|
+
"windows": "echo 'Kimi uses direct API mode — no binary install needed'"
|
|
580
|
+
},
|
|
581
|
+
"adapter": {
|
|
582
|
+
"module": "openagents.adapters.kimi",
|
|
583
|
+
"class": "KimiAdapter"
|
|
584
|
+
},
|
|
585
|
+
"launch": {
|
|
586
|
+
"args": []
|
|
587
|
+
},
|
|
588
|
+
"env_config": [
|
|
589
|
+
{
|
|
590
|
+
"name": "KIMI_API_KEY",
|
|
591
|
+
"description": "Moonshot/Kimi API key (also accepts MOONSHOT_API_KEY env var)",
|
|
592
|
+
"required": true,
|
|
593
|
+
"password": true
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
"name": "KIMI_BASE_URL",
|
|
597
|
+
"description": "Kimi API base URL (OpenAI-compatible endpoint)",
|
|
598
|
+
"required": false,
|
|
599
|
+
"default": "https://api.moonshot.ai/v1",
|
|
600
|
+
"placeholder": "https://api.moonshot.ai/v1"
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
"name": "KIMI_MODEL",
|
|
604
|
+
"description": "Kimi model name",
|
|
605
|
+
"required": false,
|
|
606
|
+
"default": "kimi-k2.6",
|
|
607
|
+
"placeholder": "kimi-k2.6"
|
|
608
|
+
}
|
|
609
|
+
],
|
|
610
|
+
"check_ready": {
|
|
611
|
+
"env_vars": [
|
|
612
|
+
"KIMI_API_KEY",
|
|
613
|
+
"MOONSHOT_API_KEY"
|
|
614
|
+
],
|
|
615
|
+
"saved_env_key": "KIMI_API_KEY",
|
|
616
|
+
"not_ready_message": "No API key — press e to configure"
|
|
617
|
+
}
|
|
618
|
+
},
|
|
560
619
|
{
|
|
561
620
|
"name": "hermes",
|
|
562
621
|
"label": "Hermes Agent",
|
package/src/adapters/base.js
CHANGED
|
@@ -52,6 +52,11 @@ class BaseAdapter {
|
|
|
52
52
|
// Per-channel task tracking for parallel execution
|
|
53
53
|
this._channelBusy = new Set();
|
|
54
54
|
this._channelQueues = {};
|
|
55
|
+
// Wall-clock timestamp of adapter init, used by the `status` control
|
|
56
|
+
// action to report uptime back to the channel. Reset on reinstantiation
|
|
57
|
+
// (e.g. after a `restart` IPC bounce) so uptime tracks "time since last
|
|
58
|
+
// restart" rather than the long-running daemon's process uptime.
|
|
59
|
+
this._startedAt = Date.now();
|
|
55
60
|
this._log = (msg) => {
|
|
56
61
|
const ts = new Date().toISOString();
|
|
57
62
|
console.log(`${ts} INFO adapter [${this.agentName}]: ${msg}`);
|
|
@@ -79,8 +84,12 @@ class BaseAdapter {
|
|
|
79
84
|
this._log(`Warning: join failed: ${e.message} \nStack: ${e.stack}`);
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
// Fast-path operations (control-event cursor + heartbeat + control poll)
|
|
88
|
+
// run BEFORE the message-cursor advance. Even though _skipExistingEvents
|
|
89
|
+
// is fast on a healthy backend, we don't want slash commands gated on
|
|
90
|
+
// its success — keeping these paths independent makes /restart and
|
|
91
|
+
// /status responsive immediately after join.
|
|
92
|
+
await this._skipExistingControlEvents();
|
|
84
93
|
const heartbeatInterval = setInterval(() => this._heartbeat(), 30000);
|
|
85
94
|
const controlPoller = this._controlPollerLoop();
|
|
86
95
|
|
|
@@ -89,6 +98,8 @@ class BaseAdapter {
|
|
|
89
98
|
try { await this._heartbeat(); } catch (e) {
|
|
90
99
|
this._log(`Heartbeat failed (non-fatal): ${e.message}`);
|
|
91
100
|
}
|
|
101
|
+
// Slow path: only the message-poll loop waits for this.
|
|
102
|
+
await this._skipExistingEvents();
|
|
92
103
|
this._log('Starting poll loop...');
|
|
93
104
|
await this._pollLoop();
|
|
94
105
|
} finally {
|
|
@@ -144,6 +155,26 @@ class BaseAdapter {
|
|
|
144
155
|
// Control polling
|
|
145
156
|
// ------------------------------------------------------------------
|
|
146
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Advance `_lastControlId` past any pending control events for this agent
|
|
160
|
+
* so we don't re-process them after a respawn. Without this, /restart
|
|
161
|
+
* triggers a daemon bounce, the new adapter starts with _lastControlId=null,
|
|
162
|
+
* polls and re-finds the same /restart event, bounces again — restart loop.
|
|
163
|
+
*/
|
|
164
|
+
async _skipExistingControlEvents() {
|
|
165
|
+
try {
|
|
166
|
+
const events = await this.client.pollControl(
|
|
167
|
+
this.workspaceId, this.agentName, this.token,
|
|
168
|
+
{ after: null }
|
|
169
|
+
);
|
|
170
|
+
if (events.length > 0) {
|
|
171
|
+
// pollControl returns ascending-by-timestamp; take the latest.
|
|
172
|
+
this._lastControlId = events[events.length - 1].id;
|
|
173
|
+
this._log(`Skipped ${events.length} existing control event(s), cursor at ${this._lastControlId}`);
|
|
174
|
+
}
|
|
175
|
+
} catch {}
|
|
176
|
+
}
|
|
177
|
+
|
|
147
178
|
async _pollControl() {
|
|
148
179
|
try {
|
|
149
180
|
const events = await this.client.pollControl(
|
|
@@ -169,9 +200,66 @@ class BaseAdapter {
|
|
|
169
200
|
}
|
|
170
201
|
|
|
171
202
|
/**
|
|
172
|
-
* Handle adapter-specific control actions. Override in subclasses
|
|
203
|
+
* Handle adapter-specific control actions. Override in subclasses to add
|
|
204
|
+
* per-adapter actions (`stop`, `restart`, …); always call
|
|
205
|
+
* `await super._onControlAction(action, payload)` from the override for
|
|
206
|
+
* actions you don't recognize, so shared actions like `status` keep
|
|
207
|
+
* working uniformly across adapter types.
|
|
208
|
+
*/
|
|
209
|
+
async _onControlAction(action, payload) {
|
|
210
|
+
if (action === 'status') {
|
|
211
|
+
await this._postStatusReport(payload);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Post a chat message back to the requesting channel summarizing agent
|
|
217
|
+
* name, type, agent-launcher version, uptime, and network. Used by the
|
|
218
|
+
* `/status` slash command.
|
|
173
219
|
*/
|
|
174
|
-
async
|
|
220
|
+
async _postStatusReport(payload) {
|
|
221
|
+
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
222
|
+
if (!channel) return;
|
|
223
|
+
|
|
224
|
+
let pkgVersion = 'unknown';
|
|
225
|
+
try {
|
|
226
|
+
const path = require('path');
|
|
227
|
+
const pkg = require(path.join(__dirname, '..', '..', 'package.json'));
|
|
228
|
+
pkgVersion = pkg.version || 'unknown';
|
|
229
|
+
} catch {}
|
|
230
|
+
|
|
231
|
+
const uptimeMs = Math.max(0, Date.now() - this._startedAt);
|
|
232
|
+
const totalSec = Math.floor(uptimeMs / 1000);
|
|
233
|
+
const days = Math.floor(totalSec / 86400);
|
|
234
|
+
const hours = Math.floor((totalSec % 86400) / 3600);
|
|
235
|
+
const minutes = Math.floor((totalSec % 3600) / 60);
|
|
236
|
+
const seconds = totalSec % 60;
|
|
237
|
+
let uptime;
|
|
238
|
+
if (days > 0) uptime = `${days}d ${hours}h ${minutes}m`;
|
|
239
|
+
else if (hours > 0) uptime = `${hours}h ${minutes}m`;
|
|
240
|
+
else if (minutes > 0) uptime = `${minutes}m ${seconds}s`;
|
|
241
|
+
else uptime = `${seconds}s`;
|
|
242
|
+
|
|
243
|
+
const adapterType = this.agentType || 'unknown';
|
|
244
|
+
const content =
|
|
245
|
+
`**Agent status**\n` +
|
|
246
|
+
`- Name: \`${this.agentName}\` (${adapterType})\n` +
|
|
247
|
+
`- Version: agent-launcher \`${pkgVersion}\`\n` +
|
|
248
|
+
`- Uptime: ${uptime}\n` +
|
|
249
|
+
`- Network: \`${this.workspaceId}\``;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
|
|
253
|
+
senderType: 'agent',
|
|
254
|
+
senderName: this.agentName,
|
|
255
|
+
messageType: 'chat',
|
|
256
|
+
metadata: { agent_mode: this._mode },
|
|
257
|
+
sessionId: this._sessionId,
|
|
258
|
+
});
|
|
259
|
+
} catch (e) {
|
|
260
|
+
this._log(`Status: failed to post: ${e && e.message ? e.message : e}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
175
263
|
|
|
176
264
|
_hasActiveWork() {
|
|
177
265
|
return this._channelBusy.size > 0;
|
package/src/adapters/claude.js
CHANGED
|
@@ -65,10 +65,87 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
65
65
|
} catch {}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
async _onControlAction(action,
|
|
68
|
+
async _onControlAction(action, payload) {
|
|
69
69
|
if (action === 'stop') {
|
|
70
|
-
|
|
70
|
+
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
71
|
+
if (channel && this._channelProcesses[channel]) {
|
|
72
|
+
this._log(`Stopping process for channel=${channel}`);
|
|
73
|
+
this._stoppingChannels.add(channel);
|
|
74
|
+
const proc = this._channelProcesses[channel];
|
|
75
|
+
await this._stopProcess(proc);
|
|
76
|
+
delete this._channelProcesses[channel];
|
|
77
|
+
delete this._channelQueues[channel];
|
|
78
|
+
try {
|
|
79
|
+
await this.sendResponse(channel, 'Execution stopped by user.');
|
|
80
|
+
} catch {}
|
|
81
|
+
} else {
|
|
82
|
+
await this._stopAllProcesses('Execution stopped by user.');
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (action === 'restart') {
|
|
87
|
+
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
88
|
+
if (channel) {
|
|
89
|
+
// Kill in-flight subprocess + clear the per-channel session BEFORE
|
|
90
|
+
// asking the daemon to bounce us. The new adapter spawned after
|
|
91
|
+
// the bounce loads sessions from disk, so the cleared state must
|
|
92
|
+
// be persisted first.
|
|
93
|
+
const proc = this._channelProcesses[channel];
|
|
94
|
+
if (proc) {
|
|
95
|
+
try { await this._stopProcess(proc); } catch {}
|
|
96
|
+
delete this._channelProcesses[channel];
|
|
97
|
+
}
|
|
98
|
+
if (this._channelSessions[channel]) {
|
|
99
|
+
delete this._channelSessions[channel];
|
|
100
|
+
try { this._saveSessions(); } catch {}
|
|
101
|
+
this._log(`Restart: cleared session for channel=${channel}`);
|
|
102
|
+
} else {
|
|
103
|
+
this._log(`Restart: no session to clear for channel=${channel}`);
|
|
104
|
+
}
|
|
105
|
+
// Post the status BEFORE the bounce so the message lands while
|
|
106
|
+
// we're still online.
|
|
107
|
+
try {
|
|
108
|
+
await this.client.sendMessage(this.workspaceId, channel, this.token,
|
|
109
|
+
'Session restarted — next message starts fresh.',
|
|
110
|
+
{
|
|
111
|
+
senderType: 'agent',
|
|
112
|
+
senderName: this.agentName,
|
|
113
|
+
messageType: 'status',
|
|
114
|
+
metadata: { agent_mode: this._mode },
|
|
115
|
+
sessionId: this._sessionId,
|
|
116
|
+
});
|
|
117
|
+
} catch (e) {
|
|
118
|
+
this._log(`Restart: failed to post status: ${e && e.message ? e.message : e}`);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
// Defensive — no channel, clear everything before the bounce.
|
|
122
|
+
this._channelSessions = {};
|
|
123
|
+
try { this._saveSessions(); } catch {}
|
|
124
|
+
await this._stopAllProcesses('Execution stopped by user.');
|
|
125
|
+
this._log('Restart: cleared all sessions (no channel param)');
|
|
126
|
+
}
|
|
127
|
+
// Ask the daemon to bounce just THIS agent — true process-level
|
|
128
|
+
// restart. Daemon's command-file poller picks up `restart:<name>`
|
|
129
|
+
// within ~1s, calls restartAgent, our run() loop exits cleanly,
|
|
130
|
+
// and a fresh adapter is spawned with a new `_startedAt`. Sibling
|
|
131
|
+
// agents on the same daemon are untouched.
|
|
132
|
+
try {
|
|
133
|
+
const path = require('path');
|
|
134
|
+
const os = require('os');
|
|
135
|
+
const fs = require('fs');
|
|
136
|
+
const cmdFile = path.join(os.homedir(), '.openagents', 'daemon.cmd');
|
|
137
|
+
fs.writeFileSync(cmdFile, `restart:${this.agentName}\n`);
|
|
138
|
+
this._log(`Restart: requested daemon bounce for agent=${this.agentName}`);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
this._log(`Restart: failed to write daemon.cmd: ${e && e.message ? e.message : e}`);
|
|
141
|
+
// Fallback: reset uptime in-place so the next /status reflects
|
|
142
|
+
// SOMETHING changed even if the daemon bounce didn't happen.
|
|
143
|
+
this._startedAt = Date.now();
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
71
146
|
}
|
|
147
|
+
// Fall through to base for shared actions (status, etc.).
|
|
148
|
+
await super._onControlAction(action, payload);
|
|
72
149
|
}
|
|
73
150
|
|
|
74
151
|
/**
|
|
@@ -301,7 +378,8 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
301
378
|
'Use workspace_get_history to read previous messages.\n' +
|
|
302
379
|
'Use workspace_get_agents to see other agents.\n',
|
|
303
380
|
'Use the openagents-workspace skill (Bash + curl) for workspace operations:\n' +
|
|
304
|
-
'reading message history, discovering agents, sharing files,
|
|
381
|
+
'reading message history, discovering agents, sharing files, browsing,\n' +
|
|
382
|
+
'managing to-do lists, setting timers, and creating routines.\n' +
|
|
305
383
|
'Refer to the skill instructions for the exact curl commands.\n'
|
|
306
384
|
);
|
|
307
385
|
}
|
|
@@ -394,9 +472,9 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
394
472
|
mcpWriteTools.push(`${pfx}tunnel_expose`, `${pfx}tunnel_close`);
|
|
395
473
|
}
|
|
396
474
|
|
|
397
|
-
// Todos &
|
|
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`);
|
|
475
|
+
// Todos, Timers & Routines (always enabled)
|
|
476
|
+
mcpTools.push(`${pfx}workspace_get_todos`, `${pfx}workspace_list_timers`, `${pfx}workspace_list_routines`);
|
|
477
|
+
mcpWriteTools.push(`${pfx}workspace_put_todos`, `${pfx}workspace_create_timer`, `${pfx}workspace_cancel_timer`, `${pfx}workspace_create_routine`, `${pfx}workspace_cancel_routine`);
|
|
400
478
|
|
|
401
479
|
if (this._mode === 'plan') {
|
|
402
480
|
cmd.push('--permission-mode', 'plan');
|
|
@@ -739,10 +817,20 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
739
817
|
|
|
740
818
|
if (lastResponseText.length > 0) {
|
|
741
819
|
const fullResponse = lastResponseText.join('\n').trim();
|
|
742
|
-
|
|
820
|
+
// "Prompt is too long" means the resumed session's context
|
|
821
|
+
// exceeded the model's limit. Clear the session and retry
|
|
822
|
+
// fresh with a bounded recap instead of the full history.
|
|
823
|
+
if (/prompt is too long/i.test(fullResponse) && this._channelSessions[msgChannel]) {
|
|
824
|
+
this._log(`Prompt too long with resumed session for ${msgChannel}, clearing and retrying`);
|
|
825
|
+
delete this._channelSessions[msgChannel];
|
|
826
|
+
this._saveSessions();
|
|
827
|
+
resolve(true);
|
|
828
|
+
} else if (fullResponse) {
|
|
743
829
|
try { await this.sendResponse(msgChannel, fullResponse); } catch {}
|
|
830
|
+
resolve(false);
|
|
831
|
+
} else {
|
|
832
|
+
resolve(false);
|
|
744
833
|
}
|
|
745
|
-
resolve(false); // done, no retry
|
|
746
834
|
} else if (this._channelSessions[msgChannel] && !everPostedAnything) {
|
|
747
835
|
// No output + had a resume session → likely stale session, retry fresh.
|
|
748
836
|
// Covers both error exits (code !== 0) and silent success (code === 0)
|
package/src/adapters/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const NanoClawAdapter = require('./nanoclaw');
|
|
|
13
13
|
const CursorAdapter = require('./cursor');
|
|
14
14
|
const HermesAdapter = require('./hermes');
|
|
15
15
|
const GeminiAdapter = require('./gemini');
|
|
16
|
+
const KimiAdapter = require('./kimi');
|
|
16
17
|
|
|
17
18
|
const ADAPTER_MAP = {
|
|
18
19
|
openclaw: OpenClawAdapter,
|
|
@@ -23,11 +24,12 @@ const ADAPTER_MAP = {
|
|
|
23
24
|
cursor: CursorAdapter,
|
|
24
25
|
hermes: HermesAdapter,
|
|
25
26
|
gemini: GeminiAdapter,
|
|
27
|
+
kimi: KimiAdapter,
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Create an adapter instance for the given agent type.
|
|
30
|
-
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes)
|
|
32
|
+
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi)
|
|
31
33
|
* @param {object} opts - Adapter constructor options
|
|
32
34
|
* @returns {BaseAdapter}
|
|
33
35
|
*/
|
|
@@ -49,6 +51,7 @@ module.exports = {
|
|
|
49
51
|
CursorAdapter,
|
|
50
52
|
HermesAdapter,
|
|
51
53
|
GeminiAdapter,
|
|
54
|
+
KimiAdapter,
|
|
52
55
|
createAdapter,
|
|
53
56
|
ADAPTER_MAP,
|
|
54
57
|
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi adapter — Moonshot AI OpenAI-compatible chat completions.
|
|
3
|
+
*
|
|
4
|
+
* Reuses LlmDirectAdapter's streaming chat-completions client, but:
|
|
5
|
+
* - reads KIMI_API_KEY / MOONSHOT_API_KEY (also accepts LLM_API_KEY / OPENAI_API_KEY)
|
|
6
|
+
* - reads KIMI_BASE_URL / LLM_BASE_URL / OPENAI_BASE_URL, defaulting to https://api.moonshot.ai/v1
|
|
7
|
+
* - reads KIMI_MODEL / LLM_MODEL, defaulting to kimi-k2.6
|
|
8
|
+
*
|
|
9
|
+
* Priority for every value: UI-saved env > process env > default.
|
|
10
|
+
* Stop / status / control flow is inherited from BaseAdapter.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const LlmDirectAdapter = require('./llm-direct');
|
|
16
|
+
|
|
17
|
+
const DEFAULT_BASE_URL = 'https://api.moonshot.ai/v1';
|
|
18
|
+
const DEFAULT_MODEL = 'kimi-k2.6';
|
|
19
|
+
|
|
20
|
+
class KimiAdapter extends LlmDirectAdapter {
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
super({
|
|
23
|
+
...opts,
|
|
24
|
+
adapterLabel: 'Kimi',
|
|
25
|
+
modelEnvVar: 'KIMI_MODEL',
|
|
26
|
+
suppressConfigLog: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const env = this.agentEnv || process.env;
|
|
30
|
+
|
|
31
|
+
const apiKey =
|
|
32
|
+
env.KIMI_API_KEY ||
|
|
33
|
+
env.MOONSHOT_API_KEY ||
|
|
34
|
+
env.LLM_API_KEY ||
|
|
35
|
+
env.OPENAI_API_KEY ||
|
|
36
|
+
'';
|
|
37
|
+
|
|
38
|
+
const baseUrl = (
|
|
39
|
+
env.KIMI_BASE_URL ||
|
|
40
|
+
env.LLM_BASE_URL ||
|
|
41
|
+
env.OPENAI_BASE_URL ||
|
|
42
|
+
DEFAULT_BASE_URL
|
|
43
|
+
).replace(/\/$/, '');
|
|
44
|
+
|
|
45
|
+
const model =
|
|
46
|
+
env.KIMI_MODEL ||
|
|
47
|
+
env.LLM_MODEL ||
|
|
48
|
+
DEFAULT_MODEL;
|
|
49
|
+
|
|
50
|
+
this._apiKey = apiKey;
|
|
51
|
+
this._baseUrl = baseUrl;
|
|
52
|
+
this._model = model;
|
|
53
|
+
this._directMode = !!(this._apiKey && this._baseUrl);
|
|
54
|
+
|
|
55
|
+
if (this._directMode) {
|
|
56
|
+
this._log(`Kimi mode: ${this._baseUrl} model=${this._model}`);
|
|
57
|
+
} else {
|
|
58
|
+
this._log(
|
|
59
|
+
'Kimi adapter started without API key. ' +
|
|
60
|
+
'Set KIMI_API_KEY (or MOONSHOT_API_KEY) via the Launcher Configure screen.'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = KimiAdapter;
|
|
@@ -37,16 +37,36 @@ class LlmDirectAdapter extends BaseAdapter {
|
|
|
37
37
|
this._model = env[this._modelEnvVar] || env.OPENCLAW_MODEL || '';
|
|
38
38
|
this._directMode = !!(this._apiKey && this._baseUrl);
|
|
39
39
|
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
if (!opts.suppressConfigLog) {
|
|
41
|
+
if (this._directMode) {
|
|
42
|
+
this._log(`Direct LLM mode: ${this._baseUrl} model=${this._model || 'gpt-4o'}`);
|
|
43
|
+
} else {
|
|
44
|
+
this._log(
|
|
45
|
+
`${this._adapterLabel} adapter started without direct API config. ` +
|
|
46
|
+
'Set OPENAI_API_KEY + OPENAI_BASE_URL for direct mode.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
this._conversationHistory = [];
|
|
52
|
+
this._activeRequests = new Set();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
stop() {
|
|
56
|
+
super.stop();
|
|
57
|
+
for (const req of this._activeRequests) {
|
|
58
|
+
try { req.destroy(new Error('LLM API request stopped')); } catch {}
|
|
59
|
+
}
|
|
60
|
+
this._activeRequests.clear();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async _onControlAction(action, _payload) {
|
|
64
|
+
if (action === 'stop') {
|
|
65
|
+
for (const req of this._activeRequests) {
|
|
66
|
+
try { req.destroy(new Error('LLM API request stopped')); } catch {}
|
|
67
|
+
}
|
|
68
|
+
this._activeRequests.clear();
|
|
69
|
+
}
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
_buildSystemPrompt(channelName) {
|
|
@@ -133,10 +153,14 @@ class LlmDirectAdapter extends BaseAdapter {
|
|
|
133
153
|
headers: { ...headers, 'Content-Length': Buffer.byteLength(payload) },
|
|
134
154
|
timeout: 300000,
|
|
135
155
|
}, (res) => {
|
|
156
|
+
const cleanup = () => this._activeRequests.delete(req);
|
|
136
157
|
if (res.statusCode !== 200) {
|
|
137
158
|
let body = '';
|
|
138
159
|
res.on('data', (c) => { body += c; });
|
|
139
|
-
res.on('end', () =>
|
|
160
|
+
res.on('end', () => {
|
|
161
|
+
cleanup();
|
|
162
|
+
reject(new Error(`LLM API returned ${res.statusCode}: ${body.slice(0, 300)}`));
|
|
163
|
+
});
|
|
140
164
|
return;
|
|
141
165
|
}
|
|
142
166
|
|
|
@@ -166,11 +190,20 @@ class LlmDirectAdapter extends BaseAdapter {
|
|
|
166
190
|
}
|
|
167
191
|
});
|
|
168
192
|
|
|
169
|
-
res.on('end', () =>
|
|
193
|
+
res.on('end', () => {
|
|
194
|
+
cleanup();
|
|
195
|
+
resolve(fullText.trim());
|
|
196
|
+
});
|
|
170
197
|
});
|
|
171
198
|
|
|
172
|
-
|
|
173
|
-
req.on('
|
|
199
|
+
this._activeRequests.add(req);
|
|
200
|
+
req.on('error', (err) => {
|
|
201
|
+
this._activeRequests.delete(req);
|
|
202
|
+
reject(err);
|
|
203
|
+
});
|
|
204
|
+
req.on('timeout', () => {
|
|
205
|
+
req.destroy(new Error('LLM API request timed out'));
|
|
206
|
+
});
|
|
174
207
|
req.write(payload);
|
|
175
208
|
req.end();
|
|
176
209
|
});
|
|
@@ -268,6 +268,34 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
268
268
|
);
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
// Routines (recurring scheduled tasks)
|
|
272
|
+
if (!isPlan) {
|
|
273
|
+
sections.push(
|
|
274
|
+
'\n### Routines (Recurring Tasks)\n\n' +
|
|
275
|
+
'Create a recurring routine that fires on a schedule and posts a message ' +
|
|
276
|
+
'to the channel, waking you to do the work. Great for daily standups, ' +
|
|
277
|
+
'periodic reviews, scheduled checks.\n\n' +
|
|
278
|
+
'**Schedule:** Specify `hour` (0-23 UTC), `minute` (0-59), and optional ' +
|
|
279
|
+
'`days` array (0=Mon, 6=Sun). Omit `days` for every day.\n\n' +
|
|
280
|
+
'**Create a routine:**\n' +
|
|
281
|
+
`\`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
|
|
282
|
+
`${baseUrl}/v1/routines -d '{"name":"Daily PR Review","message":"Review open PRs",` +
|
|
283
|
+
`"hour":8,"minute":0,` +
|
|
284
|
+
`"network":"${workspaceId}","channel":"${channelName}",` +
|
|
285
|
+
`"source":"openagents:${agentName}"}'\`\n\n` +
|
|
286
|
+
'**Create a weekday-only routine (Mon-Fri):**\n' +
|
|
287
|
+
`\`curl -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
|
|
288
|
+
`${baseUrl}/v1/routines -d '{"name":"Morning Standup","message":"Post standup summary",` +
|
|
289
|
+
`"hour":9,"minute":0,"days":[0,1,2,3,4],` +
|
|
290
|
+
`"network":"${workspaceId}","channel":"${channelName}",` +
|
|
291
|
+
`"source":"openagents:${agentName}"}'\`\n\n` +
|
|
292
|
+
'**List active routines:**\n' +
|
|
293
|
+
`\`curl -s -H "${h}" "${baseUrl}/v1/routines?network=${workspaceId}&channel=${channelName}"\`\n\n` +
|
|
294
|
+
'**Cancel a routine:**\n' +
|
|
295
|
+
`\`curl -s -X DELETE -H "${h}" ${baseUrl}/v1/routines/ROUTINE_ID\`\n`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
271
299
|
// Discovery
|
|
272
300
|
sections.push(
|
|
273
301
|
'\n### Discover Agents\n\n' +
|
|
@@ -287,7 +315,10 @@ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = '
|
|
|
287
315
|
parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode));
|
|
288
316
|
parts.push(
|
|
289
317
|
'Use workspace_get_history to read previous messages.\n' +
|
|
290
|
-
'Use workspace_get_agents to see other agents.\n'
|
|
318
|
+
'Use workspace_get_agents to see other agents.\n' +
|
|
319
|
+
'Use workspace_put_todos to track your progress with a to-do list.\n' +
|
|
320
|
+
'Use workspace_create_timer to set a reminder that wakes you up later.\n' +
|
|
321
|
+
'Use workspace_create_routine to set up recurring scheduled tasks (e.g. daily reviews).\n'
|
|
291
322
|
);
|
|
292
323
|
parts.push(buildCollaborationPrompt());
|
|
293
324
|
|
package/src/daemon.js
CHANGED
|
@@ -138,6 +138,18 @@ class Daemon {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
await this.stopAgent(agentName);
|
|
141
|
+
|
|
142
|
+
// stopAgent only waits 5s for `_adapters[name]` to disappear, but
|
|
143
|
+
// graceful adapter shutdown can take longer (control-poller cleanup,
|
|
144
|
+
// disconnect, in-flight CLI subprocess kill). If the adapter is still
|
|
145
|
+
// there when we reach _launchAgent, the duplicate-launch guard bails
|
|
146
|
+
// and the agent stays stuck in 'stopped'. Wait up to 20s so the
|
|
147
|
+
// launch sees a clean slate.
|
|
148
|
+
for (let i = 0; i < 40; i++) {
|
|
149
|
+
if (!this._adapters || !this._adapters[agentName]) break;
|
|
150
|
+
await new Promise(r => setTimeout(r, 500));
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
this._stoppedAgents.delete(agentName);
|
|
142
154
|
|
|
143
155
|
// Reload config in case it changed
|
package/src/installer.js
CHANGED
|
@@ -59,6 +59,14 @@ class Installer {
|
|
|
59
59
|
const entry = this.registry.getEntry(agentType);
|
|
60
60
|
const npmPkg = entry && entry.install ? entry.install.npm_package : null;
|
|
61
61
|
const binary = entry && entry.install ? entry.install.binary : agentType;
|
|
62
|
+
if (entry?.install?.api_only) {
|
|
63
|
+
const installed = this._hasMarker(agentType);
|
|
64
|
+
return {
|
|
65
|
+
installed,
|
|
66
|
+
managed: installed,
|
|
67
|
+
location: installed ? 'api_only' : null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
62
70
|
const installCmd = entry && entry.install ? this._getInstallCommand(entry.install) : null;
|
|
63
71
|
let npmPkgFromCmd = null;
|
|
64
72
|
if (!npmPkg && installCmd && installCmd.includes('npm install')) {
|
|
@@ -135,6 +143,29 @@ class Installer {
|
|
|
135
143
|
* @returns {{ installed: boolean, binary: string|null, version: string|null }}
|
|
136
144
|
*/
|
|
137
145
|
healthCheck(agentType) {
|
|
146
|
+
const entry = this.registry.getEntry(agentType);
|
|
147
|
+
if (entry?.install?.api_only) {
|
|
148
|
+
const info = this.getInstallInfo(agentType);
|
|
149
|
+
if (!info.installed) {
|
|
150
|
+
return {
|
|
151
|
+
installed: false,
|
|
152
|
+
binary: null,
|
|
153
|
+
version: null,
|
|
154
|
+
ready: false,
|
|
155
|
+
auth_mode: null,
|
|
156
|
+
execution_mode: 'unavailable',
|
|
157
|
+
message: 'Not installed',
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
installed: true,
|
|
163
|
+
binary: null,
|
|
164
|
+
version: null,
|
|
165
|
+
...this._evaluateReadiness(agentType, entry, null),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
138
169
|
const binary = this._whichBinary(agentType);
|
|
139
170
|
if (!binary) {
|
|
140
171
|
return {
|
|
@@ -148,7 +179,6 @@ class Installer {
|
|
|
148
179
|
};
|
|
149
180
|
}
|
|
150
181
|
|
|
151
|
-
const entry = this.registry.getEntry(agentType);
|
|
152
182
|
const checkCmd = entry && entry.install ? entry.install.check_command : null;
|
|
153
183
|
const versionCmd = checkCmd || `${entry && entry.install && entry.install.binary || agentType} --version`;
|
|
154
184
|
|
|
@@ -323,6 +353,11 @@ class Installer {
|
|
|
323
353
|
throw new Error(`No install definition for agent type: ${agentType}`);
|
|
324
354
|
}
|
|
325
355
|
|
|
356
|
+
if (entry.install.api_only) {
|
|
357
|
+
this._markInstalled(agentType);
|
|
358
|
+
return { success: true, output: `${entry.label || agentType} uses direct API mode; no binary install needed.` };
|
|
359
|
+
}
|
|
360
|
+
|
|
326
361
|
let cmd = this._getInstallCommand(entry.install);
|
|
327
362
|
if (!cmd) {
|
|
328
363
|
throw new Error(`No install command for ${agentType} on ${Installer.platform()}`);
|
|
@@ -354,6 +389,13 @@ class Installer {
|
|
|
354
389
|
throw new Error(`No install definition for agent type: ${agentType}`);
|
|
355
390
|
}
|
|
356
391
|
|
|
392
|
+
if (entry.install.api_only) {
|
|
393
|
+
if (onData) onData(`${entry.label || agentType} uses direct API mode; no binary install needed.\n`);
|
|
394
|
+
this._markInstalled(agentType);
|
|
395
|
+
if (onData) onData(`\nDone! ${agentType} is now installed.\n`);
|
|
396
|
+
return { success: true, command: 'api-only' };
|
|
397
|
+
}
|
|
398
|
+
|
|
357
399
|
let rawCmd = this._getInstallCommand(entry.install);
|
|
358
400
|
if (!rawCmd) {
|
|
359
401
|
throw new Error(`No install command for ${agentType} on ${Installer.platform()}`);
|
|
@@ -419,6 +461,11 @@ class Installer {
|
|
|
419
461
|
throw new Error(`No install definition for agent type: ${agentType}`);
|
|
420
462
|
}
|
|
421
463
|
|
|
464
|
+
if (entry.install.api_only) {
|
|
465
|
+
this._markUninstalled(agentType);
|
|
466
|
+
return { success: true, output: `${entry.label || agentType} API-only install marker removed.` };
|
|
467
|
+
}
|
|
468
|
+
|
|
422
469
|
const installCmd = this._getInstallCommand(entry.install);
|
|
423
470
|
const uninstallCmd = this._deriveUninstallCommand(installCmd, agentType);
|
|
424
471
|
if (!uninstallCmd) {
|
|
@@ -462,6 +509,13 @@ class Installer {
|
|
|
462
509
|
throw new Error(`No install definition for agent type: ${agentType}`);
|
|
463
510
|
}
|
|
464
511
|
|
|
512
|
+
if (entry.install.api_only) {
|
|
513
|
+
if (onData) onData(`${entry.label || agentType} uses direct API mode; removing install marker.\n`);
|
|
514
|
+
this._markUninstalled(agentType);
|
|
515
|
+
if (onData) onData(`\nDone! ${agentType} has been uninstalled.\n`);
|
|
516
|
+
return { success: true, command: 'api-only' };
|
|
517
|
+
}
|
|
518
|
+
|
|
465
519
|
const installCmd = this._getInstallCommand(entry.install);
|
|
466
520
|
let rawCmd = this._deriveUninstallCommand(installCmd, agentType);
|
|
467
521
|
if (!rawCmd) {
|
package/src/mcp-server.js
CHANGED
|
@@ -299,6 +299,41 @@ function buildToolDefs(disabledModules) {
|
|
|
299
299
|
required: ['timer_id'],
|
|
300
300
|
},
|
|
301
301
|
},
|
|
302
|
+
{
|
|
303
|
+
name: 'workspace_create_routine',
|
|
304
|
+
description: 'Create a recurring scheduled routine that posts a message on a repeating schedule.',
|
|
305
|
+
inputSchema: {
|
|
306
|
+
type: 'object',
|
|
307
|
+
properties: {
|
|
308
|
+
name: { type: 'string', description: 'Human-readable label for the routine (e.g. "Daily PR Review")' },
|
|
309
|
+
message: { type: 'string', description: 'Message to post each time the routine fires' },
|
|
310
|
+
hour: { type: 'integer', description: 'Hour in UTC (0-23)' },
|
|
311
|
+
minute: { type: 'integer', description: 'Minute (0-59)' },
|
|
312
|
+
days: {
|
|
313
|
+
type: 'array',
|
|
314
|
+
items: { type: 'integer' },
|
|
315
|
+
description: 'Days of week to fire (0=Mon, 6=Sun). Omit for every day.',
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
required: ['name', 'message', 'hour', 'minute'],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'workspace_list_routines',
|
|
323
|
+
description: 'List active routines in the current channel.',
|
|
324
|
+
inputSchema: { type: 'object', properties: {} },
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'workspace_cancel_routine',
|
|
328
|
+
description: 'Cancel a recurring routine by its ID.',
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: 'object',
|
|
331
|
+
properties: {
|
|
332
|
+
routine_id: { type: 'string', description: 'Routine ID to cancel' },
|
|
333
|
+
},
|
|
334
|
+
required: ['routine_id'],
|
|
335
|
+
},
|
|
336
|
+
},
|
|
302
337
|
);
|
|
303
338
|
|
|
304
339
|
return tools;
|
|
@@ -712,6 +747,39 @@ class McpServer {
|
|
|
712
747
|
return text(`Timer cancelled: ${args.timer_id}`);
|
|
713
748
|
}
|
|
714
749
|
|
|
750
|
+
case 'workspace_create_routine': {
|
|
751
|
+
const result = await this.ws.createRoutine(
|
|
752
|
+
this.workspaceId, this.channelName, this.token,
|
|
753
|
+
{
|
|
754
|
+
name: args.name,
|
|
755
|
+
message: args.message,
|
|
756
|
+
hour: args.hour,
|
|
757
|
+
minute: args.minute,
|
|
758
|
+
days: args.days,
|
|
759
|
+
source: `openagents:${this.agentName}`,
|
|
760
|
+
},
|
|
761
|
+
);
|
|
762
|
+
const daysStr = args.days ? `days [${args.days.join(',')}]` : 'every day';
|
|
763
|
+
return text(`Routine created: "${args.name}" at ${String(args.hour).padStart(2,'0')}:${String(args.minute).padStart(2,'0')} UTC, ${daysStr} (id: ${result.id})`);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
case 'workspace_list_routines': {
|
|
767
|
+
const data = await this.ws.listRoutines(this.workspaceId, this.channelName, this.token);
|
|
768
|
+
const routines = (data && data.routines) || [];
|
|
769
|
+
if (!routines.length) return text('No active routines.');
|
|
770
|
+
const lines = routines.map((r) =>
|
|
771
|
+
`- ${r.id}: "${r.name}" at ${String(r.schedule_hour).padStart(2,'0')}:${String(r.schedule_minute).padStart(2,'0')} UTC` +
|
|
772
|
+
(r.schedule_days ? ` [days: ${r.schedule_days.join(',')}]` : ' (daily)') +
|
|
773
|
+
` — next: ${r.next_fires_at} (by ${r.created_by})`
|
|
774
|
+
);
|
|
775
|
+
return text(lines.join('\n'));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
case 'workspace_cancel_routine': {
|
|
779
|
+
await this.ws.cancelRoutine(this.workspaceId, this.token, args.routine_id);
|
|
780
|
+
return text(`Routine cancelled: ${args.routine_id}`);
|
|
781
|
+
}
|
|
782
|
+
|
|
715
783
|
default:
|
|
716
784
|
throw new Error(`Unknown tool: ${name}`);
|
|
717
785
|
}
|
package/src/utils.js
CHANGED
|
@@ -12,11 +12,12 @@ function testLLMConnection(env) {
|
|
|
12
12
|
const https = require('https');
|
|
13
13
|
const http = require('http');
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const hasKimiConfig = !!(env.KIMI_API_KEY || env.MOONSHOT_API_KEY || env.KIMI_BASE_URL || env.KIMI_MODEL);
|
|
16
|
+
const apiKey = env.KIMI_API_KEY || env.MOONSHOT_API_KEY || env.LLM_API_KEY || env.OPENAI_API_KEY || env.ANTHROPIC_API_KEY || '';
|
|
16
17
|
if (!apiKey) return Promise.resolve({ success: false, error: 'No API key provided' });
|
|
17
18
|
|
|
18
|
-
let baseUrl = (env.LLM_BASE_URL || env.OPENAI_BASE_URL || 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
19
|
-
const model = env.LLM_MODEL || env.OPENCLAW_MODEL || '';
|
|
19
|
+
let baseUrl = (env.KIMI_BASE_URL || env.LLM_BASE_URL || env.OPENAI_BASE_URL || (hasKimiConfig ? 'https://api.moonshot.ai/v1' : 'https://api.openai.com/v1')).replace(/\/$/, '');
|
|
20
|
+
const model = env.KIMI_MODEL || env.LLM_MODEL || env.OPENCLAW_MODEL || '';
|
|
20
21
|
const isAnthropic = baseUrl.includes('anthropic');
|
|
21
22
|
|
|
22
23
|
if (!isAnthropic && !baseUrl.endsWith('/v1')) {
|
|
@@ -44,11 +45,13 @@ function testLLMConnection(env) {
|
|
|
44
45
|
'Authorization': `Bearer ${apiKey}`,
|
|
45
46
|
'Content-Type': 'application/json',
|
|
46
47
|
};
|
|
47
|
-
|
|
48
|
-
model: model || 'gpt-4o-mini',
|
|
49
|
-
max_completion_tokens: 32,
|
|
48
|
+
const requestBody = {
|
|
49
|
+
model: model || (hasKimiConfig ? 'kimi-k2.6' : 'gpt-4o-mini'),
|
|
50
50
|
messages: [{ role: 'user', content: 'Say hi in 5 words.' }],
|
|
51
|
-
}
|
|
51
|
+
};
|
|
52
|
+
if (hasKimiConfig) requestBody.max_tokens = 32;
|
|
53
|
+
else requestBody.max_completion_tokens = 32;
|
|
54
|
+
body = JSON.stringify(requestBody);
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
const parsedUrl = new URL(url);
|
package/src/workspace-client.js
CHANGED
|
@@ -356,16 +356,17 @@ class WorkspaceClient {
|
|
|
356
356
|
const params = new URLSearchParams({
|
|
357
357
|
network: workspaceId,
|
|
358
358
|
type: 'workspace.agent.control',
|
|
359
|
+
target: `openagents:${agentName}`,
|
|
359
360
|
limit: '10',
|
|
361
|
+
sort: 'desc',
|
|
360
362
|
});
|
|
361
363
|
if (after) params.set('after', after);
|
|
362
364
|
const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
|
|
363
365
|
const result = data.data || data;
|
|
364
366
|
const events = (result && result.events) || [];
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
});
|
|
367
|
+
// Re-sort ascending by timestamp so callers process oldest-first.
|
|
368
|
+
events.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
|
369
|
+
return events;
|
|
369
370
|
} catch {
|
|
370
371
|
return [];
|
|
371
372
|
}
|
|
@@ -576,6 +577,35 @@ class WorkspaceClient {
|
|
|
576
577
|
return data.data || data;
|
|
577
578
|
}
|
|
578
579
|
|
|
580
|
+
// ── Routines ──
|
|
581
|
+
|
|
582
|
+
async createRoutine(workspaceId, channelName, token, { name, message, hour, minute, days, source } = {}) {
|
|
583
|
+
const body = {
|
|
584
|
+
name,
|
|
585
|
+
message,
|
|
586
|
+
hour,
|
|
587
|
+
minute,
|
|
588
|
+
network: workspaceId,
|
|
589
|
+
channel: channelName,
|
|
590
|
+
source: source || 'openagents:unknown',
|
|
591
|
+
};
|
|
592
|
+
if (days) body.days = days;
|
|
593
|
+
const data = await this._post('/v1/routines', body, this._wsHeaders(token));
|
|
594
|
+
return data.data || data;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async listRoutines(workspaceId, channelName, token) {
|
|
598
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
599
|
+
if (channelName) params.set('channel', channelName);
|
|
600
|
+
const data = await this._get(`/v1/routines?${params}`, this._wsHeaders(token));
|
|
601
|
+
return data.data || data;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async cancelRoutine(workspaceId, token, routineId) {
|
|
605
|
+
const data = await this._delete(`/v1/routines/${routineId}`, this._wsHeaders(token));
|
|
606
|
+
return data.data || data;
|
|
607
|
+
}
|
|
608
|
+
|
|
579
609
|
// ── Internal helpers ──
|
|
580
610
|
|
|
581
611
|
/**
|