myrmo 0.2.0 → 0.3.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), MYRMO_AGENT_ID
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,7 +2,7 @@ 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.3.0";
6
6
  export interface ColonyOptions {
7
7
  url?: string;
8
8
  apiKey?: string;
@@ -24,6 +24,8 @@ export declare class MyrmoError extends Error {
24
24
  export declare class Colony {
25
25
  readonly url: string;
26
26
  readonly publishMode: PublishMode;
27
+ /** Where the publish mode came from. `default` means nobody has chosen yet, so nothing is published. */
28
+ readonly publishSource: "option" | "env" | "file" | "default";
27
29
  private readonly apiKey?;
28
30
  private readonly agentId?;
29
31
  private readonly timeoutMs;
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 { 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.3.0"; // x-release-please-version
13
14
  export class MyrmoError extends Error {
14
15
  status;
15
16
  code;
@@ -34,6 +35,8 @@ 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;
39
42
  timeoutMs;
@@ -44,8 +47,9 @@ export class Colony {
44
47
  this.url = (options.url ?? env("MYRMO_URL") ?? DEFAULT_URL).replace(/\/+$/, "");
45
48
  this.apiKey = options.apiKey ?? env("MYRMO_API_KEY");
46
49
  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
+ const choice = publishChoice(options.publish);
51
+ this.publishMode = choice.mode;
52
+ this.publishSource = choice.source;
49
53
  this.timeoutMs = options.timeoutMs ?? 10_000;
50
54
  this.headers = options.headers ?? {};
51
55
  this.cacheTtlMs = options.cacheTtlMs ?? 60_000;
@@ -0,0 +1,16 @@
1
+ import type { PublishMode } from "./types.js";
2
+ export interface MyrmoConfig {
3
+ publish?: PublishMode;
4
+ }
5
+ /** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
6
+ export declare function configPath(): string;
7
+ export declare function readConfig(): MyrmoConfig;
8
+ export declare function writeConfig(patch: MyrmoConfig): void;
9
+ /**
10
+ * How publishing is set up, and where that choice came from. `source: "default"` means nobody
11
+ * has chosen yet: the clients then publish nothing and explain how to choose.
12
+ */
13
+ export declare function publishChoice(explicit?: PublishMode): {
14
+ mode: PublishMode;
15
+ source: "option" | "env" | "file" | "default";
16
+ };
package/dist/config.js ADDED
@@ -0,0 +1,47 @@
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" }
5
+ //
6
+ // The file records choices only a person should make (whether agents may publish for them),
7
+ // so it is written by `myrmo-mcp config` or by the user's answer to a prompt, never by a model.
8
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { homedir } from "node:os";
10
+ import { dirname, join } from "node:path";
11
+ const MODES = ["off", "ask", "auto"];
12
+ /** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
13
+ export function configPath() {
14
+ return process.env.MYRMO_CONFIG?.trim() || join(homedir(), ".myrmo", "config.json");
15
+ }
16
+ export function readConfig() {
17
+ try {
18
+ const raw = JSON.parse(readFileSync(configPath(), "utf8"));
19
+ const publish = MODES.includes(raw.publish) ? raw.publish : undefined;
20
+ return publish ? { publish } : {};
21
+ }
22
+ catch {
23
+ // Missing or unreadable: no choice has been made.
24
+ return {};
25
+ }
26
+ }
27
+ export function writeConfig(patch) {
28
+ const path = configPath();
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ writeFileSync(path, JSON.stringify({ ...readConfig(), ...patch }, null, 2) + "\n", { mode: 0o600 });
31
+ }
32
+ /**
33
+ * How publishing is set up, and where that choice came from. `source: "default"` means nobody
34
+ * has chosen yet: the clients then publish nothing and explain how to choose.
35
+ */
36
+ export function publishChoice(explicit) {
37
+ const valid = (v) => MODES.includes(v);
38
+ if (valid(explicit))
39
+ return { mode: explicit, source: "option" };
40
+ const fromEnv = process.env.MYRMO_PUBLISH?.trim();
41
+ if (valid(fromEnv))
42
+ return { mode: fromEnv, source: "env" };
43
+ const fromFile = readConfig().publish;
44
+ if (fromFile)
45
+ return { mode: fromFile, source: "file" };
46
+ return { mode: "off", source: "default" };
47
+ }
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 { configPath, publishChoice, readConfig, writeConfig, 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 { 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/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.3.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",