@aman_asmuei/aman-agent 0.4.0 → 0.5.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.
package/dist/index.js CHANGED
@@ -13,7 +13,8 @@ var DEFAULT_HOOKS = {
13
13
  rulesCheck: true,
14
14
  workflowSuggest: true,
15
15
  evalPrompt: true,
16
- autoSessionSave: true
16
+ autoSessionSave: true,
17
+ extractMemories: true
17
18
  };
18
19
  var CONFIG_DIR = path.join(os.homedir(), ".aman-agent");
19
20
  var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
@@ -462,6 +463,74 @@ function createOllamaClient(model, baseURL) {
462
463
  // src/mcp/client.ts
463
464
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
464
465
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
466
+
467
+ // src/logger.ts
468
+ import fs3 from "fs";
469
+ import path3 from "path";
470
+ import os3 from "os";
471
+ var LOG_DIR = path3.join(os3.homedir(), ".aman-agent");
472
+ var LOG_PATH = path3.join(LOG_DIR, "debug.log");
473
+ var MAX_LOG_SIZE = 1048576;
474
+ function ensureDir() {
475
+ if (!fs3.existsSync(LOG_DIR)) {
476
+ fs3.mkdirSync(LOG_DIR, { recursive: true });
477
+ }
478
+ }
479
+ function maybeRotate() {
480
+ try {
481
+ if (!fs3.existsSync(LOG_PATH)) return;
482
+ const stat = fs3.statSync(LOG_PATH);
483
+ if (stat.size >= MAX_LOG_SIZE) {
484
+ const backupPath = LOG_PATH + ".1";
485
+ if (fs3.existsSync(backupPath)) fs3.unlinkSync(backupPath);
486
+ fs3.renameSync(LOG_PATH, backupPath);
487
+ }
488
+ } catch {
489
+ }
490
+ }
491
+ function write(level, module, message, data) {
492
+ try {
493
+ ensureDir();
494
+ maybeRotate();
495
+ const entry = {
496
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
497
+ level,
498
+ module,
499
+ message
500
+ };
501
+ if (data !== void 0) {
502
+ entry.data = data instanceof Error ? data.message : String(data);
503
+ }
504
+ fs3.appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n");
505
+ } catch {
506
+ }
507
+ }
508
+ var log = {
509
+ debug: (module, message, data) => write("debug", module, message, data),
510
+ warn: (module, message, data) => write("warn", module, message, data),
511
+ error: (module, message, data) => write("error", module, message, data)
512
+ };
513
+
514
+ // src/retry.ts
515
+ async function withRetry(fn, options) {
516
+ const { maxAttempts, baseDelay, retryable } = options;
517
+ let lastError;
518
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
519
+ try {
520
+ return await fn();
521
+ } catch (err) {
522
+ lastError = err instanceof Error ? err : new Error(String(err));
523
+ if (!retryable(lastError) || attempt === maxAttempts) {
524
+ throw lastError;
525
+ }
526
+ const delay = baseDelay * Math.pow(2, attempt - 1) * (0.5 + Math.random() * 0.5);
527
+ await new Promise((resolve) => setTimeout(resolve, delay));
528
+ }
529
+ }
530
+ throw lastError;
531
+ }
532
+
533
+ // src/mcp/client.ts
465
534
  var McpManager = class {
466
535
  connections = [];
467
536
  tools = [];
@@ -483,7 +552,8 @@ var McpManager = class {
483
552
  serverName: name
484
553
  });
485
554
  }
486
- } catch {
555
+ } catch (err) {
556
+ log.error("mcp", "Failed to connect to " + name + " MCP server", err);
487
557
  console.error(` Warning: Could not connect to ${name} MCP server`);
488
558
  }
489
559
  }
@@ -496,10 +566,10 @@ var McpManager = class {
496
566
  const conn = this.connections.find((c) => c.name === tool.serverName);
497
567
  if (!conn) return `Error: server ${tool.serverName} not connected`;
498
568
  try {
499
- const result = await conn.client.callTool({
500
- name: toolName,
501
- arguments: args
502
- });
569
+ const result = await withRetry(
570
+ () => conn.client.callTool({ name: toolName, arguments: args }),
571
+ { maxAttempts: 2, baseDelay: 500, retryable: (err) => err.message.includes("ETIMEDOUT") || err.message.includes("timeout") }
572
+ );
503
573
  if (result.content && Array.isArray(result.content)) {
504
574
  return result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n");
505
575
  }
@@ -512,7 +582,8 @@ var McpManager = class {
512
582
  for (const conn of this.connections) {
513
583
  try {
514
584
  await conn.client.close();
515
- } catch {
585
+ } catch (err) {
586
+ log.debug("mcp", "Cleanup error disconnecting " + conn.name, err);
516
587
  }
517
588
  }
518
589
  this.connections = [];
@@ -522,20 +593,23 @@ var McpManager = class {
522
593
 
523
594
  // src/agent.ts
524
595
  import * as readline from "readline";
596
+ import fs6 from "fs";
597
+ import path6 from "path";
598
+ import os6 from "os";
525
599
  import pc3 from "picocolors";
526
600
 
527
601
  // src/commands.ts
528
- import fs4 from "fs";
529
- import path4 from "path";
530
- import os4 from "os";
602
+ import fs5 from "fs";
603
+ import path5 from "path";
604
+ import os5 from "os";
531
605
  import { execFileSync } from "child_process";
532
606
  import pc from "picocolors";
533
607
 
534
608
  // src/layers/parsers.ts
535
- import fs3 from "fs";
536
- import path3 from "path";
537
- import os3 from "os";
538
- var home = os3.homedir();
609
+ import fs4 from "fs";
610
+ import path4 from "path";
611
+ import os4 from "os";
612
+ var home = os4.homedir();
539
613
  var LAYER_FILES = [
540
614
  { name: "identity", dir: ".acore", file: "core.md" },
541
615
  { name: "rules", dir: ".arules", file: "rules.md" },
@@ -571,11 +645,11 @@ function getLayerSummary(name, content) {
571
645
  }
572
646
  function getEcosystemStatus(mcpToolCount, amemConnected) {
573
647
  const layers = LAYER_FILES.map((entry) => {
574
- const filePath = path3.join(home, entry.dir, entry.file);
575
- const exists = fs3.existsSync(filePath);
648
+ const filePath = path4.join(home, entry.dir, entry.file);
649
+ const exists = fs4.existsSync(filePath);
576
650
  let summary = "not configured";
577
651
  if (exists) {
578
- const content = fs3.readFileSync(filePath, "utf-8");
652
+ const content = fs4.readFileSync(filePath, "utf-8");
579
653
  summary = getLayerSummary(entry.name, content);
580
654
  }
581
655
  return { name: entry.name, exists, path: filePath, summary };
@@ -590,10 +664,10 @@ function getEcosystemStatus(mcpToolCount, amemConnected) {
590
664
 
591
665
  // src/commands.ts
592
666
  function readEcosystemFile(filePath, label) {
593
- if (!fs4.existsSync(filePath)) {
667
+ if (!fs5.existsSync(filePath)) {
594
668
  return pc.dim(`No ${label} file found at ${filePath}`);
595
669
  }
596
- return fs4.readFileSync(filePath, "utf-8").trim();
670
+ return fs5.readFileSync(filePath, "utf-8").trim();
597
671
  }
598
672
  function parseCommand(input) {
599
673
  const trimmed = input.trim();
@@ -614,9 +688,9 @@ async function mcpWrite(ctx, layer, tool, args) {
614
688
  return pc.green(result);
615
689
  }
616
690
  async function handleIdentityCommand(action, args, ctx) {
617
- const home2 = os4.homedir();
691
+ const home2 = os5.homedir();
618
692
  if (!action) {
619
- const content = readEcosystemFile(path4.join(home2, ".acore", "core.md"), "identity (acore)");
693
+ const content = readEcosystemFile(path5.join(home2, ".acore", "core.md"), "identity (acore)");
620
694
  return { handled: true, output: content };
621
695
  }
622
696
  if (action === "update") {
@@ -640,9 +714,9 @@ async function handleIdentityCommand(action, args, ctx) {
640
714
  return { handled: true, output: pc.yellow(`Unknown action: /identity ${action}. Use /identity or /identity update <section>.`) };
641
715
  }
642
716
  async function handleRulesCommand(action, args, ctx) {
643
- const home2 = os4.homedir();
717
+ const home2 = os5.homedir();
644
718
  if (!action) {
645
- const content = readEcosystemFile(path4.join(home2, ".arules", "rules.md"), "guardrails (arules)");
719
+ const content = readEcosystemFile(path5.join(home2, ".arules", "rules.md"), "guardrails (arules)");
646
720
  return { handled: true, output: content };
647
721
  }
648
722
  if (action === "add") {
@@ -671,9 +745,9 @@ async function handleRulesCommand(action, args, ctx) {
671
745
  return { handled: true, output: pc.yellow(`Unknown action: /rules ${action}. Use /rules [add|remove|toggle].`) };
672
746
  }
673
747
  async function handleWorkflowsCommand(action, args, ctx) {
674
- const home2 = os4.homedir();
748
+ const home2 = os5.homedir();
675
749
  if (!action) {
676
- const content = readEcosystemFile(path4.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
750
+ const content = readEcosystemFile(path5.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
677
751
  return { handled: true, output: content };
678
752
  }
679
753
  if (action === "add") {
@@ -693,9 +767,9 @@ async function handleWorkflowsCommand(action, args, ctx) {
693
767
  return { handled: true, output: pc.yellow(`Unknown action: /workflows ${action}. Use /workflows [add|remove].`) };
694
768
  }
695
769
  async function handleToolsCommand(action, args, ctx) {
696
- const home2 = os4.homedir();
770
+ const home2 = os5.homedir();
697
771
  if (!action) {
698
- const content = readEcosystemFile(path4.join(home2, ".akit", "kit.md"), "tools (akit)");
772
+ const content = readEcosystemFile(path5.join(home2, ".akit", "kit.md"), "tools (akit)");
699
773
  return { handled: true, output: content };
700
774
  }
701
775
  if (action === "add") {
@@ -718,9 +792,9 @@ async function handleToolsCommand(action, args, ctx) {
718
792
  return { handled: true, output: pc.yellow(`Unknown action: /tools ${action}. Use /tools [add|remove].`) };
719
793
  }
720
794
  async function handleSkillsCommand(action, args, ctx) {
721
- const home2 = os4.homedir();
795
+ const home2 = os5.homedir();
722
796
  if (!action) {
723
- const content = readEcosystemFile(path4.join(home2, ".askill", "skills.md"), "skills (askill)");
797
+ const content = readEcosystemFile(path5.join(home2, ".askill", "skills.md"), "skills (askill)");
724
798
  return { handled: true, output: content };
725
799
  }
726
800
  if (action === "install") {
@@ -740,9 +814,9 @@ async function handleSkillsCommand(action, args, ctx) {
740
814
  return { handled: true, output: pc.yellow(`Unknown action: /skills ${action}. Use /skills [install|uninstall].`) };
741
815
  }
742
816
  async function handleEvalCommand(action, args, ctx) {
743
- const home2 = os4.homedir();
817
+ const home2 = os5.homedir();
744
818
  if (!action) {
745
- const content = readEcosystemFile(path4.join(home2, ".aeval", "eval.md"), "evaluation (aeval)");
819
+ const content = readEcosystemFile(path5.join(home2, ".aeval", "eval.md"), "evaluation (aeval)");
746
820
  return { handled: true, output: content };
747
821
  }
748
822
  if (action === "milestone") {
@@ -833,6 +907,9 @@ function handleHelp() {
833
907
  ` ${pc.cyan("/memory")} View recent memories [search|clear ...]`,
834
908
  ` ${pc.cyan("/status")} Ecosystem dashboard`,
835
909
  ` ${pc.cyan("/doctor")} Health check all layers`,
910
+ ` ${pc.cyan("/decisions")} View decision log [<project>]`,
911
+ ` ${pc.cyan("/export")} Export conversation to markdown`,
912
+ ` ${pc.cyan("/debug")} Show debug log`,
836
913
  ` ${pc.cyan("/save")} Save conversation to memory`,
837
914
  ` ${pc.cyan("/model")} Show current LLM model`,
838
915
  ` ${pc.cyan("/update")} Check for updates`,
@@ -846,9 +923,9 @@ function handleSave() {
846
923
  return { handled: true, saveConversation: true };
847
924
  }
848
925
  function handleReconfig() {
849
- const configPath = path4.join(os4.homedir(), ".aman-agent", "config.json");
850
- if (fs4.existsSync(configPath)) {
851
- fs4.unlinkSync(configPath);
926
+ const configPath = path5.join(os5.homedir(), ".aman-agent", "config.json");
927
+ if (fs5.existsSync(configPath)) {
928
+ fs5.unlinkSync(configPath);
852
929
  }
853
930
  return {
854
931
  handled: true,
@@ -862,7 +939,7 @@ function handleReconfig() {
862
939
  function handleUpdate() {
863
940
  try {
864
941
  const current = execFileSync("npm", ["view", "@aman_asmuei/aman-agent", "version"], { encoding: "utf-8" }).trim();
865
- const local = JSON.parse(fs4.readFileSync(path4.join(__dirname, "..", "package.json"), "utf-8")).version;
942
+ const local = JSON.parse(fs5.readFileSync(path5.join(__dirname, "..", "package.json"), "utf-8")).version;
866
943
  if (current === local) {
867
944
  return { handled: true, output: `${pc.green("Up to date")} \u2014 v${local}` };
868
945
  }
@@ -891,6 +968,35 @@ function handleUpdate() {
891
968
  };
892
969
  }
893
970
  }
971
+ async function handleDecisionsCommand(action, _args, ctx) {
972
+ if (!ctx.mcpManager) {
973
+ return { handled: true, output: pc.red("Decisions not available: MCP not connected.") };
974
+ }
975
+ const scope = action || void 0;
976
+ const result = await ctx.mcpManager.callTool("memory_recall", {
977
+ query: "decision",
978
+ type: "decision",
979
+ limit: 20,
980
+ ...scope ? { scope } : {}
981
+ });
982
+ if (result.startsWith("Error")) {
983
+ return { handled: true, output: pc.red(result) };
984
+ }
985
+ return { handled: true, output: pc.bold("Decision Log:\n") + result };
986
+ }
987
+ function handleExportCommand() {
988
+ return { handled: true, exportConversation: true };
989
+ }
990
+ function handleDebugCommand() {
991
+ const logPath = path5.join(os5.homedir(), ".aman-agent", "debug.log");
992
+ if (!fs5.existsSync(logPath)) {
993
+ return { handled: true, output: pc.dim("No debug log found.") };
994
+ }
995
+ const content = fs5.readFileSync(logPath, "utf-8");
996
+ const lines = content.trim().split("\n");
997
+ const last20 = lines.slice(-20).join("\n");
998
+ return { handled: true, output: pc.bold("Debug Log (last 20 entries):\n") + pc.dim(last20) };
999
+ }
894
1000
  async function handleCommand(input, ctx) {
895
1001
  const trimmed = input.trim();
896
1002
  if (!trimmed.startsWith("/")) return { handled: false };
@@ -926,6 +1032,12 @@ async function handleCommand(input, ctx) {
926
1032
  return handleDoctorCommand(ctx);
927
1033
  case "save":
928
1034
  return handleSave();
1035
+ case "decisions":
1036
+ return handleDecisionsCommand(action, args, ctx);
1037
+ case "export":
1038
+ return handleExportCommand();
1039
+ case "debug":
1040
+ return handleDebugCommand();
929
1041
  case "update-config":
930
1042
  case "reconfig":
931
1043
  return handleReconfig();
@@ -940,6 +1052,24 @@ async function handleCommand(input, ctx) {
940
1052
  // src/hooks.ts
941
1053
  import pc2 from "picocolors";
942
1054
  import * as p from "@clack/prompts";
1055
+ function getTimeContext() {
1056
+ const now = /* @__PURE__ */ new Date();
1057
+ const hour = now.getHours();
1058
+ const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
1059
+ const day = days[now.getDay()];
1060
+ let period;
1061
+ if (hour < 6) period = "late-night";
1062
+ else if (hour < 12) period = "morning";
1063
+ else if (hour < 17) period = "afternoon";
1064
+ else if (hour < 21) period = "evening";
1065
+ else period = "night";
1066
+ const timeStr = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
1067
+ const dateStr = now.toLocaleDateString();
1068
+ return `<time-context>
1069
+ Current time: ${dateStr} ${timeStr} (${period}, ${day})
1070
+ Adapt your tone naturally \u2014 don't announce the time, just be contextually appropriate.
1071
+ </time-context>`;
1072
+ }
943
1073
  var isHookCall = false;
944
1074
  async function onSessionStart(ctx) {
945
1075
  let greeting = "";
@@ -951,7 +1081,8 @@ async function onSessionStart(ctx) {
951
1081
  if (result && !result.startsWith("Error")) {
952
1082
  greeting += result;
953
1083
  }
954
- } catch {
1084
+ } catch (err) {
1085
+ log.warn("hooks", "memory_context recall failed", err);
955
1086
  } finally {
956
1087
  isHookCall = false;
957
1088
  }
@@ -964,11 +1095,26 @@ async function onSessionStart(ctx) {
964
1095
  if (greeting) greeting += "\n";
965
1096
  greeting += result;
966
1097
  }
967
- } catch {
1098
+ } catch (err) {
1099
+ log.warn("hooks", "identity_summary failed", err);
968
1100
  } finally {
969
1101
  isHookCall = false;
970
1102
  }
971
1103
  }
1104
+ const timeContext = getTimeContext();
1105
+ if (greeting) greeting += "\n" + timeContext;
1106
+ else greeting = timeContext;
1107
+ try {
1108
+ isHookCall = true;
1109
+ const reminderResult = await ctx.mcpManager.callTool("reminder_check", {});
1110
+ if (reminderResult && !reminderResult.startsWith("Error") && !reminderResult.includes("No pending")) {
1111
+ greeting += "\n\n<pending-reminders>\n" + reminderResult + "\n</pending-reminders>";
1112
+ }
1113
+ } catch (err) {
1114
+ log.debug("hooks", "reminder_check failed", err);
1115
+ } finally {
1116
+ isHookCall = false;
1117
+ }
972
1118
  if (greeting) {
973
1119
  contextInjection = `<session-context>
974
1120
  ${greeting}
@@ -1000,10 +1146,12 @@ async function onBeforeToolExec(toolName, toolArgs, ctx) {
1000
1146
  reason: parsed.violations.join("; ")
1001
1147
  };
1002
1148
  }
1003
- } catch {
1149
+ } catch (err) {
1150
+ log.debug("hooks", "rules_check parse failed", err);
1004
1151
  }
1005
1152
  return { allow: true };
1006
- } catch {
1153
+ } catch (err) {
1154
+ log.warn("hooks", "rules_check call failed", err);
1007
1155
  return { allow: true };
1008
1156
  } finally {
1009
1157
  isHookCall = false;
@@ -1035,7 +1183,8 @@ async function onWorkflowMatch(userInput, ctx) {
1035
1183
  }
1036
1184
  }
1037
1185
  return null;
1038
- } catch {
1186
+ } catch (err) {
1187
+ log.debug("hooks", "workflow_list failed", err);
1039
1188
  return null;
1040
1189
  } finally {
1041
1190
  isHookCall = false;
@@ -1054,7 +1203,8 @@ async function onSessionEnd(ctx, messages, sessionId) {
1054
1203
  role: msg.role,
1055
1204
  content: msg.content.slice(0, 5e3)
1056
1205
  });
1057
- } catch {
1206
+ } catch (err) {
1207
+ log.debug("hooks", "memory_log write failed for " + sessionId, err);
1058
1208
  } finally {
1059
1209
  isHookCall = false;
1060
1210
  }
@@ -1104,7 +1254,8 @@ async function onSessionEnd(ctx, messages, sessionId) {
1104
1254
  }
1105
1255
  }
1106
1256
  }
1107
- } catch {
1257
+ } catch (err) {
1258
+ log.warn("hooks", "session end hook failed", err);
1108
1259
  }
1109
1260
  }
1110
1261
 
@@ -1131,7 +1282,7 @@ function estimateTotalTokens(messages) {
1131
1282
  var MAX_CONVERSATION_TOKENS = 8e4;
1132
1283
  var KEEP_RECENT = 10;
1133
1284
  var KEEP_INITIAL = 2;
1134
- function trimConversation(messages, _client) {
1285
+ async function trimConversation(messages, client) {
1135
1286
  const totalTokens = estimateTotalTokens(messages);
1136
1287
  if (totalTokens < MAX_CONVERSATION_TOKENS || messages.length <= KEEP_INITIAL + KEEP_RECENT) {
1137
1288
  return;
@@ -1139,20 +1290,39 @@ function trimConversation(messages, _client) {
1139
1290
  const initial = messages.slice(0, KEEP_INITIAL);
1140
1291
  const recent = messages.slice(-KEEP_RECENT);
1141
1292
  const middle = messages.slice(KEEP_INITIAL, messages.length - KEEP_RECENT);
1142
- const summaryParts = [];
1143
- for (const msg of middle) {
1144
- if (typeof msg.content === "string" && msg.content.length > 0) {
1145
- const preview = msg.content.slice(0, 150);
1146
- summaryParts.push(`[${msg.role}]: ${preview}${msg.content.length > 150 ? "..." : ""}`);
1293
+ const middleText = middle.filter((m) => typeof m.content === "string" && m.content.length > 0).map((m) => `[${m.role}]: ${m.content.slice(0, 500)}`).slice(0, 30).join("\n");
1294
+ let summaryText;
1295
+ try {
1296
+ const summaryPrompt = "Summarize the following conversation messages in 3-5 bullet points. Preserve: decisions made, user preferences expressed, action items, and key facts discussed. Be concise.\n\n" + middleText;
1297
+ let fullText = "";
1298
+ await client.chat(
1299
+ "You are a concise summarizer. Return only bullet points, no preamble.",
1300
+ [{ role: "user", content: summaryPrompt }],
1301
+ (chunk) => {
1302
+ if (chunk.type === "text" && chunk.text) fullText += chunk.text;
1303
+ }
1304
+ );
1305
+ summaryText = `<conversation-summary>
1306
+ Summary of ${middle.length} earlier messages:
1307
+
1308
+ ${fullText}
1309
+ </conversation-summary>`;
1310
+ log.debug("context", `Summarized ${middle.length} messages via LLM`);
1311
+ } catch (err) {
1312
+ log.warn("context", "LLM summarization failed, using fallback", err);
1313
+ const summaryParts = [];
1314
+ for (const msg of middle) {
1315
+ if (typeof msg.content === "string" && msg.content.length > 0) {
1316
+ const preview = msg.content.slice(0, 150);
1317
+ summaryParts.push(`[${msg.role}]: ${preview}${msg.content.length > 150 ? "..." : ""}`);
1318
+ }
1147
1319
  }
1148
- }
1149
- const summaryText = `<conversation-summary>
1150
- The following is a summary of ${middle.length} earlier messages that were compressed to save context:
1320
+ summaryText = `<conversation-summary>
1321
+ Summary of ${middle.length} earlier messages:
1151
1322
 
1152
1323
  ${summaryParts.slice(0, 20).join("\n")}
1153
- ${summaryParts.length > 20 ? `
1154
- ... and ${summaryParts.length - 20} more messages` : ""}
1155
1324
  </conversation-summary>`;
1325
+ }
1156
1326
  messages.length = 0;
1157
1327
  messages.push(...initial);
1158
1328
  messages.push({ role: "user", content: summaryText });
@@ -1160,7 +1330,151 @@ ${summaryParts.length > 20 ? `
1160
1330
  messages.push(...recent);
1161
1331
  }
1162
1332
 
1333
+ // src/memory-extractor.ts
1334
+ var AUTO_STORE_TYPES = /* @__PURE__ */ new Set(["preference", "fact", "pattern", "topology"]);
1335
+ var CONFIRM_TYPES = /* @__PURE__ */ new Set(["decision", "correction"]);
1336
+ var VALID_TYPES = /* @__PURE__ */ new Set([...AUTO_STORE_TYPES, ...CONFIRM_TYPES]);
1337
+ var MIN_RESPONSE_LENGTH = 50;
1338
+ var MIN_TURNS_BETWEEN_EMPTY = 3;
1339
+ var EXTRACTION_PROMPT = `Analyze this conversation turn. Extract any information worth remembering long-term.
1340
+
1341
+ Return a JSON array (empty [] if nothing worth storing):
1342
+ [{
1343
+ "content": "what to remember \u2014 be specific and self-contained",
1344
+ "type": "preference|fact|pattern|decision|correction|topology",
1345
+ "tags": ["relevant", "tags"],
1346
+ "confidence": 0.0-1.0,
1347
+ "scope": "global"
1348
+ }]
1349
+
1350
+ Type guide:
1351
+ - "preference" = user likes/dislikes/preferences
1352
+ - "fact" = objective information about systems, people, projects
1353
+ - "pattern" = recurring behavior, coding style, approach
1354
+ - "topology" = how systems/components connect to each other
1355
+ - "decision" = explicit choice between alternatives (requires confirmation)
1356
+ - "correction" = user correcting a prior wrong assumption (requires confirmation)
1357
+
1358
+ Rules:
1359
+ - Only extract genuinely useful LONG-TERM information
1360
+ - Skip ephemeral things ("user asked about X" is NOT useful)
1361
+ - Be conservative \u2014 90% of turns produce nothing worth storing
1362
+ - Return ONLY the JSON array, no other text`;
1363
+ function shouldExtract(assistantResponse, turnsSinceLastExtraction, lastExtractionCount) {
1364
+ if (lastExtractionCount > 0) return true;
1365
+ if (assistantResponse.length < MIN_RESPONSE_LENGTH) return false;
1366
+ if (turnsSinceLastExtraction < MIN_TURNS_BETWEEN_EMPTY) return false;
1367
+ return true;
1368
+ }
1369
+ function parseExtractionResult(raw) {
1370
+ try {
1371
+ let cleaned = raw.trim();
1372
+ const codeBlockMatch = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
1373
+ if (codeBlockMatch) {
1374
+ cleaned = codeBlockMatch[1].trim();
1375
+ }
1376
+ const parsed = JSON.parse(cleaned);
1377
+ if (!Array.isArray(parsed)) return [];
1378
+ return parsed.filter(
1379
+ (item) => typeof item.content === "string" && item.content.length > 0 && typeof item.type === "string" && VALID_TYPES.has(item.type)
1380
+ );
1381
+ } catch {
1382
+ return [];
1383
+ }
1384
+ }
1385
+ async function extractMemories(userMessage, assistantResponse, client, mcpManager, state, confirmFn) {
1386
+ if (!shouldExtract(assistantResponse, state.turnsSinceLastExtraction, state.lastExtractionCount)) {
1387
+ state.turnsSinceLastExtraction++;
1388
+ return 0;
1389
+ }
1390
+ try {
1391
+ const conversationTurn = `User: ${userMessage.slice(0, 2e3)}
1392
+
1393
+ Assistant: ${assistantResponse.slice(0, 2e3)}`;
1394
+ let fullText = "";
1395
+ await client.chat(
1396
+ EXTRACTION_PROMPT,
1397
+ [{ role: "user", content: conversationTurn }],
1398
+ (chunk) => {
1399
+ if (chunk.type === "text" && chunk.text) fullText += chunk.text;
1400
+ }
1401
+ );
1402
+ const candidates = parseExtractionResult(fullText);
1403
+ state.turnsSinceLastExtraction = 0;
1404
+ state.lastExtractionCount = candidates.length;
1405
+ if (candidates.length === 0) return 0;
1406
+ let stored = 0;
1407
+ for (const candidate of candidates) {
1408
+ try {
1409
+ const existing = await mcpManager.callTool("memory_recall", {
1410
+ query: candidate.content,
1411
+ limit: 1
1412
+ });
1413
+ if (existing && !existing.startsWith("Error")) {
1414
+ try {
1415
+ const parsed = JSON.parse(existing);
1416
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed[0].score > 0.85) {
1417
+ log.debug("extractor", "Skipping duplicate: " + candidate.content);
1418
+ continue;
1419
+ }
1420
+ } catch {
1421
+ }
1422
+ }
1423
+ } catch {
1424
+ }
1425
+ if (CONFIRM_TYPES.has(candidate.type)) {
1426
+ const confirmed = await confirmFn(candidate.content);
1427
+ if (!confirmed) continue;
1428
+ }
1429
+ try {
1430
+ await mcpManager.callTool("memory_store", {
1431
+ content: candidate.content,
1432
+ type: candidate.type,
1433
+ tags: candidate.tags,
1434
+ confidence: candidate.confidence,
1435
+ source: "auto-extraction",
1436
+ scope: candidate.scope
1437
+ });
1438
+ stored++;
1439
+ log.debug("extractor", "Stored " + candidate.type + ": " + candidate.content);
1440
+ } catch (err) {
1441
+ log.warn("extractor", "Failed to store: " + candidate.content, err);
1442
+ }
1443
+ }
1444
+ return stored;
1445
+ } catch (err) {
1446
+ log.debug("extractor", "extraction failed", err);
1447
+ state.turnsSinceLastExtraction = 0;
1448
+ state.lastExtractionCount = 0;
1449
+ return 0;
1450
+ }
1451
+ }
1452
+
1163
1453
  // src/agent.ts
1454
+ async function recallForMessage(input, mcpManager) {
1455
+ try {
1456
+ const result = await mcpManager.callTool("memory_recall", {
1457
+ query: input,
1458
+ limit: 5,
1459
+ compact: true
1460
+ });
1461
+ if (!result || result.startsWith("Error") || result.includes("No memories found")) {
1462
+ return null;
1463
+ }
1464
+ const tokenEstimate = Math.round(result.split(/\s+/).filter(Boolean).length * 1.3);
1465
+ return {
1466
+ text: `
1467
+
1468
+ <relevant-memories>
1469
+ ${result}
1470
+ </relevant-memories>`,
1471
+ tokenEstimate
1472
+ };
1473
+ } catch (err) {
1474
+ log.debug("agent", "memory recall failed", err);
1475
+ return null;
1476
+ }
1477
+ }
1164
1478
  function generateSessionId() {
1165
1479
  const now = /* @__PURE__ */ new Date();
1166
1480
  const pad = (n) => n.toString().padStart(2, "0");
@@ -1169,6 +1483,16 @@ function generateSessionId() {
1169
1483
  async function runAgent(client, systemPrompt, aiName, model, tools, mcpManager, hooksConfig) {
1170
1484
  const messages = [];
1171
1485
  const sessionId = generateSessionId();
1486
+ const extractorState = { turnsSinceLastExtraction: 0, lastExtractionCount: 0 };
1487
+ const isRetryable = (err) => err.message.includes("Rate limit") || err.message.includes("rate limit") || err.message.includes("ECONNRESET") || err.message.includes("ETIMEDOUT") || err.message.includes("fetch failed");
1488
+ const onChunkHandler = (chunk) => {
1489
+ if (chunk.type === "text" && chunk.text) {
1490
+ process.stdout.write(chunk.text);
1491
+ }
1492
+ if (chunk.type === "done") {
1493
+ process.stdout.write("\n");
1494
+ }
1495
+ };
1172
1496
  const rl = readline.createInterface({
1173
1497
  input: process.stdin,
1174
1498
  output: process.stdout
@@ -1178,7 +1502,8 @@ async function runAgent(client, systemPrompt, aiName, model, tools, mcpManager,
1178
1502
  try {
1179
1503
  const hookCtx = { mcpManager, config: hooksConfig };
1180
1504
  await onSessionEnd(hookCtx, messages, sessionId);
1181
- } catch {
1505
+ } catch (err) {
1506
+ log.debug("agent", "session end hook failed on SIGINT", err);
1182
1507
  }
1183
1508
  }
1184
1509
  console.log(pc3.dim("\nGoodbye.\n"));
@@ -1206,7 +1531,8 @@ Type a message, ${pc3.dim("/help")} for commands, or ${pc3.dim("/quit")} to exit
1206
1531
  messages.push({ role: "user", content: session.contextInjection });
1207
1532
  messages.push({ role: "assistant", content: "I have context from our previous sessions. How can I help?" });
1208
1533
  }
1209
- } catch {
1534
+ } catch (err) {
1535
+ log.warn("agent", "session start hook failed", err);
1210
1536
  }
1211
1537
  }
1212
1538
  while (true) {
@@ -1219,13 +1545,39 @@ Type a message, ${pc3.dim("/help")} for commands, or ${pc3.dim("/quit")} to exit
1219
1545
  try {
1220
1546
  const hookCtx = { mcpManager, config: hooksConfig };
1221
1547
  await onSessionEnd(hookCtx, messages, sessionId);
1222
- } catch {
1548
+ } catch (err) {
1549
+ log.debug("agent", "session end hook failed on quit", err);
1223
1550
  }
1224
1551
  }
1225
1552
  console.log(pc3.dim("\nGoodbye.\n"));
1226
1553
  rl.close();
1227
1554
  return;
1228
1555
  }
1556
+ if (cmdResult.exportConversation) {
1557
+ try {
1558
+ const exportDir = path6.join(os6.homedir(), ".aman-agent", "exports");
1559
+ fs6.mkdirSync(exportDir, { recursive: true });
1560
+ const exportPath = path6.join(exportDir, `${sessionId}.md`);
1561
+ const lines = [
1562
+ `# Conversation \u2014 ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
1563
+ `**Model:** ${model}`,
1564
+ "",
1565
+ "---",
1566
+ ""
1567
+ ];
1568
+ for (const msg of messages) {
1569
+ if (typeof msg.content === "string") {
1570
+ const label = msg.role === "user" ? "**You:**" : `**${aiName}:**`;
1571
+ lines.push(`${label} ${msg.content}`, "");
1572
+ }
1573
+ }
1574
+ fs6.writeFileSync(exportPath, lines.join("\n"), "utf-8");
1575
+ console.log(pc3.green(`Exported to ${exportPath}`));
1576
+ } catch {
1577
+ console.log(pc3.red("Failed to export conversation."));
1578
+ }
1579
+ continue;
1580
+ }
1229
1581
  if (cmdResult.saveConversation && mcpManager) {
1230
1582
  try {
1231
1583
  await saveConversationToMemory(mcpManager, messages, sessionId);
@@ -1261,79 +1613,102 @@ ${wfMatch.steps}
1261
1613
  console.log(pc3.dim(` Using "${wfMatch.name}" workflow.`));
1262
1614
  }
1263
1615
  }
1264
- } catch {
1616
+ } catch (err) {
1617
+ log.debug("agent", "workflow match failed", err);
1265
1618
  }
1266
1619
  }
1267
- trimConversation(messages, client);
1620
+ await trimConversation(messages, client);
1268
1621
  messages.push({ role: "user", content: input });
1622
+ let augmentedSystemPrompt = activeSystemPrompt;
1623
+ if (mcpManager) {
1624
+ const recall = await recallForMessage(input, mcpManager);
1625
+ if (recall) {
1626
+ augmentedSystemPrompt = activeSystemPrompt + recall.text;
1627
+ process.stdout.write(pc3.dim(` [memories: ~${recall.tokenEstimate} tokens]
1628
+ `));
1629
+ }
1630
+ }
1269
1631
  process.stdout.write(pc3.cyan(`
1270
1632
  ${aiName} > `));
1271
1633
  try {
1272
- let response = await client.chat(
1273
- activeSystemPrompt,
1274
- messages,
1275
- (chunk) => {
1276
- if (chunk.type === "text" && chunk.text) {
1277
- process.stdout.write(chunk.text);
1278
- }
1279
- if (chunk.type === "done") {
1280
- process.stdout.write("\n");
1281
- }
1282
- },
1283
- tools
1634
+ let response = await withRetry(
1635
+ () => client.chat(augmentedSystemPrompt, messages, onChunkHandler, tools),
1636
+ { maxAttempts: 3, baseDelay: 1e3, retryable: isRetryable }
1284
1637
  );
1285
1638
  messages.push(response.message);
1286
1639
  while (response.toolUses.length > 0 && mcpManager) {
1287
- const toolResults = [];
1288
- for (const toolUse of response.toolUses) {
1289
- if (hooksConfig) {
1290
- const hookCtx = { mcpManager, config: hooksConfig };
1291
- const check = await onBeforeToolExec(toolUse.name, toolUse.input, hookCtx);
1292
- if (!check.allow) {
1293
- process.stdout.write(pc3.red(` [BLOCKED: ${check.reason}]
1640
+ const toolResults = await Promise.all(
1641
+ response.toolUses.map(async (toolUse) => {
1642
+ if (hooksConfig) {
1643
+ const hookCtx = { mcpManager, config: hooksConfig };
1644
+ const check = await onBeforeToolExec(toolUse.name, toolUse.input, hookCtx);
1645
+ if (!check.allow) {
1646
+ process.stdout.write(pc3.red(` [BLOCKED: ${check.reason}]
1647
+ `));
1648
+ return {
1649
+ type: "tool_result",
1650
+ tool_use_id: toolUse.id,
1651
+ content: `BLOCKED by guardrail: ${check.reason}`,
1652
+ is_error: true
1653
+ };
1654
+ }
1655
+ }
1656
+ process.stdout.write(pc3.dim(` [using ${toolUse.name}...]
1294
1657
  `));
1295
- toolResults.push({
1296
- type: "tool_result",
1297
- tool_use_id: toolUse.id,
1298
- content: `BLOCKED by guardrail: ${check.reason}`,
1299
- is_error: true
1658
+ const result = await mcpManager.callTool(toolUse.name, toolUse.input);
1659
+ const skipLogging = ["memory_log", "memory_recall", "memory_context", "memory_detail", "reminder_check"].includes(toolUse.name);
1660
+ if (!skipLogging) {
1661
+ mcpManager.callTool("memory_log", {
1662
+ session_id: sessionId,
1663
+ role: "system",
1664
+ content: `[tool:${toolUse.name}] input=${JSON.stringify(toolUse.input).slice(0, 500)} result=${result.slice(0, 500)}`
1665
+ }).catch(() => {
1300
1666
  });
1301
- continue;
1302
1667
  }
1303
- }
1304
- process.stdout.write(
1305
- pc3.dim(` [using ${toolUse.name}...]
1306
- `)
1307
- );
1308
- const result = await mcpManager.callTool(
1309
- toolUse.name,
1310
- toolUse.input
1311
- );
1312
- toolResults.push({
1313
- type: "tool_result",
1314
- tool_use_id: toolUse.id,
1315
- content: result
1316
- });
1317
- }
1668
+ return {
1669
+ type: "tool_result",
1670
+ tool_use_id: toolUse.id,
1671
+ content: result
1672
+ };
1673
+ })
1674
+ );
1318
1675
  messages.push({
1319
1676
  role: "user",
1320
1677
  content: toolResults
1321
1678
  });
1322
- response = await client.chat(
1323
- activeSystemPrompt,
1324
- messages,
1325
- (chunk) => {
1326
- if (chunk.type === "text" && chunk.text) {
1327
- process.stdout.write(chunk.text);
1328
- }
1329
- if (chunk.type === "done") {
1330
- process.stdout.write("\n");
1331
- }
1332
- },
1333
- tools
1679
+ response = await withRetry(
1680
+ () => client.chat(augmentedSystemPrompt, messages, onChunkHandler, tools),
1681
+ { maxAttempts: 3, baseDelay: 1e3, retryable: isRetryable }
1334
1682
  );
1335
1683
  messages.push(response.message);
1336
1684
  }
1685
+ if (mcpManager && hooksConfig?.extractMemories) {
1686
+ const assistantText = typeof response.message.content === "string" ? response.message.content : response.message.content.filter((b) => b.type === "text").map((b) => "text" in b ? b.text : "").join("");
1687
+ if (assistantText) {
1688
+ const confirmFn = async (content) => {
1689
+ return new Promise((resolve) => {
1690
+ rl.question(
1691
+ pc3.dim(` Remember: "${content}"? (y/N) `),
1692
+ (answer) => resolve(answer.toLowerCase() === "y")
1693
+ );
1694
+ });
1695
+ };
1696
+ const count = await extractMemories(
1697
+ input,
1698
+ assistantText,
1699
+ client,
1700
+ mcpManager,
1701
+ extractorState,
1702
+ confirmFn
1703
+ );
1704
+ if (count > 0) {
1705
+ process.stdout.write(pc3.dim(` [${count} memory${count > 1 ? "ies" : ""} stored]
1706
+ `));
1707
+ }
1708
+ }
1709
+ } else {
1710
+ extractorState.turnsSinceLastExtraction++;
1711
+ }
1337
1712
  } catch (error) {
1338
1713
  const message = error instanceof Error ? error.message : "Unknown error occurred";
1339
1714
  console.error(pc3.red(`
@@ -1352,15 +1727,16 @@ async function saveConversationToMemory(mcpManager, messages, sessionId) {
1352
1727
  role: msg.role,
1353
1728
  content: msg.content.slice(0, 5e3)
1354
1729
  });
1355
- } catch {
1730
+ } catch (err) {
1731
+ log.debug("agent", "memory_log write failed", err);
1356
1732
  }
1357
1733
  }
1358
1734
  }
1359
1735
 
1360
1736
  // src/index.ts
1361
- import fs5 from "fs";
1362
- import path5 from "path";
1363
- import os5 from "os";
1737
+ import fs7 from "fs";
1738
+ import path7 from "path";
1739
+ import os7 from "os";
1364
1740
  var program = new Command();
1365
1741
  program.name("aman-agent").description("Your AI companion, running locally").version("0.1.0").option("--model <model>", "Override LLM model").option("--budget <tokens>", "Token budget for system prompt (default: 8000)", parseInt).action(async (options) => {
1366
1742
  p2.intro(pc4.bold("aman agent") + pc4.dim(" \u2014 starting your AI companion"));
@@ -1472,10 +1848,10 @@ program.name("aman-agent").description("Your AI companion, running locally").ver
1472
1848
  }
1473
1849
  }
1474
1850
  p2.log.info(`Model: ${pc4.dim(model)}`);
1475
- const corePath = path5.join(os5.homedir(), ".acore", "core.md");
1851
+ const corePath = path7.join(os7.homedir(), ".acore", "core.md");
1476
1852
  let aiName = "Assistant";
1477
- if (fs5.existsSync(corePath)) {
1478
- const content = fs5.readFileSync(corePath, "utf-8");
1853
+ if (fs7.existsSync(corePath)) {
1854
+ const content = fs7.readFileSync(corePath, "utf-8");
1479
1855
  const match = content.match(/^# (.+)$/m);
1480
1856
  if (match) aiName = match[1];
1481
1857
  }
@@ -1487,6 +1863,23 @@ program.name("aman-agent").description("Your AI companion, running locally").ver
1487
1863
  const mcpTools = mcpManager.getTools();
1488
1864
  if (mcpTools.length > 0) {
1489
1865
  p2.log.success(`${mcpTools.length} MCP tools available`);
1866
+ if (mcpTools.some((t) => t.name === "memory_consolidate")) {
1867
+ try {
1868
+ const consolidateResult = await mcpManager.callTool("memory_consolidate", { dry_run: false });
1869
+ if (consolidateResult && !consolidateResult.startsWith("Error")) {
1870
+ try {
1871
+ const report = JSON.parse(consolidateResult);
1872
+ if (report.merged > 0 || report.pruned > 0 || report.promoted > 0) {
1873
+ p2.log.info(
1874
+ `Memory health: ${report.healthScore ?? "?"}% ` + pc4.dim(`(merged ${report.merged}, pruned ${report.pruned}, promoted ${report.promoted})`)
1875
+ );
1876
+ }
1877
+ } catch {
1878
+ }
1879
+ }
1880
+ } catch {
1881
+ }
1882
+ }
1490
1883
  } else {
1491
1884
  p2.log.info(
1492
1885
  "No MCP tools connected (install aman-mcp or amem for tool support)"