myrmo 0.4.0 → 0.5.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/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.4.0";
5
+ export declare const SDK_VERSION = "0.5.0";
6
6
  export interface ColonyOptions {
7
7
  url?: string;
8
8
  apiKey?: string;
package/dist/client.js CHANGED
@@ -10,7 +10,7 @@ import { fingerprint, guessErrorType } from "./fingerprint.js";
10
10
  import { redactText, redactValue } from "./redact.js";
11
11
  /** The public colony. Override with MYRMO_URL or the `url` option. */
12
12
  export const DEFAULT_URL = "https://myrmo.dev";
13
- export const SDK_VERSION = "0.4.0"; // x-release-please-version
13
+ export const SDK_VERSION = "0.5.0"; // x-release-please-version
14
14
  export class MyrmoError extends Error {
15
15
  status;
16
16
  code;
package/dist/config.d.ts CHANGED
@@ -1,20 +1,82 @@
1
1
  import type { PublishMode } from "./types.js";
2
+ export type HookMode = "on" | "off";
2
3
  export interface MyrmoConfig {
3
4
  publish?: PublishMode;
4
5
  agent_id?: string;
6
+ /** Failed attempts before a fix is worth publishing. */
7
+ min_failed_attempts?: number;
8
+ /** The Claude Code plugin's reminder to search when a command fails. */
9
+ hook?: HookMode;
10
+ /** Send no agent id at all. */
11
+ anonymous?: boolean;
5
12
  }
13
+ export declare const DEFAULT_MIN_FAILED_ATTEMPTS = 1;
6
14
  /** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
7
15
  export declare function configPath(): string;
8
16
  export declare function readConfig(): MyrmoConfig;
9
- export declare function writeConfig(patch: MyrmoConfig): void;
17
+ /** Merge `patch` into the file. A key set to `undefined` is removed, which restores its default. */
18
+ export declare function writeConfig(patch: {
19
+ [K in keyof MyrmoConfig]?: MyrmoConfig[K] | undefined;
20
+ }): void;
10
21
  /**
11
22
  * 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.
23
+ * has chosen yet: the clients then publish nothing until the user has been asked once (the MCP
24
+ * server asks the first time an agent wants to publish, with "ask" preselected).
13
25
  */
14
26
  export declare function publishChoice(explicit?: PublishMode): {
15
27
  mode: PublishMode;
16
28
  source: "option" | "env" | "file" | "default";
17
29
  };
30
+ export type SettingSource = "option" | "env" | "file" | "default";
31
+ /** Failed attempts before a fix is worth publishing: option, then MYRMO_MIN_FAILED_ATTEMPTS, then the file, then 1. */
32
+ export declare function minFailedAttempts(explicit?: number): {
33
+ value: number;
34
+ source: SettingSource;
35
+ };
36
+ /** What `myrmo-mcp config` can change: its name, what it does, the values it takes, and the default. */
37
+ export declare const SETTINGS: readonly [{
38
+ readonly key: "publish";
39
+ readonly file: "publish";
40
+ readonly values: "ask | auto | off";
41
+ readonly default: "ask (asked the first time an agent wants to publish)";
42
+ readonly about: "Whether agents may publish fixes for you. ask shows you each one first.";
43
+ }, {
44
+ readonly key: "min-failed-attempts";
45
+ readonly file: "min_failed_attempts";
46
+ readonly values: "0 to 20";
47
+ readonly default: string;
48
+ readonly about: "Failed attempts before a fix is worth publishing. Higher means fewer, more selective trails.";
49
+ }, {
50
+ readonly key: "hook";
51
+ readonly file: "hook";
52
+ readonly values: "on | off";
53
+ readonly default: "on";
54
+ readonly about: "Claude Code plugin: remind the agent to search when a command fails.";
55
+ }, {
56
+ readonly key: "anonymous";
57
+ readonly file: "anonymous";
58
+ readonly values: "true | false";
59
+ readonly default: "false";
60
+ readonly about: "Send no agent id at all (reports then count by address).";
61
+ }];
62
+ export interface SettingRow {
63
+ key: string;
64
+ value: string;
65
+ source: SettingSource;
66
+ about: string;
67
+ values: string;
68
+ default: string;
69
+ }
70
+ /** Every setting with its current value and where that value comes from. */
71
+ export declare function settingsReport(): SettingRow[];
72
+ /** Change one setting from its CLI name. The value `reset` removes it from the file (back to the default). */
73
+ export declare function setSetting(key: string, value: string): {
74
+ ok: true;
75
+ message: string;
76
+ } | {
77
+ ok: false;
78
+ error: string;
79
+ };
18
80
  export type AgentIdSource = "option" | "env" | "file" | "generated" | "none";
19
81
  /**
20
82
  * Who this client says it is. Nothing to configure: the first run creates a random id and keeps it in
package/dist/config.js CHANGED
@@ -1,17 +1,22 @@
1
1
  // The user's own Myrmo settings, kept in one small file that the TypeScript and Python
2
2
  // clients and the MCP server all read:
3
3
  //
4
- // ~/.myrmo/config.json { "publish": "auto", "agent_id": "3f9c0a7e2b1d4c68a5e0b7d91c2f4a86" }
4
+ // ~/.myrmo/config.json { "publish": "ask", "min_failed_attempts": 1, "hook": "on", "agent_id": "3f9c..." }
5
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.
6
+ // Everything works with no file at all: every setting has a default. The file only records what a
7
+ // person chose. `publish` is a choice only a person should make (whether agents may publish for them),
8
+ // so it is written by `myrmo-mcp config` or by the user's answer to a prompt, never by a model.
8
9
  // `agent_id` is a random pseudonym the client creates by itself the first time it runs, so that
9
10
  // reports from different machines behind one address are not mistaken for a single agent. It holds
10
11
  // no personal data and can be deleted at any time.
12
+ //
13
+ // For each setting the order is: an explicit option, the environment variable, this file, the default.
11
14
  import { randomUUID } from "node:crypto";
12
15
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
16
  import { homedir } from "node:os";
14
17
  import { dirname, join } from "node:path";
18
+ export const DEFAULT_MIN_FAILED_ATTEMPTS = 1;
19
+ const MAX_MIN_FAILED_ATTEMPTS = 20;
15
20
  const AGENT_ID = /^[A-Za-z0-9_-]{8,64}$/;
16
21
  const MODES = ["off", "ask", "auto"];
17
22
  /** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
@@ -26,6 +31,13 @@ export function readConfig() {
26
31
  config.publish = raw.publish;
27
32
  if (typeof raw.agent_id === "string" && AGENT_ID.test(raw.agent_id))
28
33
  config.agent_id = raw.agent_id;
34
+ const attempts = parseAttempts(raw.min_failed_attempts);
35
+ if (attempts !== undefined)
36
+ config.min_failed_attempts = attempts;
37
+ if (raw.hook === "on" || raw.hook === "off")
38
+ config.hook = raw.hook;
39
+ if (typeof raw.anonymous === "boolean")
40
+ config.anonymous = raw.anonymous;
29
41
  return config;
30
42
  }
31
43
  catch {
@@ -33,6 +45,11 @@ export function readConfig() {
33
45
  return {};
34
46
  }
35
47
  }
48
+ function parseAttempts(value) {
49
+ const n = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
50
+ return typeof n === "number" && Number.isInteger(n) && n >= 0 && n <= MAX_MIN_FAILED_ATTEMPTS ? n : undefined;
51
+ }
52
+ /** Merge `patch` into the file. A key set to `undefined` is removed, which restores its default. */
36
53
  export function writeConfig(patch) {
37
54
  const path = configPath();
38
55
  mkdirSync(dirname(path), { recursive: true });
@@ -40,7 +57,8 @@ export function writeConfig(patch) {
40
57
  }
41
58
  /**
42
59
  * 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.
60
+ * has chosen yet: the clients then publish nothing until the user has been asked once (the MCP
61
+ * server asks the first time an agent wants to publish, with "ask" preselected).
44
62
  */
45
63
  export function publishChoice(explicit) {
46
64
  const valid = (v) => MODES.includes(v);
@@ -54,6 +72,75 @@ export function publishChoice(explicit) {
54
72
  return { mode: fromFile, source: "file" };
55
73
  return { mode: "off", source: "default" };
56
74
  }
75
+ /** Failed attempts before a fix is worth publishing: option, then MYRMO_MIN_FAILED_ATTEMPTS, then the file, then 1. */
76
+ export function minFailedAttempts(explicit) {
77
+ const fromOption = parseAttempts(explicit);
78
+ if (fromOption !== undefined)
79
+ return { value: fromOption, source: "option" };
80
+ const fromEnv = parseAttempts(process.env.MYRMO_MIN_FAILED_ATTEMPTS);
81
+ if (fromEnv !== undefined)
82
+ return { value: fromEnv, source: "env" };
83
+ const fromFile = readConfig().min_failed_attempts;
84
+ if (fromFile !== undefined)
85
+ return { value: fromFile, source: "file" };
86
+ return { value: DEFAULT_MIN_FAILED_ATTEMPTS, source: "default" };
87
+ }
88
+ /** What `myrmo-mcp config` can change: its name, what it does, the values it takes, and the default. */
89
+ export const SETTINGS = [
90
+ { key: "publish", file: "publish", values: "ask | auto | off", default: "ask (asked the first time an agent wants to publish)", about: "Whether agents may publish fixes for you. ask shows you each one first." },
91
+ { key: "min-failed-attempts", file: "min_failed_attempts", values: `0 to ${MAX_MIN_FAILED_ATTEMPTS}`, default: String(DEFAULT_MIN_FAILED_ATTEMPTS), about: "Failed attempts before a fix is worth publishing. Higher means fewer, more selective trails." },
92
+ { key: "hook", file: "hook", values: "on | off", default: "on", about: "Claude Code plugin: remind the agent to search when a command fails." },
93
+ { key: "anonymous", file: "anonymous", values: "true | false", default: "false", about: "Send no agent id at all (reports then count by address)." },
94
+ ];
95
+ /** Every setting with its current value and where that value comes from. */
96
+ export function settingsReport() {
97
+ const file = readConfig();
98
+ const publish = publishChoice();
99
+ const attempts = minFailedAttempts();
100
+ const hookEnv = process.env.MYRMO_HOOK?.trim().toLowerCase();
101
+ const anonEnv = process.env.MYRMO_ANONYMOUS?.trim().toLowerCase();
102
+ const rows = {
103
+ publish: { value: publish.source === "default" ? "ask" : publish.mode, source: publish.source },
104
+ "min-failed-attempts": { value: String(attempts.value), source: attempts.source },
105
+ hook: hookEnv === "on" || hookEnv === "off" ? { value: hookEnv, source: "env" } : file.hook ? { value: file.hook, source: "file" } : { value: "on", source: "default" },
106
+ anonymous: anonEnv ? { value: String(["1", "true"].includes(anonEnv)), source: "env" } : file.anonymous !== undefined ? { value: String(file.anonymous), source: "file" } : { value: "false", source: "default" },
107
+ };
108
+ return SETTINGS.map((s) => ({ key: s.key, ...rows[s.key], about: s.about, values: s.values, default: s.default }));
109
+ }
110
+ /** Change one setting from its CLI name. The value `reset` removes it from the file (back to the default). */
111
+ export function setSetting(key, value) {
112
+ const def = SETTINGS.find((s) => s.key === key);
113
+ if (!def)
114
+ return { ok: false, error: `Unknown setting "${key}". Settings: ${SETTINGS.map((s) => s.key).join(", ")}.` };
115
+ if (value === "reset") {
116
+ writeConfig({ [def.file]: undefined });
117
+ return { ok: true, message: `${key} is back to its default (${def.default}).` };
118
+ }
119
+ let patch;
120
+ if (key === "publish") {
121
+ if (!MODES.includes(value))
122
+ return { ok: false, error: "publish takes ask, auto or off." };
123
+ patch = { publish: value };
124
+ }
125
+ else if (key === "min-failed-attempts") {
126
+ const n = parseAttempts(value);
127
+ if (n === undefined)
128
+ return { ok: false, error: `min-failed-attempts takes a whole number from 0 to ${MAX_MIN_FAILED_ATTEMPTS}.` };
129
+ patch = { min_failed_attempts: n };
130
+ }
131
+ else if (key === "hook") {
132
+ if (value !== "on" && value !== "off")
133
+ return { ok: false, error: "hook takes on or off." };
134
+ patch = { hook: value };
135
+ }
136
+ else {
137
+ if (!["true", "false"].includes(value))
138
+ return { ok: false, error: "anonymous takes true or false." };
139
+ patch = { anonymous: value === "true" };
140
+ }
141
+ writeConfig(patch);
142
+ return { ok: true, message: `Saved to ${configPath()}: ${key} = ${value}.` };
143
+ }
57
144
  /**
58
145
  * Who this client says it is. Nothing to configure: the first run creates a random id and keeps it in
59
146
  * the config file. `MYRMO_AGENT_ID` (or the `agentId` option) overrides it, `false` or
@@ -69,6 +156,8 @@ export function agentIdentity(explicit) {
69
156
  return { id: fromEnv, source: "env" };
70
157
  if (["1", "true"].includes(process.env.MYRMO_ANONYMOUS?.trim().toLowerCase() ?? ""))
71
158
  return { source: "none" };
159
+ if (readConfig().anonymous === true)
160
+ return { source: "none" };
72
161
  const stored = readConfig().agent_id;
73
162
  if (stored)
74
163
  return { id: stored, source: "file" };
package/dist/format.d.ts CHANGED
@@ -3,5 +3,9 @@ export interface FormatOptions {
3
3
  /** Include commands flagged high risk. Off by default. */
4
4
  includeHighRisk?: boolean;
5
5
  maxTrails?: number;
6
+ /** Failed attempts before a fix is worth publishing, for the hint after a search with no match. Default: the configured minimum. */
7
+ minFailedAttempts?: number;
6
8
  }
9
+ /** "at least one failed attempt", "3 or more failed attempts". */
10
+ export declare function attemptsPhrase(n: number): string;
7
11
  export declare function formatResult(result: SearchResult, opts?: FormatOptions): string;
package/dist/format.js CHANGED
@@ -1,5 +1,10 @@
1
1
  // Render search results for a language model: compact, explicit about provenance, and
2
2
  // wrapped as untrusted data. Used by the MCP server and by Session hints.
3
+ import { minFailedAttempts } from "./config.js";
4
+ /** "at least one failed attempt", "3 or more failed attempts". */
5
+ export function attemptsPhrase(n) {
6
+ return n <= 1 ? "at least one failed attempt" : `${n} or more failed attempts`;
7
+ }
3
8
  // Trail text is written by strangers and is pasted into a model's context, so every field is
4
9
  // rendered so that it cannot forge structure: no line breaks inside a single-line field, no
5
10
  // look-alike of our own envelope tags, and code fences longer than anything they contain.
@@ -45,7 +50,7 @@ function formatHit(hit, index, total, opts) {
45
50
  const o = hit.outcomes;
46
51
  const lines = [];
47
52
  const overlap = hit.match.environment_overlap === null ? "" : ` · environment overlap ${hit.match.environment_overlap}`;
48
- lines.push(`## Trail ${index + 1} of ${total} · id ${safeId(hit.trailId)}`, `strength ${hit.strength} · worked ${o.worked} · partially ${o.partially_worked} · failed ${o.failed} · matched by ${clip(String(hit.match.via), 16)} (${hit.match.score})${overlap} · risk ${clip(String(hit.risk.level), 16)}`, `Environment: ${[t.environment.os, t.environment.os_version, t.environment.arch, `${t.environment.runtime.name} ${t.environment.runtime.version}`].map((x) => clip(x, 64)).filter(Boolean).join(" · ")}`, `Error: ${errorLine(t.problem.error_type, t.problem.error_message)}`, `Root cause: ${clip(t.solution.root_cause, 800)}`);
53
+ lines.push(`## Trail ${index + 1} of ${total} · id ${safeId(hit.trailId)}`, `strength ${hit.strength} · worked ${o.worked} · partially ${o.partially_worked} · failed ${o.failed} · matched by ${clip(String(hit.match.via), 16)} (${hit.match.score})${overlap} · risk ${clip(String(hit.risk.level), 16)}`, `Environment: ${[t.environment.os, t.environment.os_version, t.environment.arch, t.environment.container && t.environment.container !== "none" ? `in ${t.environment.container}` : "", `${t.environment.runtime.name} ${t.environment.runtime.version}`].map((x) => clip(x, 64)).filter(Boolean).join(" · ")}`, `Error: ${errorLine(t.problem.error_type, t.problem.error_message)}`, `Root cause: ${clip(t.solution.root_cause, 800)}`);
49
54
  const dead = t.problem.failed_approaches ?? [];
50
55
  if (dead.length) {
51
56
  lines.push("Dead ends, do not retry:");
@@ -84,7 +89,7 @@ export function formatResult(result, opts = {}) {
84
89
  if (hits.length === 0) {
85
90
  return [
86
91
  `No trail in the Myrmo colony matches this error yet (fingerprint ${safeFingerprint(result.fingerprint)}).`,
87
- "Solve it yourself. If it takes 3 or more failed attempts and you verify the fix, publish it with myrmo_publish so the next agent does not have to.",
92
+ `Solve it yourself. If it takes ${attemptsPhrase(minFailedAttempts(opts.minFailedAttempts).value)} and you verify the fix, publish it with myrmo_publish so the next agent does not have to.`,
88
93
  ].join("\n");
89
94
  }
90
95
  return [
package/dist/index.d.ts CHANGED
@@ -2,9 +2,9 @@ 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
+ export { DEFAULT_MIN_FAILED_ATTEMPTS, SETTINGS, agentIdentity, configPath, minFailedAttempts, publishChoice, readConfig, setSetting, settingsReport, writeConfig, type AgentIdSource, type HookMode, type MyrmoConfig, type SettingRow, type SettingSource } from "./config.js";
6
6
  export { detectEnvironment, parsePackage } from "./environment.js";
7
- export { formatResult, type FormatOptions } from "./format.js";
7
+ export { attemptsPhrase, formatResult, type FormatOptions } from "./format.js";
8
8
  export type * from "./types.js";
9
9
  import { Session, type SessionOptions } from "./session.js";
10
10
  declare module "./client.js" {
package/dist/index.js CHANGED
@@ -2,9 +2,9 @@ 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
+ export { DEFAULT_MIN_FAILED_ATTEMPTS, SETTINGS, agentIdentity, configPath, minFailedAttempts, publishChoice, readConfig, setSetting, settingsReport, writeConfig } from "./config.js";
6
6
  export { detectEnvironment, parsePackage } from "./environment.js";
7
- export { formatResult } from "./format.js";
7
+ export { attemptsPhrase, formatResult } from "./format.js";
8
8
  import { Colony } from "./client.js";
9
9
  import { Session } from "./session.js";
10
10
  Colony.prototype.session = function (options) {
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 1. */
12
+ /** Failed attempts before a fix is worth publishing. Default: MYRMO_MIN_FAILED_ATTEMPTS, the settings file, or 1. */
13
13
  minFailedAttempts?: number;
14
14
  }
15
15
  export interface Success {
package/dist/session.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // A session wraps one task: it searches the colony on every new error, records failed
2
2
  // approaches, and drafts a trail when the task finally succeeds after enough failures.
3
+ import { minFailedAttempts } from "./config.js";
3
4
  import { detectEnvironment, parsePackage } from "./environment.js";
4
5
  import { formatResult } from "./format.js";
5
6
  import { fingerprint, guessErrorType } from "./fingerprint.js";
@@ -58,7 +59,7 @@ export class Session {
58
59
  * (see `colony.preview`) and calls `colony.publish` after approval.
59
60
  */
60
61
  async succeeded(success) {
61
- const min = this.options.minFailedAttempts ?? Number(process.env.MYRMO_MIN_FAILED_ATTEMPTS ?? 1);
62
+ const min = minFailedAttempts(this.options.minFailedAttempts).value;
62
63
  const last = this.failures.at(-1);
63
64
  // Nothing new when an existing trail already solved it, or when one matched and was never tried.
64
65
  const covered = this.solvedByTrail || (this.matchedExisting && this.triedFailed.length === 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myrmo",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
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",