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/cli.ts
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
import { createInterface } from "readline";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { loadConfig, saveConfig, ENGINE_MODELS, ENGINES } from "./config.js";
|
|
4
|
+
import { runGeminiHeadless } from "./geminiHeadless.js";
|
|
5
|
+
import { rememberSession, getLastSessionForProject, listSessions, clearSessions, markSessionInitialized, isSessionInitialized, saveChatMessage, loadChatHistory } from "./sessionStore.js";
|
|
6
|
+
import { MiaCodeSessionMeta, MiaCodeConfig, Engine, ChatMessage } from "./types.js";
|
|
7
|
+
import { renderEventsToText, formatHeader, formatHelpText, formatError, formatSuccess } from "./formatting.js";
|
|
8
|
+
import { runUnifierSession } from "./unifier.js";
|
|
9
|
+
import { MultilineInput, supportsMultilineInput, TabCompletion } from "./multiline-input.js";
|
|
10
|
+
import { findMatchingCommands, getCommand } from "./commands.js";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
|
|
14
|
+
/** Prompt a numbered-choice question and return the 0-based index chosen. */
|
|
15
|
+
async function promptChoice(label: string, options: string[], current?: string): Promise<number> {
|
|
16
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
17
|
+
console.log(chalk.bold(`\n${label}`));
|
|
18
|
+
options.forEach((opt, i) => {
|
|
19
|
+
const marker = opt === current ? chalk.green(" ā current") : "";
|
|
20
|
+
console.log(chalk.dim(` ${i + 1}. ${opt}${marker}`));
|
|
21
|
+
});
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
rl.question(chalk.cyan(` select (1-${options.length}): `), (answer) => {
|
|
24
|
+
rl.close();
|
|
25
|
+
const n = parseInt(answer.trim(), 10);
|
|
26
|
+
if (isNaN(n) || n < 1 || n > options.length) {
|
|
27
|
+
console.log(chalk.yellow(" cancelled (invalid choice)"));
|
|
28
|
+
resolve(-1);
|
|
29
|
+
} else {
|
|
30
|
+
resolve(n - 1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Interactive config: pick engine ā pick model ā save */
|
|
37
|
+
async function runInteractiveConfig(config: MiaCodeConfig): Promise<void> {
|
|
38
|
+
console.log(chalk.dim("\ncurrent configuration:"));
|
|
39
|
+
console.log(chalk.dim(` engine: ${config.engine}`));
|
|
40
|
+
console.log(chalk.dim(` model: ${config.model}`));
|
|
41
|
+
|
|
42
|
+
// 1. Pick engine
|
|
43
|
+
const engineIdx = await promptChoice("Select engine:", ENGINES, config.engine);
|
|
44
|
+
if (engineIdx < 0) return;
|
|
45
|
+
const engine = ENGINES[engineIdx];
|
|
46
|
+
|
|
47
|
+
// 2. Pick model for chosen engine
|
|
48
|
+
const models = ENGINE_MODELS[engine];
|
|
49
|
+
const modelIdx = await promptChoice(`Select model for ${engine}:`, models, config.model);
|
|
50
|
+
if (modelIdx < 0) return;
|
|
51
|
+
const model = models[modelIdx];
|
|
52
|
+
|
|
53
|
+
// 3. Save
|
|
54
|
+
saveConfig({ engine, model });
|
|
55
|
+
config.engine = engine;
|
|
56
|
+
config.model = model;
|
|
57
|
+
console.log(chalk.green(`\nā config updated ā engine: ${engine}, model: ${model}`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function pickSession(projectRoot: string): Promise<string | undefined> {
|
|
61
|
+
const sessions = listSessions().filter(s => s.projectRoot === projectRoot);
|
|
62
|
+
|
|
63
|
+
if (sessions.length === 0) {
|
|
64
|
+
console.log(chalk.yellow("No saved sessions for this project."));
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(chalk.bold("\nAvailable sessions:"));
|
|
69
|
+
sessions.forEach((s, idx) => {
|
|
70
|
+
console.log(chalk.dim(` ${idx + 1}. ${s.id.slice(0, 12)}... (${s.startedAt})`));
|
|
71
|
+
});
|
|
72
|
+
console.log(chalk.dim(` 0. Start new session`));
|
|
73
|
+
console.log("");
|
|
74
|
+
|
|
75
|
+
const rl = createInterface({
|
|
76
|
+
input: process.stdin,
|
|
77
|
+
output: process.stdout
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
rl.question(chalk.cyan("Select session (0-" + sessions.length + "): "), (answer) => {
|
|
82
|
+
rl.close();
|
|
83
|
+
const choice = parseInt(answer.trim(), 10);
|
|
84
|
+
|
|
85
|
+
if (isNaN(choice) || choice < 0 || choice > sessions.length) {
|
|
86
|
+
console.log(chalk.yellow("Invalid choice. Starting new session."));
|
|
87
|
+
resolve(undefined);
|
|
88
|
+
} else if (choice === 0) {
|
|
89
|
+
resolve(undefined);
|
|
90
|
+
} else {
|
|
91
|
+
resolve(sessions[choice - 1].id);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CliOptions {
|
|
98
|
+
projectRoot?: string;
|
|
99
|
+
resume?: string | boolean; // Can be session ID, true (for picker), or undefined
|
|
100
|
+
yolo?: boolean;
|
|
101
|
+
raw?: boolean;
|
|
102
|
+
engine?: Engine;
|
|
103
|
+
addDirs?: string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function runInteractiveCli(options: CliOptions = {}): Promise<void> {
|
|
107
|
+
const config = loadConfig();
|
|
108
|
+
const projectRoot = options.projectRoot
|
|
109
|
+
? path.resolve(options.projectRoot)
|
|
110
|
+
: process.cwd();
|
|
111
|
+
|
|
112
|
+
// Override engine if specified
|
|
113
|
+
if (options.engine !== undefined) {
|
|
114
|
+
config.engine = options.engine;
|
|
115
|
+
// Update model to match engine if using default
|
|
116
|
+
if (!process.env.MIA_CODE_MODEL && !options.resume) {
|
|
117
|
+
config.model = config.engine === "claude" ? "sonnet" : "gemini-2.5-pro";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Override yolo mode if specified
|
|
122
|
+
if (options.yolo !== undefined) {
|
|
123
|
+
config.yoloMode = options.yolo;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Raw mode - skip unifier, show raw events
|
|
127
|
+
const rawMode = options.raw ?? false;
|
|
128
|
+
|
|
129
|
+
// Mutable list of additional directories (from CLI + runtime /add-dir)
|
|
130
|
+
const runtimeAddDirs: string[] = [...(options.addDirs || [])];
|
|
131
|
+
|
|
132
|
+
// Handle session resume
|
|
133
|
+
let currentSessionId: string | undefined;
|
|
134
|
+
|
|
135
|
+
if (options.resume === true) {
|
|
136
|
+
// Interactive session picker
|
|
137
|
+
currentSessionId = await pickSession(projectRoot);
|
|
138
|
+
} else if (typeof options.resume === "string") {
|
|
139
|
+
// Explicit session ID provided
|
|
140
|
+
currentSessionId = options.resume;
|
|
141
|
+
} else {
|
|
142
|
+
// Auto-resume last session for this project
|
|
143
|
+
currentSessionId = getLastSessionForProject(projectRoot)?.id;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Print header
|
|
147
|
+
console.log("");
|
|
148
|
+
console.log(formatHeader(projectRoot, currentSessionId, config.engine));
|
|
149
|
+
if (!currentSessionId) {
|
|
150
|
+
console.log(chalk.dim("new session will be created on first prompt"));
|
|
151
|
+
}
|
|
152
|
+
if (supportsMultilineInput()) {
|
|
153
|
+
console.log(chalk.dim("type /help for commands, Ctrl+J for newline, Enter to submit"));
|
|
154
|
+
} else {
|
|
155
|
+
console.log(chalk.dim("type /help for commands, ctrl+c to exit"));
|
|
156
|
+
}
|
|
157
|
+
console.log("");
|
|
158
|
+
|
|
159
|
+
// Restore chat history if resuming a session
|
|
160
|
+
if (currentSessionId) {
|
|
161
|
+
const history = loadChatHistory(currentSessionId);
|
|
162
|
+
if (history.length > 0) {
|
|
163
|
+
console.log(chalk.dim(`āā restoring ${history.length} messages āā`));
|
|
164
|
+
for (const msg of history) {
|
|
165
|
+
if (msg.role === "user") {
|
|
166
|
+
console.log(chalk.cyan.bold("you> ") + chalk.cyan(msg.text.length > 200 ? msg.text.slice(0, 200) + "ā¦" : msg.text));
|
|
167
|
+
} else {
|
|
168
|
+
const preview = msg.text.length > 300 ? msg.text.slice(0, 300) + "ā¦" : msg.text;
|
|
169
|
+
console.log(chalk.green.bold("š§ šø ") + chalk.dim(preview));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
console.log(chalk.dim(`āā end history āā\n`));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Process input handles both readline and MultilineInput callbacks
|
|
177
|
+
const processInput = async (input: string, resetFn: () => void): Promise<boolean> => {
|
|
178
|
+
const trimmed = input.trim();
|
|
179
|
+
if (!trimmed) {
|
|
180
|
+
resetFn();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Handle slash commands
|
|
185
|
+
if (trimmed.startsWith("/")) {
|
|
186
|
+
const [cmd] = trimmed.slice(1).split(/\s+/);
|
|
187
|
+
const lowerCmd = cmd.toLowerCase();
|
|
188
|
+
|
|
189
|
+
switch (lowerCmd) {
|
|
190
|
+
case "exit":
|
|
191
|
+
case "quit":
|
|
192
|
+
case "q":
|
|
193
|
+
console.log(chalk.dim("š bye."));
|
|
194
|
+
return false;
|
|
195
|
+
|
|
196
|
+
case "help":
|
|
197
|
+
case "h":
|
|
198
|
+
case "?":
|
|
199
|
+
console.log(formatHelpText());
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
case "session":
|
|
203
|
+
if (currentSessionId) {
|
|
204
|
+
console.log(chalk.dim(`current session: ${currentSessionId}`));
|
|
205
|
+
console.log(chalk.dim(`project: ${projectRoot}`));
|
|
206
|
+
console.log(chalk.dim(`model: ${config.model}`));
|
|
207
|
+
} else {
|
|
208
|
+
console.log(chalk.dim("no active session"));
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case "sessions":
|
|
213
|
+
const sessions = listSessions();
|
|
214
|
+
if (sessions.length === 0) {
|
|
215
|
+
console.log(chalk.dim("no saved sessions"));
|
|
216
|
+
} else {
|
|
217
|
+
console.log(chalk.dim(`saved sessions (${sessions.length}):`));
|
|
218
|
+
for (const s of sessions.slice(-10)) {
|
|
219
|
+
const marker = s.id === currentSessionId ? chalk.green("ā ") : " ";
|
|
220
|
+
console.log(chalk.dim(`${marker}${s.id.slice(0, 12)}... | ${s.projectRoot || "unknown"} | ${s.startedAt}`));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case "clear":
|
|
226
|
+
clearSessions();
|
|
227
|
+
console.log(formatSuccess("sessions cleared"));
|
|
228
|
+
break;
|
|
229
|
+
|
|
230
|
+
case "config":
|
|
231
|
+
await runInteractiveConfig(config);
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
case "add-dir": {
|
|
235
|
+
const dirArg = trimmed.slice(trimmed.indexOf(cmd) + cmd.length).trim();
|
|
236
|
+
if (!dirArg) {
|
|
237
|
+
console.log(chalk.dim("usage: /add-dir <directory>"));
|
|
238
|
+
if (runtimeAddDirs.length > 0) {
|
|
239
|
+
console.log(chalk.dim(`\ncurrent additional dirs:`));
|
|
240
|
+
for (const d of runtimeAddDirs) {
|
|
241
|
+
console.log(chalk.dim(` š ${d}`));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
const resolved = path.resolve(dirArg);
|
|
247
|
+
try {
|
|
248
|
+
const stat = fs.statSync(resolved);
|
|
249
|
+
if (!stat.isDirectory()) {
|
|
250
|
+
console.log(formatError(`not a directory: ${resolved}`));
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
console.log(formatError(`directory not found: ${resolved}`));
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (!runtimeAddDirs.includes(resolved)) {
|
|
258
|
+
runtimeAddDirs.push(resolved);
|
|
259
|
+
}
|
|
260
|
+
console.log(formatSuccess(`added directory: ${resolved}`));
|
|
261
|
+
console.log(chalk.dim(`total additional dirs: ${runtimeAddDirs.length}`));
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
default:
|
|
266
|
+
console.log(formatError(`unknown command: /${cmd}`));
|
|
267
|
+
console.log(formatHelpText());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log("");
|
|
271
|
+
resetFn();
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Expand @file references: read file contents and inject into prompt
|
|
276
|
+
const expandedPrompt = expandFileReferences(trimmed);
|
|
277
|
+
|
|
278
|
+
// Send to engine
|
|
279
|
+
console.log("");
|
|
280
|
+
console.log(chalk.dim("ā³ thinking..."));
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Only pass sessionId if the session is initialized (acknowledged by engine)
|
|
284
|
+
const sessionToUse = currentSessionId && isSessionInitialized(currentSessionId)
|
|
285
|
+
? currentSessionId
|
|
286
|
+
: undefined;
|
|
287
|
+
|
|
288
|
+
const result = await runGeminiHeadless({
|
|
289
|
+
prompt: expandedPrompt,
|
|
290
|
+
config,
|
|
291
|
+
sessionId: sessionToUse,
|
|
292
|
+
projectRoot,
|
|
293
|
+
additionalDirs: runtimeAddDirs.length > 0 ? runtimeAddDirs : undefined
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// Clear the "thinking" line
|
|
297
|
+
process.stdout.write("\x1B[1A\x1B[2K");
|
|
298
|
+
|
|
299
|
+
if (result.sessionId) {
|
|
300
|
+
if (result.sessionId !== currentSessionId) {
|
|
301
|
+
currentSessionId = result.sessionId;
|
|
302
|
+
const meta: MiaCodeSessionMeta = {
|
|
303
|
+
id: currentSessionId,
|
|
304
|
+
startedAt: new Date().toISOString(),
|
|
305
|
+
model: config.model,
|
|
306
|
+
projectRoot,
|
|
307
|
+
initialized: true
|
|
308
|
+
};
|
|
309
|
+
rememberSession(meta);
|
|
310
|
+
console.log(chalk.dim(`session: ${currentSessionId.slice(0, 12)}...`));
|
|
311
|
+
} else if (!isSessionInitialized(currentSessionId)) {
|
|
312
|
+
// Mark existing session as initialized now that engine acknowledged it
|
|
313
|
+
markSessionInitialized(currentSessionId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Extract assistant text for history
|
|
318
|
+
let assistantText = "";
|
|
319
|
+
|
|
320
|
+
// Raw mode: show original events
|
|
321
|
+
if (rawMode) {
|
|
322
|
+
const rendered = renderEventsToText(result.events);
|
|
323
|
+
if (rendered) {
|
|
324
|
+
console.log(rendered);
|
|
325
|
+
assistantText = rendered;
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
// Unifier mode: run ceremonial interpretation
|
|
329
|
+
console.log(chalk.dim("ā³ interpreting..."));
|
|
330
|
+
try {
|
|
331
|
+
const ceremonialOutput = await runUnifierSession(result.events, trimmed, config);
|
|
332
|
+
process.stdout.write("\x1B[1A\x1B[2K");
|
|
333
|
+
if (ceremonialOutput) {
|
|
334
|
+
console.log(ceremonialOutput);
|
|
335
|
+
assistantText = ceremonialOutput;
|
|
336
|
+
}
|
|
337
|
+
} catch (unifierErr: unknown) {
|
|
338
|
+
// Fallback to raw output if unifier fails
|
|
339
|
+
process.stdout.write("\x1B[1A\x1B[2K");
|
|
340
|
+
console.log(chalk.yellow("ā unifier failed, showing raw output:"));
|
|
341
|
+
const rendered = renderEventsToText(result.events);
|
|
342
|
+
if (rendered) {
|
|
343
|
+
console.log(rendered);
|
|
344
|
+
assistantText = rendered;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Save chat history
|
|
350
|
+
if (currentSessionId) {
|
|
351
|
+
const ts = new Date().toISOString();
|
|
352
|
+
saveChatMessage(currentSessionId, { role: "user", text: trimmed, timestamp: ts });
|
|
353
|
+
if (assistantText) {
|
|
354
|
+
saveChatMessage(currentSessionId, { role: "assistant", text: assistantText, timestamp: ts });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
console.log("");
|
|
359
|
+
} catch (err: unknown) {
|
|
360
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
361
|
+
|
|
362
|
+
// Handle invalid session error gracefully
|
|
363
|
+
if (message.includes("Invalid session identifier") && currentSessionId) {
|
|
364
|
+
console.log(chalk.yellow("ā Session expired, starting fresh..."));
|
|
365
|
+
// Mark session as not initialized and retry without session
|
|
366
|
+
const meta: MiaCodeSessionMeta = {
|
|
367
|
+
id: currentSessionId,
|
|
368
|
+
startedAt: new Date().toISOString(),
|
|
369
|
+
model: config.model,
|
|
370
|
+
projectRoot,
|
|
371
|
+
initialized: false
|
|
372
|
+
};
|
|
373
|
+
rememberSession(meta);
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const result = await runGeminiHeadless({
|
|
377
|
+
prompt: trimmed,
|
|
378
|
+
config,
|
|
379
|
+
sessionId: undefined, // Don't resume
|
|
380
|
+
projectRoot,
|
|
381
|
+
additionalDirs: runtimeAddDirs.length > 0 ? runtimeAddDirs : undefined
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
process.stdout.write("\x1B[1A\x1B[2K");
|
|
385
|
+
|
|
386
|
+
if (result.sessionId) {
|
|
387
|
+
currentSessionId = result.sessionId;
|
|
388
|
+
const newMeta: MiaCodeSessionMeta = {
|
|
389
|
+
id: currentSessionId,
|
|
390
|
+
startedAt: new Date().toISOString(),
|
|
391
|
+
model: config.model,
|
|
392
|
+
projectRoot,
|
|
393
|
+
initialized: true
|
|
394
|
+
};
|
|
395
|
+
rememberSession(newMeta);
|
|
396
|
+
console.log(chalk.dim(`new session: ${currentSessionId.slice(0, 12)}...`));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const rendered = renderEventsToText(result.events);
|
|
400
|
+
if (rendered) {
|
|
401
|
+
console.log(rendered);
|
|
402
|
+
}
|
|
403
|
+
} catch (retryErr: unknown) {
|
|
404
|
+
const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
405
|
+
console.log(formatError(`engine error: ${retryMessage}`));
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
console.log(formatError(`engine error: ${message}`));
|
|
409
|
+
}
|
|
410
|
+
console.log("");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
resetFn();
|
|
414
|
+
return true;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Tab completion handler for /commands and @file paths
|
|
418
|
+
const handleTabComplete = (input: string): TabCompletion | null => {
|
|
419
|
+
// --- @file/path completion ---
|
|
420
|
+
// Find the last @ token before or at cursor position
|
|
421
|
+
const atIdx = input.lastIndexOf("@");
|
|
422
|
+
if (atIdx >= 0) {
|
|
423
|
+
const afterAt = input.slice(atIdx + 1);
|
|
424
|
+
// Only complete if there's no space after @ (still typing the path)
|
|
425
|
+
if (!afterAt.includes(" ")) {
|
|
426
|
+
return completeFilePath(input, atIdx, afterAt);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// --- /command completion (only when input starts with / with no spaces) ---
|
|
431
|
+
if (input.startsWith("/") && !input.includes(" ")) {
|
|
432
|
+
const partial = input.slice(1);
|
|
433
|
+
const matches = findMatchingCommands(partial);
|
|
434
|
+
if (matches.length === 0) return null;
|
|
435
|
+
|
|
436
|
+
// Build rich labels: "/command ā description"
|
|
437
|
+
const labels = matches.map(name => {
|
|
438
|
+
const cmd = getCommand(name);
|
|
439
|
+
const desc = cmd ? cmd.description : "";
|
|
440
|
+
return desc ? `/${name} ā ${desc}` : `/${name}`;
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
matches,
|
|
445
|
+
labels,
|
|
446
|
+
prefix: partial,
|
|
447
|
+
tokenStart: 0,
|
|
448
|
+
triggerChar: "/",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// --- /add-dir <path> directory completion ---
|
|
453
|
+
const addDirMatch = input.match(/^\/add-dir\s+(.*)/);
|
|
454
|
+
if (addDirMatch) {
|
|
455
|
+
const partial = addDirMatch[1];
|
|
456
|
+
return completeDirPath(input, input.indexOf(addDirMatch[1]), partial);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return null;
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
function fileIcon(name: string): string {
|
|
463
|
+
const ext = name.split(".").pop()?.toLowerCase() || "";
|
|
464
|
+
const icons: Record<string, string> = {
|
|
465
|
+
ts: "š·", tsx: "š·", js: "š”", jsx: "š”", json: "š",
|
|
466
|
+
md: "š", txt: "š", sh: "āļø", yaml: "š", yml: "š",
|
|
467
|
+
py: "š", rs: "š¦", go: "šµ", toml: "š", lock: "š",
|
|
468
|
+
css: "šØ", html: "š", svg: "š¼ļø", png: "š¼ļø", jpg: "š¼ļø",
|
|
469
|
+
};
|
|
470
|
+
return icons[ext] || "š";
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Complete file/directory paths after @
|
|
475
|
+
*/
|
|
476
|
+
function completeFilePath(_input: string, atIdx: number, partial: string): TabCompletion | null {
|
|
477
|
+
const cwd = process.cwd();
|
|
478
|
+
|
|
479
|
+
// Split into directory part and name prefix
|
|
480
|
+
const lastSlash = partial.lastIndexOf("/");
|
|
481
|
+
let dirPath: string;
|
|
482
|
+
let namePrefix: string;
|
|
483
|
+
|
|
484
|
+
if (lastSlash >= 0) {
|
|
485
|
+
dirPath = partial.slice(0, lastSlash + 1);
|
|
486
|
+
namePrefix = partial.slice(lastSlash + 1);
|
|
487
|
+
} else {
|
|
488
|
+
dirPath = "";
|
|
489
|
+
namePrefix = partial;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const resolvedDir = path.resolve(cwd, dirPath || ".");
|
|
493
|
+
|
|
494
|
+
let entries: fs.Dirent[];
|
|
495
|
+
try {
|
|
496
|
+
entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Filter: skip hidden files, match prefix
|
|
502
|
+
const lowerPrefix = namePrefix.toLowerCase();
|
|
503
|
+
const filtered = entries
|
|
504
|
+
.filter(e => !e.name.startsWith(".") && e.name.toLowerCase().startsWith(lowerPrefix))
|
|
505
|
+
.sort((a, b) => {
|
|
506
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
507
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
508
|
+
return a.name.localeCompare(b.name);
|
|
509
|
+
})
|
|
510
|
+
.slice(0, 20);
|
|
511
|
+
|
|
512
|
+
const matches = filtered.map(e => dirPath + e.name + (e.isDirectory() ? "/" : ""));
|
|
513
|
+
|
|
514
|
+
// Rich labels with icons
|
|
515
|
+
const labels = filtered.map(e => {
|
|
516
|
+
const icon = e.isDirectory() ? "š" : fileIcon(e.name);
|
|
517
|
+
return `${icon} @${dirPath}${e.name}${e.isDirectory() ? "/" : ""}`;
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
if (matches.length === 0) return null;
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
matches,
|
|
524
|
+
labels,
|
|
525
|
+
prefix: partial,
|
|
526
|
+
tokenStart: atIdx,
|
|
527
|
+
triggerChar: "@",
|
|
528
|
+
isFilePath: true,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Complete directory paths for /add-dir command
|
|
534
|
+
*/
|
|
535
|
+
function completeDirPath(_input: string, tokenStart: number, partial: string): TabCompletion | null {
|
|
536
|
+
const cwd = process.cwd();
|
|
537
|
+
const lastSlash = partial.lastIndexOf("/");
|
|
538
|
+
let dirPath: string;
|
|
539
|
+
let namePrefix: string;
|
|
540
|
+
|
|
541
|
+
if (lastSlash >= 0) {
|
|
542
|
+
dirPath = partial.slice(0, lastSlash + 1);
|
|
543
|
+
namePrefix = partial.slice(lastSlash + 1);
|
|
544
|
+
} else {
|
|
545
|
+
dirPath = "";
|
|
546
|
+
namePrefix = partial;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const resolvedDir = path.resolve(cwd, dirPath || ".");
|
|
550
|
+
|
|
551
|
+
let entries: fs.Dirent[];
|
|
552
|
+
try {
|
|
553
|
+
entries = fs.readdirSync(resolvedDir, { withFileTypes: true });
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const lowerPrefix = namePrefix.toLowerCase();
|
|
559
|
+
const filtered = entries
|
|
560
|
+
.filter(e => e.isDirectory() && !e.name.startsWith(".") && e.name.toLowerCase().startsWith(lowerPrefix))
|
|
561
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
562
|
+
.slice(0, 20);
|
|
563
|
+
|
|
564
|
+
const matches = filtered.map(e => dirPath + e.name + "/");
|
|
565
|
+
const labels = filtered.map(e => `š ${dirPath}${e.name}/`);
|
|
566
|
+
|
|
567
|
+
if (matches.length === 0) return null;
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
matches,
|
|
571
|
+
labels,
|
|
572
|
+
prefix: partial,
|
|
573
|
+
tokenStart,
|
|
574
|
+
triggerChar: "",
|
|
575
|
+
isFilePath: true,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Expand @file references in user input.
|
|
581
|
+
* Replaces @path/to/file with the file contents block.
|
|
582
|
+
*/
|
|
583
|
+
function expandFileReferences(input: string): string {
|
|
584
|
+
// Match @path (non-whitespace after @), but not email-like patterns
|
|
585
|
+
const refPattern = /@((?:\.{0,2}\/)?[\w./_-]+[\w._-])/g;
|
|
586
|
+
let expanded = input;
|
|
587
|
+
const seen = new Set<string>();
|
|
588
|
+
|
|
589
|
+
let match;
|
|
590
|
+
while ((match = refPattern.exec(input)) !== null) {
|
|
591
|
+
const refPath = match[1];
|
|
592
|
+
if (seen.has(refPath)) continue;
|
|
593
|
+
seen.add(refPath);
|
|
594
|
+
|
|
595
|
+
const resolved = path.resolve(process.cwd(), refPath);
|
|
596
|
+
try {
|
|
597
|
+
const stat = fs.statSync(resolved);
|
|
598
|
+
if (stat.isFile()) {
|
|
599
|
+
const content = fs.readFileSync(resolved, "utf-8");
|
|
600
|
+
const maxLen = 50_000; // safety cap
|
|
601
|
+
const truncated = content.length > maxLen
|
|
602
|
+
? content.slice(0, maxLen) + "\n... [truncated]"
|
|
603
|
+
: content;
|
|
604
|
+
const block = `\n\n--- @${refPath} ---\n${truncated}\n--- end @${refPath} ---\n`;
|
|
605
|
+
expanded = expanded.replace(`@${refPath}`, refPath) + block;
|
|
606
|
+
console.log(chalk.dim(` š attached ${refPath} (${stat.size} bytes)`));
|
|
607
|
+
} else if (stat.isDirectory()) {
|
|
608
|
+
// List directory contents
|
|
609
|
+
const entries = fs.readdirSync(resolved)
|
|
610
|
+
.filter(e => !e.startsWith("."))
|
|
611
|
+
.slice(0, 50);
|
|
612
|
+
const listing = entries.join("\n");
|
|
613
|
+
const block = `\n\n--- @${refPath} (directory listing) ---\n${listing}\n--- end @${refPath} ---\n`;
|
|
614
|
+
expanded = expanded.replace(`@${refPath}`, refPath) + block;
|
|
615
|
+
console.log(chalk.dim(` š attached ${refPath}/ listing (${entries.length} entries)`));
|
|
616
|
+
}
|
|
617
|
+
} catch {
|
|
618
|
+
// File doesn't exist ā leave as-is, might be intentional @mention
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return expanded;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Use MultilineInput if available, otherwise fall back to readline
|
|
626
|
+
if (supportsMultilineInput()) {
|
|
627
|
+
const multilineInput = new MultilineInput({
|
|
628
|
+
prompt: chalk.yellow("you> "),
|
|
629
|
+
continuationPrompt: chalk.dim("... "),
|
|
630
|
+
onSubmit: async (input) => {
|
|
631
|
+
const shouldContinue = await processInput(input, () => multilineInput.reset());
|
|
632
|
+
if (!shouldContinue) {
|
|
633
|
+
multilineInput.stop();
|
|
634
|
+
process.exit(0);
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
onClose: () => {
|
|
638
|
+
console.log(chalk.dim("\nš bye."));
|
|
639
|
+
process.exit(0);
|
|
640
|
+
},
|
|
641
|
+
onTabComplete: handleTabComplete
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
multilineInput.start();
|
|
645
|
+
} else {
|
|
646
|
+
// Fallback to readline for non-TTY environments
|
|
647
|
+
const rl = createInterface({
|
|
648
|
+
input: process.stdin,
|
|
649
|
+
output: process.stdout,
|
|
650
|
+
prompt: chalk.yellow("you> "),
|
|
651
|
+
terminal: true
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
rl.on("line", async (line) => {
|
|
655
|
+
const shouldContinue = await processInput(line, () => rl.prompt());
|
|
656
|
+
if (!shouldContinue) {
|
|
657
|
+
rl.close();
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
rl.on("close", () => {
|
|
662
|
+
console.log(chalk.dim("\nš bye."));
|
|
663
|
+
process.exit(0);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
rl.prompt();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export async function runSinglePrompt(prompt: string, options: CliOptions = {}): Promise<void> {
|
|
671
|
+
const config = loadConfig();
|
|
672
|
+
const projectRoot = options.projectRoot
|
|
673
|
+
? path.resolve(options.projectRoot)
|
|
674
|
+
: process.cwd();
|
|
675
|
+
|
|
676
|
+
if (options.engine !== undefined) {
|
|
677
|
+
config.engine = options.engine;
|
|
678
|
+
// Update model to match engine if using default
|
|
679
|
+
if (!process.env.MIA_CODE_MODEL) {
|
|
680
|
+
config.model = config.engine === "claude" ? "sonnet" : "gemini-2.5-pro";
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (options.yolo !== undefined) {
|
|
685
|
+
config.yoloMode = options.yolo;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const rawMode = options.raw ?? false;
|
|
689
|
+
|
|
690
|
+
// Handle session resume (single prompt doesn't support interactive picker)
|
|
691
|
+
let sessionId: string | undefined;
|
|
692
|
+
if (typeof options.resume === "string") {
|
|
693
|
+
sessionId = options.resume;
|
|
694
|
+
} else if (options.resume === true) {
|
|
695
|
+
// Don't auto-resume for single prompts
|
|
696
|
+
sessionId = undefined;
|
|
697
|
+
} else {
|
|
698
|
+
// options.resume is undefined - don't auto-resume
|
|
699
|
+
sessionId = undefined;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
const result = await runGeminiHeadless({
|
|
704
|
+
prompt,
|
|
705
|
+
config,
|
|
706
|
+
sessionId,
|
|
707
|
+
projectRoot,
|
|
708
|
+
additionalDirs: options.addDirs
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
if (result.sessionId) {
|
|
712
|
+
const meta: MiaCodeSessionMeta = {
|
|
713
|
+
id: result.sessionId,
|
|
714
|
+
startedAt: new Date().toISOString(),
|
|
715
|
+
model: config.model,
|
|
716
|
+
projectRoot
|
|
717
|
+
};
|
|
718
|
+
rememberSession(meta);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Raw mode: show original events
|
|
722
|
+
if (rawMode) {
|
|
723
|
+
const rendered = renderEventsToText(result.events);
|
|
724
|
+
console.log(rendered);
|
|
725
|
+
} else {
|
|
726
|
+
// Unifier mode: run ceremonial interpretation
|
|
727
|
+
try {
|
|
728
|
+
const ceremonialOutput = await runUnifierSession(result.events, prompt, config);
|
|
729
|
+
console.log(ceremonialOutput);
|
|
730
|
+
} catch (unifierErr: unknown) {
|
|
731
|
+
// Fallback to raw output if unifier fails
|
|
732
|
+
console.error(chalk.yellow("ā unifier failed, showing raw output:"));
|
|
733
|
+
const rendered = renderEventsToText(result.events);
|
|
734
|
+
console.log(rendered);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
} catch (err: unknown) {
|
|
738
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
739
|
+
console.error(formatError(message));
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
}
|