code-ollama 0.13.1 → 0.14.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/dist/assets/{tui-CCa_vqh0.js → tui-TX7C0xYg.js} +213 -55
- package/dist/cli.js +222 -21
- package/package.json +11 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as LABEL, C as withSystemMessage, D as USER, E as SYSTEM, F as VERSION, I as LIST, M as SAFE, N as APPROVE, O as PLAN_GENERATION_INSTRUCTION, P as REJECT, S as resetSystemMessage, T as ASSISTANT, _ as setClearHandler, a as tick, b as loadConfig, c as appendMessage, d as deleteSessionIfEmpty, f as listSessions, g as reset, h as clear, i as WRITE_TOOLS, j as PLAN, k as AUTO, l as createSession$1, m as updateSessionModel, n as READ_TOOLS, o as color, p as loadSession, r as TOOLS, s as write, t as executeTool, u as deleteSession, v as listModels, w as HEADER_PREFIX, x as saveConfig, y as streamChat } from "../cli.js";
|
|
2
2
|
import { readdirSync } from "node:fs";
|
|
3
|
-
import { join, relative } from "node:path";
|
|
4
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { join, relative } from "node:path";
|
|
5
5
|
import { exec } from "node:child_process";
|
|
6
6
|
import { Box, Static, Text, render, useApp, useInput, useStdout } from "ink";
|
|
7
7
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -9,43 +9,6 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
import { Select, Spinner } from "@inkjs/ui";
|
|
10
10
|
import { marked } from "marked";
|
|
11
11
|
import { markedTerminal } from "marked-terminal";
|
|
12
|
-
//#region src/constants/command.ts
|
|
13
|
-
var LIST = [
|
|
14
|
-
{
|
|
15
|
-
name: "/clear",
|
|
16
|
-
description: "clear the current session"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
name: "/model",
|
|
20
|
-
description: "switch the model"
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
name: "/search",
|
|
24
|
-
description: "configure web search"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: "/exit",
|
|
28
|
-
description: "exit the application"
|
|
29
|
-
}
|
|
30
|
-
];
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/constants/decision.ts
|
|
33
|
-
var APPROVE = "approve";
|
|
34
|
-
var REJECT = "reject";
|
|
35
|
-
//#endregion
|
|
36
|
-
//#region src/constants/mode.ts
|
|
37
|
-
var SAFE = "safe";
|
|
38
|
-
var AUTO = "auto";
|
|
39
|
-
var PLAN = "plan";
|
|
40
|
-
var LABEL = {
|
|
41
|
-
safe: "Safe",
|
|
42
|
-
auto: "Auto",
|
|
43
|
-
plan: "Plan"
|
|
44
|
-
};
|
|
45
|
-
//#endregion
|
|
46
|
-
//#region src/constants/ui.ts
|
|
47
|
-
var HEADER_PREFIX = "🦙 ";
|
|
48
|
-
//#endregion
|
|
49
12
|
//#region src/components/CodeBlock/CodeBlock.tsx
|
|
50
13
|
var highlightCache = /* @__PURE__ */ new Map();
|
|
51
14
|
var CODE_BLOCK_REGEX = /^(`{3,})(\w+)?[ \t]*\n([\s\S]*?)^\1[ \t]*$/gm;
|
|
@@ -794,8 +757,7 @@ function Input({ isDisabled = false, onInterrupt, onSubmit }) {
|
|
|
794
757
|
}, [onSubmit, resetInput]);
|
|
795
758
|
const showCommandMenu = input.startsWith("/");
|
|
796
759
|
const showFileSuggestions = !showCommandMenu && hasFileSuggestionQuery(input);
|
|
797
|
-
const handleSubmitText = useCallback(
|
|
798
|
-
await tick();
|
|
760
|
+
const handleSubmitText = useCallback((input) => {
|
|
799
761
|
if (input.startsWith("/")) return;
|
|
800
762
|
if (hasFileSuggestionQuery(input)) {
|
|
801
763
|
if (fileSuggestionRef.current) handleSelectFileSuggestion(fileSuggestionRef.current);
|
|
@@ -854,22 +816,31 @@ function hasExecutablePlan(content) {
|
|
|
854
816
|
}
|
|
855
817
|
//#endregion
|
|
856
818
|
//#region src/components/Chat/Chat.tsx
|
|
857
|
-
function Chat({ model, onCommand, mode, onModeChange, sessionId }) {
|
|
858
|
-
const
|
|
819
|
+
function Chat({ initialMessages, model, onCommand, onMessagesChange, mode, onModeChange, sessionId }) {
|
|
820
|
+
const sessionMessages = initialMessages ?? [];
|
|
821
|
+
const [messages, setMessages] = useState(sessionMessages);
|
|
859
822
|
const [streamingMessage, setStreamingMessage] = useState(null);
|
|
860
823
|
const [isLoading, setIsLoading] = useState(false);
|
|
861
824
|
const [pendingToolCall, setPendingToolCall] = useState(null);
|
|
862
825
|
const [pendingPlan, setPendingPlan] = useState(null);
|
|
863
826
|
const [interruptReason, setInterruptReason] = useState(null);
|
|
864
827
|
const abortControllerRef = useRef(null);
|
|
828
|
+
const persistedSnapshotRef = useRef("");
|
|
865
829
|
useEffect(() => {
|
|
866
|
-
setMessages(
|
|
830
|
+
setMessages(sessionMessages);
|
|
867
831
|
setStreamingMessage(null);
|
|
868
832
|
setIsLoading(false);
|
|
869
833
|
setPendingToolCall(null);
|
|
870
834
|
setPendingPlan(null);
|
|
871
835
|
setInterruptReason(null);
|
|
836
|
+
persistedSnapshotRef.current = JSON.stringify(sessionMessages);
|
|
872
837
|
}, [sessionId]);
|
|
838
|
+
useEffect(() => {
|
|
839
|
+
const snapshot = JSON.stringify(messages);
|
|
840
|
+
if (snapshot === persistedSnapshotRef.current) return;
|
|
841
|
+
persistedSnapshotRef.current = snapshot;
|
|
842
|
+
onMessagesChange?.(messages);
|
|
843
|
+
}, [messages, onMessagesChange]);
|
|
873
844
|
const buildToolResultMessage = useCallback((toolName, result) => {
|
|
874
845
|
if (result.error?.startsWith("Tool not allowed:")) return {
|
|
875
846
|
role: SYSTEM,
|
|
@@ -1452,48 +1423,224 @@ function SearchSettings({ currentUrl, onClose, onSave }) {
|
|
|
1452
1423
|
});
|
|
1453
1424
|
}
|
|
1454
1425
|
//#endregion
|
|
1426
|
+
//#region src/components/SessionManager.tsx
|
|
1427
|
+
var VIEW = /* @__PURE__ */ function(VIEW) {
|
|
1428
|
+
VIEW["MAIN"] = "main";
|
|
1429
|
+
VIEW["DELETE"] = "delete";
|
|
1430
|
+
return VIEW;
|
|
1431
|
+
}(VIEW || {});
|
|
1432
|
+
var ACTION = {
|
|
1433
|
+
BACK: "back",
|
|
1434
|
+
CLOSE: "close",
|
|
1435
|
+
DELETE_MENU: "delete-menu",
|
|
1436
|
+
DELETE_PREFIX: "delete:",
|
|
1437
|
+
NEW: "new",
|
|
1438
|
+
OPEN_PREFIX: "open:"
|
|
1439
|
+
};
|
|
1440
|
+
function formatSessionLabel(session) {
|
|
1441
|
+
const timestamp = new Date(session.updatedAt).toLocaleString();
|
|
1442
|
+
return `${session.title} (${timestamp})`;
|
|
1443
|
+
}
|
|
1444
|
+
function SessionManager({ currentSessionId, onClose, onDelete, onNew, onOpen }) {
|
|
1445
|
+
const [view, setView] = useState(VIEW.MAIN);
|
|
1446
|
+
const [error, setError] = useState();
|
|
1447
|
+
const [, refreshSessionList] = useState(0);
|
|
1448
|
+
const sessions = listSessions();
|
|
1449
|
+
const options = view === VIEW.DELETE ? [...sessions.filter(({ id }) => id !== currentSessionId).map((session) => ({
|
|
1450
|
+
label: `Delete ${formatSessionLabel(session)}`,
|
|
1451
|
+
value: `${ACTION.DELETE_PREFIX}${session.id}`
|
|
1452
|
+
})), {
|
|
1453
|
+
label: "Back",
|
|
1454
|
+
value: ACTION.BACK
|
|
1455
|
+
}] : [
|
|
1456
|
+
{
|
|
1457
|
+
label: "Start new session",
|
|
1458
|
+
value: ACTION.NEW
|
|
1459
|
+
},
|
|
1460
|
+
...sessions.map((session) => ({
|
|
1461
|
+
label: `${session.id === currentSessionId ? "Current: " : ""}${formatSessionLabel(session)}`,
|
|
1462
|
+
value: `${ACTION.OPEN_PREFIX}${session.id}`
|
|
1463
|
+
})),
|
|
1464
|
+
{
|
|
1465
|
+
label: "Delete a session",
|
|
1466
|
+
value: ACTION.DELETE_MENU
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
label: "Close",
|
|
1470
|
+
value: ACTION.CLOSE
|
|
1471
|
+
}
|
|
1472
|
+
];
|
|
1473
|
+
const handleChange = useCallback((value) => {
|
|
1474
|
+
switch (true) {
|
|
1475
|
+
case value === ACTION.CLOSE:
|
|
1476
|
+
onClose();
|
|
1477
|
+
break;
|
|
1478
|
+
case value === ACTION.NEW:
|
|
1479
|
+
onNew();
|
|
1480
|
+
break;
|
|
1481
|
+
case value === ACTION.DELETE_MENU:
|
|
1482
|
+
setView(VIEW.DELETE);
|
|
1483
|
+
break;
|
|
1484
|
+
case value === ACTION.BACK:
|
|
1485
|
+
setView(VIEW.MAIN);
|
|
1486
|
+
break;
|
|
1487
|
+
case value.startsWith(ACTION.DELETE_PREFIX):
|
|
1488
|
+
try {
|
|
1489
|
+
onDelete(value.slice(ACTION.DELETE_PREFIX.length));
|
|
1490
|
+
setError(void 0);
|
|
1491
|
+
refreshSessionList((key) => key + 1);
|
|
1492
|
+
} catch (error) {
|
|
1493
|
+
setError(error instanceof Error ? error.message : "Failed to delete session");
|
|
1494
|
+
}
|
|
1495
|
+
break;
|
|
1496
|
+
case value.startsWith(ACTION.OPEN_PREFIX):
|
|
1497
|
+
try {
|
|
1498
|
+
onOpen(value.slice(ACTION.OPEN_PREFIX.length));
|
|
1499
|
+
setError(void 0);
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
setError(error instanceof Error ? error.message : "Failed to open session");
|
|
1502
|
+
}
|
|
1503
|
+
break;
|
|
1504
|
+
}
|
|
1505
|
+
}, [
|
|
1506
|
+
onClose,
|
|
1507
|
+
onDelete,
|
|
1508
|
+
onNew,
|
|
1509
|
+
onOpen
|
|
1510
|
+
]);
|
|
1511
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
1512
|
+
flexDirection: "column",
|
|
1513
|
+
children: [
|
|
1514
|
+
/* @__PURE__ */ jsx(Text, { children: "Sessions" }),
|
|
1515
|
+
/* @__PURE__ */ jsx(SelectPromptHint, { message: view === VIEW.DELETE ? "Delete session" : "Select session" }),
|
|
1516
|
+
error && /* @__PURE__ */ jsx(Box, {
|
|
1517
|
+
marginBottom: 1,
|
|
1518
|
+
children: /* @__PURE__ */ jsx(Text, {
|
|
1519
|
+
color: "red",
|
|
1520
|
+
children: error
|
|
1521
|
+
})
|
|
1522
|
+
}),
|
|
1523
|
+
/* @__PURE__ */ jsx(SelectPrompt, {
|
|
1524
|
+
options,
|
|
1525
|
+
onCancel: onClose,
|
|
1526
|
+
onChange: handleChange
|
|
1527
|
+
}, `${view}:${String(sessions.length)}`)
|
|
1528
|
+
]
|
|
1529
|
+
});
|
|
1530
|
+
}
|
|
1531
|
+
//#endregion
|
|
1455
1532
|
//#region src/components/App.tsx
|
|
1456
1533
|
var SCREEN = /* @__PURE__ */ function(SCREEN) {
|
|
1457
1534
|
SCREEN["CHAT"] = "chat";
|
|
1458
1535
|
SCREEN["MODEL_PICKER"] = "model-picker";
|
|
1459
1536
|
SCREEN["SEARCH_SETTINGS"] = "search-settings";
|
|
1537
|
+
SCREEN["SESSION_MANAGER"] = "session-manager";
|
|
1460
1538
|
return SCREEN;
|
|
1461
1539
|
}(SCREEN || {});
|
|
1462
|
-
function
|
|
1540
|
+
function createSession(sessionId, model) {
|
|
1541
|
+
return sessionId ? loadSession(sessionId) : createSession$1(model);
|
|
1542
|
+
}
|
|
1543
|
+
function App({ sessionId }) {
|
|
1463
1544
|
const { exit } = useApp();
|
|
1464
1545
|
const [appConfig, setConfig] = useState(() => loadConfig());
|
|
1465
1546
|
const [currentScreen, setScreen] = useState(SCREEN.CHAT);
|
|
1466
1547
|
const [mode, setMode] = useState(SAFE);
|
|
1467
|
-
const [
|
|
1548
|
+
const [activeSession, setSession] = useState(() => createSession(sessionId, loadConfig().model));
|
|
1468
1549
|
const [isHeaderLoaded, setIsHeaderLoaded] = useState(false);
|
|
1550
|
+
const sessionRef = useRef(activeSession);
|
|
1551
|
+
useEffect(() => {
|
|
1552
|
+
sessionRef.current = activeSession;
|
|
1553
|
+
}, [activeSession]);
|
|
1554
|
+
useEffect(() => {
|
|
1555
|
+
return () => {
|
|
1556
|
+
const currentSession = sessionRef.current;
|
|
1557
|
+
if (!deleteSessionIfEmpty(currentSession.metadata.id) && currentSession.messages.length > 0) write(`Resume session: ${color(`code-ollama resume ${currentSession.metadata.id}`, "cyan")}\n`);
|
|
1558
|
+
};
|
|
1559
|
+
}, []);
|
|
1560
|
+
const setActiveSession = useCallback((nextSession) => {
|
|
1561
|
+
setSession((current) => {
|
|
1562
|
+
deleteSessionIfEmpty(current.metadata.id);
|
|
1563
|
+
return nextSession;
|
|
1564
|
+
});
|
|
1565
|
+
}, []);
|
|
1469
1566
|
const handleHeaderLoad = useCallback(() => {
|
|
1470
1567
|
setIsHeaderLoaded(true);
|
|
1471
1568
|
}, []);
|
|
1569
|
+
const handleCreateSession = useCallback(() => {
|
|
1570
|
+
const nextSession = createSession$1(appConfig.model);
|
|
1571
|
+
setActiveSession(nextSession);
|
|
1572
|
+
setScreen(SCREEN.CHAT);
|
|
1573
|
+
clear(nextSession.metadata.id);
|
|
1574
|
+
return nextSession;
|
|
1575
|
+
}, [appConfig.model, setActiveSession]);
|
|
1576
|
+
const handleOpenSession = useCallback((sessionId) => {
|
|
1577
|
+
if (sessionRef.current.metadata.id === sessionId) {
|
|
1578
|
+
setScreen(SCREEN.CHAT);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
setActiveSession(loadSession(sessionId));
|
|
1582
|
+
setScreen(SCREEN.CHAT);
|
|
1583
|
+
clear(sessionId);
|
|
1584
|
+
}, [setActiveSession]);
|
|
1585
|
+
const handleDeleteSession = useCallback((sessionId) => {
|
|
1586
|
+
deleteSession(sessionId);
|
|
1587
|
+
setSession((current) => {
|
|
1588
|
+
if (current.metadata.id !== sessionId) return current;
|
|
1589
|
+
return createSession$1(appConfig.model);
|
|
1590
|
+
});
|
|
1591
|
+
setScreen(SCREEN.SESSION_MANAGER);
|
|
1592
|
+
}, [appConfig.model]);
|
|
1593
|
+
const handleMessagesChange = useCallback((messages) => {
|
|
1594
|
+
setSession((current) => {
|
|
1595
|
+
const persistedMessages = messages.filter(({ content }) => content !== TURN_ABORTED_MESSAGE);
|
|
1596
|
+
if (persistedMessages.length <= current.messages.length) return current;
|
|
1597
|
+
let metadata = current.metadata;
|
|
1598
|
+
for (const message of persistedMessages.slice(current.messages.length)) metadata = appendMessage(metadata.id, message, appConfig.model);
|
|
1599
|
+
return {
|
|
1600
|
+
metadata,
|
|
1601
|
+
messages: persistedMessages
|
|
1602
|
+
};
|
|
1603
|
+
});
|
|
1604
|
+
}, [appConfig.model]);
|
|
1472
1605
|
const handleCommand = useCallback((command) => {
|
|
1473
1606
|
switch (command) {
|
|
1607
|
+
case "/session":
|
|
1608
|
+
setScreen(SCREEN.SESSION_MANAGER);
|
|
1609
|
+
break;
|
|
1474
1610
|
case "/model":
|
|
1475
1611
|
setScreen(SCREEN.MODEL_PICKER);
|
|
1476
1612
|
break;
|
|
1477
1613
|
case "/search":
|
|
1478
1614
|
setScreen(SCREEN.SEARCH_SETTINGS);
|
|
1479
1615
|
break;
|
|
1480
|
-
case "/clear":
|
|
1616
|
+
case "/clear": {
|
|
1481
1617
|
resetSystemMessage();
|
|
1482
|
-
clear();
|
|
1483
1618
|
setScreen(SCREEN.CHAT);
|
|
1484
|
-
|
|
1619
|
+
const nextSession = createSession$1(appConfig.model);
|
|
1620
|
+
setActiveSession(nextSession);
|
|
1621
|
+
clear(nextSession.metadata.id);
|
|
1485
1622
|
break;
|
|
1623
|
+
}
|
|
1486
1624
|
case "/exit":
|
|
1487
1625
|
exit();
|
|
1488
1626
|
break;
|
|
1489
1627
|
}
|
|
1490
|
-
}, [
|
|
1628
|
+
}, [
|
|
1629
|
+
appConfig.model,
|
|
1630
|
+
exit,
|
|
1631
|
+
setActiveSession
|
|
1632
|
+
]);
|
|
1491
1633
|
const handleUpdateConfig = useCallback((update) => {
|
|
1492
1634
|
setConfig((current) => ({
|
|
1493
1635
|
...current,
|
|
1494
1636
|
...update
|
|
1495
1637
|
}));
|
|
1496
1638
|
saveConfig(update);
|
|
1639
|
+
const newModel = update.model;
|
|
1640
|
+
if (newModel) setSession((current) => ({
|
|
1641
|
+
...current,
|
|
1642
|
+
metadata: updateSessionModel(current.metadata.id, newModel)
|
|
1643
|
+
}));
|
|
1497
1644
|
setScreen(SCREEN.CHAT);
|
|
1498
1645
|
}, []);
|
|
1499
1646
|
const handleClose = useCallback(() => {
|
|
@@ -1525,13 +1672,24 @@ function App() {
|
|
|
1525
1672
|
onClose: handleClose
|
|
1526
1673
|
});
|
|
1527
1674
|
break;
|
|
1675
|
+
case SCREEN.SESSION_MANAGER:
|
|
1676
|
+
screenContent = /* @__PURE__ */ jsx(SessionManager, {
|
|
1677
|
+
currentSessionId: activeSession.metadata.id,
|
|
1678
|
+
onClose: handleClose,
|
|
1679
|
+
onDelete: handleDeleteSession,
|
|
1680
|
+
onNew: handleCreateSession,
|
|
1681
|
+
onOpen: handleOpenSession
|
|
1682
|
+
});
|
|
1683
|
+
break;
|
|
1528
1684
|
case SCREEN.CHAT:
|
|
1529
1685
|
screenContent = /* @__PURE__ */ jsx(Chat, {
|
|
1686
|
+
initialMessages: activeSession.messages,
|
|
1530
1687
|
model: appConfig.model,
|
|
1531
1688
|
onCommand: handleCommand,
|
|
1689
|
+
onMessagesChange: handleMessagesChange,
|
|
1532
1690
|
mode,
|
|
1533
1691
|
onModeChange: setMode,
|
|
1534
|
-
sessionId
|
|
1692
|
+
sessionId: activeSession.metadata.id
|
|
1535
1693
|
});
|
|
1536
1694
|
break;
|
|
1537
1695
|
}
|
|
@@ -1553,15 +1711,15 @@ function App() {
|
|
|
1553
1711
|
}
|
|
1554
1712
|
//#endregion
|
|
1555
1713
|
//#region src/tui.tsx
|
|
1556
|
-
function renderApp() {
|
|
1714
|
+
function renderApp(sessionId) {
|
|
1557
1715
|
let resetKey = 0;
|
|
1558
|
-
const app = render(/* @__PURE__ */ jsx(App, {}, resetKey), {
|
|
1716
|
+
const app = render(/* @__PURE__ */ jsx(App, { sessionId }, resetKey), {
|
|
1559
1717
|
exitOnCtrlC: false,
|
|
1560
1718
|
maxFps: 60
|
|
1561
1719
|
});
|
|
1562
|
-
setClearHandler(() => {
|
|
1720
|
+
setClearHandler((nextSessionId) => {
|
|
1563
1721
|
reset();
|
|
1564
|
-
app.rerender(/* @__PURE__ */ jsx(App, {}, ++resetKey));
|
|
1722
|
+
app.rerender(/* @__PURE__ */ jsx(App, { sessionId: nextSessionId ?? sessionId }, ++resetKey));
|
|
1565
1723
|
});
|
|
1566
1724
|
}
|
|
1567
1725
|
//#endregion
|
package/dist/cli.js
CHANGED
|
@@ -1,18 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as runShell } from "./assets/shell-CipXM_WI.js";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import cac from "cac";
|
|
5
|
-
import { join } from "node:path";
|
|
6
5
|
import { homedir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
7
|
import { Ollama } from "ollama";
|
|
8
|
+
import { v7 } from "uuid";
|
|
9
|
+
//#region src/constants/command.ts
|
|
10
|
+
var LIST = [
|
|
11
|
+
{
|
|
12
|
+
name: "/clear",
|
|
13
|
+
description: "clear the current session"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "/session",
|
|
17
|
+
description: "manage sessions"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "/model",
|
|
21
|
+
description: "switch the model"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "/search",
|
|
25
|
+
description: "configure web search"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "/exit",
|
|
29
|
+
description: "exit the application"
|
|
30
|
+
}
|
|
31
|
+
];
|
|
32
|
+
//#endregion
|
|
8
33
|
//#region package.json
|
|
9
34
|
var name = "code-ollama";
|
|
10
|
-
var version = "0.
|
|
35
|
+
var version = "0.14.0";
|
|
11
36
|
//#endregion
|
|
12
37
|
//#region src/constants/package.ts
|
|
13
38
|
var NAME = name;
|
|
14
39
|
var VERSION = version;
|
|
15
40
|
//#endregion
|
|
41
|
+
//#region src/constants/config.ts
|
|
42
|
+
var DIRECTORY = join(homedir(), `.${NAME}`);
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/constants/decision.ts
|
|
45
|
+
var APPROVE = "approve";
|
|
46
|
+
var REJECT = "reject";
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/constants/mode.ts
|
|
49
|
+
var SAFE = "safe";
|
|
50
|
+
var AUTO = "auto";
|
|
51
|
+
var PLAN = "plan";
|
|
52
|
+
var LABEL = {
|
|
53
|
+
safe: "Safe",
|
|
54
|
+
auto: "Auto",
|
|
55
|
+
plan: "Plan"
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
16
58
|
//#region src/constants/prompt.ts
|
|
17
59
|
var BASE_SYSTEM_PROMPT = `You are a coding assistant that helps users write, edit, and understand code. You have access to tools for reading files, writing files, running shell commands, searching code, and searching the web
|
|
18
60
|
|
|
@@ -70,6 +112,9 @@ var VIEW_RANGE = "view_range";
|
|
|
70
112
|
var WEB_SEARCH = "web_search";
|
|
71
113
|
var WEB_FETCH = "web_fetch";
|
|
72
114
|
//#endregion
|
|
115
|
+
//#region src/constants/ui.ts
|
|
116
|
+
var HEADER_PREFIX = "🦙 ";
|
|
117
|
+
//#endregion
|
|
73
118
|
//#region src/utils/agents.ts
|
|
74
119
|
var AGENTS_FILE = "AGENTS.md";
|
|
75
120
|
function loadAgentsContent() {
|
|
@@ -104,8 +149,7 @@ function withSystemMessage(messages) {
|
|
|
104
149
|
}
|
|
105
150
|
//#endregion
|
|
106
151
|
//#region src/utils/config.ts
|
|
107
|
-
var
|
|
108
|
-
var CONFIG_PATH = join(CONFIG_DIRECTORY, "config.json");
|
|
152
|
+
var CONFIG_PATH = join(DIRECTORY, "config.json");
|
|
109
153
|
var DEFAULT_HOST = "http://localhost:11434";
|
|
110
154
|
var DEFAULT_MODEL$1 = "gemma4";
|
|
111
155
|
function readFile$1() {
|
|
@@ -129,7 +173,7 @@ function saveConfig(patch) {
|
|
|
129
173
|
...readFile$1(),
|
|
130
174
|
...patch
|
|
131
175
|
};
|
|
132
|
-
mkdirSync(
|
|
176
|
+
mkdirSync(DIRECTORY, { recursive: true });
|
|
133
177
|
writeFileSync(CONFIG_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
134
178
|
}
|
|
135
179
|
//#endregion
|
|
@@ -177,8 +221,8 @@ function setClearHandler(handler) {
|
|
|
177
221
|
/**
|
|
178
222
|
* Clear the screen with Ink.
|
|
179
223
|
*/
|
|
180
|
-
function clear() {
|
|
181
|
-
clearHandler?.();
|
|
224
|
+
function clear(sessionId) {
|
|
225
|
+
clearHandler?.(sessionId);
|
|
182
226
|
}
|
|
183
227
|
/**
|
|
184
228
|
* Reset the screen with ANSI escape sequence.
|
|
@@ -187,6 +231,156 @@ function reset() {
|
|
|
187
231
|
process.stdout.write("\x1Bc\x1B[?25l");
|
|
188
232
|
}
|
|
189
233
|
//#endregion
|
|
234
|
+
//#region src/utils/session.ts
|
|
235
|
+
var SESSIONS_DIRECTORY = join(DIRECTORY, "sessions");
|
|
236
|
+
var METADATA_FILE_NAME = "metadata.json";
|
|
237
|
+
var MESSAGES_FILE_NAME = "messages.jsonl";
|
|
238
|
+
var DEFAULT_TITLE = "New session";
|
|
239
|
+
var TITLE_MAX_LENGTH = 80;
|
|
240
|
+
function getSessionDirectory(id) {
|
|
241
|
+
return join(SESSIONS_DIRECTORY, id);
|
|
242
|
+
}
|
|
243
|
+
function getMetadataPath(id) {
|
|
244
|
+
return join(getSessionDirectory(id), METADATA_FILE_NAME);
|
|
245
|
+
}
|
|
246
|
+
function getMessagesPath(id) {
|
|
247
|
+
return join(getSessionDirectory(id), MESSAGES_FILE_NAME);
|
|
248
|
+
}
|
|
249
|
+
function ensureSessionsDirectory() {
|
|
250
|
+
mkdirSync(SESSIONS_DIRECTORY, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
function ensureSessionDirectory(id) {
|
|
253
|
+
const directory = getSessionDirectory(id);
|
|
254
|
+
mkdirSync(directory, { recursive: true });
|
|
255
|
+
return directory;
|
|
256
|
+
}
|
|
257
|
+
function readMetadata(id) {
|
|
258
|
+
const path = getMetadataPath(id);
|
|
259
|
+
if (!existsSync(path)) throw new Error(`Session not found: ${id}`);
|
|
260
|
+
try {
|
|
261
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
262
|
+
} catch {
|
|
263
|
+
throw new Error(`Invalid session metadata: ${id}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function writeMetadata(metadata) {
|
|
267
|
+
ensureSessionDirectory(metadata.id);
|
|
268
|
+
writeFileSync(getMetadataPath(metadata.id), JSON.stringify(metadata, null, 2) + "\n", "utf8");
|
|
269
|
+
}
|
|
270
|
+
function readMessages(id) {
|
|
271
|
+
const path = getMessagesPath(id);
|
|
272
|
+
if (!existsSync(path)) return [];
|
|
273
|
+
const content = readFileSync(path, "utf8").trim();
|
|
274
|
+
if (!content) return [];
|
|
275
|
+
try {
|
|
276
|
+
return content.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
277
|
+
} catch {
|
|
278
|
+
throw new Error(`Invalid session messages: ${id}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function deriveTitle(message) {
|
|
282
|
+
// v8 ignore next - title derivation is only reached for user messages
|
|
283
|
+
if (message.role !== "user") return DEFAULT_TITLE;
|
|
284
|
+
const normalized = message.content.replace(/\s+/g, " ").trim();
|
|
285
|
+
if (!normalized) return DEFAULT_TITLE;
|
|
286
|
+
return normalized.length > TITLE_MAX_LENGTH ? normalized.slice(0, TITLE_MAX_LENGTH - 1).trimEnd() + "…" : normalized;
|
|
287
|
+
}
|
|
288
|
+
function updateTitle(metadata, message) {
|
|
289
|
+
if (metadata.title !== DEFAULT_TITLE || message.role !== "user") return metadata;
|
|
290
|
+
return {
|
|
291
|
+
...metadata,
|
|
292
|
+
title: deriveTitle(message)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function createSession(model) {
|
|
296
|
+
ensureSessionsDirectory();
|
|
297
|
+
const id = v7();
|
|
298
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
299
|
+
const metadata = {
|
|
300
|
+
id,
|
|
301
|
+
createdAt: now,
|
|
302
|
+
updatedAt: now,
|
|
303
|
+
title: DEFAULT_TITLE,
|
|
304
|
+
model
|
|
305
|
+
};
|
|
306
|
+
ensureSessionDirectory(id);
|
|
307
|
+
writeMetadata(metadata);
|
|
308
|
+
writeFileSync(getMessagesPath(id), "", "utf8");
|
|
309
|
+
return {
|
|
310
|
+
metadata,
|
|
311
|
+
messages: []
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function listSessions() {
|
|
315
|
+
if (!existsSync(SESSIONS_DIRECTORY)) return [];
|
|
316
|
+
return readdirSync(SESSIONS_DIRECTORY, { withFileTypes: true }).filter((entry) => entry.isDirectory()).flatMap((entry) => {
|
|
317
|
+
try {
|
|
318
|
+
return [readMetadata(entry.name)];
|
|
319
|
+
} catch {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
}).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
323
|
+
}
|
|
324
|
+
function loadSession(id) {
|
|
325
|
+
return {
|
|
326
|
+
metadata: readMetadata(id),
|
|
327
|
+
messages: readMessages(id)
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function appendMessage(id, message, model) {
|
|
331
|
+
ensureSessionDirectory(id);
|
|
332
|
+
let metadata = readMetadata(id);
|
|
333
|
+
metadata = updateTitle(metadata, message);
|
|
334
|
+
metadata = {
|
|
335
|
+
...metadata,
|
|
336
|
+
model,
|
|
337
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
338
|
+
};
|
|
339
|
+
appendFileSync(getMessagesPath(id), JSON.stringify(message) + "\n", "utf8");
|
|
340
|
+
writeMetadata(metadata);
|
|
341
|
+
return metadata;
|
|
342
|
+
}
|
|
343
|
+
function updateSessionModel(id, model) {
|
|
344
|
+
const metadata = {
|
|
345
|
+
...readMetadata(id),
|
|
346
|
+
model
|
|
347
|
+
};
|
|
348
|
+
writeMetadata(metadata);
|
|
349
|
+
return metadata;
|
|
350
|
+
}
|
|
351
|
+
function deleteSessionIfEmpty(id) {
|
|
352
|
+
const directory = getSessionDirectory(id);
|
|
353
|
+
if (!existsSync(directory)) return false;
|
|
354
|
+
const messagesPath = getMessagesPath(id);
|
|
355
|
+
if (existsSync(messagesPath) && readFileSync(messagesPath, "utf8").trim() !== "") return false;
|
|
356
|
+
rmSync(directory, {
|
|
357
|
+
recursive: true,
|
|
358
|
+
force: false
|
|
359
|
+
});
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
function deleteSession(id) {
|
|
363
|
+
const directory = getSessionDirectory(id);
|
|
364
|
+
if (!existsSync(directory)) throw new Error(`Session not found: ${id}`);
|
|
365
|
+
rmSync(directory, {
|
|
366
|
+
recursive: true,
|
|
367
|
+
force: false
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/utils/terminal.ts
|
|
372
|
+
var ANSI_COLOR = { cyan: ["\x1B[36m", "\x1B[39m"] };
|
|
373
|
+
function color(text, name) {
|
|
374
|
+
const [open, close] = ANSI_COLOR[name];
|
|
375
|
+
return `${open}${text}${close}`;
|
|
376
|
+
}
|
|
377
|
+
function write(text) {
|
|
378
|
+
process.stdout.write(text);
|
|
379
|
+
}
|
|
380
|
+
function writeError(text) {
|
|
381
|
+
process.stderr.write(text);
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
190
384
|
//#region src/utils/time.ts
|
|
191
385
|
var tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
192
386
|
//#endregion
|
|
@@ -655,9 +849,16 @@ cli.command("run <model> <prompt>", "Run a one-off prompt").action(async (model,
|
|
|
655
849
|
try {
|
|
656
850
|
await runPrompt(model, prompt);
|
|
657
851
|
} catch (error) {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
852
|
+
writeError(`Error: ${error instanceof Error ? error.message : "Unknown error"}\n`);
|
|
853
|
+
process.exitCode = 1;
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
cli.command("resume <sessionId>", "Resume a saved session").action(async (sessionId) => {
|
|
857
|
+
try {
|
|
858
|
+
loadSession(sessionId);
|
|
859
|
+
await launchTui(sessionId);
|
|
860
|
+
} catch (error) {
|
|
861
|
+
writeError(`Error: ${error instanceof Error ? error.message : "Unknown error"}\n`);
|
|
661
862
|
process.exitCode = 1;
|
|
662
863
|
}
|
|
663
864
|
});
|
|
@@ -666,7 +867,7 @@ async function runPrompt(model, prompt) {
|
|
|
666
867
|
role: USER,
|
|
667
868
|
content: prompt
|
|
668
869
|
}], model);
|
|
669
|
-
|
|
870
|
+
write("\n");
|
|
670
871
|
}
|
|
671
872
|
async function processRunStream(messages, model) {
|
|
672
873
|
const assistantMessage = {
|
|
@@ -676,7 +877,7 @@ async function processRunStream(messages, model) {
|
|
|
676
877
|
for await (const chunk of streamChat(messages, model, TOOLS)) {
|
|
677
878
|
if (chunk.type === "content") {
|
|
678
879
|
assistantMessage.content += chunk.content;
|
|
679
|
-
|
|
880
|
+
write(chunk.content);
|
|
680
881
|
continue;
|
|
681
882
|
}
|
|
682
883
|
for (const toolCall of chunk.tool_calls) {
|
|
@@ -695,17 +896,17 @@ async function processRunStream(messages, model) {
|
|
|
695
896
|
}
|
|
696
897
|
}
|
|
697
898
|
async function main(args = process.argv.slice(2)) {
|
|
698
|
-
if (
|
|
699
|
-
const { renderApp } = await import("./assets/tui-CCa_vqh0.js");
|
|
700
|
-
reset();
|
|
701
|
-
renderApp();
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
cli.parse([
|
|
899
|
+
if (args.length) cli.parse([
|
|
705
900
|
"node",
|
|
706
901
|
"code-ollama",
|
|
707
902
|
...args
|
|
708
903
|
]);
|
|
904
|
+
else await launchTui();
|
|
905
|
+
}
|
|
906
|
+
async function launchTui(sessionId) {
|
|
907
|
+
const { renderApp } = await import("./assets/tui-TX7C0xYg.js");
|
|
908
|
+
reset();
|
|
909
|
+
renderApp(sessionId);
|
|
709
910
|
}
|
|
710
911
|
// v8 ignore start
|
|
711
912
|
function isEntrypoint(argv1 = process.argv[1]) {
|
|
@@ -719,4 +920,4 @@ function isEntrypoint(argv1 = process.argv[1]) {
|
|
|
719
920
|
if (isEntrypoint()) main();
|
|
720
921
|
// v8 ignore stop
|
|
721
922
|
//#endregion
|
|
722
|
-
export { USER as _, tick as a,
|
|
923
|
+
export { LABEL as A, withSystemMessage as C, USER as D, SYSTEM as E, VERSION as F, LIST as I, SAFE as M, APPROVE as N, PLAN_GENERATION_INSTRUCTION as O, REJECT as P, resetSystemMessage as S, ASSISTANT as T, setClearHandler as _, tick as a, loadConfig as b, appendMessage as c, deleteSessionIfEmpty as d, listSessions as f, reset as g, clear as h, WRITE_TOOLS as i, PLAN as j, AUTO as k, createSession as l, updateSessionModel as m, main, READ_TOOLS as n, color as o, loadSession as p, TOOLS as r, write as s, executeTool as t, deleteSession as u, listModels as v, HEADER_PREFIX as w, saveConfig as x, streamChat as y };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-ollama",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Ollama coding agent that runs in your terminal",
|
|
5
5
|
"author": "Mark <mark@remarkablemark.org> (https://remarkablemark.org)",
|
|
6
6
|
"type": "module",
|
|
@@ -46,16 +46,17 @@
|
|
|
46
46
|
"marked": "15.0.12",
|
|
47
47
|
"marked-terminal": "7.3.0",
|
|
48
48
|
"ollama": "0.6.3",
|
|
49
|
-
"react": "19.2.6"
|
|
49
|
+
"react": "19.2.6",
|
|
50
|
+
"uuid": "14.0.0"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
|
-
"@commitlint/cli": "21.0.
|
|
53
|
-
"@commitlint/config-conventional": "21.0.
|
|
53
|
+
"@commitlint/cli": "21.0.1",
|
|
54
|
+
"@commitlint/config-conventional": "21.0.1",
|
|
54
55
|
"@eslint/config-helpers": "0.6.0",
|
|
55
56
|
"@eslint/js": "10.0.1",
|
|
56
|
-
"@types/node": "25.
|
|
57
|
+
"@types/node": "25.7.0",
|
|
57
58
|
"@types/react": "19.2.14",
|
|
58
|
-
"@vitest/coverage-v8": "4.1.
|
|
59
|
+
"@vitest/coverage-v8": "4.1.6",
|
|
59
60
|
"eslint": "10.3.0",
|
|
60
61
|
"eslint-plugin-prettier": "5.5.5",
|
|
61
62
|
"eslint-plugin-simple-import-sort": "13.0.0",
|
|
@@ -64,12 +65,12 @@
|
|
|
64
65
|
"ink-testing-library": "4.0.0",
|
|
65
66
|
"lint-staged": "17.0.4",
|
|
66
67
|
"prettier": "3.8.3",
|
|
67
|
-
"publint": "0.3.
|
|
68
|
+
"publint": "0.3.21",
|
|
68
69
|
"tsx": "4.21.0",
|
|
69
70
|
"typescript": "6.0.3",
|
|
70
|
-
"typescript-eslint": "8.59.
|
|
71
|
-
"vite": "8.0.
|
|
72
|
-
"vitest": "4.1.
|
|
71
|
+
"typescript-eslint": "8.59.3",
|
|
72
|
+
"vite": "8.0.12",
|
|
73
|
+
"vitest": "4.1.6"
|
|
73
74
|
},
|
|
74
75
|
"files": [
|
|
75
76
|
"dist/"
|