myrmo 0.3.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/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 (or ~/.myrmo/config.json), 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.3.0";
5
+ export declare const SDK_VERSION = "0.5.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. */
@@ -28,6 +33,7 @@ export declare class Colony {
28
33
  readonly publishSource: "option" | "env" | "file" | "default";
29
34
  private readonly apiKey?;
30
35
  private readonly agentId?;
36
+ private readonly model?;
31
37
  private readonly timeoutMs;
32
38
  private readonly headers;
33
39
  private readonly cacheTtlMs;
@@ -37,7 +43,7 @@ export declare class Colony {
37
43
  private cached;
38
44
  private remember;
39
45
  /** Trails for a fingerprint, or `null` when the colony has none. */
40
- lookup(fp: string): Promise<SearchResult | null>;
46
+ lookup(fp: string, model?: string): Promise<SearchResult | null>;
41
47
  /** Find trails for an error: fingerprint first, semantic search when there is no exact match. */
42
48
  search(query: SearchQuery): Promise<SearchResult>;
43
49
  /** Tell the colony whether a trail worked. Failures matter as much as successes. */
package/dist/client.js CHANGED
@@ -4,13 +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
+ import { agentIdentity, publishChoice } from "./config.js";
8
8
  import { detectEnvironment, parsePackage } from "./environment.js";
9
9
  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.3.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;
@@ -39,6 +39,7 @@ export class Colony {
39
39
  publishSource;
40
40
  apiKey;
41
41
  agentId;
42
+ model;
42
43
  timeoutMs;
43
44
  headers;
44
45
  cacheTtlMs;
@@ -46,7 +47,9 @@ export class Colony {
46
47
  constructor(options = {}) {
47
48
  this.url = (options.url ?? env("MYRMO_URL") ?? DEFAULT_URL).replace(/\/+$/, "");
48
49
  this.apiKey = options.apiKey ?? env("MYRMO_API_KEY");
49
- this.agentId = options.agentId ?? env("MYRMO_AGENT_ID");
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;
50
53
  const choice = publishChoice(options.publish);
51
54
  this.publishMode = choice.mode;
52
55
  this.publishSource = choice.source;
@@ -54,14 +57,18 @@ export class Colony {
54
57
  this.headers = options.headers ?? {};
55
58
  this.cacheTtlMs = options.cacheTtlMs ?? 60_000;
56
59
  }
57
- async request(method, path, body) {
60
+ async request(method, path, body, extra) {
58
61
  const headers = { accept: "application/json", "user-agent": `myrmo-js/${SDK_VERSION}`, ...this.headers };
59
62
  if (body !== undefined)
60
63
  headers["content-type"] = "application/json";
61
64
  if (this.apiKey)
62
65
  headers.authorization = `Bearer ${this.apiKey}`;
63
- 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"))
64
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);
65
72
  const res = await fetch(this.url + path, {
66
73
  method,
67
74
  headers,
@@ -93,11 +100,11 @@ export class Colony {
93
100
  this.cache.set(key, { at: Date.now(), value });
94
101
  }
95
102
  /** Trails for a fingerprint, or `null` when the colony has none. */
96
- async lookup(fp) {
103
+ async lookup(fp, model) {
97
104
  const hit = this.cached(fp);
98
105
  if (hit !== undefined)
99
106
  return hit;
100
- 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);
101
108
  const value = status === 404 ? null : { fingerprint: data.fingerprint, hits: data.results.map(toHit), notice: data.notice, source: "fingerprint" };
102
109
  this.remember(fp, value);
103
110
  return value;
@@ -107,7 +114,7 @@ export class Colony {
107
114
  const error = redactText(query.error);
108
115
  const errorType = query.errorType ?? guessErrorType(error);
109
116
  const fp = fingerprint(query.runtime ?? "", errorType, error);
110
- const exact = await this.lookup(fp);
117
+ const exact = await this.lookup(fp, query.model);
111
118
  if (exact && exact.hits.length > 0)
112
119
  return exact;
113
120
  const environment = {
@@ -127,7 +134,7 @@ export class Colony {
127
134
  environment,
128
135
  limit: query.limit ?? 3,
129
136
  min_strength: query.minStrength ?? 0,
130
- });
137
+ }, query.model ? { "x-myrmo-model": query.model } : undefined);
131
138
  const value = { fingerprint: data.fingerprint, hits: data.results.map(toHit), notice: data.notice, source: "search" };
132
139
  this.remember(key, value);
133
140
  return value;
package/dist/config.d.ts CHANGED
@@ -1,16 +1,89 @@
1
1
  import type { PublishMode } from "./types.js";
2
+ export type HookMode = "on" | "off";
2
3
  export interface MyrmoConfig {
3
4
  publish?: PublishMode;
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;
4
12
  }
13
+ export declare const DEFAULT_MIN_FAILED_ATTEMPTS = 1;
5
14
  /** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
6
15
  export declare function configPath(): string;
7
16
  export declare function readConfig(): MyrmoConfig;
8
- 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;
9
21
  /**
10
22
  * 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.
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).
12
25
  */
13
26
  export declare function publishChoice(explicit?: PublishMode): {
14
27
  mode: PublishMode;
15
28
  source: "option" | "env" | "file" | "default";
16
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
+ };
80
+ export type AgentIdSource = "option" | "env" | "file" | "generated" | "none";
81
+ /**
82
+ * Who this client says it is. Nothing to configure: the first run creates a random id and keeps it in
83
+ * the config file. `MYRMO_AGENT_ID` (or the `agentId` option) overrides it, `false` or
84
+ * `MYRMO_ANONYMOUS=1` sends none. If the file cannot be written the id lives for this process only.
85
+ */
86
+ export declare function agentIdentity(explicit?: string | false): {
87
+ id?: string;
88
+ source: AgentIdSource;
89
+ };
package/dist/config.js CHANGED
@@ -1,13 +1,23 @@
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" }
4
+ // ~/.myrmo/config.json { "publish": "ask", "min_failed_attempts": 1, "hook": "on", "agent_id": "3f9c..." }
5
5
  //
6
- // The file records choices only a person should make (whether agents may publish for them),
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),
7
8
  // so it is written by `myrmo-mcp config` or by the user's answer to a prompt, never by a model.
9
+ // `agent_id` is a random pseudonym the client creates by itself the first time it runs, so that
10
+ // reports from different machines behind one address are not mistaken for a single agent. It holds
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.
14
+ import { randomUUID } from "node:crypto";
8
15
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
16
  import { homedir } from "node:os";
10
17
  import { dirname, join } from "node:path";
18
+ export const DEFAULT_MIN_FAILED_ATTEMPTS = 1;
19
+ const MAX_MIN_FAILED_ATTEMPTS = 20;
20
+ const AGENT_ID = /^[A-Za-z0-9_-]{8,64}$/;
11
21
  const MODES = ["off", "ask", "auto"];
12
22
  /** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
13
23
  export function configPath() {
@@ -16,14 +26,30 @@ export function configPath() {
16
26
  export function readConfig() {
17
27
  try {
18
28
  const raw = JSON.parse(readFileSync(configPath(), "utf8"));
19
- const publish = MODES.includes(raw.publish) ? raw.publish : undefined;
20
- return publish ? { publish } : {};
29
+ const config = {};
30
+ if (MODES.includes(raw.publish))
31
+ config.publish = raw.publish;
32
+ if (typeof raw.agent_id === "string" && AGENT_ID.test(raw.agent_id))
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;
41
+ return config;
21
42
  }
22
43
  catch {
23
44
  // Missing or unreadable: no choice has been made.
24
45
  return {};
25
46
  }
26
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. */
27
53
  export function writeConfig(patch) {
28
54
  const path = configPath();
29
55
  mkdirSync(dirname(path), { recursive: true });
@@ -31,7 +57,8 @@ export function writeConfig(patch) {
31
57
  }
32
58
  /**
33
59
  * 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.
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).
35
62
  */
36
63
  export function publishChoice(explicit) {
37
64
  const valid = (v) => MODES.includes(v);
@@ -45,3 +72,101 @@ export function publishChoice(explicit) {
45
72
  return { mode: fromFile, source: "file" };
46
73
  return { mode: "off", source: "default" };
47
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
+ }
144
+ /**
145
+ * Who this client says it is. Nothing to configure: the first run creates a random id and keeps it in
146
+ * the config file. `MYRMO_AGENT_ID` (or the `agentId` option) overrides it, `false` or
147
+ * `MYRMO_ANONYMOUS=1` sends none. If the file cannot be written the id lives for this process only.
148
+ */
149
+ export function agentIdentity(explicit) {
150
+ if (explicit === false)
151
+ return { source: "none" };
152
+ if (explicit)
153
+ return { id: explicit, source: "option" };
154
+ const fromEnv = process.env.MYRMO_AGENT_ID?.trim();
155
+ if (fromEnv)
156
+ return { id: fromEnv, source: "env" };
157
+ if (["1", "true"].includes(process.env.MYRMO_ANONYMOUS?.trim().toLowerCase() ?? ""))
158
+ return { source: "none" };
159
+ if (readConfig().anonymous === true)
160
+ return { source: "none" };
161
+ const stored = readConfig().agent_id;
162
+ if (stored)
163
+ return { id: stored, source: "file" };
164
+ const id = randomUUID().replace(/-/g, "");
165
+ try {
166
+ writeConfig({ agent_id: id });
167
+ }
168
+ catch {
169
+ // A read-only home directory: the id then lasts as long as this process.
170
+ }
171
+ return { id, source: "generated" };
172
+ }
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 { configPath, publishChoice, readConfig, writeConfig, 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 { 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/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,6 +1,6 @@
1
1
  {
2
2
  "name": "myrmo",
3
- "version": "0.3.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",