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
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
+ }