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