@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
@@ -1,4 +1,4 @@
1
1
  export declare const cliName = "agent-tui";
2
2
  export declare const cliAuthor = "AgentsWay";
3
- export declare const cliVersion = "0.4.29";
3
+ export declare const cliVersion = "0.4.31";
4
4
  export declare const legacyCliName = "agent-api-cli";
package/dist/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export const cliName = "agent-tui";
2
2
  export const cliAuthor = "AgentsWay";
3
- export const cliVersion = "0.4.29";
3
+ export const cliVersion = "0.4.31";
4
4
  export const legacyCliName = "agent-api-cli";
@@ -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 agentEngine.loadInitialConversation({ isMounted: () => mounted });
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
- focusedPanel: "activity" | "header" | "input" | "transcript";
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
- 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: [_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: [renderModel.transcript.visibleLines.map((line, index) => (_jsx(TranscriptText, { cursorColumn: focusedPanel === "transcript" && renderModel.transcript.startLine + index - 1 === transcriptCursor.line && !transcriptSelection
12
- ? transcriptCursor.column
13
- : 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 }) })] }));
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,3 @@
1
+ import { type WorkbenchTranscriptStore } from "@agent-api/app-engine/workbench";
2
+ export declare function createDefaultTranscriptStore(): WorkbenchTranscriptStore;
3
+ export declare function createSQLiteTranscriptStore(file: string): WorkbenchTranscriptStore;
@@ -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.29",
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.26",
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"