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 +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.js +7 -3
- package/dist/config.d.ts +16 -0
- package/dist/config.js +47 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/redact.js +6 -0
- package/dist/session.d.ts +15 -1
- package/dist/session.js +28 -3
- package/package.json +13 -4
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.
|
|
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.
|
|
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
|
|
48
|
-
this.publishMode =
|
|
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;
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
|
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 ??
|
|
61
|
+
const min = this.options.minFailedAttempts ?? Number(process.env.MYRMO_MIN_FAILED_ATTEMPTS ?? 1);
|
|
45
62
|
const last = this.failures.at(-1);
|
|
46
|
-
|
|
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:
|
|
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.
|
|
4
|
-
"description": "Myrmo
|
|
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
|
-
"
|
|
40
|
-
"
|
|
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",
|