ei-tui 0.1.25 → 0.2.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/README.md +42 -0
- package/package.json +1 -1
- package/src/README.md +4 -11
- package/src/cli/README.md +4 -5
- package/src/cli/retrieval.ts +3 -25
- package/src/cli.ts +3 -7
- package/src/core/AGENTS.md +1 -1
- package/src/core/constants/built-in-facts.ts +49 -0
- package/src/core/constants/index.ts +1 -0
- package/src/core/context-utils.ts +0 -1
- package/src/core/embedding-service.ts +8 -0
- package/src/core/handlers/dedup.ts +10 -16
- package/src/core/handlers/heartbeat.ts +2 -3
- package/src/core/handlers/human-extraction.ts +95 -30
- package/src/core/handlers/human-matching.ts +326 -248
- package/src/core/handlers/index.ts +8 -6
- package/src/core/handlers/persona-generation.ts +8 -8
- package/src/core/handlers/rewrite.ts +4 -29
- package/src/core/handlers/utils.ts +23 -1
- package/src/core/heartbeat-manager.ts +2 -4
- package/src/core/human-data-manager.ts +5 -27
- package/src/core/message-manager.ts +10 -10
- package/src/core/orchestrators/ceremony.ts +50 -39
- package/src/core/orchestrators/dedup-phase.ts +0 -1
- package/src/core/orchestrators/human-extraction.ts +351 -207
- package/src/core/orchestrators/index.ts +6 -4
- package/src/core/orchestrators/persona-generation.ts +3 -3
- package/src/core/processor.ts +99 -17
- package/src/core/prompt-context-builder.ts +4 -6
- package/src/core/state/human.ts +1 -26
- package/src/core/state/personas.ts +2 -2
- package/src/core/state-manager.ts +107 -14
- package/src/core/tools/builtin/read-memory.ts +7 -8
- package/src/core/types/data-items.ts +2 -4
- package/src/core/types/entities.ts +6 -4
- package/src/core/types/enums.ts +6 -9
- package/src/core/types/llm.ts +2 -2
- package/src/core/utils/crossFind.ts +2 -5
- package/src/core/utils/event-windows.ts +31 -0
- package/src/integrations/claude-code/importer.ts +8 -4
- package/src/integrations/claude-code/types.ts +2 -0
- package/src/integrations/opencode/importer.ts +7 -3
- package/src/prompts/AGENTS.md +73 -1
- package/src/prompts/ceremony/rewrite.ts +3 -22
- package/src/prompts/ceremony/types.ts +3 -3
- package/src/prompts/generation/descriptions.ts +2 -2
- package/src/prompts/generation/types.ts +2 -2
- package/src/prompts/heartbeat/types.ts +2 -2
- package/src/prompts/human/event-scan.ts +122 -0
- package/src/prompts/human/fact-find.ts +106 -0
- package/src/prompts/human/fact-scan.ts +0 -2
- package/src/prompts/human/index.ts +17 -10
- package/src/prompts/human/person-match.ts +65 -0
- package/src/prompts/human/person-scan.ts +52 -59
- package/src/prompts/human/person-update.ts +241 -0
- package/src/prompts/human/topic-match.ts +65 -0
- package/src/prompts/human/topic-scan.ts +51 -71
- package/src/prompts/human/topic-update.ts +295 -0
- package/src/prompts/human/types.ts +63 -40
- package/src/prompts/index.ts +4 -8
- package/src/prompts/persona/topics-update.ts +2 -2
- package/src/prompts/persona/traits.ts +2 -2
- package/src/prompts/persona/types.ts +3 -3
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +9 -12
- package/src/prompts/response/types.ts +2 -3
- package/src/storage/embeddings.ts +1 -1
- package/src/storage/index.ts +1 -0
- package/src/storage/indexed.ts +174 -0
- package/src/storage/merge.ts +67 -2
- package/tui/src/commands/me.tsx +5 -14
- package/tui/src/commands/settings.tsx +15 -0
- package/tui/src/context/ei.tsx +5 -14
- package/tui/src/util/yaml-serializers.ts +48 -33
- package/src/cli/commands/traits.ts +0 -25
- package/src/prompts/human/item-match.ts +0 -74
- package/src/prompts/human/item-update.ts +0 -364
- package/src/prompts/human/trait-scan.ts +0 -115
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { PersonaTraitExtractionPromptData, PromptOutput } from "./types.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { PersonaTrait } from "../../core/types.js";
|
|
3
3
|
import { formatMessagesAsPlaceholders } from "../message-utils.js";
|
|
4
4
|
|
|
5
|
-
function formatTraitsForPrompt(traits:
|
|
5
|
+
function formatTraitsForPrompt(traits: PersonaTrait[]): string {
|
|
6
6
|
if (traits.length === 0) return "(No traits yet)";
|
|
7
7
|
|
|
8
8
|
return JSON.stringify(traits.map(t => ({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PersonaTrait, Message, PersonaTopic } from "../../core/types.js";
|
|
2
2
|
|
|
3
3
|
export interface PromptOutput {
|
|
4
4
|
system: string;
|
|
@@ -7,7 +7,7 @@ export interface PromptOutput {
|
|
|
7
7
|
|
|
8
8
|
export interface PersonaTraitExtractionPromptData {
|
|
9
9
|
persona_name: string;
|
|
10
|
-
current_traits:
|
|
10
|
+
current_traits: PersonaTrait[];
|
|
11
11
|
messages_context: Message[];
|
|
12
12
|
messages_analyze: Message[];
|
|
13
13
|
}
|
|
@@ -56,7 +56,7 @@ export interface PersonaTopicUpdatePromptData {
|
|
|
56
56
|
persona_name: string;
|
|
57
57
|
short_description?: string;
|
|
58
58
|
long_description?: string;
|
|
59
|
-
traits:
|
|
59
|
+
traits: PersonaTrait[];
|
|
60
60
|
existing_topic?: PersonaTopic; // If updating existing
|
|
61
61
|
candidate: PersonaTopicScanCandidate;
|
|
62
62
|
messages_context: Message[];
|
|
@@ -34,7 +34,7 @@ function buildEiSystemPrompt(data: ResponsePromptData): string {
|
|
|
34
34
|
You are the central hub of this experience - a thoughtful AI who genuinely cares about the human's wellbeing and growth. You listen, remember, and help them reflect. You're curious about their life but never intrusive.
|
|
35
35
|
|
|
36
36
|
Your role is unique among personas:
|
|
37
|
-
- You see ALL of the human's data (facts,
|
|
37
|
+
- You see ALL of the human's data (facts, topics, people) across all groups
|
|
38
38
|
- You help them understand and navigate the system
|
|
39
39
|
- You gently help them explore their thoughts and feelings
|
|
40
40
|
- You attempt to emulate their speech patterns;
|
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
* Building blocks for constructing response prompts
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { PersonaTrait, Quote, PersonaTopic } from "../../core/types.js";
|
|
7
7
|
import type { ResponsePromptData } from "./types.js";
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
const DESCRIPTION_MAX_CHARS = 500;
|
|
10
|
+
|
|
11
|
+
function truncateDescription(description: string): string {
|
|
12
|
+
if (description.length <= DESCRIPTION_MAX_CHARS) return description;
|
|
13
|
+
return description.slice(0, DESCRIPTION_MAX_CHARS) + "…";
|
|
14
|
+
}
|
|
9
15
|
|
|
10
16
|
// =============================================================================
|
|
11
17
|
// IDENTITY SECTION
|
|
@@ -62,7 +68,7 @@ export function buildGuidelinesSection(personaName: string): string {
|
|
|
62
68
|
// TRAITS SECTION
|
|
63
69
|
// =============================================================================
|
|
64
70
|
|
|
65
|
-
export function buildTraitsSection(traits:
|
|
71
|
+
export function buildTraitsSection(traits: PersonaTrait[], header: string): string {
|
|
66
72
|
if (traits.length === 0) return "";
|
|
67
73
|
|
|
68
74
|
const sorted = [...traits].sort((a, b) => (b.strength ?? 0.5) - (a.strength ?? 0.5)).slice(0, 15);
|
|
@@ -146,14 +152,6 @@ export function buildHumanSection(human: ResponsePromptData["human"]): string {
|
|
|
146
152
|
if (facts) sections.push(`### Key Facts\n${facts}`);
|
|
147
153
|
}
|
|
148
154
|
|
|
149
|
-
// Traits
|
|
150
|
-
if (human.traits.length > 0) {
|
|
151
|
-
const traits = human.traits
|
|
152
|
-
.slice(0, 15)
|
|
153
|
-
.map(t => `- **${t.name}**: ${truncateDescription(t.description)}`)
|
|
154
|
-
.join("\n");
|
|
155
|
-
sections.push(`### Personality\n${traits}`);
|
|
156
|
-
}
|
|
157
155
|
|
|
158
156
|
// Active topics (exposure_current > 0.3)
|
|
159
157
|
const activeTopics = human.topics.filter(t => t.exposure_current > 0.3);
|
|
@@ -275,7 +273,6 @@ export function buildQuotesSection(quotes: Quote[], human: ResponsePromptData["h
|
|
|
275
273
|
|
|
276
274
|
const allDataItems = [
|
|
277
275
|
...human.facts.map(f => ({ id: f.id, name: f.name })),
|
|
278
|
-
...human.traits.map(t => ({ id: t.id, name: t.name })),
|
|
279
276
|
...human.topics.map(t => ({ id: t.id, name: t.name })),
|
|
280
277
|
...human.people.map(p => ({ id: p.id, name: p.name })),
|
|
281
278
|
];
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Based on CONTRACTS.md ResponsePromptData specification
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Fact,
|
|
6
|
+
import type { Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic } from "../../core/types.js";
|
|
7
7
|
import type { ToolDefinition } from "../../core/types.js";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -15,12 +15,11 @@ export interface ResponsePromptData {
|
|
|
15
15
|
aliases: string[];
|
|
16
16
|
short_description?: string;
|
|
17
17
|
long_description?: string;
|
|
18
|
-
traits:
|
|
18
|
+
traits: PersonaTrait[];
|
|
19
19
|
topics: PersonaTopic[];
|
|
20
20
|
};
|
|
21
21
|
human: {
|
|
22
22
|
facts: Fact[];
|
|
23
|
-
traits: Trait[];
|
|
24
23
|
topics: Topic[];
|
|
25
24
|
people: Person[];
|
|
26
25
|
quotes: Quote[];
|
|
@@ -56,7 +56,7 @@ function decodeEmbedding(value: unknown): number[] | undefined {
|
|
|
56
56
|
// Walk the entire StorageState and encode/decode all embedding fields
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
58
|
|
|
59
|
-
const HUMAN_ITEM_KEYS = ["facts", "
|
|
59
|
+
const HUMAN_ITEM_KEYS = ["facts", "topics", "people", "quotes"] as const;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Returns a new StorageState with embeddings encoded as base64 strings.
|
package/src/storage/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type { Storage } from "./interface.js";
|
|
2
2
|
export { LocalStorage } from "./local.js";
|
|
3
|
+
export { IndexedDBStorage } from "./indexed.js";
|
|
3
4
|
export { remoteSync, RemoteSync, type RemoteSyncCredentials, type RemoteTimestamp, type SyncResult, type FetchResult } from "./remote.js";
|
|
4
5
|
export { encrypt, decrypt, generateUserId, type CryptoCredentials, type EncryptedPayload } from "./crypto.js";
|
|
5
6
|
export { yoloMerge } from "./merge.js";
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { StorageState } from "../core/types.js";
|
|
2
|
+
import type { Storage } from "./interface.js";
|
|
3
|
+
import { compress, decompress, isCompressed } from "./compress.js";
|
|
4
|
+
import { encodeAllEmbeddings, decodeAllEmbeddings } from "./embeddings.js";
|
|
5
|
+
|
|
6
|
+
const DB_NAME = "ei_db";
|
|
7
|
+
const DB_VERSION = 1;
|
|
8
|
+
const STORE_NAME = "state";
|
|
9
|
+
const PRIMARY_KEY = "primary";
|
|
10
|
+
const BACKUP_KEY = "backup";
|
|
11
|
+
|
|
12
|
+
export class IndexedDBStorage implements Storage {
|
|
13
|
+
async isAvailable(): Promise<boolean> {
|
|
14
|
+
try {
|
|
15
|
+
const db = await this.openDB();
|
|
16
|
+
db.close();
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async save(state: StorageState): Promise<void> {
|
|
24
|
+
state.timestamp = new Date().toISOString();
|
|
25
|
+
try {
|
|
26
|
+
const json = JSON.stringify(encodeAllEmbeddings(state));
|
|
27
|
+
const payload = await compress(json);
|
|
28
|
+
await this.setItem(PRIMARY_KEY, payload);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (this.isQuotaError(e)) {
|
|
31
|
+
throw new Error("STORAGE_SAVE_FAILED: IndexedDB quota exceeded");
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async load(): Promise<StorageState | null> {
|
|
38
|
+
const current = await this.getItem(PRIMARY_KEY);
|
|
39
|
+
if (current) {
|
|
40
|
+
try {
|
|
41
|
+
const json = isCompressed(current) ? await decompress(current) : current;
|
|
42
|
+
return decodeAllEmbeddings(JSON.parse(json) as StorageState);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Move current state to backup location and clear primary state.
|
|
52
|
+
* Used after successful remote sync to signal "no local state to load" on next launch.
|
|
53
|
+
* Backup can be restored manually if remote pull fails.
|
|
54
|
+
*/
|
|
55
|
+
async moveToBackup(): Promise<void> {
|
|
56
|
+
const current = await this.getItem(PRIMARY_KEY);
|
|
57
|
+
if (current) {
|
|
58
|
+
// Remove primary first so backup write doesn't double-count against quota.
|
|
59
|
+
await this.deleteItem(PRIMARY_KEY);
|
|
60
|
+
await this.setItem(BACKUP_KEY, current);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read backup state without removing it.
|
|
66
|
+
* Used to peek sync credentials from a previous session's backup.
|
|
67
|
+
*/
|
|
68
|
+
async loadBackup(): Promise<StorageState | null> {
|
|
69
|
+
const backup = await this.getItem(BACKUP_KEY);
|
|
70
|
+
if (backup) {
|
|
71
|
+
try {
|
|
72
|
+
const json = isCompressed(backup) ? await decompress(backup) : backup;
|
|
73
|
+
return decodeAllEmbeddings(JSON.parse(json) as StorageState);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** No-op in browser — rolling backups are TUI-only (filesystem required). */
|
|
82
|
+
async saveRollingBackup(_state: StorageState, _maxBackups: number): Promise<void> {
|
|
83
|
+
// Intentional no-op: IndexedDB has no directory/file concept.
|
|
84
|
+
// The Processor gates this call with `this.isTUI` so it never runs in the browser.
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Private IDB helpers ──────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
private openDB(): Promise<IDBDatabase> {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
92
|
+
|
|
93
|
+
request.onupgradeneeded = (event) => {
|
|
94
|
+
const db = (event.target as IDBOpenDBRequest).result;
|
|
95
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
96
|
+
db.createObjectStore(STORE_NAME);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
request.onsuccess = (event) => {
|
|
101
|
+
resolve((event.target as IDBOpenDBRequest).result);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
request.onerror = (event) => {
|
|
105
|
+
reject((event.target as IDBOpenDBRequest).error);
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async getItem(key: string): Promise<string | null> {
|
|
111
|
+
const db = await this.openDB();
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
114
|
+
const store = tx.objectStore(STORE_NAME);
|
|
115
|
+
const request = store.get(key);
|
|
116
|
+
|
|
117
|
+
request.onsuccess = (event) => {
|
|
118
|
+
const result = (event.target as IDBRequest).result;
|
|
119
|
+
db.close();
|
|
120
|
+
resolve(result ?? null);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
request.onerror = (event) => {
|
|
124
|
+
db.close();
|
|
125
|
+
reject((event.target as IDBRequest).error);
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async setItem(key: string, value: string): Promise<void> {
|
|
131
|
+
const db = await this.openDB();
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
134
|
+
const store = tx.objectStore(STORE_NAME);
|
|
135
|
+
const request = store.put(value, key);
|
|
136
|
+
|
|
137
|
+
request.onsuccess = () => {
|
|
138
|
+
db.close();
|
|
139
|
+
resolve();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
request.onerror = (event) => {
|
|
143
|
+
db.close();
|
|
144
|
+
reject((event.target as IDBRequest).error);
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async deleteItem(key: string): Promise<void> {
|
|
150
|
+
const db = await this.openDB();
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
153
|
+
const store = tx.objectStore(STORE_NAME);
|
|
154
|
+
const request = store.delete(key);
|
|
155
|
+
|
|
156
|
+
request.onsuccess = () => {
|
|
157
|
+
db.close();
|
|
158
|
+
resolve();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
request.onerror = (event) => {
|
|
162
|
+
db.close();
|
|
163
|
+
reject((event.target as IDBRequest).error);
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private isQuotaError(e: unknown): boolean {
|
|
169
|
+
return (
|
|
170
|
+
e instanceof DOMException &&
|
|
171
|
+
(e.name === "QuotaExceededError" || e.name === "NS_ERROR_DOM_QUOTA_REACHED")
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
package/src/storage/merge.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
import type { StorageState, DataItem, Quote } from "../core/types.js";
|
|
1
|
+
import type { StorageState, DataItem, Quote, ToolProvider, ToolDefinition, ProviderAccount } from "../core/types.js";
|
|
2
|
+
|
|
3
|
+
function mergeByName<T extends { name: string }>(
|
|
4
|
+
local: T[],
|
|
5
|
+
remote: T[],
|
|
6
|
+
preferRemote: boolean,
|
|
7
|
+
): T[] {
|
|
8
|
+
const merged = [...local];
|
|
9
|
+
|
|
10
|
+
for (const remoteItem of remote) {
|
|
11
|
+
const localIndex = merged.findIndex(item => item.name === remoteItem.name);
|
|
12
|
+
if (localIndex === -1) {
|
|
13
|
+
merged.push(remoteItem);
|
|
14
|
+
} else if (preferRemote) {
|
|
15
|
+
merged[localIndex] = remoteItem;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return merged;
|
|
20
|
+
}
|
|
2
21
|
|
|
3
22
|
function mergeDataItems<T extends DataItem>(local: T[], remote: T[]): T[] {
|
|
4
23
|
const merged = [...local];
|
|
@@ -32,7 +51,6 @@ export function yoloMerge(local: StorageState, remote: StorageState): StorageSta
|
|
|
32
51
|
const merged = structuredClone(local);
|
|
33
52
|
|
|
34
53
|
merged.human.facts = mergeDataItems(merged.human.facts, remote.human.facts);
|
|
35
|
-
merged.human.traits = mergeDataItems(merged.human.traits, remote.human.traits);
|
|
36
54
|
merged.human.topics = mergeDataItems(merged.human.topics, remote.human.topics);
|
|
37
55
|
merged.human.people = mergeDataItems(merged.human.people, remote.human.people);
|
|
38
56
|
merged.human.quotes = mergeQuotes(merged.human.quotes || [], remote.human.quotes || []);
|
|
@@ -63,6 +81,53 @@ export function yoloMerge(local: StorageState, remote: StorageState): StorageSta
|
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
|
|
84
|
+
if ('traits' in merged.human) {
|
|
85
|
+
delete (merged.human as Record<string, unknown>)['traits'];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const preferRemote = remote.timestamp > local.timestamp;
|
|
89
|
+
|
|
90
|
+
if (remote.human.settings?.accounts && merged.human.settings) {
|
|
91
|
+
merged.human.settings.accounts = mergeByName<ProviderAccount>(
|
|
92
|
+
merged.human.settings?.accounts || [],
|
|
93
|
+
remote.human.settings.accounts,
|
|
94
|
+
preferRemote,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (preferRemote && remote.human.settings) {
|
|
99
|
+
const remoteSettings = remote.human.settings;
|
|
100
|
+
const localSettings = merged.human.settings || {};
|
|
101
|
+
|
|
102
|
+
if (remoteSettings.default_model !== undefined) localSettings.default_model = remoteSettings.default_model;
|
|
103
|
+
if (remoteSettings.oneshot_model !== undefined) localSettings.oneshot_model = remoteSettings.oneshot_model;
|
|
104
|
+
if (remoteSettings.rewrite_model !== undefined) localSettings.rewrite_model = remoteSettings.rewrite_model;
|
|
105
|
+
if (remoteSettings.queue_paused !== undefined) localSettings.queue_paused = remoteSettings.queue_paused;
|
|
106
|
+
if (remoteSettings.skip_quote_delete_confirm !== undefined) localSettings.skip_quote_delete_confirm = remoteSettings.skip_quote_delete_confirm;
|
|
107
|
+
if (remoteSettings.name_display !== undefined) localSettings.name_display = remoteSettings.name_display;
|
|
108
|
+
if (remoteSettings.time_mode !== undefined) localSettings.time_mode = remoteSettings.time_mode;
|
|
109
|
+
|
|
110
|
+
if (remoteSettings.opencode) localSettings.opencode = remoteSettings.opencode;
|
|
111
|
+
if (remoteSettings.ceremony) localSettings.ceremony = remoteSettings.ceremony;
|
|
112
|
+
if (remoteSettings.backup) localSettings.backup = remoteSettings.backup;
|
|
113
|
+
if (remoteSettings.claudeCode) localSettings.claudeCode = remoteSettings.claudeCode;
|
|
114
|
+
// NOTE: Do NOT merge sync credentials — always keep local sync creds
|
|
115
|
+
|
|
116
|
+
merged.human.settings = localSettings;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
merged.providers = mergeByName<ToolProvider>(
|
|
120
|
+
merged.providers || [],
|
|
121
|
+
remote.providers || [],
|
|
122
|
+
preferRemote,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
merged.tools = mergeByName<ToolDefinition>(
|
|
126
|
+
merged.tools || [],
|
|
127
|
+
remote.tools || [],
|
|
128
|
+
preferRemote,
|
|
129
|
+
);
|
|
130
|
+
|
|
66
131
|
merged.timestamp = new Date().toISOString();
|
|
67
132
|
|
|
68
133
|
return merged;
|
package/tui/src/commands/me.tsx
CHANGED
|
@@ -4,15 +4,15 @@ import { humanToYAML, humanFromYAML } from "../util/yaml-serializers.js";
|
|
|
4
4
|
import { logger } from "../util/logger.js";
|
|
5
5
|
import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
|
|
6
6
|
|
|
7
|
-
type DataType = "facts" | "
|
|
7
|
+
type DataType = "facts" | "topics" | "people";
|
|
8
8
|
|
|
9
|
-
const VALID_TYPES: DataType[] = ["facts", "
|
|
9
|
+
const VALID_TYPES: DataType[] = ["facts", "topics", "people"];
|
|
10
10
|
|
|
11
11
|
export const meCommand: Command = {
|
|
12
12
|
name: "me",
|
|
13
13
|
aliases: [],
|
|
14
14
|
description: "Edit your data in $EDITOR",
|
|
15
|
-
usage: "/me [facts|
|
|
15
|
+
usage: "/me [facts|topics|people]",
|
|
16
16
|
|
|
17
17
|
async execute(args, ctx) {
|
|
18
18
|
const human = await ctx.ei.getHuman();
|
|
@@ -23,14 +23,13 @@ export const meCommand: Command = {
|
|
|
23
23
|
: null;
|
|
24
24
|
|
|
25
25
|
if (filterArg && !filterType) {
|
|
26
|
-
ctx.showNotification(`Invalid type: ${filterArg}. Use: facts,
|
|
26
|
+
ctx.showNotification(`Invalid type: ${filterArg}. Use: facts, topics, people`, "error");
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const filteredHuman = filterType ? {
|
|
31
31
|
...human,
|
|
32
32
|
facts: filterType === "facts" ? human.facts : [],
|
|
33
|
-
traits: filterType === "traits" ? human.traits : [],
|
|
34
33
|
topics: filterType === "topics" ? human.topics : [],
|
|
35
34
|
people: filterType === "people" ? human.people : [],
|
|
36
35
|
} : human;
|
|
@@ -67,14 +66,11 @@ export const meCommand: Command = {
|
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
try {
|
|
70
|
-
const parsed = humanFromYAML(result.content);
|
|
69
|
+
const parsed = humanFromYAML(result.content, filteredHuman);
|
|
71
70
|
|
|
72
71
|
for (const id of parsed.deletedFactIds) {
|
|
73
72
|
await ctx.ei.removeDataItem("fact", id);
|
|
74
73
|
}
|
|
75
|
-
for (const id of parsed.deletedTraitIds) {
|
|
76
|
-
await ctx.ei.removeDataItem("trait", id);
|
|
77
|
-
}
|
|
78
74
|
for (const id of parsed.deletedTopicIds) {
|
|
79
75
|
await ctx.ei.removeDataItem("topic", id);
|
|
80
76
|
}
|
|
@@ -85,9 +81,6 @@ export const meCommand: Command = {
|
|
|
85
81
|
for (const fact of parsed.facts) {
|
|
86
82
|
await ctx.ei.upsertFact(fact);
|
|
87
83
|
}
|
|
88
|
-
for (const trait of parsed.traits) {
|
|
89
|
-
await ctx.ei.upsertTrait(trait);
|
|
90
|
-
}
|
|
91
84
|
for (const topic of parsed.topics) {
|
|
92
85
|
await ctx.ei.upsertTopic(topic);
|
|
93
86
|
}
|
|
@@ -96,11 +89,9 @@ export const meCommand: Command = {
|
|
|
96
89
|
}
|
|
97
90
|
|
|
98
91
|
const deleteCount = parsed.deletedFactIds.length +
|
|
99
|
-
parsed.deletedTraitIds.length +
|
|
100
92
|
parsed.deletedTopicIds.length +
|
|
101
93
|
parsed.deletedPersonIds.length;
|
|
102
94
|
const updateCount = parsed.facts.length +
|
|
103
|
-
parsed.traits.length +
|
|
104
95
|
parsed.topics.length +
|
|
105
96
|
parsed.people.length;
|
|
106
97
|
|
|
@@ -45,6 +45,21 @@ export const settingsCommand: Command = {
|
|
|
45
45
|
// Validate provider name in default_model (case-insensitive match + auto-correct)
|
|
46
46
|
const llmAccounts = human.settings?.accounts?.filter(a => a.type === "llm") ?? [];
|
|
47
47
|
newSettings.default_model = validateModelProvider(newSettings.default_model, llmAccounts);
|
|
48
|
+
|
|
49
|
+
if (newSettings.opencode?.extraction_model) {
|
|
50
|
+
newSettings.opencode.extraction_model = validateModelProvider(
|
|
51
|
+
newSettings.opencode.extraction_model,
|
|
52
|
+
llmAccounts
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (newSettings.claudeCode?.extraction_model) {
|
|
57
|
+
newSettings.claudeCode.extraction_model = validateModelProvider(
|
|
58
|
+
newSettings.claudeCode.extraction_model,
|
|
59
|
+
llmAccounts
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
await ctx.ei.updateSettings(newSettings);
|
|
49
64
|
ctx.showNotification("Settings updated", "info");
|
|
50
65
|
return;
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -23,7 +23,6 @@ import type {
|
|
|
23
23
|
HumanEntity,
|
|
24
24
|
HumanSettings,
|
|
25
25
|
Fact,
|
|
26
|
-
Trait,
|
|
27
26
|
Topic,
|
|
28
27
|
Person,
|
|
29
28
|
Quote,
|
|
@@ -78,10 +77,9 @@ export interface EiContextValue {
|
|
|
78
77
|
updateHuman: (updates: Partial<HumanEntity>) => Promise<void>;
|
|
79
78
|
updateSettings: (updates: Partial<HumanSettings>) => Promise<void>;
|
|
80
79
|
upsertFact: (fact: Fact) => Promise<void>;
|
|
81
|
-
upsertTrait: (trait: Trait) => Promise<void>;
|
|
82
80
|
upsertTopic: (topic: Topic) => Promise<void>;
|
|
83
81
|
upsertPerson: (person: Person) => Promise<void>;
|
|
84
|
-
removeDataItem: (type: "fact" | "
|
|
82
|
+
removeDataItem: (type: "fact" | "topic" | "person", id: string) => Promise<void>;
|
|
85
83
|
syncStatus: () => { configured: boolean; envBased: boolean };
|
|
86
84
|
triggerSync: () => Promise<{ success: boolean; error?: string }>;
|
|
87
85
|
getGroupList: () => Promise<string[]>;
|
|
@@ -92,10 +90,9 @@ export interface EiContextValue {
|
|
|
92
90
|
quotesVersion: () => number;
|
|
93
91
|
searchHumanData: (
|
|
94
92
|
query: string,
|
|
95
|
-
options?: { types?: Array<"fact" | "
|
|
93
|
+
options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number }
|
|
96
94
|
) => Promise<{
|
|
97
95
|
facts: Fact[];
|
|
98
|
-
traits: Trait[];
|
|
99
96
|
topics: Topic[];
|
|
100
97
|
people: Person[];
|
|
101
98
|
quotes: Quote[];
|
|
@@ -329,11 +326,6 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
329
326
|
await processor.upsertFact(fact);
|
|
330
327
|
};
|
|
331
328
|
|
|
332
|
-
const upsertTrait = async (trait: Trait) => {
|
|
333
|
-
if (!processor) return;
|
|
334
|
-
await processor.upsertTrait(trait);
|
|
335
|
-
};
|
|
336
|
-
|
|
337
329
|
const upsertTopic = async (topic: Topic) => {
|
|
338
330
|
if (!processor) return;
|
|
339
331
|
await processor.upsertTopic(topic);
|
|
@@ -344,7 +336,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
344
336
|
await processor.upsertPerson(person);
|
|
345
337
|
};
|
|
346
338
|
|
|
347
|
-
const removeDataItem = async (type: "fact" | "
|
|
339
|
+
const removeDataItem = async (type: "fact" | "topic" | "person", id: string) => {
|
|
348
340
|
if (!processor) return;
|
|
349
341
|
await processor.removeDataItem(type, id);
|
|
350
342
|
};
|
|
@@ -450,9 +442,9 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
450
442
|
|
|
451
443
|
const searchHumanData = async (
|
|
452
444
|
query: string,
|
|
453
|
-
options?: { types?: Array<"fact" | "
|
|
445
|
+
options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number }
|
|
454
446
|
) => {
|
|
455
|
-
if (!processor) return { facts: [],
|
|
447
|
+
if (!processor) return { facts: [], topics: [], people: [], quotes: [] };
|
|
456
448
|
return processor.searchHumanData(query, options);
|
|
457
449
|
};
|
|
458
450
|
|
|
@@ -648,7 +640,6 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
648
640
|
updateHuman,
|
|
649
641
|
updateSettings,
|
|
650
642
|
upsertFact,
|
|
651
|
-
upsertTrait,
|
|
652
643
|
upsertTopic,
|
|
653
644
|
upsertPerson,
|
|
654
645
|
removeDataItem,
|