instar 0.6.11 → 0.6.13
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/.vercel/README.txt +11 -0
- package/.vercel/project.json +1 -0
- package/README.md +52 -5
- package/dist/cli.js +23 -3
- package/dist/commands/init.js +438 -4
- package/dist/commands/server.js +37 -4
- package/dist/core/AnthropicIntelligenceProvider.d.ts +24 -0
- package/dist/core/AnthropicIntelligenceProvider.js +68 -0
- package/dist/core/ClaudeCliIntelligenceProvider.d.ts +21 -0
- package/dist/core/ClaudeCliIntelligenceProvider.js +59 -0
- package/dist/core/EvolutionManager.d.ts +157 -0
- package/dist/core/EvolutionManager.js +432 -0
- package/dist/core/RelationshipManager.d.ts +67 -0
- package/dist/core/RelationshipManager.js +358 -4
- package/dist/core/SessionManager.d.ts +3 -1
- package/dist/core/SessionManager.js +14 -6
- package/dist/core/types.d.ts +212 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/scaffold/templates.js +36 -0
- package/dist/server/AgentServer.d.ts +2 -0
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/routes.d.ts +2 -0
- package/dist/server/routes.js +218 -0
- package/package.json +1 -1
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evolution Manager — the feedback loop that turns running into evolving.
|
|
3
|
+
*
|
|
4
|
+
* Four subsystems, one principle: every interaction is an opportunity
|
|
5
|
+
* to improve. Not during batch reflection hours later, but at the
|
|
6
|
+
* moment the insight is freshest.
|
|
7
|
+
*
|
|
8
|
+
* Subsystems:
|
|
9
|
+
* 1. Evolution Queue — staged self-improvement proposals
|
|
10
|
+
* 2. Learning Registry — structured, searchable insights
|
|
11
|
+
* 3. Capability Gap Tracker — "what am I missing?"
|
|
12
|
+
* 4. Action Queue — commitment tracking with stale detection
|
|
13
|
+
*
|
|
14
|
+
* Born from Portal's engagement pipeline (Steps 8-11) and proven
|
|
15
|
+
* across 100+ evolution proposals and 10 platform engagement skills.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
export class EvolutionManager {
|
|
20
|
+
stateDir;
|
|
21
|
+
config;
|
|
22
|
+
constructor(config) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.stateDir = config.stateDir;
|
|
25
|
+
}
|
|
26
|
+
// ── File I/O ────────────────────────────────────────────────────
|
|
27
|
+
filePath(name) {
|
|
28
|
+
return path.join(this.stateDir, 'state', 'evolution', `${name}.json`);
|
|
29
|
+
}
|
|
30
|
+
readFile(name, defaultValue) {
|
|
31
|
+
const fp = this.filePath(name);
|
|
32
|
+
if (!fs.existsSync(fp))
|
|
33
|
+
return defaultValue;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
console.warn(`[EvolutionManager] Corrupted file: ${fp}`);
|
|
39
|
+
return defaultValue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
writeFile(name, data) {
|
|
43
|
+
const fp = this.filePath(name);
|
|
44
|
+
const dir = path.dirname(fp);
|
|
45
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
const tmpPath = fp + `.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
47
|
+
try {
|
|
48
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
49
|
+
fs.renameSync(tmpPath, fp);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
try {
|
|
53
|
+
fs.unlinkSync(tmpPath);
|
|
54
|
+
}
|
|
55
|
+
catch { /* ignore */ }
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
now() {
|
|
60
|
+
return new Date().toISOString();
|
|
61
|
+
}
|
|
62
|
+
// ── Evolution Queue ─────────────────────────────────────────────
|
|
63
|
+
loadEvolution() {
|
|
64
|
+
return this.readFile('evolution-queue', {
|
|
65
|
+
proposals: [],
|
|
66
|
+
stats: { totalProposals: 0, byStatus: {}, byType: {}, lastUpdated: this.now() },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
saveEvolution(state) {
|
|
70
|
+
// Recompute stats
|
|
71
|
+
const statusCounts = {};
|
|
72
|
+
const typeCounts = {};
|
|
73
|
+
for (const p of state.proposals) {
|
|
74
|
+
statusCounts[p.status] = (statusCounts[p.status] || 0) + 1;
|
|
75
|
+
typeCounts[p.type] = (typeCounts[p.type] || 0) + 1;
|
|
76
|
+
}
|
|
77
|
+
state.stats = {
|
|
78
|
+
totalProposals: state.proposals.length,
|
|
79
|
+
byStatus: statusCounts,
|
|
80
|
+
byType: typeCounts,
|
|
81
|
+
lastUpdated: this.now(),
|
|
82
|
+
};
|
|
83
|
+
// Archive old implemented/rejected proposals if over limit
|
|
84
|
+
const max = this.config.maxProposals || 200;
|
|
85
|
+
if (state.proposals.length > max) {
|
|
86
|
+
const active = state.proposals.filter(p => !['implemented', 'rejected'].includes(p.status));
|
|
87
|
+
const archived = state.proposals.filter(p => ['implemented', 'rejected'].includes(p.status));
|
|
88
|
+
// Keep most recent archived
|
|
89
|
+
const keep = archived.slice(-Math.max(0, max - active.length));
|
|
90
|
+
state.proposals = [...active, ...keep];
|
|
91
|
+
}
|
|
92
|
+
this.writeFile('evolution-queue', state);
|
|
93
|
+
}
|
|
94
|
+
nextProposalId(state) {
|
|
95
|
+
const existing = new Set(state.proposals.map(p => p.id));
|
|
96
|
+
let num = 1;
|
|
97
|
+
while (existing.has(`EVO-${String(num).padStart(3, '0')}`))
|
|
98
|
+
num++;
|
|
99
|
+
return `EVO-${String(num).padStart(3, '0')}`;
|
|
100
|
+
}
|
|
101
|
+
addProposal(opts) {
|
|
102
|
+
const state = this.loadEvolution();
|
|
103
|
+
const id = this.nextProposalId(state);
|
|
104
|
+
const proposal = {
|
|
105
|
+
id,
|
|
106
|
+
title: opts.title,
|
|
107
|
+
source: opts.source,
|
|
108
|
+
description: opts.description,
|
|
109
|
+
type: opts.type,
|
|
110
|
+
impact: opts.impact || 'medium',
|
|
111
|
+
effort: opts.effort || 'medium',
|
|
112
|
+
status: 'proposed',
|
|
113
|
+
proposedBy: opts.proposedBy || 'agent',
|
|
114
|
+
proposedAt: this.now(),
|
|
115
|
+
tags: opts.tags,
|
|
116
|
+
};
|
|
117
|
+
state.proposals.push(proposal);
|
|
118
|
+
this.saveEvolution(state);
|
|
119
|
+
return proposal;
|
|
120
|
+
}
|
|
121
|
+
updateProposalStatus(id, status, resolution) {
|
|
122
|
+
const state = this.loadEvolution();
|
|
123
|
+
const proposal = state.proposals.find(p => p.id === id);
|
|
124
|
+
if (!proposal)
|
|
125
|
+
return false;
|
|
126
|
+
proposal.status = status;
|
|
127
|
+
if (resolution)
|
|
128
|
+
proposal.resolution = resolution;
|
|
129
|
+
if (status === 'implemented')
|
|
130
|
+
proposal.implementedAt = this.now();
|
|
131
|
+
this.saveEvolution(state);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
listProposals(filter) {
|
|
135
|
+
const state = this.loadEvolution();
|
|
136
|
+
let proposals = state.proposals;
|
|
137
|
+
if (filter?.status)
|
|
138
|
+
proposals = proposals.filter(p => p.status === filter.status);
|
|
139
|
+
if (filter?.type)
|
|
140
|
+
proposals = proposals.filter(p => p.type === filter.type);
|
|
141
|
+
return proposals;
|
|
142
|
+
}
|
|
143
|
+
getEvolutionStats() {
|
|
144
|
+
return this.loadEvolution().stats;
|
|
145
|
+
}
|
|
146
|
+
// ── Learning Registry ───────────────────────────────────────────
|
|
147
|
+
loadLearnings() {
|
|
148
|
+
return this.readFile('learning-registry', {
|
|
149
|
+
learnings: [],
|
|
150
|
+
stats: { totalLearnings: 0, applied: 0, pending: 0, byCategory: {}, lastUpdated: this.now() },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
saveLearnings(state) {
|
|
154
|
+
const categoryCounts = {};
|
|
155
|
+
let applied = 0;
|
|
156
|
+
for (const l of state.learnings) {
|
|
157
|
+
categoryCounts[l.category] = (categoryCounts[l.category] || 0) + 1;
|
|
158
|
+
if (l.applied)
|
|
159
|
+
applied++;
|
|
160
|
+
}
|
|
161
|
+
state.stats = {
|
|
162
|
+
totalLearnings: state.learnings.length,
|
|
163
|
+
applied,
|
|
164
|
+
pending: state.learnings.length - applied,
|
|
165
|
+
byCategory: categoryCounts,
|
|
166
|
+
lastUpdated: this.now(),
|
|
167
|
+
};
|
|
168
|
+
const max = this.config.maxLearnings || 500;
|
|
169
|
+
if (state.learnings.length > max) {
|
|
170
|
+
const unapplied = state.learnings.filter(l => !l.applied);
|
|
171
|
+
const appliedEntries = state.learnings.filter(l => l.applied);
|
|
172
|
+
const keep = appliedEntries.slice(-Math.max(0, max - unapplied.length));
|
|
173
|
+
state.learnings = [...unapplied, ...keep];
|
|
174
|
+
}
|
|
175
|
+
this.writeFile('learning-registry', state);
|
|
176
|
+
}
|
|
177
|
+
nextLearningId(state) {
|
|
178
|
+
const existing = new Set(state.learnings.map(l => l.id));
|
|
179
|
+
let num = 1;
|
|
180
|
+
while (existing.has(`LRN-${String(num).padStart(3, '0')}`))
|
|
181
|
+
num++;
|
|
182
|
+
return `LRN-${String(num).padStart(3, '0')}`;
|
|
183
|
+
}
|
|
184
|
+
addLearning(opts) {
|
|
185
|
+
const state = this.loadLearnings();
|
|
186
|
+
const id = this.nextLearningId(state);
|
|
187
|
+
const learning = {
|
|
188
|
+
id,
|
|
189
|
+
title: opts.title,
|
|
190
|
+
category: opts.category,
|
|
191
|
+
description: opts.description,
|
|
192
|
+
source: opts.source,
|
|
193
|
+
tags: opts.tags || [],
|
|
194
|
+
applied: false,
|
|
195
|
+
evolutionRelevance: opts.evolutionRelevance,
|
|
196
|
+
};
|
|
197
|
+
state.learnings.push(learning);
|
|
198
|
+
this.saveLearnings(state);
|
|
199
|
+
return learning;
|
|
200
|
+
}
|
|
201
|
+
markLearningApplied(id, appliedTo) {
|
|
202
|
+
const state = this.loadLearnings();
|
|
203
|
+
const learning = state.learnings.find(l => l.id === id);
|
|
204
|
+
if (!learning)
|
|
205
|
+
return false;
|
|
206
|
+
learning.applied = true;
|
|
207
|
+
learning.appliedTo = appliedTo;
|
|
208
|
+
this.saveLearnings(state);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
listLearnings(filter) {
|
|
212
|
+
const state = this.loadLearnings();
|
|
213
|
+
let learnings = state.learnings;
|
|
214
|
+
if (filter?.category)
|
|
215
|
+
learnings = learnings.filter(l => l.category === filter.category);
|
|
216
|
+
if (filter?.applied !== undefined)
|
|
217
|
+
learnings = learnings.filter(l => l.applied === filter.applied);
|
|
218
|
+
return learnings;
|
|
219
|
+
}
|
|
220
|
+
getLearningStats() {
|
|
221
|
+
return this.loadLearnings().stats;
|
|
222
|
+
}
|
|
223
|
+
// ── Capability Gap Tracker ──────────────────────────────────────
|
|
224
|
+
loadGaps() {
|
|
225
|
+
return this.readFile('capability-gaps', {
|
|
226
|
+
gaps: [],
|
|
227
|
+
stats: { totalGaps: 0, bySeverity: {}, byCategory: {}, addressed: 0, lastUpdated: this.now() },
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
saveGaps(state) {
|
|
231
|
+
const severityCounts = {};
|
|
232
|
+
const categoryCounts = {};
|
|
233
|
+
let addressed = 0;
|
|
234
|
+
for (const g of state.gaps) {
|
|
235
|
+
severityCounts[g.severity] = (severityCounts[g.severity] || 0) + 1;
|
|
236
|
+
categoryCounts[g.category] = (categoryCounts[g.category] || 0) + 1;
|
|
237
|
+
if (g.status === 'addressed')
|
|
238
|
+
addressed++;
|
|
239
|
+
}
|
|
240
|
+
state.stats = {
|
|
241
|
+
totalGaps: state.gaps.length,
|
|
242
|
+
bySeverity: severityCounts,
|
|
243
|
+
byCategory: categoryCounts,
|
|
244
|
+
addressed,
|
|
245
|
+
lastUpdated: this.now(),
|
|
246
|
+
};
|
|
247
|
+
const max = this.config.maxGaps || 200;
|
|
248
|
+
if (state.gaps.length > max) {
|
|
249
|
+
const open = state.gaps.filter(g => g.status === 'identified');
|
|
250
|
+
const closed = state.gaps.filter(g => g.status !== 'identified');
|
|
251
|
+
const keep = closed.slice(-Math.max(0, max - open.length));
|
|
252
|
+
state.gaps = [...open, ...keep];
|
|
253
|
+
}
|
|
254
|
+
this.writeFile('capability-gaps', state);
|
|
255
|
+
}
|
|
256
|
+
nextGapId(state) {
|
|
257
|
+
const existing = new Set(state.gaps.map(g => g.id));
|
|
258
|
+
let num = 1;
|
|
259
|
+
while (existing.has(`GAP-${String(num).padStart(3, '0')}`))
|
|
260
|
+
num++;
|
|
261
|
+
return `GAP-${String(num).padStart(3, '0')}`;
|
|
262
|
+
}
|
|
263
|
+
addGap(opts) {
|
|
264
|
+
const state = this.loadGaps();
|
|
265
|
+
const id = this.nextGapId(state);
|
|
266
|
+
const gap = {
|
|
267
|
+
id,
|
|
268
|
+
title: opts.title,
|
|
269
|
+
category: opts.category,
|
|
270
|
+
severity: opts.severity,
|
|
271
|
+
description: opts.description,
|
|
272
|
+
discoveredFrom: {
|
|
273
|
+
context: opts.context,
|
|
274
|
+
platform: opts.platform,
|
|
275
|
+
discoveredAt: this.now(),
|
|
276
|
+
session: opts.session,
|
|
277
|
+
},
|
|
278
|
+
currentState: opts.currentState || 'Not implemented',
|
|
279
|
+
proposedSolution: opts.proposedSolution,
|
|
280
|
+
status: 'identified',
|
|
281
|
+
};
|
|
282
|
+
state.gaps.push(gap);
|
|
283
|
+
this.saveGaps(state);
|
|
284
|
+
return gap;
|
|
285
|
+
}
|
|
286
|
+
addressGap(id, resolution) {
|
|
287
|
+
const state = this.loadGaps();
|
|
288
|
+
const gap = state.gaps.find(g => g.id === id);
|
|
289
|
+
if (!gap)
|
|
290
|
+
return false;
|
|
291
|
+
gap.status = 'addressed';
|
|
292
|
+
gap.resolution = resolution;
|
|
293
|
+
gap.addressedAt = this.now();
|
|
294
|
+
this.saveGaps(state);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
listGaps(filter) {
|
|
298
|
+
const state = this.loadGaps();
|
|
299
|
+
let gaps = state.gaps;
|
|
300
|
+
if (filter?.severity)
|
|
301
|
+
gaps = gaps.filter(g => g.severity === filter.severity);
|
|
302
|
+
if (filter?.category)
|
|
303
|
+
gaps = gaps.filter(g => g.category === filter.category);
|
|
304
|
+
if (filter?.status)
|
|
305
|
+
gaps = gaps.filter(g => g.status === filter.status);
|
|
306
|
+
return gaps;
|
|
307
|
+
}
|
|
308
|
+
getGapStats() {
|
|
309
|
+
return this.loadGaps().stats;
|
|
310
|
+
}
|
|
311
|
+
// ── Action Queue ────────────────────────────────────────────────
|
|
312
|
+
loadActions() {
|
|
313
|
+
return this.readFile('action-queue', {
|
|
314
|
+
actions: [],
|
|
315
|
+
stats: { totalActions: 0, pending: 0, completed: 0, overdue: 0, lastUpdated: this.now() },
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
saveActions(state) {
|
|
319
|
+
let pending = 0, completed = 0, overdue = 0;
|
|
320
|
+
const now = new Date();
|
|
321
|
+
for (const a of state.actions) {
|
|
322
|
+
if (a.status === 'completed')
|
|
323
|
+
completed++;
|
|
324
|
+
else if (a.status === 'pending' || a.status === 'in_progress') {
|
|
325
|
+
pending++;
|
|
326
|
+
if (a.dueBy && new Date(a.dueBy) < now)
|
|
327
|
+
overdue++;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
state.stats = {
|
|
331
|
+
totalActions: state.actions.length,
|
|
332
|
+
pending,
|
|
333
|
+
completed,
|
|
334
|
+
overdue,
|
|
335
|
+
lastUpdated: this.now(),
|
|
336
|
+
};
|
|
337
|
+
const max = this.config.maxActions || 300;
|
|
338
|
+
if (state.actions.length > max) {
|
|
339
|
+
const active = state.actions.filter(a => !['completed', 'cancelled'].includes(a.status));
|
|
340
|
+
const done = state.actions.filter(a => ['completed', 'cancelled'].includes(a.status));
|
|
341
|
+
const keep = done.slice(-Math.max(0, max - active.length));
|
|
342
|
+
state.actions = [...active, ...keep];
|
|
343
|
+
}
|
|
344
|
+
this.writeFile('action-queue', state);
|
|
345
|
+
}
|
|
346
|
+
nextActionId(state) {
|
|
347
|
+
const existing = new Set(state.actions.map(a => a.id));
|
|
348
|
+
let num = 1;
|
|
349
|
+
while (existing.has(`ACT-${String(num).padStart(3, '0')}`))
|
|
350
|
+
num++;
|
|
351
|
+
return `ACT-${String(num).padStart(3, '0')}`;
|
|
352
|
+
}
|
|
353
|
+
addAction(opts) {
|
|
354
|
+
const state = this.loadActions();
|
|
355
|
+
const id = this.nextActionId(state);
|
|
356
|
+
const action = {
|
|
357
|
+
id,
|
|
358
|
+
title: opts.title,
|
|
359
|
+
description: opts.description,
|
|
360
|
+
priority: opts.priority || 'medium',
|
|
361
|
+
status: 'pending',
|
|
362
|
+
commitTo: opts.commitTo,
|
|
363
|
+
createdAt: this.now(),
|
|
364
|
+
dueBy: opts.dueBy,
|
|
365
|
+
source: opts.source,
|
|
366
|
+
tags: opts.tags,
|
|
367
|
+
};
|
|
368
|
+
state.actions.push(action);
|
|
369
|
+
this.saveActions(state);
|
|
370
|
+
return action;
|
|
371
|
+
}
|
|
372
|
+
updateAction(id, updates) {
|
|
373
|
+
const state = this.loadActions();
|
|
374
|
+
const action = state.actions.find(a => a.id === id);
|
|
375
|
+
if (!action)
|
|
376
|
+
return false;
|
|
377
|
+
if (updates.status) {
|
|
378
|
+
action.status = updates.status;
|
|
379
|
+
if (updates.status === 'completed')
|
|
380
|
+
action.completedAt = this.now();
|
|
381
|
+
}
|
|
382
|
+
if (updates.resolution)
|
|
383
|
+
action.resolution = updates.resolution;
|
|
384
|
+
this.saveActions(state);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
listActions(filter) {
|
|
388
|
+
const state = this.loadActions();
|
|
389
|
+
let actions = state.actions;
|
|
390
|
+
if (filter?.status)
|
|
391
|
+
actions = actions.filter(a => a.status === filter.status);
|
|
392
|
+
if (filter?.priority)
|
|
393
|
+
actions = actions.filter(a => a.priority === filter.priority);
|
|
394
|
+
return actions;
|
|
395
|
+
}
|
|
396
|
+
getOverdueActions() {
|
|
397
|
+
const state = this.loadActions();
|
|
398
|
+
const now = new Date();
|
|
399
|
+
return state.actions.filter(a => (a.status === 'pending' || a.status === 'in_progress') &&
|
|
400
|
+
a.dueBy && new Date(a.dueBy) < now);
|
|
401
|
+
}
|
|
402
|
+
getActionStats() {
|
|
403
|
+
return this.loadActions().stats;
|
|
404
|
+
}
|
|
405
|
+
// ── Cross-System Queries ────────────────────────────────────────
|
|
406
|
+
/**
|
|
407
|
+
* Get a full dashboard of evolution health.
|
|
408
|
+
* Useful for session-start orientation and status reporting.
|
|
409
|
+
*/
|
|
410
|
+
getDashboard() {
|
|
411
|
+
const evolution = this.getEvolutionStats();
|
|
412
|
+
const learnings = this.getLearningStats();
|
|
413
|
+
const gaps = this.getGapStats();
|
|
414
|
+
const actions = this.getActionStats();
|
|
415
|
+
const overdue = this.getOverdueActions();
|
|
416
|
+
const highlights = [];
|
|
417
|
+
const proposed = evolution.byStatus['proposed'] || 0;
|
|
418
|
+
if (proposed > 0)
|
|
419
|
+
highlights.push(`${proposed} evolution proposal(s) awaiting review`);
|
|
420
|
+
if (learnings.pending > 0)
|
|
421
|
+
highlights.push(`${learnings.pending} learning(s) not yet applied`);
|
|
422
|
+
const criticalGaps = gaps.bySeverity['critical'] || 0;
|
|
423
|
+
if (criticalGaps > 0)
|
|
424
|
+
highlights.push(`${criticalGaps} critical capability gap(s)`);
|
|
425
|
+
if (overdue.length > 0)
|
|
426
|
+
highlights.push(`${overdue.length} overdue action item(s)`);
|
|
427
|
+
if (highlights.length === 0)
|
|
428
|
+
highlights.push('All systems healthy — no pending evolution items');
|
|
429
|
+
return { evolution, learnings, gaps, actions, highlights };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
//# sourceMappingURL=EvolutionManager.js.map
|
|
@@ -16,8 +16,16 @@ export declare class RelationshipManager {
|
|
|
16
16
|
private relationships;
|
|
17
17
|
/** Maps "channel_type:identifier" -> relationship ID for cross-platform resolution */
|
|
18
18
|
private channelIndex;
|
|
19
|
+
/** Maps normalized name -> set of relationship IDs for fuzzy name resolution */
|
|
20
|
+
private nameIndex;
|
|
19
21
|
private config;
|
|
20
22
|
constructor(config: RelationshipManagerConfig);
|
|
23
|
+
/** Normalize a name for fuzzy matching: lowercase, trim, collapse whitespace, strip leading @ */
|
|
24
|
+
private normalizeName;
|
|
25
|
+
/** Add a record to the name index */
|
|
26
|
+
private indexName;
|
|
27
|
+
/** Remove a record from the name index */
|
|
28
|
+
private unindexName;
|
|
21
29
|
/** Validate a record ID is a valid UUID format to prevent path traversal. */
|
|
22
30
|
private validateId;
|
|
23
31
|
/**
|
|
@@ -26,10 +34,47 @@ export declare class RelationshipManager {
|
|
|
26
34
|
* this returns the same relationship.
|
|
27
35
|
*/
|
|
28
36
|
findOrCreate(name: string, channel: UserChannel): RelationshipRecord;
|
|
37
|
+
/**
|
|
38
|
+
* LLM-supervised version of findOrCreate.
|
|
39
|
+
* When an intelligence provider is configured:
|
|
40
|
+
* - Heuristics narrow candidates (channel match, name match)
|
|
41
|
+
* - LLM confirms ambiguous name matches before linking
|
|
42
|
+
* - LLM can detect matches that string heuristics miss
|
|
43
|
+
*
|
|
44
|
+
* Falls back to sync findOrCreate when no provider is available.
|
|
45
|
+
*/
|
|
46
|
+
findOrCreateAsync(name: string, channel: UserChannel): Promise<RelationshipRecord>;
|
|
47
|
+
/**
|
|
48
|
+
* LLM-supervised duplicate detection.
|
|
49
|
+
* Runs heuristic findDuplicates() first, then asks the LLM to confirm
|
|
50
|
+
* each candidate group. Returns only LLM-confirmed duplicates.
|
|
51
|
+
*
|
|
52
|
+
* Falls back to heuristic-only when no provider is available.
|
|
53
|
+
*/
|
|
54
|
+
findDuplicatesAsync(): Promise<Array<{
|
|
55
|
+
records: RelationshipRecord[];
|
|
56
|
+
reason: string;
|
|
57
|
+
confirmed: boolean;
|
|
58
|
+
}>>;
|
|
59
|
+
/**
|
|
60
|
+
* Ask the LLM whether a new name+channel belongs to one of the candidate records.
|
|
61
|
+
* Returns the matching record, or null if the LLM says it's a new person.
|
|
62
|
+
*/
|
|
63
|
+
private askIdentityMatch;
|
|
64
|
+
/**
|
|
65
|
+
* Ask the LLM to confirm whether a group of records are truly duplicates.
|
|
66
|
+
*/
|
|
67
|
+
private askDuplicateConfirmation;
|
|
29
68
|
/**
|
|
30
69
|
* Resolve a channel identifier to an existing relationship, or null.
|
|
31
70
|
*/
|
|
32
71
|
resolveByChannel(channel: UserChannel): RelationshipRecord | null;
|
|
72
|
+
/**
|
|
73
|
+
* Resolve by name using fuzzy matching. Returns all matches.
|
|
74
|
+
* Handles: case differences, leading @, underscores vs hyphens vs spaces.
|
|
75
|
+
* Port of Portal's _find_existing_person() pattern.
|
|
76
|
+
*/
|
|
77
|
+
resolveByName(name: string): RelationshipRecord[];
|
|
33
78
|
/**
|
|
34
79
|
* Get a relationship by ID.
|
|
35
80
|
*/
|
|
@@ -38,6 +83,16 @@ export declare class RelationshipManager {
|
|
|
38
83
|
* Get all relationships, optionally sorted by significance or recency.
|
|
39
84
|
*/
|
|
40
85
|
getAll(sortBy?: 'significance' | 'recent' | 'name'): RelationshipRecord[];
|
|
86
|
+
/**
|
|
87
|
+
* Detect potential duplicate relationships that could be merged.
|
|
88
|
+
* Port of Portal's find_potential_duplicates() pattern.
|
|
89
|
+
* Returns groups of records that likely represent the same person,
|
|
90
|
+
* with a reason string explaining why they were flagged.
|
|
91
|
+
*/
|
|
92
|
+
findDuplicates(): Array<{
|
|
93
|
+
records: RelationshipRecord[];
|
|
94
|
+
reason: string;
|
|
95
|
+
}>;
|
|
41
96
|
/**
|
|
42
97
|
* Record an interaction with a person. Updates recency, count, and interaction log.
|
|
43
98
|
*/
|
|
@@ -62,6 +117,18 @@ export declare class RelationshipManager {
|
|
|
62
117
|
* Delete a relationship and its disk file.
|
|
63
118
|
*/
|
|
64
119
|
delete(id: string): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Update the category for a relationship.
|
|
122
|
+
*/
|
|
123
|
+
updateCategory(id: string, category: string): void;
|
|
124
|
+
/**
|
|
125
|
+
* Add tags to a relationship (deduplicates).
|
|
126
|
+
*/
|
|
127
|
+
addTags(id: string, tags: string[]): void;
|
|
128
|
+
/**
|
|
129
|
+
* Remove tags from a relationship.
|
|
130
|
+
*/
|
|
131
|
+
removeTags(id: string, tags: string[]): void;
|
|
65
132
|
/**
|
|
66
133
|
* Generate context string for injection into a Claude session before interacting
|
|
67
134
|
* with a known person. This is what makes the agent "know" who it's talking to.
|