@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 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 (or the environment).`);
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
- console.error(e instanceof Error ? e.message : String(e));
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;;;;;;;uDAOmD,CACpD,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,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,OAAO,CAAC,KAAK,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
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
@@ -1,3 +1,3 @@
1
- /** Interactive chat REPL with the agent. */
1
+ /** Interactive chat TUI with a stats sidebar. */
2
2
  export declare function chatCommand(): Promise<void>;
3
3
  //# sourceMappingURL=chat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../src/chat.ts"],"names":[],"mappings":"AAIA,4CAA4C;AAC5C,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAwCjD"}
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 readline from "node:readline";
2
- import { loadConfig, SoferAgent } from "@sofer_agent/core";
1
+ import { SoferAgent, loadConfig } from "@sofer_agent/core";
3
2
  import { requireSecrets } from "./secrets.js";
4
- /** Interactive chat REPL with the agent. */
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
- console.log(`\x1b[1m${agent.name}\x1b[0m ready ${agent.address}`);
9
- console.log(`model: ${config.brain.model} · type a message, or /exit to quit\n`);
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 reply = await agent.chat(text);
29
- console.log(`${label} ${reply}\n`);
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
- console.error(`\x1b[31merror:\x1b[0m ${e instanceof Error ? e.message : String(e)}\n`);
16
+ tui.addMessage("error", e instanceof Error ? e.message : String(e));
33
17
  }
34
- rl.prompt();
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,QAAQ,MAAM,eAAe,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,4CAA4C;AAC5C,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,OAAO,CAAC,GAAG,CAAC,UAAU,KAAK,CAAC,IAAI,mBAAmB,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IACpE,OAAO,CAAC,GAAG,CAAC,UAAU,MAAM,CAAC,KAAK,CAAC,KAAK,2CAA2C,CAAC,CAAC;IAErF,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC;QAClC,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,sBAAsB;KAC/B,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,WAAW,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC;IAC5D,EAAE,CAAC,MAAM,EAAE,CAAC;IAEZ,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;QAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACzB,IAAI,IAAI,KAAK,OAAO,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACzC,EAAE,CAAC,KAAK,EAAE,CAAC;YACX,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,EAAE,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrC,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,KAAK,IAAI,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACzF,CAAC;QACD,EAAE,CAAC,MAAM,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAClC,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAClB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"info.d.ts","sourceRoot":"","sources":["../src/info.ts"],"names":[],"mappings":"AAEA,wCAAwC;AACxC,wBAAsB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CA0CjD"}
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,EAAE,WAAW,EAAE,iBAAiB,EAAE,UAAU,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE1G,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"}
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
@@ -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.0",
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.1.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 (or the environment).`,
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
- console.error(e instanceof Error ? e.message : String(e));
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 readline from "node:readline";
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 REPL with the agent. */
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
- console.log(`\x1b[1m${agent.name}\x1b[0m ready ${agent.address}`);
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
- const rl = readline.createInterface({
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 reply = await agent.chat(text);
33
- console.log(`${label} ${reply}\n`);
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
- console.error(`\x1b[31merror:\x1b[0m ${e instanceof Error ? e.message : String(e)}\n`);
18
+ tui.addMessage("error", e instanceof Error ? e.message : String(e));
36
19
  }
37
- rl.prompt();
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 { AgentClient, keypairFromSecret, loadConfig, makeSuiClient, suiBalance } from "@sofer_agent/core";
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
+ }