@mizchi/tornado 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@mizchi/tornado",
3
+ "version": "0.1.0",
4
+ "description": "Multi-agent development orchestrator with TUI",
5
+ "bin": {
6
+ "tornado": "bin/tornado.js"
7
+ },
8
+ "files": [
9
+ "bin/",
10
+ "sdk/"
11
+ ],
12
+ "type": "commonjs",
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/mizchi/tornado"
19
+ },
20
+ "homepage": "https://github.com/mizchi/tornado",
21
+ "bugs": {
22
+ "url": "https://github.com/mizchi/tornado/issues"
23
+ },
24
+ "keywords": [
25
+ "moonbit",
26
+ "agent",
27
+ "orchestration",
28
+ "tui",
29
+ "cli"
30
+ ],
31
+ "author": "mizchi",
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@anthropic-ai/claude-agent-sdk": "^0.2.49",
35
+ "@openai/codex-sdk": "^0.104.0"
36
+ }
37
+ }
@@ -0,0 +1,87 @@
1
+ // Claude Agent SDK runner
2
+ // stdout: JSONL (structured data for MoonBit parser)
3
+ // stderr: human-readable progress (real-time via inherited fd)
4
+ import { query } from "@anthropic-ai/claude-agent-sdk";
5
+
6
+ const opts = JSON.parse(process.argv[2]);
7
+
8
+ const queryOpts = {
9
+ includePartialMessages: true,
10
+ permissionMode: "bypassPermissions",
11
+ allowDangerouslySkipPermissions: true,
12
+ cwd: opts.cwd || process.cwd(),
13
+ };
14
+
15
+ if (opts.sessionId) queryOpts.resume = opts.sessionId;
16
+ if (opts.model) queryOpts.model = opts.model;
17
+ if (opts.systemPrompt) queryOpts.systemPrompt = opts.systemPrompt;
18
+
19
+ function log(tag, msg) {
20
+ process.stderr.write(`[${tag}] ${msg}\n`);
21
+ }
22
+
23
+ let totalCost = 0;
24
+
25
+ for await (const message of query({
26
+ prompt: opts.prompt,
27
+ options: queryOpts,
28
+ })) {
29
+ // Write structured JSONL to stdout
30
+ process.stdout.write(JSON.stringify(message) + "\n");
31
+
32
+ // Write human-readable progress to stderr (real-time)
33
+ switch (message.type) {
34
+ case "system":
35
+ if (message.subtype === "init") {
36
+ log("Claude", `Session init: model=${message.model || "unknown"}, session=${message.session_id || "new"}`);
37
+ }
38
+ break;
39
+
40
+ case "stream_event": {
41
+ const event = message.event;
42
+ if (event?.type === "content_block_start") {
43
+ const block = event.content_block;
44
+ if (block?.type === "tool_use") {
45
+ log("Claude", `Tool: ${block.name}`);
46
+ } else if (block?.type === "thinking") {
47
+ log("Claude", "Thinking...");
48
+ } else if (block?.type === "text") {
49
+ log("Claude", "Generating...");
50
+ }
51
+ }
52
+ break;
53
+ }
54
+
55
+ case "assistant": {
56
+ const content = message.message?.content;
57
+ if (Array.isArray(content)) {
58
+ for (const block of content) {
59
+ if (block.type === "tool_use") {
60
+ const inputPreview = JSON.stringify(block.input).slice(0, 100);
61
+ log("Claude", `${block.name}(${inputPreview})`);
62
+ }
63
+ }
64
+ }
65
+ break;
66
+ }
67
+
68
+ case "result": {
69
+ const parts = [`Result: ${message.subtype}`];
70
+ if (message.total_cost_usd) {
71
+ totalCost = message.total_cost_usd;
72
+ parts.push(`cost=$${message.total_cost_usd.toFixed(4)}`);
73
+ }
74
+ if (message.duration_ms) {
75
+ parts.push(`${(message.duration_ms / 1000).toFixed(1)}s`);
76
+ }
77
+ if (message.usage) {
78
+ const { input_tokens, output_tokens } = message.usage;
79
+ if (input_tokens || output_tokens) {
80
+ parts.push(`${input_tokens || 0}in/${output_tokens || 0}out`);
81
+ }
82
+ }
83
+ log("Claude", parts.join(", "));
84
+ break;
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,208 @@
1
+ // Codex SDK runner
2
+ // stdout: JSONL (structured data for MoonBit parser)
3
+ // stderr: human-readable progress (real-time via inherited fd)
4
+ import { Codex } from "@openai/codex-sdk";
5
+
6
+ const opts = JSON.parse(process.argv[2]);
7
+
8
+ function log(tag, msg) {
9
+ process.stderr.write(`[${tag}] ${msg}\n`);
10
+ }
11
+
12
+ const codex = new Codex();
13
+
14
+ const threadOpts = {
15
+ model: opts.model || undefined,
16
+ workingDirectory: opts.cwd || process.cwd(),
17
+ approvalPolicy: "never",
18
+ };
19
+
20
+ let thread;
21
+ if (opts.threadId) {
22
+ log("Codex", `Resuming thread: ${opts.threadId}`);
23
+ thread = codex.resumeThread(opts.threadId, threadOpts);
24
+ } else {
25
+ log("Codex", "Starting new thread");
26
+ thread = codex.startThread(threadOpts);
27
+ }
28
+
29
+ // Emit thread.started with thread_id
30
+ const initEvent = {
31
+ type: "system",
32
+ subtype: "init",
33
+ session_id: thread.id,
34
+ model: opts.model || "default",
35
+ };
36
+ process.stdout.write(JSON.stringify(initEvent) + "\n");
37
+ log("Codex", `Thread: ${thread.id}`);
38
+
39
+ const { events } = await thread.runStreamed(opts.prompt);
40
+
41
+ for await (const event of events) {
42
+ // Write JSONL to stdout (normalized to tornado format)
43
+ switch (event.type) {
44
+ case "item.started": {
45
+ const item = event.item;
46
+ const normalized = normalizeItemStart(item);
47
+ if (normalized) {
48
+ process.stdout.write(JSON.stringify(normalized) + "\n");
49
+ log("Codex", `${normalized._display || item.type}`);
50
+ }
51
+ break;
52
+ }
53
+ case "item.completed": {
54
+ const item = event.item;
55
+ const normalized = normalizeItemComplete(item);
56
+ if (normalized) {
57
+ process.stdout.write(JSON.stringify(normalized) + "\n");
58
+ log("Codex", `Done: ${normalized._display || item.type}`);
59
+ }
60
+ break;
61
+ }
62
+ case "turn.completed": {
63
+ const usage = event.usage || {};
64
+ const resultEvent = {
65
+ type: "result",
66
+ subtype: "success",
67
+ session_id: thread.id,
68
+ usage: {
69
+ input_tokens: usage.input_tokens || 0,
70
+ output_tokens: usage.output_tokens || 0,
71
+ cache_read_input_tokens: usage.cached_input_tokens || 0,
72
+ cache_creation_input_tokens: 0,
73
+ },
74
+ };
75
+ process.stdout.write(JSON.stringify(resultEvent) + "\n");
76
+ const parts = ["Result: success"];
77
+ if (usage.input_tokens || usage.output_tokens) {
78
+ parts.push(`${usage.input_tokens || 0}in/${usage.output_tokens || 0}out`);
79
+ if (usage.cached_input_tokens) {
80
+ parts.push(`cached=${usage.cached_input_tokens}`);
81
+ }
82
+ }
83
+ log("Codex", parts.join(", "));
84
+ break;
85
+ }
86
+ case "turn.failed": {
87
+ const errorEvent = {
88
+ type: "result",
89
+ subtype: "error",
90
+ session_id: thread.id,
91
+ is_error: true,
92
+ result: event.error?.message || "Turn failed",
93
+ };
94
+ process.stdout.write(JSON.stringify(errorEvent) + "\n");
95
+ log("Codex", `Failed: ${event.error?.message || "unknown error"}`);
96
+ break;
97
+ }
98
+ case "item.updated": {
99
+ // Intermediate updates - emit as info
100
+ break;
101
+ }
102
+ }
103
+ }
104
+
105
+ // Normalize a started item to tornado event format
106
+ function normalizeItemStart(item) {
107
+ switch (item.type) {
108
+ case "command_execution":
109
+ return {
110
+ type: "assistant",
111
+ message: {
112
+ content: [{
113
+ type: "tool_use",
114
+ name: "Bash",
115
+ input: { command: item.command || "" },
116
+ }],
117
+ },
118
+ _display: `Bash(${(item.command || "").slice(0, 80)})`,
119
+ };
120
+ case "file_change": {
121
+ const changes = item.changes || [];
122
+ const paths = changes.map(c => c.path).join(", ");
123
+ return {
124
+ type: "assistant",
125
+ message: {
126
+ content: [{
127
+ type: "tool_use",
128
+ name: "Edit",
129
+ input: { file_path: paths },
130
+ }],
131
+ },
132
+ _display: `Edit(${paths.slice(0, 80)})`,
133
+ };
134
+ }
135
+ case "mcp_tool_call":
136
+ return {
137
+ type: "assistant",
138
+ message: {
139
+ content: [{
140
+ type: "tool_use",
141
+ name: item.server_name || "mcp",
142
+ input: item.arguments || {},
143
+ }],
144
+ },
145
+ _display: `MCP(${item.server_name || "?"})`,
146
+ };
147
+ case "agent_message":
148
+ return {
149
+ type: "content_block_start",
150
+ content_block: { type: "text" },
151
+ _display: "Generating...",
152
+ };
153
+ case "reasoning":
154
+ return {
155
+ type: "content_block_start",
156
+ content_block: { type: "thinking" },
157
+ _display: "Thinking...",
158
+ };
159
+ default:
160
+ return null;
161
+ }
162
+ }
163
+
164
+ // Normalize a completed item to tornado event format
165
+ function normalizeItemComplete(item) {
166
+ switch (item.type) {
167
+ case "command_execution":
168
+ return {
169
+ type: "tool_result",
170
+ tool_name: "Bash",
171
+ content: item.output || `exit_code=${item.exit_code || 0}`,
172
+ _display: `Bash: exit=${item.exit_code || 0}`,
173
+ };
174
+ case "file_change": {
175
+ const changes = item.changes || [];
176
+ const summary = changes.map(c => `${c.kind}: ${c.path}`).join(", ");
177
+ return {
178
+ type: "tool_result",
179
+ tool_name: "Edit",
180
+ content: summary || "file changed",
181
+ _display: `Edit: ${summary.slice(0, 80)}`,
182
+ };
183
+ }
184
+ case "mcp_tool_call":
185
+ return {
186
+ type: "tool_result",
187
+ tool_name: item.server_name || "mcp",
188
+ content: item.result || item.error || "",
189
+ _display: `MCP: ${(item.result || item.error || "").slice(0, 80)}`,
190
+ };
191
+ case "agent_message": {
192
+ const text = item.text || "";
193
+ // Emit as output lines
194
+ return {
195
+ type: "assistant",
196
+ message: {
197
+ content: [{
198
+ type: "text",
199
+ text: text,
200
+ }],
201
+ },
202
+ _display: `Message: ${text.slice(0, 80)}`,
203
+ };
204
+ }
205
+ default:
206
+ return null;
207
+ }
208
+ }
@@ -0,0 +1,34 @@
1
+ // Background stdin watcher - reads user input and writes to interrupt file
2
+ // Spawned by the main process with stdio: ['inherit', 'pipe', 'inherit']
3
+ import { createInterface } from "readline";
4
+ import { appendFileSync, mkdirSync, writeFileSync } from "fs";
5
+
6
+ const INTERRUPT_FILE = ".tornado/interrupt.txt";
7
+
8
+ try {
9
+ mkdirSync(".tornado", { recursive: true });
10
+ } catch {}
11
+ // Clear stale interrupt
12
+ try {
13
+ writeFileSync(INTERRUPT_FILE, "", "utf-8");
14
+ } catch {}
15
+
16
+ const rl = createInterface({
17
+ input: process.stdin,
18
+ output: process.stderr,
19
+ terminal: true,
20
+ prompt: "",
21
+ });
22
+
23
+ rl.on("line", (line) => {
24
+ const trimmed = line.trim();
25
+ if (trimmed) {
26
+ appendFileSync(INTERRUPT_FILE, trimmed + "\n", "utf-8");
27
+ process.stderr.write(`\x1b[33m[USER] Queued: ${trimmed}\x1b[0m\n`);
28
+ }
29
+ });
30
+
31
+ rl.on("close", () => process.exit(0));
32
+
33
+ // Exit when parent dies
34
+ process.on("disconnect", () => process.exit(0));