ei-tui 0.6.6 → 0.7.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/package.json +1 -1
- package/src/cli/README.md +16 -7
- package/src/cli/commands/people.ts +1 -0
- package/src/cli/mcp.ts +36 -11
- package/src/cli/persona-filter.ts +42 -0
- package/src/cli/retrieval.ts +3 -1
- package/src/cli.ts +18 -6
- package/src/core/handlers/human-extraction.ts +1 -0
- package/src/core/handlers/human-matching.ts +20 -4
- package/src/core/handlers/index.ts +2 -1
- package/src/core/handlers/persona-response.ts +5 -0
- package/src/core/handlers/utils.ts +4 -1
- package/src/core/orchestrators/ceremony.ts +2 -2
- package/src/core/orchestrators/human-extraction.ts +24 -7
- package/src/core/persona-manager.ts +3 -0
- package/src/core/processor.ts +22 -2
- package/src/core/prompt-context-builder.ts +40 -10
- package/src/core/queue-manager.ts +18 -0
- package/src/core/room-manager.ts +21 -4
- package/src/core/state-manager.ts +74 -0
- package/src/core/tools/builtin/read-memory.ts +1 -1
- package/src/core/types/data-items.ts +1 -0
- package/src/core/types/entities.ts +13 -0
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +4 -0
- package/src/core/types/rooms.ts +2 -0
- package/src/core/utils/identifier-utils.ts +24 -0
- package/src/core/utils/theme-codec.ts +78 -0
- package/src/integrations/claude-code/importer.ts +3 -57
- package/src/integrations/cursor/importer.ts +2 -52
- package/src/integrations/opencode/importer.ts +1 -0
- package/src/prompts/response/sections.ts +1 -1
- package/src/prompts/response/types.ts +1 -0
- package/src/prompts/room/index.ts +2 -2
- package/src/prompts/room/sections.ts +4 -4
- package/src/prompts/room/types.ts +4 -0
- package/tui/src/commands/activate.tsx +7 -6
- package/tui/src/commands/context.tsx +188 -2
- package/tui/src/components/CYPTreeOverlay.tsx +357 -0
- package/tui/src/components/MAPScoreOverlay.tsx +300 -0
- package/tui/src/components/MessageList.tsx +14 -3
- package/tui/src/components/RoomMessageList.tsx +15 -3
- package/tui/src/context/ei.tsx +20 -0
- package/tui/src/util/cyp-tree.ts +62 -0
- package/tui/src/util/yaml-context.ts +87 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { PersonaEntity, HumanEntity, DataItemBase, Quote, RoomEntity } from "./types.js";
|
|
1
|
+
import type { PersonaEntity, HumanEntity, DataItemBase, Quote, RoomEntity, RoomMessage } from "./types.js";
|
|
2
|
+
import { RoomMode, ContextStatus } from "./types.js";
|
|
2
3
|
import { StateManager } from "./state-manager.js";
|
|
3
4
|
import { getEmbeddingService, findTopK } from "./embedding-service.js";
|
|
4
5
|
import type { ResponsePromptData, PromptOutput } from "../prompts/index.js";
|
|
@@ -121,7 +122,12 @@ export async function filterHumanDataByVisibility(
|
|
|
121
122
|
selectRelevantQuotes(human.quotes ?? [], currentMessage),
|
|
122
123
|
]);
|
|
123
124
|
const { topics, people } = capTopicsAndPeople(rawTopics, rawPeople);
|
|
125
|
+
const humanName =
|
|
126
|
+
human.settings?.name_display ||
|
|
127
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
128
|
+
"Human";
|
|
124
129
|
return {
|
|
130
|
+
name: humanName,
|
|
125
131
|
facts,
|
|
126
132
|
topics,
|
|
127
133
|
people,
|
|
@@ -158,7 +164,12 @@ export async function filterHumanDataByVisibility(
|
|
|
158
164
|
]);
|
|
159
165
|
const { topics, people } = capTopicsAndPeople(rawTopics, rawPeople);
|
|
160
166
|
|
|
167
|
+
const humanName =
|
|
168
|
+
human.settings?.name_display ||
|
|
169
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
170
|
+
"Human";
|
|
161
171
|
return {
|
|
172
|
+
name: humanName,
|
|
162
173
|
facts,
|
|
163
174
|
topics,
|
|
164
175
|
people,
|
|
@@ -264,14 +275,30 @@ export async function buildRoomResponsePromptData(
|
|
|
264
275
|
? [...sm.getRoomMessages(room.id)].sort((a, b) => a.timestamp.localeCompare(b.timestamp))
|
|
265
276
|
: activePath;
|
|
266
277
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
278
|
+
let sourceMessages: RoomMessage[];
|
|
279
|
+
if (room.mode === RoomMode.FreeForAll) {
|
|
280
|
+
const contextWindowHours = room.context_window_hours
|
|
281
|
+
?? human.settings?.default_context_window_hours
|
|
282
|
+
?? 8;
|
|
283
|
+
const windowCutoff = new Date(Date.now() - contextWindowHours * 60 * 60 * 1000).toISOString();
|
|
284
|
+
const boundaryMs = room.context_boundary ? new Date(room.context_boundary).getTime() : 0;
|
|
285
|
+
sourceMessages = allSourceMessages.filter(m => {
|
|
286
|
+
const msgMs = new Date(m.timestamp).getTime();
|
|
287
|
+
if (m.context_status === ContextStatus.Always) return true;
|
|
288
|
+
if (m.context_status === ContextStatus.Never) return false;
|
|
289
|
+
if (msgMs < new Date(windowCutoff).getTime()) return false;
|
|
290
|
+
if (boundaryMs && msgMs < boundaryMs) return false;
|
|
291
|
+
return true;
|
|
292
|
+
});
|
|
293
|
+
const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
|
|
294
|
+
if (byCount.length > sourceMessages.length) sourceMessages = byCount;
|
|
295
|
+
} else {
|
|
296
|
+
const contextWindowHours = human.settings?.default_context_window_hours ?? 8;
|
|
297
|
+
const windowCutoff = new Date(Date.now() - contextWindowHours * 60 * 60 * 1000).toISOString();
|
|
298
|
+
const byTime = allSourceMessages.filter(m => m.timestamp >= windowCutoff);
|
|
299
|
+
const byCount = allSourceMessages.slice(-MIN_ROOM_MESSAGES);
|
|
300
|
+
sourceMessages = byTime.length >= byCount.length ? byTime : byCount;
|
|
301
|
+
}
|
|
275
302
|
|
|
276
303
|
const lastMessage = sourceMessages[sourceMessages.length - 1];
|
|
277
304
|
const currentMessage = lastMessage ? getMessageContent(lastMessage) : undefined;
|
|
@@ -297,7 +324,10 @@ export async function buildRoomResponsePromptData(
|
|
|
297
324
|
}
|
|
298
325
|
otherParticipants.push({
|
|
299
326
|
id: "human",
|
|
300
|
-
name:
|
|
327
|
+
name:
|
|
328
|
+
human.settings?.name_display ||
|
|
329
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
330
|
+
"Human",
|
|
301
331
|
traits: [],
|
|
302
332
|
is_human: true,
|
|
303
333
|
});
|
|
@@ -77,3 +77,21 @@ export async function submitOneShot(
|
|
|
77
77
|
data: { guid },
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
export async function submitOneShotJSON(
|
|
82
|
+
sm: StateManager,
|
|
83
|
+
getOneshotModel: () => string | undefined,
|
|
84
|
+
guid: string,
|
|
85
|
+
systemPrompt: string,
|
|
86
|
+
userPrompt: string
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
sm.queue_enqueue({
|
|
89
|
+
type: LLMRequestType.JSON,
|
|
90
|
+
priority: LLMPriority.High,
|
|
91
|
+
system: systemPrompt,
|
|
92
|
+
user: userPrompt,
|
|
93
|
+
next_step: LLMNextStep.HandleOneShotJSON,
|
|
94
|
+
model: getOneshotModel(),
|
|
95
|
+
data: { guid },
|
|
96
|
+
});
|
|
97
|
+
}
|
package/src/core/room-manager.ts
CHANGED
|
@@ -149,8 +149,19 @@ export async function sendFfaMessage(
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
const now = new Date().toISOString();
|
|
152
|
+
|
|
153
|
+
// FFA human messages always hang off the room root (the initial message with parent_id === null)
|
|
154
|
+
// so the tree is a flat star: root → every human turn, each human turn → persona responses.
|
|
155
|
+
// This gives the context window a bounded, predictable shape instead of a chain.
|
|
156
|
+
const ffaRootMsg = sm.getRoomMessages(roomId).find(m => m.parent_id === null);
|
|
157
|
+
if (!ffaRootMsg) {
|
|
158
|
+
onError({ code: "ROOM_NO_ROOT", message: "FFA room has no root message. Try archiving and recreating the room." });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const ffaParentId = ffaRootMsg.id;
|
|
162
|
+
|
|
152
163
|
const existing = sm.getRoomMessages(roomId).find(
|
|
153
|
-
m => m.role === "human" && m.
|
|
164
|
+
m => m.role === "human" && m.id === room.active_node_id && m.parent_id === ffaParentId
|
|
154
165
|
);
|
|
155
166
|
|
|
156
167
|
let humanMsgId: string;
|
|
@@ -164,7 +175,7 @@ export async function sendFfaMessage(
|
|
|
164
175
|
} else {
|
|
165
176
|
const msg: RoomMessage = {
|
|
166
177
|
id: crypto.randomUUID(),
|
|
167
|
-
parent_id:
|
|
178
|
+
parent_id: ffaParentId,
|
|
168
179
|
role: "human",
|
|
169
180
|
verbal_response: content ?? undefined,
|
|
170
181
|
silence_reason: content ? undefined : (silenceReason ?? "passed"),
|
|
@@ -278,9 +289,14 @@ export async function activateRoom(
|
|
|
278
289
|
|
|
279
290
|
const currentRound = allMessages.filter(m => m.parent_id === room.active_node_id);
|
|
280
291
|
|
|
292
|
+
const humanDisplayName =
|
|
293
|
+
human.settings?.name_display ||
|
|
294
|
+
human.facts?.find(f => f.name === "Nickname/Preferred Name")?.description ||
|
|
295
|
+
"Human";
|
|
296
|
+
|
|
281
297
|
const context: RoomHistoryMessage[] = sm.getRoomActivePath(roomId).map(m => ({
|
|
282
298
|
speaker_name: m.role === "human"
|
|
283
|
-
?
|
|
299
|
+
? humanDisplayName
|
|
284
300
|
: (sm.persona_getById(m.persona_id ?? "")?.display_name ?? "Unknown"),
|
|
285
301
|
speaker_id: m.role === "human" ? "human" : (m.persona_id ?? ""),
|
|
286
302
|
verbal_response: getMessageContent(m) || undefined,
|
|
@@ -290,7 +306,7 @@ export async function activateRoom(
|
|
|
290
306
|
const candidates: RoomJudgeCandidate[] = currentRound.map(m => ({
|
|
291
307
|
message_id: m.id,
|
|
292
308
|
speaker_name: m.role === "human"
|
|
293
|
-
?
|
|
309
|
+
? humanDisplayName
|
|
294
310
|
: (sm.persona_getById(m.persona_id ?? "")?.display_name ?? "Unknown"),
|
|
295
311
|
speaker_id: m.role === "human" ? "human" : (m.persona_id ?? ""),
|
|
296
312
|
verbal_response: getMessageContent(m) || undefined,
|
|
@@ -305,6 +321,7 @@ export async function activateRoom(
|
|
|
305
321
|
long_description: judgePersona.long_description,
|
|
306
322
|
traits: judgePersona.traits,
|
|
307
323
|
},
|
|
324
|
+
human: { name: humanDisplayName },
|
|
308
325
|
context,
|
|
309
326
|
candidates,
|
|
310
327
|
});
|
|
@@ -17,7 +17,9 @@ import type {
|
|
|
17
17
|
RoomSummary,
|
|
18
18
|
RoomCreationInput,
|
|
19
19
|
} from "./types.js";
|
|
20
|
+
import { RoomMode } from "./types.js";
|
|
20
21
|
import { BUILT_IN_FACT_NAMES } from './constants/built-in-facts.js';
|
|
22
|
+
import type { ThemeDefinition } from './types/entities.js';
|
|
21
23
|
import type { Storage } from "../storage/interface.js";
|
|
22
24
|
import {
|
|
23
25
|
HumanState,
|
|
@@ -68,6 +70,8 @@ export class StateManager {
|
|
|
68
70
|
this.migrateInterestedPersonas();
|
|
69
71
|
this.migrateProviderModel();
|
|
70
72
|
this.migrateRoomMessageContent();
|
|
73
|
+
this.migrateThemes();
|
|
74
|
+
this.migrateFfaParentIds();
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
private migrateRoomMessageContent(): void {
|
|
@@ -566,6 +570,38 @@ export class StateManager {
|
|
|
566
570
|
this.persistenceState.scheduleSave(this.buildStorageState());
|
|
567
571
|
}
|
|
568
572
|
|
|
573
|
+
private migrateThemes(): void {
|
|
574
|
+
const human = this.humanState.get();
|
|
575
|
+
if (!human.settings) return;
|
|
576
|
+
if (human.settings.custom_themes !== undefined) return;
|
|
577
|
+
human.settings.custom_themes = [];
|
|
578
|
+
this.humanState.set(human);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private migrateFfaParentIds(): void {
|
|
582
|
+
const rooms = this.roomState.getAll(true);
|
|
583
|
+
let migratedCount = 0;
|
|
584
|
+
|
|
585
|
+
for (const room of rooms) {
|
|
586
|
+
if (room.mode !== RoomMode.FreeForAll) continue;
|
|
587
|
+
const rootMsg = room.messages.find(m => m.parent_id === null);
|
|
588
|
+
if (!rootMsg) continue;
|
|
589
|
+
|
|
590
|
+
for (const msg of room.messages) {
|
|
591
|
+
if (msg.role !== "human") continue;
|
|
592
|
+
if (msg.id === rootMsg.id) continue;
|
|
593
|
+
if (msg.parent_id === rootMsg.id) continue;
|
|
594
|
+
msg.parent_id = rootMsg.id;
|
|
595
|
+
migratedCount++;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (migratedCount > 0) {
|
|
600
|
+
this.scheduleSave();
|
|
601
|
+
console.log(`[StateManager] Migrated ${migratedCount} FFA human messages to root parent_id`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
569
605
|
getHuman(): HumanEntity {
|
|
570
606
|
return this.humanState.get();
|
|
571
607
|
}
|
|
@@ -575,6 +611,44 @@ export class StateManager {
|
|
|
575
611
|
this.scheduleSave();
|
|
576
612
|
}
|
|
577
613
|
|
|
614
|
+
human_theme_getActive(): string | undefined {
|
|
615
|
+
return this.getHuman().settings?.active_theme;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
human_theme_setActive(id: string | undefined): void {
|
|
619
|
+
const human = this.getHuman();
|
|
620
|
+
human.settings ??= {};
|
|
621
|
+
human.settings.active_theme = id;
|
|
622
|
+
this.setHuman(human);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
human_theme_getAll(): ThemeDefinition[] {
|
|
626
|
+
return this.getHuman().settings?.custom_themes ?? [];
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
human_theme_upsert(theme: ThemeDefinition): void {
|
|
630
|
+
const human = this.getHuman();
|
|
631
|
+
human.settings ??= {};
|
|
632
|
+
human.settings.custom_themes ??= [];
|
|
633
|
+
const idx = human.settings.custom_themes.findIndex(t => t.id === theme.id);
|
|
634
|
+
if (idx >= 0) {
|
|
635
|
+
human.settings.custom_themes[idx] = theme;
|
|
636
|
+
} else {
|
|
637
|
+
human.settings.custom_themes.push(theme);
|
|
638
|
+
}
|
|
639
|
+
this.setHuman(human);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
human_theme_remove(id: string): boolean {
|
|
643
|
+
const human = this.getHuman();
|
|
644
|
+
const themes = human.settings?.custom_themes ?? [];
|
|
645
|
+
const idx = themes.findIndex(t => t.id === id);
|
|
646
|
+
if (idx < 0) return false;
|
|
647
|
+
themes.splice(idx, 1);
|
|
648
|
+
this.setHuman(human);
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
|
|
578
652
|
human_fact_upsert(fact: Fact): void {
|
|
579
653
|
this.humanState.fact_upsert(fact);
|
|
580
654
|
this.scheduleSave();
|
|
@@ -57,7 +57,7 @@ export function createReadMemoryExecutor(searchHumanData: SearchHumanData, getPe
|
|
|
57
57
|
const output: Record<string, unknown[]> = {};
|
|
58
58
|
if (results.facts.length > 0) output.facts = results.facts.map(f => ({ name: f.name, description: f.description }));
|
|
59
59
|
if (results.topics.length > 0) output.topics = results.topics.map(t => ({ name: t.name, description: t.description }));
|
|
60
|
-
if (results.people.length > 0) output.people = results.people.map(p => ({ name: p.name, relationship: p.relationship, description: p.description }));
|
|
60
|
+
if (results.people.length > 0) output.people = results.people.map(p => ({ name: p.name, relationship: p.relationship, description: p.description, identifiers: p.identifiers ?? [] }));
|
|
61
61
|
if (results.quotes.length > 0) output.quotes = results.quotes.map(q => ({ text: q.text, speaker: q.speaker }));
|
|
62
62
|
|
|
63
63
|
if (Object.keys(output).length === 0) {
|
|
@@ -15,6 +15,7 @@ export interface DataItemBase {
|
|
|
15
15
|
learned_by?: string; // Persona ID that originally learned this item (stable UUID)
|
|
16
16
|
last_changed_by?: string; // Persona ID that most recently updated this item (stable UUID)
|
|
17
17
|
interested_personas?: string[]; // Persona IDs that have extracted/touched this item (accumulated)
|
|
18
|
+
sources?: string[]; // Namespaced source identifiers — where items were learned from. Format: "provider:id" (e.g., "opencode:ses_abc123", "cursor:composerId"). Grow-only union.
|
|
18
19
|
persona_groups?: string[];
|
|
19
20
|
embedding?: number[];
|
|
20
21
|
rewrite_checked?: boolean; // True after rewrite scan finds no changes. Cleared automatically when extraction upserts a fresh item.
|
|
@@ -91,6 +91,14 @@ export interface ProviderAccount {
|
|
|
91
91
|
created_at: string; // ISO timestamp
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
export interface ThemeDefinition {
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
base?: string;
|
|
98
|
+
encoded: string;
|
|
99
|
+
created_at: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
export interface HumanSettings {
|
|
95
103
|
default_model?: string; // Will store ModelConfig.id GUID post-migration
|
|
96
104
|
oneshot_model?: string; // Model for AI-assist (wand) requests; falls back to default_model. Will store ModelConfig.id GUID post-migration.
|
|
@@ -111,6 +119,8 @@ export interface HumanSettings {
|
|
|
111
119
|
backup?: BackupConfig;
|
|
112
120
|
claudeCode?: import("../../integrations/claude-code/types.js").ClaudeCodeSettings;
|
|
113
121
|
cursor?: import("../../integrations/cursor/types.js").CursorSettings;
|
|
122
|
+
active_theme?: string;
|
|
123
|
+
custom_themes?: ThemeDefinition[];
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
export interface HumanEntity {
|
|
@@ -152,6 +162,9 @@ export interface PersonaEntity {
|
|
|
152
162
|
tools?: string[]; // IDs of ToolDefinitions this persona can use. Empty/absent = no tool access.
|
|
153
163
|
reflection_last_asked?: string; // ISO timestamp. Set ONLY when Persona explicitly surfaces identity drift (mentioned_reflection: true).
|
|
154
164
|
description_embedding?: number[]; // Embedding of long_description (short_description fallback). Excludes traits. See embedding-service.ts:getPersonaDescriptionText.
|
|
165
|
+
avatar_emoji?: string; // Single emoji character used as avatar in place of initials.
|
|
166
|
+
avatar_image?: string; // Base64-encoded 64×64 image used as avatar (takes priority over avatar_emoji).
|
|
167
|
+
preferred_theme?: string; // Theme ID (built-in name or ThemeDefinition.id). Applied to chat panel when this persona is active.
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
export interface PersonaCreationInput {
|
package/src/core/types/enums.ts
CHANGED
|
@@ -38,6 +38,7 @@ export enum LLMNextStep {
|
|
|
38
38
|
HandleHeartbeatCheck = "handleHeartbeatCheck",
|
|
39
39
|
HandleEiHeartbeat = "handleEiHeartbeat",
|
|
40
40
|
HandleOneShot = "handleOneShot",
|
|
41
|
+
HandleOneShotJSON = "handleOneShotJSON",
|
|
41
42
|
// Tool calling continuation (second LLM call after tool execution, may loop for more tool calls).
|
|
42
43
|
// data.toolHistory: serialized LLMHistoryMessage[] (assistant + tool result messages)
|
|
43
44
|
// data.toolCallCounts: serialized Map entries [[name, count], ...] carrying per-tool call counts
|
|
@@ -56,6 +56,9 @@ export interface PersonaSummary {
|
|
|
56
56
|
unread_count: number;
|
|
57
57
|
last_activity?: string;
|
|
58
58
|
context_boundary?: string;
|
|
59
|
+
avatar_emoji?: string;
|
|
60
|
+
avatar_image?: string;
|
|
61
|
+
preferred_theme?: string;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
export interface MessageQueryOptions {
|
|
@@ -99,6 +102,7 @@ export interface Ei_Interface {
|
|
|
99
102
|
onError?: (error: EiError) => void;
|
|
100
103
|
onStateImported?: () => void;
|
|
101
104
|
onOneShotReturned?: (guid: string, content: string) => void;
|
|
105
|
+
onOneShotJSONReturned?: (guid: string, parsed: unknown) => void;
|
|
102
106
|
onContextBoundaryChanged?: (personaId: string) => void;
|
|
103
107
|
onSaveAndExitStart?: () => void;
|
|
104
108
|
onSaveAndExitFinish?: () => void;
|
package/src/core/types/rooms.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface RoomEntity {
|
|
|
38
38
|
last_updated: string;
|
|
39
39
|
last_activity: string;
|
|
40
40
|
capture_used?: boolean;
|
|
41
|
+
context_window_hours?: number; // FFA only; falls back to human.settings.default_context_window_hours
|
|
42
|
+
context_boundary?: string; // FFA only; ISO timestamp; same semantics as persona context_boundary
|
|
41
43
|
messages: RoomMessage[];
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
import type { PersonIdentifier } from "../types/data-items.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
|
+
import { BUILT_IN_IDENTIFIER_TYPES } from "../constants/built-in-identifier-types.js";
|
|
3
4
|
|
|
4
5
|
export const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
6
|
|
|
7
|
+
function toNormalizedKey(s: string): string {
|
|
8
|
+
return s.replace(/[^a-z0-9]/gi, '').toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Fuzzy-matches LLM-provided type against built-in + in-use types (strip non-alphanumeric, lowercase).
|
|
12
|
+
// "nickname" -> "Nickname", "full_name" -> "Full Name", "EMAIL" -> "Email", "Slack RNP" -> "Slack RNP" (custom, no match)
|
|
13
|
+
export function normalizeIdentifierType(llmType: string, state: StateManager): string {
|
|
14
|
+
const inUseTypes = state.getHuman().people.flatMap(p =>
|
|
15
|
+
(p.identifiers ?? []).map(i => i.type)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const canonicalMap = new Map<string, string>();
|
|
19
|
+
for (const t of [...BUILT_IN_IDENTIFIER_TYPES, ...inUseTypes]) {
|
|
20
|
+
const key = toNormalizedKey(t);
|
|
21
|
+
if (!canonicalMap.has(key)) {
|
|
22
|
+
canonicalMap.set(key, t);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const normalized = toNormalizedKey(llmType);
|
|
27
|
+
return canonicalMap.get(normalized) ?? llmType;
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
export function sanitizeEiPersonaIdentifiers(
|
|
7
31
|
identifiers: PersonIdentifier[],
|
|
8
32
|
state: StateManager
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ThemeDefinition } from "../types/entities.js";
|
|
2
|
+
|
|
3
|
+
const VERSION = "v1";
|
|
4
|
+
const PREFIX = `ei-theme:${VERSION}:`;
|
|
5
|
+
const TOKEN_COUNT = 37;
|
|
6
|
+
const HEX_LENGTH = 6;
|
|
7
|
+
|
|
8
|
+
export const THEME_TOKEN_ORDER: readonly string[] = [
|
|
9
|
+
"bg-primary", "bg-secondary", "bg-tertiary",
|
|
10
|
+
"border", "border-light",
|
|
11
|
+
"text-primary", "text-secondary", "text-muted",
|
|
12
|
+
"accent", "accent-hover",
|
|
13
|
+
"success", "success-hover",
|
|
14
|
+
"warning", "warning-text",
|
|
15
|
+
"danger",
|
|
16
|
+
"status-thinking", "status-ready", "status-unread", "status-paused",
|
|
17
|
+
"room-cyp", "room-ffa", "room-map",
|
|
18
|
+
"archive-bg-start", "archive-bg-end", "archive-border",
|
|
19
|
+
"ai-assist-start", "ai-assist-end",
|
|
20
|
+
"code-bg", "code-bg-controls", "code-border",
|
|
21
|
+
"code-text", "code-text-muted",
|
|
22
|
+
"code-accent", "code-string", "code-error", "code-success", "code-special",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export const BUILT_IN_THEME_NAMES: readonly string[] = [
|
|
26
|
+
"default", "dark", "coder", "depressing", "cotton-candy",
|
|
27
|
+
"crimuh", "spoopy", "lovey-dovey", "lucky",
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
export type ThemeTokenMap = Record<string, string>;
|
|
31
|
+
|
|
32
|
+
export function encodeTheme(tokens: ThemeTokenMap): string {
|
|
33
|
+
const hex = THEME_TOKEN_ORDER.map((key) => {
|
|
34
|
+
const value = tokens[`--ei-${key}`] ?? tokens[key] ?? "000000";
|
|
35
|
+
return value.replace(/^#/, "").toLowerCase().padEnd(HEX_LENGTH, "0").slice(0, HEX_LENGTH);
|
|
36
|
+
}).join("");
|
|
37
|
+
return PREFIX + btoa(hex);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function decodeTheme(encoded: string): ThemeTokenMap | null {
|
|
41
|
+
if (!encoded.startsWith(PREFIX)) return null;
|
|
42
|
+
try {
|
|
43
|
+
const hex = atob(encoded.slice(PREFIX.length));
|
|
44
|
+
if (hex.length !== TOKEN_COUNT * HEX_LENGTH) return null;
|
|
45
|
+
const tokens: ThemeTokenMap = {};
|
|
46
|
+
for (let i = 0; i < TOKEN_COUNT; i++) {
|
|
47
|
+
const key = THEME_TOKEN_ORDER[i];
|
|
48
|
+
tokens[`--ei-${key}`] = `#${hex.slice(i * HEX_LENGTH, (i + 1) * HEX_LENGTH)}`;
|
|
49
|
+
}
|
|
50
|
+
return tokens;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function themeToStyleString(tokens: ThemeTokenMap): string {
|
|
57
|
+
return Object.entries(tokens)
|
|
58
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
59
|
+
.join("\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isBuiltInTheme(id: string): boolean {
|
|
63
|
+
return (BUILT_IN_THEME_NAMES as readonly string[]).includes(id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function makeThemeDefinition(
|
|
67
|
+
name: string,
|
|
68
|
+
tokens: ThemeTokenMap,
|
|
69
|
+
base?: string,
|
|
70
|
+
): ThemeDefinition {
|
|
71
|
+
return {
|
|
72
|
+
id: crypto.randomUUID(),
|
|
73
|
+
name,
|
|
74
|
+
base,
|
|
75
|
+
encoded: encodeTheme(tokens),
|
|
76
|
+
created_at: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
-
import type { Ei_Interface,
|
|
2
|
+
import type { Ei_Interface, Message, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
|
|
3
3
|
import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
|
|
4
4
|
import type { IClaudeCodeReader, ClaudeCodeSession, ClaudeCodeMessage } from "./types.js";
|
|
5
5
|
import {
|
|
6
6
|
CLAUDE_CODE_PERSONA_NAME,
|
|
7
|
-
CLAUDE_CODE_TOPIC_GROUPS,
|
|
8
7
|
MIN_SESSION_AGE_MS,
|
|
9
8
|
} from "./types.js";
|
|
10
9
|
import { ClaudeCodeReader } from "./reader.js";
|
|
@@ -20,8 +19,6 @@ import { isProcessRunning } from "../process-check.js";
|
|
|
20
19
|
|
|
21
20
|
export interface ClaudeCodeImportResult {
|
|
22
21
|
sessionsProcessed: number;
|
|
23
|
-
topicsCreated: number;
|
|
24
|
-
topicsUpdated: number;
|
|
25
22
|
messagesImported: number;
|
|
26
23
|
personaCreated: boolean;
|
|
27
24
|
extractionScansQueued: number;
|
|
@@ -109,47 +106,6 @@ function ensureClaudeCodePersona(
|
|
|
109
106
|
return persona;
|
|
110
107
|
}
|
|
111
108
|
|
|
112
|
-
// =============================================================================
|
|
113
|
-
// Topic Management
|
|
114
|
-
// =============================================================================
|
|
115
|
-
|
|
116
|
-
function ensureSessionTopic(
|
|
117
|
-
session: ClaudeCodeSession,
|
|
118
|
-
stateManager: StateManager
|
|
119
|
-
): "created" | "updated" | "unchanged" {
|
|
120
|
-
const human = stateManager.getHuman();
|
|
121
|
-
const existingTopic = human.topics.find((t) => t.id === session.id);
|
|
122
|
-
|
|
123
|
-
if (existingTopic) {
|
|
124
|
-
if (existingTopic.name !== session.title) {
|
|
125
|
-
const updatedTopic: Topic = {
|
|
126
|
-
...existingTopic,
|
|
127
|
-
name: session.title,
|
|
128
|
-
last_updated: new Date().toISOString(),
|
|
129
|
-
};
|
|
130
|
-
stateManager.human_topic_upsert(updatedTopic);
|
|
131
|
-
return "updated";
|
|
132
|
-
}
|
|
133
|
-
return "unchanged";
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const newTopic: Topic = {
|
|
137
|
-
id: session.id,
|
|
138
|
-
name: session.title,
|
|
139
|
-
description: `Claude Code session in ${session.cwd}`,
|
|
140
|
-
sentiment: 0,
|
|
141
|
-
exposure_current: 0.5,
|
|
142
|
-
exposure_desired: 0.3,
|
|
143
|
-
persona_groups: CLAUDE_CODE_TOPIC_GROUPS,
|
|
144
|
-
learned_by: stateManager.persona_getByName(CLAUDE_CODE_PERSONA_NAME)?.id ?? undefined,
|
|
145
|
-
last_updated: new Date().toISOString(),
|
|
146
|
-
learned_on: new Date().toISOString(),
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
stateManager.human_topic_upsert(newTopic);
|
|
150
|
-
return "created";
|
|
151
|
-
}
|
|
152
|
-
|
|
153
109
|
// =============================================================================
|
|
154
110
|
// State Helpers
|
|
155
111
|
// =============================================================================
|
|
@@ -205,26 +161,15 @@ export async function importClaudeCodeSessions(
|
|
|
205
161
|
|
|
206
162
|
const result: ClaudeCodeImportResult = {
|
|
207
163
|
sessionsProcessed: 0,
|
|
208
|
-
topicsCreated: 0,
|
|
209
|
-
topicsUpdated: 0,
|
|
210
164
|
messagesImported: 0,
|
|
211
165
|
personaCreated: false,
|
|
212
166
|
extractionScansQueued: 0,
|
|
213
167
|
};
|
|
214
168
|
|
|
215
|
-
// ─── Step 1:
|
|
169
|
+
// ─── Step 1: Get all sessions ─────────────────────────────────────────
|
|
216
170
|
const allSessions = await reader.getSessions();
|
|
217
171
|
|
|
218
|
-
for (const session of allSessions) {
|
|
219
|
-
const topicResult = ensureSessionTopic(session, stateManager);
|
|
220
|
-
if (topicResult === "created") result.topicsCreated++;
|
|
221
|
-
else if (topicResult === "updated") result.topicsUpdated++;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
172
|
if (signal?.aborted) return result;
|
|
225
|
-
if (result.topicsCreated > 0 || result.topicsUpdated > 0) {
|
|
226
|
-
eiInterface?.onHumanUpdated?.();
|
|
227
|
-
}
|
|
228
173
|
|
|
229
174
|
// ─── Step 2: Find next unprocessed session ────────────────────────────
|
|
230
175
|
const human = stateManager.getHuman();
|
|
@@ -323,6 +268,7 @@ export async function importClaudeCodeSessions(
|
|
|
323
268
|
personaDisplayName: persona.display_name,
|
|
324
269
|
messages_context: contextMsgs,
|
|
325
270
|
messages_analyze: toAnalyze,
|
|
271
|
+
sources: [`claudecode:${targetSession.id}`],
|
|
326
272
|
};
|
|
327
273
|
|
|
328
274
|
const ccSettings = stateManager.getHuman().settings?.claudeCode;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
-
import type { Ei_Interface,
|
|
2
|
+
import type { Ei_Interface, Message, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
|
|
3
3
|
import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
|
|
4
4
|
import type { ICursorReader, CursorSession, CursorMessage } from "./types.js";
|
|
5
5
|
import {
|
|
6
6
|
CURSOR_PERSONA_NAME,
|
|
7
|
-
CURSOR_TOPIC_GROUPS,
|
|
8
7
|
MIN_SESSION_AGE_MS,
|
|
9
8
|
} from "./types.js";
|
|
10
9
|
import { CursorReader } from "./reader.js";
|
|
@@ -16,8 +15,6 @@ import {
|
|
|
16
15
|
|
|
17
16
|
export interface CursorImportResult {
|
|
18
17
|
sessionsProcessed: number;
|
|
19
|
-
topicsCreated: number;
|
|
20
|
-
topicsUpdated: number;
|
|
21
18
|
messagesImported: number;
|
|
22
19
|
personaCreated: boolean;
|
|
23
20
|
extractionScansQueued: number;
|
|
@@ -97,43 +94,6 @@ function ensureCursorPersona(
|
|
|
97
94
|
return persona;
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
function ensureSessionTopic(
|
|
101
|
-
session: CursorSession,
|
|
102
|
-
stateManager: StateManager
|
|
103
|
-
): "created" | "updated" | "unchanged" {
|
|
104
|
-
const human = stateManager.getHuman();
|
|
105
|
-
const existingTopic = human.topics.find((t) => t.id === session.id);
|
|
106
|
-
|
|
107
|
-
if (existingTopic) {
|
|
108
|
-
if (existingTopic.name !== session.name) {
|
|
109
|
-
const updatedTopic: Topic = {
|
|
110
|
-
...existingTopic,
|
|
111
|
-
name: session.name,
|
|
112
|
-
last_updated: new Date().toISOString(),
|
|
113
|
-
};
|
|
114
|
-
stateManager.human_topic_upsert(updatedTopic);
|
|
115
|
-
return "updated";
|
|
116
|
-
}
|
|
117
|
-
return "unchanged";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const newTopic: Topic = {
|
|
121
|
-
id: session.id,
|
|
122
|
-
name: session.name,
|
|
123
|
-
description: `Cursor session in ${session.workspacePath}`,
|
|
124
|
-
sentiment: 0,
|
|
125
|
-
exposure_current: 0.5,
|
|
126
|
-
exposure_desired: 0.3,
|
|
127
|
-
persona_groups: CURSOR_TOPIC_GROUPS,
|
|
128
|
-
learned_by: stateManager.persona_getByName(CURSOR_PERSONA_NAME)?.id ?? undefined,
|
|
129
|
-
last_updated: new Date().toISOString(),
|
|
130
|
-
learned_on: new Date().toISOString(),
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
stateManager.human_topic_upsert(newTopic);
|
|
134
|
-
return "created";
|
|
135
|
-
}
|
|
136
|
-
|
|
137
97
|
function updateProcessedState(
|
|
138
98
|
stateManager: StateManager,
|
|
139
99
|
session: CursorSession
|
|
@@ -173,8 +133,6 @@ export async function importCursorSessions(
|
|
|
173
133
|
|
|
174
134
|
const result: CursorImportResult = {
|
|
175
135
|
sessionsProcessed: 0,
|
|
176
|
-
topicsCreated: 0,
|
|
177
|
-
topicsUpdated: 0,
|
|
178
136
|
messagesImported: 0,
|
|
179
137
|
personaCreated: false,
|
|
180
138
|
extractionScansQueued: 0,
|
|
@@ -182,16 +140,7 @@ export async function importCursorSessions(
|
|
|
182
140
|
|
|
183
141
|
const allSessions = await reader.getSessions();
|
|
184
142
|
|
|
185
|
-
for (const session of allSessions) {
|
|
186
|
-
const topicResult = ensureSessionTopic(session, stateManager);
|
|
187
|
-
if (topicResult === "created") result.topicsCreated++;
|
|
188
|
-
else if (topicResult === "updated") result.topicsUpdated++;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
143
|
if (signal?.aborted) return result;
|
|
192
|
-
if (result.topicsCreated > 0 || result.topicsUpdated > 0) {
|
|
193
|
-
eiInterface?.onHumanUpdated?.();
|
|
194
|
-
}
|
|
195
144
|
|
|
196
145
|
const human = stateManager.getHuman();
|
|
197
146
|
const processedSessions = human.settings?.cursor?.processed_sessions ?? {};
|
|
@@ -278,6 +227,7 @@ export async function importCursorSessions(
|
|
|
278
227
|
personaDisplayName: persona.display_name,
|
|
279
228
|
messages_context: contextMsgs,
|
|
280
229
|
messages_analyze: toAnalyze,
|
|
230
|
+
sources: [`cursor:${targetSession.id}`],
|
|
281
231
|
};
|
|
282
232
|
|
|
283
233
|
queueAllScans(context, stateManager, { external_filter: "only" });
|