casabot 1.1.8 → 1.1.11
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/Logo.svg +24 -0
- package/Logo2.png +0 -0
- package/README.md +4 -0
- package/dist/agent/base.d.ts +1 -1
- package/dist/agent/base.js +38 -2
- package/dist/cli/index.js +22 -0
- package/dist/tui/app.js +142 -21
- package/package.json +2 -1
- package/skills/agent/SKILL.md +51 -4
- package/src/agent/base.ts +43 -1
- package/src/cli/index.ts +26 -0
- package/src/tui/app.tsx +280 -30
package/Logo.svg
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg width="3701" height="1227" viewBox="0 0 3701 1227" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect width="3701" height="1227" fill="white"/>
|
|
3
|
+
<path d="M660.525 238.407C676.289 208.559 719.039 208.559 734.803 238.407L775.914 316.25C785.995 335.338 808.903 343.676 828.895 335.534L910.424 302.328C941.686 289.596 974.435 317.076 967.324 350.073L948.781 436.13C944.233 457.232 956.423 478.345 976.971 484.958L1060.77 511.927C1092.9 522.268 1100.33 564.369 1073.67 585.076L1004.15 639.08C987.1 652.322 982.866 676.33 994.357 694.604L1041.22 769.129C1059.18 797.705 1037.81 834.728 1004.08 833.455L916.107 830.137C894.536 829.323 875.861 844.994 872.916 866.378L860.908 953.588C856.304 987.027 816.132 1001.65 791.11 978.992L725.855 919.904C709.853 905.416 685.475 905.416 669.473 919.904L604.218 978.992C579.196 1001.65 539.024 987.027 534.42 953.588L522.412 866.378C519.467 844.994 500.792 829.323 479.221 830.137L391.252 833.455C357.521 834.728 336.145 797.705 354.113 769.129L400.971 694.604C412.461 676.33 408.228 652.322 391.181 639.08L321.659 585.076C295.002 564.369 302.426 522.268 334.557 511.927L418.357 484.958C438.905 478.345 451.094 457.232 446.547 436.13L428.004 350.073C420.893 317.076 453.642 289.596 484.904 302.328L566.433 335.534C586.425 343.676 609.333 335.338 619.414 316.25L660.525 238.407Z" fill="#EA3A23"/>
|
|
4
|
+
<path d="M797.472 250.022C822.885 227.806 862.796 243.126 866.816 276.641L877.3 364.047C879.871 385.479 898.27 401.473 919.851 401.036L1007.87 399.254C1041.61 398.57 1062.34 435.961 1043.88 464.218L995.724 537.914C983.917 555.985 987.73 580.063 1004.54 593.601L1073.11 648.81C1099.4 669.979 1091.25 711.944 1058.94 721.723L974.682 747.225C954.022 753.479 941.466 774.375 945.644 795.553L962.683 881.921C969.216 915.037 935.993 941.941 904.958 928.665L824.021 894.042C804.174 885.552 781.124 893.489 770.711 912.398L728.248 989.511C711.966 1019.08 669.222 1018.33 653.982 988.215L614.235 909.666C604.489 890.406 581.73 881.669 561.599 889.461L479.503 921.239C448.024 933.423 415.759 905.377 423.445 872.508L443.487 786.788C448.402 765.769 436.583 744.447 416.153 737.476L332.837 709.048C300.891 698.148 294.203 655.924 321.218 635.685L391.671 582.903C408.947 569.96 413.599 546.03 402.429 527.558L356.878 452.227C339.413 423.342 361.431 386.697 395.134 388.558L483.032 393.411C504.586 394.601 523.532 379.259 526.849 357.929L540.377 270.943C545.564 237.589 585.986 223.671 610.608 246.761L674.822 306.978C690.568 321.744 714.943 322.17 731.195 307.962L797.472 250.022Z" fill="#2BADEE" fill-opacity="0.5"/>
|
|
5
|
+
<circle cx="487.27" cy="557.093" r="116.956" fill="white"/>
|
|
6
|
+
<circle cx="871.191" cy="531.667" r="116.956" fill="white"/>
|
|
7
|
+
<circle cx="430.063" cy="545.651" r="34.3241" fill="black"/>
|
|
8
|
+
<circle cx="923.313" cy="497.343" r="34.3241" fill="black"/>
|
|
9
|
+
<path d="M822.883 682.312C822.883 698.756 819.645 715.039 813.352 730.232C807.059 745.424 797.835 759.228 786.208 770.856C774.58 782.484 760.776 791.707 745.583 798C730.391 804.293 714.108 807.532 697.664 807.532C681.22 807.532 664.937 804.293 649.744 798C634.552 791.707 620.748 782.484 609.12 770.856C597.493 759.228 588.269 745.424 581.976 730.232C575.683 715.039 572.444 698.756 572.444 682.312L629.702 682.312C629.702 691.237 631.46 700.075 634.875 708.32C638.291 716.566 643.297 724.058 649.607 730.369C655.918 736.68 663.41 741.686 671.656 745.101C679.902 748.516 688.739 750.274 697.664 750.274C706.589 750.274 715.426 748.516 723.672 745.101C731.917 741.686 739.41 736.68 745.72 730.369C752.031 724.058 757.037 716.566 760.453 708.32C763.868 700.075 765.626 691.237 765.626 682.312H822.883Z" fill="black"/>
|
|
10
|
+
<path d="M1685.16 695.25C1696.64 670.106 1732.36 670.106 1743.84 695.25V695.25C1749.56 707.757 1762.57 715.269 1776.25 713.962V713.962C1803.77 711.335 1821.63 742.271 1805.6 764.788V764.788C1797.62 775.988 1797.62 791.012 1805.6 802.212V802.212C1821.63 824.729 1803.77 855.665 1776.25 853.038V853.038C1762.57 851.731 1749.56 859.243 1743.84 871.75V871.75C1732.36 896.894 1696.64 896.894 1685.16 871.75V871.75C1679.44 859.243 1666.43 851.731 1652.75 853.038V853.038C1625.23 855.665 1607.37 824.729 1623.4 802.212V802.212C1631.38 791.012 1631.38 775.988 1623.4 764.788V764.788C1607.37 742.271 1625.23 711.335 1652.75 713.962V713.962C1666.43 715.269 1679.44 707.757 1685.16 695.25V695.25Z" fill="#EA3A23"/>
|
|
11
|
+
<path d="M2697.58 657.708C2710.48 633.24 2745.52 633.24 2758.42 657.708V657.708C2765.46 671.079 2780.42 678.279 2795.26 675.453V675.453C2822.44 670.28 2844.28 697.676 2833.19 723.015V723.015C2827.14 736.862 2830.83 753.042 2842.3 762.888V762.888C2863.28 780.906 2855.48 815.068 2828.76 822.197V822.197C2814.15 826.093 2803.81 839.068 2803.26 854.172V854.172C2802.26 881.813 2770.69 897.017 2748.45 880.567V880.567C2736.3 871.578 2719.7 871.578 2707.55 880.567V880.567C2685.31 897.017 2653.74 881.813 2652.74 854.172V854.172C2652.19 839.068 2641.85 826.093 2627.24 822.197V822.197C2600.52 815.068 2592.72 780.906 2613.7 762.888V762.888C2625.17 753.042 2628.86 736.862 2622.81 723.015V723.015C2611.72 697.676 2633.56 670.28 2660.74 675.453V675.453C2675.58 678.279 2690.54 671.079 2697.58 657.708V657.708Z" fill="black"/>
|
|
12
|
+
<path d="M3078.87 866.916C3058.12 899.232 3010.88 899.232 2990.13 866.916L2976.57 845.808C2969.43 834.689 2958.37 826.657 2945.59 823.3L2921.33 816.929C2884.18 807.174 2869.58 762.253 2893.9 732.527L2909.79 713.11C2918.15 702.882 2922.38 689.885 2921.62 676.692L2920.18 651.646C2917.98 613.302 2956.19 585.539 2991.98 599.482L3015.35 608.59C3027.67 613.388 3041.33 613.388 3053.65 608.59L3077.02 599.482C3112.81 585.539 3151.02 613.302 3148.82 651.646L3147.38 676.692C3146.62 689.885 3150.85 702.882 3159.21 713.11L3175.1 732.527C3199.42 762.253 3184.82 807.174 3147.67 816.929L3123.41 823.3C3110.63 826.657 3099.57 834.689 3092.43 845.808L3078.87 866.916Z" fill="#2BADEE"/>
|
|
13
|
+
<path d="M1545.44 819.589C1515.81 849.148 1478.24 869.518 1437.26 878.238C1396.29 886.957 1353.65 883.654 1314.52 868.728C1275.38 853.801 1241.41 827.888 1216.71 794.12C1192.01 760.351 1177.64 720.17 1175.33 678.43C1173.02 636.69 1182.87 595.176 1203.7 558.901C1224.52 522.625 1255.43 493.141 1292.68 474.008C1329.93 454.874 1371.94 446.911 1413.63 451.079C1455.33 455.247 1494.92 471.367 1527.62 497.495L1481.9 554.471C1460.22 537.151 1433.98 526.465 1406.34 523.702C1378.71 520.94 1350.86 526.219 1326.16 538.902C1301.47 551.584 1280.98 571.129 1267.18 595.175C1253.38 619.221 1246.84 646.74 1248.37 674.408C1249.91 702.077 1259.43 728.712 1275.8 751.096C1292.18 773.48 1314.69 790.657 1340.64 800.552C1366.58 810.446 1394.84 812.636 1422 806.856C1449.17 801.076 1474.07 787.573 1493.71 767.979L1545.44 819.589Z" fill="black"/>
|
|
14
|
+
<path d="M1962.5 777C1937.17 777 1912.88 766.886 1894.97 748.882C1877.06 730.879 1867 706.461 1867 681C1867 655.539 1877.06 631.121 1894.97 613.118C1912.88 595.114 1937.17 585 1962.5 585L1962.5 639.079C1951.44 639.079 1940.83 643.495 1933.01 651.357C1925.19 659.219 1920.8 669.882 1920.8 681C1920.8 692.118 1925.19 702.781 1933.01 710.643C1940.83 718.505 1951.44 722.921 1962.5 722.921L1962.5 777Z" fill="black"/>
|
|
15
|
+
<path d="M1962.5 701C1987.83 701 2012.12 711.062 2030.03 728.971C2047.94 746.881 2058 771.172 2058 796.5C2058 821.828 2047.94 846.119 2030.03 864.029C2012.12 881.938 1987.83 892 1962.5 892L1962.5 838.203C1973.56 838.203 1984.17 833.809 1991.99 825.989C1999.81 818.168 2004.2 807.56 2004.2 796.5C2004.2 785.44 1999.81 774.832 1991.99 767.011C1984.17 759.191 1973.56 754.797 1962.5 754.797L1962.5 701Z" fill="black"/>
|
|
16
|
+
<path d="M1624.31 640.094C1631.91 618.283 1647.15 599.963 1667.22 588.527C1687.28 577.092 1710.81 573.316 1733.45 577.899C1756.09 582.482 1776.3 595.113 1790.33 613.452C1804.37 631.792 1811.29 654.598 1809.8 677.646L1753.63 674.024C1754.24 664.56 1751.4 655.197 1745.64 647.667C1739.87 640.137 1731.57 634.951 1722.28 633.069C1712.99 631.188 1703.32 632.738 1695.09 637.433C1686.85 642.128 1680.59 649.65 1677.47 658.605L1624.31 640.094Z" fill="black"/>
|
|
17
|
+
<rect x="2148.83" y="455.492" width="75" height="420.804" transform="rotate(9 2148.83 455.492)" fill="black"/>
|
|
18
|
+
<rect x="2522" y="450" width="75" height="434" fill="black"/>
|
|
19
|
+
<rect x="3262" y="455" width="75" height="434" fill="black"/>
|
|
20
|
+
<rect x="3417" y="539" width="75" height="234" transform="rotate(90 3417 539)" fill="black"/>
|
|
21
|
+
<rect x="3417" y="817" width="75" height="155" transform="rotate(90 3417 817)" fill="black"/>
|
|
22
|
+
<rect x="2223" y="466.871" width="75" height="420.804" transform="rotate(-13 2223 466.871)" fill="black"/>
|
|
23
|
+
<rect x="2413" y="657" width="75" height="280" transform="rotate(90 2413 657)" fill="black"/>
|
|
24
|
+
</svg>
|
package/Logo2.png
ADDED
|
Binary file
|
package/README.md
CHANGED
package/dist/agent/base.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ChatProvider } from "../providers/base.js";
|
|
2
2
|
import type { Message, Skill, ConversationHistory } from "../config/types.js";
|
|
3
3
|
export declare function buildSystemPrompt(skills: Skill[]): string;
|
|
4
|
-
export declare function runAgent(provider: ChatProvider, userMessage: string, conversation: ConversationHistory, skills: Skill[]): AsyncGenerator<Message>;
|
|
4
|
+
export declare function runAgent(provider: ChatProvider, userMessage: string, conversation: ConversationHistory, skills: Skill[], signal: AbortSignal): AsyncGenerator<Message>;
|
|
5
5
|
//# sourceMappingURL=base.d.ts.map
|
package/dist/agent/base.js
CHANGED
|
@@ -3,6 +3,18 @@ import { appendMessage } from "../history/store.js";
|
|
|
3
3
|
import { formatSkillsForPrompt } from "../skills/loader.js";
|
|
4
4
|
import { CASABOT_HOME } from "../config/manager.js";
|
|
5
5
|
const MAX_ITERATIONS = 20;
|
|
6
|
+
const MAX_RETRIES = 3;
|
|
7
|
+
const RETRY_BASE_DELAY_MS = 2000;
|
|
8
|
+
function raceAbort(promise, signal) {
|
|
9
|
+
if (signal.aborted)
|
|
10
|
+
return Promise.reject(new Error("AbortError"));
|
|
11
|
+
return Promise.race([
|
|
12
|
+
promise,
|
|
13
|
+
new Promise((_, reject) => {
|
|
14
|
+
signal.addEventListener("abort", () => reject(new Error("AbortError")), { once: true });
|
|
15
|
+
}),
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
6
18
|
export function buildSystemPrompt(skills) {
|
|
7
19
|
const skillList = formatSkillsForPrompt(skills);
|
|
8
20
|
return `You are the base agent of CasAbot. Cassiopeia A — Freely creates everything, like a supernova explosion.
|
|
@@ -33,23 +45,47 @@ export function buildSystemPrompt(skills) {
|
|
|
33
45
|
## ${skillList}
|
|
34
46
|
`;
|
|
35
47
|
}
|
|
36
|
-
export async function* runAgent(provider, userMessage, conversation, skills) {
|
|
48
|
+
export async function* runAgent(provider, userMessage, conversation, skills, signal) {
|
|
37
49
|
const systemPrompt = buildSystemPrompt(skills);
|
|
38
50
|
const userMsg = { role: "user", content: userMessage };
|
|
39
51
|
await appendMessage(conversation, userMsg);
|
|
40
52
|
const tools = [TERMINAL_TOOL];
|
|
41
53
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
54
|
+
if (signal.aborted)
|
|
55
|
+
return;
|
|
42
56
|
const messagesWithSystem = [
|
|
43
57
|
{ role: "system", content: systemPrompt },
|
|
44
58
|
...conversation.messages,
|
|
45
59
|
];
|
|
46
|
-
|
|
60
|
+
let assistantMsg;
|
|
61
|
+
for (let attempt = 0;; attempt++) {
|
|
62
|
+
try {
|
|
63
|
+
assistantMsg = await raceAbort(provider.chat(messagesWithSystem, tools), signal);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
if (signal.aborted)
|
|
68
|
+
return;
|
|
69
|
+
if (attempt >= MAX_RETRIES)
|
|
70
|
+
throw err;
|
|
71
|
+
const delay = RETRY_BASE_DELAY_MS * 2 ** attempt;
|
|
72
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
73
|
+
const retryMsg = {
|
|
74
|
+
role: "assistant",
|
|
75
|
+
content: `⏳ API request failed: ${errorMsg}. Retrying in ${delay}ms... (${attempt + 1}/${MAX_RETRIES})`,
|
|
76
|
+
};
|
|
77
|
+
yield retryMsg;
|
|
78
|
+
await raceAbort(new Promise((resolve) => setTimeout(resolve, delay)), signal);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
47
81
|
await appendMessage(conversation, assistantMsg);
|
|
48
82
|
yield assistantMsg;
|
|
49
83
|
if (!assistantMsg.toolCalls?.length) {
|
|
50
84
|
return;
|
|
51
85
|
}
|
|
52
86
|
for (const toolCall of assistantMsg.toolCalls) {
|
|
87
|
+
if (signal.aborted)
|
|
88
|
+
return;
|
|
53
89
|
let result;
|
|
54
90
|
if (toolCall.name === "run_command") {
|
|
55
91
|
try {
|
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { dirname, join } from "path";
|
|
2
5
|
import { Command } from "commander";
|
|
3
6
|
import { loadConfig, saveConfig, getDefaultConfig, ensureDirectories } from "../config/manager.js";
|
|
4
7
|
import { createProvider } from "../providers/index.js";
|
|
@@ -6,6 +9,24 @@ import { loadSkills } from "../skills/loader.js";
|
|
|
6
9
|
import { createConversation } from "../history/store.js";
|
|
7
10
|
import { startTUI } from "../tui/app.js";
|
|
8
11
|
import { setupWizard } from "./setup.js";
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
async function displayLogo() {
|
|
14
|
+
try {
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const asciify = require("asciify-image");
|
|
17
|
+
const logoPath = join(__dirname, "..", "..", "Logo2.png");
|
|
18
|
+
const ascii = await asciify(logoPath, {
|
|
19
|
+
fit: "box",
|
|
20
|
+
width: "50%",
|
|
21
|
+
color: true,
|
|
22
|
+
});
|
|
23
|
+
console.log(ascii);
|
|
24
|
+
console.log("");
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Logo display is non-critical; silently skip on failure
|
|
28
|
+
}
|
|
29
|
+
}
|
|
9
30
|
const program = new Command();
|
|
10
31
|
program
|
|
11
32
|
.name("casabot")
|
|
@@ -58,6 +79,7 @@ program
|
|
|
58
79
|
const provider = createProvider(providerConfig);
|
|
59
80
|
const skills = await loadSkills();
|
|
60
81
|
const conversation = createConversation();
|
|
82
|
+
await displayLogo();
|
|
61
83
|
startTUI(provider, conversation, skills);
|
|
62
84
|
}
|
|
63
85
|
catch (err) {
|
package/dist/tui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useCallback, useMemo } from "react";
|
|
2
|
+
import { useState, useCallback, useMemo, useRef, useEffect } from "react";
|
|
3
3
|
import { render, Box, Text, Static, useInput, useApp, useStdout } from "ink";
|
|
4
4
|
import TextInput from "ink-text-input";
|
|
5
5
|
import Spinner from "ink-spinner";
|
|
@@ -7,6 +7,9 @@ import Gradient from "ink-gradient";
|
|
|
7
7
|
import { Marked } from "marked";
|
|
8
8
|
import { markedTerminal } from "marked-terminal";
|
|
9
9
|
import { runAgent } from "../agent/base.js";
|
|
10
|
+
import { createConversation, listConversations, saveConversation, appendMessage, } from "../history/store.js";
|
|
11
|
+
const BRAND_BLUE = "#2BADEE";
|
|
12
|
+
const BRAND_RED = "#EA3A23";
|
|
10
13
|
function renderMarkdown(content) {
|
|
11
14
|
const width = Math.max((process.stdout.columns ?? 80) - 8, 40);
|
|
12
15
|
const md = new Marked({ gfm: true });
|
|
@@ -20,21 +23,33 @@ function truncateOutput(content, maxLines = 8) {
|
|
|
20
23
|
return (lines.slice(0, maxLines).join("\n") +
|
|
21
24
|
`\n … ${lines.length - maxLines} more lines`);
|
|
22
25
|
}
|
|
26
|
+
function formatDate(iso) {
|
|
27
|
+
const d = new Date(iso);
|
|
28
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
29
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
30
|
+
}
|
|
31
|
+
function getPreview(conv, maxLen = 50) {
|
|
32
|
+
const firstUser = conv.messages.find((m) => m.role === "user");
|
|
33
|
+
if (!firstUser)
|
|
34
|
+
return "(empty session)";
|
|
35
|
+
const text = firstUser.content.replace(/\n/g, " ").trim();
|
|
36
|
+
return text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text;
|
|
37
|
+
}
|
|
23
38
|
function HRule({ columns }) {
|
|
24
39
|
return (_jsx(Box, { paddingX: 1, width: columns, children: _jsx(Text, { dimColor: true, children: "─".repeat(Math.max(columns - 4, 1)) }) }));
|
|
25
40
|
}
|
|
26
41
|
function HeaderBlock({ columns }) {
|
|
27
|
-
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, width: columns, children: [_jsx(Box, { paddingX: 2, children: _jsx(Gradient, {
|
|
42
|
+
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, width: columns, children: [_jsx(Box, { paddingX: 2, children: _jsx(Gradient, { colors: [BRAND_RED, BRAND_BLUE], children: _jsx(Text, { bold: true, children: "✦ CasAbot" }) }) }), _jsx(Box, { paddingX: 2, children: _jsx(Text, { wrap: "wrap", dimColor: true, children: "Cassiopeia A — Freely creates everything, like a supernova explosion." }) }), _jsx(HRule, { columns: columns })] }));
|
|
28
43
|
}
|
|
29
44
|
function UserMessageView({ content, columns }) {
|
|
30
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, width: columns, children: [_jsx(Text, { color:
|
|
45
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, width: columns, children: [_jsx(Text, { color: BRAND_RED, bold: true, children: "▶ You" }), _jsx(Box, { marginLeft: 2, width: Math.max(columns - 6, 10), children: _jsx(Text, { wrap: "wrap", children: content }) })] }));
|
|
31
46
|
}
|
|
32
47
|
function AssistantMessageView({ content, columns, }) {
|
|
33
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, width: columns, children: [_jsx(Text, { color:
|
|
48
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, width: columns, children: [_jsx(Text, { color: BRAND_BLUE, bold: true, children: "✦ CasAbot" }), _jsx(Box, { marginLeft: 2, width: Math.max(columns - 6, 10), children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(content) }) })] }));
|
|
34
49
|
}
|
|
35
50
|
function ToolCallsView({ message, columns, }) {
|
|
36
51
|
const boxWidth = Math.max(columns - 6, 10);
|
|
37
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, width: columns, children: [_jsx(Text, { color:
|
|
52
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, width: columns, children: [_jsx(Text, { color: BRAND_BLUE, bold: true, children: "✦ CasAbot" }), message.content ? (_jsx(Box, { marginLeft: 2, width: boxWidth, children: _jsx(Text, { wrap: "wrap", children: renderMarkdown(message.content) }) })) : null, _jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, borderStyle: "round", borderColor: BRAND_RED, paddingX: 1, width: boxWidth, overflow: "hidden", children: [_jsx(Text, { color: BRAND_RED, bold: true, children: "⚡ Tool Calls" }), message.toolCalls?.map((tc, i) => {
|
|
38
53
|
let display = tc.arguments;
|
|
39
54
|
try {
|
|
40
55
|
const args = JSON.parse(tc.arguments);
|
|
@@ -74,60 +89,166 @@ function WelcomeHint({ columns }) {
|
|
|
74
89
|
return (_jsxs(Box, { flexDirection: "column", paddingX: 2, marginTop: 1, marginBottom: 1, width: columns, children: [_jsx(Text, { dimColor: true, children: "Type a message below to get started." }), _jsx(Text, { dimColor: true, children: "CasAbot will orchestrate agents to help you." })] }));
|
|
75
90
|
}
|
|
76
91
|
function ProcessingIndicator({ columns }) {
|
|
77
|
-
return (_jsxs(Box, { paddingX: 2, marginTop: 1, gap: 1, width: columns, children: [_jsx(Text, { color:
|
|
92
|
+
return (_jsxs(Box, { paddingX: 2, marginTop: 1, gap: 1, width: columns, children: [_jsx(Text, { color: BRAND_BLUE, children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: BRAND_BLUE, children: "Thinking…" })] }));
|
|
78
93
|
}
|
|
79
|
-
function
|
|
94
|
+
function HistoryBrowser({ columns, currentId, onSelect, onBack, }) {
|
|
95
|
+
const [conversations, setConversations] = useState([]);
|
|
96
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
97
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
listConversations()
|
|
100
|
+
.then((convs) => {
|
|
101
|
+
setConversations(convs);
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
})
|
|
104
|
+
.catch(() => {
|
|
105
|
+
setIsLoading(false);
|
|
106
|
+
});
|
|
107
|
+
}, []);
|
|
108
|
+
useInput((ch, key) => {
|
|
109
|
+
if (key.escape) {
|
|
110
|
+
onBack();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (key.return && conversations.length > 0) {
|
|
114
|
+
onSelect(conversations[selectedIndex]);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (key.upArrow) {
|
|
118
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
119
|
+
}
|
|
120
|
+
if (key.downArrow && conversations.length > 0) {
|
|
121
|
+
setSelectedIndex((prev) => Math.min(conversations.length - 1, prev + 1));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
const boxWidth = Math.max(columns - 4, 10);
|
|
125
|
+
return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, width: columns, children: [_jsx(Box, { paddingX: 2, children: _jsx(Gradient, { colors: [BRAND_RED, BRAND_BLUE], children: _jsx(Text, { bold: true, children: "📋 Session History" }) }) }), _jsx(HRule, { columns: columns }), isLoading ? (_jsxs(Box, { paddingX: 2, marginTop: 1, gap: 1, children: [_jsx(Text, { color: BRAND_BLUE, children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { color: BRAND_BLUE, children: "Loading sessions…" })] })) : conversations.length === 0 ? (_jsx(Box, { paddingX: 2, marginTop: 1, children: _jsx(Text, { dimColor: true, children: "No previous sessions found." }) })) : (_jsx(Box, { flexDirection: "column", marginX: 2, marginTop: 1, borderStyle: "round", borderColor: "gray", paddingX: 1, paddingY: 1, width: boxWidth, overflow: "hidden", children: conversations.map((conv, i) => {
|
|
126
|
+
const isSelected = i === selectedIndex;
|
|
127
|
+
const isCurrent = conv.id === currentId;
|
|
128
|
+
const dateStr = formatDate(conv.startedAt);
|
|
129
|
+
const preview = getPreview(conv, Math.max(boxWidth - dateStr.length - 12, 20));
|
|
130
|
+
const msgCount = conv.messages.filter((m) => m.role === "user").length;
|
|
131
|
+
return (_jsxs(Box, { width: boxWidth - 4, children: [_jsx(Text, { color: isSelected ? BRAND_BLUE : undefined, bold: isSelected, dimColor: !isSelected, children: isSelected ? " ▶ " : " " }), _jsx(Text, { color: isSelected ? BRAND_BLUE : undefined, bold: isSelected, dimColor: !isSelected, children: dateStr }), _jsx(Text, { dimColor: !isSelected, children: " │ " }), _jsx(Text, { color: isSelected ? "white" : undefined, dimColor: !isSelected, wrap: "truncate", children: preview }), isCurrent ? (_jsx(Text, { color: BRAND_RED, bold: true, children: " (current)" })) : null, _jsx(Text, { dimColor: true, children: ` [${msgCount}]` })] }, conv.id));
|
|
132
|
+
}) })), _jsx(HRule, { columns: columns }), _jsxs(Box, { paddingX: 2, width: columns, justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: "↑↓ navigate Enter select ESC back" }), _jsx(Text, { dimColor: true, children: `${conversations.length} sessions` })] })] }));
|
|
133
|
+
}
|
|
134
|
+
function App({ provider, conversation: initialConversation, skills, }) {
|
|
80
135
|
const [messages, setMessages] = useState([]);
|
|
81
136
|
const [input, setInput] = useState("");
|
|
82
137
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
138
|
+
const [mode, setMode] = useState("chat");
|
|
139
|
+
const conversationRef = useRef(initialConversation);
|
|
140
|
+
const abortControllerRef = useRef(null);
|
|
83
141
|
const { exit } = useApp();
|
|
84
142
|
const { stdout } = useStdout();
|
|
85
143
|
const columns = stdout.columns ?? 80;
|
|
144
|
+
const handleCancel = useCallback(() => {
|
|
145
|
+
if (abortControllerRef.current) {
|
|
146
|
+
abortControllerRef.current.abort();
|
|
147
|
+
}
|
|
148
|
+
}, []);
|
|
86
149
|
const handleSubmit = useCallback(async (text) => {
|
|
87
150
|
const trimmed = text.trim();
|
|
88
151
|
if (!trimmed || isProcessing)
|
|
89
152
|
return;
|
|
153
|
+
if (trimmed === "/new") {
|
|
154
|
+
setInput("");
|
|
155
|
+
if (conversationRef.current.messages.length > 0) {
|
|
156
|
+
await saveConversation(conversationRef.current);
|
|
157
|
+
}
|
|
158
|
+
process.stdout.write("\x1Bc");
|
|
159
|
+
const newConv = createConversation();
|
|
160
|
+
conversationRef.current = newConv;
|
|
161
|
+
setMessages([]);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (trimmed === "/history") {
|
|
165
|
+
setInput("");
|
|
166
|
+
if (conversationRef.current.messages.length > 0) {
|
|
167
|
+
await saveConversation(conversationRef.current);
|
|
168
|
+
}
|
|
169
|
+
setMode("history");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
90
172
|
setInput("");
|
|
91
173
|
setIsProcessing(true);
|
|
92
174
|
const userMsg = { role: "user", content: trimmed };
|
|
93
175
|
setMessages((prev) => [...prev, userMsg]);
|
|
176
|
+
const controller = new AbortController();
|
|
177
|
+
abortControllerRef.current = controller;
|
|
94
178
|
try {
|
|
95
|
-
const generator = runAgent(provider, trimmed,
|
|
179
|
+
const generator = runAgent(provider, trimmed, conversationRef.current, skills, controller.signal);
|
|
96
180
|
for await (const msg of generator) {
|
|
181
|
+
if (controller.signal.aborted)
|
|
182
|
+
break;
|
|
97
183
|
setMessages((prev) => [...prev, msg]);
|
|
98
184
|
}
|
|
99
185
|
}
|
|
100
186
|
catch (err) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
187
|
+
if (!controller.signal.aborted) {
|
|
188
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
189
|
+
setMessages((prev) => [
|
|
190
|
+
...prev,
|
|
191
|
+
{ role: "assistant", content: `❌ Error: ${errorMsg}` },
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (controller.signal.aborted) {
|
|
196
|
+
const cancelMsg = {
|
|
197
|
+
role: "assistant",
|
|
198
|
+
content: "⏹ Cancelled.",
|
|
199
|
+
};
|
|
200
|
+
setMessages((prev) => [...prev, cancelMsg]);
|
|
201
|
+
await appendMessage(conversationRef.current, cancelMsg);
|
|
106
202
|
}
|
|
203
|
+
abortControllerRef.current = null;
|
|
107
204
|
setIsProcessing(false);
|
|
108
|
-
}, [isProcessing, provider,
|
|
205
|
+
}, [isProcessing, provider, skills]);
|
|
206
|
+
const handleHistorySelect = useCallback(async (conv) => {
|
|
207
|
+
if (conversationRef.current.messages.length > 0) {
|
|
208
|
+
await saveConversation(conversationRef.current);
|
|
209
|
+
}
|
|
210
|
+
process.stdout.write("\x1Bc");
|
|
211
|
+
conversationRef.current = conv;
|
|
212
|
+
setMessages([...conv.messages]);
|
|
213
|
+
setMode("chat");
|
|
214
|
+
}, []);
|
|
109
215
|
useInput((ch, key) => {
|
|
110
|
-
if (
|
|
111
|
-
|
|
216
|
+
if (mode !== "chat")
|
|
217
|
+
return;
|
|
218
|
+
if (isProcessing) {
|
|
219
|
+
if (key.escape || (key.ctrl && ch === "c")) {
|
|
220
|
+
handleCancel();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
if (key.ctrl && ch === "c") {
|
|
225
|
+
exit();
|
|
226
|
+
}
|
|
112
227
|
}
|
|
113
228
|
});
|
|
114
229
|
const userCount = messages.filter((m) => m.role === "user").length;
|
|
230
|
+
const convId = conversationRef.current.id;
|
|
115
231
|
const items = useMemo(() => [
|
|
116
|
-
{ key:
|
|
232
|
+
{ key: `header-${convId}`, type: "header" },
|
|
117
233
|
...messages.map((msg, i) => ({
|
|
118
|
-
key:
|
|
234
|
+
key: `${convId}-msg-${i}`,
|
|
119
235
|
type: "message",
|
|
120
236
|
message: msg,
|
|
121
237
|
})),
|
|
122
|
-
], [messages]);
|
|
238
|
+
], [messages, convId]);
|
|
239
|
+
if (mode === "history") {
|
|
240
|
+
return (_jsx(HistoryBrowser, { columns: columns, currentId: conversationRef.current.id, onSelect: handleHistorySelect, onBack: () => setMode("chat") }));
|
|
241
|
+
}
|
|
123
242
|
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(Static, { items: items, children: (item) => {
|
|
124
243
|
if (item.type === "header") {
|
|
125
244
|
return (_jsx(Box, { flexDirection: "column", width: columns, children: _jsx(HeaderBlock, { columns: columns }) }, item.key));
|
|
126
245
|
}
|
|
127
246
|
return (_jsx(Box, { flexDirection: "column", width: columns, children: _jsx(MessageView, { message: item.message, columns: columns }) }, item.key));
|
|
128
|
-
} }), messages.length === 0 && !isProcessing && _jsx(WelcomeHint, { columns: columns }), isProcessing && _jsx(ProcessingIndicator, { columns: columns }), _jsx(HRule, { columns: columns }), _jsx(Box, { paddingX: 1, width: columns, children: _jsxs(Box, { borderStyle: "round", borderColor: isProcessing ? "gray" :
|
|
247
|
+
} }), messages.length === 0 && !isProcessing && _jsx(WelcomeHint, { columns: columns }), isProcessing && _jsx(ProcessingIndicator, { columns: columns }), _jsx(HRule, { columns: columns }), _jsx(Box, { paddingX: 1, width: columns, children: _jsxs(Box, { borderStyle: "round", borderColor: isProcessing ? "gray" : BRAND_BLUE, paddingX: 1, width: Math.max(columns - 2, 10), overflow: "hidden", children: [_jsx(Text, { color: BRAND_BLUE, bold: true, children: "❯ " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: (val) => {
|
|
129
248
|
handleSubmit(val).catch(() => { });
|
|
130
|
-
}, placeholder: "Type your message\u2026", focus: !isProcessing, showCursor: true })] }) }), _jsxs(Box, { paddingX: 2, width: columns, justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children:
|
|
249
|
+
}, placeholder: "Type your message\u2026", focus: !isProcessing, showCursor: true })] }) }), _jsxs(Box, { paddingX: 2, width: columns, justifyContent: "space-between", children: [_jsx(Text, { dimColor: true, children: isProcessing
|
|
250
|
+
? "ESC / Ctrl+C cancel"
|
|
251
|
+
: "/new /history Ctrl+C exit" }), _jsxs(Text, { dimColor: true, children: [userCount, " ", userCount === 1 ? "message" : "messages"] })] })] }));
|
|
131
252
|
}
|
|
132
253
|
export function startTUI(provider, conversation, skills) {
|
|
133
254
|
render(_jsx(App, { provider: provider, conversation: conversation, skills: skills }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "casabot",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.11",
|
|
4
4
|
"description": "CasAbot — Skill-driven multi-agent orchestrator system",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"license": "Apache-2.0",
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
31
|
+
"asciify-image": "^0.1.10",
|
|
31
32
|
"chalk": "^5.4.1",
|
|
32
33
|
"commander": "^13.1.0",
|
|
33
34
|
"gray-matter": "^4.0.3",
|
package/skills/agent/SKILL.md
CHANGED
|
@@ -17,15 +17,62 @@ This manual explains how the base agent creates and manages podman-based sub-age
|
|
|
17
17
|
|
|
18
18
|
podman must be installed before creating sub-agents.
|
|
19
19
|
|
|
20
|
+
### Step 1: Check if podman is already installed
|
|
21
|
+
|
|
20
22
|
```bash
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
which podman && podman --version
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If podman is found, skip to Section 2.
|
|
27
|
+
|
|
28
|
+
### Step 2: Gather requirements from the user
|
|
29
|
+
|
|
30
|
+
> **Important:** Before installing podman, you **must** ask the user the following questions. Do not assume or proceed without their answers.
|
|
31
|
+
|
|
32
|
+
1. **Detect or ask the distro:**
|
|
33
|
+
```bash
|
|
34
|
+
cat /etc/os-release
|
|
35
|
+
```
|
|
36
|
+
If the distro cannot be determined, ask the user: *"Which Linux distribution are you using? (e.g. Ubuntu, Fedora, Arch, Debian, RHEL, etc.)"*
|
|
23
37
|
|
|
24
|
-
|
|
38
|
+
2. **Ask about sudo privileges:**
|
|
39
|
+
*"Do you have sudo (root) privileges on this system?"*
|
|
40
|
+
— If the user does not have sudo, guide them to request it from an administrator, or suggest rootless podman setup if possible.
|
|
41
|
+
|
|
42
|
+
3. **Ask about rootless mode:**
|
|
43
|
+
*"Would you like to run podman in rootless mode (recommended for security)?"*
|
|
44
|
+
|
|
45
|
+
4. **Ask about special requirements:**
|
|
46
|
+
*"Do you have any specific requirements? (e.g. a particular podman version, a custom storage location, proxy settings, etc.)"*
|
|
47
|
+
|
|
48
|
+
### Step 3: Install podman based on user's answers
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Debian / Ubuntu
|
|
25
52
|
sudo apt update && sudo apt install -y podman
|
|
26
53
|
|
|
27
|
-
#
|
|
54
|
+
# Fedora / RHEL / CentOS
|
|
28
55
|
sudo dnf install -y podman
|
|
56
|
+
|
|
57
|
+
# Arch Linux
|
|
58
|
+
sudo pacman -S podman
|
|
59
|
+
|
|
60
|
+
# openSUSE
|
|
61
|
+
sudo zypper install -y podman
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
If the user requested rootless mode, also configure subuid/subgid:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)
|
|
68
|
+
podman system migrate
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Step 4: Verify installation
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
podman --version
|
|
75
|
+
podman info
|
|
29
76
|
```
|
|
30
77
|
|
|
31
78
|
## 2. Configure podman storage
|
package/src/agent/base.ts
CHANGED
|
@@ -6,6 +6,22 @@ import { formatSkillsForPrompt } from "../skills/loader.js";
|
|
|
6
6
|
import { CASABOT_HOME } from "../config/manager.js";
|
|
7
7
|
|
|
8
8
|
const MAX_ITERATIONS = 20;
|
|
9
|
+
const MAX_RETRIES = 3;
|
|
10
|
+
const RETRY_BASE_DELAY_MS = 2000;
|
|
11
|
+
|
|
12
|
+
function raceAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
13
|
+
if (signal.aborted) return Promise.reject(new Error("AbortError"));
|
|
14
|
+
return Promise.race([
|
|
15
|
+
promise,
|
|
16
|
+
new Promise<never>((_, reject) => {
|
|
17
|
+
signal.addEventListener(
|
|
18
|
+
"abort",
|
|
19
|
+
() => reject(new Error("AbortError")),
|
|
20
|
+
{ once: true },
|
|
21
|
+
);
|
|
22
|
+
}),
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
9
25
|
|
|
10
26
|
export function buildSystemPrompt(skills: Skill[]): string {
|
|
11
27
|
const skillList = formatSkillsForPrompt(skills);
|
|
@@ -44,6 +60,7 @@ export async function* runAgent(
|
|
|
44
60
|
userMessage: string,
|
|
45
61
|
conversation: ConversationHistory,
|
|
46
62
|
skills: Skill[],
|
|
63
|
+
signal: AbortSignal,
|
|
47
64
|
): AsyncGenerator<Message> {
|
|
48
65
|
const systemPrompt = buildSystemPrompt(skills);
|
|
49
66
|
|
|
@@ -53,11 +70,34 @@ export async function* runAgent(
|
|
|
53
70
|
const tools = [TERMINAL_TOOL];
|
|
54
71
|
|
|
55
72
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
|
73
|
+
if (signal.aborted) return;
|
|
74
|
+
|
|
56
75
|
const messagesWithSystem: Message[] = [
|
|
57
76
|
{ role: "system", content: systemPrompt },
|
|
58
77
|
...conversation.messages,
|
|
59
78
|
];
|
|
60
|
-
|
|
79
|
+
|
|
80
|
+
let assistantMsg: Message;
|
|
81
|
+
for (let attempt = 0; ; attempt++) {
|
|
82
|
+
try {
|
|
83
|
+
assistantMsg = await raceAbort(provider.chat(messagesWithSystem, tools), signal);
|
|
84
|
+
break;
|
|
85
|
+
} catch (err: unknown) {
|
|
86
|
+
if (signal.aborted) return;
|
|
87
|
+
if (attempt >= MAX_RETRIES) throw err;
|
|
88
|
+
|
|
89
|
+
const delay = RETRY_BASE_DELAY_MS * 2 ** attempt;
|
|
90
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
91
|
+
const retryMsg: Message = {
|
|
92
|
+
role: "assistant",
|
|
93
|
+
content: `⏳ API request failed: ${errorMsg}. Retrying in ${delay}ms... (${attempt + 1}/${MAX_RETRIES})`,
|
|
94
|
+
};
|
|
95
|
+
yield retryMsg;
|
|
96
|
+
|
|
97
|
+
await raceAbort(new Promise((resolve) => setTimeout(resolve, delay)), signal);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
61
101
|
await appendMessage(conversation, assistantMsg);
|
|
62
102
|
yield assistantMsg;
|
|
63
103
|
|
|
@@ -66,6 +106,8 @@ export async function* runAgent(
|
|
|
66
106
|
}
|
|
67
107
|
|
|
68
108
|
for (const toolCall of assistantMsg.toolCalls) {
|
|
109
|
+
if (signal.aborted) return;
|
|
110
|
+
|
|
69
111
|
let result: string;
|
|
70
112
|
|
|
71
113
|
if (toolCall.name === "run_command") {
|
package/src/cli/index.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { createRequire } from "module";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { dirname, join } from "path";
|
|
3
6
|
import { Command } from "commander";
|
|
4
7
|
import { loadConfig, saveConfig, getDefaultConfig, ensureDirectories } from "../config/manager.js";
|
|
5
8
|
import { createProvider } from "../providers/index.js";
|
|
@@ -8,6 +11,28 @@ import { createConversation } from "../history/store.js";
|
|
|
8
11
|
import { startTUI } from "../tui/app.js";
|
|
9
12
|
import { setupWizard } from "./setup.js";
|
|
10
13
|
|
|
14
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
|
|
16
|
+
async function displayLogo(): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const asciify = require("asciify-image") as (
|
|
20
|
+
path: string,
|
|
21
|
+
opts: Record<string, unknown>,
|
|
22
|
+
) => Promise<string>;
|
|
23
|
+
const logoPath = join(__dirname, "..", "..", "Logo2.png");
|
|
24
|
+
const ascii = await asciify(logoPath, {
|
|
25
|
+
fit: "box",
|
|
26
|
+
width: "50%",
|
|
27
|
+
color: true,
|
|
28
|
+
});
|
|
29
|
+
console.log(ascii);
|
|
30
|
+
console.log("");
|
|
31
|
+
} catch {
|
|
32
|
+
// Logo display is non-critical; silently skip on failure
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
11
36
|
const program = new Command();
|
|
12
37
|
|
|
13
38
|
program
|
|
@@ -70,6 +95,7 @@ program
|
|
|
70
95
|
const skills = await loadSkills();
|
|
71
96
|
const conversation = createConversation();
|
|
72
97
|
|
|
98
|
+
await displayLogo();
|
|
73
99
|
startTUI(provider, conversation, skills);
|
|
74
100
|
} catch (err: unknown) {
|
|
75
101
|
const msg = err instanceof Error ? err.message : String(err);
|
package/src/tui/app.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useCallback, useMemo } from "react";
|
|
1
|
+
import React, { useState, useCallback, useMemo, useRef, useEffect } from "react";
|
|
2
2
|
import { render, Box, Text, Static, useInput, useApp, useStdout } from "ink";
|
|
3
3
|
import TextInput from "ink-text-input";
|
|
4
4
|
import Spinner from "ink-spinner";
|
|
@@ -9,6 +9,16 @@ import type { ChatProvider } from "../providers/base.js";
|
|
|
9
9
|
import type { ConversationHistory, Message, Skill } from "../config/types.js";
|
|
10
10
|
import { runAgent } from "../agent/base.js";
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
createConversation,
|
|
14
|
+
listConversations,
|
|
15
|
+
saveConversation,
|
|
16
|
+
appendMessage,
|
|
17
|
+
} from "../history/store.js";
|
|
18
|
+
|
|
19
|
+
const BRAND_BLUE = "#2BADEE";
|
|
20
|
+
const BRAND_RED = "#EA3A23";
|
|
21
|
+
|
|
12
22
|
function renderMarkdown(content: string): string {
|
|
13
23
|
const width = Math.max((process.stdout.columns ?? 80) - 8, 40);
|
|
14
24
|
const md = new Marked({ gfm: true });
|
|
@@ -25,6 +35,19 @@ function truncateOutput(content: string, maxLines = 8): string {
|
|
|
25
35
|
);
|
|
26
36
|
}
|
|
27
37
|
|
|
38
|
+
function formatDate(iso: string): string {
|
|
39
|
+
const d = new Date(iso);
|
|
40
|
+
const pad = (n: number) => String(n).padStart(2, "0");
|
|
41
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getPreview(conv: ConversationHistory, maxLen = 50): string {
|
|
45
|
+
const firstUser = conv.messages.find((m) => m.role === "user");
|
|
46
|
+
if (!firstUser) return "(empty session)";
|
|
47
|
+
const text = firstUser.content.replace(/\n/g, " ").trim();
|
|
48
|
+
return text.length > maxLen ? text.slice(0, maxLen - 1) + "…" : text;
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
function HRule({ columns }: { columns: number }): React.ReactElement {
|
|
29
52
|
return (
|
|
30
53
|
<Box paddingX={1} width={columns}>
|
|
@@ -37,7 +60,7 @@ function HeaderBlock({ columns }: { columns: number }): React.ReactElement {
|
|
|
37
60
|
return (
|
|
38
61
|
<Box flexDirection="column" paddingTop={1} width={columns}>
|
|
39
62
|
<Box paddingX={2}>
|
|
40
|
-
<Gradient
|
|
63
|
+
<Gradient colors={[BRAND_RED, BRAND_BLUE]}>
|
|
41
64
|
<Text bold>{"✦ CasAbot"}</Text>
|
|
42
65
|
</Gradient>
|
|
43
66
|
</Box>
|
|
@@ -54,7 +77,7 @@ function HeaderBlock({ columns }: { columns: number }): React.ReactElement {
|
|
|
54
77
|
function UserMessageView({ content, columns }: { content: string; columns: number }): React.ReactElement {
|
|
55
78
|
return (
|
|
56
79
|
<Box flexDirection="column" paddingX={2} marginTop={1} width={columns}>
|
|
57
|
-
<Text color=
|
|
80
|
+
<Text color={BRAND_RED} bold>
|
|
58
81
|
{"▶ You"}
|
|
59
82
|
</Text>
|
|
60
83
|
<Box marginLeft={2} width={Math.max(columns - 6, 10)}>
|
|
@@ -73,7 +96,7 @@ function AssistantMessageView({
|
|
|
73
96
|
}): React.ReactElement {
|
|
74
97
|
return (
|
|
75
98
|
<Box flexDirection="column" paddingX={2} marginTop={1} width={columns}>
|
|
76
|
-
<Text color=
|
|
99
|
+
<Text color={BRAND_BLUE} bold>
|
|
77
100
|
{"✦ CasAbot"}
|
|
78
101
|
</Text>
|
|
79
102
|
<Box marginLeft={2} width={Math.max(columns - 6, 10)}>
|
|
@@ -93,7 +116,7 @@ function ToolCallsView({
|
|
|
93
116
|
const boxWidth = Math.max(columns - 6, 10);
|
|
94
117
|
return (
|
|
95
118
|
<Box flexDirection="column" paddingX={2} marginTop={1} width={columns}>
|
|
96
|
-
<Text color=
|
|
119
|
+
<Text color={BRAND_BLUE} bold>
|
|
97
120
|
{"✦ CasAbot"}
|
|
98
121
|
</Text>
|
|
99
122
|
{message.content ? (
|
|
@@ -106,12 +129,12 @@ function ToolCallsView({
|
|
|
106
129
|
marginLeft={2}
|
|
107
130
|
marginTop={1}
|
|
108
131
|
borderStyle="round"
|
|
109
|
-
borderColor=
|
|
132
|
+
borderColor={BRAND_RED}
|
|
110
133
|
paddingX={1}
|
|
111
134
|
width={boxWidth}
|
|
112
135
|
overflow="hidden"
|
|
113
136
|
>
|
|
114
|
-
<Text color=
|
|
137
|
+
<Text color={BRAND_RED} bold>
|
|
115
138
|
{"⚡ Tool Calls"}
|
|
116
139
|
</Text>
|
|
117
140
|
{message.toolCalls?.map((tc, i) => {
|
|
@@ -200,10 +223,143 @@ function WelcomeHint({ columns }: { columns: number }): React.ReactElement {
|
|
|
200
223
|
function ProcessingIndicator({ columns }: { columns: number }): React.ReactElement {
|
|
201
224
|
return (
|
|
202
225
|
<Box paddingX={2} marginTop={1} gap={1} width={columns}>
|
|
203
|
-
<Text color=
|
|
226
|
+
<Text color={BRAND_BLUE}>
|
|
204
227
|
<Spinner type="dots" />
|
|
205
228
|
</Text>
|
|
206
|
-
<Text color=
|
|
229
|
+
<Text color={BRAND_BLUE}>{"Thinking…"}</Text>
|
|
230
|
+
</Box>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface HistoryBrowserProps {
|
|
235
|
+
columns: number;
|
|
236
|
+
currentId: string;
|
|
237
|
+
onSelect: (conversation: ConversationHistory) => void;
|
|
238
|
+
onBack: () => void;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function HistoryBrowser({
|
|
242
|
+
columns,
|
|
243
|
+
currentId,
|
|
244
|
+
onSelect,
|
|
245
|
+
onBack,
|
|
246
|
+
}: HistoryBrowserProps): React.ReactElement {
|
|
247
|
+
const [conversations, setConversations] = useState<ConversationHistory[]>([]);
|
|
248
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
249
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
250
|
+
|
|
251
|
+
useEffect(() => {
|
|
252
|
+
listConversations()
|
|
253
|
+
.then((convs) => {
|
|
254
|
+
setConversations(convs);
|
|
255
|
+
setIsLoading(false);
|
|
256
|
+
})
|
|
257
|
+
.catch(() => {
|
|
258
|
+
setIsLoading(false);
|
|
259
|
+
});
|
|
260
|
+
}, []);
|
|
261
|
+
|
|
262
|
+
useInput((ch, key) => {
|
|
263
|
+
if (key.escape) {
|
|
264
|
+
onBack();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (key.return && conversations.length > 0) {
|
|
268
|
+
onSelect(conversations[selectedIndex]);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (key.upArrow) {
|
|
272
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
273
|
+
}
|
|
274
|
+
if (key.downArrow && conversations.length > 0) {
|
|
275
|
+
setSelectedIndex((prev) => Math.min(conversations.length - 1, prev + 1));
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const boxWidth = Math.max(columns - 4, 10);
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<Box flexDirection="column" paddingTop={1} width={columns}>
|
|
283
|
+
<Box paddingX={2}>
|
|
284
|
+
<Gradient colors={[BRAND_RED, BRAND_BLUE]}>
|
|
285
|
+
<Text bold>{"📋 Session History"}</Text>
|
|
286
|
+
</Gradient>
|
|
287
|
+
</Box>
|
|
288
|
+
|
|
289
|
+
<HRule columns={columns} />
|
|
290
|
+
|
|
291
|
+
{isLoading ? (
|
|
292
|
+
<Box paddingX={2} marginTop={1} gap={1}>
|
|
293
|
+
<Text color={BRAND_BLUE}>
|
|
294
|
+
<Spinner type="dots" />
|
|
295
|
+
</Text>
|
|
296
|
+
<Text color={BRAND_BLUE}>{"Loading sessions…"}</Text>
|
|
297
|
+
</Box>
|
|
298
|
+
) : conversations.length === 0 ? (
|
|
299
|
+
<Box paddingX={2} marginTop={1}>
|
|
300
|
+
<Text dimColor>{"No previous sessions found."}</Text>
|
|
301
|
+
</Box>
|
|
302
|
+
) : (
|
|
303
|
+
<Box
|
|
304
|
+
flexDirection="column"
|
|
305
|
+
marginX={2}
|
|
306
|
+
marginTop={1}
|
|
307
|
+
borderStyle="round"
|
|
308
|
+
borderColor="gray"
|
|
309
|
+
paddingX={1}
|
|
310
|
+
paddingY={1}
|
|
311
|
+
width={boxWidth}
|
|
312
|
+
overflow="hidden"
|
|
313
|
+
>
|
|
314
|
+
{conversations.map((conv, i) => {
|
|
315
|
+
const isSelected = i === selectedIndex;
|
|
316
|
+
const isCurrent = conv.id === currentId;
|
|
317
|
+
const dateStr = formatDate(conv.startedAt);
|
|
318
|
+
const preview = getPreview(conv, Math.max(boxWidth - dateStr.length - 12, 20));
|
|
319
|
+
const msgCount = conv.messages.filter((m) => m.role === "user").length;
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<Box key={conv.id} width={boxWidth - 4}>
|
|
323
|
+
<Text
|
|
324
|
+
color={isSelected ? BRAND_BLUE : undefined}
|
|
325
|
+
bold={isSelected}
|
|
326
|
+
dimColor={!isSelected}
|
|
327
|
+
>
|
|
328
|
+
{isSelected ? " ▶ " : " "}
|
|
329
|
+
</Text>
|
|
330
|
+
<Text
|
|
331
|
+
color={isSelected ? BRAND_BLUE : undefined}
|
|
332
|
+
bold={isSelected}
|
|
333
|
+
dimColor={!isSelected}
|
|
334
|
+
>
|
|
335
|
+
{dateStr}
|
|
336
|
+
</Text>
|
|
337
|
+
<Text dimColor={!isSelected}>{" │ "}</Text>
|
|
338
|
+
<Text
|
|
339
|
+
color={isSelected ? "white" : undefined}
|
|
340
|
+
dimColor={!isSelected}
|
|
341
|
+
wrap="truncate"
|
|
342
|
+
>
|
|
343
|
+
{preview}
|
|
344
|
+
</Text>
|
|
345
|
+
{isCurrent ? (
|
|
346
|
+
<Text color={BRAND_RED} bold>{" (current)"}</Text>
|
|
347
|
+
) : null}
|
|
348
|
+
<Text dimColor>
|
|
349
|
+
{` [${msgCount}]`}
|
|
350
|
+
</Text>
|
|
351
|
+
</Box>
|
|
352
|
+
);
|
|
353
|
+
})}
|
|
354
|
+
</Box>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
<HRule columns={columns} />
|
|
358
|
+
|
|
359
|
+
<Box paddingX={2} width={columns} justifyContent="space-between">
|
|
360
|
+
<Text dimColor>{"↑↓ navigate Enter select ESC back"}</Text>
|
|
361
|
+
<Text dimColor>{`${conversations.length} sessions`}</Text>
|
|
362
|
+
</Box>
|
|
207
363
|
</Box>
|
|
208
364
|
);
|
|
209
365
|
}
|
|
@@ -212,6 +368,8 @@ type DisplayItem =
|
|
|
212
368
|
| { key: string; type: "header" }
|
|
213
369
|
| { key: string; type: "message"; message: Message };
|
|
214
370
|
|
|
371
|
+
type AppMode = "chat" | "history";
|
|
372
|
+
|
|
215
373
|
interface AppProps {
|
|
216
374
|
provider: ChatProvider;
|
|
217
375
|
conversation: ConversationHistory;
|
|
@@ -220,61 +378,149 @@ interface AppProps {
|
|
|
220
378
|
|
|
221
379
|
function App({
|
|
222
380
|
provider,
|
|
223
|
-
conversation,
|
|
381
|
+
conversation: initialConversation,
|
|
224
382
|
skills,
|
|
225
383
|
}: AppProps): React.ReactElement {
|
|
226
384
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
227
385
|
const [input, setInput] = useState("");
|
|
228
386
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
387
|
+
const [mode, setMode] = useState<AppMode>("chat");
|
|
388
|
+
const conversationRef = useRef<ConversationHistory>(initialConversation);
|
|
389
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
229
390
|
const { exit } = useApp();
|
|
230
391
|
const { stdout } = useStdout();
|
|
231
392
|
const columns = stdout.columns ?? 80;
|
|
232
393
|
|
|
394
|
+
const handleCancel = useCallback(() => {
|
|
395
|
+
if (abortControllerRef.current) {
|
|
396
|
+
abortControllerRef.current.abort();
|
|
397
|
+
}
|
|
398
|
+
}, []);
|
|
399
|
+
|
|
233
400
|
const handleSubmit = useCallback(
|
|
234
401
|
async (text: string) => {
|
|
235
402
|
const trimmed = text.trim();
|
|
236
403
|
if (!trimmed || isProcessing) return;
|
|
237
404
|
|
|
405
|
+
if (trimmed === "/new") {
|
|
406
|
+
setInput("");
|
|
407
|
+
if (conversationRef.current.messages.length > 0) {
|
|
408
|
+
await saveConversation(conversationRef.current);
|
|
409
|
+
}
|
|
410
|
+
process.stdout.write("\x1Bc");
|
|
411
|
+
const newConv = createConversation();
|
|
412
|
+
conversationRef.current = newConv;
|
|
413
|
+
setMessages([]);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (trimmed === "/history") {
|
|
418
|
+
setInput("");
|
|
419
|
+
if (conversationRef.current.messages.length > 0) {
|
|
420
|
+
await saveConversation(conversationRef.current);
|
|
421
|
+
}
|
|
422
|
+
setMode("history");
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
238
426
|
setInput("");
|
|
239
427
|
setIsProcessing(true);
|
|
240
428
|
|
|
241
429
|
const userMsg: Message = { role: "user", content: trimmed };
|
|
242
430
|
setMessages((prev) => [...prev, userMsg]);
|
|
243
431
|
|
|
432
|
+
const controller = new AbortController();
|
|
433
|
+
abortControllerRef.current = controller;
|
|
434
|
+
|
|
244
435
|
try {
|
|
245
|
-
const generator = runAgent(
|
|
436
|
+
const generator = runAgent(
|
|
437
|
+
provider,
|
|
438
|
+
trimmed,
|
|
439
|
+
conversationRef.current,
|
|
440
|
+
skills,
|
|
441
|
+
controller.signal,
|
|
442
|
+
);
|
|
246
443
|
for await (const msg of generator) {
|
|
444
|
+
if (controller.signal.aborted) break;
|
|
247
445
|
setMessages((prev) => [...prev, msg]);
|
|
248
446
|
}
|
|
249
447
|
} catch (err: unknown) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
448
|
+
if (!controller.signal.aborted) {
|
|
449
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
450
|
+
setMessages((prev) => [
|
|
451
|
+
...prev,
|
|
452
|
+
{ role: "assistant", content: `❌ Error: ${errorMsg}` },
|
|
453
|
+
]);
|
|
454
|
+
}
|
|
255
455
|
}
|
|
256
456
|
|
|
457
|
+
if (controller.signal.aborted) {
|
|
458
|
+
const cancelMsg: Message = {
|
|
459
|
+
role: "assistant",
|
|
460
|
+
content: "⏹ Cancelled.",
|
|
461
|
+
};
|
|
462
|
+
setMessages((prev) => [...prev, cancelMsg]);
|
|
463
|
+
await appendMessage(conversationRef.current, cancelMsg);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
abortControllerRef.current = null;
|
|
257
467
|
setIsProcessing(false);
|
|
258
468
|
},
|
|
259
|
-
[isProcessing, provider,
|
|
469
|
+
[isProcessing, provider, skills],
|
|
260
470
|
);
|
|
261
471
|
|
|
472
|
+
const handleHistorySelect = useCallback(async (conv: ConversationHistory) => {
|
|
473
|
+
if (conversationRef.current.messages.length > 0) {
|
|
474
|
+
await saveConversation(conversationRef.current);
|
|
475
|
+
}
|
|
476
|
+
process.stdout.write("\x1Bc");
|
|
477
|
+
conversationRef.current = conv;
|
|
478
|
+
setMessages([...conv.messages]);
|
|
479
|
+
setMode("chat");
|
|
480
|
+
}, []);
|
|
481
|
+
|
|
262
482
|
useInput((ch, key) => {
|
|
263
|
-
if (
|
|
264
|
-
|
|
483
|
+
if (mode !== "chat") return;
|
|
484
|
+
|
|
485
|
+
if (isProcessing) {
|
|
486
|
+
if (key.escape || (key.ctrl && ch === "c")) {
|
|
487
|
+
handleCancel();
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
if (key.ctrl && ch === "c") {
|
|
491
|
+
exit();
|
|
492
|
+
}
|
|
265
493
|
}
|
|
266
494
|
});
|
|
267
495
|
|
|
268
496
|
const userCount = messages.filter((m) => m.role === "user").length;
|
|
269
497
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
type: "
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
498
|
+
const convId = conversationRef.current.id;
|
|
499
|
+
|
|
500
|
+
const items = useMemo(
|
|
501
|
+
(): DisplayItem[] => [
|
|
502
|
+
{ key: `header-${convId}`, type: "header" },
|
|
503
|
+
...messages.map(
|
|
504
|
+
(msg, i): DisplayItem => ({
|
|
505
|
+
key: `${convId}-msg-${i}`,
|
|
506
|
+
type: "message",
|
|
507
|
+
message: msg,
|
|
508
|
+
}),
|
|
509
|
+
),
|
|
510
|
+
],
|
|
511
|
+
[messages, convId],
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
if (mode === "history") {
|
|
515
|
+
return (
|
|
516
|
+
<HistoryBrowser
|
|
517
|
+
columns={columns}
|
|
518
|
+
currentId={conversationRef.current.id}
|
|
519
|
+
onSelect={handleHistorySelect}
|
|
520
|
+
onBack={() => setMode("chat")}
|
|
521
|
+
/>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
278
524
|
|
|
279
525
|
return (
|
|
280
526
|
<Box flexDirection="column" width={columns}>
|
|
@@ -303,12 +549,12 @@ function App({
|
|
|
303
549
|
<Box paddingX={1} width={columns}>
|
|
304
550
|
<Box
|
|
305
551
|
borderStyle="round"
|
|
306
|
-
borderColor={isProcessing ? "gray" :
|
|
552
|
+
borderColor={isProcessing ? "gray" : BRAND_BLUE}
|
|
307
553
|
paddingX={1}
|
|
308
554
|
width={Math.max(columns - 2, 10)}
|
|
309
555
|
overflow="hidden"
|
|
310
556
|
>
|
|
311
|
-
<Text color=
|
|
557
|
+
<Text color={BRAND_BLUE} bold>
|
|
312
558
|
{"❯ "}
|
|
313
559
|
</Text>
|
|
314
560
|
<TextInput
|
|
@@ -325,7 +571,11 @@ function App({
|
|
|
325
571
|
</Box>
|
|
326
572
|
|
|
327
573
|
<Box paddingX={2} width={columns} justifyContent="space-between">
|
|
328
|
-
<Text dimColor>
|
|
574
|
+
<Text dimColor>
|
|
575
|
+
{isProcessing
|
|
576
|
+
? "ESC / Ctrl+C cancel"
|
|
577
|
+
: "/new /history Ctrl+C exit"}
|
|
578
|
+
</Text>
|
|
329
579
|
<Text dimColor>
|
|
330
580
|
{userCount} {userCount === 1 ? "message" : "messages"}
|
|
331
581
|
</Text>
|