@nookplot/cli 0.6.14 → 0.6.15

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.
@@ -25,343 +25,26 @@
25
25
  *
26
26
  * @module commands/online
27
27
  */
28
- import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, unlinkSync, statSync } from "node:fs";
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); // Signal 0 = just check existence
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) // Allow internal flags without error
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
- await runDaemonLoop(config, reactive, cmdOpts.exec, agentApiUrl, agentCli);
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(` ${e}`));
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 for server-push signal delivery
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 mode is on (required for signals)
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(` Callback registered ${callbackUrl}`));
216
+ console.log(chalk.green(` Callback registered -> ${callbackUrl}`));
517
217
  }
518
218
  }
519
219
  }
520
- // Auto-detect agent handler for autonomous responses (unless --exec is set)
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
- // If --agent-api was explicitly set, use that; otherwise detect
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(" Triggers will be routed through your agent's own LLM/personality"));
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(" Triggers will be routed through your agent via CLI"));
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 a child process that runs this same command with --_daemon
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, // node
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(` Reactive + Agent API — auto-responding as your agent`));
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(` Reactive + Agent CLI — auto-responding via ${agentCliBinary}`));
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(` Reactive + Exec handler`));
282
+ console.log(chalk.green(` Reactive + Exec handler`));
592
283
  }
593
284
  else {
594
- console.log(chalk.green(` Reactive mode — triggers ${EVENTS_FILE}`));
285
+ console.log(chalk.green(` Reactive mode — triggers -> ${EVENTS_FILE}`));
595
286
  }
596
287
  }
597
- if (cmdOpts.exec) {
598
- console.log(chalk.dim(` Exec ${cmdOpts.exec}`));
599
- }
600
- if (agentApiUrl) {
601
- console.log(chalk.dim(` Agent → ${agentApiUrl}`));
602
- }
603
- if (agentCliBinary) {
604
- console.log(chalk.dim(` Agent ${agentCliBinary} agent`));
605
- }
606
- if (callbackUrl) {
607
- console.log(chalk.dim(` Callback ${callbackUrl}`));
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(` Stopped (PID ${pid})`));
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(" Offline"));
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(` 🟢 Online (PID ${pid})`));
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 malformed lines */ }
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(` 📨 ${eventCount} events received${summary ? ` (${summary})` : ""}`));
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