ei-tui 0.1.3 → 0.1.4
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 +31 -34
- package/package.json +5 -1
- package/src/README.md +85 -1
- package/src/cli/README.md +29 -21
- package/src/cli/retrieval.ts +5 -17
- package/src/cli.ts +69 -0
- package/src/core/handlers/index.ts +91 -158
- package/src/core/orchestrators/ceremony.ts +1 -1
- package/src/core/processor.ts +172 -45
- package/src/core/state/checkpoints.ts +4 -0
- package/src/core/state/queue.ts +1 -10
- package/src/core/state-manager.ts +1 -7
- package/src/core/types.ts +4 -5
- package/src/core/utils/crossFind.ts +44 -0
- package/src/core/utils/index.ts +4 -0
- package/src/integrations/opencode/importer.ts +117 -690
- package/src/prompts/heartbeat/ei.ts +61 -133
- package/src/prompts/heartbeat/types.ts +47 -17
- package/src/prompts/index.ts +2 -5
- package/tui/README.md +77 -3
- package/tui/src/commands/editor.tsx +1 -1
- package/tui/src/components/CommandSuggest.tsx +50 -0
- package/tui/src/components/PromptInput.tsx +111 -29
- package/tui/src/components/Sidebar.tsx +6 -2
- package/tui/src/context/ei.tsx +10 -0
- package/tui/src/context/keyboard.tsx +90 -2
- package/tui/src/util/clipboard.ts +73 -0
- package/tui/src/util/yaml-serializers.ts +12 -4
- package/src/prompts/validation/ei.ts +0 -93
- package/src/prompts/validation/index.ts +0 -6
- package/src/prompts/validation/types.ts +0 -22
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createEffect, createSignal } from "solid-js";
|
|
2
|
+
import { getAllCommands } from "../commands/registry";
|
|
1
3
|
import type { TextareaRenderable, KeyBinding } from "@opentui/core";
|
|
2
4
|
import { useEi } from "../context/ei";
|
|
3
5
|
import { useKeyboardNav } from "../context/keyboard";
|
|
@@ -20,6 +22,9 @@ import { quotesCommand } from "../commands/quotes";
|
|
|
20
22
|
import { providerCommand } from "../commands/provider";
|
|
21
23
|
import { setSyncCommand } from "../commands/setsync";
|
|
22
24
|
import { useOverlay } from "../context/overlay";
|
|
25
|
+
import { CommandSuggest } from "./CommandSuggest";
|
|
26
|
+
import { useKeyboard } from "@opentui/solid";
|
|
27
|
+
import type { KeyEvent } from "@opentui/core";
|
|
23
28
|
|
|
24
29
|
const TEXTAREA_KEYBINDINGS: KeyBinding[] = [
|
|
25
30
|
{ name: "return", action: "submit" },
|
|
@@ -30,7 +35,7 @@ const TEXTAREA_KEYBINDINGS: KeyBinding[] = [
|
|
|
30
35
|
export function PromptInput() {
|
|
31
36
|
const ei = useEi();
|
|
32
37
|
const { sendMessage, activePersonaId, stopProcessor, showNotification } = ei;
|
|
33
|
-
const { registerTextarea, registerEditorHandler, exitApp, renderer } = useKeyboardNav();
|
|
38
|
+
const { registerTextarea, registerEditorHandler, exitApp, renderer, resetHistoryIndex } = useKeyboardNav();
|
|
34
39
|
const { showOverlay, hideOverlay, overlayRenderer } = useOverlay();
|
|
35
40
|
|
|
36
41
|
registerCommand(helpCommand);
|
|
@@ -54,6 +59,65 @@ export function PromptInput() {
|
|
|
54
59
|
|
|
55
60
|
let textareaRef: TextareaRenderable | undefined;
|
|
56
61
|
|
|
62
|
+
const [inputText, setInputText] = createSignal("");
|
|
63
|
+
const [suggestIndex, setSuggestIndex] = createSignal(0);
|
|
64
|
+
|
|
65
|
+
const suggestMatches = () => {
|
|
66
|
+
const raw = inputText().trim();
|
|
67
|
+
if (!raw.startsWith("/")) return [];
|
|
68
|
+
const query = raw.slice(1).split(/\s/)[0].replace(/!$/, "").toLowerCase();
|
|
69
|
+
return getAllCommands().filter(
|
|
70
|
+
(cmd) =>
|
|
71
|
+
cmd.name.startsWith(query) ||
|
|
72
|
+
cmd.aliases.some((a) => a.startsWith(query))
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const suggestVisible = () => suggestMatches().length > 0 && !overlayRenderer();
|
|
77
|
+
|
|
78
|
+
createEffect(() => {
|
|
79
|
+
inputText();
|
|
80
|
+
setSuggestIndex(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
createEffect(() => {
|
|
84
|
+
activePersonaId();
|
|
85
|
+
resetHistoryIndex();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
useKeyboard((event: KeyEvent) => {
|
|
89
|
+
if (!suggestVisible()) return;
|
|
90
|
+
|
|
91
|
+
if (event.name === "up") {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
setSuggestIndex(i => Math.max(0, i - 1));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (event.name === "down") {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
setSuggestIndex(i => Math.min(suggestMatches().length - 1, i + 1));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (event.name === "tab" || event.name === "right") {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
const match = suggestMatches()[suggestIndex()];
|
|
104
|
+
if (match) {
|
|
105
|
+
textareaRef?.setText(`/${match.name} `);
|
|
106
|
+
setInputText(`/${match.name} `);
|
|
107
|
+
textareaRef?.gotoBufferEnd();
|
|
108
|
+
setSuggestIndex(0);
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (event.name === "escape") {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
textareaRef?.clear();
|
|
115
|
+
setInputText("");
|
|
116
|
+
setSuggestIndex(0);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
57
121
|
const getCommandContext = (): CommandContext => ({
|
|
58
122
|
showOverlay,
|
|
59
123
|
hideOverlay,
|
|
@@ -62,21 +126,24 @@ export function PromptInput() {
|
|
|
62
126
|
stopProcessor,
|
|
63
127
|
ei,
|
|
64
128
|
renderer,
|
|
65
|
-
setInputText: (text: string) =>
|
|
129
|
+
setInputText: (text: string) => {
|
|
130
|
+
textareaRef?.setText(text);
|
|
131
|
+
setInputText(text);
|
|
132
|
+
},
|
|
66
133
|
getInputText: () => textareaRef?.plainText || "",
|
|
67
134
|
});
|
|
68
135
|
|
|
69
136
|
const handleSubmit = async () => {
|
|
70
137
|
const text = textareaRef?.plainText?.trim();
|
|
71
138
|
if (!text) return;
|
|
72
|
-
|
|
139
|
+
|
|
73
140
|
if (text.startsWith("/")) {
|
|
74
141
|
const isEditorCmd = text.startsWith("/editor") ||
|
|
75
142
|
text.startsWith("/edit") ||
|
|
76
143
|
text.startsWith("/e ") ||
|
|
77
144
|
text === "/e";
|
|
78
|
-
const opensEditorForData = text.startsWith("/me") ||
|
|
79
|
-
text.startsWith("/details") ||
|
|
145
|
+
const opensEditorForData = text.startsWith("/me") ||
|
|
146
|
+
text.startsWith("/details") ||
|
|
80
147
|
text.startsWith("/d ") ||
|
|
81
148
|
text === "/d" ||
|
|
82
149
|
text.startsWith("/settings") ||
|
|
@@ -87,18 +154,24 @@ export function PromptInput() {
|
|
|
87
154
|
text.startsWith("/q ") ||
|
|
88
155
|
text.startsWith("/context") ||
|
|
89
156
|
text.startsWith("/messages");
|
|
90
|
-
|
|
157
|
+
|
|
91
158
|
if (!isEditorCmd && !opensEditorForData) {
|
|
92
159
|
textareaRef?.clear();
|
|
160
|
+
setInputText("");
|
|
93
161
|
}
|
|
94
162
|
await parseAndExecute(text, getCommandContext());
|
|
95
163
|
if (opensEditorForData) {
|
|
96
164
|
textareaRef?.clear();
|
|
165
|
+
setInputText("");
|
|
97
166
|
}
|
|
167
|
+
setSuggestIndex(0);
|
|
98
168
|
return;
|
|
99
169
|
}
|
|
100
|
-
|
|
170
|
+
|
|
101
171
|
textareaRef?.clear();
|
|
172
|
+
setInputText("");
|
|
173
|
+
resetHistoryIndex();
|
|
174
|
+
setSuggestIndex(0);
|
|
102
175
|
if (!activePersonaId()) return;
|
|
103
176
|
await sendMessage(text);
|
|
104
177
|
};
|
|
@@ -115,31 +188,40 @@ export function PromptInput() {
|
|
|
115
188
|
};
|
|
116
189
|
|
|
117
190
|
return (
|
|
118
|
-
<box
|
|
191
|
+
<box
|
|
192
|
+
flexDirection="column"
|
|
119
193
|
flexShrink={0}
|
|
120
|
-
border={["top"]}
|
|
121
|
-
borderStyle="single"
|
|
122
|
-
backgroundColor="#0f3460"
|
|
123
|
-
paddingLeft={1}
|
|
124
|
-
paddingRight={1}
|
|
125
|
-
paddingTop={0.5}
|
|
126
|
-
paddingBottom={0.5}
|
|
127
194
|
>
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
registerTextarea(r);
|
|
132
|
-
}}
|
|
133
|
-
focused={!overlayRenderer()}
|
|
134
|
-
onSubmit={() => void handleSubmit()}
|
|
135
|
-
placeholder={getPlaceholder()}
|
|
136
|
-
textColor="#eee8d5"
|
|
137
|
-
backgroundColor="#0f3460"
|
|
138
|
-
cursorColor="#eee8d5"
|
|
139
|
-
minHeight={1}
|
|
140
|
-
maxHeight={6}
|
|
141
|
-
keyBindings={overlayRenderer() ? [] : TEXTAREA_KEYBINDINGS}
|
|
195
|
+
<CommandSuggest
|
|
196
|
+
input={inputText}
|
|
197
|
+
highlightIndex={suggestIndex}
|
|
142
198
|
/>
|
|
199
|
+
<box
|
|
200
|
+
border={["top"]}
|
|
201
|
+
borderStyle="single"
|
|
202
|
+
backgroundColor="#0f3460"
|
|
203
|
+
paddingLeft={1}
|
|
204
|
+
paddingRight={1}
|
|
205
|
+
paddingTop={0.5}
|
|
206
|
+
paddingBottom={0.5}
|
|
207
|
+
>
|
|
208
|
+
<textarea
|
|
209
|
+
ref={(r: TextareaRenderable) => {
|
|
210
|
+
textareaRef = r;
|
|
211
|
+
registerTextarea(r);
|
|
212
|
+
}}
|
|
213
|
+
focused={!overlayRenderer()}
|
|
214
|
+
onSubmit={() => void handleSubmit()}
|
|
215
|
+
onContentChange={() => setInputText(textareaRef?.plainText ?? "")}
|
|
216
|
+
placeholder={getPlaceholder()}
|
|
217
|
+
textColor="#eee8d5"
|
|
218
|
+
backgroundColor="#0f3460"
|
|
219
|
+
cursorColor="#eee8d5"
|
|
220
|
+
minHeight={1}
|
|
221
|
+
maxHeight={6}
|
|
222
|
+
keyBindings={overlayRenderer() ? [] : TEXTAREA_KEYBINDINGS}
|
|
223
|
+
/>
|
|
224
|
+
</box>
|
|
143
225
|
</box>
|
|
144
226
|
);
|
|
145
227
|
}
|
|
@@ -71,6 +71,7 @@ export function Sidebar() {
|
|
|
71
71
|
|
|
72
72
|
return (
|
|
73
73
|
<box
|
|
74
|
+
flexDirection="column"
|
|
74
75
|
backgroundColor={
|
|
75
76
|
isActive() && highlightedPersona() === persona.id
|
|
76
77
|
? "#3d5a80"
|
|
@@ -78,12 +79,15 @@ export function Sidebar() {
|
|
|
78
79
|
? "#2d3748"
|
|
79
80
|
: "transparent"
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
marginBottom={
|
|
82
|
+
paddingX={1}
|
|
83
|
+
marginBottom={1}
|
|
83
84
|
>
|
|
84
85
|
<text fg={textColor()}>
|
|
85
86
|
{getLabel()}
|
|
86
87
|
</text>
|
|
88
|
+
<text fg="#586e75" wrapMode="word" height={2} visible={!!persona.short_description}>
|
|
89
|
+
{persona.short_description ?? ""}
|
|
90
|
+
</text>
|
|
87
91
|
</box>
|
|
88
92
|
);
|
|
89
93
|
}}
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -98,6 +98,7 @@ export interface EiContextValue {
|
|
|
98
98
|
dismissWelcomeOverlay: () => void;
|
|
99
99
|
deleteMessages: (personaId: string, messageIds: string[]) => Promise<void>;
|
|
100
100
|
setMessageContextStatus: (personaId: string, messageId: string, status: ContextStatus) => Promise<void>;
|
|
101
|
+
recallPendingMessages: () => Promise<string>;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
const EiContext = createContext<EiContextValue>();
|
|
@@ -378,6 +379,14 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
378
379
|
await processor.setMessageContextStatus(personaId, messageId, status);
|
|
379
380
|
};
|
|
380
381
|
|
|
382
|
+
const recallPendingMessages = async (): Promise<string> => {
|
|
383
|
+
if (!processor) return "";
|
|
384
|
+
const personaId = store.activePersonaId;
|
|
385
|
+
if (!personaId) return "";
|
|
386
|
+
return processor.recallPendingMessages(personaId);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
|
|
381
390
|
const searchHumanData = async (
|
|
382
391
|
query: string,
|
|
383
392
|
options?: { types?: Array<"fact" | "trait" | "topic" | "person" | "quote">; limit?: number }
|
|
@@ -591,6 +600,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
591
600
|
dismissWelcomeOverlay: () => setShowWelcomeOverlay(false),
|
|
592
601
|
deleteMessages,
|
|
593
602
|
setMessageContextStatus,
|
|
603
|
+
recallPendingMessages,
|
|
594
604
|
};
|
|
595
605
|
|
|
596
606
|
return (
|
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
type ParentComponent,
|
|
6
6
|
type Accessor,
|
|
7
7
|
} from "solid-js";
|
|
8
|
-
import { useKeyboard, useRenderer } from "@opentui/solid";
|
|
8
|
+
import { useKeyboard, useRenderer, useSelectionHandler } from "@opentui/solid";
|
|
9
9
|
import type { ScrollBoxRenderable, KeyEvent, TextareaRenderable, CliRenderer } from "@opentui/core";
|
|
10
10
|
import type { PersonaSummary } from "../../../src/core/types.js";
|
|
11
11
|
import { useEi } from "./ei";
|
|
12
12
|
import { logger } from "../util/logger";
|
|
13
|
+
import { copyToClipboard } from "../util/clipboard";
|
|
13
14
|
|
|
14
15
|
export type Panel = "sidebar" | "messages" | "input";
|
|
15
16
|
|
|
@@ -23,6 +24,7 @@ interface KeyboardContextValue {
|
|
|
23
24
|
toggleSidebar: () => void;
|
|
24
25
|
exitApp: () => Promise<void>;
|
|
25
26
|
renderer: CliRenderer;
|
|
27
|
+
resetHistoryIndex: () => void;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const KeyboardContext = createContext<KeyboardContextValue>();
|
|
@@ -31,11 +33,13 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
31
33
|
const [focusedPanel, setFocusedPanel] = createSignal<Panel>("input");
|
|
32
34
|
const [sidebarVisible, setSidebarVisible] = createSignal(true);
|
|
33
35
|
const renderer = useRenderer();
|
|
34
|
-
const { queueStatus, abortCurrentOperation, resumeQueue, personas, activePersonaId, selectPersona, saveAndExit, showNotification } = useEi();
|
|
36
|
+
const { queueStatus, abortCurrentOperation, resumeQueue, personas, activePersonaId, selectPersona, saveAndExit, showNotification, messages, recallPendingMessages } = useEi();
|
|
35
37
|
|
|
36
38
|
let messageScrollRef: ScrollBoxRenderable | null = null;
|
|
37
39
|
let textareaRef: TextareaRenderable | null = null;
|
|
38
40
|
let editorHandler: (() => Promise<void>) | null = null;
|
|
41
|
+
let historyIndex = -1; // -1 = not browsing history
|
|
42
|
+
let savedDraft = ""; // input text saved when history browsing starts
|
|
39
43
|
|
|
40
44
|
const registerMessageScroll = (scrollbox: ScrollBoxRenderable) => {
|
|
41
45
|
messageScrollRef = scrollbox;
|
|
@@ -123,6 +127,67 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
123
127
|
return;
|
|
124
128
|
}
|
|
125
129
|
|
|
130
|
+
|
|
131
|
+
if (event.name === "up" && !event.ctrl && !event.shift && !event.meta) {
|
|
132
|
+
if (!textareaRef) return;
|
|
133
|
+
const cursor = textareaRef.logicalCursor;
|
|
134
|
+
// Only intercept when cursor is at the very beginning (row 0, col 0)
|
|
135
|
+
if (cursor.row !== 0 || cursor.col !== 0) return;
|
|
136
|
+
// Don't intercept when slash-command suggest panel is visible
|
|
137
|
+
if (textareaRef.plainText.startsWith("/")) return;
|
|
138
|
+
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
// First Up from fresh state: check for pending (unread) messages to recall
|
|
141
|
+
if (historyIndex === -1) {
|
|
142
|
+
const hasPending = messages().some(m => m.role === "human" && !m.read);
|
|
143
|
+
if (hasPending) {
|
|
144
|
+
savedDraft = textareaRef.plainText;
|
|
145
|
+
void recallPendingMessages().then(recalled => {
|
|
146
|
+
if (recalled) {
|
|
147
|
+
textareaRef!.setText(recalled);
|
|
148
|
+
textareaRef!.gotoBufferHome();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Navigate backward through sent-message history
|
|
155
|
+
const history = messages().filter(m => m.role === "human").map(m => m.content);
|
|
156
|
+
if (history.length === 0) return;
|
|
157
|
+
if (historyIndex === -1) {
|
|
158
|
+
savedDraft = textareaRef.plainText;
|
|
159
|
+
}
|
|
160
|
+
historyIndex = Math.min(historyIndex + 1, history.length - 1);
|
|
161
|
+
// history is newest-last; index 0 = most recent
|
|
162
|
+
const entry = history[history.length - 1 - historyIndex];
|
|
163
|
+
textareaRef.setText(entry);
|
|
164
|
+
textareaRef.gotoBufferHome(); // cursor at start so next Up continues backward
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (event.name === "down" && !event.ctrl && !event.shift && !event.meta) {
|
|
169
|
+
if (!textareaRef || historyIndex === -1) return;
|
|
170
|
+
// Only intercept when cursor is at the very end
|
|
171
|
+
if (textareaRef.cursorOffset !== textareaRef.plainText.length) return;
|
|
172
|
+
// Don't intercept when slash-command suggest panel is visible
|
|
173
|
+
if (textareaRef.plainText.startsWith("/")) return;
|
|
174
|
+
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
if (historyIndex === 0) {
|
|
177
|
+
// Back to the draft
|
|
178
|
+
historyIndex = -1;
|
|
179
|
+
textareaRef.setText(savedDraft);
|
|
180
|
+
textareaRef.gotoBufferEnd();
|
|
181
|
+
} else {
|
|
182
|
+
historyIndex -= 1;
|
|
183
|
+
const history = messages().filter(m => m.role === "human").map(m => m.content);
|
|
184
|
+
const entry = history[history.length - 1 - historyIndex];
|
|
185
|
+
textareaRef.setText(entry);
|
|
186
|
+
textareaRef.gotoBufferEnd(); // cursor at end so next Down continues forward
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
126
191
|
if (!messageScrollRef) return;
|
|
127
192
|
|
|
128
193
|
const scrollAmount = messageScrollRef.height;
|
|
@@ -136,6 +201,28 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
136
201
|
}
|
|
137
202
|
});
|
|
138
203
|
|
|
204
|
+
|
|
205
|
+
useSelectionHandler((selection) => {
|
|
206
|
+
const text = selection.getSelectedText();
|
|
207
|
+
if (!text || text.length === 0) return;
|
|
208
|
+
logger.info(`Selection detected: ${text.length} chars, copying...`);
|
|
209
|
+
void copyToClipboard(text)
|
|
210
|
+
.then(() => {
|
|
211
|
+
showNotification(`Copied ${text.length} chars`, "info");
|
|
212
|
+
renderer.clearSelection();
|
|
213
|
+
logger.info(`Clipboard copy succeeded`);
|
|
214
|
+
})
|
|
215
|
+
.catch((err: unknown) => {
|
|
216
|
+
logger.error(`Clipboard copy failed: ${String(err)}`);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const resetHistoryIndex = () => {
|
|
221
|
+
historyIndex = -1;
|
|
222
|
+
savedDraft = "";
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
|
|
139
226
|
const value: KeyboardContextValue = {
|
|
140
227
|
focusedPanel,
|
|
141
228
|
setFocusedPanel,
|
|
@@ -146,6 +233,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
|
|
|
146
233
|
toggleSidebar,
|
|
147
234
|
exitApp,
|
|
148
235
|
renderer,
|
|
236
|
+
resetHistoryIndex,
|
|
149
237
|
};
|
|
150
238
|
|
|
151
239
|
return (
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { platform } from "os";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Write OSC 52 escape sequence to stdout.
|
|
5
|
+
* Works in terminals that support it (Kitty, Alacritty, etc.).
|
|
6
|
+
* Apple Terminal.app does NOT support OSC 52 — use copyNative() for macOS.
|
|
7
|
+
*/
|
|
8
|
+
function writeOsc52(text: string): void {
|
|
9
|
+
if (!process.stdout.isTTY) return;
|
|
10
|
+
const base64 = Buffer.from(text).toString("base64");
|
|
11
|
+
const osc52 = `\x1b]52;c;${base64}\x07`;
|
|
12
|
+
const inTmux = process.env["TMUX"] || process.env["STY"];
|
|
13
|
+
const sequence = inTmux ? `\x1bPtmux;\x1b${osc52}\x1b\\` : osc52;
|
|
14
|
+
process.stdout.write(sequence);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Copy text to clipboard using the best available native method.
|
|
19
|
+
* Mirrors OpenCode's clipboard.ts approach.
|
|
20
|
+
*/
|
|
21
|
+
export async function copyToClipboard(text: string): Promise<void> {
|
|
22
|
+
// Always attempt OSC 52 (works over SSH, in supported terminals)
|
|
23
|
+
writeOsc52(text);
|
|
24
|
+
|
|
25
|
+
const os = platform();
|
|
26
|
+
|
|
27
|
+
if (os === "darwin") {
|
|
28
|
+
// osascript is the reliable path on macOS (works in Apple Terminal + tmux)
|
|
29
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
30
|
+
const proc = Bun.spawn(
|
|
31
|
+
["osascript", "-e", `set the clipboard to "${escaped}"`],
|
|
32
|
+
{ stdout: "ignore", stderr: "ignore" },
|
|
33
|
+
);
|
|
34
|
+
await proc.exited.catch(() => {});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (os === "linux") {
|
|
39
|
+
if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
|
|
40
|
+
const proc = Bun.spawn(["wl-copy"], {
|
|
41
|
+
stdin: "pipe",
|
|
42
|
+
stdout: "ignore",
|
|
43
|
+
stderr: "ignore",
|
|
44
|
+
});
|
|
45
|
+
proc.stdin.write(text);
|
|
46
|
+
proc.stdin.end();
|
|
47
|
+
await proc.exited.catch(() => {});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (Bun.which("xclip")) {
|
|
51
|
+
const proc = Bun.spawn(["xclip", "-selection", "clipboard"], {
|
|
52
|
+
stdin: "pipe",
|
|
53
|
+
stdout: "ignore",
|
|
54
|
+
stderr: "ignore",
|
|
55
|
+
});
|
|
56
|
+
proc.stdin.write(text);
|
|
57
|
+
proc.stdin.end();
|
|
58
|
+
await proc.exited.catch(() => {});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (Bun.which("xsel")) {
|
|
62
|
+
const proc = Bun.spawn(["xsel", "--clipboard", "--input"], {
|
|
63
|
+
stdin: "pipe",
|
|
64
|
+
stdout: "ignore",
|
|
65
|
+
stderr: "ignore",
|
|
66
|
+
});
|
|
67
|
+
proc.stdin.write(text);
|
|
68
|
+
proc.stdin.end();
|
|
69
|
+
await proc.exited.catch(() => {});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -220,7 +220,7 @@ export function personaToYAML(persona: PersonaEntity, allGroups?: string[]): str
|
|
|
220
220
|
: persona.topics.map(({ name, perspective, approach, personal_stake, exposure_current, exposure_desired }) => ({
|
|
221
221
|
name, perspective, approach, personal_stake, exposure_current, exposure_desired
|
|
222
222
|
})),
|
|
223
|
-
heartbeat_delay_ms: persona.heartbeat_delay_ms,
|
|
223
|
+
heartbeat_delay_ms: persona.heartbeat_delay_ms || 'default',
|
|
224
224
|
context_window_hours: persona.context_window_hours,
|
|
225
225
|
is_paused: persona.is_paused || undefined,
|
|
226
226
|
pause_until: persona.pause_until,
|
|
@@ -322,7 +322,7 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity): P
|
|
|
322
322
|
groups_visible: groupsVisible,
|
|
323
323
|
traits,
|
|
324
324
|
topics,
|
|
325
|
-
heartbeat_delay_ms: data.heartbeat_delay_ms,
|
|
325
|
+
heartbeat_delay_ms: stripPlaceholder(data.heartbeat_delay_ms, 'default'),
|
|
326
326
|
context_window_hours: data.context_window_hours,
|
|
327
327
|
is_paused: data.is_paused ?? false,
|
|
328
328
|
pause_until: data.pause_until,
|
|
@@ -347,7 +347,7 @@ export function humanToYAML(human: HumanEntity): string {
|
|
|
347
347
|
|
|
348
348
|
return YAML.stringify(data, {
|
|
349
349
|
lineWidth: 0,
|
|
350
|
-
});
|
|
350
|
+
}).replace(/^(\s+validated:\s+\S+)$/mg, '$1 # none | ei | human');
|
|
351
351
|
}
|
|
352
352
|
|
|
353
353
|
export interface HumanYAMLResult {
|
|
@@ -640,6 +640,10 @@ export function newProviderFromYAML(yamlContent: string): ProviderAccount {
|
|
|
640
640
|
data.default_model = undefined;
|
|
641
641
|
}
|
|
642
642
|
|
|
643
|
+
if (data.token_limit !== undefined && data.token_limit !== null && (typeof data.token_limit !== "number" || isNaN(data.token_limit))) {
|
|
644
|
+
throw new Error(`token_limit must be a number (got: ${JSON.stringify(data.token_limit)}). Note: underscore separators (100_000) are not valid in YAML.`);
|
|
645
|
+
}
|
|
646
|
+
|
|
643
647
|
return {
|
|
644
648
|
id: crypto.randomUUID(),
|
|
645
649
|
name: data.name,
|
|
@@ -687,6 +691,10 @@ export function providerFromYAML(yamlContent: string, original: ProviderAccount)
|
|
|
687
691
|
throw new Error("Provider URL is required");
|
|
688
692
|
}
|
|
689
693
|
|
|
694
|
+
if (data.token_limit !== undefined && data.token_limit !== null && (typeof data.token_limit !== "number" || isNaN(data.token_limit))) {
|
|
695
|
+
throw new Error(`token_limit must be a number (got: ${JSON.stringify(data.token_limit)}). Note: underscore separators (100_000) are not valid in YAML.`);
|
|
696
|
+
}
|
|
697
|
+
|
|
690
698
|
return {
|
|
691
699
|
id: original.id,
|
|
692
700
|
name: data.name,
|
|
@@ -752,4 +760,4 @@ export function contextFromYAML(yamlContent: string): ContextYAMLResult {
|
|
|
752
760
|
}
|
|
753
761
|
|
|
754
762
|
return { messages, deletedMessageIds };
|
|
755
|
-
}
|
|
763
|
+
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import type { EiValidationPromptData, PromptOutput } from "./types.js";
|
|
2
|
-
import type { DataItemBase } from "../../core/types.js";
|
|
3
|
-
|
|
4
|
-
function formatDataItem(item: DataItemBase, label: string): string {
|
|
5
|
-
return `### ${label}
|
|
6
|
-
- **Name**: ${item.name}
|
|
7
|
-
- **Description**: ${item.description}
|
|
8
|
-
- **Sentiment**: ${item.sentiment}
|
|
9
|
-
- **Last Updated**: ${item.last_updated}
|
|
10
|
-
${item.learned_by ? `- **Learned By**: ${item.learned_by}` : ""}`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function buildEiValidationPrompt(data: EiValidationPromptData): PromptOutput {
|
|
14
|
-
if (!data.item_name || !data.data_type) {
|
|
15
|
-
throw new Error("buildEiValidationPrompt: item_name and data_type are required");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const roleFragment = `You are Ei, the system guide and arbiter of truth for the human's data.
|
|
19
|
-
|
|
20
|
-
When other personas learn things about the human, those changes come to you for validation. Your job is to ensure data quality and consistency.`;
|
|
21
|
-
|
|
22
|
-
const contextFragment = `# Validation Request
|
|
23
|
-
|
|
24
|
-
**Type**: ${data.data_type.toUpperCase()}
|
|
25
|
-
**Item**: "${data.item_name}"
|
|
26
|
-
**Source**: ${data.source_persona}
|
|
27
|
-
**Context**: ${data.context}`;
|
|
28
|
-
|
|
29
|
-
const dataFragment = data.current_item
|
|
30
|
-
? `# Data Comparison
|
|
31
|
-
|
|
32
|
-
${formatDataItem(data.current_item, "Current (existing data)")}
|
|
33
|
-
|
|
34
|
-
${formatDataItem(data.proposed_item, "Proposed (from " + data.source_persona + ")")}`
|
|
35
|
-
: `# New Data
|
|
36
|
-
|
|
37
|
-
${formatDataItem(data.proposed_item, "Proposed (from " + data.source_persona + ")")}
|
|
38
|
-
|
|
39
|
-
*(This is a NEW ${data.data_type} - no existing data to compare)*`;
|
|
40
|
-
|
|
41
|
-
const guidelinesFragment = `# Validation Guidelines
|
|
42
|
-
|
|
43
|
-
## ACCEPT if:
|
|
44
|
-
- Change is factual and well-evidenced
|
|
45
|
-
- New information is consistent with what you know about the human
|
|
46
|
-
- Source persona's interpretation seems reasonable
|
|
47
|
-
- Data improves understanding of the human
|
|
48
|
-
|
|
49
|
-
## MODIFY if:
|
|
50
|
-
- Partially correct but needs refinement
|
|
51
|
-
- Description could be clearer or more accurate
|
|
52
|
-
- Sentiment or other fields seem off
|
|
53
|
-
- Good information but poorly expressed
|
|
54
|
-
|
|
55
|
-
## REJECT if:
|
|
56
|
-
- Contradicts known facts
|
|
57
|
-
- Seems like a hallucination or misunderstanding
|
|
58
|
-
- Would misrepresent the human
|
|
59
|
-
- Source persona lacks context to make this claim
|
|
60
|
-
|
|
61
|
-
## Considerations
|
|
62
|
-
- ${data.source_persona} may have context you don't
|
|
63
|
-
- The human's data should be accurate, not just convenient
|
|
64
|
-
- When in doubt, lean toward accepting with modifications`;
|
|
65
|
-
|
|
66
|
-
const outputFragment = `# Response Format
|
|
67
|
-
|
|
68
|
-
\`\`\`json
|
|
69
|
-
{
|
|
70
|
-
"decision": "accept" | "modify" | "reject",
|
|
71
|
-
"reason": "Brief explanation of your decision",
|
|
72
|
-
"modified_item": { ... } // Only if decision is "modify"
|
|
73
|
-
}
|
|
74
|
-
\`\`\`
|
|
75
|
-
|
|
76
|
-
If modifying, include the corrected item with all fields.`;
|
|
77
|
-
|
|
78
|
-
const system = `${roleFragment}
|
|
79
|
-
|
|
80
|
-
${contextFragment}
|
|
81
|
-
|
|
82
|
-
${dataFragment}
|
|
83
|
-
|
|
84
|
-
${guidelinesFragment}
|
|
85
|
-
|
|
86
|
-
${outputFragment}`;
|
|
87
|
-
|
|
88
|
-
const user = `Review the ${data.data_type} "${data.item_name}" proposed by ${data.source_persona}.
|
|
89
|
-
|
|
90
|
-
Should this change be accepted, modified, or rejected?`;
|
|
91
|
-
|
|
92
|
-
return { system, user };
|
|
93
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { DataItemBase } from "../../core/types.js";
|
|
2
|
-
|
|
3
|
-
export interface PromptOutput {
|
|
4
|
-
system: string;
|
|
5
|
-
user: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface EiValidationPromptData {
|
|
9
|
-
validation_type: "cross_persona";
|
|
10
|
-
item_name: string;
|
|
11
|
-
data_type: "fact" | "trait" | "topic" | "person";
|
|
12
|
-
context: string;
|
|
13
|
-
source_persona: string;
|
|
14
|
-
current_item?: DataItemBase;
|
|
15
|
-
proposed_item: DataItemBase;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface EiValidationResult {
|
|
19
|
-
decision: "accept" | "modify" | "reject";
|
|
20
|
-
reason: string;
|
|
21
|
-
modified_item?: DataItemBase;
|
|
22
|
-
}
|