myrmo 0.9.0 → 0.10.1
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 +7 -2
- package/dist/client.js +15 -6
- package/dist/format.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/names.d.ts +3 -0
- package/dist/names.js +88 -0
- package/dist/redact.d.ts +6 -0
- package/dist/redact.js +80 -0
- package/package.json +1 -1
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, Validation } 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.10.1";
|
|
6
6
|
export interface ColonyOptions {
|
|
7
7
|
url?: string;
|
|
8
8
|
apiKey?: string;
|
|
@@ -70,10 +70,15 @@ export declare class Colony {
|
|
|
70
70
|
counted: boolean;
|
|
71
71
|
strength: number;
|
|
72
72
|
}>;
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* The trail exactly as it would be sent, with every redaction counted. Sends nothing. `names` lists runs of
|
|
75
|
+
* capitalised words that could be an organisation, customer, person or project: not redacted, to be checked by
|
|
76
|
+
* whoever approves.
|
|
77
|
+
*/
|
|
74
78
|
preview(trail: Trail): {
|
|
75
79
|
trail: Trail;
|
|
76
80
|
redactions: RedactionReport;
|
|
81
|
+
names: string[];
|
|
77
82
|
};
|
|
78
83
|
/**
|
|
79
84
|
* Ask the colony whether it would accept this trail, without publishing it: the schema check publishing
|
package/dist/client.js
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
import { agentIdentity, publishChoice } from "./config.js";
|
|
8
8
|
import { detectEnvironment, parsePackage } from "./environment.js";
|
|
9
9
|
import { fingerprint2, guessErrorType } from "./fingerprint.js";
|
|
10
|
-
import {
|
|
10
|
+
import { namesConflict } from "./names.js";
|
|
11
|
+
import { possibleNames, redactText, redactValue } from "./redact.js";
|
|
11
12
|
/** The public colony. Override with MYRMO_URL or the `url` option. */
|
|
12
13
|
export const DEFAULT_URL = "https://myrmo.dev";
|
|
13
|
-
export const SDK_VERSION = "0.
|
|
14
|
+
export const SDK_VERSION = "0.10.1"; // x-release-please-version
|
|
14
15
|
/** What a colony that is restarting or overloaded answers with. */
|
|
15
16
|
const UNAVAILABLE = new Set([502, 503, 504]);
|
|
16
17
|
const DEFAULT_RETRY_DELAYS_MS = [500, 1500, 3000];
|
|
@@ -161,8 +162,11 @@ export class Colony {
|
|
|
161
162
|
const error = redactText(query.error);
|
|
162
163
|
const errorType = query.errorType ?? guessErrorType(error);
|
|
163
164
|
const fp = fingerprint2(error);
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
// fp2 erases the module path, image or repository an error names: an exact hit about another one is no answer.
|
|
166
|
+
const found = await this.lookup(fp, query.model);
|
|
167
|
+
const kept = found?.hits.filter((hit) => !namesConflict(error, String(hit.trail?.problem?.error_message ?? ""))) ?? [];
|
|
168
|
+
const exact = found && kept.length > 0 ? (kept.length === found.hits.length ? found : { ...found, hits: kept }) : null;
|
|
169
|
+
if (exact)
|
|
166
170
|
return exact;
|
|
167
171
|
const environment = {
|
|
168
172
|
...detectEnvironment(),
|
|
@@ -203,10 +207,15 @@ export class Colony {
|
|
|
203
207
|
this.cache.clear();
|
|
204
208
|
return { trailId: data.trail_id, counted: data.counted, strength: data.strength };
|
|
205
209
|
}
|
|
206
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* The trail exactly as it would be sent, with every redaction counted. Sends nothing. `names` lists runs of
|
|
212
|
+
* capitalised words that could be an organisation, customer, person or project: not redacted, to be checked by
|
|
213
|
+
* whoever approves.
|
|
214
|
+
*/
|
|
207
215
|
preview(trail) {
|
|
208
216
|
const redactions = {};
|
|
209
|
-
|
|
217
|
+
const redacted = redactValue(trail, redactions);
|
|
218
|
+
return { trail: redacted, redactions, names: possibleNames(redacted) };
|
|
210
219
|
}
|
|
211
220
|
/**
|
|
212
221
|
* Ask the colony whether it would accept this trail, without publishing it: the schema check publishing
|
package/dist/format.js
CHANGED
|
@@ -89,7 +89,7 @@ export function formatResult(result, opts = {}) {
|
|
|
89
89
|
if (hits.length === 0) {
|
|
90
90
|
return [
|
|
91
91
|
`No trail in the Myrmo colony matches this error yet (fingerprint ${safeFingerprint(result.fingerprint)}).`,
|
|
92
|
-
`
|
|
92
|
+
`Carry on without it; searching again for the same line will not help. Afterwards, publishing is optional: only if solving it takes ${attemptsPhrase(minFailedAttempts(opts.minFailedAttempts).value)} and you verify the fix (myrmo_publish; the user approves what is sent).`,
|
|
93
93
|
].join("\n");
|
|
94
94
|
}
|
|
95
95
|
return [
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { Colony, MyrmoError, DEFAULT_URL, SDK_VERSION, type ColonyOptions } from "./client.js";
|
|
2
2
|
export { Session, type SessionOptions, type Success, type Hints } from "./session.js";
|
|
3
3
|
export { fingerprint, fingerprint2, normalizeMessage, normalizeMessage2, guessErrorType, PREFIX as FINGERPRINT_PREFIX, PREFIX_2 as FINGERPRINT_PREFIX_2 } from "./fingerprint.js";
|
|
4
|
-
export {
|
|
4
|
+
export { namesConflict, placeholderNames } from "./names.js";
|
|
5
|
+
export { possibleNames, redactText, redactValue, type RedactionReport } from "./redact.js";
|
|
5
6
|
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
7
|
export { detectEnvironment, parsePackage } from "./environment.js";
|
|
7
8
|
export { attemptsPhrase, formatResult, type FormatOptions } from "./format.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { Colony, MyrmoError, DEFAULT_URL, SDK_VERSION } from "./client.js";
|
|
2
2
|
export { Session } from "./session.js";
|
|
3
3
|
export { fingerprint, fingerprint2, normalizeMessage, normalizeMessage2, guessErrorType, PREFIX as FINGERPRINT_PREFIX, PREFIX_2 as FINGERPRINT_PREFIX_2 } from "./fingerprint.js";
|
|
4
|
-
export {
|
|
4
|
+
export { namesConflict, placeholderNames } from "./names.js";
|
|
5
|
+
export { possibleNames, redactText, redactValue } from "./redact.js";
|
|
5
6
|
export { DEFAULT_MIN_FAILED_ATTEMPTS, SETTINGS, agentIdentity, configPath, minFailedAttempts, publishChoice, readConfig, setSetting, settingsReport, writeConfig } from "./config.js";
|
|
6
7
|
export { detectEnvironment, parsePackage } from "./environment.js";
|
|
7
8
|
export { attemptsPhrase, formatResult } from "./format.js";
|
package/dist/names.d.ts
ADDED
package/dist/names.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Names that fp2 erases, and whether two messages name different things.
|
|
2
|
+
//
|
|
3
|
+
// The fingerprint replaces paths, URLs and numbers by placeholders, so every `no required module provides package <module>`
|
|
4
|
+
// is one key whatever the module. An exact lookup can therefore answer with a trail about another package. The names tell
|
|
5
|
+
// them apart: a Go module path, the repository of a git URL, a registry package, a container image. Absolute paths, files and
|
|
6
|
+
// numbers are the machine's and are not names.
|
|
7
|
+
//
|
|
8
|
+
// A port of `placeholder_names` in server/src/relevance.rs; protocol/placeholder_names.v1.vectors.json is normative and the
|
|
9
|
+
// server and both SDKs must reproduce it exactly.
|
|
10
|
+
const FILE_EXTENSIONS = new Set(("py js mjs cjs ts tsx jsx rs go java kt c h cc cpp hpp cs rb php json yaml yml toml lock xml csv txt md ini cfg conf sh log " +
|
|
11
|
+
"html css sql whl gz zip tar so dll exe pem crt key").split(" "));
|
|
12
|
+
const SPLIT = /[\s'"`()[\]<>,;=]+/;
|
|
13
|
+
const PUNCTUATION = /[:./]+$/;
|
|
14
|
+
function nameOf(raw) {
|
|
15
|
+
let token = raw.toLowerCase();
|
|
16
|
+
const scheme = token.indexOf("://");
|
|
17
|
+
if (scheme !== -1)
|
|
18
|
+
token = token.slice(scheme + 3);
|
|
19
|
+
const cut = token.search(/[?#]/);
|
|
20
|
+
if (cut !== -1)
|
|
21
|
+
token = token.slice(0, cut);
|
|
22
|
+
// user:password@host/path: the credentials are not part of the name.
|
|
23
|
+
let at = token.indexOf("@");
|
|
24
|
+
if (at !== -1) {
|
|
25
|
+
const head = token.slice(0, at);
|
|
26
|
+
const tail = token.slice(at + 1);
|
|
27
|
+
if (head && !head.includes("/") && tail.includes("/"))
|
|
28
|
+
token = tail;
|
|
29
|
+
}
|
|
30
|
+
// name@1.2.3: the version is not part of the name.
|
|
31
|
+
at = token.lastIndexOf("@");
|
|
32
|
+
if (at !== -1) {
|
|
33
|
+
const head = token.slice(0, at);
|
|
34
|
+
const tail = token.slice(at + 1);
|
|
35
|
+
if (head.includes("/") && !tail.includes("/"))
|
|
36
|
+
token = head;
|
|
37
|
+
}
|
|
38
|
+
token = token.replace(PUNCTUATION, "");
|
|
39
|
+
if (token.endsWith(".git"))
|
|
40
|
+
token = token.slice(0, -4).replace(PUNCTUATION, "");
|
|
41
|
+
// An image tag, or a line and column (`main.rs:5:3`): a colon after the last slash ends the name.
|
|
42
|
+
const slash = token.lastIndexOf("/");
|
|
43
|
+
if (slash !== -1) {
|
|
44
|
+
const colon = token.indexOf(":", slash);
|
|
45
|
+
if (colon !== -1)
|
|
46
|
+
token = token.slice(0, colon);
|
|
47
|
+
}
|
|
48
|
+
if (!token.includes("/") || !/^[a-z0-9]/.test(token))
|
|
49
|
+
return null;
|
|
50
|
+
const segments = token.split("/");
|
|
51
|
+
if (segments.length > 8 || segments.some((segment) => segment === ""))
|
|
52
|
+
return null;
|
|
53
|
+
const last = segments[segments.length - 1];
|
|
54
|
+
if (last.includes(".") && FILE_EXTENSIONS.has(last.slice(last.lastIndexOf(".") + 1)))
|
|
55
|
+
return null;
|
|
56
|
+
const host = segments[0].split(":")[0];
|
|
57
|
+
const tld = host.includes(".") ? host.slice(host.lastIndexOf(".") + 1) : "";
|
|
58
|
+
const hostLike = host === "localhost" || (tld.length >= 2 && /^[a-z]+$/.test(tld));
|
|
59
|
+
const pair = segments.length === 2 && !segments[0].includes(".");
|
|
60
|
+
return (hostLike || pair) && /[a-z]/.test(token) ? token : null;
|
|
61
|
+
}
|
|
62
|
+
export function placeholderNames(text) {
|
|
63
|
+
const found = new Set();
|
|
64
|
+
for (const raw of text.split(SPLIT)) {
|
|
65
|
+
if (!raw)
|
|
66
|
+
continue;
|
|
67
|
+
const name = nameOf(raw);
|
|
68
|
+
if (name)
|
|
69
|
+
found.add(name);
|
|
70
|
+
}
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
function shareAName(a, b) {
|
|
74
|
+
const lastOf = (s) => s.slice(s.lastIndexOf("/") + 1);
|
|
75
|
+
for (const x of a)
|
|
76
|
+
for (const y of b)
|
|
77
|
+
if (x === y || x.startsWith(`${y}/`) || y.startsWith(`${x}/`) || lastOf(x) === lastOf(y))
|
|
78
|
+
return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
/** True when the query and the message both spell such names and share none: another package, so another error. */
|
|
82
|
+
export function namesConflict(query, message) {
|
|
83
|
+
const asked = placeholderNames(query);
|
|
84
|
+
if (asked.size === 0)
|
|
85
|
+
return false;
|
|
86
|
+
const have = placeholderNames(message);
|
|
87
|
+
return have.size > 0 && !shareAName(asked, have);
|
|
88
|
+
}
|
package/dist/redact.d.ts
CHANGED
|
@@ -2,3 +2,9 @@ export type RedactionReport = Record<string, number>;
|
|
|
2
2
|
export declare function redactText(input: string, report?: RedactionReport): string;
|
|
3
3
|
/** Deep copy of `value` with every string redacted. */
|
|
4
4
|
export declare function redactValue<T>(value: T, report?: RedactionReport): T;
|
|
5
|
+
/**
|
|
6
|
+
* Runs of capitalised words that look like a name (`Acme Data Systems`), for a person to check before approving a
|
|
7
|
+
* publication. This does not redact anything: a name looks like any other word, so the colony cannot remove one by
|
|
8
|
+
* itself. A run at the start of a sentence loses its first word, which is capitalised anyway.
|
|
9
|
+
*/
|
|
10
|
+
export declare function possibleNames(value: unknown, limit?: number): string[];
|
package/dist/redact.js
CHANGED
|
@@ -55,6 +55,42 @@ const card = ([m]) => {
|
|
|
55
55
|
};
|
|
56
56
|
/** `name = value` becomes `name = <redacted:kind>` unless the value is a reference. */
|
|
57
57
|
const keepPrefix = (kind, valueGroup) => (m) => isReference(m[valueGroup]) ? null : m.slice(1, valueGroup).join("") + full(kind);
|
|
58
|
+
// Configuration values that name an organisation or a person: `edc.ui.organization=Acme`, `"owner": "Ana"`,
|
|
59
|
+
// `LABEL maintainer=...`. The key decides, never the value, because a name looks like any other word. A key
|
|
60
|
+
// matches only when its last part is one of these words, so `org.eclipse.edc:dcp-core` (a Maven coordinate) and
|
|
61
|
+
// `--org-id` are left alone. A product or connector `*.title` is included: it is usually the owner's brand.
|
|
62
|
+
const ORG_KEY = String.raw `(?:(?:[A-Za-z0-9_.\-]{0,64}[._\-])?(?:(?:organi[sz]ations?|org|compan(?:y|ies)|tenants?|customers?|owners?|authors?|` +
|
|
63
|
+
String.raw `contacts?|maintainers?|publishers?|vendors?|employers?)(?:[_.\-]?(?:name|title))?|` +
|
|
64
|
+
String.raw `(?:client|display|full|legal|trade|business|brand)[_.\-]?name)|` +
|
|
65
|
+
String.raw `(?:[A-Za-z0-9_.\-]{0,64}[._\-])?(?:connector|product|portal|brand|app|ui|site)[._\-](?:[A-Za-z0-9_.\-]{0,64}[._\-])?title)`;
|
|
66
|
+
/** Values that are not a name: placeholders, types and generic words. */
|
|
67
|
+
const ORG_KEEP = new Set([
|
|
68
|
+
"default", "unknown", "example", "test", "testing", "user", "users", "admin", "administrator", "root", "me",
|
|
69
|
+
"self", "system", "anonymous", "n/a", "na", "tbd", "todo", "unset", "any", "object", "dict", "list", "set",
|
|
70
|
+
"array", "map", "number", "float", "date", "datetime", "name", "author", "owner", "org", "organization",
|
|
71
|
+
"company", "customer", "tenant",
|
|
72
|
+
]);
|
|
73
|
+
/** A type annotation such as `Optional[str]`, not a name. */
|
|
74
|
+
const ORG_TYPE = /^[A-Za-z_][A-Za-z0-9_.]*\[[A-Za-z0-9_., \[\]]*\]$/;
|
|
75
|
+
/**
|
|
76
|
+
* The key of a rule anchored to the start of a line cannot begin with `-`, or the `-Dkey=value` of a command would
|
|
77
|
+
* take the rest of the command with it.
|
|
78
|
+
*/
|
|
79
|
+
const ORG_KEY_LINE = ORG_KEY.replace(String.raw `[A-Za-z0-9_.\-]{0,64}[._\-]`, String.raw `[A-Za-z0-9_][A-Za-z0-9_.\-]{0,63}[._\-]`);
|
|
80
|
+
function orgKept(value, inline) {
|
|
81
|
+
const lower = value.toLowerCase();
|
|
82
|
+
return (isReference(value) ||
|
|
83
|
+
value.startsWith("<") ||
|
|
84
|
+
!/\p{L}/u.test(value) ||
|
|
85
|
+
ORG_TYPE.test(value) ||
|
|
86
|
+
ORG_KEEP.has(lower) ||
|
|
87
|
+
["your", "example", "sample", "my-", "my_"].some((p) => lower.startsWith(p)) ||
|
|
88
|
+
(inline && [...value].some((c) => "${}[]()*\\".includes(c))));
|
|
89
|
+
}
|
|
90
|
+
/** Replaces the value group with `<redacted:org>`, keeping every group before and after it. */
|
|
91
|
+
const orgValue = (valueGroup, inline) => (m) => orgKept(m[valueGroup], inline)
|
|
92
|
+
? null
|
|
93
|
+
: m.slice(1).map((group, i) => (i + 1 === valueGroup ? full("org") : group)).join("");
|
|
58
94
|
const SCHEMES = ["bearer", "basic", "token", "digest", "negotiate", "ntlm", "hawk"];
|
|
59
95
|
const authorization = (m) => {
|
|
60
96
|
const [, prefix, scheme, value] = m;
|
|
@@ -126,6 +162,10 @@ const RULES = [
|
|
|
126
162
|
r("card", String.raw `\b[3-6](?:[ \-]?[0-9]){12,18}\b`, "", card),
|
|
127
163
|
r("home_path", String.raw `(\/home\/|\/Users\/)[^/\s'\x22<>]+`, "", (m) => `${m[1]}<user>`),
|
|
128
164
|
r("home_path", String.raw `([a-z]:\\Users\\)[^\\\s'\x22<>]+`, "i", (m) => `${m[1]}<user>`),
|
|
165
|
+
// The org rules come last: they match on the key, so every more specific kind has already had its turn.
|
|
166
|
+
r("org", String.raw `(^|[^A-Za-z0-9_.\-])(` + ORG_KEY + String.raw `['\x22]?[ \t]*[=:][ \t]*)(['\x22])([^'\x22\r\n]{2,120})(['\x22])`, "i", orgValue(4, false)),
|
|
167
|
+
r("org", String.raw `(^[ \t]*(?:-[ \t]+)?` + ORG_KEY_LINE + String.raw `['\x22]?[ \t]*[=:][ \t]*)([^\s'\x22<>][^\r\n]*?)([ \t]*\r?$)`, "im", orgValue(2, false)),
|
|
168
|
+
r("org", String.raw `(^|[^A-Za-z0-9_.\-])(` + ORG_KEY + String.raw `['\x22]?[ \t]*[=:][ \t]*)([^\s'\x22<>,;]{2,80})`, "i", orgValue(3, true)),
|
|
129
169
|
];
|
|
130
170
|
export function redactText(input, report = {}) {
|
|
131
171
|
let text = input;
|
|
@@ -160,3 +200,43 @@ export function redactValue(value, report = {}) {
|
|
|
160
200
|
}
|
|
161
201
|
return value;
|
|
162
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Runs of capitalised words that look like a name (`Acme Data Systems`), for a person to check before approving a
|
|
205
|
+
* publication. This does not redact anything: a name looks like any other word, so the colony cannot remove one by
|
|
206
|
+
* itself. A run at the start of a sentence loses its first word, which is capitalised anyway.
|
|
207
|
+
*/
|
|
208
|
+
export function possibleNames(value, limit = 10) {
|
|
209
|
+
const found = new Set();
|
|
210
|
+
const capitalised = (w) => w.length >= 2 && w[0] !== w[0].toLowerCase();
|
|
211
|
+
const scan = (text) => {
|
|
212
|
+
let run = [];
|
|
213
|
+
const flush = () => {
|
|
214
|
+
if (run.length && /(?:^|[.!?:]\s+|\n\s*)$/.test(text.slice(0, run[0].index)))
|
|
215
|
+
run = run.slice(1);
|
|
216
|
+
if (run.length >= 2)
|
|
217
|
+
found.add(text.slice(run[0].index, run[run.length - 1].end));
|
|
218
|
+
run = [];
|
|
219
|
+
};
|
|
220
|
+
for (const m of text.matchAll(/\p{L}+/gu)) {
|
|
221
|
+
const word = { index: m.index ?? 0, end: (m.index ?? 0) + m[0].length };
|
|
222
|
+
if (!capitalised(m[0])) {
|
|
223
|
+
flush();
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (run.length && !(text[run[run.length - 1].end] === " " && run[run.length - 1].end + 1 === word.index))
|
|
227
|
+
flush();
|
|
228
|
+
run.push(word);
|
|
229
|
+
}
|
|
230
|
+
flush();
|
|
231
|
+
};
|
|
232
|
+
const walk = (v) => {
|
|
233
|
+
if (typeof v === "string")
|
|
234
|
+
scan(v);
|
|
235
|
+
else if (Array.isArray(v))
|
|
236
|
+
v.forEach(walk);
|
|
237
|
+
else if (v && typeof v === "object")
|
|
238
|
+
Object.values(v).forEach(walk);
|
|
239
|
+
};
|
|
240
|
+
walk(value);
|
|
241
|
+
return [...found].slice(0, limit);
|
|
242
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myrmo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
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",
|