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 +1 -1
- package/dist/client.d.ts +10 -4
- package/dist/client.js +16 -9
- package/dist/config.d.ts +75 -2
- package/dist/config.js +130 -5
- package/dist/format.d.ts +4 -0
- package/dist/format.js +7 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -1
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
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)
|
|
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.
|
|
5
|
+
export declare const SDK_VERSION = "0.5.0";
|
|
6
6
|
export interface ColonyOptions {
|
|
7
7
|
url?: string;
|
|
8
8
|
apiKey?: string;
|
|
9
|
-
/**
|
|
10
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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": "
|
|
4
|
+
// ~/.myrmo/config.json { "publish": "ask", "min_failed_attempts": 1, "hook": "on", "agent_id": "3f9c..." }
|
|
5
5
|
//
|
|
6
|
-
//
|
|
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
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
"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",
|