ei-tui 0.1.3
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/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +63 -0
- package/src/README.md +96 -0
- package/src/cli/README.md +47 -0
- package/src/cli/commands/facts.ts +25 -0
- package/src/cli/commands/people.ts +25 -0
- package/src/cli/commands/quotes.ts +19 -0
- package/src/cli/commands/topics.ts +25 -0
- package/src/cli/commands/traits.ts +25 -0
- package/src/cli/retrieval.ts +269 -0
- package/src/cli.ts +176 -0
- package/src/core/AGENTS.md +104 -0
- package/src/core/embedding-service.ts +241 -0
- package/src/core/handlers/index.ts +1057 -0
- package/src/core/index.ts +4 -0
- package/src/core/llm-client.ts +265 -0
- package/src/core/model-context-windows.ts +49 -0
- package/src/core/orchestrators/ceremony.ts +500 -0
- package/src/core/orchestrators/extraction-chunker.ts +138 -0
- package/src/core/orchestrators/human-extraction.ts +457 -0
- package/src/core/orchestrators/index.ts +28 -0
- package/src/core/orchestrators/persona-generation.ts +76 -0
- package/src/core/orchestrators/persona-topics.ts +117 -0
- package/src/core/personas/index.ts +5 -0
- package/src/core/personas/opencode-agent.ts +81 -0
- package/src/core/processor.ts +1413 -0
- package/src/core/queue-processor.ts +197 -0
- package/src/core/state/checkpoints.ts +68 -0
- package/src/core/state/human.ts +176 -0
- package/src/core/state/index.ts +5 -0
- package/src/core/state/personas.ts +217 -0
- package/src/core/state/queue.ts +144 -0
- package/src/core/state-manager.ts +347 -0
- package/src/core/types.ts +421 -0
- package/src/core/utils/decay.ts +33 -0
- package/src/index.ts +1 -0
- package/src/integrations/opencode/importer.ts +896 -0
- package/src/integrations/opencode/index.ts +16 -0
- package/src/integrations/opencode/json-reader.ts +304 -0
- package/src/integrations/opencode/reader-factory.ts +35 -0
- package/src/integrations/opencode/sqlite-reader.ts +189 -0
- package/src/integrations/opencode/types.ts +244 -0
- package/src/prompts/AGENTS.md +62 -0
- package/src/prompts/ceremony/description-check.ts +47 -0
- package/src/prompts/ceremony/expire.ts +30 -0
- package/src/prompts/ceremony/explore.ts +60 -0
- package/src/prompts/ceremony/index.ts +11 -0
- package/src/prompts/ceremony/types.ts +42 -0
- package/src/prompts/generation/descriptions.ts +91 -0
- package/src/prompts/generation/index.ts +15 -0
- package/src/prompts/generation/persona.ts +155 -0
- package/src/prompts/generation/seeds.ts +31 -0
- package/src/prompts/generation/types.ts +47 -0
- package/src/prompts/heartbeat/check.ts +179 -0
- package/src/prompts/heartbeat/ei.ts +208 -0
- package/src/prompts/heartbeat/index.ts +15 -0
- package/src/prompts/heartbeat/types.ts +70 -0
- package/src/prompts/human/fact-scan.ts +152 -0
- package/src/prompts/human/index.ts +32 -0
- package/src/prompts/human/item-match.ts +74 -0
- package/src/prompts/human/item-update.ts +322 -0
- package/src/prompts/human/person-scan.ts +115 -0
- package/src/prompts/human/topic-scan.ts +135 -0
- package/src/prompts/human/trait-scan.ts +115 -0
- package/src/prompts/human/types.ts +127 -0
- package/src/prompts/index.ts +90 -0
- package/src/prompts/message-utils.ts +39 -0
- package/src/prompts/persona/index.ts +16 -0
- package/src/prompts/persona/topics-match.ts +69 -0
- package/src/prompts/persona/topics-scan.ts +98 -0
- package/src/prompts/persona/topics-update.ts +157 -0
- package/src/prompts/persona/traits.ts +117 -0
- package/src/prompts/persona/types.ts +74 -0
- package/src/prompts/response/index.ts +147 -0
- package/src/prompts/response/sections.ts +355 -0
- package/src/prompts/response/types.ts +38 -0
- package/src/prompts/validation/ei.ts +93 -0
- package/src/prompts/validation/index.ts +6 -0
- package/src/prompts/validation/types.ts +22 -0
- package/src/storage/crypto.ts +96 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/interface.ts +9 -0
- package/src/storage/local.ts +79 -0
- package/src/storage/merge.ts +69 -0
- package/src/storage/remote.ts +145 -0
- package/src/templates/welcome.ts +91 -0
- package/tui/README.md +62 -0
- package/tui/bunfig.toml +4 -0
- package/tui/src/app.tsx +55 -0
- package/tui/src/commands/archive.tsx +93 -0
- package/tui/src/commands/context.tsx +124 -0
- package/tui/src/commands/delete.tsx +71 -0
- package/tui/src/commands/details.tsx +41 -0
- package/tui/src/commands/editor.tsx +46 -0
- package/tui/src/commands/help.tsx +12 -0
- package/tui/src/commands/me.tsx +145 -0
- package/tui/src/commands/model.ts +47 -0
- package/tui/src/commands/new.ts +31 -0
- package/tui/src/commands/pause.ts +46 -0
- package/tui/src/commands/persona.tsx +58 -0
- package/tui/src/commands/provider.tsx +124 -0
- package/tui/src/commands/quit.ts +22 -0
- package/tui/src/commands/quotes.tsx +172 -0
- package/tui/src/commands/registry.test.ts +137 -0
- package/tui/src/commands/registry.ts +130 -0
- package/tui/src/commands/resume.ts +39 -0
- package/tui/src/commands/setsync.tsx +43 -0
- package/tui/src/commands/settings.tsx +83 -0
- package/tui/src/components/ConfirmOverlay.tsx +51 -0
- package/tui/src/components/ConflictOverlay.tsx +78 -0
- package/tui/src/components/HelpOverlay.tsx +69 -0
- package/tui/src/components/Layout.tsx +24 -0
- package/tui/src/components/MessageList.tsx +174 -0
- package/tui/src/components/PersonaListOverlay.tsx +186 -0
- package/tui/src/components/PromptInput.tsx +145 -0
- package/tui/src/components/ProviderListOverlay.tsx +208 -0
- package/tui/src/components/QuotesOverlay.tsx +157 -0
- package/tui/src/components/Sidebar.tsx +95 -0
- package/tui/src/components/StatusBar.tsx +77 -0
- package/tui/src/components/WelcomeOverlay.tsx +73 -0
- package/tui/src/context/ei.tsx +623 -0
- package/tui/src/context/keyboard.tsx +164 -0
- package/tui/src/context/overlay.tsx +53 -0
- package/tui/src/index.tsx +8 -0
- package/tui/src/storage/file.ts +185 -0
- package/tui/src/util/duration.ts +32 -0
- package/tui/src/util/editor.ts +188 -0
- package/tui/src/util/logger.ts +109 -0
- package/tui/src/util/persona-editor.tsx +181 -0
- package/tui/src/util/provider-editor.tsx +168 -0
- package/tui/src/util/syntax.ts +35 -0
- package/tui/src/util/yaml-serializers.ts +755 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { LLMRequest, LLMResponse, LLMRequestType, ProviderAccount, ChatMessage, Message } from "./types.js";
|
|
2
|
+
import { callLLMRaw, parseJSONResponse, cleanResponseContent } from "./llm-client.js";
|
|
3
|
+
import { hydratePromptPlaceholders } from "../prompts/message-utils.js";
|
|
4
|
+
|
|
5
|
+
type QueueProcessorState = "idle" | "busy";
|
|
6
|
+
type ResponseCallback = (response: LLMResponse) => Promise<void>;
|
|
7
|
+
type MessageFetcher = (personaId: string) => ChatMessage[];
|
|
8
|
+
type RawMessageFetcher = (personaId: string) => Message[];
|
|
9
|
+
|
|
10
|
+
export interface QueueProcessorStartOptions {
|
|
11
|
+
accounts?: ProviderAccount[];
|
|
12
|
+
messageFetcher?: MessageFetcher;
|
|
13
|
+
rawMessageFetcher?: RawMessageFetcher;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class QueueProcessor {
|
|
17
|
+
private state: QueueProcessorState = "idle";
|
|
18
|
+
private abortController: AbortController | null = null;
|
|
19
|
+
private currentCallback: ResponseCallback | null = null;
|
|
20
|
+
private currentAccounts: ProviderAccount[] | undefined;
|
|
21
|
+
private currentMessageFetcher: MessageFetcher | undefined;
|
|
22
|
+
private currentRawMessageFetcher: RawMessageFetcher | undefined;
|
|
23
|
+
|
|
24
|
+
getState(): QueueProcessorState {
|
|
25
|
+
return this.state;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
start(request: LLMRequest, callback: ResponseCallback, options?: QueueProcessorStartOptions): void {
|
|
29
|
+
if (this.state !== "idle") {
|
|
30
|
+
throw new Error("QUEUE_BUSY: QueueProcessor is already processing a request");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.state = "busy";
|
|
34
|
+
this.currentCallback = callback;
|
|
35
|
+
this.currentAccounts = options?.accounts;
|
|
36
|
+
this.currentMessageFetcher = options?.messageFetcher;
|
|
37
|
+
this.currentRawMessageFetcher = options?.rawMessageFetcher;
|
|
38
|
+
this.abortController = new AbortController();
|
|
39
|
+
|
|
40
|
+
this.processRequest(request)
|
|
41
|
+
.then((response) => {
|
|
42
|
+
this.finishWith(response);
|
|
43
|
+
})
|
|
44
|
+
.catch((error) => {
|
|
45
|
+
this.finishWith({
|
|
46
|
+
request,
|
|
47
|
+
success: false,
|
|
48
|
+
content: null,
|
|
49
|
+
error: error instanceof Error ? error.message : String(error),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
abort(): void {
|
|
55
|
+
if (this.abortController) {
|
|
56
|
+
this.abortController.abort();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private finishWith(response: LLMResponse): void {
|
|
61
|
+
const mortalKombat = this.currentCallback ? this.currentCallback : () => Promise.resolve();
|
|
62
|
+
mortalKombat(response).finally(() => {
|
|
63
|
+
this.state = "idle";
|
|
64
|
+
this.currentCallback = null;
|
|
65
|
+
this.currentAccounts = undefined;
|
|
66
|
+
this.currentMessageFetcher = undefined;
|
|
67
|
+
this.currentRawMessageFetcher = undefined;
|
|
68
|
+
this.abortController = null;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async processRequest(request: LLMRequest): Promise<LLMResponse> {
|
|
73
|
+
let messages: ChatMessage[] = [];
|
|
74
|
+
|
|
75
|
+
if (request.type === "response" as LLMRequestType) {
|
|
76
|
+
const personaId = request.data.personaId as string | undefined;
|
|
77
|
+
if (personaId && this.currentMessageFetcher) {
|
|
78
|
+
messages = this.currentMessageFetcher(personaId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let hydratedSystem = request.system;
|
|
83
|
+
let hydratedUser = request.user;
|
|
84
|
+
|
|
85
|
+
if (this.currentRawMessageFetcher) {
|
|
86
|
+
const personaId = request.data.personaId as string | undefined;
|
|
87
|
+
if (personaId) {
|
|
88
|
+
const rawMessages = this.currentRawMessageFetcher(personaId);
|
|
89
|
+
const messageMap = new Map<string, Message>();
|
|
90
|
+
for (const msg of rawMessages) {
|
|
91
|
+
messageMap.set(msg.id, msg);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const placeholderCount = (request.user.match(/\[mid:[^\]]+\]/g) || []).length;
|
|
95
|
+
console.log(`[QueueProcessor] Hydrating ${placeholderCount} placeholders with ${messageMap.size} messages for ${personaId}`);
|
|
96
|
+
|
|
97
|
+
hydratedSystem = hydratePromptPlaceholders(request.system, messageMap);
|
|
98
|
+
hydratedUser = hydratePromptPlaceholders(request.user, messageMap);
|
|
99
|
+
|
|
100
|
+
const hydratedPlaceholderCount = (hydratedUser.match(/\[mid:[^\]]+\]/g) || []).length;
|
|
101
|
+
if (hydratedPlaceholderCount > 0) {
|
|
102
|
+
console.log(`[QueueProcessor] WARNING: ${hydratedPlaceholderCount} placeholders not hydrated!`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const { content, finishReason } = await callLLMRaw(
|
|
108
|
+
hydratedSystem,
|
|
109
|
+
hydratedUser,
|
|
110
|
+
messages,
|
|
111
|
+
request.model,
|
|
112
|
+
{ signal: this.abortController?.signal },
|
|
113
|
+
this.currentAccounts
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (!content) {
|
|
117
|
+
return {
|
|
118
|
+
request,
|
|
119
|
+
success: false,
|
|
120
|
+
content: null,
|
|
121
|
+
error: "Empty response from LLM",
|
|
122
|
+
finish_reason: finishReason ?? undefined,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return this.handleResponseType(request, content, finishReason);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private handleResponseType(
|
|
130
|
+
request: LLMRequest,
|
|
131
|
+
content: string,
|
|
132
|
+
finishReason: string | null
|
|
133
|
+
): LLMResponse {
|
|
134
|
+
switch (request.type) {
|
|
135
|
+
case "json" as LLMRequestType:
|
|
136
|
+
return this.handleJSONResponse(request, content, finishReason);
|
|
137
|
+
case "response" as LLMRequestType:
|
|
138
|
+
return this.handleConversationResponse(request, content, finishReason);
|
|
139
|
+
case "raw" as LLMRequestType:
|
|
140
|
+
default:
|
|
141
|
+
return {
|
|
142
|
+
request,
|
|
143
|
+
success: true,
|
|
144
|
+
content,
|
|
145
|
+
finish_reason: finishReason ?? undefined,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private handleJSONResponse(
|
|
151
|
+
request: LLMRequest,
|
|
152
|
+
content: string,
|
|
153
|
+
finishReason: string | null
|
|
154
|
+
): LLMResponse {
|
|
155
|
+
try {
|
|
156
|
+
const parsed = parseJSONResponse(content);
|
|
157
|
+
return {
|
|
158
|
+
request,
|
|
159
|
+
success: true,
|
|
160
|
+
content,
|
|
161
|
+
parsed,
|
|
162
|
+
finish_reason: finishReason ?? undefined,
|
|
163
|
+
};
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return {
|
|
166
|
+
request,
|
|
167
|
+
success: false,
|
|
168
|
+
content,
|
|
169
|
+
error: `JSON parse failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
170
|
+
finish_reason: finishReason ?? undefined,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private handleConversationResponse(
|
|
176
|
+
request: LLMRequest,
|
|
177
|
+
content: string,
|
|
178
|
+
finishReason: string | null
|
|
179
|
+
): LLMResponse {
|
|
180
|
+
const cleaned = cleanResponseContent(content);
|
|
181
|
+
|
|
182
|
+
const noMessagePatterns = [
|
|
183
|
+
/^no\s*(new\s*)?(message|response)/i,
|
|
184
|
+
/^nothing\s+to\s+(say|add)/i,
|
|
185
|
+
/^\[no\s+message\]/i,
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const isNoMessage = noMessagePatterns.some((p) => p.test(cleaned));
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
request,
|
|
192
|
+
success: true,
|
|
193
|
+
content: isNoMessage ? null : cleaned,
|
|
194
|
+
finish_reason: finishReason ?? undefined,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { StorageState } from "../types.js";
|
|
2
|
+
import type { Storage } from "../../storage/interface.js";
|
|
3
|
+
|
|
4
|
+
const DEBOUNCE_MS = 100;
|
|
5
|
+
|
|
6
|
+
export class PersistenceState {
|
|
7
|
+
private storage: Storage | null = null;
|
|
8
|
+
private saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
9
|
+
private pendingState: StorageState | null = null;
|
|
10
|
+
private loadedExistingData = false;
|
|
11
|
+
|
|
12
|
+
setStorage(storage: Storage): void {
|
|
13
|
+
this.storage = storage;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
scheduleSave(state: StorageState): void {
|
|
17
|
+
this.pendingState = state;
|
|
18
|
+
|
|
19
|
+
if (this.saveTimeout) {
|
|
20
|
+
clearTimeout(this.saveTimeout);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.saveTimeout = setTimeout(async () => {
|
|
24
|
+
this.saveTimeout = null;
|
|
25
|
+
if (this.pendingState) {
|
|
26
|
+
await this.saveNow(this.pendingState);
|
|
27
|
+
this.pendingState = null;
|
|
28
|
+
}
|
|
29
|
+
}, DEBOUNCE_MS);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async saveNow(state: StorageState): Promise<void> {
|
|
33
|
+
if (!this.storage) throw new Error("Storage not initialized");
|
|
34
|
+
await this.storage.save(state);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async load(): Promise<StorageState | null> {
|
|
38
|
+
if (!this.storage) throw new Error("Storage not initialized");
|
|
39
|
+
const state = await this.storage.load();
|
|
40
|
+
this.loadedExistingData = state !== null;
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
hasExistingData(): boolean {
|
|
45
|
+
return this.loadedExistingData;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async flush(): Promise<void> {
|
|
49
|
+
if (this.saveTimeout) {
|
|
50
|
+
clearTimeout(this.saveTimeout);
|
|
51
|
+
this.saveTimeout = null;
|
|
52
|
+
}
|
|
53
|
+
if (this.pendingState && this.storage) {
|
|
54
|
+
await this.storage.save(this.pendingState);
|
|
55
|
+
this.pendingState = null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async moveToBackup(): Promise<void> {
|
|
60
|
+
if (!this.storage) throw new Error("Storage not initialized");
|
|
61
|
+
await this.storage.moveToBackup();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async loadBackup(): Promise<StorageState | null> {
|
|
65
|
+
if (!this.storage) throw new Error("Storage not initialized");
|
|
66
|
+
return this.storage.loadBackup();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { HumanEntity, Fact, Trait, Topic, Person, Quote } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export function createDefaultHumanEntity(): HumanEntity {
|
|
4
|
+
return {
|
|
5
|
+
entity: "human",
|
|
6
|
+
facts: [],
|
|
7
|
+
traits: [],
|
|
8
|
+
topics: [],
|
|
9
|
+
people: [],
|
|
10
|
+
quotes: [],
|
|
11
|
+
last_updated: new Date().toISOString(),
|
|
12
|
+
last_activity: new Date().toISOString(),
|
|
13
|
+
settings: {
|
|
14
|
+
ceremony: {
|
|
15
|
+
time: "09:00",
|
|
16
|
+
},
|
|
17
|
+
opencode: {
|
|
18
|
+
integration: false,
|
|
19
|
+
polling_interval_ms: 1800000,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class HumanState {
|
|
26
|
+
private human: HumanEntity = createDefaultHumanEntity();
|
|
27
|
+
|
|
28
|
+
load(entity: HumanEntity): void {
|
|
29
|
+
this.human = entity;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get(): HumanEntity {
|
|
33
|
+
return this.human;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set(entity: HumanEntity): void {
|
|
37
|
+
this.human = entity;
|
|
38
|
+
this.human.last_updated = new Date().toISOString();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fact_upsert(fact: Fact): void {
|
|
42
|
+
const idx = this.human.facts.findIndex((f) => f.id === fact.id);
|
|
43
|
+
fact.last_updated = new Date().toISOString();
|
|
44
|
+
if (idx >= 0) {
|
|
45
|
+
this.human.facts[idx] = fact;
|
|
46
|
+
} else {
|
|
47
|
+
this.human.facts.push(fact);
|
|
48
|
+
}
|
|
49
|
+
this.human.last_updated = new Date().toISOString();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fact_remove(id: string): boolean {
|
|
53
|
+
const idx = this.human.facts.findIndex((f) => f.id === id);
|
|
54
|
+
if (idx >= 0) {
|
|
55
|
+
this.human.facts.splice(idx, 1);
|
|
56
|
+
// Clean up quote references
|
|
57
|
+
this.human.quotes.forEach((q) => {
|
|
58
|
+
q.data_item_ids = q.data_item_ids.filter((itemId) => itemId !== id);
|
|
59
|
+
});
|
|
60
|
+
this.human.last_updated = new Date().toISOString();
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
trait_upsert(trait: Trait): void {
|
|
67
|
+
const idx = this.human.traits.findIndex((t) => t.id === trait.id);
|
|
68
|
+
trait.last_updated = new Date().toISOString();
|
|
69
|
+
if (idx >= 0) {
|
|
70
|
+
this.human.traits[idx] = trait;
|
|
71
|
+
} else {
|
|
72
|
+
this.human.traits.push(trait);
|
|
73
|
+
}
|
|
74
|
+
this.human.last_updated = new Date().toISOString();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
trait_remove(id: string): boolean {
|
|
78
|
+
const idx = this.human.traits.findIndex((t) => t.id === id);
|
|
79
|
+
if (idx >= 0) {
|
|
80
|
+
this.human.traits.splice(idx, 1);
|
|
81
|
+
// Clean up quote references
|
|
82
|
+
this.human.quotes.forEach((q) => {
|
|
83
|
+
q.data_item_ids = q.data_item_ids.filter((itemId) => itemId !== id);
|
|
84
|
+
});
|
|
85
|
+
this.human.last_updated = new Date().toISOString();
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
topic_upsert(topic: Topic): void {
|
|
92
|
+
const idx = this.human.topics.findIndex((t) => t.id === topic.id);
|
|
93
|
+
topic.last_updated = new Date().toISOString();
|
|
94
|
+
if (idx >= 0) {
|
|
95
|
+
this.human.topics[idx] = topic;
|
|
96
|
+
} else {
|
|
97
|
+
this.human.topics.push(topic);
|
|
98
|
+
}
|
|
99
|
+
this.human.last_updated = new Date().toISOString();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
topic_remove(id: string): boolean {
|
|
103
|
+
const idx = this.human.topics.findIndex((t) => t.id === id);
|
|
104
|
+
if (idx >= 0) {
|
|
105
|
+
this.human.topics.splice(idx, 1);
|
|
106
|
+
// Clean up quote references
|
|
107
|
+
this.human.quotes.forEach((q) => {
|
|
108
|
+
q.data_item_ids = q.data_item_ids.filter((itemId) => itemId !== id);
|
|
109
|
+
});
|
|
110
|
+
this.human.last_updated = new Date().toISOString();
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
person_upsert(person: Person): void {
|
|
117
|
+
const idx = this.human.people.findIndex((p) => p.id === person.id);
|
|
118
|
+
person.last_updated = new Date().toISOString();
|
|
119
|
+
if (idx >= 0) {
|
|
120
|
+
this.human.people[idx] = person;
|
|
121
|
+
} else {
|
|
122
|
+
this.human.people.push(person);
|
|
123
|
+
}
|
|
124
|
+
this.human.last_updated = new Date().toISOString();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
person_remove(id: string): boolean {
|
|
128
|
+
const idx = this.human.people.findIndex((p) => p.id === id);
|
|
129
|
+
if (idx >= 0) {
|
|
130
|
+
this.human.people.splice(idx, 1);
|
|
131
|
+
// Clean up quote references
|
|
132
|
+
this.human.quotes.forEach((q) => {
|
|
133
|
+
q.data_item_ids = q.data_item_ids.filter((itemId) => itemId !== id);
|
|
134
|
+
});
|
|
135
|
+
this.human.last_updated = new Date().toISOString();
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
quote_add(quote: Quote): void {
|
|
142
|
+
if (!quote.created_at) {
|
|
143
|
+
quote.created_at = new Date().toISOString();
|
|
144
|
+
}
|
|
145
|
+
this.human.quotes.push(quote);
|
|
146
|
+
this.human.last_updated = new Date().toISOString();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
quote_update(id: string, updates: Partial<Quote>): boolean {
|
|
150
|
+
const idx = this.human.quotes.findIndex((q) => q.id === id);
|
|
151
|
+
if (idx >= 0) {
|
|
152
|
+
this.human.quotes[idx] = { ...this.human.quotes[idx], ...updates };
|
|
153
|
+
this.human.last_updated = new Date().toISOString();
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
quote_remove(id: string): boolean {
|
|
160
|
+
const idx = this.human.quotes.findIndex((q) => q.id === id);
|
|
161
|
+
if (idx >= 0) {
|
|
162
|
+
this.human.quotes.splice(idx, 1);
|
|
163
|
+
this.human.last_updated = new Date().toISOString();
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
quote_getForMessage(messageId: string): Quote[] {
|
|
170
|
+
return this.human.quotes.filter((q) => q.message_id === messageId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
quote_getForDataItem(dataItemId: string): Quote[] {
|
|
174
|
+
return this.human.quotes.filter((q) => q.data_item_ids.includes(dataItemId));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { PersonaEntity, Message, ContextStatus } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export interface PersonaData {
|
|
4
|
+
entity: PersonaEntity;
|
|
5
|
+
messages: Message[];
|
|
6
|
+
contextWindow?: { start: string; end: string };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class PersonaState {
|
|
10
|
+
private personas: Map<string, PersonaData> = new Map();
|
|
11
|
+
|
|
12
|
+
load(personas: Record<string, { entity: PersonaEntity; messages: Message[] }>): void {
|
|
13
|
+
this.personas = new Map(
|
|
14
|
+
Object.entries(personas).map(([id, data]) => [
|
|
15
|
+
id,
|
|
16
|
+
{ entity: data.entity, messages: data.messages },
|
|
17
|
+
])
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export(): Record<string, { entity: PersonaEntity; messages: Message[] }> {
|
|
22
|
+
const result: Record<string, { entity: PersonaEntity; messages: Message[] }> = {};
|
|
23
|
+
for (const [id, data] of this.personas) {
|
|
24
|
+
result[id] = { entity: data.entity, messages: data.messages };
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getAll(): PersonaEntity[] {
|
|
30
|
+
return Array.from(this.personas.values()).map((p) => p.entity);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getById(id: string): PersonaEntity | null {
|
|
34
|
+
return this.personas.get(id)?.entity ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getByName(nameOrAlias: string): PersonaEntity | null {
|
|
38
|
+
const searchTerm = nameOrAlias.toLowerCase();
|
|
39
|
+
|
|
40
|
+
// Priority 1: Exact display_name match
|
|
41
|
+
for (const data of this.personas.values()) {
|
|
42
|
+
if (data.entity.display_name.toLowerCase() === searchTerm) {
|
|
43
|
+
return data.entity;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Priority 2: Exact alias match
|
|
48
|
+
for (const data of this.personas.values()) {
|
|
49
|
+
if (data.entity.aliases?.some(alias => alias.toLowerCase() === searchTerm)) {
|
|
50
|
+
return data.entity;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Priority 3: Unambiguous partial match
|
|
55
|
+
const partialMatches: PersonaEntity[] = [];
|
|
56
|
+
for (const data of this.personas.values()) {
|
|
57
|
+
const displayNameLower = data.entity.display_name.toLowerCase();
|
|
58
|
+
const aliasesLower = data.entity.aliases?.map(a => a.toLowerCase()) ?? [];
|
|
59
|
+
|
|
60
|
+
const matchesDisplayName = displayNameLower.includes(searchTerm);
|
|
61
|
+
const matchesAlias = aliasesLower.some(alias => alias.includes(searchTerm));
|
|
62
|
+
|
|
63
|
+
if (matchesDisplayName || matchesAlias) {
|
|
64
|
+
partialMatches.push(data.entity);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return partialMatches.length === 1 ? partialMatches[0] : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
add(entity: PersonaEntity): void {
|
|
72
|
+
this.personas.set(entity.id, { entity, messages: [] });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
update(personaId: string, updates: Partial<PersonaEntity>): boolean {
|
|
76
|
+
const data = this.personas.get(personaId);
|
|
77
|
+
if (!data) return false;
|
|
78
|
+
data.entity = { ...data.entity, ...updates, last_updated: new Date().toISOString() };
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
archive(personaId: string): boolean {
|
|
83
|
+
const data = this.personas.get(personaId);
|
|
84
|
+
if (!data) return false;
|
|
85
|
+
data.entity.is_archived = true;
|
|
86
|
+
data.entity.archived_at = new Date().toISOString();
|
|
87
|
+
data.entity.last_updated = new Date().toISOString();
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
unarchive(personaId: string): boolean {
|
|
92
|
+
const data = this.personas.get(personaId);
|
|
93
|
+
if (!data) return false;
|
|
94
|
+
data.entity.is_archived = false;
|
|
95
|
+
data.entity.archived_at = undefined;
|
|
96
|
+
data.entity.last_updated = new Date().toISOString();
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
delete(personaId: string): boolean {
|
|
101
|
+
return this.personas.delete(personaId);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
messages_get(personaId: string): Message[] {
|
|
105
|
+
const messages = this.personas.get(personaId)?.messages ?? [];
|
|
106
|
+
return messages.map(m => ({ ...m }));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
messages_append(personaId: string, message: Message): void {
|
|
110
|
+
const data = this.personas.get(personaId);
|
|
111
|
+
if (!data) return;
|
|
112
|
+
data.messages.push(message);
|
|
113
|
+
data.entity.last_activity = message.timestamp;
|
|
114
|
+
data.entity.last_updated = new Date().toISOString();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
messages_sort(personaId: string): void {
|
|
118
|
+
const data = this.personas.get(personaId);
|
|
119
|
+
if (!data) return;
|
|
120
|
+
data.messages.sort((a, b) =>
|
|
121
|
+
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
messages_setContextStatus(
|
|
126
|
+
personaId: string,
|
|
127
|
+
messageId: string,
|
|
128
|
+
status: ContextStatus
|
|
129
|
+
): boolean {
|
|
130
|
+
const data = this.personas.get(personaId);
|
|
131
|
+
if (!data) return false;
|
|
132
|
+
const msg = data.messages.find((m) => m.id === messageId);
|
|
133
|
+
if (!msg) return false;
|
|
134
|
+
msg.context_status = status;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
messages_markRead(personaId: string, messageId: string): boolean {
|
|
139
|
+
const data = this.personas.get(personaId);
|
|
140
|
+
if (!data) return false;
|
|
141
|
+
const msg = data.messages.find((m) => m.id === messageId);
|
|
142
|
+
if (!msg) return false;
|
|
143
|
+
msg.read = true;
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
messages_markPendingAsRead(personaId: string): number {
|
|
148
|
+
const data = this.personas.get(personaId);
|
|
149
|
+
if (!data) return 0;
|
|
150
|
+
let count = 0;
|
|
151
|
+
for (const msg of data.messages) {
|
|
152
|
+
if (msg.role === "human" && !msg.read) {
|
|
153
|
+
msg.read = true;
|
|
154
|
+
count++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return count;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
messages_countUnread(personaId: string): number {
|
|
161
|
+
const data = this.personas.get(personaId);
|
|
162
|
+
if (!data) return 0;
|
|
163
|
+
return data.messages.filter(m => m.role === "system" && !m.read).length;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
messages_markAllRead(personaId: string): number {
|
|
167
|
+
const data = this.personas.get(personaId);
|
|
168
|
+
if (!data) return 0;
|
|
169
|
+
let count = 0;
|
|
170
|
+
for (const msg of data.messages) {
|
|
171
|
+
if (!msg.read) {
|
|
172
|
+
msg.read = true;
|
|
173
|
+
count++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return count;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
messages_remove(personaId: string, messageIds: string[]): Message[] {
|
|
180
|
+
const data = this.personas.get(personaId);
|
|
181
|
+
if (!data) return [];
|
|
182
|
+
const idsSet = new Set(messageIds);
|
|
183
|
+
const removed: Message[] = [];
|
|
184
|
+
data.messages = data.messages.filter((m) => {
|
|
185
|
+
if (idsSet.has(m.id)) {
|
|
186
|
+
removed.push(m);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
});
|
|
191
|
+
return removed;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
messages_getUnextracted(personaId: string, flag: "f" | "r" | "p" | "o", limit?: number): Message[] {
|
|
195
|
+
const data = this.personas.get(personaId);
|
|
196
|
+
if (!data) return [];
|
|
197
|
+
const unextracted = data.messages.filter(m => m[flag] !== true);
|
|
198
|
+
if (limit && unextracted.length > limit) {
|
|
199
|
+
return unextracted.slice(0, limit).map(m => ({ ...m }));
|
|
200
|
+
}
|
|
201
|
+
return unextracted.map(m => ({ ...m }));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
messages_markExtracted(personaId: string, messageIds: string[], flag: "f" | "r" | "p" | "o"): number {
|
|
205
|
+
const data = this.personas.get(personaId);
|
|
206
|
+
if (!data) return 0;
|
|
207
|
+
const idsSet = new Set(messageIds);
|
|
208
|
+
let count = 0;
|
|
209
|
+
for (const msg of data.messages) {
|
|
210
|
+
if (idsSet.has(msg.id) && msg[flag] !== true) {
|
|
211
|
+
msg[flag] = true;
|
|
212
|
+
count++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return count;
|
|
216
|
+
}
|
|
217
|
+
}
|