chainlesschain 0.37.12 → 0.40.1

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 (48) hide show
  1. package/package.json +3 -2
  2. package/src/commands/agent.js +7 -1
  3. package/src/commands/ask.js +24 -9
  4. package/src/commands/chat.js +7 -1
  5. package/src/commands/cli-anything.js +266 -0
  6. package/src/commands/compliance.js +216 -0
  7. package/src/commands/dao.js +312 -0
  8. package/src/commands/dlp.js +278 -0
  9. package/src/commands/evomap.js +558 -0
  10. package/src/commands/hardening.js +230 -0
  11. package/src/commands/matrix.js +168 -0
  12. package/src/commands/nostr.js +185 -0
  13. package/src/commands/pqc.js +162 -0
  14. package/src/commands/scim.js +218 -0
  15. package/src/commands/serve.js +109 -0
  16. package/src/commands/siem.js +156 -0
  17. package/src/commands/social.js +480 -0
  18. package/src/commands/terraform.js +148 -0
  19. package/src/constants.js +1 -0
  20. package/src/index.js +60 -0
  21. package/src/lib/autonomous-agent.js +487 -0
  22. package/src/lib/cli-anything-bridge.js +379 -0
  23. package/src/lib/cli-context-engineering.js +472 -0
  24. package/src/lib/compliance-manager.js +290 -0
  25. package/src/lib/content-recommender.js +205 -0
  26. package/src/lib/dao-governance.js +296 -0
  27. package/src/lib/dlp-engine.js +304 -0
  28. package/src/lib/evomap-client.js +135 -0
  29. package/src/lib/evomap-federation.js +240 -0
  30. package/src/lib/evomap-governance.js +250 -0
  31. package/src/lib/evomap-manager.js +227 -0
  32. package/src/lib/git-integration.js +1 -1
  33. package/src/lib/hardening-manager.js +275 -0
  34. package/src/lib/llm-providers.js +14 -1
  35. package/src/lib/matrix-bridge.js +196 -0
  36. package/src/lib/nostr-bridge.js +195 -0
  37. package/src/lib/permanent-memory.js +370 -0
  38. package/src/lib/plan-mode.js +211 -0
  39. package/src/lib/pqc-manager.js +196 -0
  40. package/src/lib/scim-manager.js +212 -0
  41. package/src/lib/session-manager.js +38 -0
  42. package/src/lib/siem-exporter.js +137 -0
  43. package/src/lib/social-manager.js +283 -0
  44. package/src/lib/task-model-selector.js +232 -0
  45. package/src/lib/terraform-manager.js +201 -0
  46. package/src/lib/ws-server.js +474 -0
  47. package/src/repl/agent-repl.js +796 -41
  48. package/src/repl/chat-repl.js +14 -6
@@ -26,6 +26,22 @@ import { fileURLToPath } from "url";
26
26
  import { logger } from "../lib/logger.js";
27
27
  import { getPlanModeManager, PlanState } from "../lib/plan-mode.js";
28
28
  import { CLISkillLoader } from "../lib/skill-loader.js";
29
+ import { bootstrap, shutdown } from "../runtime/bootstrap.js";
30
+ import {
31
+ createSession,
32
+ saveMessages,
33
+ getSession,
34
+ } from "../lib/session-manager.js";
35
+ import { storeMemory, consolidateMemory } from "../lib/hierarchical-memory.js";
36
+ import { CLIContextEngineering } from "../lib/cli-context-engineering.js";
37
+ import { createChatFn } from "../lib/cowork-adapter.js";
38
+ import {
39
+ detectTaskType,
40
+ selectModelForTask,
41
+ } from "../lib/task-model-selector.js";
42
+ import { executeHooks, HookEvents } from "../lib/hook-manager.js";
43
+ import { CLIPermanentMemory } from "../lib/permanent-memory.js";
44
+ import { CLIAutonomousAgent, GoalStatus } from "../lib/autonomous-agent.js";
29
45
 
30
46
  /**
31
47
  * Tool definitions for function calling
@@ -195,7 +211,12 @@ const TOOLS = [
195
211
  const skillLoader = new CLISkillLoader();
196
212
 
197
213
  /**
198
- * Execute a tool call (with plan mode filtering)
214
+ * Reference to the runtime DB for hook execution (set during startAgentRepl)
215
+ */
216
+ let _hookDb = null;
217
+
218
+ /**
219
+ * Execute a tool call (with plan mode filtering and hook pipeline)
199
220
  */
200
221
  async function executeTool(name, args) {
201
222
  // Plan mode: check if tool is allowed
@@ -218,6 +239,61 @@ async function executeTool(name, args) {
218
239
  };
219
240
  }
220
241
 
242
+ // PreToolUse hook
243
+ if (_hookDb) {
244
+ try {
245
+ await executeHooks(_hookDb, HookEvents.PreToolUse, {
246
+ tool: name,
247
+ args,
248
+ timestamp: new Date().toISOString(),
249
+ });
250
+ } catch (_err) {
251
+ // Hook failure should not block tool execution
252
+ }
253
+ }
254
+
255
+ let toolResult;
256
+ try {
257
+ toolResult = await _executeToolInner(name, args);
258
+ } catch (err) {
259
+ // ToolError hook
260
+ if (_hookDb) {
261
+ try {
262
+ await executeHooks(_hookDb, HookEvents.ToolError, {
263
+ tool: name,
264
+ args,
265
+ error: err.message,
266
+ });
267
+ } catch (_err) {
268
+ // Non-critical
269
+ }
270
+ }
271
+ throw err;
272
+ }
273
+
274
+ // PostToolUse hook
275
+ if (_hookDb) {
276
+ try {
277
+ await executeHooks(_hookDb, HookEvents.PostToolUse, {
278
+ tool: name,
279
+ args,
280
+ result:
281
+ typeof toolResult === "object"
282
+ ? JSON.stringify(toolResult).substring(0, 500)
283
+ : String(toolResult).substring(0, 500),
284
+ });
285
+ } catch (_err) {
286
+ // Non-critical
287
+ }
288
+ }
289
+
290
+ return toolResult;
291
+ }
292
+
293
+ /**
294
+ * Inner tool execution logic (separated for hook wrapping)
295
+ */
296
+ async function _executeToolInner(name, args) {
221
297
  switch (name) {
222
298
  case "read_file": {
223
299
  const filePath = path.resolve(args.path);
@@ -402,13 +478,23 @@ async function executeTool(name, args) {
402
478
  }
403
479
 
404
480
  /**
405
- * Send a chat completion request with tools
481
+ * Send a chat completion request with tools.
482
+ * Supports all 7 providers via cowork-adapter: ollama, anthropic, openai, deepseek, dashscope, gemini, mistral
406
483
  */
407
- async function chatWithTools(messages, options) {
408
- const { provider, model, baseUrl, apiKey } = options;
484
+ async function chatWithTools(rawMessages, options) {
485
+ const { provider, model, baseUrl, apiKey, contextEngine: ce } = options;
486
+
487
+ // Build optimized messages via context engine (or use raw)
488
+ // Find last user message for relevance matching (not tool/assistant)
489
+ const lastUserMsg = [...rawMessages].reverse().find((m) => m.role === "user");
490
+ const messages = ce
491
+ ? ce.buildOptimizedMessages(rawMessages, {
492
+ userQuery: lastUserMsg?.content,
493
+ })
494
+ : rawMessages;
409
495
 
410
496
  if (provider === "ollama") {
411
- // Ollama supports tool calling for some models
497
+ // Ollama supports tool calling natively
412
498
  const response = await fetch(`${baseUrl}/api/chat`, {
413
499
  method: "POST",
414
500
  headers: { "Content-Type": "application/json" },
@@ -425,43 +511,153 @@ async function chatWithTools(messages, options) {
425
511
  }
426
512
 
427
513
  return await response.json();
428
- } else if (provider === "openai") {
514
+ }
515
+
516
+ if (provider === "anthropic") {
517
+ // Anthropic: extract system messages, use tools format
518
+ const key = apiKey || process.env.ANTHROPIC_API_KEY;
519
+ if (!key) throw new Error("ANTHROPIC_API_KEY required");
520
+
521
+ const systemMsgs = messages.filter((m) => m.role === "system");
522
+ const otherMsgs = messages.filter((m) => m.role !== "system");
523
+
524
+ // Convert TOOLS to Anthropic format
525
+ const anthropicTools = TOOLS.map((t) => ({
526
+ name: t.function.name,
527
+ description: t.function.description,
528
+ input_schema: t.function.parameters,
529
+ }));
530
+
531
+ const body = {
532
+ model: model || "claude-sonnet-4-20250514",
533
+ max_tokens: 4096,
534
+ messages: otherMsgs,
535
+ tools: anthropicTools,
536
+ };
537
+ if (systemMsgs.length > 0) {
538
+ body.system = systemMsgs.map((m) => m.content).join("\n");
539
+ }
540
+
429
541
  const url =
430
542
  baseUrl && baseUrl !== "http://localhost:11434"
431
543
  ? baseUrl
432
- : "https://api.openai.com/v1";
433
- const key = apiKey || process.env.OPENAI_API_KEY;
434
- if (!key) throw new Error("API key required");
544
+ : "https://api.anthropic.com/v1";
435
545
 
436
- const response = await fetch(`${url}/chat/completions`, {
546
+ const response = await fetch(`${url}/messages`, {
437
547
  method: "POST",
438
548
  headers: {
439
549
  "Content-Type": "application/json",
440
- Authorization: `Bearer ${key}`,
550
+ "x-api-key": key,
551
+ "anthropic-version": "2023-06-01",
441
552
  },
442
- body: JSON.stringify({
443
- model: model || "gpt-4o-mini",
444
- messages,
445
- tools: TOOLS,
446
- }),
553
+ body: JSON.stringify(body),
447
554
  });
448
555
 
449
556
  if (!response.ok) {
450
- throw new Error(`API error: ${response.status}`);
557
+ throw new Error(`Anthropic error: ${response.status}`);
451
558
  }
452
559
 
453
560
  const data = await response.json();
454
- // Normalize to Ollama-like format
455
- if (!data.choices || !data.choices[0]) {
456
- throw new Error("Invalid API response: no choices returned");
457
- }
458
- const choice = data.choices[0];
459
- return {
460
- message: choice.message,
461
- };
561
+ // Normalize Anthropic response to Ollama-like format
562
+ return _normalizeAnthropicResponse(data);
563
+ }
564
+
565
+ // OpenAI-compatible providers (openai, deepseek, dashscope, mistral, gemini)
566
+ const providerUrls = {
567
+ openai: "https://api.openai.com/v1",
568
+ deepseek: "https://api.deepseek.com/v1",
569
+ dashscope: "https://dashscope.aliyuncs.com/compatible-mode/v1",
570
+ mistral: "https://api.mistral.ai/v1",
571
+ gemini: "https://generativelanguage.googleapis.com/v1beta/openai",
572
+ volcengine: "https://ark.cn-beijing.volces.com/api/v3",
573
+ };
574
+
575
+ const providerApiKeyEnvs = {
576
+ openai: "OPENAI_API_KEY",
577
+ deepseek: "DEEPSEEK_API_KEY",
578
+ dashscope: "DASHSCOPE_API_KEY",
579
+ mistral: "MISTRAL_API_KEY",
580
+ gemini: "GEMINI_API_KEY",
581
+ volcengine: "VOLCENGINE_API_KEY",
582
+ };
583
+
584
+ const url =
585
+ baseUrl && baseUrl !== "http://localhost:11434"
586
+ ? baseUrl
587
+ : providerUrls[provider];
588
+
589
+ if (!url) {
590
+ throw new Error(
591
+ `Unsupported provider: ${provider}. Supported: ollama, anthropic, openai, deepseek, dashscope, mistral, gemini, volcengine`,
592
+ );
593
+ }
594
+
595
+ const envKey = providerApiKeyEnvs[provider] || "OPENAI_API_KEY";
596
+ const key = apiKey || process.env[envKey];
597
+ if (!key) throw new Error(`${envKey} required for provider ${provider}`);
598
+
599
+ const defaultModels = {
600
+ openai: "gpt-4o-mini",
601
+ deepseek: "deepseek-chat",
602
+ dashscope: "qwen-turbo",
603
+ mistral: "mistral-large-latest",
604
+ gemini: "gemini-2.0-flash",
605
+ volcengine: "doubao-seed-1-6-251015",
606
+ };
607
+
608
+ const response = await fetch(`${url}/chat/completions`, {
609
+ method: "POST",
610
+ headers: {
611
+ "Content-Type": "application/json",
612
+ Authorization: `Bearer ${key}`,
613
+ },
614
+ body: JSON.stringify({
615
+ model: model || defaultModels[provider] || "gpt-4o-mini",
616
+ messages,
617
+ tools: TOOLS,
618
+ }),
619
+ });
620
+
621
+ if (!response.ok) {
622
+ throw new Error(`${provider} API error: ${response.status}`);
623
+ }
624
+
625
+ const data = await response.json();
626
+ // Normalize to Ollama-like format
627
+ if (!data.choices || !data.choices[0]) {
628
+ throw new Error("Invalid API response: no choices returned");
629
+ }
630
+ const choice = data.choices[0];
631
+ return {
632
+ message: choice.message,
633
+ };
634
+ }
635
+
636
+ /**
637
+ * Normalize Anthropic API response to Ollama-like format for uniform handling
638
+ */
639
+ function _normalizeAnthropicResponse(data) {
640
+ const content = data.content || [];
641
+ const textBlocks = content.filter((b) => b.type === "text");
642
+ const toolBlocks = content.filter((b) => b.type === "tool_use");
643
+
644
+ const message = {
645
+ role: "assistant",
646
+ content: textBlocks.map((b) => b.text).join("\n") || "",
647
+ };
648
+
649
+ if (toolBlocks.length > 0) {
650
+ message.tool_calls = toolBlocks.map((b) => ({
651
+ id: b.id,
652
+ type: "function",
653
+ function: {
654
+ name: b.name,
655
+ arguments: JSON.stringify(b.input),
656
+ },
657
+ }));
462
658
  }
463
659
 
464
- throw new Error(`Unsupported provider: ${provider}`);
660
+ return { message };
465
661
  }
466
662
 
467
663
  /**
@@ -556,7 +752,8 @@ function formatToolArgs(name, args) {
556
752
  }
557
753
  }
558
754
 
559
- const SYSTEM_PROMPT = `You are ChainlessChain AI Assistant, a powerful agentic coding assistant running in the terminal.
755
+ function getBaseSystemPrompt() {
756
+ return `You are ChainlessChain AI Assistant, a powerful agentic coding assistant running in the terminal.
560
757
 
561
758
  You have access to tools that let you read files, write files, edit files, run shell commands, and search the codebase. When the user asks you to do something, USE THE TOOLS to actually do it — don't just describe what should be done.
562
759
 
@@ -570,6 +767,7 @@ Key behaviors:
570
767
  - Be concise but thorough
571
768
 
572
769
  Current working directory: ${process.cwd()}`;
770
+ }
573
771
 
574
772
  /**
575
773
  * Start the agentic REPL
@@ -578,9 +776,82 @@ export async function startAgentRepl(options = {}) {
578
776
  let model = options.model || "qwen2:7b";
579
777
  let provider = options.provider || "ollama";
580
778
  const baseUrl = options.baseUrl || "http://localhost:11434";
581
- const apiKey = options.apiKey || process.env.OPENAI_API_KEY;
779
+ const apiKey = options.apiKey || null;
780
+
781
+ // Bootstrap runtime (best-effort, DB not required)
782
+ let db = null;
783
+ let contextEngine = null;
784
+ let sessionId = null;
785
+
786
+ try {
787
+ const ctx = await bootstrap({ verbose: false });
788
+ db = ctx.db || null;
789
+ } catch (_err) {
790
+ // Continue without DB — static prompt fallback
791
+ }
792
+
793
+ // Initialize permanent memory
794
+ let permanentMemory = null;
795
+ try {
796
+ const dataDir = process.env.CHAINLESSCHAIN_DATA_DIR || process.cwd();
797
+ const memoryDir = path.join(dataDir, "memory");
798
+ permanentMemory = new CLIPermanentMemory({ db, memoryDir });
799
+ permanentMemory.initialize();
800
+ } catch (_err) {
801
+ // Non-critical
802
+ }
582
803
 
583
- const messages = [{ role: "system", content: SYSTEM_PROMPT }];
804
+ contextEngine = new CLIContextEngineering({ db, permanentMemory });
805
+
806
+ // Initialize autonomous agent
807
+ const autonomousAgent = new CLIAutonomousAgent();
808
+
809
+ // Set hook DB reference for tool pipeline
810
+ _hookDb = db;
811
+
812
+ // Resume existing session or create new one
813
+ if (db && options.sessionId) {
814
+ try {
815
+ const existing = getSession(db, options.sessionId);
816
+ if (existing && existing.messages) {
817
+ sessionId = existing.id;
818
+ }
819
+ } catch (_err) {
820
+ // Non-critical — will create new session
821
+ }
822
+ }
823
+
824
+ if (db && !sessionId) {
825
+ try {
826
+ const session = createSession(db, {
827
+ title: `Agent ${new Date().toISOString().slice(0, 10)}`,
828
+ provider,
829
+ model,
830
+ });
831
+ sessionId = session.id;
832
+ } catch (_err) {
833
+ // Non-critical
834
+ }
835
+ }
836
+
837
+ const messages = [{ role: "system", content: getBaseSystemPrompt() }];
838
+
839
+ // Load resumed session messages
840
+ if (db && options.sessionId && sessionId) {
841
+ try {
842
+ const existing = getSession(db, sessionId);
843
+ if (existing && existing.messages) {
844
+ const parsed =
845
+ typeof existing.messages === "string"
846
+ ? JSON.parse(existing.messages)
847
+ : existing.messages;
848
+ messages.push(...parsed.filter((m) => m.role !== "system"));
849
+ logger.info(`Resumed session ${sessionId} (${parsed.length} messages)`);
850
+ }
851
+ } catch (_err) {
852
+ // Non-critical
853
+ }
854
+ }
584
855
 
585
856
  const getPrompt = () => {
586
857
  const planManager = getPlanModeManager();
@@ -605,6 +876,12 @@ export async function startAgentRepl(options = {}) {
605
876
  logger.log(
606
877
  chalk.gray(`Model: ${model} Provider: ${provider} CWD: ${process.cwd()}`),
607
878
  );
879
+ if (sessionId) {
880
+ logger.log(chalk.gray(`Session: ${sessionId}`));
881
+ }
882
+ if (db) {
883
+ logger.log(chalk.gray("Context: instinct + memory + notes (DB connected)"));
884
+ }
608
885
  logger.log(
609
886
  chalk.gray(
610
887
  "Describe what you want to do. I can read/write files, run commands, and more.",
@@ -640,7 +917,23 @@ export async function startAgentRepl(options = {}) {
640
917
  );
641
918
  logger.log(` ${chalk.cyan("/provider")} Show/change provider`);
642
919
  logger.log(` ${chalk.cyan("/clear")} Clear conversation`);
643
- logger.log(` ${chalk.cyan("/compact")} Keep only last 4 messages`);
920
+ logger.log(
921
+ ` ${chalk.cyan("/compact")} Smart compact (importance-based)`,
922
+ );
923
+ logger.log(
924
+ ` ${chalk.cyan("/task")} Set task objective (/task <objective>)`,
925
+ );
926
+ logger.log(` ${chalk.cyan("/task clear")} Clear current task`);
927
+ logger.log(` ${chalk.cyan("/session")} Show current session info`);
928
+ logger.log(
929
+ ` ${chalk.cyan("/reindex")} Reindex notes for BM25 search`,
930
+ );
931
+ logger.log(
932
+ ` ${chalk.cyan("/stats")} Show context engine statistics`,
933
+ );
934
+ logger.log(
935
+ ` ${chalk.cyan("/auto")} Autonomous goal execution (ReAct loop)`,
936
+ );
644
937
  logger.log(
645
938
  ` ${chalk.cyan("/cowork")} Multi-agent collaboration (debate, compare)`,
646
939
  );
@@ -657,7 +950,10 @@ export async function startAgentRepl(options = {}) {
657
950
  logger.log(" Run shell commands (git, npm, etc.)");
658
951
  logger.log(" Search codebase by filename or content");
659
952
  logger.log(" Run 138 built-in skills (code-review, summarize, etc.)");
660
- logger.log(" Plan mode: analyze first, execute after approval\n");
953
+ logger.log(" Plan mode: analyze first, execute after approval");
954
+ logger.log(
955
+ " Context engineering: instinct + memory + notes injection\n",
956
+ );
661
957
  prompt();
662
958
  return;
663
959
  }
@@ -677,10 +973,31 @@ export async function startAgentRepl(options = {}) {
677
973
  if (trimmed.startsWith("/provider")) {
678
974
  const arg = trimmed.slice(9).trim();
679
975
  if (arg) {
680
- provider = arg;
681
- logger.info(`Provider: ${chalk.cyan(provider)}`);
976
+ const supported = [
977
+ "ollama",
978
+ "anthropic",
979
+ "openai",
980
+ "deepseek",
981
+ "dashscope",
982
+ "mistral",
983
+ "gemini",
984
+ "volcengine",
985
+ ];
986
+ if (supported.includes(arg)) {
987
+ provider = arg;
988
+ logger.info(`Provider: ${chalk.cyan(provider)}`);
989
+ } else {
990
+ logger.info(
991
+ `Unsupported provider. Available: ${supported.join(", ")}`,
992
+ );
993
+ }
682
994
  } else {
683
995
  logger.info(`Current provider: ${chalk.cyan(provider)}`);
996
+ logger.info(
997
+ chalk.gray(
998
+ "Available: ollama, anthropic, openai, deepseek, dashscope, mistral, gemini, volcengine",
999
+ ),
1000
+ );
684
1001
  }
685
1002
  prompt();
686
1003
  return;
@@ -694,13 +1011,107 @@ export async function startAgentRepl(options = {}) {
694
1011
  }
695
1012
 
696
1013
  if (trimmed === "/compact") {
697
- // Keep system prompt + last 4 messages
698
- if (messages.length > 5) {
1014
+ if (contextEngine && messages.length > 5) {
1015
+ const compacted = contextEngine.smartCompact(messages);
1016
+ messages.length = 0;
1017
+ messages.push(...compacted);
1018
+ logger.info(
1019
+ `Compacted to ${messages.length} messages (importance-based)`,
1020
+ );
1021
+ } else if (messages.length > 5) {
1022
+ // Fallback: original logic
699
1023
  const systemMsg = messages[0];
700
1024
  const recent = messages.slice(-4);
701
1025
  messages.length = 0;
702
1026
  messages.push(systemMsg, ...recent);
703
- logger.info("Conversation compacted to last 4 messages");
1027
+ logger.info("Compacted to last 4 messages");
1028
+ }
1029
+ prompt();
1030
+ return;
1031
+ }
1032
+
1033
+ // Task commands
1034
+ if (trimmed.startsWith("/task")) {
1035
+ const taskArg = trimmed.slice(5).trim();
1036
+ if (taskArg === "clear") {
1037
+ contextEngine.clearTask();
1038
+ logger.info("Task cleared");
1039
+ } else if (taskArg) {
1040
+ contextEngine.setTask(taskArg);
1041
+ logger.info(`Task set: ${chalk.cyan(taskArg)}`);
1042
+ } else {
1043
+ if (contextEngine.taskContext) {
1044
+ logger.info(
1045
+ `Current task: ${chalk.cyan(contextEngine.taskContext.objective)}`,
1046
+ );
1047
+ } else {
1048
+ logger.info("No task set. Usage: /task <objective>");
1049
+ }
1050
+ }
1051
+ prompt();
1052
+ return;
1053
+ }
1054
+
1055
+ // Session info
1056
+ if (trimmed.startsWith("/session")) {
1057
+ const sessionArg = trimmed.slice(8).trim();
1058
+ if (sessionArg.startsWith("resume ")) {
1059
+ const resumeId = sessionArg.slice(7).trim();
1060
+ if (!db) {
1061
+ logger.info("No database available for session resume");
1062
+ } else {
1063
+ try {
1064
+ const existing = getSession(db, resumeId);
1065
+ if (existing && existing.messages) {
1066
+ const parsed =
1067
+ typeof existing.messages === "string"
1068
+ ? JSON.parse(existing.messages)
1069
+ : existing.messages;
1070
+ messages.length = 1; // keep system prompt
1071
+ messages.push(...parsed.filter((m) => m.role !== "system"));
1072
+ sessionId = existing.id;
1073
+ logger.info(
1074
+ `Resumed session ${sessionId} (${parsed.length} messages)`,
1075
+ );
1076
+ } else {
1077
+ logger.info(`Session not found: ${resumeId}`);
1078
+ }
1079
+ } catch (err) {
1080
+ logger.error(`Resume failed: ${err.message}`);
1081
+ }
1082
+ }
1083
+ } else {
1084
+ logger.info(`Session ID: ${sessionId || "none"}`);
1085
+ logger.info(`Messages: ${messages.length}`);
1086
+ logger.info(`DB: ${db ? "connected" : "not available"}`);
1087
+ }
1088
+ prompt();
1089
+ return;
1090
+ }
1091
+
1092
+ // Reindex notes
1093
+ if (trimmed === "/reindex") {
1094
+ if (contextEngine) {
1095
+ contextEngine.reindexNotes();
1096
+ const stats = contextEngine.getStats();
1097
+ logger.info(`Notes reindexed: ${stats.notesIndexed} documents`);
1098
+ } else {
1099
+ logger.info("Context engine not available");
1100
+ }
1101
+ prompt();
1102
+ return;
1103
+ }
1104
+
1105
+ // Stats
1106
+ if (trimmed === "/stats") {
1107
+ if (contextEngine) {
1108
+ const stats = contextEngine.getStats();
1109
+ logger.info(`DB connected: ${stats.hasDb}`);
1110
+ logger.info(`Notes indexed: ${stats.notesIndexed}`);
1111
+ logger.info(`Error history: ${stats.errorCount}`);
1112
+ logger.info(`Active task: ${stats.hasTask}`);
1113
+ } else {
1114
+ logger.info("Context engine not available");
704
1115
  }
705
1116
  prompt();
706
1117
  return;
@@ -715,10 +1126,16 @@ export async function startAgentRepl(options = {}) {
715
1126
  if (!subCmd || subCmd === "help") {
716
1127
  logger.log(chalk.bold("\nCowork Commands:"));
717
1128
  logger.log(
718
- ` ${chalk.cyan("/cowork debate <file>")} Multi-perspective code review`,
1129
+ ` ${chalk.cyan("/cowork debate <file>")} Multi-perspective code review`,
1130
+ );
1131
+ logger.log(
1132
+ ` ${chalk.cyan("/cowork compare <prompt>")} A/B solution comparison`,
1133
+ );
1134
+ logger.log(
1135
+ ` ${chalk.cyan("/cowork graph <path>")} Code knowledge graph (ASCII)`,
719
1136
  );
720
1137
  logger.log(
721
- ` ${chalk.cyan("/cowork compare <prompt>")} A/B solution comparison`,
1138
+ ` ${chalk.cyan("/cowork decision <topic>")} Architecture decision tracking`,
722
1139
  );
723
1140
  logger.log("");
724
1141
  } else if (subCmd === "debate" && coworkInput) {
@@ -787,9 +1204,88 @@ export async function startAgentRepl(options = {}) {
787
1204
  } catch (err) {
788
1205
  logger.error(`Compare failed: ${err.message}`);
789
1206
  }
1207
+ } else if (subCmd === "graph" && coworkInput) {
1208
+ try {
1209
+ const { analyzeCodeKnowledgeGraph } =
1210
+ await import("../lib/cowork/code-knowledge-graph-cli.js");
1211
+ process.stdout.write(chalk.gray("\n Analyzing code graph...\n"));
1212
+ const result = await analyzeCodeKnowledgeGraph({
1213
+ target: coworkInput,
1214
+ llmOptions: { provider, model, baseUrl, apiKey },
1215
+ });
1216
+ // ASCII dependency graph
1217
+ if (result.entities && result.entities.length > 0) {
1218
+ process.stdout.write(chalk.bold(" Code Knowledge Graph:\n"));
1219
+ for (const entity of result.entities.slice(0, 15)) {
1220
+ const deps = (entity.dependencies || []).slice(0, 3).join(", ");
1221
+ process.stdout.write(
1222
+ ` ${chalk.cyan(entity.name)} [${entity.type}]${deps ? ` → ${deps}` : ""}\n`,
1223
+ );
1224
+ }
1225
+ if (result.relationships && result.relationships.length > 0) {
1226
+ process.stdout.write(chalk.bold("\n Relationships:\n"));
1227
+ for (const rel of result.relationships.slice(0, 10)) {
1228
+ process.stdout.write(
1229
+ ` ${rel.source} ${chalk.gray(`—${rel.type}→`)} ${rel.target}\n`,
1230
+ );
1231
+ }
1232
+ }
1233
+ } else {
1234
+ process.stdout.write(
1235
+ ` ${JSON.stringify(result).substring(0, 500)}\n`,
1236
+ );
1237
+ }
1238
+ process.stdout.write("\n");
1239
+ messages.push({
1240
+ role: "assistant",
1241
+ content: `[Cowork Graph] Analyzed ${(result.entities || []).length} entities with ${(result.relationships || []).length} relationships.`,
1242
+ });
1243
+ } catch (err) {
1244
+ logger.error(`Graph analysis failed: ${err.message}`);
1245
+ }
1246
+ } else if (subCmd === "decision" && coworkInput) {
1247
+ try {
1248
+ const { analyzeDecisions } =
1249
+ await import("../lib/cowork/decision-kb-cli.js");
1250
+ process.stdout.write(chalk.gray("\n Analyzing decisions...\n"));
1251
+ const result = await analyzeDecisions({
1252
+ target: coworkInput,
1253
+ llmOptions: { provider, model, baseUrl, apiKey },
1254
+ });
1255
+ if (result.decisions && result.decisions.length > 0) {
1256
+ process.stdout.write(chalk.bold(" Architecture Decisions:\n"));
1257
+ for (const d of result.decisions) {
1258
+ const statusColor =
1259
+ d.status === "accepted"
1260
+ ? chalk.green
1261
+ : d.status === "rejected"
1262
+ ? chalk.red
1263
+ : chalk.yellow;
1264
+ process.stdout.write(
1265
+ ` ${statusColor(`[${d.status || "proposed"}]`)} ${chalk.cyan(d.title || d.id)}\n`,
1266
+ );
1267
+ if (d.rationale) {
1268
+ process.stdout.write(
1269
+ ` ${chalk.gray(d.rationale.substring(0, 100))}\n`,
1270
+ );
1271
+ }
1272
+ }
1273
+ } else {
1274
+ process.stdout.write(
1275
+ ` ${JSON.stringify(result).substring(0, 500)}\n`,
1276
+ );
1277
+ }
1278
+ process.stdout.write("\n");
1279
+ messages.push({
1280
+ role: "assistant",
1281
+ content: `[Cowork Decision] Found ${(result.decisions || []).length} architecture decisions.`,
1282
+ });
1283
+ } catch (err) {
1284
+ logger.error(`Decision analysis failed: ${err.message}`);
1285
+ }
790
1286
  } else {
791
1287
  logger.info(
792
- "Usage: /cowork debate <file-or-topic> or /cowork compare <prompt>",
1288
+ "Usage: /cowork debate <file> | compare <prompt> | graph <path> | decision <topic>",
793
1289
  );
794
1290
  }
795
1291
 
@@ -797,6 +1293,144 @@ export async function startAgentRepl(options = {}) {
797
1293
  return;
798
1294
  }
799
1295
 
1296
+ // Autonomous agent commands
1297
+ if (trimmed.startsWith("/auto")) {
1298
+ const autoArg = trimmed.slice(5).trim();
1299
+
1300
+ if (!autoArg || autoArg === "help") {
1301
+ logger.log(chalk.bold("\nAutonomous Agent Commands:"));
1302
+ logger.log(
1303
+ ` ${chalk.cyan("/auto <goal>")} Submit a goal for autonomous execution`,
1304
+ );
1305
+ logger.log(
1306
+ ` ${chalk.cyan("/auto status")} Show current goal status`,
1307
+ );
1308
+ logger.log(
1309
+ ` ${chalk.cyan("/auto pause")} Pause the running goal`,
1310
+ );
1311
+ logger.log(` ${chalk.cyan("/auto resume")} Resume a paused goal`);
1312
+ logger.log(
1313
+ ` ${chalk.cyan("/auto cancel")} Cancel the running goal`,
1314
+ );
1315
+ logger.log(` ${chalk.cyan("/auto list")} List all goals`);
1316
+ logger.log("");
1317
+ } else if (autoArg === "status") {
1318
+ const goals = autonomousAgent.listGoals();
1319
+ const running = goals.find(
1320
+ (g) =>
1321
+ g.status === GoalStatus.RUNNING || g.status === GoalStatus.PAUSED,
1322
+ );
1323
+ if (running) {
1324
+ const detail = autonomousAgent.getGoalStatus(running.id);
1325
+ logger.info(`Goal: ${chalk.cyan(detail.description)}`);
1326
+ logger.info(
1327
+ `Status: ${detail.status} Steps: ${detail.steps.length} Iterations: ${detail.iterations}`,
1328
+ );
1329
+ for (const step of detail.steps) {
1330
+ const icon =
1331
+ step.status === "completed"
1332
+ ? "✓"
1333
+ : step.status === "running"
1334
+ ? "→"
1335
+ : step.status === "failed"
1336
+ ? "✗"
1337
+ : "○";
1338
+ logger.log(
1339
+ ` ${icon} ${step.description} ${step.error ? chalk.red(`(${step.error})`) : ""}`,
1340
+ );
1341
+ }
1342
+ } else {
1343
+ logger.info("No active goal. Use /auto <goal> to submit one.");
1344
+ }
1345
+ } else if (autoArg === "pause") {
1346
+ const goals = autonomousAgent.listGoals();
1347
+ const running = goals.find((g) => g.status === GoalStatus.RUNNING);
1348
+ if (running) {
1349
+ autonomousAgent.pauseGoal(running.id);
1350
+ logger.info(`Paused goal: ${running.description}`);
1351
+ } else {
1352
+ logger.info("No running goal to pause.");
1353
+ }
1354
+ } else if (autoArg === "resume") {
1355
+ const goals = autonomousAgent.listGoals();
1356
+ const paused = goals.find((g) => g.status === GoalStatus.PAUSED);
1357
+ if (paused) {
1358
+ autonomousAgent.resumeGoal(paused.id);
1359
+ logger.info(`Resumed goal: ${paused.description}`);
1360
+ } else {
1361
+ logger.info("No paused goal to resume.");
1362
+ }
1363
+ } else if (autoArg === "cancel") {
1364
+ const goals = autonomousAgent.listGoals();
1365
+ const active = goals.find(
1366
+ (g) =>
1367
+ g.status === GoalStatus.RUNNING || g.status === GoalStatus.PAUSED,
1368
+ );
1369
+ if (active) {
1370
+ autonomousAgent.cancelGoal(active.id);
1371
+ logger.info(`Cancelled goal: ${active.description}`);
1372
+ } else {
1373
+ logger.info("No active goal to cancel.");
1374
+ }
1375
+ } else if (autoArg === "list") {
1376
+ const goals = autonomousAgent.listGoals();
1377
+ if (goals.length === 0) {
1378
+ logger.info("No goals submitted yet.");
1379
+ } else {
1380
+ for (const g of goals) {
1381
+ logger.log(
1382
+ ` [${g.status}] ${g.description} (${g.steps} steps, ${g.iterations} iterations)`,
1383
+ );
1384
+ }
1385
+ }
1386
+ } else {
1387
+ // Submit new goal
1388
+ // Lazy-init autonomous agent with LLM chat function
1389
+ if (!autonomousAgent._initialized) {
1390
+ const chatFn = createChatFn({ provider, model, baseUrl, apiKey });
1391
+ autonomousAgent.initialize({
1392
+ llmChat: chatFn,
1393
+ toolExecutor: executeTool,
1394
+ });
1395
+ }
1396
+
1397
+ // Set up event listeners for live output
1398
+ const goalListener = (evt) => {
1399
+ if (evt.goalId) {
1400
+ if (evt.result)
1401
+ process.stdout.write(
1402
+ chalk.green(` Goal completed: ${evt.result}\n`),
1403
+ );
1404
+ if (evt.error)
1405
+ process.stdout.write(chalk.red(` Goal failed: ${evt.error}\n`));
1406
+ }
1407
+ };
1408
+ const stepListener = (evt) => {
1409
+ process.stdout.write(chalk.gray(` [step] ${evt.step}\n`));
1410
+ };
1411
+
1412
+ autonomousAgent.on("goal:completed", goalListener);
1413
+ autonomousAgent.on("goal:failed", goalListener);
1414
+ autonomousAgent.on("step:started", stepListener);
1415
+ autonomousAgent.on("step:completed", (evt) => {
1416
+ process.stdout.write(chalk.green(` [done] ${evt.step}\n`));
1417
+ });
1418
+
1419
+ logger.info(`Submitting goal: ${chalk.cyan(autoArg)}`);
1420
+ try {
1421
+ const { goalId } = await autonomousAgent.submitGoal(autoArg);
1422
+ logger.info(
1423
+ `Goal ${goalId} submitted. Use /auto status to track progress.`,
1424
+ );
1425
+ } catch (err) {
1426
+ logger.error(`Failed to submit goal: ${err.message}`);
1427
+ }
1428
+ }
1429
+
1430
+ prompt();
1431
+ return;
1432
+ }
1433
+
800
1434
  // Plan mode commands
801
1435
  if (trimmed.startsWith("/plan")) {
802
1436
  const planManager = getPlanModeManager();
@@ -858,6 +1492,58 @@ export async function startAgentRepl(options = {}) {
858
1492
  planManager.rejectPlan("User rejected");
859
1493
  logger.info("Plan rejected. Exited plan mode.");
860
1494
  }
1495
+ } else if (subCmd === "risk") {
1496
+ if (!planManager.isActive() || !planManager.currentPlan) {
1497
+ logger.info("No active plan to assess.");
1498
+ } else {
1499
+ const risk = planManager.getRiskAssessment();
1500
+ logger.log(
1501
+ `\nRisk Level: ${chalk.bold(risk.level.toUpperCase())} (total: ${risk.totalScore}, max: ${risk.maxScore}, avg: ${risk.averageScore})`,
1502
+ );
1503
+ for (const item of risk.itemScores) {
1504
+ const color =
1505
+ item.score >= 6
1506
+ ? chalk.red
1507
+ : item.score >= 3
1508
+ ? chalk.yellow
1509
+ : chalk.green;
1510
+ logger.log(` ${color(`[${item.score}]`)} ${item.title}`);
1511
+ }
1512
+ logger.log("");
1513
+ }
1514
+ } else if (subCmd === "execute") {
1515
+ if (!planManager.isActive()) {
1516
+ logger.info("No active plan.");
1517
+ } else if (planManager.state !== PlanState.APPROVED) {
1518
+ logger.info("Plan must be approved first. Use /plan approve.");
1519
+ } else {
1520
+ logger.info("Executing plan items in DAG order...");
1521
+ try {
1522
+ const { results, success } = await planManager.executePlan(
1523
+ async (item) => {
1524
+ if (item.tool && item.params) {
1525
+ process.stdout.write(
1526
+ chalk.gray(` [${item.tool}] ${item.title}\n`),
1527
+ );
1528
+ return await executeTool(item.tool, item.params);
1529
+ }
1530
+ return { skipped: true };
1531
+ },
1532
+ );
1533
+ for (const r of results) {
1534
+ const icon = r.success ? chalk.green("✓") : chalk.red("✗");
1535
+ logger.log(
1536
+ ` ${icon} ${r.item.title}${r.error ? ` — ${r.error}` : ""}`,
1537
+ );
1538
+ }
1539
+ logger.info(
1540
+ `Plan execution ${success ? "completed" : "finished with errors"}.`,
1541
+ );
1542
+ planManager.exitPlanMode({ savePlan: true });
1543
+ } catch (err) {
1544
+ logger.error(`Plan execution failed: ${err.message}`);
1545
+ }
1546
+ }
861
1547
  } else if (subCmd === "exit") {
862
1548
  if (planManager.isActive()) {
863
1549
  planManager.exitPlanMode({ savePlan: true });
@@ -878,13 +1564,27 @@ export async function startAgentRepl(options = {}) {
878
1564
  // Add user message
879
1565
  messages.push({ role: "user", content: trimmed });
880
1566
 
1567
+ // Auto-select best model based on task type
1568
+ let activeModel = model;
1569
+ const taskDetection = detectTaskType(trimmed);
1570
+ if (taskDetection.confidence > 0.3) {
1571
+ const recommended = selectModelForTask(provider, taskDetection.taskType);
1572
+ if (recommended && recommended !== activeModel) {
1573
+ activeModel = recommended;
1574
+ logger.info(
1575
+ chalk.gray(`[auto] ${taskDetection.name} → ${activeModel}`),
1576
+ );
1577
+ }
1578
+ }
1579
+
881
1580
  try {
882
1581
  process.stdout.write("\n");
883
1582
  const response = await agentLoop(messages, {
884
1583
  provider,
885
- model,
1584
+ model: activeModel,
886
1585
  baseUrl,
887
1586
  apiKey,
1587
+ contextEngine,
888
1588
  });
889
1589
 
890
1590
  if (response) {
@@ -893,9 +1593,34 @@ export async function startAgentRepl(options = {}) {
893
1593
  } else {
894
1594
  process.stdout.write("\n");
895
1595
  }
1596
+
1597
+ // Auto-save session
1598
+ if (db && sessionId) {
1599
+ try {
1600
+ saveMessages(db, sessionId, messages);
1601
+ } catch (_e) {
1602
+ // Non-critical
1603
+ }
1604
+ }
1605
+ // Store as episodic memory
1606
+ if (db) {
1607
+ try {
1608
+ storeMemory(db, trimmed, { importance: 0.3, type: "episodic" });
1609
+ } catch (_e) {
1610
+ // Non-critical
1611
+ }
1612
+ }
896
1613
  } catch (err) {
897
1614
  logger.error(`Error: ${err.message}`);
898
1615
 
1616
+ // Record error for context injection
1617
+ if (contextEngine) {
1618
+ contextEngine.recordError({
1619
+ step: "agent-loop",
1620
+ message: err.message,
1621
+ });
1622
+ }
1623
+
899
1624
  // If connection error, provide helpful message
900
1625
  if (
901
1626
  err.message.includes("ECONNREFUSED") ||
@@ -911,7 +1636,37 @@ export async function startAgentRepl(options = {}) {
911
1636
  prompt();
912
1637
  });
913
1638
 
914
- rl.on("close", () => {
1639
+ rl.on("close", async () => {
1640
+ // Save session on exit
1641
+ if (db && sessionId) {
1642
+ try {
1643
+ saveMessages(db, sessionId, messages);
1644
+ } catch (_e) {
1645
+ // Non-critical
1646
+ }
1647
+ }
1648
+ // Auto-summarize session into permanent memory
1649
+ if (permanentMemory && messages.length > 4) {
1650
+ try {
1651
+ permanentMemory.autoSummarize(messages);
1652
+ } catch (_e) {
1653
+ // Non-critical
1654
+ }
1655
+ }
1656
+ // Consolidate memory
1657
+ if (db) {
1658
+ try {
1659
+ consolidateMemory(db);
1660
+ } catch (_e) {
1661
+ // Non-critical
1662
+ }
1663
+ }
1664
+ // Shutdown runtime
1665
+ try {
1666
+ await shutdown();
1667
+ } catch (_e) {
1668
+ // Non-critical
1669
+ }
915
1670
  process.exit(0);
916
1671
  });
917
1672
  }