@rk0429/agentic-relay 21.3.0 → 22.1.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.
Files changed (113) hide show
  1. package/dist/application/chat/chat-daemon-service.d.ts +88 -0
  2. package/dist/application/chat/chat-daemon-service.js +358 -0
  3. package/dist/application/chat/chat-daemon-service.js.map +1 -0
  4. package/dist/application/chat/chat-inbound-handler.d.ts +35 -0
  5. package/dist/application/chat/chat-inbound-handler.js +193 -0
  6. package/dist/application/chat/chat-inbound-handler.js.map +1 -0
  7. package/dist/application/chat/chat-logging.d.ts +8 -0
  8. package/dist/application/chat/chat-logging.js +16 -0
  9. package/dist/application/chat/chat-logging.js.map +1 -0
  10. package/dist/application/chat/chat-outbound-handler.d.ts +17 -0
  11. package/dist/application/chat/chat-outbound-handler.js +207 -0
  12. package/dist/application/chat/chat-outbound-handler.js.map +1 -0
  13. package/dist/application/chat/chat-question-service.d.ts +37 -0
  14. package/dist/application/chat/chat-question-service.js +135 -0
  15. package/dist/application/chat/chat-question-service.js.map +1 -0
  16. package/dist/application/chat/chat-setup-service.d.ts +27 -0
  17. package/dist/application/chat/chat-setup-service.js +183 -0
  18. package/dist/application/chat/chat-setup-service.js.map +1 -0
  19. package/dist/application/chat/message-queue-manager.d.ts +13 -0
  20. package/dist/application/chat/message-queue-manager.js +34 -0
  21. package/dist/application/chat/message-queue-manager.js.map +1 -0
  22. package/dist/application/housekeeping.js +6 -2
  23. package/dist/application/housekeeping.js.map +1 -1
  24. package/dist/application/routine-status-query-service.d.ts +17 -0
  25. package/dist/application/routine-status-query-service.js +111 -0
  26. package/dist/application/routine-status-query-service.js.map +1 -1
  27. package/dist/bin/relay.d.ts +4 -2
  28. package/dist/bin/relay.js +301 -12
  29. package/dist/bin/relay.js.map +1 -1
  30. package/dist/core/types.d.ts +8 -0
  31. package/dist/core/types.js +1 -0
  32. package/dist/core/types.js.map +1 -1
  33. package/dist/domain/chat/message-cleaner.d.ts +11 -0
  34. package/dist/domain/chat/message-cleaner.js +54 -0
  35. package/dist/domain/chat/message-cleaner.js.map +1 -0
  36. package/dist/domain/chat/message-router.d.ts +8 -0
  37. package/dist/domain/chat/message-router.js +97 -0
  38. package/dist/domain/chat/message-router.js.map +1 -0
  39. package/dist/domain/chat/message-splitter.d.ts +3 -0
  40. package/dist/domain/chat/message-splitter.js +148 -0
  41. package/dist/domain/chat/message-splitter.js.map +1 -0
  42. package/dist/domain/chat/platform-connection.d.ts +31 -0
  43. package/dist/domain/chat/platform-connection.js +67 -0
  44. package/dist/domain/chat/platform-connection.js.map +1 -0
  45. package/dist/domain/chat/platform-types.d.ts +18 -0
  46. package/dist/domain/chat/platform-types.js +14 -0
  47. package/dist/domain/chat/platform-types.js.map +1 -0
  48. package/dist/domain/chat/ports.d.ts +31 -0
  49. package/dist/domain/chat/ports.js +2 -0
  50. package/dist/domain/chat/ports.js.map +1 -0
  51. package/dist/domain/chat/session-bridge.d.ts +41 -0
  52. package/dist/domain/chat/session-bridge.js +93 -0
  53. package/dist/domain/chat/session-bridge.js.map +1 -0
  54. package/dist/domain/chat/types.d.ts +85 -0
  55. package/dist/domain/chat/types.js +2 -0
  56. package/dist/domain/chat/types.js.map +1 -0
  57. package/dist/domain/chat/user-question.d.ts +33 -0
  58. package/dist/domain/chat/user-question.js +73 -0
  59. package/dist/domain/chat/user-question.js.map +1 -0
  60. package/dist/domain/config.d.ts +3 -1
  61. package/dist/domain/config.js +9 -0
  62. package/dist/domain/config.js.map +1 -1
  63. package/dist/domain/routine-loader.d.ts +2 -0
  64. package/dist/domain/routine-loader.js +3 -0
  65. package/dist/domain/routine-loader.js.map +1 -1
  66. package/dist/infrastructure/chat/adapter-factory.d.ts +5 -0
  67. package/dist/infrastructure/chat/adapter-factory.js +15 -0
  68. package/dist/infrastructure/chat/adapter-factory.js.map +1 -0
  69. package/dist/infrastructure/chat/adapters/discord-adapter.d.ts +23 -0
  70. package/dist/infrastructure/chat/adapters/discord-adapter.js +199 -0
  71. package/dist/infrastructure/chat/adapters/discord-adapter.js.map +1 -0
  72. package/dist/infrastructure/chat/adapters/slack-adapter.d.ts +24 -0
  73. package/dist/infrastructure/chat/adapters/slack-adapter.js +155 -0
  74. package/dist/infrastructure/chat/adapters/slack-adapter.js.map +1 -0
  75. package/dist/infrastructure/chat/agent-gateway-impl.d.ts +21 -0
  76. package/dist/infrastructure/chat/agent-gateway-impl.js +101 -0
  77. package/dist/infrastructure/chat/agent-gateway-impl.js.map +1 -0
  78. package/dist/infrastructure/chat/chat-ipc-client.d.ts +33 -0
  79. package/dist/infrastructure/chat/chat-ipc-client.js +70 -0
  80. package/dist/infrastructure/chat/chat-ipc-client.js.map +1 -0
  81. package/dist/infrastructure/chat/chat-ipc-server.d.ts +19 -0
  82. package/dist/infrastructure/chat/chat-ipc-server.js +125 -0
  83. package/dist/infrastructure/chat/chat-ipc-server.js.map +1 -0
  84. package/dist/infrastructure/chat/chat-logger.d.ts +24 -0
  85. package/dist/infrastructure/chat/chat-logger.js +86 -0
  86. package/dist/infrastructure/chat/chat-logger.js.map +1 -0
  87. package/dist/infrastructure/chat/credentials-repository.d.ts +8 -0
  88. package/dist/infrastructure/chat/credentials-repository.js +84 -0
  89. package/dist/infrastructure/chat/credentials-repository.js.map +1 -0
  90. package/dist/infrastructure/chat/session-bridge-repository.d.ts +13 -0
  91. package/dist/infrastructure/chat/session-bridge-repository.js +120 -0
  92. package/dist/infrastructure/chat/session-bridge-repository.js.map +1 -0
  93. package/dist/infrastructure/chat/token-masking.d.ts +3 -0
  94. package/dist/infrastructure/chat/token-masking.js +22 -0
  95. package/dist/infrastructure/chat/token-masking.js.map +1 -0
  96. package/dist/infrastructure/store/relay-store.js +3 -1
  97. package/dist/infrastructure/store/relay-store.js.map +1 -1
  98. package/dist/interfaces/cli/chat-cli.d.ts +34 -0
  99. package/dist/interfaces/cli/chat-cli.js +68 -0
  100. package/dist/interfaces/cli/chat-cli.js.map +1 -0
  101. package/dist/interfaces/cli/relay-cli-args.d.ts +16 -1
  102. package/dist/interfaces/cli/relay-cli-args.js +135 -2
  103. package/dist/interfaces/cli/relay-cli-args.js.map +1 -1
  104. package/dist/interfaces/mcp/chat-tools.d.ts +34 -0
  105. package/dist/interfaces/mcp/chat-tools.js +131 -0
  106. package/dist/interfaces/mcp/chat-tools.js.map +1 -0
  107. package/dist/interfaces/mcp/relay-mcp-server.js +43 -6
  108. package/dist/interfaces/mcp/relay-mcp-server.js.map +1 -1
  109. package/package.json +5 -1
  110. package/scripts/check-chat-boundary.mjs +220 -0
  111. package/scripts/check-chat-boundary.test.ts +139 -0
  112. package/scripts/check-circular-deps.mjs +199 -0
  113. package/scripts/check-circular-deps.test.ts +62 -0
package/dist/bin/relay.js CHANGED
@@ -1,11 +1,18 @@
1
1
  #!/usr/bin/env -S node --disable-warning=ExperimentalWarning
2
- import { fork } from "node:child_process";
2
+ import { execFile, fork } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { existsSync } from "node:fs";
5
5
  import { access, readFile, realpath, rm } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { fileURLToPath } from "node:url";
9
+ import { promisify } from "node:util";
10
+ import { ChatDaemonService, } from "../application/chat/chat-daemon-service.js";
11
+ import { ChatInboundHandler } from "../application/chat/chat-inbound-handler.js";
12
+ import { ChatOutboundHandler } from "../application/chat/chat-outbound-handler.js";
13
+ import { ChatQuestionService, InMemoryUserQuestionRepository, } from "../application/chat/chat-question-service.js";
14
+ import { ChatSetupService } from "../application/chat/chat-setup-service.js";
15
+ import { MessageQueueManager } from "../application/chat/message-queue-manager.js";
9
16
  import { CoreManagementService, } from "../application/core-management-service.js";
10
17
  import { LoopEventLogSink } from "../application/event-log-sink.js";
11
18
  import { isProcessAlive, reconcileAbandonedRunningTaskSessions, runStartupHousekeeping, } from "../application/housekeeping.js";
@@ -24,24 +31,33 @@ import { VscodeSetupService } from "../application/vscode-setup-service.js";
24
31
  import { WorkflowExecutionService, } from "../application/workflow-execution-service.js";
25
32
  import { ValidationError } from "../core/errors.js";
26
33
  import { buildCrossBackendHandoffPrompt, createCrossBackendHandoffInfo, } from "../domain/handoff.js";
34
+ import { resolveChatConfig } from "../domain/config.js";
27
35
  import { resolveRole } from "../domain/role.js";
28
36
  import { resolveBackend, validateRoutingRules } from "../domain/routing.js";
29
37
  import { loadRoutine } from "../domain/routine-loader.js";
30
38
  import { BackendRegistry } from "../infrastructure/backends/backend-registry.js";
39
+ import { AdapterFactory } from "../infrastructure/chat/adapter-factory.js";
40
+ import { AgentGatewayImpl } from "../infrastructure/chat/agent-gateway-impl.js";
41
+ import { ChatIpcServer } from "../infrastructure/chat/chat-ipc-server.js";
42
+ import { ChatLogger } from "../infrastructure/chat/chat-logger.js";
43
+ import { CredentialsFileRepository } from "../infrastructure/chat/credentials-repository.js";
44
+ import { SessionBridgeFileRepository } from "../infrastructure/chat/session-bridge-repository.js";
31
45
  import { CommandExecutionGateway } from "../infrastructure/command-execution-gateway.js";
32
46
  import { CliBackendExecutor } from "../infrastructure/backends/cli-backend-executor.js";
33
47
  import { ChildProcessExecutor, ChildProcessInteractiveRunner, } from "../infrastructure/process/process-executor.js";
34
48
  import { LoopStateRepository } from "../infrastructure/store/loop-state-repository.js";
35
49
  import { RelayStore } from "../infrastructure/store/relay-store.js";
36
- import { isMissingFileError } from "../infrastructure/store/file-utils.js";
50
+ import { isMissingFileError, writeJsonAtomic } from "../infrastructure/store/file-utils.js";
37
51
  import { RoutineExecutionLeaseRepository } from "../infrastructure/store/routine-execution-lease-repository.js";
38
52
  import { RoutineRuntimeProjectionRepository } from "../infrastructure/store/routine-runtime-projection-repository.js";
39
53
  import { RoutineStateRepository } from "../infrastructure/store/routine-state-repository.js";
54
+ import { handleChatCommand, } from "../interfaces/cli/chat-cli.js";
40
55
  import { parseRelayCliArgs, } from "../interfaces/cli/relay-cli-args.js";
41
56
  import { createLoopProgressReporter } from "../interfaces/cli/loop-progress-shell.js";
42
57
  import { runRelayInteractiveShell } from "../interfaces/cli/relay-shell.js";
43
58
  import { renderVisualization } from "../interfaces/cli/visualization-renderer.js";
44
59
  import { createRelayMcpServer, serveRelayMcpServer, } from "../interfaces/mcp/relay-mcp-server.js";
60
+ const execFileAsync = promisify(execFile);
45
61
  export const AGENTIC_SETUP_PROMPT = "ワークスペースのエージェント基盤をセットアップしてください。 .agents/skills/agentic-setup/SKILL.md の手順に従って実行してください。";
46
62
  export function buildRoutineStatusQueryService(options) {
47
63
  return new RoutineStatusQueryService({
@@ -52,6 +68,9 @@ export function buildRoutineStatusQueryService(options) {
52
68
  stateRepo: new RoutineStateRepository(options.store),
53
69
  runtimeRepo: new RoutineRuntimeProjectionRepository(options.store),
54
70
  store: options.store,
71
+ leaseRepo: new RoutineExecutionLeaseRepository(options.store),
72
+ loopStateFiles: () => options.store.listLoopStateFiles(),
73
+ deleteLoopState: (filename) => options.store.deleteLoopState(filename),
55
74
  });
56
75
  }
57
76
  async function main() {
@@ -184,9 +203,78 @@ async function main() {
184
203
  workflowRepo,
185
204
  configDir,
186
205
  });
206
+ const createChatSetupService = () => new ChatSetupService({
207
+ credentialsRepository: new CredentialsFileRepository(store.rootDir),
208
+ prompt: promptFromStdin,
209
+ confirm: confirmFromStdin,
210
+ openUrl: openUrlInBrowser,
211
+ logger: (message) => writeLine(process.stderr, message),
212
+ });
213
+ const createChatDaemonService = () => {
214
+ const credentialsRepository = new CredentialsFileRepository(store.rootDir);
215
+ const sessionBridgeRepository = new SessionBridgeFileRepository(store.rootDir);
216
+ const outboundHandler = new ChatOutboundHandler();
217
+ const questionService = new ChatQuestionService({
218
+ userQuestionRepository: new InMemoryUserQuestionRepository(),
219
+ });
220
+ const spawnService = new SpawnAgentsService({
221
+ backendRegistry,
222
+ backendExecutor,
223
+ store,
224
+ taskService,
225
+ cwd,
226
+ env: process.env,
227
+ });
228
+ const inboundHandler = new ChatInboundHandler({
229
+ agentGateway: new AgentGatewayImpl(spawnService, taskService, store, backendRegistry),
230
+ sessionBridgeRepository,
231
+ outboundHandler,
232
+ questionService,
233
+ messageQueueManager: new MessageQueueManager(),
234
+ logger: (message) => writeLine(process.stderr, message),
235
+ });
236
+ const ipcServer = new ChatIpcServer({
237
+ rootDir: store.rootDir,
238
+ outboundHandler,
239
+ questionService,
240
+ });
241
+ return new ChatDaemonService({
242
+ store,
243
+ credentialsRepository,
244
+ sessionBridgeRepository,
245
+ adapterFactory: new AdapterFactory(),
246
+ resolveChatConfig,
247
+ outboundHandler,
248
+ questionService,
249
+ inboundHandler,
250
+ ipcServer,
251
+ getProcessStatus: async (pid) => {
252
+ if (!isProcessAlive(pid)) {
253
+ return { alive: false };
254
+ }
255
+ try {
256
+ const { stdout } = await execFileAsync("ps", ["-o", "lstart=", "-p", String(pid)]);
257
+ const startedAt = stdout.trim();
258
+ const parsed = new Date(startedAt);
259
+ return {
260
+ alive: true,
261
+ startedAt: Number.isNaN(parsed.getTime()) ? null : parsed.toISOString(),
262
+ };
263
+ }
264
+ catch {
265
+ return {
266
+ alive: true,
267
+ };
268
+ }
269
+ },
270
+ writeJsonAtomic,
271
+ createLogger: (options) => new ChatLogger(options),
272
+ });
273
+ };
187
274
  if (command.kind === "routine-run" ||
188
275
  command.kind === "routine-start" ||
189
276
  command.kind === "routine-stop" ||
277
+ command.kind === "routine-cleanup" ||
190
278
  command.kind === "routine-list" ||
191
279
  command.kind === "routine-status") {
192
280
  const exitCode = await handleRoutineCommand(command, {
@@ -217,6 +305,26 @@ async function main() {
217
305
  }
218
306
  return;
219
307
  }
308
+ if (command.kind === "chat-setup" ||
309
+ command.kind === "chat-start" ||
310
+ command.kind === "chat-stop" ||
311
+ command.kind === "chat-status") {
312
+ const exitCode = await handleChatCommand(command, {
313
+ cwd,
314
+ argv: process.argv.slice(2),
315
+ scriptPath: process.argv[1] ?? fileURLToPath(import.meta.url),
316
+ env: process.env,
317
+ stdout: process.stdout,
318
+ stderr: process.stderr,
319
+ createSetupService: createChatSetupService,
320
+ createDaemonService: createChatDaemonService,
321
+ forkProcess: (modulePath, args, options) => fork(modulePath, args, options),
322
+ });
323
+ if (exitCode !== undefined) {
324
+ process.exitCode = exitCode;
325
+ }
326
+ return;
327
+ }
220
328
  if (command.kind === "mcp") {
221
329
  const installed = await backendRegistry.detectInstalled();
222
330
  if (installed.length === 0) {
@@ -246,6 +354,7 @@ async function main() {
246
354
  routineStatusQueryService: buildRoutineStatusQueryService({
247
355
  store,
248
356
  workflowRepo,
357
+ configDir: command.configDir,
249
358
  }),
250
359
  allowSpawnAgents: process.env.RELAY_ALLOW_SPAWN_AGENTS !== "0",
251
360
  shutdownSignal: shutdownController.signal,
@@ -351,9 +460,9 @@ export async function handleRoutineCommand(command, dependencies) {
351
460
  const placeholderTime = new Date().toISOString();
352
461
  warnOnBuiltInRoutineVarOverrides(command.vars, dependencies.stderr);
353
462
  const routineVars = buildRoutineLoadVars(command.vars, placeholderTime);
354
- await dependencies.runStartupHousekeeping(dependencies.store, {
463
+ await suppressLoopStateWarnings(() => dependencies.runStartupHousekeeping(dependencies.store, {
355
464
  protectedLoopStateFile: command.resumeStateFile,
356
- });
465
+ }));
357
466
  await dependencies.reconcileAbandonedRunningTaskSessions(dependencies.store, dependencies.taskService);
358
467
  const loadedRoutine = await dependencies.loadRoutine(workflowPath, dependencies.workflowRepo, routineVars);
359
468
  warnOnBuiltInRoutineYamlVarOverrides(loadedRoutine.rawYamlVarKeys, dependencies.stderr);
@@ -375,7 +484,7 @@ export async function handleRoutineCommand(command, dependencies) {
375
484
  vars: command.vars ?? {},
376
485
  })
377
486
  : undefined;
378
- dependencies.stdout.write(renderRoutineDryRun(loadedRoutine, workflowPath, resumePreflight));
487
+ dependencies.stdout.write(renderRoutineDryRun(loadedRoutine, workflowPath, resumePreflight, command.vars, loadedRoutine.rawYamlVars));
379
488
  return 0;
380
489
  }
381
490
  enforceAllowCommandRun(workflowPath, loadedRoutine.capabilitySummary, command.allowCommandRun ?? false);
@@ -481,6 +590,26 @@ export async function handleRoutineCommand(command, dependencies) {
481
590
  await stopRoutineDaemon(dependencies);
482
591
  return 0;
483
592
  }
593
+ case "routine-cleanup": {
594
+ const service = dependencies.createRoutineStatusQueryService({
595
+ configDir: resolveRoutineConfigDir(command.configDir, dependencies.store, dependencies.cwd),
596
+ });
597
+ const result = await service.cleanup(command.dryRun ?? false);
598
+ if (result.targets.length === 0) {
599
+ dependencies.stderr.write("No cleanup targets found.\n");
600
+ return 0;
601
+ }
602
+ if (command.dryRun) {
603
+ dependencies.stderr.write("Cleanup targets (dry-run):\n");
604
+ }
605
+ else {
606
+ dependencies.stderr.write(`Cleaned up ${result.deleted} file(s):\n`);
607
+ }
608
+ for (const target of result.targets) {
609
+ dependencies.stderr.write(` ${target.kind}: ${target.filePath} — ${target.reason}\n`);
610
+ }
611
+ return 0;
612
+ }
484
613
  case "routine-list": {
485
614
  const service = dependencies.createRoutineStatusQueryService({
486
615
  configDir: resolveRoutineConfigDir(command.configDir, dependencies.store, dependencies.cwd),
@@ -501,11 +630,31 @@ export async function handleRoutineCommand(command, dependencies) {
501
630
  }
502
631
  }
503
632
  }
633
+ async function suppressLoopStateWarnings(fn) {
634
+ const origWarn = console.warn;
635
+ console.warn = (...args) => {
636
+ const msg = typeof args[0] === "string" ? args[0] : "";
637
+ if (msg.startsWith("Ignoring invalid loop state file")) {
638
+ console.debug(...args);
639
+ return;
640
+ }
641
+ origWarn.apply(console, args);
642
+ };
643
+ try {
644
+ return await fn();
645
+ }
646
+ finally {
647
+ console.warn = origWarn;
648
+ }
649
+ }
504
650
  export function renderHelpText(topic, scriptName = path.basename(fileURLToPath(import.meta.url))) {
505
651
  if (topic) {
506
652
  if (isRoutineHelpTopic(topic)) {
507
653
  return renderRoutineHelp(topic, scriptName);
508
654
  }
655
+ if (isChatHelpTopic(topic)) {
656
+ return renderChatHelp(topic, scriptName);
657
+ }
509
658
  return renderCoreSubcommandHelp(topic, scriptName);
510
659
  }
511
660
  const commandName = path.parse(scriptName).name;
@@ -514,8 +663,13 @@ export function renderHelpText(topic, scriptName = path.basename(fileURLToPath(i
514
663
  ` ${scriptName} routine run <path> [--config <dir>] [--var key=value ...] [--dry-run] [--resume-state <file>] [--force-resume] [--allow-command-run]`,
515
664
  ` ${scriptName} routine start [--config <dir>] [--daemon] [--allow-command-run]`,
516
665
  ` ${scriptName} routine stop`,
666
+ ` ${scriptName} routine cleanup [--config <dir>] [--dry-run]`,
517
667
  ` ${scriptName} routine list [--config <dir>] [--format <table|tsv|json>]`,
518
668
  ` ${scriptName} routine status [name] [--config <dir>] [--format <table|tsv|json>] [--limit <n>]`,
669
+ ` ${scriptName} chat setup (--slack | --discord)`,
670
+ ` ${scriptName} chat start [--foreground]`,
671
+ ` ${scriptName} chat stop`,
672
+ ` ${scriptName} chat status`,
519
673
  ` ${scriptName} core <setup|update|remove> [options]`,
520
674
  ` ${scriptName} setup-vscode [--target <code|code-insiders|cursor>] [--uninstall]`,
521
675
  ` ${scriptName} visualize`,
@@ -527,8 +681,13 @@ export function renderHelpText(topic, scriptName = path.basename(fileURLToPath(i
527
681
  ` ${commandName} routine run [options] Run one routine YAML immediately`,
528
682
  ` ${commandName} routine start [options] Start the routine daemon`,
529
683
  ` ${commandName} routine stop Stop the routine daemon`,
684
+ ` ${commandName} routine cleanup [options] Remove stale routine metadata`,
530
685
  ` ${commandName} routine list [options] List known routines`,
531
686
  ` ${commandName} routine status [options] Show routine status details`,
687
+ ` ${commandName} chat setup [options] Configure Slack or Discord integration`,
688
+ ` ${commandName} chat start [options] Start the chat daemon`,
689
+ ` ${commandName} chat stop Stop the chat daemon`,
690
+ ` ${commandName} chat status Show chat daemon status`,
532
691
  ` ${commandName} core setup [options] Setup .agents/ from agentic-core dist`,
533
692
  ` ${commandName} core update [options] Update .agents/ from latest dist`,
534
693
  ` ${commandName} core remove [options] Remove relay core managed files`,
@@ -584,6 +743,82 @@ function renderCoreSubcommandHelp(topic, scriptName) {
584
743
  function isRoutineHelpTopic(topic) {
585
744
  return topic.startsWith("routine");
586
745
  }
746
+ function isChatHelpTopic(topic) {
747
+ return topic.startsWith("chat");
748
+ }
749
+ function renderChatHelp(topic, scriptName) {
750
+ if (topic === "chat") {
751
+ return [
752
+ `Usage: ${scriptName} chat setup (--slack | --discord)`,
753
+ ` ${scriptName} chat start [--foreground]`,
754
+ ` ${scriptName} chat stop`,
755
+ ` ${scriptName} chat status`,
756
+ "",
757
+ "Chat commands:",
758
+ " setup Configure Slack or Discord credentials",
759
+ " start Start the chat daemon",
760
+ " stop Stop the chat daemon",
761
+ " status Show chat daemon status",
762
+ "",
763
+ "Options:",
764
+ " --slack Setup Slack integration",
765
+ " --discord Setup Discord integration",
766
+ " --foreground Run the chat daemon in the foreground",
767
+ " -h, --help Show this help",
768
+ ].join("\n");
769
+ }
770
+ const commandName = path.parse(scriptName).name;
771
+ const allSubcommandsReference = `See '${commandName} chat --help' for all subcommands.`;
772
+ switch (topic) {
773
+ case "chat-setup":
774
+ return [
775
+ `Usage: ${scriptName} chat setup (--slack | --discord)`,
776
+ "",
777
+ "Configure Slack or Discord credentials for the chat daemon.",
778
+ "",
779
+ "Options:",
780
+ " --slack Configure Slack",
781
+ " --discord Configure Discord",
782
+ " -h, --help Show this help",
783
+ "",
784
+ allSubcommandsReference,
785
+ ].join("\n");
786
+ case "chat-start":
787
+ return [
788
+ `Usage: ${scriptName} chat start [--foreground]`,
789
+ "",
790
+ "Start the chat daemon.",
791
+ "",
792
+ "Options:",
793
+ " --foreground Run in the foreground for debugging",
794
+ " -h, --help Show this help",
795
+ "",
796
+ allSubcommandsReference,
797
+ ].join("\n");
798
+ case "chat-stop":
799
+ return [
800
+ `Usage: ${scriptName} chat stop`,
801
+ "",
802
+ "Stop the chat daemon.",
803
+ "",
804
+ "Options:",
805
+ " -h, --help Show this help",
806
+ "",
807
+ allSubcommandsReference,
808
+ ].join("\n");
809
+ case "chat-status":
810
+ return [
811
+ `Usage: ${scriptName} chat status`,
812
+ "",
813
+ "Show chat daemon status.",
814
+ "",
815
+ "Options:",
816
+ " -h, --help Show this help",
817
+ "",
818
+ allSubcommandsReference,
819
+ ].join("\n");
820
+ }
821
+ }
587
822
  function renderRoutineHelp(topic, scriptName) {
588
823
  if (topic === "routine") {
589
824
  return renderRoutineAggregateHelp(scriptName);
@@ -633,6 +868,19 @@ function renderRoutineHelp(topic, scriptName) {
633
868
  "",
634
869
  allSubcommandsReference,
635
870
  ].join("\n");
871
+ case "routine-cleanup":
872
+ return [
873
+ `Usage: ${scriptName} routine cleanup [options]`,
874
+ "",
875
+ "Remove stale routine metadata files.",
876
+ "",
877
+ "Options:",
878
+ " --config <dir> Override the routine config directory",
879
+ " --dry-run Show cleanup targets without deleting them",
880
+ " -h, --help Show this help",
881
+ "",
882
+ allSubcommandsReference,
883
+ ].join("\n");
636
884
  case "routine-list":
637
885
  return [
638
886
  `Usage: ${scriptName} routine list [options]`,
@@ -655,7 +903,7 @@ function renderRoutineHelp(topic, scriptName) {
655
903
  "Options:",
656
904
  " --config <dir> Override the routine config directory",
657
905
  " --format <fmt> Output format (table, tsv, json)",
658
- " --limit <n> Limit the number of status log entries",
906
+ " --limit <n> Limit the number of status log entries (default: 10)",
659
907
  " -h, --help Show this help",
660
908
  "",
661
909
  allSubcommandsReference,
@@ -667,6 +915,7 @@ function renderRoutineAggregateHelp(scriptName) {
667
915
  `Usage: ${scriptName} routine run <workflow.yaml> [options]`,
668
916
  ` ${scriptName} routine start [options]`,
669
917
  ` ${scriptName} routine stop`,
918
+ ` ${scriptName} routine cleanup [options]`,
670
919
  ` ${scriptName} routine list [options]`,
671
920
  ` ${scriptName} routine status [name] [options]`,
672
921
  "",
@@ -674,6 +923,7 @@ function renderRoutineAggregateHelp(scriptName) {
674
923
  " run Run one routine YAML immediately",
675
924
  " start Start the routine daemon",
676
925
  " stop Stop the routine daemon",
926
+ " cleanup Remove stale routine metadata",
677
927
  " list List known routines",
678
928
  " status Show routine status details",
679
929
  "",
@@ -684,9 +934,9 @@ function renderRoutineAggregateHelp(scriptName) {
684
934
  " --allow-command-run Allow trusted workflows that declare command.run",
685
935
  " --resume-state <file> Resume a loop-capable routine from checkpoint state",
686
936
  " --force-resume Override workflow digest mismatch during resume",
687
- " --dry-run Render routine structure without executing it",
937
+ " --dry-run Render routine structure or cleanup targets without executing deletion",
688
938
  " --var key=value Set template variables for one routine run",
689
- " --limit <n> Limit the number of status log entries",
939
+ " --limit <n> Limit the number of status log entries (default: 10)",
690
940
  " -h, --help Show this help",
691
941
  ].join("\n");
692
942
  }
@@ -953,6 +1203,9 @@ function printRoutineRunResult(result, writer = process.stderr) {
953
1203
  writeLine(writer, lines.join("\n"));
954
1204
  }
955
1205
  const BUILT_IN_ROUTINE_VAR_KEYS = ["scheduled_time", "trigger_time"];
1206
+ function isBuiltInRoutineVarKey(key) {
1207
+ return BUILT_IN_ROUTINE_VAR_KEYS.includes(key);
1208
+ }
956
1209
  function buildRoutineLoadVars(vars, placeholderValue) {
957
1210
  return {
958
1211
  ...(vars ?? {}),
@@ -1034,7 +1287,18 @@ async function validateRoutineDryRunResumeState(options) {
1034
1287
  });
1035
1288
  }
1036
1289
  }
1037
- function renderRoutineDryRun(loadedRoutine, workflowPath, resumePreflight) {
1290
+ function renderRoutineDryRun(loadedRoutine, workflowPath, resumePreflight, vars, yamlVars) {
1291
+ const cliVarKeys = new Set(Object.keys(vars ?? {}));
1292
+ const emptyVarWarnings = [
1293
+ ...Object.entries(vars ?? {})
1294
+ .filter(([key, value]) => value === "" && !isBuiltInRoutineVarKey(key))
1295
+ .map(([key]) => key),
1296
+ ...Object.entries(yamlVars ?? {})
1297
+ .filter(([key, value]) => value === "" &&
1298
+ !isBuiltInRoutineVarKey(key) &&
1299
+ !cliVarKeys.has(key))
1300
+ .map(([key]) => key),
1301
+ ].map((key) => ` - Template variable "{{${key}}}" resolved to empty string.`);
1038
1302
  const lines = [
1039
1303
  `Routine: ${loadedRoutine.routineDefinition.name.value}`,
1040
1304
  ...(loadedRoutine.routineDefinition.description
@@ -1058,6 +1322,7 @@ function renderRoutineDryRun(loadedRoutine, workflowPath, resumePreflight) {
1058
1322
  : []),
1059
1323
  ]
1060
1324
  : []),
1325
+ ...(emptyVarWarnings.length > 0 ? ["Warnings:", ...emptyVarWarnings] : []),
1061
1326
  "",
1062
1327
  renderDryRun(loadedRoutine.routineDefinition.workflow, workflowPath),
1063
1328
  ];
@@ -1087,7 +1352,7 @@ function renderRoutineList(result, format = "table") {
1087
1352
  formatOptionalTimestamp(entry.nextScheduledAt),
1088
1353
  formatRoutineStatusWithOrphan(entry.status, entry.orphan),
1089
1354
  ].join("\t")),
1090
- ...invalid.map((entry) => `# Skipped: ${path.basename(entry.yamlPath)}: ${entry.error.replace(/\r?\n/g, " | ")}`),
1355
+ ...invalid.map((entry) => `# ${path.basename(entry.yamlPath)}: ${firstLine(stripYamlPathPrefix(entry.yamlPath, entry.error))}`),
1091
1356
  ];
1092
1357
  return `${lines.join("\n")}\n`;
1093
1358
  }
@@ -1117,10 +1382,14 @@ function renderRoutineList(result, format = "table") {
1117
1382
  }
1118
1383
  function renderInvalidRoutineFiles(invalid) {
1119
1384
  return [
1120
- `Skipped ${invalid.length} file(s):`,
1121
- ...invalid.map((entry) => ` - ${path.basename(entry.yamlPath)}: ${firstLine(entry.error)}`),
1385
+ `Skipped (${invalid.length}):`,
1386
+ ...invalid.map((entry) => ` - ${path.basename(entry.yamlPath)}: ${firstLine(stripYamlPathPrefix(entry.yamlPath, entry.error))}`),
1122
1387
  ].join("\n");
1123
1388
  }
1389
+ function stripYamlPathPrefix(yamlPath, error) {
1390
+ const prefix = `${yamlPath}: `;
1391
+ return error.startsWith(prefix) ? error.slice(prefix.length) : error;
1392
+ }
1124
1393
  function firstLine(value) {
1125
1394
  return value.split(/\r?\n/u, 1)[0] ?? "";
1126
1395
  }
@@ -1446,6 +1715,26 @@ function confirmFromStdin(message) {
1446
1715
  });
1447
1716
  });
1448
1717
  }
1718
+ function promptFromStdin(message) {
1719
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1720
+ return new Promise((resolve) => {
1721
+ rl.question(message, (answer) => {
1722
+ rl.close();
1723
+ resolve(answer);
1724
+ });
1725
+ });
1726
+ }
1727
+ async function openUrlInBrowser(url) {
1728
+ if (process.platform === "darwin") {
1729
+ await execFileAsync("open", [url]);
1730
+ return;
1731
+ }
1732
+ if (process.platform === "win32") {
1733
+ await execFileAsync("cmd", ["/c", "start", "", url]);
1734
+ return;
1735
+ }
1736
+ await execFileAsync("xdg-open", [url]);
1737
+ }
1449
1738
  async function isExecutedAsScript(metaUrl) {
1450
1739
  const entryPath = process.argv[1];
1451
1740
  if (!entryPath) {