ei-tui 1.6.9 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +2 -1
- package/skills/coding-harness-reflect/SKILL.md +230 -0
- package/skills/ei-curate/SKILL.md +174 -0
- package/skills/ei-curate/references/cli.md +160 -0
- package/skills/ei-curate/references/provenance.md +88 -0
- package/skills/ei-curate/references/recipes.md +144 -0
- package/skills/ei-curate/references/talking-to-the-user.md +71 -0
- package/skills/ei-persona/SKILL.md +238 -0
- package/skills/ei-persona/references/cli.md +265 -0
- package/skills/ei-persona/references/recipes.md +247 -0
- package/skills/ei-persona/references/talking-to-the-user.md +107 -0
- package/src/cli/README.md +72 -2
- package/src/cli/corrections-endpoints.ts +297 -0
- package/src/cli/corrections-writer.ts +138 -0
- package/src/cli/install.ts +117 -25
- package/src/cli/mcp.ts +80 -1
- package/src/cli/persona-corrections.ts +442 -0
- package/src/cli/retrieval.ts +46 -2
- package/src/cli.ts +131 -1
- package/src/core/corrections.ts +233 -0
- package/src/core/handlers/human-matching.ts +2 -2
- package/src/core/persona-tools.ts +92 -0
- package/src/core/processor.ts +113 -1
- package/src/core/state/human.ts +10 -0
- package/src/core/state/personas.ts +8 -0
- package/src/core/state-manager.ts +11 -0
- package/src/core/utils/identifier-utils.ts +3 -2
- package/src/storage/file-lock.ts +120 -0
- package/tui/README.md +2 -0
- package/tui/src/commands/provider.tsx +1 -1
- package/tui/src/components/WelcomeOverlay.tsx +3 -3
- package/tui/src/context/ei.tsx +14 -0
- package/tui/src/index.tsx +13 -1
- package/tui/src/storage/file.ts +15 -83
- package/tui/src/util/instance-lock.ts +3 -2
- package/tui/src/util/yaml-persona.ts +7 -38
|
@@ -80,6 +80,14 @@ export class PersonaState {
|
|
|
80
80
|
return true;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** Full replace of a persona's entity (not a merge) — used by the live-drain path for corrections-queue upserts, where record.record is already a complete PersonaEntity and any field genuinely absent from it must NOT survive from the prior value. Preserves .messages untouched, same as update(). */
|
|
84
|
+
replace(personaId: string, entity: PersonaEntity): boolean {
|
|
85
|
+
const data = this.personas.get(personaId);
|
|
86
|
+
if (!data) return false;
|
|
87
|
+
data.entity = { ...entity, last_updated: new Date().toISOString() };
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
83
91
|
archive(personaId: string): boolean {
|
|
84
92
|
const data = this.personas.get(personaId);
|
|
85
93
|
if (!data) return false;
|
|
@@ -715,6 +715,11 @@ export class StateManager {
|
|
|
715
715
|
this.scheduleSave();
|
|
716
716
|
}
|
|
717
717
|
|
|
718
|
+
human_quote_upsert(quote: Quote): void {
|
|
719
|
+
this.humanState.quote_upsert(quote);
|
|
720
|
+
this.scheduleSave();
|
|
721
|
+
}
|
|
722
|
+
|
|
718
723
|
human_quote_update(id: string, updates: Partial<Quote>): boolean {
|
|
719
724
|
const result = this.humanState.quote_update(id, updates);
|
|
720
725
|
this.scheduleSave();
|
|
@@ -758,6 +763,12 @@ export class StateManager {
|
|
|
758
763
|
return result;
|
|
759
764
|
}
|
|
760
765
|
|
|
766
|
+
persona_replace(personaId: string, entity: PersonaEntity): boolean {
|
|
767
|
+
const result = this.personaState.replace(personaId, entity);
|
|
768
|
+
this.scheduleSave();
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
|
|
761
772
|
persona_archive(personaId: string): boolean {
|
|
762
773
|
const result = this.personaState.archive(personaId);
|
|
763
774
|
this.scheduleSave();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PersonIdentifier } from "../types/data-items.js";
|
|
2
|
+
import type { PersonaEntity } from "../types/entities.js";
|
|
2
3
|
import type { StateManager } from "../state-manager.js";
|
|
3
4
|
import { BUILT_IN_IDENTIFIER_TYPES } from "../constants/built-in-identifier-types.js";
|
|
4
5
|
|
|
@@ -29,12 +30,12 @@ export function normalizeIdentifierType(llmType: string, state: StateManager): s
|
|
|
29
30
|
|
|
30
31
|
export function sanitizeEiPersonaIdentifiers(
|
|
31
32
|
identifiers: PersonIdentifier[],
|
|
32
|
-
|
|
33
|
+
personas: PersonaEntity[]
|
|
33
34
|
): PersonIdentifier[] {
|
|
34
35
|
return identifiers.map(id => {
|
|
35
36
|
if (id.type !== 'Ei Persona' && id.type !== 'AI Persona') return id;
|
|
36
37
|
if (UUID_REGEX.test(id.value)) return { ...id, type: 'Ei Persona' };
|
|
37
|
-
const matched =
|
|
38
|
+
const matched = personas.find(p =>
|
|
38
39
|
p.display_name === id.value || p.aliases?.includes(id.value)
|
|
39
40
|
);
|
|
40
41
|
if (matched) return { ...id, type: 'Ei Persona', value: matched.id };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-based advisory locking + atomic writes, shared by any Storage
|
|
3
|
+
* implementation (and CLI tooling) that needs to serialize writes to a
|
|
4
|
+
* JSON file on disk without a database.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from tui/src/storage/file.ts so the CLI's corrections.json
|
|
7
|
+
* writer/drainer can use identical lock semantics instead of forking a
|
|
8
|
+
* second implementation.
|
|
9
|
+
*
|
|
10
|
+
* fs/promises is imported dynamically per-function, not statically at
|
|
11
|
+
* module scope: this module is transitively imported by src/core/corrections.ts
|
|
12
|
+
* -> src/core/processor.ts, which Web's Vite build bundles for the browser.
|
|
13
|
+
* A static `import { readFile } from "fs/promises"` makes rollup try to
|
|
14
|
+
* resolve named exports against Vite's browser-externalized stub, which
|
|
15
|
+
* has none — that's a hard build failure, not a runtime one (same pattern
|
|
16
|
+
* already used in src/cli.ts and src/cli/install.ts for this exact reason).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const LOCK_TIMEOUT_MS = 5000;
|
|
20
|
+
const LOCK_RETRY_DELAY_MS = 50;
|
|
21
|
+
|
|
22
|
+
// Plain executor form, not Promise.withResolvers — this module runs on
|
|
23
|
+
// whatever Node the CLI/TUI ships with, and withResolvers only landed in
|
|
24
|
+
// Node 22. No behavior difference, just broader runtime compatibility.
|
|
25
|
+
function delay(ms: number): Promise<void> {
|
|
26
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getLockPath(filePath: string): string {
|
|
30
|
+
return `${filePath}.lock`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Acquire an advisory lock on filePath. Stale locks (older than
|
|
35
|
+
* LOCK_TIMEOUT_MS) are broken automatically. Returns false if the lock
|
|
36
|
+
* could not be acquired within LOCK_TIMEOUT_MS.
|
|
37
|
+
*/
|
|
38
|
+
export async function acquireLock(filePath: string): Promise<boolean> {
|
|
39
|
+
const { readFile, writeFile, unlink } = await import(/* @vite-ignore */ "fs/promises");
|
|
40
|
+
const lockPath = getLockPath(filePath);
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
|
|
43
|
+
while (Date.now() - startTime < LOCK_TIMEOUT_MS) {
|
|
44
|
+
// readFile throws ENOENT when the lock is absent — treated the same as
|
|
45
|
+
// "lock vanished mid-read" below: proceed straight to acquire.
|
|
46
|
+
let lockContent: string | null = null;
|
|
47
|
+
try {
|
|
48
|
+
lockContent = await readFile(lockPath, "utf-8");
|
|
49
|
+
} catch {
|
|
50
|
+
lockContent = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (lockContent !== null) {
|
|
54
|
+
const lockTime = parseInt(lockContent, 10);
|
|
55
|
+
if (!isNaN(lockTime) && Date.now() - lockTime > LOCK_TIMEOUT_MS) {
|
|
56
|
+
try {
|
|
57
|
+
await unlink(lockPath);
|
|
58
|
+
} catch {}
|
|
59
|
+
} else {
|
|
60
|
+
await delay(LOCK_RETRY_DELAY_MS);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// "wx" = exclusive create, fails with EEXIST if the path already
|
|
67
|
+
// exists. Without this flag, two callers that both observed the lock
|
|
68
|
+
// as absent (the check above) could both reach this writeFile and
|
|
69
|
+
// both succeed — writeFile has no atomicity of its own, it just
|
|
70
|
+
// overwrites. "wx" makes the write itself the atomic test-and-set:
|
|
71
|
+
// only one caller can win it, the other gets EEXIST and falls into
|
|
72
|
+
// the catch below to retry from the top of the loop.
|
|
73
|
+
await writeFile(lockPath, Date.now().toString(), { flag: "wx" });
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
await delay(LOCK_RETRY_DELAY_MS);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function releaseLock(filePath: string): Promise<void> {
|
|
84
|
+
const { unlink } = await import(/* @vite-ignore */ "fs/promises");
|
|
85
|
+
const lockPath = getLockPath(filePath);
|
|
86
|
+
try {
|
|
87
|
+
await unlink(lockPath);
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Run fn() while holding filePath's advisory lock. Throws
|
|
93
|
+
* STORAGE_LOCK_TIMEOUT if the lock can't be acquired in time.
|
|
94
|
+
*/
|
|
95
|
+
export async function withLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
|
|
96
|
+
const acquired = await acquireLock(filePath);
|
|
97
|
+
if (!acquired) {
|
|
98
|
+
throw new Error("STORAGE_LOCK_TIMEOUT: Could not acquire file lock");
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
return await fn();
|
|
102
|
+
} finally {
|
|
103
|
+
await releaseLock(filePath);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Write content to filePath via temp-file + rename, so readers never see a partial write. */
|
|
108
|
+
export async function atomicWrite(filePath: string, content: string): Promise<void> {
|
|
109
|
+
const { writeFile, rename, unlink } = await import(/* @vite-ignore */ "fs/promises");
|
|
110
|
+
const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
111
|
+
try {
|
|
112
|
+
await writeFile(tempPath, content);
|
|
113
|
+
await rename(tempPath, filePath);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
try {
|
|
116
|
+
await unlink(tempPath);
|
|
117
|
+
} catch {}
|
|
118
|
+
throw e;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/tui/README.md
CHANGED
|
@@ -174,6 +174,8 @@ Rooms have three modes, set at creation time:
|
|
|
174
174
|
| `/tools` | | Manage tool providers — enable/disable tools per persona |
|
|
175
175
|
| `/auth <service>` | | Authenticate with an external service via OAuth. Supported: `spotify`, `slack` |
|
|
176
176
|
|
|
177
|
+
> Disabling a tool provider hides its tools from `/details`' YAML editor, but doesn't revoke them — a persona's grants for a disabled provider's tools survive an unrelated `/details` edit and reappear once you re-enable the provider with `/tools` or `/provider`.
|
|
178
|
+
|
|
177
179
|
### Editor
|
|
178
180
|
|
|
179
181
|
| Command | Aliases | Description |
|
|
@@ -48,7 +48,7 @@ export async function openModelOverlay(ctx: Parameters<Command["execute"]>[1]):
|
|
|
48
48
|
const models = await buildModelList(ctx);
|
|
49
49
|
|
|
50
50
|
if (models.length === 0) {
|
|
51
|
-
ctx
|
|
51
|
+
await createProviderViaEditor(ctx);
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -90,9 +90,9 @@ export function WelcomeOverlay(props: WelcomeOverlayProps) {
|
|
|
90
90
|
<box visible={!hasAny()} flexDirection="column">
|
|
91
91
|
<text fg="#dc322f">No LLM provider detected.</text>
|
|
92
92
|
<text> </text>
|
|
93
|
-
<text fg="#93a1a1">
|
|
94
|
-
<text fg="#93a1a1">
|
|
95
|
-
<text fg="#93a1a1">
|
|
93
|
+
<text fg="#93a1a1">Use /provider new to configure one manually, or</text>
|
|
94
|
+
<text fg="#93a1a1">start LMStudio (port 1234) / Ollama (port 11434), or</text>
|
|
95
|
+
<text fg="#93a1a1">set ANTHROPIC_API_KEY, OPENAI_API_KEY, etc. and restart.</text>
|
|
96
96
|
</box>
|
|
97
97
|
|
|
98
98
|
<text> </text>
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
useContext,
|
|
4
4
|
onMount,
|
|
5
5
|
onCleanup,
|
|
6
|
+
For,
|
|
6
7
|
Match,
|
|
7
8
|
Switch,
|
|
8
9
|
createSignal,
|
|
@@ -183,6 +184,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
183
184
|
const [showWelcomeOverlay, setShowWelcomeOverlay] = createSignal(false);
|
|
184
185
|
const [detectedProviders, setDetectedProviders] = createSignal<ProviderDetectionStatus[]>([]);
|
|
185
186
|
const [firstBootDefaultModel, setFirstBootDefaultModel] = createSignal<string | undefined>(undefined);
|
|
187
|
+
const [bootError, setBootError] = createSignal<string | null>(null);
|
|
186
188
|
const [conflictData, setConflictData] = createSignal<StateConflictData | null>(null);
|
|
187
189
|
|
|
188
190
|
let processor: Processor | null = null;
|
|
@@ -970,6 +972,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
970
972
|
await finishBootstrap();
|
|
971
973
|
} catch (err: any) {
|
|
972
974
|
logger.error(`bootstrap() failed: ${err?.message || err}`);
|
|
975
|
+
setBootError(err?.message || String(err));
|
|
973
976
|
}
|
|
974
977
|
}
|
|
975
978
|
|
|
@@ -1089,6 +1092,17 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
1089
1092
|
<Match when={store.ready}>
|
|
1090
1093
|
<EiContext.Provider value={value}>{props.children}</EiContext.Provider>
|
|
1091
1094
|
</Match>
|
|
1095
|
+
<Match when={bootError()}>
|
|
1096
|
+
<box width="100%" height="100%" justifyContent="center" alignItems="center" flexDirection="column">
|
|
1097
|
+
<text fg="#dc322f">Ei failed to start</text>
|
|
1098
|
+
<text> </text>
|
|
1099
|
+
<For each={bootError()!.split('\n')}>
|
|
1100
|
+
{(line) => <text fg="#93a1a1">{line || " "}</text>}
|
|
1101
|
+
</For>
|
|
1102
|
+
<text> </text>
|
|
1103
|
+
<text fg="#586e75">Press Ctrl+C to exit</text>
|
|
1104
|
+
</box>
|
|
1105
|
+
</Match>
|
|
1092
1106
|
<Match when={!store.ready}>
|
|
1093
1107
|
<box width="100%" height="100%" justifyContent="center" alignItems="center">
|
|
1094
1108
|
<text>Loading Ei...</text>
|
package/tui/src/index.tsx
CHANGED
|
@@ -14,7 +14,19 @@ if (args.includes("--version") || args.includes("version") || args.includes("-v"
|
|
|
14
14
|
|
|
15
15
|
const storage = new FileStorage(Bun.env.EI_DATA_PATH);
|
|
16
16
|
const lock = new InstanceLock(storage.getDataPath());
|
|
17
|
-
const lockResult = await lock.acquire()
|
|
17
|
+
const lockResult = await lock.acquire().catch((e) => {
|
|
18
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
19
|
+
const dataPath = storage.getDataPath();
|
|
20
|
+
process.stderr.write(
|
|
21
|
+
`\nEi cannot start: cannot write to data directory.\n\n` +
|
|
22
|
+
` Path: ${dataPath}\n` +
|
|
23
|
+
` Error: ${msg}\n\n` +
|
|
24
|
+
`Fix options:\n` +
|
|
25
|
+
` - Fix Permissions (sudo chown $USER $EI_DATA_PATH)\n` +
|
|
26
|
+
` - Change Data Path (EI_DATA_PATH=~/ei-data ei)\n\n`
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
18
30
|
|
|
19
31
|
if (!lockResult.acquired) {
|
|
20
32
|
process.stderr.write(
|
package/tui/src/storage/file.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { StorageState } from "../../../src/core/types";
|
|
2
2
|
import type { Storage } from "../../../src/storage/interface";
|
|
3
3
|
import { encodeAllEmbeddings, decodeAllEmbeddings } from "../../../src/storage/embeddings";
|
|
4
|
+
import { withLock, atomicWrite } from "../../../src/storage/file-lock";
|
|
4
5
|
import { join } from "path";
|
|
5
6
|
import { mkdir, rename, unlink, readdir } from "fs/promises";
|
|
6
7
|
import { resolveDataPath } from "../util/resolve-data-path.js";
|
|
@@ -8,8 +9,6 @@ import { resolveDataPath } from "../util/resolve-data-path.js";
|
|
|
8
9
|
const STATE_FILE = "state.json";
|
|
9
10
|
const BACKUP_FILE = "state.backup.json";
|
|
10
11
|
const BACKUPS_DIR = "backups";
|
|
11
|
-
const LOCK_TIMEOUT_MS = 5000;
|
|
12
|
-
const LOCK_RETRY_DELAY_MS = 50;
|
|
13
12
|
|
|
14
13
|
export class FileStorage implements Storage {
|
|
15
14
|
private readonly dataPath: string;
|
|
@@ -39,9 +38,9 @@ export class FileStorage implements Storage {
|
|
|
39
38
|
const filePath = join(this.dataPath, STATE_FILE);
|
|
40
39
|
state.timestamp = new Date().toISOString();
|
|
41
40
|
|
|
42
|
-
await
|
|
41
|
+
await withLock(filePath, async () => {
|
|
43
42
|
try {
|
|
44
|
-
await
|
|
43
|
+
await atomicWrite(filePath, JSON.stringify(encodeAllEmbeddings(state), null, 2));
|
|
45
44
|
} catch (e) {
|
|
46
45
|
if (this.isQuotaError(e)) {
|
|
47
46
|
throw new Error("STORAGE_SAVE_FAILED: Disk quota exceeded");
|
|
@@ -89,7 +88,7 @@ export class FileStorage implements Storage {
|
|
|
89
88
|
async saveBackup(state: StorageState): Promise<void> {
|
|
90
89
|
await this.ensureDataDir();
|
|
91
90
|
const backupPath = join(this.dataPath, BACKUP_FILE);
|
|
92
|
-
await
|
|
91
|
+
await atomicWrite(backupPath, JSON.stringify(encodeAllEmbeddings(state), null, 2));
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
/**
|
|
@@ -130,7 +129,7 @@ export class FileStorage implements Storage {
|
|
|
130
129
|
].join("") + ".json";
|
|
131
130
|
|
|
132
131
|
const destPath = join(backupsPath, name);
|
|
133
|
-
await
|
|
132
|
+
await atomicWrite(destPath, JSON.stringify(encodeAllEmbeddings(state), null, 2));
|
|
134
133
|
|
|
135
134
|
// Prune: keep only the newest maxBackups files
|
|
136
135
|
const entries = await readdir(backupsPath);
|
|
@@ -149,8 +148,16 @@ export class FileStorage implements Storage {
|
|
|
149
148
|
private async ensureDataDir(): Promise<void> {
|
|
150
149
|
try {
|
|
151
150
|
await mkdir(this.dataPath, { recursive: true });
|
|
152
|
-
} catch {
|
|
153
|
-
|
|
151
|
+
} catch (e: any) {
|
|
152
|
+
if (e?.code === 'EACCES' || e?.code === 'EPERM') {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Cannot create data directory: ${this.dataPath}\n` +
|
|
155
|
+
`Fix options:\n` +
|
|
156
|
+
` - Fix Permissions (sudo chown $USER $EI_DATA_PATH)\n` +
|
|
157
|
+
` - Change Data Path (EI_DATA_PATH=~/ei-data ei-tui)`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
// Other errors (e.g., race conditions) are safe to ignore
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
|
|
@@ -160,79 +167,4 @@ export class FileStorage implements Storage {
|
|
|
160
167
|
(e.message.includes("ENOSPC") || e.message.includes("quota"))
|
|
161
168
|
);
|
|
162
169
|
}
|
|
163
|
-
|
|
164
|
-
private async atomicWrite(filePath: string, content: string): Promise<void> {
|
|
165
|
-
const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
166
|
-
try {
|
|
167
|
-
await Bun.write(tempPath, content);
|
|
168
|
-
await rename(tempPath, filePath);
|
|
169
|
-
} catch (e) {
|
|
170
|
-
try {
|
|
171
|
-
await unlink(tempPath);
|
|
172
|
-
} catch {}
|
|
173
|
-
throw e;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
private getLockPath(filePath: string): string {
|
|
178
|
-
return `${filePath}.lock`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private async acquireLock(filePath: string): Promise<boolean> {
|
|
182
|
-
const lockPath = this.getLockPath(filePath);
|
|
183
|
-
const startTime = Date.now();
|
|
184
|
-
|
|
185
|
-
while (Date.now() - startTime < LOCK_TIMEOUT_MS) {
|
|
186
|
-
const lockFile = Bun.file(lockPath);
|
|
187
|
-
if (await lockFile.exists()) {
|
|
188
|
-
// Read may throw if another writer deleted the lock between exists() and text() —
|
|
189
|
-
// treat that as "lock is gone, proceed to acquire" by falling through.
|
|
190
|
-
let lockContent: string;
|
|
191
|
-
try {
|
|
192
|
-
lockContent = await lockFile.text();
|
|
193
|
-
} catch {
|
|
194
|
-
// Lock vanished in the race window — retry from top to re-check state cleanly.
|
|
195
|
-
await new Promise((r) => setTimeout(r, LOCK_RETRY_DELAY_MS));
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
const lockTime = parseInt(lockContent, 10);
|
|
199
|
-
if (!isNaN(lockTime) && Date.now() - lockTime > LOCK_TIMEOUT_MS) {
|
|
200
|
-
try {
|
|
201
|
-
await unlink(lockPath);
|
|
202
|
-
} catch {}
|
|
203
|
-
} else {
|
|
204
|
-
await new Promise((r) => setTimeout(r, LOCK_RETRY_DELAY_MS));
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
await Bun.write(lockPath, Date.now().toString());
|
|
211
|
-
return true;
|
|
212
|
-
} catch {
|
|
213
|
-
await new Promise((r) => setTimeout(r, LOCK_RETRY_DELAY_MS));
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private async releaseLock(filePath: string): Promise<void> {
|
|
221
|
-
const lockPath = this.getLockPath(filePath);
|
|
222
|
-
try {
|
|
223
|
-
await unlink(lockPath);
|
|
224
|
-
} catch {}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private async withLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
|
|
228
|
-
const acquired = await this.acquireLock(filePath);
|
|
229
|
-
if (!acquired) {
|
|
230
|
-
throw new Error("STORAGE_LOCK_TIMEOUT: Could not acquire file lock");
|
|
231
|
-
}
|
|
232
|
-
try {
|
|
233
|
-
return await fn();
|
|
234
|
-
} finally {
|
|
235
|
-
await this.releaseLock(filePath);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
170
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { join } from "path";
|
|
2
|
-
import { unlink } from "fs/promises";
|
|
1
|
+
import { join, dirname } from "path";
|
|
2
|
+
import { unlink, mkdir } from "fs/promises";
|
|
3
3
|
|
|
4
4
|
const LOCK_FILE = "ei.lock";
|
|
5
5
|
|
|
@@ -69,6 +69,7 @@ export class InstanceLock {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
private async writeLock(): Promise<void> {
|
|
72
|
+
await mkdir(dirname(this.lockPath), { recursive: true });
|
|
72
73
|
const data: LockData = {
|
|
73
74
|
pid: process.pid,
|
|
74
75
|
started: new Date().toISOString(),
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ProviderAccount,
|
|
8
8
|
} from "../../../src/core/types.js";
|
|
9
9
|
import { modelGuidToDisplay, displayToModelGuid } from "./yaml-shared.js";
|
|
10
|
+
import { buildPersonaToolsMap, resolvePersonaToolsFromMap, preserveHiddenToolGrants } from "../../../src/core/persona-tools.js";
|
|
10
11
|
import { parseDuration, formatDuration } from "./duration.js";
|
|
11
12
|
|
|
12
13
|
const PLACEHOLDER_LONG_DESC = "Detailed description of this persona's personality, background, and role";
|
|
@@ -62,43 +63,6 @@ const PLACEHOLDER_TOPIC: YAMLPersonaTopic = {
|
|
|
62
63
|
exposure_desired: 0.5,
|
|
63
64
|
};
|
|
64
65
|
|
|
65
|
-
function buildPersonaToolsMap(
|
|
66
|
-
enabledToolIds: string[],
|
|
67
|
-
allTools: ToolDefinition[],
|
|
68
|
-
allProviders: import('../../../src/core/types.js').ToolProvider[]
|
|
69
|
-
): Record<string, Record<string, boolean>> | undefined {
|
|
70
|
-
if (allTools.length === 0) return undefined;
|
|
71
|
-
const enabledSet = new Set(enabledToolIds);
|
|
72
|
-
const result: Record<string, Record<string, boolean>> = {};
|
|
73
|
-
for (const provider of allProviders.filter(p => p.enabled)) {
|
|
74
|
-
const providerTools = allTools.filter(t => t.provider_id === provider.id);
|
|
75
|
-
if (providerTools.length === 0) continue;
|
|
76
|
-
result[provider.display_name] = Object.fromEntries(
|
|
77
|
-
providerTools.map(t => [t.display_name, enabledSet.has(t.id)])
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
return Object.keys(result).length > 0 ? result : undefined;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function resolvePersonaToolsFromMap(
|
|
84
|
-
toolsMap: Record<string, Record<string, boolean>> | undefined,
|
|
85
|
-
allTools: ToolDefinition[],
|
|
86
|
-
allProviders: import('../../../src/core/types.js').ToolProvider[]
|
|
87
|
-
): string[] | undefined {
|
|
88
|
-
if (!toolsMap) return undefined;
|
|
89
|
-
const enabledIds: string[] = [];
|
|
90
|
-
for (const [providerDisplayName, toolToggles] of Object.entries(toolsMap)) {
|
|
91
|
-
const provider = allProviders.find(p => p.display_name === providerDisplayName);
|
|
92
|
-
if (!provider) continue;
|
|
93
|
-
for (const [toolDisplayName, enabled] of Object.entries(toolToggles)) {
|
|
94
|
-
if (!enabled) continue;
|
|
95
|
-
const tool = allTools.find(t => t.provider_id === provider.id && t.display_name === toolDisplayName);
|
|
96
|
-
if (tool) enabledIds.push(tool.id);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return enabledIds.length > 0 ? enabledIds : [];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
66
|
export function newPersonaToYAML(name: string, allTools?: ToolDefinition[], allProviders?: import('../../../src/core/types.js').ToolProvider[]): string {
|
|
103
67
|
const toolsMap = buildPersonaToolsMap([], allTools ?? [], allProviders ?? []);
|
|
104
68
|
|
|
@@ -337,7 +301,12 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity, al
|
|
|
337
301
|
pause_until: data.pause_until,
|
|
338
302
|
is_static: data.is_static ?? false,
|
|
339
303
|
include_message_timestamps: data.include_message_timestamps ?? false,
|
|
340
|
-
tools:
|
|
304
|
+
tools: preserveHiddenToolGrants(
|
|
305
|
+
resolvePersonaToolsFromMap(data.tools, allTools ?? [], allProviders ?? []),
|
|
306
|
+
original.tools,
|
|
307
|
+
allTools ?? [],
|
|
308
|
+
allProviders ?? []
|
|
309
|
+
),
|
|
341
310
|
last_updated: new Date().toISOString(),
|
|
342
311
|
};
|
|
343
312
|
|