@calltelemetry/openclaw-linear 0.6.1 → 0.7.0

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/src/infra/cli.ts CHANGED
@@ -13,6 +13,13 @@ import { resolveLinearToken, AUTH_PROFILES_PATH, LINEAR_GRAPHQL_URL } from "../a
13
13
  import { LINEAR_OAUTH_AUTH_URL, LINEAR_OAUTH_TOKEN_URL, LINEAR_AGENT_SCOPES } from "../api/auth.js";
14
14
  import { listWorktrees } from "./codex-worktree.js";
15
15
  import { loadPrompts, clearPromptCache } from "../pipeline/pipeline.js";
16
+ import {
17
+ formatMessage,
18
+ parseNotificationsConfig,
19
+ sendToTarget,
20
+ type NotifyKind,
21
+ type NotifyPayload,
22
+ } from "./notify.js";
16
23
 
17
24
  function prompt(question: string): Promise<string> {
18
25
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -343,6 +350,186 @@ export function registerCli(program: Command, api: OpenClawPluginApi): void {
343
350
  }
344
351
  });
345
352
 
353
+ // --- openclaw openclaw-linear notify ---
354
+ const notifyCmd = linear
355
+ .command("notify")
356
+ .description("Manage dispatch lifecycle notifications");
357
+
358
+ notifyCmd
359
+ .command("status")
360
+ .description("Show current notification target configuration")
361
+ .action(async () => {
362
+ const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
363
+ const config = parseNotificationsConfig(pluginConfig);
364
+
365
+ console.log("\nNotification Targets");
366
+ console.log("─".repeat(50));
367
+
368
+ if (!config.targets?.length) {
369
+ console.log("\n No notification targets configured.");
370
+ console.log(" Run 'openclaw openclaw-linear notify setup' to configure.\n");
371
+ return;
372
+ }
373
+
374
+ for (const t of config.targets) {
375
+ const acct = t.accountId ? ` (account: ${t.accountId})` : "";
376
+ console.log(` ${t.channel}: ${t.target}${acct}`);
377
+ }
378
+
379
+ // Show event toggles if any are suppressed
380
+ const suppressed = Object.entries(config.events ?? {})
381
+ .filter(([, v]) => v === false)
382
+ .map(([k]) => k);
383
+ if (suppressed.length > 0) {
384
+ console.log(`\n Suppressed events: ${suppressed.join(", ")}`);
385
+ }
386
+
387
+ console.log();
388
+ });
389
+
390
+ notifyCmd
391
+ .command("test")
392
+ .description("Send a test notification to all configured targets")
393
+ .option("--channel <name>", "Test only targets for a specific channel (discord, slack, telegram, etc.)")
394
+ .action(async (opts: { channel?: string }) => {
395
+ const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
396
+ const config = parseNotificationsConfig(pluginConfig);
397
+
398
+ const testPayload: NotifyPayload = {
399
+ identifier: "TEST-0",
400
+ title: "Test notification from Linear plugin",
401
+ status: "test",
402
+ };
403
+ const testKind: NotifyKind = "dispatch";
404
+ const message = formatMessage(testKind, testPayload);
405
+
406
+ console.log("\nSending test notification...\n");
407
+
408
+ if (!config.targets?.length) {
409
+ console.error(" No notification targets configured. Run 'openclaw openclaw-linear notify setup' first.\n");
410
+ process.exitCode = 1;
411
+ return;
412
+ }
413
+
414
+ const targets = opts.channel
415
+ ? config.targets.filter((t) => t.channel === opts.channel)
416
+ : config.targets;
417
+
418
+ if (targets.length === 0) {
419
+ console.error(` No targets found for channel "${opts.channel}".\n`);
420
+ process.exitCode = 1;
421
+ return;
422
+ }
423
+
424
+ for (const target of targets) {
425
+ try {
426
+ await sendToTarget(target, message, api.runtime);
427
+ console.log(` ${target.channel}: SENT to ${target.target}`);
428
+ console.log(` "${message}"`);
429
+ } catch (err) {
430
+ console.error(` ${target.channel}: FAILED — ${err instanceof Error ? err.message : String(err)}`);
431
+ }
432
+ }
433
+
434
+ console.log();
435
+ });
436
+
437
+ notifyCmd
438
+ .command("setup")
439
+ .description("Interactive setup for notification targets")
440
+ .action(async () => {
441
+ const pluginConfig = (api as any).pluginConfig as Record<string, unknown> | undefined;
442
+ const config = parseNotificationsConfig(pluginConfig);
443
+
444
+ console.log("\nNotification Target Setup");
445
+ console.log("─".repeat(50));
446
+ console.log(" Dispatch lifecycle notifications can be sent to any OpenClaw channel.");
447
+ console.log(" Add multiple targets for fan-out delivery.\n");
448
+
449
+ // Show current targets
450
+ if (config.targets?.length) {
451
+ console.log(" Current targets:");
452
+ for (const t of config.targets) {
453
+ const acct = t.accountId ? ` (account: ${t.accountId})` : "";
454
+ console.log(` ${t.channel}: ${t.target}${acct}`);
455
+ }
456
+ console.log();
457
+ }
458
+
459
+ const newTargets = [...(config.targets ?? [])];
460
+ const supportedChannels = ["discord", "slack", "telegram", "signal"];
461
+
462
+ // Add targets loop
463
+ let addMore = true;
464
+ while (addMore) {
465
+ const channelAnswer = await prompt(
466
+ `Add notification target? (${supportedChannels.join("/")}) or blank to finish: `,
467
+ );
468
+ if (!channelAnswer) {
469
+ addMore = false;
470
+ break;
471
+ }
472
+
473
+ const channel = channelAnswer.toLowerCase().trim();
474
+ const targetId = await prompt(` ${channel} target ID (channel/group/user): `);
475
+ if (!targetId) continue;
476
+
477
+ let accountId: string | undefined;
478
+ if (channel === "slack") {
479
+ const acct = await prompt(" Slack account ID (leave blank for default): ");
480
+ accountId = acct || undefined;
481
+ }
482
+
483
+ newTargets.push({ channel, target: targetId, ...(accountId ? { accountId } : {}) });
484
+ console.log(` Added: ${channel} → ${targetId}\n`);
485
+ }
486
+
487
+ // Summary
488
+ console.log("\nConfiguration Summary");
489
+ console.log("─".repeat(50));
490
+ if (newTargets.length === 0) {
491
+ console.log(" No targets configured (notifications disabled).");
492
+ } else {
493
+ for (const t of newTargets) {
494
+ const acct = t.accountId ? ` (account: ${t.accountId})` : "";
495
+ console.log(` ${t.channel}: ${t.target}${acct}`);
496
+ }
497
+ }
498
+
499
+ if (JSON.stringify(newTargets) === JSON.stringify(config.targets ?? [])) {
500
+ console.log("\n No changes made.\n");
501
+ return;
502
+ }
503
+
504
+ // Write config
505
+ const confirmAnswer = await prompt("\nApply these changes? [Y/n]: ");
506
+ if (confirmAnswer.toLowerCase() === "n") {
507
+ console.log(" Aborted.\n");
508
+ return;
509
+ }
510
+
511
+ try {
512
+ const runtimeConfig = api.runtime.config.loadConfig() as Record<string, any>;
513
+ const pluginEntries = runtimeConfig.plugins?.entries ?? {};
514
+ const linearConfig = pluginEntries["openclaw-linear"]?.config ?? {};
515
+ linearConfig.notifications = {
516
+ ...linearConfig.notifications,
517
+ targets: newTargets,
518
+ };
519
+ pluginEntries["openclaw-linear"] = {
520
+ ...pluginEntries["openclaw-linear"],
521
+ config: linearConfig,
522
+ };
523
+ runtimeConfig.plugins = { ...runtimeConfig.plugins, entries: pluginEntries };
524
+ api.runtime.config.writeConfigFile(runtimeConfig);
525
+ console.log("\n Configuration saved. Restart gateway to apply: systemctl --user restart openclaw-gateway\n");
526
+ } catch (err) {
527
+ console.error(`\n Failed to save config: ${err instanceof Error ? err.message : String(err)}`);
528
+ console.error(" You can manually add these values to openclaw.json → plugins.entries.openclaw-linear.config\n");
529
+ process.exitCode = 1;
530
+ }
531
+ });
532
+
346
533
  // --- openclaw openclaw-linear doctor ---
347
534
  linear
348
535
  .command("doctor")
@@ -258,12 +258,12 @@ describe("checkConnectivity", () => {
258
258
  expect(apiCheck?.severity).toBe("pass");
259
259
  });
260
260
 
261
- it("reports Discord not configured as pass", async () => {
261
+ it("reports notifications not configured as pass", async () => {
262
262
  vi.stubGlobal("fetch", vi.fn(async () => { throw new Error("should not be called"); }));
263
263
  const checks = await checkConnectivity({});
264
- const discordCheck = checks.find((c) => c.label.includes("Discord"));
265
- expect(discordCheck?.severity).toBe("pass");
266
- expect(discordCheck?.label).toContain("not configured");
264
+ const notifCheck = checks.find((c) => c.label.includes("Notifications"));
265
+ expect(notifCheck?.severity).toBe("pass");
266
+ expect(notifCheck?.label).toContain("not configured");
267
267
  });
268
268
 
269
269
  it("reports webhook skip when gateway not running", async () => {
@@ -530,36 +530,14 @@ export async function checkConnectivity(pluginConfig?: Record<string, unknown>,
530
530
  }
531
531
  }
532
532
 
533
- // Discord notifications
534
- const flowDiscordChannel = pluginConfig?.flowDiscordChannel as string | undefined;
535
- if (!flowDiscordChannel) {
536
- checks.push(pass("Discord notifications: not configured (skipped)"));
533
+ // Notification targets
534
+ const notifRaw = pluginConfig?.notifications as { targets?: { channel: string; target: string }[] } | undefined;
535
+ const notifTargets = notifRaw?.targets ?? [];
536
+ if (notifTargets.length === 0) {
537
+ checks.push(pass("Notifications: not configured (skipped)"));
537
538
  } else {
538
- // Read Discord bot token from openclaw.json
539
- let discordBotToken: string | undefined;
540
- try {
541
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
542
- const config = JSON.parse(readFileSync(configPath, "utf8"));
543
- discordBotToken = config?.channels?.discord?.token as string | undefined;
544
- } catch {
545
- // Can't read config
546
- }
547
-
548
- if (!discordBotToken) {
549
- checks.push(warn("Discord notifications: no bot token found in openclaw.json"));
550
- } else {
551
- try {
552
- const res = await fetch(`https://discord.com/api/v10/channels/${flowDiscordChannel}`, {
553
- headers: { Authorization: `Bot ${discordBotToken}` },
554
- });
555
- if (res.ok) {
556
- checks.push(pass("Discord notifications: enabled"));
557
- } else {
558
- checks.push(warn(`Discord channel check: ${res.status} ${res.statusText}`));
559
- }
560
- } catch (err) {
561
- checks.push(warn(`Discord API unreachable: ${err instanceof Error ? err.message : String(err)}`));
562
- }
539
+ for (const t of notifTargets) {
540
+ checks.push(pass(`Notifications: ${t.channel} ${t.target}`));
563
541
  }
564
542
  }
565
543