Skip to content

Commit e535bfc

Browse files
committed
feat: auto-run .md files with ma in inline commands
When a command inline (!`...`) contains a path ending in .md, automatically prefix it with `ma` to execute it as an agent. This enables composing markdown agents together without explicitly typing `ma`. Examples: - !`helper.claude.md` → ma helper.claude.md - !`./task.md arg1` → ma ./task.md arg1 - !`~/agents/tool.md` → ma ~/agents/tool.md
1 parent 6983997 commit e535bfc

2 files changed

Lines changed: 87 additions & 6 deletions

File tree

src/imports.test.ts

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { test, expect, beforeAll, afterAll } from "bun:test";
2-
import { expandImports, hasImports, toCanonicalPath } from "./imports";
1+
import { test, expect, beforeAll, afterAll, describe } from "bun:test";
2+
import { expandImports, hasImports, toCanonicalPath, isMarkdownFileCommand } from "./imports";
33
import { mkdtemp, rm, symlink } from "node:fs/promises";
44
import { tmpdir } from "node:os";
55
import { join } from "node:path";
@@ -422,3 +422,63 @@ test("expandImports runs bun commands in invocationCwd", async () => {
422422
expect(result).toContain("bun-invocation-dir");
423423
expect(result).not.toContain("bun-agent-dir");
424424
});
425+
426+
// Auto-run .md files with ma tests
427+
describe("isMarkdownFileCommand", () => {
428+
test("detects simple .md file", () => {
429+
expect(isMarkdownFileCommand("foo.md")).toBe(true);
430+
});
431+
432+
test("detects relative path .md file", () => {
433+
expect(isMarkdownFileCommand("./foo.md")).toBe(true);
434+
expect(isMarkdownFileCommand("../foo.md")).toBe(true);
435+
});
436+
437+
test("detects home path .md file", () => {
438+
expect(isMarkdownFileCommand("~/foo.md")).toBe(true);
439+
expect(isMarkdownFileCommand("~/.ma/foo.md")).toBe(true);
440+
});
441+
442+
test("detects absolute path .md file", () => {
443+
expect(isMarkdownFileCommand("/path/to/foo.md")).toBe(true);
444+
});
445+
446+
test("detects compound .md file names", () => {
447+
expect(isMarkdownFileCommand("foo.claude.md")).toBe(true);
448+
expect(isMarkdownFileCommand("task.i.claude.md")).toBe(true);
449+
});
450+
451+
test("detects .md file with arguments", () => {
452+
expect(isMarkdownFileCommand("foo.md arg1 arg2")).toBe(true);
453+
expect(isMarkdownFileCommand("./task.claude.md --verbose")).toBe(true);
454+
});
455+
456+
test("does NOT match non-.md files", () => {
457+
expect(isMarkdownFileCommand("foo.txt")).toBe(false);
458+
expect(isMarkdownFileCommand("foo.js")).toBe(false);
459+
expect(isMarkdownFileCommand("echo hello")).toBe(false);
460+
expect(isMarkdownFileCommand("ls -la")).toBe(false);
461+
});
462+
463+
test("does NOT match commands containing .md elsewhere", () => {
464+
expect(isMarkdownFileCommand("echo foo.md")).toBe(false);
465+
expect(isMarkdownFileCommand("cat foo.md")).toBe(false);
466+
expect(isMarkdownFileCommand("grep pattern foo.md")).toBe(false);
467+
});
468+
469+
test("does NOT match .md in the middle of command", () => {
470+
expect(isMarkdownFileCommand("cp foo.md bar.md")).toBe(false);
471+
});
472+
});
473+
474+
test("expandImports does not affect non-.md commands", async () => {
475+
const content = "!`echo 'not an md file'`";
476+
const result = await expandImports(content, testDir);
477+
expect(result).toContain("not an md file");
478+
});
479+
480+
test("expandImports preserves normal shell commands", async () => {
481+
const content = "!`echo hello.txt`";
482+
const result = await expandImports(content, testDir);
483+
expect(result).toContain("hello.txt");
484+
});

src/imports.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,20 @@ async function processFileImport(
740740
return expandImports(content, dirname(resolvedPath), newStack, verbose, importCtx);
741741
}
742742

743+
/**
744+
* Pattern to detect markdown file paths that should be auto-run with `ma`
745+
* Matches: foo.md, ./foo.md, ~/foo.md, /path/to/foo.md, foo.claude.md, etc.
746+
* The command must start with a path-like pattern and end with .md
747+
*/
748+
const MD_FILE_COMMAND_PATTERN = /^(~?\.?\.?\/)?[^\s]+\.md(\s|$)/;
749+
750+
/**
751+
* Check if a command looks like a markdown file that should be run with `ma`
752+
*/
753+
export function isMarkdownFileCommand(command: string): boolean {
754+
return MD_FILE_COMMAND_PATTERN.test(command.trim());
755+
}
756+
743757
/**
744758
* Process a single command inline
745759
*/
@@ -749,8 +763,15 @@ async function processCommandInline(
749763
verbose: boolean,
750764
importCtx?: ImportContext
751765
): Promise<string> {
752-
// Always log command execution to stderr for visibility
753-
console.error(`[imports] Executing: ${command}`);
766+
// Auto-prefix markdown files with `ma` to run them as agents
767+
let actualCommand = command;
768+
if (isMarkdownFileCommand(command)) {
769+
actualCommand = `ma ${command}`;
770+
console.error(`[imports] Auto-running .md file with ma: ${actualCommand}`);
771+
} else {
772+
// Always log command execution to stderr for visibility
773+
console.error(`[imports] Executing: ${command}`);
774+
}
754775

755776
// Use importCtx.env if provided, otherwise fall back to process.env
756777
const env = importCtx?.env ?? process.env;
@@ -760,7 +781,7 @@ async function processCommandInline(
760781
const commandCwd = importCtx?.invocationCwd ?? currentFileDir;
761782

762783
try {
763-
const result = Bun.spawnSync(["sh", "-c", command], {
784+
const result = Bun.spawnSync(["sh", "-c", actualCommand], {
764785
cwd: commandCwd,
765786
stdout: "pipe",
766787
stderr: "pipe",
@@ -785,7 +806,7 @@ async function processCommandInline(
785806
}
786807
return output;
787808
} catch (err) {
788-
throw new Error(`Command failed: ${command} - ${(err as Error).message}`);
809+
throw new Error(`Command failed: ${actualCommand} - ${(err as Error).message}`);
789810
}
790811
}
791812

0 commit comments

Comments
 (0)