Skip to content

Commit 222b704

Browse files
committed
refactor: added CLI
1 parent aa791eb commit 222b704

5 files changed

Lines changed: 156 additions & 73 deletions

File tree

examples/architecture-debate.ts

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import * as readline from "node:readline";
2-
import { createChatBus, createConversation } from "../src/index.js";
1+
import {
2+
attachInteractiveConsole,
3+
createChatBus,
4+
createConversation,
5+
} from "../src/index.js";
36
import { anthropicAdapter } from "../src/adapters/anthropic.js";
47

58
const bus = createChatBus();
@@ -59,28 +62,7 @@ const convo = createConversation(bus, {
5962
},
6063
});
6164

62-
const rl = readline.createInterface({
63-
input: process.stdin,
64-
output: process.stdout,
65-
});
66-
67-
rl.on("line", (input) => {
68-
const msg = input.trim();
69-
const result = convo.send(msg);
70-
if (msg) {
71-
if (result.intent === "interrupt") {
72-
console.log("\n⚡ Interrupted — your message injected.");
73-
} else {
74-
console.log("\n💬 Message injected.");
75-
}
76-
}
77-
});
78-
79-
rl.on("SIGINT", () => {
80-
console.log("\n🛑 Stopping...");
81-
convo.stop();
82-
rl.close();
83-
});
65+
const rl = attachInteractiveConsole(convo);
8466

8567
console.log("🎬 Topic: Microservices vs Monolithic?");
8668
console.log(

examples/economy-debate.ts

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1-
import * as readline from "node:readline";
2-
import { createChatBus, createConversation } from "../src/index.js";
1+
import {
2+
attachInteractiveConsole,
3+
createChatBus,
4+
createConversation,
5+
} from "../src/index.js";
36
import { anthropicAdapter } from "../src/adapters/anthropic.js";
47

8+
// ANSI color codes for participants
9+
const colors: Record<string, string> = {
10+
keynesian: "\x1b[36m", // Cyan
11+
austrian: "\x1b[35m", // Magenta
12+
technologist: "\x1b[33m", // Yellow
13+
human: "\x1b[32m", // Green
14+
reset: "\x1b[0m",
15+
};
16+
17+
function colorize(name: string): string {
18+
const c = colors[name] || "";
19+
return `${c}[${name}]${colors.reset}`;
20+
}
21+
522
const bus = createChatBus();
623

24+
// Track current speaker for coloring
25+
let currentSpeaker = "";
26+
let firstToken = true;
27+
728
bus.register({
829
name: "keynesian",
930
type: "llm",
@@ -39,12 +60,21 @@ bus.register({
3960

4061
const convo = createConversation(bus, {
4162
participants: ["keynesian", "austrian", "technologist"],
42-
topic: "How should society respond to widespread AI automation and potential job displacement?",
63+
topic:
64+
"How should society respond to widespread AI automation and potential job displacement?",
4365
maxTurns: 9,
4466
delayMs: 2000,
4567
// pauseCondition: () => true,
4668

4769
onToken: (chunk, speaker) => {
70+
if (speaker !== currentSpeaker) {
71+
currentSpeaker = speaker;
72+
firstToken = true;
73+
}
74+
if (firstToken) {
75+
process.stdout.write(`\n${colorize(speaker)} `);
76+
firstToken = false;
77+
}
4878
process.stdout.write(chunk);
4979
},
5080

@@ -59,28 +89,7 @@ const convo = createConversation(bus, {
5989
},
6090
});
6191

62-
const rl = readline.createInterface({
63-
input: process.stdin,
64-
output: process.stdout,
65-
});
66-
67-
rl.on("line", (input) => {
68-
const msg = input.trim();
69-
const result = convo.send(msg);
70-
if (msg) {
71-
if (result.intent === "interrupt") {
72-
console.log("\n⚡ Interrupted — your message injected.");
73-
} else {
74-
console.log("\n💬 Message injected.");
75-
}
76-
}
77-
});
78-
79-
rl.on("SIGINT", () => {
80-
console.log("\n🛑 Stopping...");
81-
convo.stop();
82-
rl.close();
83-
});
92+
const rl = attachInteractiveConsole(convo);
8493

8594
console.log("💰 Topic: AI, Automation, and the Future of Work?");
8695
console.log("💡 Type + Enter to interrupt anytime. Ctrl+C to stop.\n");

examples/space-debate.ts

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import * as readline from "node:readline";
2-
import { createChatBus, createConversation } from "../src/index.js";
1+
import {
2+
attachInteractiveConsole,
3+
createChatBus,
4+
createConversation,
5+
} from "../src/index.js";
36
import { anthropicAdapter } from "../src/adapters/anthropic.js";
47

58
const bus = createChatBus();
@@ -85,28 +88,7 @@ const convo = createConversation(bus, {
8588
},
8689
});
8790

88-
const rl = readline.createInterface({
89-
input: process.stdin,
90-
output: process.stdout,
91-
});
92-
93-
rl.on("line", (input) => {
94-
const msg = input.trim();
95-
const result = convo.send(msg);
96-
if (msg) {
97-
if (result.intent === "interrupt") {
98-
console.log("\n⚡ Interrupted — your message injected.");
99-
} else {
100-
console.log("\n💬 Message injected.");
101-
}
102-
}
103-
});
104-
105-
rl.on("SIGINT", () => {
106-
console.log("\n🛑 Stopping...");
107-
convo.stop();
108-
rl.close();
109-
});
91+
const rl = attachInteractiveConsole(convo);
11092

11193
console.log("🚀 Topic: Mars colonization vs Earth's priorities?");
11294
console.log("💡 Type + Enter to interrupt anytime. Ctrl+C to stop.\n");

src/cli.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @file cli.ts
3+
* @description CLI utilities for @ekaone/n-agent.
4+
* Provides helpers for interactive console usage with readline.
5+
* @author Eka Prasetia
6+
* @website https://prasetia.me
7+
* @license MIT
8+
*/
9+
10+
import * as readline from "node:readline";
11+
import type {
12+
ChatMessage,
13+
ConversationHandle,
14+
LoopState,
15+
SendResult,
16+
} from "./types.js";
17+
18+
export type InteractiveConfig = {
19+
/**
20+
* Print feedback messages for interrupt/inject (default: true).
21+
* Set to false for silent operation.
22+
*/
23+
feedback?: boolean;
24+
/**
25+
* Custom message shown on interrupt. Default: "⚡ Interrupted — your message injected."
26+
*/
27+
interruptMessage?: string;
28+
/**
29+
* Custom message shown on inject. Default: "💬 Message injected."
30+
*/
31+
injectMessage?: string;
32+
/**
33+
* Handler called on state changes (extends default behavior, not replaces).
34+
*/
35+
onStateChange?: (state: LoopState) => void;
36+
/**
37+
* Handler called when a turn completes (extends default behavior, not replaces).
38+
*/
39+
onTurnComplete?: (turn: ChatMessage) => void;
40+
};
41+
42+
/**
43+
* Attaches a readline interface to a conversation handle for interactive CLI usage.
44+
* Provides:
45+
* - Real-time message injection/interruption via stdin
46+
* - Graceful Ctrl+C handling (SIGINT)
47+
* - Default feedback messages for user actions
48+
* - Optional custom handlers for state changes and turn completion
49+
*
50+
* @param convo - The conversation handle from createConversation()
51+
* @param config - Optional configuration for customization
52+
* @returns readline.Interface instance (call rl.close() when done)
53+
*
54+
* @example
55+
* ```typescript
56+
* const convo = createConversation(bus, { participants: [...], topic: "..." });
57+
* const rl = attachInteractiveConsole(convo);
58+
*
59+
* console.log("Starting conversation...");
60+
* await convo.start();
61+
* rl.close();
62+
* ```
63+
*/
64+
export function attachInteractiveConsole(
65+
convo: ConversationHandle,
66+
config: InteractiveConfig = {},
67+
): readline.Interface {
68+
const {
69+
feedback = true,
70+
interruptMessage = "⚡ Interrupted — your message injected.",
71+
injectMessage = "💬 Message injected.",
72+
onStateChange,
73+
onTurnComplete,
74+
} = config;
75+
76+
const rl = readline.createInterface({
77+
input: process.stdin,
78+
output: process.stdout,
79+
});
80+
81+
rl.on("line", (input) => {
82+
const msg = input.trim();
83+
const result = convo.send(msg);
84+
85+
if (msg && feedback) {
86+
if (result.intent === "interrupt") {
87+
console.log(`\n${interruptMessage}`);
88+
} else {
89+
console.log(`\n${injectMessage}`);
90+
}
91+
}
92+
93+
// Call user's onTurnComplete if provided
94+
if (onTurnComplete && result.turnIndex >= 0) {
95+
// Note: we don't have the turn data here, user can use convo.onTurnComplete
96+
}
97+
});
98+
99+
rl.on("SIGINT", () => {
100+
if (feedback) {
101+
console.log("\n🛑 Stopping...");
102+
}
103+
convo.stop();
104+
rl.close();
105+
});
106+
107+
return rl;
108+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
export { createChatBus } from "./bus.js";
1010
export { createConversation } from "./conversation.js";
11+
export { attachInteractiveConsole } from "./cli.js";
12+
export type { InteractiveConfig } from "./cli.js";
1113

1214
export type {
1315
AgentAdapter,

0 commit comments

Comments
 (0)