@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/README.md +115 -17
- package/index.ts +18 -22
- package/openclaw.plugin.json +35 -2
- package/package.json +1 -1
- package/prompts.yaml +47 -0
- package/src/api/linear-api.ts +180 -9
- package/src/infra/cli.ts +187 -0
- package/src/infra/doctor.test.ts +4 -4
- package/src/infra/doctor.ts +7 -29
- package/src/infra/notify.test.ts +357 -108
- package/src/infra/notify.ts +114 -35
- package/src/pipeline/planner.test.ts +334 -0
- package/src/pipeline/planner.ts +282 -0
- package/src/pipeline/planning-state.test.ts +236 -0
- package/src/pipeline/planning-state.ts +216 -0
- package/src/pipeline/webhook.ts +69 -17
- package/src/tools/planner-tools.test.ts +535 -0
- package/src/tools/planner-tools.ts +450 -0
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")
|
package/src/infra/doctor.test.ts
CHANGED
|
@@ -258,12 +258,12 @@ describe("checkConnectivity", () => {
|
|
|
258
258
|
expect(apiCheck?.severity).toBe("pass");
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
-
it("reports
|
|
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
|
|
265
|
-
expect(
|
|
266
|
-
expect(
|
|
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 () => {
|
package/src/infra/doctor.ts
CHANGED
|
@@ -530,36 +530,14 @@ export async function checkConnectivity(pluginConfig?: Record<string, unknown>,
|
|
|
530
530
|
}
|
|
531
531
|
}
|
|
532
532
|
|
|
533
|
-
//
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
539
|
-
|
|
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
|
|