@agent-api/cli 0.4.29 → 0.4.31
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/dist/runtime.d.ts
CHANGED
package/dist/runtime.js
CHANGED
package/dist/tui/ink/app.js
CHANGED
|
@@ -7,6 +7,7 @@ import { buildWorkbenchRenderModel, copyTextFromActivitySelection, copyTextFromH
|
|
|
7
7
|
import { InkAuthGate, InkWorkbenchScreen } from "./components.js";
|
|
8
8
|
import { detectClipboardCapabilities, formatClipboardCapabilities, readClipboard, writeClipboard, } from "../clipboard.js";
|
|
9
9
|
import { disableMouseReporting, parseMouseEvent } from "../mouse.js";
|
|
10
|
+
import { createDefaultTranscriptStore } from "../transcript-store.js";
|
|
10
11
|
export function ChatApp({ options }) {
|
|
11
12
|
return _jsx(AuthenticatedChatApp, { options: options });
|
|
12
13
|
}
|
|
@@ -138,11 +139,21 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
|
|
|
138
139
|
const [terminalState, setTerminalState] = useState(() => initialWorkbenchTerminalState());
|
|
139
140
|
const [spinnerFrame, setSpinnerFrame] = useState(0);
|
|
140
141
|
const agentEngineRef = useRef(null);
|
|
142
|
+
const transcriptStoreRef = useRef(undefined);
|
|
143
|
+
if (transcriptStoreRef.current === undefined) {
|
|
144
|
+
try {
|
|
145
|
+
transcriptStoreRef.current = createDefaultTranscriptStore();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
transcriptStoreRef.current = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
141
151
|
if (!agentEngineRef.current) {
|
|
142
152
|
agentEngineRef.current = createAgentEngine({
|
|
143
153
|
authController,
|
|
144
154
|
baseOptions: options,
|
|
145
155
|
profileName,
|
|
156
|
+
services: transcriptStoreRef.current ? { transcriptStore: transcriptStoreRef.current } : undefined,
|
|
146
157
|
onDeleteProfile,
|
|
147
158
|
onExit: app.exit,
|
|
148
159
|
onLogin,
|
|
@@ -240,6 +251,21 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
|
|
|
240
251
|
if (selection)
|
|
241
252
|
return copyTextFromHeaderSelection(renderModel.header.lines, selection);
|
|
242
253
|
}
|
|
254
|
+
if (target === "conversation") {
|
|
255
|
+
const selection = selectedPanelRange(terminalState.conversationSelectionAnchor, terminalState.conversationCursor);
|
|
256
|
+
if (selection)
|
|
257
|
+
return copyTextFromHeaderSelection(renderModel.conversation.lines, selection);
|
|
258
|
+
}
|
|
259
|
+
if (target === "workspace") {
|
|
260
|
+
const selection = selectedPanelRange(terminalState.workspaceSelectionAnchor, terminalState.workspaceCursor);
|
|
261
|
+
if (selection)
|
|
262
|
+
return copyTextFromHeaderSelection(renderModel.workspace.lines, selection);
|
|
263
|
+
}
|
|
264
|
+
if (target === "workdir") {
|
|
265
|
+
const selection = selectedPanelRange(terminalState.workdirSelectionAnchor, terminalState.workdirCursor);
|
|
266
|
+
if (selection)
|
|
267
|
+
return copyTextFromHeaderSelection(renderModel.workdir.lines, selection);
|
|
268
|
+
}
|
|
243
269
|
if (target === "activity") {
|
|
244
270
|
const selection = selectedPanelRange(terminalState.activitySelectionAnchor, terminalState.activityCursor);
|
|
245
271
|
if (selection)
|
|
@@ -250,7 +276,10 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
|
|
|
250
276
|
useEffect(() => {
|
|
251
277
|
let mounted = true;
|
|
252
278
|
void agentEngine.maybeCheckForUpdate({ isMounted: () => mounted });
|
|
253
|
-
void
|
|
279
|
+
void (async () => {
|
|
280
|
+
await agentEngine.loadWorkspaceContext({ isMounted: () => mounted });
|
|
281
|
+
await agentEngine.loadInitialConversation({ isMounted: () => mounted });
|
|
282
|
+
})();
|
|
254
283
|
void agentEngine.loadInitialSettings({ isMounted: () => mounted });
|
|
255
284
|
return () => {
|
|
256
285
|
mounted = false;
|
|
@@ -309,6 +338,20 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
|
|
|
309
338
|
});
|
|
310
339
|
if (!sameTerminalState(result.state, terminalState))
|
|
311
340
|
setTerminalState(result.state);
|
|
341
|
+
if (shouldLoadOlderTranscript(normalizedKey, result.state, renderModel)) {
|
|
342
|
+
void agentEngine.loadOlderTranscript().then((count) => {
|
|
343
|
+
if (count > 0) {
|
|
344
|
+
setTerminalState((current) => ({ ...current, focusedPanel: "transcript", transcriptOffset: Number.MAX_SAFE_INTEGER }));
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (shouldLoadNewerTranscript(normalizedKey, result.state)) {
|
|
349
|
+
void agentEngine.loadNewerTranscript().then((count) => {
|
|
350
|
+
if (count > 0) {
|
|
351
|
+
setTerminalState((current) => ({ ...current, focusedPanel: "transcript", transcriptOffset: 0 }));
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
312
355
|
for (const effect of result.effects) {
|
|
313
356
|
switch (effect.type) {
|
|
314
357
|
case "exit":
|
|
@@ -345,12 +388,18 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
|
|
|
345
388
|
case "paste":
|
|
346
389
|
void pasteClipboardIntoInput();
|
|
347
390
|
break;
|
|
391
|
+
case "switch_conversation":
|
|
392
|
+
void submitInput(`/switch ${effect.name}`);
|
|
393
|
+
break;
|
|
394
|
+
case "switch_workspace":
|
|
395
|
+
void submitInput(`/workspace ${effect.id}`);
|
|
396
|
+
break;
|
|
348
397
|
}
|
|
349
398
|
}
|
|
350
399
|
});
|
|
351
400
|
useEffect(() => {
|
|
352
401
|
void agentEngine.startInitialPrompt();
|
|
353
|
-
}, [agentEngine, state.busy, state.contextEnabled, state.workdir]);
|
|
402
|
+
}, [agentEngine, state.busy, state.contextEnabled, state.currentWorkspaceId, state.workdir]);
|
|
354
403
|
useEffect(() => {
|
|
355
404
|
if (!state.busy) {
|
|
356
405
|
setSpinnerFrame(0);
|
|
@@ -364,7 +413,18 @@ function WorkbenchApp({ authController, onLogin, onLogout, onDeleteProfile, onSw
|
|
|
364
413
|
useEffect(() => {
|
|
365
414
|
return () => agentEngine.dispose();
|
|
366
415
|
}, [agentEngine]);
|
|
367
|
-
return (_jsx(InkWorkbenchScreen, { activityCursor: terminalState.activityCursor, activitySelection: selectedPanelRange(terminalState.activitySelectionAnchor, terminalState.activityCursor), focusedPanel: terminalState.focusedPanel, headerCursor: terminalState.headerCursor, headerSelection: selectedPanelRange(terminalState.headerSelectionAnchor, terminalState.headerCursor), renderModel: renderModel, spinnerFrame: spinnerFrame, transcriptCursor: terminalState.transcriptCursor, transcriptSelection: selectedPanelRange(terminalState.transcriptSelectionAnchor, terminalState.transcriptCursor) }));
|
|
416
|
+
return (_jsx(InkWorkbenchScreen, { activityCursor: terminalState.activityCursor, activitySelection: selectedPanelRange(terminalState.activitySelectionAnchor, terminalState.activityCursor), conversationCursor: terminalState.conversationCursor, conversationSelection: selectedPanelRange(terminalState.conversationSelectionAnchor, terminalState.conversationCursor), focusedPanel: terminalState.focusedPanel, headerCursor: terminalState.headerCursor, headerSelection: selectedPanelRange(terminalState.headerSelectionAnchor, terminalState.headerCursor), renderModel: renderModel, spinnerFrame: spinnerFrame, transcriptCursor: terminalState.transcriptCursor, transcriptSelection: selectedPanelRange(terminalState.transcriptSelectionAnchor, terminalState.transcriptCursor), workspaceCursor: terminalState.workspaceCursor, workspaceSelection: selectedPanelRange(terminalState.workspaceSelectionAnchor, terminalState.workspaceCursor), workdirCursor: terminalState.workdirCursor, workdirSelection: selectedPanelRange(terminalState.workdirSelectionAnchor, terminalState.workdirCursor) }));
|
|
417
|
+
}
|
|
418
|
+
function shouldLoadOlderTranscript(key, state, renderModel) {
|
|
419
|
+
return Boolean(key.pageUp &&
|
|
420
|
+
state.focusedPanel === "transcript" &&
|
|
421
|
+
renderModel.transcript.maxOffset > 0 &&
|
|
422
|
+
state.transcriptOffset >= renderModel.transcript.maxOffset);
|
|
423
|
+
}
|
|
424
|
+
function shouldLoadNewerTranscript(key, state) {
|
|
425
|
+
return Boolean(key.pageDown &&
|
|
426
|
+
state.focusedPanel === "transcript" &&
|
|
427
|
+
state.transcriptOffset <= 0);
|
|
368
428
|
}
|
|
369
429
|
function useTerminalSize(stdout) {
|
|
370
430
|
const [size, setSize] = useState(() => ({
|
|
@@ -425,6 +485,9 @@ function sameTerminalState(a, b) {
|
|
|
425
485
|
return a.activityCursor.line === b.activityCursor.line
|
|
426
486
|
&& a.activityCursor.column === b.activityCursor.column
|
|
427
487
|
&& samePositionOrNull(a.activitySelectionAnchor, b.activitySelectionAnchor)
|
|
488
|
+
&& a.conversationCursor.line === b.conversationCursor.line
|
|
489
|
+
&& a.conversationCursor.column === b.conversationCursor.column
|
|
490
|
+
&& samePositionOrNull(a.conversationSelectionAnchor, b.conversationSelectionAnchor)
|
|
428
491
|
&& a.cursor === b.cursor
|
|
429
492
|
&& a.draft === b.draft
|
|
430
493
|
&& a.focusedPanel === b.focusedPanel
|
|
@@ -436,7 +499,13 @@ function sameTerminalState(a, b) {
|
|
|
436
499
|
&& a.transcriptCursor.line === b.transcriptCursor.line
|
|
437
500
|
&& a.transcriptCursor.column === b.transcriptCursor.column
|
|
438
501
|
&& a.transcriptOffset === b.transcriptOffset
|
|
439
|
-
&& samePositionOrNull(a.transcriptSelectionAnchor, b.transcriptSelectionAnchor)
|
|
502
|
+
&& samePositionOrNull(a.transcriptSelectionAnchor, b.transcriptSelectionAnchor)
|
|
503
|
+
&& a.workspaceCursor.line === b.workspaceCursor.line
|
|
504
|
+
&& a.workspaceCursor.column === b.workspaceCursor.column
|
|
505
|
+
&& samePositionOrNull(a.workspaceSelectionAnchor, b.workspaceSelectionAnchor)
|
|
506
|
+
&& a.workdirCursor.line === b.workdirCursor.line
|
|
507
|
+
&& a.workdirCursor.column === b.workdirCursor.column
|
|
508
|
+
&& samePositionOrNull(a.workdirSelectionAnchor, b.workdirSelectionAnchor);
|
|
440
509
|
}
|
|
441
510
|
function samePositionOrNull(a, b) {
|
|
442
511
|
if (a === null || b === null)
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { type WorkbenchPanelPosition, type WorkbenchPanelSelection, type WorkbenchRenderModel } from "@agent-api/app-engine/terminal";
|
|
3
3
|
import { type AuthGateState } from "@agent-api/app-engine/workbench";
|
|
4
|
-
export declare function InkWorkbenchScreen({ activityCursor, activitySelection, focusedPanel, headerCursor, headerSelection, renderModel, spinnerFrame, transcriptCursor, transcriptSelection, }: {
|
|
4
|
+
export declare function InkWorkbenchScreen({ activityCursor, activitySelection, conversationCursor, conversationSelection, focusedPanel, headerCursor, headerSelection, renderModel, spinnerFrame, transcriptCursor, transcriptSelection, workspaceCursor, workspaceSelection, workdirCursor, workdirSelection, }: {
|
|
5
5
|
activityCursor: WorkbenchPanelPosition;
|
|
6
6
|
activitySelection: WorkbenchPanelSelection | null;
|
|
7
|
-
|
|
7
|
+
conversationCursor: WorkbenchPanelPosition;
|
|
8
|
+
conversationSelection: WorkbenchPanelSelection | null;
|
|
9
|
+
focusedPanel: "activity" | "conversation" | "header" | "input" | "transcript" | "workspace" | "workdir";
|
|
8
10
|
headerCursor: WorkbenchPanelPosition;
|
|
9
11
|
headerSelection: WorkbenchPanelSelection | null;
|
|
10
12
|
renderModel: WorkbenchRenderModel;
|
|
11
13
|
spinnerFrame: number;
|
|
12
14
|
transcriptCursor: WorkbenchPanelPosition;
|
|
13
15
|
transcriptSelection: WorkbenchPanelSelection | null;
|
|
16
|
+
workspaceCursor: WorkbenchPanelPosition;
|
|
17
|
+
workspaceSelection: WorkbenchPanelSelection | null;
|
|
18
|
+
workdirCursor: WorkbenchPanelPosition;
|
|
19
|
+
workdirSelection: WorkbenchPanelSelection | null;
|
|
14
20
|
}): React.JSX.Element;
|
|
15
21
|
export declare function InkAuthGate({ cursorVisible, state }: {
|
|
16
22
|
cursorVisible: boolean;
|
|
@@ -2,15 +2,16 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { activityColor, busySpinner, } from "@agent-api/app-engine/terminal";
|
|
4
4
|
import { authMethods, } from "@agent-api/app-engine/workbench";
|
|
5
|
-
export function InkWorkbenchScreen({ activityCursor, activitySelection, focusedPanel, headerCursor, headerSelection, renderModel, spinnerFrame, transcriptCursor, transcriptSelection, }) {
|
|
5
|
+
export function InkWorkbenchScreen({ activityCursor, activitySelection, conversationCursor, conversationSelection, focusedPanel, headerCursor, headerSelection, renderModel, spinnerFrame, transcriptCursor, transcriptSelection, workspaceCursor, workspaceSelection, workdirCursor, workdirSelection, }) {
|
|
6
6
|
const activity = (_jsxs(Box, { borderColor: panelBorderColor(focusedPanel === "activity"), borderStyle: "round", flexShrink: 0, flexDirection: "column", height: renderModel.activityHeight, marginLeft: renderModel.layout === "wide" ? 1 : 0, paddingX: 1, width: renderModel.layout === "wide" ? "27%" : "100%", children: [_jsx(Text, { bold: true, color: focusedPanel === "activity" ? "cyan" : undefined, wrap: "truncate", children: "Activity" }), renderModel.visibleActivities.map((activity, index) => {
|
|
7
7
|
const cursor = focusedPanel === "activity" && index === activityCursor.line;
|
|
8
8
|
const text = `${new Date(activity.timestamp).toLocaleTimeString()} ${activity.text}`;
|
|
9
9
|
return (_jsxs(Text, { color: activityColor(activity.level), wrap: "truncate", children: [cursor ? _jsx(Text, { color: "cyan", children: "\u203A " }) : _jsx(Text, { children: " " }), _jsx(SelectableText, { cursorColumn: cursor && !activitySelection ? activityCursor.column : null, selection: lineSelection(index, activitySelection), text: text || " " })] }, activity.id));
|
|
10
10
|
})] }));
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const sidePanels = (_jsxs(Box, { flexDirection: "column", flexShrink: 0, marginRight: renderModel.layout === "wide" ? 1 : 0, width: renderModel.layout === "wide" ? renderModel.workdirPanelWidth : "100%", children: [_jsx(InfoPanel, { cursor: conversationCursor, focused: focusedPanel === "conversation", height: renderModel.conversationHeight, lines: renderModel.conversation.lines, selection: conversationSelection, title: "Conversation" }), _jsx(Box, { marginTop: renderModel.layout === "wide" ? 1 : 0, children: _jsx(InfoPanel, { cursor: workdirCursor, focused: focusedPanel === "workdir", height: renderModel.workdirHeight, lines: renderModel.workdir.lines, selection: workdirSelection, title: "Workdir" }) }), _jsx(Box, { marginTop: renderModel.layout === "wide" ? 1 : 0, children: _jsx(InfoPanel, { cursor: workspaceCursor, focused: focusedPanel === "workspace", height: renderModel.workspaceHeight, lines: renderModel.workspace.lines, selection: workspaceSelection, title: "Workspace" }) })] }));
|
|
12
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { focused: focusedPanel === "header", cursor: headerCursor, selection: headerSelection, contextEnabled: renderModel.header.contextEnabled, conversation: renderModel.header.conversation, conversationId: renderModel.header.conversationId, conversationPreviousResponseId: renderModel.header.conversationPreviousResponseId, conversationStatus: renderModel.header.conversationStatus, lines: renderModel.header.lines, model: renderModel.header.model, accessMode: renderModel.header.accessMode, pendingLocalLabel: renderModel.header.pendingLocalLabel, preset: renderModel.header.preset, profile: renderModel.header.profile, renderMode: renderModel.header.renderMode, workdir: renderModel.header.workdir }), _jsxs(Box, { height: renderModel.viewportHeight, flexDirection: renderModel.layout === "wide" ? "row" : "column", children: [renderModel.layout === "wide" && sidePanels, _jsxs(Box, { flexDirection: renderModel.layout === "wide" ? "row" : "column", flexGrow: 1, children: [_jsxs(Box, { borderStyle: "round", borderColor: panelBorderColor(focusedPanel === "transcript"), flexGrow: renderModel.layout === "wide" ? 1 : 0, flexDirection: "column", height: renderModel.transcript.viewportHeight + 2, paddingX: 1, width: renderModel.layout === "wide" ? undefined : "100%", children: [_jsxs(Text, { bold: true, color: renderModel.transcriptStatus.color, wrap: "truncate", children: ["Transcript \u00B7 ", renderModel.transcriptStatus.label] }), renderModel.transcript.visibleLines.map((line, index) => (_jsx(TranscriptText, { cursorColumn: focusedPanel === "transcript" && renderModel.transcript.startLine + index - 1 === transcriptCursor.line && !transcriptSelection
|
|
13
|
+
? transcriptCursor.column
|
|
14
|
+
: null, line: line, lineSelection: lineSelection(renderModel.transcript.startLine + index - 1, transcriptSelection), lineCursor: focusedPanel === "transcript" && renderModel.transcript.startLine + index - 1 === transcriptCursor.line }, line.id))), renderModel.transcript.visibleLines.length === 0 && _jsx(Text, { color: "gray", children: "No transcript lines." })] }), activity] })] }), _jsxs(Box, { borderStyle: "round", borderColor: panelBorderColor(focusedPanel === "input"), paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [renderModel.input.fullAccess && (_jsx(Text, { color: "red", bold: true, inverse: true, children: "FULL ACCESS" })), renderModel.input.fullAccess && _jsx(Text, { children: " " }), _jsx(Text, { color: renderModel.input.busy ? "yellow" : "green", children: renderModel.input.label }), renderModel.input.statusText && (_jsxs(Text, { color: "yellow", children: [" ", busySpinner(spinnerFrame), " ", renderModel.input.statusText] }))] }), _jsx(Box, { flexDirection: "column", children: renderModel.input.lines.map((line, index) => (_jsx(Text, { wrap: "truncate", children: line.spans.map((span, spanIndex) => (_jsx(Text, { inverse: span.inverse, children: span.text }, spanIndex))) }, index))) })] }), _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "gray", wrap: "truncate", children: renderModel.footerText }) })] }));
|
|
14
15
|
}
|
|
15
16
|
function TranscriptText({ cursorColumn, line, lineCursor, lineSelection, }) {
|
|
16
17
|
const anchor = lineCursor ? _jsx(Text, { color: "cyan", children: "\u203A " }) : line.anchor ? _jsx(Text, { color: "cyan", children: "\u25B8 " }) : _jsx(Text, { children: " " });
|
|
@@ -99,6 +100,9 @@ function Header({ cursor, focused, selection, contextEnabled, conversation, conv
|
|
|
99
100
|
];
|
|
100
101
|
return (_jsx(Box, { borderStyle: "round", borderColor: panelBorderColor(focused), paddingX: 1, flexDirection: "column", children: renderedLines.map((line, index) => (_jsxs(Text, { bold: line.bold || (focused && index === cursor.line), color: focused && index === 0 ? "cyan" : line.color, wrap: "truncate", children: [focused && index === cursor.line ? _jsx(Text, { color: "cyan", children: "\u203A " }) : _jsx(Text, { children: " " }), _jsx(SelectableText, { bold: line.bold, color: line.color, cursorColumn: focused && index === cursor.line && !selection ? cursor.column : null, selection: lineSelection(index, selection), text: line.text })] }, index))) }));
|
|
101
102
|
}
|
|
103
|
+
function InfoPanel({ cursor, focused, height, lines, selection, title, }) {
|
|
104
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: panelBorderColor(focused), flexDirection: "column", height: height, paddingX: 1, children: [_jsx(Text, { bold: true, color: focused ? "cyan" : undefined, wrap: "truncate", children: title }), lines.slice(0, Math.max(0, height - 2)).map((line, index) => (_jsxs(Text, { bold: focused && index === cursor.line, color: "gray", wrap: "truncate", children: [focused && index === cursor.line ? _jsx(Text, { color: "cyan", children: "\u203A " }) : _jsx(Text, { children: " " }), _jsx(SelectableText, { cursorColumn: focused && index === cursor.line && !selection ? cursor.column : null, selection: lineSelection(index, selection), text: line || " " })] }, `${title}:${index}`)))] }));
|
|
105
|
+
}
|
|
102
106
|
function panelBorderColor(focused) {
|
|
103
107
|
return focused ? "cyan" : "gray";
|
|
104
108
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { currentAgentAppRuntime, } from "@agent-api/app-engine/core";
|
|
5
|
+
import { createFileTranscriptStore, formatTranscript, summarizeMessages, } from "@agent-api/app-engine/workbench";
|
|
6
|
+
export function createDefaultTranscriptStore() {
|
|
7
|
+
const dataDir = currentAgentAppRuntime().runtime.dirs.data;
|
|
8
|
+
mkdirSync(dataDir, { recursive: true });
|
|
9
|
+
try {
|
|
10
|
+
return createSQLiteTranscriptStore(path.join(dataDir, "transcripts.sqlite3"));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return createFileTranscriptStore(path.join(dataDir, "transcripts"));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function createSQLiteTranscriptStore(file) {
|
|
17
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
18
|
+
const db = new Database(file);
|
|
19
|
+
db.pragma("journal_mode = WAL");
|
|
20
|
+
db.pragma("foreign_keys = ON");
|
|
21
|
+
db.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS transcript_messages (
|
|
23
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
conversation_id TEXT NOT NULL,
|
|
25
|
+
message_id TEXT NOT NULL,
|
|
26
|
+
role TEXT NOT NULL,
|
|
27
|
+
kind TEXT,
|
|
28
|
+
text TEXT NOT NULL,
|
|
29
|
+
created_at INTEGER NOT NULL,
|
|
30
|
+
updated_at INTEGER NOT NULL,
|
|
31
|
+
UNIQUE(conversation_id, message_id)
|
|
32
|
+
);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_transcript_messages_conversation_seq
|
|
34
|
+
ON transcript_messages(conversation_id, seq);
|
|
35
|
+
`);
|
|
36
|
+
const insertMessage = db.prepare(`
|
|
37
|
+
INSERT INTO transcript_messages (
|
|
38
|
+
conversation_id, message_id, role, kind, text, created_at, updated_at
|
|
39
|
+
) VALUES (
|
|
40
|
+
@conversationId, @messageId, @role, @kind, @text, @now, @now
|
|
41
|
+
)
|
|
42
|
+
ON CONFLICT(conversation_id, message_id) DO UPDATE SET
|
|
43
|
+
role = excluded.role,
|
|
44
|
+
kind = excluded.kind,
|
|
45
|
+
text = excluded.text,
|
|
46
|
+
updated_at = excluded.updated_at
|
|
47
|
+
`);
|
|
48
|
+
const appendDelta = db.prepare(`
|
|
49
|
+
UPDATE transcript_messages
|
|
50
|
+
SET text = text || @delta, updated_at = @now
|
|
51
|
+
WHERE conversation_id = @conversationId AND message_id = @messageId
|
|
52
|
+
`);
|
|
53
|
+
const recentMessages = db.prepare(`
|
|
54
|
+
SELECT seq, message_id, role, kind, text
|
|
55
|
+
FROM transcript_messages
|
|
56
|
+
WHERE conversation_id = ?
|
|
57
|
+
ORDER BY seq DESC
|
|
58
|
+
LIMIT ?
|
|
59
|
+
`);
|
|
60
|
+
const beforeMessages = db.prepare(`
|
|
61
|
+
SELECT seq, message_id, role, kind, text
|
|
62
|
+
FROM transcript_messages
|
|
63
|
+
WHERE conversation_id = ? AND seq < ?
|
|
64
|
+
ORDER BY seq DESC
|
|
65
|
+
LIMIT ?
|
|
66
|
+
`);
|
|
67
|
+
const afterMessages = db.prepare(`
|
|
68
|
+
SELECT seq, message_id, role, kind, text
|
|
69
|
+
FROM transcript_messages
|
|
70
|
+
WHERE conversation_id = ? AND seq > ?
|
|
71
|
+
ORDER BY seq ASC
|
|
72
|
+
LIMIT ?
|
|
73
|
+
`);
|
|
74
|
+
const allMessages = db.prepare(`
|
|
75
|
+
SELECT seq, message_id, role, kind, text
|
|
76
|
+
FROM transcript_messages
|
|
77
|
+
WHERE conversation_id = ?
|
|
78
|
+
ORDER BY seq ASC
|
|
79
|
+
`);
|
|
80
|
+
const conversationUpdatedAt = db.prepare(`
|
|
81
|
+
SELECT COUNT(*) AS count, MAX(updated_at) AS updated_at
|
|
82
|
+
FROM transcript_messages
|
|
83
|
+
WHERE conversation_id = ?
|
|
84
|
+
`);
|
|
85
|
+
const deleteConversation = db.prepare("DELETE FROM transcript_messages WHERE conversation_id = ?");
|
|
86
|
+
return {
|
|
87
|
+
async appendMessage(conversationId, message) {
|
|
88
|
+
insertMessage.run({
|
|
89
|
+
conversationId,
|
|
90
|
+
messageId: message.id,
|
|
91
|
+
role: message.role,
|
|
92
|
+
kind: message.kind ?? null,
|
|
93
|
+
text: message.text,
|
|
94
|
+
now: nowSeconds(),
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
async appendMessageDelta(conversationId, messageId, delta) {
|
|
98
|
+
appendDelta.run({
|
|
99
|
+
conversationId,
|
|
100
|
+
messageId,
|
|
101
|
+
delta,
|
|
102
|
+
now: nowSeconds(),
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
async clearConversation(conversationId) {
|
|
106
|
+
deleteConversation.run(conversationId);
|
|
107
|
+
},
|
|
108
|
+
async exportConversation(conversationId) {
|
|
109
|
+
return formatTranscript(rowsToMessages(allMessages.all(conversationId)));
|
|
110
|
+
},
|
|
111
|
+
async getConversationSummary(conversationId) {
|
|
112
|
+
const stats = conversationUpdatedAt.get(conversationId);
|
|
113
|
+
return summarizeMessages(rowsToMessages(allMessages.all(conversationId)), {
|
|
114
|
+
updatedAt: typeof stats?.updated_at === "number" ? stats.updated_at : undefined,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
async loadAfterMessages(conversationId, afterSeq, limit) {
|
|
118
|
+
return rowsToMessages(afterMessages.all(conversationId, afterSeq, Math.max(0, limit)));
|
|
119
|
+
},
|
|
120
|
+
async loadBeforeMessages(conversationId, beforeSeq, limit) {
|
|
121
|
+
return rowsToMessages(beforeMessages.all(conversationId, beforeSeq, Math.max(0, limit))).reverse();
|
|
122
|
+
},
|
|
123
|
+
async loadRecentMessages(conversationId, limit) {
|
|
124
|
+
return rowsToMessages(recentMessages.all(conversationId, Math.max(0, limit))).reverse();
|
|
125
|
+
},
|
|
126
|
+
dispose() {
|
|
127
|
+
db.close();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function rowsToMessages(rows) {
|
|
132
|
+
return rows.map((row) => {
|
|
133
|
+
const record = row;
|
|
134
|
+
const message = {
|
|
135
|
+
id: String(record.message_id ?? ""),
|
|
136
|
+
role: roleValue(record.role),
|
|
137
|
+
text: String(record.text ?? ""),
|
|
138
|
+
};
|
|
139
|
+
if (typeof record.seq === "number")
|
|
140
|
+
message.transcriptSeq = record.seq;
|
|
141
|
+
if (record.kind === "tool")
|
|
142
|
+
message.kind = "tool";
|
|
143
|
+
return message;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function roleValue(value) {
|
|
147
|
+
return value === "user" || value === "assistant" || value === "system" ? value : "system";
|
|
148
|
+
}
|
|
149
|
+
function nowSeconds() {
|
|
150
|
+
return Math.floor(Date.now() / 1000);
|
|
151
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-api/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.31",
|
|
4
4
|
"description": "First-class command line interface for Agent API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/scalebox-dev/agent-tui#readme",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
26
26
|
"build:app-engine": "npm run build -w @agent-api/app-engine",
|
|
27
27
|
"build": "npm run build:app-engine && npm run clean && tsc -p tsconfig.json && node scripts/prepare-bin.mjs",
|
|
28
|
+
"dev-build": "npm run dev:link",
|
|
28
29
|
"dev": "npm run build && node dist/index.js",
|
|
29
30
|
"dev:link": "node scripts/dev-link.mjs",
|
|
30
31
|
"start": "node dist/index.js",
|
|
@@ -35,12 +36,14 @@
|
|
|
35
36
|
"test": "npm run sync-version && npm run build && npm run smoke -w @agent-api/app-engine && node --test test/*.test.mjs"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@agent-api/app-engine": "^0.1.
|
|
39
|
+
"@agent-api/app-engine": "^0.1.28",
|
|
40
|
+
"better-sqlite3": "^12.11.1",
|
|
39
41
|
"commander": "^14.0.3",
|
|
40
42
|
"ink": "^6.8.0",
|
|
41
43
|
"react": "^19.2.7"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
46
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
44
47
|
"@types/node": "^24.0.0",
|
|
45
48
|
"@types/react": "^19.2.17",
|
|
46
49
|
"typescript": "^5.0.0"
|