@aman_asmuei/aman-agent 0.17.1 → 0.17.2

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
@@ -232,7 +232,7 @@ function toAnthropicMessages(messages) {
232
232
  function createAnthropicClient(apiKey, model) {
233
233
  const client = new Anthropic({ apiKey });
234
234
  return {
235
- async chat(systemPrompt, messages, onChunk, tools) {
235
+ async chat(systemPrompt, messages, onChunk, tools, options) {
236
236
  const anthropicMessages = toAnthropicMessages(messages);
237
237
  const hasTools = tools && tools.length > 0;
238
238
  try {
@@ -242,7 +242,7 @@ function createAnthropicClient(apiKey, model) {
242
242
  let currentBlockIndex = -1;
243
243
  const createParams = {
244
244
  model,
245
- max_tokens: 8192,
245
+ max_tokens: options?.maxOutputTokens ?? 8192,
246
246
  system: systemPrompt,
247
247
  messages: anthropicMessages,
248
248
  stream: true
@@ -330,7 +330,9 @@ function createAnthropicClient(apiKey, model) {
330
330
 
331
331
  // src/llm/openai.ts
332
332
  import OpenAI from "openai";
333
- function toOpenAIMessages(systemPrompt, messages) {
333
+
334
+ // src/llm/openai-compat.ts
335
+ function toOpenAICompatibleMessages(systemPrompt, messages) {
334
336
  const result = [
335
337
  { role: "system", content: systemPrompt }
336
338
  ];
@@ -398,18 +400,20 @@ function toOpenAIMessages(systemPrompt, messages) {
398
400
  }
399
401
  return result;
400
402
  }
403
+
404
+ // src/llm/openai.ts
401
405
  function createOpenAIClient(apiKey, model) {
402
406
  const client = new OpenAI({ apiKey });
403
407
  return {
404
- async chat(systemPrompt, messages, onChunk, tools) {
405
- const openaiMessages = toOpenAIMessages(systemPrompt, messages);
408
+ async chat(systemPrompt, messages, onChunk, tools, options) {
409
+ const openaiMessages = toOpenAICompatibleMessages(systemPrompt, messages);
406
410
  const hasTools = tools && tools.length > 0;
407
411
  try {
408
412
  let fullText = "";
409
413
  const toolCallAccumulators = /* @__PURE__ */ new Map();
410
414
  const createParams = {
411
415
  model,
412
- max_tokens: 8192,
416
+ max_tokens: options?.maxOutputTokens ?? 8192,
413
417
  messages: openaiMessages,
414
418
  stream: true
415
419
  };
@@ -495,74 +499,6 @@ function createOpenAIClient(apiKey, model) {
495
499
 
496
500
  // src/llm/ollama.ts
497
501
  import OpenAI2 from "openai";
498
- function toOllamaMessages(systemPrompt, messages) {
499
- const result = [
500
- { role: "system", content: systemPrompt }
501
- ];
502
- for (const m of messages) {
503
- if (typeof m.content === "string") {
504
- result.push({
505
- role: m.role,
506
- content: m.content
507
- });
508
- } else if (m.role === "assistant") {
509
- const textParts = m.content.filter((b) => b.type === "text");
510
- const toolUseParts = m.content.filter((b) => b.type === "tool_use");
511
- const text2 = textParts.map((b) => "text" in b ? b.text : "").join("");
512
- if (toolUseParts.length > 0) {
513
- result.push({
514
- role: "assistant",
515
- content: text2 || null,
516
- tool_calls: toolUseParts.map((b) => ({
517
- id: "id" in b ? b.id : "",
518
- type: "function",
519
- function: {
520
- name: "name" in b ? b.name : "",
521
- arguments: JSON.stringify("input" in b ? b.input : {})
522
- }
523
- }))
524
- });
525
- } else {
526
- result.push({ role: "assistant", content: text2 });
527
- }
528
- } else if (m.role === "user") {
529
- const toolResults = m.content.filter((b) => b.type === "tool_result");
530
- if (toolResults.length > 0) {
531
- for (const tr of toolResults) {
532
- if (tr.type === "tool_result") {
533
- result.push({
534
- role: "tool",
535
- tool_call_id: tr.tool_use_id,
536
- content: tr.content
537
- });
538
- }
539
- }
540
- } else {
541
- const hasImages = m.content.some((b) => b.type === "image");
542
- if (hasImages) {
543
- const parts = [];
544
- for (const b of m.content) {
545
- if (b.type === "text") {
546
- parts.push({ type: "text", text: b.text });
547
- } else if (b.type === "image") {
548
- parts.push({
549
- type: "image_url",
550
- image_url: {
551
- url: `data:${b.source.media_type};base64,${b.source.data}`
552
- }
553
- });
554
- }
555
- }
556
- result.push({ role: "user", content: parts });
557
- } else {
558
- const text2 = m.content.map((b) => "text" in b ? b.text : "").join("");
559
- result.push({ role: "user", content: text2 });
560
- }
561
- }
562
- }
563
- }
564
- return result;
565
- }
566
502
  function createOllamaClient(model, baseURL) {
567
503
  const client = new OpenAI2({
568
504
  baseURL: baseURL || "http://localhost:11434/v1",
@@ -570,15 +506,15 @@ function createOllamaClient(model, baseURL) {
570
506
  // Ollama doesn't require a real key
571
507
  });
572
508
  return {
573
- async chat(systemPrompt, messages, onChunk, tools) {
574
- const ollamaMessages = toOllamaMessages(systemPrompt, messages);
509
+ async chat(systemPrompt, messages, onChunk, tools, options) {
510
+ const ollamaMessages = toOpenAICompatibleMessages(systemPrompt, messages);
575
511
  const hasTools = tools && tools.length > 0;
576
512
  try {
577
513
  let fullText = "";
578
514
  const toolCallAccumulators = /* @__PURE__ */ new Map();
579
515
  const createParams = {
580
516
  model,
581
- max_tokens: 8192,
517
+ max_tokens: options?.maxOutputTokens ?? 4096,
582
518
  messages: ollamaMessages,
583
519
  stream: true
584
520
  };
@@ -730,12 +666,18 @@ async function withRetry(fn, options) {
730
666
  }
731
667
 
732
668
  // src/mcp/client.ts
669
+ var TOOL_CALL_TIMEOUT_MS = 3e4;
733
670
  var McpManager = class {
734
671
  connections = [];
735
672
  tools = [];
736
- async connect(name, command, args) {
673
+ async connect(name, command, args, env) {
737
674
  try {
738
- const transport = new StdioClientTransport({ command, args, stderr: "pipe" });
675
+ const transport = new StdioClientTransport({
676
+ command,
677
+ args,
678
+ stderr: "pipe",
679
+ env: env ? env : void 0
680
+ });
739
681
  const client = new Client({
740
682
  name: `aman-agent-${name}`,
741
683
  version: "0.1.0"
@@ -746,9 +688,16 @@ var McpManager = class {
746
688
  log.debug("mcp", `[${name} stderr] ${chunk.toString().trim()}`);
747
689
  });
748
690
  }
749
- this.connections.push({ name, client, transport });
691
+ this.connections.push({ name, client, transport, connectParams: { command, args, env } });
750
692
  const toolsResult = await client.listTools();
751
693
  for (const tool of toolsResult.tools) {
694
+ const existing = this.tools.find((t) => t.name === tool.name);
695
+ if (existing) {
696
+ log.warn(
697
+ "mcp",
698
+ `Warning: tool "${tool.name}" from server "${name}" shadows existing tool from "${existing.serverName}"`
699
+ );
700
+ }
752
701
  this.tools.push({
753
702
  name: tool.name,
754
703
  description: tool.description || "",
@@ -771,7 +720,15 @@ var McpManager = class {
771
720
  if (!conn) return `Error: server ${tool.serverName} not connected`;
772
721
  try {
773
722
  const result = await withRetry(
774
- () => conn.client.callTool({ name: toolName, arguments: args }),
723
+ () => Promise.race([
724
+ conn.client.callTool({ name: toolName, arguments: args }),
725
+ new Promise(
726
+ (_, reject) => setTimeout(
727
+ () => reject(new Error(`Tool ${toolName} timed out after 30s`)),
728
+ TOOL_CALL_TIMEOUT_MS
729
+ )
730
+ )
731
+ ]),
775
732
  { maxAttempts: 2, baseDelay: 500, retryable: (err) => err.message.includes("ETIMEDOUT") || err.message.includes("timeout") }
776
733
  );
777
734
  if (result.content && Array.isArray(result.content)) {
@@ -782,6 +739,23 @@ var McpManager = class {
782
739
  return `Error calling ${toolName}: ${error instanceof Error ? error.message : String(error)}`;
783
740
  }
784
741
  }
742
+ async reconnect(name) {
743
+ const connIndex = this.connections.findIndex((c) => c.name === name);
744
+ if (connIndex === -1) {
745
+ log.error("mcp", `Cannot reconnect: no connection found for "${name}"`);
746
+ return;
747
+ }
748
+ const conn = this.connections[connIndex];
749
+ const { command, args, env } = conn.connectParams;
750
+ try {
751
+ await conn.client.close();
752
+ } catch (err) {
753
+ log.debug("mcp", `Error closing old connection for ${name}`, err);
754
+ }
755
+ this.connections.splice(connIndex, 1);
756
+ this.tools = this.tools.filter((t) => t.serverName !== name);
757
+ await this.connect(name, command, args, env);
758
+ }
785
759
  async disconnect() {
786
760
  for (const conn of this.connections) {
787
761
  try {
@@ -893,7 +867,27 @@ async function initMemory(project) {
893
867
  const amemDir = process.env.AMEM_DIR ?? path5.join(os5.homedir(), ".amem");
894
868
  if (!fs5.existsSync(amemDir)) fs5.mkdirSync(amemDir, { recursive: true });
895
869
  const dbPath = process.env.AMEM_DB ?? path5.join(amemDir, "memory.db");
896
- db = createDatabase(dbPath);
870
+ try {
871
+ db = createDatabase(dbPath);
872
+ } catch (err) {
873
+ const backupPath = `${dbPath}.corrupt.${Date.now()}`;
874
+ try {
875
+ if (fs5.existsSync(dbPath)) {
876
+ fs5.renameSync(dbPath, backupPath);
877
+ if (fs5.existsSync(`${dbPath}-wal`)) fs5.unlinkSync(`${dbPath}-wal`);
878
+ if (fs5.existsSync(`${dbPath}-shm`)) fs5.unlinkSync(`${dbPath}-shm`);
879
+ console.error(`[amem] Database corrupted \u2014 backed up to ${backupPath}`);
880
+ console.error("[amem] Creating fresh database. Previous memories are in the backup file.");
881
+ db = createDatabase(dbPath);
882
+ } else {
883
+ throw err;
884
+ }
885
+ } catch {
886
+ console.error(`[amem] Failed to initialize memory: ${err instanceof Error ? err.message : String(err)}`);
887
+ console.error(`[amem] Try deleting ${amemDir} to reset: rm -rf ${amemDir}`);
888
+ throw err;
889
+ }
890
+ }
897
891
  currentProject = project ?? "global";
898
892
  preloadEmbeddings();
899
893
  setTimeout(() => {
@@ -974,12 +968,19 @@ async function memoryForget(opts) {
974
968
  }
975
969
  return { deleted: 0, message: "Provide an id, type, or query to forget." };
976
970
  }
971
+ var memoryConfig = {};
972
+ function setMemoryConfig(config) {
973
+ memoryConfig = config;
974
+ }
975
+ function getMaxRecallTokens() {
976
+ return memoryConfig.maxRecallTokens ?? 1500;
977
+ }
977
978
  function memoryConsolidate(dryRun = false) {
978
979
  return consolidateMemories(getDb(), cosineSimilarity, {
979
980
  dryRun,
980
- maxStaleDays: 90,
981
- minConfidence: 0.3,
982
- minAccessCount: 0
981
+ maxStaleDays: memoryConfig.maxStaleDays ?? 90,
982
+ minConfidence: memoryConfig.minConfidence ?? 0.3,
983
+ minAccessCount: memoryConfig.minAccessCount ?? 0
983
984
  });
984
985
  }
985
986
  function isMemoryInitialized() {
@@ -2136,7 +2137,7 @@ async function handleMemoryCommand(action, args, ctx) {
2136
2137
  }
2137
2138
  try {
2138
2139
  if (args[0] === "--type" && args[1]) {
2139
- const result2 = await memoryForget({ query: args[1], type: args[1] });
2140
+ const result2 = await memoryForget({ type: args[1] });
2140
2141
  return { handled: true, output: result2.deleted > 0 ? pc3.green(result2.message) : pc3.dim(result2.message) };
2141
2142
  }
2142
2143
  const result = await memoryForget({ query: args.join(" ") });
@@ -2378,6 +2379,10 @@ function handleHelp() {
2378
2379
  ` ${pc3.cyan("/debug")} Show debug log`,
2379
2380
  ` ${pc3.cyan("/save")} Save conversation to memory`,
2380
2381
  ` ${pc3.cyan("/model")} Show current LLM model`,
2382
+ ` ${pc3.cyan("/plan")} Manage multi-step plans`,
2383
+ ` ${pc3.cyan("/profile")} Switch agent profiles`,
2384
+ ` ${pc3.cyan("/delegate")} Delegate tasks to sub-agents`,
2385
+ ` ${pc3.cyan("/team")} Manage agent teams`,
2381
2386
  ` ${pc3.cyan("/update")} Check for updates`,
2382
2387
  ` ${pc3.cyan("/reset")} Full reset [all|memory|config|identity|rules]`,
2383
2388
  ` ${pc3.cyan("/clear")} Clear conversation history`,
@@ -2443,7 +2448,7 @@ function handleReset(action) {
2443
2448
  function handleUpdate() {
2444
2449
  try {
2445
2450
  const current = execFileSync("npm", ["view", "@aman_asmuei/aman-agent", "version"], { encoding: "utf-8" }).trim();
2446
- const local = JSON.parse(fs9.readFileSync(path9.join(__dirname, "..", "package.json"), "utf-8")).version;
2451
+ const local = true ? "0.17.2" : "unknown";
2447
2452
  if (current === local) {
2448
2453
  return { handled: true, output: `${pc3.green("Up to date")} \u2014 v${local}` };
2449
2454
  }
@@ -4017,8 +4022,8 @@ Rules:
4017
4022
  - Be conservative \u2014 90% of turns produce nothing worth storing
4018
4023
  - Return ONLY the JSON array, no other text`;
4019
4024
  function shouldExtract(assistantResponse, turnsSinceLastExtraction, lastExtractionCount) {
4020
- if (lastExtractionCount > 0) return true;
4021
4025
  if (assistantResponse.length < MIN_RESPONSE_LENGTH) return false;
4026
+ if (lastExtractionCount > 0 && turnsSinceLastExtraction >= 1) return true;
4022
4027
  if (turnsSinceLastExtraction < MIN_TURNS_BETWEEN_EMPTY) return false;
4023
4028
  return true;
4024
4029
  }
@@ -4333,13 +4338,20 @@ async function recallForMessage(input) {
4333
4338
  return null;
4334
4339
  }
4335
4340
  const tokenEstimate = result.tokenEstimate ?? Math.round(result.text.split(/\s+/).filter(Boolean).length * 1.3);
4341
+ const MAX_MEMORY_TOKENS = getMaxRecallTokens();
4342
+ let memoryText = result.text;
4343
+ if (tokenEstimate > MAX_MEMORY_TOKENS) {
4344
+ const maxChars = MAX_MEMORY_TOKENS * 4;
4345
+ memoryText = memoryText.slice(0, maxChars) + "\n[... memory truncated to fit token budget]";
4346
+ log.debug("agent", `memory recall truncated from ~${tokenEstimate} to ~${MAX_MEMORY_TOKENS} tokens`);
4347
+ }
4336
4348
  return {
4337
4349
  text: `
4338
4350
 
4339
4351
  <relevant-memories>
4340
- ${result.text}
4352
+ ${memoryText}
4341
4353
  </relevant-memories>`,
4342
- tokenEstimate
4354
+ tokenEstimate: Math.min(tokenEstimate, MAX_MEMORY_TOKENS)
4343
4355
  };
4344
4356
  } catch (err) {
4345
4357
  log.debug("agent", "memory recall failed", err);
@@ -4395,7 +4407,7 @@ async function runAgent(client, systemPrompt, aiName, model, tools, mcpManager,
4395
4407
  shownHints: loadShownHints(),
4396
4408
  hintShownThisSession: false
4397
4409
  };
4398
- const isRetryable2 = (err) => err.message.includes("Rate limit") || err.message.includes("rate limit") || err.message.includes("ECONNRESET") || err.message.includes("ETIMEDOUT") || err.message.includes("fetch failed");
4410
+ const isRetryable2 = (err) => err.message.includes("Rate limit") || err.message.includes("rate limit") || err.message.includes("ECONNRESET") || err.message.includes("ETIMEDOUT") || err.message.includes("fetch failed") || err.message.includes("socket hang up") || err.message.includes("network socket disconnected") || err.message.includes("ENOTFOUND") || err.message.includes("EAI_AGAIN");
4399
4411
  let responseBuffer = "";
4400
4412
  const onChunkHandler = (chunk) => {
4401
4413
  if (chunk.type === "text" && chunk.text) {
@@ -4813,7 +4825,18 @@ ${converted}
4813
4825
  { maxAttempts: 3, baseDelay: 1e3, retryable: isRetryable2 }
4814
4826
  );
4815
4827
  messages.push(response.message);
4828
+ const MAX_TOOL_TURNS = 20;
4829
+ let toolTurnCount = 0;
4816
4830
  while (response.toolUses.length > 0 && mcpManager) {
4831
+ toolTurnCount++;
4832
+ if (toolTurnCount > MAX_TOOL_TURNS) {
4833
+ messages.push({
4834
+ role: "assistant",
4835
+ content: "Tool execution limit reached (20). Breaking to prevent infinite loop."
4836
+ });
4837
+ console.log(pc6.yellow("\n Tool execution limit reached (20). Breaking to prevent infinite loop."));
4838
+ break;
4839
+ }
4817
4840
  const toolResults = await Promise.all(
4818
4841
  response.toolUses.map(async (toolUse) => {
4819
4842
  if (hooksConfig) {
@@ -5128,10 +5151,22 @@ function bootstrapEcosystem() {
5128
5151
  "- Respect the user's preferences stored in memory"
5129
5152
  ].join("\n"), "utf-8");
5130
5153
  }
5154
+ const flowDir = path14.join(home2, ".aflow");
5155
+ const flowPath = path14.join(flowDir, "flow.md");
5156
+ if (!fs14.existsSync(flowPath)) {
5157
+ fs14.mkdirSync(flowDir, { recursive: true });
5158
+ fs14.writeFileSync(flowPath, "# Workflows\n\n_No workflows defined yet. Use /workflows add to create one._\n", "utf-8");
5159
+ }
5160
+ const skillDir = path14.join(home2, ".askill");
5161
+ const skillPath = path14.join(skillDir, "skills.md");
5162
+ if (!fs14.existsSync(skillPath)) {
5163
+ fs14.mkdirSync(skillDir, { recursive: true });
5164
+ fs14.writeFileSync(skillPath, "# Skills\n\n_No skills installed yet. Use /skills install to add domain expertise._\n", "utf-8");
5165
+ }
5131
5166
  return true;
5132
5167
  }
5133
5168
  var program = new Command();
5134
- program.name("aman-agent").description("Your AI companion, running locally").version("0.17.1").option("--model <model>", "Override LLM model").option("--budget <tokens>", "Token budget for system prompt (default: 8000)", parseInt).option("--profile <name>", "Use a specific agent profile (e.g., coder, writer, researcher)").action(async (options) => {
5169
+ program.name("aman-agent").description("Your AI companion, running locally").version("0.17.2").option("--model <model>", "Override LLM model").option("--budget <tokens>", "Token budget for system prompt (default: 8000)", parseInt).option("--profile <name>", "Use a specific agent profile (e.g., coder, writer, researcher)").action(async (options) => {
5135
5170
  p2.intro(pc7.bold("aman agent") + pc7.dim(" \u2014 your AI companion"));
5136
5171
  let config = loadConfig();
5137
5172
  if (!config) {
@@ -5258,6 +5293,7 @@ program.name("aman-agent").description("Your AI companion, running locally").ver
5258
5293
  }
5259
5294
  }
5260
5295
  const aiName = getProfileAiName(profile);
5296
+ if (config.memory) setMemoryConfig(config.memory);
5261
5297
  try {
5262
5298
  await initMemory();
5263
5299
  } catch (err) {
@@ -5281,13 +5317,16 @@ program.name("aman-agent").description("Your AI companion, running locally").ver
5281
5317
  const mcpManager = new McpManager();
5282
5318
  const mcpSpinner = p2.spinner();
5283
5319
  mcpSpinner.start("Connecting to MCP servers");
5284
- await mcpManager.connect("aman", "npx", ["-y", "@aman_asmuei/aman-mcp"]);
5320
+ const connections = [
5321
+ mcpManager.connect("aman", "npx", ["-y", "@aman_asmuei/aman-mcp"])
5322
+ ];
5285
5323
  if (config.mcpServers) {
5286
5324
  for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
5287
5325
  if (name === "aman" || name === "amem") continue;
5288
- await mcpManager.connect(name, serverConfig.command, serverConfig.args);
5326
+ connections.push(mcpManager.connect(name, serverConfig.command, serverConfig.args, serverConfig.env));
5289
5327
  }
5290
5328
  }
5329
+ await Promise.all(connections);
5291
5330
  const mcpTools = mcpManager.getTools();
5292
5331
  mcpSpinner.stop("MCP connected");
5293
5332
  if (mcpTools.length > 0) {