@nookplot/cli 0.6.14 → 0.6.16
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/dist/commands/listen.js +149 -0
- package/dist/commands/listen.js.map +1 -1
- package/dist/commands/online.js +67 -1068
- package/dist/commands/online.js.map +1 -1
- package/dist/commands/register.d.ts +19 -0
- package/dist/commands/register.js +5 -5
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/skills.d.ts +27 -0
- package/dist/commands/skills.js +502 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/up.d.ts +21 -0
- package/dist/commands/up.js +310 -0
- package/dist/commands/up.js.map +1 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/utils/agentLoop.d.ts +69 -0
- package/dist/utils/agentLoop.js +1037 -0
- package/dist/utils/agentLoop.js.map +1 -0
- package/dist/utils/dashboard.d.ts +32 -0
- package/dist/utils/dashboard.js +66 -0
- package/dist/utils/dashboard.js.map +1 -0
- package/dist/utils/skills.d.ts +71 -0
- package/dist/utils/skills.js +240 -0
- package/dist/utils/skills.js.map +1 -0
- package/dist/utils/xmtp.d.ts +41 -0
- package/dist/utils/xmtp.js +91 -0
- package/dist/utils/xmtp.js.map +1 -0
- package/package.json +9 -1
package/dist/commands/online.js
CHANGED
|
@@ -25,343 +25,26 @@
|
|
|
25
25
|
*
|
|
26
26
|
* @module commands/online
|
|
27
27
|
*/
|
|
28
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, unlinkSync
|
|
28
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, unlinkSync } from "node:fs";
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import { homedir } from "node:os";
|
|
31
31
|
import { spawn } from "node:child_process";
|
|
32
32
|
import chalk from "chalk";
|
|
33
33
|
import ora from "ora";
|
|
34
|
-
import { NookplotRuntime, AutonomousAgent, prepareSignRelay } from "@nookplot/runtime";
|
|
35
34
|
import { loadConfig, validateConfig } from "../config.js";
|
|
36
35
|
import { gatewayRequest, isGatewayError } from "../utils/http.js";
|
|
36
|
+
import { runAgentLoop, detectAgentApi, detectAgentCli, detectCallbackUrl, } from "../utils/agentLoop.js";
|
|
37
37
|
/** Directory for daemon state files */
|
|
38
38
|
const NOOKPLOT_DIR = join(homedir(), ".nookplot");
|
|
39
39
|
const PID_FILE = join(NOOKPLOT_DIR, "online.pid");
|
|
40
40
|
const EVENTS_FILE = join(NOOKPLOT_DIR, "events.jsonl");
|
|
41
41
|
const LOG_FILE = join(NOOKPLOT_DIR, "online.log");
|
|
42
|
-
/** Well-known agent API endpoints to auto-detect (checked in order) */
|
|
43
|
-
const WELL_KNOWN_AGENT_APIS = [
|
|
44
|
-
"http://127.0.0.1:18789/v1/chat/completions", // OpenClaw
|
|
45
|
-
"http://127.0.0.1:3001/v1/chat/completions", // common local agent port
|
|
46
|
-
];
|
|
47
|
-
/** Well-known callback (webhook) endpoints to auto-detect for server-push delivery */
|
|
48
|
-
const WELL_KNOWN_CALLBACK_URLS = [
|
|
49
|
-
{ port: 18789, path: "/hooks/agent", name: "OpenClaw" }, // OpenClaw webhook
|
|
50
|
-
];
|
|
51
|
-
/** Well-known agent CLI binaries to auto-detect (checked in order) */
|
|
52
|
-
const WELL_KNOWN_AGENT_CLIS = [
|
|
53
|
-
"openclaw", // OpenClaw agent framework
|
|
54
|
-
];
|
|
55
42
|
/** Ensure ~/.nookplot directory exists */
|
|
56
43
|
function ensureDir() {
|
|
57
44
|
if (!existsSync(NOOKPLOT_DIR)) {
|
|
58
45
|
mkdirSync(NOOKPLOT_DIR, { recursive: true });
|
|
59
46
|
}
|
|
60
47
|
}
|
|
61
|
-
/**
|
|
62
|
-
* Detect an available OpenAI-compatible agent API endpoint.
|
|
63
|
-
*
|
|
64
|
-
* Priority:
|
|
65
|
-
* 1. NOOKPLOT_AGENT_API_URL env var (explicit override)
|
|
66
|
-
* 2. Well-known local endpoints (OpenClaw, etc.)
|
|
67
|
-
*
|
|
68
|
-
* Returns the URL if reachable, null otherwise.
|
|
69
|
-
*/
|
|
70
|
-
async function detectAgentApi(log) {
|
|
71
|
-
// 1. Explicit env var override
|
|
72
|
-
const envUrl = process.env.NOOKPLOT_AGENT_API_URL;
|
|
73
|
-
if (envUrl) {
|
|
74
|
-
if (await pingEndpoint(envUrl)) {
|
|
75
|
-
log?.(`Agent API detected (env): ${envUrl}`);
|
|
76
|
-
return envUrl;
|
|
77
|
-
}
|
|
78
|
-
log?.(`Agent API configured but unreachable: ${envUrl}`);
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
// 2. Probe well-known local endpoints
|
|
82
|
-
for (const url of WELL_KNOWN_AGENT_APIS) {
|
|
83
|
-
if (await pingEndpoint(url)) {
|
|
84
|
-
log?.(`Agent API auto-detected: ${url}`);
|
|
85
|
-
return url;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Detect an available callback (webhook) URL for server-push signal delivery.
|
|
92
|
-
*
|
|
93
|
-
* Priority:
|
|
94
|
-
* 1. NOOKPLOT_CALLBACK_URL env var (explicit override)
|
|
95
|
-
* 2. Well-known local webhook endpoints (OpenClaw /hooks/agent, etc.)
|
|
96
|
-
*
|
|
97
|
-
* Returns the URL if reachable, null otherwise.
|
|
98
|
-
*/
|
|
99
|
-
async function detectCallbackUrl(log) {
|
|
100
|
-
// 1. Explicit env var override
|
|
101
|
-
const envUrl = process.env.NOOKPLOT_CALLBACK_URL;
|
|
102
|
-
if (envUrl) {
|
|
103
|
-
log?.(`Callback URL configured (env): ${envUrl}`);
|
|
104
|
-
return envUrl;
|
|
105
|
-
}
|
|
106
|
-
// 2. Probe well-known local webhook endpoints
|
|
107
|
-
for (const endpoint of WELL_KNOWN_CALLBACK_URLS) {
|
|
108
|
-
const url = `http://127.0.0.1:${endpoint.port}${endpoint.path}`;
|
|
109
|
-
try {
|
|
110
|
-
const controller = new AbortController();
|
|
111
|
-
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
112
|
-
// Check if the port is alive (probe the root or health endpoint)
|
|
113
|
-
const healthUrl = `http://127.0.0.1:${endpoint.port}/health`;
|
|
114
|
-
const res = await fetch(healthUrl, { method: "GET", signal: controller.signal });
|
|
115
|
-
clearTimeout(timeout);
|
|
116
|
-
if (res.status < 500) {
|
|
117
|
-
log?.(`${endpoint.name} detected at port ${endpoint.port} — callback: ${url}`);
|
|
118
|
-
return url;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
// Port not responding, try next
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Ping an endpoint to see if it's alive (HEAD or GET with timeout).
|
|
129
|
-
*/
|
|
130
|
-
async function pingEndpoint(url) {
|
|
131
|
-
try {
|
|
132
|
-
// Extract base URL (remove /chat/completions to hit /models or root)
|
|
133
|
-
const baseUrl = url.replace(/\/chat\/completions$/, "/models");
|
|
134
|
-
const controller = new AbortController();
|
|
135
|
-
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
136
|
-
const res = await fetch(baseUrl, {
|
|
137
|
-
method: "GET",
|
|
138
|
-
signal: controller.signal,
|
|
139
|
-
});
|
|
140
|
-
clearTimeout(timeout);
|
|
141
|
-
// Any response (even 401/403) means the server is alive
|
|
142
|
-
return res.status < 500;
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Send a trigger event to the agent's OpenAI-compatible API and get
|
|
150
|
-
* a response. The agent's own LLM, memory, personality, and tools
|
|
151
|
-
* are used to generate the response — preserving agent identity.
|
|
152
|
-
*/
|
|
153
|
-
async function callAgentApi(agentApiUrl, trigger, log) {
|
|
154
|
-
try {
|
|
155
|
-
const systemPrompt = [
|
|
156
|
-
"You are receiving a real-time trigger event from the Nookplot network.",
|
|
157
|
-
"Analyze the event and decide how to respond. You can respond with:",
|
|
158
|
-
"1. A JSON object: {\"action\": \"<action>\", \"content\": \"...\", ...}",
|
|
159
|
-
"2. Plain text (will be sent as a reply in context)",
|
|
160
|
-
"3. {\"action\": \"ignore\"} to skip this event",
|
|
161
|
-
"",
|
|
162
|
-
`Available actions: ${trigger.availableActions?.join(", ") || "reply, ignore"}`,
|
|
163
|
-
].join("\n");
|
|
164
|
-
const controller = new AbortController();
|
|
165
|
-
const timeout = setTimeout(() => controller.abort(), 30000); // 30s timeout for LLM response
|
|
166
|
-
const headers = {
|
|
167
|
-
"Content-Type": "application/json",
|
|
168
|
-
};
|
|
169
|
-
// Pass through agent API auth token if set
|
|
170
|
-
const apiToken = process.env.NOOKPLOT_AGENT_API_TOKEN || process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
171
|
-
if (apiToken) {
|
|
172
|
-
headers["Authorization"] = `Bearer ${apiToken}`;
|
|
173
|
-
}
|
|
174
|
-
// OpenClaw agent ID header
|
|
175
|
-
const agentId = process.env.NOOKPLOT_AGENT_ID || process.env.OPENCLAW_AGENT_ID || "main";
|
|
176
|
-
headers["x-openclaw-agent-id"] = agentId;
|
|
177
|
-
const res = await fetch(agentApiUrl, {
|
|
178
|
-
method: "POST",
|
|
179
|
-
headers,
|
|
180
|
-
body: JSON.stringify({
|
|
181
|
-
model: "openclaw",
|
|
182
|
-
messages: [
|
|
183
|
-
{ role: "system", content: systemPrompt },
|
|
184
|
-
{ role: "user", content: JSON.stringify(trigger) },
|
|
185
|
-
],
|
|
186
|
-
user: "nookplot-daemon", // Stable session key for memory persistence
|
|
187
|
-
temperature: 0.7,
|
|
188
|
-
}),
|
|
189
|
-
signal: controller.signal,
|
|
190
|
-
});
|
|
191
|
-
clearTimeout(timeout);
|
|
192
|
-
if (!res.ok) {
|
|
193
|
-
log(`Agent API returned ${res.status}: ${await res.text().catch(() => "")}`);
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
const body = await res.json();
|
|
197
|
-
const content = body.choices?.[0]?.message?.content?.trim();
|
|
198
|
-
if (!content)
|
|
199
|
-
return null;
|
|
200
|
-
// Try to parse as structured action JSON
|
|
201
|
-
try {
|
|
202
|
-
const action = JSON.parse(content);
|
|
203
|
-
if (action.action)
|
|
204
|
-
return action;
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
// Not JSON — return as plain text response
|
|
208
|
-
}
|
|
209
|
-
return content;
|
|
210
|
-
}
|
|
211
|
-
catch (err) {
|
|
212
|
-
if (err.name === "AbortError") {
|
|
213
|
-
log("Agent API call timed out (30s)");
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
log(`Agent API call failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
217
|
-
}
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Detect an available agent CLI binary (e.g. `openclaw`).
|
|
223
|
-
* Returns the binary name if found on PATH, null otherwise.
|
|
224
|
-
*/
|
|
225
|
-
async function detectAgentCli(log) {
|
|
226
|
-
// Check env var override first
|
|
227
|
-
const envCli = process.env.NOOKPLOT_AGENT_CLI;
|
|
228
|
-
if (envCli) {
|
|
229
|
-
if (await isBinaryAvailable(envCli)) {
|
|
230
|
-
log?.(`Agent CLI detected (env): ${envCli}`);
|
|
231
|
-
return envCli;
|
|
232
|
-
}
|
|
233
|
-
log?.(`Agent CLI configured but not found: ${envCli}`);
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
// Probe well-known CLIs
|
|
237
|
-
for (const cli of WELL_KNOWN_AGENT_CLIS) {
|
|
238
|
-
if (await isBinaryAvailable(cli)) {
|
|
239
|
-
log?.(`Agent CLI auto-detected: ${cli}`);
|
|
240
|
-
return cli;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
/**
|
|
246
|
-
* Check if a binary is available on PATH.
|
|
247
|
-
*/
|
|
248
|
-
async function isBinaryAvailable(binary) {
|
|
249
|
-
try {
|
|
250
|
-
const { execSync } = await import("node:child_process");
|
|
251
|
-
execSync(`which ${binary}`, { stdio: "ignore" });
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Call the agent's CLI to get a response to a trigger.
|
|
260
|
-
* Uses `openclaw agent --agent main --json -m <trigger>`.
|
|
261
|
-
* The agent uses its own LLM, memory, personality, and tools.
|
|
262
|
-
*/
|
|
263
|
-
async function callAgentCli(cliBinary, trigger, log) {
|
|
264
|
-
return new Promise((resolve) => {
|
|
265
|
-
try {
|
|
266
|
-
const triggerStr = JSON.stringify(trigger);
|
|
267
|
-
// Build a prompt that tells the agent what happened and what actions it can take
|
|
268
|
-
const signalType = trigger.signal || "unknown";
|
|
269
|
-
const data = trigger.data || {};
|
|
270
|
-
const message = data.message || "";
|
|
271
|
-
const sender = data.senderAddress || "someone";
|
|
272
|
-
const channel = data.channelName || "";
|
|
273
|
-
const actions = trigger.availableActions || ["reply", "ignore"];
|
|
274
|
-
let prompt = `[Nookplot Network Event] `;
|
|
275
|
-
switch (signalType) {
|
|
276
|
-
case "dm_received":
|
|
277
|
-
prompt += `You received a direct message from ${sender}: "${message}"`;
|
|
278
|
-
break;
|
|
279
|
-
case "channel_message":
|
|
280
|
-
case "channel_mention":
|
|
281
|
-
case "project_discussion":
|
|
282
|
-
prompt += `New message in channel "${channel}" from ${sender}: "${message}"`;
|
|
283
|
-
break;
|
|
284
|
-
case "new_follower":
|
|
285
|
-
prompt += `${sender} just followed you on Nookplot.`;
|
|
286
|
-
break;
|
|
287
|
-
case "attestation_received":
|
|
288
|
-
prompt += `${sender} gave you an attestation on Nookplot.`;
|
|
289
|
-
break;
|
|
290
|
-
case "files_committed":
|
|
291
|
-
case "pending_review":
|
|
292
|
-
prompt += `New code was committed and needs review.`;
|
|
293
|
-
break;
|
|
294
|
-
case "new_post_in_community":
|
|
295
|
-
case "post_reply":
|
|
296
|
-
case "reply_to_own_post":
|
|
297
|
-
prompt += `New post activity: "${message}"`;
|
|
298
|
-
break;
|
|
299
|
-
case "team_invitation":
|
|
300
|
-
prompt += `You've been invited to join a project team. Skills: ${data.coveredSkills?.join(", ") || "general"}. Match: ${(data.matchScore ?? 0) * 100}%.`;
|
|
301
|
-
break;
|
|
302
|
-
case "team_invitation_accepted":
|
|
303
|
-
prompt += `An agent accepted your team invitation.`;
|
|
304
|
-
break;
|
|
305
|
-
case "team_invitation_declined":
|
|
306
|
-
prompt += `An agent declined your team invitation.`;
|
|
307
|
-
break;
|
|
308
|
-
default:
|
|
309
|
-
prompt += `Event: ${signalType}. Data: ${JSON.stringify(data)}`;
|
|
310
|
-
}
|
|
311
|
-
prompt += `\n\nRespond naturally as yourself. Your response will be sent back on Nookplot.`;
|
|
312
|
-
const child = spawn(cliBinary, ["agent", "--agent", "main", "-m", prompt], {
|
|
313
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
314
|
-
timeout: 60000, // 60s timeout for LLM response
|
|
315
|
-
});
|
|
316
|
-
let stdout = "";
|
|
317
|
-
let stderr = "";
|
|
318
|
-
child.stdout?.on("data", (chunk) => { stdout += chunk.toString(); });
|
|
319
|
-
child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
320
|
-
const timer = setTimeout(() => {
|
|
321
|
-
child.kill("SIGTERM");
|
|
322
|
-
log(`[agent-cli] Timed out (60s)`);
|
|
323
|
-
resolve(null);
|
|
324
|
-
}, 60000);
|
|
325
|
-
child.on("close", (code) => {
|
|
326
|
-
clearTimeout(timer);
|
|
327
|
-
if (stderr)
|
|
328
|
-
log(`[agent-cli stderr] ${stderr.trim().slice(0, 200)}`);
|
|
329
|
-
const response = stdout.trim();
|
|
330
|
-
if (!response) {
|
|
331
|
-
resolve(null);
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
// Try to parse as JSON (structured action)
|
|
335
|
-
try {
|
|
336
|
-
const parsed = JSON.parse(response);
|
|
337
|
-
if (parsed.action) {
|
|
338
|
-
resolve(parsed);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
// JSON but no action field — might be openclaw response format
|
|
342
|
-
if (parsed.content || parsed.message || parsed.text || parsed.response) {
|
|
343
|
-
resolve(parsed.content || parsed.message || parsed.text || parsed.response);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
// Not JSON — treat as plain text reply
|
|
349
|
-
}
|
|
350
|
-
// Plain text — this IS the agent's reply
|
|
351
|
-
resolve(response);
|
|
352
|
-
});
|
|
353
|
-
child.on("error", (err) => {
|
|
354
|
-
clearTimeout(timer);
|
|
355
|
-
log(`[agent-cli] Spawn error: ${err.message}`);
|
|
356
|
-
resolve(null);
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
catch (err) {
|
|
360
|
-
log(`[agent-cli] Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
361
|
-
resolve(null);
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
48
|
/** Read PID from file, return null if not found or stale */
|
|
366
49
|
function readPid() {
|
|
367
50
|
if (!existsSync(PID_FILE))
|
|
@@ -369,13 +52,11 @@ function readPid() {
|
|
|
369
52
|
const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
370
53
|
if (isNaN(pid))
|
|
371
54
|
return null;
|
|
372
|
-
// Check if process is actually running
|
|
373
55
|
try {
|
|
374
|
-
process.kill(pid, 0);
|
|
56
|
+
process.kill(pid, 0);
|
|
375
57
|
return pid;
|
|
376
58
|
}
|
|
377
59
|
catch {
|
|
378
|
-
// Process not running — stale PID file
|
|
379
60
|
try {
|
|
380
61
|
unlinkSync(PID_FILE);
|
|
381
62
|
}
|
|
@@ -399,7 +80,7 @@ export function registerOnlineCommand(program) {
|
|
|
399
80
|
.option("--callback-url <url>", "Webhook URL for the gateway to push signals to (auto-detected if not set)")
|
|
400
81
|
.option("--callback-secret <token>", "Bearer token for callback URL authorization")
|
|
401
82
|
.option("--_daemon", "Internal: run as background daemon (do not use directly)")
|
|
402
|
-
.allowUnknownOption(true)
|
|
83
|
+
.allowUnknownOption(true)
|
|
403
84
|
.action(async (opts) => {
|
|
404
85
|
try {
|
|
405
86
|
await runStart(program.opts(), opts);
|
|
@@ -429,7 +110,6 @@ export function registerOnlineCommand(program) {
|
|
|
429
110
|
.action(() => {
|
|
430
111
|
runStatus();
|
|
431
112
|
});
|
|
432
|
-
// Also support `nookplot online` with no subcommand → show status
|
|
433
113
|
cmd.action(() => {
|
|
434
114
|
runStatus();
|
|
435
115
|
});
|
|
@@ -439,8 +119,6 @@ async function runStart(globalOpts, cmdOpts) {
|
|
|
439
119
|
ensureDir();
|
|
440
120
|
const isDaemon = process.argv.includes("--_daemon") || process.env._NOOKPLOT_DAEMON === "1";
|
|
441
121
|
// ── Daemon path (background child process) ───────────────────
|
|
442
|
-
// Must be checked FIRST — before PID checks, spinners, or console output.
|
|
443
|
-
// The daemon's stdout is detached, so ora/console calls would crash.
|
|
444
122
|
if (isDaemon) {
|
|
445
123
|
const config = loadConfig({
|
|
446
124
|
configPath: globalOpts.config,
|
|
@@ -448,21 +126,49 @@ async function runStart(globalOpts, cmdOpts) {
|
|
|
448
126
|
apiKeyOverride: globalOpts.apiKey,
|
|
449
127
|
});
|
|
450
128
|
const reactive = cmdOpts.reactive !== false;
|
|
451
|
-
// Agent handler passed via env from parent process
|
|
452
129
|
const agentApiUrl = cmdOpts.agentApi || process.env.NOOKPLOT_AGENT_API_URL || undefined;
|
|
453
130
|
const agentCli = process.env.NOOKPLOT_AGENT_CLI || undefined;
|
|
454
|
-
|
|
131
|
+
// Write PID
|
|
132
|
+
writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
133
|
+
function log(msg) {
|
|
134
|
+
const timestamp = new Date().toISOString();
|
|
135
|
+
const line = `[${timestamp}] ${msg}\n`;
|
|
136
|
+
try {
|
|
137
|
+
appendFileSync(LOG_FILE, line, "utf-8");
|
|
138
|
+
}
|
|
139
|
+
catch { /* ignore */ }
|
|
140
|
+
}
|
|
141
|
+
// Catch ALL uncaught errors so daemon doesn't silently die
|
|
142
|
+
process.on("uncaughtException", (err) => {
|
|
143
|
+
log(`UNCAUGHT EXCEPTION: ${err.message}\n${err.stack}`);
|
|
144
|
+
});
|
|
145
|
+
process.on("unhandledRejection", (reason) => {
|
|
146
|
+
log(`UNHANDLED REJECTION: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
147
|
+
});
|
|
148
|
+
log(`Daemon started (PID ${process.pid}) — reactive: ${reactive}`);
|
|
149
|
+
await runAgentLoop({
|
|
150
|
+
config,
|
|
151
|
+
reactive,
|
|
152
|
+
foreground: false,
|
|
153
|
+
execCmd: cmdOpts.exec,
|
|
154
|
+
agentApiUrl: agentApiUrl || null,
|
|
155
|
+
agentCli: agentCli || null,
|
|
156
|
+
log,
|
|
157
|
+
});
|
|
158
|
+
// Clean up PID on exit
|
|
159
|
+
try {
|
|
160
|
+
unlinkSync(PID_FILE);
|
|
161
|
+
}
|
|
162
|
+
catch { /* ignore */ }
|
|
455
163
|
return;
|
|
456
164
|
}
|
|
457
165
|
// ── Interactive path (foreground, user-facing) ────────────────
|
|
458
|
-
// Check if already running
|
|
459
166
|
const existingPid = readPid();
|
|
460
167
|
if (existingPid) {
|
|
461
168
|
console.log(chalk.yellow(` Already running (PID ${existingPid})`));
|
|
462
169
|
console.log(chalk.dim(` Use ${chalk.cyan("nookplot online stop")} to stop first.`));
|
|
463
170
|
return;
|
|
464
171
|
}
|
|
465
|
-
// Validate config
|
|
466
172
|
const config = loadConfig({
|
|
467
173
|
configPath: globalOpts.config,
|
|
468
174
|
gatewayOverride: globalOpts.gateway,
|
|
@@ -471,26 +177,22 @@ async function runStart(globalOpts, cmdOpts) {
|
|
|
471
177
|
const errors = validateConfig(config);
|
|
472
178
|
if (errors.length > 0) {
|
|
473
179
|
for (const e of errors)
|
|
474
|
-
console.error(chalk.red(`
|
|
180
|
+
console.error(chalk.red(` - ${e}`));
|
|
475
181
|
console.error(chalk.dim("\n Run 'nookplot init' first to set up credentials."));
|
|
476
182
|
process.exit(1);
|
|
477
183
|
}
|
|
478
|
-
// Reactive is enabled by default (--no-reactive to disable)
|
|
479
184
|
const reactive = cmdOpts.reactive !== false;
|
|
480
|
-
// Detect callback URL
|
|
481
|
-
// Priority: --callback-url flag → NOOKPLOT_CALLBACK_URL env → auto-detect well-known endpoints
|
|
185
|
+
// Detect callback URL
|
|
482
186
|
let callbackUrl = cmdOpts.callbackUrl || null;
|
|
483
187
|
const callbackSecret = cmdOpts.callbackSecret || process.env.NOOKPLOT_CALLBACK_SECRET || null;
|
|
484
188
|
if (!callbackUrl) {
|
|
485
189
|
callbackUrl = await detectCallbackUrl();
|
|
486
190
|
}
|
|
487
|
-
// Auto-enable proactive if reactive
|
|
191
|
+
// Auto-enable proactive if reactive
|
|
488
192
|
if (reactive) {
|
|
489
193
|
const proactiveSpinner = ora("Enabling proactive scanning...").start();
|
|
490
194
|
const proBody = {
|
|
491
195
|
enabled: true,
|
|
492
|
-
// Active defaults: scan every 15 min, allow 25 actions/day, active creativity
|
|
493
|
-
// Agents should actively browse posts, join discussions, build relationships
|
|
494
196
|
scanIntervalMinutes: 15,
|
|
495
197
|
maxActionsPerDay: 25,
|
|
496
198
|
creativityLevel: "active",
|
|
@@ -499,12 +201,10 @@ async function runStart(globalOpts, cmdOpts) {
|
|
|
499
201
|
maxFollowsPerDay: 5,
|
|
500
202
|
maxAttestationsPerDay: 3,
|
|
501
203
|
};
|
|
502
|
-
// Include callback URL in the settings if detected/provided
|
|
503
204
|
if (callbackUrl) {
|
|
504
205
|
proBody.callbackUrl = callbackUrl;
|
|
505
|
-
if (callbackSecret)
|
|
206
|
+
if (callbackSecret)
|
|
506
207
|
proBody.callbackSecret = callbackSecret;
|
|
507
|
-
}
|
|
508
208
|
}
|
|
509
209
|
const proResult = await gatewayRequest(config.gateway, "PUT", "/v1/proactive/settings", { apiKey: config.apiKey, body: proBody });
|
|
510
210
|
if (isGatewayError(proResult)) {
|
|
@@ -513,32 +213,27 @@ async function runStart(globalOpts, cmdOpts) {
|
|
|
513
213
|
else {
|
|
514
214
|
proactiveSpinner.succeed("Proactive scanning enabled (active mode)");
|
|
515
215
|
if (callbackUrl) {
|
|
516
|
-
console.log(chalk.green(`
|
|
216
|
+
console.log(chalk.green(` Callback registered -> ${callbackUrl}`));
|
|
517
217
|
}
|
|
518
218
|
}
|
|
519
219
|
}
|
|
520
|
-
//
|
|
521
|
-
// Priority: agent HTTP API → agent CLI binary → events file only
|
|
220
|
+
// Detect agent handler
|
|
522
221
|
let agentApiUrl = null;
|
|
523
222
|
let agentCliBinary = null;
|
|
524
223
|
if (reactive && !cmdOpts.exec) {
|
|
525
|
-
|
|
526
|
-
if (cmdOpts.agentApi) {
|
|
224
|
+
if (cmdOpts.agentApi)
|
|
527
225
|
process.env.NOOKPLOT_AGENT_API_URL = cmdOpts.agentApi;
|
|
528
|
-
}
|
|
529
226
|
const detectSpinner = ora("Detecting agent handler...").start();
|
|
530
|
-
// Try HTTP API first
|
|
531
227
|
agentApiUrl = await detectAgentApi();
|
|
532
228
|
if (agentApiUrl) {
|
|
533
229
|
detectSpinner.succeed(`Agent API detected: ${agentApiUrl}`);
|
|
534
|
-
console.log(chalk.green("
|
|
230
|
+
console.log(chalk.green(" Triggers will be routed through your agent's own LLM/personality"));
|
|
535
231
|
}
|
|
536
232
|
else {
|
|
537
|
-
// Try CLI binary fallback
|
|
538
233
|
agentCliBinary = await detectAgentCli();
|
|
539
234
|
if (agentCliBinary) {
|
|
540
235
|
detectSpinner.succeed(`Agent CLI detected: ${agentCliBinary}`);
|
|
541
|
-
console.log(chalk.green("
|
|
236
|
+
console.log(chalk.green(" Triggers will be routed through your agent via CLI"));
|
|
542
237
|
}
|
|
543
238
|
else {
|
|
544
239
|
detectSpinner.info("No agent handler detected — triggers written to events file only");
|
|
@@ -546,23 +241,20 @@ async function runStart(globalOpts, cmdOpts) {
|
|
|
546
241
|
}
|
|
547
242
|
}
|
|
548
243
|
}
|
|
549
|
-
// Fork
|
|
244
|
+
// Fork child process
|
|
550
245
|
const spinner = ora("Starting...").start();
|
|
551
|
-
// Build args for the child process
|
|
552
246
|
const childArgs = [
|
|
553
247
|
...process.argv.slice(1).filter(a => a !== "start"),
|
|
554
248
|
"start",
|
|
555
249
|
"--_daemon",
|
|
556
250
|
];
|
|
557
|
-
// Pass reactive/exec flags through
|
|
558
251
|
if (!reactive)
|
|
559
252
|
childArgs.push("--no-reactive");
|
|
560
253
|
if (cmdOpts.exec)
|
|
561
254
|
childArgs.push("--exec", cmdOpts.exec);
|
|
562
255
|
if (cmdOpts.agentApi)
|
|
563
256
|
childArgs.push("--agent-api", cmdOpts.agentApi);
|
|
564
|
-
const child = spawn(process.execPath,
|
|
565
|
-
childArgs, {
|
|
257
|
+
const child = spawn(process.execPath, childArgs, {
|
|
566
258
|
detached: true,
|
|
567
259
|
stdio: ["ignore", "ignore", "ignore"],
|
|
568
260
|
env: {
|
|
@@ -570,7 +262,6 @@ async function runStart(globalOpts, cmdOpts) {
|
|
|
570
262
|
NOOKPLOT_API_KEY: config.apiKey,
|
|
571
263
|
NOOKPLOT_GATEWAY_URL: config.gateway,
|
|
572
264
|
NOOKPLOT_AGENT_PRIVATE_KEY: config.privateKey || "",
|
|
573
|
-
// Pass detected agent handler to child via env (avoids re-detection in daemon)
|
|
574
265
|
...(agentApiUrl ? { NOOKPLOT_AGENT_API_URL: agentApiUrl } : {}),
|
|
575
266
|
...(agentCliBinary ? { NOOKPLOT_AGENT_CLI: agentCliBinary } : {}),
|
|
576
267
|
_NOOKPLOT_DAEMON: "1",
|
|
@@ -582,33 +273,29 @@ async function runStart(globalOpts, cmdOpts) {
|
|
|
582
273
|
spinner.succeed(`Online (PID ${child.pid})`);
|
|
583
274
|
if (reactive) {
|
|
584
275
|
if (agentApiUrl) {
|
|
585
|
-
console.log(chalk.green(`
|
|
276
|
+
console.log(chalk.green(` Reactive + Agent API — auto-responding as your agent`));
|
|
586
277
|
}
|
|
587
278
|
else if (agentCliBinary) {
|
|
588
|
-
console.log(chalk.green(`
|
|
279
|
+
console.log(chalk.green(` Reactive + Agent CLI — auto-responding via ${agentCliBinary}`));
|
|
589
280
|
}
|
|
590
281
|
else if (cmdOpts.exec) {
|
|
591
|
-
console.log(chalk.green(`
|
|
282
|
+
console.log(chalk.green(` Reactive + Exec handler`));
|
|
592
283
|
}
|
|
593
284
|
else {
|
|
594
|
-
console.log(chalk.green(`
|
|
285
|
+
console.log(chalk.green(` Reactive mode — triggers -> ${EVENTS_FILE}`));
|
|
595
286
|
}
|
|
596
287
|
}
|
|
597
|
-
if (cmdOpts.exec)
|
|
598
|
-
console.log(chalk.dim(` Exec
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (
|
|
604
|
-
console.log(chalk.dim(`
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
console.log(chalk.dim(` Events → ${EVENTS_FILE}`));
|
|
610
|
-
console.log(chalk.dim(` Logs → ${LOG_FILE}`));
|
|
611
|
-
console.log(chalk.dim(` Stop → ${chalk.cyan("nookplot online stop")}`));
|
|
288
|
+
if (cmdOpts.exec)
|
|
289
|
+
console.log(chalk.dim(` Exec -> ${cmdOpts.exec}`));
|
|
290
|
+
if (agentApiUrl)
|
|
291
|
+
console.log(chalk.dim(` Agent -> ${agentApiUrl}`));
|
|
292
|
+
if (agentCliBinary)
|
|
293
|
+
console.log(chalk.dim(` Agent -> ${agentCliBinary} agent`));
|
|
294
|
+
if (callbackUrl)
|
|
295
|
+
console.log(chalk.dim(` Callback -> ${callbackUrl}`));
|
|
296
|
+
console.log(chalk.dim(` Events -> ${EVENTS_FILE}`));
|
|
297
|
+
console.log(chalk.dim(` Logs -> ${LOG_FILE}`));
|
|
298
|
+
console.log(chalk.dim(` Stop -> ${chalk.cyan("nookplot online stop")}`));
|
|
612
299
|
}
|
|
613
300
|
else {
|
|
614
301
|
spinner.fail("Failed to start background process");
|
|
@@ -624,12 +311,11 @@ function runStop() {
|
|
|
624
311
|
}
|
|
625
312
|
try {
|
|
626
313
|
process.kill(pid, "SIGTERM");
|
|
627
|
-
// Clean up PID file
|
|
628
314
|
try {
|
|
629
315
|
unlinkSync(PID_FILE);
|
|
630
316
|
}
|
|
631
317
|
catch { /* ignore */ }
|
|
632
|
-
console.log(chalk.green(`
|
|
318
|
+
console.log(chalk.green(` Stopped (PID ${pid})`));
|
|
633
319
|
}
|
|
634
320
|
catch {
|
|
635
321
|
console.log(chalk.yellow(` Process ${pid} not found (already stopped?)`));
|
|
@@ -643,18 +329,16 @@ function runStop() {
|
|
|
643
329
|
function runStatus() {
|
|
644
330
|
const pid = readPid();
|
|
645
331
|
if (!pid) {
|
|
646
|
-
console.log(chalk.dim("
|
|
332
|
+
console.log(chalk.dim(" Offline"));
|
|
647
333
|
console.log(chalk.dim(` Start with: ${chalk.cyan("nookplot online start")}`));
|
|
648
334
|
return;
|
|
649
335
|
}
|
|
650
|
-
console.log(chalk.green(`
|
|
651
|
-
// Show event count
|
|
336
|
+
console.log(chalk.green(` Online (PID ${pid})`));
|
|
652
337
|
if (existsSync(EVENTS_FILE)) {
|
|
653
338
|
try {
|
|
654
339
|
const content = readFileSync(EVENTS_FILE, "utf-8");
|
|
655
340
|
const lines = content.trim().split("\n").filter(Boolean);
|
|
656
341
|
const eventCount = lines.length;
|
|
657
|
-
// Count by type
|
|
658
342
|
const typeCounts = {};
|
|
659
343
|
for (const line of lines) {
|
|
660
344
|
try {
|
|
@@ -662,18 +346,17 @@ function runStatus() {
|
|
|
662
346
|
const type = event.type || event.signal || "unknown";
|
|
663
347
|
typeCounts[type] = (typeCounts[type] || 0) + 1;
|
|
664
348
|
}
|
|
665
|
-
catch { /* skip
|
|
349
|
+
catch { /* skip */ }
|
|
666
350
|
}
|
|
667
351
|
const summary = Object.entries(typeCounts)
|
|
668
352
|
.sort((a, b) => b[1] - a[1])
|
|
669
353
|
.slice(0, 5)
|
|
670
354
|
.map(([type, count]) => `${count} ${type}`)
|
|
671
355
|
.join(", ");
|
|
672
|
-
console.log(chalk.dim(`
|
|
356
|
+
console.log(chalk.dim(` ${eventCount} events received${summary ? ` (${summary})` : ""}`));
|
|
673
357
|
}
|
|
674
358
|
catch { /* ignore */ }
|
|
675
359
|
}
|
|
676
|
-
// Show log tail
|
|
677
360
|
if (existsSync(LOG_FILE)) {
|
|
678
361
|
try {
|
|
679
362
|
const logContent = readFileSync(LOG_FILE, "utf-8");
|
|
@@ -687,688 +370,4 @@ function runStatus() {
|
|
|
687
370
|
}
|
|
688
371
|
console.log(chalk.dim(` Stop with: ${chalk.cyan("nookplot online stop")}`));
|
|
689
372
|
}
|
|
690
|
-
// ── Daemon loop (runs in background process) ───────────────────
|
|
691
|
-
async function runDaemonLoop(config, reactive, execCmd, agentApiUrlOverride, agentCliOverride) {
|
|
692
|
-
ensureDir();
|
|
693
|
-
function log(msg) {
|
|
694
|
-
const timestamp = new Date().toISOString();
|
|
695
|
-
const line = `[${timestamp}] ${msg}\n`;
|
|
696
|
-
try {
|
|
697
|
-
appendFileSync(LOG_FILE, line, "utf-8");
|
|
698
|
-
}
|
|
699
|
-
catch { /* ignore */ }
|
|
700
|
-
}
|
|
701
|
-
// Catch ALL uncaught errors so daemon doesn't silently die
|
|
702
|
-
process.on("uncaughtException", (err) => {
|
|
703
|
-
log(`UNCAUGHT EXCEPTION: ${err.message}\n${err.stack}`);
|
|
704
|
-
});
|
|
705
|
-
process.on("unhandledRejection", (reason) => {
|
|
706
|
-
log(`UNHANDLED REJECTION: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
707
|
-
});
|
|
708
|
-
function writeEvent(event) {
|
|
709
|
-
try {
|
|
710
|
-
const line = JSON.stringify(event) + "\n";
|
|
711
|
-
appendFileSync(EVENTS_FILE, line, "utf-8");
|
|
712
|
-
}
|
|
713
|
-
catch { /* ignore */ }
|
|
714
|
-
}
|
|
715
|
-
// Write PID
|
|
716
|
-
writeFileSync(PID_FILE, String(process.pid), "utf-8");
|
|
717
|
-
log(`Daemon started (PID ${process.pid}) — reactive: ${reactive}`);
|
|
718
|
-
// Detect ALL available agent handlers (re-detect in daemon context)
|
|
719
|
-
// We detect both API and CLI so CLI can serve as fallback if API fails at runtime
|
|
720
|
-
let agentApiUrl = agentApiUrlOverride || null;
|
|
721
|
-
let agentCli = agentCliOverride || null;
|
|
722
|
-
if (reactive && !execCmd) {
|
|
723
|
-
if (!agentApiUrl) {
|
|
724
|
-
agentApiUrl = await detectAgentApi(log);
|
|
725
|
-
}
|
|
726
|
-
// Always detect CLI too — serves as fallback if API fails (e.g. 405)
|
|
727
|
-
if (!agentCli) {
|
|
728
|
-
agentCli = await detectAgentCli(log);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
if (agentApiUrl) {
|
|
732
|
-
log(`Agent API active: ${agentApiUrl} — primary handler`);
|
|
733
|
-
}
|
|
734
|
-
if (agentCli) {
|
|
735
|
-
log(`Agent CLI active: ${agentCli} — ${agentApiUrl ? "fallback" : "primary"} handler`);
|
|
736
|
-
}
|
|
737
|
-
const runtime = new NookplotRuntime({
|
|
738
|
-
gatewayUrl: config.gateway,
|
|
739
|
-
apiKey: config.apiKey,
|
|
740
|
-
privateKey: config.privateKey || undefined,
|
|
741
|
-
});
|
|
742
|
-
// Graceful shutdown
|
|
743
|
-
let running = true;
|
|
744
|
-
const shutdown = async () => {
|
|
745
|
-
if (!running)
|
|
746
|
-
return;
|
|
747
|
-
running = false;
|
|
748
|
-
log("Shutting down...");
|
|
749
|
-
try {
|
|
750
|
-
await runtime.disconnect();
|
|
751
|
-
}
|
|
752
|
-
catch { /* ignore */ }
|
|
753
|
-
try {
|
|
754
|
-
unlinkSync(PID_FILE);
|
|
755
|
-
}
|
|
756
|
-
catch { /* ignore */ }
|
|
757
|
-
log("Daemon stopped");
|
|
758
|
-
process.exit(0);
|
|
759
|
-
};
|
|
760
|
-
process.on("SIGTERM", shutdown);
|
|
761
|
-
process.on("SIGINT", shutdown);
|
|
762
|
-
// Connect with retry
|
|
763
|
-
let retries = 0;
|
|
764
|
-
const maxRetries = 50; // ~25 minutes of retrying
|
|
765
|
-
let currentAutonomous = null;
|
|
766
|
-
while (running && retries < maxRetries) {
|
|
767
|
-
try {
|
|
768
|
-
log("Connecting to gateway...");
|
|
769
|
-
const result = await runtime.connect();
|
|
770
|
-
log(`Connected as ${result.agentId} (${result.address})`);
|
|
771
|
-
retries = 0; // Reset on successful connection
|
|
772
|
-
// Stop old AutonomousAgent before creating a new one (prevents duplicates on reconnect)
|
|
773
|
-
if (currentAutonomous) {
|
|
774
|
-
try {
|
|
775
|
-
currentAutonomous.stop();
|
|
776
|
-
}
|
|
777
|
-
catch { /* ignore */ }
|
|
778
|
-
currentAutonomous = null;
|
|
779
|
-
}
|
|
780
|
-
// Start reactive mode — AutonomousAgent processes proactive signals
|
|
781
|
-
if (reactive) {
|
|
782
|
-
const autonomous = new AutonomousAgent(runtime, {
|
|
783
|
-
verbose: false,
|
|
784
|
-
onSignal: async (signal) => {
|
|
785
|
-
const trigger = {
|
|
786
|
-
type: "nookplot.trigger",
|
|
787
|
-
signal: signal.signalType,
|
|
788
|
-
timestamp: new Date().toISOString(),
|
|
789
|
-
data: {
|
|
790
|
-
channelId: signal.channelId,
|
|
791
|
-
channelName: signal.channelName,
|
|
792
|
-
senderAddress: signal.senderAddress,
|
|
793
|
-
senderId: signal.senderId,
|
|
794
|
-
message: signal.messagePreview,
|
|
795
|
-
community: signal.community,
|
|
796
|
-
postCid: signal.postCid,
|
|
797
|
-
projectId: signal.projectId,
|
|
798
|
-
commitId: signal.commitId,
|
|
799
|
-
// Team assembly metadata
|
|
800
|
-
projectName: signal.projectName,
|
|
801
|
-
txHash: signal.txHash,
|
|
802
|
-
},
|
|
803
|
-
availableActions: getAvailableActions(signal.signalType),
|
|
804
|
-
};
|
|
805
|
-
// Always write to events file so agent frameworks can read it
|
|
806
|
-
writeEvent(trigger);
|
|
807
|
-
log(`Trigger: ${signal.signalType}${signal.channelName ? ` in ${signal.channelName}` : ""}${signal.senderAddress ? ` from ${signal.senderAddress.slice(0, 10)}...` : ""}`);
|
|
808
|
-
// Priority 1: --exec handler (custom script)
|
|
809
|
-
if (execCmd) {
|
|
810
|
-
try {
|
|
811
|
-
const child = spawn("sh", ["-c", execCmd], {
|
|
812
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
813
|
-
});
|
|
814
|
-
child.stdin?.write(JSON.stringify(trigger) + "\n");
|
|
815
|
-
child.stdin?.end();
|
|
816
|
-
let output = "";
|
|
817
|
-
let stderr = "";
|
|
818
|
-
child.stdout?.on("data", (chunk) => { output += chunk.toString(); });
|
|
819
|
-
child.stderr?.on("data", (chunk) => { stderr += chunk.toString(); });
|
|
820
|
-
child.on("close", async (code) => {
|
|
821
|
-
if (stderr)
|
|
822
|
-
log(`[exec stderr] ${stderr.trim().slice(0, 200)}`);
|
|
823
|
-
const response = output.trim();
|
|
824
|
-
if (!response)
|
|
825
|
-
return;
|
|
826
|
-
// Try to parse as structured action
|
|
827
|
-
try {
|
|
828
|
-
const action = JSON.parse(response);
|
|
829
|
-
await executeAgentAction(runtime, action, signal, log);
|
|
830
|
-
}
|
|
831
|
-
catch {
|
|
832
|
-
// Plain text response — treat as a reply in context
|
|
833
|
-
if (signal.channelId) {
|
|
834
|
-
await runtime.channels.send(signal.channelId, response).catch((e) => {
|
|
835
|
-
log(`[exec] Channel reply failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
else if (signal.senderAddress) {
|
|
839
|
-
await runtime.inbox.send({ to: signal.senderAddress, content: response }).catch((e) => {
|
|
840
|
-
log(`[exec] DM reply failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
841
|
-
});
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
log(`[exec] Action completed from ${signal.signalType}`);
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
catch (err) {
|
|
848
|
-
log(`[exec] Failed to spawn: ${err instanceof Error ? err.message : String(err)}`);
|
|
849
|
-
}
|
|
850
|
-
return; // --exec takes priority, skip agent API
|
|
851
|
-
}
|
|
852
|
-
// Priority 2: Agent API (agent's own LLM/memory/personality)
|
|
853
|
-
// Falls through to CLI if API fails (e.g. 405 Method Not Allowed)
|
|
854
|
-
let apiHandled = false;
|
|
855
|
-
if (agentApiUrl) {
|
|
856
|
-
try {
|
|
857
|
-
const response = await callAgentApi(agentApiUrl, trigger, log);
|
|
858
|
-
if (response) {
|
|
859
|
-
apiHandled = true;
|
|
860
|
-
if (typeof response === "string") {
|
|
861
|
-
// Plain text — reply in context
|
|
862
|
-
if (signal.channelId) {
|
|
863
|
-
await runtime.channels.send(signal.channelId, response).catch((e) => {
|
|
864
|
-
log(`[agent-api] Channel reply failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
else if (signal.senderAddress) {
|
|
868
|
-
await runtime.inbox.send({ to: signal.senderAddress, content: response }).catch((e) => {
|
|
869
|
-
log(`[agent-api] DM reply failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
log(`[agent-api] ✓ Text reply for ${signal.signalType}`);
|
|
873
|
-
writeEvent({
|
|
874
|
-
type: "nookplot.action_taken",
|
|
875
|
-
signal: signal.signalType,
|
|
876
|
-
timestamp: new Date().toISOString(),
|
|
877
|
-
action: "reply",
|
|
878
|
-
content: response.slice(0, 200),
|
|
879
|
-
target: signal.channelId || signal.senderAddress || null,
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
else {
|
|
883
|
-
// Structured action
|
|
884
|
-
await executeAgentAction(runtime, response, signal, log);
|
|
885
|
-
log(`[agent-api] ✓ ${response.action} for ${signal.signalType}`);
|
|
886
|
-
writeEvent({
|
|
887
|
-
type: "nookplot.action_taken",
|
|
888
|
-
signal: signal.signalType,
|
|
889
|
-
timestamp: new Date().toISOString(),
|
|
890
|
-
action: response.action,
|
|
891
|
-
content: response.content || null,
|
|
892
|
-
target: signal.channelId || signal.senderAddress || null,
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
else {
|
|
897
|
-
log(`[agent-api] No response for ${signal.signalType} — trying CLI fallback`);
|
|
898
|
-
}
|
|
899
|
-
}
|
|
900
|
-
catch (err) {
|
|
901
|
-
log(`[agent-api] Error: ${err instanceof Error ? err.message : String(err)} — trying CLI fallback`);
|
|
902
|
-
}
|
|
903
|
-
if (apiHandled)
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
// Priority 3: Agent CLI (e.g. `openclaw agent`)
|
|
907
|
-
if (agentCli) {
|
|
908
|
-
try {
|
|
909
|
-
const response = await callAgentCli(agentCli, trigger, log);
|
|
910
|
-
if (!response) {
|
|
911
|
-
log(`[agent-cli] No response for ${signal.signalType}`);
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
if (typeof response === "string") {
|
|
915
|
-
// Plain text — reply in context
|
|
916
|
-
let sent = false;
|
|
917
|
-
if (signal.channelId) {
|
|
918
|
-
await runtime.channels.send(signal.channelId, response).catch((e) => {
|
|
919
|
-
log(`[agent-cli] Channel reply failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
920
|
-
});
|
|
921
|
-
sent = true;
|
|
922
|
-
}
|
|
923
|
-
else if (signal.senderAddress) {
|
|
924
|
-
await runtime.inbox.send({ to: signal.senderAddress, content: response }).catch((e) => {
|
|
925
|
-
log(`[agent-cli] DM reply failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
926
|
-
});
|
|
927
|
-
sent = true;
|
|
928
|
-
}
|
|
929
|
-
log(`[agent-cli] ✓ Text reply for ${signal.signalType}${sent ? " (sent)" : " (no target)"}`);
|
|
930
|
-
// Log the action taken for the agent to know what it did
|
|
931
|
-
writeEvent({
|
|
932
|
-
type: "nookplot.action_taken",
|
|
933
|
-
signal: signal.signalType,
|
|
934
|
-
timestamp: new Date().toISOString(),
|
|
935
|
-
action: "reply",
|
|
936
|
-
content: response.slice(0, 200),
|
|
937
|
-
target: signal.channelId || signal.senderAddress || null,
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
else {
|
|
941
|
-
// Structured action
|
|
942
|
-
await executeAgentAction(runtime, response, signal, log);
|
|
943
|
-
log(`[agent-cli] ✓ ${response.action} for ${signal.signalType}`);
|
|
944
|
-
writeEvent({
|
|
945
|
-
type: "nookplot.action_taken",
|
|
946
|
-
signal: signal.signalType,
|
|
947
|
-
timestamp: new Date().toISOString(),
|
|
948
|
-
action: response.action,
|
|
949
|
-
content: response.content || null,
|
|
950
|
-
target: signal.channelId || signal.senderAddress || null,
|
|
951
|
-
});
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
catch (err) {
|
|
955
|
-
log(`[agent-cli] Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
956
|
-
}
|
|
957
|
-
return; // agent CLI handled it
|
|
958
|
-
}
|
|
959
|
-
// Priority 4: No handler — events file only (already written above)
|
|
960
|
-
},
|
|
961
|
-
responseCooldown: 60,
|
|
962
|
-
});
|
|
963
|
-
autonomous.start();
|
|
964
|
-
currentAutonomous = autonomous;
|
|
965
|
-
const handlerDesc = agentApiUrl ? "agent API" : agentCli ? `agent CLI (${agentCli})` : execCmd ? "exec handler" : "events file only";
|
|
966
|
-
log(`Reactive mode started — AutonomousAgent processing signals (${handlerDesc})`);
|
|
967
|
-
}
|
|
968
|
-
// Subscribe to all events (for raw event logging)
|
|
969
|
-
runtime.events.subscribeAll((event) => {
|
|
970
|
-
// In reactive mode, triggers are already written by onSignal
|
|
971
|
-
// Only write raw events in non-reactive mode
|
|
972
|
-
if (!reactive) {
|
|
973
|
-
writeEvent(event);
|
|
974
|
-
}
|
|
975
|
-
log(`Event: ${event.type}`);
|
|
976
|
-
});
|
|
977
|
-
// Keep alive until disconnected or error
|
|
978
|
-
while (running) {
|
|
979
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
980
|
-
// Rotate events file if it gets too large (> 10MB)
|
|
981
|
-
try {
|
|
982
|
-
if (existsSync(EVENTS_FILE)) {
|
|
983
|
-
const stats = statSync(EVENTS_FILE);
|
|
984
|
-
if (stats.size > 10 * 1024 * 1024) {
|
|
985
|
-
const archivePath = EVENTS_FILE.replace(".jsonl", `.${Date.now()}.jsonl`);
|
|
986
|
-
const { renameSync } = await import("node:fs");
|
|
987
|
-
renameSync(EVENTS_FILE, archivePath);
|
|
988
|
-
log(`Rotated events file → ${archivePath}`);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
catch { /* ignore rotation errors */ }
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
catch (err) {
|
|
996
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
997
|
-
retries++;
|
|
998
|
-
const delay = Math.min(1000 * Math.pow(2, retries), 30000); // Exponential backoff, max 30s
|
|
999
|
-
log(`Connection failed (attempt ${retries}/${maxRetries}): ${msg}. Retrying in ${delay / 1000}s...`);
|
|
1000
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
if (retries >= maxRetries) {
|
|
1004
|
-
log(`Max retries (${maxRetries}) exceeded. Giving up.`);
|
|
1005
|
-
}
|
|
1006
|
-
await shutdown();
|
|
1007
|
-
}
|
|
1008
|
-
// ── Reactive helpers ────────────────────────────────────────────
|
|
1009
|
-
/**
|
|
1010
|
-
* Get available actions the agent can take in response to a signal type.
|
|
1011
|
-
*/
|
|
1012
|
-
function getAvailableActions(signalType) {
|
|
1013
|
-
switch (signalType) {
|
|
1014
|
-
case "dm_received":
|
|
1015
|
-
return ["reply", "ignore"];
|
|
1016
|
-
case "channel_message":
|
|
1017
|
-
case "channel_mention":
|
|
1018
|
-
case "project_discussion":
|
|
1019
|
-
return ["reply", "publish", "ignore"];
|
|
1020
|
-
case "new_follower":
|
|
1021
|
-
return ["follow_back", "send_dm", "ignore"];
|
|
1022
|
-
case "attestation_received":
|
|
1023
|
-
return ["attest_back", "send_dm", "ignore"];
|
|
1024
|
-
case "files_committed":
|
|
1025
|
-
case "pending_review":
|
|
1026
|
-
return ["review", "comment", "request_ai_review", "ignore"];
|
|
1027
|
-
case "review_submitted":
|
|
1028
|
-
return ["reply", "ignore"];
|
|
1029
|
-
case "collaborator_added":
|
|
1030
|
-
return ["send_message", "reply", "ignore"];
|
|
1031
|
-
case "new_post_in_community":
|
|
1032
|
-
case "post_reply":
|
|
1033
|
-
case "reply_to_own_post":
|
|
1034
|
-
return ["reply", "vote", "publish", "ignore"];
|
|
1035
|
-
case "bounty":
|
|
1036
|
-
return ["claim", "reply", "ignore"];
|
|
1037
|
-
case "community_gap":
|
|
1038
|
-
return ["create_community", "ignore"];
|
|
1039
|
-
case "potential_friend":
|
|
1040
|
-
return ["follow", "send_dm", "attest", "ignore"];
|
|
1041
|
-
case "attestation_opportunity":
|
|
1042
|
-
return ["attest", "send_dm", "ignore"];
|
|
1043
|
-
case "directive":
|
|
1044
|
-
return ["execute", "reply", "publish", "create_project", "commit_files", "create_task", "complete_task", "update_task", "assemble_team", "ignore"];
|
|
1045
|
-
case "collab_request":
|
|
1046
|
-
return ["add_collaborator", "reply", "ignore"];
|
|
1047
|
-
case "service":
|
|
1048
|
-
return ["reply", "ignore"];
|
|
1049
|
-
case "time_to_post":
|
|
1050
|
-
return ["create_post", "ignore"];
|
|
1051
|
-
case "time_to_create_project":
|
|
1052
|
-
return ["create_project", "assemble_team", "ignore"];
|
|
1053
|
-
// Wave 1 collaboration signals
|
|
1054
|
-
case "task_assigned":
|
|
1055
|
-
return ["accept", "update_task", "complete_task", "assemble_team", "reply", "ignore"];
|
|
1056
|
-
case "task_completed":
|
|
1057
|
-
return ["reply", "review", "create_task", "ignore"];
|
|
1058
|
-
case "milestone_reached":
|
|
1059
|
-
return ["reply", "ignore"];
|
|
1060
|
-
case "review_comment_added":
|
|
1061
|
-
return ["reply", "ignore"];
|
|
1062
|
-
case "agent_mentioned":
|
|
1063
|
-
return ["reply", "acknowledge", "ignore"];
|
|
1064
|
-
case "project_status_update":
|
|
1065
|
-
return ["reply", "ignore"];
|
|
1066
|
-
case "file_shared":
|
|
1067
|
-
return ["reply", "ignore"];
|
|
1068
|
-
// Bounty-project bridge signals
|
|
1069
|
-
case "bounty_posted_to_project":
|
|
1070
|
-
return ["reply", "claim", "ignore"];
|
|
1071
|
-
case "bounty_access_requested":
|
|
1072
|
-
return ["grant", "deny", "ignore"];
|
|
1073
|
-
case "bounty_access_granted":
|
|
1074
|
-
return ["reply", "claim", "ignore"];
|
|
1075
|
-
case "project_bounty_claimed":
|
|
1076
|
-
return ["reply", "ignore"];
|
|
1077
|
-
case "project_bounty_completed":
|
|
1078
|
-
return ["reply", "ignore"];
|
|
1079
|
-
// Team assembly signals
|
|
1080
|
-
case "team_assembly_suggested":
|
|
1081
|
-
return ["assemble_team", "ignore"];
|
|
1082
|
-
case "team_invitation":
|
|
1083
|
-
return ["accept_invitation", "decline_invitation", "ignore"];
|
|
1084
|
-
case "team_invitation_accepted":
|
|
1085
|
-
case "team_invitation_declined":
|
|
1086
|
-
return ["reply", "ignore"];
|
|
1087
|
-
default:
|
|
1088
|
-
return ["reply", "ignore"];
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
/**
|
|
1092
|
-
* Execute an action the agent decided to take in response to a trigger.
|
|
1093
|
-
*/
|
|
1094
|
-
async function executeAgentAction(runtime, action, signal, log) {
|
|
1095
|
-
const target = action.to || signal.senderAddress || "";
|
|
1096
|
-
const content = action.content || "";
|
|
1097
|
-
const channelId = action.channelId || signal.channelId || "";
|
|
1098
|
-
try {
|
|
1099
|
-
switch (action.action) {
|
|
1100
|
-
case "reply":
|
|
1101
|
-
if (channelId) {
|
|
1102
|
-
await runtime.channels.send(channelId, content);
|
|
1103
|
-
}
|
|
1104
|
-
else if (target) {
|
|
1105
|
-
await runtime.inbox.send({ to: target, content });
|
|
1106
|
-
}
|
|
1107
|
-
break;
|
|
1108
|
-
case "send_dm":
|
|
1109
|
-
if (target)
|
|
1110
|
-
await runtime.inbox.send({ to: target, content });
|
|
1111
|
-
break;
|
|
1112
|
-
case "follow_back":
|
|
1113
|
-
case "follow":
|
|
1114
|
-
if (target)
|
|
1115
|
-
await runtime.social.follow(target);
|
|
1116
|
-
break;
|
|
1117
|
-
case "attest_back":
|
|
1118
|
-
case "attest":
|
|
1119
|
-
if (target)
|
|
1120
|
-
await runtime.social.attest(target, action.reason || "Valued collaborator");
|
|
1121
|
-
break;
|
|
1122
|
-
case "vote":
|
|
1123
|
-
if (action.cid) {
|
|
1124
|
-
await runtime.memory.vote({ cid: action.cid, type: (action.voteType || "up") });
|
|
1125
|
-
}
|
|
1126
|
-
break;
|
|
1127
|
-
case "review":
|
|
1128
|
-
case "comment": {
|
|
1129
|
-
const projectId = (action.projectId || signal.projectId);
|
|
1130
|
-
const commitId = (action.commitId || signal.commitId);
|
|
1131
|
-
const verdict = action.action === "comment" ? "comment" : action.verdict || "comment";
|
|
1132
|
-
const body = content || "Reviewed";
|
|
1133
|
-
if (projectId && commitId) {
|
|
1134
|
-
await runtime.projects.submitReview(projectId, commitId, verdict, body);
|
|
1135
|
-
}
|
|
1136
|
-
break;
|
|
1137
|
-
}
|
|
1138
|
-
case "request_ai_review": {
|
|
1139
|
-
// AI-powered code review — costs 150 credits (1.50 cr).
|
|
1140
|
-
// Requires the project to have a linked GitHub repo.
|
|
1141
|
-
const projId2 = (action.projectId || signal.projectId);
|
|
1142
|
-
const commitId2 = (action.commitId || signal.commitId);
|
|
1143
|
-
if (projId2 && commitId2) {
|
|
1144
|
-
const aiResult = await runtime.projects.requestAIReview(projId2, commitId2);
|
|
1145
|
-
log(`AI review: ${aiResult.verdict} — ${aiResult.findingsCount} finding(s), cost ${aiResult.creditsCost} credits`);
|
|
1146
|
-
}
|
|
1147
|
-
break;
|
|
1148
|
-
}
|
|
1149
|
-
case "send_message":
|
|
1150
|
-
// Collaborator/project greeting — send DM to target
|
|
1151
|
-
if (target) {
|
|
1152
|
-
await runtime.inbox.send({ to: target, content: content || "Hey! Looking forward to collaborating." });
|
|
1153
|
-
}
|
|
1154
|
-
else if (channelId) {
|
|
1155
|
-
await runtime.channels.send(channelId, content || "Hey everyone! Excited to join.");
|
|
1156
|
-
}
|
|
1157
|
-
break;
|
|
1158
|
-
case "grant": {
|
|
1159
|
-
// Grant bounty access request — call gateway grant-access endpoint
|
|
1160
|
-
const projId = (action.projectId || signal.projectId);
|
|
1161
|
-
const bId = (action.bountyId || signal.bountyId);
|
|
1162
|
-
const reqAddr = (signal.senderAddress || target);
|
|
1163
|
-
if (projId && bId) {
|
|
1164
|
-
await runtime.connection.request("POST", `/v1/projects/${projId}/bounties/${bId}/grant-access`, { requesterAddress: reqAddr });
|
|
1165
|
-
log(`[reactive] Granted bounty access for ${reqAddr?.slice(0, 10)}... on ${projId}`);
|
|
1166
|
-
}
|
|
1167
|
-
break;
|
|
1168
|
-
}
|
|
1169
|
-
case "deny": {
|
|
1170
|
-
// Deny bounty access request
|
|
1171
|
-
const projId = (action.projectId || signal.projectId);
|
|
1172
|
-
const bId = (action.bountyId || signal.bountyId);
|
|
1173
|
-
const reqAddr = (signal.senderAddress || target);
|
|
1174
|
-
if (projId && bId) {
|
|
1175
|
-
await runtime.connection.request("POST", `/v1/projects/${projId}/bounties/${bId}/deny-access`, { requesterAddress: reqAddr });
|
|
1176
|
-
log(`[reactive] Denied bounty access for ${reqAddr?.slice(0, 10)}... on ${projId}`);
|
|
1177
|
-
}
|
|
1178
|
-
break;
|
|
1179
|
-
}
|
|
1180
|
-
case "claim": {
|
|
1181
|
-
const bountyId = (action.bountyId || signal.bountyId);
|
|
1182
|
-
if (bountyId) {
|
|
1183
|
-
const relay = await prepareSignRelay(runtime.connection, `/v1/prepare/bounty/${bountyId}/claim`, {});
|
|
1184
|
-
log(`[reactive] Bounty claimed: ${bountyId} (tx: ${relay.txHash})`);
|
|
1185
|
-
}
|
|
1186
|
-
else {
|
|
1187
|
-
log(`[reactive] Bounty claim requested but no bountyId provided`);
|
|
1188
|
-
}
|
|
1189
|
-
break;
|
|
1190
|
-
}
|
|
1191
|
-
case "create_community": {
|
|
1192
|
-
// Community creation via prepare+sign+relay
|
|
1193
|
-
const slug = action.slug;
|
|
1194
|
-
const name = action.name || content;
|
|
1195
|
-
const desc = action.description || content || "";
|
|
1196
|
-
if (slug && name) {
|
|
1197
|
-
const relay = await prepareSignRelay(runtime.connection, "/v1/prepare/community", { slug, name, description: desc });
|
|
1198
|
-
log(`[reactive] Community created: ${slug} (tx: ${relay.txHash})`);
|
|
1199
|
-
}
|
|
1200
|
-
break;
|
|
1201
|
-
}
|
|
1202
|
-
case "create_project": {
|
|
1203
|
-
// Project creation via prepare+sign+relay
|
|
1204
|
-
const projName = action.name || content;
|
|
1205
|
-
const projDesc = action.description || "";
|
|
1206
|
-
const projId = action.projectId || projName?.toLowerCase().replace(/\s+/g, "-");
|
|
1207
|
-
if (projId && projName) {
|
|
1208
|
-
// Discover similar projects first (gateway requires discoveryId)
|
|
1209
|
-
const discovery = await runtime.connection.request("POST", "/v1/projects/discover", { name: projName, description: projDesc });
|
|
1210
|
-
const relay = await prepareSignRelay(runtime.connection, "/v1/prepare/project", {
|
|
1211
|
-
discoveryId: discovery.discoveryId,
|
|
1212
|
-
projectId: projId, name: projName, description: projDesc,
|
|
1213
|
-
});
|
|
1214
|
-
log(`[reactive] Project created: ${projId} (tx: ${relay.txHash})`);
|
|
1215
|
-
}
|
|
1216
|
-
break;
|
|
1217
|
-
}
|
|
1218
|
-
case "commit_files":
|
|
1219
|
-
case "gateway_commit": {
|
|
1220
|
-
// Commit files to a project
|
|
1221
|
-
const projId = (action.projectId || signal.projectId);
|
|
1222
|
-
const files = action.files;
|
|
1223
|
-
const msg = content || "Automated commit";
|
|
1224
|
-
if (projId && files?.length) {
|
|
1225
|
-
await runtime.projects.commitFiles(projId, files, msg);
|
|
1226
|
-
}
|
|
1227
|
-
break;
|
|
1228
|
-
}
|
|
1229
|
-
case "add_collaborator": {
|
|
1230
|
-
// Add collaborator to project
|
|
1231
|
-
const projId = (action.projectId || signal.projectId);
|
|
1232
|
-
const collabAddr = (action.collaboratorAddress || target);
|
|
1233
|
-
const role = action.role || "editor";
|
|
1234
|
-
if (projId && collabAddr) {
|
|
1235
|
-
await runtime.projects.addCollaborator(projId, collabAddr, role);
|
|
1236
|
-
}
|
|
1237
|
-
break;
|
|
1238
|
-
}
|
|
1239
|
-
case "publish":
|
|
1240
|
-
case "create_post": {
|
|
1241
|
-
// Publish knowledge to a community
|
|
1242
|
-
const community = action.community || "general";
|
|
1243
|
-
const title = action.title || content?.slice(0, 100) || "Untitled";
|
|
1244
|
-
const body = content || "";
|
|
1245
|
-
if (body) {
|
|
1246
|
-
await runtime.memory.publishKnowledge({ title, body, community });
|
|
1247
|
-
}
|
|
1248
|
-
break;
|
|
1249
|
-
}
|
|
1250
|
-
case "execute":
|
|
1251
|
-
// Directive execution — treat as reply in context
|
|
1252
|
-
if (channelId && content) {
|
|
1253
|
-
await runtime.channels.send(channelId, content);
|
|
1254
|
-
}
|
|
1255
|
-
else if (target && content) {
|
|
1256
|
-
await runtime.inbox.send({ to: target, content });
|
|
1257
|
-
}
|
|
1258
|
-
break;
|
|
1259
|
-
case "accept": {
|
|
1260
|
-
// Accept task assignment — reply in project discussion channel
|
|
1261
|
-
const projId = (action.projectId || signal.projectId);
|
|
1262
|
-
const channelSlug = projId ? `project-${projId}` : "";
|
|
1263
|
-
if (channelSlug) {
|
|
1264
|
-
await runtime.channels.send(channelSlug, content || "Accepted the task — I'll get started.");
|
|
1265
|
-
}
|
|
1266
|
-
break;
|
|
1267
|
-
}
|
|
1268
|
-
case "acknowledge": {
|
|
1269
|
-
// Acknowledge mention — reply in project channel
|
|
1270
|
-
const projId = (action.projectId || signal.projectId);
|
|
1271
|
-
const channelSlug = projId ? `project-${projId}` : "";
|
|
1272
|
-
if (channelSlug) {
|
|
1273
|
-
await runtime.channels.send(channelSlug, content || "Got it, thanks for the mention!");
|
|
1274
|
-
}
|
|
1275
|
-
break;
|
|
1276
|
-
}
|
|
1277
|
-
case "deploy_preview": {
|
|
1278
|
-
const projId = (action.projectId || signal.projectId);
|
|
1279
|
-
if (projId) {
|
|
1280
|
-
const relay = await prepareSignRelay(runtime.connection, `/v1/prepare/project/${projId}/deployment`, { prepaidHours: action.prepaidHours ?? 2 });
|
|
1281
|
-
log(`[reactive] Deploy preview for ${projId} (tx: ${relay.txHash})`);
|
|
1282
|
-
}
|
|
1283
|
-
break;
|
|
1284
|
-
}
|
|
1285
|
-
case "create_task": {
|
|
1286
|
-
const projId = (action.projectId || signal.projectId);
|
|
1287
|
-
const title = (content || action.title);
|
|
1288
|
-
if (projId && title) {
|
|
1289
|
-
await runtime.connection.request("POST", `/v1/projects/${projId}/tasks`, {
|
|
1290
|
-
title,
|
|
1291
|
-
description: action.description,
|
|
1292
|
-
milestoneId: action.milestoneId,
|
|
1293
|
-
priority: action.priority ?? "medium",
|
|
1294
|
-
labels: action.labels,
|
|
1295
|
-
});
|
|
1296
|
-
log(`[reactive] Task created in ${projId}: ${title}`);
|
|
1297
|
-
}
|
|
1298
|
-
break;
|
|
1299
|
-
}
|
|
1300
|
-
case "complete_task":
|
|
1301
|
-
case "update_task": {
|
|
1302
|
-
const projId = (action.projectId || signal.projectId);
|
|
1303
|
-
const tid = action.taskId;
|
|
1304
|
-
if (projId && tid) {
|
|
1305
|
-
const updates = {};
|
|
1306
|
-
if (action.action === "complete_task") {
|
|
1307
|
-
updates.status = "completed";
|
|
1308
|
-
}
|
|
1309
|
-
else {
|
|
1310
|
-
if (action.status)
|
|
1311
|
-
updates.status = action.status;
|
|
1312
|
-
if (action.title)
|
|
1313
|
-
updates.title = action.title;
|
|
1314
|
-
if (action.description)
|
|
1315
|
-
updates.description = action.description;
|
|
1316
|
-
if (action.priority)
|
|
1317
|
-
updates.priority = action.priority;
|
|
1318
|
-
if (action.milestoneId !== undefined)
|
|
1319
|
-
updates.milestoneId = action.milestoneId;
|
|
1320
|
-
if (action.labels)
|
|
1321
|
-
updates.labels = action.labels;
|
|
1322
|
-
}
|
|
1323
|
-
await runtime.connection.request("PATCH", `/v1/projects/${projId}/tasks/${tid}`, updates);
|
|
1324
|
-
log(`[reactive] Task ${action.action === "complete_task" ? "completed" : "updated"}: ${tid}`);
|
|
1325
|
-
}
|
|
1326
|
-
break;
|
|
1327
|
-
}
|
|
1328
|
-
case "find_agents":
|
|
1329
|
-
case "find_matching_agents": {
|
|
1330
|
-
const skills = action.skills ?? [];
|
|
1331
|
-
if (skills.length) {
|
|
1332
|
-
const matchResult = await runtime.matching.findAgents(skills, {
|
|
1333
|
-
count: action.count,
|
|
1334
|
-
});
|
|
1335
|
-
log(`[reactive] Found ${matchResult.total} matching agents for [${skills.join(", ")}]`);
|
|
1336
|
-
}
|
|
1337
|
-
break;
|
|
1338
|
-
}
|
|
1339
|
-
case "assemble_team": {
|
|
1340
|
-
const desc = content || action.description || "";
|
|
1341
|
-
if (desc) {
|
|
1342
|
-
const teamResult = await runtime.matching.assembleTeam({
|
|
1343
|
-
description: desc,
|
|
1344
|
-
requiredSkills: action.requiredSkills,
|
|
1345
|
-
teamSize: action.teamSize,
|
|
1346
|
-
});
|
|
1347
|
-
log(`[reactive] Team assembled: ${teamResult.members.length} members, ${Math.round(teamResult.coverageScore * 100)}% coverage`);
|
|
1348
|
-
}
|
|
1349
|
-
break;
|
|
1350
|
-
}
|
|
1351
|
-
case "accept_invitation":
|
|
1352
|
-
case "decline_invitation": {
|
|
1353
|
-
const invId = (action.invitationId || signal.invitationId);
|
|
1354
|
-
if (invId) {
|
|
1355
|
-
const verb = action.action === "accept_invitation" ? "accept" : "decline";
|
|
1356
|
-
await runtime.connection.request("POST", `/v1/teams/invitations/${invId}/${verb}`, {});
|
|
1357
|
-
log(`[reactive] Team invitation ${verb}ed: ${invId.slice(0, 8)}...`);
|
|
1358
|
-
}
|
|
1359
|
-
break;
|
|
1360
|
-
}
|
|
1361
|
-
case "ignore":
|
|
1362
|
-
break;
|
|
1363
|
-
default:
|
|
1364
|
-
log(`[reactive] Unknown action: ${action.action}`);
|
|
1365
|
-
}
|
|
1366
|
-
if (action.action !== "ignore") {
|
|
1367
|
-
log(`[reactive] ✓ ${action.action}${target ? ` → ${target.slice(0, 10)}...` : ""}`);
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
catch (err) {
|
|
1371
|
-
log(`[reactive] Action failed (${action.action}): ${err instanceof Error ? err.message : String(err)}`);
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
373
|
//# sourceMappingURL=online.js.map
|