chainlesschain 0.40.3 → 0.41.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.
@@ -446,6 +446,139 @@ Keep values concise (single words or short strings).`;
446
446
  this._tableCreated = true;
447
447
  }
448
448
 
449
+ /**
450
+ * Detect intent type from a user message using keyword/regex matching.
451
+ *
452
+ * @param {string} userMessage
453
+ * @returns {{ type: string, entities: object } | null} Detected intent or null
454
+ */
455
+ static detectIntent(userMessage) {
456
+ if (!userMessage || typeof userMessage !== "string") return null;
457
+
458
+ const msg = userMessage.toLowerCase().trim();
459
+
460
+ // Intent detection patterns — ordered by specificity
461
+ const patterns = [
462
+ {
463
+ type: "create_file",
464
+ keywords: [
465
+ /\bcreate\s+(a\s+)?file\b/,
466
+ /\bnew\s+file\b/,
467
+ /\bscaffold\b/,
468
+ /\bgenerate\s+(a\s+)?file\b/,
469
+ ],
470
+ },
471
+ {
472
+ type: "deploy",
473
+ keywords: [/\bdeploy\b/, /\bship\s+(it|this)\b/, /\bpush\s+to\s+prod/],
474
+ },
475
+ {
476
+ type: "refactor",
477
+ keywords: [/\brefactor\b/, /\brestructure\b/, /\breorganize\b/],
478
+ },
479
+ {
480
+ type: "test",
481
+ keywords: [
482
+ /\bwrite\s+tests?\b/,
483
+ /\badd\s+tests?\b/,
484
+ /\btest\s+(this|it|the)\b/,
485
+ /\bunit\s+test\b/,
486
+ ],
487
+ },
488
+ {
489
+ type: "analyze",
490
+ keywords: [/\banalyze\b/, /\baudit\b/, /\breview\s+(the\s+)?code\b/],
491
+ },
492
+ {
493
+ type: "search",
494
+ keywords: [
495
+ /\bsearch\s+for\b/,
496
+ /\bfind\s+(all|the|every)\b/,
497
+ /\bgrep\b/,
498
+ /\blook\s+for\b/,
499
+ ],
500
+ },
501
+ {
502
+ type: "install",
503
+ keywords: [
504
+ /\binstall\b/,
505
+ /\badd\s+(a\s+)?package\b/,
506
+ /\badd\s+(a\s+)?dependency\b/,
507
+ ],
508
+ },
509
+ {
510
+ type: "generate",
511
+ keywords: [
512
+ /\bgenerate\b/,
513
+ /\bcreate\s+(a\s+)?(component|test|api|config)\b/,
514
+ ],
515
+ },
516
+ {
517
+ type: "edit_file",
518
+ keywords: [
519
+ /\bedit\s+(the\s+)?file\b/,
520
+ /\bmodify\s+(the\s+)?file\b/,
521
+ /\bchange\s+(the\s+)?file\b/,
522
+ ],
523
+ },
524
+ ];
525
+
526
+ for (const { type, keywords } of patterns) {
527
+ for (const re of keywords) {
528
+ if (re.test(msg)) {
529
+ // Try to extract entities from the message
530
+ const entities = CLISlotFiller._extractEntities(type, msg);
531
+ return { type, entities };
532
+ }
533
+ }
534
+ }
535
+
536
+ return null;
537
+ }
538
+
539
+ /**
540
+ * Extract entities from a matched message.
541
+ * @private
542
+ */
543
+ static _extractEntities(intentType, msg) {
544
+ const entities = {};
545
+
546
+ // Try to extract file path references
547
+ const pathMatch = msg.match(
548
+ /(?:in|at|to|from)\s+["`']?([./\w-]+\.\w+)["`']?/,
549
+ );
550
+ if (pathMatch) {
551
+ if (intentType === "create_file") {
552
+ entities.path = pathMatch[1];
553
+ } else {
554
+ entities.target = pathMatch[1];
555
+ }
556
+ }
557
+
558
+ // Try to extract file type from extension mentions
559
+ const extMatch = msg.match(/\.(js|ts|py|json|md|html|css|vue|yaml|yml)\b/);
560
+ if (extMatch && intentType === "create_file") {
561
+ entities.fileType = EXT_TO_FILE_TYPE[`.${extMatch[1]}`] || extMatch[1];
562
+ }
563
+
564
+ // Try to extract platform for deploy
565
+ if (intentType === "deploy") {
566
+ if (msg.includes("docker")) entities.platform = "docker";
567
+ else if (msg.includes("vercel")) entities.platform = "vercel";
568
+ else if (msg.includes("aws")) entities.platform = "aws";
569
+ }
570
+
571
+ // Try to extract package name for install
572
+ if (intentType === "install") {
573
+ const pkgMatch = msg.match(/install\s+(\S+)/);
574
+ if (pkgMatch && !["a", "the", "this", "it"].includes(pkgMatch[1])) {
575
+ entities.package = pkgMatch[1];
576
+ }
577
+ }
578
+
579
+ return entities;
580
+ }
581
+
449
582
  /**
450
583
  * Get slot definitions for an intent type.
451
584
  */
@@ -9,6 +9,7 @@
9
9
  import { agentLoop, formatToolArgs } from "./agent-core.js";
10
10
  import { detectTaskType, selectModelForTask } from "./task-model-selector.js";
11
11
  import { PlanState } from "./plan-mode.js";
12
+ import { CLISlotFiller } from "./slot-filler.js";
12
13
 
13
14
  export class WSAgentHandler {
14
15
  /**
@@ -67,6 +68,12 @@ export class WSAgentHandler {
67
68
  }
68
69
  }
69
70
 
71
+ // Create slot filler for interactive parameter collection
72
+ const slotFiller = new CLISlotFiller({
73
+ interaction: this.interaction,
74
+ db: this.db,
75
+ });
76
+
70
77
  // Run agent loop
71
78
  const loopOptions = {
72
79
  provider: session.provider,
@@ -76,10 +83,20 @@ export class WSAgentHandler {
76
83
  contextEngine: session.contextEngine,
77
84
  hookDb: this.db,
78
85
  cwd: session.projectRoot,
86
+ slotFiller,
87
+ interaction: this.interaction,
79
88
  };
80
89
 
81
90
  for await (const event of agentLoop(session.messages, loopOptions)) {
82
91
  switch (event.type) {
92
+ case "slot-filling":
93
+ this.interaction.emit("slot-filling", {
94
+ requestId,
95
+ slot: event.slot,
96
+ question: event.question,
97
+ });
98
+ break;
99
+
83
100
  case "tool-executing":
84
101
  this.interaction.emit("tool-executing", {
85
102
  requestId,
@@ -270,7 +270,7 @@ export class ChainlessChainWSServer extends EventEmitter {
270
270
  await this._handleSessionCreate(id, ws, message);
271
271
  break;
272
272
  case "session-resume":
273
- this._handleSessionResume(id, ws, message);
273
+ await this._handleSessionResume(id, ws, message);
274
274
  break;
275
275
  case "session-message":
276
276
  this._handleSessionMessage(id, ws, message);
@@ -557,7 +557,7 @@ export class ChainlessChainWSServer extends EventEmitter {
557
557
  }
558
558
 
559
559
  /** @private */
560
- _handleSessionResume(id, ws, message) {
560
+ async _handleSessionResume(id, ws, message) {
561
561
  if (!this.sessionManager) {
562
562
  this._send(ws, {
563
563
  id,
@@ -581,6 +581,34 @@ export class ChainlessChainWSServer extends EventEmitter {
581
581
  return;
582
582
  }
583
583
 
584
+ // Rebuild interaction adapter and handler for the resumed session
585
+ if (!this.sessionHandlers.has(sessionId)) {
586
+ try {
587
+ const { WebSocketInteractionAdapter } =
588
+ await import("./interaction-adapter.js");
589
+ session.interaction = new WebSocketInteractionAdapter(ws, sessionId);
590
+
591
+ let handler;
592
+ if (session.type === "chat") {
593
+ const { WSChatHandler } = await import("./ws-chat-handler.js");
594
+ handler = new WSChatHandler({
595
+ session,
596
+ interaction: session.interaction,
597
+ });
598
+ } else {
599
+ const { WSAgentHandler } = await import("./ws-agent-handler.js");
600
+ handler = new WSAgentHandler({
601
+ session,
602
+ interaction: session.interaction,
603
+ db: this.sessionManager.db,
604
+ });
605
+ }
606
+ this.sessionHandlers.set(sessionId, handler);
607
+ } catch (_err) {
608
+ // Handler creation failed — session resumed without handler
609
+ }
610
+ }
611
+
584
612
  // Filter out system messages for history
585
613
  const history = (session.messages || []).filter((m) => m.role !== "system");
586
614