@gethmy/mcp 2.8.3 → 2.8.5
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/cli.js +481 -36
- package/dist/index.js +481 -36
- package/dist/lib/api-client.js +17 -4
- package/package.json +1 -1
- package/src/api-client.ts +8 -0
- package/src/auto-session.ts +19 -9
- package/src/graph-expansion.ts +153 -0
- package/src/memory-park.ts +183 -4
- package/src/memory-provenance.ts +177 -0
- package/src/memory-tags.ts +88 -0
- package/src/server.ts +379 -43
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-time provenance + signal-derived confidence/importance.
|
|
3
|
+
*
|
|
4
|
+
* Card #273: "Set meaningful confidence, importance & provenance at write
|
|
5
|
+
* time". Three deterministic, rule-based concerns live here (NO LLM rating —
|
|
6
|
+
* LLM-rated importance was reverted in 43782a4 for poisoning the corpus and
|
|
7
|
+
* stays deferred):
|
|
8
|
+
*
|
|
9
|
+
* 1. `MemoryOrigin` — provenance stamped into `metadata.origin`. There is no
|
|
10
|
+
* dedicated provenance column; it rides inside the existing metadata jsonb.
|
|
11
|
+
* 2. `defaultConfidenceForSource` — source/trust-derived confidence default
|
|
12
|
+
* used only when the caller did NOT pass an explicit confidence.
|
|
13
|
+
* 3. `importanceWithSignalBump` — per-type importance default plus a bounded
|
|
14
|
+
* bump for signal density (file paths, "Why"/"How to apply" sections,
|
|
15
|
+
* proper nouns). Pairs with the Park rescore so ranking differentiates.
|
|
16
|
+
*
|
|
17
|
+
* Pure and side-effect free — safe to unit test without network/DB.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Provenance shape (task 1)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** Where a memory came from. */
|
|
25
|
+
export type MemorySource = "manual" | "assistant" | "agent-run" | "import";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Provenance record stored at `metadata.origin`. Input for provenance badges
|
|
29
|
+
* (companion UI card) and auto-vs-curated hygiene filtering. All fields except
|
|
30
|
+
* `source` are optional so partial context still round-trips.
|
|
31
|
+
*/
|
|
32
|
+
export interface MemoryOrigin {
|
|
33
|
+
source: MemorySource;
|
|
34
|
+
/** Card the write originated from (uuid or short id as string). */
|
|
35
|
+
source_card_id?: string;
|
|
36
|
+
/** Agent/working-memory session that produced the write. */
|
|
37
|
+
source_session_id?: string;
|
|
38
|
+
/** Human or agent identifier that authored the memory. */
|
|
39
|
+
author?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Trust hint carried through from the write call (e.g. 'document',
|
|
42
|
+
* 'manual', 'agent'). Persisted so downstream hygiene can reason about it —
|
|
43
|
+
* previously `source_trust` only reached the Floor and was dropped.
|
|
44
|
+
*/
|
|
45
|
+
source_trust?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build a `MemoryOrigin`, dropping undefined fields so the stored jsonb stays
|
|
50
|
+
* compact. `source` is always present.
|
|
51
|
+
*/
|
|
52
|
+
export function buildOrigin(input: {
|
|
53
|
+
source: MemorySource;
|
|
54
|
+
source_card_id?: string | null;
|
|
55
|
+
source_session_id?: string | null;
|
|
56
|
+
author?: string | null;
|
|
57
|
+
source_trust?: string | null;
|
|
58
|
+
}): MemoryOrigin {
|
|
59
|
+
const origin: MemoryOrigin = { source: input.source };
|
|
60
|
+
if (input.source_card_id) origin.source_card_id = input.source_card_id;
|
|
61
|
+
if (input.source_session_id)
|
|
62
|
+
origin.source_session_id = input.source_session_id;
|
|
63
|
+
if (input.author) origin.author = input.author;
|
|
64
|
+
if (input.source_trust) origin.source_trust = input.source_trust;
|
|
65
|
+
return origin;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Source-derived confidence default (task 5)
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Default confidence by source/trust, applied ONLY when the caller omits an
|
|
74
|
+
* explicit confidence. An explicit value always wins (handled by the caller).
|
|
75
|
+
*
|
|
76
|
+
* Rule (deterministic, documented):
|
|
77
|
+
* - manual capture (human at the keyboard): high — 0.9
|
|
78
|
+
* - `source_trust === 'document'` import: high — 0.9 (curated long-form)
|
|
79
|
+
* - assistant-authored (board assistant on the user's behalf): 0.8
|
|
80
|
+
* - agent-run auto-extract / low-trust import: moderate — 0.6
|
|
81
|
+
*
|
|
82
|
+
* The point is non-uniformity: most live entities sat at 1.0, which made
|
|
83
|
+
* `minConfidence` filtering inert. Auto-written memories should start lower so
|
|
84
|
+
* a curated pattern outranks a freshly auto-extracted episode at equal
|
|
85
|
+
* relevance/recency.
|
|
86
|
+
*/
|
|
87
|
+
export function defaultConfidenceForSource(input: {
|
|
88
|
+
source: MemorySource;
|
|
89
|
+
source_trust?: string;
|
|
90
|
+
}): number {
|
|
91
|
+
if (input.source_trust === "document") return 0.9;
|
|
92
|
+
if (input.source_trust === "manual") return 0.9;
|
|
93
|
+
switch (input.source) {
|
|
94
|
+
case "manual":
|
|
95
|
+
return 0.9;
|
|
96
|
+
case "assistant":
|
|
97
|
+
return 0.8;
|
|
98
|
+
case "agent-run":
|
|
99
|
+
return 0.6;
|
|
100
|
+
case "import":
|
|
101
|
+
return 0.6;
|
|
102
|
+
default:
|
|
103
|
+
return 0.7;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Importance signal-density bump (task 6)
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Per-type importance default. Mirrors the edge function `IMPORTANCE_DEFAULTS`
|
|
113
|
+
* and `memory-park.ts` `TYPE_IMPORTANCE_DEFAULT`; duplicated here so the write
|
|
114
|
+
* path doesn't import the rescorer. Values in [1,10].
|
|
115
|
+
*/
|
|
116
|
+
export const TYPE_IMPORTANCE_DEFAULT: Record<string, number> = {
|
|
117
|
+
preference: 9,
|
|
118
|
+
lesson: 8,
|
|
119
|
+
decision: 8,
|
|
120
|
+
pattern: 7,
|
|
121
|
+
solution: 7,
|
|
122
|
+
procedure: 7,
|
|
123
|
+
error: 5,
|
|
124
|
+
context: 5,
|
|
125
|
+
task: 5,
|
|
126
|
+
agent: 5,
|
|
127
|
+
relationship: 6,
|
|
128
|
+
commitment: 7,
|
|
129
|
+
project: 6,
|
|
130
|
+
handoff: 6,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// A file path / module path, e.g. `src/foo/bar.ts`, `packages/x/y`.
|
|
134
|
+
const FILE_PATH =
|
|
135
|
+
/(?:[\w.-]+\/){1,}[\w.-]+\.[a-z]{1,4}\b|(?:[\w-]+\/){2,}[\w-]+/i;
|
|
136
|
+
// A "Why" or "How to apply" style guidance heading.
|
|
137
|
+
const GUIDANCE_SECTION =
|
|
138
|
+
/\b(why|how to apply|how to use|takeaway|root cause)\b/i;
|
|
139
|
+
// A proper noun / identifier in the body (PascalCase, camelCase, snake_case,
|
|
140
|
+
// or a backtick-fenced symbol).
|
|
141
|
+
const PROPER_NOUN =
|
|
142
|
+
/`[^`]+`|\b[A-Z][a-z0-9]+[A-Z][A-Za-z0-9]*\b|\b[a-z]+_[a-z][\w]*\b/;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Count distinct signal-density cues present in the content. Bounded [0,3].
|
|
146
|
+
* +1 — contains a file/module path
|
|
147
|
+
* +1 — contains a "Why"/"How to apply"/"Takeaway"/"Root cause" section
|
|
148
|
+
* +1 — contains a proper noun / code identifier
|
|
149
|
+
*/
|
|
150
|
+
export function signalDensity(content: string): number {
|
|
151
|
+
let n = 0;
|
|
152
|
+
if (FILE_PATH.test(content)) n += 1;
|
|
153
|
+
if (GUIDANCE_SECTION.test(content)) n += 1;
|
|
154
|
+
if (PROPER_NOUN.test(content)) n += 1;
|
|
155
|
+
return n;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Importance = per-type default + bounded signal-density bump, clamped to
|
|
160
|
+
* [1,10]. Deterministic and rule-based.
|
|
161
|
+
*
|
|
162
|
+
* The bump is +1 per cue, capped at +2 total, so a dense, file-path-bearing
|
|
163
|
+
* lesson edges out a thin one of the same type — giving the Park rescore a
|
|
164
|
+
* non-flat importance term to work with. A caller-provided importance always
|
|
165
|
+
* takes precedence and never enters this function.
|
|
166
|
+
*/
|
|
167
|
+
export function importanceWithSignalBump(
|
|
168
|
+
type: string,
|
|
169
|
+
content: string,
|
|
170
|
+
): number {
|
|
171
|
+
const base = TYPE_IMPORTANCE_DEFAULT[type] ?? 5;
|
|
172
|
+
const bump = Math.min(2, signalDensity(content));
|
|
173
|
+
const v = base + bump;
|
|
174
|
+
if (v < 1) return 1;
|
|
175
|
+
if (v > 10) return 10;
|
|
176
|
+
return v;
|
|
177
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory tag normalization + lint (card #274).
|
|
3
|
+
*
|
|
4
|
+
* Memory tags were inconsistent and broke tag-based recall: card references
|
|
5
|
+
* appeared as both `card:259` (colon) and `card-175` / `card 229` (dash/space),
|
|
6
|
+
* so `harmony_recall({tags:["card:259"]})` missed the variants.
|
|
7
|
+
*
|
|
8
|
+
* Canonical card-ref form is `card:<n>` (colon) — this aligns with the active
|
|
9
|
+
* programmatic write path: the agent daemon's `episode-writer.ts` already tags
|
|
10
|
+
* episodes with `` `card:${short_id}` ``. We normalize toward that, not away.
|
|
11
|
+
*
|
|
12
|
+
* Rules are intentionally small and deterministic (no LLM, no taxonomy):
|
|
13
|
+
* - trim surrounding whitespace
|
|
14
|
+
* - lowercase
|
|
15
|
+
* - collapse internal whitespace runs to a single space
|
|
16
|
+
* - canonicalize card refs: `card-<n>` / `card <n>` / `card#<n>` → `card:<n>`
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Matches a card ref written with any common separator, e.g. `card-12`,
|
|
20
|
+
* `card 12`, `card#12`, `card:12`. Captures the numeric short id. */
|
|
21
|
+
const CARD_REF = /^card[\s:#-]+(\d+)$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a single tag to its canonical form.
|
|
25
|
+
* Pure: trims, lowercases, collapses internal whitespace, canonicalizes card refs.
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeTag(tag: string): string {
|
|
28
|
+
const cleaned = tag.trim().toLowerCase().replace(/\s+/g, " ");
|
|
29
|
+
const cardMatch = cleaned.match(CARD_REF);
|
|
30
|
+
if (cardMatch) {
|
|
31
|
+
return `card:${cardMatch[1]}`;
|
|
32
|
+
}
|
|
33
|
+
return cleaned;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalize a list of tags: map each through {@link normalizeTag}, drop empties,
|
|
38
|
+
* and dedupe while preserving first-seen order.
|
|
39
|
+
*/
|
|
40
|
+
export function normalizeTags(tags: string[]): string[] {
|
|
41
|
+
const seen = new Set<string>();
|
|
42
|
+
const out: string[] = [];
|
|
43
|
+
for (const raw of tags) {
|
|
44
|
+
const norm = normalizeTag(raw);
|
|
45
|
+
if (!norm || seen.has(norm)) continue;
|
|
46
|
+
seen.add(norm);
|
|
47
|
+
out.push(norm);
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A single advisory tag-lint suggestion. Non-fatal. */
|
|
53
|
+
export interface TagSuggestion {
|
|
54
|
+
/** The original tag as written by the caller. */
|
|
55
|
+
tag: string;
|
|
56
|
+
/** The canonical form we'd prefer. */
|
|
57
|
+
suggestion: string;
|
|
58
|
+
/** Short human-readable reason. */
|
|
59
|
+
reason: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Advisory tag lint (card #274). Returns non-fatal suggestions for tags that
|
|
64
|
+
* differ from their canonical form (case variants, dash/space/hash card refs,
|
|
65
|
+
* surrounding/internal whitespace). NEVER rejects — purely informational.
|
|
66
|
+
*
|
|
67
|
+
* Clean tags (already canonical) produce no suggestions, so a fully-normalized
|
|
68
|
+
* input list returns `[]`.
|
|
69
|
+
*/
|
|
70
|
+
export function lintTags(tags: string[]): TagSuggestion[] {
|
|
71
|
+
const suggestions: TagSuggestion[] = [];
|
|
72
|
+
for (const tag of tags) {
|
|
73
|
+
const norm = normalizeTag(tag);
|
|
74
|
+
if (norm === tag) continue;
|
|
75
|
+
let reason: string;
|
|
76
|
+
if (CARD_REF.test(tag.trim().toLowerCase()) && norm.startsWith("card:")) {
|
|
77
|
+
reason = "non-canonical card ref; use 'card:<n>'";
|
|
78
|
+
} else if (tag !== tag.trim() || /\s{2,}/.test(tag)) {
|
|
79
|
+
reason = "whitespace; trim and collapse";
|
|
80
|
+
} else if (tag !== tag.toLowerCase()) {
|
|
81
|
+
reason = "case variant; use lowercase";
|
|
82
|
+
} else {
|
|
83
|
+
reason = "normalize to canonical form";
|
|
84
|
+
}
|
|
85
|
+
suggestions.push({ tag, suggestion: norm, reason });
|
|
86
|
+
}
|
|
87
|
+
return suggestions;
|
|
88
|
+
}
|