myrmo 0.3.0 → 0.4.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.4.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.4.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,6 +1,7 @@
1
1
  import type { PublishMode } from "./types.js";
2
2
  export interface MyrmoConfig {
3
3
  publish?: PublishMode;
4
+ agent_id?: string;
4
5
  }
5
6
  /** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
6
7
  export declare function configPath(): string;
@@ -14,3 +15,13 @@ export declare function publishChoice(explicit?: PublishMode): {
14
15
  mode: PublishMode;
15
16
  source: "option" | "env" | "file" | "default";
16
17
  };
18
+ export type AgentIdSource = "option" | "env" | "file" | "generated" | "none";
19
+ /**
20
+ * Who this client says it is. Nothing to configure: the first run creates a random id and keeps it in
21
+ * the config file. `MYRMO_AGENT_ID` (or the `agentId` option) overrides it, `false` or
22
+ * `MYRMO_ANONYMOUS=1` sends none. If the file cannot be written the id lives for this process only.
23
+ */
24
+ export declare function agentIdentity(explicit?: string | false): {
25
+ id?: string;
26
+ source: AgentIdSource;
27
+ };
package/dist/config.js CHANGED
@@ -1,13 +1,18 @@
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": "auto", "agent_id": "3f9c0a7e2b1d4c68a5e0b7d91c2f4a86" }
5
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.
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.
8
+ // `agent_id` is a random pseudonym the client creates by itself the first time it runs, so that
9
+ // reports from different machines behind one address are not mistaken for a single agent. It holds
10
+ // no personal data and can be deleted at any time.
11
+ import { randomUUID } from "node:crypto";
8
12
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
13
  import { homedir } from "node:os";
10
14
  import { dirname, join } from "node:path";
15
+ const AGENT_ID = /^[A-Za-z0-9_-]{8,64}$/;
11
16
  const MODES = ["off", "ask", "auto"];
12
17
  /** `MYRMO_CONFIG` if set, else `~/.myrmo/config.json`. */
13
18
  export function configPath() {
@@ -16,8 +21,12 @@ export function configPath() {
16
21
  export function readConfig() {
17
22
  try {
18
23
  const raw = JSON.parse(readFileSync(configPath(), "utf8"));
19
- const publish = MODES.includes(raw.publish) ? raw.publish : undefined;
20
- return publish ? { publish } : {};
24
+ const config = {};
25
+ if (MODES.includes(raw.publish))
26
+ config.publish = raw.publish;
27
+ if (typeof raw.agent_id === "string" && AGENT_ID.test(raw.agent_id))
28
+ config.agent_id = raw.agent_id;
29
+ return config;
21
30
  }
22
31
  catch {
23
32
  // Missing or unreadable: no choice has been made.
@@ -45,3 +54,30 @@ export function publishChoice(explicit) {
45
54
  return { mode: fromFile, source: "file" };
46
55
  return { mode: "off", source: "default" };
47
56
  }
57
+ /**
58
+ * Who this client says it is. Nothing to configure: the first run creates a random id and keeps it in
59
+ * the config file. `MYRMO_AGENT_ID` (or the `agentId` option) overrides it, `false` or
60
+ * `MYRMO_ANONYMOUS=1` sends none. If the file cannot be written the id lives for this process only.
61
+ */
62
+ export function agentIdentity(explicit) {
63
+ if (explicit === false)
64
+ return { source: "none" };
65
+ if (explicit)
66
+ return { id: explicit, source: "option" };
67
+ const fromEnv = process.env.MYRMO_AGENT_ID?.trim();
68
+ if (fromEnv)
69
+ return { id: fromEnv, source: "env" };
70
+ if (["1", "true"].includes(process.env.MYRMO_ANONYMOUS?.trim().toLowerCase() ?? ""))
71
+ return { source: "none" };
72
+ const stored = readConfig().agent_id;
73
+ if (stored)
74
+ return { id: stored, source: "file" };
75
+ const id = randomUUID().replace(/-/g, "");
76
+ try {
77
+ writeConfig({ agent_id: id });
78
+ }
79
+ catch {
80
+ // A read-only home directory: the id then lasts as long as this process.
81
+ }
82
+ return { id, source: "generated" };
83
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +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
+ export { agentIdentity, configPath, publishChoice, readConfig, writeConfig, type AgentIdSource, type MyrmoConfig } from "./config.js";
6
6
  export { detectEnvironment, parsePackage } from "./environment.js";
7
7
  export { formatResult, type FormatOptions } from "./format.js";
8
8
  export type * from "./types.js";
package/dist/index.js CHANGED
@@ -2,7 +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
+ export { agentIdentity, configPath, publishChoice, readConfig, writeConfig } from "./config.js";
6
6
  export { detectEnvironment, parsePackage } from "./environment.js";
7
7
  export { formatResult } from "./format.js";
8
8
  import { Colony } from "./client.js";
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.4.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",