mia-code 0.2.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.
Files changed (103) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.coaia/pde/d77620fc-1cd9-47e2-ba00-c03e114e42e9.jsonl +16 -0
  3. package/.coaia/pde/de44d838-b58b-4e91-b791-dd3b0f940ed1.jsonl +60 -0
  4. package/.gemini/settings.json +8 -0
  5. package/.hch/issue_.env +4 -0
  6. package/.hch/issue_add__2601211715.json +77 -0
  7. package/.hch/issue_add__2601211715.md +4 -0
  8. package/.hch/issue_add__2602242020.json +78 -0
  9. package/.hch/issue_add__2602242020.md +7 -0
  10. package/.hch/issues.json +2312 -0
  11. package/.hch/issues.md +30 -0
  12. package/260123084839.coaia-narrative.autoRevisionOfInitial_NewStructuralTensionChart-to-initiate-HierarchicalThinking.txt +5 -0
  13. package/2602010101.issue.txt +31 -0
  14. package/BUGS.md +242 -0
  15. package/CLAUDE.md +2 -0
  16. package/ENHANCEMENTS.md +129 -0
  17. package/FEATURES_ENDING_SESSIONS.md +21 -0
  18. package/FIXES.md +114 -0
  19. package/GUILLAUME.md +77 -0
  20. package/KINSHIP.md +50 -0
  21. package/LAUNCH__session_id__MiaCodeNextWorkReviewAndCommits_2601312020.sh +7 -0
  22. package/PHASE_2.md +153 -0
  23. package/PHASE_2_IMPLEMENTATION.md +134 -0
  24. package/README.md +203 -0
  25. package/RESUME__issueMaker__540244c2-b096-40d8-8c3f-398408d3e0eb.2602041757.sh +1 -0
  26. package/RUN_COPILOT_with_related_folders__260130.sh +2 -0
  27. package/WS__mia-code__260214__IAIP_PDE.code-workspace +29 -0
  28. package/WS__mia-code__src332__260122.code-workspace +23 -0
  29. package/_env.sh +12 -0
  30. package/dist/cli.d.ts +11 -0
  31. package/dist/cli.js +679 -0
  32. package/dist/commands.d.ts +43 -0
  33. package/dist/commands.js +108 -0
  34. package/dist/config.d.ts +8 -0
  35. package/dist/config.js +57 -0
  36. package/dist/formatting.d.ts +12 -0
  37. package/dist/formatting.js +133 -0
  38. package/dist/geminiHeadless.d.ts +25 -0
  39. package/dist/geminiHeadless.js +246 -0
  40. package/dist/index.d.ts +2 -0
  41. package/dist/index.js +186 -0
  42. package/dist/mcp/config-generator.d.ts +23 -0
  43. package/dist/mcp/config-generator.js +116 -0
  44. package/dist/mcp/index.d.ts +18 -0
  45. package/dist/mcp/index.js +43 -0
  46. package/dist/mcp/miaco-server.d.ts +15 -0
  47. package/dist/mcp/miaco-server.js +161 -0
  48. package/dist/mcp/miatel-server.d.ts +15 -0
  49. package/dist/mcp/miatel-server.js +123 -0
  50. package/dist/mcp/miawa-server.d.ts +15 -0
  51. package/dist/mcp/miawa-server.js +125 -0
  52. package/dist/mcp/utils.d.ts +51 -0
  53. package/dist/mcp/utils.js +76 -0
  54. package/dist/multiline-input.d.ts +98 -0
  55. package/dist/multiline-input.js +630 -0
  56. package/dist/narrative/index.d.ts +9 -0
  57. package/dist/narrative/index.js +11 -0
  58. package/dist/narrative/router.d.ts +89 -0
  59. package/dist/narrative/router.js +186 -0
  60. package/dist/narrative/tracer.d.ts +75 -0
  61. package/dist/narrative/tracer.js +180 -0
  62. package/dist/sessionStore.d.ts +10 -0
  63. package/dist/sessionStore.js +93 -0
  64. package/dist/types.d.ts +44 -0
  65. package/dist/types.js +1 -0
  66. package/dist/unifier.d.ts +6 -0
  67. package/dist/unifier.js +147 -0
  68. package/issue-358--architecture/ARCHITECTURE_OVERVIEW.md +60 -0
  69. package/issue-358--architecture/CLI_INTEGRATION.md +61 -0
  70. package/issue-358--architecture/COVER_ART_BRIEF.md +68 -0
  71. package/issue-358--architecture/MEMORY_SYSTEM.md +89 -0
  72. package/issue-358--architecture/PERSONA_REGISTRY.md +97 -0
  73. package/issue-358--architecture/PODCAST_PRODUCTION_PLAN.md +61 -0
  74. package/issue-358--architecture/PODCAST_SCRIPT_FINAL.md +109 -0
  75. package/issue-358--architecture/PROTOTYPE_CHARACTER_SPEC.md +59 -0
  76. package/issue-358--architecture/RESOURCES.md +41 -0
  77. package/issue-358--architecture/TEAM_LISTENING_GUIDE.md +53 -0
  78. package/llms-gemini-cli.txt +145 -0
  79. package/package.json +39 -0
  80. package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/checkpoints/index.md +6 -0
  81. package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/events.jsonl +213 -0
  82. package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/plan.md +243 -0
  83. package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/workspace.yaml +5 -0
  84. package/src/cli.ts +742 -0
  85. package/src/commands.ts +127 -0
  86. package/src/config.ts +67 -0
  87. package/src/formatting.ts +157 -0
  88. package/src/geminiHeadless.ts +300 -0
  89. package/src/index.ts +194 -0
  90. package/src/mcp/config-generator.ts +141 -0
  91. package/src/mcp/index.ts +55 -0
  92. package/src/mcp/miaco-server.ts +199 -0
  93. package/src/mcp/miatel-server.ts +138 -0
  94. package/src/mcp/miawa-server.ts +158 -0
  95. package/src/mcp/utils.ts +121 -0
  96. package/src/multiline-input.ts +739 -0
  97. package/src/narrative/index.ts +33 -0
  98. package/src/narrative/router.ts +260 -0
  99. package/src/narrative/tracer.ts +249 -0
  100. package/src/sessionStore.ts +111 -0
  101. package/src/types.ts +49 -0
  102. package/src/unifier.ts +171 -0
  103. package/tsconfig.json +15 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Command registry for mia-code CLI
3
+ * Provides metadata for slash commands including descriptions and argument specs
4
+ */
5
+
6
+ export interface CommandArg {
7
+ name: string;
8
+ description: string;
9
+ required: boolean;
10
+ }
11
+
12
+ export interface SlashCommand {
13
+ name: string;
14
+ aliases: string[];
15
+ description: string;
16
+ args: CommandArg[];
17
+ }
18
+
19
+ /**
20
+ * Registry of all available slash commands
21
+ */
22
+ export const SLASH_COMMANDS: SlashCommand[] = [
23
+ {
24
+ name: "exit",
25
+ aliases: ["quit", "q"],
26
+ description: "Exit the CLI",
27
+ args: []
28
+ },
29
+ {
30
+ name: "help",
31
+ aliases: ["h", "?"],
32
+ description: "Show this help message",
33
+ args: []
34
+ },
35
+ {
36
+ name: "session",
37
+ aliases: [],
38
+ description: "Show current session info",
39
+ args: []
40
+ },
41
+ {
42
+ name: "sessions",
43
+ aliases: [],
44
+ description: "List all saved sessions",
45
+ args: []
46
+ },
47
+ {
48
+ name: "clear",
49
+ aliases: [],
50
+ description: "Clear sessions for current project",
51
+ args: []
52
+ },
53
+ {
54
+ name: "config",
55
+ aliases: [],
56
+ description: "Configure engine & model (interactive)",
57
+ args: []
58
+ },
59
+ {
60
+ name: "add-dir",
61
+ aliases: [],
62
+ description: "Add directory for tool access",
63
+ args: [{ name: "path", description: "Directory path to add", required: true }]
64
+ }
65
+ ];
66
+
67
+ /**
68
+ * Get all command names including aliases (without the leading /)
69
+ */
70
+ export function getAllCommandNames(): string[] {
71
+ const names: string[] = [];
72
+ for (const cmd of SLASH_COMMANDS) {
73
+ names.push(cmd.name);
74
+ names.push(...cmd.aliases);
75
+ }
76
+ return names;
77
+ }
78
+
79
+ /**
80
+ * Find commands that match a partial input (for tab completion)
81
+ * @param partial The partial command (without leading /)
82
+ * @returns Array of matching command names
83
+ */
84
+ export function findMatchingCommands(partial: string): string[] {
85
+ const lowerPartial = partial.toLowerCase();
86
+ const allNames = getAllCommandNames();
87
+ return allNames.filter(name => name.toLowerCase().startsWith(lowerPartial));
88
+ }
89
+
90
+ /**
91
+ * Get the longest common prefix of matching commands
92
+ * @param matches Array of matching command names
93
+ * @returns The longest common prefix
94
+ */
95
+ export function getLongestCommonPrefix(matches: string[]): string {
96
+ if (matches.length === 0) return "";
97
+ if (matches.length === 1) return matches[0];
98
+
99
+ let prefix = matches[0];
100
+ for (let i = 1; i < matches.length; i++) {
101
+ while (!matches[i].toLowerCase().startsWith(prefix.toLowerCase())) {
102
+ prefix = prefix.slice(0, -1);
103
+ if (prefix === "") return "";
104
+ }
105
+ }
106
+ return prefix;
107
+ }
108
+
109
+ /**
110
+ * Get command by name or alias
111
+ */
112
+ export function getCommand(name: string): SlashCommand | undefined {
113
+ const lowerName = name.toLowerCase();
114
+ return SLASH_COMMANDS.find(cmd =>
115
+ cmd.name === lowerName || cmd.aliases.includes(lowerName)
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Format command for display in tab completion hints
121
+ */
122
+ export function formatCommandHint(cmd: SlashCommand): string {
123
+ const aliasStr = cmd.aliases.length > 0
124
+ ? ` (aliases: ${cmd.aliases.map(a => "/" + a).join(", ")})`
125
+ : "";
126
+ return `/${cmd.name}${aliasStr} - ${cmd.description}`;
127
+ }
package/src/config.ts ADDED
@@ -0,0 +1,67 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { MiaCodeConfig, Engine } from "./types.js";
5
+
6
+ const CONFIG_FILE = path.join(os.homedir(), ".mia-code.json");
7
+
8
+ /** Available models per engine */
9
+ export const ENGINE_MODELS: Record<Engine, string[]> = {
10
+ claude: ["sonnet", "haiku", "opus"],
11
+ copilot: ["claude-sonnet-4.6", "claude-opus-4.6", "claude-haiku-4.5", "gpt-5-mini", "gpt-4.1"],
12
+ gemini: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-3.0-pro", "gemini-3.0-flash"],
13
+ };
14
+
15
+ /** All supported engines */
16
+ export const ENGINES: Engine[] = ["gemini", "claude", "copilot"];
17
+
18
+ const defaultConfig: MiaCodeConfig = {
19
+ engine: (process.env.MIA_CODE_ENGINE as "gemini" | "claude" | "copilot") || "gemini",
20
+ geminiBinary: process.env.MIA_CODE_GEMINI_BIN || "gemini",
21
+ claudeBinary: process.env.MIA_CODE_CLAUDE_BIN || "claude",
22
+ copilotBinary: process.env.MIA_CODE_COPILOT_BIN || "copilot",
23
+ model: process.env.MIA_CODE_MODEL || "", // Will be set based on engine if empty
24
+ headlessOutputFormat: "stream-json",
25
+ defaultMode: "code",
26
+ defaultProjectRoot: null,
27
+ yoloMode: false
28
+ };
29
+
30
+ export function loadConfig(): MiaCodeConfig {
31
+ try {
32
+ let merged = { ...defaultConfig };
33
+
34
+ if (fs.existsSync(CONFIG_FILE)) {
35
+ const raw = fs.readFileSync(CONFIG_FILE, "utf8");
36
+ const parsed = JSON.parse(raw);
37
+ merged = { ...merged, ...parsed };
38
+ }
39
+
40
+ // Set default model based on engine if not specified
41
+ if (!merged.model) {
42
+ merged.model = merged.engine === "claude" ? "sonnet"
43
+ : merged.engine === "copilot" ? "gpt-4.1"
44
+ : "gemini-2.5-pro";
45
+ }
46
+
47
+ return merged;
48
+ } catch {
49
+ // Silent fallback
50
+ const fallback = { ...defaultConfig };
51
+ if (!fallback.model) {
52
+ fallback.model = fallback.engine === "claude" ? "sonnet"
53
+ : fallback.engine === "copilot" ? "gpt-4.1"
54
+ : "gemini-2.5-pro";
55
+ }
56
+ return fallback;
57
+ }
58
+ }
59
+
60
+ export function saveConfig(partial: Partial<MiaCodeConfig>): void {
61
+ const merged = { ...loadConfig(), ...partial };
62
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf8");
63
+ }
64
+
65
+ export function getConfigPath(): string {
66
+ return CONFIG_FILE;
67
+ }
@@ -0,0 +1,157 @@
1
+ import chalk from "chalk";
2
+ import { GeminiJsonEvent } from "./types.js";
3
+
4
+ export interface RenderOptions {
5
+ showRawToolEvents?: boolean;
6
+ showTimestamps?: boolean;
7
+ compact?: boolean;
8
+ }
9
+
10
+ export function renderEventsToText(
11
+ events: GeminiJsonEvent[],
12
+ opts: RenderOptions = {}
13
+ ): string {
14
+ const { showRawToolEvents = false, showTimestamps = false, compact = false } = opts;
15
+ const out: string[] = [];
16
+
17
+ // Consolidate consecutive assistant messages
18
+ let assistantTextBuffer: string[] = [];
19
+
20
+ const flushAssistantBuffer = () => {
21
+ if (assistantTextBuffer.length > 0) {
22
+ const fullText = assistantTextBuffer.join("").trim();
23
+ if (fullText) {
24
+ out.push(formatAssistantText(fullText, compact));
25
+ }
26
+ assistantTextBuffer = [];
27
+ }
28
+ };
29
+
30
+ for (const evt of events) {
31
+ switch (evt.type) {
32
+ case "init": {
33
+ flushAssistantBuffer();
34
+ const sid = evt.session_id ? ` (${evt.session_id.slice(0, 8)}...)` : "";
35
+ const ts = showTimestamps && evt.timestamp ? ` [${evt.timestamp}]` : "";
36
+ out.push(chalk.dim(`🧠🌸 session init${sid}${ts}`));
37
+ break;
38
+ }
39
+ case "message": {
40
+ if (evt.role === "assistant") {
41
+ const content = evt.text || evt.content || "";
42
+ if (content) {
43
+ assistantTextBuffer.push(content);
44
+ }
45
+ } else if (evt.role === "user") {
46
+ flushAssistantBuffer();
47
+ out.push(chalk.cyan.bold(`you:`));
48
+ out.push(chalk.cyan(evt.text || evt.content || ""));
49
+ }
50
+ break;
51
+ }
52
+ case "tool_use": {
53
+ flushAssistantBuffer();
54
+ if (!showRawToolEvents) break;
55
+ out.push(chalk.magenta(`🔧 tool_use: ${evt.tool?.name || (evt as any).name || ""}`));
56
+ break;
57
+ }
58
+ case "tool_result": {
59
+ flushAssistantBuffer();
60
+ if (!showRawToolEvents) break;
61
+ out.push(chalk.magenta(`✓ tool_result: ${evt.tool?.name || (evt as any).name || ""}`));
62
+ break;
63
+ }
64
+ case "error": {
65
+ flushAssistantBuffer();
66
+ out.push(
67
+ chalk.red(`❌ error: ${evt.error?.code ?? ""} - ${evt.error?.message ?? ""}`)
68
+ );
69
+ break;
70
+ }
71
+ case "result": {
72
+ flushAssistantBuffer();
73
+ if (!compact) {
74
+ out.push(chalk.dim("🧠🌸 session complete"));
75
+ }
76
+ break;
77
+ }
78
+ case "raw_text": {
79
+ if (evt.text) {
80
+ assistantTextBuffer.push(evt.text);
81
+ }
82
+ break;
83
+ }
84
+ default:
85
+ // Handle unknown event types with text content
86
+ const text = evt.text || evt.content;
87
+ if (text) {
88
+ assistantTextBuffer.push(String(text));
89
+ }
90
+ break;
91
+ }
92
+ }
93
+
94
+ // Flush any remaining assistant text
95
+ flushAssistantBuffer();
96
+
97
+ return out.join("\n");
98
+ }
99
+
100
+ function formatAssistantText(text: string, compact: boolean): string {
101
+ const trimmed = text.trim();
102
+ if (!trimmed) return "";
103
+
104
+ // Check if text already has 🧠🌸 miawa: prefix (agent self-formats)
105
+ const hasPrefix = trimmed.startsWith("🧠🌸 miawa:");
106
+
107
+ if (compact) {
108
+ return trimmed;
109
+ }
110
+
111
+ // If agent already formatted, use it as-is
112
+ if (hasPrefix) {
113
+ return trimmed;
114
+ }
115
+
116
+ // Otherwise, add our prefix
117
+ return [chalk.green.bold("🧠🌸 miawa:"), "", trimmed].join("\n");
118
+ }
119
+
120
+ export function formatSpinner(message: string): string {
121
+ return chalk.dim(`⏳ ${message}`);
122
+ }
123
+
124
+ export function formatError(message: string): string {
125
+ return chalk.red(`❌ ${message}`);
126
+ }
127
+
128
+ export function formatSuccess(message: string): string {
129
+ return chalk.green(`✓ ${message}`);
130
+ }
131
+
132
+ export function formatHeader(projectRoot: string, sessionId?: string, engine?: string): string {
133
+ const lines: string[] = [];
134
+ const engineName = engine === "claude" ? "Claude" : engine === "copilot" ? "Copilot" : "Gemini";
135
+ lines.push(chalk.bold(`🧠🌸 mia-code — ${engineName}-backed terminal agent`));
136
+ lines.push(chalk.dim(`project: ${projectRoot}`));
137
+ if (sessionId) {
138
+ lines.push(chalk.dim(`session: ${sessionId.slice(0, 12)}...`));
139
+ }
140
+ return lines.join("\n");
141
+ }
142
+
143
+ export function formatHelpText(): string {
144
+ return chalk.dim(`
145
+ Commands:
146
+ /exit, /quit Exit the CLI
147
+ /session Show current session info
148
+ /sessions List all sessions
149
+ /clear Clear session for current project
150
+ /config Configure engine & model (interactive)
151
+ /add-dir <path> Add directory for tool access
152
+ /help Show this help
153
+
154
+ Note: By default, output is interpreted through the Miawa Unifier.
155
+ Use --raw flag to see uninterpreted agent output.
156
+ `);
157
+ }
@@ -0,0 +1,300 @@
1
+ import { spawn, ChildProcess } from "child_process";
2
+ import { MiaCodeConfig, GeminiJsonEvent } from "./types.js";
3
+
4
+ export interface GeminiHeadlessOptions {
5
+ prompt: string;
6
+ config: MiaCodeConfig;
7
+ sessionId?: string;
8
+ projectRoot?: string | null;
9
+ additionalDirs?: string[];
10
+ onEvent?: (event: GeminiJsonEvent) => void;
11
+ }
12
+
13
+ export interface GeminiHeadlessResult {
14
+ sessionId?: string;
15
+ events: GeminiJsonEvent[];
16
+ }
17
+
18
+ export function runGeminiHeadless(
19
+ opts: GeminiHeadlessOptions
20
+ ): Promise<GeminiHeadlessResult> {
21
+ const { prompt, config, sessionId, projectRoot, additionalDirs, onEvent } = opts;
22
+
23
+ const binary = config.engine === "claude" ? config.claudeBinary
24
+ : config.engine === "copilot" ? config.copilotBinary
25
+ : config.geminiBinary;
26
+ const args: string[] = [];
27
+
28
+ // Positional prompt for non-interactive mode
29
+ args.push(prompt);
30
+
31
+ // Engine-specific flags
32
+ let actualOutputFormat = config.headlessOutputFormat;
33
+
34
+ if (config.engine === "claude") {
35
+ // Claude uses --print for non-interactive mode
36
+ args.push("--print");
37
+ // Claude --print with stream-json requires --verbose, use json instead
38
+ actualOutputFormat = config.headlessOutputFormat === "stream-json" ? "json" : config.headlessOutputFormat;
39
+ args.push("--output-format", actualOutputFormat);
40
+ } else if (config.engine === "copilot") {
41
+ // Copilot-cli uses --print for non-interactive mode
42
+ args.push("--print");
43
+ actualOutputFormat = "json";
44
+ args.push("--output-format", actualOutputFormat);
45
+ } else {
46
+ // Gemini uses --output-format directly
47
+ args.push("--output-format", config.headlessOutputFormat);
48
+ }
49
+
50
+ // Model
51
+ if (config.model) {
52
+ args.push("--model", config.model);
53
+ }
54
+
55
+ // YOLO mode for auto-approval
56
+ if (config.yoloMode) {
57
+ if (config.engine === "claude" || config.engine === "copilot") {
58
+ args.push("--dangerously-skip-permissions");
59
+ } else {
60
+ args.push("--yolo");
61
+ }
62
+ }
63
+
64
+ // Session resume
65
+ if (sessionId) {
66
+ args.push("--resume", sessionId);
67
+ }
68
+
69
+ // Additional directories
70
+ if (additionalDirs) {
71
+ for (const dir of additionalDirs) {
72
+ args.push("--add-dir", dir);
73
+ }
74
+ }
75
+
76
+ const child = spawn(binary, args, {
77
+ cwd: projectRoot || process.cwd(),
78
+ // Claude/copilot --print mode needs stdin closed, Gemini needs it open
79
+ stdio: (config.engine === "claude" || config.engine === "copilot") ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
80
+ env: { ...process.env, FORCE_COLOR: "1" }
81
+ });
82
+
83
+ return new Promise((resolve, reject) => {
84
+ let stdoutBuf = "";
85
+ let stderrBuf = "";
86
+ const events: GeminiJsonEvent[] = [];
87
+ let discoveredSessionId: string | undefined;
88
+
89
+ // Claude/copilot with json format returns single JSON object, not JSONL
90
+ const isClaudeJsonMode = (config.engine === "claude" || config.engine === "copilot") && actualOutputFormat === "json";
91
+
92
+ child.stdout?.on("data", (chunk: Buffer) => {
93
+ const text = chunk.toString("utf8");
94
+ stdoutBuf += text;
95
+
96
+ // Only parse line-by-line for stream-json format (Gemini or Claude with stream-json)
97
+ if (!isClaudeJsonMode) {
98
+ // Parse JSONL (stream-json format)
99
+ const lines = stdoutBuf.split("\n");
100
+ stdoutBuf = lines.pop() ?? ""; // Keep last incomplete line in buffer
101
+
102
+ for (const line of lines) {
103
+ const trimmed = line.trim();
104
+ if (!trimmed) continue;
105
+ try {
106
+ const evt = JSON.parse(trimmed) as GeminiJsonEvent;
107
+ if (evt.session_id && !discoveredSessionId) {
108
+ discoveredSessionId = evt.session_id;
109
+ }
110
+ events.push(evt);
111
+ if (onEvent) {
112
+ onEvent(evt);
113
+ }
114
+ } catch {
115
+ // Non-JSON line, could be raw text output
116
+ const textEvt: GeminiJsonEvent = { type: "raw_text", text: trimmed };
117
+ events.push(textEvt);
118
+ if (onEvent) {
119
+ onEvent(textEvt);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ // For Claude json mode, just accumulate - don't split or parse yet
125
+ });
126
+
127
+ child.stderr?.on("data", (chunk: Buffer) => {
128
+ stderrBuf += chunk.toString("utf8");
129
+ });
130
+
131
+ child.on("error", (err) => {
132
+ reject(err);
133
+ });
134
+
135
+ child.on("close", (code) => {
136
+ // Handle remaining buffer
137
+ const trimmed = stdoutBuf.trim();
138
+
139
+ if (trimmed) {
140
+ try {
141
+ const obj = JSON.parse(trimmed) as any;
142
+
143
+ // Claude --print with --output-format json returns {type: "result", result: "text"}
144
+ if (obj.type === "result" && obj.result) {
145
+ // Convert to standard message format
146
+ const resultEvt: GeminiJsonEvent = {
147
+ type: "message",
148
+ role: "assistant",
149
+ text: obj.result,
150
+ session_id: obj.session_id
151
+ };
152
+ events.push(resultEvt);
153
+
154
+ // Also add a completion event
155
+ events.push({ type: "result", session_id: obj.session_id });
156
+
157
+ if (obj.session_id && !discoveredSessionId) {
158
+ discoveredSessionId = obj.session_id;
159
+ }
160
+ if (onEvent) {
161
+ onEvent(resultEvt);
162
+ }
163
+ } else if (Array.isArray(obj)) {
164
+ for (const evt of obj) {
165
+ events.push(evt);
166
+ if (evt.session_id && !discoveredSessionId) {
167
+ discoveredSessionId = evt.session_id;
168
+ }
169
+ if (onEvent) {
170
+ onEvent(evt);
171
+ }
172
+ }
173
+ } else {
174
+ // Generic event
175
+ events.push(obj);
176
+ if (obj.session_id && !discoveredSessionId) {
177
+ discoveredSessionId = obj.session_id;
178
+ }
179
+ if (onEvent) {
180
+ onEvent(obj);
181
+ }
182
+ }
183
+ } catch {
184
+ // Raw text output
185
+ if (trimmed) {
186
+ const textEvt: GeminiJsonEvent = { type: "raw_text", text: trimmed };
187
+ events.push(textEvt);
188
+ if (onEvent) {
189
+ onEvent(textEvt);
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ // Handle specific exit codes
196
+ if (code === 42) {
197
+ // Gemini-specific: Invalid session identifier
198
+ reject(new Error(`Invalid session identifier "${sessionId}"`));
199
+ return;
200
+ }
201
+
202
+ if (code !== 0 && !events.length) {
203
+ reject(new Error(`${config.engine} exited with code ${code}\n${stderrBuf}`));
204
+ return;
205
+ }
206
+
207
+ resolve({
208
+ sessionId: discoveredSessionId || sessionId,
209
+ events
210
+ });
211
+ });
212
+ });
213
+ }
214
+
215
+ export interface GeminiStreamOptions {
216
+ prompt: string;
217
+ config: MiaCodeConfig;
218
+ sessionId?: string;
219
+ projectRoot?: string | null;
220
+ onData: (text: string) => void;
221
+ onEvent?: (event: GeminiJsonEvent) => void;
222
+ onComplete?: (sessionId?: string) => void;
223
+ }
224
+
225
+ export function streamGemini(opts: GeminiStreamOptions): ChildProcess {
226
+ const { prompt, config, sessionId, projectRoot, onData, onEvent, onComplete } = opts;
227
+
228
+ const binary = config.engine === "claude" ? config.claudeBinary
229
+ : config.engine === "copilot" ? config.copilotBinary
230
+ : config.geminiBinary;
231
+ const args: string[] = [];
232
+ args.push(prompt);
233
+
234
+ if (config.engine === "claude" || config.engine === "copilot") {
235
+ args.push("--print");
236
+ }
237
+ args.push("--output-format", "stream-json");
238
+
239
+ if (config.model) {
240
+ args.push("--model", config.model);
241
+ }
242
+
243
+ if (config.yoloMode) {
244
+ if (config.engine === "claude" || config.engine === "copilot") {
245
+ args.push("--dangerously-skip-permissions");
246
+ } else {
247
+ args.push("--yolo");
248
+ }
249
+ }
250
+
251
+ if (sessionId) {
252
+ args.push("--resume", sessionId);
253
+ }
254
+
255
+ const child = spawn(binary, args, {
256
+ cwd: projectRoot || process.cwd(),
257
+ stdio: (config.engine === "claude" || config.engine === "copilot") ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
258
+ env: { ...process.env, FORCE_COLOR: "1" }
259
+ });
260
+
261
+ let stdoutBuf = "";
262
+ let discoveredSessionId: string | undefined;
263
+
264
+ child.stdout?.on("data", (chunk: Buffer) => {
265
+ const text = chunk.toString("utf8");
266
+ stdoutBuf += text;
267
+
268
+ const lines = stdoutBuf.split("\n");
269
+ stdoutBuf = lines.pop() ?? "";
270
+
271
+ for (const line of lines) {
272
+ const trimmed = line.trim();
273
+ if (!trimmed) continue;
274
+ try {
275
+ const evt = JSON.parse(trimmed) as GeminiJsonEvent;
276
+ if (evt.session_id && !discoveredSessionId) {
277
+ discoveredSessionId = evt.session_id;
278
+ }
279
+ if (onEvent) {
280
+ onEvent(evt);
281
+ }
282
+ // Extract text content for display
283
+ const textContent = evt.text || evt.content || (evt.type === "raw_text" ? evt.text : null);
284
+ if (textContent) {
285
+ onData(textContent);
286
+ }
287
+ } catch {
288
+ onData(trimmed);
289
+ }
290
+ }
291
+ });
292
+
293
+ child.on("close", () => {
294
+ if (onComplete) {
295
+ onComplete(discoveredSessionId);
296
+ }
297
+ });
298
+
299
+ return child;
300
+ }