@sofer_agent/cli 0.1.0 → 0.2.1
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/bin.js +27 -2
- package/dist/bin.js.map +1 -1
- package/dist/chat.d.ts +1 -1
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +10 -32
- package/dist/chat.js.map +1 -1
- package/dist/info.d.ts.map +1 -1
- package/dist/info.js +1 -1
- package/dist/info.js.map +1 -1
- package/dist/tui.d.ts +49 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +265 -0
- package/dist/tui.js.map +1 -0
- package/package.json +2 -2
- package/src/bin.ts +30 -2
- package/src/chat.ts +10 -34
- package/src/info.ts +7 -1
- package/src/tui.ts +295 -0
package/dist/bin.js
CHANGED
|
@@ -9,9 +9,27 @@ function printHelp() {
|
|
|
9
9
|
sofer chat with your agent (default)
|
|
10
10
|
sofer init mint the agent + provision Walrus memory
|
|
11
11
|
sofer info print on-chain agent state
|
|
12
|
+
sofer setup print the .env template to configure
|
|
12
13
|
sofer help show this help
|
|
13
14
|
|
|
14
|
-
Reads config + secrets from .env
|
|
15
|
+
Reads config + secrets from a .env file or the environment.`);
|
|
16
|
+
}
|
|
17
|
+
function printSetup() {
|
|
18
|
+
console.log(`# Copy this into a .env file in your project directory, then fill in:
|
|
19
|
+
# - ANTHROPIC_API_KEY (your Claude API key)
|
|
20
|
+
# - SOFER_SUI_SECRET_KEY (or let \`sofer init\` generate one)
|
|
21
|
+
#
|
|
22
|
+
# Then run: sofer init
|
|
23
|
+
|
|
24
|
+
# --- Sui chain ---
|
|
25
|
+
SUI_NETWORK=testnet
|
|
26
|
+
SUI_RPC_URL=https://fullnode.testnet.sui.io:443
|
|
27
|
+
SOFER_PACKAGE_ID=0xd5291ac6fd0de787f9fe0bc16c677ef5bafffd920936a05b14e7a8a142e46434
|
|
28
|
+
SOFER_AGENT_OBJECT_ID=
|
|
29
|
+
|
|
30
|
+
# --- Brain (Claude) ---
|
|
31
|
+
ANTHROPIC_API_KEY=
|
|
32
|
+
SOFER_MODEL=claude-opus-4-8`);
|
|
15
33
|
}
|
|
16
34
|
async function main() {
|
|
17
35
|
loadDotenv();
|
|
@@ -26,6 +44,9 @@ async function main() {
|
|
|
26
44
|
case "info":
|
|
27
45
|
await infoCommand();
|
|
28
46
|
break;
|
|
47
|
+
case "setup":
|
|
48
|
+
printSetup();
|
|
49
|
+
break;
|
|
29
50
|
case "help":
|
|
30
51
|
case "-h":
|
|
31
52
|
case "--help":
|
|
@@ -38,7 +59,11 @@ async function main() {
|
|
|
38
59
|
}
|
|
39
60
|
}
|
|
40
61
|
main().catch((e) => {
|
|
41
|
-
|
|
62
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
63
|
+
console.error(msg);
|
|
64
|
+
if (msg.includes("SOFER_PACKAGE_ID") || msg.includes("SOFER_SUI_SECRET_KEY") || msg.includes("ANTHROPIC_API_KEY")) {
|
|
65
|
+
console.error("\nFirst time? Run `sofer setup` for the .env template, then `sofer init`.");
|
|
66
|
+
}
|
|
42
67
|
process.exit(1);
|
|
43
68
|
});
|
|
44
69
|
//# sourceMappingURL=bin.js.map
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CACT
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CACT;;;;;;;;4DAQwD,CACzD,CAAC;AACJ,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,CAAC,GAAG,CACT;;;;;;;;;;;;;;4BAcwB,CACzB,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,UAAU,EAAE,CAAC;IACb,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC;IACtC,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,MAAM;YACT,MAAM,WAAW,EAAE,CAAC;YACpB,MAAM;QACR,KAAK,MAAM;YACT,MAAM,WAAW,EAAE,CAAC;YACpB,MAAM;QACR,KAAK,MAAM;YACT,MAAM,WAAW,EAAE,CAAC;YACpB,MAAM;QACR,KAAK,OAAO;YACV,UAAU,EAAE,CAAC;YACb,MAAM;QACR,KAAK,MAAM,CAAC;QACZ,KAAK,IAAI,CAAC;QACV,KAAK,QAAQ;YACX,SAAS,EAAE,CAAC;YACZ,MAAM;QACR;YACE,OAAO,CAAC,KAAK,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;YAC3C,SAAS,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACjB,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACvD,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,IAAI,GAAG,CAAC,QAAQ,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,sBAAsB,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC;QAClH,OAAO,CAAC,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC7F,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/chat.d.ts
CHANGED
package/dist/chat.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"AAIA,
|
|
1
|
+
{"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"AAIA,iDAAiD;AACjD,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAgBjD"}
|
package/dist/chat.js
CHANGED
|
@@ -1,43 +1,21 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { loadConfig, SoferAgent } from "@sofer_agent/core";
|
|
1
|
+
import { SoferAgent, loadConfig } from "@sofer_agent/core";
|
|
3
2
|
import { requireSecrets } from "./secrets.js";
|
|
4
|
-
|
|
3
|
+
import { SoferTui } from "./tui.js";
|
|
4
|
+
/** Interactive chat TUI with a stats sidebar. */
|
|
5
5
|
export async function chatCommand() {
|
|
6
6
|
const config = loadConfig();
|
|
7
7
|
const agent = await SoferAgent.create(config, requireSecrets());
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const rl = readline.createInterface({
|
|
11
|
-
input: process.stdin,
|
|
12
|
-
output: process.stdout,
|
|
13
|
-
prompt: "\x1b[36myou>\x1b[0m ",
|
|
14
|
-
});
|
|
15
|
-
const label = `\x1b[32m${agent.name.toLowerCase()}>\x1b[0m`;
|
|
16
|
-
rl.prompt();
|
|
17
|
-
rl.on("line", async (line) => {
|
|
18
|
-
const text = line.trim();
|
|
19
|
-
if (text === "/exit" || text === "/quit") {
|
|
20
|
-
rl.close();
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
if (!text) {
|
|
24
|
-
rl.prompt();
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
8
|
+
const tui = new SoferTui(agent, config);
|
|
9
|
+
await tui.run(async (line) => {
|
|
27
10
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
11
|
+
const { text, turnUsage } = await agent.chat(line);
|
|
12
|
+
tui.addMessage("agent", text);
|
|
13
|
+
tui.setUsage(turnUsage.inputTokens, turnUsage.outputTokens);
|
|
30
14
|
}
|
|
31
15
|
catch (e) {
|
|
32
|
-
|
|
16
|
+
tui.addMessage("error", e instanceof Error ? e.message : String(e));
|
|
33
17
|
}
|
|
34
|
-
|
|
35
|
-
});
|
|
36
|
-
await new Promise((resolve) => {
|
|
37
|
-
rl.on("close", () => {
|
|
38
|
-
console.log("bye.");
|
|
39
|
-
resolve();
|
|
40
|
-
});
|
|
18
|
+
tui.render();
|
|
41
19
|
});
|
|
42
20
|
}
|
|
43
21
|
//# sourceMappingURL=chat.js.map
|
package/dist/chat.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,iDAAiD;AACjD,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IAEhE,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAExC,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QAC3B,IAAI,CAAC;YACH,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACnD,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;YAC9B,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,WAAW,EAAE,SAAS,CAAC,YAAY,CAAC,CAAC;QAC9D,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,GAAG,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACtE,CAAC;QACD,GAAG,CAAC,MAAM,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/info.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"info.d.ts","sourceRoot":"","sources":["../src/info.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"info.d.ts","sourceRoot":"","sources":["../src/info.ts"],"names":[],"mappings":"AAQA,wCAAwC;AACxC,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CA0CjD"}
|
package/dist/info.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AgentClient, keypairFromSecret, loadConfig, makeSuiClient, suiBalance } from "@sofer_agent/core";
|
|
1
|
+
import { AgentClient, keypairFromSecret, loadConfig, makeSuiClient, suiBalance, } from "@sofer_agent/core";
|
|
2
2
|
/** Print the agent's on-chain state. */
|
|
3
3
|
export async function infoCommand() {
|
|
4
4
|
const config = loadConfig();
|
package/dist/info.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"info.js","sourceRoot":"","sources":["../src/info.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"info.js","sourceRoot":"","sources":["../src/info.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,UAAU,GACX,MAAM,mBAAmB,CAAC;AAE3B,wCAAwC;AACxC,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC;QACzC,OAAO;IACT,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,6DAA6D,CAAC,CAAC;QAC3E,OAAO;IACT,CAAC;IACD,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;IACtD,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACtD,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;QAChD,OAAO;IACT,CAAC;IAED,IAAI,OAA2B,CAAC;IAChC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAChD,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC;QAC7E,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;IACrD,CAAC;IAED,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,SAAS,CACZ;QACE,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,MAAM,CAAC,SAAS;QACzB,aAAa,EAAE,CAAC,CAAC,EAAE;QACnB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,WAAW,EAAE,CAAC,CAAC,KAAK;QACpB,aAAa,EAAE,CAAC,CAAC,aAAa;QAC9B,iBAAiB,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC;QACnD,OAAO;KACR,EACD,IAAI,EACJ,CAAC,CACF,CACF,CAAC;AACJ,CAAC"}
|
package/dist/tui.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { SoferConfig } from "@sofer_agent/core";
|
|
2
|
+
import type { SoferAgent } from "@sofer_agent/core";
|
|
3
|
+
interface ChatLine {
|
|
4
|
+
role: "you" | "agent" | "error";
|
|
5
|
+
text: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TuiState {
|
|
8
|
+
messages: ChatLine[];
|
|
9
|
+
lastTurnIn: number;
|
|
10
|
+
lastTurnOut: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class SoferTui {
|
|
13
|
+
private readonly agent;
|
|
14
|
+
private readonly config;
|
|
15
|
+
private state;
|
|
16
|
+
private inputBuf;
|
|
17
|
+
private cursor;
|
|
18
|
+
private rl;
|
|
19
|
+
private columns;
|
|
20
|
+
private rows;
|
|
21
|
+
private chatH;
|
|
22
|
+
private chatW;
|
|
23
|
+
private scrollOff;
|
|
24
|
+
private resolve;
|
|
25
|
+
private onLine;
|
|
26
|
+
constructor(agent: SoferAgent, config: SoferConfig);
|
|
27
|
+
get width(): number;
|
|
28
|
+
get height(): number;
|
|
29
|
+
private measure;
|
|
30
|
+
addMessage(role: ChatLine["role"], text: string): void;
|
|
31
|
+
setUsage(lastTurnIn: number, lastTurnOut: number): void;
|
|
32
|
+
/** Start the interactive TUI loop. Returns when the user exits. */
|
|
33
|
+
run(onLine: (line: string) => Promise<void>): Promise<void>;
|
|
34
|
+
private shutdown;
|
|
35
|
+
private handleInput;
|
|
36
|
+
private submit;
|
|
37
|
+
private handleResize;
|
|
38
|
+
/** Reset cursor, clear, redraw everything. */
|
|
39
|
+
render(): void;
|
|
40
|
+
private buildFrame;
|
|
41
|
+
private wrapLine;
|
|
42
|
+
private pad;
|
|
43
|
+
private blank;
|
|
44
|
+
private sidebarRow;
|
|
45
|
+
private buildSidebar;
|
|
46
|
+
private shortModel;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=tui.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAKpD,UAAU,QAAQ;IAChB,IAAI,EAAE,KAAK,GAAG,OAAO,GAAG,OAAO,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,QAAQ,EAAE,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,QAAQ;IAcjB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAdzB,OAAO,CAAC,KAAK,CAA6D;IAC1E,OAAO,CAAC,QAAQ,CAAM;IACtB,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,EAAE,CAAmC;IAC7C,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAAyC;gBAGpC,KAAK,EAAE,UAAU,EACjB,MAAM,EAAE,WAAW;IAGtC,IAAI,KAAK,WAER;IACD,IAAI,MAAM,WAET;IAED,OAAO,CAAC,OAAO;IAOf,UAAU,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAItD,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAKvD,mEAAmE;IAC7D,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAejE,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,WAAW,CAsCjB;YAEY,MAAM;IAoBpB,OAAO,CAAC,YAAY,CAGlB;IAEF,8CAA8C;IAC9C,MAAM,IAAI,IAAI;IAQd,OAAO,CAAC,UAAU;IA8ClB,OAAO,CAAC,QAAQ;IA0BhB,OAAO,CAAC,GAAG;IAQX,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,UAAU;IAelB,OAAO,CAAC,YAAY;IAwBpB,OAAO,CAAC,UAAU;CAQnB"}
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
const SIDEBAR_W = 28;
|
|
2
|
+
const MIN_CHAT_W = 30;
|
|
3
|
+
export class SoferTui {
|
|
4
|
+
agent;
|
|
5
|
+
config;
|
|
6
|
+
state = { messages: [], lastTurnIn: 0, lastTurnOut: 0 };
|
|
7
|
+
inputBuf = "";
|
|
8
|
+
cursor = 0;
|
|
9
|
+
rl = null;
|
|
10
|
+
columns = 80;
|
|
11
|
+
rows = 24;
|
|
12
|
+
chatH = 20;
|
|
13
|
+
chatW = 50;
|
|
14
|
+
scrollOff = 0;
|
|
15
|
+
resolve = null;
|
|
16
|
+
onLine = null;
|
|
17
|
+
constructor(agent, config) {
|
|
18
|
+
this.agent = agent;
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
get width() {
|
|
22
|
+
return this.columns;
|
|
23
|
+
}
|
|
24
|
+
get height() {
|
|
25
|
+
return this.rows;
|
|
26
|
+
}
|
|
27
|
+
measure() {
|
|
28
|
+
this.columns = process.stdout.columns ?? 80;
|
|
29
|
+
this.rows = process.stdout.rows ?? 24;
|
|
30
|
+
this.chatW = Math.max(MIN_CHAT_W, this.columns - SIDEBAR_W - 1);
|
|
31
|
+
this.chatH = this.rows - 3;
|
|
32
|
+
}
|
|
33
|
+
addMessage(role, text) {
|
|
34
|
+
this.state.messages.push({ role, text });
|
|
35
|
+
}
|
|
36
|
+
setUsage(lastTurnIn, lastTurnOut) {
|
|
37
|
+
this.state.lastTurnIn = lastTurnIn;
|
|
38
|
+
this.state.lastTurnOut = lastTurnOut;
|
|
39
|
+
}
|
|
40
|
+
/** Start the interactive TUI loop. Returns when the user exits. */
|
|
41
|
+
async run(onLine) {
|
|
42
|
+
this.onLine = onLine;
|
|
43
|
+
this.measure();
|
|
44
|
+
if (process.stdin.isTTY)
|
|
45
|
+
process.stdin.setRawMode?.(true);
|
|
46
|
+
process.stdin.on("data", this.handleInput);
|
|
47
|
+
process.stdout.on("resize", this.handleResize);
|
|
48
|
+
process.on("SIGWINCH", this.handleResize);
|
|
49
|
+
this.render();
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
this.resolve = resolve;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
shutdown() {
|
|
55
|
+
process.stdin.setRawMode?.(false);
|
|
56
|
+
process.stdin.removeAllListeners("data");
|
|
57
|
+
process.stdout.removeAllListeners("resize");
|
|
58
|
+
process.removeAllListeners("SIGWINCH");
|
|
59
|
+
this.render(); // final render in normal mode
|
|
60
|
+
process.stdout.write("\n");
|
|
61
|
+
this.resolve?.();
|
|
62
|
+
}
|
|
63
|
+
// ── Input handling ──────────────────────────────────────────────────────
|
|
64
|
+
handleInput = (data) => {
|
|
65
|
+
const str = data.toString();
|
|
66
|
+
for (const ch of str) {
|
|
67
|
+
const code = ch.charCodeAt(0);
|
|
68
|
+
if (code === 3) {
|
|
69
|
+
process.exit(0);
|
|
70
|
+
} // Ctrl-C
|
|
71
|
+
if (code === 4 && this.inputBuf.length === 0) {
|
|
72
|
+
this.shutdown();
|
|
73
|
+
return;
|
|
74
|
+
} // Ctrl-D on empty
|
|
75
|
+
if (code === 13) {
|
|
76
|
+
this.submit();
|
|
77
|
+
return;
|
|
78
|
+
} // Enter
|
|
79
|
+
if (code === 127) {
|
|
80
|
+
// Backspace
|
|
81
|
+
if (this.cursor > 0) {
|
|
82
|
+
this.inputBuf =
|
|
83
|
+
this.inputBuf.slice(0, this.cursor - 1) + this.inputBuf.slice(this.cursor);
|
|
84
|
+
this.cursor--;
|
|
85
|
+
}
|
|
86
|
+
this.render();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (code === 27 && this.inputBuf.length > 0) {
|
|
90
|
+
// Escape clears input
|
|
91
|
+
this.inputBuf = "";
|
|
92
|
+
this.cursor = 0;
|
|
93
|
+
this.render();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (ch >= " ") {
|
|
97
|
+
this.inputBuf = this.inputBuf.slice(0, this.cursor) + ch + this.inputBuf.slice(this.cursor);
|
|
98
|
+
this.cursor++;
|
|
99
|
+
this.render();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
async submit() {
|
|
104
|
+
const line = this.inputBuf.trim();
|
|
105
|
+
this.inputBuf = "";
|
|
106
|
+
this.cursor = 0;
|
|
107
|
+
if (!line) {
|
|
108
|
+
this.render();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (line === "/exit" || line === "/quit") {
|
|
112
|
+
this.shutdown();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.addMessage("you", line);
|
|
116
|
+
this.scrollOff = 0; // auto-scroll to latest
|
|
117
|
+
this.render();
|
|
118
|
+
await this.onLine?.(line);
|
|
119
|
+
}
|
|
120
|
+
// ── Terminal rendering ──────────────────────────────────────────────────
|
|
121
|
+
handleResize = () => {
|
|
122
|
+
this.measure();
|
|
123
|
+
this.render();
|
|
124
|
+
};
|
|
125
|
+
/** Reset cursor, clear, redraw everything. */
|
|
126
|
+
render() {
|
|
127
|
+
this.measure();
|
|
128
|
+
const out = process.stdout;
|
|
129
|
+
out.write("\x1b[?25l"); // hide cursor
|
|
130
|
+
out.write(this.buildFrame());
|
|
131
|
+
out.write("\x1b[?25h"); // show cursor
|
|
132
|
+
}
|
|
133
|
+
buildFrame() {
|
|
134
|
+
const { columns, rows, chatW, chatH } = this;
|
|
135
|
+
const rightW = columns - chatW - 1; // border column
|
|
136
|
+
let buf = "\x1b[H"; // cursor to top-left
|
|
137
|
+
// ── Body ──────────────────────────────────────────────────────────────
|
|
138
|
+
// We draw row by row. Each row: chat area | border | sidebar
|
|
139
|
+
const msgs = this.state.messages;
|
|
140
|
+
const wrappedLines = msgs.flatMap((m) => this.wrapLine(m, chatW));
|
|
141
|
+
const totalLines = wrappedLines.length;
|
|
142
|
+
const maxScroll = Math.max(0, totalLines - chatH);
|
|
143
|
+
if (this.scrollOff > maxScroll)
|
|
144
|
+
this.scrollOff = maxScroll;
|
|
145
|
+
if (this.scrollOff < 0)
|
|
146
|
+
this.scrollOff = 0;
|
|
147
|
+
const visible = wrappedLines.slice(this.scrollOff, this.scrollOff + chatH);
|
|
148
|
+
for (let r = 0; r < chatH; r++) {
|
|
149
|
+
const line = visible[r];
|
|
150
|
+
buf += line ? this.pad(line, chatW) : this.blank(chatW);
|
|
151
|
+
if (r === 0)
|
|
152
|
+
buf += "┬";
|
|
153
|
+
else if (r === chatH - 1)
|
|
154
|
+
buf += "┴";
|
|
155
|
+
else
|
|
156
|
+
buf += "│";
|
|
157
|
+
buf += this.sidebarRow(r, chatH, rightW);
|
|
158
|
+
buf += "\n";
|
|
159
|
+
}
|
|
160
|
+
// ── Divider ───────────────────────────────────────────────────────────
|
|
161
|
+
buf += `${"─".repeat(chatW)}┴${"─".repeat(rightW)}\n`;
|
|
162
|
+
// ── Input line ────────────────────────────────────────────────────────
|
|
163
|
+
const prefix = "\x1b[36myou>\x1b[0m ";
|
|
164
|
+
const prefixW = 4;
|
|
165
|
+
const maxInputW = columns - prefixW - 1;
|
|
166
|
+
let typed = this.inputBuf;
|
|
167
|
+
if (typed.length > maxInputW) {
|
|
168
|
+
const start = typed.length - maxInputW;
|
|
169
|
+
typed = typed.slice(start);
|
|
170
|
+
}
|
|
171
|
+
buf += `${prefix}${typed}\x1b[K`; // clear to end of line
|
|
172
|
+
// Place cursor at end of input
|
|
173
|
+
const cursorCol = prefixW + Math.min(this.cursor, maxInputW);
|
|
174
|
+
buf += `\x1b[${chatH + 2};${cursorCol}H`;
|
|
175
|
+
return buf;
|
|
176
|
+
}
|
|
177
|
+
wrapLine(msg, width) {
|
|
178
|
+
const label = msg.role === "you"
|
|
179
|
+
? "\x1b[36myou>\x1b[0m "
|
|
180
|
+
: msg.role === "agent"
|
|
181
|
+
? `\x1b[32m${this.agent.name.toLowerCase()}>\x1b[0m `
|
|
182
|
+
: "\x1b[31merror>\x1b[0m ";
|
|
183
|
+
const content = msg.text;
|
|
184
|
+
const full = label + content;
|
|
185
|
+
if (full.length <= width)
|
|
186
|
+
return [full];
|
|
187
|
+
// simple word-wrap
|
|
188
|
+
const lines = [];
|
|
189
|
+
let current = label;
|
|
190
|
+
const words = content.split(" ");
|
|
191
|
+
for (const word of words) {
|
|
192
|
+
if (current.length + 1 + word.length <= width) {
|
|
193
|
+
current += (current === label ? "" : " ") + word;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
if (current.length > label.length)
|
|
197
|
+
lines.push(current);
|
|
198
|
+
current = ` ${word}`; // indent continuation
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (current.length > label.length)
|
|
202
|
+
lines.push(current);
|
|
203
|
+
return lines.length > 0 ? lines : [full];
|
|
204
|
+
}
|
|
205
|
+
pad(s, w) {
|
|
206
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping
|
|
207
|
+
const ansiRe = /\x1b\[\d*;?\d*m/g;
|
|
208
|
+
const stripped = s.replace(ansiRe, "");
|
|
209
|
+
const padW = Math.max(0, w - stripped.length);
|
|
210
|
+
return s + " ".repeat(padW);
|
|
211
|
+
}
|
|
212
|
+
blank(w) {
|
|
213
|
+
return " ".repeat(w);
|
|
214
|
+
}
|
|
215
|
+
// ── Sidebar ─────────────────────────────────────────────────────────────
|
|
216
|
+
sidebarRow(row, total, width) {
|
|
217
|
+
const innerW = width - 2; // left & right padding
|
|
218
|
+
const lines = this.buildSidebar(innerW);
|
|
219
|
+
// center the sidebar content vertically
|
|
220
|
+
const gap = Math.max(0, total - lines.length);
|
|
221
|
+
const topPad = Math.floor(gap / 2);
|
|
222
|
+
const idx = row - topPad;
|
|
223
|
+
if (idx < 0 || idx >= lines.length)
|
|
224
|
+
return " ".repeat(width);
|
|
225
|
+
const line = lines[idx];
|
|
226
|
+
if (!line)
|
|
227
|
+
return " ".repeat(width);
|
|
228
|
+
return ` ${this.pad(line, innerW)} `;
|
|
229
|
+
}
|
|
230
|
+
buildSidebar(width) {
|
|
231
|
+
const { agent, config, state } = this;
|
|
232
|
+
const addr = `${agent.address.slice(0, 6)}...${agent.address.slice(-4)}`;
|
|
233
|
+
const totalIn = agent.usage.tokens.inputTokens;
|
|
234
|
+
const totalOut = agent.usage.tokens.outputTokens;
|
|
235
|
+
const cost = agent.usage.costUsd.toFixed(4);
|
|
236
|
+
return [
|
|
237
|
+
`\x1b[1m${agent.name}\x1b[0m`,
|
|
238
|
+
"─".repeat(width),
|
|
239
|
+
`Model: ${this.shortModel(config.brain.model)}`,
|
|
240
|
+
`Max out: ${config.brain.maxOutputTokens}`,
|
|
241
|
+
"",
|
|
242
|
+
`Turn in: ${state.lastTurnIn}`,
|
|
243
|
+
`Turn out: ${state.lastTurnOut}`,
|
|
244
|
+
`Total in: ${totalIn}`,
|
|
245
|
+
`Total out:${totalOut}`,
|
|
246
|
+
`Cost: \x1b[33m$${cost}\x1b[0m`,
|
|
247
|
+
"",
|
|
248
|
+
`Addr: ${addr}`,
|
|
249
|
+
`Network: ${config.network}`,
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
shortModel(model) {
|
|
253
|
+
const m = model.toLowerCase();
|
|
254
|
+
if (m.includes("sonnet"))
|
|
255
|
+
return "Sonnet 4";
|
|
256
|
+
if (m.includes("haiku"))
|
|
257
|
+
return "Haiku 3.5";
|
|
258
|
+
if (m.includes("opus"))
|
|
259
|
+
return "Opus 4";
|
|
260
|
+
if (m.includes("fable"))
|
|
261
|
+
return "Fable 5";
|
|
262
|
+
return model;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//# sourceMappingURL=tui.js.map
|
package/dist/tui.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui.js","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sofer_agent/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"sofer": "./dist/bin.js"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"cli"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@sofer_agent/core": "0.
|
|
28
|
+
"@sofer_agent/core": "0.2.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^22.10.2",
|
package/src/bin.ts
CHANGED
|
@@ -11,9 +11,30 @@ function printHelp(): void {
|
|
|
11
11
|
sofer chat with your agent (default)
|
|
12
12
|
sofer init mint the agent + provision Walrus memory
|
|
13
13
|
sofer info print on-chain agent state
|
|
14
|
+
sofer setup print the .env template to configure
|
|
14
15
|
sofer help show this help
|
|
15
16
|
|
|
16
|
-
Reads config + secrets from .env
|
|
17
|
+
Reads config + secrets from a .env file or the environment.`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function printSetup(): void {
|
|
22
|
+
console.log(
|
|
23
|
+
`# Copy this into a .env file in your project directory, then fill in:
|
|
24
|
+
# - ANTHROPIC_API_KEY (your Claude API key)
|
|
25
|
+
# - SOFER_SUI_SECRET_KEY (or let \`sofer init\` generate one)
|
|
26
|
+
#
|
|
27
|
+
# Then run: sofer init
|
|
28
|
+
|
|
29
|
+
# --- Sui chain ---
|
|
30
|
+
SUI_NETWORK=testnet
|
|
31
|
+
SUI_RPC_URL=https://fullnode.testnet.sui.io:443
|
|
32
|
+
SOFER_PACKAGE_ID=0xd5291ac6fd0de787f9fe0bc16c677ef5bafffd920936a05b14e7a8a142e46434
|
|
33
|
+
SOFER_AGENT_OBJECT_ID=
|
|
34
|
+
|
|
35
|
+
# --- Brain (Claude) ---
|
|
36
|
+
ANTHROPIC_API_KEY=
|
|
37
|
+
SOFER_MODEL=claude-opus-4-8`,
|
|
17
38
|
);
|
|
18
39
|
}
|
|
19
40
|
|
|
@@ -30,6 +51,9 @@ async function main(): Promise<void> {
|
|
|
30
51
|
case "info":
|
|
31
52
|
await infoCommand();
|
|
32
53
|
break;
|
|
54
|
+
case "setup":
|
|
55
|
+
printSetup();
|
|
56
|
+
break;
|
|
33
57
|
case "help":
|
|
34
58
|
case "-h":
|
|
35
59
|
case "--help":
|
|
@@ -43,6 +67,10 @@ async function main(): Promise<void> {
|
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
main().catch((e) => {
|
|
46
|
-
|
|
70
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
71
|
+
console.error(msg);
|
|
72
|
+
if (msg.includes("SOFER_PACKAGE_ID") || msg.includes("SOFER_SUI_SECRET_KEY") || msg.includes("ANTHROPIC_API_KEY")) {
|
|
73
|
+
console.error("\nFirst time? Run `sofer setup` for the .env template, then `sofer init`.");
|
|
74
|
+
}
|
|
47
75
|
process.exit(1);
|
|
48
76
|
});
|
package/src/chat.ts
CHANGED
|
@@ -1,46 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { loadConfig, SoferAgent } from "@sofer_agent/core";
|
|
1
|
+
import { SoferAgent, loadConfig } from "@sofer_agent/core";
|
|
3
2
|
import { requireSecrets } from "./secrets.js";
|
|
3
|
+
import { SoferTui } from "./tui.js";
|
|
4
4
|
|
|
5
|
-
/** Interactive chat
|
|
5
|
+
/** Interactive chat TUI with a stats sidebar. */
|
|
6
6
|
export async function chatCommand(): Promise<void> {
|
|
7
7
|
const config = loadConfig();
|
|
8
8
|
const agent = await SoferAgent.create(config, requireSecrets());
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
console.log(`model: ${config.brain.model} · type a message, or /exit to quit\n`);
|
|
10
|
+
const tui = new SoferTui(agent, config);
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
input: process.stdin,
|
|
15
|
-
output: process.stdout,
|
|
16
|
-
prompt: "\x1b[36myou>\x1b[0m ",
|
|
17
|
-
});
|
|
18
|
-
const label = `\x1b[32m${agent.name.toLowerCase()}>\x1b[0m`;
|
|
19
|
-
rl.prompt();
|
|
20
|
-
|
|
21
|
-
rl.on("line", async (line) => {
|
|
22
|
-
const text = line.trim();
|
|
23
|
-
if (text === "/exit" || text === "/quit") {
|
|
24
|
-
rl.close();
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (!text) {
|
|
28
|
-
rl.prompt();
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
12
|
+
await tui.run(async (line) => {
|
|
31
13
|
try {
|
|
32
|
-
const
|
|
33
|
-
|
|
14
|
+
const { text, turnUsage } = await agent.chat(line);
|
|
15
|
+
tui.addMessage("agent", text);
|
|
16
|
+
tui.setUsage(turnUsage.inputTokens, turnUsage.outputTokens);
|
|
34
17
|
} catch (e) {
|
|
35
|
-
|
|
18
|
+
tui.addMessage("error", e instanceof Error ? e.message : String(e));
|
|
36
19
|
}
|
|
37
|
-
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
await new Promise<void>((resolve) => {
|
|
41
|
-
rl.on("close", () => {
|
|
42
|
-
console.log("bye.");
|
|
43
|
-
resolve();
|
|
44
|
-
});
|
|
20
|
+
tui.render();
|
|
45
21
|
});
|
|
46
22
|
}
|
package/src/info.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AgentClient,
|
|
3
|
+
keypairFromSecret,
|
|
4
|
+
loadConfig,
|
|
5
|
+
makeSuiClient,
|
|
6
|
+
suiBalance,
|
|
7
|
+
} from "@sofer_agent/core";
|
|
2
8
|
|
|
3
9
|
/** Print the agent's on-chain state. */
|
|
4
10
|
export async function infoCommand(): Promise<void> {
|
package/src/tui.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type * as readline from "node:readline";
|
|
2
|
+
import type { SoferConfig } from "@sofer_agent/core";
|
|
3
|
+
import type { SoferAgent } from "@sofer_agent/core";
|
|
4
|
+
|
|
5
|
+
const SIDEBAR_W = 28;
|
|
6
|
+
const MIN_CHAT_W = 30;
|
|
7
|
+
|
|
8
|
+
interface ChatLine {
|
|
9
|
+
role: "you" | "agent" | "error";
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TuiState {
|
|
14
|
+
messages: ChatLine[];
|
|
15
|
+
lastTurnIn: number;
|
|
16
|
+
lastTurnOut: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SoferTui {
|
|
20
|
+
private state: TuiState = { messages: [], lastTurnIn: 0, lastTurnOut: 0 };
|
|
21
|
+
private inputBuf = "";
|
|
22
|
+
private cursor = 0;
|
|
23
|
+
private rl: readline.Interface | null = null;
|
|
24
|
+
private columns = 80;
|
|
25
|
+
private rows = 24;
|
|
26
|
+
private chatH = 20;
|
|
27
|
+
private chatW = 50;
|
|
28
|
+
private scrollOff = 0;
|
|
29
|
+
private resolve: (() => void) | null = null;
|
|
30
|
+
private onLine: ((line: string) => void) | null = null;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly agent: SoferAgent,
|
|
34
|
+
private readonly config: SoferConfig,
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
get width() {
|
|
38
|
+
return this.columns;
|
|
39
|
+
}
|
|
40
|
+
get height() {
|
|
41
|
+
return this.rows;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private measure(): void {
|
|
45
|
+
this.columns = process.stdout.columns ?? 80;
|
|
46
|
+
this.rows = process.stdout.rows ?? 24;
|
|
47
|
+
this.chatW = Math.max(MIN_CHAT_W, this.columns - SIDEBAR_W - 1);
|
|
48
|
+
this.chatH = this.rows - 3;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addMessage(role: ChatLine["role"], text: string): void {
|
|
52
|
+
this.state.messages.push({ role, text });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setUsage(lastTurnIn: number, lastTurnOut: number): void {
|
|
56
|
+
this.state.lastTurnIn = lastTurnIn;
|
|
57
|
+
this.state.lastTurnOut = lastTurnOut;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Start the interactive TUI loop. Returns when the user exits. */
|
|
61
|
+
async run(onLine: (line: string) => Promise<void>): Promise<void> {
|
|
62
|
+
this.onLine = onLine;
|
|
63
|
+
this.measure();
|
|
64
|
+
|
|
65
|
+
if (process.stdin.isTTY) process.stdin.setRawMode?.(true);
|
|
66
|
+
process.stdin.on("data", this.handleInput);
|
|
67
|
+
process.stdout.on("resize", this.handleResize);
|
|
68
|
+
process.on("SIGWINCH", this.handleResize);
|
|
69
|
+
|
|
70
|
+
this.render();
|
|
71
|
+
return new Promise<void>((resolve) => {
|
|
72
|
+
this.resolve = resolve;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private shutdown(): void {
|
|
77
|
+
process.stdin.setRawMode?.(false);
|
|
78
|
+
process.stdin.removeAllListeners("data");
|
|
79
|
+
process.stdout.removeAllListeners("resize");
|
|
80
|
+
process.removeAllListeners("SIGWINCH");
|
|
81
|
+
this.render(); // final render in normal mode
|
|
82
|
+
process.stdout.write("\n");
|
|
83
|
+
this.resolve?.();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Input handling ──────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
private handleInput = (data: Buffer): void => {
|
|
89
|
+
const str = data.toString();
|
|
90
|
+
for (const ch of str) {
|
|
91
|
+
const code = ch.charCodeAt(0);
|
|
92
|
+
if (code === 3) {
|
|
93
|
+
process.exit(0);
|
|
94
|
+
} // Ctrl-C
|
|
95
|
+
if (code === 4 && this.inputBuf.length === 0) {
|
|
96
|
+
this.shutdown();
|
|
97
|
+
return;
|
|
98
|
+
} // Ctrl-D on empty
|
|
99
|
+
if (code === 13) {
|
|
100
|
+
this.submit();
|
|
101
|
+
return;
|
|
102
|
+
} // Enter
|
|
103
|
+
if (code === 127) {
|
|
104
|
+
// Backspace
|
|
105
|
+
if (this.cursor > 0) {
|
|
106
|
+
this.inputBuf =
|
|
107
|
+
this.inputBuf.slice(0, this.cursor - 1) + this.inputBuf.slice(this.cursor);
|
|
108
|
+
this.cursor--;
|
|
109
|
+
}
|
|
110
|
+
this.render();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (code === 27 && this.inputBuf.length > 0) {
|
|
114
|
+
// Escape clears input
|
|
115
|
+
this.inputBuf = "";
|
|
116
|
+
this.cursor = 0;
|
|
117
|
+
this.render();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (ch >= " ") {
|
|
121
|
+
this.inputBuf = this.inputBuf.slice(0, this.cursor) + ch + this.inputBuf.slice(this.cursor);
|
|
122
|
+
this.cursor++;
|
|
123
|
+
this.render();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
private async submit(): Promise<void> {
|
|
129
|
+
const line = this.inputBuf.trim();
|
|
130
|
+
this.inputBuf = "";
|
|
131
|
+
this.cursor = 0;
|
|
132
|
+
if (!line) {
|
|
133
|
+
this.render();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (line === "/exit" || line === "/quit") {
|
|
137
|
+
this.shutdown();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.addMessage("you", line);
|
|
141
|
+
this.scrollOff = 0; // auto-scroll to latest
|
|
142
|
+
this.render();
|
|
143
|
+
await this.onLine?.(line);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Terminal rendering ──────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
private handleResize = (): void => {
|
|
149
|
+
this.measure();
|
|
150
|
+
this.render();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Reset cursor, clear, redraw everything. */
|
|
154
|
+
render(): void {
|
|
155
|
+
this.measure();
|
|
156
|
+
const out = process.stdout;
|
|
157
|
+
out.write("\x1b[?25l"); // hide cursor
|
|
158
|
+
out.write(this.buildFrame());
|
|
159
|
+
out.write("\x1b[?25h"); // show cursor
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private buildFrame(): string {
|
|
163
|
+
const { columns, rows, chatW, chatH } = this;
|
|
164
|
+
const rightW = columns - chatW - 1; // border column
|
|
165
|
+
|
|
166
|
+
let buf = "\x1b[H"; // cursor to top-left
|
|
167
|
+
|
|
168
|
+
// ── Body ──────────────────────────────────────────────────────────────
|
|
169
|
+
// We draw row by row. Each row: chat area | border | sidebar
|
|
170
|
+
const msgs = this.state.messages;
|
|
171
|
+
const wrappedLines = msgs.flatMap((m) => this.wrapLine(m, chatW));
|
|
172
|
+
const totalLines = wrappedLines.length;
|
|
173
|
+
const maxScroll = Math.max(0, totalLines - chatH);
|
|
174
|
+
if (this.scrollOff > maxScroll) this.scrollOff = maxScroll;
|
|
175
|
+
if (this.scrollOff < 0) this.scrollOff = 0;
|
|
176
|
+
const visible = wrappedLines.slice(this.scrollOff, this.scrollOff + chatH);
|
|
177
|
+
|
|
178
|
+
for (let r = 0; r < chatH; r++) {
|
|
179
|
+
const line = visible[r];
|
|
180
|
+
buf += line ? this.pad(line, chatW) : this.blank(chatW);
|
|
181
|
+
if (r === 0) buf += "┬";
|
|
182
|
+
else if (r === chatH - 1) buf += "┴";
|
|
183
|
+
else buf += "│";
|
|
184
|
+
buf += this.sidebarRow(r, chatH, rightW);
|
|
185
|
+
buf += "\n";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Divider ───────────────────────────────────────────────────────────
|
|
189
|
+
buf += `${"─".repeat(chatW)}┴${"─".repeat(rightW)}\n`;
|
|
190
|
+
|
|
191
|
+
// ── Input line ────────────────────────────────────────────────────────
|
|
192
|
+
const prefix = "\x1b[36myou>\x1b[0m ";
|
|
193
|
+
const prefixW = 4;
|
|
194
|
+
const maxInputW = columns - prefixW - 1;
|
|
195
|
+
let typed = this.inputBuf;
|
|
196
|
+
if (typed.length > maxInputW) {
|
|
197
|
+
const start = typed.length - maxInputW;
|
|
198
|
+
typed = typed.slice(start);
|
|
199
|
+
}
|
|
200
|
+
buf += `${prefix}${typed}\x1b[K`; // clear to end of line
|
|
201
|
+
// Place cursor at end of input
|
|
202
|
+
const cursorCol = prefixW + Math.min(this.cursor, maxInputW);
|
|
203
|
+
buf += `\x1b[${chatH + 2};${cursorCol}H`;
|
|
204
|
+
|
|
205
|
+
return buf;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private wrapLine(msg: ChatLine, width: number): string[] {
|
|
209
|
+
const label =
|
|
210
|
+
msg.role === "you"
|
|
211
|
+
? "\x1b[36myou>\x1b[0m "
|
|
212
|
+
: msg.role === "agent"
|
|
213
|
+
? `\x1b[32m${this.agent.name.toLowerCase()}>\x1b[0m `
|
|
214
|
+
: "\x1b[31merror>\x1b[0m ";
|
|
215
|
+
const content = msg.text;
|
|
216
|
+
const full = label + content;
|
|
217
|
+
if (full.length <= width) return [full];
|
|
218
|
+
// simple word-wrap
|
|
219
|
+
const lines: string[] = [];
|
|
220
|
+
let current = label;
|
|
221
|
+
const words = content.split(" ");
|
|
222
|
+
for (const word of words) {
|
|
223
|
+
if (current.length + 1 + word.length <= width) {
|
|
224
|
+
current += (current === label ? "" : " ") + word;
|
|
225
|
+
} else {
|
|
226
|
+
if (current.length > label.length) lines.push(current);
|
|
227
|
+
current = ` ${word}`; // indent continuation
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (current.length > label.length) lines.push(current);
|
|
231
|
+
return lines.length > 0 ? lines : [full];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private pad(s: string, w: number): string {
|
|
235
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping
|
|
236
|
+
const ansiRe = /\x1b\[\d*;?\d*m/g;
|
|
237
|
+
const stripped = s.replace(ansiRe, "");
|
|
238
|
+
const padW = Math.max(0, w - stripped.length);
|
|
239
|
+
return s + " ".repeat(padW);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private blank(w: number): string {
|
|
243
|
+
return " ".repeat(w);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── Sidebar ─────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
private sidebarRow(row: number, total: number, width: number): string {
|
|
249
|
+
const innerW = width - 2; // left & right padding
|
|
250
|
+
const lines = this.buildSidebar(innerW);
|
|
251
|
+
|
|
252
|
+
// center the sidebar content vertically
|
|
253
|
+
const gap = Math.max(0, total - lines.length);
|
|
254
|
+
const topPad = Math.floor(gap / 2);
|
|
255
|
+
const idx = row - topPad;
|
|
256
|
+
|
|
257
|
+
if (idx < 0 || idx >= lines.length) return " ".repeat(width);
|
|
258
|
+
const line = lines[idx];
|
|
259
|
+
if (!line) return " ".repeat(width);
|
|
260
|
+
return ` ${this.pad(line, innerW)} `;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private buildSidebar(width: number): string[] {
|
|
264
|
+
const { agent, config, state } = this;
|
|
265
|
+
const addr = `${agent.address.slice(0, 6)}...${agent.address.slice(-4)}`;
|
|
266
|
+
const totalIn = agent.usage.tokens.inputTokens;
|
|
267
|
+
const totalOut = agent.usage.tokens.outputTokens;
|
|
268
|
+
const cost = agent.usage.costUsd.toFixed(4);
|
|
269
|
+
|
|
270
|
+
return [
|
|
271
|
+
`\x1b[1m${agent.name}\x1b[0m`,
|
|
272
|
+
"─".repeat(width),
|
|
273
|
+
`Model: ${this.shortModel(config.brain.model)}`,
|
|
274
|
+
`Max out: ${config.brain.maxOutputTokens}`,
|
|
275
|
+
"",
|
|
276
|
+
`Turn in: ${state.lastTurnIn}`,
|
|
277
|
+
`Turn out: ${state.lastTurnOut}`,
|
|
278
|
+
`Total in: ${totalIn}`,
|
|
279
|
+
`Total out:${totalOut}`,
|
|
280
|
+
`Cost: \x1b[33m$${cost}\x1b[0m`,
|
|
281
|
+
"",
|
|
282
|
+
`Addr: ${addr}`,
|
|
283
|
+
`Network: ${config.network}`,
|
|
284
|
+
];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private shortModel(model: string): string {
|
|
288
|
+
const m = model.toLowerCase();
|
|
289
|
+
if (m.includes("sonnet")) return "Sonnet 4";
|
|
290
|
+
if (m.includes("haiku")) return "Haiku 3.5";
|
|
291
|
+
if (m.includes("opus")) return "Opus 4";
|
|
292
|
+
if (m.includes("fable")) return "Fable 5";
|
|
293
|
+
return model;
|
|
294
|
+
}
|
|
295
|
+
}
|