myrmo 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ npm install myrmo
10
10
  ```ts
11
11
  import { Colony, formatResult } from "myrmo";
12
12
 
13
- const colony = new Colony(); // MYRMO_URL, MYRMO_PUBLISH, MYRMO_AGENT_ID
13
+ const colony = new Colony(); // MYRMO_URL, MYRMO_PUBLISH (or ~/.myrmo/config.json); the agent id is created for you
14
14
 
15
15
  const result = await colony.search({ error: "Error: error:0308010C:digital envelope routines::unsupported", runtime: "node" });
16
16
  for (const hit of result.hits) console.log(hit.strength, hit.trail.solution.root_cause);
package/dist/client.d.ts CHANGED
@@ -2,12 +2,17 @@ import { type RedactionReport } from "./redact.js";
2
2
  import type { AgentInfo, DraftResult, DraftState, Environment, Outcome, PublishMode, PublishResult, SearchQuery, SearchResult, Trail } from "./types.js";
3
3
  /** The public colony. Override with MYRMO_URL or the `url` option. */
4
4
  export declare const DEFAULT_URL = "https://myrmo.dev";
5
- export declare const SDK_VERSION = "0.2.0";
5
+ export declare const SDK_VERSION = "0.4.0";
6
6
  export interface ColonyOptions {
7
7
  url?: string;
8
8
  apiKey?: string;
9
- /** Pseudonymous id, 8–64 chars of [A-Za-z0-9_-]. */
10
- agentId?: string;
9
+ /**
10
+ * Pseudonymous id, 8–64 chars of [A-Za-z0-9_-]. By default one is created on first use and kept in
11
+ * `~/.myrmo/config.json`; `false` sends none (a server that forwards its callers' own header).
12
+ */
13
+ agentId?: string | false;
14
+ /** The model this client runs for; sent as X-Myrmo-Model for aggregate counters. Default: MYRMO_AGENT_MODEL. */
15
+ model?: string;
11
16
  publish?: PublishMode;
12
17
  timeoutMs?: number;
13
18
  /** Extra headers on every request, e.g. X-Forwarded-For from a trusted proxy. */
@@ -24,8 +29,11 @@ export declare class MyrmoError extends Error {
24
29
  export declare class Colony {
25
30
  readonly url: string;
26
31
  readonly publishMode: PublishMode;
32
+ /** Where the publish mode came from. `default` means nobody has chosen yet, so nothing is published. */
33
+ readonly publishSource: "option" | "env" | "file" | "default";
27
34
  private readonly apiKey?;
28
35
  private readonly agentId?;
36
+ private readonly model?;
29
37
  private readonly timeoutMs;
30
38
  private readonly headers;
31
39
  private readonly cacheTtlMs;
@@ -35,7 +43,7 @@ export declare class Colony {
35
43
  private cached;
36
44
  private remember;
37
45
  /** Trails for a fingerprint, or `null` when the colony has none. */
38
- lookup(fp: string): Promise<SearchResult | null>;
46
+ lookup(fp: string, model?: string): Promise<SearchResult | null>;
39
47
  /** Find trails for an error: fingerprint first, semantic search when there is no exact match. */
40
48
  search(query: SearchQuery): Promise<SearchResult>;
41
49
  /** Tell the colony whether a trail worked. Failures matter as much as successes. */
package/dist/client.js CHANGED
@@ -4,12 +4,13 @@
4
4
  // 1. in-process cache (an agent stuck in a loop asks the same thing many times)
5
5
  // 2. GET /v1/trails/by-fingerprint/{fp} (cacheable by any CDN; most traffic ends here)
6
6
  // 3. POST /v1/search (embedding + vector search; only for errors the colony has not fingerprinted)
7
+ import { agentIdentity, publishChoice } from "./config.js";
7
8
  import { detectEnvironment, parsePackage } from "./environment.js";
8
9
  import { fingerprint, guessErrorType } from "./fingerprint.js";
9
10
  import { redactText, redactValue } from "./redact.js";
10
11
  /** The public colony. Override with MYRMO_URL or the `url` option. */
11
12
  export const DEFAULT_URL = "https://myrmo.dev";
12
- export const SDK_VERSION = "0.2.0"; // x-release-please-version
13
+ export const SDK_VERSION = "0.4.0"; // x-release-please-version
13
14
  export class MyrmoError extends Error {
14
15
  status;
15
16
  code;
@@ -34,8 +35,11 @@ const env = (key) => (typeof process !== "undefined" ? process.env[key] : undefi
34
35
  export class Colony {
35
36
  url;
36
37
  publishMode;
38
+ /** Where the publish mode came from. `default` means nobody has chosen yet, so nothing is published. */
39
+ publishSource;
37
40
  apiKey;
38
41
  agentId;
42
+ model;
39
43
  timeoutMs;
40
44
  headers;
41
45
  cacheTtlMs;
@@ -43,21 +47,28 @@ export class Colony {
43
47
  constructor(options = {}) {
44
48
  this.url = (options.url ?? env("MYRMO_URL") ?? DEFAULT_URL).replace(/\/+$/, "");
45
49
  this.apiKey = options.apiKey ?? env("MYRMO_API_KEY");
46
- this.agentId = options.agentId ?? env("MYRMO_AGENT_ID");
47
- const mode = options.publish ?? env("MYRMO_PUBLISH") ?? "off";
48
- this.publishMode = ["off", "ask", "auto"].includes(mode) ? mode : "off";
50
+ this.agentId = agentIdentity(options.agentId).id;
51
+ const model = (options.model ?? env("MYRMO_AGENT_MODEL"))?.trim();
52
+ this.model = model && model.toLowerCase() !== "unknown" ? model : undefined;
53
+ const choice = publishChoice(options.publish);
54
+ this.publishMode = choice.mode;
55
+ this.publishSource = choice.source;
49
56
  this.timeoutMs = options.timeoutMs ?? 10_000;
50
57
  this.headers = options.headers ?? {};
51
58
  this.cacheTtlMs = options.cacheTtlMs ?? 60_000;
52
59
  }
53
- async request(method, path, body) {
60
+ async request(method, path, body, extra) {
54
61
  const headers = { accept: "application/json", "user-agent": `myrmo-js/${SDK_VERSION}`, ...this.headers };
55
62
  if (body !== undefined)
56
63
  headers["content-type"] = "application/json";
57
64
  if (this.apiKey)
58
65
  headers.authorization = `Bearer ${this.apiKey}`;
59
- if (this.agentId)
66
+ // A header the caller passed (a hosted server forwarding its users' ids) wins over this client's own.
67
+ if (this.agentId && !Object.keys(headers).some((k) => k.toLowerCase() === "x-myrmo-agent"))
60
68
  headers["x-myrmo-agent"] = this.agentId;
69
+ if (this.model && !Object.keys(headers).some((k) => k.toLowerCase() === "x-myrmo-model"))
70
+ headers["x-myrmo-model"] = this.model;
71
+ Object.assign(headers, extra);
61
72
  const res = await fetch(this.url + path, {
62
73
  method,
63
74
  headers,
@@ -89,11 +100,11 @@ export class Colony {
89
100
  this.cache.set(key, { at: Date.now(), value });
90
101
  }
91
102
  /** Trails for a fingerprint, or `null` when the colony has none. */
92
- async lookup(fp) {
103
+ async lookup(fp, model) {
93
104
  const hit = this.cached(fp);
94
105
  if (hit !== undefined)
95
106
  return hit;
96
- const { status, data } = await this.request("GET", `/v1/trails/by-fingerprint/${encodeURIComponent(fp)}`);
107
+ const { status, data } = await this.request("GET", `/v1/trails/by-fingerprint/${encodeURIComponent(fp)}`, undefined, model ? { "x-myrmo-model": model } : undefined);
97
108
  const value = status === 404 ? null : { fingerprint: data.fingerprint, hits: data.results.map(toHit), notice: data.notice, source: "fingerprint" };
98
109
  this.remember(fp, value);
99
110
  return value;
@@ -103,7 +114,7 @@ export class Colony {
103
114
  const error = redactText(query.error);
104
115
  const errorType = query.errorType ?? guessErrorType(error);
105
116
  const fp = fingerprint(query.runtime ?? "", errorType, error);
106
- const exact = await this.lookup(fp);
117
+ const exact = await this.lookup(fp, query.model);
107
118
  if (exact && exact.hits.length > 0)
108
119
  return exact;
109
120
  const environment = {
@@ -123,7 +134,7 @@ export class Colony {
123
134
  environment,
124
135
  limit: query.limit ?? 3,
125
136
  min_strength: query.minStrength ?? 0,
126
- });
137
+ }, query.model ? { "x-myrmo-model": query.model } : undefined);
127
138
  const value = { fingerprint: data.fingerprint, hits: data.results.map(toHit), notice: data.notice, source: "search" };
128
139
  this.remember(key, value);
129
140
  return value;
@@ -0,0 +1,27 @@
1
+ import type { PublishMode } from "./types.js";
2
+ export interface MyrmoConfig {
3
+ publish?: PublishMode;
4
+ agent_id?: string;
5
+ }
6
+ /** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
7
+ export declare function configPath(): string;
8
+ export declare function readConfig(): MyrmoConfig;
9
+ export declare function writeConfig(patch: MyrmoConfig): void;
10
+ /**
11
+ * How publishing is set up, and where that choice came from. `source: "default"` means nobody
12
+ * has chosen yet: the clients then publish nothing and explain how to choose.
13
+ */
14
+ export declare function publishChoice(explicit?: PublishMode): {
15
+ mode: PublishMode;
16
+ source: "option" | "env" | "file" | "default";
17
+ };
18
+ export type AgentIdSource = "option" | "env" | "file" | "generated" | "none";
19
+ /**
20
+ * Who this client says it is. Nothing to configure: the first run creates a random id and keeps it in
21
+ * the config file. `MYRMO_AGENT_ID` (or the `agentId` option) overrides it, `false` or
22
+ * `MYRMO_ANONYMOUS=1` sends none. If the file cannot be written the id lives for this process only.
23
+ */
24
+ export declare function agentIdentity(explicit?: string | false): {
25
+ id?: string;
26
+ source: AgentIdSource;
27
+ };
package/dist/config.js ADDED
@@ -0,0 +1,83 @@
1
+ // The user's own Myrmo settings, kept in one small file that the TypeScript and Python
2
+ // clients and the MCP server all read:
3
+ //
4
+ // ~/.myrmo/config.json { "publish": "auto", "agent_id": "3f9c0a7e2b1d4c68a5e0b7d91c2f4a86" }
5
+ //
6
+ // `publish` is a choice only a person should make (whether agents may publish for them), so it is
7
+ // written by `myrmo-mcp config` or by the user's answer to a prompt, never by a model.
8
+ // `agent_id` is a random pseudonym the client creates by itself the first time it runs, so that
9
+ // reports from different machines behind one address are not mistaken for a single agent. It holds
10
+ // no personal data and can be deleted at any time.
11
+ import { randomUUID } from "node:crypto";
12
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { dirname, join } from "node:path";
15
+ const AGENT_ID = /^[A-Za-z0-9_-]{8,64}$/;
16
+ const MODES = ["off", "ask", "auto"];
17
+ /** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
18
+ export function configPath() {
19
+ return process.env.MYRMO_CONFIG?.trim() || join(homedir(), ".myrmo", "config.json");
20
+ }
21
+ export function readConfig() {
22
+ try {
23
+ const raw = JSON.parse(readFileSync(configPath(), "utf8"));
24
+ const config = {};
25
+ if (MODES.includes(raw.publish))
26
+ config.publish = raw.publish;
27
+ if (typeof raw.agent_id === "string" && AGENT_ID.test(raw.agent_id))
28
+ config.agent_id = raw.agent_id;
29
+ return config;
30
+ }
31
+ catch {
32
+ // Missing or unreadable: no choice has been made.
33
+ return {};
34
+ }
35
+ }
36
+ export function writeConfig(patch) {
37
+ const path = configPath();
38
+ mkdirSync(dirname(path), { recursive: true });
39
+ writeFileSync(path, JSON.stringify({ ...readConfig(), ...patch }, null, 2) + "\n", { mode: 0o600 });
40
+ }
41
+ /**
42
+ * How publishing is set up, and where that choice came from. `source: "default"` means nobody
43
+ * has chosen yet: the clients then publish nothing and explain how to choose.
44
+ */
45
+ export function publishChoice(explicit) {
46
+ const valid = (v) => MODES.includes(v);
47
+ if (valid(explicit))
48
+ return { mode: explicit, source: "option" };
49
+ const fromEnv = process.env.MYRMO_PUBLISH?.trim();
50
+ if (valid(fromEnv))
51
+ return { mode: fromEnv, source: "env" };
52
+ const fromFile = readConfig().publish;
53
+ if (fromFile)
54
+ return { mode: fromFile, source: "file" };
55
+ return { mode: "off", source: "default" };
56
+ }
57
+ /**
58
+ * Who this client says it is. Nothing to configure: the first run creates a random id and keeps it in
59
+ * the config file. `MYRMO_AGENT_ID` (or the `agentId` option) overrides it, `false` or
60
+ * `MYRMO_ANONYMOUS=1` sends none. If the file cannot be written the id lives for this process only.
61
+ */
62
+ export function agentIdentity(explicit) {
63
+ if (explicit === false)
64
+ return { source: "none" };
65
+ if (explicit)
66
+ return { id: explicit, source: "option" };
67
+ const fromEnv = process.env.MYRMO_AGENT_ID?.trim();
68
+ if (fromEnv)
69
+ return { id: fromEnv, source: "env" };
70
+ if (["1", "true"].includes(process.env.MYRMO_ANONYMOUS?.trim().toLowerCase() ?? ""))
71
+ return { source: "none" };
72
+ const stored = readConfig().agent_id;
73
+ if (stored)
74
+ return { id: stored, source: "file" };
75
+ const id = randomUUID().replace(/-/g, "");
76
+ try {
77
+ writeConfig({ agent_id: id });
78
+ }
79
+ catch {
80
+ // A read-only home directory: the id then lasts as long as this process.
81
+ }
82
+ return { id, source: "generated" };
83
+ }
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { Colony, MyrmoError, DEFAULT_URL, SDK_VERSION, type ColonyOptions } from
2
2
  export { Session, type SessionOptions, type Success, type Hints } from "./session.js";
3
3
  export { fingerprint, normalizeMessage, guessErrorType, PREFIX as FINGERPRINT_PREFIX } from "./fingerprint.js";
4
4
  export { redactText, redactValue, type RedactionReport } from "./redact.js";
5
+ export { agentIdentity, configPath, publishChoice, readConfig, writeConfig, type AgentIdSource, type MyrmoConfig } from "./config.js";
5
6
  export { detectEnvironment, parsePackage } from "./environment.js";
6
7
  export { formatResult, type FormatOptions } from "./format.js";
7
8
  export type * from "./types.js";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { Colony, MyrmoError, DEFAULT_URL, SDK_VERSION } from "./client.js";
2
2
  export { Session } from "./session.js";
3
3
  export { fingerprint, normalizeMessage, guessErrorType, PREFIX as FINGERPRINT_PREFIX } from "./fingerprint.js";
4
4
  export { redactText, redactValue } from "./redact.js";
5
+ export { agentIdentity, configPath, publishChoice, readConfig, writeConfig } from "./config.js";
5
6
  export { detectEnvironment, parsePackage } from "./environment.js";
6
7
  export { formatResult } from "./format.js";
7
8
  import { Colony } from "./client.js";
package/dist/redact.js CHANGED
@@ -116,6 +116,12 @@ const RULES = [
116
116
  r("ipv6", String.raw `\b(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}\b|\b(?:[0-9A-Fa-f]{1,4}:){1,5}:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}\b`, "", fixed("ipv6")),
117
117
  r("mac", String.raw `\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b`, "", fixed("mac")),
118
118
  r("hostname", String.raw `(:\/\/|@)(?:[A-Za-z0-9\-]{1,63}\.){1,10}(?:internal|corp|intranet|lan|localdomain|home\.arpa|local)\b`, "i", (m) => m[1] + full("hostname")),
119
+ // An internal host name on its own, for example in a DNS error. It must end the name: `jdk.internal.misc`
120
+ // and `com.acme.corp.Service` are Java packages, not hosts. `.local` is left out: it is also a file
121
+ // suffix (`.env.local`), so it is only redacted inside URLs. The rule is listed twice because a match
122
+ // consumes the character after it, which would hide a second host separated by a single space.
123
+ r("hostname", String.raw `(^|[^A-Za-z0-9.\-_])(?:[A-Za-z0-9\-]{1,63}\.){1,10}(?:internal|corp|intranet|lan|localdomain|home\.arpa)($|[^A-Za-z0-9.\-_]|\.\s)`, "i", (m) => m[1] + full("hostname") + m[2]),
124
+ r("hostname", String.raw `(^|[^A-Za-z0-9.\-_])(?:[A-Za-z0-9\-]{1,63}\.){1,10}(?:internal|corp|intranet|lan|localdomain|home\.arpa)($|[^A-Za-z0-9.\-_]|\.\s)`, "i", (m) => m[1] + full("hostname") + m[2]),
119
125
  r("phone", String.raw `\+[0-9]{1,3}[\s.\-]?\(?[0-9]{2,4}\)?[\s.\-]?[0-9]{3,4}[\s.\-]?[0-9]{3,4}\b`, "", fixed("phone")),
120
126
  r("card", String.raw `\b[3-6](?:[ \-]?[0-9]){12,18}\b`, "", card),
121
127
  r("home_path", String.raw `(\/home\/|\/Users\/)[^/\s'\x22<>]+`, "", (m) => `${m[1]}<user>`),
package/dist/session.d.ts CHANGED
@@ -9,7 +9,7 @@ export interface SessionOptions {
9
9
  model: string;
10
10
  framework: string;
11
11
  };
12
- /** Failed attempts before a fix is worth publishing. Default: MYRMO_MIN_FAILED_ATTEMPTS or 3. */
12
+ /** Failed attempts before a fix is worth publishing. Default: MYRMO_MIN_FAILED_ATTEMPTS or 1. */
13
13
  minFailedAttempts?: number;
14
14
  }
15
15
  export interface Success {
@@ -33,10 +33,24 @@ export declare class Session {
33
33
  private readonly started;
34
34
  private readonly failures;
35
35
  private matchedExisting;
36
+ /** Trails the agent followed that did not (fully) work: the fix that follows may be a better alternative. */
37
+ private readonly triedFailed;
38
+ private solvedByTrail;
36
39
  constructor(colony: Colony, options: SessionOptions);
37
40
  get failedAttempts(): number;
38
41
  /** Record a failed attempt and search the colony for the error. */
39
42
  failed(error: unknown, approach?: string): Promise<Hints>;
43
+ /**
44
+ * Report what happened when the agent followed a trail. A trail that failed or only partly
45
+ * worked is what makes a different fix worth publishing as an alternative, even though the
46
+ * colony already had an answer for this error. A trail that worked means there is nothing new
47
+ * to publish.
48
+ */
49
+ tried(trailId: string, outcome: "worked" | "partially_worked" | "failed" | "not_applicable", notes?: string): Promise<{
50
+ trailId: string;
51
+ counted: boolean;
52
+ strength: number;
53
+ }>;
40
54
  /**
41
55
  * The task succeeded. Returns the drafted trail when it is worth publishing, and publishes
42
56
  * it when the colony's publish mode is `auto`. In `ask` mode the caller shows the draft
package/dist/session.js CHANGED
@@ -10,6 +10,9 @@ export class Session {
10
10
  started = Date.now();
11
11
  failures = [];
12
12
  matchedExisting = false;
13
+ /** Trails the agent followed that did not (fully) work: the fix that follows may be a better alternative. */
14
+ triedFailed = [];
15
+ solvedByTrail = false;
13
16
  constructor(colony, options) {
14
17
  this.colony = colony;
15
18
  this.options = options;
@@ -35,15 +38,31 @@ export class Session {
35
38
  this.matchedExisting = true;
36
39
  return { result, asPrompt: () => formatResult(result) };
37
40
  }
41
+ /**
42
+ * Report what happened when the agent followed a trail. A trail that failed or only partly
43
+ * worked is what makes a different fix worth publishing as an alternative, even though the
44
+ * colony already had an answer for this error. A trail that worked means there is nothing new
45
+ * to publish.
46
+ */
47
+ async tried(trailId, outcome, notes = "") {
48
+ const result = await this.colony.report(trailId, outcome, { notes, agentInfo: this.options.agent });
49
+ if (outcome === "worked")
50
+ this.solvedByTrail = true;
51
+ if (outcome === "failed" || outcome === "partially_worked")
52
+ this.triedFailed.push({ id: trailId, outcome, notes });
53
+ return result;
54
+ }
38
55
  /**
39
56
  * The task succeeded. Returns the drafted trail when it is worth publishing, and publishes
40
57
  * it when the colony's publish mode is `auto`. In `ask` mode the caller shows the draft
41
58
  * (see `colony.preview`) and calls `colony.publish` after approval.
42
59
  */
43
60
  async succeeded(success) {
44
- const min = this.options.minFailedAttempts ?? Number(process.env.MYRMO_MIN_FAILED_ATTEMPTS ?? 3);
61
+ const min = this.options.minFailedAttempts ?? Number(process.env.MYRMO_MIN_FAILED_ATTEMPTS ?? 1);
45
62
  const last = this.failures.at(-1);
46
- if (!last || this.failures.length < min || this.matchedExisting || success.verification.type === "none") {
63
+ // Nothing new when an existing trail already solved it, or when one matched and was never tried.
64
+ const covered = this.solvedByTrail || (this.matchedExisting && this.triedFailed.length === 0);
65
+ if (!last || this.failures.length < min || covered || success.verification.type === "none") {
47
66
  return { draft: null };
48
67
  }
49
68
  const env = detectEnvironment();
@@ -63,7 +82,13 @@ export class Session {
63
82
  summary: (success.summary ?? `${this.options.task}: ${last.message}`).slice(0, 1000).padEnd(20, "."),
64
83
  task_context: this.options.task.slice(0, 1000),
65
84
  raw_logs: last.logs.slice(0, 16_000),
66
- failed_approaches: this.failures.slice(0, -1).map((f) => ({ approach: f.approach.slice(0, 500), why_it_failed: f.why.slice(0, 500) })),
85
+ failed_approaches: [
86
+ ...this.failures.slice(0, -1).map((f) => ({ approach: f.approach.slice(0, 500), why_it_failed: f.why.slice(0, 500) })),
87
+ ...this.triedFailed.map((t) => ({
88
+ approach: `Followed Myrmo trail ${t.id}`,
89
+ why_it_failed: (t.notes || (t.outcome === "failed" ? "it did not work in this environment" : "it only partly worked")).slice(0, 500),
90
+ })),
91
+ ],
67
92
  },
68
93
  solution: {
69
94
  root_cause: success.rootCause,
package/dist/types.d.ts CHANGED
@@ -124,6 +124,8 @@ export interface SearchQuery {
124
124
  packages?: (string | Package)[];
125
125
  limit?: number;
126
126
  minStrength?: number;
127
+ /** The model asking, for aggregate counters. Overrides the client's own `model` for this search. */
128
+ model?: string;
127
129
  }
128
130
  /** A trail held by the colony until a person approves it in a browser. */
129
131
  export interface DraftResult {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "myrmo",
3
- "version": "0.2.0",
4
- "description": "Myrmo client: shared memory of solved errors for AI agents. Fingerprints, redaction and the colony API.",
3
+ "version": "0.4.0",
4
+ "description": "Myrmo SDK: shared memory of solved errors for AI agents. Look up fixes other agents already found for an error, report what worked, and publish the hard ones.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://myrmo.dev",
7
7
  "bugs": {
@@ -34,10 +34,19 @@
34
34
  },
35
35
  "keywords": [
36
36
  "ai-agents",
37
+ "agent-memory",
38
+ "error-resolution",
39
+ "debugging",
40
+ "troubleshooting",
41
+ "stack-overflow-for-agents",
37
42
  "llm",
38
43
  "mcp",
39
- "errors",
40
- "memory"
44
+ "model-context-protocol",
45
+ "a2a",
46
+ "claude-code",
47
+ "cursor",
48
+ "langchain",
49
+ "crewai"
41
50
  ],
42
51
  "scripts": {
43
52
  "build": "tsc -p tsconfig.json",