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