ei-tui 0.1.24 → 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 +34 -14
- 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 +60 -46
- package/src/core/orchestrators/dedup-phase.ts +11 -5
- 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 +113 -22
- 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/dedup.ts +41 -7
- 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/app.tsx +7 -5
- package/tui/src/commands/archive.tsx +2 -2
- package/tui/src/commands/context.tsx +3 -4
- package/tui/src/commands/delete.tsx +4 -4
- package/tui/src/commands/dlq.ts +3 -4
- package/tui/src/commands/help.tsx +1 -1
- package/tui/src/commands/me.tsx +8 -18
- package/tui/src/commands/persona.tsx +2 -2
- package/tui/src/commands/provider.tsx +3 -5
- package/tui/src/commands/queue.ts +3 -4
- package/tui/src/commands/quotes.tsx +6 -8
- package/tui/src/commands/registry.ts +1 -1
- package/tui/src/commands/setsync.tsx +2 -2
- package/tui/src/commands/settings.tsx +18 -4
- package/tui/src/commands/spotify-auth.ts +0 -1
- package/tui/src/commands/tools.tsx +4 -5
- package/tui/src/context/ei.tsx +5 -14
- package/tui/src/context/overlay.tsx +17 -6
- package/tui/src/util/editor.ts +22 -11
- package/tui/src/util/persona-editor.tsx +6 -8
- package/tui/src/util/provider-editor.tsx +6 -8
- package/tui/src/util/toolkit-editor.tsx +3 -4
- 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/app.tsx
CHANGED
|
@@ -10,23 +10,25 @@ import { StatusBar } from "./components/StatusBar";
|
|
|
10
10
|
import { Show } from "solid-js";
|
|
11
11
|
import { useEi } from "./context/ei";
|
|
12
12
|
import { WelcomeOverlay } from "./components/WelcomeOverlay";
|
|
13
|
+
import { useRenderer } from "@opentui/solid";
|
|
13
14
|
|
|
14
15
|
function AppContent() {
|
|
15
|
-
const { overlayRenderer,
|
|
16
|
+
const { overlayRenderer, showOverlay } = useOverlay();
|
|
16
17
|
const { showWelcomeOverlay, dismissWelcomeOverlay } = useEi();
|
|
17
|
-
|
|
18
|
+
const renderer = useRenderer();
|
|
18
19
|
// Show welcome overlay when LLM detection determines no provider is configured
|
|
19
20
|
createEffect(() => {
|
|
20
21
|
if (showWelcomeOverlay()) {
|
|
21
|
-
showOverlay((onDismiss) => (
|
|
22
|
+
showOverlay((onDismiss, _hideForEditor) => (
|
|
22
23
|
<WelcomeOverlay onDismiss={() => {
|
|
23
24
|
dismissWelcomeOverlay();
|
|
24
25
|
onDismiss();
|
|
25
26
|
}} />
|
|
26
|
-
));
|
|
27
|
+
), renderer);
|
|
27
28
|
}
|
|
28
29
|
});
|
|
29
30
|
|
|
31
|
+
|
|
30
32
|
return (
|
|
31
33
|
<box flexDirection="column" width="100%" height="100%">
|
|
32
34
|
<Layout
|
|
@@ -36,7 +38,7 @@ function AppContent() {
|
|
|
36
38
|
/>
|
|
37
39
|
<StatusBar />
|
|
38
40
|
<Show when={overlayRenderer()}>
|
|
39
|
-
{overlayRenderer()!(
|
|
41
|
+
{overlayRenderer()!()}
|
|
40
42
|
</Show>
|
|
41
43
|
</box>
|
|
42
44
|
);
|
|
@@ -16,7 +16,7 @@ export const archiveCommand: Command = {
|
|
|
16
16
|
ctx.showNotification("No archived personas", "info");
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
ctx.showOverlay((hideOverlay) => (
|
|
19
|
+
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
20
20
|
<PersonaListOverlay
|
|
21
21
|
personas={archived}
|
|
22
22
|
activePersonaId={null}
|
|
@@ -30,7 +30,7 @@ export const archiveCommand: Command = {
|
|
|
30
30
|
}}
|
|
31
31
|
onDismiss={hideOverlay}
|
|
32
32
|
/>
|
|
33
|
-
));
|
|
33
|
+
), ctx.renderer);
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -90,12 +90,12 @@ export const contextCommand: Command = {
|
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
const shouldReEdit = await new Promise<boolean>((resolve) => {
|
|
93
|
-
ctx.showOverlay((hideOverlay) => (
|
|
93
|
+
ctx.showOverlay((hideOverlay, hideForEditor) => (
|
|
94
94
|
<ConfirmOverlay
|
|
95
95
|
message={`YAML parse error:\n${errorMsg}\n\nRe-edit?`}
|
|
96
96
|
onConfirm={() => {
|
|
97
97
|
logger.debug("[context] user confirmed re-edit");
|
|
98
|
-
|
|
98
|
+
hideForEditor();
|
|
99
99
|
resolve(true);
|
|
100
100
|
}}
|
|
101
101
|
onCancel={() => {
|
|
@@ -104,7 +104,7 @@ export const contextCommand: Command = {
|
|
|
104
104
|
resolve(false);
|
|
105
105
|
}}
|
|
106
106
|
/>
|
|
107
|
-
));
|
|
107
|
+
), ctx.renderer);
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
logger.debug("[context] shouldReEdit", { shouldReEdit, iteration: editorIteration });
|
|
@@ -112,7 +112,6 @@ export const contextCommand: Command = {
|
|
|
112
112
|
if (shouldReEdit) {
|
|
113
113
|
yamlContent = result.content;
|
|
114
114
|
logger.debug("[context] continuing to next iteration");
|
|
115
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
116
115
|
continue;
|
|
117
116
|
} else {
|
|
118
117
|
ctx.showNotification("Changes discarded", "info");
|
|
@@ -14,13 +14,13 @@ export const deleteCommand: Command = {
|
|
|
14
14
|
|
|
15
15
|
const confirmAndDelete = async (personaId: string, displayName: string) => {
|
|
16
16
|
const confirmed = await new Promise<boolean>((resolve) => {
|
|
17
|
-
ctx.showOverlay((hideOverlay) => (
|
|
17
|
+
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
18
18
|
<ConfirmOverlay
|
|
19
19
|
message={`Delete "${displayName}"?\nThis cannot be undone.`}
|
|
20
20
|
onConfirm={() => { hideOverlay(); resolve(true); }}
|
|
21
21
|
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
22
22
|
/>
|
|
23
|
-
));
|
|
23
|
+
), ctx.renderer);
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
if (confirmed) {
|
|
@@ -36,7 +36,7 @@ export const deleteCommand: Command = {
|
|
|
36
36
|
ctx.showNotification("No personas available to delete", "info");
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
|
-
ctx.showOverlay((hideOverlay) => (
|
|
39
|
+
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
40
40
|
<PersonaListOverlay
|
|
41
41
|
personas={deletable}
|
|
42
42
|
activePersonaId={null}
|
|
@@ -48,7 +48,7 @@ export const deleteCommand: Command = {
|
|
|
48
48
|
}}
|
|
49
49
|
onDismiss={hideOverlay}
|
|
50
50
|
/>
|
|
51
|
-
));
|
|
51
|
+
), ctx.renderer);
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
package/tui/src/commands/dlq.ts
CHANGED
|
@@ -52,18 +52,17 @@ export const dlqCommand: Command = {
|
|
|
52
52
|
const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
|
53
53
|
|
|
54
54
|
const shouldReEdit = await new Promise<boolean>((resolve) => {
|
|
55
|
-
ctx.showOverlay((hideOverlay) =>
|
|
55
|
+
ctx.showOverlay((hideOverlay, hideForEditor) =>
|
|
56
56
|
ConfirmOverlay({
|
|
57
57
|
message: `YAML error:\n${errorMsg}\n\nRe-edit?`,
|
|
58
|
-
onConfirm: () => {
|
|
58
|
+
onConfirm: () => { hideForEditor(); resolve(true); },
|
|
59
59
|
onCancel: () => { hideOverlay(); resolve(false); },
|
|
60
60
|
})
|
|
61
|
-
);
|
|
61
|
+
, ctx.renderer);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
if (shouldReEdit) {
|
|
65
65
|
yamlContent = result.content;
|
|
66
|
-
await new Promise(r => setTimeout(r, 50));
|
|
67
66
|
continue;
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -7,6 +7,6 @@ export const helpCommand: Command = {
|
|
|
7
7
|
description: "Show help screen",
|
|
8
8
|
usage: "/help or /h",
|
|
9
9
|
execute: async (_args, ctx) => {
|
|
10
|
-
ctx.showOverlay((
|
|
10
|
+
ctx.showOverlay((_hideOverlay, _hideForEditor) => <HelpOverlay onDismiss={_hideOverlay} />, ctx.renderer);
|
|
11
11
|
},
|
|
12
12
|
};
|
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
|
|
|
@@ -112,12 +103,12 @@ export const meCommand: Command = {
|
|
|
112
103
|
logger.debug("[me] YAML parse error, prompting for re-edit", { iteration: editorIteration, error: errorMsg });
|
|
113
104
|
|
|
114
105
|
const shouldReEdit = await new Promise<boolean>((resolve) => {
|
|
115
|
-
ctx.showOverlay((hideOverlay) => (
|
|
106
|
+
ctx.showOverlay((hideOverlay, hideForEditor) => (
|
|
116
107
|
<ConfirmOverlay
|
|
117
108
|
message={`YAML parse error:\n${errorMsg}\n\nRe-edit?`}
|
|
118
109
|
onConfirm={() => {
|
|
119
110
|
logger.debug("[me] user confirmed re-edit");
|
|
120
|
-
|
|
111
|
+
hideForEditor();
|
|
121
112
|
resolve(true);
|
|
122
113
|
}}
|
|
123
114
|
onCancel={() => {
|
|
@@ -126,7 +117,7 @@ export const meCommand: Command = {
|
|
|
126
117
|
resolve(false);
|
|
127
118
|
}}
|
|
128
119
|
/>
|
|
129
|
-
));
|
|
120
|
+
), ctx.renderer);
|
|
130
121
|
});
|
|
131
122
|
|
|
132
123
|
logger.debug("[me] shouldReEdit", { shouldReEdit, iteration: editorIteration });
|
|
@@ -134,7 +125,6 @@ export const meCommand: Command = {
|
|
|
134
125
|
if (shouldReEdit) {
|
|
135
126
|
yamlContent = result.content;
|
|
136
127
|
logger.debug("[me] continuing to next iteration");
|
|
137
|
-
await new Promise(r => setTimeout(r, 50));
|
|
138
128
|
continue;
|
|
139
129
|
} else {
|
|
140
130
|
ctx.showNotification("Changes discarded", "info");
|
|
@@ -13,7 +13,7 @@ export const personaCommand: Command = {
|
|
|
13
13
|
const unarchived = ctx.ei.personas().filter(p => !p.is_archived);
|
|
14
14
|
|
|
15
15
|
if (args.length === 0) {
|
|
16
|
-
ctx.showOverlay((hideOverlay) => (
|
|
16
|
+
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
17
17
|
<PersonaListOverlay
|
|
18
18
|
personas={unarchived}
|
|
19
19
|
activePersonaId={ctx.ei.activePersonaId()}
|
|
@@ -25,7 +25,7 @@ export const personaCommand: Command = {
|
|
|
25
25
|
}}
|
|
26
26
|
onDismiss={hideOverlay}
|
|
27
27
|
/>
|
|
28
|
-
));
|
|
28
|
+
), ctx.renderer);
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
|