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.
- package/.claude/settings.local.json +9 -0
- package/.coaia/pde/d77620fc-1cd9-47e2-ba00-c03e114e42e9.jsonl +16 -0
- package/.coaia/pde/de44d838-b58b-4e91-b791-dd3b0f940ed1.jsonl +60 -0
- package/.gemini/settings.json +8 -0
- package/.hch/issue_.env +4 -0
- package/.hch/issue_add__2601211715.json +77 -0
- package/.hch/issue_add__2601211715.md +4 -0
- package/.hch/issue_add__2602242020.json +78 -0
- package/.hch/issue_add__2602242020.md +7 -0
- package/.hch/issues.json +2312 -0
- package/.hch/issues.md +30 -0
- package/260123084839.coaia-narrative.autoRevisionOfInitial_NewStructuralTensionChart-to-initiate-HierarchicalThinking.txt +5 -0
- package/2602010101.issue.txt +31 -0
- package/BUGS.md +242 -0
- package/CLAUDE.md +2 -0
- package/ENHANCEMENTS.md +129 -0
- package/FEATURES_ENDING_SESSIONS.md +21 -0
- package/FIXES.md +114 -0
- package/GUILLAUME.md +77 -0
- package/KINSHIP.md +50 -0
- package/LAUNCH__session_id__MiaCodeNextWorkReviewAndCommits_2601312020.sh +7 -0
- package/PHASE_2.md +153 -0
- package/PHASE_2_IMPLEMENTATION.md +134 -0
- package/README.md +203 -0
- package/RESUME__issueMaker__540244c2-b096-40d8-8c3f-398408d3e0eb.2602041757.sh +1 -0
- package/RUN_COPILOT_with_related_folders__260130.sh +2 -0
- package/WS__mia-code__260214__IAIP_PDE.code-workspace +29 -0
- package/WS__mia-code__src332__260122.code-workspace +23 -0
- package/_env.sh +12 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +679 -0
- package/dist/commands.d.ts +43 -0
- package/dist/commands.js +108 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +57 -0
- package/dist/formatting.d.ts +12 -0
- package/dist/formatting.js +133 -0
- package/dist/geminiHeadless.d.ts +25 -0
- package/dist/geminiHeadless.js +246 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +186 -0
- package/dist/mcp/config-generator.d.ts +23 -0
- package/dist/mcp/config-generator.js +116 -0
- package/dist/mcp/index.d.ts +18 -0
- package/dist/mcp/index.js +43 -0
- package/dist/mcp/miaco-server.d.ts +15 -0
- package/dist/mcp/miaco-server.js +161 -0
- package/dist/mcp/miatel-server.d.ts +15 -0
- package/dist/mcp/miatel-server.js +123 -0
- package/dist/mcp/miawa-server.d.ts +15 -0
- package/dist/mcp/miawa-server.js +125 -0
- package/dist/mcp/utils.d.ts +51 -0
- package/dist/mcp/utils.js +76 -0
- package/dist/multiline-input.d.ts +98 -0
- package/dist/multiline-input.js +630 -0
- package/dist/narrative/index.d.ts +9 -0
- package/dist/narrative/index.js +11 -0
- package/dist/narrative/router.d.ts +89 -0
- package/dist/narrative/router.js +186 -0
- package/dist/narrative/tracer.d.ts +75 -0
- package/dist/narrative/tracer.js +180 -0
- package/dist/sessionStore.d.ts +10 -0
- package/dist/sessionStore.js +93 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.js +1 -0
- package/dist/unifier.d.ts +6 -0
- package/dist/unifier.js +147 -0
- package/issue-358--architecture/ARCHITECTURE_OVERVIEW.md +60 -0
- package/issue-358--architecture/CLI_INTEGRATION.md +61 -0
- package/issue-358--architecture/COVER_ART_BRIEF.md +68 -0
- package/issue-358--architecture/MEMORY_SYSTEM.md +89 -0
- package/issue-358--architecture/PERSONA_REGISTRY.md +97 -0
- package/issue-358--architecture/PODCAST_PRODUCTION_PLAN.md +61 -0
- package/issue-358--architecture/PODCAST_SCRIPT_FINAL.md +109 -0
- package/issue-358--architecture/PROTOTYPE_CHARACTER_SPEC.md +59 -0
- package/issue-358--architecture/RESOURCES.md +41 -0
- package/issue-358--architecture/TEAM_LISTENING_GUIDE.md +53 -0
- package/llms-gemini-cli.txt +145 -0
- package/package.json +39 -0
- package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/checkpoints/index.md +6 -0
- package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/events.jsonl +213 -0
- package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/plan.md +243 -0
- package/samples/copilot/session-state/be76abaa-a27f-4725-b2a9-22fb45f7e0f7/workspace.yaml +5 -0
- package/src/cli.ts +742 -0
- package/src/commands.ts +127 -0
- package/src/config.ts +67 -0
- package/src/formatting.ts +157 -0
- package/src/geminiHeadless.ts +300 -0
- package/src/index.ts +194 -0
- package/src/mcp/config-generator.ts +141 -0
- package/src/mcp/index.ts +55 -0
- package/src/mcp/miaco-server.ts +199 -0
- package/src/mcp/miatel-server.ts +138 -0
- package/src/mcp/miawa-server.ts +158 -0
- package/src/mcp/utils.ts +121 -0
- package/src/multiline-input.ts +739 -0
- package/src/narrative/index.ts +33 -0
- package/src/narrative/router.ts +260 -0
- package/src/narrative/tracer.ts +249 -0
- package/src/sessionStore.ts +111 -0
- package/src/types.ts +49 -0
- package/src/unifier.ts +171 -0
- package/tsconfig.json +15 -0
package/src/commands.ts
ADDED
|
@@ -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
|
+
}
|