Skip to content

Commit 679047f

Browse files
committed
feat: add _i alias and CLI flags for interactive mode
- Add --_interactive and -_i CLI flags (consumed by ma, not passed to command) - Add _i as shorthand alias for _interactive in frontmatter - Fix handling of empty YAML values (_interactive: and _i: now enable interactive mode) - Add comprehensive tests for applyInteractiveMode function - Update README with new CLI flags and frontmatter options
1 parent 47a0548 commit 679047f

4 files changed

Lines changed: 134 additions & 10 deletions

File tree

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ The `--feature_name` and `--target_dir` flags are consumed by markdown-agent for
173173
| `env` | object | Set process environment variables |
174174
| `env` | string[] | Pass as `--env` flags to command |
175175
| `$1`, `$2`... | string | Map positional args to flags (e.g., `$1: prompt`) |
176-
| `_interactive` | boolean | Enable interactive mode (overrides print-mode defaults) |
176+
| `_interactive` / `_i` | boolean | Enable interactive mode (overrides print-mode defaults) |
177177
| `_subcommand` | string/string[] | Prepend subcommand(s) to CLI args |
178178

179179
### All Other Keys → CLI Flags
@@ -199,7 +199,7 @@ p: true # → -p (single char = short flag)
199199

200200
## Print vs Interactive Mode
201201

202-
All commands run in **print mode by default** (non-interactive, exit after completion). Use the `.i.` filename marker or `_interactive: true` to enable interactive mode.
202+
All commands run in **print mode by default** (non-interactive, exit after completion). Use the `.i.` filename marker, `_interactive` frontmatter, or CLI flags to enable interactive mode.
203203

204204
### Print Mode (Default)
205205

@@ -221,16 +221,23 @@ task.i.codex.md # Runs: codex "..." (interactive session)
221221
task.i.gemini.md # Runs: gemini --prompt-interactive "..."
222222
```
223223

224-
Or use `_interactive: true` in frontmatter:
224+
Or use `_interactive` (or `_i`) in frontmatter:
225225

226226
```yaml
227227
---
228-
_interactive: true
228+
_interactive: true # or _interactive: (empty), or _i:
229229
model: opus
230230
---
231231
Review this code with me interactively.
232232
```
233233

234+
Or use CLI flags:
235+
236+
```bash
237+
ma task.claude.md --_interactive # Enable interactive mode
238+
ma task.claude.md -_i # Short form
239+
```
240+
234241
---
235242

236243
## Global Configuration
@@ -478,12 +485,18 @@ Command resolution:
478485
All frontmatter keys are passed as CLI flags to the command.
479486
Global defaults can be set in ~/.markdown-agent/config.yaml
480487
488+
ma-specific flags (consumed, not passed to command):
489+
--command, -c Specify command to run
490+
--dry-run Preview without executing
491+
--_interactive, -_i Enable interactive mode
492+
481493
Examples:
482494
ma task.claude.md -p "print mode"
483495
ma task.claude.md --model opus --verbose
484496
ma commit.gemini.md
485497
ma task.md --command claude
486498
ma task.md -c gemini
499+
ma task.claude.md -_i # Run in interactive mode
487500
488501
Without a file:
489502
ma --setup Configure shell to run .md files directly

src/config.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
loadGlobalConfig,
44
getCommandDefaults,
55
applyDefaults,
6+
applyInteractiveMode,
67
clearConfigCache,
78
findGitRoot,
89
loadProjectConfig,
@@ -310,3 +311,92 @@ describe("config cascade", () => {
310311
expect(config.commands?.claude?.model).toBe("opus");
311312
});
312313
});
314+
315+
describe("applyInteractiveMode", () => {
316+
test("removes print flag for claude with _interactive: true", () => {
317+
const frontmatter = { print: true, model: "opus", _interactive: true };
318+
const result = applyInteractiveMode(frontmatter, "claude");
319+
expect(result.print).toBeUndefined();
320+
expect(result._interactive).toBeUndefined();
321+
expect(result.model).toBe("opus");
322+
});
323+
324+
test("removes print flag for claude with _i: true", () => {
325+
const frontmatter = { print: true, model: "opus", _i: true };
326+
const result = applyInteractiveMode(frontmatter, "claude");
327+
expect(result.print).toBeUndefined();
328+
expect(result._i).toBeUndefined();
329+
expect(result.model).toBe("opus");
330+
});
331+
332+
test("handles _interactive with null value (YAML empty key)", () => {
333+
const frontmatter = { print: true, _interactive: null };
334+
const result = applyInteractiveMode(frontmatter, "claude");
335+
expect(result.print).toBeUndefined();
336+
expect(result._interactive).toBeUndefined();
337+
});
338+
339+
test("handles _i with null value (YAML empty key)", () => {
340+
const frontmatter = { print: true, _i: null };
341+
const result = applyInteractiveMode(frontmatter, "claude");
342+
expect(result.print).toBeUndefined();
343+
expect(result._i).toBeUndefined();
344+
});
345+
346+
test("handles _interactive with empty string value", () => {
347+
const frontmatter = { print: true, _interactive: "" };
348+
const result = applyInteractiveMode(frontmatter, "claude");
349+
expect(result.print).toBeUndefined();
350+
});
351+
352+
test("handles _i with empty string value", () => {
353+
const frontmatter = { print: true, _i: "" };
354+
const result = applyInteractiveMode(frontmatter, "claude");
355+
expect(result.print).toBeUndefined();
356+
});
357+
358+
test("does not trigger interactive mode with _interactive: false", () => {
359+
const frontmatter = { print: true, _interactive: false };
360+
const result = applyInteractiveMode(frontmatter, "claude");
361+
expect(result.print).toBe(true);
362+
});
363+
364+
test("does not trigger interactive mode when _interactive not present", () => {
365+
const frontmatter = { print: true, model: "opus" };
366+
const result = applyInteractiveMode(frontmatter, "claude");
367+
expect(result.print).toBe(true);
368+
});
369+
370+
test("triggers interactive mode via external flag (interactiveFromExternal)", () => {
371+
const frontmatter = { print: true, model: "opus" };
372+
const result = applyInteractiveMode(frontmatter, "claude", true);
373+
expect(result.print).toBeUndefined();
374+
expect(result.model).toBe("opus");
375+
});
376+
377+
test("changes copilot $1 from prompt to interactive", () => {
378+
const frontmatter = { $1: "prompt", silent: true, _interactive: true };
379+
const result = applyInteractiveMode(frontmatter, "copilot");
380+
expect(result.$1).toBe("interactive");
381+
expect(result.silent).toBe(true);
382+
});
383+
384+
test("removes _subcommand for codex", () => {
385+
const frontmatter = { _subcommand: "exec", _interactive: true };
386+
const result = applyInteractiveMode(frontmatter, "codex");
387+
expect(result._subcommand).toBeUndefined();
388+
});
389+
390+
test("adds prompt-interactive for gemini", () => {
391+
const frontmatter = { model: "pro", _interactive: true };
392+
const result = applyInteractiveMode(frontmatter, "gemini");
393+
expect(result.$1).toBe("prompt-interactive");
394+
});
395+
396+
test("unknown command just removes _interactive", () => {
397+
const frontmatter = { custom: "value", _interactive: true };
398+
const result = applyInteractiveMode(frontmatter, "my-custom-cli");
399+
expect(result._interactive).toBeUndefined();
400+
expect(result.custom).toBe("value");
401+
});
402+
});

src/config.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,29 @@ export const BUILTIN_DEFAULTS: GlobalConfig = {
5555
export function applyInteractiveMode(
5656
frontmatter: AgentFrontmatter,
5757
command: string,
58-
interactiveFromFilename: boolean = false
58+
interactiveFromExternal: boolean = false
5959
): AgentFrontmatter {
60-
// Check if _interactive is enabled (truthy, empty string, or from filename)
61-
const interactiveMode = frontmatter._interactive ?? interactiveFromFilename;
62-
if (!interactiveMode && interactiveMode !== "") {
60+
// Check if _interactive or _i is enabled
61+
// Can be: true, empty string (YAML key with no value), null (YAML key with explicit null), or external trigger
62+
// NOTE: We check key existence separately because ?? treats null as "nullish" and skips to next value
63+
const hasInteractiveKey = "_interactive" in frontmatter;
64+
const hasIKey = "_i" in frontmatter;
65+
const interactiveValue = hasInteractiveKey ? frontmatter._interactive : frontmatter._i;
66+
const interactiveMode = interactiveFromExternal ||
67+
interactiveValue === true ||
68+
interactiveValue === "" ||
69+
(hasInteractiveKey && interactiveValue === null) ||
70+
(hasIKey && interactiveValue === null) ||
71+
(interactiveValue !== undefined && interactiveValue !== false);
72+
73+
if (!interactiveMode) {
6374
return frontmatter;
6475
}
6576

66-
// Remove _interactive from output (it's a meta-key, not a CLI flag)
77+
// Remove _interactive and _i from output (they're meta-keys, not CLI flags)
6778
const result = { ...frontmatter };
6879
delete result._interactive;
80+
delete result._i;
6981

7082
switch (command) {
7183
case "copilot":

src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,14 @@ async function main() {
218218
remainingArgs.splice(trustIndex, 1); // Consume it
219219
}
220220

221+
// Check for --_interactive or -_i flag (consumed by ma, enables interactive mode)
222+
let interactiveFromCli = false;
223+
const interactiveIndex = remainingArgs.findIndex(arg => arg === "--_interactive" || arg === "-_i");
224+
if (interactiveIndex !== -1) {
225+
interactiveFromCli = true;
226+
remainingArgs.splice(interactiveIndex, 1); // Consume it
227+
}
228+
221229
// Resolve command: CLI --command > MA_COMMAND env > filename
222230
let command: string;
223231
try {
@@ -242,7 +250,8 @@ async function main() {
242250
const interactiveFromFilename = hasInteractiveMarker(localFilePath);
243251

244252
// Apply _interactive mode transformations (converts print defaults to interactive mode per command)
245-
frontmatter = applyInteractiveMode(frontmatter, command, interactiveFromFilename);
253+
// Interactive can be triggered by: CLI flag (--_interactive/-_i), filename (.i.), or frontmatter (_interactive/_i)
254+
frontmatter = applyInteractiveMode(frontmatter, command, interactiveFromFilename || interactiveFromCli);
246255

247256
// Extract and apply environment variables (object form) to process.env
248257
// This must happen BEFORE import expansion so !`command` inlines can use them

0 commit comments

Comments
 (0)